@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 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
- ## Multiple Templates
48
- A component can carry several HTML templates (e.g. default / error / empty states), each referenced by its own distinct token in the javascript. Pass a `templates` array:
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, prepended to every template
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.htmls -> [{ name, path, getHtml }, ...] in input order
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 an optional per-template `cssLinkHref` override. The single-template `htmlPath` + `jsReplacement` options still work and are treated as one template.
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
- htmlPath: '/some/path/file.html',
111
- jsReplacement: '__REPLACEMENT_IN_JS__',
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] = [result.jsPath, result.cssPath, result.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
- * **htmlPath** {String} - Full path to the input html file
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
- When supplied, takes precedence over the flat `htmlPath`/`jsReplacement` options. Shared `cssPath`/`cssLinkHref` are applied to every template. Duplicate resolved output names throw.
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
- + **htmls** {Array}, ordered list (matching `templates` input order) of `{ name, path, getHtml }` one entry per html template. `getHtml()`/`htmlPath` above are shorthands for the first entry.
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
- const htmls = this.htmlOutputs.map(output => ({
135
- name: output.name,
136
- path: output.path,
137
- async getHtml () {
138
- return fs.readFile(output.path, { encoding: 'utf8' });
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
- // Backward-compatible shorthands resolve to the first html output.
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
- * @param {Object} [options] - optional options
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] - full path to the input css file
36
- * @param {String} [options.cssLinkHref] - http href to css resource
37
- * @param {String} [options.htmlPath] - full path to the input html file (single template)
38
- * @param {String} [options.jsReplacement] - Replacement token in the js file (single template)
39
- * @param {Array} [options.templates] - list of templates, each {name, htmlPath, token,
40
- * cssLinkHref}. When supplied, takes precedence over the flat options. Shared
41
- * cssPath/cssLinkHref are applied to every template unless a template overrides
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
- * @param {Boolean} [options.deprecationWarnings=true] - default true, emit console.warn
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
- // Deprecation / migration warnings for the flat single-template API. These
66
- // options (and the getHtml()/htmlPath result shorthands) are removed in
67
- // v4.0.0, where "templates" becomes required and html outputs become a named
68
- // map (result.html[name]). Suppressed by deprecationWarnings: false or the
69
- // WEB_COMPONENT_BUILD_NO_DEPRECATION_WARNINGS env var.
70
- const usesFlatOptions = (!Array.isArray(templates) || templates.length === 0)
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
- // Resolve the template list. When `templates` is absent, synthesize a single
87
- // template from the flat options for backward compatibility.
88
- const resolved = (Array.isArray(templates) && templates.length > 0)
89
- ? templates.map((t, i) => ({
90
- name: t.name || (t.htmlPath ? htmlBaseName(t.htmlPath) : `template-${i}`),
91
- htmlPath: t.htmlPath,
92
- token: t.token,
93
- cssLinkHref: t.cssLinkHref !== undefined ? t.cssLinkHref : cssLinkHref
94
- }))
95
- : [{
96
- name: htmlPath ? htmlBaseName(htmlPath) : undefined,
97
- htmlPath,
98
- token: jsReplacement,
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, cssText;
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
- cssText = await build.minifyCss(cleancssOptions);
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 shared style/link prepended) and
134
- // write it, or compute an inline css/link payload when there is no html file.
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 (cssText) {
144
- $('body').prepend(`<style>${cssText}</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 (cssText) {
152
- payload = `<style>${cssText}</style>`;
153
- } else if (template.cssLinkHref) {
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.5.0",
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.9.1",
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",