@jay-framework/stack-server-runtime 0.15.5 → 0.16.0

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 (3) hide show
  1. package/dist/index.d.ts +121 -14
  2. package/dist/index.js +529 -62
  3. package/package.json +13 -13
package/dist/index.d.ts CHANGED
@@ -1,5 +1,4 @@
1
- import { AnyJayStackComponentDefinition, PageProps, AnySlowlyRenderResult, UrlParams, JayStackComponentDefinition, AnyFastRenderResult, HttpMethod, CacheOptions, JayAction, JayActionDefinition, ServiceMarker } from '@jay-framework/fullstack-component';
2
- import { JayComponentCore } from '@jay-framework/component';
1
+ import { AnyJayStackComponentDefinition, PageProps, AnySlowlyRenderResult, UrlParams, AnyFastRenderResult, HttpMethod, CacheOptions, JayAction, JayActionDefinition, JayStreamAction, JayStreamActionDefinition, HeadTag, ServiceMarker } from '@jay-framework/fullstack-component';
3
2
  import { ViteDevServer } from 'vite';
4
3
  import { JayRoute } from '@jay-framework/stack-route-scanner';
5
4
  import { WithValidations, JsonSchemaProperty, PluginManifest } from '@jay-framework/compiler-shared';
@@ -49,6 +48,10 @@ interface LoadedPageParts {
49
48
  discoveredInstances: DiscoveredHeadlessInstance[];
50
49
  /** Discovered forEach <jay:xxx> instances from the jay-html (DL#109) */
51
50
  forEachInstances: ForEachHeadlessInstance[];
51
+ /** Absolute paths to linked CSS files (from <link rel="stylesheet">) for dev-server watching */
52
+ linkedCssFiles: string[];
53
+ /** Absolute paths to headfull FS component jay-html files for dev-server watching */
54
+ linkedComponentFiles: string[];
52
55
  }
53
56
  interface LoadPagePartsOptions {
54
57
  /**
@@ -72,8 +75,11 @@ interface SlowlyChangingPhase {
72
75
  declare class DevSlowlyChangingPhase implements SlowlyChangingPhase {
73
76
  runSlowlyForPage(pageParams: UrlParams, pageProps: PageProps, parts: Array<DevServerPagePart>, discoveredInstances?: DiscoveredHeadlessInstance[], headlessInstanceComponents?: HeadlessInstanceComponent[], jayHtmlPath?: string): Promise<AnySlowlyRenderResult>;
74
77
  }
75
- declare function runLoadParams<Refs extends object, SlowVS extends object, FastVS extends object, InteractiveVS extends object, Services extends Array<any>, Contexts extends Array<any>, PropsT extends object, Params extends UrlParams, CompCore extends JayComponentCore<PropsT, InteractiveVS>>(compDefinition: JayStackComponentDefinition<Refs, SlowVS, FastVS, InteractiveVS, Services, Contexts, PropsT, Params, CompCore>, services: Services): Promise<void>;
76
- declare function runSlowlyChangingRender<Refs extends object, SlowVS extends object, FastVS extends object, InteractiveVS extends object, Services extends Array<any>, Contexts extends Array<any>, PropsT extends object, Params extends UrlParams, CompCore extends JayComponentCore<PropsT, InteractiveVS>>(compDefinition: JayStackComponentDefinition<Refs, SlowVS, FastVS, InteractiveVS, Services, Contexts, PropsT, Params, CompCore>): void;
78
+ /**
79
+ * Run loadParams for all parts (page + keyed headless components).
80
+ * Yields param batches from each part that has loadParams.
81
+ */
82
+ declare function runLoadParams(parts: DevServerPagePart[]): AsyncGenerator<Record<string, string>[]>;
77
83
 
78
84
  /**
79
85
  * Server-side slow render orchestration for headless component instances.
@@ -97,6 +103,8 @@ interface InstancePhaseData {
97
103
  }>;
98
104
  /** CarryForward per instance (keyed by coordinate path, e.g. "p1/product-card:0") */
99
105
  carryForwards: Record<string, object>;
106
+ /** Slow ViewState per instance (keyed by coordinate path) */
107
+ slowViewStates?: Record<string, object>;
100
108
  /** ForEach instances that need fast-phase per-item rendering */
101
109
  forEachInstances?: ForEachHeadlessInstance[];
102
110
  }
