Skip to content

Commit 7c29d20

Browse files
Allow custom output paths when copying from node modules
1 parent 8e37e95 commit 7c29d20

File tree

4 files changed

+166
-42
lines changed

4 files changed

+166
-42
lines changed

README.md

+39-8
Original file line numberDiff line numberDiff line change
@@ -22,21 +22,44 @@ dotnet add {project} package ecoAPM.StatiqPipelines
2222

2323
## Usage
2424

25-
This package currently contains one pipeline and one module.
25+
This package currently contains one pipeline and two modules.
2626

2727
### CopyFromNPM
2828

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

3131
```c#
32-
bootstrapper.AddPipeline("NPM", new CopyFromNPM(new [] {
32+
var files = new [] {
3333
"bootstrap/dist/css/bootstrap.min.css",
34-
"bootstrap/dist/js/bootstrap.min.js",
35-
"jquery/dist/jquery.min.js",
36-
"marked/marked.min.js",
37-
"notosans/*",
38-
"vue/dist/vue.global.prod.js"
39-
});
34+
"jquery/dist/jquery.min.js"
35+
};
36+
bootstrapper.AddPipeline("NPM", new CopyFromNPM(files, "assets");
37+
```
38+
39+
The copied files can then be referenced from markup:
40+
41+
```html
42+
<link src="/assets/bootstrap.min.css"/>
43+
<script src="/assets/jquery.min.js"></script>
44+
```
45+
46+
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.
47+
48+
```c#
49+
var files = new Dictionary<string, string> {
50+
{ "bootstrap/dist/css/bootstrap.min.css", "" },
51+
{ "jquery/dist/jquery.min.js", "" },
52+
{ "@fontsource/noto-sans/*", "fonts" }
53+
};
54+
bootstrapper.AddPipeline("NPM", new CopyFromNPM(files);
55+
```
56+
57+
Note that the output path is optional and defaults to `lib`.
58+
59+
```html
60+
<link src="/lib/bootstrap.min.css"/>
61+
<script src="/lib/jquery.min.js"></script>
62+
<link src="/lib/fonts/latin-300.css"/>
4063
```
4164

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

76+
### NodeRestore
77+
78+
This module simply runs `npm`/`yarn` install as part of the build pipeline.
79+
80+
```c#
81+
bootstrapper.ModifyPipeline("Content", p => p.InputModules.Add(new NodeRestore()));
82+
```
83+
5384
## Contributing
5485

5586
Please be sure to read and follow ecoAPM's [Contribution Guidelines](CONTRIBUTING.md) when submitting issues or pull requests.

StatiqPipelines.Tests/CopyFromNPMTests.cs

+77-16
Original file line numberDiff line numberDiff line change
@@ -11,32 +11,31 @@ public class CopyFromNPMTests
1111
public async Task NodeModulesAreTranslated()
1212
{
1313
//arrange
14-
var context = new TestExecutionContext();
15-
context.FileSystem.GetInputFile("/node_modules/x/1.js").OpenWrite();
16-
context.FileSystem.GetInputFile("/node_modules/y/2.js").OpenWrite();
14+
var context = new TestExecutionContext { FileSystem = { RootPath = "/code/app" } };
15+
context.FileSystem.GetInputFile("/code/app/node_modules/x/1.js").OpenWrite();
16+
context.FileSystem.GetInputFile("/code/app/node_modules/y/2.js").OpenWrite();
1717

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

2121
//act
22-
var tasks = input.Select(async i => await i.ExecuteAsync(context));
23-
var output = await Task.WhenAll(tasks);
22+
var output = await input.ExecuteAsync(context);
2423

2524
//assert
26-
var files = output.SelectMany(d => d).Select(d => d.Source.FileName.ToString()).ToArray();
25+
var files = output.Select(d => d.Source.FileName.ToString()).ToArray();
2726
Assert.Equal(2, files.Length);
2827
Assert.Contains("1.js", files);
2928
Assert.Contains("2.js", files);
3029
}
3130

3231
[Fact]
33-
public async Task CopyToFlattensOutputPathForFiles()
32+
public async Task CopyToFlattensOutputByDefault()
3433
{
3534
//arrange
3635
var docs = new List<IDocument>
3736
{
38-
new TestDocument(new NormalizedPath("/node_modules/x/1.js")),
39-
new TestDocument(new NormalizedPath("/node_modules/y/2.js"))
37+
new TestDocument(new NormalizedPath("/code/app/node_modules/x/1.js")),
38+
new TestDocument(new NormalizedPath("/code/app/node_modules/y/2.js"))
4039
};
4140
var context = new TestExecutionContext();
4241
context.SetInputs(docs);
@@ -54,26 +53,88 @@ public async Task CopyToFlattensOutputPathForFiles()
5453
}
5554

