@zenithbuild/cli 0.7.10 → 0.7.12

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 (111) hide show
  1. package/README.md +14 -2
  2. package/dist/adapters/adapter-netlify-static.d.ts +2 -5
  3. package/dist/adapters/adapter-netlify.d.ts +2 -5
  4. package/dist/adapters/adapter-netlify.js +22 -5
  5. package/dist/adapters/adapter-types.d.ts +32 -13
  6. package/dist/adapters/adapter-types.js +0 -59
  7. package/dist/adapters/adapter-vercel-static.d.ts +2 -5
  8. package/dist/adapters/adapter-vercel.d.ts +2 -5
  9. package/dist/adapters/adapter-vercel.js +21 -6
  10. package/dist/adapters/copy-hosted-page-runtime.d.ts +2 -1
  11. package/dist/adapters/copy-hosted-page-runtime.js +68 -3
  12. package/dist/adapters/resolve-adapter.d.ts +6 -4
  13. package/dist/build/compiler-runtime.js +3 -0
  14. package/dist/build/expression-rewrites.d.ts +3 -1
  15. package/dist/build/expression-rewrites.js +14 -2
  16. package/dist/build/page-component-loop.d.ts +1 -0
  17. package/dist/build/page-component-loop.js +66 -6
  18. package/dist/build/page-ir-normalization.js +7 -0
  19. package/dist/build/page-loop-state.d.ts +2 -4
  20. package/dist/build/page-loop-state.js +17 -9
  21. package/dist/build/page-loop.js +18 -8
  22. package/dist/build/scoped-expression-context.d.ts +5 -0
  23. package/dist/build/scoped-expression-context.js +133 -0
  24. package/dist/build/server-script.js +13 -36
  25. package/dist/build/type-declarations.d.ts +2 -1
  26. package/dist/build/type-declarations.js +29 -52
  27. package/dist/build-output-manifest.d.ts +10 -6
  28. package/dist/build-output-manifest.js +4 -1
  29. package/dist/build.js +11 -2
  30. package/dist/component-instance-ir.js +1 -0
  31. package/dist/component-occurrences.d.ts +9 -0
  32. package/dist/component-occurrences.js +18 -0
  33. package/dist/config-plugins.d.ts +12 -0
  34. package/dist/config-plugins.js +100 -0
  35. package/dist/config.d.ts +1 -0
  36. package/dist/config.js +56 -5
  37. package/dist/dev-build-session/helpers.js +27 -7
  38. package/dist/dev-build-session/session.js +19 -10
  39. package/dist/dev-server/build-error-response.d.ts +21 -0
  40. package/dist/dev-server/build-error-response.js +48 -0
  41. package/dist/dev-server/port-fallback.d.ts +15 -0
  42. package/dist/dev-server/port-fallback.js +61 -0
  43. package/dist/dev-server/request-handler.js +58 -5
  44. package/dist/dev-server/watcher.js +15 -0
  45. package/dist/dev-server.d.ts +5 -2
  46. package/dist/dev-server.js +129 -49
  47. package/dist/global-middleware-runtime-source.d.ts +15 -0
  48. package/dist/global-middleware-runtime-source.js +62 -0
  49. package/dist/global-middleware.d.ts +13 -0
  50. package/dist/global-middleware.js +252 -0
  51. package/dist/images/remote-fetch.d.ts +12 -0
  52. package/dist/images/remote-fetch.js +257 -0
  53. package/dist/images/service.d.ts +10 -0
  54. package/dist/images/service.js +9 -46
  55. package/dist/index.js +12 -2
  56. package/dist/manifest.d.ts +9 -1
  57. package/dist/manifest.js +70 -25
  58. package/dist/preview/request-handler.js +78 -5
  59. package/dist/preview/server-runner.d.ts +7 -2
  60. package/dist/preview/server-runner.js +19 -6
  61. package/dist/preview/server-script-runner-template.js +97 -29
  62. package/dist/resource-response.js +25 -8
  63. package/dist/resource-route-module.js +5 -22
  64. package/dist/route-classification.d.ts +11 -0
  65. package/dist/route-classification.js +21 -0
  66. package/dist/route-handler-export-analysis.d.ts +22 -0
  67. package/dist/route-handler-export-analysis.js +41 -0
  68. package/dist/scoped-server-data/analyze-owner-file.d.ts +3 -0
  69. package/dist/scoped-server-data/analyze-owner-file.js +149 -0
  70. package/dist/scoped-server-data/diagnostics.d.ts +18 -0
  71. package/dist/scoped-server-data/diagnostics.js +32 -0
  72. package/dist/scoped-server-data/lowering.d.ts +27 -0
  73. package/dist/scoped-server-data/lowering.js +242 -0
  74. package/dist/scoped-server-data/manifest-integration.d.ts +4 -0
  75. package/dist/scoped-server-data/manifest-integration.js +125 -0
  76. package/dist/scoped-server-data/owner-scanner.d.ts +6 -0
  77. package/dist/scoped-server-data/owner-scanner.js +55 -0
  78. package/dist/scoped-server-data/parse-owner-server-block.d.ts +12 -0
  79. package/dist/scoped-server-data/parse-owner-server-block.js +35 -0
  80. package/dist/scoped-server-data/runtime.d.ts +24 -0
  81. package/dist/scoped-server-data/runtime.js +121 -0
  82. package/dist/scoped-server-data/serialization-set.d.ts +2 -0
  83. package/dist/scoped-server-data/serialization-set.js +52 -0
  84. package/dist/scoped-server-data/static-props.d.ts +12 -0
  85. package/dist/scoped-server-data/static-props.js +307 -0
  86. package/dist/scoped-server-data/type-declarations.d.ts +10 -0
  87. package/dist/scoped-server-data/type-declarations.js +368 -0
  88. package/dist/scoped-server-data/types.d.ts +74 -0
  89. package/dist/scoped-server-data/types.js +1 -0
  90. package/dist/server-contract/auth-control-flow.d.ts +1 -0
  91. package/dist/server-contract/auth-control-flow.js +10 -0
  92. package/dist/server-contract/resolve.d.ts +19 -0
  93. package/dist/server-contract/resolve.js +85 -13
  94. package/dist/server-contract/resolved-envelope.d.ts +9 -0
  95. package/dist/server-contract/resolved-envelope.js +14 -0
  96. package/dist/server-contract/stage.js +1 -10
  97. package/dist/server-module-output.d.ts +9 -0
  98. package/dist/server-module-output.js +250 -0
  99. package/dist/server-output.d.ts +7 -1
  100. package/dist/server-output.js +144 -195
  101. package/dist/server-route-names.d.ts +2 -0
  102. package/dist/server-route-names.js +38 -0
  103. package/dist/server-runtime/matched-route-pipeline.d.ts +1 -0
  104. package/dist/server-runtime/matched-route-pipeline.js +1 -0
  105. package/dist/server-runtime/node-server.js +26 -3
  106. package/dist/server-runtime/route-render.d.ts +12 -3
  107. package/dist/server-runtime/route-render.js +67 -13
  108. package/dist/types/generate-env-dts.js +2 -44
  109. package/dist/types/zenith-env-dts.d.ts +4 -0
  110. package/dist/types/zenith-env-dts.js +96 -0
  111. package/package.json +3 -6
