@jay-framework/stack-server-runtime 0.15.4 → 0.15.6

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 +55 -3
  2. package/dist/index.js +311 -56
  3. package/package.json +13 -13
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { AnyJayStackComponentDefinition, PageProps, AnySlowlyRenderResult, UrlParams, JayStackComponentDefinition, AnyFastRenderResult, HttpMethod, CacheOptions, JayAction, JayActionDefinition, ServiceMarker } from '@jay-framework/fullstack-component';
1
+ import { AnyJayStackComponentDefinition, PageProps, AnySlowlyRenderResult, UrlParams, JayStackComponentDefinition, AnyFastRenderResult, HttpMethod, CacheOptions, JayAction, JayActionDefinition, HeadTag, ServiceMarker } from '@jay-framework/fullstack-component';
2
2
  import { JayComponentCore } from '@jay-framework/component';
3
3
  import { ViteDevServer } from 'vite';
4
4
  import { JayRoute } from '@jay-framework/stack-route-scanner';
@@ -49,6 +49,10 @@ interface LoadedPageParts {
49
49
  discoveredInstances: DiscoveredHeadlessInstance[];
50
50
  /** Discovered forEach <jay:xxx> instances from the jay-html (DL#109) */
51
51
  forEachInstances: ForEachHeadlessInstance[];
52
+ /** Absolute paths to linked CSS files (from <link rel="stylesheet">) for dev-server watching */
53
+ linkedCssFiles: string[];
54
+ /** Absolute paths to headfull FS component jay-html files for dev-server watching */
55
+ linkedComponentFiles: string[];
52
56
  }
53
57
  interface LoadPagePartsOptions {
54
58
  /**
@@ -97,6 +101,8 @@ interface InstancePhaseData {
97
101
  }>;
98
102
  /** CarryForward per instance (keyed by coordinate path, e.g. "p1/product-card:0") */
99
103
  carryForwards: Record<string, object>;
104
+ /** Slow ViewState per instance (keyed by coordinate path) */
105
+ slowViewStates?: Record<string, object>;
100
106
  /** ForEach instances that need fast-phase per-item rendering */
101
107
  forEachInstances?: ForEachHeadlessInstance[];
102
108
  }
@@ -626,7 +632,11 @@ declare function clearServerElementCache(): void;
626
632
  * 3. Build hydration script (uses ?jay-hydrate query for hydrate target)
627
633
  * 4. Return full HTML page string
628
634
  */
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>;
635
+ 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,
636
+ /** Source directory for headfull FS file resolution when jayHtmlDir is pre-rendered */
637
+ sourceDir?: string,
638
+ /** Head tags to inject into <head> during SSR (Design Log #127) */
639
+ headTags?: HeadTag[]): Promise<string>;
630
640
 
631
641
  /**
632
642
  * Service registry for Jay Stack server-side dependency injection.
@@ -906,10 +916,25 @@ interface ActionIndexEntry {
906
916
  /** Contract entry within a plugin in plugins-index.yaml */
907
917
  interface PluginContractEntry {
908
918
  name: string;
919
+ description?: string;
909
920
  type: 'static' | 'dynamic';
910
921
  path: string;
911
922
  metadata?: Record<string, unknown>;
912
923
  }
924
+ /** Service entry in plugins-index.yaml (DL#125) */
925
+ interface ServiceIndexEntry {
926
+ name: string;
927
+ marker: string;
928
+ description?: string;
929
+ doc?: string;
930
+ }
931
+ /** Context entry in plugins-index.yaml (DL#125) */
932
+ interface ContextIndexEntry {
933
+ name: string;
934
+ marker: string;
935
+ description?: string;
936
+ doc?: string;
937
+ }
913
938
  /** Entry for plugins-index.yaml (Design Log #85) */
914
939
  interface PluginsIndexEntry {
915
940
  name: string;
@@ -917,6 +942,10 @@ interface PluginsIndexEntry {
917
942
  contracts: PluginContractEntry[];
918
943
  /** Actions with .jay-action metadata (exposed to AI agents) */
919
944
  actions?: ActionIndexEntry[];
945
+ /** Server-side services provided by this plugin (DL#125) */
946
+ services?: ServiceIndexEntry[];
947
+ /** Client-side contexts provided by this plugin (DL#125) */
948
+ contexts?: ContextIndexEntry[];
920
949
  }
921
950
  interface PluginsIndex {
922
951
  plugins: PluginsIndexEntry[];
@@ -1150,4 +1179,27 @@ declare function executePluginReferences(plugin: PluginWithReferences, options:
1150
1179
  verbose?: boolean;
1151
1180
  }): Promise<PluginReferencesResult>;
1152
1181
 
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 };
1182
+ /**
1183
+ * Head tag utilities for SSR head injection (Design Log #127).
1184
+ *
1185
+ * Components declare HeadTag[] via phaseOutput(). The SSR pipeline collects
1186
+ * tags from all sources, deduplicates with last-write-wins + collision warning,
1187
+ * and serializes to HTML for injection into <head>.
1188
+ */
1189
+
1190
+ /**
1191
+ * Compute a unique identity key for deduplication.
1192
+ * Returns undefined for tags that should always be included (no dedup).
1193
+ */
1194
+ declare function tagIdentityKey(tag: HeadTag): string | undefined;
1195
+ /**
1196
+ * Merge head tags from multiple sources with last-write-wins.
1197
+ * Warns on collision via logger.
1198
+ */
1199
+ declare function mergeHeadTags(sources: HeadTag[][]): HeadTag[];
1200
+ /**
1201
+ * Serialize an array of HeadTag objects into an HTML string.
1202
+ */
1203
+ declare function serializeHeadTags(tags: HeadTag[]): string;
1204
+
1205
+ 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 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, 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, runSlowlyChangingRender, scanPlugins, serializeHeadTags, setClientInitData, slowRenderInstances, sortPluginsByDependencies, tagIdentityKey, validateForEachInstances };
package/dist/index.js CHANGED
@@ -11,14 +11,14 @@ 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
- import { getLogger } from "@jay-framework/logger";
19
+ import crypto from "node:crypto";
19
20
  import * as fs from "node:fs";
