@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,10 +3,10 @@ import path from 'node:path';
|
|
|
3
3
|
import { load } from 'cheerio';
|
|
4
4
|
import type { Cheerio, CheerioAPI } from 'cheerio';
|
|
5
5
|
import type { AnyNode } from 'domhandler';
|
|
6
|
-
import { glob } from 'glob';
|
|
7
6
|
import { minify } from 'html-minifier-terser';
|
|
8
7
|
import { FOLDERS, FILES, FILE_NAMES, EXTENSIONS } from '../core/constants.js';
|
|
9
8
|
import { ensureDir, readFile, writeFile, pathExists, remove } from '../utils/fs.js';
|
|
9
|
+
import { scanGlob } from '../utils/glob.js';
|
|
10
10
|
import type { Builder, BuilderContext } from './types.js';
|
|
11
11
|
import { getPageDirectories } from '../core/pages.js';
|
|
12
12
|
import { readPageManifest, readSharedAssets } from '../assets/assetManifest.js';
|
|
@@ -16,525 +16,588 @@ import { getImageDimensions } from '../assets/imageOptimizer.js';
|
|
|
16
16
|
import { applyLazyLoading } from '../html/lazyLoad.js';
|
|
17
17
|
import { addSubresourceIntegrity } from '../html/htmlSecurity.js';
|
|
18
18
|
import { injectResourceHints } from '../html/resourceHints.js';
|
|
19
|
-
import {
|
|
19
|
+
import {
|
|
20
|
+
ensureAppShellCriticalCss,
|
|
21
|
+
ensureDocsShellCriticalCss,
|
|
22
|
+
inlineCriticalCss,
|
|
23
|
+
} from '../html/criticalCss.js';
|
|
20
24
|
import { findPageFromChangedFile } from '../utils/pathMatch.js';
|
|
21
25
|
import { emitDiagnostic } from '../core/diagnostics.js';
|
|
22
26
|
import type { EnableFlags } from '../types.js';
|
|
23
|
-
import {
|
|
24
|
-
|
|
25
|
-
|
|
27
|
+
import {
|
|
28
|
+
resolvePageAssetUrl,
|
|
29
|
+
resolvePageHtmlDir,
|
|
30
|
+
resolvePagesUrlPrefix,
|
|
31
|
+
} from '../utils/pagePaths.js';
|
|
26
32
|
|
|
27
33
|
export function createHtmlBuilder(context: BuilderContext): Builder {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
34
|
+
return {
|
|
35
|
+
name: 'html',
|
|
36
|
+
async build(): Promise<void> {
|
|
37
|
+
await buildHtml(context);
|
|
38
|
+
},
|
|
39
|
+
async publish(): Promise<void> {
|
|
40
|
+
await publishHtml(context);
|
|
41
|
+
},
|
|
42
|
+
};
|
|
37
43
|
}
|
|
38
44
|
|
|
39
45
|
async function buildHtml(context: BuilderContext): Promise<void> {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
,
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
46
|
+
const { config } = context;
|
|
47
|
+
if (
|
|
48
|
+
!shouldProcess(context, [
|
|
49
|
+
{ directory: config.paths.src.pages, extensions: [EXTENSIONS.html] },
|
|
50
|
+
{
|
|
51
|
+
directory: config.paths.src.pages,
|
|
52
|
+
extensions: [EXTENSIONS.ts, EXTENSIONS.js, '.tsx', '.jsx'],
|
|
53
|
+
},
|
|
54
|
+
{ directory: config.paths.src.app, extensions: [EXTENSIONS.html, EXTENSIONS.js] },
|
|
55
|
+
// `webstir enable ...` modifies package.json and can change which opt-in scripts should be injected.
|
|
56
|
+
{ directory: config.paths.workspace, extensions: ['.json'] },
|
|
57
|
+
])
|
|
58
|
+
) {
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const appTemplatePath = path.join(config.paths.src.app, FILE_NAMES.htmlAppTemplate);
|
|
63
|
+
if (!(await pathExists(appTemplatePath))) {
|
|
64
|
+
throw new Error(`Base application HTML file not found: ${appTemplatePath}`);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const templateHtml = await readFile(appTemplatePath);
|
|
68
|
+
validateAppTemplate(templateHtml, appTemplatePath);
|
|
69
|
+
|
|
70
|
+
const targetPage = findPageFromChangedFile(context.changedFile, config.paths.src.pages);
|
|
71
|
+
const pages = await getPageDirectories(config.paths.src.pages);
|
|
72
|
+
await ensureDir(config.paths.build.frontend);
|
|
73
|
+
|
|
74
|
+
for (const page of pages) {
|
|
75
|
+
if (targetPage && page.name !== targetPage) {
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
const pageHtmlFiles = await scanGlob('**/*.html', { cwd: page.directory });
|
|
79
|
+
|
|
80
|
+
if (pageHtmlFiles.length === 0) {
|
|
81
|
+
warn(`No HTML fragments found for page '${page.name}'.`);
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const targetDir = path.join(config.paths.build.pages, page.name);
|
|
86
|
+
await ensureDir(targetDir);
|
|
87
|
+
|
|
88
|
+
for (const relativeHtml of pageHtmlFiles) {
|
|
89
|
+
const sourceHtmlPath = path.join(page.directory, relativeHtml);
|
|
90
|
+
const fragment = await readFile(sourceHtmlPath);
|
|
91
|
+
validatePageFragment(fragment, sourceHtmlPath);
|
|
92
|
+
|
|
93
|
+
const mergedHtml = mergeTemplates(templateHtml, fragment);
|
|
94
|
+
const mergedWithScripts = injectOptInScripts(
|
|
95
|
+
mergedHtml,
|
|
96
|
+
context.enable,
|
|
97
|
+
page.name,
|
|
98
|
+
page.directory,
|
|
99
|
+
sourceHtmlPath,
|
|
100
|
+
);
|
|
101
|
+
const targetPath = path.join(targetDir, path.basename(relativeHtml));
|
|
102
|
+
await writeFile(targetPath, mergedWithScripts);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Copy the app template for reference in the build output.
|
|
107
|
+
const buildAppDir = path.join(config.paths.build.frontend, FOLDERS.app);
|
|
108
|
+
await ensureDir(buildAppDir);
|
|
109
|
+
await writeFile(path.join(buildAppDir, FILE_NAMES.htmlAppTemplate), templateHtml);
|
|
103
110
|
}
|
|
104
111
|
|
|
105
112
|
async function publishHtml(context: BuilderContext): Promise<void> {
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
}
|
|
113
|
+
const { config } = context;
|
|
114
|
+
const buildPagesRoot = config.paths.build.pages;
|
|
115
|
+
if (!(await pathExists(buildPagesRoot))) {
|
|
116
|
+
warn('Skipping HTML publish because no build artifacts were found. Run build first.');
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const targetPage = findPageFromChangedFile(context.changedFile, config.paths.src.pages);
|
|
121
|
+
const pages = await getPageDirectories(buildPagesRoot);
|
|
122
|
+
const shared = await readSharedAssets(config.paths.dist.frontend);
|
|
123
|
+
const pagesUrlPrefix = resolvePagesUrlPrefix(config.paths.dist.frontend, config.paths.dist.pages);
|
|
124
|
+
const buildPagesUrlPrefix = resolvePagesUrlPrefix(
|
|
125
|
+
config.paths.build.frontend,
|
|
126
|
+
config.paths.build.pages,
|
|
127
|
+
);
|
|
128
|
+
const useRootIndex = pagesUrlPrefix.length === 0;
|
|
129
|
+
|
|
130
|
+
for (const page of pages) {
|
|
131
|
+
if (targetPage && page.name !== targetPage) {
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
const assetDir = path.join(config.paths.dist.pages, page.name);
|
|
135
|
+
const distDir = resolvePageHtmlDir(config.paths.dist.pages, page.name, useRootIndex);
|
|
136
|
+
|
|
137
|
+
const htmlFiles = await scanGlob('**/*.html', { cwd: page.directory });
|
|
138
|
+
|
|
139
|
+
const manifest = await readPageManifest(assetDir, page.name);
|
|
140
|
+
|
|
141
|
+
for (const relativeHtml of htmlFiles) {
|
|
142
|
+
const sourcePath = path.join(page.directory, relativeHtml);
|
|
143
|
+
const html = await readFile(sourcePath);
|
|
144
|
+
const rewritten = await rewriteForPublish(
|
|
145
|
+
context,
|
|
146
|
+
html,
|
|
147
|
+
page.name,
|
|
148
|
+
manifest,
|
|
149
|
+
page.directory,
|
|
150
|
+
shared,
|
|
151
|
+
{
|
|
152
|
+
pagesUrlPrefix,
|
|
153
|
+
buildPagesUrlPrefix,
|
|
154
|
+
useRootIndex,
|
|
155
|
+
},
|
|
156
|
+
);
|
|
157
|
+
const outputPath = path.join(distDir, relativeHtml);
|
|
158
|
+
await ensureDir(path.dirname(outputPath));
|
|
159
|
+
await writeFile(outputPath, rewritten);
|
|
160
|
+
await handlePrecompression(context, outputPath);
|
|
155
161
|
}
|
|
162
|
+
}
|
|
156
163
|
}
|
|
157
164
|
|
|
158
165
|
function mergeTemplates(appHtml: string, pageHtml: string): string {
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
166
|
+
const app = load(appHtml);
|
|
167
|
+
const page = load(pageHtml);
|
|
168
|
+
|
|
169
|
+
const appMain = app('main').first();
|
|
170
|
+
const pageMain = page('main').first();
|
|
171
|
+
if (appMain.length === 0) {
|
|
172
|
+
throw new Error('Base application template is missing a <main> element.');
|
|
173
|
+
}
|
|
174
|
+
if (pageMain.length === 0) {
|
|
175
|
+
throw new Error('Page fragment is missing a <main> element.');
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const appHead = app('head').first();
|
|
179
|
+
const pageHead = page('head').first();
|
|
180
|
+
if (appHead.length === 0 || pageHead.length === 0) {
|
|
181
|
+
throw new Error('Templates must include a <head> element.');
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const appBody = app('body').first();
|
|
185
|
+
const pageBody = page('body').first();
|
|
186
|
+
if (appBody.length && pageBody.length) {
|
|
187
|
+
const pageBodyClass = pageBody.attr('class');
|
|
188
|
+
if (pageBodyClass) {
|
|
189
|
+
const existing = appBody.attr('class');
|
|
190
|
+
const merged = existing ? `${existing} ${pageBodyClass}` : pageBodyClass;
|
|
191
|
+
appBody.attr('class', merged);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
appHead.append(pageHead.children());
|
|
196
|
+
appMain.html(pageMain.html() ?? '');
|
|
197
|
+
|
|
198
|
+
return app.root().html() ?? '';
|
|
192
199
|
}
|
|
193
200
|
|
|
194
201
|
function injectOptInScripts(
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
202
|
+
html: string,
|
|
203
|
+
enable: EnableFlags | undefined,
|
|
204
|
+
pageName: string,
|
|
205
|
+
pageDir: string,
|
|
206
|
+
_sourceHtmlPath: string,
|
|
200
207
|
): string {
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
if (
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
208
|
+
const document = load(html);
|
|
209
|
+
|
|
210
|
+
rewritePageRelativeAssets(document, pageName);
|
|
211
|
+
|
|
212
|
+
if (enable?.spa) {
|
|
213
|
+
const existing = document(
|
|
214
|
+
`script[src="/${FOLDERS.pages}/${pageName}/${FILES.index}${EXTENSIONS.js}"]`,
|
|
215
|
+
);
|
|
216
|
+
if (existing.length === 0) {
|
|
217
|
+
document('head').append(
|
|
218
|
+
`<script type="module" src="/${FOLDERS.pages}/${pageName}/${FILES.index}${EXTENSIONS.js}"></script>`,
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const tsCandidate = path.join(pageDir, `${FILES.index}${EXTENSIONS.ts}`);
|
|
224
|
+
const tsxCandidate = path.join(pageDir, `${FILES.index}.tsx`);
|
|
225
|
+
const jsCandidate = path.join(pageDir, `${FILES.index}${EXTENSIONS.js}`);
|
|
226
|
+
const jsxCandidate = path.join(pageDir, `${FILES.index}.jsx`);
|
|
227
|
+
const pageScriptExists = [tsCandidate, tsxCandidate, jsCandidate, jsxCandidate].some(
|
|
228
|
+
(candidate) => fs.existsSync(candidate),
|
|
229
|
+
);
|
|
230
|
+
if (pageScriptExists) {
|
|
231
|
+
const hasScript =
|
|
232
|
+
document(`script[src="/${FOLDERS.pages}/${pageName}/${FILES.index}${EXTENSIONS.js}"]`)
|
|
233
|
+
.length > 0;
|
|
234
|
+
if (!hasScript) {
|
|
235
|
+
document('head').append(
|
|
236
|
+
`<script type="module" src="/${FOLDERS.pages}/${pageName}/${FILES.index}${EXTENSIONS.js}"></script>`,
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
return document.root().html() ?? html;
|
|
230
242
|
}
|
|
231
243
|
|
|
232
244
|
function rewritePageRelativeAssets(document: CheerioAPI, pageName: string): void {
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
245
|
+
const pagePrefix = `/${FOLDERS.pages}/${pageName}/`;
|
|
246
|
+
|
|
247
|
+
document('link[rel="stylesheet"]').each((_, element) => {
|
|
248
|
+
const node = document(element);
|
|
249
|
+
const href = node.attr('href');
|
|
250
|
+
if (
|
|
251
|
+
!href ||
|
|
252
|
+
href.startsWith('/') ||
|
|
253
|
+
href.startsWith('http:') ||
|
|
254
|
+
href.startsWith('https:') ||
|
|
255
|
+
href.startsWith('data:')
|
|
256
|
+
) {
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
node.attr('href', `${pagePrefix}${href}`);
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
document('script[src]').each((_, element) => {
|
|
263
|
+
const node = document(element);
|
|
264
|
+
const src = node.attr('src');
|
|
265
|
+
if (
|
|
266
|
+
!src ||
|
|
267
|
+
src.startsWith('/') ||
|
|
268
|
+
src.startsWith('http:') ||
|
|
269
|
+
src.startsWith('https:') ||
|
|
270
|
+
src.startsWith('data:')
|
|
271
|
+
) {
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
node.attr('src', `${pagePrefix}${src}`);
|
|
275
|
+
});
|
|
252
276
|
}
|
|
253
277
|
|
|
254
278
|
async function rewriteForPublish(
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
279
|
+
context: BuilderContext,
|
|
280
|
+
html: string,
|
|
281
|
+
pageName: string,
|
|
282
|
+
manifest: { js?: string; css?: string },
|
|
283
|
+
pageDirectory: string,
|
|
284
|
+
shared: { css?: string; js?: string } | null,
|
|
285
|
+
options: {
|
|
286
|
+
readonly pagesUrlPrefix: string;
|
|
287
|
+
readonly buildPagesUrlPrefix: string;
|
|
288
|
+
readonly useRootIndex: boolean;
|
|
289
|
+
},
|
|
266
290
|
): Promise<string> {
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
].
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
291
|
+
const document = load(html);
|
|
292
|
+
const { pagesUrlPrefix, buildPagesUrlPrefix, useRootIndex } = options;
|
|
293
|
+
const buildScriptHref = resolvePageAssetUrl(
|
|
294
|
+
buildPagesUrlPrefix,
|
|
295
|
+
pageName,
|
|
296
|
+
`${FILES.index}${EXTENSIONS.js}`,
|
|
297
|
+
);
|
|
298
|
+
const buildCssHref = resolvePageAssetUrl(
|
|
299
|
+
buildPagesUrlPrefix,
|
|
300
|
+
pageName,
|
|
301
|
+
`${FILES.index}${EXTENSIONS.css}`,
|
|
302
|
+
);
|
|
303
|
+
|
|
304
|
+
removeDevScripts(document);
|
|
305
|
+
|
|
306
|
+
const appCssHref = shared?.css ? `/app/${shared.css}` : `/${FOLDERS.app}/app.css`;
|
|
307
|
+
if (shared?.css) {
|
|
308
|
+
document(`link[href="/app/app.css"]`).attr('href', appCssHref);
|
|
309
|
+
}
|
|
310
|
+
ensureStylesheetPreload(document, appCssHref);
|
|
311
|
+
ensureAppShellCriticalCss(document, appCssHref);
|
|
312
|
+
if (document('[data-scope="docs"]').length > 0) {
|
|
313
|
+
ensureDocsShellCriticalCss(document);
|
|
314
|
+
}
|
|
315
|
+
if (shared?.js) {
|
|
316
|
+
document(`script[src="/app/app.js"]`).attr('src', `/app/${shared.js}`).attr('type', 'module');
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
const scriptSelector = [
|
|
320
|
+
`script[src="${FILES.index}${EXTENSIONS.js}"]`,
|
|
321
|
+
`script[src="${buildScriptHref}"]`,
|
|
322
|
+
].join(', ');
|
|
323
|
+
if (manifest.js) {
|
|
324
|
+
document(scriptSelector)
|
|
325
|
+
.attr('src', resolvePageAssetUrl(pagesUrlPrefix, pageName, manifest.js))
|
|
326
|
+
.attr('type', 'module');
|
|
327
|
+
} else {
|
|
328
|
+
document(scriptSelector).remove();
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
const cssSelector = [
|
|
332
|
+
`link[href="${FILES.index}${EXTENSIONS.css}"]`,
|
|
333
|
+
`link[href="${buildCssHref}"]`,
|
|
334
|
+
].join(', ');
|
|
335
|
+
if (manifest.css) {
|
|
336
|
+
document(cssSelector).attr('href', resolvePageAssetUrl(pagesUrlPrefix, pageName, manifest.css));
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
applyLazyLoading(document);
|
|
340
|
+
|
|
341
|
+
if (context.config.features.imageOptimization) {
|
|
342
|
+
await addImageDimensions(document, context, pageDirectory);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
if (context.config.features.htmlSecurity) {
|
|
346
|
+
await inlineCriticalCss(
|
|
347
|
+
document,
|
|
348
|
+
pageName,
|
|
349
|
+
context.config.paths.dist.pages,
|
|
350
|
+
pagesUrlPrefix,
|
|
351
|
+
manifest.css,
|
|
352
|
+
);
|
|
353
|
+
const sriResult = await addSubresourceIntegrity(document, {
|
|
354
|
+
allowExternalFetch: context.config.features.externalResourceIntegrity,
|
|
355
|
+
});
|
|
356
|
+
if (sriResult.skippedExternalResources.length > 0) {
|
|
357
|
+
const resources = Array.from(new Set(sriResult.skippedExternalResources));
|
|
358
|
+
const message =
|
|
359
|
+
resources.length === 1
|
|
360
|
+
? `Skipped automatic SRI for ${resources[0]} because external resource fetching is disabled.`
|
|
361
|
+
: `Skipped automatic SRI for ${resources.length} external resources because external resource fetching is disabled.`;
|
|
362
|
+
emitDiagnostic({
|
|
363
|
+
code: 'frontend.sri.external_fetch_disabled',
|
|
364
|
+
kind: 'sri',
|
|
365
|
+
stage: 'html.publish',
|
|
366
|
+
severity: 'warning',
|
|
367
|
+
message,
|
|
368
|
+
data: { resources },
|
|
369
|
+
suggestion:
|
|
370
|
+
'Add integrity attributes manually or set features.externalResourceIntegrity to true to opt in.',
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
if (sriResult.failures.length > 0) {
|
|
374
|
+
const resources = sriResult.failures;
|
|
375
|
+
const message =
|
|
376
|
+
resources.length === 1
|
|
377
|
+
? `Failed to compute subresource integrity for ${resources[0]}.`
|
|
378
|
+
: `Failed to compute subresource integrity for ${resources.length} resources.`;
|
|
379
|
+
emitDiagnostic({
|
|
380
|
+
code: 'frontend.sri.unresolved',
|
|
381
|
+
kind: 'sri',
|
|
382
|
+
stage: 'html.publish',
|
|
383
|
+
severity: 'warning',
|
|
384
|
+
message,
|
|
385
|
+
data: { resources },
|
|
386
|
+
suggestion:
|
|
387
|
+
'Verify the resource is reachable and not blocked by auth or network constraints.',
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
const hints = injectResourceHints(document, pageName, pagesUrlPrefix, useRootIndex);
|
|
392
|
+
if (hints.missingHead) {
|
|
393
|
+
emitDiagnostic({
|
|
394
|
+
code: 'frontend.resourceHints.missingHead',
|
|
395
|
+
kind: 'resource-hints',
|
|
396
|
+
stage: 'html.publish',
|
|
397
|
+
severity: 'warning',
|
|
398
|
+
message: 'Unable to inject resource hints because <head> is missing.',
|
|
399
|
+
data: { candidates: hints.candidates },
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
dedupeHeadMeta(document, 'name');
|
|
405
|
+
dedupeHeadMeta(document, 'property');
|
|
406
|
+
dedupeHeadLinks(document, 'rel');
|
|
407
|
+
|
|
408
|
+
const htmlOutput = document.root().html() ?? '';
|
|
409
|
+
return await minifyHtml(htmlOutput);
|
|
353
410
|
}
|
|
354
411
|
|
|
355
412
|
async function handlePrecompression(context: BuilderContext, outputPath: string): Promise<void> {
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
413
|
+
if (context.config.features.precompression) {
|
|
414
|
+
await createCompressedVariants(outputPath);
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
await Promise.all([
|
|
419
|
+
remove(`${outputPath}${EXTENSIONS.br}`).catch(() => undefined),
|
|
420
|
+
remove(`${outputPath}${EXTENSIONS.gz}`).catch(() => undefined),
|
|
421
|
+
]);
|
|
365
422
|
}
|
|
366
423
|
|
|
367
424
|
function validateAppTemplate(html: string, filePath: string): void {
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
425
|
+
const doc = load(html);
|
|
426
|
+
if (doc('main').length === 0) {
|
|
427
|
+
throw new Error(`Base template missing <main> container (${filePath}).`);
|
|
428
|
+
}
|
|
429
|
+
if (doc('head').length === 0) {
|
|
430
|
+
throw new Error(`Base template missing <head> section (${filePath}).`);
|
|
431
|
+
}
|
|
375
432
|
}
|
|
376
433
|
|
|
377
434
|
function validatePageFragment(html: string, filePath: string): void {
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
435
|
+
const doc = load(html);
|
|
436
|
+
if (doc('main').length === 0) {
|
|
437
|
+
throw new Error(`Page fragment missing <main> section (${filePath}).`);
|
|
438
|
+
}
|
|
439
|
+
if (doc('head').length === 0) {
|
|
440
|
+
throw new Error(`Page fragment missing <head> section (${filePath}).`);
|
|
441
|
+
}
|
|
385
442
|
}
|
|
386
443
|
|
|
387
444
|
function warn(message: string): void {
|
|
388
|
-
|
|
445
|
+
console.warn(`[webstir-frontend][html] ${message}`);
|
|
389
446
|
}
|
|
390
447
|
|
|
391
448
|
function ensureStylesheetPreload(document: CheerioAPI, href: string): void {
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
449
|
+
const head = document('head').first();
|
|
450
|
+
if (head.length === 0) {
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
const existingPreload = document(`link[rel="preload"][href="${href}"]`).first();
|
|
455
|
+
if (existingPreload.length > 0) {
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
const stylesheet = document(`link[rel="stylesheet"][href="${href}"]`).first();
|
|
460
|
+
if (stylesheet.length > 0) {
|
|
461
|
+
stylesheet.attr('fetchpriority', 'high');
|
|
462
|
+
}
|
|
463
|
+
const preloadTag = `<link rel="preload" as="style" href="${href}">`;
|
|
464
|
+
if (stylesheet.length > 0) {
|
|
465
|
+
stylesheet.before(preloadTag);
|
|
466
|
+
} else {
|
|
467
|
+
head.append(preloadTag);
|
|
468
|
+
}
|
|
412
469
|
}
|
|
413
470
|
|
|
414
471
|
function dedupeHeadMeta(document: CheerioAPI, attribute: 'name' | 'property'): void {
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
472
|
+
const head = document('head').first();
|
|
473
|
+
if (head.length === 0) {
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
419
476
|
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
477
|
+
const seen = new Map<string, Cheerio<AnyNode>>();
|
|
478
|
+
head.find(`meta[${attribute}]`).each((_, element) => {
|
|
479
|
+
const value = element.attribs?.[attribute];
|
|
480
|
+
if (!value) {
|
|
481
|
+
return;
|
|
482
|
+
}
|
|
426
483
|
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
484
|
+
const key = value.toLowerCase();
|
|
485
|
+
const previous = seen.get(key);
|
|
486
|
+
if (previous) {
|
|
487
|
+
previous.remove();
|
|
488
|
+
}
|
|
432
489
|
|
|
433
|
-
|
|
434
|
-
|
|
490
|
+
seen.set(key, document(element));
|
|
491
|
+
});
|
|
435
492
|
}
|
|
436
493
|
|
|
437
494
|
function dedupeHeadLinks(document: CheerioAPI, attribute: 'rel'): void {
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
495
|
+
const head = document('head').first();
|
|
496
|
+
if (head.length === 0) {
|
|
497
|
+
return;
|
|
498
|
+
}
|
|
442
499
|
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
500
|
+
const seen = new Map<string, Cheerio<AnyNode>>();
|
|
501
|
+
head.find(`link[${attribute}]`).each((_, element) => {
|
|
502
|
+
const value = element.attribs?.[attribute];
|
|
503
|
+
if (!value) {
|
|
504
|
+
return;
|
|
505
|
+
}
|
|
449
506
|
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
507
|
+
const key = value.toLowerCase();
|
|
508
|
+
const previous = seen.get(key);
|
|
509
|
+
if (previous) {
|
|
510
|
+
previous.remove();
|
|
511
|
+
}
|
|
455
512
|
|
|
456
|
-
|
|
457
|
-
|
|
513
|
+
seen.set(key, document(element));
|
|
514
|
+
});
|
|
458
515
|
}
|
|
459
516
|
|
|
460
517
|
function removeDevScripts(document: CheerioAPI): void {
|
|
461
|
-
|
|
462
|
-
|
|
518
|
+
removeDevScript(document, `/${FILES.refreshJs}`);
|
|
519
|
+
removeDevScript(document, `/${FILES.hmrJs}`);
|
|
463
520
|
}
|
|
464
521
|
|
|
465
522
|
function removeDevScript(document: CheerioAPI, selector: string): void {
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
523
|
+
document(`script[src="${selector}"]`).each((_, element) => {
|
|
524
|
+
const script = document(element);
|
|
525
|
+
const next = script.next();
|
|
526
|
+
script.remove();
|
|
527
|
+
|
|
528
|
+
if (isWhitespaceTextNode(next)) {
|
|
529
|
+
next.remove();
|
|
530
|
+
}
|
|
531
|
+
});
|
|
475
532
|
}
|
|
476
533
|
|
|
477
534
|
function isWhitespaceTextNode(node: Cheerio<AnyNode>): boolean {
|
|
478
|
-
|
|
479
|
-
&& node[0].type === 'text'
|
|
480
|
-
&& (node[0].data ?? '').trim().length === 0;
|
|
535
|
+
return node.length > 0 && node[0].type === 'text' && (node[0].data ?? '').trim().length === 0;
|
|
481
536
|
}
|
|
482
537
|
|
|
483
538
|
async function minifyHtml(html: string): Promise<string> {
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
539
|
+
return minify(html, {
|
|
540
|
+
collapseWhitespace: true,
|
|
541
|
+
keepClosingSlash: true,
|
|
542
|
+
minifyCSS: true,
|
|
543
|
+
minifyJS: false,
|
|
544
|
+
removeComments: true,
|
|
545
|
+
removeOptionalTags: false,
|
|
546
|
+
removeAttributeQuotes: false,
|
|
547
|
+
});
|
|
493
548
|
}
|
|
494
549
|
|
|
495
|
-
async function addImageDimensions(
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
550
|
+
async function addImageDimensions(
|
|
551
|
+
document: CheerioAPI,
|
|
552
|
+
context: BuilderContext,
|
|
553
|
+
pageDirectory: string,
|
|
554
|
+
): Promise<void> {
|
|
555
|
+
const { config } = context;
|
|
556
|
+
const images = document('img').toArray();
|
|
557
|
+
|
|
558
|
+
await Promise.all(
|
|
559
|
+
images.map(async (element) => {
|
|
560
|
+
const img = document(element);
|
|
561
|
+
if (img.attr('width') || img.attr('height')) {
|
|
562
|
+
return;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
const src = img.attr('src');
|
|
566
|
+
if (!src || isExternalSource(src)) {
|
|
567
|
+
return;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
const assetPath = resolveAssetPath(src, pageDirectory, config.paths.build.frontend);
|
|
571
|
+
if (!assetPath || !(await pathExists(assetPath))) {
|
|
572
|
+
return;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
const dimensions = await getImageDimensions(assetPath);
|
|
576
|
+
if (!dimensions) {
|
|
577
|
+
return;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
img.attr('width', dimensions.width.toString());
|
|
581
|
+
img.attr('height', dimensions.height.toString());
|
|
582
|
+
}),
|
|
583
|
+
);
|
|
523
584
|
}
|
|
524
585
|
|
|
525
586
|
function isExternalSource(src: string): boolean {
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
587
|
+
return (
|
|
588
|
+
src.startsWith('http://') ||
|
|
589
|
+
src.startsWith('https://') ||
|
|
590
|
+
src.startsWith('data:') ||
|
|
591
|
+
src.startsWith('//')
|
|
592
|
+
);
|
|
530
593
|
}
|
|
531
594
|
|
|
532
595
|
function resolveAssetPath(src: string, pageDirectory: string, buildRoot: string): string | null {
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
596
|
+
const normalized = src.replace(/\\/g, '/');
|
|
597
|
+
if (normalized.startsWith('/')) {
|
|
598
|
+
const relative = normalized.replace(/^\//, '');
|
|
599
|
+
return path.join(buildRoot, relative);
|
|
600
|
+
}
|
|
538
601
|
|
|
539
|
-
|
|
602
|
+
return path.join(pageDirectory, normalized);
|
|
540
603
|
}
|