@jay-framework/stack-server-runtime 0.15.5 → 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 +282 -49
  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,12 +11,12 @@ 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,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
  }
@@ -378,6 +388,7 @@ function resolveBinding(binding, item) {
378
388
  async function renderFastChangingData(pageParams, pageProps, carryForward, parts, instancePhaseData, forEachInstances, headlessInstanceComponents, mergedSlowViewState, query = {}) {
379
389
  let fastViewState = {};
380
390
  let fastCarryForward = {};
391
+ const fastHeadTagSources = [];
381
392
  for (const part of parts) {
382
393
  const { compDefinition, key, contractInfo } = part;
383
394
  if (compDefinition.fastRender) {
@@ -405,6 +416,9 @@ async function renderFastChangingData(pageParams, pageProps, carryForward, parts
405
416
  fastViewState[key] = fastRenderedPart.rendered;
406
417
  fastCarryForward[key] = fastRenderedPart.carryForward;
407
418
  }
419
+ if (fastRenderedPart.headTags) {
420
+ fastHeadTagSources.push(fastRenderedPart.headTags);
421
+ }
408
422
  } else
409
423
  return fastRenderedPart;
410
424
  }
@@ -419,18 +433,25 @@ async function renderFastChangingData(pageParams, pageProps, carryForward, parts
419
433
  for (const instance of instancePhaseData.discovered) {
420
434
  const coordKey = instance.coordinate.join("/");
421
435
  const comp = componentByContractName.get(instance.contractName);
422
- if (!comp || !comp.compDefinition.fastRender)
436
+ if (!comp)
423
437
  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;
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
+ }
433
452
  }
453
+ } else {
454
+ instanceViewStates[coordKey] = instanceSlowVS ?? {};
434
455
  }
435
456
  }
436
457
  }
@@ -491,7 +512,11 @@ async function renderFastChangingData(pageParams, pageProps, carryForward, parts
491
512
  if (Object.keys(instanceCarryForwards).length > 0) {
492
513
  fastCarryForward.__headlessInstances = instanceCarryForwards;
493
514
  }
494
- 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);
495
520
  }
