@localnerve/web-component-build 3.5.0 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +41 -29
- package/lib/build.js +28 -22
- package/lib/index.js +96 -68
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -10,6 +10,7 @@ Assembles a web component from its parts, allows developers to author the compon
|
|
|
10
10
|
The parts are processed and written to an output directory, then exposed to a calling build process.
|
|
11
11
|
|
|
12
12
|
* [Why This Exists](#why-this-exists)
|
|
13
|
+
* [Examples](#examples)
|
|
13
14
|
* [Processing Possibilities](#processing-map)
|
|
14
15
|
* [Multiple Templates](#multiple-templates)
|
|
15
16
|
* [Trusted Types Helpers](#trusted-types-helpers)
|
|
@@ -24,6 +25,21 @@ The parts are processed and written to an output directory, then exposed to a ca
|
|
|
24
25
|
3. Expose HTML for the web component to builds for companion templates and/or DSD for SSR builds
|
|
25
26
|
4. Enable/ease paying these conveniences forward in web component distribution packages
|
|
26
27
|
|
|
28
|
+
## Examples
|
|
29
|
+
New here? The [`examples/`](./examples/) directory has seven self-contained examples — each with its own fixtures, build script, and README — covering the most common ways to build a web component with this library. Clone the repo and run any one from the root:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
node examples/js-css-html/build.mjs # the canonical js + css + html build
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
* [js-css-html](./examples/js-css-html/) — minified css + html injected into a JS token; all three outputs written
|
|
36
|
+
* [pure-js](./examples/pure-js/) — javascript-only components (no templates)
|
|
37
|
+
* [pure-css](./examples/pure-css/) — minify a stylesheet for distribution / CSP hashes
|
|
38
|
+
* [inline-style-no-html](./examples/inline-style-no-html/) — css injected as a bare `<style>` payload, no html file
|
|
39
|
+
* [link-href](./examples/link-href/) — reference an external stylesheet with a `<link>` tag instead of inlining css
|
|
40
|
+
* [multi-template](./examples/multi-template/) — several authored states (default / empty / error) and the `sharedMultiTemplate` option, with a renderable demo page
|
|
41
|
+
* [trusted-types](./examples/trusted-types/) — components authored against the Trusted Types helpers, with an XSS-probe demo
|
|
42
|
+
|
|
27
43
|
## Processing Map
|
|
28
44
|
The following is a table of _some_ of the possible input, processing, and output combos. See [options](#options-object-optional) for detailed explanations.
|
|
29
45
|
|
|
@@ -44,24 +60,28 @@ The following is a table of _some_ of the possible input, processing, and output
|
|
|
44
60
|
|
|
45
61
|
> By default, html minification minifies any css found therein.
|
|
46
62
|
|
|
47
|
-
##
|
|
48
|
-
A component can carry several HTML templates (e.g. default / error / empty states), each referenced by its own distinct token in the javascript
|
|
63
|
+
## Templates
|
|
64
|
+
The `templates` array is how html (and css/link) are described to the build. A component can carry several HTML templates (e.g. default / error / empty states), each referenced by its own distinct token in the javascript:
|
|
49
65
|
|
|
50
66
|
```javascript
|
|
51
67
|
const result = await build(outputDir, {
|
|
52
68
|
jsPath: '/some/path/file.js',
|
|
53
|
-
cssPath: '/some/path/file.css', // shared
|
|
69
|
+
cssPath: '/some/path/file.css', // shared; embedding follows `sharedMultiTemplate` (default "first")
|
|
54
70
|
templates: [
|
|
55
71
|
{ name: 'default', htmlPath: '/some/path/default.html', token: '__TPL_DEFAULT__' },
|
|
56
72
|
{ name: 'error', htmlPath: '/some/path/error.html', token: '__TPL_ERROR__' }
|
|
57
73
|
]
|
|
58
74
|
});
|
|
59
|
-
// result.
|
|
75
|
+
// result.html -> { default: {name, path, getHtml}, error: {...} } keyed by template name
|
|
60
76
|
```
|
|
61
|
-
Each entry takes `name` (output filename, defaults to the input basename), `htmlPath`, `token` (String or RegExp), and
|
|
77
|
+
Each entry takes `name` (output filename, defaults to the input basename), `htmlPath`, `token` (String or RegExp), and optional per-template `cssPath` / `cssLinkHref` overrides that fall back to the shared values. A template may omit `htmlPath` (then only its css/link payload is injected). Pure javascript or css builds pass no templates.
|
|
78
|
+
|
|
79
|
+
> By default (`sharedMultiTemplate: "first"`) the shared `cssPath`/`cssLinkHref` are embedded only in the **first** template that uses them, so several templates of one component placed into a single shadow root do not duplicate the css. Set `sharedMultiTemplate: "every"` to embed the shared styles in each template's output instead, keeping every html self-contained (needed when a template may ship alone, e.g. per-state SSR or standalone fragments). Per-template `cssPath`/`cssLinkHref` overrides are always embedded in their own template.
|
|
62
80
|
|
|
63
81
|
> Injection is **syntax-aware**: markup is spliced into the token's string/template literal with escaping for that context, so it may safely contain quotes, backticks, `${`, or backslashes.
|
|
64
82
|
|
|
83
|
+
> **Tokens MUST be unique in the javascript source.** The injector locates each token by its first occurrence in the file — if a token string also appears in a comment, a log message, or any other place, injection targets that occurrence instead (and throws when it isn't inside a string/template literal). Pick tokens that can only ever appear as the replacement placeholder (e.g. `__MY_COMPONENT_TPL__`), and don't write them anywhere else in the file.
|
|
84
|
+
|
|
65
85
|
## Trusted Types Helpers
|
|
66
86
|
|
|
67
87
|
In addition to `build`, this package exports a small set of browser runtime helpers so that web components can be authored to work **with** and **without** [Trusted Types](https://developer.mozilla.org/en-US/docs/Web/API/Trusted_Types) enforcement (CSP `require-trusted-types-for 'script'`).
|
|
@@ -107,8 +127,9 @@ shadowRoot.innerHTML = trustedHtml('my-component', '<div>…static template…</
|
|
|
107
127
|
cssPath: '/some/path/file.css',
|
|
108
128
|
cssLinkHref: '//some/path/file.css',
|
|
109
129
|
jsPath: '/some/path/file.js',
|
|
110
|
-
|
|
111
|
-
|
|
130
|
+
templates: [
|
|
131
|
+
{ name: 'index', htmlPath: '/some/path/file.html', token: '__REPLACEMENT_IN_JS__' }
|
|
132
|
+
],
|
|
112
133
|
terserOptions: { /* terser options */ },
|
|
113
134
|
htmlminOptions: { /* html-minifier options */ },
|
|
114
135
|
cleancssOptions: { /* clean-css options */ },
|
|
@@ -118,11 +139,12 @@ shadowRoot.innerHTML = trustedHtml('my-component', '<div>…static template…</
|
|
|
118
139
|
|
|
119
140
|
// Retrieve processed content
|
|
120
141
|
const [js, css, html] = await Promise.all([
|
|
121
|
-
result.getJs(), result.getCss(), result.getHtml()
|
|
142
|
+
result.getJs(), result.getCss(), result.html.index.getHtml()
|
|
122
143
|
]);
|
|
123
144
|
|
|
124
145
|
// Retrieve output paths
|
|
125
|
-
const [jsPath, cssPath, htmlPath] =
|
|
146
|
+
const [jsPath, cssPath, htmlPath] =
|
|
147
|
+
[result.jsPath, result.cssPath, result.html.index.path];
|
|
126
148
|
```
|
|
127
149
|
|
|
128
150
|
## API
|
|
@@ -132,7 +154,7 @@ build (outputDir, options): Result
|
|
|
132
154
|
```
|
|
133
155
|
|
|
134
156
|
### outputDir {String}, required
|
|
135
|
-
Full path to the output directory where css, html, and javascript output are written.
|
|
157
|
+
Full path to the output directory where css, html, and javascript output are written. The directory **must already exist** — `build()` throws upfront if it doesn't (or is not a directory), and never creates or cleans it itself. Creating it is the caller's job (`fs.mkdir(outputDir, { recursive: true })`); cleaning stale outputs between builds is up to your build pipeline too.
|
|
136
158
|
|
|
137
159
|
### Options {Object}, optional*
|
|
138
160
|
\* Not really. One or more of `cssPath`, `jsPath`, and/or `htmlPath` **must** be supplied. They have no default, so if no options are supplied, this library throws an exception.
|
|
@@ -148,26 +170,18 @@ Full path to the output directory where css, html, and javascript output are wri
|
|
|
148
170
|
If supplied:
|
|
149
171
|
+ href will be wrapped in a `link` tag
|
|
150
172
|
+ resulting `link` will be prepended to the html file if `htmlPath` supplied
|
|
151
|
-
+ resulting `link` will be inserted into the javascript file if no `htmlPath` supplied and `jsReplacement` and `jsPath` supplied
|
|
173
|
+
+ resulting `link` will be inserted into the javascript file if no `htmlPath` supplied and `jsReplacement` and `jsPath` supplied
|
|
152
174
|
|
|
153
|
-
* **
|
|
154
|
-
If supplied:
|
|
155
|
-
+ css will be prepended in a `style` tag
|
|
156
|
-
+ cssLinkHref will be prepended in a `link` tag
|
|
157
|
-
+ html will be inserted into the javascript file if `jsReplacement` and `jsPath` is supplied
|
|
175
|
+
* **sharedMultiTemplate** {String} - How SHARED `cssPath`/`cssLinkHref` are embedded across templates. Defaults to `"first"`: shared styles are embedded only in the first template that uses them, so several templates of one component placed into a single shadow root do not duplicate the css (later templates carry markup only). Use `"every"` to embed the shared styles in each template's output, keeping every html self-contained (needed when a template may ship alone, e.g. per-state SSR/standalone fragments). Per-template `cssPath`/`cssLinkHref` overrides are always embedded in their own template, in either mode. Any other value throws.
|
|
158
176
|
|
|
159
177
|
* **jsPath** {String} - Full path to the input javascript file
|
|
160
|
-
* **jsReplacement** {String|RegExp} - The replacement pattern for the css or html in the javascript file (single template). See [pattern](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/replace#pattern) for full documentation
|
|
161
|
-
If supplied:
|
|
162
|
-
+ A replacement will be attempted in the javascript file, `jsPath` must also be supplied
|
|
163
|
-
+ If **not supplied** or falsy, No replacement will be attempted and all assets are just copied to `outputDir`
|
|
164
|
-
|
|
165
178
|
* **templates** {Array} - Zero or more templates, each an object with:
|
|
166
|
-
* **name** {String} - Output filename without extension (`${name}.html`). Defaults to the input basename.
|
|
179
|
+
* **name** {String} - Output filename without extension (written as `${name}.html`). Defaults to the input basename.
|
|
167
180
|
* **htmlPath** {String} - Full path to the input html for this template.
|
|
168
|
-
* **token** {String|RegExp} - The placeholder in the javascript to replace.
|
|
181
|
+
* **token** {String|RegExp} - The placeholder in the javascript to replace. See [pattern](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/replace#pattern). **Must be unique in the source file** — the first occurrence is the one replaced, so a token string mentioned in a comment or other literal redirects injection there.
|
|
182
|
+
* **cssPath** {String} - Optional per-template css override (falls back to the shared `cssPath`).
|
|
169
183
|
* **cssLinkHref** {String} - Optional per-template link href override (falls back to the shared `cssLinkHref`).
|
|
170
|
-
|
|
184
|
+
Shared `cssPath`/`cssLinkHref` follow the `sharedMultiTemplate` mode: embedded in the first template that uses them by default (`"first"`), or in every template when `sharedMultiTemplate: "every"`. Per-template overrides are always embedded in their own template. A token with no `jsPath` throws, as do duplicate resolved output names and invalid `sharedMultiTemplate` values. The flat `htmlPath`/`jsReplacement` options were removed in v4 and now throw a migration error.
|
|
171
185
|
|
|
172
186
|
* **terserOptions** {Object} - The [javascript minifier options](https://github.com/terser/terser/blob/master/README.md#minify-options) object
|
|
173
187
|
Defaults:
|
|
@@ -194,22 +208,20 @@ Full path to the output directory where css, html, and javascript output are wri
|
|
|
194
208
|
|
|
195
209
|
* **minifySkip** {Boolean} - True to skip all minifications, defaults to false
|
|
196
210
|
|
|
211
|
+
* **deprecationWarnings** {Boolean} - False to disable deprecation warnings, defaults to true. If omitted, deprecation warnings are suppressed by defining environment variable `WEB_COMPONENT_BUILD_NO_DEPRECATION_WARNINGS`
|
|
212
|
+
|
|
197
213
|
### Result {Object}
|
|
198
214
|
The output of the build process. Allows access to the output paths and full output content. Format:
|
|
199
215
|
|
|
200
216
|
+ **cssPath** {String}, The full path to the output css
|
|
201
217
|
|
|
202
|
-
+ **htmlPath** {String}, The full path to the output html
|
|
203
|
-
|
|
204
218
|
+ **jsPath** {String}, The full path to the output javascript
|
|
205
219
|
|
|
206
220
|
+ **getCss** {asyncFunction}, gets the output css
|
|
207
221
|
|
|
208
|
-
+ **getHtml** {asyncFunction}, gets the output html
|
|
209
|
-
|
|
210
222
|
+ **getJs** {asyncFunction}, gets the output javascript
|
|
211
223
|
|
|
212
|
-
+ **
|
|
224
|
+
+ **html** {Object}, A map keyed by template name. Each entry: `{ name, path, getHtml }` where `path` is the full path to that template's output html and `getHtml` (async) returns its content.
|
|
213
225
|
|
|
214
226
|
## License
|
|
215
227
|
* [BSD-3 Clasuse, Alex Grant, LocalNerve](LICENSE.md)
|
package/lib/build.js
CHANGED
|
@@ -73,10 +73,10 @@ class WebComponentBuild {
|
|
|
73
73
|
const cssText = await fs.readFile(this.inputCssFile, {
|
|
74
74
|
encoding: 'utf8'
|
|
75
75
|
});
|
|
76
|
-
|
|
76
|
+
|
|
77
77
|
const cleanCss =
|
|
78
78
|
this.minifySkip ? cssText : new CleanCss(options).minify(cssText).styles;
|
|
79
|
-
|
|
79
|
+
|
|
80
80
|
await fs.writeFile(this.outputCssFile, cleanCss, {
|
|
81
81
|
encoding: 'utf8'
|
|
82
82
|
});
|
|
@@ -84,6 +84,19 @@ class WebComponentBuild {
|
|
|
84
84
|
return cleanCss;
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
+
/**
|
|
88
|
+
* Minify a css file at an explicit path (used for per-template css overrides)
|
|
89
|
+
* without writing it to the output directory.
|
|
90
|
+
*
|
|
91
|
+
* @param {String} cssPath - The full path to the css file.
|
|
92
|
+
* @param {Object} options - clean-css options
|
|
93
|
+
* @returns {String} minified css.
|
|
94
|
+
*/
|
|
95
|
+
async minifyCssFile (cssPath, options = {}) {
|
|
96
|
+
const cssText = await fs.readFile(cssPath, { encoding: 'utf8' });
|
|
97
|
+
return this.minifySkip ? cssText : new CleanCss(options).minify(cssText).styles;
|
|
98
|
+
}
|
|
99
|
+
|
|
87
100
|
/**
|
|
88
101
|
* Minify the given html and write it to a specific output file.
|
|
89
102
|
*
|
|
@@ -131,13 +144,17 @@ class WebComponentBuild {
|
|
|
131
144
|
const _cssPath = this.outputCssFile;
|
|
132
145
|
const _jsPath = this.outputJsFile;
|
|
133
146
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
147
|
+
// v4: html outputs are exposed as a map keyed by template name.
|
|
148
|
+
const html = {};
|
|
149
|
+
for (const output of this.htmlOutputs) {
|
|
150
|
+
html[output.name] = {
|
|
151
|
+
name: output.name,
|
|
152
|
+
path: output.path,
|
|
153
|
+
async getHtml () {
|
|
154
|
+
return fs.readFile(output.path, { encoding: 'utf8' });
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
}
|
|
141
158
|
|
|
142
159
|
return {
|
|
143
160
|
get cssPath () {
|
|
@@ -150,17 +167,7 @@ class WebComponentBuild {
|
|
|
150
167
|
}
|
|
151
168
|
return cssText;
|
|
152
169
|
},
|
|
153
|
-
|
|
154
|
-
get htmlPath () {
|
|
155
|
-
return htmls[0] ? htmls[0].path : undefined;
|
|
156
|
-
},
|
|
157
|
-
async getHtml () {
|
|
158
|
-
let htmlText;
|
|
159
|
-
if (htmls[0]) {
|
|
160
|
-
htmlText = await htmls[0].getHtml();
|
|
161
|
-
}
|
|
162
|
-
return htmlText;
|
|
163
|
-
},
|
|
170
|
+
html,
|
|
164
171
|
get jsPath () {
|
|
165
172
|
return _jsPath;
|
|
166
173
|
},
|
|
@@ -170,8 +177,7 @@ class WebComponentBuild {
|
|
|
170
177
|
jsText = await fs.readFile(_jsPath, { encoding: 'utf8' });
|
|
171
178
|
}
|
|
172
179
|
return jsText;
|
|
173
|
-
}
|
|
174
|
-
htmls
|
|
180
|
+
}
|
|
175
181
|
};
|
|
176
182
|
}
|
|
177
183
|
}
|
package/lib/index.js
CHANGED
|
@@ -11,7 +11,6 @@ import * as fs from 'node:fs/promises';
|
|
|
11
11
|
import * as cheerio from 'cheerio';
|
|
12
12
|
import { createBuild, defaultHtmlMinifyOptions } from './build.js';
|
|
13
13
|
import { injectTokens } from './replace.js';
|
|
14
|
-
import { log, deprecationsEnabled } from './log.js';
|
|
15
14
|
|
|
16
15
|
/**
|
|
17
16
|
* Derive an html output base name (without extension) from an input path.
|
|
@@ -29,96 +28,108 @@ function htmlBaseName (htmlPath) {
|
|
|
29
28
|
/**
|
|
30
29
|
* Build entry point.
|
|
31
30
|
*
|
|
32
|
-
* @param {String} outputDir - full path to the output directory
|
|
33
|
-
*
|
|
31
|
+
* @param {String} outputDir - full path to the output directory. MUST exist before
|
|
32
|
+
* calling build(); the library never creates (or cleans) directories itself.
|
|
33
|
+
* @param {Object} options - build options
|
|
34
|
+
* @param {Array} options.templates - REQUIRED. List of templates, each an object with:
|
|
35
|
+
* name (output filename), htmlPath, token (String|RegExp), and optional cssPath /
|
|
36
|
+
* cssLinkHref overrides that fall back to the shared top-level values.
|
|
34
37
|
* @param {String} [options.jsPath] - full path to the javascript file
|
|
35
|
-
* @param {String} [options.cssPath] -
|
|
36
|
-
* @param {String} [options.cssLinkHref] -
|
|
37
|
-
* @param {String} [options.
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
* cssPath/cssLinkHref are
|
|
42
|
-
* its own cssLinkHref.
|
|
38
|
+
* @param {String} [options.cssPath] - shared css; embedded into templates per the `sharedMultiTemplate` mode (default "first")
|
|
39
|
+
* @param {String} [options.cssLinkHref] - shared link href; embedded into templates per the `sharedMultiTemplate` mode (default "first")
|
|
40
|
+
* @param {String} [options.sharedMultiTemplate] - how SHARED css/link are embedded across templates:
|
|
41
|
+
* "first" (default) embeds them only in the first template that uses them, so several
|
|
42
|
+
* templates of one component placed in a single shadow root do not duplicate the
|
|
43
|
+
* styles; "every" embeds them in each template (each output stays self-contained).
|
|
44
|
+
* Per-template cssPath/cssLinkHref overrides are ALWAYS embedded in their own template.
|
|
43
45
|
* @param {Object} [options.terserOptions] - Js minifier options (terser)
|
|
44
46
|
* @param {Object} [options.htmlminOptions] - html-minifier options
|
|
45
47
|
* @param {Object} [options.cleancssOptions] - clean-css options
|
|
46
48
|
* @param {Boolean} [options.minifySkip] - default false, flag to skip all minification (debug)
|
|
47
|
-
* @
|
|
48
|
-
* messages for flat single-template options and the htmlPath/getHtml shorthands
|
|
49
|
-
* that are removed in v4.0.0. Set false (or env WEB_COMPONENT_BUILD_NO_DEPRECATION_WARNINGS) to suppress.
|
|
50
|
-
* @returns {Object} Interface to getCss, getHtml, getJs, and htmls for further processing
|
|
49
|
+
* @returns {Object} Interface exposing cssPath/getCss, jsPath/getJs, and an html map keyed by template name
|
|
51
50
|
*/
|
|
52
51
|
export async function build (outputDir, {
|
|
53
52
|
jsPath,
|
|
54
53
|
cssPath,
|
|
55
54
|
cssLinkHref,
|
|
56
|
-
htmlPath,
|
|
57
|
-
jsReplacement,
|
|
58
55
|
templates = [],
|
|
56
|
+
htmlPath, // v4: removed — detected only to emit a migration error
|
|
57
|
+
jsReplacement, // v4: removed — detected only to emit a migration error
|
|
58
|
+
sharedMultiTemplate = 'first',
|
|
59
59
|
terserOptions,
|
|
60
60
|
htmlminOptions,
|
|
61
61
|
cleancssOptions,
|
|
62
|
-
minifySkip = false
|
|
63
|
-
deprecationWarnings
|
|
62
|
+
minifySkip = false
|
|
64
63
|
} = {}) {
|
|
65
|
-
//
|
|
66
|
-
//
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
&& Boolean(htmlPath || jsReplacement);
|
|
72
|
-
|
|
73
|
-
if (usesFlatOptions && deprecationsEnabled({ deprecationWarnings })) {
|
|
74
|
-
log(
|
|
75
|
-
'web-component-build',
|
|
76
|
-
'The flat single-template options "htmlPath"/"jsReplacement" are deprecated and will be removed in v4.0.0. Migrate to the "templates" array, e.g. templates: [{ name, htmlPath, token }].',
|
|
77
|
-
'warn'
|
|
78
|
-
);
|
|
79
|
-
log(
|
|
80
|
-
'web-component-build',
|
|
81
|
-
'In v4.0.0 the result\'s html surface changes from getHtml()/htmlPath to a named map: result.html[name] (each { name, path, getHtml }). Per-template cssPath/cssLinkHref overrides will also be supported.',
|
|
82
|
-
'warn'
|
|
64
|
+
// v4: the flat single-template options are gone. Detect them so callers get a
|
|
65
|
+
// clear migration message instead of a silent no-op.
|
|
66
|
+
if (htmlPath !== undefined || jsReplacement !== undefined) {
|
|
67
|
+
throw new Error(
|
|
68
|
+
'v4 removed the flat "htmlPath"/"jsReplacement" options. Migrate to the ' +
|
|
69
|
+
'"templates" array, e.g. templates: [{ name, htmlPath, token }].'
|
|
83
70
|
);
|
|
84
71
|
}
|
|
72
|
+
if (!Array.isArray(templates)) {
|
|
73
|
+
throw new Error('"templates" must be an array.');
|
|
74
|
+
}
|
|
75
|
+
if (sharedMultiTemplate !== 'first' && sharedMultiTemplate !== 'every') {
|
|
76
|
+
throw new Error('"sharedMultiTemplate" must be "first" or "every".');
|
|
77
|
+
}
|
|
85
78
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
cssLinkHref
|
|
100
|
-
}];
|
|
101
|
-
|
|
102
|
-
const hasHtmlTemplate = resolved.some(t => t.htmlPath);
|
|
103
|
-
if (!jsPath && !cssPath && !hasHtmlTemplate) {
|
|
79
|
+
const resolved = templates.map((t, i) => ({
|
|
80
|
+
name: t.name || (t.htmlPath ? htmlBaseName(t.htmlPath) : `template-${i}`),
|
|
81
|
+
htmlPath: t.htmlPath,
|
|
82
|
+
token: t.token,
|
|
83
|
+
cssPath: t.cssPath !== undefined ? t.cssPath : cssPath,
|
|
84
|
+
cssLinkHref: t.cssLinkHref !== undefined ? t.cssLinkHref : cssLinkHref
|
|
85
|
+
}));
|
|
86
|
+
|
|
87
|
+
// A token with no jsPath to inject into is always an error.
|
|
88
|
+
if (!jsPath && resolved.some(t => t.token)) {
|
|
89
|
+
throw new Error('Invalid input, a replacement token was supplied without jsPath. Did you forget \'jsPath\'?');
|
|
90
|
+
}
|
|
91
|
+
if (!jsPath && !cssPath && resolved.every(t => !t.htmlPath)) {
|
|
104
92
|
throw new Error(
|
|
105
|
-
'One of jsPath, cssPath, or htmlPath MUST be supplied to do something\n meaningful. Did you forget something?'
|
|
93
|
+
'One of jsPath, cssPath, or a template htmlPath MUST be supplied to do something\n meaningful. Did you forget something?'
|
|
106
94
|
);
|
|
107
95
|
}
|
|
108
96
|
|
|
97
|
+
// outputDir is the caller's responsibility: check it exists (and is a directory)
|
|
98
|
+
// upfront so a typo'd path fails fast with a clear message instead of ENOENT
|
|
99
|
+
// deep inside the first write.
|
|
100
|
+
let stat;
|
|
101
|
+
try {
|
|
102
|
+
stat = await fs.stat(outputDir);
|
|
103
|
+
} catch {
|
|
104
|
+
throw new Error(`outputDir does not exist: "${outputDir}". Create it before calling build().`);
|
|
105
|
+
}
|
|
106
|
+
if (!stat.isDirectory()) {
|
|
107
|
+
throw new Error(`outputDir is not a directory: "${outputDir}".`);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// The shared (top-level) cssPath is the canonical output css file. Per-template
|
|
111
|
+
// css overrides are minified on demand and used only for embedding.
|
|
109
112
|
const build = createBuild(outputDir, jsPath, cssPath, minifySkip);
|
|
110
|
-
let jsText
|
|
113
|
+
let jsText;
|
|
111
114
|
|
|
112
115
|
if (jsPath) {
|
|
113
116
|
jsText = await fs.readFile(jsPath, { encoding: 'utf8' });
|
|
114
|
-
} else if (jsReplacement || resolved.some(t => t.token)) {
|
|
115
|
-
throw new Error('Invalid input, a replacement token was supplied without jsPath. Did you forget \'jsPath\'?');
|
|
116
117
|
}
|
|
117
118
|
|
|
119
|
+
const cssCache = new Map(); // css path -> minified text
|
|
118
120
|
if (cssPath) {
|
|
119
|
-
|
|
121
|
+
cssCache.set(cssPath, await build.minifyCss(cleancssOptions));
|
|
120
122
|
}
|
|
121
123
|
|
|
124
|
+
// Resolve the css text to embed for a template's effective css path.
|
|
125
|
+
const cssFor = async p => {
|
|
126
|
+
if (!p) return undefined;
|
|
127
|
+
if (!cssCache.has(p)) {
|
|
128
|
+
cssCache.set(p, await build.minifyCssFile(p, cleancssOptions));
|
|
129
|
+
}
|
|
130
|
+
return cssCache.get(p);
|
|
131
|
+
};
|
|
132
|
+
|
|
122
133
|
// Detect duplicate html output names before writing anything.
|
|
123
134
|
const seenNames = new Set();
|
|
124
135
|
for (const template of resolved.filter(t => t.htmlPath)) {
|
|
@@ -130,28 +141,45 @@ export async function build (outputDir, {
|
|
|
130
141
|
seenNames.add(template.name);
|
|
131
142
|
}
|
|
132
143
|
|
|
133
|
-
// Process each template: minify its html (with
|
|
134
|
-
// write it, or compute an inline css/link payload when
|
|
144
|
+
// Process each template: minify its html (with its style/link prepended per the
|
|
145
|
+
// sharedMultiTemplate mode) and write it, or compute an inline css/link payload when
|
|
146
|
+
// there is no html file. Shared css/link are embedded only in the first template
|
|
147
|
+
// that uses them unless sharedMultiTemplate is "every"; per-template cssPath/cssLinkHref
|
|
148
|
+
// overrides are ALWAYS embedded in their own template (and keep every output
|
|
149
|
+
// self-contained).
|
|
135
150
|
const injection = [];
|
|
151
|
+
let sharedCssEmbedded;
|
|
152
|
+
let sharedLinkEmbedded;
|
|
136
153
|
for (const template of resolved) {
|
|
154
|
+
const hasOwnCss = template.cssPath !== undefined && template.cssPath !== cssPath;
|
|
155
|
+
const hasOwnLink =
|
|
156
|
+
template.cssLinkHref !== undefined && template.cssLinkHref !== cssLinkHref;
|
|
157
|
+
const embedCss = hasOwnCss || sharedMultiTemplate === 'every' || !sharedCssEmbedded;
|
|
158
|
+
const embedLink = hasOwnLink || sharedMultiTemplate === 'every' || !sharedLinkEmbedded;
|
|
159
|
+
|
|
160
|
+
const templateCss = embedCss ? await cssFor(template.cssPath) : undefined;
|
|
137
161
|
let payload;
|
|
138
162
|
if (template.htmlPath) {
|
|
139
163
|
const outputPath = build.addHtmlOutput(template.name);
|
|
140
164
|
const sourceHtml = await fs.readFile(template.htmlPath, { encoding: 'utf8' });
|
|
141
165
|
const $ = cheerio.load(sourceHtml);
|
|
142
166
|
const opts = { ...(htmlminOptions || defaultHtmlMinifyOptions) };
|
|
143
|
-
if (
|
|
144
|
-
$('body').prepend(`<style>${
|
|
167
|
+
if (templateCss) {
|
|
168
|
+
$('body').prepend(`<style>${templateCss}</style>`);
|
|
145
169
|
opts.minifyCSS = cleancssOptions;
|
|
170
|
+
if (!hasOwnCss) sharedCssEmbedded = true;
|
|
146
171
|
}
|
|
147
|
-
if (template.cssLinkHref) {
|
|
172
|
+
if (embedLink && template.cssLinkHref) {
|
|
148
173
|
$('body').prepend(`<link href="${template.cssLinkHref}" rel="stylesheet" />`);
|
|
174
|
+
if (!hasOwnLink) sharedLinkEmbedded = true;
|
|
149
175
|
}
|
|
150
176
|
payload = await build.writeHtml($('body').html(), outputPath, opts);
|
|
151
|
-
} else if (
|
|
152
|
-
payload = `<style>${
|
|
153
|
-
|
|
177
|
+
} else if (templateCss) {
|
|
178
|
+
payload = `<style>${templateCss}</style>`;
|
|
179
|
+
if (!hasOwnCss) sharedCssEmbedded = true;
|
|
180
|
+
} else if (embedLink && template.cssLinkHref) {
|
|
154
181
|
payload = `<link href="${template.cssLinkHref}" rel="stylesheet" />`;
|
|
182
|
+
if (!hasOwnLink) sharedLinkEmbedded = true;
|
|
155
183
|
}
|
|
156
184
|
|
|
157
185
|
if (payload !== undefined && template.token) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@localnerve/web-component-build",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "4.0.0",
|
|
4
4
|
"description": "A library to help build web components",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -62,7 +62,7 @@
|
|
|
62
62
|
},
|
|
63
63
|
"devDependencies": {
|
|
64
64
|
"@eslint/js": "^10.0.1",
|
|
65
|
-
"eslint": "^10.
|
|
65
|
+
"eslint": "^10.10.0",
|
|
66
66
|
"eslint-plugin-n": "^18.3.0",
|
|
67
67
|
"globals": "^17.12.0",
|
|
68
68
|
"he": "^1.2.0",
|