Skip to content

Commit

Permalink
Allow custom output paths when copying from node modules
Browse files Browse the repository at this point in the history
  • Loading branch information
SteveDesmond-ca committed Jul 3, 2023
1 parent 8e37e95 commit 7c29d20
Show file tree
Hide file tree
Showing 4 changed files with 166 additions and 42 deletions.
47 changes: 39 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,21 +22,44 @@ dotnet add {project} package ecoAPM.StatiqPipelines

## Usage

This package currently contains one pipeline and one module.
This package currently contains one pipeline and two modules.

### CopyFromNPM

This pipeline copies files from the `node_modules` directory to a set location in your output.

```c#
bootstrapper.AddPipeline("NPM", new CopyFromNPM(new [] {
var files = new [] {
"bootstrap/dist/css/bootstrap.min.css",
"bootstrap/dist/js/bootstrap.min.js",
"jquery/dist/jquery.min.js",
"marked/marked.min.js",
"notosans/*",
"vue/dist/vue.global.prod.js"
});
"jquery/dist/jquery.min.js"
};
bootstrapper.AddPipeline("NPM", new CopyFromNPM(files, "assets");
```

The copied files can then be referenced from markup:

```html
<link src="/assets/bootstrap.min.css"/>
<script src="/assets/jquery.min.js"></script>
```

A dictionary can be used to specify the output path for a given input. An empty string value flattens output with the input filename, as above.

```c#
var files = new Dictionary<string, string> {
{ "bootstrap/dist/css/bootstrap.min.css", "" },
{ "jquery/dist/jquery.min.js", "" },
{ "@fontsource/noto-sans/*", "fonts" }
};
bootstrapper.AddPipeline("NPM", new CopyFromNPM(files);
```

Note that the output path is optional and defaults to `lib`.

```html
<link src="/lib/bootstrap.min.css"/>
<script src="/lib/jquery.min.js"></script>
<link src="/lib/fonts/latin-300.css"/>
```

