@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
package/dist/config/schema.js
CHANGED
|
@@ -9,7 +9,7 @@ export const frontendPathSchema = z.object({
|
|
|
9
9
|
content: z.string(),
|
|
10
10
|
images: z.string(),
|
|
11
11
|
fonts: z.string(),
|
|
12
|
-
media: z.string()
|
|
12
|
+
media: z.string(),
|
|
13
13
|
}),
|
|
14
14
|
build: z.object({
|
|
15
15
|
root: z.string(),
|
|
@@ -19,7 +19,7 @@ export const frontendPathSchema = z.object({
|
|
|
19
19
|
content: z.string(),
|
|
20
20
|
images: z.string(),
|
|
21
21
|
fonts: z.string(),
|
|
22
|
-
media: z.string()
|
|
22
|
+
media: z.string(),
|
|
23
23
|
}),
|
|
24
24
|
dist: z.object({
|
|
25
25
|
root: z.string(),
|
|
@@ -29,16 +29,17 @@ export const frontendPathSchema = z.object({
|
|
|
29
29
|
content: z.string(),
|
|
30
30
|
images: z.string(),
|
|
31
31
|
fonts: z.string(),
|
|
32
|
-
media: z.string()
|
|
33
|
-
})
|
|
32
|
+
media: z.string(),
|
|
33
|
+
}),
|
|
34
34
|
});
|
|
35
35
|
export const frontendFeatureFlagsSchema = z.object({
|
|
36
36
|
htmlSecurity: z.boolean().default(true),
|
|
37
|
+
externalResourceIntegrity: z.boolean().default(false),
|
|
37
38
|
imageOptimization: z.boolean().default(true),
|
|
38
|
-
precompression: z.boolean().default(true)
|
|
39
|
+
precompression: z.boolean().default(true),
|
|
39
40
|
});
|
|
40
41
|
export const frontendConfigSchema = z.object({
|
|
41
42
|
version: z.literal(1),
|
|
42
43
|
paths: frontendPathSchema,
|
|
43
|
-
features: frontendFeatureFlagsSchema
|
|
44
|
+
features: frontendFeatureFlagsSchema,
|
|
44
45
|
});
|
package/dist/config/setup.js
CHANGED
package/dist/config/workspace.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
|
-
import path from 'path';
|
|
2
|
+
import path from 'node:path';
|
|
3
3
|
import { FOLDERS } from '../core/constants.js';
|
|
4
4
|
import { frontendFeatureFlagsSchema } from './schema.js';
|
|
5
5
|
const DEFAULT_FEATURE_FLAGS = {
|
|
6
6
|
htmlSecurity: true,
|
|
7
|
+
externalResourceIntegrity: false,
|
|
7
8
|
imageOptimization: true,
|
|
8
|
-
precompression: true
|
|
9
|
+
precompression: true,
|
|
9
10
|
};
|
|
10
11
|
export function buildConfig(workspaceRoot) {
|
|
11
12
|
const srcRoot = path.join(workspaceRoot, FOLDERS.src);
|
|
@@ -27,7 +28,7 @@ export function buildConfig(workspaceRoot) {
|
|
|
27
28
|
content: srcContentRoot,
|
|
28
29
|
images: path.join(frontendRoot, FOLDERS.images),
|
|
29
30
|
fonts: path.join(frontendRoot, FOLDERS.fonts),
|
|
30
|
-
media: path.join(frontendRoot, FOLDERS.media)
|
|
31
|
+
media: path.join(frontendRoot, FOLDERS.media),
|
|
31
32
|
},
|
|
32
33
|
build: {
|
|
33
34
|
root: buildRoot,
|
|
@@ -37,7 +38,7 @@ export function buildConfig(workspaceRoot) {
|
|
|
37
38
|
content: path.join(buildFrontend, FOLDERS.pages, 'docs'),
|
|
38
39
|
images: path.join(buildFrontend, FOLDERS.images),
|
|
39
40
|
fonts: path.join(buildFrontend, FOLDERS.fonts),
|
|
40
|
-
media: path.join(buildFrontend, FOLDERS.media)
|
|
41
|
+
media: path.join(buildFrontend, FOLDERS.media),
|
|
41
42
|
},
|
|
42
43
|
dist: {
|
|
43
44
|
root: distRoot,
|
|
@@ -47,10 +48,10 @@ export function buildConfig(workspaceRoot) {
|
|
|
47
48
|
content: path.join(distFrontend, FOLDERS.pages, 'docs'),
|
|
48
49
|
images: path.join(distFrontend, FOLDERS.images),
|
|
49
50
|
fonts: path.join(distFrontend, FOLDERS.fonts),
|
|
50
|
-
media: path.join(distFrontend, FOLDERS.media)
|
|
51
|
-
}
|
|
51
|
+
media: path.join(distFrontend, FOLDERS.media),
|
|
52
|
+
},
|
|
52
53
|
},
|
|
53
|
-
features: loadFeatureFlags(frontendRoot)
|
|
54
|
+
features: loadFeatureFlags(frontendRoot),
|
|
54
55
|
};
|
|
55
56
|
}
|
|
56
57
|
function resolveContentRoot(workspaceRoot, frontendRoot) {
|
|
@@ -111,8 +112,9 @@ function loadFeatureFlags(frontendRoot) {
|
|
|
111
112
|
const overrides = frontendFeatureFlagsSchema.parse(overridesSource);
|
|
112
113
|
return {
|
|
113
114
|
htmlSecurity: overrides.htmlSecurity,
|
|
115
|
+
externalResourceIntegrity: overrides.externalResourceIntegrity,
|
|
114
116
|
imageOptimization: overrides.imageOptimization,
|
|
115
|
-
precompression: overrides.precompression
|
|
117
|
+
precompression: overrides.precompression,
|
|
116
118
|
};
|
|
117
119
|
}
|
|
118
120
|
catch (error) {
|
|
@@ -127,5 +129,5 @@ function extractOverrideSource(value) {
|
|
|
127
129
|
return container;
|
|
128
130
|
}
|
|
129
131
|
}
|
|
130
|
-
return
|
|
132
|
+
return value && typeof value === 'object' ? value : {};
|
|
131
133
|
}
|
package/dist/core/constants.d.ts
CHANGED
|
@@ -24,7 +24,7 @@ export declare const FOLDERS: {
|
|
|
24
24
|
};
|
|
25
25
|
export declare const FILES: {
|
|
26
26
|
readonly packageJson: "package.json";
|
|
27
|
-
readonly
|
|
27
|
+
readonly bunLock: "bun.lock";
|
|
28
28
|
readonly tsBuildInfo: ".tsbuildinfo";
|
|
29
29
|
readonly baseTsConfigJson: "base.tsconfig.json";
|
|
30
30
|
readonly manifestJson: "manifest.json";
|
package/dist/core/constants.js
CHANGED
|
@@ -20,11 +20,11 @@ export const FOLDERS = {
|
|
|
20
20
|
nodeModules: 'node_modules',
|
|
21
21
|
seed: 'seed',
|
|
22
22
|
demo: 'demo',
|
|
23
|
-
temp: 'temp'
|
|
23
|
+
temp: 'temp',
|
|
24
24
|
};
|
|
25
25
|
export const FILES = {
|
|
26
26
|
packageJson: 'package.json',
|
|
27
|
-
|
|
27
|
+
bunLock: 'bun.lock',
|
|
28
28
|
tsBuildInfo: '.tsbuildinfo',
|
|
29
29
|
baseTsConfigJson: 'base.tsconfig.json',
|
|
30
30
|
manifestJson: 'manifest.json',
|
|
@@ -33,7 +33,7 @@ export const FILES = {
|
|
|
33
33
|
indexHtml: 'index.html',
|
|
34
34
|
refreshJs: 'refresh.js',
|
|
35
35
|
hmrJs: 'hmr.js',
|
|
36
|
-
robotsTxt: 'robots.txt'
|
|
36
|
+
robotsTxt: 'robots.txt',
|
|
37
37
|
};
|
|
38
38
|
export const EXTENSIONS = {
|
|
39
39
|
html: '.html',
|
|
@@ -63,8 +63,8 @@ export const EXTENSIONS = {
|
|
|
63
63
|
ogg: '.ogg',
|
|
64
64
|
mp4: '.mp4',
|
|
65
65
|
webm: '.webm',
|
|
66
|
-
mov: '.mov'
|
|
66
|
+
mov: '.mov',
|
|
67
67
|
};
|
|
68
68
|
export const FILE_NAMES = {
|
|
69
|
-
htmlAppTemplate: 'app.html'
|
|
69
|
+
htmlAppTemplate: 'app.html',
|
|
70
70
|
};
|
package/dist/core/diagnostics.js
CHANGED
|
@@ -2,7 +2,7 @@ export const STRUCTURED_DIAGNOSTIC_PREFIX = 'WEBSTIR_DIAGNOSTIC ';
|
|
|
2
2
|
export function emitDiagnostic(event) {
|
|
3
3
|
const payload = {
|
|
4
4
|
type: 'diagnostic',
|
|
5
|
-
...event
|
|
5
|
+
...event,
|
|
6
6
|
};
|
|
7
7
|
const logMessage = `[webstir-frontend][${event.code}] ${event.message}`;
|
|
8
8
|
switch (event.severity) {
|
package/dist/core/pages.js
CHANGED
|
@@ -1,23 +1,23 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
|
-
import { glob } from 'glob';
|
|
3
2
|
import { pathExists } from '../utils/fs.js';
|
|
3
|
+
import { scanDirectories } from '../utils/glob.js';
|
|
4
4
|
export async function getPages(root) {
|
|
5
5
|
const directories = await getPageDirectories(root);
|
|
6
6
|
return directories.map((entry) => ({
|
|
7
7
|
name: entry.name,
|
|
8
|
-
directory: entry.directory
|
|
8
|
+
directory: entry.directory,
|
|
9
9
|
}));
|
|
10
10
|
}
|
|
11
11
|
export async function getPageDirectories(root) {
|
|
12
12
|
if (!(await pathExists(root))) {
|
|
13
13
|
return [];
|
|
14
14
|
}
|
|
15
|
-
const entries = await
|
|
15
|
+
const entries = await scanDirectories('*/', { cwd: root, absolute: false });
|
|
16
16
|
return entries.map((entry) => {
|
|
17
17
|
const name = entry.endsWith('/') ? entry.slice(0, -1) : entry;
|
|
18
18
|
return {
|
|
19
19
|
name,
|
|
20
|
-
directory: path.join(root, name)
|
|
20
|
+
directory: path.join(root, name),
|
|
21
21
|
};
|
|
22
22
|
});
|
|
23
23
|
}
|
package/dist/hooks.js
CHANGED
|
@@ -6,7 +6,7 @@ const EMPTY_HOOKS = {
|
|
|
6
6
|
pipelineBefore: [],
|
|
7
7
|
pipelineAfter: [],
|
|
8
8
|
builderBefore: new Map(),
|
|
9
|
-
builderAfter: new Map()
|
|
9
|
+
builderAfter: new Map(),
|
|
10
10
|
};
|
|
11
11
|
export async function loadHooks(workspaceRoot, cacheBust) {
|
|
12
12
|
const configPath = findConfigPath(workspaceRoot);
|
|
@@ -39,7 +39,7 @@ export function createHookContext(config, mode, changedFile, builderName) {
|
|
|
39
39
|
mode,
|
|
40
40
|
workspaceRoot: config.paths.workspace,
|
|
41
41
|
builderName,
|
|
42
|
-
changedFile
|
|
42
|
+
changedFile,
|
|
43
43
|
};
|
|
44
44
|
}
|
|
45
45
|
export async function executeHooks(label, handlers, context) {
|
|
@@ -87,7 +87,7 @@ function normalizeHooks(raw, configPath) {
|
|
|
87
87
|
pipelineBefore,
|
|
88
88
|
pipelineAfter,
|
|
89
89
|
builderBefore,
|
|
90
|
-
builderAfter
|
|
90
|
+
builderAfter,
|
|
91
91
|
};
|
|
92
92
|
}
|
|
93
93
|
function normalizeHandlerSet(value, label) {
|
package/dist/html/criticalCss.js
CHANGED
|
@@ -4,7 +4,8 @@ import { EXTENSIONS } from '../core/constants.js';
|
|
|
4
4
|
import { pathExists, readFile, stat } from '../utils/fs.js';
|
|
5
5
|
import { resolvePageAssetUrl } from '../utils/pagePaths.js';
|
|
6
6
|
const INLINE_THRESHOLD_BYTES = 6 * 1024;
|
|
7
|
-
const csso = (cssoModule.default ??
|
|
7
|
+
const csso = (cssoModule.default ??
|
|
8
|
+
cssoModule);
|
|
8
9
|
function minifyCriticalCss(css) {
|
|
9
10
|
return csso.minify(css).css;
|
|
10
11
|
}
|
|
@@ -178,10 +179,12 @@ export function ensureDocsShellCriticalCss(document) {
|
|
|
178
179
|
if (existing.length > 0) {
|
|
179
180
|
return;
|
|
180
181
|
}
|
|
181
|
-
const docsStylesheet = document('link[rel="stylesheet"]')
|
|
182
|
+
const docsStylesheet = document('link[rel="stylesheet"]')
|
|
183
|
+
.filter((_, element) => {
|
|
182
184
|
const href = document(element).attr('href');
|
|
183
185
|
return typeof href === 'string' && href.includes('/docs/');
|
|
184
|
-
})
|
|
186
|
+
})
|
|
187
|
+
.first();
|
|
185
188
|
const styleTag = `<style data-critical="docs">\n${DOCS_SHELL_CRITICAL_CSS}\n</style>`;
|
|
186
189
|
if (docsStylesheet.length > 0) {
|
|
187
190
|
docsStylesheet.before(styleTag);
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import type { CheerioAPI } from 'cheerio';
|
|
2
|
+
export interface SubresourceIntegrityOptions {
|
|
3
|
+
readonly allowExternalFetch?: boolean;
|
|
4
|
+
readonly fetcher?: typeof fetch;
|
|
5
|
+
}
|
|
2
6
|
export interface SubresourceIntegrityResult {
|
|
3
7
|
readonly failures: string[];
|
|
8
|
+
readonly skippedExternalResources: string[];
|
|
4
9
|
}
|
|
5
|
-
export declare function addSubresourceIntegrity(document: CheerioAPI): Promise<SubresourceIntegrityResult>;
|
|
10
|
+
export declare function addSubresourceIntegrity(document: CheerioAPI, options?: SubresourceIntegrityOptions): Promise<SubresourceIntegrityResult>;
|
|
@@ -1,22 +1,29 @@
|
|
|
1
|
-
import { createHash } from 'node:crypto';
|
|
2
1
|
const HTTP_TIMEOUT_MS = 5000;
|
|
3
|
-
export async function addSubresourceIntegrity(document) {
|
|
2
|
+
export async function addSubresourceIntegrity(document, options = {}) {
|
|
4
3
|
const failures = [];
|
|
4
|
+
const skippedExternalResources = [];
|
|
5
5
|
await Promise.all([
|
|
6
|
-
processScripts(document, failures),
|
|
7
|
-
processStylesheets(document, failures)
|
|
6
|
+
processScripts(document, failures, skippedExternalResources, options),
|
|
7
|
+
processStylesheets(document, failures, skippedExternalResources, options),
|
|
8
8
|
]);
|
|
9
|
-
return { failures };
|
|
9
|
+
return { failures, skippedExternalResources };
|
|
10
10
|
}
|
|
11
|
-
async function processScripts(document, failures) {
|
|
11
|
+
async function processScripts(document, failures, skippedExternalResources, options) {
|
|
12
12
|
const scripts = document('script[src]').toArray();
|
|
13
13
|
await Promise.all(scripts.map(async (element) => {
|
|
14
14
|
const script = document(element);
|
|
15
15
|
const src = script.attr('src');
|
|
16
|
-
if (!src ||
|
|
16
|
+
if (!src || script.attr('integrity')) {
|
|
17
17
|
return;
|
|
18
18
|
}
|
|
19
|
-
|
|
19
|
+
if (!isExternal(src)) {
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
if (!options.allowExternalFetch) {
|
|
23
|
+
skippedExternalResources.push(src);
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
const sri = await fetchIntegrity(src, options.fetcher ?? fetch);
|
|
20
27
|
if (!sri) {
|
|
21
28
|
failures.push(src);
|
|
22
29
|
return;
|
|
@@ -27,15 +34,22 @@ async function processScripts(document, failures) {
|
|
|
27
34
|
}
|
|
28
35
|
}));
|
|
29
36
|
}
|
|
30
|
-
async function processStylesheets(document, failures) {
|
|
37
|
+
async function processStylesheets(document, failures, skippedExternalResources, options) {
|
|
31
38
|
const links = document('link[rel="stylesheet"][href]').toArray();
|
|
32
39
|
await Promise.all(links.map(async (element) => {
|
|
33
40
|
const link = document(element);
|
|
34
41
|
const href = link.attr('href');
|
|
35
|
-
if (!href ||
|
|
42
|
+
if (!href || link.attr('integrity')) {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
if (!isExternal(href)) {
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
if (!options.allowExternalFetch) {
|
|
49
|
+
skippedExternalResources.push(href);
|
|
36
50
|
return;
|
|
37
51
|
}
|
|
38
|
-
const sri = await fetchIntegrity(href);
|
|
52
|
+
const sri = await fetchIntegrity(href, options.fetcher ?? fetch);
|
|
39
53
|
if (!sri) {
|
|
40
54
|
failures.push(href);
|
|
41
55
|
return;
|
|
@@ -49,18 +63,18 @@ async function processStylesheets(document, failures) {
|
|
|
49
63
|
function isExternal(url) {
|
|
50
64
|
return url.startsWith('http://') || url.startsWith('https://') || url.startsWith('//');
|
|
51
65
|
}
|
|
52
|
-
async function fetchIntegrity(url) {
|
|
66
|
+
async function fetchIntegrity(url, fetcher) {
|
|
53
67
|
try {
|
|
54
68
|
const normalizedUrl = url.startsWith('//') ? `https:${url}` : url;
|
|
55
69
|
const controller = new AbortController();
|
|
56
70
|
const timeout = setTimeout(() => controller.abort(), HTTP_TIMEOUT_MS);
|
|
57
71
|
try {
|
|
58
|
-
const response = await
|
|
72
|
+
const response = await fetcher(normalizedUrl, { signal: controller.signal });
|
|
59
73
|
if (!response.ok) {
|
|
60
74
|
return null;
|
|
61
75
|
}
|
|
62
76
|
const arrayBuffer = await response.arrayBuffer();
|
|
63
|
-
const hash =
|
|
77
|
+
const hash = new Bun.CryptoHasher('sha384').update(Buffer.from(arrayBuffer)).digest('base64');
|
|
64
78
|
return `sha384-${hash}`;
|
|
65
79
|
}
|
|
66
80
|
finally {
|
package/dist/html/lazyLoad.js
CHANGED
|
@@ -10,7 +10,7 @@ export async function createPageScaffold(options) {
|
|
|
10
10
|
const mode = options.mode ?? 'standard';
|
|
11
11
|
const writes = [
|
|
12
12
|
writeFile(path.join(pageDir, `${FILES.index}${EXTENSIONS.html}`), buildHtmlTemplate(options.pageName, mode)),
|
|
13
|
-
writeFile(path.join(pageDir, `${FILES.index}${EXTENSIONS.css}`), buildCssTemplate(options.pageName))
|
|
13
|
+
writeFile(path.join(pageDir, `${FILES.index}${EXTENSIONS.css}`), buildCssTemplate(options.pageName)),
|
|
14
14
|
];
|
|
15
15
|
if (mode === 'standard') {
|
|
16
16
|
writes.push(writeFile(path.join(pageDir, `${FILES.index}${EXTENSIONS.ts}`), buildScriptTemplate()));
|
|
@@ -6,7 +6,7 @@ export function injectResourceHints(document, currentPage, pagesUrlPrefix, useRo
|
|
|
6
6
|
return {
|
|
7
7
|
added: 0,
|
|
8
8
|
candidates: pages,
|
|
9
|
-
missingHead: pages.length > 0
|
|
9
|
+
missingHead: pages.length > 0,
|
|
10
10
|
};
|
|
11
11
|
}
|
|
12
12
|
if (pages.length === 0) {
|
|
@@ -35,7 +35,10 @@ function normalizePageName(href, pagesUrlPrefix) {
|
|
|
35
35
|
return null;
|
|
36
36
|
}
|
|
37
37
|
const lower = href.toLowerCase();
|
|
38
|
-
if (lower.startsWith('http://') ||
|
|
38
|
+
if (lower.startsWith('http://') ||
|
|
39
|
+
lower.startsWith('https://') ||
|
|
40
|
+
lower.startsWith('mailto:') ||
|
|
41
|
+
lower.startsWith('#')) {
|
|
39
42
|
return null;
|
|
40
43
|
}
|
|
41
44
|
let path = href.split('#')[0]?.split('?')[0] ?? '';
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
package/dist/inspect.js
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { FILES, FILE_NAMES } from './core/constants.js';
|
|
3
|
+
import { getPageDirectories } from './core/pages.js';
|
|
4
|
+
import { buildConfig } from './config/workspace.js';
|
|
5
|
+
import { pathExists, readJson } from './utils/fs.js';
|
|
6
|
+
const PAGE_SCRIPT_EXTENSIONS = ['.ts', '.tsx', '.js', '.jsx'];
|
|
7
|
+
export async function inspectFrontendWorkspace(workspaceRoot) {
|
|
8
|
+
const config = buildConfig(workspaceRoot);
|
|
9
|
+
const packageJson = await readWorkspacePackageInspect(workspaceRoot);
|
|
10
|
+
const appShell = await inspectAppShell(config.paths.src.app);
|
|
11
|
+
const pages = await inspectPages(config.paths.src.pages);
|
|
12
|
+
const content = await inspectContent(config.paths.src.content);
|
|
13
|
+
return {
|
|
14
|
+
workspaceRoot,
|
|
15
|
+
config,
|
|
16
|
+
packageJson,
|
|
17
|
+
appShell,
|
|
18
|
+
pages,
|
|
19
|
+
content,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
async function readWorkspacePackageInspect(workspaceRoot) {
|
|
23
|
+
const packagePath = path.join(workspaceRoot, FILES.packageJson);
|
|
24
|
+
const pkg = await readJson(packagePath);
|
|
25
|
+
const enable = pkg?.webstir?.enable;
|
|
26
|
+
return {
|
|
27
|
+
path: packagePath,
|
|
28
|
+
exists: pkg !== null,
|
|
29
|
+
mode: pkg?.webstir?.mode,
|
|
30
|
+
enable: {
|
|
31
|
+
raw: enable,
|
|
32
|
+
known: normalizeKnownEnableFlags(enable),
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
async function inspectAppShell(appRoot) {
|
|
37
|
+
const templatePath = path.join(appRoot, FILE_NAMES.htmlAppTemplate);
|
|
38
|
+
const stylesheetPath = path.join(appRoot, 'app.css');
|
|
39
|
+
const scriptPath = await resolveFirstExistingPath(appRoot, 'app', PAGE_SCRIPT_EXTENSIONS);
|
|
40
|
+
const [exists, templateExists, stylesheetExists, scriptExists] = await Promise.all([
|
|
41
|
+
pathExists(appRoot),
|
|
42
|
+
pathExists(templatePath),
|
|
43
|
+
pathExists(stylesheetPath),
|
|
44
|
+
pathExists(scriptPath),
|
|
45
|
+
]);
|
|
46
|
+
return {
|
|
47
|
+
root: appRoot,
|
|
48
|
+
exists,
|
|
49
|
+
templatePath,
|
|
50
|
+
templateExists,
|
|
51
|
+
stylesheetPath,
|
|
52
|
+
stylesheetExists,
|
|
53
|
+
scriptPath,
|
|
54
|
+
scriptExists,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
async function inspectPages(pagesRoot) {
|
|
58
|
+
const pages = await getPageDirectories(pagesRoot);
|
|
59
|
+
return await Promise.all(pages.map(async (page) => {
|
|
60
|
+
const htmlPath = path.join(page.directory, FILES.indexHtml);
|
|
61
|
+
const stylesheetPath = path.join(page.directory, `${FILES.index}.css`);
|
|
62
|
+
const scriptPath = await resolveFirstExistingPath(page.directory, FILES.index, PAGE_SCRIPT_EXTENSIONS);
|
|
63
|
+
const [htmlExists, stylesheetExists, scriptExists] = await Promise.all([
|
|
64
|
+
pathExists(htmlPath),
|
|
65
|
+
pathExists(stylesheetPath),
|
|
66
|
+
pathExists(scriptPath),
|
|
67
|
+
]);
|
|
68
|
+
return {
|
|
69
|
+
name: page.name,
|
|
70
|
+
directory: page.directory,
|
|
71
|
+
htmlPath,
|
|
72
|
+
htmlExists,
|
|
73
|
+
stylesheetPath,
|
|
74
|
+
stylesheetExists,
|
|
75
|
+
scriptPath,
|
|
76
|
+
scriptExists,
|
|
77
|
+
};
|
|
78
|
+
}));
|
|
79
|
+
}
|
|
80
|
+
async function inspectContent(contentRoot) {
|
|
81
|
+
const sidebarOverridePath = path.join(contentRoot, '_sidebar.json');
|
|
82
|
+
const [exists, sidebarOverrideExists] = await Promise.all([
|
|
83
|
+
pathExists(contentRoot),
|
|
84
|
+
pathExists(sidebarOverridePath),
|
|
85
|
+
]);
|
|
86
|
+
return {
|
|
87
|
+
root: contentRoot,
|
|
88
|
+
exists,
|
|
89
|
+
sidebarOverridePath,
|
|
90
|
+
sidebarOverrideExists,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
async function resolveFirstExistingPath(root, baseName, extensions) {
|
|
94
|
+
for (const extension of extensions) {
|
|
95
|
+
const candidate = path.join(root, `${baseName}${extension}`);
|
|
96
|
+
if (await pathExists(candidate)) {
|
|
97
|
+
return candidate;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return path.join(root, `${baseName}${extensions[0] ?? ''}`);
|
|
101
|
+
}
|
|
102
|
+
function normalizeKnownEnableFlags(value) {
|
|
103
|
+
return {
|
|
104
|
+
spa: value?.spa === true,
|
|
105
|
+
clientNav: value?.clientNav === true,
|
|
106
|
+
backend: value?.backend === true,
|
|
107
|
+
search: value?.search === true,
|
|
108
|
+
contentNav: value?.contentNav === true,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
@@ -22,7 +22,7 @@ export async function ensureSsgViewMetadataForPage(options) {
|
|
|
22
22
|
name: viewName,
|
|
23
23
|
path: viewPath,
|
|
24
24
|
renderMode: 'ssg',
|
|
25
|
-
staticPaths: [viewPath]
|
|
25
|
+
staticPaths: [viewPath],
|
|
26
26
|
};
|
|
27
27
|
if (existingIndex >= 0) {
|
|
28
28
|
existingViews[existingIndex] = nextView;
|
|
@@ -36,9 +36,9 @@ export async function ensureSsgViewMetadataForPage(options) {
|
|
|
36
36
|
...webstir,
|
|
37
37
|
moduleManifest: {
|
|
38
38
|
...moduleConfig,
|
|
39
|
-
views: existingViews
|
|
40
|
-
}
|
|
41
|
-
}
|
|
39
|
+
views: existingViews,
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
42
|
};
|
|
43
43
|
await writeJson(pkgPath, nextPkg);
|
|
44
44
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
|
-
import { glob } from 'glob';
|
|
3
2
|
import { FOLDERS, FILES } from '../../core/constants.js';
|
|
4
3
|
import { copy, ensureDir, pathExists, readJson } from '../../utils/fs.js';
|
|
4
|
+
import { scanGlob } from '../../utils/glob.js';
|
|
5
5
|
import { getPageDirectories } from '../../core/pages.js';
|
|
6
6
|
import { assertNoSsgRoutesInModuleConfig } from './validation.js';
|
|
7
7
|
import { runSsgSeo } from './seo.js';
|
|
@@ -53,10 +53,7 @@ async function applyDocsContentAliases(distRoot, distPagesRoot) {
|
|
|
53
53
|
if (!(await pathExists(docsRoot))) {
|
|
54
54
|
return;
|
|
55
55
|
}
|
|
56
|
-
const indexes = await
|
|
57
|
-
cwd: distPagesRoot,
|
|
58
|
-
nodir: true
|
|
59
|
-
});
|
|
56
|
+
const indexes = await scanGlob('docs/**/index.html', { cwd: distPagesRoot });
|
|
60
57
|
for (const relativeIndex of indexes) {
|
|
61
58
|
const sourceIndex = path.join(distPagesRoot, relativeIndex);
|
|
62
59
|
if (!(await pathExists(sourceIndex))) {
|
package/dist/modes/ssg/seo.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
|
-
import { glob } from 'glob';
|
|
3
2
|
import { load } from 'cheerio';
|
|
4
3
|
import { ensureDir, pathExists, readFile, writeFile } from '../../utils/fs.js';
|
|
5
4
|
import { FILES } from '../../core/constants.js';
|
|
5
|
+
import { scanGlob } from '../../utils/glob.js';
|
|
6
6
|
export async function runSsgSeo(distRoot, options = {}) {
|
|
7
7
|
const pages = await discoverHtmlPages(distRoot);
|
|
8
8
|
await validateInternalLinks(pages, distRoot);
|
|
@@ -13,14 +13,14 @@ async function discoverHtmlPages(distRoot) {
|
|
|
13
13
|
if (!(await pathExists(distRoot))) {
|
|
14
14
|
return [];
|
|
15
15
|
}
|
|
16
|
-
const files = await
|
|
16
|
+
const files = (await scanGlob('**/index.html', { cwd: distRoot })).filter((relative) => !relative.split(path.sep).join('/').startsWith('pages/'));
|
|
17
17
|
const pages = files
|
|
18
18
|
.map((relative) => {
|
|
19
19
|
const normalized = relative.split(path.sep).join('/');
|
|
20
20
|
const urlPath = toUrlPath(normalized);
|
|
21
21
|
return {
|
|
22
22
|
filePath: path.join(distRoot, relative),
|
|
23
|
-
urlPath
|
|
23
|
+
urlPath,
|
|
24
24
|
};
|
|
25
25
|
})
|
|
26
26
|
.filter((page) => Boolean(page.urlPath));
|
|
@@ -165,10 +165,10 @@ async function writeSitemap(distRoot, pages, siteUrl) {
|
|
|
165
165
|
.join('\n');
|
|
166
166
|
const xml = [
|
|
167
167
|
'<?xml version="1.0" encoding="UTF-8"?>',
|
|
168
|
-
comment
|
|
168
|
+
`${comment}<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">`,
|
|
169
169
|
entries,
|
|
170
170
|
'</urlset>',
|
|
171
|
-
''
|
|
171
|
+
'',
|
|
172
172
|
].join('\n');
|
|
173
173
|
const outputPath = path.join(distRoot, 'sitemap.xml');
|
|
174
174
|
await ensureDir(path.dirname(outputPath));
|