@jay-framework/dev-server 0.13.0 → 0.15.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 +7 -3
  2. package/dist/index.js +226 -352
  3. package/package.json +18 -14
package/dist/index.d.ts CHANGED
@@ -13,12 +13,16 @@ interface DevServerOptions {
13
13
  pagesRootFolder?: string;
14
14
  /**
15
15
  * Folder where build artifacts are stored.
16
- * Pre-rendered jay-html files are written to `<buildFolder>/slow-render-cache/`.
16
+ * Pre-rendered jay-html files are written to `<buildFolder>/pre-rendered/`.
17
17
  * Defaults to `<projectRootFolder>/build`.
18
18
  */
19
19
  buildFolder?: string;
20
20
  jayRollupConfig: JayRollupConfig;
21
- dontCacheSlowly: boolean;
21
+ /**
22
+ * Disable server-side rendering. When true, pages are served as client-only
23
+ * (element target, no hydration). Useful for development without SSR overhead.
24
+ */
25
+ disableSSR?: boolean;
22
26
  /**
23
27
  * Disable automation integration.
24
28
  * When false (default), pages are wrapped with automation API for dev tooling.
@@ -191,7 +195,7 @@ declare function createViteServer(options: CreateViteServerOptions): Promise<Vit
191
195
  *
192
196
  * This is a convenience wrapper around createViteServer with CLI-appropriate defaults.
193
197
  * Disables dependency optimization and ignores the build/ folder to avoid errors
194
- * from stale build artifacts (e.g., build/client-scripts/ referencing build/slow-render-cache/).
198
+ * from stale build artifacts (e.g., build/debug/client-entry/ referencing build/pre-rendered/).
195
199
  */
196
200
  declare function createViteForCli(options: {
197
201
  projectRoot: string;
package/dist/index.js CHANGED
@@ -10,12 +10,13 @@ import { createRequire } from "module";
10
10
  import "@jay-framework/compiler-shared";
11
11
  import * as path from "node:path";
12
12
  import path__default from "node:path";
13
- import { discoverHeadlessInstances, parseContract, slowRenderTransform, JAY_IMPORT_RESOLVER, resolveHeadlessInstances } from "@jay-framework/compiler-jay-html";
13
+ import { JAY_IMPORT_RESOLVER, injectHeadfullFSTemplates, parseContract, slowRenderTransform, discoverHeadlessInstances, resolveHeadlessInstances } from "@jay-framework/compiler-jay-html";
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 crypto from "node:crypto";
17
18
  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, getServiceRegistry, materializeContracts, loadPageParts, renderFastChangingData, validateForEachInstances, slowRenderInstances, generateClientScript, getClientInitData, resolveServices } from "@jay-framework/stack-server-runtime";
19
+ import { discoverPluginsWithInit, sortPluginsByDependencies, executePluginServerInits, runInitCallbacks, actionRegistry, discoverAndRegisterActions, discoverAllPluginActions, runShutdownCallbacks, clearLifecycleCallbacks, clearServiceRegistry, clearClientInitData, DevSlowlyChangingPhase, SlowRenderCache, preparePluginClientInits, invalidateServerElementCache, getServiceRegistry, materializeContracts, loadPageParts, renderFastChangingData, generateClientScript, getClientInitData, generateSSRPageHtml, validateForEachInstances, slowRenderInstances } from "@jay-framework/stack-server-runtime";
19
20
  import fs$1 from "node:fs/promises";
20
21
  import { pathToFileURL } from "node:url";
21
22
  const s$1 = createRequire(import.meta.url), e$1 = s$1("typescript"), c$1 = new Proxy(e$1, {
@@ -1364,9 +1365,11 @@ async function createViteServer(options) {
1364
1365
  logLevel = "info",
1365
1366
  clearScreen = true
1366
1367
  } = options;
1368
+ const instanceId = crypto.randomBytes(4).toString("hex");
1369
+ const cacheDir = path__default.join(projectRoot, "node_modules", `.vite-${instanceId}`);
1367
1370
  const vite = await createServer({
1368
1371
  // Don't start HTTP server - we use middleware mode
1369
- server: { middlewareMode: true },
1372
+ server: { middlewareMode: true, watch: { ignored: ["**/build/**"] } },
1370
1373
  // Use Jay Stack compiler for .jay-html and other custom transforms
1371
1374
  plugins: [...jayStackCompiler(jayRollupConfig)],
1372
1375
  // Custom app type (no default middleware)
@@ -1375,6 +1378,8 @@ async function createViteServer(options) {
1375
1378
  base,
1376
1379
  // Root directory for module resolution
1377
1380
  root: pagesRoot,
1381
+ // Isolate dep optimization cache per instance
1382
+ cacheDir,
1378
1383
  // SSR configuration
1379
1384
  ssr: {
1380
1385
  // Mark jay-framework packages as external so Vite uses Node's require
@@ -1390,6 +1395,13 @@ async function createViteServer(options) {
1390
1395
  logLevel,
1391
1396
  clearScreen
1392
1397
  });
1398
+ const originalClose = vite.close.bind(vite);
1399
+ vite.close = async () => {
1400
+ await originalClose();
1401
+ const { rm } = await import("node:fs/promises");
1402
+ await rm(cacheDir, { recursive: true, force: true }).catch(() => {
1403
+ });
1404
+ };
1393
1405
  return vite;
1394
1406
  }
1395
1407
  async function createViteForCli(options) {
@@ -1841,7 +1853,7 @@ function defaults(options) {
1841
1853
  pagesRootFolder,
1842
1854
  projectRootFolder,
1843
1855
  buildFolder,
1844
- dontCacheSlowly: options.dontCacheSlowly,
1856
+ disableSSR: options.disableSSR,
1845
1857
  jayRollupConfig: {
1846
1858
  ...options.jayRollupConfig || {},
1847
1859
  tsConfigFilePath
@@ -1896,41 +1908,12 @@ function mkRoute(route, vite, slowlyPhase, options, slowRenderCache, projectInit
1896
1908
  language: "en",
1897
1909
  url
1898
1910
  };
1899
- const useSlowRenderCache = !options.dontCacheSlowly;
1900
- let cachedEntry = useSlowRenderCache ? slowRenderCache.get(route.jayHtmlPath, pageParams) : void 0;
1901
- if (cachedEntry) {
1902
- try {
1903
- await fs$1.access(cachedEntry.preRenderedPath);
1904
- } catch {
1905
- getLogger().info(
1906
- `[SlowRender] Cached file missing, rebuilding: ${cachedEntry.preRenderedPath}`
1907
- );
1908
- await slowRenderCache.invalidate(route.jayHtmlPath);
1909
- cachedEntry = void 0;
1910
- }
1911
- }
1912
- if (cachedEntry) {
1913
- await handleCachedRequest(
1914
- vite,
1915
- route,
1916
- options,
1917
- cachedEntry,
1918
- pageParams,
1919
- pageProps,
1920
- allPluginClientInits,
1921
- allPluginsWithInit,
1922
- projectInit,
1923
- res,
1924
- url,
1925
- timing
1926
- );
1927
- } else if (useSlowRenderCache) {
1928
- await handlePreRenderRequest(
1911
+ if (options.disableSSR) {
1912
+ await handleClientOnlyRequest(
1929
1913
  vite,
1930
1914
  route,
1931
1915
  options,
1932
1916
  slowlyPhase,
1933
- slowRenderCache,
1934
1917
  pageParams,
1935
1918
  pageProps,
1936
1919
  allPluginClientInits,
@@ -1941,20 +1924,39 @@ function mkRoute(route, vite, slowlyPhase, options, slowRenderCache, projectInit
1941
1924
  timing
1942
1925
  );
1943
1926
  } else {
1944
- await handleDirectRequest(
1945
- vite,
1946
- route,
1947
- options,
1948
- slowlyPhase,
1949
- pageParams,
1950
- pageProps,
1951
- allPluginClientInits,
1952
- allPluginsWithInit,
1953
- projectInit,
1954
- res,
1955
- url,
1956
- timing
1957
- );
1927
+ const cachedEntry = await slowRenderCache.get(route.jayHtmlPath, pageParams);
1928
+ if (cachedEntry) {
1929
+ await handleCachedRequest(
1930
+ vite,
1931
+ route,
1932
+ options,
1933
+ cachedEntry,
1934
+ pageParams,
1935
+ pageProps,
1936
+ allPluginClientInits,
1937
+ allPluginsWithInit,
1938
+ projectInit,
1939
+ res,
1940
+ url,
1941
+ timing
1942
+ );
1943
+ } else {
1944
+ await handlePreRenderRequest(
1945
+ vite,
1946
+ route,
1947
+ options,
1948
+ slowlyPhase,
1949
+ slowRenderCache,
1950
+ pageParams,
1951
+ pageProps,
1952
+ allPluginClientInits,
1953
+ allPluginsWithInit,
1954
+ projectInit,
1955
+ res,
1956
+ url,
1957
+ timing
1958
+ );
1959
+ }
1958
1960
  }
1959
1961
  } catch (e2) {
1960
1962
  vite?.ssrFixStacktrace(e2);
@@ -1973,7 +1975,10 @@ async function handleCachedRequest(vite, route, options, cachedEntry, pageParams
1973
1975
  options.pagesRootFolder,
1974
1976
  options.projectRootFolder,
1975
1977
  options.jayRollupConfig,
1976
- { preRenderedPath: cachedEntry.preRenderedPath }
1978
+ {
1979
+ preRenderedPath: cachedEntry.preRenderedPath,
1980
+ preRenderedContent: cachedEntry.preRenderedContent
1981
+ }
1977
1982
  );
1978
1983
  timing?.recordViteSsr(Date.now() - loadStart);
1979
1984
  if (!pagePartsResult.val) {
@@ -1988,12 +1993,19 @@ async function handleCachedRequest(vite, route, options, cachedEntry, pageParams
1988
1993
  allPluginsWithInit,
1989
1994
  usedPackages
1990
1995
  );
1996
+ const instancePhaseData = cachedEntry.carryForward?.__instances;
1997
+ const forEachInstances = instancePhaseData?.forEachInstances;
1998
+ const headlessComps = pagePartsResult.val.headlessInstanceComponents;
1991
1999
  const fastStart = Date.now();
1992
2000
  const renderedFast = await renderFastChangingData(
1993
2001
  pageParams,
1994
2002
  pageProps,
1995
2003
  cachedEntry.carryForward,
1996
- pageParts
2004
+ pageParts,
2005
+ instancePhaseData,
2006
+ forEachInstances,
2007
+ headlessComps,
2008
+ cachedEntry.slowViewState
1997
2009
  );
1998
2010
  timing?.recordFastRender(Date.now() - fastStart);
1999
2011
  if (renderedFast.kind !== "PhaseOutput") {
@@ -2001,45 +2013,14 @@ async function handleCachedRequest(vite, route, options, cachedEntry, pageParams
2001
2013
  timing?.end();
2002
2014
  return;
2003
2015
  }
2004
- let fastViewState = renderedFast.rendered;
2005
- let fastCarryForward = renderedFast.carryForward;
2006
- const instancePhaseData = cachedEntry.carryForward?.__instances;
2007
- const headlessComps = pagePartsResult.val.headlessInstanceComponents;
2008
- if (instancePhaseData && headlessComps.length > 0) {
2009
- const instanceFastResult = await renderFastChangingDataForInstances(
2010
- instancePhaseData,
2011
- headlessComps
2012
- );
2013
- if (instanceFastResult) {
2014
- fastViewState = {
2015
- ...fastViewState,
2016
- __headlessInstances: instanceFastResult.viewStates
2017
- };
2018
- fastCarryForward = {
2019
- ...fastCarryForward,
2020
- __headlessInstances: instanceFastResult.carryForwards
2021
- };
2022
- }
2023
- if (instancePhaseData.forEachInstances && instancePhaseData.forEachInstances.length > 0) {
2024
- const forEachResult = await renderFastChangingDataForForEachInstances(
2025
- instancePhaseData.forEachInstances,
2026
- headlessComps,
2027
- fastViewState
2028
- );
2029
- if (forEachResult) {
2030
- const existingHeadless = fastViewState.__headlessInstances || {};
2031
- fastViewState = {
2032
- ...fastViewState,
2033
- __headlessInstances: { ...existingHeadless, ...forEachResult }
2034
- };
2035
- }
2036
- }
2037
- }
2016
+ const fastViewState = renderedFast.rendered;
2017
+ const fastCarryForward = renderedFast.carryForward;
2038
2018
  await sendResponse(
2039
2019
  vite,
2040
2020
  res,
2041
2021
  url,
2042
2022
  cachedEntry.preRenderedPath,
2023
+ route.jayHtmlPath,
2043
2024
  pageParts,
2044
2025
  fastViewState,
2045
2026
  fastCarryForward,
@@ -2048,7 +2029,8 @@ async function handleCachedRequest(vite, route, options, cachedEntry, pageParams
2048
2029
  pluginsForPage,
2049
2030
  options,
2050
2031
  cachedEntry.slowViewState,
2051
- timing
2032
+ timing,
2033
+ cachedEntry.preRenderedContent
2052
2034
  );
2053
2035
  }
2054
2036
  async function handlePreRenderRequest(vite, route, options, slowlyPhase, slowRenderCache, pageParams, pageProps, allPluginClientInits, allPluginsWithInit, projectInit, res, url, timing) {
@@ -2071,7 +2053,10 @@ async function handlePreRenderRequest(vite, route, options, slowlyPhase, slowRen
2071
2053
  const renderedSlowly = await slowlyPhase.runSlowlyForPage(
2072
2054
  pageParams,
2073
2055
  pageProps,
2074
- initialPartsResult.val.parts
2056
+ initialPartsResult.val.parts,
2057
+ void 0,
2058
+ void 0,
2059
+ route.jayHtmlPath
2075
2060
  );
2076
2061
  if (renderedSlowly.kind !== "PhaseOutput") {
2077
2062
  timing?.recordSlowRender(Date.now() - slowStart);
@@ -2107,98 +2092,30 @@ async function handlePreRenderRequest(vite, route, options, slowlyPhase, slowRen
2107
2092
  };
2108
2093
  }
2109
2094
  const carryForward = instancePhaseDataForCache ? { ...renderedSlowly.carryForward, __instances: instancePhaseDataForCache } : renderedSlowly.carryForward;
2110
- const preRenderedPath = await slowRenderCache.set(
2095
+ const cachedEntry = await slowRenderCache.set(
2111
2096
  route.jayHtmlPath,
2112
2097
  pageParams,
2113
2098
  preRenderResult.preRenderedJayHtml,
2114
2099
  renderedSlowly.rendered,
2115
2100
  carryForward
2116
2101
  );
2117
- getLogger().info(`[SlowRender] Cached pre-rendered jay-html at ${preRenderedPath}`);
2118
- const pagePartsResult = await loadPageParts(
2102
+ getLogger().info(`[SlowRender] Cached pre-rendered jay-html at ${cachedEntry.preRenderedPath}`);
2103
+ await handleCachedRequest(
2119
2104
  vite,
2120
2105
  route,
2121
- options.pagesRootFolder,
2122
- options.projectRootFolder,
2123
- options.jayRollupConfig,
2124
- { preRenderedPath }
2125
- );
2126
- if (!pagePartsResult.val) {
2127
- getLogger().info(pagePartsResult.validations.join("\n"));
2128
- res.status(500).end(pagePartsResult.validations.join("\n"));
2129
- timing?.end();
2130
- return;
2131
- }
2132
- const { parts: pageParts, clientTrackByMap, usedPackages } = pagePartsResult.val;
2133
- const pluginsForPage = filterPluginsForPage(
2134
- allPluginClientInits,
2135
- allPluginsWithInit,
2136
- usedPackages
2137
- );
2138
- const fastStart = Date.now();
2139
- const renderedFast = await renderFastChangingData(
2106
+ options,
2107
+ cachedEntry,
2140
2108
  pageParams,
2141
2109
  pageProps,
2142
- carryForward,
2143
- pageParts
2144
- );
2145
- timing?.recordFastRender(Date.now() - fastStart);
2146
- if (renderedFast.kind !== "PhaseOutput") {
2147
- handleOtherResponseCodes(res, renderedFast);
2148
- timing?.end();
2149
- return;
2150
- }
2151
- let fastViewState = renderedFast.rendered;
2152
- let fastCarryForward = renderedFast.carryForward;
2153
- const instancePhaseData = carryForward?.__instances;
2154
- const headlessComps = pagePartsResult.val.headlessInstanceComponents;
2155
- if (instancePhaseData && headlessComps.length > 0) {
2156
- const instanceFastResult = await renderFastChangingDataForInstances(
2157
- instancePhaseData,
2158
- headlessComps
2159
- );
2160
- if (instanceFastResult) {
2161
- fastViewState = {
2162
- ...fastViewState,
2163
- __headlessInstances: instanceFastResult.viewStates
2164
- };
2165
- fastCarryForward = {
2166
- ...fastCarryForward,
2167
- __headlessInstances: instanceFastResult.carryForwards
2168
- };
2169
- }
2170
- if (instancePhaseData.forEachInstances && instancePhaseData.forEachInstances.length > 0) {
2171
- const forEachResult = await renderFastChangingDataForForEachInstances(
2172
- instancePhaseData.forEachInstances,
2173
- headlessComps,
2174
- fastViewState
2175
- );
2176
- if (forEachResult) {
2177
- const existingHeadless = fastViewState.__headlessInstances || {};
2178
- fastViewState = {
2179
- ...fastViewState,
2180
- __headlessInstances: { ...existingHeadless, ...forEachResult }
2181
- };
2182
- }
2183
- }
2184
- }
2185
- await sendResponse(
2186
- vite,
2110
+ allPluginClientInits,
2111
+ allPluginsWithInit,
2112
+ projectInit,
2187
2113
  res,
2188
2114
  url,
2189
- preRenderedPath,
2190
- pageParts,
2191
- fastViewState,
2192
- fastCarryForward,
2193
- clientTrackByMap,
2194
- projectInit,
2195
- pluginsForPage,
2196
- options,
2197
- renderedSlowly.rendered,
2198
2115
  timing
2199
2116
  );
2200
2117
  }
2201
- async function handleDirectRequest(vite, route, options, slowlyPhase, pageParams, pageProps, allPluginClientInits, allPluginsWithInit, projectInit, res, url, timing) {
2118
+ async function handleClientOnlyRequest(vite, route, options, slowlyPhase, pageParams, pageProps, allPluginClientInits, allPluginsWithInit, projectInit, res, url, timing) {
2202
2119
  const loadStart = Date.now();
2203
2120
  const pagePartsResult = await loadPageParts(
2204
2121
  vite,
@@ -2218,7 +2135,10 @@ async function handleDirectRequest(vite, route, options, slowlyPhase, pageParams
2218
2135
  parts: pageParts,
2219
2136
  serverTrackByMap,
2220
2137
  clientTrackByMap,
2221
- usedPackages
2138
+ usedPackages,
2139
+ headlessInstanceComponents,
2140
+ discoveredInstances,
2141
+ forEachInstances
2222
2142
  } = pagePartsResult.val;
2223
2143
  const pluginsForPage = filterPluginsForPage(
2224
2144
  allPluginClientInits,
@@ -2226,7 +2146,14 @@ async function handleDirectRequest(vite, route, options, slowlyPhase, pageParams
2226
2146
  usedPackages
2227
2147
  );
2228
2148
  const slowStart = Date.now();
2229
- const renderedSlowly = await slowlyPhase.runSlowlyForPage(pageParams, pageProps, pageParts);
2149
+ const renderedSlowly = await slowlyPhase.runSlowlyForPage(
2150
+ pageParams,
2151
+ pageProps,
2152
+ pageParts,
2153
+ discoveredInstances,
2154
+ headlessInstanceComponents,
2155
+ route.jayHtmlPath
2156
+ );
2230
2157
  if (renderedSlowly.kind !== "PhaseOutput") {
2231
2158
  timing?.recordSlowRender(Date.now() - slowStart);
2232
2159
  if (renderedSlowly.kind === "ClientError") {
@@ -2235,131 +2162,107 @@ async function handleDirectRequest(vite, route, options, slowlyPhase, pageParams
2235
2162
  timing?.end();
2236
2163
  return;
2237
2164
  }
2238
- let instanceViewStates;
2239
- let instancePhaseDataForFast;
2240
- let forEachInstancesForFast;
2241
- const headlessInstanceComponents = pagePartsResult.val.headlessInstanceComponents ?? [];
2242
- if (headlessInstanceComponents.length > 0) {
2243
- const jayHtmlContent = await fs$1.readFile(route.jayHtmlPath, "utf-8");
2244
- const discoveryResult = discoverHeadlessInstances(jayHtmlContent);
2245
- if (discoveryResult.forEachInstances.length > 0) {
2246
- const validationErrors = validateForEachInstances(
2247
- discoveryResult.forEachInstances,
2248
- headlessInstanceComponents
2249
- );
2250
- if (validationErrors.length > 0) {
2251
- getLogger().error(
2252
- `[SlowRender] ForEach instance validation failed: ${validationErrors.join(", ")}`
2253
- );
2254
- res.status(500).end(validationErrors.join("\n"));
2255
- timing?.end();
2256
- return;
2257
- }
2258
- forEachInstancesForFast = discoveryResult.forEachInstances;
2259
- }
2260
- if (discoveryResult.instances.length > 0) {
2261
- const slowResult = await slowRenderInstances(
2262
- discoveryResult.instances,
2263
- headlessInstanceComponents
2264
- );
2265
- if (slowResult) {
2266
- instanceViewStates = { ...slowResult.slowViewStates };
2267
- instancePhaseDataForFast = slowResult.instancePhaseData;
2268
- }
2269
- }
2270
- }
2271
2165
  timing?.recordSlowRender(Date.now() - slowStart);
2166
+ const instancePhaseData = renderedSlowly.carryForward?.__instances;
2272
2167
  const fastStart = Date.now();
2273
2168
  const renderedFast = await renderFastChangingData(
2274
2169
  pageParams,
2275
2170
  pageProps,
2276
2171
  renderedSlowly.carryForward,
2277
- pageParts
2172
+ pageParts,
2173
+ instancePhaseData,
2174
+ forEachInstances,
2175
+ headlessInstanceComponents,
2176
+ renderedSlowly.rendered
2278
2177
  );
2279
- if (instancePhaseDataForFast && instanceViewStates) {
2280
- const instanceFastResult = await renderFastChangingDataForInstances(
2281
- instancePhaseDataForFast,
2282
- headlessInstanceComponents
2283
- );
2284
- if (instanceFastResult) {
2285
- for (const [coordKey, fastVS] of Object.entries(instanceFastResult.viewStates)) {
2286
- instanceViewStates[coordKey] = {
2287
- ...instanceViewStates[coordKey] || {},
2288
- ...fastVS
2289
- };
2290
- }
2291
- }
2292
- }
2293
- if (forEachInstancesForFast && renderedFast.kind === "PhaseOutput") {
2294
- const forEachResult = await renderFastChangingDataForForEachInstances(
2295
- forEachInstancesForFast,
2296
- headlessInstanceComponents,
2297
- { ...renderedSlowly.rendered, ...renderedFast.rendered }
2298
- );
2299
- if (forEachResult) {
2300
- if (!instanceViewStates)
2301
- instanceViewStates = {};
2302
- for (const [coordKey, fastVS] of Object.entries(forEachResult)) {
2303
- instanceViewStates[coordKey] = fastVS;
2304
- }
2305
- }
2306
- }
2307
2178
  timing?.recordFastRender(Date.now() - fastStart);
2308
2179
  if (renderedFast.kind !== "PhaseOutput") {
2309
2180
  handleOtherResponseCodes(res, renderedFast);
2310
2181
  timing?.end();
2311
2182
  return;
2312
2183
  }
2313
- let viewState;
2314
- if (serverTrackByMap && Object.keys(serverTrackByMap).length > 0) {
2315
- viewState = deepMergeViewStates(
2316
- renderedSlowly.rendered,
2317
- renderedFast.rendered,
2318
- serverTrackByMap
2319
- );
2320
- } else {
2321
- viewState = { ...renderedSlowly.rendered, ...renderedFast.rendered };
2322
- }
2323
- if (instanceViewStates && Object.keys(instanceViewStates).length > 0) {
2324
- viewState = {
2325
- ...viewState,
2326
- __headlessInstances: instanceViewStates
2327
- };
2328
- }
2329
- await sendResponse(
2330
- vite,
2331
- res,
2332
- url,
2333
- route.jayHtmlPath,
2334
- pageParts,
2335
- viewState,
2336
- renderedFast.carryForward,
2337
- clientTrackByMap,
2338
- projectInit,
2339
- pluginsForPage,
2340
- options,
2341
- void 0,
2342
- timing
2184
+ const viewState = deepMergeViewStates(
2185
+ renderedSlowly.rendered,
2186
+ renderedFast.rendered,
2187
+ serverTrackByMap || {}
2343
2188
  );
2344
- }
2345
- async function sendResponse(vite, res, url, jayHtmlPath, pageParts, viewState, carryForward, clientTrackByMap, projectInit, pluginsForPage, options, slowViewState, timing) {
2189
+ const fastCF = renderedFast.carryForward;
2346
2190
  const pageHtml = generateClientScript(
2347
2191
  viewState,
2348
- carryForward,
2192
+ fastCF,
2349
2193
  pageParts,
2350
- jayHtmlPath,
2194
+ route.jayHtmlPath,
2351
2195
  clientTrackByMap,
2352
2196
  getClientInitData(),
2353
2197
  projectInit,
2354
2198
  pluginsForPage,
2355
2199
  {
2356
- enableAutomation: !options.disableAutomation,
2357
- slowViewState
2200
+ enableAutomation: !options.disableAutomation
2358
2201
  }
2359
2202
  );
2360
2203
  if (options.buildFolder) {
2361
2204
  const pageName = !url || url === "/" ? "index" : url.replace(/^\//, "").replace(/\//g, "-");
2362
- const clientScriptDir = path__default.join(options.buildFolder, "client-scripts");
2205
+ const clientScriptDir = path__default.join(options.buildFolder, "debug", "client-entry");
2206
+ await fs$1.mkdir(clientScriptDir, { recursive: true });
2207
+ await fs$1.writeFile(path__default.join(clientScriptDir, `${pageName}.html`), pageHtml, "utf-8");
2208
+ }
2209
+ const viteStart = Date.now();
2210
+ const compiledPageHtml = await vite.transformIndexHtml(!!url ? url : "/", pageHtml);
2211
+ timing?.recordViteClient(Date.now() - viteStart);
2212
+ res.status(200).set({ "Content-Type": "text/html" }).send(compiledPageHtml);
2213
+ timing?.end();
2214
+ }
2215
+ async function sendResponse(vite, res, url, jayHtmlPath, sourceJayHtmlPath, pageParts, viewState, carryForward, clientTrackByMap, projectInit, pluginsForPage, options, slowViewState, timing, preLoadedContent) {
2216
+ let pageHtml;
2217
+ const routeDir = path__default.dirname(path__default.relative(options.pagesRootFolder, sourceJayHtmlPath));
2218
+ try {
2219
+ let jayHtmlContent = preLoadedContent ?? await fs$1.readFile(jayHtmlPath, "utf-8");
2220
+ const jayHtmlFilename = path__default.basename(jayHtmlPath);
2221
+ const jayHtmlDir = path__default.dirname(jayHtmlPath);
2222
+ const sourceDir = path__default.dirname(sourceJayHtmlPath);
2223
+ jayHtmlContent = injectHeadfullFSTemplates(jayHtmlContent, sourceDir, JAY_IMPORT_RESOLVER);
2224
+ pageHtml = await generateSSRPageHtml(
2225
+ vite,
2226
+ jayHtmlContent,
2227
+ jayHtmlFilename,
2228
+ jayHtmlDir,
2229
+ viewState,
2230
+ jayHtmlPath,
2231
+ pageParts,
2232
+ carryForward,
2233
+ clientTrackByMap,
2234
+ getClientInitData(),
2235
+ options.buildFolder,
2236
+ options.projectRootFolder,
2237
+ routeDir,
2238
+ options.jayRollupConfig?.tsConfigFilePath,
2239
+ projectInit,
2240
+ pluginsForPage,
2241
+ {
2242
+ enableAutomation: !options.disableAutomation,
2243
+ slowViewState
2244
+ }
2245
+ );
2246
+ } catch (err) {
2247
+ getLogger().warn(`[SSR] Failed, falling back to client rendering: ${err.message}`);
2248
+ pageHtml = generateClientScript(
2249
+ viewState,
2250
+ carryForward,
2251
+ pageParts,
2252
+ jayHtmlPath,
2253
+ clientTrackByMap,
2254
+ getClientInitData(),
2255
+ projectInit,
2256
+ pluginsForPage,
2257
+ {
2258
+ enableAutomation: !options.disableAutomation,
2259
+ slowViewState
2260
+ }
2261
+ );
2262
+ }
2263
+ if (options.buildFolder) {
2264
+ const pageName = !url || url === "/" ? "index" : url.replace(/^\//, "").replace(/\//g, "-");
2265
+ const clientScriptDir = path__default.join(options.buildFolder, "debug", "client-entry");
2363
2266
  await fs$1.mkdir(clientScriptDir, { recursive: true });
2364
2267
  await fs$1.writeFile(path__default.join(clientScriptDir, `${pageName}.html`), pageHtml, "utf-8");
2365
2268
  }
@@ -2390,12 +2293,23 @@ async function preRenderJayHtml(route, slowViewState, headlessContracts, headles
2390
2293
  return void 0;
2391
2294
  }
2392
2295
  }
2393
- const result = slowRenderTransform({
2296
+ if (!contract && slowViewState && Object.keys(slowViewState).length > 0) {
2297
+ getLogger().warn(
2298
+ `[SlowRender] Page ${route.jayHtmlPath} has slow ViewState but no contract. Without a contract, slow bindings cannot be resolved. Move data to withFastRender or add a .jay-contract file with phase annotations.`
2299
+ );
2300
+ }
2301
+ const sourceDir = path__default.dirname(route.jayHtmlPath);
2302
+ const jayHtmlWithTemplates = injectHeadfullFSTemplates(
2394
2303
  jayHtmlContent,
2304
+ sourceDir,
2305
+ JAY_IMPORT_RESOLVER
2306
+ );
2307
+ const result = slowRenderTransform({
2308
+ jayHtmlContent: jayHtmlWithTemplates,
2395
2309
  slowViewState,
2396
2310
  contract,
2397
2311
  headlessContracts,
2398
- sourceDir: path__default.dirname(route.jayHtmlPath),
2312
+ sourceDir,
2399
2313
  importResolver: JAY_IMPORT_RESOLVER
2400
2314
  });
2401
2315
  if (!result.val) {
@@ -2446,89 +2360,42 @@ async function preRenderJayHtml(route, slowViewState, headlessContracts, headles
2446
2360
  );
2447
2361
  }
2448
2362
  }
2449
- }
2450
- }
2451
- return { preRenderedJayHtml, instancePhaseData, forEachInstances };
2452
- }
2453
- async function renderFastChangingDataForInstances(instancePhaseData, headlessInstanceComponents) {
2454
- const componentByContractName = /* @__PURE__ */ new Map();
2455
- for (const comp of headlessInstanceComponents) {
2456
- componentByContractName.set(comp.contractName, comp);
2457
- }
2458
- const viewStates = {};
2459
- const carryForwards = {};
2460
- let hasResults = false;
2461
- for (const instance of instancePhaseData.discovered) {
2462
- const coordKey = instance.coordinate.join("/");
2463
- const comp = componentByContractName.get(instance.contractName);
2464
- if (!comp || !comp.compDefinition.fastRender) {
2465
- continue;
2466
- }
2467
- const instanceCarryForward = instancePhaseData.carryForwards[coordKey] || {};
2468
- const services = resolveServices(comp.compDefinition.services);
2469
- const fastResult = await comp.compDefinition.fastRender(
2470
- instance.props,
2471
- instanceCarryForward,
2472
- ...services
2473
- );
2474
- if (fastResult.kind === "PhaseOutput") {
2475
- viewStates[coordKey] = fastResult.rendered;
2476
- carryForwards[coordKey] = fastResult.carryForward;
2477
- hasResults = true;
2478
- }
2479
- }
2480
- return hasResults ? { viewStates, carryForwards } : void 0;
2481
- }
2482
- async function renderFastChangingDataForForEachInstances(forEachInstances, headlessInstanceComponents, mergedViewState) {
2483
- const componentByContractName = /* @__PURE__ */ new Map();
2484
- for (const comp of headlessInstanceComponents) {
2485
- componentByContractName.set(comp.contractName, comp);
2486
- }
2487
- const viewStates = {};
2488
- let hasResults = false;
2489
- for (const instance of forEachInstances) {
2490
- const comp = componentByContractName.get(instance.contractName);
2491
- if (!comp)
2492
- continue;
2493
- const items = resolvePathValue(mergedViewState, instance.forEachPath);
2494
- if (!Array.isArray(items))
2495
- continue;
2496
- for (const item of items) {
2497
- const trackByValue = String(item[instance.trackBy]);
2498
- const props = {};
2499
- for (const [propName, binding] of Object.entries(instance.propBindings)) {
2500
- props[propName] = resolveBinding(String(binding), item);
2501
- }
2502
- if (comp.compDefinition.fastRender) {
2503
- const services = resolveServices(comp.compDefinition.services);
2504
- const fastResult = await comp.compDefinition.fastRender(props, ...services);
2505
- if (fastResult.kind === "PhaseOutput") {
2506
- const coord = [trackByValue, instance.coordinateSuffix].toString();
2507
- viewStates[coord] = fastResult.rendered;
2508
- hasResults = true;
2363
+ if (!instancePhaseData) {
2364
+ const componentByContractName = /* @__PURE__ */ new Map();
2365
+ for (const comp of headlessInstanceComponents) {
2366
+ componentByContractName.set(comp.contractName, comp);
2509
2367
  }
2368
+ instancePhaseData = {
2369
+ discovered: discoveryResult.instances.filter((i) => componentByContractName.has(i.contractName)).map((i) => {
2370
+ const comp = componentByContractName.get(i.contractName);
2371
+ const contractProps = comp.contract?.props ?? [];
2372
+ const normalizedProps = {};
2373
+ for (const [key, value] of Object.entries(i.props)) {
2374
+ const match = contractProps.find(
2375
+ (p) => p.name.toLowerCase() === key.toLowerCase()
2376
+ );
2377
+ normalizedProps[match ? match.name : key] = String(value);
2378
+ }
2379
+ return {
2380
+ contractName: i.contractName,
2381
+ props: normalizedProps,
2382
+ coordinate: i.coordinate
2383
+ };
2384
+ }),
2385
+ carryForwards: {}
2386
+ };
2510
2387
  }
2511
2388
  }
2512
2389
  }
2513
- return hasResults ? viewStates : void 0;
2514
- }
2515
- function resolvePathValue(obj, path2) {
2516
- return path2.split(".").reduce((current, segment) => current?.[segment], obj);
2517
- }
2518
- function resolveBinding(binding, item) {
2519
- const match = binding.match(/^\{(.+)\}$/);
2520
- if (match) {
2521
- return String(item[match[1]] ?? "");
2522
- }
2523
- return binding;
2390
+ return { preRenderedJayHtml, instancePhaseData, forEachInstances };
2524
2391
  }
2525
- async function materializeDynamicContracts(projectRootFolder, buildFolder, viteServer) {
2392
+ async function materializeDynamicContracts(projectRootFolder, viteServer) {
2526
2393
  try {
2527
2394
  const services = getServiceRegistry();
2528
2395
  const result = await materializeContracts(
2529
2396
  {
2530
2397
  projectRoot: projectRootFolder,
2531
- outputDir: path__default.join(buildFolder, "materialized-contracts"),
2398
+ outputDir: path__default.join(projectRootFolder, "agent-kit", "materialized-contracts"),
2532
2399
  verbose: false,
2533
2400
  viteServer
2534
2401
  },
@@ -2544,14 +2411,19 @@ async function materializeDynamicContracts(projectRootFolder, buildFolder, viteS
2544
2411
  }
2545
2412
  async function mkDevServer(rawOptions) {
2546
2413
  const options = defaults(rawOptions);
2547
- const {
2548
- publicBaseUrlPath,
2549
- pagesRootFolder,
2550
- projectRootFolder,
2551
- buildFolder,
2552
- jayRollupConfig,
2553
- dontCacheSlowly
2554
- } = options;
2414
+ const { publicBaseUrlPath, pagesRootFolder, projectRootFolder, buildFolder, jayRollupConfig } = options;
2415
+ if (buildFolder) {
2416
+ try {
2417
+ const entries = await fs$1.readdir(buildFolder);
2418
+ for (const entry of entries) {
2419
+ if (entry !== "pre-rendered") {
2420
+ await fs$1.rm(path__default.join(buildFolder, entry), { recursive: true, force: true }).catch(() => {
2421
+ });
2422
+ }
2423
+ }
2424
+ } catch {
2425
+ }
2426
+ }
2555
2427
  const viteLogLevel = options.logLevel === "silent" ? "silent" : options.logLevel === "verbose" ? "info" : "warn";
2556
2428
  const lifecycleManager = new ServiceLifecycleManager(projectRootFolder);
2557
2429
  setupGracefulShutdown(lifecycleManager);
@@ -2564,12 +2436,13 @@ async function mkDevServer(rawOptions) {
2564
2436
  });
2565
2437
  lifecycleManager.setViteServer(vite);
2566
2438
  await lifecycleManager.initialize();
2567
- await materializeDynamicContracts(projectRootFolder, buildFolder, vite);
2439
+ await materializeDynamicContracts(projectRootFolder, vite);
2568
2440
  setupServiceHotReload(vite, lifecycleManager);
2569
2441
  setupActionRouter(vite);
2570
- const routes = await initRoutes(pagesRootFolder);
2571
- const slowlyPhase = new DevSlowlyChangingPhase(dontCacheSlowly);
2572
- const slowRenderCacheDir = path__default.join(buildFolder, "slow-render-cache");
2442
+ const allRoutes = await initRoutes(pagesRootFolder);
2443
+ const routes = buildFolder ? allRoutes.filter((route) => !route.jayHtmlPath.startsWith(buildFolder)) : allRoutes;
2444
+ const slowlyPhase = new DevSlowlyChangingPhase();
2445
+ const slowRenderCacheDir = path__default.join(buildFolder, "pre-rendered");
2573
2446
  const slowRenderCache = new SlowRenderCache(slowRenderCacheDir, pagesRootFolder);
2574
2447
  setupSlowRenderCacheInvalidation(vite, slowRenderCache, pagesRootFolder);
2575
2448
  const projectInit = lifecycleManager.getProjectInit() ?? void 0;
@@ -2636,6 +2509,7 @@ function setupSlowRenderCacheInvalidation(vite, cache, pagesRootFolder) {
2636
2509
  return;
2637
2510
  }
2638
2511
  if (changedPath.endsWith(".jay-html")) {
2512
+ invalidateServerElementCache(changedPath);
2639
2513
  cache.invalidate(changedPath).then(() => {
2640
2514
  getLogger().info(`[SlowRender] Cache invalidated for ${changedPath}`);
2641
2515
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jay-framework/dev-server",
3
- "version": "0.13.0",
3
+ "version": "0.15.0",
4
4
  "type": "module",
5
5
  "license": "Apache-2.0",
6
6
  "main": "dist/index.js",
@@ -16,31 +16,35 @@
16
16
  "build:check-types": "tsc",
17
17
  "test-definitions": "jay-cli definitions test",
18
18
  "test-runtime": "jay-cli runtime test",
19
+ "serve-fixture": "npx tsx test/serve-fixture.ts",
19
20
  "clean": "rimraf dist",
20
21
  "confirm": "npm run clean && npm run build && npm run build:check-types && npm run test",
21
22
  "test": "vitest run",
22
23
  "test:watch": "vitest"
23
24
  },
24
25
  "dependencies": {
25
- "@jay-framework/compiler-jay-stack": "^0.13.0",
26
- "@jay-framework/compiler-shared": "^0.13.0",
27
- "@jay-framework/component": "^0.13.0",
28
- "@jay-framework/fullstack-component": "^0.13.0",
29
- "@jay-framework/logger": "^0.13.0",
30
- "@jay-framework/runtime": "^0.13.0",
31
- "@jay-framework/stack-client-runtime": "^0.13.0",
32
- "@jay-framework/stack-route-scanner": "^0.13.0",
33
- "@jay-framework/stack-server-runtime": "^0.13.0",
34
- "@jay-framework/view-state-merge": "^0.13.0",
26
+ "@jay-framework/compiler-jay-stack": "^0.15.0",
27
+ "@jay-framework/compiler-shared": "^0.15.0",
28
+ "@jay-framework/component": "^0.15.0",
29
+ "@jay-framework/fullstack-component": "^0.15.0",
30
+ "@jay-framework/logger": "^0.15.0",
31
+ "@jay-framework/runtime": "^0.15.0",
32
+ "@jay-framework/stack-client-runtime": "^0.15.0",
33
+ "@jay-framework/stack-route-scanner": "^0.15.0",
34
+ "@jay-framework/stack-server-runtime": "^0.15.0",
35
+ "@jay-framework/view-state-merge": "^0.15.0",
35
36
  "vite": "^5.0.11"
36
37
  },
37
38
  "devDependencies": {
38
- "@jay-framework/dev-environment": "^0.13.0",
39
- "@jay-framework/jay-cli": "^0.13.0",
40
- "@jay-framework/stack-client-runtime": "^0.13.0",
39
+ "@jay-framework/dev-environment": "^0.15.0",
40
+ "@jay-framework/jay-cli": "^0.15.0",
41
+ "@jay-framework/stack-client-runtime": "^0.15.0",
42
+ "@playwright/test": "^1.58.2",
41
43
  "@types/express": "^5.0.2",
42
44
  "@types/node": "^22.15.21",
45
+ "jsdom": "^23.2.0",
43
46
  "nodemon": "^3.0.3",
47
+ "playwright": "^1.58.2",
44
48
  "replace-in-file": "^7.1.0",
45
49
  "rimraf": "^5.0.5",
46
50
  "tsup": "^8.0.1",