@jay-framework/dev-server 0.10.0 → 0.12.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 -2
  2. package/dist/index.js +842 -123
  3. package/package.json +14 -13
package/dist/index.d.ts CHANGED
@@ -2,15 +2,34 @@ import { ViteDevServer, Connect } from 'vite';
2
2
  import { JayRoute } from '@jay-framework/stack-route-scanner';
3
3
  import { RequestHandler } from 'express-serve-static-core';
4
4
  import { JayRollupConfig } from '@jay-framework/rollup-plugin';
5
+ import { LogLevel } from '@jay-framework/logger';
5
6
  import { ProjectClientInitInfo, PluginWithInit, ActionRegistry } from '@jay-framework/stack-server-runtime';
6
7
  import { RequestHandler as RequestHandler$1 } from 'express';
8
+ import { JayRollupConfig as JayRollupConfig$1 } from '@jay-framework/compiler-jay-stack';
7
9
 
8
10
  interface DevServerOptions {
9
11
  publicBaseUrlPath?: string;
10
12
  projectRootFolder?: string;
11
13
  pagesRootFolder?: string;
14
+ /**
15
+ * Folder where build artifacts are stored.
16
+ * Pre-rendered jay-html files are written to `<buildFolder>/slow-render-cache/`.
17
+ * Defaults to `<projectRootFolder>/build`.
18
+ */
19
+ buildFolder?: string;
12
20
  jayRollupConfig: JayRollupConfig;
13
21
  dontCacheSlowly: boolean;
22
+ /**
23
+ * Disable automation integration.
24
+ * When false (default), pages are wrapped with automation API for dev tooling.
25
+ * The automation API is available at `window.__jay.automation` and via `AUTOMATION_CONTEXT`.
26
+ */
27
+ disableAutomation?: boolean;
28
+ /**
29
+ * Log level for dev server output.
30
+ * Controls both Jay logging and Vite logging.
31
+ */
32
+ logLevel?: LogLevel;
14
33
  }
15
34
 
16
35
  /**
@@ -88,7 +107,7 @@ interface DevServer {
88
107
  routes: DevServerRoute[];
89
108
  lifecycleManager: ServiceLifecycleManager;
90
109
  }
91
- declare function mkDevServer(options: DevServerOptions): Promise<DevServer>;
110
+ declare function mkDevServer(rawOptions: DevServerOptions): Promise<DevServer>;
92
111
 
93
112
  /**
94
113
  * Action Router for Jay Stack dev server.
@@ -139,4 +158,44 @@ declare function createActionRouter(options?: ActionRouterOptions): RequestHandl
139
158
  */
140
159
  declare function actionBodyParser(): RequestHandler$1;
141
160
 
142
- export { ACTION_ENDPOINT_BASE, type ActionRouterOptions, type DevServer, type DevServerOptions, type DevServerRoute, actionBodyParser, createActionRouter, mkDevServer };
161
+ /**
162
+ * Vite Factory
163
+ *
164
+ * Single source of truth for creating Vite servers.
165
+ * Used by both the dev-server and CLI commands.
166
+ */
167
+
168
+ interface CreateViteServerOptions {
169
+ /** Project root directory */
170
+ projectRoot: string;
171
+ /** Root directory for pages (defaults to projectRoot) */
172
+ pagesRoot?: string;
173
+ /** Base URL path for public assets */
174
+ base?: string;
175
+ /** Jay Stack compiler config (optional, will use defaults if not provided) */
176
+ jayRollupConfig?: JayRollupConfig$1;
177
+ /** Log level (defaults to 'info' for dev-server, 'warn' for CLI) */
178
+ logLevel?: 'info' | 'warn' | 'error' | 'silent';
179
+ /** Whether to clear screen on rebuild */
180
+ clearScreen?: boolean;
181
+ }
182
+ /**
183
+ * Creates a Vite server configured for Jay Stack.
184
+ *
185
+ * This is the single source of truth for Vite configuration.
186
+ * Both dev-server and CLI use this function.
187
+ */
188
+ declare function createViteServer(options: CreateViteServerOptions): Promise<ViteDevServer>;
189
+ /**
190
+ * Creates a minimal Vite server for CLI usage.
191
+ *
192
+ * This is a convenience wrapper around createViteServer with CLI-appropriate defaults.
193
+ * 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/).
195
+ */
196
+ declare function createViteForCli(options: {
197
+ projectRoot: string;
198
+ tsConfigFilePath?: string;
199
+ }): Promise<ViteDevServer>;
200
+
201
+ export { ACTION_ENDPOINT_BASE, type ActionRouterOptions, type CreateViteServerOptions, type DevServer, type DevServerOptions, type DevServerRoute, actionBodyParser, createActionRouter, createViteForCli, createViteServer, mkDevServer };
package/dist/index.js CHANGED
@@ -5,15 +5,18 @@ var __publicField = (obj, key, value) => {
5
5
  return value;
6
6
  };
7
7
  import { createServer } from "vite";
8
- import { scanRoutes, routeToExpressRoute } from "@jay-framework/stack-route-scanner";
9
- import { discoverPluginsWithInit, sortPluginsByDependencies, executePluginServerInits, runInitCallbacks, actionRegistry, discoverAndRegisterActions, discoverAllPluginActions, runShutdownCallbacks, clearLifecycleCallbacks, clearServiceRegistry, clearClientInitData, DevSlowlyChangingPhase, preparePluginClientInits, loadPageParts, renderFastChangingData, getClientInitData, generateClientScript } from "@jay-framework/stack-server-runtime";
10
8
  import { jayRuntime } from "@jay-framework/vite-plugin";
11
9
  import { createRequire } from "module";
12
10
  import "@jay-framework/compiler-shared";
13
11
  import * as path from "node:path";
14
12
  import path__default from "node:path";
15
- import "@jay-framework/compiler-jay-html";
13
+ import { discoverHeadlessInstances, parseContract, slowRenderTransform, JAY_IMPORT_RESOLVER, resolveHeadlessInstances } from "@jay-framework/compiler-jay-html";
14
+ import { getLogger, getDevLogger } from "@jay-framework/logger";
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, getServiceRegistry, materializeContracts, loadPageParts, renderFastChangingData, slowRenderInstances, generateClientScript, getClientInitData, resolveServices } from "@jay-framework/stack-server-runtime";
19
+ import fs$1 from "node:fs/promises";
17
20
  import { pathToFileURL } from "node:url";
