@webstir-io/webstir-frontend 0.1.40 → 0.1.41

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (138) hide show
  1. package/README.md +124 -60
  2. package/dist/assets/imageOptimizer.js +10 -15
  3. package/dist/assets/precompression.js +1 -1
  4. package/dist/builders/contentBuilder.js +102 -90
  5. package/dist/builders/cssBuilder.js +25 -19
  6. package/dist/builders/htmlBuilder.js +57 -42
  7. package/dist/builders/index.js +1 -1
  8. package/dist/builders/jsBuilder.js +219 -76
  9. package/dist/builders/staticAssetsBuilder.js +27 -9
  10. package/dist/builders/types.d.ts +1 -0
  11. package/dist/cli.d.ts +1 -1
  12. package/dist/cli.js +6 -30
  13. package/dist/config/manifest.js +7 -6
  14. package/dist/config/paths.js +2 -2
  15. package/dist/config/schema.d.ts +8 -0
  16. package/dist/config/schema.js +7 -6
  17. package/dist/config/setup.js +1 -1
  18. package/dist/config/workspace.js +11 -9
  19. package/dist/core/constants.d.ts +1 -1
  20. package/dist/core/constants.js +5 -5
  21. package/dist/core/diagnostics.js +1 -1
  22. package/dist/core/pages.js +4 -4
  23. package/dist/hooks.js +3 -3
  24. package/dist/html/criticalCss.js +6 -3
  25. package/dist/html/htmlSecurity.d.ts +6 -1
  26. package/dist/html/htmlSecurity.js +28 -14
  27. package/dist/html/lazyLoad.js +1 -1
  28. package/dist/html/pageScaffold.js +1 -1
  29. package/dist/html/resourceHints.js +5 -2
  30. package/dist/index.d.ts +2 -0
  31. package/dist/index.js +2 -0
  32. package/dist/inspect.d.ts +2 -0
  33. package/dist/inspect.js +110 -0
  34. package/dist/modes/ssg/metadata.js +4 -4
  35. package/dist/modes/ssg/routing.js +2 -5
  36. package/dist/modes/ssg/seo.js +5 -5
  37. package/dist/modes/ssg/views.js +17 -11
  38. package/dist/operations.js +18 -10
  39. package/dist/pipeline.d.ts +1 -0
  40. package/dist/pipeline.js +6 -1
  41. package/dist/provider.js +28 -24
  42. package/dist/runtime/boundary.d.ts +28 -0
  43. package/dist/runtime/boundary.js +247 -0
  44. package/dist/runtime/index.d.ts +1 -0
  45. package/dist/runtime/index.js +1 -0
  46. package/dist/types.d.ts +52 -0
  47. package/dist/utils/fs.d.ts +11 -10
  48. package/dist/utils/fs.js +48 -20
  49. package/dist/utils/glob.d.ts +8 -0
  50. package/dist/utils/glob.js +21 -0
  51. package/dist/utils/hash.js +1 -2
  52. package/dist/utils/pagePaths.js +2 -2
  53. package/package.json +19 -14
  54. package/scripts/publish.sh +2 -94
  55. package/scripts/update-contract.sh +12 -10
  56. package/src/assets/assetManifest.ts +39 -29
  57. package/src/assets/imageOptimizer.ts +91 -82
  58. package/src/assets/precompression.ts +22 -16
  59. package/src/builders/contentBuilder.ts +1224 -1149
  60. package/src/builders/cssBuilder.ts +466 -417
  61. package/src/builders/htmlBuilder.ts +511 -448
  62. package/src/builders/index.ts +7 -7
  63. package/src/builders/jsBuilder.ts +538 -280
  64. package/src/builders/staticAssetsBuilder.ts +166 -135
  65. package/src/builders/types.ts +7 -6
  66. package/src/cli.ts +66 -90
  67. package/src/config/manifest.ts +16 -14
  68. package/src/config/paths.ts +5 -5
  69. package/src/config/schema.ts +38 -37
  70. package/src/config/setup.ts +7 -7
  71. package/src/config/workspace.ts +118 -116
  72. package/src/config/workspaceManifest.ts +14 -14
  73. package/src/core/constants.ts +62 -62
  74. package/src/core/diagnostics.ts +26 -26
  75. package/src/core/pages.ts +19 -19
  76. package/src/hooks.ts +128 -118
  77. package/src/html/criticalCss.ts +84 -77
  78. package/src/html/htmlSecurity.ts +107 -66
  79. package/src/html/lazyLoad.ts +22 -19
  80. package/src/html/pageScaffold.ts +37 -28
  81. package/src/html/resourceHints.ts +83 -74
  82. package/src/index.ts +2 -0
  83. package/src/inspect.ts +158 -0
  84. package/src/modes/ssg/metadata.ts +53 -51
  85. package/src/modes/ssg/routing.ts +177 -177
  86. package/src/modes/ssg/seo.ts +208 -200
  87. package/src/modes/ssg/validation.ts +31 -25
  88. package/src/modes/ssg/views.ts +257 -238
  89. package/src/operations.ts +105 -95
  90. package/src/pipeline.ts +81 -69
  91. package/src/provider.ts +184 -176
  92. package/src/runtime/boundary.ts +325 -0
  93. package/src/runtime/index.ts +1 -0
  94. package/src/types.ts +107 -48
  95. package/src/utils/changedFile.ts +22 -22
  96. package/src/utils/fs.ts +73 -26
  97. package/src/utils/glob.ts +38 -0
  98. package/src/utils/hash.ts +2 -4
  99. package/src/utils/pagePaths.ts +35 -23
  100. package/src/utils/pathMatch.ts +26 -23
  101. package/tests/add-page-defaults.test.js +44 -39
  102. package/tests/bundlerParity.test.js +252 -0
  103. package/tests/cli.contract.test.js +13 -0
  104. package/tests/content-pages.test.js +108 -13
  105. package/tests/css-app-imports.test.js +22 -11
  106. package/tests/css-page-imports.test.js +26 -13
  107. package/tests/diagnostics.test.js +39 -36
  108. package/tests/features.test.js +48 -43
  109. package/tests/hooks.test.js +58 -42
  110. package/tests/htmlSecurity.test.js +66 -0
  111. package/tests/inspect.test.js +148 -0
  112. package/tests/provider.integration.test.js +71 -20
  113. package/tests/runtime.test.js +493 -0
  114. package/tests/ssg-defaults.test.js +284 -177
  115. package/tests/ssg-guardrails.test.js +51 -51
  116. package/tsconfig.json +3 -10
  117. package/dist/watch/frontendFiles.d.ts +0 -3
  118. package/dist/watch/frontendFiles.js +0 -25
  119. package/dist/watch/hotUpdateTracker.d.ts +0 -51
  120. package/dist/watch/hotUpdateTracker.js +0 -205
  121. package/dist/watch/pipelineHelpers.d.ts +0 -26
  122. package/dist/watch/pipelineHelpers.js +0 -177
  123. package/dist/watch/types.d.ts +0 -27
  124. package/dist/watch/types.js +0 -1
  125. package/dist/watch/watchCoordinator.d.ts +0 -36
  126. package/dist/watch/watchCoordinator.js +0 -551
  127. package/dist/watch/watchDaemon.d.ts +0 -17
  128. package/dist/watch/watchDaemon.js +0 -127
  129. package/dist/watch/watchReporter.d.ts +0 -21
  130. package/dist/watch/watchReporter.js +0 -64
  131. package/scripts/smoke.mjs +0 -35
  132. package/src/watch/frontendFiles.ts +0 -32
  133. package/src/watch/hotUpdateTracker.ts +0 -285
  134. package/src/watch/pipelineHelpers.ts +0 -242
  135. package/src/watch/types.ts +0 -23
  136. package/src/watch/watchCoordinator.ts +0 -666
  137. package/src/watch/watchDaemon.ts +0 -144
  138. package/src/watch/watchReporter.ts +0 -98
@@ -42,17 +42,17 @@ export async function generateSsgViewData(config) {
42
42
  try {
43
43
  data = await spec.load(ssrContext);
44
44
  }
45
- catch {
46
- // Best-effort only; skip paths that fail to load.
47
- continue;
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
- // Best-effort only.
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) {
@@ -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', { changedFile: options.changedFile, enable });
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', { changedFile: options.changedFile, enable });
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) {
@@ -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 = { config, changedFile: options.changedFile, enable: options.enable };
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' && (frontendMode === 'ssg' || (frontendMode === undefined && workspaceMode.mode === 'ssg'));
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, { changedFile: undefined, enable: workspaceMode.enable });
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 glob('**/*', {
92
+ const matches = await scanGlob('**/*', {
85
93
  cwd: buildRoot,
86
- nodir: true,
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
+ }