@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.
- package/README.md +124 -60
- package/dist/assets/imageOptimizer.js +10 -15
- package/dist/assets/precompression.js +1 -1
- package/dist/builders/contentBuilder.js +102 -90
- package/dist/builders/cssBuilder.js +25 -19
- package/dist/builders/htmlBuilder.js +57 -42
- package/dist/builders/index.js +1 -1
- package/dist/builders/jsBuilder.js +219 -76
- package/dist/builders/staticAssetsBuilder.js +27 -9
- package/dist/builders/types.d.ts +1 -0
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +6 -30
- package/dist/config/manifest.js +7 -6
- package/dist/config/paths.js +2 -2
- package/dist/config/schema.d.ts +8 -0
- package/dist/config/schema.js +7 -6
- package/dist/config/setup.js +1 -1
- package/dist/config/workspace.js +11 -9
- package/dist/core/constants.d.ts +1 -1
- package/dist/core/constants.js +5 -5
- package/dist/core/diagnostics.js +1 -1
- package/dist/core/pages.js +4 -4
- package/dist/hooks.js +3 -3
- package/dist/html/criticalCss.js +6 -3
- package/dist/html/htmlSecurity.d.ts +6 -1
- package/dist/html/htmlSecurity.js +28 -14
- package/dist/html/lazyLoad.js +1 -1
- package/dist/html/pageScaffold.js +1 -1
- package/dist/html/resourceHints.js +5 -2
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/inspect.d.ts +2 -0
- package/dist/inspect.js +110 -0
- package/dist/modes/ssg/metadata.js +4 -4
- package/dist/modes/ssg/routing.js +2 -5
- package/dist/modes/ssg/seo.js +5 -5
- package/dist/modes/ssg/views.js +17 -11
- package/dist/operations.js +18 -10
- package/dist/pipeline.d.ts +1 -0
- package/dist/pipeline.js +6 -1
- package/dist/provider.js +28 -24
- package/dist/runtime/boundary.d.ts +28 -0
- package/dist/runtime/boundary.js +247 -0
- package/dist/runtime/index.d.ts +1 -0
- package/dist/runtime/index.js +1 -0
- package/dist/types.d.ts +52 -0
- package/dist/utils/fs.d.ts +11 -10
- package/dist/utils/fs.js +48 -20
- package/dist/utils/glob.d.ts +8 -0
- package/dist/utils/glob.js +21 -0
- package/dist/utils/hash.js +1 -2
- package/dist/utils/pagePaths.js +2 -2
- package/package.json +19 -14
- package/scripts/publish.sh +2 -94
- package/scripts/update-contract.sh +12 -10
- package/src/assets/assetManifest.ts +39 -29
- package/src/assets/imageOptimizer.ts +91 -82
- package/src/assets/precompression.ts +22 -16
- package/src/builders/contentBuilder.ts +1224 -1149
- package/src/builders/cssBuilder.ts +466 -417
- package/src/builders/htmlBuilder.ts +511 -448
- package/src/builders/index.ts +7 -7
- package/src/builders/jsBuilder.ts +538 -280
- package/src/builders/staticAssetsBuilder.ts +166 -135
- package/src/builders/types.ts +7 -6
- package/src/cli.ts +66 -90
- package/src/config/manifest.ts +16 -14
- package/src/config/paths.ts +5 -5
- package/src/config/schema.ts +38 -37
- package/src/config/setup.ts +7 -7
- package/src/config/workspace.ts +118 -116
- package/src/config/workspaceManifest.ts +14 -14
- package/src/core/constants.ts +62 -62
- package/src/core/diagnostics.ts +26 -26
- package/src/core/pages.ts +19 -19
- package/src/hooks.ts +128 -118
- package/src/html/criticalCss.ts +84 -77
- package/src/html/htmlSecurity.ts +107 -66
- package/src/html/lazyLoad.ts +22 -19
- package/src/html/pageScaffold.ts +37 -28
- package/src/html/resourceHints.ts +83 -74
- package/src/index.ts +2 -0
- package/src/inspect.ts +158 -0
- package/src/modes/ssg/metadata.ts +53 -51
- package/src/modes/ssg/routing.ts +177 -177
- package/src/modes/ssg/seo.ts +208 -200
- package/src/modes/ssg/validation.ts +31 -25
- package/src/modes/ssg/views.ts +257 -238
- package/src/operations.ts +105 -95
- package/src/pipeline.ts +81 -69
- package/src/provider.ts +184 -176
- package/src/runtime/boundary.ts +325 -0
- package/src/runtime/index.ts +1 -0
- package/src/types.ts +107 -48
- package/src/utils/changedFile.ts +22 -22
- package/src/utils/fs.ts +73 -26
- package/src/utils/glob.ts +38 -0
- package/src/utils/hash.ts +2 -4
- package/src/utils/pagePaths.ts +35 -23
- package/src/utils/pathMatch.ts +26 -23
- package/tests/add-page-defaults.test.js +44 -39
- package/tests/bundlerParity.test.js +252 -0
- package/tests/cli.contract.test.js +13 -0
- package/tests/content-pages.test.js +108 -13
- package/tests/css-app-imports.test.js +22 -11
- package/tests/css-page-imports.test.js +26 -13
- package/tests/diagnostics.test.js +39 -36
- package/tests/features.test.js +48 -43
- package/tests/hooks.test.js +58 -42
- package/tests/htmlSecurity.test.js +66 -0
- package/tests/inspect.test.js +148 -0
- package/tests/provider.integration.test.js +71 -20
- package/tests/runtime.test.js +493 -0
- package/tests/ssg-defaults.test.js +284 -177
- package/tests/ssg-guardrails.test.js +51 -51
- package/tsconfig.json +3 -10
- package/dist/watch/frontendFiles.d.ts +0 -3
- package/dist/watch/frontendFiles.js +0 -25
- package/dist/watch/hotUpdateTracker.d.ts +0 -51
- package/dist/watch/hotUpdateTracker.js +0 -205
- package/dist/watch/pipelineHelpers.d.ts +0 -26
- package/dist/watch/pipelineHelpers.js +0 -177
- package/dist/watch/types.d.ts +0 -27
- package/dist/watch/types.js +0 -1
- package/dist/watch/watchCoordinator.d.ts +0 -36
- package/dist/watch/watchCoordinator.js +0 -551
- package/dist/watch/watchDaemon.d.ts +0 -17
- package/dist/watch/watchDaemon.js +0 -127
- package/dist/watch/watchReporter.d.ts +0 -21
- package/dist/watch/watchReporter.js +0 -64
- package/scripts/smoke.mjs +0 -35
- package/src/watch/frontendFiles.ts +0 -32
- package/src/watch/hotUpdateTracker.ts +0 -285
- package/src/watch/pipelineHelpers.ts +0 -242
- package/src/watch/types.ts +0 -23
- package/src/watch/watchCoordinator.ts +0 -666
- package/src/watch/watchDaemon.ts +0 -144
- 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 ??
|
|
18
|
-
|
|
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, {
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
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('/') ||
|
|
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\/([^'"
|
|
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
|
|
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+['"])(
|
|
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
|
-
{
|
|
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
|
|
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
|
|
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,
|
|
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
|
|
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}"]`)
|
|
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 ||
|
|
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 ||
|
|
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
|
-
|
|
420
|
-
|
|
421
|
-
|
|
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, '/');
|