496
521
  function generatePromiseReconstruction(outcomes) {
497
522
  if (outcomes.length === 0)
@@ -635,7 +660,8 @@ async function generateClientScript(defaultViewState, fastCarryForward, parts, j
635
660
  import { render } from '${jayHtmlPath}';
636
661
  ${partImports}${slowViewStateDecl}
637
662
  const viewState = ${JSON.stringify(defaultViewState)};
638
- ${generatePromiseReconstruction(outcomes)} const fastCarryForward = ${JSON.stringify(fastCarryForward)};
663
+ ${generatePromiseReconstruction(outcomes)}
664
+ const fastCarryForward = ${JSON.stringify(fastCarryForward)};
639
665
  const trackByMap = ${JSON.stringify(trackByMap)};
640
666
  ${clientInitExecution}
641
667
  const target = document.getElementById('target');
@@ -651,6 +677,65 @@ function asyncSwapScript(id, html) {
651
677
  const escapedHtml = html.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
652
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>`;
653
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
+ }
654
739
  const serverModuleCache = /* @__PURE__ */ new Map();
655
740
  function invalidateServerElementCache(jayHtmlPath) {
656
741
  if (serverModuleCache.delete(jayHtmlPath)) {
@@ -660,7 +745,7 @@ function invalidateServerElementCache(jayHtmlPath) {
660
745
  function clearServerElementCache() {
661
746
  serverModuleCache.clear();
662
747
  }
663
- 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) {
664
749
  const jayHtmlPath = path__default.join(jayHtmlDir, jayHtmlFilename);
665
750
  let cached = serverModuleCache.get(jayHtmlPath);
666
751
  if (!cached) {
@@ -672,7 +757,8 @@ async function generateSSRPageHtml(vite, jayHtmlContent, jayHtmlFilename, jayHtm
672
757
  buildFolder,
673
758
  projectRoot,
674
759
  routeDir,
675
- tsConfigFilePath
760
+ tsConfigFilePath,
761
+ sourceDir
676
762
  );
677
763
  serverModuleCache.set(jayHtmlPath, cached);
678
764
  }
@@ -728,14 +814,16 @@ async function generateSSRPageHtml(vite, jayHtmlContent, jayHtmlFilename, jayHtm
728
814
  return ` <link rel="${link.rel}" href="${link.href}"${attrs} />`;
729
815
  }).join("\n");
730
816
  const cssLink = cached.cssHref ? ` <link rel="stylesheet" href="${cached.cssHref}" />` : "";
731
- const headExtras = [headLinksHtml, cssLink].filter((_) => _).join("\n");
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");
732
821
  return `<!doctype html>
733
822
  <html lang="en">
734
823
  <head>
735
824
  <meta charset="UTF-8" />
736
825
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
737
- <title>Vite + TS</title>
738
- ${headExtras ? headExtras + "\n" : ""} </head>
826
+ ${titleHtml}${headExtras ? headExtras + "\n" : ""} </head>
739
827
  <body>
740
828
  <div id="target">${ssrHtml}</div>${asyncScripts}
741
829
  ${hydrationScript}
@@ -752,14 +840,15 @@ function rebaseRelativeImports(code, fromDir, toDir) {
752
840
  return `from "${newRelPath}"`;
753
841
  });
754
842
  }
755
- async function compileAndLoadServerElement(vite, jayHtmlContent, jayHtmlFilename, jayHtmlDir, buildFolder, projectRoot, routeDir, tsConfigFilePath) {
843
+ async function compileAndLoadServerElement(vite, jayHtmlContent, jayHtmlFilename, jayHtmlDir, buildFolder, projectRoot, routeDir, tsConfigFilePath, sourceDir) {
756
844
  const jayFile = await parseJayFile(
757
845
  jayHtmlContent,
758
846
  jayHtmlFilename,
759
847
  jayHtmlDir,
760
848
  { relativePath: tsConfigFilePath },
761
849
  JAY_IMPORT_RESOLVER,
762
- projectRoot
850
+ projectRoot,
851
+ sourceDir
763
852
  );
764
853
  const parsedJayFile = checkValidationErrors(jayFile);
765
854
  const pageName = jayHtmlFilename.replace(".jay-html", "");
@@ -785,6 +874,8 @@ async function compileAndLoadServerElement(vite, jayHtmlContent, jayHtmlFilename
785
874
  if (existingModule) {
786
875
  vite.moduleGraph.invalidateModule(existingModule);
787
876
  }
877
+ const jayHtmlPath = path__default.join(serverElementDir, jayHtmlFilename);
878
+ invalidateJayHtmlModules(vite, jayHtmlPath);
788
879
  const serverModule = await vite.ssrLoadModule(serverElementPath);
789
880
  let cssHref;
790
881
  if (parsedJayFile.css) {
@@ -806,6 +897,34 @@ async function compileAndLoadServerElement(vite, jayHtmlContent, jayHtmlFilename
806
897
  cssHref
807
898
  };
808
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
+ }
809
928
  function generateHydrationScript(defaultViewState, fastCarryForward, parts, jayHtmlPath, trackByMap = {}, clientInitData2 = {}, projectInit, pluginInits = [], options = {}, asyncOutcomes = []) {
810
929
  const {
811
930
  partImports,
@@ -907,7 +1026,29 @@ async function loadPageParts(vite, route, pagesBase, projectBase, jayRollupConfi
907
1026
  contract: hi.contract,
908
1027
  contractPath: hi.contractPath
909
1028
  }));
910
- 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
+ }
911
1052
  return {
912
1053
  parts,
913
1054
  serverTrackByMap: jayHtml.serverTrackByMap,
@@ -916,7 +1057,9 @@ async function loadPageParts(vite, route, pagesBase, projectBase, jayRollupConfi
916
1057
  headlessContracts,
917
1058
  headlessInstanceComponents,
918
1059
  discoveredInstances: discoveryResult.instances,
919
- forEachInstances: discoveryResult.forEachInstances
1060
+ forEachInstances: discoveryResult.forEachInstances,
1061
+ linkedCssFiles: jayHtml.linkedCssFiles ?? [],
1062
+ linkedComponentFiles: jayHtml.linkedComponentFiles ?? []
920
1063
  };
921
1064
  });
922
1065
  }
@@ -931,31 +1074,32 @@ async function slowRenderInstances(discovered, headlessInstanceComponents) {
931
1074
  const carryForwards = {};
932
1075
  for (const instance of discovered) {
933
1076
  const comp = componentByContractName.get(instance.contractName);
934
- if (!comp || !comp.compDefinition.slowlyRender) {
1077
+ if (!comp)
935
1078
  continue;
936
- }
937
1079
  const contractProps = comp.contract?.props ?? [];
938
1080
  const normalizedProps = {};
939
1081
  for (const [key, value] of Object.entries(instance.props)) {
940
1082
  const match = contractProps.find((p) => p.name.toLowerCase() === key.toLowerCase());
941
1083
  normalizedProps[match ? match.name : key] = value;
942
1084
  }
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
- });
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
+ }
959
1103
  }
960
1104
  }
