@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/modes/ssg/views.js
CHANGED
|
@@ -42,17 +42,17 @@ export async function generateSsgViewData(config) {
|
|
|
42
42
|
try {
|
|
43
43
|
data = await spec.load(ssrContext);
|
|
44
44
|
}
|
|
45
|
-
catch {
|
|
46
|
-
|
|
47
|
-
|
|
45
|
+
catch (error) {
|
|
46
|
+
const viewLabel = viewName || viewPathTemplate || normalizedPath;
|
|
47
|
+
throw new Error(`[webstir-frontend] failed to load SSG view data for "${viewLabel}" at ${normalizedPath}: ${formatErrorMessage(error)}`);
|
|
48
48
|
}
|
|
49
|
-
const pageName = normalizedPath === '/' ? FOLDERS.home : firstPathSegment(normalizedPath) ?? FOLDERS.home;
|
|
49
|
+
const pageName = normalizedPath === '/' ? FOLDERS.home : (firstPathSegment(normalizedPath) ?? FOLDERS.home);
|
|
50
50
|
const entries = perPageData.get(pageName) ?? [];
|
|
51
51
|
entries.push({
|
|
52
52
|
viewName: viewName || viewPathTemplate || normalizedPath,
|
|
53
53
|
path: normalizedPath,
|
|
54
54
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
|
55
|
-
data
|
|
55
|
+
data,
|
|
56
56
|
});
|
|
57
57
|
perPageData.set(pageName, entries);
|
|
58
58
|
}
|
|
@@ -82,7 +82,7 @@ async function loadBackendModuleDefinition(workspaceRoot) {
|
|
|
82
82
|
path.join(buildRoot, 'module.js'),
|
|
83
83
|
path.join(buildRoot, 'module.mjs'),
|
|
84
84
|
path.join(buildRoot, 'module', 'index.js'),
|
|
85
|
-
path.join(buildRoot, 'module', 'index.mjs')
|
|
85
|
+
path.join(buildRoot, 'module', 'index.mjs'),
|
|
86
86
|
];
|
|
87
87
|
for (const fullPath of candidates) {
|
|
88
88
|
if (!(await pathExists(fullPath))) {
|
|
@@ -96,8 +96,8 @@ async function loadBackendModuleDefinition(workspaceRoot) {
|
|
|
96
96
|
return candidate;
|
|
97
97
|
}
|
|
98
98
|
}
|
|
99
|
-
catch {
|
|
100
|
-
|
|
99
|
+
catch (error) {
|
|
100
|
+
throw new Error(`[webstir-frontend] failed to import backend module definition from ${fullPath}: ${formatErrorMessage(error)}`);
|
|
101
101
|
}
|
|
102
102
|
}
|
|
103
103
|
return undefined;
|
|
@@ -124,6 +124,12 @@ function normalizePath(value) {
|
|
|
124
124
|
}
|
|
125
125
|
return s;
|
|
126
126
|
}
|
|
127
|
+
function formatErrorMessage(error) {
|
|
128
|
+
if (error instanceof Error && error.message) {
|
|
129
|
+
return error.message;
|
|
130
|
+
}
|
|
131
|
+
return String(error);
|
|
132
|
+
}
|
|
127
133
|
function firstPathSegment(pathname) {
|
|
128
134
|
const [, segment] = pathname.split('/');
|
|
129
135
|
if (!segment) {
|
|
@@ -172,7 +178,7 @@ function createMinimalSsrContext(pathname, params) {
|
|
|
172
178
|
},
|
|
173
179
|
entries() {
|
|
174
180
|
return process.env;
|
|
175
|
-
}
|
|
181
|
+
},
|
|
176
182
|
};
|
|
177
183
|
const logger = {
|
|
178
184
|
level: 'info',
|
|
@@ -193,7 +199,7 @@ function createMinimalSsrContext(pathname, params) {
|
|
|
193
199
|
},
|
|
194
200
|
with(_bindings) {
|
|
195
201
|
return this;
|
|
196
|
-
}
|
|
202
|
+
},
|
|
197
203
|
};
|
|
198
204
|
return {
|
|
199
205
|
url,
|
|
@@ -204,7 +210,7 @@ function createMinimalSsrContext(pathname, params) {
|
|
|
204
210
|
session: null,
|
|
205
211
|
env: envAccessor,
|
|
206
212
|
logger,
|
|
207
|
-
now: () => new Date()
|
|
213
|
+
now: () => new Date(),
|
|
208
214
|
};
|
|
209
215
|
}
|
|
210
216
|
function getEffectiveStaticPaths(meta, definition, isSsgWorkspace) {
|
package/dist/operations.js
CHANGED
|
@@ -2,7 +2,7 @@ import { readdir } from 'node:fs/promises';
|
|
|
2
2
|
import { runPipeline } from './pipeline.js';
|
|
3
3
|
import { createPageScaffold } from './html/pageScaffold.js';
|
|
4
4
|
import { prepareWorkspaceConfig } from './config/setup.js';
|
|
5
|
-
import { applySsgRouting, assertNoSsgRoutes, ensureSsgViewMetadataForPage, generateSsgViewData } from './modes/ssg/index.js';
|
|
5
|
+
import { applySsgRouting, assertNoSsgRoutes, ensureSsgViewMetadataForPage, generateSsgViewData, } from './modes/ssg/index.js';
|
|
6
6
|
import path from 'node:path';
|
|
7
7
|
import { FOLDERS } from './core/constants.js';
|
|
8
8
|
import { pathExists, readJson, remove } from './utils/fs.js';
|
|
@@ -10,7 +10,11 @@ export async function runBuild(options) {
|
|
|
10
10
|
const config = await prepareWorkspaceConfig(options.workspaceRoot);
|
|
11
11
|
const enable = await readWorkspaceEnableFlags(options.workspaceRoot);
|
|
12
12
|
console.info('[webstir-frontend] Running build pipeline...');
|
|
13
|
-
await runPipeline(config, 'build', {
|
|
13
|
+
await runPipeline(config, 'build', {
|
|
14
|
+
changedFile: options.changedFile,
|
|
15
|
+
enable,
|
|
16
|
+
env: process.env,
|
|
17
|
+
});
|
|
14
18
|
console.info('[webstir-frontend] Build pipeline completed.');
|
|
15
19
|
}
|
|
16
20
|
export async function runPublish(options) {
|
|
@@ -22,7 +26,7 @@ export async function runPublish(options) {
|
|
|
22
26
|
if (options.publishMode === 'ssg') {
|
|
23
27
|
await assertNoSsgRoutes(config.paths.workspace);
|
|
24
28
|
}
|
|
25
|
-
await runPipeline(publishConfig, 'publish', { enable });
|
|
29
|
+
await runPipeline(publishConfig, 'publish', { enable, env: process.env });
|
|
26
30
|
if (options.publishMode === 'ssg') {
|
|
27
31
|
await generateSsgViewData(publishConfig);
|
|
28
32
|
await applySsgRouting(publishConfig);
|
|
@@ -34,7 +38,11 @@ export async function runRebuild(options) {
|
|
|
34
38
|
const config = await prepareWorkspaceConfig(options.workspaceRoot);
|
|
35
39
|
const enable = await readWorkspaceEnableFlags(options.workspaceRoot);
|
|
36
40
|
console.info('[webstir-frontend] Running rebuild pipeline...');
|
|
37
|
-
await runPipeline(config, 'build', {
|
|
41
|
+
await runPipeline(config, 'build', {
|
|
42
|
+
changedFile: options.changedFile,
|
|
43
|
+
enable,
|
|
44
|
+
env: process.env,
|
|
45
|
+
});
|
|
38
46
|
console.info('[webstir-frontend] Rebuild pipeline completed.');
|
|
39
47
|
}
|
|
40
48
|
export async function runAddPage(options) {
|
|
@@ -48,13 +56,13 @@ export async function runAddPage(options) {
|
|
|
48
56
|
mode: effectiveSsg ? 'ssg' : 'standard',
|
|
49
57
|
paths: {
|
|
50
58
|
pages: config.paths.src.pages,
|
|
51
|
-
app: config.paths.src.app
|
|
52
|
-
}
|
|
59
|
+
app: config.paths.src.app,
|
|
60
|
+
},
|
|
53
61
|
});
|
|
54
62
|
if (effectiveSsg) {
|
|
55
63
|
await ensureSsgViewMetadataForPage({
|
|
56
64
|
workspaceRoot: options.workspaceRoot,
|
|
57
|
-
pageName: options.pageName
|
|
65
|
+
pageName: options.pageName,
|
|
58
66
|
});
|
|
59
67
|
}
|
|
60
68
|
console.info('[webstir-frontend] Page scaffold created.');
|
|
@@ -76,9 +84,9 @@ function applySsgPublishLayout(config) {
|
|
|
76
84
|
dist: {
|
|
77
85
|
...config.paths.dist,
|
|
78
86
|
pages: distPages,
|
|
79
|
-
content: distContent
|
|
80
|
-
}
|
|
81
|
-
}
|
|
87
|
+
content: distContent,
|
|
88
|
+
},
|
|
89
|
+
},
|
|
82
90
|
};
|
|
83
91
|
}
|
|
84
92
|
async function readWorkspaceEnableFlags(workspaceRoot) {
|
package/dist/pipeline.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type { EnableFlags, FrontendConfig } from './types.js';
|
|
|
2
2
|
export interface PipelineOptions {
|
|
3
3
|
readonly changedFile?: string;
|
|
4
4
|
readonly enable?: EnableFlags;
|
|
5
|
+
readonly env?: Record<string, string | undefined>;
|
|
5
6
|
}
|
|
6
7
|
export type PipelineMode = 'build' | 'publish';
|
|
7
8
|
export declare function runPipeline(config: FrontendConfig, mode: PipelineMode, options?: PipelineOptions): Promise<void>;
|
package/dist/pipeline.js
CHANGED
|
@@ -2,7 +2,12 @@ import { performance } from 'node:perf_hooks';
|
|
|
2
2
|
import { createBuilders } from './builders/index.js';
|
|
3
3
|
import { createHookContext, executeHooks, loadHooks } from './hooks.js';
|
|
4
4
|
export async function runPipeline(config, mode, options = {}) {
|
|
5
|
-
const context = {
|
|
5
|
+
const context = {
|
|
6
|
+
config,
|
|
7
|
+
changedFile: options.changedFile,
|
|
8
|
+
enable: options.enable,
|
|
9
|
+
env: options.env,
|
|
10
|
+
};
|
|
6
11
|
const builders = createBuilders(context);
|
|
7
12
|
const hooks = await loadHooks(config.paths.workspace, mode === 'build');
|
|
8
13
|
const pipelineContext = createHookContext(config, mode, options.changedFile);
|
package/dist/provider.js
CHANGED
|
@@ -2,11 +2,11 @@ import path from 'node:path';
|
|
|
2
2
|
import { readdir } from 'node:fs/promises';
|
|
3
3
|
import fs from 'node:fs';
|
|
4
4
|
import { createRequire } from 'node:module';
|
|
5
|
-
import { glob } from 'glob';
|
|
6
5
|
import { runPipeline } from './pipeline.js';
|
|
7
6
|
import { prepareWorkspaceConfig } from './config/setup.js';
|
|
8
7
|
import { FOLDERS } from './core/constants.js';
|
|
9
8
|
import { pathExists, readJson, remove } from './utils/fs.js';
|
|
9
|
+
import { scanGlob } from './utils/glob.js';
|
|
10
10
|
import { applySsgRouting, assertNoSsgRoutes, generateSsgViewData } from './modes/ssg/index.js';
|
|
11
11
|
const require = createRequire(import.meta.url);
|
|
12
12
|
const pkg = require('../package.json');
|
|
@@ -14,7 +14,7 @@ function resolveWorkspacePaths(workspaceRoot) {
|
|
|
14
14
|
return {
|
|
15
15
|
sourceRoot: path.join(workspaceRoot, 'src', 'frontend'),
|
|
16
16
|
buildRoot: path.join(workspaceRoot, 'build', 'frontend'),
|
|
17
|
-
testsRoot: path.join(workspaceRoot, 'src', 'frontend', 'tests')
|
|
17
|
+
testsRoot: path.join(workspaceRoot, 'src', 'frontend', 'tests'),
|
|
18
18
|
};
|
|
19
19
|
}
|
|
20
20
|
async function buildModule(options) {
|
|
@@ -22,12 +22,20 @@ async function buildModule(options) {
|
|
|
22
22
|
const mode = normalizeMode(options.env?.WEBSTIR_MODULE_MODE);
|
|
23
23
|
const workspaceMode = await readWorkspaceMode(options.workspaceRoot);
|
|
24
24
|
const frontendMode = normalizeFrontendMode(options.env?.WEBSTIR_FRONTEND_MODE);
|
|
25
|
-
const shouldRunSsgPublish = mode === 'publish' &&
|
|
25
|
+
const shouldRunSsgPublish = mode === 'publish' &&
|
|
26
|
+
(frontendMode === 'ssg' || (frontendMode === undefined && workspaceMode.mode === 'ssg'));
|
|
26
27
|
const publishConfig = shouldRunSsgPublish ? applySsgPublishLayout(config) : config;
|
|
27
28
|
if (shouldRunSsgPublish) {
|
|
28
29
|
await assertNoSsgRoutes(config.paths.workspace);
|
|
29
30
|
}
|
|
30
|
-
await runPipeline(publishConfig, mode, {
|
|
31
|
+
await runPipeline(publishConfig, mode, {
|
|
32
|
+
changedFile: undefined,
|
|
33
|
+
enable: workspaceMode.enable,
|
|
34
|
+
env: {
|
|
35
|
+
...process.env,
|
|
36
|
+
...options.env,
|
|
37
|
+
},
|
|
38
|
+
});
|
|
31
39
|
if (shouldRunSsgPublish) {
|
|
32
40
|
await generateSsgViewData(publishConfig);
|
|
33
41
|
await applySsgRouting(publishConfig);
|
|
@@ -37,7 +45,7 @@ async function buildModule(options) {
|
|
|
37
45
|
const manifest = createManifest(config, artifacts, workspaceMode.mode, workspaceMode.isSsg);
|
|
38
46
|
return {
|
|
39
47
|
artifacts,
|
|
40
|
-
manifest
|
|
48
|
+
manifest,
|
|
41
49
|
};
|
|
42
50
|
}
|
|
43
51
|
function applySsgPublishLayout(config) {
|
|
@@ -51,9 +59,9 @@ function applySsgPublishLayout(config) {
|
|
|
51
59
|
dist: {
|
|
52
60
|
...config.paths.dist,
|
|
53
61
|
pages: distPages,
|
|
54
|
-
content: distContent
|
|
55
|
-
}
|
|
56
|
-
}
|
|
62
|
+
content: distContent,
|
|
63
|
+
},
|
|
64
|
+
},
|
|
57
65
|
};
|
|
58
66
|
}
|
|
59
67
|
async function removeLegacyPagesFolder(config) {
|
|
@@ -81,10 +89,9 @@ async function getScaffoldAssets() {
|
|
|
81
89
|
}
|
|
82
90
|
async function collectArtifacts(config) {
|
|
83
91
|
const buildRoot = config.paths.build.frontend;
|
|
84
|
-
const matches = await
|
|
92
|
+
const matches = await scanGlob('**/*', {
|
|
85
93
|
cwd: buildRoot,
|
|
86
|
-
|
|
87
|
-
dot: false
|
|
94
|
+
dot: false,
|
|
88
95
|
});
|
|
89
96
|
return matches.map((relative) => {
|
|
90
97
|
const absolutePath = path.join(buildRoot, relative);
|
|
@@ -92,7 +99,7 @@ async function collectArtifacts(config) {
|
|
|
92
99
|
const artifactType = ext === '.js' || ext === '.mjs' ? 'bundle' : 'asset';
|
|
93
100
|
return {
|
|
94
101
|
path: absolutePath,
|
|
95
|
-
type: artifactType
|
|
102
|
+
type: artifactType,
|
|
96
103
|
};
|
|
97
104
|
});
|
|
98
105
|
}
|
|
@@ -120,14 +127,14 @@ function createManifest(config, assets, workspaceMode, isSsgWorkspace) {
|
|
|
120
127
|
else if (!isSsg) {
|
|
121
128
|
diagnostics.push({
|
|
122
129
|
severity: 'warn',
|
|
123
|
-
message: 'No JavaScript entry points found under build/frontend.'
|
|
130
|
+
message: 'No JavaScript entry points found under build/frontend.',
|
|
124
131
|
});
|
|
125
132
|
}
|
|
126
133
|
}
|
|
127
134
|
return {
|
|
128
135
|
entryPoints,
|
|
129
136
|
staticAssets,
|
|
130
|
-
diagnostics
|
|
137
|
+
diagnostics,
|
|
131
138
|
};
|
|
132
139
|
}
|
|
133
140
|
async function readWorkspaceMode(workspaceRoot) {
|
|
@@ -136,11 +143,11 @@ async function readWorkspaceMode(workspaceRoot) {
|
|
|
136
143
|
const mode = pkg?.webstir?.mode;
|
|
137
144
|
const normalizedMode = typeof mode === 'string' ? mode.toLowerCase() : undefined;
|
|
138
145
|
const views = pkg?.webstir?.moduleManifest?.views;
|
|
139
|
-
const hasSsgView = Array.isArray(views) && views.some(view => view.renderMode?.toLowerCase() === 'ssg');
|
|
146
|
+
const hasSsgView = Array.isArray(views) && views.some((view) => view.renderMode?.toLowerCase() === 'ssg');
|
|
140
147
|
return {
|
|
141
148
|
mode,
|
|
142
149
|
isSsg: normalizedMode === 'ssg' || hasSsgView,
|
|
143
|
-
enable: pkg?.webstir?.enable
|
|
150
|
+
enable: pkg?.webstir?.enable,
|
|
144
151
|
};
|
|
145
152
|
}
|
|
146
153
|
function normalizeFrontendMode(value) {
|
|
@@ -148,11 +155,7 @@ function normalizeFrontendMode(value) {
|
|
|
148
155
|
return undefined;
|
|
149
156
|
}
|
|
150
157
|
const normalized = value.trim().toLowerCase();
|
|
151
|
-
return normalized === 'ssg'
|
|
152
|
-
? 'ssg'
|
|
153
|
-
: normalized === 'bundle'
|
|
154
|
-
? 'bundle'
|
|
155
|
-
: undefined;
|
|
158
|
+
return normalized === 'ssg' ? 'ssg' : normalized === 'bundle' ? 'bundle' : undefined;
|
|
156
159
|
}
|
|
157
160
|
export const frontendProvider = {
|
|
158
161
|
metadata: {
|
|
@@ -161,8 +164,9 @@ export const frontendProvider = {
|
|
|
161
164
|
version: pkg.version ?? '0.0.0',
|
|
162
165
|
compatibility: {
|
|
163
166
|
minCliVersion: '0.1.0',
|
|
164
|
-
nodeRange: pkg.engines?.node ?? '>=20.18.1'
|
|
165
|
-
|
|
167
|
+
nodeRange: pkg.engines?.node ?? '>=20.18.1',
|
|
168
|
+
...(pkg.engines?.bun ? { notes: `Requires Bun ${pkg.engines.bun} at runtime.` } : {}),
|
|
169
|
+
},
|
|
166
170
|
},
|
|
167
171
|
resolveWorkspace(options) {
|
|
168
172
|
return resolveWorkspacePaths(options.workspaceRoot);
|
|
@@ -172,5 +176,5 @@ export const frontendProvider = {
|
|
|
172
176
|
},
|
|
173
177
|
async getScaffoldAssets() {
|
|
174
178
|
return await getScaffoldAssets();
|
|
175
|
-
}
|
|
179
|
+
},
|
|
176
180
|
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export type CleanupHandler = () => void | Promise<void>;
|
|
2
|
+
export interface CleanupScope {
|
|
3
|
+
add(cleanup: CleanupHandler): void;
|
|
4
|
+
dispose(): Promise<void>;
|
|
5
|
+
}
|
|
6
|
+
export interface ManagedObserver {
|
|
7
|
+
disconnect(): void;
|
|
8
|
+
}
|
|
9
|
+
export interface BoundaryScope extends CleanupScope {
|
|
10
|
+
mountChild<TState>(boundary: Boundary<TState>, root: Element): Promise<Boundary<TState>>;
|
|
11
|
+
}
|
|
12
|
+
export interface BoundarySpec<TState = void> {
|
|
13
|
+
mount(root: Element, scope: BoundaryScope): TState | Promise<TState>;
|
|
14
|
+
unmount?(state: TState, scope: BoundaryScope): void | Promise<void>;
|
|
15
|
+
snapshotState?(state: TState): unknown | Promise<unknown>;
|
|
16
|
+
restoreState?(root: Element, scope: BoundaryScope, state: unknown): TState | Promise<TState>;
|
|
17
|
+
}
|
|
18
|
+
export interface Boundary<TState = void> {
|
|
19
|
+
mount(root: Element): Promise<TState>;
|
|
20
|
+
unmount(): Promise<void>;
|
|
21
|
+
}
|
|
22
|
+
export declare function createCleanupScope(): CleanupScope;
|
|
23
|
+
export declare function listen(scope: CleanupScope, target: EventTarget, type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void;
|
|
24
|
+
export declare function scheduleTimeout(scope: CleanupScope, callback: TimerHandler, delay?: number, ...args: unknown[]): Parameters<typeof clearTimeout>[0];
|
|
25
|
+
export declare function scheduleInterval(scope: CleanupScope, callback: TimerHandler, delay?: number, ...args: unknown[]): Parameters<typeof clearInterval>[0];
|
|
26
|
+
export declare function trackObserver<TObserver extends ManagedObserver>(scope: CleanupScope, observer: TObserver): TObserver;
|
|
27
|
+
export declare function createAbortController(scope: CleanupScope): AbortController;
|
|
28
|
+
export declare function defineBoundary<TState = void>(spec: BoundarySpec<TState>): Boundary<TState>;
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
export function createCleanupScope() {
|
|
2
|
+
const cleanups = [];
|
|
3
|
+
let disposed = false;
|
|
4
|
+
let disposePromise = null;
|
|
5
|
+
return {
|
|
6
|
+
add(cleanup) {
|
|
7
|
+
if (disposed) {
|
|
8
|
+
throw new Error('Cleanup scope has already been disposed.');
|
|
9
|
+
}
|
|
10
|
+
cleanups.push(cleanup);
|
|
11
|
+
},
|
|
12
|
+
dispose() {
|
|
13
|
+
if (disposePromise) {
|
|
14
|
+
return disposePromise;
|
|
15
|
+
}
|
|
16
|
+
disposed = true;
|
|
17
|
+
disposePromise = (async () => {
|
|
18
|
+
let firstError;
|
|
19
|
+
for (let index = cleanups.length - 1; index >= 0; index -= 1) {
|
|
20
|
+
const cleanup = cleanups[index];
|
|
21
|
+
if (!cleanup) {
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
try {
|
|
25
|
+
await cleanup();
|
|
26
|
+
}
|
|
27
|
+
catch (error) {
|
|
28
|
+
if (firstError === undefined) {
|
|
29
|
+
firstError = error;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
cleanups.length = 0;
|
|
34
|
+
if (firstError !== undefined) {
|
|
35
|
+
throw firstError;
|
|
36
|
+
}
|
|
37
|
+
})();
|
|
38
|
+
return disposePromise;
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
export function listen(scope, target, type, listener, options) {
|
|
43
|
+
target.addEventListener(type, listener, options);
|
|
44
|
+
scope.add(() => {
|
|
45
|
+
target.removeEventListener(type, listener, options);
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
export function scheduleTimeout(scope, callback, delay = 0, ...args) {
|
|
49
|
+
const handle = setTimeout(callback, delay, ...args);
|
|
50
|
+
scope.add(() => {
|
|
51
|
+
clearTimeout(handle);
|
|
52
|
+
});
|
|
53
|
+
return handle;
|
|
54
|
+
}
|
|
55
|
+
export function scheduleInterval(scope, callback, delay = 0, ...args) {
|
|
56
|
+
const handle = setInterval(callback, delay, ...args);
|
|
57
|
+
scope.add(() => {
|
|
58
|
+
clearInterval(handle);
|
|
59
|
+
});
|
|
60
|
+
return handle;
|
|
61
|
+
}
|
|
62
|
+
export function trackObserver(scope, observer) {
|
|
63
|
+
scope.add(() => {
|
|
64
|
+
observer.disconnect();
|
|
65
|
+
});
|
|
66
|
+
return observer;
|
|
67
|
+
}
|
|
68
|
+
export function createAbortController(scope) {
|
|
69
|
+
const controller = new AbortController();
|
|
70
|
+
scope.add(() => {
|
|
71
|
+
controller.abort();
|
|
72
|
+
});
|
|
73
|
+
return controller;
|
|
74
|
+
}
|
|
75
|
+
function createBoundaryScope() {
|
|
76
|
+
const scope = createCleanupScope();
|
|
77
|
+
const children = new Set();
|
|
78
|
+
let disposed = false;
|
|
79
|
+
let disposeChildrenPromise = null;
|
|
80
|
+
return {
|
|
81
|
+
...scope,
|
|
82
|
+
async mountChild(boundary, root) {
|
|
83
|
+
if (disposed) {
|
|
84
|
+
throw new Error('Boundary scope has already been disposed.');
|
|
85
|
+
}
|
|
86
|
+
await boundary.mount(root);
|
|
87
|
+
children.delete(boundary);
|
|
88
|
+
children.add(boundary);
|
|
89
|
+
return boundary;
|
|
90
|
+
},
|
|
91
|
+
async disposeChildren() {
|
|
92
|
+
if (disposeChildrenPromise) {
|
|
93
|
+
return disposeChildrenPromise;
|
|
94
|
+
}
|
|
95
|
+
disposed = true;
|
|
96
|
+
disposeChildrenPromise = (async () => {
|
|
97
|
+
let firstError;
|
|
98
|
+
const orderedChildren = Array.from(children);
|
|
99
|
+
for (let index = orderedChildren.length - 1; index >= 0; index -= 1) {
|
|
100
|
+
const child = orderedChildren[index];
|
|
101
|
+
if (!child) {
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
try {
|
|
105
|
+
await child.unmount();
|
|
106
|
+
}
|
|
107
|
+
catch (error) {
|
|
108
|
+
if (firstError === undefined) {
|
|
109
|
+
firstError = error;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
children.clear();
|
|
114
|
+
if (firstError !== undefined) {
|
|
115
|
+
throw firstError;
|
|
116
|
+
}
|
|
117
|
+
})();
|
|
118
|
+
return disposeChildrenPromise;
|
|
119
|
+
},
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
export function defineBoundary(spec) {
|
|
123
|
+
let currentRoot = null;
|
|
124
|
+
let currentScope = null;
|
|
125
|
+
let currentChildScope = null;
|
|
126
|
+
let currentState;
|
|
127
|
+
let pendingHotState;
|
|
128
|
+
let hasPendingHotState = false;
|
|
129
|
+
let mountPromise = null;
|
|
130
|
+
let unmountPromise = null;
|
|
131
|
+
const reset = (preserveHotState = false) => {
|
|
132
|
+
currentRoot = null;
|
|
133
|
+
currentScope = null;
|
|
134
|
+
currentChildScope = null;
|
|
135
|
+
currentState = undefined;
|
|
136
|
+
mountPromise = null;
|
|
137
|
+
unmountPromise = null;
|
|
138
|
+
if (!preserveHotState) {
|
|
139
|
+
pendingHotState = undefined;
|
|
140
|
+
hasPendingHotState = false;
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
return {
|
|
144
|
+
async mount(root) {
|
|
145
|
+
if (currentScope || mountPromise || unmountPromise) {
|
|
146
|
+
throw new Error('Boundary is already mounted.');
|
|
147
|
+
}
|
|
148
|
+
currentRoot = root;
|
|
149
|
+
currentChildScope = createBoundaryScope();
|
|
150
|
+
currentScope = currentChildScope;
|
|
151
|
+
const scope = currentScope;
|
|
152
|
+
const hotState = hasPendingHotState ? pendingHotState : undefined;
|
|
153
|
+
pendingHotState = undefined;
|
|
154
|
+
hasPendingHotState = false;
|
|
155
|
+
mountPromise = (async () => {
|
|
156
|
+
try {
|
|
157
|
+
const state = hotState !== undefined && spec.restoreState
|
|
158
|
+
? await spec.restoreState(root, scope, hotState)
|
|
159
|
+
: await spec.mount(root, scope);
|
|
160
|
+
currentState = state;
|
|
161
|
+
return state;
|
|
162
|
+
}
|
|
163
|
+
catch (error) {
|
|
164
|
+
await currentChildScope?.disposeChildren().catch(() => undefined);
|
|
165
|
+
await scope.dispose().catch(() => undefined);
|
|
166
|
+
reset();
|
|
167
|
+
throw error;
|
|
168
|
+
}
|
|
169
|
+
})();
|
|
170
|
+
return await mountPromise;
|
|
171
|
+
},
|
|
172
|
+
async unmount() {
|
|
173
|
+
if (!currentScope) {
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
if (unmountPromise) {
|
|
177
|
+
return await unmountPromise;
|
|
178
|
+
}
|
|
179
|
+
const scope = currentScope;
|
|
180
|
+
const childScope = currentChildScope;
|
|
181
|
+
const state = currentState;
|
|
182
|
+
const mountTask = mountPromise;
|
|
183
|
+
const root = currentRoot;
|
|
184
|
+
unmountPromise = (async () => {
|
|
185
|
+
let firstError;
|
|
186
|
+
let capturedHotState;
|
|
187
|
+
let hasCapturedHotState = false;
|
|
188
|
+
if (mountTask) {
|
|
189
|
+
try {
|
|
190
|
+
await mountTask;
|
|
191
|
+
}
|
|
192
|
+
catch (error) {
|
|
193
|
+
firstError = error;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
if (!firstError && spec.snapshotState) {
|
|
197
|
+
try {
|
|
198
|
+
capturedHotState = await spec.snapshotState(state);
|
|
199
|
+
hasCapturedHotState = capturedHotState !== undefined;
|
|
200
|
+
}
|
|
201
|
+
catch (error) {
|
|
202
|
+
firstError = error;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
if (childScope) {
|
|
206
|
+
try {
|
|
207
|
+
await childScope.disposeChildren();
|
|
208
|
+
}
|
|
209
|
+
catch (error) {
|
|
210
|
+
if (firstError === undefined) {
|
|
211
|
+
firstError = error;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
if (!firstError && spec.unmount && root) {
|
|
216
|
+
try {
|
|
217
|
+
await spec.unmount(state, scope);
|
|
218
|
+
}
|
|
219
|
+
catch (error) {
|
|
220
|
+
firstError = error;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
try {
|
|
224
|
+
await scope.dispose();
|
|
225
|
+
}
|
|
226
|
+
catch (error) {
|
|
227
|
+
if (firstError === undefined) {
|
|
228
|
+
firstError = error;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
if (firstError === undefined && hasCapturedHotState) {
|
|
232
|
+
pendingHotState = capturedHotState;
|
|
233
|
+
hasPendingHotState = true;
|
|
234
|
+
}
|
|
235
|
+
else {
|
|
236
|
+
pendingHotState = undefined;
|
|
237
|
+
hasPendingHotState = false;
|
|
238
|
+
}
|
|
239
|
+
reset(hasCapturedHotState && firstError === undefined);
|
|
240
|
+
if (firstError !== undefined) {
|
|
241
|
+
throw firstError;
|
|
242
|
+
}
|
|
243
|
+
})();
|
|
244
|
+
return await unmountPromise;
|
|
245
|
+
},
|
|
246
|
+
};
|
|
247
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './boundary.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './boundary.js';
|
package/dist/types.d.ts
CHANGED
|
@@ -52,6 +52,7 @@ export interface FrontendPathConfig {
|
|
|
52
52
|
}
|
|
53
53
|
export interface FrontendFeatureFlags {
|
|
54
54
|
readonly htmlSecurity: boolean;
|
|
55
|
+
readonly externalResourceIntegrity: boolean;
|
|
55
56
|
readonly imageOptimization: boolean;
|
|
56
57
|
readonly precompression: boolean;
|
|
57
58
|
}
|
|
@@ -59,3 +60,54 @@ export interface AddPageCommandOptions extends FrontendCommandOptions {
|
|
|
59
60
|
readonly pageName: string;
|
|
60
61
|
readonly ssg?: boolean;
|
|
61
62
|
}
|
|
63
|
+
export interface FrontendWorkspaceKnownEnableFlags {
|
|
64
|
+
readonly spa: boolean;
|
|
65
|
+
readonly clientNav: boolean;
|
|
66
|
+
readonly backend: boolean;
|
|
67
|
+
readonly search: boolean;
|
|
68
|
+
readonly contentNav: boolean;
|
|
69
|
+
}
|
|
70
|
+
export interface FrontendWorkspaceEnableFlagsInspect {
|
|
71
|
+
readonly raw?: Record<string, unknown>;
|
|
72
|
+
readonly known: FrontendWorkspaceKnownEnableFlags;
|
|
73
|
+
}
|
|
74
|
+
export interface FrontendWorkspacePackageInspect {
|
|
75
|
+
readonly path: string;
|
|
76
|
+
readonly exists: boolean;
|
|
77
|
+
readonly mode?: string;
|
|
78
|
+
readonly enable: FrontendWorkspaceEnableFlagsInspect;
|
|
79
|
+
}
|
|
80
|
+
export interface FrontendWorkspaceAppShellInspect {
|
|
81
|
+
readonly root: string;
|
|
82
|
+
readonly exists: boolean;
|
|
83
|
+
readonly templatePath: string;
|
|
84
|
+
readonly templateExists: boolean;
|
|
85
|
+
readonly stylesheetPath: string;
|
|
86
|
+
readonly stylesheetExists: boolean;
|
|
87
|
+
readonly scriptPath: string;
|
|
88
|
+
readonly scriptExists: boolean;
|
|
89
|
+
}
|
|
90
|
+
export interface FrontendWorkspacePageInspect {
|
|
91
|
+
readonly name: string;
|
|
92
|
+
readonly directory: string;
|
|
93
|
+
readonly htmlPath: string;
|
|
94
|
+
readonly htmlExists: boolean;
|
|
95
|
+
readonly stylesheetPath: string;
|
|
96
|
+
readonly stylesheetExists: boolean;
|
|
97
|
+
readonly scriptPath: string;
|
|
98
|
+
readonly scriptExists: boolean;
|
|
99
|
+
}
|
|
100
|
+
export interface FrontendWorkspaceContentInspect {
|
|
101
|
+
readonly root: string;
|
|
102
|
+
readonly exists: boolean;
|
|
103
|
+
readonly sidebarOverridePath: string;
|
|
104
|
+
readonly sidebarOverrideExists: boolean;
|
|
105
|
+
}
|
|
106
|
+
export interface FrontendWorkspaceInspectResult {
|
|
107
|
+
readonly workspaceRoot: string;
|
|
108
|
+
readonly config: FrontendConfig;
|
|
109
|
+
readonly packageJson: FrontendWorkspacePackageInspect;
|
|
110
|
+
readonly appShell: FrontendWorkspaceAppShellInspect;
|
|
111
|
+
readonly pages: readonly FrontendWorkspacePageInspect[];
|
|
112
|
+
readonly content: FrontendWorkspaceContentInspect;
|
|
113
|
+
}
|