@webstir-io/webstir-frontend 0.1.40 → 0.1.41

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.
Files changed (138) hide show
  1. package/README.md +124 -60
  2. package/dist/assets/imageOptimizer.js +10 -15
  3. package/dist/assets/precompression.js +1 -1
  4. package/dist/builders/contentBuilder.js +102 -90
  5. package/dist/builders/cssBuilder.js +25 -19
  6. package/dist/builders/htmlBuilder.js +57 -42
  7. package/dist/builders/index.js +1 -1
  8. package/dist/builders/jsBuilder.js +219 -76
  9. package/dist/builders/staticAssetsBuilder.js +27 -9
  10. package/dist/builders/types.d.ts +1 -0
  11. package/dist/cli.d.ts +1 -1
  12. package/dist/cli.js +6 -30
  13. package/dist/config/manifest.js +7 -6
  14. package/dist/config/paths.js +2 -2
  15. package/dist/config/schema.d.ts +8 -0
  16. package/dist/config/schema.js +7 -6
  17. package/dist/config/setup.js +1 -1
  18. package/dist/config/workspace.js +11 -9
  19. package/dist/core/constants.d.ts +1 -1
  20. package/dist/core/constants.js +5 -5
  21. package/dist/core/diagnostics.js +1 -1
  22. package/dist/core/pages.js +4 -4
  23. package/dist/hooks.js +3 -3
  24. package/dist/html/criticalCss.js +6 -3
  25. package/dist/html/htmlSecurity.d.ts +6 -1
  26. package/dist/html/htmlSecurity.js +28 -14
  27. package/dist/html/lazyLoad.js +1 -1
  28. package/dist/html/pageScaffold.js +1 -1
  29. package/dist/html/resourceHints.js +5 -2
  30. package/dist/index.d.ts +2 -0
  31. package/dist/index.js +2 -0
  32. package/dist/inspect.d.ts +2 -0
  33. package/dist/inspect.js +110 -0
  34. package/dist/modes/ssg/metadata.js +4 -4
  35. package/dist/modes/ssg/routing.js +2 -5
  36. package/dist/modes/ssg/seo.js +5 -5
  37. package/dist/modes/ssg/views.js +17 -11
  38. package/dist/operations.js +18 -10
  39. package/dist/pipeline.d.ts +1 -0
  40. package/dist/pipeline.js +6 -1
  41. package/dist/provider.js +28 -24
  42. package/dist/runtime/boundary.d.ts +28 -0
  43. package/dist/runtime/boundary.js +247 -0
  44. package/dist/runtime/index.d.ts +1 -0
  45. package/dist/runtime/index.js +1 -0
  46. package/dist/types.d.ts +52 -0
  47. package/dist/utils/fs.d.ts +11 -10
  48. package/dist/utils/fs.js +48 -20
  49. package/dist/utils/glob.d.ts +8 -0
  50. package/dist/utils/glob.js +21 -0
  51. package/dist/utils/hash.js +1 -2
  52. package/dist/utils/pagePaths.js +2 -2
  53. package/package.json +19 -14
  54. package/scripts/publish.sh +2 -94
  55. package/scripts/update-contract.sh +12 -10
  56. package/src/assets/assetManifest.ts +39 -29
  57. package/src/assets/imageOptimizer.ts +91 -82
  58. package/src/assets/precompression.ts +22 -16
  59. package/src/builders/contentBuilder.ts +1224 -1149
  60. package/src/builders/cssBuilder.ts +466 -417
  61. package/src/builders/htmlBuilder.ts +511 -448
  62. package/src/builders/index.ts +7 -7
  63. package/src/builders/jsBuilder.ts +538 -280
  64. package/src/builders/staticAssetsBuilder.ts +166 -135
  65. package/src/builders/types.ts +7 -6
  66. package/src/cli.ts +66 -90
  67. package/src/config/manifest.ts +16 -14
  68. package/src/config/paths.ts +5 -5
  69. package/src/config/schema.ts +38 -37
  70. package/src/config/setup.ts +7 -7
  71. package/src/config/workspace.ts +118 -116
  72. package/src/config/workspaceManifest.ts +14 -14
  73. package/src/core/constants.ts +62 -62
  74. package/src/core/diagnostics.ts +26 -26
  75. package/src/core/pages.ts +19 -19
  76. package/src/hooks.ts +128 -118
  77. package/src/html/criticalCss.ts +84 -77
  78. package/src/html/htmlSecurity.ts +107 -66
  79. package/src/html/lazyLoad.ts +22 -19
  80. package/src/html/pageScaffold.ts +37 -28
  81. package/src/html/resourceHints.ts +83 -74
  82. package/src/index.ts +2 -0
  83. package/src/inspect.ts +158 -0
  84. package/src/modes/ssg/metadata.ts +53 -51
  85. package/src/modes/ssg/routing.ts +177 -177
  86. package/src/modes/ssg/seo.ts +208 -200
  87. package/src/modes/ssg/validation.ts +31 -25
  88. package/src/modes/ssg/views.ts +257 -238
  89. package/src/operations.ts +105 -95
  90. package/src/pipeline.ts +81 -69
  91. package/src/provider.ts +184 -176
  92. package/src/runtime/boundary.ts +325 -0
  93. package/src/runtime/index.ts +1 -0
  94. package/src/types.ts +107 -48
  95. package/src/utils/changedFile.ts +22 -22
  96. package/src/utils/fs.ts +73 -26
  97. package/src/utils/glob.ts +38 -0
  98. package/src/utils/hash.ts +2 -4
  99. package/src/utils/pagePaths.ts +35 -23
  100. package/src/utils/pathMatch.ts +26 -23
  101. package/tests/add-page-defaults.test.js +44 -39
  102. package/tests/bundlerParity.test.js +252 -0
  103. package/tests/cli.contract.test.js +13 -0
  104. package/tests/content-pages.test.js +108 -13
  105. package/tests/css-app-imports.test.js +22 -11
  106. package/tests/css-page-imports.test.js +26 -13
  107. package/tests/diagnostics.test.js +39 -36
  108. package/tests/features.test.js +48 -43
  109. package/tests/hooks.test.js +58 -42
  110. package/tests/htmlSecurity.test.js +66 -0
  111. package/tests/inspect.test.js +148 -0
  112. package/tests/provider.integration.test.js +71 -20
  113. package/tests/runtime.test.js +493 -0
  114. package/tests/ssg-defaults.test.js +284 -177
  115. package/tests/ssg-guardrails.test.js +51 -51
  116. package/tsconfig.json +3 -10
  117. package/dist/watch/frontendFiles.d.ts +0 -3
  118. package/dist/watch/frontendFiles.js +0 -25
  119. package/dist/watch/hotUpdateTracker.d.ts +0 -51
  120. package/dist/watch/hotUpdateTracker.js +0 -205
  121. package/dist/watch/pipelineHelpers.d.ts +0 -26
  122. package/dist/watch/pipelineHelpers.js +0 -177
  123. package/dist/watch/types.d.ts +0 -27
  124. package/dist/watch/types.js +0 -1
  125. package/dist/watch/watchCoordinator.d.ts +0 -36
  126. package/dist/watch/watchCoordinator.js +0 -551
  127. package/dist/watch/watchDaemon.d.ts +0 -17
  128. package/dist/watch/watchDaemon.js +0 -127
  129. package/dist/watch/watchReporter.d.ts +0 -21
  130. package/dist/watch/watchReporter.js +0 -64
  131. package/scripts/smoke.mjs +0 -35
  132. package/src/watch/frontendFiles.ts +0 -32
  133. package/src/watch/hotUpdateTracker.ts +0 -285
  134. package/src/watch/pipelineHelpers.ts +0 -242
  135. package/src/watch/types.ts +0 -23
  136. package/src/watch/watchCoordinator.ts +0 -666
  137. package/src/watch/watchDaemon.ts +0 -144
  138. package/src/watch/watchReporter.ts +0 -98