20
21
  import { createRequire as createRequire$1 } from "node:module";
21
- import crypto from "node:crypto";
22
22
  const serviceRegistry = /* @__PURE__ */ new Map();
23
23
  function registerService(marker, service) {
24
24
  serviceRegistry.set(marker, service);
@@ -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,6 +172,9 @@ 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
  }
@@ -203,7 +213,7 @@ new JayObjectType("Error", {
203
213
  stack: new JayAtomicType("string")
204
214
  });
205
215
  function isOptionalType(aType) {
206
- return aType.kind === 13;
216
+ return aType.kind === 14;
207
217
  }
208
218
  function isAtomicType(aType) {
209
219
  return aType.kind === 0;
@@ -220,6 +230,9 @@ function isObjectType(aType) {
220
230
  function isArrayType(aType) {
221
231
  return aType.kind === 9;
222
232
  }
233
+ function isRecordType(aType) {
234
+ return aType.kind === 11;
235
+ }
223
236
  function jayTypeToJsonSchema(type) {
224
237
  if (isOptionalType(type)) {
225
238
  return jayTypeToJsonSchema(type.innerType);
@@ -244,6 +257,13 @@ function jayTypeToJsonSchema(type) {
244
257
  }
245
258
  return { type: "array" };
246
259
  }
260
+ if (isRecordType(type)) {
261
+ const valueSchema = jayTypeToJsonSchema(type.itemType);
262
+ if (valueSchema) {
263
+ return { type: "object", additionalProperties: valueSchema };
264
+ }
265
+ return { type: "object" };
266
+ }
247
267
  if (isObjectType(type)) {
248
268
  const properties = {};
249
269
  const required = [];
@@ -368,6 +388,7 @@ function resolveBinding(binding, item) {
368
388
  async function renderFastChangingData(pageParams, pageProps, carryForward, parts, instancePhaseData, forEachInstances, headlessInstanceComponents, mergedSlowViewState, query = {}) {
369
389
  let fastViewState = {};
370
390
  let fastCarryForward = {};
391
+ const fastHeadTagSources = [];
371
392
  for (const part of parts) {
372
393
  const { compDefinition, key, contractInfo } = part;
373
394
  if (compDefinition.fastRender) {
@@ -395,6 +416,9 @@ async function renderFastChangingData(pageParams, pageProps, carryForward, parts
395
416
  fastViewState[key] = fastRenderedPart.rendered;
396
417
  fastCarryForward[key] = fastRenderedPart.carryForward;
397
418
  }
419
+ if (fastRenderedPart.headTags) {
420
+ fastHeadTagSources.push(fastRenderedPart.headTags);
421
+ }
398
422
  } else
399
423
  return fastRenderedPart;
400
424
  }
@@ -409,18 +433,25 @@ async function renderFastChangingData(pageParams, pageProps, carryForward, parts
409
433
  for (const instance of instancePhaseData.discovered) {
410
434
  const coordKey = instance.coordinate.join("/");
411
435
  const comp = componentByContractName.get(instance.contractName);
412
- if (!comp || !comp.compDefinition.fastRender)
436
+ if (!comp)
413
437
  continue;
414
- const services = resolveServices(comp.compDefinition.services);
415
- const cf = instancePhaseData.carryForwards[coordKey];
416
- const instanceProps = { ...instance.props, query };
417
- const fastResult = comp.compDefinition.slowlyRender ? await comp.compDefinition.fastRender(instanceProps, cf, ...services) : await comp.compDefinition.fastRender(instanceProps, ...services);
418
- if (fastResult.kind === "PhaseOutput") {
419
- const instanceSlowVS = carryForward?.__instanceSlowViewStates?.[coordKey];
420
- instanceViewStates[coordKey] = instanceSlowVS ? { ...instanceSlowVS, ...fastResult.rendered } : fastResult.rendered;
421
- if (fastResult.carryForward) {
422
- instanceCarryForwards[coordKey] = fastResult.carryForward;
438
+ const instanceSlowVS = instancePhaseData.slowViewStates?.[coordKey] ?? carryForward?.__instanceSlowViewStates?.[coordKey];
439
+ if (comp.compDefinition.fastRender) {
440
+ const services = resolveServices(comp.compDefinition.services);
441
+ const cf = instancePhaseData.carryForwards[coordKey];
442
+ const instanceProps = { ...instance.props, query };
443
+ const fastResult = comp.compDefinition.slowlyRender ? await comp.compDefinition.fastRender(instanceProps, cf, ...services) : await comp.compDefinition.fastRender(instanceProps, ...services);
444
+ if (fastResult.kind === "PhaseOutput") {
445
+ instanceViewStates[coordKey] = instanceSlowVS ? { ...instanceSlowVS, ...fastResult.rendered } : fastResult.rendered;
446
+ if (fastResult.carryForward) {
447
+ instanceCarryForwards[coordKey] = fastResult.carryForward;
448
+ }
449
+ if (fastResult.headTags) {
450
+ fastHeadTagSources.push(fastResult.headTags);
451
+ }
423
452
  }
453
+ } else {
454
+ instanceViewStates[coordKey] = instanceSlowVS ?? {};
424
455
  }
425
456
  }
426
457
  }
@@ -481,7 +512,11 @@ async function renderFastChangingData(pageParams, pageProps, carryForward, parts
481
512
  if (Object.keys(instanceCarryForwards).length > 0) {
482
513
  fastCarryForward.__headlessInstances = instanceCarryForwards;
483
514
  }
484
- return Promise.resolve(phaseOutput(fastViewState, fastCarryForward));
515
+ const result = phaseOutput(fastViewState, fastCarryForward);
516
+ if (fastHeadTagSources.length > 0) {
517
+ result.headTags = fastHeadTagSources.flat();
518
+ }
519
+ return Promise.resolve(result);
485
520
  }
486
521
  function generatePromiseReconstruction(outcomes) {
487
522
  if (outcomes.length === 0)
@@ -625,7 +660,8 @@ async function generateClientScript(defaultViewState, fastCarryForward, parts, j
625
660
  import { render } from '${jayHtmlPath}';
626
661
  ${partImports}${slowViewStateDecl}
627
662
  const viewState = ${JSON.stringify(defaultViewState)};
628
- ${generatePromiseReconstruction(outcomes)} const fastCarryForward = ${JSON.stringify(fastCarryForward)};
663
+ ${generatePromiseReconstruction(outcomes)}
664
+ const fastCarryForward = ${JSON.stringify(fastCarryForward)};
629
665
  const trackByMap = ${JSON.stringify(trackByMap)};
630
666
  ${clientInitExecution}
631
667
  const target = document.getElementById('target');
@@ -641,6 +677,65 @@ function asyncSwapScript(id, html) {
641
677
  const escapedHtml = html.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
642
678
  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>`;
643
679
  }
680
+ function tagIdentityKey(tag) {
681
+ const t = tag.tag.toLowerCase();
682
+ if (t === "title")
683
+ return "title";
684
+ if (t === "meta") {
685
+ if (tag.attrs?.name)
686
+ return `meta:name:${tag.attrs.name}`;
687
+ if (tag.attrs?.property)
688
+ return `meta:property:${tag.attrs.property}`;
689
+ if (tag.attrs?.charset !== void 0)
690
+ return "meta:charset";
691
+ return void 0;
692
+ }
693
+ if (t === "link") {
694
+ if (tag.attrs?.rel === "canonical")
695
+ return "link:canonical";
696
+ return void 0;
697
+ }
698
+ return void 0;
699
+ }
700
+ function mergeHeadTags(sources) {
701
+ const byKey = /* @__PURE__ */ new Map();
702
+ const result = [];
703
+ for (let si = 0; si < sources.length; si++) {
704
+ for (const tag of sources[si]) {
705
+ const key = tagIdentityKey(tag);
706
+ if (key) {
707
+ const existing = byKey.get(key);
708
+ if (existing && existing.sourceIndex !== si) {
709
+ getLogger().warn(
710
+ `[head-tags] Collision on "${key}" — overwriting with tag from source ${si}`
711
+ );
712
+ }
713
+ byKey.set(key, { tag, sourceIndex: si });
714
+ } else {
715
+ result.push(tag);
716
+ }
717
+ }
718
+ }
719
+ return [...[...byKey.values()].map((v) => v.tag), ...result];
720
+ }
721
+ function escapeAttr(value) {
722
+ return value.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
723
+ }
724
+ function escapeHtml(value) {
725
+ return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
726
+ }
727
+ const VOID_ELEMENTS = /* @__PURE__ */ new Set(["meta", "link", "base", "br", "hr", "img", "input"]);
728
+ function serializeHeadTags(tags) {
729
+ return tags.map((tag) => {
730
+ const t = tag.tag.toLowerCase();
731
+ const attrs = tag.attrs ? Object.entries(tag.attrs).map(([k, v]) => ` ${k}="${escapeAttr(v)}"`).join("") : "";
732
+ if (VOID_ELEMENTS.has(t)) {
733
+ return ` <${t}${attrs} />`;
734
+ }
735
+ const children = tag.children ? escapeHtml(tag.children) : "";
736
+ return ` <${t}${attrs}>${children}</${t}>`;
737
+ }).join("\n");
738
+ }
644
739
  const serverModuleCache = /* @__PURE__ */ new Map();
645
740
  function invalidateServerElementCache(jayHtmlPath) {
646
741
  if (serverModuleCache.delete(jayHtmlPath)) {
@@ -650,7 +745,7 @@ function invalidateServerElementCache(jayHtmlPath) {
650
745
  function clearServerElementCache() {
651
746
  serverModuleCache.clear();
652
747
  }
653
- async function generateSSRPageHtml(vite, jayHtmlContent, jayHtmlFilename, jayHtmlDir, viewState, jayHtmlImportPath, parts, carryForward, trackByMap = {}, clientInitData2 = {}, buildFolder, projectRoot, routeDir, tsConfigFilePath, projectInit, pluginInits = [], options = {}) {
748
+ async function generateSSRPageHtml(vite, jayHtmlContent, jayHtmlFilename, jayHtmlDir, viewState, jayHtmlImportPath, parts, carryForward, trackByMap = {}, clientInitData2 = {}, buildFolder, projectRoot, routeDir, tsConfigFilePath, projectInit, pluginInits = [], options = {}, sourceDir, headTags) {
654
749
  const jayHtmlPath = path__default.join(jayHtmlDir, jayHtmlFilename);
655
750
  let cached = serverModuleCache.get(jayHtmlPath);
656
751
  if (!cached) {
@@ -662,7 +757,8 @@ async function generateSSRPageHtml(vite, jayHtmlContent, jayHtmlFilename, jayHtm
662
757
  buildFolder,
663
758
  projectRoot,
664
759
  routeDir,
665
- tsConfigFilePath
760
+ tsConfigFilePath,
761
+ sourceDir
666
762
  );
667
763
  serverModuleCache.set(jayHtmlPath, cached);
668
764
  }
@@ -717,17 +813,17 @@ async function generateSSRPageHtml(vite, jayHtmlContent, jayHtmlFilename, jayHtm
717
813
  const attrs = Object.entries(link.attributes).map(([k, v]) => ` ${k}="${v}"`).join("");
718
814
  return ` <link rel="${link.rel}" href="${link.href}"${attrs} />`;
719
815
  }).join("\n");
720
- const inlineCss = cached.css ? ` <style>
721
- ${cached.css}
722
- </style>` : "";
723
- const headExtras = [headLinksHtml, inlineCss].filter((_) => _).join("\n");
816
+ const cssLink = cached.cssHref ? ` <link rel="stylesheet" href="${cached.cssHref}" />` : "";
817
+ const headTagsHtml = headTags && headTags.length > 0 ? serializeHeadTags(headTags) : "";
818
+ const hasCustomTitle = headTags?.some((t) => t.tag.toLowerCase() === "title");
819
+ const titleHtml = hasCustomTitle ? "" : " <title>Vite + TS</title>\n";
820
+ const headExtras = [headLinksHtml, cssLink, headTagsHtml].filter((_) => _).join("\n");
724
821
  return `<!doctype html>
725
822
  <html lang="en">
726
823
  <head>
727
824
  <meta charset="UTF-8" />
728
825
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
729
- <title>Vite + TS</title>
730
- ${headExtras ? headExtras + "\n" : ""} </head>
826
+ ${titleHtml}${headExtras ? headExtras + "\n" : ""} </head>
731
827
  <body>
732
828
  <div id="target">${ssrHtml}</div>${asyncScripts}
733
829
  ${hydrationScript}
@@ -744,14 +840,15 @@ function rebaseRelativeImports(code, fromDir, toDir) {
744
840
  return `from "${newRelPath}"`;
745
841
  });
746
842
  }
747
- async function compileAndLoadServerElement(vite, jayHtmlContent, jayHtmlFilename, jayHtmlDir, buildFolder, projectRoot, routeDir, tsConfigFilePath) {
843
+ async function compileAndLoadServerElement(vite, jayHtmlContent, jayHtmlFilename, jayHtmlDir, buildFolder, projectRoot, routeDir, tsConfigFilePath, sourceDir) {
748
844
  const jayFile = await parseJayFile(
749
845
  jayHtmlContent,
750
846
  jayHtmlFilename,
751
847
  jayHtmlDir,
752
848
  { relativePath: tsConfigFilePath },
753
849
  JAY_IMPORT_RESOLVER,
754
- projectRoot
850
+ projectRoot,
851
+ sourceDir
755
852
  );
756
853
  const parsedJayFile = checkValidationErrors(jayFile);
757
854
  const pageName = jayHtmlFilename.replace(".jay-html", "");
@@ -777,13 +874,57 @@ async function compileAndLoadServerElement(vite, jayHtmlContent, jayHtmlFilename
777
874
  if (existingModule) {
778
875
  vite.moduleGraph.invalidateModule(existingModule);
779
876
  }
877
+ const jayHtmlPath = path__default.join(serverElementDir, jayHtmlFilename);
878
+ invalidateJayHtmlModules(vite, jayHtmlPath);
780
879
  const serverModule = await vite.ssrLoadModule(serverElementPath);
880
+ let cssHref;
881
+ if (parsedJayFile.css) {
882
+ const cssFilename = jayHtmlFilename.replace(".jay-html", ".css");
883
+ const cssPath = path__default.join(serverElementDir, cssFilename);
884
+ await fs$2.writeFile(cssPath, parsedJayFile.css, "utf-8");
885
+ const cssModules = vite.moduleGraph.getModulesByFile(cssPath);
886
+ if (cssModules) {
887
+ for (const mod of cssModules) {
888
+ vite.moduleGraph.invalidateModule(mod);
889
+ }
890
+ }
891
+ const hash = crypto.createHash("md5").update(parsedJayFile.css).digest("hex").slice(0, 8);
892
+ cssHref = "/@fs" + cssPath + "?v=" + hash + "&direct";
893
+ }
781
894
  return {
782
895
  renderToStream: serverModule.renderToStream,
783
896
  headLinks: parsedJayFile.headLinks,
784
- css: parsedJayFile.css
897
+ cssHref
785
898
  };
786
899
  }
900
+ function invalidateJayHtmlModules(vite, jayHtmlPath) {
901
+ let count = 0;
902
+ const byFile = vite.moduleGraph.getModulesByFile(jayHtmlPath);
903
+ if (byFile) {
904
+ for (const mod of byFile) {
905
+ vite.moduleGraph.invalidateModule(mod);
906
+ count++;
907
+ }
908
+ }
909
+ const knownIds = [jayHtmlPath + ".ts", jayHtmlPath + JAY_QUERY_HYDRATE + ".ts"];
910
+ for (const id of knownIds) {
911
+ const mod = vite.moduleGraph.getModuleById(id);
912
+ if (mod) {
913
+ vite.moduleGraph.invalidateModule(mod);
914
+ count++;
915
+ }
916
+ }
917
+ const idMap = vite.moduleGraph.idToModuleMap;
918
+ for (const [id, mod] of idMap) {
919
+ if (id.includes(jayHtmlPath)) {
920
+ vite.moduleGraph.invalidateModule(mod);
921
+ count++;
922
+ }
923
+ }
924
+ if (count > 0) {
925
+ getLogger().info(`[SSR] Invalidated ${count} Vite module(s) for ${jayHtmlPath}`);
926
+ }
927
+ }
787
928
  function generateHydrationScript(defaultViewState, fastCarryForward, parts, jayHtmlPath, trackByMap = {}, clientInitData2 = {}, projectInit, pluginInits = [], options = {}, asyncOutcomes = []) {
788
929
  const {
789
930
  partImports,
@@ -885,7 +1026,29 @@ async function loadPageParts(vite, route, pagesBase, projectBase, jayRollupConfi
885
1026
  contract: hi.contract,
886
1027
  contractPath: hi.contractPath
887
1028
  }));
888
- const discoveryResult = headlessInstanceComponents.length > 0 ? discoverHeadlessInstances(jayHtmlSource) : { instances: [], forEachInstances: [], preRenderedJayHtml: jayHtmlSource };
1029
+ const jayHtmlForDiscovery = injectHeadfullFSTemplates(
1030
+ jayHtmlSource,
1031
+ dirName,
1032
+ JAY_IMPORT_RESOLVER
1033
+ );
1034
+ let discoveryResult;
1035
+ if (headlessInstanceComponents.length > 0) {
1036
+ const firstDiscovery = discoverHeadlessInstances(jayHtmlForDiscovery);
1037
+ const headlessContractNameSet = new Set(
1038
+ jayHtml.headlessImports.map((hi) => hi.contractName)
1039
+ );
1040
+ const jayHtmlWithCoords = assignCoordinatesToJayHtml(
1041
+ firstDiscovery.preRenderedJayHtml,
1042
+ headlessContractNameSet
1043
+ );
1044
+ discoveryResult = discoverHeadlessInstances(jayHtmlWithCoords);
1045
+ } else {
1046
+ discoveryResult = {
1047
+ instances: [],
1048
+ forEachInstances: [],
1049
+ preRenderedJayHtml: jayHtmlSource
1050
+ };
1051
+ }
889
1052
  return {
890
1053
  parts,
891
1054
  serverTrackByMap: jayHtml.serverTrackByMap,
@@ -894,7 +1057,9 @@ async function loadPageParts(vite, route, pagesBase, projectBase, jayRollupConfi
894
1057
  headlessContracts,
895
1058
  headlessInstanceComponents,
896
1059
  discoveredInstances: discoveryResult.instances,
897
- forEachInstances: discoveryResult.forEachInstances
1060
+ forEachInstances: discoveryResult.forEachInstances,
1061
+ linkedCssFiles: jayHtml.linkedCssFiles ?? [],
1062
+ linkedComponentFiles: jayHtml.linkedComponentFiles ?? []
898
1063
  };
899
1064
  });
900
1065
  }
@@ -909,31 +1074,32 @@ async function slowRenderInstances(discovered, headlessInstanceComponents) {
909
1074
  const carryForwards = {};
910
1075
  for (const instance of discovered) {
911
1076
  const comp = componentByContractName.get(instance.contractName);
912
- if (!comp || !comp.compDefinition.slowlyRender) {
1077
+ if (!comp)
913
1078
  continue;
914
- }
915
1079
  const contractProps = comp.contract?.props ?? [];
916
1080
  const normalizedProps = {};
917
1081
  for (const [key, value] of Object.entries(instance.props)) {
918
1082
  const match = contractProps.find((p) => p.name.toLowerCase() === key.toLowerCase());
919
1083
  normalizedProps[match ? match.name : key] = value;
920
1084
  }
921
- const services = resolveServices(comp.compDefinition.services);
922
- const slowResult = await comp.compDefinition.slowlyRender(normalizedProps, ...services);
923
- if (slowResult.kind === "PhaseOutput") {
924
- const coordKey = instance.coordinate.join("/");
925
- resolvedData.push({
926
- coordinate: instance.coordinate,
927
- contract: comp.contract,
928
- slowViewState: slowResult.rendered
929
- });
930
- slowViewStates[coordKey] = slowResult.rendered;
931
- carryForwards[coordKey] = slowResult.carryForward;
932
- discoveredForFast.push({
933
- contractName: instance.contractName,
934
- props: normalizedProps,
935
- coordinate: instance.coordinate
936
- });
1085
+ discoveredForFast.push({
1086
+ contractName: instance.contractName,
1087
+ props: normalizedProps,
1088
+ coordinate: instance.coordinate
1089
+ });
1090
+ if (comp.compDefinition.slowlyRender) {
1091
+ const services = resolveServices(comp.compDefinition.services);
1092
+ const slowResult = await comp.compDefinition.slowlyRender(normalizedProps, ...services);
1093
+ if (slowResult.kind === "PhaseOutput") {
1094
+ const coordKey = instance.coordinate.join("/");
1095
+ resolvedData.push({
1096
+ coordinate: instance.coordinate,
1097
+ contract: comp.contract,
1098
+ slowViewState: slowResult.rendered
1099
+ });
1100
+ slowViewStates[coordKey] = slowResult.rendered;
1101
+ carryForwards[coordKey] = slowResult.carryForward;
1102
+ }
937
1103
  }
938
1104
  }
939
1105
  if (discoveredForFast.length === 0) {
@@ -942,7 +1108,7 @@ async function slowRenderInstances(discovered, headlessInstanceComponents) {
942
1108
  return {
943
1109
  resolvedData,
944
1110
  slowViewStates,
945
- instancePhaseData: { discovered: discoveredForFast, carryForwards }
1111
+ instancePhaseData: { discovered: discoveredForFast, carryForwards, slowViewStates }
946
1112
  };
947
1113
  }
948
1114
  function validateForEachInstances(forEachInstances, headlessInstanceComponents) {
@@ -1957,7 +2123,7 @@ class SlowRenderCache {
1957
2123
  try {
1958
2124
  const files = await fs$2.readdir(cacheSubDir);
1959
2125
  for (const file of files) {
1960
- if (file.startsWith(basename) && file.endsWith(".jay-html")) {
2126
+ if (file.startsWith(basename)) {
1961
2127
  try {
1962
2128
  await fs$2.unlink(path__default.join(cacheSubDir, file));
1963
2129
  } catch {
@@ -2131,11 +2297,34 @@ async function materializeContracts(options, services = /* @__PURE__ */ new Map(
2131
2297
  const { manifest } = plugin;
2132
2298
  const pluginRelPath = path.relative(projectRoot, plugin.pluginPath);
2133
2299
  if (!pluginsIndexMap.has(plugin.name)) {
2134
- pluginsIndexMap.set(plugin.name, {
2300
+ const entry = {
2135
2301
  path: "./" + pluginRelPath.replace(/\\/g, "/"),
2136
2302
  contracts: [],
2137
2303
  actions: []
2138
- });
2304
+ };
2305
+ if (manifest.services?.length) {
2306
+ entry.services = manifest.services.map((s2) => {
2307
+ const docPath = s2.doc ? "./" + path.relative(projectRoot, path.resolve(plugin.pluginPath, s2.doc)) : void 0;
2308
+ return {
2309
+ name: s2.name,
2310
+ marker: s2.marker,
2311
+ ...s2.description && { description: s2.description },
2312
+ ...docPath && { doc: docPath }
2313
+ };
2314
+ });
2315
+ }
2316
+ if (manifest.contexts?.length) {
2317
+ entry.contexts = manifest.contexts.map((c) => {
2318
+ const docPath = c.doc ? "./" + path.relative(projectRoot, path.resolve(plugin.pluginPath, c.doc)) : void 0;
2319
+ return {
2320
+ name: c.name,
2321
+ marker: c.marker,
2322
+ ...c.description && { description: c.description },
2323
+ ...docPath && { doc: docPath }
2324
+ };
2325
+ });
2326
+ }
2327
+ pluginsIndexMap.set(plugin.name, entry);
2139
2328
  }
2140
2329
  if (!dynamicOnly && manifest.contracts) {
2141
2330
  for (const contract of manifest.contracts) {
@@ -2145,8 +2334,20 @@ async function materializeContracts(options, services = /* @__PURE__ */ new Map(
2145
2334
  projectRoot
2146
2335
  );
2147
2336
  const relativePath = path.relative(projectRoot, contractPath);
2337
+ let description = contract.description;
2338
+ if (!description) {
2339
+ try {
2340
+ const contractContent = fs.readFileSync(contractPath, "utf-8");
2341
+ const parsed = YAML.parse(contractContent);
2342
+ if (parsed?.description && typeof parsed.description === "string") {
2343
+ description = parsed.description;
2344
+ }
2345
+ } catch {
2346
+ }
2347
+ }
2148
2348
  pluginsIndexMap.get(plugin.name).contracts.push({
2149
2349
  name: contract.name,
2350
+ ...description && { description },
2150
2351
  type: "static",
2151
2352
  path: "./" + relativePath
2152
2353
  });
@@ -2180,8 +2381,17 @@ async function materializeContracts(options, services = /* @__PURE__ */ new Map(
2180
2381
  const filePath = path.join(pluginOutputDir, fileName);
2181
2382
  fs.writeFileSync(filePath, generated.yaml, "utf-8");
2182
2383
  const relativePath = path.relative(projectRoot, filePath);
2384
+ let dynDescription;
2385
+ try {
2386
+ const parsedYaml = YAML.parse(generated.yaml);
2387
+ if (parsedYaml?.description && typeof parsedYaml.description === "string") {
2388
+ dynDescription = parsedYaml.description;
2389
+ }
2390
+ } catch {
2391
+ }
2183
2392
  const contractEntry = {
2184
2393
  name: fullName,
2394
+ ...dynDescription && { description: dynDescription },
2185
2395
  type: "dynamic",
2186
2396
  path: "./" + relativePath,
2187
2397
  ...generated.metadata && { metadata: generated.metadata }
@@ -2217,7 +2427,10 @@ async function materializeContracts(options, services = /* @__PURE__ */ new Map(
2217
2427
  if (!metadata)
2218
2428
  continue;
2219
2429
  const actionRelPath = path.relative(projectRoot, metadataFilePath);
2220
- pluginsIndexMap.get(plugin.name).actions.push({
2430
+ const pluginEntry = pluginsIndexMap.get(plugin.name);
2431
+ if (!pluginEntry.actions)
2432
+ pluginEntry.actions = [];
2433
+ pluginEntry.actions.push({
2221
2434
  name: metadata.name,
2222
2435
  description: metadata.description,
2223
2436
  path: "./" + actionRelPath.replace(/\\/g, "/")
@@ -2233,7 +2446,9 @@ async function materializeContracts(options, services = /* @__PURE__ */ new Map(
2233
2446
  name,
2234
2447
  path: data.path,
2235
2448
  contracts: data.contracts,
2236
- ...data.actions.length > 0 && { actions: data.actions }
2449
+ ...data.actions && data.actions.length > 0 && { actions: data.actions },
2450
+ ...data.services?.length && { services: data.services },
2451
+ ...data.contexts?.length && { contexts: data.contexts }
2237
2452
  }))
2238
2453
  };
2239
2454
  fs.mkdirSync(outputDir, { recursive: true });
@@ -2264,10 +2479,33 @@ async function listContracts(options) {
2264
2479
  const { manifest } = plugin;
2265
2480
  const pluginRelPath = path.relative(projectRoot, plugin.pluginPath);
2266
2481
  if (!pluginsMap.has(plugin.name)) {
2267
- pluginsMap.set(plugin.name, {
2482
+ const entry = {
2268
2483
  path: "./" + pluginRelPath.replace(/\\/g, "/"),
2269
2484
  contracts: []
2270
- });
2485
+ };
2486
+ if (manifest.services?.length) {
2487
+ entry.services = manifest.services.map((s2) => {
2488
+ const docPath = s2.doc ? "./" + path.relative(projectRoot, path.resolve(plugin.pluginPath, s2.doc)) : void 0;
2489
+ return {
2490
+ name: s2.name,
2491
+ marker: s2.marker,
2492
+ ...s2.description && { description: s2.description },
2493
+ ...docPath && { doc: docPath }
2494
+ };
2495
+ });
2496
+ }
2497
+ if (manifest.contexts?.length) {
2498
+ entry.contexts = manifest.contexts.map((c) => {
2499
+ const docPath = c.doc ? "./" + path.relative(projectRoot, path.resolve(plugin.pluginPath, c.doc)) : void 0;
2500
+ return {
2501
+ name: c.name,
2502
+ marker: c.marker,
2503
+ ...c.description && { description: c.description },
2504
+ ...docPath && { doc: docPath }
2505
+ };
2506
+ });
2507
+ }
2508
+ pluginsMap.set(plugin.name, entry);
2271
2509
  }
2272
2510
  if (!dynamicOnly && manifest.contracts) {
2273
2511
  for (const contract of manifest.contracts) {
@@ -2277,8 +2515,20 @@ async function listContracts(options) {
2277
2515
  projectRoot
2278
2516
  );
2279
2517
  const relativePath = path.relative(projectRoot, contractPath);
2518
+ let listDescription = contract.description;
2519
+ if (!listDescription) {
2520
+ try {
2521
+ const contractContent = fs.readFileSync(contractPath, "utf-8");
2522
+ const parsed = YAML.parse(contractContent);
2523
+ if (parsed?.description && typeof parsed.description === "string") {
2524
+ listDescription = parsed.description;
2525
+ }
2526
+ } catch {
2527
+ }
2528
+ }
2280
2529
  pluginsMap.get(plugin.name).contracts.push({
2281
2530
  name: contract.name,
2531
+ ...listDescription && { description: listDescription },
2282
2532
  type: "static",
2283
2533
  path: "./" + relativePath
2284
2534
  });
@@ -2299,7 +2549,9 @@ async function listContracts(options) {
2299
2549
  plugins: Array.from(pluginsMap.entries()).map(([name, data]) => ({
2300
2550
  name,
2301
2551
  path: data.path,
2302
- contracts: data.contracts
2552
+ contracts: data.contracts,
2553
+ ...data.services?.length && { services: data.services },
2554
+ ...data.contexts?.length && { contexts: data.contexts }
2303
2555
  }))
2304
2556
  };
2305
2557
  }
@@ -2469,6 +2721,7 @@ export {
2469
2721
  loadActionMetadata,
2470
2722
  loadPageParts,
2471
2723
  materializeContracts,
2724
+ mergeHeadTags,
2472
2725
  onInit,
2473
2726
  onShutdown,
2474
2727
  parseActionMetadata,
@@ -2484,8 +2737,10 @@ export {
2484
2737
  runShutdownCallbacks,
2485
2738
  runSlowlyChangingRender,
2486
2739
  scanPlugins,
2740
+ serializeHeadTags,
2487
2741
  setClientInitData,
2488
2742
  slowRenderInstances,
2489
2743
  sortPluginsByDependencies,
2744
+ tagIdentityKey,
2490
2745
  validateForEachInstances
2491
2746
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jay-framework/stack-server-runtime",
3
- "version": "0.15.4",
3
+ "version": "0.15.6",
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.4",
30
- "@jay-framework/compiler-shared": "^0.15.4",
31
- "@jay-framework/component": "^0.15.4",
32
- "@jay-framework/fullstack-component": "^0.15.4",
33
- "@jay-framework/logger": "^0.15.4",
34
- "@jay-framework/runtime": "^0.15.4",
35
- "@jay-framework/ssr-runtime": "^0.15.4",
36
- "@jay-framework/stack-route-scanner": "^0.15.4",
37
- "@jay-framework/view-state-merge": "^0.15.4",
29
+ "@jay-framework/compiler-jay-html": "^0.15.6",
30
+ "@jay-framework/compiler-shared": "^0.15.6",
31
+ "@jay-framework/component": "^0.15.6",
32
+ "@jay-framework/fullstack-component": "^0.15.6",
33
+ "@jay-framework/logger": "^0.15.6",
34
+ "@jay-framework/runtime": "^0.15.6",
35
+ "@jay-framework/ssr-runtime": "^0.15.6",
36
+ "@jay-framework/stack-route-scanner": "^0.15.6",
37
+ "@jay-framework/view-state-merge": "^0.15.6",
38
38
  "yaml": "^2.3.4"
39
39
  },
40
40
  "devDependencies": {
41
- "@jay-framework/dev-environment": "^0.15.4",
42
- "@jay-framework/jay-cli": "^0.15.4",
43
- "@jay-framework/stack-client-runtime": "^0.15.4",
41
+ "@jay-framework/dev-environment": "^0.15.6",
42
+ "@jay-framework/jay-cli": "^0.15.6",
43
+ "@jay-framework/stack-client-runtime": "^0.15.6",
44
44
  "@types/express": "^5.0.2",
45
45
  "@types/node": "^22.15.21",
46
46
  "nodemon": "^3.0.3",