@@ -185,23 +193,43 @@ declare function resolveActionMetadataPath(actionPath: string, pluginDir: string
185
193
  */
186
194
 
187
195
  /**
188
- * Registered action entry with resolved metadata.
196
+ * Base fields shared by all registered action types.
189
197
  */
190
- interface RegisteredAction {
198
+ interface RegisteredActionBase {
191
199
  /** Unique action name */
192
200
  actionName: string;
193
201
  /** HTTP method */
194
202
  method: HttpMethod;
195
- /** Cache options (for GET requests) */
196
- cacheOptions?: CacheOptions;
197
203
  /** Service markers for dependency injection */
198
204
  services: any[];
199
- /** The handler function */
200
- handler: (input: any, ...services: any[]) => Promise<any>;
201
205
  /** Optional metadata from .jay-action file (description, input/output schemas).
202
206
  * Actions with metadata are exposed to AI agents; those without are not. */
203
207
  metadata?: ActionMetadata;
204
208
  }
209
+ /**
210
+ * Registered request-response action entry.
211
+ * Uses `isStreaming` as a discriminator for the union.
212
+ */
213
+ interface RegisteredAction extends RegisteredActionBase {
214
+ /** Discriminator: false or absent for regular actions */
215
+ isStreaming?: false;
216
+ /** Cache options (for GET requests) */
217
+ cacheOptions?: CacheOptions;
218
+ /** The handler function */
219
+ handler: (input: any, ...services: any[]) => Promise<any>;
220
+ }
221
+ /**
222
+ * Registered streaming action entry (DL#129).
223
+ */
224
+ interface RegisteredStreamAction extends RegisteredActionBase {
225
+ method: 'POST';
226
+ /** Discriminator: true for streaming actions */
227
+ isStreaming: true;
228
+ /** The generator handler function */
229
+ handler: (input: any, ...services: any[]) => AsyncIterable<any>;
230
+ }
231
+ /** Union of all registered action types, discriminated by `isStreaming`. */
232
+ type RegisteredActionEntry = RegisteredAction | RegisteredStreamAction;
205
233
  /**
206
234
  * Result of executing an action.
207
235
  */
@@ -252,7 +280,7 @@ declare class ActionRegistry {
252
280
  * @param actionName - The unique action name
253
281
  * @returns The registered action or undefined
254
282
  */
255
- get(actionName: string): RegisteredAction | undefined;
283
+ get(actionName: string): RegisteredActionEntry | undefined;
256
284
  /**
257
285
  * Checks if an action is registered.
258
286
  *
@@ -304,6 +332,18 @@ declare class ActionRegistry {
304
332
  * @returns Cache-Control header value or undefined
305
333
  */
306
334
  getCacheHeaders(actionName: string): string | undefined;
335
+ /**
336
+ * Register a streaming action.
337
+ */
338
+ registerStream<I, C, S extends any[]>(action: JayStreamAction<I, C> & JayStreamActionDefinition<I, C, S>): void;
339
+ /**
340
+ * Check if a registered action is a streaming action.
341
+ */
342
+ isStreaming(actionName: string): boolean;
343
+ /**
344
+ * Execute a streaming action, returning an async iterable of chunks.
345
+ */
346
+ executeStream(actionName: string, input: unknown): AsyncGenerator<any>;
307
347
  }
308
348
  /**
309
349
  * Default action registry instance.
@@ -319,7 +359,7 @@ declare function registerAction<I, O, S extends any[]>(action: JayAction<I, O> &
319
359
  * Retrieves a registered action by name from the default registry.
320
360
  * @deprecated Use actionRegistry.get() instead
321
361
  */
322
- declare function getRegisteredAction(actionName: string): RegisteredAction | undefined;
362
+ declare function getRegisteredAction(actionName: string): RegisteredActionEntry | undefined;
323
363
  /**
324
364
  * Checks if an action is registered in the default registry.
325
365
  * @deprecated Use actionRegistry.has() instead
@@ -569,6 +609,8 @@ interface GenerateClientScriptOptions {
569
609
  * so that AI/automation tools can see the complete page state.
570
610
  */
571
611
  slowViewState?: object;
612
+ /** Route pattern (e.g., /products/kitan{/:category}) for freeze entries */
613
+ routePattern?: string;
572
614
  }
573
615
  /**
574
616
  * Shared fragments generated by buildScriptFragments().
@@ -626,7 +668,23 @@ declare function clearServerElementCache(): void;
626
668
  * 3. Build hydration script (uses ?jay-hydrate query for hydrate target)
627
669
  * 4. Return full HTML page string
628
670
  */
629
- declare function generateSSRPageHtml(vite: ViteDevServer, jayHtmlContent: string, jayHtmlFilename: string, jayHtmlDir: string, viewState: object, jayHtmlImportPath: string, parts: DevServerPagePart[], carryForward: object, trackByMap: TrackByMap, clientInitData: Record<string, Record<string, any>>, buildFolder: string, projectRoot: string, routeDir: string, tsConfigFilePath?: string, projectInit?: ProjectClientInitInfo, pluginInits?: PluginClientInitInfo[], options?: GenerateClientScriptOptions): Promise<string>;
671
+ declare function generateSSRPageHtml(vite: ViteDevServer, jayHtmlContent: string, jayHtmlFilename: string, jayHtmlDir: string, viewState: object, jayHtmlImportPath: string, parts: DevServerPagePart[], carryForward: object, trackByMap: TrackByMap, clientInitData: Record<string, Record<string, any>>, buildFolder: string, projectRoot: string, routeDir: string, tsConfigFilePath?: string, projectInit?: ProjectClientInitInfo, pluginInits?: PluginClientInitInfo[], options?: GenerateClientScriptOptions,
672
+ /** Source directory for headfull FS file resolution when jayHtmlDir is pre-rendered */
673
+ sourceDir?: string,
674
+ /** Head tags to inject into <head> during SSR (Design Log #127) */
675
+ headTags?: HeadTag[]): Promise<string>;
676
+ /**
677
+ * Generate a frozen page — pure SSR HTML with no client scripts (DL#127).
678
+ *
679
+ * Uses the same server element module as generateSSRPageHtml, but:
680
+ * - No hydration script
681
+ * - No Vite client
682
+ * - No component runtime
683
+ * - Just rendered HTML + CSS
684
+ *
685
+ * @param format - 'page' for full HTML document, 'fragment' for body-only (shadow DOM)
686
+ */
687
+ declare function generateFrozenPageHtml(vite: ViteDevServer, jayHtmlContent: string, jayHtmlFilename: string, jayHtmlDir: string, viewState: object, buildFolder: string, projectRoot: string, routeDir: string, tsConfigFilePath?: string, sourceDir?: string, format?: 'page' | 'fragment', freezeName?: string): Promise<string>;
630
688
 
631
689
  /**
632
690
  * Service registry for Jay Stack server-side dependency injection.
@@ -906,10 +964,30 @@ interface ActionIndexEntry {
906
964
  /** Contract entry within a plugin in plugins-index.yaml */
907
965
  interface PluginContractEntry {
908
966
  name: string;
967
+ description?: string;
909
968
  type: 'static' | 'dynamic';
910
969
  path: string;
911
970
  metadata?: Record<string, unknown>;
912
971
  }
972
+ /** Service entry in plugins-index.yaml (DL#125) */
973
+ interface ServiceIndexEntry {
974
+ name: string;
975
+ marker: string;
976
+ description?: string;
977
+ doc?: string;
978
+ }
979
+ /** Context entry in plugins-index.yaml (DL#125) */
980
+ interface ContextIndexEntry {
981
+ name: string;
982
+ marker: string;
983
+ description?: string;
984
+ doc?: string;
985
+ }
986
+ /** Route entry in plugins-index.yaml (DL#130) */
987
+ interface RouteIndexEntry {
988
+ path: string;
989
+ description?: string;
990
+ }
913
991
  /** Entry for plugins-index.yaml (Design Log #85) */
914
992
  interface PluginsIndexEntry {
915
993
  name: string;
@@ -917,6 +995,12 @@ interface PluginsIndexEntry {
917
995
  contracts: PluginContractEntry[];
918
996
  /** Actions with .jay-action metadata (exposed to AI agents) */
919
997
  actions?: ActionIndexEntry[];
998
+ /** Server-side services provided by this plugin (DL#125) */
999
+ services?: ServiceIndexEntry[];
1000
+ /** Client-side contexts provided by this plugin (DL#125) */
1001
+ contexts?: ContextIndexEntry[];
1002
+ /** Plugin-provided routes (DL#130) */
1003
+ routes?: RouteIndexEntry[];
920
1004
  }
921
1005
  interface PluginsIndex {
922
1006
  plugins: PluginsIndexEntry[];
@@ -1150,4 +1234,27 @@ declare function executePluginReferences(plugin: PluginWithReferences, options:
1150
1234
  verbose?: boolean;
1151
1235
  }): Promise<PluginReferencesResult>;
1152
1236
 
1153
- export { type ActionDiscoveryOptions, type ActionDiscoveryResult, type ActionErrorResponse, type ActionExecutionResult, type ActionIndexEntry, type ActionMetadata, ActionRegistry, type ActionSchema, type DevServerPagePart, DevSlowlyChangingPhase, type GenerateClientScriptOptions, type HeadlessInstanceComponent, type InstancePhaseData, type InstanceSlowRenderResult, type LoadedPageParts, type MaterializeContractsOptions, type MaterializeResult, type PluginActionDiscoveryOptions, type PluginClientInitInfo, type PluginContractEntry, type PluginInitDiscoveryOptions, type PluginReferencesContext, type PluginReferencesHandler, type PluginReferencesResult, type PluginScanOptions, type PluginSetupContext, type PluginSetupHandler, type PluginSetupResult, type PluginWithInit, type PluginWithReferences, type PluginWithSetup, type PluginsIndex, type PluginsIndexEntry, type ProjectClientInitInfo, type RegisteredAction, type ScannedPlugin, type ScriptFragments, SlowRenderCache, type SlowRenderCacheEntry, type SlowlyChangingPhase, type ViteSSRLoader, actionRegistry, buildAutomationWrap, buildScriptFragments, clearActionRegistry, clearClientInitData, clearLifecycleCallbacks, clearServerElementCache, clearServiceRegistry, discoverAllPluginActions, discoverAndRegisterActions, discoverPluginActions, discoverPluginsWithInit, discoverPluginsWithReferences, discoverPluginsWithSetup, executeAction, executePluginReferences, executePluginServerInits, executePluginSetup, generateClientScript, generatePromiseReconstruction, generateSSRPageHtml, getActionCacheHeaders, getClientInitData, getClientInitDataForKey, getRegisteredAction, getRegisteredActionNames, getService, getServiceRegistry, hasAction, hasService, invalidateServerElementCache, listContracts, loadActionMetadata, loadPageParts, materializeContracts, onInit, onShutdown, parseActionMetadata, preparePluginClientInits, registerAction, registerService, renderFastChangingData, resolveActionMetadataPath, resolveServices, resolveViewStatePromises, runInitCallbacks, runLoadParams, runShutdownCallbacks, runSlowlyChangingRender, scanPlugins, setClientInitData, slowRenderInstances, sortPluginsByDependencies, validateForEachInstances };
1237
+ /**
1238
+ * Head tag utilities for SSR head injection (Design Log #127).
1239
+ *
1240
+ * Components declare HeadTag[] via phaseOutput(). The SSR pipeline collects
1241
+ * tags from all sources, deduplicates with last-write-wins + collision warning,
1242
+ * and serializes to HTML for injection into <head>.
1243
+ */
1244
+
1245
+ /**
1246
+ * Compute a unique identity key for deduplication.
1247
+ * Returns undefined for tags that should always be included (no dedup).
1248
+ */
1249
+ declare function tagIdentityKey(tag: HeadTag): string | undefined;
1250
+ /**
1251
+ * Merge head tags from multiple sources with last-write-wins.
1252
+ * Warns on collision via logger.
1253
+ */
1254
+ declare function mergeHeadTags(sources: HeadTag[][]): HeadTag[];
1255
+ /**
1256
+ * Serialize an array of HeadTag objects into an HTML string.
1257
+ */
1258
+ declare function serializeHeadTags(tags: HeadTag[]): string;
1259
+
1260
+ export { type ActionDiscoveryOptions, type ActionDiscoveryResult, type ActionErrorResponse, type ActionExecutionResult, type ActionIndexEntry, type ActionMetadata, ActionRegistry, type ActionSchema, type ContextIndexEntry, type DevServerPagePart, DevSlowlyChangingPhase, type GenerateClientScriptOptions, type HeadlessInstanceComponent, type InstancePhaseData, type InstanceSlowRenderResult, type LoadedPageParts, type MaterializeContractsOptions, type MaterializeResult, type PluginActionDiscoveryOptions, type PluginClientInitInfo, type PluginContractEntry, type PluginInitDiscoveryOptions, type PluginReferencesContext, type PluginReferencesHandler, type PluginReferencesResult, type PluginScanOptions, type PluginSetupContext, type PluginSetupHandler, type PluginSetupResult, type PluginWithInit, type PluginWithReferences, type PluginWithSetup, type PluginsIndex, type PluginsIndexEntry, type ProjectClientInitInfo, type RegisteredAction, type RegisteredActionBase, type RegisteredActionEntry, type RegisteredStreamAction, type RouteIndexEntry, type ScannedPlugin, type ScriptFragments, type ServiceIndexEntry, SlowRenderCache, type SlowRenderCacheEntry, type SlowlyChangingPhase, type ViteSSRLoader, actionRegistry, buildAutomationWrap, buildScriptFragments, clearActionRegistry, clearClientInitData, clearLifecycleCallbacks, clearServerElementCache, clearServiceRegistry, discoverAllPluginActions, discoverAndRegisterActions, discoverPluginActions, discoverPluginsWithInit, discoverPluginsWithReferences, discoverPluginsWithSetup, executeAction, executePluginReferences, executePluginServerInits, executePluginSetup, generateClientScript, generateFrozenPageHtml, generatePromiseReconstruction, generateSSRPageHtml, getActionCacheHeaders, getClientInitData, getClientInitDataForKey, getRegisteredAction, getRegisteredActionNames, getService, getServiceRegistry, hasAction, hasService, invalidateServerElementCache, listContracts, loadActionMetadata, loadPageParts, materializeContracts, mergeHeadTags, onInit, onShutdown, parseActionMetadata, preparePluginClientInits, registerAction, registerService, renderFastChangingData, resolveActionMetadataPath, resolveServices, resolveViewStatePromises, runInitCallbacks, runLoadParams, runShutdownCallbacks, scanPlugins, serializeHeadTags, setClientInitData, slowRenderInstances, sortPluginsByDependencies, tagIdentityKey, validateForEachInstances };
package/dist/index.js CHANGED
@@ -4,19 +4,19 @@ var __publicField = (obj, key, value) => {
4
4
  __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
5
5
  return value;
6
6
  };
7
- import { phaseOutput, isJayAction } from "@jay-framework/fullstack-component";
7
+ import { phaseOutput, isJayAction, isJayStreamAction } from "@jay-framework/fullstack-component";
8
8
  import "prettier";
9
9
  import "js-beautify";
10
10
  import fs$1 from "fs";
11
11
  import path$1 from "path";
12
12
  import YAML from "yaml";
13
13
  import { createRequire } from "module";
14
- import { parseJayFile, JAY_IMPORT_RESOLVER, generateServerElementFile, discoverHeadlessInstances, parseAction } from "@jay-framework/compiler-jay-html";
14
+ import { parseJayFile, JAY_IMPORT_RESOLVER, generateServerElementFile, injectHeadfullFSTemplates, discoverHeadlessInstances, assignCoordinatesToJayHtml, parseAction } from "@jay-framework/compiler-jay-html";
15
+ import { getLogger } from "@jay-framework/logger";
15
16
  import fs$2 from "node:fs/promises";
16
17
  import * as path from "node:path";
17
18
  import path__default from "node:path";
18
19
  import crypto from "node:crypto";
19
- import { getLogger } from "@jay-framework/logger";
20
20
  import * as fs from "node:fs";
21
21
  import { createRequire as createRequire$1 } from "node:module";
22
22
  const serviceRegistry = /* @__PURE__ */ new Map();
@@ -87,6 +87,7 @@ class DevSlowlyChangingPhase {
87
87
  async runSlowlyForPage(pageParams, pageProps, parts, discoveredInstances, headlessInstanceComponents, jayHtmlPath) {
88
88
  let slowlyViewState = {};
89
89
  let carryForward = {};
90
+ const slowHeadTagSources = [];
90
91
  for (const part of parts) {
91
92
  const { compDefinition, key, contractInfo } = part;
92
93
  if (compDefinition.slowlyRender) {
@@ -111,6 +112,9 @@ class DevSlowlyChangingPhase {
111
112
  slowlyViewState[key] = slowlyRenderedPart.rendered;
112
113
  carryForward[key] = slowlyRenderedPart.carryForward;
113
114
  }
115
+ if (slowlyRenderedPart.headTags) {
116
+ slowHeadTagSources.push(slowlyRenderedPart.headTags);
117
+ }
114
118
  } else
115
119
  return slowlyRenderedPart;
116
120
  }
@@ -158,6 +162,9 @@ class DevSlowlyChangingPhase {
158
162
  contract: comp.contract,
159
163
  slowViewState: slowResult.rendered
160
164
  });
165
+ if (slowResult.headTags) {
166
+ slowHeadTagSources.push(slowResult.headTags);
167
+ }
161
168
  }
162
169
  }
163
170
  }