### NiceURL
Expand All @@ -50,6 +73,14 @@ instead of the default `output/category/page.html`
bootstrapper.ModifyPipeline("Content", p => p.ProcessModules.Add(new NiceURL()));
```

### NodeRestore

This module simply runs `npm`/`yarn` install as part of the build pipeline.

```c#
bootstrapper.ModifyPipeline("Content", p => p.InputModules.Add(new NodeRestore()));
```

## Contributing

Please be sure to read and follow ecoAPM's [Contribution Guidelines](CONTRIBUTING.md) when submitting issues or pull requests.
93 changes: 77 additions & 16 deletions StatiqPipelines.Tests/CopyFromNPMTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,32 +11,31 @@ public class CopyFromNPMTests
public async Task NodeModulesAreTranslated()
{
//arrange
var context = new TestExecutionContext();
context.FileSystem.GetInputFile("/node_modules/x/1.js").OpenWrite();
context.FileSystem.GetInputFile("/node_modules/y/2.js").OpenWrite();
var context = new TestExecutionContext { FileSystem = { RootPath = "/code/app" } };
context.FileSystem.GetInputFile("/code/app/node_modules/x/1.js").OpenWrite();
context.FileSystem.GetInputFile("/code/app/node_modules/y/2.js").OpenWrite();

var pipeline = new CopyFromNPM(new[] { "x/1.js", "y/2.js" }, "assets/js");
var input = pipeline.InputModules.Where(m => m is ReadFiles);
var input = pipeline.InputModules.First(m => m is ReadFiles);

//act
var tasks = input.Select(async i => await i.ExecuteAsync(context));
var output = await Task.WhenAll(tasks);
var output = await input.ExecuteAsync(context);

//assert
var files = output.SelectMany(d => d).Select(d => d.Source.FileName.ToString()).ToArray();
var files = output.Select(d => d.Source.FileName.ToString()).ToArray();
Assert.Equal(2, files.Length);
Assert.Contains("1.js", files);
Assert.Contains("2.js", files);
}

[Fact]
public async Task CopyToFlattensOutputPathForFiles()
public async Task CopyToFlattensOutputByDefault()
{
//arrange
var docs = new List<IDocument>
{
new TestDocument(new NormalizedPath("/node_modules/x/1.js")),
new TestDocument(new NormalizedPath("/node_modules/y/2.js"))
new TestDocument(new NormalizedPath("/code/app/node_modules/x/1.js")),
new TestDocument(new NormalizedPath("/code/app/node_modules/y/2.js"))
};
var context = new TestExecutionContext();
context.SetInputs(docs);
Expand All @@ -54,26 +53,88 @@ public async Task CopyToFlattensOutputPathForFiles()
}

[Fact]
public async Task CopyToDoesNotFlattenOutputPathForDirectories()
public async Task CopyToFlattensOutputForEmptyValues()
{
//arrange
var docs = new List<IDocument>
{
new TestDocument(new NormalizedPath("/code/app/node_modules/x/1.js")),
new TestDocument(new NormalizedPath("/code/app/node_modules/y/2.js"))
};
var context = new TestExecutionContext();
context.SetInputs(docs);

var files = new Dictionary<string, string>
{
{ "x/1.js", "" },
{ "y/2.js", " " }
};
var pipeline = new CopyFromNPM(files, "assets/js");
var copy = pipeline.ProcessModules.First(m => m is SetDestination);

//act
var output = await copy.ExecuteAsync(context);

//assert
var outputDocs = output.ToArray();
Assert.Equal("assets/js/1.js", outputDocs[0].Destination);
Assert.Equal("assets/js/2.js", outputDocs[1].Destination);
}

[Fact]
public async Task CopyToUsesSpecifiedValues()
{
//arrange
var docs = new List<IDocument>
{
new TestDocument(new NormalizedPath("/code/app/node_modules/x/y/1.js")),
new TestDocument(new NormalizedPath("/code/app/node_modules/x/y/z/2.js"))
};
var context = new TestExecutionContext();
context.SetInputs(docs);

var files = new Dictionary<string, string>
{
{ "x/y/1.js", "y/1.js" },
{ "x/y/z/2.js", "y/z/2.js" }
};
var pipeline = new CopyFromNPM(files, "assets/js");
var copy = pipeline.ProcessModules.First(m => m is SetDestination);

//act
var output = await copy.ExecuteAsync(context);

//assert
var outputDocs = output.ToArray();
Assert.Equal("assets/js/y/1.js", outputDocs[0].Destination);
Assert.Equal("assets/js/y/z/2.js", outputDocs[1].Destination);
}

[Fact]
public async Task CanCopyToOutputUsingWildcardKeys()
{
//arrange
var docs = new List<IDocument>
{
new TestDocument(new NormalizedPath("/node_modules/x/1.js")),
new TestDocument(new NormalizedPath("/node_modules/x/y/2.js"))
new TestDocument(new NormalizedPath("/code/app/node_modules/x/1.js")),
new TestDocument(new NormalizedPath("/code/app/node_modules/x/2.js"))
};
var context = new TestExecutionContext();
context.SetInputs(docs);

var pipeline = new CopyFromNPM(new[] { "x" }, "assets/js");
var files = new Dictionary<string, string>
{
{ "x/*", "y" }
};
var pipeline = new CopyFromNPM(files, "assets/js");
var copy = pipeline.ProcessModules.First(m => m is SetDestination);

//act
var output = await copy.ExecuteAsync(context);

//assert
var outputDocs = output.ToArray();
Assert.Equal("assets/js/x/1.js", outputDocs[0].Destination);
Assert.Equal("assets/js/x/y/2.js", outputDocs[1].Destination);
Assert.Equal("assets/js/y/1.js", outputDocs[0].Destination);
Assert.Equal("assets/js/y/2.js", outputDocs[1].Destination);
}
}
66 changes: 49 additions & 17 deletions StatiqPipelines/CopyFromNPM.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,27 +6,49 @@ namespace ecoAPM.StatiqPipelines;
public class CopyFromNPM : Pipeline
{
private const string NodeModulesDirectory = "node_modules/";
private Dictionary<string, ReadFiles> _files;

private readonly IDictionary<string, string> _paths;

/// <summary>
/// Copies specific files from a `node_modules` directory to the output
/// </summary>
/// <param name="paths">The file paths (relative to `node_modules`) to copy</param>
/// <param name="output">The path (relative to the output root) where the files will be copied</param>
/// <param name="flatten">Flatten all files into the <see cref="output">output</see> directory</param>
public CopyFromNPM(IEnumerable<string> paths, string output = "lib")
: this(Flatten(paths), output)
{
_files = paths.ToDictionary(p => p, p => new ReadFiles(npmPath(p)));
}

/// <summary>
/// Copies specific files from a `node_modules` directory to the output
/// </summary>
/// <param name="paths">The file paths (relative to `node_modules`) to copy</param>
/// <param name="output">The path (relative to the output root) where the files will be copied</param>
public CopyFromNPM(Dictionary<string, string> paths, string output = "lib")
{
_paths = paths;

Isolated = true;
InputModules = new ModuleList { new NodeRestore() };
InputModules.AddRange(_files.Values);
InputModules = new ModuleList
{
new NodeRestore(),
new ReadFiles(_paths.Keys.Select(npmPath))
};

ProcessModules = new ModuleList { CopyTo(output) };
ProcessModules = new ModuleList
{
CopyTo(output)
};

OutputModules = new ModuleList { new WriteFiles() };
OutputModules = new ModuleList
{
new WriteFiles()
};
}

private static Dictionary<string, string> Flatten(IEnumerable<string> paths)
=> paths.ToDictionary(p => p, path => new NormalizedPath(path).FileName.ToString());

private static string npmPath(string path)
=> IExecutionContext.Current.FileSystem
.GetRootDirectory(NodeModulesDirectory).GetFile(path)
Expand All @@ -38,17 +60,27 @@ private SetDestination CopyTo(string output)
private Config<NormalizedPath> SetPath(string output)
=> Config.FromDocument(d => NewPath(output, d));

private NormalizedPath NewPath(string output, IDocument d)
=> new(OutputPath(output, d));
private NormalizedPath NewPath(string output, IDocument doc)
=> new(OutputPath(output, doc));

private string OutputPath(string output, IDocument d)
=> Path.Combine(output, RelativeOutputPath(d));
private string OutputPath(string output, IDocument doc)
=> Path.Combine(output, RelativeOutputPath(doc));

private string RelativeOutputPath(IDocument d)
=> _files.ContainsKey(RelativePath(d))
? d.Source.FileName.ToString()
: RelativePath(d);
private string RelativeOutputPath(IDocument doc)
=> _paths.TryGetValue(RelativePath(doc.Source), out var path)
? !path.IsNullOrWhiteSpace() ? path : doc.Source.FileName.ToString()
: HandleWildcard(doc);

private string HandleWildcard(IDocument doc)
{
var paths = _paths.ToDictionary(p => p.Key.Split("*")[0], p => p.Value);
var match = paths.FirstOrDefault(p => doc.Source.FullPath.Contains(p.Key));
var value = !match.Value.IsNullOrWhiteSpace()
? Path.Combine(match.Value, doc.Source.FileName.ToString()).Replace("\\", "/")
: doc.Source.FileName.ToString();
return value;
}

private static string RelativePath(IDocument d)
=> d.Source.RootRelative.ToString().RemoveStart(NodeModulesDirectory);
private static string RelativePath(NormalizedPath p)
=> p.RootRelative.ToString().Split(NodeModulesDirectory)[1];
}
2 changes: 1 addition & 1 deletion StatiqPipelines/StatiqPipelines.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<Version>1.1.0</Version>
<Version>1.2.0</Version>
<PackageId>ecoAPM.StatiqPipelines</PackageId>
<RootNamespace>ecoAPM.StatiqPipelines</RootNamespace>
<Description>Pipelines and helpers used in ecoAPM's static sites</Description>
Expand Down

0 comments on commit 7c29d20

Please sign in to comment.