18
21
  const s$1 = createRequire(import.meta.url), e$1 = s$1("typescript"), c$1 = new Proxy(e$1, {
19
22
  get(t, r) {
@@ -1019,7 +1022,7 @@ function createImportChainTracker(options = {}) {
1019
1022
  importChain.clear();
1020
1023
  detectedServerModules.clear();
1021
1024
  if (verbose) {
1022
- console.log("[import-chain-tracker] Build started, tracking imports...");
1025
+ getLogger().info("[import-chain-tracker] Build started, tracking imports...");
1023
1026
  }
1024
1027
  },
1025
1028
  resolveId(source, importer, options2) {
@@ -1031,7 +1034,7 @@ function createImportChainTracker(options = {}) {
1031
1034
  }
1032
1035
  if (importer) {
1033
1036
  if (verbose) {
1034
- console.log(
1037
+ getLogger().info(
1035
1038
  `[import-chain-tracker] ${shortenPath(importer)} imports ${source}`
1036
1039
  );
1037
1040
  }
@@ -1048,14 +1051,14 @@ function createImportChainTracker(options = {}) {
1048
1051
  if (isServerOnlyModule(id)) {
1049
1052
  detectedServerModules.add(id);
1050
1053
  const chain = buildImportChain(id);
1051
- console.error(
1054
+ getLogger().error(
1052
1055
  `
1053
1056
  [import-chain-tracker] ⚠️ Server-only module detected in client build!`
1054
1057
  );
1055
- console.error(`Module: ${shortenPath(id)}`);
1056
- console.error(`Import chain:`);
1057
- console.error(formatChain(chain));
1058
- console.error("");
1058
+ getLogger().error(`Module: ${shortenPath(id)}`);
1059
+ getLogger().error(`Import chain:`);
1060
+ getLogger().error(formatChain(chain));
1061
+ getLogger().error("");
1059
1062
  }
1060
1063
  const importRegex = /import\s+(?:(?:\{[^}]*\}|[^{}\s,]+)\s+from\s+)?['"]([^'"]+)['"]/g;
1061
1064
  let match;
@@ -1065,16 +1068,18 @@ function createImportChainTracker(options = {}) {
1065
1068
  if (isServerOnlyModule(importedModule)) {
1066
1069
  if (!detectedServerModules.has(importedModule)) {
1067
1070
  detectedServerModules.add(importedModule);
1068
- console.error(
1071
+ getLogger().error(
1069
1072
  `
1070
1073
  [import-chain-tracker] ⚠️ Server-only import detected in client build!`
1071
1074
  );
1072
- console.error(`Module "${importedModule}" imported by: ${shortenPath(id)}`);
1075
+ getLogger().error(
1076
+ `Module "${importedModule}" imported by: ${shortenPath(id)}`
1077
+ );
1073
1078
  const chain = buildImportChain(id);
1074
1079
  chain.push(importedModule);
1075
- console.error(`Import chain:`);
1076
- console.error(formatChain(chain));
1077
- console.error("");
1080
+ getLogger().error(`Import chain:`);
1081
+ getLogger().error(formatChain(chain));
1082
+ getLogger().error("");
1078
1083
  }
1079
1084
  }
1080
1085
  }
@@ -1082,27 +1087,151 @@ function createImportChainTracker(options = {}) {
1082
1087
  },
1083
1088
  buildEnd() {
1084
1089
  if (detectedServerModules.size > 0) {
1085
- console.warn(
1090
+ getLogger().warn(
1086
1091
  `
1087
1092
  [import-chain-tracker] ⚠️ ${detectedServerModules.size} server-only module(s) detected during transform:`
1088
1093
  );
1089
1094
  for (const mod of detectedServerModules) {
1090
- console.warn(` - ${mod}`);
1095
+ getLogger().warn(` - ${mod}`);
1091
1096
  }
1092
- console.warn(
1097
+ getLogger().warn(
1093
1098
  "\nNote: These may be stripped by the code-split transform if only used in .withServer()."
1094
1099
  );
1095
- console.warn(
1100
+ getLogger().warn(
1096
1101
  'If build fails with "not exported" errors, check the import chains above.\n'
1097
1102
  );
1098
1103
  } else if (verbose) {
1099
- console.log(
1104
+ getLogger().info(
1100
1105
  "[import-chain-tracker] ✅ No server-only modules detected in client build"
1101
1106
  );
1102
1107
  }
1103
1108
  }
1104
1109
  };
1105
1110
  }
1111
+ const require2 = createRequire$1(import.meta.url);
1112
+ function createDefaultPluginDetector() {
1113
+ const cache = /* @__PURE__ */ new Map();
1114
+ return {
1115
+ isJayPluginWithClientExport(packageName, projectRoot) {
1116
+ const cacheKey = `${packageName}:${projectRoot}`;
1117
+ if (cache.has(cacheKey)) {
1118
+ return cache.get(cacheKey);
1119
+ }
1120
+ let result = false;
1121
+ try {
1122
+ require2.resolve(`${packageName}/plugin.yaml`, { paths: [projectRoot] });
1123
+ try {
1124
+ require2.resolve(`${packageName}/client`, { paths: [projectRoot] });
1125
+ result = true;
1126
+ } catch {
1127
+ result = false;
1128
+ }
1129
+ } catch {
1130
+ result = false;
1131
+ }
1132
+ cache.set(cacheKey, result);
1133
+ return result;
1134
+ }
1135
+ };
1136
+ }
1137
+ function extractPackageName(source) {
1138
+ if (source.startsWith(".") || source.startsWith("/")) {
1139
+ return null;
1140
+ }
1141
+ if (source.startsWith("@")) {
1142
+ const parts2 = source.split("/");
1143
+ if (parts2.length >= 2) {
1144
+ return `${parts2[0]}/${parts2[1]}`;
1145
+ }
1146
+ return null;
1147
+ }
1148
+ const parts = source.split("/");
1149
+ return parts[0];
1150
+ }
1151
+ function isSubpathImport(source, packageName) {
1152
+ return source.length > packageName.length && source[packageName.length] === "/";
1153
+ }
1154
+ const IMPORT_REGEX = /import\s+(.+?)\s+from\s+(['"])([^'"]+)\2/g;
1155
+ const EXPORT_FROM_REGEX = /export\s+(.+?)\s+from\s+(['"])([^'"]+)\2/g;
1156
+ function transformImports(options) {
1157
+ const { code, projectRoot, filePath, pluginDetector, verbose = false } = options;
1158
+ let hasChanges = false;
1159
+ let result = code;
1160
+ result = result.replace(IMPORT_REGEX, (match, clause, quote, source) => {
1161
+ const packageName = extractPackageName(source);
1162
+ if (!packageName)
1163
+ return match;
1164
+ if (isSubpathImport(source, packageName))
1165
+ return match;
1166
+ if (!pluginDetector.isJayPluginWithClientExport(packageName, projectRoot))
1167
+ return match;
1168
+ hasChanges = true;
1169
+ const newSource = `${packageName}/client`;
1170
+ if (verbose) {
1171
+ getLogger().info(
1172
+ `[plugin-client-import] Rewriting import ${source} -> ${newSource} (in ${path.basename(filePath)})`
1173
+ );
1174
+ }
1175
+ return `import ${clause} from ${quote}${newSource}${quote}`;
1176
+ });
1177
+ result = result.replace(EXPORT_FROM_REGEX, (match, clause, quote, source) => {
1178
+ const packageName = extractPackageName(source);
1179
+ if (!packageName)
1180
+ return match;
1181
+ if (isSubpathImport(source, packageName))
1182
+ return match;
1183
+ if (!pluginDetector.isJayPluginWithClientExport(packageName, projectRoot))
1184
+ return match;
1185
+ hasChanges = true;
1186
+ const newSource = `${packageName}/client`;
1187
+ if (verbose) {
1188
+ getLogger().info(
1189
+ `[plugin-client-import] Rewriting export ${source} -> ${newSource} (in ${path.basename(filePath)})`
1190
+ );
1191
+ }
1192
+ return `export ${clause} from ${quote}${newSource}${quote}`;
1193
+ });
1194
+ return { code: result, hasChanges };
1195
+ }
1196
+ function createPluginClientImportResolver(options = {}) {
1197
+ const { verbose = false } = options;
1198
+ let projectRoot = options.projectRoot || process.cwd();
1199
+ let isSSRBuild = false;
1200
+ const pluginDetector = options.pluginDetector || createDefaultPluginDetector();
1201
+ return {
1202
+ name: "jay-stack:plugin-client-import",
1203
+ enforce: "pre",
1204
+ configResolved(config) {
1205
+ projectRoot = config.root || projectRoot;
1206
+ isSSRBuild = !!config.build?.ssr;
1207
+ },
1208
+ transform(code, id, transformOptions) {
1209
+ if (transformOptions?.ssr || isSSRBuild) {
1210
+ return null;
1211
+ }
1212
+ if (!id.endsWith(".ts") && !id.endsWith(".js") && !id.includes(".ts?") && !id.includes(".js?")) {
1213
+ return null;
1214
+ }
1215
+ if (id.includes("node_modules") && !id.includes("@jay-framework")) {
1216
+ return null;
1217
+ }
1218
+ if (!code.includes("@jay-framework/") && !code.includes("from '@") && !code.includes('from "@')) {
1219
+ return null;
1220
+ }
1221
+ const result = transformImports({
1222
+ code,
1223
+ projectRoot,
1224
+ filePath: id,
1225
+ pluginDetector,
1226
+ verbose
1227
+ });
1228
+ if (!result.hasChanges) {
1229
+ return null;
1230
+ }
1231
+ return { code: result.code };
1232
+ }
1233
+ };
1234
+ }
1106
1235
  function jayStackCompiler(options = {}) {
1107
1236
  const { trackImports, ...jayOptions } = options;
1108
1237
  const moduleCache = /* @__PURE__ */ new Map();
@@ -1112,6 +1241,7 @@ function jayStackCompiler(options = {}) {
1112
1241
  if (shouldTrackImports) {
1113
1242
  plugins.push(createImportChainTracker(trackerOptions));
1114
1243
  }
1244
+ plugins.push(createPluginClientImportResolver({ verbose: !!shouldTrackImports }));
1115
1245
  plugins.push(
1116
1246
  // First: Jay Stack code splitting transformation
1117
1247
  {
@@ -1131,7 +1261,7 @@ function jayStackCompiler(options = {}) {
1131
1261
  try {
1132
1262
  return transformJayStackBuilder(code, id, environment);
1133
1263
  } catch (error) {
1134
- console.error(`[jay-stack:code-split] Error transforming ${id}:`, error);
1264
+ getLogger().error(`[jay-stack:code-split] Error transforming ${id}: ${error}`);
1135
1265
  return null;
1136
1266
  }
1137
1267
  }
@@ -1172,6 +1302,13 @@ function jayStackCompiler(options = {}) {
1172
1302
  } else {
1173
1303
  return null;
1174
1304
  }
1305
+ } else if (resolvedPath.endsWith(".js") && !fs.existsSync(resolvedPath)) {
1306
+ const tsPath = resolvedPath.slice(0, -3) + ".ts";
1307
+ if (fs.existsSync(tsPath)) {
1308
+ resolvedPath = tsPath;
1309
+ } else {
1310
+ return null;
1311
+ }
1175
1312
  }
1176
1313
  return `\0jay-action:${resolvedPath}`;
1177
1314
  },
@@ -1184,12 +1321,14 @@ function jayStackCompiler(options = {}) {
1184
1321
  try {
1185
1322
  code = await fs.promises.readFile(actualPath, "utf-8");
1186
1323
  } catch (err) {
1187
- console.error(`[action-transform] Could not read ${actualPath}:`, err);
1324
+ getLogger().error(
1325
+ `[action-transform] Could not read ${actualPath}: ${err}`
1326
+ );
1188
1327
  return null;
1189
1328
  }
1190
1329
  const actions = extractActionsFromSource(code, actualPath);
1191
1330
  if (actions.length === 0) {
1192
- console.warn(`[action-transform] No actions found in ${actualPath}`);
1331
+ getLogger().warn(`[action-transform] No actions found in ${actualPath}`);
1193
1332
  return null;
1194
1333
  }
1195
1334
  const lines = [
@@ -1216,6 +1355,69 @@ function jayStackCompiler(options = {}) {
1216
1355
  );
1217
1356
  return plugins;
1218
1357
  }
1358
+ async function createViteServer(options) {
1359
+ const {
1360
+ projectRoot,
1361
+ pagesRoot = projectRoot,
1362
+ base,
1363
+ jayRollupConfig = { tsConfigFilePath: path__default.join(projectRoot, "tsconfig.json") },
1364
+ logLevel = "info",
1365
+ clearScreen = true
1366
+ } = options;
1367
+ const vite = await createServer({
1368
+ // Don't start HTTP server - we use middleware mode
1369
+ server: { middlewareMode: true },
1370
+ // Use Jay Stack compiler for .jay-html and other custom transforms
1371
+ plugins: [...jayStackCompiler(jayRollupConfig)],
1372
+ // Custom app type (no default middleware)
1373
+ appType: "custom",
1374
+ // Base URL path
1375
+ base,
1376
+ // Root directory for module resolution
1377
+ root: pagesRoot,
1378
+ // SSR configuration
1379
+ ssr: {
1380
+ // Mark jay-framework packages as external so Vite uses Node's require
1381
+ // This ensures all packages share the same module instances (Symbol identity)
1382
+ external: ["@jay-framework/stack-server-runtime", "@jay-framework/fullstack-component"]
1383
+ },
1384
+ // Disable automatic entry point discovery for pre-bundling —
1385
+ // we run in middleware mode with no HTML files, so Vite can't auto-detect entries
1386
+ optimizeDeps: {
1387
+ entries: []
1388
+ },
1389
+ // Logging
1390
+ logLevel,
1391
+ clearScreen
1392
+ });
1393
+ return vite;
1394
+ }
1395
+ async function createViteForCli(options) {
1396
+ const { projectRoot, tsConfigFilePath = path__default.join(projectRoot, "tsconfig.json") } = options;
1397
+ const vite = await createServer({
1398
+ server: {
1399
+ middlewareMode: true,
1400
+ watch: {
1401
+ ignored: ["**/build/**"]
1402
+ }
1403
+ },
1404
+ plugins: [
1405
+ ...jayStackCompiler({ tsConfigFilePath })
1406
+ ],
1407
+ appType: "custom",
1408
+ root: projectRoot,
1409
+ ssr: {
1410
+ external: ["@jay-framework/stack-server-runtime", "@jay-framework/fullstack-component"]
1411
+ },
1412
+ // Disable dependency optimization — CLI only uses SSR, no browser bundles
1413
+ optimizeDeps: {
1414
+ entries: []
1415
+ },
1416
+ logLevel: "warn",
1417
+ clearScreen: false
1418
+ });
1419
+ return vite;
1420
+ }
1219
1421
  class ServiceLifecycleManager {
1220
1422
  constructor(projectRoot, sourceBase = "src") {
1221
1423
  /** Path to project's lib/init.ts (makeJayInit pattern) */
@@ -1254,8 +1456,9 @@ class ServiceLifecycleManager {
1254
1456
  * 4. Auto-discovering and registering actions
1255
1457
  */
1256
1458
  async initialize() {
1459
+ const log = getLogger();
1257
1460
  if (this.isInitialized) {
1258
- console.warn("[Services] Already initialized, skipping...");
1461
+ log.warn("[Services] Already initialized, skipping...");
1259
1462
  return;
1260
1463
  }
1261
1464
  this.projectInitFilePath = this.findProjectInitFile();
@@ -1265,18 +1468,18 @@ class ServiceLifecycleManager {
1265
1468
  });
1266
1469
  this.pluginsWithInit = sortPluginsByDependencies(discoveredPlugins);
1267
1470
  if (this.pluginsWithInit.length > 0) {
1268
- console.log(
1471
+ log.info(
1269
1472
  `[Services] Found ${this.pluginsWithInit.length} plugin(s) with init: ${this.pluginsWithInit.map((p) => p.name).join(", ")}`
1270
1473
  );
1271
1474
  }
1272
1475
  await executePluginServerInits(this.pluginsWithInit, this.viteServer ?? void 0, true);
1273
1476
  if (this.projectInitFilePath) {
1274
- console.log("[DevServer] Loading project init: src/init.ts");
1477
+ log.info("[DevServer] Loading project init: src/init.ts");
1275
1478
  try {
1276
1479
  if (this.viteServer) {
1277
1480
  const module = await this.viteServer.ssrLoadModule(this.projectInitFilePath);
1278
1481
  if (module.init?._serverInit) {
1279
- console.log("[DevServer] Running server init: project");
1482
+ log.info("[DevServer] Running server init: project");
1280
1483
  const { setClientInitData } = await import("@jay-framework/stack-server-runtime");
1281
1484
  const clientData = await module.init._serverInit();
1282
1485
  if (clientData !== void 0 && clientData !== null) {
@@ -1288,14 +1491,14 @@ class ServiceLifecycleManager {
1288
1491
  await import(fileUrl);
1289
1492
  }
1290
1493
  } catch (error) {
1291
- console.error("[Services] Failed to load project init:", error);
1494
+ log.error(`[Services] Failed to load project init: ${error}`);
1292
1495
  throw error;
1293
1496
  }
1294
1497
  } else {
1295
- console.log("[Services] No init.ts found, skipping project initialization");
1498
+ log.info("[Services] No init.ts found, skipping project initialization");
1296
1499
  }
1297
1500
  await runInitCallbacks();
1298
- console.log("[Services] Initialization complete");
1501
+ log.info("[Services] Initialization complete");
1299
1502
  await this.discoverActions();
1300
1503
  this.isInitialized = true;
1301
1504
  }
@@ -1303,6 +1506,7 @@ class ServiceLifecycleManager {
1303
1506
  * Auto-discovers and registers actions from project and plugins.
1304
1507
  */
1305
1508
  async discoverActions() {
1509
+ const log = getLogger();
1306
1510
  let totalActions = 0;
1307
1511
  try {
1308
1512
  const result = await discoverAndRegisterActions({
@@ -1314,7 +1518,7 @@ class ServiceLifecycleManager {
1314
1518
  });
1315
1519
  totalActions += result.actionCount;
1316
1520
  } catch (error) {
1317
- console.error("[Actions] Failed to auto-discover project actions:", error);
1521
+ log.error(`[Actions] Failed to auto-discover project actions: ${error}`);
1318
1522
  }
1319
1523
  try {
1320
1524
  const pluginActions = await discoverAllPluginActions({
@@ -1325,20 +1529,21 @@ class ServiceLifecycleManager {
1325
1529
  });
1326
1530
  totalActions += pluginActions.length;
1327
1531
  } catch (error) {
1328
- console.error("[Actions] Failed to auto-discover plugin actions:", error);
1532
+ log.error(`[Actions] Failed to auto-discover plugin actions: ${error}`);
1329
1533
  }
1330
1534
  if (totalActions > 0) {
1331
- console.log(`[Actions] Auto-registered ${totalActions} action(s) total`);
1535
+ log.info(`[Actions] Auto-registered ${totalActions} action(s) total`);
1332
1536
  }
1333
1537
  }
1334
1538
  /**
1335
1539
  * Shuts down services gracefully with timeout
1336
1540
  */
1337
1541
  async shutdown(timeoutMs = 5e3) {
1542
+ const log = getLogger();
1338
1543
  if (!this.isInitialized) {
1339
1544
  return;
1340
1545
  }
1341
- console.log("[Services] Shutting down...");
1546
+ log.info("[Services] Shutting down...");
1342
1547
  try {
1343
1548
  await Promise.race([
1344
1549
  runShutdownCallbacks(),
@@ -1346,12 +1551,12 @@ class ServiceLifecycleManager {
1346
1551
  (_, reject) => setTimeout(() => reject(new Error("Shutdown timeout")), timeoutMs)
1347
1552
  )
1348
1553
  ]);
1349
- console.log("[Services] Shutdown complete");
1554
+ log.info("[Services] Shutdown complete");
1350
1555
  } catch (error) {
1351
1556
  if (error.message === "Shutdown timeout") {
1352
- console.warn("[Services] Shutdown timed out after", timeoutMs, "ms");
1557
+ log.warn(`[Services] Shutdown timed out after ${timeoutMs}ms`);
1353
1558
  } else {
1354
- console.error("[Services] Shutdown error:", error);
1559
+ log.error(`[Services] Shutdown error: ${error}`);
1355
1560
  }
1356
1561
  } finally {
1357
1562
  this.isInitialized = false;
@@ -1361,7 +1566,8 @@ class ServiceLifecycleManager {
1361
1566
  * Hot reload: shutdown, clear caches, re-import, and re-initialize
1362
1567
  */
1363
1568
  async reload() {
1364
- console.log("[Services] Reloading services...");
1569
+ const log = getLogger();
1570
+ log.info("[Services] Reloading services...");
1365
1571
  await this.shutdown();
1366
1572
  clearLifecycleCallbacks();
1367
1573
  clearServiceRegistry();
@@ -1377,7 +1583,7 @@ class ServiceLifecycleManager {
1377
1583
  }
1378
1584
  this.isInitialized = false;
1379
1585
  await this.initialize();
1380
- console.log("[Services] Reload complete");
1586
+ log.info("[Services] Reload complete");
1381
1587
  }
1382
1588
  /**
1383
1589
  * Returns the path to the init file if found.
@@ -1630,11 +1836,13 @@ function defaults(options) {
1630
1836
  projectRootFolder,
1631
1837
  options.pagesRootFolder || "./src/pages"
1632
1838
  );
1839
+ const buildFolder = options.buildFolder || path__default.resolve(projectRootFolder, "./build");
1633
1840
  const tsConfigFilePath = options.jayRollupConfig.tsConfigFilePath || path__default.resolve(projectRootFolder, "./tsconfig.json");
1634
1841
  return {
1635
1842
  publicBaseUrlPath,
1636
1843
  pagesRootFolder,
1637
1844
  projectRootFolder,
1845
+ buildFolder,
1638
1846
  dontCacheSlowly: options.dontCacheSlowly,
1639
1847
  jayRollupConfig: {
1640
1848
  ...options.jayRollupConfig || {},
@@ -1650,124 +1858,601 @@ function handleOtherResponseCodes(res, renderedResult) {
1650
1858
  else
1651
1859
  res.status(renderedResult.status).end("redirect to " + renderedResult.location);
1652
1860
  }
1653
- function mkRoute(route, vite, slowlyPhase, options, projectInit, allPluginsWithInit = [], allPluginClientInits = []) {
1654
- const path2 = routeToExpressRoute(route);
1861
+ function filterPluginsForPage(allPluginClientInits, allPluginsWithInit, usedPackages) {
1862
+ const pluginsByPackage = /* @__PURE__ */ new Map();
1863
+ for (const plugin of allPluginsWithInit) {
1864
+ pluginsByPackage.set(plugin.packageName, plugin);
1865
+ }
1866
+ const expandedPackages = new Set(usedPackages);
1867
+ const toProcess = [...usedPackages];
1868
+ while (toProcess.length > 0) {
1869
+ const packageName = toProcess.pop();
1870
+ const plugin = pluginsByPackage.get(packageName);
1871
+ if (!plugin)
1872
+ continue;
1873
+ for (const dep of plugin.dependencies) {
1874
+ if (pluginsByPackage.has(dep) && !expandedPackages.has(dep)) {
1875
+ expandedPackages.add(dep);
1876
+ toProcess.push(dep);
1877
+ }
1878
+ }
1879
+ }
1880
+ return allPluginClientInits.filter((plugin) => {
1881
+ const pluginInfo = allPluginsWithInit.find((p) => p.name === plugin.name);
1882
+ return pluginInfo && expandedPackages.has(pluginInfo.packageName);
1883
+ });
1884
+ }
1885
+ function mkRoute(route, vite, slowlyPhase, options, slowRenderCache, projectInit, allPluginsWithInit = [], allPluginClientInits = []) {
1886
+ const routePath = routeToExpressRoute(route);
1655
1887
  const handler = async (req, res) => {
1888
+ const timing = getDevLogger()?.startRequest(req.method, req.path);
1656
1889
  try {
1657
1890
  const url = req.originalUrl.replace(options.publicBaseUrlPath, "");
1658
- const pageParams = req.params;
1891
+ const pageParams = { ...route.inferredParams, ...req.params };
1659
1892
  const pageProps = {
1660
1893
  language: "en",
1661
1894
  url
1662
1895
  };
1663
- let viewState, carryForward;
1664
- const pagePartsResult = await loadPageParts(
1665
- vite,
1666
- route,
1667
- options.pagesRootFolder,
1668
- options.projectRootFolder,
1669
- options.jayRollupConfig
1670
- );
1671
- if (pagePartsResult.val) {
1672
- const {
1673
- parts: pageParts,
1674
- serverTrackByMap,
1675
- clientTrackByMap,
1676
- usedPackages
1677
- } = pagePartsResult.val;
1678
- const pluginsForPage = allPluginClientInits.filter((plugin) => {
1679
- const pluginInfo = allPluginsWithInit.find((p) => p.name === plugin.name);
1680
- return pluginInfo && usedPackages.has(pluginInfo.packageName);
1681
- });
1682
- const renderedSlowly = await slowlyPhase.runSlowlyForPage(
1896
+ const useSlowRenderCache = !options.dontCacheSlowly;
1897
+ let cachedEntry = useSlowRenderCache ? slowRenderCache.get(route.jayHtmlPath, pageParams) : void 0;
1898
+ if (cachedEntry) {
1899
+ try {
1900
+ await fs$1.access(cachedEntry.preRenderedPath);
1901
+ } catch {
1902
+ getLogger().info(
1903
+ `[SlowRender] Cached file missing, rebuilding: ${cachedEntry.preRenderedPath}`
1904
+ );
1905
+ await slowRenderCache.invalidate(route.jayHtmlPath);
1906
+ cachedEntry = void 0;
1907
+ }
1908
+ }
1909
+ if (cachedEntry) {
1910
+ await handleCachedRequest(
1911
+ vite,
1912
+ route,
1913
+ options,
1914
+ cachedEntry,
1683
1915
  pageParams,
1684
1916
  pageProps,
1685
- pageParts
1917
+ allPluginClientInits,
1918
+ allPluginsWithInit,
1919
+ projectInit,
1920
+ res,
1921
+ url,
1922
+ timing
1923
+ );
1924
+ } else if (useSlowRenderCache) {
1925
+ await handlePreRenderRequest(
1926
+ vite,
1927
+ route,
1928
+ options,
1929
+ slowlyPhase,
1930
+ slowRenderCache,
1931
+ pageParams,
1932
+ pageProps,
1933
+ allPluginClientInits,
1934
+ allPluginsWithInit,
1935
+ projectInit,
1936
+ res,
1937
+ url,
1938
+ timing
1686
1939
  );
1687
- if (renderedSlowly.kind === "PhaseOutput") {
1688
- const renderedFast = await renderFastChangingData(
1689
- pageParams,
1690
- pageProps,
1691
- renderedSlowly.carryForward,
1692
- pageParts
1693
- );
1694
- if (renderedFast.kind === "PhaseOutput") {
1695
- if (serverTrackByMap && Object.keys(serverTrackByMap).length > 0) {
1696
- viewState = deepMergeViewStates(
1697
- renderedSlowly.rendered,
1698
- renderedFast.rendered,
1699
- serverTrackByMap
1700
- );
1701
- } else {
1702
- viewState = { ...renderedSlowly.rendered, ...renderedFast.rendered };
1703
- }
1704
- carryForward = renderedFast.carryForward;
1705
- const pageHtml = generateClientScript(
1706
- viewState,
1707
- carryForward,
1708
- pageParts,
1709
- route.jayHtmlPath,
1710
- clientTrackByMap,
1711
- getClientInitData(),
1712
- projectInit,
1713
- pluginsForPage
1714
- );
1715
- const compiledPageHtml = await vite.transformIndexHtml(
1716
- !!url ? url : "/",
1717
- pageHtml
1718
- );
1719
- res.status(200).set({ "Content-Type": "text/html" }).send(compiledPageHtml);
1720
- } else {
1721
- handleOtherResponseCodes(res, renderedFast);
1722
- }
1723
- } else if (renderedSlowly.kind === "ClientError") {
1724
- handleOtherResponseCodes(res, renderedSlowly);
1725
- }
1726
1940
  } else {
1727
- console.log(pagePartsResult.validations.join("\n"));
1728
- res.status(500).end(pagePartsResult.validations.join("\n"));
1941
+ await handleDirectRequest(
1942
+ vite,
1943
+ route,
1944
+ options,
1945
+ slowlyPhase,
1946
+ pageParams,
1947
+ pageProps,
1948
+ allPluginClientInits,
1949
+ allPluginsWithInit,
1950
+ projectInit,
1951
+ res,
1952
+ url,
1953
+ timing
1954
+ );
1729
1955
  }
1730
1956
  } catch (e2) {
1731
1957
  vite?.ssrFixStacktrace(e2);
1732
- console.log(e2.stack);
1958
+ getLogger().error(e2.stack);
1733
1959
  res.status(500).end(e2.stack);
1960
+ timing?.end();
1734
1961
  }
1735
1962
  };
1736
- return { path: path2, handler, fsRoute: route };
1963
+ return { path: routePath, handler, fsRoute: route };
1737
1964
  }
1738
- async function mkDevServer(options) {
1965
+ async function handleCachedRequest(vite, route, options, cachedEntry, pageParams, pageProps, allPluginClientInits, allPluginsWithInit, projectInit, res, url, timing) {
1966
+ const loadStart = Date.now();
1967
+ const pagePartsResult = await loadPageParts(
1968
+ vite,
1969
+ route,
1970
+ options.pagesRootFolder,
1971
+ options.projectRootFolder,
1972
+ options.jayRollupConfig,
1973
+ { preRenderedPath: cachedEntry.preRenderedPath }
1974
+ );
1975
+ timing?.recordViteSsr(Date.now() - loadStart);
1976
+ if (!pagePartsResult.val) {
1977
+ getLogger().info(pagePartsResult.validations.join("\n"));
1978
+ res.status(500).end(pagePartsResult.validations.join("\n"));
1979
+ timing?.end();
1980
+ return;
1981
+ }
1982
+ const { parts: pageParts, clientTrackByMap, usedPackages } = pagePartsResult.val;
1983
+ const pluginsForPage = filterPluginsForPage(
1984
+ allPluginClientInits,
1985
+ allPluginsWithInit,
1986
+ usedPackages
1987
+ );
1988
+ const fastStart = Date.now();
1989
+ const renderedFast = await renderFastChangingData(
1990
+ pageParams,
1991
+ pageProps,
1992
+ cachedEntry.carryForward,
1993
+ pageParts
1994
+ );
1995
+ timing?.recordFastRender(Date.now() - fastStart);
1996
+ if (renderedFast.kind !== "PhaseOutput") {
1997
+ handleOtherResponseCodes(res, renderedFast);
1998
+ timing?.end();
1999
+ return;
2000
+ }
2001
+ let fastViewState = renderedFast.rendered;
2002
+ let fastCarryForward = renderedFast.carryForward;
2003
+ const instancePhaseData = cachedEntry.carryForward?.__instances;
2004
+ if (instancePhaseData && pagePartsResult.val.headlessInstanceComponents.length > 0) {
2005
+ const instanceFastResult = await renderFastChangingDataForInstances(
2006
+ instancePhaseData,
2007
+ pagePartsResult.val.headlessInstanceComponents
2008
+ );
2009
+ if (instanceFastResult) {
2010
+ fastViewState = {
2011
+ ...fastViewState,
2012
+ __headlessInstances: instanceFastResult.viewStates
2013
+ };
2014
+ fastCarryForward = {
2015
+ ...fastCarryForward,
2016
+ __headlessInstances: instanceFastResult.carryForwards
2017
+ };
2018
+ }
2019
+ }
2020
+ await sendResponse(
2021
+ vite,
2022
+ res,
2023
+ url,
2024
+ cachedEntry.preRenderedPath,
2025
+ pageParts,
2026
+ fastViewState,
2027
+ fastCarryForward,
2028
+ clientTrackByMap,
2029
+ projectInit,
2030
+ pluginsForPage,
2031
+ options,
2032
+ cachedEntry.slowViewState,
2033
+ timing
2034
+ );
2035
+ }
2036
+ async function handlePreRenderRequest(vite, route, options, slowlyPhase, slowRenderCache, pageParams, pageProps, allPluginClientInits, allPluginsWithInit, projectInit, res, url, timing) {
2037
+ const loadStart = Date.now();
2038
+ const initialPartsResult = await loadPageParts(
2039
+ vite,
2040
+ route,
2041
+ options.pagesRootFolder,
2042
+ options.projectRootFolder,
2043
+ options.jayRollupConfig
2044
+ );
2045
+ timing?.recordViteSsr(Date.now() - loadStart);
2046
+ if (!initialPartsResult.val) {
2047
+ getLogger().info(initialPartsResult.validations.join("\n"));
2048
+ res.status(500).end(initialPartsResult.validations.join("\n"));
2049
+ timing?.end();
2050
+ return;
2051
+ }
2052
+ const slowStart = Date.now();
2053
+ const renderedSlowly = await slowlyPhase.runSlowlyForPage(
2054
+ pageParams,
2055
+ pageProps,
2056
+ initialPartsResult.val.parts
2057
+ );
2058
+ if (renderedSlowly.kind !== "PhaseOutput") {
2059
+ timing?.recordSlowRender(Date.now() - slowStart);
2060
+ if (renderedSlowly.kind === "ClientError") {
2061
+ handleOtherResponseCodes(res, renderedSlowly);
2062
+ }
2063
+ timing?.end();
2064
+ return;
2065
+ }
2066
+ const preRenderResult = await preRenderJayHtml(
2067
+ route,
2068
+ renderedSlowly.rendered,
2069
+ initialPartsResult.val.headlessContracts,
2070
+ initialPartsResult.val.headlessInstanceComponents
2071
+ );
2072
+ timing?.recordSlowRender(Date.now() - slowStart);
2073
+ if (!preRenderResult) {
2074
+ res.status(500).end("Failed to pre-render jay-html");
2075
+ timing?.end();
2076
+ return;
2077
+ }
2078
+ const carryForward = preRenderResult.instancePhaseData ? { ...renderedSlowly.carryForward, __instances: preRenderResult.instancePhaseData } : renderedSlowly.carryForward;
2079
+ const preRenderedPath = await slowRenderCache.set(
2080
+ route.jayHtmlPath,
2081
+ pageParams,
2082
+ preRenderResult.preRenderedJayHtml,
2083
+ renderedSlowly.rendered,
2084
+ carryForward
2085
+ );
2086
+ getLogger().info(`[SlowRender] Cached pre-rendered jay-html at ${preRenderedPath}`);
2087
+ const pagePartsResult = await loadPageParts(
2088
+ vite,
2089
+ route,
2090
+ options.pagesRootFolder,
2091
+ options.projectRootFolder,
2092
+ options.jayRollupConfig,
2093
+ { preRenderedPath }
2094
+ );
2095
+ if (!pagePartsResult.val) {
2096
+ getLogger().info(pagePartsResult.validations.join("\n"));
2097
+ res.status(500).end(pagePartsResult.validations.join("\n"));
2098
+ timing?.end();
2099
+ return;
2100
+ }
2101
+ const { parts: pageParts, clientTrackByMap, usedPackages } = pagePartsResult.val;
2102
+ const pluginsForPage = filterPluginsForPage(
2103
+ allPluginClientInits,
2104
+ allPluginsWithInit,
2105
+ usedPackages
2106
+ );
2107
+ const fastStart = Date.now();
2108
+ const renderedFast = await renderFastChangingData(
2109
+ pageParams,
2110
+ pageProps,
2111
+ carryForward,
2112
+ pageParts
2113
+ );
2114
+ timing?.recordFastRender(Date.now() - fastStart);
2115
+ if (renderedFast.kind !== "PhaseOutput") {
2116
+ handleOtherResponseCodes(res, renderedFast);
2117
+ timing?.end();
2118
+ return;
2119
+ }
2120
+ let fastViewState = renderedFast.rendered;
2121
+ let fastCarryForward = renderedFast.carryForward;
2122
+ const instancePhaseData = carryForward?.__instances;
2123
+ if (instancePhaseData && pagePartsResult.val.headlessInstanceComponents.length > 0) {
2124
+ const instanceFastResult = await renderFastChangingDataForInstances(
2125
+ instancePhaseData,
2126
+ pagePartsResult.val.headlessInstanceComponents
2127
+ );
2128
+ if (instanceFastResult) {
2129
+ fastViewState = {
2130
+ ...fastViewState,
2131
+ __headlessInstances: instanceFastResult.viewStates
2132
+ };
2133
+ fastCarryForward = {
2134
+ ...fastCarryForward,
2135
+ __headlessInstances: instanceFastResult.carryForwards
2136
+ };
2137
+ }
2138
+ }
2139
+ await sendResponse(
2140
+ vite,
2141
+ res,
2142
+ url,
2143
+ preRenderedPath,
2144
+ pageParts,
2145
+ fastViewState,
2146
+ fastCarryForward,
2147
+ clientTrackByMap,
2148
+ projectInit,
2149
+ pluginsForPage,
2150
+ options,
2151
+ renderedSlowly.rendered,
2152
+ timing
2153
+ );
2154
+ }
2155
+ async function handleDirectRequest(vite, route, options, slowlyPhase, pageParams, pageProps, allPluginClientInits, allPluginsWithInit, projectInit, res, url, timing) {
2156
+ const loadStart = Date.now();
2157
+ const pagePartsResult = await loadPageParts(
2158
+ vite,
2159
+ route,
2160
+ options.pagesRootFolder,
2161
+ options.projectRootFolder,
2162
+ options.jayRollupConfig
2163
+ );
2164
+ timing?.recordViteSsr(Date.now() - loadStart);
2165
+ if (!pagePartsResult.val) {
2166
+ getLogger().info(pagePartsResult.validations.join("\n"));
2167
+ res.status(500).end(pagePartsResult.validations.join("\n"));
2168
+ timing?.end();
2169
+ return;
2170
+ }
2171
+ const {
2172
+ parts: pageParts,
2173
+ serverTrackByMap,
2174
+ clientTrackByMap,
2175
+ usedPackages
2176
+ } = pagePartsResult.val;
2177
+ const pluginsForPage = filterPluginsForPage(
2178
+ allPluginClientInits,
2179
+ allPluginsWithInit,
2180
+ usedPackages
2181
+ );
2182
+ const slowStart = Date.now();
2183
+ const renderedSlowly = await slowlyPhase.runSlowlyForPage(pageParams, pageProps, pageParts);
2184
+ if (renderedSlowly.kind !== "PhaseOutput") {
2185
+ timing?.recordSlowRender(Date.now() - slowStart);
2186
+ if (renderedSlowly.kind === "ClientError") {
2187
+ handleOtherResponseCodes(res, renderedSlowly);
2188
+ }
2189
+ timing?.end();
2190
+ return;
2191
+ }
2192
+ let instanceViewStates;
2193
+ let instancePhaseDataForFast;
2194
+ const headlessInstanceComponents = pagePartsResult.val.headlessInstanceComponents ?? [];
2195
+ if (headlessInstanceComponents.length > 0) {
2196
+ const jayHtmlContent = await fs$1.readFile(route.jayHtmlPath, "utf-8");
2197
+ const discoveryResult = discoverHeadlessInstances(jayHtmlContent);
2198
+ if (discoveryResult.instances.length > 0) {
2199
+ const slowResult = await slowRenderInstances(
2200
+ discoveryResult.instances,
2201
+ headlessInstanceComponents
2202
+ );
2203
+ if (slowResult) {
2204
+ instanceViewStates = { ...slowResult.slowViewStates };
2205
+ instancePhaseDataForFast = slowResult.instancePhaseData;
2206
+ }
2207
+ }
2208
+ }
2209
+ timing?.recordSlowRender(Date.now() - slowStart);
2210
+ const fastStart = Date.now();
2211
+ const renderedFast = await renderFastChangingData(
2212
+ pageParams,
2213
+ pageProps,
2214
+ renderedSlowly.carryForward,
2215
+ pageParts
2216
+ );
2217
+ if (instancePhaseDataForFast && instanceViewStates) {
2218
+ const instanceFastResult = await renderFastChangingDataForInstances(
2219
+ instancePhaseDataForFast,
2220
+ headlessInstanceComponents
2221
+ );
2222
+ if (instanceFastResult) {
2223
+ for (const [coordKey, fastVS] of Object.entries(instanceFastResult.viewStates)) {
2224
+ instanceViewStates[coordKey] = {
2225
+ ...instanceViewStates[coordKey] || {},
2226
+ ...fastVS
2227
+ };
2228
+ }
2229
+ }
2230
+ }
2231
+ timing?.recordFastRender(Date.now() - fastStart);
2232
+ if (renderedFast.kind !== "PhaseOutput") {
2233
+ handleOtherResponseCodes(res, renderedFast);
2234
+ timing?.end();
2235
+ return;
2236
+ }
2237
+ let viewState;
2238
+ if (serverTrackByMap && Object.keys(serverTrackByMap).length > 0) {
2239
+ viewState = deepMergeViewStates(
2240
+ renderedSlowly.rendered,
2241
+ renderedFast.rendered,
2242
+ serverTrackByMap
2243
+ );
2244
+ } else {
2245
+ viewState = { ...renderedSlowly.rendered, ...renderedFast.rendered };
2246
+ }
2247
+ if (instanceViewStates && Object.keys(instanceViewStates).length > 0) {
2248
+ viewState = {
2249
+ ...viewState,
2250
+ __headlessInstances: instanceViewStates
2251
+ };
2252
+ }
2253
+ await sendResponse(
2254
+ vite,
2255
+ res,
2256
+ url,
2257
+ route.jayHtmlPath,
2258
+ pageParts,
2259
+ viewState,
2260
+ renderedFast.carryForward,
2261
+ clientTrackByMap,
2262
+ projectInit,
2263
+ pluginsForPage,
2264
+ options,
2265
+ void 0,
2266
+ timing
2267
+ );
2268
+ }
2269
+ async function sendResponse(vite, res, url, jayHtmlPath, pageParts, viewState, carryForward, clientTrackByMap, projectInit, pluginsForPage, options, slowViewState, timing) {
2270
+ const pageHtml = generateClientScript(
2271
+ viewState,
2272
+ carryForward,
2273
+ pageParts,
2274
+ jayHtmlPath,
2275
+ clientTrackByMap,
2276
+ getClientInitData(),
2277
+ projectInit,
2278
+ pluginsForPage,
2279
+ {
2280
+ enableAutomation: !options.disableAutomation,
2281
+ slowViewState
2282
+ }
2283
+ );
2284
+ if (options.buildFolder) {
2285
+ const pageName = !url || url === "/" ? "index" : url.replace(/^\//, "").replace(/\//g, "-");
2286
+ const clientScriptDir = path__default.join(options.buildFolder, "client-scripts");
2287
+ await fs$1.mkdir(clientScriptDir, { recursive: true });
2288
+ await fs$1.writeFile(path__default.join(clientScriptDir, `${pageName}.html`), pageHtml, "utf-8");
2289
+ }
2290
+ const viteStart = Date.now();
2291
+ const compiledPageHtml = await vite.transformIndexHtml(!!url ? url : "/", pageHtml);
2292
+ timing?.recordViteClient(Date.now() - viteStart);
2293
+ res.status(200).set({ "Content-Type": "text/html" }).send(compiledPageHtml);
2294
+ timing?.end();
2295
+ }
2296
+ async function preRenderJayHtml(route, slowViewState, headlessContracts, headlessInstanceComponents) {
2297
+ const jayHtmlContent = await fs$1.readFile(route.jayHtmlPath, "utf-8");
2298
+ const contractPath = route.jayHtmlPath.replace(".jay-html", ".jay-contract");
2299
+ let contract;
2300
+ try {
2301
+ const contractContent = await fs$1.readFile(contractPath, "utf-8");
2302
+ const parseResult = parseContract(contractContent, path__default.basename(contractPath));
2303
+ if (parseResult.val) {
2304
+ contract = parseResult.val;
2305
+ } else if (parseResult.validations.length > 0) {
2306
+ getLogger().error(
2307
+ `[SlowRender] Contract parse error for ${contractPath}: ${parseResult.validations.join(", ")}`
2308
+ );
2309
+ return void 0;
2310
+ }
2311
+ } catch (error) {
2312
+ if (error.code !== "ENOENT") {
2313
+ getLogger().error(`[SlowRender] Error reading contract ${contractPath}: ${error}`);
2314
+ return void 0;
2315
+ }
2316
+ }
2317
+ const result = slowRenderTransform({
2318
+ jayHtmlContent,
2319
+ slowViewState,
2320
+ contract,
2321
+ headlessContracts,
2322
+ sourceDir: path__default.dirname(route.jayHtmlPath),
2323
+ importResolver: JAY_IMPORT_RESOLVER
2324
+ });
2325
+ if (!result.val) {
2326
+ if (result.validations.length > 0) {
2327
+ getLogger().error(
2328
+ `[SlowRender] Transform failed for ${route.jayHtmlPath}: ${result.validations.join(", ")}`
2329
+ );
2330
+ }
2331
+ return void 0;
2332
+ }
2333
+ let preRenderedJayHtml = result.val.preRenderedJayHtml;
2334
+ let instancePhaseData;
2335
+ if (headlessInstanceComponents.length > 0) {
2336
+ const discoveryResult = discoverHeadlessInstances(preRenderedJayHtml);
2337
+ preRenderedJayHtml = discoveryResult.preRenderedJayHtml;
2338
+ if (discoveryResult.instances.length > 0) {
2339
+ const slowResult = await slowRenderInstances(
2340
+ discoveryResult.instances,
2341
+ headlessInstanceComponents
2342
+ );
2343
+ if (slowResult) {
2344
+ instancePhaseData = slowResult.instancePhaseData;
2345
+ const pass2Result = resolveHeadlessInstances(
2346
+ preRenderedJayHtml,
2347
+ slowResult.resolvedData,
2348
+ JAY_IMPORT_RESOLVER
2349
+ );
2350
+ if (pass2Result.val) {
2351
+ preRenderedJayHtml = pass2Result.val;
2352
+ }
2353
+ if (pass2Result.validations.length > 0) {
2354
+ getLogger().error(
2355
+ `[SlowRender] Instance resolution warnings for ${route.jayHtmlPath}: ${pass2Result.validations.join(", ")}`
2356
+ );
2357
+ }
2358
+ }
2359
+ }
2360
+ }
2361
+ return { preRenderedJayHtml, instancePhaseData };
2362
+ }
2363
+ async function renderFastChangingDataForInstances(instancePhaseData, headlessInstanceComponents) {
2364
+ const componentByContractName = /* @__PURE__ */ new Map();
2365
+ for (const comp of headlessInstanceComponents) {
2366
+ componentByContractName.set(comp.contractName, comp);
2367
+ }
2368
+ const viewStates = {};
2369
+ const carryForwards = {};
2370
+ let hasResults = false;
2371
+ for (const instance of instancePhaseData.discovered) {
2372
+ const coordKey = instance.coordinate.join("/");
2373
+ const comp = componentByContractName.get(instance.contractName);
2374
+ if (!comp || !comp.compDefinition.fastRender) {
2375
+ continue;
2376
+ }
2377
+ const instanceCarryForward = instancePhaseData.carryForwards[coordKey] || {};
2378
+ const services = resolveServices(comp.compDefinition.services);
2379
+ const fastResult = await comp.compDefinition.fastRender(
2380
+ instance.props,
2381
+ instanceCarryForward,
2382
+ ...services
2383
+ );
2384
+ if (fastResult.kind === "PhaseOutput") {
2385
+ viewStates[coordKey] = fastResult.rendered;
2386
+ carryForwards[coordKey] = fastResult.carryForward;
2387
+ hasResults = true;
2388
+ }
2389
+ }
2390
+ return hasResults ? { viewStates, carryForwards } : void 0;
2391
+ }
2392
+ async function materializeDynamicContracts(projectRootFolder, buildFolder, viteServer) {
2393
+ try {
2394
+ const services = getServiceRegistry();
2395
+ const result = await materializeContracts(
2396
+ {
2397
+ projectRoot: projectRootFolder,
2398
+ outputDir: path__default.join(buildFolder, "materialized-contracts"),
2399
+ verbose: false,
2400
+ viteServer
2401
+ },
2402
+ services
2403
+ );
2404
+ const dynamicCount = result.dynamicCount;
2405
+ if (dynamicCount > 0) {
2406
+ getLogger().info(`[Contracts] Materialized ${dynamicCount} dynamic contract(s)`);
2407
+ }
2408
+ } catch (error) {
2409
+ getLogger().warn(`[Contracts] Failed to materialize dynamic contracts: ${error.message}`);
2410
+ }
2411
+ }
2412
+ async function mkDevServer(rawOptions) {
2413
+ const options = defaults(rawOptions);
1739
2414
  const {
1740
2415
  publicBaseUrlPath,
1741
2416
  pagesRootFolder,
1742
2417
  projectRootFolder,
2418
+ buildFolder,
1743
2419
  jayRollupConfig,
1744
2420
  dontCacheSlowly
1745
- } = defaults(options);
2421
+ } = options;
2422
+ const viteLogLevel = options.logLevel === "silent" ? "silent" : options.logLevel === "verbose" ? "info" : "warn";
1746
2423
  const lifecycleManager = new ServiceLifecycleManager(projectRootFolder);
1747
2424
  setupGracefulShutdown(lifecycleManager);
1748
- const vite = await createServer({
1749
- server: { middlewareMode: true },
1750
- plugins: [...jayStackCompiler(jayRollupConfig)],
1751
- appType: "custom",
2425
+ const vite = await createViteServer({
2426
+ projectRoot: projectRootFolder,
2427
+ pagesRoot: pagesRootFolder,
1752
2428
  base: publicBaseUrlPath,
1753
- root: pagesRootFolder,
1754
- ssr: {
1755
- // Mark stack-server-runtime as external so Vite uses Node's require
1756
- // This ensures lib/init.ts and dev-server share the same module instance
1757
- external: ["@jay-framework/stack-server-runtime"]
1758
- }
2429
+ jayRollupConfig,
2430
+ logLevel: viteLogLevel
1759
2431
  });
1760
2432
  lifecycleManager.setViteServer(vite);
1761
2433
  await lifecycleManager.initialize();
2434
+ await materializeDynamicContracts(projectRootFolder, buildFolder, vite);
1762
2435
  setupServiceHotReload(vite, lifecycleManager);
1763
2436
  setupActionRouter(vite);
1764
2437
  const routes = await initRoutes(pagesRootFolder);
1765
2438
  const slowlyPhase = new DevSlowlyChangingPhase(dontCacheSlowly);
2439
+ const slowRenderCacheDir = path__default.join(buildFolder, "slow-render-cache");
2440
+ const slowRenderCache = new SlowRenderCache(slowRenderCacheDir, pagesRootFolder);
2441
+ setupSlowRenderCacheInvalidation(vite, slowRenderCache, pagesRootFolder);
1766
2442
  const projectInit = lifecycleManager.getProjectInit() ?? void 0;
1767
2443
  const pluginsWithInit = lifecycleManager.getPluginsWithInit();
1768
2444
  const pluginClientInits = preparePluginClientInits(pluginsWithInit);
1769
2445
  const devServerRoutes = routes.map(
1770
- (route) => mkRoute(route, vite, slowlyPhase, options, projectInit, pluginsWithInit, pluginClientInits)
2446
+ (route) => mkRoute(
2447
+ route,
2448
+ vite,
2449
+ slowlyPhase,
2450
+ options,
2451
+ slowRenderCache,
2452
+ projectInit,
2453
+ pluginsWithInit,
2454
+ pluginClientInits
2455
+ )
1771
2456
  );
1772
2457
  return {
1773
2458
  server: vite.middlewares,
@@ -1778,7 +2463,7 @@ async function mkDevServer(options) {
1778
2463
  }
1779
2464
  function setupGracefulShutdown(lifecycleManager) {
1780
2465
  const shutdown = async (signal) => {
1781
- console.log(`
2466
+ getLogger().important(`
1782
2467
  ${signal} received, shutting down gracefully...`);
1783
2468
  await lifecycleManager.shutdown();
1784
2469
  process.exit(0);
@@ -1794,7 +2479,7 @@ function setupServiceHotReload(vite, lifecycleManager) {
1794
2479
  vite.watcher.add(initFilePath);
1795
2480
  vite.watcher.on("change", async (changedPath) => {
1796
2481
  if (changedPath === initFilePath) {
1797
- console.log("[Services] lib/init.ts changed, reloading services...");
2482
+ getLogger().info("[Services] lib/init.ts changed, reloading services...");
1798
2483
  try {
1799
2484
  await lifecycleManager.reload();
1800
2485
  vite.ws.send({
@@ -1802,7 +2487,7 @@ function setupServiceHotReload(vite, lifecycleManager) {
1802
2487
  path: "*"
1803
2488
  });
1804
2489
  } catch (error) {
1805
- console.error("[Services] Failed to reload services:", error);
2490
+ getLogger().error(`[Services] Failed to reload services: ${error}`);
1806
2491
  }
1807
2492
  }
1808
2493
  });
@@ -1810,11 +2495,45 @@ function setupServiceHotReload(vite, lifecycleManager) {
1810
2495
  function setupActionRouter(vite) {
1811
2496
  vite.middlewares.use(actionBodyParser());
1812
2497
  vite.middlewares.use(ACTION_ENDPOINT_BASE, createActionRouter());
1813
- console.log(`[Actions] Action router mounted at ${ACTION_ENDPOINT_BASE}`);
2498
+ getLogger().info(`[Actions] Action router mounted at ${ACTION_ENDPOINT_BASE}`);
2499
+ }
2500
+ function setupSlowRenderCacheInvalidation(vite, cache, pagesRootFolder) {
2501
+ vite.watcher.on("change", (changedPath) => {
2502
+ if (!changedPath.startsWith(pagesRootFolder)) {
2503
+ return;
2504
+ }
2505
+ if (changedPath.endsWith(".jay-html")) {
2506
+ cache.invalidate(changedPath).then(() => {
2507
+ getLogger().info(`[SlowRender] Cache invalidated for ${changedPath}`);
2508
+ });
2509
+ return;
2510
+ }
2511
+ if (changedPath.endsWith("page.ts")) {
2512
+ const dir = path__default.dirname(changedPath);
2513
+ const jayHtmlPath = path__default.join(dir, "page.jay-html");
2514
+ cache.invalidate(jayHtmlPath).then(() => {
2515
+ getLogger().info(
2516
+ `[SlowRender] Cache invalidated for ${jayHtmlPath} (page.ts changed)`
2517
+ );
2518
+ });
2519
+ return;
2520
+ }
2521
+ if (changedPath.endsWith(".jay-contract")) {
2522
+ const jayHtmlPath = changedPath.replace(".jay-contract", ".jay-html");
2523
+ cache.invalidate(jayHtmlPath).then(() => {
2524
+ getLogger().info(
2525
+ `[SlowRender] Cache invalidated for ${jayHtmlPath} (contract changed)`
2526
+ );
2527
+ });
2528
+ return;
2529
+ }
2530
+ });
1814
2531
  }
1815
2532
  export {
1816
2533
  ACTION_ENDPOINT_BASE,
1817
2534
  actionBodyParser,
1818
2535
  createActionRouter,
2536
+ createViteForCli,
2537
+ createViteServer,
1819
2538
  mkDevServer
1820
2539
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jay-framework/dev-server",
3
- "version": "0.10.0",
3
+ "version": "0.12.0",
4
4
  "type": "module",
5
5
  "license": "Apache-2.0",
6
6
  "main": "dist/index.js",
@@ -22,21 +22,22 @@
22
22
  "test:watch": "vitest"
23
23
  },
24
24
  "dependencies": {
25
- "@jay-framework/compiler-jay-stack": "^0.10.0",
26
- "@jay-framework/compiler-shared": "^0.10.0",
27
- "@jay-framework/component": "^0.10.0",
28
- "@jay-framework/fullstack-component": "^0.10.0",
29
- "@jay-framework/runtime": "^0.10.0",
30
- "@jay-framework/stack-client-runtime": "^0.10.0",
31
- "@jay-framework/stack-route-scanner": "^0.10.0",
32
- "@jay-framework/stack-server-runtime": "^0.10.0",
33
- "@jay-framework/view-state-merge": "^0.10.0",
25
+ "@jay-framework/compiler-jay-stack": "^0.12.0",
26
+ "@jay-framework/compiler-shared": "^0.12.0",
27
+ "@jay-framework/component": "^0.12.0",
28
+ "@jay-framework/fullstack-component": "^0.12.0",
29
+ "@jay-framework/logger": "^0.12.0",
30
+ "@jay-framework/runtime": "^0.12.0",
31
+ "@jay-framework/stack-client-runtime": "^0.12.0",
32
+ "@jay-framework/stack-route-scanner": "^0.12.0",
33
+ "@jay-framework/stack-server-runtime": "^0.12.0",
34
+ "@jay-framework/view-state-merge": "^0.12.0",
34
35
  "vite": "^5.0.11"
35
36
  },
36
37
  "devDependencies": {
37
- "@jay-framework/dev-environment": "^0.10.0",
38
- "@jay-framework/jay-cli": "^0.10.0",
39
- "@jay-framework/stack-client-runtime": "^0.10.0",
38
+ "@jay-framework/dev-environment": "^0.12.0",
39
+ "@jay-framework/jay-cli": "^0.12.0",
40
+ "@jay-framework/stack-client-runtime": "^0.12.0",
40
41
  "@types/express": "^5.0.2",
41
42
  "@types/node": "^22.15.21",
42
43
  "nodemon": "^3.0.3",