@@ -3,19 +3,20 @@ import postcss from 'postcss';
3
3
  import autoprefixer from 'autoprefixer';
4
4
  import customMedia from 'postcss-custom-media';
5
5
  import * as cssoModule from 'csso';
6
- import { glob } from 'glob';
7
6
  import { FOLDERS, FILES, EXTENSIONS } from '../core/constants.js';
8
7
  import { ensureDir, pathExists, readFile, writeFile, remove, copy } from '../utils/fs.js';
8
+ import { scanGlob } from '../utils/glob.js';
9
9
  import { getPages } from '../core/pages.js';
10
10
  import { hashContent } from '../utils/hash.js';
11
- import { updatePageManifest, updateSharedAssets, readSharedAssets } from '../assets/assetManifest.js';
11
+ import { updatePageManifest, updateSharedAssets, readSharedAssets, } from '../assets/assetManifest.js';
12
12
  import { createCompressedVariants } from '../assets/precompression.js';
13
13
  import { shouldProcess } from '../utils/changedFile.js';
14
14
  import { findPageFromChangedFile } from '../utils/pathMatch.js';
15
15
  const MODULE_SUFFIX = '.module';
16
16
  const APP_CSS_BASENAME = 'app';
17
- const csso = (cssoModule.default ?? cssoModule);
18
- const PAGE_IMPORT_PATTERN = /@import\s+(?:url\()?[\s]*['"]([^'"\)]+)['"][\s]*\)?\s*;?/g;
17
+ const csso = (cssoModule.default ??
18
+ cssoModule);
19
+ const PAGE_IMPORT_PATTERN = /@import\s+(?:url\()?[\s]*['"]([^'"]+)['"][\s]*\)?\s*;?/g;
19
20
  export function createCssBuilder(context) {
20
21
  return {
21
22
  name: 'css',
@@ -24,14 +25,14 @@ export function createCssBuilder(context) {
24
25
  },
25
26
  async publish() {
26
27
  await processCss(context, true);
27
- }
28
+ },
28
29
  };