@@ -4,6 +4,7 @@ import { cloneComponentIrForInstance } from '../component-instance-ir.js';
4
4
  import { renderPropsLiteralFromAttrs } from './scoped-identifier-rewrite.js';
5
5
  import { extractTemplate, isDocumentMode } from '../resolve-components.js';
6
6
  import { buildComponentExpressionRewrite, mergeExpressionRewriteMaps, resolveStateKeyFromBindings } from './expression-rewrites.js';
7
+ import { applyScopedDataContextToExpressionRewrite, resolveScopedExpressionContext } from './scoped-expression-context.js';
7
8
  import { mergeComponentIr } from './merge-component-ir.js';
8
9
  import { stripStyleBlocks } from './compiler-runtime.js';
9
10
  import { extractDeclaredIdentifiers } from './typescript-expression-utils.js';
@@ -17,6 +18,55 @@ function createEmptyExpressionRewrite() {
17
18
  sequence: []
18
19
  };
19
20
  }
21
+ const SERVER_CONST_RE = /(?:^|\n)\s*const\s+([A-Za-z_$][\w$]*)\s*=/g;
22
+ function stripOwnerServerBlocks(source) {
23
+ return String(source || '').replace(/<script\b([^>]*)>([\s\S]*?)<\/script>/gi, (full, attrs) => {
24
+ if (/\bserver\b/i.test(String(attrs || ''))) {
25
+ return '';
26
+ }
27
+ return full;
28
+ });
29
+ }
30
+ function collectServerConstNames(source) {
31
+ const names = [];
32
+ for (const match of String(source || '').matchAll(SERVER_CONST_RE)) {
33
+ const name = String(match[1] || '');
34
+ if (name && !names.includes(name)) {
35
+ names.push(name);
36
+ }
37
+ }
38
+ return names;
39
+ }
40
+ function createClientPlaceholderPrelude(source, strippedSource) {
41
+ const names = [];
42
+ String(source || '').replace(/<script\b([^>]*)>([\s\S]*?)<\/script>/gi, (full, attrs, body) => {
43
+ if (/\bserver\b/i.test(String(attrs || ''))) {
44
+ for (const name of collectServerConstNames(body)) {
45
+ const refRe = new RegExp(`\\b${escapeRegExp(name)}\\b`);
46
+ if (refRe.test(strippedSource) && !names.includes(name)) {
47
+ names.push(name);
48
+ }
49
+ }
50
+ }
51
+ return full;
52
+ });
53
+ if (names.length === 0) {
54
+ return '';
55
+ }
56
+ return [
57
+ '<script lang="ts">',
58
+ ...names.map((name) => `const ${name} = undefined;`),
59
+ '</script>',
60
+ ''
61
+ ].join('\n');
62
+ }
63
+ function prepareOwnerClientCompileSource(source) {
64
+ const strippedSource = stripOwnerServerBlocks(source);
65
+ return stripStyleBlocks(`${createClientPlaceholderPrelude(source, strippedSource)}${strippedSource}`);
66
+ }
67
+ function escapeRegExp(value) {
68
+ return String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
69
+ }
20
70
  async function resolveComponentIr({ compPath, componentSource, compilerOpts, compilerBin, timedRunCompiler, cooperativeYield, componentIrCache, compilerTotals, pageStats, startupProfile, emitCompilerWarning }) {
21
71
  let compIr;
22
72
  if (componentIrCache.has(compPath)) {
@@ -27,7 +77,7 @@ async function resolveComponentIr({ compPath, componentSource, compilerOpts, com
27
77
  else {
28
78
  await cooperativeYield();
29
79
  const componentCompileStartedAt = performance.now();
30
- compIr = timedRunCompiler('component', compPath, stripStyleBlocks(componentSource), compilerOpts, { compilerToolchain: compilerBin, onWarning: emitCompilerWarning });
80
+ compIr = timedRunCompiler('component', compPath, prepareOwnerClientCompileSource(componentSource), compilerOpts, { compilerToolchain: compilerBin, onWarning: emitCompilerWarning });
31
81
  pageStats.pageComponentCompileMs += startupProfile.roundMs(performance.now() - componentCompileStartedAt);
32
82
  compilerTotals.componentCacheMisses += 1;
33
83
  pageStats.pageComponentCacheMisses += 1;
@@ -72,7 +122,7 @@ async function resolveOwnerRewriteContext({ occurrence, sourceFile, compilerOpts
72
122
  pageComponentLoopBreakdown.ownerSourceReadMs += startupProfile.roundMs(performance.now() - ownerSourceReadStartedAt);
73
123
  await cooperativeYield();
74
124
  const ownerCompileStartedAt = performance.now();
75
- ownerIr = timedRunCompiler('component', ownerPath, stripStyleBlocks(ownerSource), compilerOpts, { compilerToolchain: compilerBin, onWarning: emitCompilerWarning });
125
+ ownerIr = timedRunCompiler('component', ownerPath, prepareOwnerClientCompileSource(ownerSource), compilerOpts, { compilerToolchain: compilerBin, onWarning: emitCompilerWarning });
76
126
  pageStats.pageComponentCompileMs += startupProfile.roundMs(performance.now() - ownerCompileStartedAt);
77
127
  compilerTotals.componentCacheMisses += 1;
78
128
  pageStats.pageComponentCacheMisses += 1;
@@ -136,6 +186,7 @@ export async function buildPageOwnerContext({ componentOccurrences, sourceFile,
136
186
  }
137
187
  export async function runPageComponentLoop({ componentOccurrences, occurrenceCountByPath, sourceFile, registry, compilerOpts, compilerBin, timedRunCompiler, cooperativeYield, startupProfile, compilerTotals, emitCompilerWarning, componentIrCache, componentDocumentModeCache, componentExpressionRewriteCache, expressionRewriteMetrics, pageOwnerExpressionRewrite, pageIr, pageIrMergeCache, seenStaticImports, pageExpressionRewriteMap, pageExpressionBindingMap, pageAmbiguousExpressionMap, knownRefKeys, componentOccurrencePlans, imagePropsLiterals, pagePhase, pageBindingResolutionBreakdown, pageMergeBreakdown, pageComponentLoopBreakdown, hoistedCodeTransformCache, pageStats }) {
138
188
  let componentInstanceCounter = 0;
189
+ const scopedOccurrenceIndexByOwnerKey = new Map();
139
190
  for (const occurrence of componentOccurrences) {
140
191
  await cooperativeYield();
141
192
  const compName = occurrence.name;
@@ -146,7 +197,9 @@ export async function runPageComponentLoop({ componentOccurrences, occurrenceCou
146
197
  const componentSourceReadStartedAt = performance.now();
147
198
  const componentSource = readFileSync(compPath, 'utf8');
148
199
  pageComponentLoopBreakdown.componentSourceReadMs += startupProfile.roundMs(performance.now() - componentSourceReadStartedAt);
149
- const occurrenceCount = occurrenceCountByPath.get(compPath) || 0;
200
+ const occurrenceCount = occurrenceCountByPath.get(compPath)
201
+ || occurrenceCountByPath.get(compName)
202
+ || 0;
150
203
  const compIr = await resolveComponentIr({
151
204
  compPath,
152
205
  componentSource,
@@ -175,6 +228,10 @@ export async function runPageComponentLoop({ componentOccurrences, occurrenceCou
175
228
  pageComponentLoopBreakdown,
176
229
  startupProfile
177
230
  });
231
+ const scopedContext = resolveScopedExpressionContext(pageIr, compPath, scopedOccurrenceIndexByOwnerKey);
232
+ const scopedExpressionRewrite = scopedContext
233
+ ? applyScopedDataContextToExpressionRewrite(expressionRewrite, scopedContext)
234
+ : expressionRewrite;
178
235
  const { attrExpressionRewrite } = await resolveOwnerRewriteContext({
179
236
  occurrence,
180
237
  sourceFile,
@@ -220,13 +277,16 @@ export async function runPageComponentLoop({ componentOccurrences, occurrenceCou
220
277
  }, seenStaticImports, knownRefKeys, pageIrMergeCache, pageMergeBreakdown, hoistedCodeTransformCache);
221
278
  pagePhase.mergeMs += startupProfile.roundMs(performance.now() - mergeStartedAt);
222
279
  if (useIsolatedInstance) {
280
+ const scopedInstanceRewrite = scopedContext
281
+ ? applyScopedDataContextToExpressionRewrite(instanceState.instanceRewrite, scopedContext)
282
+ : instanceState.instanceRewrite;
223
283
  componentOccurrencePlans.push({
224
- rewrite: instanceState.instanceRewrite,
225
- expressionSequence: instanceState.instanceRewrite.sequence,
284
+ rewrite: scopedInstanceRewrite,
285
+ expressionSequence: scopedInstanceRewrite.sequence,
226
286
  refSequence: instanceState.refIdentifierPairs
227
287
  });
228
288
  continue;
229
289
  }
230
- mergeExpressionRewriteMaps(pageExpressionRewriteMap, pageExpressionBindingMap, pageAmbiguousExpressionMap, expressionRewrite, pageIrMergeCache, pageBindingResolutionBreakdown);
290
+ mergeExpressionRewriteMaps(pageExpressionRewriteMap, pageExpressionBindingMap, pageAmbiguousExpressionMap, scopedExpressionRewrite, pageIrMergeCache, pageBindingResolutionBreakdown);
231
291
  }
232
292
  }
@@ -50,6 +50,12 @@ export function applyExpressionRewrites(pageIr, expressionMap, bindingMap, ambig
50
50
  if (rewritten && rewritten !== current && bindings[index].literal === current) {
51
51
  bindings[index].literal = rewritten;
52
52
  }
53
+ if (rewrittenBinding &&
54
+ typeof bindings[index].scoped_data_key === 'string' &&
55
+ bindings[index].scoped_data_key.length > 0 &&
56
+ !(typeof rewrittenBinding.scoped_data_key === 'string' && rewrittenBinding.scoped_data_key.length > 0)) {
57
+ continue;
58
+ }
53
59
  if (rewrittenBinding) {
54
60
  bindings[index].compiled_expr = rewrittenBinding.compiled_expr;
55
61
  bindings[index].signal_index = rewrittenBinding.signal_index;
@@ -57,6 +63,7 @@ export function applyExpressionRewrites(pageIr, expressionMap, bindingMap, ambig
57
63
  bindings[index].state_index = rewrittenBinding.state_index;
58
64
  bindings[index].component_instance = rewrittenBinding.component_instance;
59
65
  bindings[index].component_binding = rewrittenBinding.component_binding;
66
+ bindings[index].scoped_data_key = rewrittenBinding.scoped_data_key;
60
67
  }
61
68
  else if (rewritten && rewritten !== current && bindings[index].compiled_expr === current) {
62
69
  bindings[index].compiled_expr = rewritten;
@@ -169,15 +169,13 @@ export function createPageLoopExecutionState(): {
169
169
  envelopes: never[];
170
170
  };
171
171
  export function preparePageIrForMerge(pageIr: any): void;
172
- export function applyServerEnvelopeToPageIr({ pageIr, composedServer, hasGuard, hasLoad, hasAction, entry, srcDir, sourceFile }: {
172
+ export function applyServerEnvelopeToPageIr({ pageIr, composedServer, entry, srcDir, sourceFile, scopedMetadata }: {
173
173
  pageIr: any;
174
174
  composedServer: any;
175
- hasGuard: any;
176
- hasLoad: any;
177
- hasAction: any;
178
175
  entry: any;
179
176
  srcDir: any;
180
177
  sourceFile: any;
178
+ scopedMetadata: any;
181
179
  }): void;
182
180
  export function buildOccurrenceCountByPath(componentOccurrences: any): Map<any, any>;
183
181
  export function createPageBuildState(): {
@@ -1,4 +1,6 @@
1
1
  import { relative } from 'node:path';
2
+ import { classifyPageRoute } from '../route-classification.js';
3
+ import { assertNoScopedServerBuildErrors } from '../manifest.js';
2
4
  import { createBindingResolutionTotals, createComponentLoopPhaseTotals, createMergePhaseTotals, createOccurrenceApplyPhaseTotals, createPagePhaseTotals, createScopedRewritePhaseTotals } from './page-loop-metrics.js';
3
5
  export function createPageLoopState() {
4
6
  return {
@@ -46,22 +48,28 @@ export function preparePageIrForMerge(pageIr) {
46
48
  pageIr.hoisted.state = pageIr.hoisted.state || [];
47
49
  pageIr.hoisted.code = pageIr.hoisted.code || [];
48
50
  }
49
- export function applyServerEnvelopeToPageIr({ pageIr, composedServer, hasGuard, hasLoad, hasAction, entry, srcDir, sourceFile }) {
51
+ export function applyServerEnvelopeToPageIr({ pageIr, composedServer, entry, srcDir, sourceFile, scopedMetadata }) {
52
+ assertNoScopedServerBuildErrors(scopedMetadata.diagnostics, entry.file);
53
+ const classification = classifyPageRoute({
54
+ file: entry.file,
55
+ serverScript: composedServer.serverScript,
56
+ hasScopedServerData: scopedMetadata.hasScopedServerData
57
+ });
50
58
  if (composedServer.serverScript) {
51
59
  const { has_action: _unusedHasAction, export_paths: _unusedExportPaths, ...serverScript } = composedServer.serverScript;
52
60
  pageIr.server_script = serverScript;
53
- pageIr.prerender = composedServer.serverScript.prerender === true;
61
+ pageIr.prerender = classification.prerender;
54
62
  if (pageIr.ssr_data === undefined) {
55
63
  pageIr.ssr_data = null;
56
64
  }
57
65
  }
58
- if (pageIr.prerender === true && (hasGuard || hasLoad || hasAction)) {
59
- throw new Error(`[zenith] Build failed for ${entry.file}: protected routes require SSR/runtime. ` +
60
- 'Cannot prerender a static route with a `guard`, `load`, or `action` function.');
61
- }
62
- pageIr.has_guard = hasGuard;
63
- pageIr.has_load = hasLoad;
64
- pageIr.has_action = hasAction;
66
+ pageIr.has_guard = classification.hasGuard;
67
+ pageIr.has_load = classification.hasLoad;
68
+ pageIr.has_action = classification.hasAction;
69
+ pageIr.has_scoped_server_data = scopedMetadata.hasScopedServerData;
70
+ pageIr.scoped_server_data = scopedMetadata.hasScopedServerData
71
+ ? scopedMetadata.scopedServerData
72
+ : [];
65
73
  pageIr.guard_module_ref = composedServer.guardPath ? relative(srcDir, composedServer.guardPath).replaceAll('\\', '/') : null;
66
74
  pageIr.load_module_ref = composedServer.loadPath ? relative(srcDir, composedServer.loadPath).replaceAll('\\', '/') : null;
67
75
  pageIr.action_module_ref = composedServer.actionPath ? relative(srcDir, composedServer.actionPath).replaceAll('\\', '/') : null;
@@ -14,6 +14,7 @@ import { deferComponentRuntimeBlock } from './hoisted-code-transforms.js';
14
14
  import { addBreakdown, emitPageLoopSummary, recordPageProfile } from './page-loop-metrics.js';
15
15
  import { applyServerEnvelopeToPageIr, buildOccurrenceCountByPath, createPageBuildState, createPageLoopCaches, createPageLoopExecutionState } from './page-loop-state.js';
16
16
  import { extractServerScript } from './server-script.js';
17
+ import { analyzeRouteScopedServerMetadata } from '../manifest.js';
17
18
  /**
18
19
  * @param {{
19
20
  * manifest: Array<{ path: string, file: string }>
@@ -56,13 +57,20 @@ export async function buildPageEnvelopes(input) {
56
57
  const expandedStartedAt = performance.now();
57
58
  const { expandedSource } = expandComponents(rawSource, registry, sourceFile);
58
59
  pageExpandMs = startupProfile.roundMs(performance.now() - expandedStartedAt);
60
+ const usesInternalExpandedSource = expandedSource !== rawSource;
61
+ if (usesInternalExpandedSource) {
62
+ const rawServerExtracted = extractServerScript(rawSource, sourceFile, compilerOpts);
63
+ timedRunCompiler('page', sourceFile, rawServerExtracted.source, compilerOpts, { compilerToolchain: compilerBin, onWarning: emitCompilerWarning });
64
+ }
59
65
  const expandedServerExtractStartedAt = performance.now();
60
66
  const extractedServer = extractServerScript(expandedSource, sourceFile, compilerOpts);
61
67
  pagePhase.serverExtractMs += startupProfile.roundMs(performance.now() - expandedServerExtractStartedAt);
62
68
  const compileSource = extractedServer.source;
63
69
  await cooperativeYield();
64
70
  const pageCompileStartedAt = performance.now();
65
- let pageIr = timedRunCompiler('page', sourceFile, compileSource, compilerOpts, { compilerToolchain: compilerBin, onWarning: emitCompilerWarning });
71
+ let pageIr = timedRunCompiler('page', sourceFile, compileSource, usesInternalExpandedSource
72
+ ? { ...compilerOpts, internalAllowUnboundMarkup: true }
73
+ : compilerOpts, { compilerToolchain: compilerBin, onWarning: emitCompilerWarning });
66
74
  pageCompileMs = startupProfile.roundMs(performance.now() - pageCompileStartedAt);
67
75
  const composedServer = composeServerScriptEnvelope({
68
76
  sourceFile,
@@ -71,18 +79,20 @@ export async function buildPageEnvelopes(input) {
71
79
  adjacentLoadPath: adjacentLoad,
72
80
  adjacentActionPath: adjacentAction
73
81
  });
74
- const hasGuard = composedServer.serverScript?.has_guard === true;
75
- const hasLoad = composedServer.serverScript?.has_load === true;
76
- const hasAction = composedServer.serverScript?.has_action === true;
82
+ const scopedMetadata = analyzeRouteScopedServerMetadata({
83
+ pageSource: rawSource,
84
+ pageFile: sourceFile,
85
+ registry,
86
+ srcDir,
87
+ compilerOpts
88
+ });
77
89
  applyServerEnvelopeToPageIr({
78
90
  pageIr,
79
91
  composedServer,
80
- hasGuard,
81
- hasLoad,
82
- hasAction,
83
92
  entry,
84
93
  srcDir,
85
- sourceFile
94
+ sourceFile,
95
+ scopedMetadata
86
96
  });
87
97
  const pageIrMergeCache = createPageIrMergeCache(pageIr);
88
98
  const seenStaticImports = new Set();
@@ -0,0 +1,5 @@
1
+ export function applyScopedDataContextToExpressionRewrite(componentRewrite: any, scopedContext: any): any;
2
+ export function resolveScopedExpressionContext(pageIr: any, compPath: any, occurrenceIndexByOwnerKey: any): {
3
+ scopedDataKey: any;
4
+ serializedVariableNames: any;
5
+ } | null;
@@ -0,0 +1,133 @@
1
+ function escapeRegExp(value) {
2
+ return String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
3
+ }
4
+ function scopedDataKey(scopedContext) {
5
+ return typeof scopedContext?.scopedDataKey === 'string' && scopedContext.scopedDataKey.length > 0
6
+ ? scopedContext.scopedDataKey
7
+ : null;
8
+ }
9
+ function scopedDataExpressionForSource(expression, scopedContext) {
10
+ const match = String(expression || '').trim().match(/^([A-Za-z_$][\w$]*)([\s\S]*)$/);
11
+ if (!match)
12
+ return null;
13
+ const root = match[1];
14
+ const rest = match[2] || '';
15
+ if (root === 'data' || root === 'ssr')
16
+ return `${root}${rest}`;
17
+ for (const name of Array.isArray(scopedContext?.serializedVariableNames) ? scopedContext.serializedVariableNames : []) {
18
+ if (typeof name === 'string' &&
19
+ name.length > 0 &&
20
+ (root === name || new RegExp(`(?:^|_)${escapeRegExp(name)}(?:__inst\\d+)?$`).test(root))) {
21
+ return `data.${name}${rest}`;
22
+ }
23
+ }
24
+ return null;
25
+ }
26
+ function scopedExpressionCandidate(raw, rewritten, binding, scopedContext) {
27
+ if (!scopedDataKey(scopedContext))
28
+ return null;
29
+ for (const candidate of [rewritten, binding?.compiled_expr, binding?.literal, raw]) {
30
+ const scopedExpression = scopedDataExpressionForSource(candidate, scopedContext);
31
+ if (scopedExpression)
32
+ return scopedExpression;
33
+ }
34
+ return null;
35
+ }
36
+ function createScopedBinding(scopedExpression, scopedContext, baseBinding = null) {
37
+ return scopedExpression
38
+ ? {
39
+ ...(baseBinding && typeof baseBinding === 'object' ? baseBinding : {}),
40
+ compiled_expr: scopedExpression,
41
+ literal: scopedExpression,
42
+ signal_index: null,
43
+ signal_indices: [],
44
+ state_index: null,
45
+ component_instance: null,
46
+ component_binding: null,
47
+ scoped_data_key: scopedDataKey(scopedContext)
48
+ }
49
+ : null;
50
+ }
51
+ function applyScopedContextToBinding(binding, scopedContext, fallbackExpression = null) {
52
+ if (!binding || typeof binding !== 'object' || !scopedDataKey(scopedContext))
53
+ return binding;
54
+ const scopedExpression = scopedDataExpressionForSource(binding.compiled_expr, scopedContext)
55
+ || scopedDataExpressionForSource(binding.literal, scopedContext)
56
+ || scopedDataExpressionForSource(fallbackExpression, scopedContext);
57
+ return scopedExpression ? createScopedBinding(scopedExpression, scopedContext, binding) : binding;
58
+ }
59
+ function hasScopedBinding(binding) {
60
+ return typeof binding?.scoped_data_key === 'string' && binding.scoped_data_key.length > 0;
61
+ }
62
+ export function applyScopedDataContextToExpressionRewrite(componentRewrite, scopedContext) {
63
+ if (!componentRewrite || !scopedDataKey(scopedContext))
64
+ return componentRewrite;
65
+ const next = {
66
+ map: new Map(),
67
+ bindings: new Map(),
68
+ signals: componentRewrite.signals,
69
+ stateBindings: componentRewrite.stateBindings,
70
+ ambiguous: new Set(componentRewrite.ambiguous),
71
+ sequence: []
72
+ };
73
+ for (const item of Array.isArray(componentRewrite.sequence) ? componentRewrite.sequence : []) {
74
+ const scopedBinding = applyScopedContextToBinding(item.binding, scopedContext, item.rewritten || item.raw);
75
+ const scopedExpression = scopedExpressionCandidate(item.raw, item.rewritten, scopedBinding, scopedContext);
76
+ next.sequence.push({
77
+ raw: item.raw,
78
+ rewritten: scopedExpression || item.rewritten,
79
+ binding: hasScopedBinding(scopedBinding)
80
+ ? scopedBinding
81
+ : createScopedBinding(scopedExpression, scopedContext)
82
+ });
83
+ }
84
+ for (const [raw, binding] of componentRewrite.bindings.entries()) {
85
+ next.bindings.set(raw, applyScopedContextToBinding(binding, scopedContext, raw));
86
+ }
87
+ for (const [raw, rewritten] of componentRewrite.map.entries()) {
88
+ const binding = next.bindings.get(raw);
89
+ next.map.set(raw, scopedExpressionCandidate(raw, rewritten, binding, scopedContext) || rewritten);
90
+ }
91
+ return next;
92
+ }
93
+ function normalizeSourcePath(value) {
94
+ return String(value || '').replaceAll('\\', '/');
95
+ }
96
+ function ownerKeyMatchesPath(ownerKey, filePath) {
97
+ const key = normalizeSourcePath(ownerKey);
98
+ const file = normalizeSourcePath(filePath);
99
+ return key.length > 0 && (file === key || file.endsWith(`/${key}`));
100
+ }
101
+ export function resolveScopedExpressionContext(pageIr, compPath, occurrenceIndexByOwnerKey) {
102
+ const entries = Array.isArray(pageIr?.scoped_server_data) ? pageIr.scoped_server_data : [];
103
+ const matches = entries.filter((entry) => entry &&
104
+ typeof entry === 'object' &&
105
+ (entry.ownerKind === 'layout' || entry.ownerKind === 'component') &&
106
+ ownerKeyMatchesPath(entry.ownerKey, compPath));
107
+ const entry = matches.length === 1 ? matches[0] : null;
108
+ if (!entry || typeof entry.ownerKey !== 'string')
109
+ return null;
110
+ let scopedDataKey = null;
111
+ if (entry.ownerKind === 'layout') {
112
+ scopedDataKey = `layout:${entry.ownerKey}`;
113
+ }
114
+ else if (entry.ownerKind === 'component' && entry.instanceStrategy === 'per-instance') {
115
+ const index = occurrenceIndexByOwnerKey.get(entry.ownerKey) || 0;
116
+ occurrenceIndexByOwnerKey.set(entry.ownerKey, index + 1);
117
+ const instance = Array.isArray(entry.instances) ? entry.instances[index] : null;
118
+ scopedDataKey = typeof instance?.key === 'string' && instance.key.length > 0
119
+ ? instance.key
120
+ : `component:${entry.ownerKey}:o${index}`;
121
+ }
122
+ else if (entry.ownerKind === 'component') {
123
+ scopedDataKey = `component:${entry.ownerKey}`;
124
+ }
125
+ return scopedDataKey
126
+ ? {
127
+ scopedDataKey,
128
+ serializedVariableNames: Array.isArray(entry.serializedVariableNames)
129
+ ? entry.serializedVariableNames
130
+ : []
131
+ }
132
+ : null;
133
+ }
@@ -1,5 +1,6 @@
1
1
  import { readFileSync } from 'node:fs';
2
2
  import { findNextKnownComponentTag } from '../component-tag-parser.js';
3
+ import { readRouteHandlerExport } from '../route-handler-export-analysis.js';
3
4
  import { extractStaticExportPaths } from '../static-export-paths.js';
4
5
  /**
5
6
  * @param {string} source
@@ -56,40 +57,25 @@ export function extractServerScript(source, sourceFile, compilerOpts = {}) {
56
57
  ` Reason: <script server> block is empty\n` +
57
58
  ` Example: export const data = { ... }`);
58
59
  }
59
- const loadFnMatch = serverSource.match(/\bexport\s+(?:async\s+)?function\s+load\s*\(([^)]*)\)/);
60
- const loadConstParenMatch = serverSource.match(/\bexport\s+const\s+load\s*=\s*(?:async\s*)?\(([^)]*)\)\s*=>/);
61
- const loadConstSingleArgMatch = serverSource.match(/\bexport\s+const\s+load\s*=\s*(?:async\s*)?([a-zA-Z_$][a-zA-Z0-9_$]*)\s*=>/);
62
- const hasLoad = Boolean(loadFnMatch || loadConstParenMatch || loadConstSingleArgMatch);
63
- const loadMatchCount = Number(Boolean(loadFnMatch)) +
64
- Number(Boolean(loadConstParenMatch)) +
65
- Number(Boolean(loadConstSingleArgMatch));
66
- if (loadMatchCount > 1) {
60
+ const loadExport = readRouteHandlerExport(serverSource, 'load');
61
+ const hasLoad = loadExport.hasExport;
62
+ if (loadExport.matchCount > 1) {
67
63
  throw new Error(`Zenith server script contract violation:\n` +
68
64
  ` File: ${sourceFile}\n` +
69
65
  ` Reason: multiple load exports detected\n` +
70
66
  ` Example: keep exactly one export const load = async (ctx) => ({ ... })`);
71
67
  }
72
- const guardFnMatch = serverSource.match(/\bexport\s+(?:async\s+)?function\s+guard\s*\(([^)]*)\)/);
73
- const guardConstParenMatch = serverSource.match(/\bexport\s+const\s+guard\s*=\s*(?:async\s*)?\(([^)]*)\)\s*=>/);
74
- const guardConstSingleArgMatch = serverSource.match(/\bexport\s+const\s+guard\s*=\s*(?:async\s*)?([a-zA-Z_$][a-zA-Z0-9_$]*)\s*=>/);
75
- const hasGuard = Boolean(guardFnMatch || guardConstParenMatch || guardConstSingleArgMatch);
76
- const guardMatchCount = Number(Boolean(guardFnMatch)) +
77
- Number(Boolean(guardConstParenMatch)) +
78
- Number(Boolean(guardConstSingleArgMatch));
79
- if (guardMatchCount > 1) {
68
+ const guardExport = readRouteHandlerExport(serverSource, 'guard');
69
+ const hasGuard = guardExport.hasExport;
70
+ if (guardExport.matchCount > 1) {
80
71
  throw new Error(`Zenith server script contract violation:\n` +
81
72
  ` File: ${sourceFile}\n` +
82
73
  ` Reason: multiple guard exports detected\n` +
83
74
  ` Example: keep exactly one export const guard = async (ctx) => ({ ... })`);
84
75
  }
85
- const actionFnMatch = serverSource.match(/\bexport\s+(?:async\s+)?function\s+action\s*\(([^)]*)\)/);
86
- const actionConstParenMatch = serverSource.match(/\bexport\s+const\s+action\s*=\s*(?:async\s*)?\(([^)]*)\)\s*=>/);
87
- const actionConstSingleArgMatch = serverSource.match(/\bexport\s+const\s+action\s*=\s*(?:async\s*)?([a-zA-Z_$][a-zA-Z0-9_$]*)\s*=>/);
88
- const hasAction = Boolean(actionFnMatch || actionConstParenMatch || actionConstSingleArgMatch);
89
- const actionMatchCount = Number(Boolean(actionFnMatch)) +
90
- Number(Boolean(actionConstParenMatch)) +
91
- Number(Boolean(actionConstSingleArgMatch));
92
- if (actionMatchCount > 1) {
76
+ const actionExport = readRouteHandlerExport(serverSource, 'action');
77
+ const hasAction = actionExport.hasExport;
78
+ if (actionExport.matchCount > 1) {
93
79
  throw new Error(`Zenith server script contract violation:\n` +
94
80
  ` File: ${sourceFile}\n` +
95
81
  ` Reason: multiple action exports detected\n` +
@@ -112,10 +98,7 @@ export function extractServerScript(source, sourceFile, compilerOpts = {}) {
112
98
  ` Example: use only export const data or export const load`);
113
99
  }
114
100
  if (hasLoad) {
115
- const singleArg = String(loadConstSingleArgMatch?.[1] || '').trim();
116
- const paramsText = String((loadFnMatch || loadConstParenMatch)?.[1] || '').trim();
117
- const arity = singleArg ? 1 : paramsText.length === 0 ? 0 : paramsText.split(',').length;
118
- if (arity !== 1) {
101
+ if (loadExport.arity !== null && loadExport.arity !== 1) {
119
102
  throw new Error(`Zenith server script contract violation:\n` +
120
103
  ` File: ${sourceFile}\n` +
121
104
  ` Reason: load(ctx) must accept exactly one argument\n` +
@@ -123,10 +106,7 @@ export function extractServerScript(source, sourceFile, compilerOpts = {}) {
123
106
  }
124
107
  }
125
108
  if (hasGuard) {
126
- const singleArg = String(guardConstSingleArgMatch?.[1] || '').trim();
127
- const paramsText = String((guardFnMatch || guardConstParenMatch)?.[1] || '').trim();
128
- const arity = singleArg ? 1 : paramsText.length === 0 ? 0 : paramsText.split(',').length;
129
- if (arity !== 1) {
109
+ if (guardExport.arity !== null && guardExport.arity !== 1) {
130
110
  throw new Error(`Zenith server script contract violation:\n` +
131
111
  ` File: ${sourceFile}\n` +
132
112
  ` Reason: guard(ctx) must accept exactly one argument\n` +
@@ -134,10 +114,7 @@ export function extractServerScript(source, sourceFile, compilerOpts = {}) {
134
114
  }
135
115
  }
136
116
  if (hasAction) {
137
- const singleArg = String(actionConstSingleArgMatch?.[1] || '').trim();
138
- const paramsText = String((actionFnMatch || actionConstParenMatch)?.[1] || '').trim();
139
- const arity = singleArg ? 1 : paramsText.length === 0 ? 0 : paramsText.split(',').length;
140
- if (arity !== 1) {
117
+ if (actionExport.arity !== null && actionExport.arity !== 1) {
141
118
  throw new Error(`Zenith server script contract violation:\n` +
142
119
  ` File: ${sourceFile}\n` +
143
120
  ` Reason: action(ctx) must accept exactly one argument\n` +
@@ -4,13 +4,14 @@
4
4
  */
5
5
  export function deriveProjectRootFromPagesDir(pagesDir: string): string;
6
6
  /**
7
- * @param {{ manifest: Array<{ path: string, file: string }>, pagesDir: string }} input
7
+ * @param {{ manifest: Array<{ path: string, file: string, scoped_server_data?: import('../scoped-server-data/types.js').ManifestScopedServerDataEntry[] }>, pagesDir: string }} input
8
8
  * @returns {Promise<void>}
9
9
  */
10
10
  export function ensureZenithTypeDeclarations(input: {
11
11
  manifest: Array<{
12
12
  path: string;
13
13
  file: string;
14
+ scoped_server_data?: import("../scoped-server-data/types.js").ManifestScopedServerDataEntry[];
14
15
  }>;
15
16
  pagesDir: string;
16
17
  }): Promise<void>;
@@ -1,6 +1,9 @@
1
1
  import { existsSync, readFileSync, writeFileSync } from 'node:fs';
2
2
  import { mkdir } from 'node:fs/promises';
3
3
  import { basename, dirname, join, resolve } from 'node:path';
4
+ import { renderZenithEnvDts } from '../types/zenith-env-dts.js';
5
+ const SCOPED_SERVER_TYPE_DECLARATIONS_HELPER_UNAVAILABLE = '[Zenith:ScopedServerData] Type declaration helper is unavailable. Run the CLI build step before generating scoped server data type declarations.';
6
+ let scopedTypeDeclarationRendererPromise = null;
4
7
  /**
5
8
  * @param {string} targetPath
6
9
  * @param {string} next
@@ -61,58 +64,25 @@ function renderZenithRouteDts(manifest) {
61
64
  return `${lines.join('\n')}\n`;
62
65
  }
63
66
  /**
64
- * @returns {string}
67
+ * @returns {Promise<(input: { manifest: Array<{ scoped_server_data?: import('../scoped-server-data/types.js').ManifestScopedServerDataEntry[] }>, srcDir: string }) => string>}
65
68
  */
66
- function renderZenithEnvDts() {
67
- return [
68
- '// Auto-generated by Zenith CLI. Do not edit manually.',
69
- 'export {};',
70
- '',
71
- 'declare global {',
72
- ' namespace Zenith {',
73
- ' type Params = Record<string, string>;',
74
- '',
75
- ' interface ErrorState {',
76
- ' status?: number;',
77
- ' code?: string;',
78
- ' message: string;',
79
- ' }',
80
- '',
81
- ' type PageData = Record<string, unknown> & { __zenith_error?: ErrorState };',
82
- '',
83
- ' interface RouteMeta {',
84
- ' id: string;',
85
- ' file: string;',
86
- ' pattern: string;',
87
- ' }',
88
- '',
89
- ' interface LoadContext {',
90
- ' params: Params;',
91
- ' url: URL;',
92
- ' request: Request;',
93
- ' route: RouteMeta;',
94
- ' }',
95
- '',
96
- ' type Load<T extends PageData = PageData> = (ctx: LoadContext) => Promise<T> | T;',
97
- '',
98
- ' interface Fragment {',
99
- ' __zenith_fragment: true;',
100
- ' mount: (anchor: Node | null) => void;',
101
- ' unmount: () => void;',
102
- ' }',
103
- '',
104
- ' type Renderable =',
105
- ' | string',
106
- ' | number',
107
- ' | boolean',
108
- ' | null',
109
- ' | undefined',
110
- ' | Renderable[]',
111
- ' | Fragment;',
112
- ' }',
113
- '}',
114
- ''
115
- ].join('\n');
69
+ async function getScopedServerDataTypeRenderer() {
70
+ if (!scopedTypeDeclarationRendererPromise) {
71
+ scopedTypeDeclarationRendererPromise = import('../scoped-server-data/type-declarations.js')
72
+ .then((helper) => {
73
+ if (typeof helper.renderScopedServerDataDts !== 'function') {
74
+ throw new Error(SCOPED_SERVER_TYPE_DECLARATIONS_HELPER_UNAVAILABLE);
75
+ }
76
+ return helper.renderScopedServerDataDts;
77
+ })
78
+ .catch((error) => {
79
+ if (error && error.message === SCOPED_SERVER_TYPE_DECLARATIONS_HELPER_UNAVAILABLE) {
80
+ throw error;
81
+ }
82
+ throw new Error(SCOPED_SERVER_TYPE_DECLARATIONS_HELPER_UNAVAILABLE);
83
+ });
84
+ }
85
+ return scopedTypeDeclarationRendererPromise;
116
86
  }
117
87
  /**
118
88
  * @param {string} pagesDir
@@ -127,17 +97,24 @@ export function deriveProjectRootFromPagesDir(pagesDir) {
127
97
  return parent;
128
98
  }
129
99
  /**
130
- * @param {{ manifest: Array<{ path: string, file: string }>, pagesDir: string }} input
100
+ * @param {{ manifest: Array<{ path: string, file: string, scoped_server_data?: import('../scoped-server-data/types.js').ManifestScopedServerDataEntry[] }>, pagesDir: string }} input
131
101
  * @returns {Promise<void>}
132
102
  */
133
103
  export async function ensureZenithTypeDeclarations(input) {
134
104
  const projectRoot = deriveProjectRootFromPagesDir(input.pagesDir);
105
+ const srcDir = dirname(resolve(input.pagesDir));
135
106
  const zenithDir = resolve(projectRoot, '.zenith');
136
107
  await mkdir(zenithDir, { recursive: true });
137
108
  const envPath = join(zenithDir, 'zenith-env.d.ts');
138
109
  const routesPath = join(zenithDir, 'zenith-routes.d.ts');
110
+ const scopedPath = join(zenithDir, 'zenith-scoped-server-data.d.ts');
111
+ const renderScopedServerDataDts = await getScopedServerDataTypeRenderer();
139
112
  writeIfChanged(envPath, renderZenithEnvDts());
140
113
  writeIfChanged(routesPath, renderZenithRouteDts(input.manifest));
114
+ writeIfChanged(scopedPath, renderScopedServerDataDts({
115
+ manifest: input.manifest,
116
+ srcDir
117
+ }));
141
118
  const tsconfigPath = resolve(projectRoot, 'tsconfig.json');
142
119
  if (!existsSync(tsconfigPath)) {
143
120
  return;