@@ -165,13 +172,21 @@ class DevSlowlyChangingPhase {
165
172
  carryForward.__instanceSlowViewStates = instanceSlowViewStates;
166
173
  carryForward.__instanceResolvedData = instanceResolvedData;
167
174
  }
175
+ if (slowHeadTagSources.length > 0) {
176
+ carryForward.__slowHeadTags = slowHeadTagSources;
177
+ }
168
178
  return phaseOutput(slowlyViewState, carryForward);
169
179
  }
170
180
  }
171
- async function runLoadParams(compDefinition, services) {
172
- compDefinition.loadParams(services);
173
- }
174
- function runSlowlyChangingRender(compDefinition) {
181
+ async function* runLoadParams(parts) {
182
+ for (const part of parts) {
183
+ if (part.compDefinition.loadParams) {
184
+ const services = resolveServices(part.compDefinition.services);
185
+ for await (const batch of part.compDefinition.loadParams(services)) {
186
+ yield batch;
187
+ }
188
+ }
189
+ }
175
190
  }
176
191
  var __defProp2 = Object.defineProperty;
177
192
  var __defNormalProp2 = (obj, key, value) => key in obj ? __defProp2(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
@@ -378,6 +393,7 @@ function resolveBinding(binding, item) {
378
393
  async function renderFastChangingData(pageParams, pageProps, carryForward, parts, instancePhaseData, forEachInstances, headlessInstanceComponents, mergedSlowViewState, query = {}) {
379
394
  let fastViewState = {};
380
395
  let fastCarryForward = {};
396
+ const fastHeadTagSources = [];
381
397
  for (const part of parts) {
382
398
  const { compDefinition, key, contractInfo } = part;
383
399
  if (compDefinition.fastRender) {
@@ -392,11 +408,7 @@ async function renderFastChangingData(pageParams, pageProps, carryForward, parts
392
408
  metadata: contractInfo.metadata
393
409
  }
394
410
  };
395
- const fastRenderedPart = await compDefinition.fastRender(
396
- partProps,
397
- partSlowlyCarryForward,
398
- ...services
399
- );
411
+ const fastRenderedPart = compDefinition.slowlyRender ? await compDefinition.fastRender(partProps, partSlowlyCarryForward, ...services) : await compDefinition.fastRender(partProps, ...services);
400
412
  if (fastRenderedPart.kind === "PhaseOutput") {
401
413
  if (!key) {
402
414
  fastViewState = { ...fastViewState, ...fastRenderedPart.rendered };
@@ -405,6 +417,9 @@ async function renderFastChangingData(pageParams, pageProps, carryForward, parts
405
417
  fastViewState[key] = fastRenderedPart.rendered;
406
418
  fastCarryForward[key] = fastRenderedPart.carryForward;
407
419
  }
420
+ if (fastRenderedPart.headTags) {
421
+ fastHeadTagSources.push(fastRenderedPart.headTags);
422
+ }
408
423
  } else
409
424
  return fastRenderedPart;
410
425
  }
@@ -419,18 +434,25 @@ async function renderFastChangingData(pageParams, pageProps, carryForward, parts
419
434
  for (const instance of instancePhaseData.discovered) {
420
435
  const coordKey = instance.coordinate.join("/");
421
436
  const comp = componentByContractName.get(instance.contractName);
422
- if (!comp || !comp.compDefinition.fastRender)
437
+ if (!comp)
423
438
  continue;
424
- const services = resolveServices(comp.compDefinition.services);
425
- const cf = instancePhaseData.carryForwards[coordKey];
426
- const instanceProps = { ...instance.props, query };
427
- const fastResult = comp.compDefinition.slowlyRender ? await comp.compDefinition.fastRender(instanceProps, cf, ...services) : await comp.compDefinition.fastRender(instanceProps, ...services);
428
- if (fastResult.kind === "PhaseOutput") {
429
- const instanceSlowVS = carryForward?.__instanceSlowViewStates?.[coordKey];
430
- instanceViewStates[coordKey] = instanceSlowVS ? { ...instanceSlowVS, ...fastResult.rendered } : fastResult.rendered;
431
- if (fastResult.carryForward) {
432
- instanceCarryForwards[coordKey] = fastResult.carryForward;
439
+ const instanceSlowVS = instancePhaseData.slowViewStates?.[coordKey] ?? carryForward?.__instanceSlowViewStates?.[coordKey];
440
+ if (comp.compDefinition.fastRender) {
441
+ const services = resolveServices(comp.compDefinition.services);
442
+ const cf = instancePhaseData.carryForwards[coordKey];
443
+ const instanceProps = { ...instance.props, query };
444
+ const fastResult = comp.compDefinition.slowlyRender ? await comp.compDefinition.fastRender(instanceProps, cf, ...services) : await comp.compDefinition.fastRender(instanceProps, ...services);
445
+ if (fastResult.kind === "PhaseOutput") {
446
+ instanceViewStates[coordKey] = instanceSlowVS ? { ...instanceSlowVS, ...fastResult.rendered } : fastResult.rendered;
447
+ if (fastResult.carryForward) {
448
+ instanceCarryForwards[coordKey] = fastResult.carryForward;
449
+ }
450
+ if (fastResult.headTags) {
451
+ fastHeadTagSources.push(fastResult.headTags);
452
+ }
433
453
  }
454
+ } else {
455
+ instanceViewStates[coordKey] = instanceSlowVS ?? {};
434
456
  }
435
457
  }
436
458
  }
@@ -491,7 +513,11 @@ async function renderFastChangingData(pageParams, pageProps, carryForward, parts
491
513
  if (Object.keys(instanceCarryForwards).length > 0) {
492
514
  fastCarryForward.__headlessInstances = instanceCarryForwards;
493
515
  }
494
- return Promise.resolve(phaseOutput(fastViewState, fastCarryForward));
516
+ const result = phaseOutput(fastViewState, fastCarryForward);
517
+ if (fastHeadTagSources.length > 0) {
518
+ result.headTags = fastHeadTagSources.flat();
519
+ }
520
+ return Promise.resolve(result);
495
521
  }
496
522
  function generatePromiseReconstruction(outcomes) {
497
523
  if (outcomes.length === 0)
@@ -551,6 +577,80 @@ ${parts.map((part) => " " + part.clientPart).join(",\n")}
551
577
  slowViewStateDecl
552
578
  };
553
579
  }
580
+ function buildFreezeScript(routePattern) {
581
+ const routePatternLiteral = routePattern ? `'${routePattern}'` : "undefined";
582
+ return `
583
+ // Page Freeze (DL#127, DL#128 iframe addendum)
584
+ // Sticky embed mode: URL param sets a session cookie so in-iframe navigation preserves it
585
+ if (new URLSearchParams(window.location.search).has('_jay_embed')) {
586
+ document.cookie = '_jay_embed=1;path=/;samesite=lax';
587
+ }
588
+ const __jayEmbedMode = document.cookie.split(';').some(c => c.trim().startsWith('_jay_embed='));
589
+
590
+ async function __jayDoFreeze() {
591
+ const automation = window.__jay?.automation;
592
+ if (!automation) return;
593
+
594
+ // Visual feedback: white flash
595
+ const flash = document.createElement('div');
596
+ flash.style.cssText = 'position:fixed;inset:0;background:white;z-index:999999;opacity:0.8;pointer-events:none;transition:opacity 0.3s';
597
+ document.body.appendChild(flash);
598
+ requestAnimationFrame(() => { flash.style.opacity = '0'; });
599
+ setTimeout(() => flash.remove(), 400);
600
+
601
+ // Audio feedback: camera shutter
602
+ try {
603
+ const ctx = new AudioContext();
604
+ const buf = ctx.createBuffer(1, ctx.sampleRate * 0.15, ctx.sampleRate);
605
+ const data = buf.getChannelData(0);
606
+ for (let i = 0; i < data.length; i++) {
607
+ data[i] = (Math.random() * 2 - 1) * Math.exp(-i / (ctx.sampleRate * 0.02));
608
+ }
609
+ const src = ctx.createBufferSource();
610
+ src.buffer = buf;
611
+ src.connect(ctx.destination);
612
+ src.start();
613
+ } catch {}
614
+
615
+ // Capture and save
616
+ try {
617
+ const state = automation.getPageState();
618
+ const route = window.location.pathname;
619
+ const routePattern = ${routePatternLiteral};
620
+ const resp = await fetch('/_jay/freeze', {
621
+ method: 'POST',
622
+ headers: { 'Content-Type': 'application/json' },
623
+ body: JSON.stringify({ route, routePattern, viewState: state.viewState }),
624
+ });
625
+ const { id } = await resp.json();
626
+ if (__jayEmbedMode) {
627
+ window.parent.postMessage({ type: 'jay:freeze', id, route }, '*');
628
+ } else {
629
+ window.open(route + '?_jay_freeze=' + id, '_blank');
630
+ }
631
+ } catch (err) {
632
+ console.error('[Freeze] Failed:', err);
633
+ }
634
+ }
635
+
636
+ if (__jayEmbedMode) {
637
+ // Notify parent of the current route on load (DL#128 route addendum)
638
+ window.parent.postMessage({ type: 'jay:route', route: window.location.pathname, routePattern: ${routePatternLiteral} }, '*');
639
+
640
+ // Embed mode: parent triggers freeze via postMessage
641
+ window.addEventListener('message', (e) => {
642
+ if (e.data?.type === 'jay:requestFreeze') __jayDoFreeze();
643
+ });
644
+ } else {
645
+ // Standalone: Alt+S / Option+S keyboard shortcut
646
+ document.addEventListener('keydown', (e) => {
647
+ if (e.altKey && e.code === 'KeyS') {
648
+ e.preventDefault();
649
+ __jayDoFreeze();
650
+ }
651
+ });
652
+ }`;
653
+ }
554
654
  function buildAutomationWrap(options, mode) {
555
655
  const { enableAutomation = true, slowViewState } = options;
556
656
  const hasSlowViewState = slowViewState && Object.keys(slowViewState).length > 0;
@@ -561,6 +661,7 @@ function buildAutomationWrap(options, mode) {
561
661
  }
562
662
  const appendLine = appendDom ? `
563
663
  target.appendChild(wrapped.element.dom);` : "";
664
+ const freezeScript = buildFreezeScript(options.routePattern);
564
665
  if (hasSlowViewState) {
565
666
  return `
566
667
  // Wrap with automation for dev tooling
@@ -570,7 +671,8 @@ function buildAutomationWrap(options, mode) {
570
671
  registerGlobalContext(AUTOMATION_CONTEXT, wrapped.automation);
571
672
  window.__jay = window.__jay || {};
572
673
  window.__jay.automation = wrapped.automation;
573
- window.dispatchEvent(new Event('jay:automation-ready'));${appendLine}`;
674
+ window.dispatchEvent(new Event('jay:automation-ready'));${appendLine}
675
+ ${freezeScript}`;
574
676
  }
575
677
  return `
576
678
  // Wrap with automation for dev tooling
@@ -578,7 +680,8 @@ function buildAutomationWrap(options, mode) {
578
680
  registerGlobalContext(AUTOMATION_CONTEXT, wrapped.automation);
579
681
  window.__jay = window.__jay || {};
580
682
  window.__jay.automation = wrapped.automation;
581
- window.dispatchEvent(new Event('jay:automation-ready'));${appendLine}`;
683
+ window.dispatchEvent(new Event('jay:automation-ready'));${appendLine}
684
+ ${freezeScript}`;
582
685
  }
583
686
  async function resolveViewStatePromises(viewState) {
584
687
  const entries = Object.entries(viewState);
@@ -635,7 +738,8 @@ async function generateClientScript(defaultViewState, fastCarryForward, parts, j
635
738
  import { render } from '${jayHtmlPath}';
636
739
  ${partImports}${slowViewStateDecl}
637
740
  const viewState = ${JSON.stringify(defaultViewState)};
638
- ${generatePromiseReconstruction(outcomes)} const fastCarryForward = ${JSON.stringify(fastCarryForward)};
741
+ ${generatePromiseReconstruction(outcomes)}
742
+ const fastCarryForward = ${JSON.stringify(fastCarryForward)};
639
743
  const trackByMap = ${JSON.stringify(trackByMap)};
640
744
  ${clientInitExecution}
641
745
  const target = document.getElementById('target');
@@ -651,6 +755,65 @@ function asyncSwapScript(id, html) {
651
755
  const escapedHtml = html.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
652
756
  return `<script>(function(){var t=document.querySelector('[jay-async="${id}:pending"]');if(t){var d=document.createElement('div');d.innerHTML='${escapedHtml}';t.replaceWith(d.firstChild);}window.__jay&&window.__jay.hydrateAsync&&window.__jay.hydrateAsync('${id}');})()<\/script>`;
653
757
  }
758
+ function tagIdentityKey(tag) {
759
+ const t = tag.tag.toLowerCase();
760
+ if (t === "title")
761
+ return "title";
762
+ if (t === "meta") {
763
+ if (tag.attrs?.name)
764
+ return `meta:name:${tag.attrs.name}`;
765
+ if (tag.attrs?.property)
766
+ return `meta:property:${tag.attrs.property}`;
767
+ if (tag.attrs?.charset !== void 0)
768
+ return "meta:charset";
769
+ return void 0;
770
+ }
771
+ if (t === "link") {
772
+ if (tag.attrs?.rel === "canonical")
773
+ return "link:canonical";
774
+ return void 0;
775
+ }
776
+ return void 0;
777
+ }
778
+ function mergeHeadTags(sources) {
779
+ const byKey = /* @__PURE__ */ new Map();
780
+ const result = [];
781
+ for (let si = 0; si < sources.length; si++) {
782
+ for (const tag of sources[si]) {
783
+ const key = tagIdentityKey(tag);
784
+ if (key) {
785
+ const existing = byKey.get(key);
786
+ if (existing && existing.sourceIndex !== si) {
787
+ getLogger().warn(
788
+ `[head-tags] Collision on "${key}" — overwriting with tag from source ${si}`
789
+ );
790
+ }
791
+ byKey.set(key, { tag, sourceIndex: si });
792
+ } else {
793
+ result.push(tag);
794
+ }
795
+ }
796
+ }
797
+ return [...[...byKey.values()].map((v) => v.tag), ...result];
798
+ }
799
+ function escapeAttr(value) {
800
+ return value.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
801
+ }
802
+ function escapeHtml(value) {
803
+ return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
804
+ }
805
+ const VOID_ELEMENTS = /* @__PURE__ */ new Set(["meta", "link", "base", "br", "hr", "img", "input"]);
806
+ function serializeHeadTags(tags) {
807
+ return tags.map((tag) => {
808
+ const t = tag.tag.toLowerCase();
809
+ const attrs = tag.attrs ? Object.entries(tag.attrs).map(([k, v]) => ` ${k}="${escapeAttr(v)}"`).join("") : "";
810
+ if (VOID_ELEMENTS.has(t)) {
811
+ return ` <${t}${attrs} />`;
812
+ }
813
+ const children = tag.children ? escapeHtml(tag.children) : "";
814
+ return ` <${t}${attrs}>${children}</${t}>`;
815
+ }).join("\n");
816
+ }
654
817
  const serverModuleCache = /* @__PURE__ */ new Map();
655
818
  function invalidateServerElementCache(jayHtmlPath) {
656
819
  if (serverModuleCache.delete(jayHtmlPath)) {
@@ -660,7 +823,7 @@ function invalidateServerElementCache(jayHtmlPath) {
660
823
  function clearServerElementCache() {
661
824
  serverModuleCache.clear();
662
825
  }
663
- async function generateSSRPageHtml(vite, jayHtmlContent, jayHtmlFilename, jayHtmlDir, viewState, jayHtmlImportPath, parts, carryForward, trackByMap = {}, clientInitData2 = {}, buildFolder, projectRoot, routeDir, tsConfigFilePath, projectInit, pluginInits = [], options = {}) {
826
+ async function generateSSRPageHtml(vite, jayHtmlContent, jayHtmlFilename, jayHtmlDir, viewState, jayHtmlImportPath, parts, carryForward, trackByMap = {}, clientInitData2 = {}, buildFolder, projectRoot, routeDir, tsConfigFilePath, projectInit, pluginInits = [], options = {}, sourceDir, headTags) {
664
827
  const jayHtmlPath = path__default.join(jayHtmlDir, jayHtmlFilename);
665
828
  let cached = serverModuleCache.get(jayHtmlPath);
666
829
  if (!cached) {
@@ -672,7 +835,8 @@ async function generateSSRPageHtml(vite, jayHtmlContent, jayHtmlFilename, jayHtm
672
835
  buildFolder,
673
836
  projectRoot,
674
837
  routeDir,
675
- tsConfigFilePath
838
+ tsConfigFilePath,
839
+ sourceDir
676
840
  );
677
841
  serverModuleCache.set(jayHtmlPath, cached);
678
842
  }
@@ -728,20 +892,97 @@ async function generateSSRPageHtml(vite, jayHtmlContent, jayHtmlFilename, jayHtm
728
892
  return ` <link rel="${link.rel}" href="${link.href}"${attrs} />`;
729
893
  }).join("\n");
730
894
  const cssLink = cached.cssHref ? ` <link rel="stylesheet" href="${cached.cssHref}" />` : "";
731
- const headExtras = [headLinksHtml, cssLink].filter((_) => _).join("\n");
895
+ const headTagsHtml = headTags && headTags.length > 0 ? serializeHeadTags(headTags) : "";
896
+ const hasCustomTitle = headTags?.some((t) => t.tag.toLowerCase() === "title");
897
+ const titleHtml = hasCustomTitle ? "" : " <title>Vite + TS</title>\n";
898
+ const headExtras = [headLinksHtml, cssLink, headTagsHtml].filter((_) => _).join("\n");
732
899
  return `<!doctype html>
733
900
  <html lang="en">
734
901
  <head>
735
902
  <meta charset="UTF-8" />
736
903
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
737
- <title>Vite + TS</title>
738
- ${headExtras ? headExtras + "\n" : ""} </head>
904
+ ${titleHtml}${headExtras ? headExtras + "\n" : ""} </head>
739
905
  <body>
740
906
  <div id="target">${ssrHtml}</div>${asyncScripts}
741
907
  ${hydrationScript}
742
908
  </body>
743
909
  </html>`;
744
910
  }
911
+ async function generateFrozenPageHtml(vite, jayHtmlContent, jayHtmlFilename, jayHtmlDir, viewState, buildFolder, projectRoot, routeDir, tsConfigFilePath, sourceDir, format = "page", freezeName) {
912
+ const jayHtmlPath = path__default.join(jayHtmlDir, jayHtmlFilename);
913
+ let cached = serverModuleCache.get(jayHtmlPath);
914
+ if (!cached) {
915
+ cached = await compileAndLoadServerElement(
916
+ vite,
917
+ jayHtmlContent,
918
+ jayHtmlFilename,
919
+ jayHtmlDir,
920
+ buildFolder,
921
+ projectRoot,
922
+ routeDir,
923
+ tsConfigFilePath,
924
+ sourceDir
925
+ );
926
+ serverModuleCache.set(jayHtmlPath, cached);
927
+ }
928
+ const htmlChunks = [];
929
+ const ctx = {
930
+ write: (chunk) => {
931
+ htmlChunks.push(chunk);
932
+ },
933
+ onAsync: () => {
934
+ }
935
+ };
936
+ cached.renderToStream(viewState, ctx);
937
+ const ssrHtml = htmlChunks.join("");
938
+ if (format === "fragment") {
939
+ let inlineCss = "";
940
+ if (cached.cssHref) {
941
+ try {
942
+ const cssPath = cached.cssHref.replace(/^\/@fs/, "").replace(/\?.*$/, "");
943
+ const cssContent = await fs$2.readFile(cssPath, "utf-8");
944
+ inlineCss = `<style>${cssContent}</style>`;
945
+ } catch {
946
+ inlineCss = `<link rel="stylesheet" href="${cached.cssHref}" />`;
947
+ }
948
+ }
949
+ return `${inlineCss}
950
+ ${ssrHtml}`;
951
+ }
952
+ const headLinksHtml = cached.headLinks.map((link) => {
953
+ const attrs = Object.entries(link.attributes).map(([k, v]) => ` ${k}="${v}"`).join("");
954
+ return ` <link rel="${link.rel}" href="${link.href}"${attrs} />`;
955
+ }).join("\n");
956
+ const cssLink = cached.cssHref ? ` <link rel="stylesheet" href="${cached.cssHref}" />` : "";
957
+ const headExtras = [headLinksHtml, cssLink].filter((_) => _).join("\n");
958
+ const label = freezeName ? ` — ${freezeName}` : "";
959
+ return `<!doctype html>
960
+ <html lang="en">
961
+ <head>
962
+ <meta charset="UTF-8" />
963
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
964
+ <title>Frozen${label}</title>
965
+ ${headExtras ? headExtras + "\n" : ""} <style>
966
+ body::before {
967
+ content: 'FROZEN${label ? `: ${freezeName}` : ""}';
968
+ position: fixed;
969
+ top: 0;
970
+ right: 0;
971
+ background: #1a1a2e;
972
+ color: #e0e0ff;
973
+ padding: 2px 10px;
974
+ font: 11px/1.6 system-ui;
975
+ z-index: 99999;
976
+ border-bottom-left-radius: 4px;
977
+ opacity: 0.8;
978
+ }
979
+ </style>
980
+ </head>
981
+ <body>
982
+ <div id="target">${ssrHtml}</div>
983
+ </body>
984
+ </html>`;
985
+ }
745
986
  function rebaseRelativeImports(code, fromDir, toDir) {
746
987
  return code.replace(/from "(\.\.\/[^"]+)"/g, (_match, relPath) => {
747
988
  const absolutePath = path__default.resolve(fromDir, relPath);
@@ -752,14 +993,15 @@ function rebaseRelativeImports(code, fromDir, toDir) {
752
993
  return `from "${newRelPath}"`;
753
994
  });
754
995
  }
755
- async function compileAndLoadServerElement(vite, jayHtmlContent, jayHtmlFilename, jayHtmlDir, buildFolder, projectRoot, routeDir, tsConfigFilePath) {
996
+ async function compileAndLoadServerElement(vite, jayHtmlContent, jayHtmlFilename, jayHtmlDir, buildFolder, projectRoot, routeDir, tsConfigFilePath, sourceDir) {
756
997
  const jayFile = await parseJayFile(
757
998
  jayHtmlContent,
758
999
  jayHtmlFilename,
759
1000
  jayHtmlDir,
760
1001
  { relativePath: tsConfigFilePath },
761
1002
  JAY_IMPORT_RESOLVER,
762
- projectRoot
1003
+ projectRoot,
1004
+ sourceDir
763
1005
  );
764
1006
  const parsedJayFile = checkValidationErrors(jayFile);
765
1007
  const pageName = jayHtmlFilename.replace(".jay-html", "");
@@ -785,6 +1027,8 @@ async function compileAndLoadServerElement(vite, jayHtmlContent, jayHtmlFilename
785
1027
  if (existingModule) {
786
1028
  vite.moduleGraph.invalidateModule(existingModule);
787
1029
  }
1030
+ const jayHtmlPath = path__default.join(serverElementDir, jayHtmlFilename);
1031
+ invalidateJayHtmlModules(vite, jayHtmlPath);
788
1032
  const serverModule = await vite.ssrLoadModule(serverElementPath);
789
1033
  let cssHref;
790
1034
  if (parsedJayFile.css) {
@@ -806,6 +1050,34 @@ async function compileAndLoadServerElement(vite, jayHtmlContent, jayHtmlFilename
806
1050
  cssHref
807
1051
  };
808
1052
  }
1053
+ function invalidateJayHtmlModules(vite, jayHtmlPath) {
1054
+ let count = 0;
1055
+ const byFile = vite.moduleGraph.getModulesByFile(jayHtmlPath);
1056
+ if (byFile) {
1057
+ for (const mod of byFile) {
1058
+ vite.moduleGraph.invalidateModule(mod);
1059
+ count++;
1060
+ }
1061
+ }
1062
+ const knownIds = [jayHtmlPath + ".ts", jayHtmlPath + JAY_QUERY_HYDRATE + ".ts"];
1063
+ for (const id of knownIds) {
1064
+ const mod = vite.moduleGraph.getModuleById(id);
1065
+ if (mod) {
1066
+ vite.moduleGraph.invalidateModule(mod);
1067
+ count++;
1068
+ }
1069
+ }
1070
+ const idMap = vite.moduleGraph.idToModuleMap;
1071
+ for (const [id, mod] of idMap) {
1072
+ if (id.includes(jayHtmlPath)) {
1073
+ vite.moduleGraph.invalidateModule(mod);
1074
+ count++;
1075
+ }
1076
+ }
1077
+ if (count > 0) {
1078
+ getLogger().info(`[SSR] Invalidated ${count} Vite module(s) for ${jayHtmlPath}`);
1079
+ }
1080
+ }
809
1081
  function generateHydrationScript(defaultViewState, fastCarryForward, parts, jayHtmlPath, trackByMap = {}, clientInitData2 = {}, projectInit, pluginInits = [], options = {}, asyncOutcomes = []) {
810
1082
  const {
811
1083
  partImports,
@@ -907,7 +1179,29 @@ async function loadPageParts(vite, route, pagesBase, projectBase, jayRollupConfi
907
1179
  contract: hi.contract,
908
1180
  contractPath: hi.contractPath
909
1181
  }));
910
- const discoveryResult = headlessInstanceComponents.length > 0 ? discoverHeadlessInstances(jayHtmlSource) : { instances: [], forEachInstances: [], preRenderedJayHtml: jayHtmlSource };
1182
+ const jayHtmlForDiscovery = injectHeadfullFSTemplates(
1183
+ jayHtmlSource,
1184
+ dirName,
1185
+ JAY_IMPORT_RESOLVER
1186
+ );
1187
+ let discoveryResult;
1188
+ if (headlessInstanceComponents.length > 0) {
1189
+ const firstDiscovery = discoverHeadlessInstances(jayHtmlForDiscovery);
1190
+ const headlessContractNameSet = new Set(
1191
+ jayHtml.headlessImports.map((hi) => hi.contractName)
1192
+ );
1193
+ const jayHtmlWithCoords = assignCoordinatesToJayHtml(
1194
+ firstDiscovery.preRenderedJayHtml,
1195
+ headlessContractNameSet
1196
+ );
1197
+ discoveryResult = discoverHeadlessInstances(jayHtmlWithCoords);
1198
+ } else {
1199
+ discoveryResult = {
1200
+ instances: [],
1201
+ forEachInstances: [],
1202
+ preRenderedJayHtml: jayHtmlSource
1203
+ };
1204
+ }
911
1205
  return {
912
1206
  parts,
913
1207
  serverTrackByMap: jayHtml.serverTrackByMap,
@@ -916,7 +1210,9 @@ async function loadPageParts(vite, route, pagesBase, projectBase, jayRollupConfi
916
1210
  headlessContracts,
917
1211
  headlessInstanceComponents,
918
1212
  discoveredInstances: discoveryResult.instances,
919
- forEachInstances: discoveryResult.forEachInstances
1213
+ forEachInstances: discoveryResult.forEachInstances,
1214
+ linkedCssFiles: jayHtml.linkedCssFiles ?? [],
1215
+ linkedComponentFiles: jayHtml.linkedComponentFiles ?? []
920
1216
  };
921
1217
  });
922
1218
  }
@@ -931,31 +1227,32 @@ async function slowRenderInstances(discovered, headlessInstanceComponents) {
931
1227
  const carryForwards = {};
932
1228
  for (const instance of discovered) {
933
1229
  const comp = componentByContractName.get(instance.contractName);
934
- if (!comp || !comp.compDefinition.slowlyRender) {
1230
+ if (!comp)
935
1231
  continue;
936
- }
937
1232
  const contractProps = comp.contract?.props ?? [];
938
1233
  const normalizedProps = {};
939
1234
  for (const [key, value] of Object.entries(instance.props)) {
940
1235
  const match = contractProps.find((p) => p.name.toLowerCase() === key.toLowerCase());
941
1236
  normalizedProps[match ? match.name : key] = value;
942
1237
  }
943
- const services = resolveServices(comp.compDefinition.services);
944
- const slowResult = await comp.compDefinition.slowlyRender(normalizedProps, ...services);
945
- if (slowResult.kind === "PhaseOutput") {
946
- const coordKey = instance.coordinate.join("/");
947
- resolvedData.push({
948
- coordinate: instance.coordinate,
949
- contract: comp.contract,
950
- slowViewState: slowResult.rendered
951
- });
952
- slowViewStates[coordKey] = slowResult.rendered;
953
- carryForwards[coordKey] = slowResult.carryForward;
954
- discoveredForFast.push({
955
- contractName: instance.contractName,
956
- props: normalizedProps,
957
- coordinate: instance.coordinate
958
- });
1238
+ discoveredForFast.push({
1239
+ contractName: instance.contractName,
1240
+ props: normalizedProps,
1241
+ coordinate: instance.coordinate
1242
+ });
1243
+ if (comp.compDefinition.slowlyRender) {
1244
+ const services = resolveServices(comp.compDefinition.services);
1245
+ const slowResult = await comp.compDefinition.slowlyRender(normalizedProps, ...services);
1246
+ if (slowResult.kind === "PhaseOutput") {
1247
+ const coordKey = instance.coordinate.join("/");
1248
+ resolvedData.push({
1249
+ coordinate: instance.coordinate,
1250
+ contract: comp.contract,
1251
+ slowViewState: slowResult.rendered
1252
+ });
1253
+ slowViewStates[coordKey] = slowResult.rendered;
1254
+ carryForwards[coordKey] = slowResult.carryForward;
1255
+ }
959
1256
  }
960
1257
  }
961
1258
  if (discoveredForFast.length === 0) {
@@ -964,7 +1261,7 @@ async function slowRenderInstances(discovered, headlessInstanceComponents) {
964
1261
  return {
965
1262
  resolvedData,
966
1263
  slowViewStates,
967
- instancePhaseData: { discovered: discoveredForFast, carryForwards }
1264
+ instancePhaseData: { discovered: discoveredForFast, carryForwards, slowViewStates }
968
1265
  };
969
1266
  }
970
1267
  function validateForEachInstances(forEachInstances, headlessInstanceComponents) {
@@ -1082,6 +1379,16 @@ class ActionRegistry {
1082
1379
  }
1083
1380
  };
1084
1381
  }
1382
+ if (action.isStreaming) {
1383
+ return {
1384
+ success: false,
1385
+ error: {
1386
+ code: "STREAMING_ACTION",
1387
+ message: `Action '${actionName}' is a streaming action — use executeStream() instead`,
1388
+ isActionError: false
1389
+ }
1390
+ };
1391
+ }
1085
1392
  try {
1086
1393
  const services = resolveServices(action.services);
1087
1394
  const result = await action.handler(input, ...services);
@@ -1135,6 +1442,40 @@ class ActionRegistry {
1135
1442
  }
1136
1443
  return parts.length > 0 ? parts.join(", ") : void 0;
1137
1444
  }
1445
+ // --- Streaming actions (DL#129) ---
1446
+ /**
1447
+ * Register a streaming action.
1448
+ */
1449
+ registerStream(action) {
1450
+ const entry = {
1451
+ actionName: action.actionName,
1452
+ method: "POST",
1453
+ isStreaming: true,
1454
+ services: action.services,
1455
+ handler: action.handler
1456
+ };
1457
+ this.actions.set(action.actionName, entry);
1458
+ }
1459
+ /**
1460
+ * Check if a registered action is a streaming action.
1461
+ */
1462
+ isStreaming(actionName) {
1463
+ const action = this.actions.get(actionName);
1464
+ return !!action?.isStreaming;
1465
+ }
1466
+ /**
1467
+ * Execute a streaming action, returning an async iterable of chunks.
1468
+ */
1469
+ async *executeStream(actionName, input) {
1470
+ const action = this.actions.get(actionName);
1471
+ if (!action || !action.isStreaming) {
1472
+ throw new Error(`Streaming action '${actionName}' not found`);
1473
+ }
1474
+ const services = resolveServices(action.services);
1475
+ for await (const chunk of action.handler(input, ...services)) {
1476
+ yield chunk;
1477
+ }
1478
+ }
1138
1479
  }
1139
1480
  const actionRegistry = new ActionRegistry();
1140
1481
  function registerAction(action) {
@@ -1258,6 +1599,15 @@ async function discoverAndRegisterActions(options) {
1258
1599
  `[Actions] Registered: ${exportValue.actionName}`
1259
1600
  );
1260
1601
  }
1602
+ } else if (isJayStreamAction(exportValue)) {
1603
+ registry.registerStream(exportValue);
1604
+ result.actionNames.push(exportValue.actionName);
1605
+ result.actionCount++;
1606
+ if (verbose) {
1607
+ getLogger().info(
1608
+ `[Actions] Registered stream: ${exportValue.actionName}`
1609
+ );
1610
+ }
1261
1611
  }
1262
1612
  }
1263
1613
  } catch (error) {
@@ -1414,6 +1764,13 @@ async function registerNpmPluginActions(packageName, pluginConfig, pluginDir, re
1414
1764
  if (verbose) {
1415
1765
  getLogger().info(`[Actions] Registered NPM plugin action: ${registeredName}`);
1416
1766
  }
1767
+ } else if (actionExport && isJayStreamAction(actionExport)) {
1768
+ registry.registerStream(actionExport);
1769
+ const registeredName = actionExport.actionName;
1770
+ registeredActions.push(registeredName);
1771
+ if (verbose) {
1772
+ getLogger().info(`[Actions] Registered NPM plugin stream: ${registeredName}`);
1773
+ }
1417
1774
  } else {
1418
1775
  getLogger().warn(
1419
1776
  `[Actions] NPM plugin "${packageName}" declares action "${actionName}" but it's not exported or not a JayAction`
@@ -1482,6 +1839,13 @@ async function discoverPluginActions(pluginPath, projectRoot, registry = actionR
1482
1839
  if (verbose) {
1483
1840
  getLogger().info(`[Actions] Registered plugin action: ${registeredName}`);
1484
1841
  }
1842
+ } else if (actionExport && isJayStreamAction(actionExport)) {
1843
+ registry.registerStream(actionExport);
1844
+ const registeredName = actionExport.actionName;
1845
+ registeredActions.push(registeredName);
1846
+ if (verbose) {
1847
+ getLogger().info(`[Actions] Registered plugin stream: ${registeredName}`);
1848
+ }
1485
1849
  } else {
1486
1850
  getLogger().warn(
1487
1851
  `[Actions] Plugin "${pluginName}" declares action "${actionName}" but it's not exported or not a JayAction`
@@ -2153,11 +2517,40 @@ async function materializeContracts(options, services = /* @__PURE__ */ new Map(
2153
2517
  const { manifest } = plugin;
2154
2518
  const pluginRelPath = path.relative(projectRoot, plugin.pluginPath);
2155
2519
  if (!pluginsIndexMap.has(plugin.name)) {
2156
- pluginsIndexMap.set(plugin.name, {
2520
+ const entry = {
2157
2521
  path: "./" + pluginRelPath.replace(/\\/g, "/"),
2158
2522
  contracts: [],
2159
2523
  actions: []
2160
- });
2524
+ };
2525
+ if (manifest.services?.length) {
2526
+ entry.services = manifest.services.map((s2) => {
2527
+ const docPath = s2.doc ? "./" + path.relative(projectRoot, path.resolve(plugin.pluginPath, s2.doc)) : void 0;
2528
+ return {
2529
+ name: s2.name,
2530
+ marker: s2.marker,
2531
+ ...s2.description && { description: s2.description },
2532
+ ...docPath && { doc: docPath }
2533
+ };
2534
+ });
2535
+ }
2536
+ if (manifest.contexts?.length) {
2537
+ entry.contexts = manifest.contexts.map((c) => {
2538
+ const docPath = c.doc ? "./" + path.relative(projectRoot, path.resolve(plugin.pluginPath, c.doc)) : void 0;
2539
+ return {
2540
+ name: c.name,
2541
+ marker: c.marker,
2542
+ ...c.description && { description: c.description },
2543
+ ...docPath && { doc: docPath }
2544
+ };
2545
+ });
2546
+ }
2547
+ if (manifest.routes?.length) {
2548
+ entry.routes = manifest.routes.map((r) => ({
2549
+ path: r.path,
2550
+ ...r.description && { description: r.description }
2551
+ }));
2552
+ }
2553
+ pluginsIndexMap.set(plugin.name, entry);
2161
2554
  }
2162
2555
  if (!dynamicOnly && manifest.contracts) {
2163
2556
  for (const contract of manifest.contracts) {
@@ -2167,8 +2560,20 @@ async function materializeContracts(options, services = /* @__PURE__ */ new Map(
2167
2560
  projectRoot
2168
2561
  );
2169
2562
  const relativePath = path.relative(projectRoot, contractPath);
2563
+ let description = contract.description;
2564
+ if (!description) {
2565
+ try {
2566
+ const contractContent = fs.readFileSync(contractPath, "utf-8");
2567
+ const parsed = YAML.parse(contractContent);
2568
+ if (parsed?.description && typeof parsed.description === "string") {
2569
+ description = parsed.description;
2570
+ }
2571
+ } catch {
2572
+ }
2573
+ }
2170
2574
  pluginsIndexMap.get(plugin.name).contracts.push({
2171
2575
  name: contract.name,
2576
+ ...description && { description },
2172
2577
  type: "static",
2173
2578
  path: "./" + relativePath
2174
2579
  });
@@ -2202,8 +2607,17 @@ async function materializeContracts(options, services = /* @__PURE__ */ new Map(
2202
2607
  const filePath = path.join(pluginOutputDir, fileName);
2203
2608
  fs.writeFileSync(filePath, generated.yaml, "utf-8");
2204
2609
  const relativePath = path.relative(projectRoot, filePath);
2610
+ let dynDescription;
2611
+ try {
2612
+ const parsedYaml = YAML.parse(generated.yaml);
2613
+ if (parsedYaml?.description && typeof parsedYaml.description === "string") {
2614
+ dynDescription = parsedYaml.description;
2615
+ }
2616
+ } catch {
2617
+ }
2205
2618
  const contractEntry = {
2206
2619
  name: fullName,
2620
+ ...dynDescription && { description: dynDescription },
2207
2621
  type: "dynamic",
2208
2622
  path: "./" + relativePath,
2209
2623
  ...generated.metadata && { metadata: generated.metadata }
@@ -2239,7 +2653,10 @@ async function materializeContracts(options, services = /* @__PURE__ */ new Map(
2239
2653
  if (!metadata)
2240
2654
  continue;
2241
2655
  const actionRelPath = path.relative(projectRoot, metadataFilePath);
2242
- pluginsIndexMap.get(plugin.name).actions.push({
2656
+ const pluginEntry = pluginsIndexMap.get(plugin.name);
2657
+ if (!pluginEntry.actions)
2658
+ pluginEntry.actions = [];
2659
+ pluginEntry.actions.push({
2243
2660
  name: metadata.name,
2244
2661
  description: metadata.description,
2245
2662
  path: "./" + actionRelPath.replace(/\\/g, "/")
@@ -2255,7 +2672,10 @@ async function materializeContracts(options, services = /* @__PURE__ */ new Map(
2255
2672
  name,
2256
2673
  path: data.path,
2257
2674
  contracts: data.contracts,
2258
- ...data.actions.length > 0 && { actions: data.actions }
2675
+ ...data.actions && data.actions.length > 0 && { actions: data.actions },
2676
+ ...data.services?.length && { services: data.services },
2677
+ ...data.contexts?.length && { contexts: data.contexts },
2678
+ ...data.routes?.length && { routes: data.routes }
2259
2679
  }))
2260
2680
  };
2261
2681
  fs.mkdirSync(outputDir, { recursive: true });
@@ -2286,10 +2706,39 @@ async function listContracts(options) {
2286
2706
  const { manifest } = plugin;
2287
2707
  const pluginRelPath = path.relative(projectRoot, plugin.pluginPath);
2288
2708
  if (!pluginsMap.has(plugin.name)) {
2289
- pluginsMap.set(plugin.name, {
2709
+ const entry = {
2290
2710
  path: "./" + pluginRelPath.replace(/\\/g, "/"),
2291
2711
  contracts: []
2292
- });
2712
+ };
2713
+ if (manifest.services?.length) {
2714
+ entry.services = manifest.services.map((s2) => {
2715
+ const docPath = s2.doc ? "./" + path.relative(projectRoot, path.resolve(plugin.pluginPath, s2.doc)) : void 0;
2716
+ return {
2717
+ name: s2.name,
2718
+ marker: s2.marker,
2719
+ ...s2.description && { description: s2.description },
2720
+ ...docPath && { doc: docPath }
2721
+ };
2722
+ });
2723
+ }
2724
+ if (manifest.contexts?.length) {
2725
+ entry.contexts = manifest.contexts.map((c) => {
2726
+ const docPath = c.doc ? "./" + path.relative(projectRoot, path.resolve(plugin.pluginPath, c.doc)) : void 0;
2727
+ return {
2728
+ name: c.name,
2729
+ marker: c.marker,
2730
+ ...c.description && { description: c.description },
2731
+ ...docPath && { doc: docPath }
2732
+ };
2733
+ });
2734
+ }
2735
+ if (manifest.routes?.length) {
2736
+ entry.routes = manifest.routes.map((r) => ({
2737
+ path: r.path,
2738
+ ...r.description && { description: r.description }
2739
+ }));
2740
+ }
2741
+ pluginsMap.set(plugin.name, entry);
2293
2742
  }
2294
2743
  if (!dynamicOnly && manifest.contracts) {
2295
2744
  for (const contract of manifest.contracts) {
@@ -2299,8 +2748,20 @@ async function listContracts(options) {
2299
2748
  projectRoot
2300
2749
  );
2301
2750
  const relativePath = path.relative(projectRoot, contractPath);
2751
+ let listDescription = contract.description;
2752
+ if (!listDescription) {
2753
+ try {
2754
+ const contractContent = fs.readFileSync(contractPath, "utf-8");
2755
+ const parsed = YAML.parse(contractContent);
2756
+ if (parsed?.description && typeof parsed.description === "string") {
2757
+ listDescription = parsed.description;
2758
+ }
2759
+ } catch {
2760
+ }
2761
+ }
2302
2762
  pluginsMap.get(plugin.name).contracts.push({
2303
2763
  name: contract.name,
2764
+ ...listDescription && { description: listDescription },
2304
2765
  type: "static",
2305
2766
  path: "./" + relativePath
2306
2767
  });
@@ -2321,7 +2782,10 @@ async function listContracts(options) {
2321
2782
  plugins: Array.from(pluginsMap.entries()).map(([name, data]) => ({
2322
2783
  name,
2323
2784
  path: data.path,
2324
- contracts: data.contracts
2785
+ contracts: data.contracts,
2786
+ ...data.services?.length && { services: data.services },
2787
+ ...data.contexts?.length && { contexts: data.contexts },
2788
+ ...data.routes?.length && { routes: data.routes }
2325
2789
  }))
2326
2790
  };
2327
2791
  }
@@ -2475,6 +2939,7 @@ export {
2475
2939
  executePluginServerInits,
2476
2940
  executePluginSetup,
2477
2941
  generateClientScript,
2942
+ generateFrozenPageHtml,
2478
2943
  generatePromiseReconstruction,
2479
2944
  generateSSRPageHtml,
2480
2945
  getActionCacheHeaders,
@@ -2491,6 +2956,7 @@ export {
2491
2956
  loadActionMetadata,
2492
2957
  loadPageParts,
2493
2958
  materializeContracts,
2959
+ mergeHeadTags,
2494
2960
  onInit,
2495
2961
  onShutdown,
2496
2962
  parseActionMetadata,
@@ -2504,10 +2970,11 @@ export {
2504
2970
  runInitCallbacks,
2505
2971
  runLoadParams,
2506
2972
  runShutdownCallbacks,
2507
- runSlowlyChangingRender,
2508
2973
  scanPlugins,
2974
+ serializeHeadTags,
2509
2975
  setClientInitData,
2510
2976
  slowRenderInstances,
2511
2977
  sortPluginsByDependencies,
2978
+ tagIdentityKey,
2512
2979
  validateForEachInstances
2513
2980
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jay-framework/stack-server-runtime",
3
- "version": "0.15.5",
3
+ "version": "0.16.0",
4
4
  "license": "Apache-2.0",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.mts",
@@ -26,21 +26,21 @@
26
26
  "test:watch": "vitest"
27
27
  },
28
28
  "dependencies": {
29
- "@jay-framework/compiler-jay-html": "^0.15.5",
30
- "@jay-framework/compiler-shared": "^0.15.5",
31
- "@jay-framework/component": "^0.15.5",
32
- "@jay-framework/fullstack-component": "^0.15.5",
33
- "@jay-framework/logger": "^0.15.5",
34
- "@jay-framework/runtime": "^0.15.5",
35
- "@jay-framework/ssr-runtime": "^0.15.5",
36
- "@jay-framework/stack-route-scanner": "^0.15.5",
37
- "@jay-framework/view-state-merge": "^0.15.5",
29
+ "@jay-framework/compiler-jay-html": "^0.16.0",
30
+ "@jay-framework/compiler-shared": "^0.16.0",
31
+ "@jay-framework/component": "^0.16.0",
32
+ "@jay-framework/fullstack-component": "^0.16.0",
33
+ "@jay-framework/logger": "^0.16.0",
34
+ "@jay-framework/runtime": "^0.16.0",
35
+ "@jay-framework/ssr-runtime": "^0.16.0",
36
+ "@jay-framework/stack-route-scanner": "^0.16.0",
37
+ "@jay-framework/view-state-merge": "^0.16.0",
38
38
  "yaml": "^2.3.4"
39
39
  },
40
40
  "devDependencies": {
41
- "@jay-framework/dev-environment": "^0.15.5",
42
- "@jay-framework/jay-cli": "^0.15.5",
43
- "@jay-framework/stack-client-runtime": "^0.15.5",
41
+ "@jay-framework/dev-environment": "^0.16.0",
42
+ "@jay-framework/jay-cli": "^0.16.0",
43
+ "@jay-framework/stack-client-runtime": "^0.16.0",
44
44
  "@types/express": "^5.0.2",
45
45
  "@types/node": "^22.15.21",
46
46
  "nodemon": "^3.0.3",