961
1105
  if (discoveredForFast.length === 0) {
@@ -964,7 +1108,7 @@ async function slowRenderInstances(discovered, headlessInstanceComponents) {
964
1108
  return {
965
1109
  resolvedData,
966
1110
  slowViewStates,
967
- instancePhaseData: { discovered: discoveredForFast, carryForwards }
1111
+ instancePhaseData: { discovered: discoveredForFast, carryForwards, slowViewStates }
968
1112
  };
969
1113
  }
970
1114
  function validateForEachInstances(forEachInstances, headlessInstanceComponents) {
@@ -2153,11 +2297,34 @@ async function materializeContracts(options, services = /* @__PURE__ */ new Map(
2153
2297
  const { manifest } = plugin;
2154
2298
  const pluginRelPath = path.relative(projectRoot, plugin.pluginPath);
2155
2299
  if (!pluginsIndexMap.has(plugin.name)) {
2156
- pluginsIndexMap.set(plugin.name, {
2300
+ const entry = {
2157
2301
  path: "./" + pluginRelPath.replace(/\\/g, "/"),
2158
2302
  contracts: [],
2159
2303
  actions: []
2160
- });
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);
2161
2328
  }
2162
2329
  if (!dynamicOnly && manifest.contracts) {
2163
2330
  for (const contract of manifest.contracts) {
@@ -2167,8 +2334,20 @@ async function materializeContracts(options, services = /* @__PURE__ */ new Map(
2167
2334
  projectRoot
2168
2335
  );
2169
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
+ }
2170
2348
  pluginsIndexMap.get(plugin.name).contracts.push({
2171
2349
  name: contract.name,
2350
+ ...description && { description },
2172
2351
  type: "static",
2173
2352
  path: "./" + relativePath
2174
2353
  });
@@ -2202,8 +2381,17 @@ async function materializeContracts(options, services = /* @__PURE__ */ new Map(
2202
2381
  const filePath = path.join(pluginOutputDir, fileName);
2203
2382
  fs.writeFileSync(filePath, generated.yaml, "utf-8");
2204
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
+ }
2205
2392
  const contractEntry = {
2206
2393
  name: fullName,
2394
+ ...dynDescription && { description: dynDescription },
2207
2395
  type: "dynamic",
2208
2396
  path: "./" + relativePath,
2209
2397
  ...generated.metadata && { metadata: generated.metadata }
@@ -2239,7 +2427,10 @@ async function materializeContracts(options, services = /* @__PURE__ */ new Map(
2239
2427
  if (!metadata)
2240
2428
  continue;
2241
2429
  const actionRelPath = path.relative(projectRoot, metadataFilePath);
2242
- pluginsIndexMap.get(plugin.name).actions.push({
2430
+ const pluginEntry = pluginsIndexMap.get(plugin.name);
2431
+ if (!pluginEntry.actions)
2432
+ pluginEntry.actions = [];
2433
+ pluginEntry.actions.push({
2243
2434
  name: metadata.name,
2244
2435
  description: metadata.description,
2245
2436
  path: "./" + actionRelPath.replace(/\\/g, "/")
@@ -2255,7 +2446,9 @@ async function materializeContracts(options, services = /* @__PURE__ */ new Map(
2255
2446
  name,
2256
2447
  path: data.path,
2257
2448
  contracts: data.contracts,
2258
- ...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 }
2259
2452
  }))
2260
2453
  };
2261
2454
  fs.mkdirSync(outputDir, { recursive: true });
@@ -2286,10 +2479,33 @@ async function listContracts(options) {
2286
2479
  const { manifest } = plugin;
2287
2480
  const pluginRelPath = path.relative(projectRoot, plugin.pluginPath);
2288
2481
  if (!pluginsMap.has(plugin.name)) {
2289
- pluginsMap.set(plugin.name, {
2482
+ const entry = {
2290
2483
  path: "./" + pluginRelPath.replace(/\\/g, "/"),
2291
2484
  contracts: []
2292
- });
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);
2293
2509
  }
2294
2510
  if (!dynamicOnly && manifest.contracts) {
2295
2511
  for (const contract of manifest.contracts) {
@@ -2299,8 +2515,20 @@ async function listContracts(options) {
2299
2515
  projectRoot
2300
2516
  );
2301
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
+ }
2302
2529
  pluginsMap.get(plugin.name).contracts.push({
2303
2530
  name: contract.name,
2531
+ ...listDescription && { description: listDescription },
2304
2532
  type: "static",
2305
2533
  path: "./" + relativePath
2306
2534
  });
@@ -2321,7 +2549,9 @@ async function listContracts(options) {
2321
2549
  plugins: Array.from(pluginsMap.entries()).map(([name, data]) => ({
2322
2550
  name,
2323
2551
  path: data.path,
2324
- contracts: data.contracts
2552
+ contracts: data.contracts,
2553
+ ...data.services?.length && { services: data.services },
2554
+ ...data.contexts?.length && { contexts: data.contexts }
2325
2555
  }))
2326
2556
  };
2327
2557
  }
@@ -2491,6 +2721,7 @@ export {
2491
2721
  loadActionMetadata,
2492
2722
  loadPageParts,
2493
2723
  materializeContracts,
2724
+ mergeHeadTags,
2494
2725
  onInit,
2495
2726
  onShutdown,
2496
2727
  parseActionMetadata,
@@ -2506,8 +2737,10 @@ export {
2506
2737
  runShutdownCallbacks,
2507
2738
  runSlowlyChangingRender,
2508
2739
  scanPlugins,
2740
+ serializeHeadTags,
2509
2741
  setClientInitData,
2510
2742
  slowRenderInstances,
2511
2743
  sortPluginsByDependencies,
2744
+ tagIdentityKey,
2512
2745
  validateForEachInstances
2513
2746
  };
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.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.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.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.5",
42
- "@jay-framework/jay-cli": "^0.15.5",
43
- "@jay-framework/stack-client-runtime": "^0.15.5",
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",