5655
[Fact]
57-
public async Task CopyToDoesNotFlattenOutputPathForDirectories()
56+
public async Task CopyToFlattensOutputForEmptyValues()
57+
{
58+
//arrange
59+
var docs = new List<IDocument>
60+
{
61+
new TestDocument(new NormalizedPath("/code/app/node_modules/x/1.js")),
62+
new TestDocument(new NormalizedPath("/code/app/node_modules/y/2.js"))
63+
};
64+
var context = new TestExecutionContext();
65+
context.SetInputs(docs);
66+
67+
var files = new Dictionary<string, string>
68+
{
69+
{ "x/1.js", "" },
70+
{ "y/2.js", " " }
71+
};
72+
var pipeline = new CopyFromNPM(files, "assets/js");
73+
var copy = pipeline.ProcessModules.First(m => m is SetDestination);
74+
75+
//act
76+
var output = await copy.ExecuteAsync(context);
77+
78+
//assert
79+
var outputDocs = output.ToArray();
80+
Assert.Equal("assets/js/1.js", outputDocs[0].Destination);
81+
Assert.Equal("assets/js/2.js", outputDocs[1].Destination);
82+
}
83+
84+
[Fact]
85+
public async Task CopyToUsesSpecifiedValues()
86+
{
87+
//arrange
88+
var docs = new List<IDocument>
89+
{
90+
new TestDocument(new NormalizedPath("/code/app/node_modules/x/y/1.js")),
91+
new TestDocument(new NormalizedPath("/code/app/node_modules/x/y/z/2.js"))
92+
};
93+
var context = new TestExecutionContext();
94+
context.SetInputs(docs);
95+
96+
var files = new Dictionary<string, string>
97+
{
98+
{ "x/y/1.js", "y/1.js" },
99+
{ "x/y/z/2.js", "y/z/2.js" }
100+
};
101+
var pipeline = new CopyFromNPM(files, "assets/js");
102+
var copy = pipeline.ProcessModules.First(m => m is SetDestination);
103+
104+
//act
105+
var output = await copy.ExecuteAsync(context);
106+
107+
//assert
108+
var outputDocs = output.ToArray();
109+
Assert.Equal("assets/js/y/1.js", outputDocs[0].Destination);
110+
Assert.Equal("assets/js/y/z/2.js", outputDocs[1].Destination);
111+
}
112+
113+
[Fact]
114+
public async Task CanCopyToOutputUsingWildcardKeys()
58115
{
59116
//arrange
60117
var docs = new List<IDocument>
61118
{
62-
new TestDocument(new NormalizedPath("/node_modules/x/1.js")),
63-
new TestDocument(new NormalizedPath("/node_modules/x/y/2.js"))
119+
new TestDocument(new NormalizedPath("/code/app/node_modules/x/1.js")),
120+
new TestDocument(new NormalizedPath("/code/app/node_modules/x/2.js"))
64121
};
65122
var context = new TestExecutionContext();
66123
context.SetInputs(docs);
67124

68-
var pipeline = new CopyFromNPM(new[] { "x" }, "assets/js");
125+
var files = new Dictionary<string, string>
126+
{
127+
{ "x/*", "y" }
128+
};
129+
var pipeline = new CopyFromNPM(files, "assets/js");
69130
var copy = pipeline.ProcessModules.First(m => m is SetDestination);
70131

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

74135
//assert
75136
var outputDocs = output.ToArray();
76-
Assert.Equal("assets/js/x/1.js", outputDocs[0].Destination);
77-
Assert.Equal("assets/js/x/y/2.js", outputDocs[1].Destination);
137+
Assert.Equal("assets/js/y/1.js", outputDocs[0].Destination);
138+
Assert.Equal("assets/js/y/2.js", outputDocs[1].Destination);
78139
}
79140
}

StatiqPipelines/CopyFromNPM.cs