29
30
  }
30
31
  async function processCss(context, isProduction) {
31
32
  const { config } = context;
32
33
  if (!shouldProcess(context, [
33
34
  { directory: config.paths.src.pages, extensions: [EXTENSIONS.css] },
34
- { directory: config.paths.src.frontend, extensions: [EXTENSIONS.css] }
35
+ { directory: config.paths.src.frontend, extensions: [EXTENSIONS.css] },
35
36
  ])) {
36
37
  return;
37
38
  }
@@ -51,7 +52,10 @@ async function processCss(context, isProduction) {
51
52
  const css = await readFile(entryPath);
52
53
  const inlinedCss = await inlinePageImports(css, page.directory);
53
54
  const prepared = applyCustomMediaPrelude(inlinedCss, customMediaPrelude);
54
- const processed = await processor.process(prepared, { from: entryPath, map: !isProduction ? { inline: true } : false });
55
+ const processed = await processor.process(prepared, {
56
+ from: entryPath,
57
+ map: !isProduction ? { inline: true } : false,
58
+ });
55
59
  const normalized = resolveAppImports(processed.css, isProduction ? sharedArtifacts.appCss : undefined);
56
60
  if (isProduction) {
57
61
  const inlined = await inlineAppImports(normalized, config.paths.dist.frontend);
@@ -83,7 +87,7 @@ async function emitProductionCss(config, pageName, css) {
83
87
  else {
84
88
  await Promise.all([
85
89
  remove(`${outputPath}${EXTENSIONS.br}`).catch(() => undefined),
86
- remove(`${outputPath}${EXTENSIONS.gz}`).catch(() => undefined)
90
+ remove(`${outputPath}${EXTENSIONS.gz}`).catch(() => undefined),
87
91
  ]);
88
92
  }
89
93
  await updatePageManifest(outputDir, pageName, (manifest) => {
@@ -91,7 +95,7 @@ async function emitProductionCss(config, pageName, css) {
91
95
  });
92
96
  }
93
97
  async function syncPageCssAssetsForDevelopment(pageDirectory, outputDir, entryPath) {
94
- const sourceFiles = await glob('**/*.css', { cwd: pageDirectory, nodir: true });
98
+ const sourceFiles = await scanGlob('**/*.css', { cwd: pageDirectory });
95
99
  const entryRelative = normalizeForwardSlashes(path.relative(pageDirectory, entryPath));
96
100
  const copySet = new Set();
97
101
  for (const relative of sourceFiles) {
@@ -105,7 +109,7 @@ async function syncPageCssAssetsForDevelopment(pageDirectory, outputDir, entryPa
105
109
  await ensureDir(path.dirname(destinationPath));
106
110
  await copy(sourcePath, destinationPath);
107
111
  }
108
- const existingFiles = await glob('**/*.css', { cwd: outputDir, nodir: true });
112
+ const existingFiles = await scanGlob('**/*.css', { cwd: outputDir });
109
113
  for (const relative of existingFiles) {
110
114
  const normalized = normalizeForwardSlashes(relative);
111
115
  if (normalized === `${FILES.index}${EXTENSIONS.css}`) {
@@ -128,7 +132,7 @@ async function processAppCss(config, isProduction, processor, customMediaPrelude
128
132
  const rewritten = rewriteAppStyleImports(processed.css, stylesMap);
129
133
  const inlined = await inlineAppImports(rewritten, config.paths.dist.frontend);
130
134
  const fileName = await emitAppProductionCss(config, inlined);
131
- await updateSharedAssets(config.paths.dist.frontend, shared => {
135
+ await updateSharedAssets(config.paths.dist.frontend, (shared) => {
132
136
  shared.css = fileName;
133
137
  });
134
138
  return { appCss: fileName };
@@ -187,7 +191,7 @@ async function emitAppProductionCss(config, css) {
187
191
  else {
188
192
  await Promise.all([
189
193
  remove(`${outputPath}${EXTENSIONS.br}`).catch(() => undefined),
190
- remove(`${outputPath}${EXTENSIONS.gz}`).catch(() => undefined)
194
+ remove(`${outputPath}${EXTENSIONS.gz}`).catch(() => undefined),
191
195
  ]);
192
196
  }
193
197
  // Remove previously hashed variants to avoid stale files.
@@ -208,7 +212,7 @@ async function syncAppStyles(sourceAppDir, destinationAppDir, processor, customM
208
212
  }
209
213
  const stylesDestination = path.join(destinationAppDir, 'styles');
210
214
  await ensureDir(stylesDestination);
211
- const files = await glob('**/*', { cwd: stylesSource, nodir: true });
215
+ const files = await scanGlob('**/*', { cwd: stylesSource });
212
216
  for (const relative of files) {
213
217
  const sourcePath = path.join(stylesSource, relative);
214
218
  const destinationPath = path.join(stylesDestination, relative);
@@ -227,7 +231,7 @@ async function computeAppStylesVersion(sourceAppDir) {
227
231
  if (!(await pathExists(stylesDir))) {
228
232
  return 'no-styles';
229
233
  }
230
- const files = (await glob('**/*.css', { cwd: stylesDir, nodir: true })).sort((a, b) => a.localeCompare(b));
234
+ const files = await scanGlob('**/*.css', { cwd: stylesDir });
231
235
  if (files.length === 0) {
232
236
  return 'no-styles';
233
237
  }
@@ -294,7 +298,9 @@ function shouldInlinePageImport(importPath) {
294
298
  if (!importPath.endsWith(EXTENSIONS.css)) {
295
299
  return false;
296
300
  }
297
- if (importPath.startsWith('/') || importPath.startsWith('http:') || importPath.startsWith('https:')) {
301
+ if (importPath.startsWith('/') ||
302
+ importPath.startsWith('http:') ||
303
+ importPath.startsWith('https:')) {
298
304
  return false;
299
305
  }
300
306
  if (importPath.startsWith('@') || importPath.includes('?') || importPath.includes('#')) {
@@ -310,7 +316,7 @@ function isWithin(candidate, root) {
310
316
  return relative.length > 0 && !relative.startsWith('..') && !path.isAbsolute(relative);
311
317
  }
312
318
  async function inlineAppImports(css, distRoot, seen = new Set()) {
313
- const importPattern = /@import\s+(?:url\()?[\s]*['"]\/app\/([^'"\)]+)['"][\s]*\)?;?/g;
319
+ const importPattern = /@import\s+(?:url\()?[\s]*['"]\/app\/([^'"]+)['"][\s]*\)?;?/g;
314
320
  const segments = [];
315
321
  let lastIndex = 0;
316
322
  for (const match of css.matchAll(importPattern)) {
@@ -357,7 +363,7 @@ async function emitAppStylesProduction(config, processor, customMediaPrelude) {
357
363
  }
358
364
  const destinationDir = path.join(config.paths.dist.frontend, FOLDERS.app, 'styles');
359
365
  await remove(destinationDir).catch(() => undefined);
360
- const files = await glob('**/*.css', { cwd: sourceDir, nodir: true });
366
+ const files = await scanGlob('**/*.css', { cwd: sourceDir });
361
367
  for (const relative of files) {
362
368
  const sourcePath = path.join(sourceDir, relative);
363
369
  const source = applyCustomMediaPrelude(await readFile(sourcePath), customMediaPrelude);
@@ -376,7 +382,7 @@ async function emitAppStylesProduction(config, processor, customMediaPrelude) {
376
382
  else {
377
383
  await Promise.all([
378
384
  remove(`${destinationPath}${EXTENSIONS.br}`).catch(() => undefined),
379
- remove(`${destinationPath}${EXTENSIONS.gz}`).catch(() => undefined)
385
+ remove(`${destinationPath}${EXTENSIONS.gz}`).catch(() => undefined),
380
386
  ]);
381
387
  }
382
388
  mapping.set(normalizeForwardSlashes(relative), normalizeForwardSlashes(path.join('styles', relativeHashedPath)));
@@ -391,7 +397,7 @@ function rewriteAppStyleImports(css, stylesMap) {
391
397
  for (const [original, hashed] of stylesMap.entries()) {
392
398
  const normalizedOriginal = original.startsWith('styles/') ? original : `styles/${original}`;
393
399
  const escaped = escapeRegExp(normalizedOriginal);
394
- const pattern = new RegExp(`(@import\\s+['"])(?:\.\/)?${escaped}(['"];?)`, 'g');
400
+ const pattern = new RegExp(`(@import\\s+['"])(?:\\./)?${escaped}(['"];?)`, 'g');
395
401
  result = result.replace(pattern, `$1/app/${hashed}$2`);
396
402
  }
397
403
  return result;
@@ -1,10 +1,10 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { load } from 'cheerio';
4
- import { glob } from 'glob';
5
4
  import { minify } from 'html-minifier-terser';
6
5
  import { FOLDERS, FILES, FILE_NAMES, EXTENSIONS } from '../core/constants.js';
7
6
  import { ensureDir, readFile, writeFile, pathExists, remove } from '../utils/fs.js';
7
+ import { scanGlob } from '../utils/glob.js';
8
8
  import { getPageDirectories } from '../core/pages.js';
9
9
  import { readPageManifest, readSharedAssets } from '../assets/assetManifest.js';
10
10
  import { createCompressedVariants } from '../assets/precompression.js';
@@ -13,10 +13,10 @@ import { getImageDimensions } from '../assets/imageOptimizer.js';
13
13
  import { applyLazyLoading } from '../html/lazyLoad.js';
14
14
  import { addSubresourceIntegrity } from '../html/htmlSecurity.js';
15
15
  import { injectResourceHints } from '../html/resourceHints.js';
16
- import { ensureAppShellCriticalCss, ensureDocsShellCriticalCss, inlineCriticalCss } from '../html/criticalCss.js';
16
+ import { ensureAppShellCriticalCss, ensureDocsShellCriticalCss, inlineCriticalCss, } from '../html/criticalCss.js';
17
17
  import { findPageFromChangedFile } from '../utils/pathMatch.js';
18
18
  import { emitDiagnostic } from '../core/diagnostics.js';
19
- import { resolvePageAssetUrl, resolvePageHtmlDir, resolvePagesUrlPrefix } from '../utils/pagePaths.js';
19
+ import { resolvePageAssetUrl, resolvePageHtmlDir, resolvePagesUrlPrefix, } from '../utils/pagePaths.js';
20
20
  export function createHtmlBuilder(context) {
21
21
  return {
22
22
  name: 'html',
@@ -25,17 +25,20 @@ export function createHtmlBuilder(context) {
25
25
  },
26
26
  async publish() {
27
27
  await publishHtml(context);
28
- }
28
+ },
29
29
  };
30
30
  }
31
31
  async function buildHtml(context) {
32
32
  const { config } = context;
33
33
  if (!shouldProcess(context, [
34
34
  { directory: config.paths.src.pages, extensions: [EXTENSIONS.html] },
35
- { directory: config.paths.src.pages, extensions: [EXTENSIONS.ts, EXTENSIONS.js, '.tsx', '.jsx'] },
35
+ {
36
+ directory: config.paths.src.pages,
37
+ extensions: [EXTENSIONS.ts, EXTENSIONS.js, '.tsx', '.jsx'],
38
+ },
36
39
  { directory: config.paths.src.app, extensions: [EXTENSIONS.html, EXTENSIONS.js] },
37
40
  // `webstir enable ...` modifies package.json and can change which opt-in scripts should be injected.
38
- { directory: config.paths.workspace, extensions: ['.json'] }
41
+ { directory: config.paths.workspace, extensions: ['.json'] },
39
42
  ])) {
40
43
  return;
41
44
  }
@@ -52,10 +55,7 @@ async function buildHtml(context) {
52
55
  if (targetPage && page.name !== targetPage) {
53
56
  continue;
54
57
  }
55
- const pageHtmlFiles = await glob('**/*.html', {
56
- cwd: page.directory,
57
- nodir: true
58
- });
58
+ const pageHtmlFiles = await scanGlob('**/*.html', { cwd: page.directory });
59
59
  if (pageHtmlFiles.length === 0) {
60
60
  warn(`No HTML fragments found for page '${page.name}'.`);
61
61
  continue;
@@ -96,10 +96,7 @@ async function publishHtml(context) {
96
96
  }
97
97
  const assetDir = path.join(config.paths.dist.pages, page.name);
98
98
  const distDir = resolvePageHtmlDir(config.paths.dist.pages, page.name, useRootIndex);
99
- const htmlFiles = await glob('**/*.html', {
100
- cwd: page.directory,
101
- nodir: true
102
- });
99
+ const htmlFiles = await scanGlob('**/*.html', { cwd: page.directory });
103
100
  const manifest = await readPageManifest(assetDir, page.name);
104
101
  for (const relativeHtml of htmlFiles) {
105
102
  const sourcePath = path.join(page.directory, relativeHtml);
@@ -107,7 +104,7 @@ async function publishHtml(context) {
107
104
  const rewritten = await rewriteForPublish(context, html, page.name, manifest, page.directory, shared, {
108
105
  pagesUrlPrefix,
109
106
  buildPagesUrlPrefix,
110
- useRootIndex
107
+ useRootIndex,
111
108
  });
112
109
  const outputPath = path.join(distDir, relativeHtml);
113
110
  await ensureDir(path.dirname(outputPath));
@@ -146,13 +143,10 @@ function mergeTemplates(appHtml, pageHtml) {
146
143
  appMain.html(pageMain.html() ?? '');
147
144
  return app.root().html() ?? '';
148
145
  }
149
- function injectOptInScripts(html, enable, pageName, pageDir, sourceHtmlPath) {
150
- if (!enable) {
151
- return html;
152
- }
146
+ function injectOptInScripts(html, enable, pageName, pageDir, _sourceHtmlPath) {
153
147
  const document = load(html);
154
148
  rewritePageRelativeAssets(document, pageName);
155
- if (enable.spa) {
149
+ if (enable?.spa) {
156
150
  const existing = document(`script[src="/${FOLDERS.pages}/${pageName}/${FILES.index}${EXTENSIONS.js}"]`);
157
151
  if (existing.length === 0) {
158
152
  document('head').append(`<script type="module" src="/${FOLDERS.pages}/${pageName}/${FILES.index}${EXTENSIONS.js}"></script>`);
@@ -162,10 +156,10 @@ function injectOptInScripts(html, enable, pageName, pageDir, sourceHtmlPath) {
162
156
  const tsxCandidate = path.join(pageDir, `${FILES.index}.tsx`);
163
157
  const jsCandidate = path.join(pageDir, `${FILES.index}${EXTENSIONS.js}`);
164
158
  const jsxCandidate = path.join(pageDir, `${FILES.index}.jsx`);
165
- const pageScriptExists = [tsCandidate, tsxCandidate, jsCandidate, jsxCandidate]
166
- .some(candidate => fs.existsSync(candidate));
159
+ const pageScriptExists = [tsCandidate, tsxCandidate, jsCandidate, jsxCandidate].some((candidate) => fs.existsSync(candidate));
167
160
  if (pageScriptExists) {
168
- const hasScript = document(`script[src="/${FOLDERS.pages}/${pageName}/${FILES.index}${EXTENSIONS.js}"]`).length > 0;
161
+ const hasScript = document(`script[src="/${FOLDERS.pages}/${pageName}/${FILES.index}${EXTENSIONS.js}"]`)
162
+ .length > 0;
169
163
  if (!hasScript) {
170
164
  document('head').append(`<script type="module" src="/${FOLDERS.pages}/${pageName}/${FILES.index}${EXTENSIONS.js}"></script>`);
171
165
  }
@@ -177,7 +171,11 @@ function rewritePageRelativeAssets(document, pageName) {
177
171
  document('link[rel="stylesheet"]').each((_, element) => {
178
172
  const node = document(element);
179
173
  const href = node.attr('href');
180
- if (!href || href.startsWith('/') || href.startsWith('http:') || href.startsWith('https:') || href.startsWith('data:')) {
174
+ if (!href ||
175
+ href.startsWith('/') ||
176
+ href.startsWith('http:') ||
177
+ href.startsWith('https:') ||
178
+ href.startsWith('data:')) {
181
179
  return;
182
180
  }
183
181
  node.attr('href', `${pagePrefix}${href}`);
@@ -185,7 +183,11 @@ function rewritePageRelativeAssets(document, pageName) {
185
183
  document('script[src]').each((_, element) => {
186
184
  const node = document(element);
187
185
  const src = node.attr('src');
188
- if (!src || src.startsWith('/') || src.startsWith('http:') || src.startsWith('https:') || src.startsWith('data:')) {
186
+ if (!src ||
187
+ src.startsWith('/') ||
188
+ src.startsWith('http:') ||
189
+ src.startsWith('https:') ||
190
+ src.startsWith('data:')) {
189
191
  return;
190
192
  }
191
193
  node.attr('src', `${pagePrefix}${src}`);
@@ -207,13 +209,11 @@ async function rewriteForPublish(context, html, pageName, manifest, pageDirector
207
209
  ensureDocsShellCriticalCss(document);
208
210
  }
209
211
  if (shared?.js) {
210
- document(`script[src="/app/app.js"]`)
211
- .attr('src', `/app/${shared.js}`)
212
- .attr('type', 'module');
212
+ document(`script[src="/app/app.js"]`).attr('src', `/app/${shared.js}`).attr('type', 'module');
213
213
  }
214
214
  const scriptSelector = [
215
215
  `script[src="${FILES.index}${EXTENSIONS.js}"]`,
216
- `script[src="${buildScriptHref}"]`
216
+ `script[src="${buildScriptHref}"]`,
217
217
  ].join(', ');
218
218
  if (manifest.js) {
219
219
  document(scriptSelector)
@@ -225,7 +225,7 @@ async function rewriteForPublish(context, html, pageName, manifest, pageDirector
225
225
  }
226
226
  const cssSelector = [
227
227
  `link[href="${FILES.index}${EXTENSIONS.css}"]`,
228
- `link[href="${buildCssHref}"]`
228
+ `link[href="${buildCssHref}"]`,
229
229
  ].join(', ');
230
230
  if (manifest.css) {
231
231
  document(cssSelector).attr('href', resolvePageAssetUrl(pagesUrlPrefix, pageName, manifest.css));
@@ -236,7 +236,24 @@ async function rewriteForPublish(context, html, pageName, manifest, pageDirector
236
236
  }
237
237
  if (context.config.features.htmlSecurity) {
238
238
  await inlineCriticalCss(document, pageName, context.config.paths.dist.pages, pagesUrlPrefix, manifest.css);
239
- const sriResult = await addSubresourceIntegrity(document);
239
+ const sriResult = await addSubresourceIntegrity(document, {
240
+ allowExternalFetch: context.config.features.externalResourceIntegrity,
241
+ });
242
+ if (sriResult.skippedExternalResources.length > 0) {
243
+ const resources = Array.from(new Set(sriResult.skippedExternalResources));
244
+ const message = resources.length === 1
245
+ ? `Skipped automatic SRI for ${resources[0]} because external resource fetching is disabled.`
246
+ : `Skipped automatic SRI for ${resources.length} external resources because external resource fetching is disabled.`;
247
+ emitDiagnostic({
248
+ code: 'frontend.sri.external_fetch_disabled',
249
+ kind: 'sri',
250
+ stage: 'html.publish',
251
+ severity: 'warning',
252
+ message,
253
+ data: { resources },
254
+ suggestion: 'Add integrity attributes manually or set features.externalResourceIntegrity to true to opt in.',
255
+ });
256
+ }
240
257
  if (sriResult.failures.length > 0) {
241
258
  const resources = sriResult.failures;
242
259
  const message = resources.length === 1
@@ -249,7 +266,7 @@ async function rewriteForPublish(context, html, pageName, manifest, pageDirector
249
266
  severity: 'warning',
250
267
  message,
251
268
  data: { resources },
252
- suggestion: 'Verify the resource is reachable and not blocked by auth or network constraints.'
269
+ suggestion: 'Verify the resource is reachable and not blocked by auth or network constraints.',
253
270
  });
254
271
  }
255
272
  const hints = injectResourceHints(document, pageName, pagesUrlPrefix, useRootIndex);
@@ -260,7 +277,7 @@ async function rewriteForPublish(context, html, pageName, manifest, pageDirector
260
277
  stage: 'html.publish',
261
278
  severity: 'warning',
262
279
  message: 'Unable to inject resource hints because <head> is missing.',
263
- data: { candidates: hints.candidates }
280
+ data: { candidates: hints.candidates },
264
281
  });
265
282
  }
266
283
  }
@@ -277,7 +294,7 @@ async function handlePrecompression(context, outputPath) {
277
294
  }
278
295
  await Promise.all([
279
296
  remove(`${outputPath}${EXTENSIONS.br}`).catch(() => undefined),
280
- remove(`${outputPath}${EXTENSIONS.gz}`).catch(() => undefined)
297
+ remove(`${outputPath}${EXTENSIONS.gz}`).catch(() => undefined),
281
298
  ]);
282
299
  }
283
300
  function validateAppTemplate(html, filePath) {
@@ -375,9 +392,7 @@ function removeDevScript(document, selector) {
375
392
  });
376
393
  }
377
394
  function isWhitespaceTextNode(node) {
378
- return node.length > 0
379
- && node[0].type === 'text'
380
- && (node[0].data ?? '').trim().length === 0;
395
+ return node.length > 0 && node[0].type === 'text' && (node[0].data ?? '').trim().length === 0;
381
396
  }
382
397
  async function minifyHtml(html) {
383
398
  return minify(html, {
@@ -387,7 +402,7 @@ async function minifyHtml(html) {
387
402
  minifyJS: false,
388
403
  removeComments: true,
389
404
  removeOptionalTags: false,
390
- removeAttributeQuotes: false
405
+ removeAttributeQuotes: false,
391
406
  });
392
407
  }
393
408
  async function addImageDimensions(document, context, pageDirectory) {
@@ -415,10 +430,10 @@ async function addImageDimensions(document, context, pageDirectory) {
415
430
  }));
416
431
  }
417
432
  function isExternalSource(src) {
418
- return src.startsWith('http://')
419
- || src.startsWith('https://')
420
- || src.startsWith('data:')
421
- || src.startsWith('//');
433
+ return (src.startsWith('http://') ||
434
+ src.startsWith('https://') ||
435
+ src.startsWith('data:') ||
436
+ src.startsWith('//'));
422
437
  }
423
438
  function resolveAssetPath(src, pageDirectory, buildRoot) {
424
439
  const normalized = src.replace(/\\/g, '/');
@@ -9,6 +9,6 @@ export function createBuilders(context) {
9
9
  createCssBuilder(context),
10
10
  createHtmlBuilder(context),
11
11
  createContentBuilder(context),
12
- createStaticAssetsBuilder(context)
12
+ createStaticAssetsBuilder(context),
13
13
  ];
14
14
  }