@jay-framework/dev-server 0.15.6 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/dist/index.d.ts +61 -1
  2. package/dist/index.js +387 -25
  3. package/package.json +14 -14
package/dist/index.d.ts CHANGED
@@ -5,6 +5,7 @@ import { JayRollupConfig } from '@jay-framework/rollup-plugin';
5
5
  import { LogLevel } from '@jay-framework/logger';
6
6
  import { Server } from 'node:http';
7
7
  import { ProjectClientInitInfo, PluginWithInit, ActionRegistry } from '@jay-framework/stack-server-runtime';
8
+ import * as _jay_framework_fullstack_component from '@jay-framework/fullstack-component';
8
9
  import { RequestHandler as RequestHandler$1 } from 'express';
9
10
  import { JayRollupConfig as JayRollupConfig$1 } from '@jay-framework/compiler-jay-stack';
10
11
 
@@ -103,6 +104,62 @@ declare class ServiceLifecycleManager {
103
104
  isReady(): boolean;
104
105
  }
105
106
 
107
+ /**
108
+ * Page Freeze — Design Log #127
109
+ *
110
+ * Captures a page's ViewState at a point in time and serves it as a
111
+ * static SSR snapshot. Enables designers to create multiple frozen
112
+ * views of a page in different states for side-by-side comparison.
113
+ */
114
+ interface FreezeEntry {
115
+ id: string;
116
+ name?: string;
117
+ /** The concrete URL path (e.g., /products/kitan) */
118
+ route: string;
119
+ /** The route pattern (e.g., /products/kitan{/:category}) */
120
+ routePattern?: string;
121
+ viewState: object;
122
+ createdAt: string;
123
+ }
124
+ declare class FreezeStore {
125
+ private readonly dir;
126
+ constructor(buildFolder: string);
127
+ save(route: string, viewState: object, routePattern?: string): Promise<FreezeEntry>;
128
+ get(id: string): Promise<FreezeEntry | undefined>;
129
+ list(route?: string): Promise<FreezeEntry[]>;
130
+ rename(id: string, name: string): Promise<boolean>;
131
+ delete(id: string): Promise<boolean>;
132
+ }
133
+
134
+ /**
135
+ * Service marker for DevServerService.
136
+ * Use with `.withServices(DEV_SERVER_SERVICE)` in actions and components.
137
+ */
138
+ declare const DEV_SERVER_SERVICE: _jay_framework_fullstack_component.ServiceMarker<DevServerService>;
139
+ interface RouteInfo {
140
+ path: string;
141
+ jayHtmlPath: string;
142
+ compPath: string;
143
+ }
144
+ declare class DevServerService {
145
+ private routes;
146
+ private vite;
147
+ private pagesBase;
148
+ private projectBase;
149
+ private jayRollupConfig;
150
+ private _freezeStore?;
151
+ constructor(routes: DevServerRoute[], vite: ViteDevServer, pagesBase: string, projectBase: string, jayRollupConfig: JayRollupConfig, _freezeStore?: FreezeStore);
152
+ get freezeStore(): FreezeStore | undefined;
153
+ /** List all page routes in the project. */
154
+ listRoutes(): RouteInfo[];
155
+ /**
156
+ * Run loadParams for a route, yielding param batches as an async generator.
157
+ * Loads all page parts (page component + keyed headless components) and
158
+ * calls loadParams on each one that defines it.
159
+ */
160
+ loadRouteParams(routePath: string): AsyncGenerator<Record<string, string>[]>;
161
+ }
162
+
106
163
  interface DevServerRoute {
107
164
  path: string;
108
165
  handler: RequestHandler;
@@ -113,6 +170,9 @@ interface DevServer {
113
170
  viteServer: ViteDevServer;
114
171
  routes: DevServerRoute[];
115
172
  lifecycleManager: ServiceLifecycleManager;
173
+ freezeStore?: FreezeStore;
174
+ /** Public API for design board applications and CLI (DL#128) */
175
+ service: DevServerService;
116
176
  }
117
177
  declare function mkDevServer(rawOptions: DevServerOptions): Promise<DevServer>;
118
178
 
@@ -207,4 +267,4 @@ declare function createViteForCli(options: {
207
267
  tsConfigFilePath?: string;
208
268
  }): Promise<ViteDevServer>;
209
269
 
210
- export { ACTION_ENDPOINT_BASE, type ActionRouterOptions, type CreateViteServerOptions, type DevServer, type DevServerOptions, type DevServerRoute, actionBodyParser, createActionRouter, createViteForCli, createViteServer, mkDevServer };
270
+ export { ACTION_ENDPOINT_BASE, type ActionRouterOptions, type CreateViteServerOptions, DEV_SERVER_SERVICE, type DevServer, type DevServerOptions, type DevServerRoute, DevServerService, type FreezeEntry, FreezeStore, type RouteInfo, actionBodyParser, createActionRouter, createViteForCli, createViteServer, mkDevServer };
package/dist/index.js CHANGED
@@ -14,10 +14,14 @@ import { JAY_IMPORT_RESOLVER, injectHeadfullFSTemplates, parseContract, slowRend
14
14
  import { getLogger, getDevLogger } from "@jay-framework/logger";
15
15
  import { createRequire as createRequire$1 } from "node:module";
16
16
  import * as fs from "node:fs";
17
- import { scanRoutes, routeToExpressRoute } from "@jay-framework/stack-route-scanner";
18
- import { discoverPluginsWithInit, sortPluginsByDependencies, executePluginServerInits, runInitCallbacks, actionRegistry, discoverAndRegisterActions, discoverAllPluginActions, runShutdownCallbacks, clearLifecycleCallbacks, clearServiceRegistry, clearClientInitData, DevSlowlyChangingPhase, SlowRenderCache, preparePluginClientInits, clearServerElementCache, getServiceRegistry, materializeContracts, loadPageParts, renderFastChangingData, mergeHeadTags, generateClientScript, getClientInitData, generateSSRPageHtml, validateForEachInstances, slowRenderInstances } from "@jay-framework/stack-server-runtime";
19
- import fs$1 from "node:fs/promises";
17
+ import fs__default$1 from "node:fs";
18
+ import { scanRoutes, createRoute, routeToExpressRoute } from "@jay-framework/stack-route-scanner";
19
+ import { discoverPluginsWithInit, sortPluginsByDependencies, executePluginServerInits, runInitCallbacks, actionRegistry, discoverAndRegisterActions, discoverAllPluginActions, runShutdownCallbacks, clearLifecycleCallbacks, clearServiceRegistry, clearClientInitData, loadPageParts, runLoadParams, DevSlowlyChangingPhase, SlowRenderCache, preparePluginClientInits, registerService, clearServerElementCache, scanPlugins, getServiceRegistry, materializeContracts, renderFastChangingData, mergeHeadTags, generateClientScript, getClientInitData, generateSSRPageHtml, generateFrozenPageHtml, validateForEachInstances, slowRenderInstances } from "@jay-framework/stack-server-runtime";
20
+ import * as fs$1 from "node:fs/promises";
21
+ import fs__default from "node:fs/promises";
20
22
  import { pathToFileURL } from "node:url";
23
+ import { randomUUID } from "node:crypto";
24
+ import { createJayService } from "@jay-framework/fullstack-component";
21
25
  const s$1 = createRequire(import.meta.url), e$1 = s$1("typescript"), c$1 = new Proxy(e$1, {
22
26
  get(t, r) {
23
27
  return t[r];
@@ -927,9 +931,16 @@ function extractActionFromExpression(node) {
927
931
  }
928
932
  if (c$1.isIdentifier(expr)) {
929
933
  const funcName = expr.text;
930
- if (funcName === "makeJayAction" || funcName === "makeJayQuery") {
934
+ if (funcName === "makeJayAction" || funcName === "makeJayQuery" || funcName === "makeJayStream") {
931
935
  const nameArg = current.arguments[0];
932
936
  if (nameArg && c$1.isStringLiteral(nameArg)) {
937
+ if (funcName === "makeJayStream") {
938
+ return {
939
+ actionName: nameArg.text,
940
+ method: "POST",
941
+ isStreaming: true
942
+ };
943
+ }
933
944
  method = funcName === "makeJayQuery" ? "GET" : "POST";
934
945
  if (explicitMethod) {
935
946
  method = explicitMethod;
@@ -1331,14 +1342,27 @@ function jayStackCompiler(options = {}) {
1331
1342
  getLogger().warn(`[action-transform] No actions found in ${actualPath}`);
1332
1343
  return null;
1333
1344
  }
1345
+ const hasRegularActions = actions.some((a) => !a.isStreaming);
1346
+ const hasStreamActions = actions.some((a) => a.isStreaming);
1347
+ const importNames = [];
1348
+ if (hasRegularActions)
1349
+ importNames.push("createActionCaller");
1350
+ if (hasStreamActions)
1351
+ importNames.push("createStreamCaller");
1334
1352
  const lines = [
1335
- `import { createActionCaller } from '@jay-framework/stack-client-runtime';`,
1353
+ `import { ${importNames.join(", ")} } from '@jay-framework/stack-client-runtime';`,
1336
1354
  ""
1337
1355
  ];
1338
1356
  for (const action of actions) {
1339
- lines.push(
1340
- `export const ${action.exportName} = createActionCaller('${action.actionName}', '${action.method}');`
1341
- );
1357
+ if (action.isStreaming) {
1358
+ lines.push(
1359
+ `export const ${action.exportName} = createStreamCaller('${action.actionName}');`
1360
+ );
1361
+ } else {
1362
+ lines.push(
1363
+ `export const ${action.exportName} = createActionCaller('${action.actionName}', '${action.method}');`
1364
+ );
1365
+ }
1342
1366
  }
1343
1367
  if (code.includes("ActionError")) {
1344
1368
  lines.push(
@@ -1758,6 +1782,22 @@ function createActionRouter(options) {
1758
1782
  requestMethod,
1759
1783
  ACTION_ENDPOINT_BASE + "/" + actionName
1760
1784
  );
1785
+ if (registry.isStreaming(actionName)) {
1786
+ res.setHeader("Content-Type", "application/x-ndjson");
1787
+ res.setHeader("Transfer-Encoding", "chunked");
1788
+ try {
1789
+ const generator = registry.executeStream(actionName, input);
1790
+ for await (const chunk of generator) {
1791
+ res.write(JSON.stringify({ chunk }) + "\n");
1792
+ }
1793
+ res.write(JSON.stringify({ done: true }) + "\n");
1794
+ } catch (err) {
1795
+ res.write(JSON.stringify({ error: err.message }) + "\n");
1796
+ }
1797
+ res.end();
1798
+ timing?.end();
1799
+ return;
1800
+ }
1761
1801
  const result = await registry.execute(actionName, input);
1762
1802
  if (requestMethod === "GET" && result.success) {
1763
1803
  const cacheHeaders = registry.getCacheHeaders(actionName);
@@ -1831,6 +1871,124 @@ function actionBodyParser() {
1831
1871
  });
1832
1872
  };
1833
1873
  }
1874
+ class FreezeStore {
1875
+ constructor(buildFolder) {
1876
+ __publicField(this, "dir");
1877
+ this.dir = path.join(buildFolder, "freezes");
1878
+ }
1879
+ async save(route, viewState, routePattern) {
1880
+ await fs$1.mkdir(this.dir, { recursive: true });
1881
+ const entry = {
1882
+ id: randomUUID().slice(0, 8),
1883
+ route,
1884
+ ...routePattern && { routePattern },
1885
+ viewState,
1886
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
1887
+ };
1888
+ await fs$1.writeFile(
1889
+ path.join(this.dir, `${entry.id}.json`),
1890
+ JSON.stringify(entry, null, 2),
1891
+ "utf-8"
1892
+ );
1893
+ return entry;
1894
+ }
1895
+ async get(id) {
1896
+ try {
1897
+ const content = await fs$1.readFile(path.join(this.dir, `${id}.json`), "utf-8");
1898
+ return JSON.parse(content);
1899
+ } catch {
1900
+ return void 0;
1901
+ }
1902
+ }
1903
+ async list(route) {
1904
+ try {
1905
+ const files = await fs$1.readdir(this.dir);
1906
+ const entries = [];
1907
+ for (const file of files) {
1908
+ if (!file.endsWith(".json"))
1909
+ continue;
1910
+ try {
1911
+ const content = await fs$1.readFile(path.join(this.dir, file), "utf-8");
1912
+ const entry = JSON.parse(content);
1913
+ if (!route || entry.routePattern === route || entry.route === route) {
1914
+ entries.push(entry);
1915
+ }
1916
+ } catch {
1917
+ }
1918
+ }
1919
+ return entries.sort(
1920
+ (a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
1921
+ );
1922
+ } catch {
1923
+ return [];
1924
+ }
1925
+ }
1926
+ async rename(id, name) {
1927
+ const entry = await this.get(id);
1928
+ if (!entry)
1929
+ return false;
1930
+ entry.name = name;
1931
+ await fs$1.writeFile(
1932
+ path.join(this.dir, `${id}.json`),
1933
+ JSON.stringify(entry, null, 2),
1934
+ "utf-8"
1935
+ );
1936
+ return true;
1937
+ }
1938
+ async delete(id) {
1939
+ try {
1940
+ await fs$1.unlink(path.join(this.dir, `${id}.json`));
1941
+ return true;
1942
+ } catch {
1943
+ return false;
1944
+ }
1945
+ }
1946
+ }
1947
+ const DEV_SERVER_SERVICE = createJayService("DevServerService");
1948
+ class DevServerService {
1949
+ constructor(routes, vite, pagesBase, projectBase, jayRollupConfig, _freezeStore) {
1950
+ this.routes = routes;
1951
+ this.vite = vite;
1952
+ this.pagesBase = pagesBase;
1953
+ this.projectBase = projectBase;
1954
+ this.jayRollupConfig = jayRollupConfig;
1955
+ this._freezeStore = _freezeStore;
1956
+ }
1957
+ get freezeStore() {
1958
+ return this._freezeStore;
1959
+ }
1960
+ /** List all page routes in the project. */
1961
+ listRoutes() {
1962
+ return this.routes.map((r) => ({
1963
+ path: r.path,
1964
+ jayHtmlPath: r.fsRoute.jayHtmlPath,
1965
+ compPath: r.fsRoute.compPath
1966
+ }));
1967
+ }
1968
+ /**
1969
+ * Run loadParams for a route, yielding param batches as an async generator.
1970
+ * Loads all page parts (page component + keyed headless components) and
1971
+ * calls loadParams on each one that defines it.
1972
+ */
1973
+ async *loadRouteParams(routePath) {
1974
+ const matched = this.routes.find((r) => r.path === routePath);
1975
+ if (!matched) {
1976
+ getLogger().error(`[loadRouteParams] Route [${routePath}] not found`);
1977
+ throw new Error(`Route "${routePath}" not found`);
1978
+ }
1979
+ const loaded = await loadPageParts(
1980
+ this.vite,
1981
+ matched.fsRoute,
1982
+ this.pagesBase,
1983
+ this.projectBase,
1984
+ this.jayRollupConfig
1985
+ );
1986
+ if (!loaded.val) {
1987
+ return;
1988
+ }
1989
+ yield* runLoadParams(loaded.val.parts);
1990
+ }
1991
+ }
1834
1992
  let _watchLinkedFiles = () => {
1835
1993
  };
1836
1994
  async function initRoutes(pagesBaseFolder) {
@@ -1839,6 +1997,82 @@ async function initRoutes(pagesBaseFolder) {
1839
1997
  compFilename: "page.ts"
1840
1998
  });
1841
1999
  }
2000
+ async function scanPluginRoutes(projectRoot, projectRoutes) {
2001
+ const plugins = await scanPlugins({ projectRoot, includeDevDeps: true });
2002
+ const projectPaths = new Set(projectRoutes.map((r) => r.rawRoute));
2003
+ const pluginRoutes = [];
2004
+ for (const [, plugin] of plugins) {
2005
+ if (!plugin.manifest.routes)
2006
+ continue;
2007
+ for (const route of plugin.manifest.routes) {
2008
+ if (projectPaths.has(route.path)) {
2009
+ getLogger().info(
2010
+ `[Routes] Plugin "${plugin.name}" route ${route.path} skipped — project route takes precedence`
2011
+ );
2012
+ continue;
2013
+ }
2014
+ const jayHtmlPath = resolvePluginExport(plugin.pluginPath, route.jayHtml);
2015
+ if (!jayHtmlPath) {
2016
+ getLogger().warn(
2017
+ `[Routes] Plugin "${plugin.name}" route ${route.path}: jayHtml "${route.jayHtml}" not found`
2018
+ );
2019
+ continue;
2020
+ }
2021
+ const compPath = route.component.startsWith(".") ? path__default.resolve(plugin.pluginPath, route.component) : resolvePluginModule(plugin);
2022
+ pluginRoutes.push(createRoute(route.path, jayHtmlPath, compPath));
2023
+ getLogger().info(`[Routes] Plugin "${plugin.name}" provides route ${route.path}`);
2024
+ }
2025
+ }
2026
+ return pluginRoutes;
2027
+ }
2028
+ function resolvePluginExport(pluginPath, exportSubpath) {
2029
+ const normalized = exportSubpath.replace(/^\.\//, "");
2030
+ const packageJsonPath = path__default.join(pluginPath, "package.json");
2031
+ try {
2032
+ const packageJson = JSON.parse(fs__default$1.readFileSync(packageJsonPath, "utf-8"));
2033
+ if (packageJson.exports) {
2034
+ const exportKey = "./" + normalized;
2035
+ const exportValue = packageJson.exports[exportKey];
2036
+ if (exportValue) {
2037
+ const resolved = typeof exportValue === "string" ? exportValue : exportValue.default || exportValue.import || exportValue.require;
2038
+ if (resolved) {
2039
+ const fullPath = path__default.join(pluginPath, resolved);
2040
+ return fullPath;
2041
+ }
2042
+ }
2043
+ }
2044
+ } catch {
2045
+ }
2046
+ for (const dir of ["dist", "lib", ""]) {
2047
+ const candidate = path__default.join(pluginPath, dir, normalized);
2048
+ try {
2049
+ fs__default$1.accessSync(candidate);
2050
+ return candidate;
2051
+ } catch {
2052
+ }
2053
+ }
2054
+ return void 0;
2055
+ }
2056
+ function resolvePluginModule(plugin) {
2057
+ const modulePath = plugin.manifest.module || "index";
2058
+ for (const ext of [".ts", ".js", "/index.ts", "/index.js"]) {
2059
+ const candidate = path__default.join(plugin.pluginPath, modulePath + ext);
2060
+ try {
2061
+ fs__default$1.accessSync(candidate);
2062
+ return candidate;
2063
+ } catch {
2064
+ }
2065
+ }
2066
+ for (const ext of [".ts", ".js"]) {
2067
+ const candidate = path__default.join(plugin.pluginPath, "lib", path__default.basename(modulePath) + ext);
2068
+ try {
2069
+ fs__default$1.accessSync(candidate);
2070
+ return candidate;
2071
+ } catch {
2072
+ }
2073
+ }
2074
+ return path__default.join(plugin.pluginPath, modulePath);
2075
+ }
1842
2076
  function defaults(options) {
1843
2077
  const publicBaseUrlPath = options.publicBaseUrlPath || process.env.BASE || "/";
1844
2078
  const projectRootFolder = options.projectRootFolder || ".";
@@ -1900,7 +2134,7 @@ function filterPluginsForPage(allPluginClientInits, allPluginsWithInit, usedPack
1900
2134
  return pluginInfo && expandedPackages.has(pluginInfo.packageName);
1901
2135
  });
1902
2136
  }
1903
- function mkRoute(route, vite, slowlyPhase, options, slowRenderCache, projectInit, allPluginsWithInit = [], allPluginClientInits = []) {
2137
+ function mkRoute(route, vite, slowlyPhase, options, slowRenderCache, freezeStore, projectInit, allPluginsWithInit = [], allPluginClientInits = []) {
1904
2138
  const routePath = routeToExpressRoute(route);
1905
2139
  const handler = async (req, res) => {
1906
2140
  const timing = getDevLogger()?.startRequest(req.method, req.path);
@@ -1916,6 +2150,23 @@ function mkRoute(route, vite, slowlyPhase, options, slowRenderCache, projectInit
1916
2150
  for (const [key, value] of urlObj.searchParams) {
1917
2151
  query[key] = value;
1918
2152
  }
2153
+ const freezeId = query["_jay_freeze"];
2154
+ if (freezeId && freezeStore) {
2155
+ timing?.annotate("[FROZEN]");
2156
+ await handleFrozenRequest(
2157
+ vite,
2158
+ route,
2159
+ options,
2160
+ freezeStore,
2161
+ slowRenderCache,
2162
+ freezeId,
2163
+ pageParams,
2164
+ query["format"] === "fragment" ? "fragment" : "page",
2165
+ res,
2166
+ timing
2167
+ );
2168
+ return;
2169
+ }
1919
2170
  if (options.disableSSR) {
1920
2171
  await handleClientOnlyRequest(
1921
2172
  vite,
@@ -2048,6 +2299,7 @@ async function handleCachedRequest(vite, route, options, cachedEntry, pageParams
2048
2299
  projectInit,
2049
2300
  pluginsForPage,
2050
2301
  options,
2302
+ routeToExpressRoute(route),
2051
2303
  cachedEntry.slowViewState,
2052
2304
  timing,
2053
2305
  cachedEntry.preRenderedContent,
@@ -2227,14 +2479,15 @@ async function handleClientOnlyRequest(vite, route, options, slowlyPhase, pagePa
2227
2479
  projectInit,
2228
2480
  pluginsForPage,
2229
2481
  {
2230
- enableAutomation: !options.disableAutomation
2482
+ enableAutomation: !options.disableAutomation,
2483
+ routePattern: routeToExpressRoute(route)
2231
2484
  }
2232
2485
  );
2233
2486
  if (options.buildFolder) {
2234
2487
  const pageName = !url || url === "/" ? "index" : url.replace(/^\//, "").replace(/\//g, "-");
2235
2488
  const clientScriptDir = path__default.join(options.buildFolder, "debug", "client-entry");
2236
- await fs$1.mkdir(clientScriptDir, { recursive: true });
2237
- await fs$1.writeFile(path__default.join(clientScriptDir, `${pageName}.html`), pageHtml, "utf-8");
2489
+ await fs__default.mkdir(clientScriptDir, { recursive: true });
2490
+ await fs__default.writeFile(path__default.join(clientScriptDir, `${pageName}.html`), pageHtml, "utf-8");
2238
2491
  }
2239
2492
  const viteStart = Date.now();
2240
2493
  const compiledPageHtml = await vite.transformIndexHtml(!!url ? url : "/", pageHtml);
@@ -2242,11 +2495,11 @@ async function handleClientOnlyRequest(vite, route, options, slowlyPhase, pagePa
2242
2495
  res.status(200).set({ "Content-Type": "text/html" }).send(compiledPageHtml);
2243
2496
  timing?.end();
2244
2497
  }
2245
- async function sendResponse(vite, res, url, jayHtmlPath, sourceJayHtmlPath, pageParts, viewState, carryForward, clientTrackByMap, projectInit, pluginsForPage, options, slowViewState, timing, preLoadedContent, headTags) {
2498
+ async function sendResponse(vite, res, url, jayHtmlPath, sourceJayHtmlPath, pageParts, viewState, carryForward, clientTrackByMap, projectInit, pluginsForPage, options, routePattern, slowViewState, timing, preLoadedContent, headTags) {
2246
2499
  let pageHtml;
2247
2500
  const routeDir = path__default.dirname(path__default.relative(options.pagesRootFolder, sourceJayHtmlPath));
2248
2501
  try {
2249
- let jayHtmlContent = preLoadedContent ?? await fs$1.readFile(jayHtmlPath, "utf-8");
2502
+ let jayHtmlContent = preLoadedContent ?? await fs__default.readFile(jayHtmlPath, "utf-8");
2250
2503
  const jayHtmlFilename = path__default.basename(jayHtmlPath);
2251
2504
  const jayHtmlDir = path__default.dirname(jayHtmlPath);
2252
2505
  const sourceDir = path__default.dirname(sourceJayHtmlPath);
@@ -2270,7 +2523,8 @@ async function sendResponse(vite, res, url, jayHtmlPath, sourceJayHtmlPath, page
2270
2523
  pluginsForPage,
2271
2524
  {
2272
2525
  enableAutomation: !options.disableAutomation,
2273
- slowViewState
2526
+ slowViewState,
2527
+ routePattern
2274
2528
  },
2275
2529
  // Pass source directory for headfull FS file resolution when using pre-rendered path
2276
2530
  jayHtmlDir !== sourceDir ? sourceDir : void 0,
@@ -2289,15 +2543,16 @@ async function sendResponse(vite, res, url, jayHtmlPath, sourceJayHtmlPath, page
2289
2543
  pluginsForPage,
2290
2544
  {
2291
2545
  enableAutomation: !options.disableAutomation,
2292
- slowViewState
2546
+ slowViewState,
2547
+ routePattern
2293
2548
  }
2294
2549
  );
2295
2550
  }
2296
2551
  if (options.buildFolder) {
2297
2552
  const pageName = !url || url === "/" ? "index" : url.replace(/^\//, "").replace(/\//g, "-");
2298
2553
  const clientScriptDir = path__default.join(options.buildFolder, "debug", "client-entry");
2299
- await fs$1.mkdir(clientScriptDir, { recursive: true });
2300
- await fs$1.writeFile(path__default.join(clientScriptDir, `${pageName}.html`), pageHtml, "utf-8");
2554
+ await fs__default.mkdir(clientScriptDir, { recursive: true });
2555
+ await fs__default.writeFile(path__default.join(clientScriptDir, `${pageName}.html`), pageHtml, "utf-8");
2301
2556
  }
2302
2557
  const viteStart = Date.now();
2303
2558
  const compiledPageHtml = await vite.transformIndexHtml(!!url ? url : "/", pageHtml);
@@ -2305,12 +2560,62 @@ async function sendResponse(vite, res, url, jayHtmlPath, sourceJayHtmlPath, page
2305
2560
  res.status(200).set({ "Content-Type": "text/html" }).send(compiledPageHtml);
2306
2561
  timing?.end();
2307
2562
  }
2563
+ async function handleFrozenRequest(vite, route, options, freezeStore, slowRenderCache, freezeId, pageParams, format, res, timing) {
2564
+ const entry = await freezeStore.get(freezeId);
2565
+ if (!entry) {
2566
+ getLogger().warn(`[Freeze] Freeze "${freezeId}" not found`);
2567
+ res.status(404).send(`Freeze "${freezeId}" not found`);
2568
+ timing?.end();
2569
+ return;
2570
+ }
2571
+ const label = entry.name ? `"${entry.name}" (${freezeId})` : freezeId;
2572
+ getLogger().info(`[Freeze] Serving frozen page ${label} for ${route.rawRoute} [${format}]`);
2573
+ try {
2574
+ const cachedEntry = await slowRenderCache.get(route.jayHtmlPath, pageParams);
2575
+ const jayHtmlPath = cachedEntry?.preRenderedPath ?? route.jayHtmlPath;
2576
+ const jayHtmlContent = cachedEntry?.preRenderedContent ?? await fs__default.readFile(jayHtmlPath, "utf-8");
2577
+ const jayHtmlFilename = path__default.basename(jayHtmlPath);
2578
+ const jayHtmlDir = path__default.dirname(jayHtmlPath);
2579
+ const sourceDir = path__default.dirname(route.jayHtmlPath);
2580
+ const routeDir = path__default.dirname(path__default.relative(options.pagesRootFolder, route.jayHtmlPath));
2581
+ const { injectHeadfullFSTemplates: injectHeadfullFSTemplates2 } = await import("@jay-framework/compiler-jay-html");
2582
+ const { JAY_IMPORT_RESOLVER: JAY_IMPORT_RESOLVER2 } = await import("@jay-framework/compiler-jay-html");
2583
+ const fullJayHtml = injectHeadfullFSTemplates2(
2584
+ jayHtmlContent,
2585
+ sourceDir,
2586
+ JAY_IMPORT_RESOLVER2
2587
+ );
2588
+ const html = await generateFrozenPageHtml(
2589
+ vite,
2590
+ fullJayHtml,
2591
+ jayHtmlFilename,
2592
+ jayHtmlDir,
2593
+ entry.viewState,
2594
+ options.buildFolder,
2595
+ options.projectRootFolder,
2596
+ routeDir,
2597
+ options.jayRollupConfig?.tsConfigFilePath,
2598
+ void 0,
2599
+ format,
2600
+ entry.name
2601
+ );
2602
+ const headers = { "Content-Type": "text/html" };
2603
+ if (format === "fragment") {
2604
+ headers["Access-Control-Allow-Origin"] = "*";
2605
+ }
2606
+ res.status(200).set(headers).send(html);
2607
+ } catch (err) {
2608
+ getLogger().warn(`[Freeze] Failed to render frozen page: ${err.message}`);
2609
+ res.status(500).send(`Failed to render frozen page: ${err.message}`);
2610
+ }
2611
+ timing?.end();
2612
+ }
2308
2613
  async function preRenderJayHtml(route, slowViewState, headlessContracts, headlessInstanceComponents, partKeys = []) {
2309
- const jayHtmlContent = await fs$1.readFile(route.jayHtmlPath, "utf-8");
2614
+ const jayHtmlContent = await fs__default.readFile(route.jayHtmlPath, "utf-8");
2310
2615
  const contractPath = route.jayHtmlPath.replace(".jay-html", ".jay-contract");
2311
2616
  let contract;
2312
2617
  try {
2313
- const contractContent = await fs$1.readFile(contractPath, "utf-8");
2618
+ const contractContent = await fs__default.readFile(contractPath, "utf-8");
2314
2619
  const parseResult = parseContract(contractContent, path__default.basename(contractPath));
2315
2620
  if (parseResult.val) {
2316
2621
  contract = parseResult.val;
@@ -2452,8 +2757,15 @@ async function mkDevServer(rawOptions) {
2452
2757
  const options = defaults(rawOptions);
2453
2758
  const { publicBaseUrlPath, pagesRootFolder, projectRootFolder, buildFolder, jayRollupConfig } = options;
2454
2759
  if (buildFolder) {
2455
- await fs$1.rm(buildFolder, { recursive: true, force: true }).catch(() => {
2456
- });
2760
+ try {
2761
+ const entries = await fs__default.readdir(buildFolder).catch(() => []);
2762
+ for (const entry of entries) {
2763
+ if (entry === "freezes")
2764
+ continue;
2765
+ await fs__default.rm(path__default.join(buildFolder, entry), { recursive: true, force: true });
2766
+ }
2767
+ } catch {
2768
+ }
2457
2769
  }
2458
2770
  const viteLogLevel = options.logLevel === "silent" ? "silent" : options.logLevel === "verbose" ? "info" : "warn";
2459
2771
  const lifecycleManager = new ServiceLifecycleManager(projectRootFolder);
@@ -2471,8 +2783,10 @@ async function mkDevServer(rawOptions) {
2471
2783
  await materializeDynamicContracts(projectRootFolder, vite);
2472
2784
  setupServiceHotReload(vite, lifecycleManager);
2473
2785
  setupActionRouter(vite);
2474
- const allRoutes = await initRoutes(pagesRootFolder);
2475
- const routes = buildFolder ? allRoutes.filter((route) => !route.jayHtmlPath.startsWith(buildFolder)) : allRoutes;
2786
+ const projectRoutes = await initRoutes(pagesRootFolder);
2787
+ const filteredProjectRoutes = buildFolder ? projectRoutes.filter((route) => !route.jayHtmlPath.startsWith(buildFolder)) : projectRoutes;
2788
+ const pluginRoutes = await scanPluginRoutes(projectRootFolder, filteredProjectRoutes);
2789
+ const routes = [...filteredProjectRoutes, ...pluginRoutes];
2476
2790
  const slowlyPhase = new DevSlowlyChangingPhase();
2477
2791
  const slowRenderCacheDir = path__default.join(buildFolder, "pre-rendered");
2478
2792
  const slowRenderCache = new SlowRenderCache(slowRenderCacheDir, pagesRootFolder);
@@ -2484,6 +2798,10 @@ async function mkDevServer(rawOptions) {
2484
2798
  const projectInit = lifecycleManager.getProjectInit() ?? void 0;
2485
2799
  const pluginsWithInit = lifecycleManager.getPluginsWithInit();
2486
2800
  const pluginClientInits = preparePluginClientInits(pluginsWithInit);
2801
+ const freezeStore = buildFolder ? new FreezeStore(buildFolder) : void 0;
2802
+ if (freezeStore) {
2803
+ setupFreezeEndpoint(vite, freezeStore);
2804
+ }
2487
2805
  const devServerRoutes = routes.map(
2488
2806
  (route) => mkRoute(
2489
2807
  route,
@@ -2491,16 +2809,28 @@ async function mkDevServer(rawOptions) {
2491
2809
  slowlyPhase,
2492
2810
  options,
2493
2811
  slowRenderCache,
2812
+ freezeStore,
2494
2813
  projectInit,
2495
2814
  pluginsWithInit,
2496
2815
  pluginClientInits
2497
2816
  )
2498
2817
  );
2818
+ const service = new DevServerService(
2819
+ devServerRoutes,
2820
+ vite,
2821
+ options.pagesRootFolder,
2822
+ options.projectRootFolder,
2823
+ options.jayRollupConfig,
2824
+ freezeStore
2825
+ );
2826
+ registerService(DEV_SERVER_SERVICE, service);
2499
2827
  return {
2500
2828
  server: vite.middlewares,
2501
2829
  viteServer: vite,
2502
2830
  routes: devServerRoutes,
2503
- lifecycleManager
2831
+ lifecycleManager,
2832
+ freezeStore,
2833
+ service
2504
2834
  };
2505
2835
  }
2506
2836
  function setupGracefulShutdown(lifecycleManager) {
@@ -2539,6 +2869,35 @@ function setupActionRouter(vite) {
2539
2869
  vite.middlewares.use(ACTION_ENDPOINT_BASE, createActionRouter());
2540
2870
  getLogger().info(`[Actions] Action router mounted at ${ACTION_ENDPOINT_BASE}`);
2541
2871
  }
2872
+ function setupFreezeEndpoint(vite, freezeStore) {
2873
+ vite.middlewares.use((req, res, next) => {
2874
+ if (req.method === "POST" && (req.url === "/_jay/freeze" || req.originalUrl === "/_jay/freeze")) {
2875
+ let body = "";
2876
+ req.on("data", (chunk) => body += chunk);
2877
+ req.on("end", async () => {
2878
+ try {
2879
+ const { route, routePattern, viewState } = JSON.parse(body);
2880
+ if (!route || !viewState) {
2881
+ res.writeHead(400, { "Content-Type": "application/json" });
2882
+ res.end(JSON.stringify({ error: "Missing route or viewState" }));
2883
+ return;
2884
+ }
2885
+ const entry = await freezeStore.save(route, viewState, routePattern);
2886
+ getLogger().info(`[Freeze] Saved freeze "${entry.id}" for ${route}`);
2887
+ res.writeHead(200, { "Content-Type": "application/json" });
2888
+ res.end(JSON.stringify(entry));
2889
+ } catch (err) {
2890
+ getLogger().warn(`[Freeze] Failed to save: ${err.message}`);
2891
+ res.writeHead(500, { "Content-Type": "application/json" });
2892
+ res.end(JSON.stringify({ error: err.message }));
2893
+ }
2894
+ });
2895
+ } else {
2896
+ next();
2897
+ }
2898
+ });
2899
+ getLogger().info("[Freeze] Freeze endpoint mounted at /_jay/freeze");
2900
+ }
2542
2901
  function setupSlowRenderCacheInvalidation(vite, cache, pagesRootFolder, projectRootFolder) {
2543
2902
  const watchedFiles = /* @__PURE__ */ new Set();
2544
2903
  const watchLinkedFiles = (files) => {
@@ -2600,6 +2959,9 @@ function setupSlowRenderCacheInvalidation(vite, cache, pagesRootFolder, projectR
2600
2959
  }
2601
2960
  export {
2602
2961
  ACTION_ENDPOINT_BASE,
2962
+ DEV_SERVER_SERVICE,
2963
+ DevServerService,
2964
+ FreezeStore,
2603
2965
  actionBodyParser,
2604
2966
  createActionRouter,
2605
2967
  createViteForCli,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jay-framework/dev-server",
3
- "version": "0.15.6",
3
+ "version": "0.16.0",
4
4
  "type": "module",
5
5
  "license": "Apache-2.0",
6
6
  "main": "dist/index.js",
@@ -23,22 +23,22 @@
23
23
  "test:watch": "vitest"
24
24
  },
25
25
  "dependencies": {
26
- "@jay-framework/compiler-jay-stack": "^0.15.6",
27
- "@jay-framework/compiler-shared": "^0.15.6",
28
- "@jay-framework/component": "^0.15.6",
29
- "@jay-framework/fullstack-component": "^0.15.6",
30
- "@jay-framework/logger": "^0.15.6",
31
- "@jay-framework/runtime": "^0.15.6",
32
- "@jay-framework/stack-client-runtime": "^0.15.6",
33
- "@jay-framework/stack-route-scanner": "^0.15.6",
34
- "@jay-framework/stack-server-runtime": "^0.15.6",
35
- "@jay-framework/view-state-merge": "^0.15.6",
26
+ "@jay-framework/compiler-jay-stack": "^0.16.0",
27
+ "@jay-framework/compiler-shared": "^0.16.0",
28
+ "@jay-framework/component": "^0.16.0",
29
+ "@jay-framework/fullstack-component": "^0.16.0",
30
+ "@jay-framework/logger": "^0.16.0",
31
+ "@jay-framework/runtime": "^0.16.0",
32
+ "@jay-framework/stack-client-runtime": "^0.16.0",
33
+ "@jay-framework/stack-route-scanner": "^0.16.0",
34
+ "@jay-framework/stack-server-runtime": "^0.16.0",
35
+ "@jay-framework/view-state-merge": "^0.16.0",
36
36
  "vite": "^5.0.11"
37
37
  },
38
38
  "devDependencies": {
39
- "@jay-framework/dev-environment": "^0.15.6",
40
- "@jay-framework/jay-cli": "^0.15.6",
41
- "@jay-framework/stack-client-runtime": "^0.15.6",
39
+ "@jay-framework/dev-environment": "^0.16.0",
40
+ "@jay-framework/jay-cli": "^0.16.0",
41
+ "@jay-framework/stack-client-runtime": "^0.16.0",
42
42
  "@playwright/test": "^1.58.2",
43
43
  "@types/express": "^5.0.2",
44
44
  "@types/node": "^22.15.21",