@jay-framework/stack-server-runtime 0.15.6 → 0.16.1
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.
- package/dist/index.d.ts +70 -13
- package/dist/index.js +256 -16
- package/package.json +13 -13
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import { AnyJayStackComponentDefinition, PageProps, AnySlowlyRenderResult, UrlParams,
|
|
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';
|
|
@@ -76,8 +75,11 @@ interface SlowlyChangingPhase {
|
|
|
76
75
|
declare class DevSlowlyChangingPhase implements SlowlyChangingPhase {
|
|
77
76
|
runSlowlyForPage(pageParams: UrlParams, pageProps: PageProps, parts: Array<DevServerPagePart>, discoveredInstances?: DiscoveredHeadlessInstance[], headlessInstanceComponents?: HeadlessInstanceComponent[], jayHtmlPath?: string): Promise<AnySlowlyRenderResult>;
|
|
78
77
|
}
|
|
79
|
-
|
|
80
|
-
|
|
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>[]>;
|
|
81
83
|
|
|
82
84
|
/**
|
|
83
85
|
* Server-side slow render orchestration for headless component instances.
|
|
@@ -191,23 +193,45 @@ declare function resolveActionMetadataPath(actionPath: string, pluginDir: string
|
|
|
191
193
|
*/
|
|
192
194
|
|
|
193
195
|
/**
|
|
194
|
-
*
|
|
196
|
+
* Base fields shared by all registered action types.
|
|
195
197
|
*/
|
|
196
|
-
interface
|
|
198
|
+
interface RegisteredActionBase {
|
|
197
199
|
/** Unique action name */
|
|
198
200
|
actionName: string;
|
|
199
201
|
/** HTTP method */
|
|
200
202
|
method: HttpMethod;
|
|
201
|
-
/** Cache options (for GET requests) */
|
|
202
|
-
cacheOptions?: CacheOptions;
|
|
203
203
|
/** Service markers for dependency injection */
|
|
204
204
|
services: any[];
|
|
205
|
-
/** The handler function */
|
|
206
|
-
handler: (input: any, ...services: any[]) => Promise<any>;
|
|
207
205
|
/** Optional metadata from .jay-action file (description, input/output schemas).
|
|
208
206
|
* Actions with metadata are exposed to AI agents; those without are not. */
|
|
209
207
|
metadata?: ActionMetadata;
|
|
208
|
+
/** Whether this action accepts file uploads (DL#131) */
|
|
209
|
+
acceptsFiles?: boolean;
|
|
210
210
|
}
|
|
211
|
+
/**
|
|
212
|
+
* Registered request-response action entry.
|
|
213
|
+
* Uses `isStreaming` as a discriminator for the union.
|
|
214
|
+
*/
|
|
215
|
+
interface RegisteredAction extends RegisteredActionBase {
|
|
216
|
+
/** Discriminator: false or absent for regular actions */
|
|
217
|
+
isStreaming?: false;
|
|
218
|
+
/** Cache options (for GET requests) */
|
|
219
|
+
cacheOptions?: CacheOptions;
|
|
220
|
+
/** The handler function */
|
|
221
|
+
handler: (input: any, ...services: any[]) => Promise<any>;
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Registered streaming action entry (DL#129).
|
|
225
|
+
*/
|
|
226
|
+
interface RegisteredStreamAction extends RegisteredActionBase {
|
|
227
|
+
method: 'POST';
|
|
228
|
+
/** Discriminator: true for streaming actions */
|
|
229
|
+
isStreaming: true;
|
|
230
|
+
/** The generator handler function */
|
|
231
|
+
handler: (input: any, ...services: any[]) => AsyncIterable<any>;
|
|
232
|
+
}
|
|
233
|
+
/** Union of all registered action types, discriminated by `isStreaming`. */
|
|
234
|
+
type RegisteredActionEntry = RegisteredAction | RegisteredStreamAction;
|
|
211
235
|
/**
|
|
212
236
|
* Result of executing an action.
|
|
213
237
|
*/
|
|
@@ -258,7 +282,7 @@ declare class ActionRegistry {
|
|
|
258
282
|
* @param actionName - The unique action name
|
|
259
283
|
* @returns The registered action or undefined
|
|
260
284
|
*/
|
|
261
|
-
get(actionName: string):
|
|
285
|
+
get(actionName: string): RegisteredActionEntry | undefined;
|
|
262
286
|
/**
|
|
263
287
|
* Checks if an action is registered.
|
|
264
288
|
*
|
|
@@ -310,6 +334,18 @@ declare class ActionRegistry {
|
|
|
310
334
|
* @returns Cache-Control header value or undefined
|
|
311
335
|
*/
|
|
312
336
|
getCacheHeaders(actionName: string): string | undefined;
|
|
337
|
+
/**
|
|
338
|
+
* Register a streaming action.
|
|
339
|
+
*/
|
|
340
|
+
registerStream<I, C, S extends any[]>(action: JayStreamAction<I, C> & JayStreamActionDefinition<I, C, S>): void;
|
|
341
|
+
/**
|
|
342
|
+
* Check if a registered action is a streaming action.
|
|
343
|
+
*/
|
|
344
|
+
isStreaming(actionName: string): boolean;
|
|
345
|
+
/**
|
|
346
|
+
* Execute a streaming action, returning an async iterable of chunks.
|
|
347
|
+
*/
|
|
348
|
+
executeStream(actionName: string, input: unknown): AsyncGenerator<any>;
|
|
313
349
|
}
|
|
314
350
|
/**
|
|
315
351
|
* Default action registry instance.
|
|
@@ -325,7 +361,7 @@ declare function registerAction<I, O, S extends any[]>(action: JayAction<I, O> &
|
|
|
325
361
|
* Retrieves a registered action by name from the default registry.
|
|
326
362
|
* @deprecated Use actionRegistry.get() instead
|
|
327
363
|
*/
|
|
328
|
-
declare function getRegisteredAction(actionName: string):
|
|
364
|
+
declare function getRegisteredAction(actionName: string): RegisteredActionEntry | undefined;
|
|
329
365
|
/**
|
|
330
366
|
* Checks if an action is registered in the default registry.
|
|
331
367
|
* @deprecated Use actionRegistry.has() instead
|
|
@@ -575,6 +611,8 @@ interface GenerateClientScriptOptions {
|
|
|
575
611
|
* so that AI/automation tools can see the complete page state.
|
|
576
612
|
*/
|
|
577
613
|
slowViewState?: object;
|
|
614
|
+
/** Route pattern (e.g., /products/kitan{/:category}) for freeze entries */
|
|
615
|
+
routePattern?: string;
|
|
578
616
|
}
|
|
579
617
|
/**
|
|
580
618
|
* Shared fragments generated by buildScriptFragments().
|
|
@@ -637,6 +675,18 @@ declare function generateSSRPageHtml(vite: ViteDevServer, jayHtmlContent: string
|
|
|
637
675
|
sourceDir?: string,
|
|
638
676
|
/** Head tags to inject into <head> during SSR (Design Log #127) */
|
|
639
677
|
headTags?: HeadTag[]): Promise<string>;
|
|
678
|
+
/**
|
|
679
|
+
* Generate a frozen page — pure SSR HTML with no client scripts (DL#127).
|
|
680
|
+
*
|
|
681
|
+
* Uses the same server element module as generateSSRPageHtml, but:
|
|
682
|
+
* - No hydration script
|
|
683
|
+
* - No Vite client
|
|
684
|
+
* - No component runtime
|
|
685
|
+
* - Just rendered HTML + CSS
|
|
686
|
+
*
|
|
687
|
+
* @param format - 'page' for full HTML document, 'fragment' for body-only (shadow DOM)
|
|
688
|
+
*/
|
|
689
|
+
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>;
|
|
640
690
|
|
|
641
691
|
/**
|
|
642
692
|
* Service registry for Jay Stack server-side dependency injection.
|
|
@@ -935,6 +985,11 @@ interface ContextIndexEntry {
|
|
|
935
985
|
description?: string;
|
|
936
986
|
doc?: string;
|
|
937
987
|
}
|
|
988
|
+
/** Route entry in plugins-index.yaml (DL#130) */
|
|
989
|
+
interface RouteIndexEntry {
|
|
990
|
+
path: string;
|
|
991
|
+
description?: string;
|
|
992
|
+
}
|
|
938
993
|
/** Entry for plugins-index.yaml (Design Log #85) */
|
|
939
994
|
interface PluginsIndexEntry {
|
|
940
995
|
name: string;
|
|
@@ -946,6 +1001,8 @@ interface PluginsIndexEntry {
|
|
|
946
1001
|
services?: ServiceIndexEntry[];
|
|
947
1002
|
/** Client-side contexts provided by this plugin (DL#125) */
|
|
948
1003
|
contexts?: ContextIndexEntry[];
|
|
1004
|
+
/** Plugin-provided routes (DL#130) */
|
|
1005
|
+
routes?: RouteIndexEntry[];
|
|
949
1006
|
}
|
|
950
1007
|
interface PluginsIndex {
|
|
951
1008
|
plugins: PluginsIndexEntry[];
|
|
@@ -1202,4 +1259,4 @@ declare function mergeHeadTags(sources: HeadTag[][]): HeadTag[];
|
|
|
1202
1259
|
*/
|
|
1203
1260
|
declare function serializeHeadTags(tags: HeadTag[]): string;
|
|
1204
1261
|
|
|
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,
|
|
1262
|
+
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,7 +4,7 @@ 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";
|
|
@@ -178,10 +178,15 @@ class DevSlowlyChangingPhase {
|
|
|
178
178
|
return phaseOutput(slowlyViewState, carryForward);
|
|
179
179
|
}
|
|
180
180
|
}
|
|
181
|
-
async function runLoadParams(
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
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
|
+
}
|
|
185
190
|
}
|
|
186
191
|
var __defProp2 = Object.defineProperty;
|
|
187
192
|
var __defNormalProp2 = (obj, key, value) => key in obj ? __defProp2(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
@@ -199,6 +204,7 @@ new JayAtomicType("string");
|
|
|
199
204
|
new JayAtomicType("number");
|
|
200
205
|
new JayAtomicType("boolean");
|
|
201
206
|
new JayAtomicType("Date");
|
|
207
|
+
new JayAtomicType("file");
|
|
202
208
|
new JayAtomicType("Unknown");
|
|
203
209
|
class JayObjectType {
|
|
204
210
|
constructor(name, props) {
|
|
@@ -242,6 +248,9 @@ function jayTypeToJsonSchema(type) {
|
|
|
242
248
|
if (name === "string" || name === "number" || name === "boolean") {
|
|
243
249
|
return { type: name };
|
|
244
250
|
}
|
|
251
|
+
if (name === "file") {
|
|
252
|
+
return { type: "string", description: "Binary file upload (JayFile)" };
|
|
253
|
+
}
|
|
245
254
|
return { type: "string" };
|
|
246
255
|
}
|
|
247
256
|
if (isEnumType(type)) {
|
|
@@ -403,11 +412,7 @@ async function renderFastChangingData(pageParams, pageProps, carryForward, parts
|
|
|
403
412
|
metadata: contractInfo.metadata
|
|
404
413
|
}
|
|
405
414
|
};
|
|
406
|
-
const fastRenderedPart = await compDefinition.fastRender(
|
|
407
|
-
partProps,
|
|
408
|
-
partSlowlyCarryForward,
|
|
409
|
-
...services
|
|
410
|
-
);
|
|
415
|
+
const fastRenderedPart = compDefinition.slowlyRender ? await compDefinition.fastRender(partProps, partSlowlyCarryForward, ...services) : await compDefinition.fastRender(partProps, ...services);
|
|
411
416
|
if (fastRenderedPart.kind === "PhaseOutput") {
|
|
412
417
|
if (!key) {
|
|
413
418
|
fastViewState = { ...fastViewState, ...fastRenderedPart.rendered };
|
|
@@ -576,6 +581,80 @@ ${parts.map((part) => " " + part.clientPart).join(",\n")}
|
|
|
576
581
|
slowViewStateDecl
|
|
577
582
|
};
|
|
578
583
|
}
|
|
584
|
+
function buildFreezeScript(routePattern) {
|
|
585
|
+
const routePatternLiteral = routePattern ? `'${routePattern}'` : "undefined";
|
|
586
|
+
return `
|
|
587
|
+
// Page Freeze (DL#127, DL#128 iframe addendum)
|
|
588
|
+
// Sticky embed mode: URL param sets a session cookie so in-iframe navigation preserves it
|
|
589
|
+
if (new URLSearchParams(window.location.search).has('_jay_embed')) {
|
|
590
|
+
document.cookie = '_jay_embed=1;path=/;samesite=lax';
|
|
591
|
+
}
|
|
592
|
+
const __jayEmbedMode = document.cookie.split(';').some(c => c.trim().startsWith('_jay_embed='));
|
|
593
|
+
|
|
594
|
+
async function __jayDoFreeze() {
|
|
595
|
+
const automation = window.__jay?.automation;
|
|
596
|
+
if (!automation) return;
|
|
597
|
+
|
|
598
|
+
// Visual feedback: white flash
|
|
599
|
+
const flash = document.createElement('div');
|
|
600
|
+
flash.style.cssText = 'position:fixed;inset:0;background:white;z-index:999999;opacity:0.8;pointer-events:none;transition:opacity 0.3s';
|
|
601
|
+
document.body.appendChild(flash);
|
|
602
|
+
requestAnimationFrame(() => { flash.style.opacity = '0'; });
|
|
603
|
+
setTimeout(() => flash.remove(), 400);
|
|
604
|
+
|
|
605
|
+
// Audio feedback: camera shutter
|
|
606
|
+
try {
|
|
607
|
+
const ctx = new AudioContext();
|
|
608
|
+
const buf = ctx.createBuffer(1, ctx.sampleRate * 0.15, ctx.sampleRate);
|
|
609
|
+
const data = buf.getChannelData(0);
|
|
610
|
+
for (let i = 0; i < data.length; i++) {
|
|
611
|
+
data[i] = (Math.random() * 2 - 1) * Math.exp(-i / (ctx.sampleRate * 0.02));
|
|
612
|
+
}
|
|
613
|
+
const src = ctx.createBufferSource();
|
|
614
|
+
src.buffer = buf;
|
|
615
|
+
src.connect(ctx.destination);
|
|
616
|
+
src.start();
|
|
617
|
+
} catch {}
|
|
618
|
+
|
|
619
|
+
// Capture and save
|
|
620
|
+
try {
|
|
621
|
+
const state = automation.getPageState();
|
|
622
|
+
const route = window.location.pathname;
|
|
623
|
+
const routePattern = ${routePatternLiteral};
|
|
624
|
+
const resp = await fetch('/_jay/freeze', {
|
|
625
|
+
method: 'POST',
|
|
626
|
+
headers: { 'Content-Type': 'application/json' },
|
|
627
|
+
body: JSON.stringify({ route, routePattern, viewState: state.viewState }),
|
|
628
|
+
});
|
|
629
|
+
const { id } = await resp.json();
|
|
630
|
+
if (__jayEmbedMode) {
|
|
631
|
+
window.parent.postMessage({ type: 'jay:freeze', id, route }, '*');
|
|
632
|
+
} else {
|
|
633
|
+
window.open(route + '?_jay_freeze=' + id, '_blank');
|
|
634
|
+
}
|
|
635
|
+
} catch (err) {
|
|
636
|
+
console.error('[Freeze] Failed:', err);
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
if (__jayEmbedMode) {
|
|
641
|
+
// Notify parent of the current route on load (DL#128 route addendum)
|
|
642
|
+
window.parent.postMessage({ type: 'jay:route', route: window.location.pathname, routePattern: ${routePatternLiteral} }, '*');
|
|
643
|
+
|
|
644
|
+
// Embed mode: parent triggers freeze via postMessage
|
|
645
|
+
window.addEventListener('message', (e) => {
|
|
646
|
+
if (e.data?.type === 'jay:requestFreeze') __jayDoFreeze();
|
|
647
|
+
});
|
|
648
|
+
} else {
|
|
649
|
+
// Standalone: Alt+S / Option+S keyboard shortcut
|
|
650
|
+
document.addEventListener('keydown', (e) => {
|
|
651
|
+
if (e.altKey && e.code === 'KeyS') {
|
|
652
|
+
e.preventDefault();
|
|
653
|
+
__jayDoFreeze();
|
|
654
|
+
}
|
|
655
|
+
});
|
|
656
|
+
}`;
|
|
657
|
+
}
|
|
579
658
|
function buildAutomationWrap(options, mode) {
|
|
580
659
|
const { enableAutomation = true, slowViewState } = options;
|
|
581
660
|
const hasSlowViewState = slowViewState && Object.keys(slowViewState).length > 0;
|
|
@@ -586,6 +665,7 @@ function buildAutomationWrap(options, mode) {
|
|
|
586
665
|
}
|
|
587
666
|
const appendLine = appendDom ? `
|
|
588
667
|
target.appendChild(wrapped.element.dom);` : "";
|
|
668
|
+
const freezeScript = buildFreezeScript(options.routePattern);
|
|
589
669
|
if (hasSlowViewState) {
|
|
590
670
|
return `
|
|
591
671
|
// Wrap with automation for dev tooling
|
|
@@ -595,7 +675,8 @@ function buildAutomationWrap(options, mode) {
|
|
|
595
675
|
registerGlobalContext(AUTOMATION_CONTEXT, wrapped.automation);
|
|
596
676
|
window.__jay = window.__jay || {};
|
|
597
677
|
window.__jay.automation = wrapped.automation;
|
|
598
|
-
window.dispatchEvent(new Event('jay:automation-ready'));${appendLine}
|
|
678
|
+
window.dispatchEvent(new Event('jay:automation-ready'));${appendLine}
|
|
679
|
+
${freezeScript}`;
|
|
599
680
|
}
|
|
600
681
|
return `
|
|
601
682
|
// Wrap with automation for dev tooling
|
|
@@ -603,7 +684,8 @@ function buildAutomationWrap(options, mode) {
|
|
|
603
684
|
registerGlobalContext(AUTOMATION_CONTEXT, wrapped.automation);
|
|
604
685
|
window.__jay = window.__jay || {};
|
|
605
686
|
window.__jay.automation = wrapped.automation;
|
|
606
|
-
window.dispatchEvent(new Event('jay:automation-ready'));${appendLine}
|
|
687
|
+
window.dispatchEvent(new Event('jay:automation-ready'));${appendLine}
|
|
688
|
+
${freezeScript}`;
|
|
607
689
|
}
|
|
608
690
|
async function resolveViewStatePromises(viewState) {
|
|
609
691
|
const entries = Object.entries(viewState);
|
|
@@ -830,6 +912,81 @@ ${titleHtml}${headExtras ? headExtras + "\n" : ""} </head>
|
|
|
830
912
|
</body>
|
|
831
913
|
</html>`;
|
|
832
914
|
}
|
|
915
|
+
async function generateFrozenPageHtml(vite, jayHtmlContent, jayHtmlFilename, jayHtmlDir, viewState, buildFolder, projectRoot, routeDir, tsConfigFilePath, sourceDir, format = "page", freezeName) {
|
|
916
|
+
const jayHtmlPath = path__default.join(jayHtmlDir, jayHtmlFilename);
|
|
917
|
+
let cached = serverModuleCache.get(jayHtmlPath);
|
|
918
|
+
if (!cached) {
|
|
919
|
+
cached = await compileAndLoadServerElement(
|
|
920
|
+
vite,
|
|
921
|
+
jayHtmlContent,
|
|
922
|
+
jayHtmlFilename,
|
|
923
|
+
jayHtmlDir,
|
|
924
|
+
buildFolder,
|
|
925
|
+
projectRoot,
|
|
926
|
+
routeDir,
|
|
927
|
+
tsConfigFilePath,
|
|
928
|
+
sourceDir
|
|
929
|
+
);
|
|
930
|
+
serverModuleCache.set(jayHtmlPath, cached);
|
|
931
|
+
}
|
|
932
|
+
const htmlChunks = [];
|
|
933
|
+
const ctx = {
|
|
934
|
+
write: (chunk) => {
|
|
935
|
+
htmlChunks.push(chunk);
|
|
936
|
+
},
|
|
937
|
+
onAsync: () => {
|
|
938
|
+
}
|
|
939
|
+
};
|
|
940
|
+
cached.renderToStream(viewState, ctx);
|
|
941
|
+
const ssrHtml = htmlChunks.join("");
|
|
942
|
+
if (format === "fragment") {
|
|
943
|
+
let inlineCss = "";
|
|
944
|
+
if (cached.cssHref) {
|
|
945
|
+
try {
|
|
946
|
+
const cssPath = cached.cssHref.replace(/^\/@fs/, "").replace(/\?.*$/, "");
|
|
947
|
+
const cssContent = await fs$2.readFile(cssPath, "utf-8");
|
|
948
|
+
inlineCss = `<style>${cssContent}</style>`;
|
|
949
|
+
} catch {
|
|
950
|
+
inlineCss = `<link rel="stylesheet" href="${cached.cssHref}" />`;
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
return `${inlineCss}
|
|
954
|
+
${ssrHtml}`;
|
|
955
|
+
}
|
|
956
|
+
const headLinksHtml = cached.headLinks.map((link) => {
|
|
957
|
+
const attrs = Object.entries(link.attributes).map(([k, v]) => ` ${k}="${v}"`).join("");
|
|
958
|
+
return ` <link rel="${link.rel}" href="${link.href}"${attrs} />`;
|
|
959
|
+
}).join("\n");
|
|
960
|
+
const cssLink = cached.cssHref ? ` <link rel="stylesheet" href="${cached.cssHref}" />` : "";
|
|
961
|
+
const headExtras = [headLinksHtml, cssLink].filter((_) => _).join("\n");
|
|
962
|
+
const label = freezeName ? ` — ${freezeName}` : "";
|
|
963
|
+
return `<!doctype html>
|
|
964
|
+
<html lang="en">
|
|
965
|
+
<head>
|
|
966
|
+
<meta charset="UTF-8" />
|
|
967
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
968
|
+
<title>Frozen${label}</title>
|
|
969
|
+
${headExtras ? headExtras + "\n" : ""} <style>
|
|
970
|
+
body::before {
|
|
971
|
+
content: 'FROZEN${label ? `: ${freezeName}` : ""}';
|
|
972
|
+
position: fixed;
|
|
973
|
+
top: 0;
|
|
974
|
+
right: 0;
|
|
975
|
+
background: #1a1a2e;
|
|
976
|
+
color: #e0e0ff;
|
|
977
|
+
padding: 2px 10px;
|
|
978
|
+
font: 11px/1.6 system-ui;
|
|
979
|
+
z-index: 99999;
|
|
980
|
+
border-bottom-left-radius: 4px;
|
|
981
|
+
opacity: 0.8;
|
|
982
|
+
}
|
|
983
|
+
</style>
|
|
984
|
+
</head>
|
|
985
|
+
<body>
|
|
986
|
+
<div id="target">${ssrHtml}</div>
|
|
987
|
+
</body>
|
|
988
|
+
</html>`;
|
|
989
|
+
}
|
|
833
990
|
function rebaseRelativeImports(code, fromDir, toDir) {
|
|
834
991
|
return code.replace(/from "(\.\.\/[^"]+)"/g, (_match, relPath) => {
|
|
835
992
|
const absolutePath = path__default.resolve(fromDir, relPath);
|
|
@@ -1142,7 +1299,8 @@ class ActionRegistry {
|
|
|
1142
1299
|
method: action.method,
|
|
1143
1300
|
cacheOptions: action.cacheOptions,
|
|
1144
1301
|
services: action.services,
|
|
1145
|
-
handler: action.handler
|
|
1302
|
+
handler: action.handler,
|
|
1303
|
+
...action.acceptsFiles && { acceptsFiles: true }
|
|
1146
1304
|
};
|
|
1147
1305
|
this.actions.set(action.actionName, entry);
|
|
1148
1306
|
}
|
|
@@ -1226,6 +1384,16 @@ class ActionRegistry {
|
|
|
1226
1384
|
}
|
|
1227
1385
|
};
|
|
1228
1386
|
}
|
|
1387
|
+
if (action.isStreaming) {
|
|
1388
|
+
return {
|
|
1389
|
+
success: false,
|
|
1390
|
+
error: {
|
|
1391
|
+
code: "STREAMING_ACTION",
|
|
1392
|
+
message: `Action '${actionName}' is a streaming action — use executeStream() instead`,
|
|
1393
|
+
isActionError: false
|
|
1394
|
+
}
|
|
1395
|
+
};
|
|
1396
|
+
}
|
|
1229
1397
|
try {
|
|
1230
1398
|
const services = resolveServices(action.services);
|
|
1231
1399
|
const result = await action.handler(input, ...services);
|
|
@@ -1279,6 +1447,41 @@ class ActionRegistry {
|
|
|
1279
1447
|
}
|
|
1280
1448
|
return parts.length > 0 ? parts.join(", ") : void 0;
|
|
1281
1449
|
}
|
|
1450
|
+
// --- Streaming actions (DL#129) ---
|
|
1451
|
+
/**
|
|
1452
|
+
* Register a streaming action.
|
|
1453
|
+
*/
|
|
1454
|
+
registerStream(action) {
|
|
1455
|
+
const entry = {
|
|
1456
|
+
actionName: action.actionName,
|
|
1457
|
+
method: "POST",
|
|
1458
|
+
isStreaming: true,
|
|
1459
|
+
services: action.services,
|
|
1460
|
+
handler: action.handler,
|
|
1461
|
+
...action.acceptsFiles && { acceptsFiles: true }
|
|
1462
|
+
};
|
|
1463
|
+
this.actions.set(action.actionName, entry);
|
|
1464
|
+
}
|
|
1465
|
+
/**
|
|
1466
|
+
* Check if a registered action is a streaming action.
|
|
1467
|
+
*/
|
|
1468
|
+
isStreaming(actionName) {
|
|
1469
|
+
const action = this.actions.get(actionName);
|
|
1470
|
+
return !!action?.isStreaming;
|
|
1471
|
+
}
|
|
1472
|
+
/**
|
|
1473
|
+
* Execute a streaming action, returning an async iterable of chunks.
|
|
1474
|
+
*/
|
|
1475
|
+
async *executeStream(actionName, input) {
|
|
1476
|
+
const action = this.actions.get(actionName);
|
|
1477
|
+
if (!action || !action.isStreaming) {
|
|
1478
|
+
throw new Error(`Streaming action '${actionName}' not found`);
|
|
1479
|
+
}
|
|
1480
|
+
const services = resolveServices(action.services);
|
|
1481
|
+
for await (const chunk of action.handler(input, ...services)) {
|
|
1482
|
+
yield chunk;
|
|
1483
|
+
}
|
|
1484
|
+
}
|
|
1282
1485
|
}
|
|
1283
1486
|
const actionRegistry = new ActionRegistry();
|
|
1284
1487
|
function registerAction(action) {
|
|
@@ -1402,6 +1605,15 @@ async function discoverAndRegisterActions(options) {
|
|
|
1402
1605
|
`[Actions] Registered: ${exportValue.actionName}`
|
|
1403
1606
|
);
|
|
1404
1607
|
}
|
|
1608
|
+
} else if (isJayStreamAction(exportValue)) {
|
|
1609
|
+
registry.registerStream(exportValue);
|
|
1610
|
+
result.actionNames.push(exportValue.actionName);
|
|
1611
|
+
result.actionCount++;
|
|
1612
|
+
if (verbose) {
|
|
1613
|
+
getLogger().info(
|
|
1614
|
+
`[Actions] Registered stream: ${exportValue.actionName}`
|
|
1615
|
+
);
|
|
1616
|
+
}
|
|
1405
1617
|
}
|
|
1406
1618
|
}
|
|
1407
1619
|
} catch (error) {
|
|
@@ -1558,6 +1770,13 @@ async function registerNpmPluginActions(packageName, pluginConfig, pluginDir, re
|
|
|
1558
1770
|
if (verbose) {
|
|
1559
1771
|
getLogger().info(`[Actions] Registered NPM plugin action: ${registeredName}`);
|
|
1560
1772
|
}
|
|
1773
|
+
} else if (actionExport && isJayStreamAction(actionExport)) {
|
|
1774
|
+
registry.registerStream(actionExport);
|
|
1775
|
+
const registeredName = actionExport.actionName;
|
|
1776
|
+
registeredActions.push(registeredName);
|
|
1777
|
+
if (verbose) {
|
|
1778
|
+
getLogger().info(`[Actions] Registered NPM plugin stream: ${registeredName}`);
|
|
1779
|
+
}
|
|
1561
1780
|
} else {
|
|
1562
1781
|
getLogger().warn(
|
|
1563
1782
|
`[Actions] NPM plugin "${packageName}" declares action "${actionName}" but it's not exported or not a JayAction`
|
|
@@ -1626,6 +1845,13 @@ async function discoverPluginActions(pluginPath, projectRoot, registry = actionR
|
|
|
1626
1845
|
if (verbose) {
|
|
1627
1846
|
getLogger().info(`[Actions] Registered plugin action: ${registeredName}`);
|
|
1628
1847
|
}
|
|
1848
|
+
} else if (actionExport && isJayStreamAction(actionExport)) {
|
|
1849
|
+
registry.registerStream(actionExport);
|
|
1850
|
+
const registeredName = actionExport.actionName;
|
|
1851
|
+
registeredActions.push(registeredName);
|
|
1852
|
+
if (verbose) {
|
|
1853
|
+
getLogger().info(`[Actions] Registered plugin stream: ${registeredName}`);
|
|
1854
|
+
}
|
|
1629
1855
|
} else {
|
|
1630
1856
|
getLogger().warn(
|
|
1631
1857
|
`[Actions] Plugin "${pluginName}" declares action "${actionName}" but it's not exported or not a JayAction`
|
|
@@ -2324,6 +2550,12 @@ async function materializeContracts(options, services = /* @__PURE__ */ new Map(
|
|
|
2324
2550
|
};
|
|
2325
2551
|
});
|
|
2326
2552
|
}
|
|
2553
|
+
if (manifest.routes?.length) {
|
|
2554
|
+
entry.routes = manifest.routes.map((r) => ({
|
|
2555
|
+
path: r.path,
|
|
2556
|
+
...r.description && { description: r.description }
|
|
2557
|
+
}));
|
|
2558
|
+
}
|
|
2327
2559
|
pluginsIndexMap.set(plugin.name, entry);
|
|
2328
2560
|
}
|
|
2329
2561
|
if (!dynamicOnly && manifest.contracts) {
|
|
@@ -2448,7 +2680,8 @@ async function materializeContracts(options, services = /* @__PURE__ */ new Map(
|
|
|
2448
2680
|
contracts: data.contracts,
|
|
2449
2681
|
...data.actions && data.actions.length > 0 && { actions: data.actions },
|
|
2450
2682
|
...data.services?.length && { services: data.services },
|
|
2451
|
-
...data.contexts?.length && { contexts: data.contexts }
|
|
2683
|
+
...data.contexts?.length && { contexts: data.contexts },
|
|
2684
|
+
...data.routes?.length && { routes: data.routes }
|
|
2452
2685
|
}))
|
|
2453
2686
|
};
|
|
2454
2687
|
fs.mkdirSync(outputDir, { recursive: true });
|
|
@@ -2505,6 +2738,12 @@ async function listContracts(options) {
|
|
|
2505
2738
|
};
|
|
2506
2739
|
});
|
|
2507
2740
|
}
|
|
2741
|
+
if (manifest.routes?.length) {
|
|
2742
|
+
entry.routes = manifest.routes.map((r) => ({
|
|
2743
|
+
path: r.path,
|
|
2744
|
+
...r.description && { description: r.description }
|
|
2745
|
+
}));
|
|
2746
|
+
}
|
|
2508
2747
|
pluginsMap.set(plugin.name, entry);
|
|
2509
2748
|
}
|
|
2510
2749
|
if (!dynamicOnly && manifest.contracts) {
|
|
@@ -2551,7 +2790,8 @@ async function listContracts(options) {
|
|
|
2551
2790
|
path: data.path,
|
|
2552
2791
|
contracts: data.contracts,
|
|
2553
2792
|
...data.services?.length && { services: data.services },
|
|
2554
|
-
...data.contexts?.length && { contexts: data.contexts }
|
|
2793
|
+
...data.contexts?.length && { contexts: data.contexts },
|
|
2794
|
+
...data.routes?.length && { routes: data.routes }
|
|
2555
2795
|
}))
|
|
2556
2796
|
};
|
|
2557
2797
|
}
|
|
@@ -2705,6 +2945,7 @@ export {
|
|
|
2705
2945
|
executePluginServerInits,
|
|
2706
2946
|
executePluginSetup,
|
|
2707
2947
|
generateClientScript,
|
|
2948
|
+
generateFrozenPageHtml,
|
|
2708
2949
|
generatePromiseReconstruction,
|
|
2709
2950
|
generateSSRPageHtml,
|
|
2710
2951
|
getActionCacheHeaders,
|
|
@@ -2735,7 +2976,6 @@ export {
|
|
|
2735
2976
|
runInitCallbacks,
|
|
2736
2977
|
runLoadParams,
|
|
2737
2978
|
runShutdownCallbacks,
|
|
2738
|
-
runSlowlyChangingRender,
|
|
2739
2979
|
scanPlugins,
|
|
2740
2980
|
serializeHeadTags,
|
|
2741
2981
|
setClientInitData,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jay-framework/stack-server-runtime",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.1",
|
|
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.
|
|
30
|
-
"@jay-framework/compiler-shared": "^0.
|
|
31
|
-
"@jay-framework/component": "^0.
|
|
32
|
-
"@jay-framework/fullstack-component": "^0.
|
|
33
|
-
"@jay-framework/logger": "^0.
|
|
34
|
-
"@jay-framework/runtime": "^0.
|
|
35
|
-
"@jay-framework/ssr-runtime": "^0.
|
|
36
|
-
"@jay-framework/stack-route-scanner": "^0.
|
|
37
|
-
"@jay-framework/view-state-merge": "^0.
|
|
29
|
+
"@jay-framework/compiler-jay-html": "^0.16.1",
|
|
30
|
+
"@jay-framework/compiler-shared": "^0.16.1",
|
|
31
|
+
"@jay-framework/component": "^0.16.1",
|
|
32
|
+
"@jay-framework/fullstack-component": "^0.16.1",
|
|
33
|
+
"@jay-framework/logger": "^0.16.1",
|
|
34
|
+
"@jay-framework/runtime": "^0.16.1",
|
|
35
|
+
"@jay-framework/ssr-runtime": "^0.16.1",
|
|
36
|
+
"@jay-framework/stack-route-scanner": "^0.16.1",
|
|
37
|
+
"@jay-framework/view-state-merge": "^0.16.1",
|
|
38
38
|
"yaml": "^2.3.4"
|
|
39
39
|
},
|
|
40
40
|
"devDependencies": {
|
|
41
|
-
"@jay-framework/dev-environment": "^0.
|
|
42
|
-
"@jay-framework/jay-cli": "^0.
|
|
43
|
-
"@jay-framework/stack-client-runtime": "^0.
|
|
41
|
+
"@jay-framework/dev-environment": "^0.16.1",
|
|
42
|
+
"@jay-framework/jay-cli": "^0.16.1",
|
|
43
|
+
"@jay-framework/stack-client-runtime": "^0.16.1",
|
|
44
44
|
"@types/express": "^5.0.2",
|
|
45
45
|
"@types/node": "^22.15.21",
|
|
46
46
|
"nodemon": "^3.0.3",
|