+49-17
Original file line numberDiff line numberDiff line change
@@ -6,27 +6,49 @@ namespace ecoAPM.StatiqPipelines;
66
public class CopyFromNPM : Pipeline
77
{
88
private const string NodeModulesDirectory = "node_modules/";
9-
private Dictionary<string, ReadFiles> _files;
9+
10+
private readonly IDictionary<string, string> _paths;
1011

1112
/// <summary>
1213
/// Copies specific files from a `node_modules` directory to the output
1314
/// </summary>
1415
/// <param name="paths">The file paths (relative to `node_modules`) to copy</param>
1516
/// <param name="output">The path (relative to the output root) where the files will be copied</param>
16-
/// <param name="flatten">Flatten all files into the <see cref="output">output</see> directory</param>
1717
public CopyFromNPM(IEnumerable<string> paths, string output = "lib")
18+
: this(Flatten(paths), output)
1819
{
19-
_files = paths.ToDictionary(p => p, p => new ReadFiles(npmPath(p)));
20+
}
21+
22+
/// <summary>
23+
/// Copies specific files from a `node_modules` directory to the output
24+
/// </summary>
25+
/// <param name="paths">The file paths (relative to `node_modules`) to copy</param>
26+
/// <param name="output">The path (relative to the output root) where the files will be copied</param>
27+
public CopyFromNPM(Dictionary<string, string> paths, string output = "lib")
28+
{
29+
_paths = paths;
2030

2131
Isolated = true;
22-
InputModules = new ModuleList { new NodeRestore() };
23-
InputModules.AddRange(_files.Values);
32+
InputModules = new ModuleList
33+
{
34+
new NodeRestore(),
35+
new ReadFiles(_paths.Keys.Select(npmPath))
36+
};
2437

25-
ProcessModules = new ModuleList { CopyTo(output) };
38+
ProcessModules = new ModuleList
39+
{
40+
CopyTo(output)
41+
};
2642

27-
OutputModules = new ModuleList { new WriteFiles() };
43+
OutputModules = new ModuleList
44+
{
45+
new WriteFiles()
46+
};
2847
}
2948

49+
private static Dictionary<string, string> Flatten(IEnumerable<string> paths)
50+
=> paths.ToDictionary(p => p, path => new NormalizedPath(path).FileName.ToString());
51+
3052
private static string npmPath(string path)
3153
=> IExecutionContext.Current.FileSystem
3254
.GetRootDirectory(NodeModulesDirectory).GetFile(path)
@@ -38,17 +60,27 @@ private SetDestination CopyTo(string output)
3860
private Config<NormalizedPath> SetPath(string output)
3961
=> Config.FromDocument(d => NewPath(output, d));
4062

41-
private NormalizedPath NewPath(string output, IDocument d)
42-
=> new(OutputPath(output, d));
63+
private NormalizedPath NewPath(string output, IDocument doc)
64+
=> new(OutputPath(output, doc));
4365

44-
private string OutputPath(string output, IDocument d)
45-
=> Path.Combine(output, RelativeOutputPath(d));
66+
private string OutputPath(string output, IDocument doc)
67+
=> Path.Combine(output, RelativeOutputPath(doc));
4668

47-
private string RelativeOutputPath(IDocument d)
48-
=> _files.ContainsKey(RelativePath(d))
49-
? d.Source.FileName.ToString()
50-
: RelativePath(d);
69+
private string RelativeOutputPath(IDocument doc)
70+
=> _paths.TryGetValue(RelativePath(doc.Source), out var path)
71+
? !path.IsNullOrWhiteSpace() ? path : doc.Source.FileName.ToString()
72+
: HandleWildcard(doc);
73+
74+
private string HandleWildcard(IDocument doc)
75+
{
76+
var paths = _paths.ToDictionary(p => p.Key.Split("*")[0], p => p.Value);
77+
var match = paths.FirstOrDefault(p => doc.Source.FullPath.Contains(p.Key));
78+
var value = !match.Value.IsNullOrWhiteSpace()
79+
? Path.Combine(match.Value, doc.Source.FileName.ToString()).Replace("\\", "/")
80+
: doc.Source.FileName.ToString();
81+
return value;
82+
}
5183

52-
private static string RelativePath(IDocument d)
53-
=> d.Source.RootRelative.ToString().RemoveStart(NodeModulesDirectory);
84+
private static string RelativePath(NormalizedPath p)
85+
=> p.RootRelative.ToString().Split(NodeModulesDirectory)[1];
5486
}

StatiqPipelines/StatiqPipelines.csproj

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
<PropertyGroup>
44
<TargetFramework>net6.0</TargetFramework>
5-
<Version>1.1.0</Version>
5+
<Version>1.2.0</Version>
66
<PackageId>ecoAPM.StatiqPipelines</PackageId>
77
<RootNamespace>ecoAPM.StatiqPipelines</RootNamespace>
88
<Description>Pipelines and helpers used in ecoAPM's static sites</Description>

0 commit comments

Comments
 (0)