@jay-framework/production-server 0.17.3 → 0.17.4

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 +100 -5
  2. package/dist/index.js +987 -251
  3. package/package.json +12 -12
package/dist/index.js CHANGED
@@ -12,15 +12,16 @@ import path from "node:path";
12
12
  import fs from "node:fs/promises";
13
13
  import { createRequire } from "node:module";
14
14
  import { parseJayFile, JAY_IMPORT_RESOLVER, injectHeadfullFSTemplates, discoverHeadlessInstances, assignCoordinatesToJayHtml, generateServerElementFile, parseContract, slowRenderTransform, resolveHeadlessInstances } from "@jay-framework/compiler-jay-html";
15
- import { DevSlowlyChangingPhase, slowRenderInstances, scanPlugins, runLoadParams, renderFastChangingData, mergeHeadTags, serializeHeadTags, getClientInitData, actionRegistry, setClientInitData } from "@jay-framework/stack-server-runtime";
15
+ import { DevSlowlyChangingPhase, slowRenderInstances, scanPlugins, runLoadParams, renderFastChangingData, mergeHeadTags, serializeHeadTags, getClientInitData, actionRegistry, setClientInitData, parseCookies } from "@jay-framework/stack-server-runtime";
16
16
  import { checkValidationErrors } from "@jay-framework/compiler-shared";
17
17
  import { jayRuntime } from "@jay-framework/vite-plugin";
18
18
  import crypto from "node:crypto";
19
19
  import fs$1 from "node:fs";
20
20
  import http from "node:http";
21
+ import { Readable } from "node:stream";
21
22
  import { deepMergeViewStates } from "@jay-framework/view-state-merge";
22
23
  import { asyncSwapScript } from "@jay-framework/ssr-runtime";
23
- import { isJayAction, isJayStreamAction } from "@jay-framework/fullstack-component";
24
+ import { isJayAction, isJayStreamAction, isJayWebhook } from "@jay-framework/fullstack-component";
24
25
  async function discoverServerEntries(projectRoot, pagesRoot) {
25
26
  const logger = getLogger();
26
27
  const routes = await scanRoutes(pagesRoot, {
@@ -57,7 +58,7 @@ async function discoverServerEntries(projectRoot, pagesRoot) {
57
58
  const dirPath = path.join(scanDir, dir.name);
58
59
  const files = await fs.readdir(dirPath);
59
60
  for (const file of files) {
60
- if (file.endsWith(".ts") && !file.endsWith(".d.ts") && file !== "init.ts" && file !== "page.ts") {
61
+ if (file.endsWith(".ts") && !file.endsWith(".d.ts") && file !== "page.ts") {
61
62
  const entryName = `${subDir}/${dir.name}/${file.replace(/\.ts$/, "")}`;
62
63
  pages[entryName] = path.join(dirPath, file);
63
64
  }
@@ -226,6 +227,7 @@ async function loadProductionPageParts(route, pageModule, jayHtmlContent, projec
226
227
  const jayHtml = checkValidationErrors(jayHtmlWithValidations);
227
228
  const headlessInstanceComponents = [];
228
229
  const keyedPartModules = [];
230
+ const headlessModuleInfos = [];
229
231
  const headlessImports = jayHtml.headlessImports ?? [];
230
232
  getLogger().info(
231
233
  `[Build] headlessImports for ${fileName}: ${headlessImports.length}, keys: ${Object.keys(jayHtml).join(",")}`
@@ -261,21 +263,26 @@ async function loadProductionPageParts(route, pageModule, jayHtmlContent, projec
261
263
  const headlessCompDef = headlessModule[name];
262
264
  if (headlessImport.key) {
263
265
  const clientModulePath = isLocalModule ? path.resolve(dirName, module) : `${module}/client`;
266
+ const ci = headlessImport.contract ? { contractName: headlessImport.contract.name, metadata: headlessImport.metadata } : void 0;
264
267
  parts.push({
265
268
  key: headlessImport.key,
266
269
  compDefinition: headlessCompDef,
267
270
  clientImport: "",
268
271
  clientPart: "",
269
- contractInfo: headlessImport.contract ? {
270
- contractName: headlessImport.contract.name,
271
- metadata: headlessImport.metadata
272
- } : void 0
272
+ contractInfo: ci
273
273
  });
274
274
  keyedPartModules.push({
275
275
  key: headlessImport.key,
276
276
  modulePath: clientModulePath,
277
277
  exportName: name
278
278
  });
279
+ headlessModuleInfos.push({
280
+ modulePath,
281
+ exportName: name,
282
+ isLocal: isLocalModule,
283
+ key: headlessImport.key,
284
+ contractInfo: ci
285
+ });
279
286
  }
280
287
  if (!headlessImport.key && headlessImport.contract) {
281
288
  headlessInstanceComponents.push({
@@ -283,6 +290,13 @@ async function loadProductionPageParts(route, pageModule, jayHtmlContent, projec
283
290
  compDefinition: headlessCompDef,
284
291
  contract: headlessImport.contract
285
292
  });
293
+ headlessModuleInfos.push({
294
+ modulePath,
295
+ exportName: name,
296
+ isLocal: isLocalModule,
297
+ contractName: headlessImport.contractName,
298
+ propNames: headlessImport.contract.props?.map((p) => p.name) ?? []
299
+ });
286
300
  }
287
301
  }
288
302
  const headlessContracts = (jayHtml.headlessImports ?? []).filter((hi) => hi.contract && hi.key).map((hi) => ({
@@ -315,10 +329,101 @@ async function loadProductionPageParts(route, pageModule, jayHtmlContent, projec
315
329
  discoveredInstances,
316
330
  forEachInstances,
317
331
  keyedPartModules,
332
+ headlessModuleInfos,
318
333
  serverTrackByMap: jayHtml.serverTrackByMap,
319
334
  clientTrackByMap: jayHtml.clientTrackByMap
320
335
  };
321
336
  }
337
+ function buildPagePartsConfig(pageParts, pageServerModule, pageExportName, buildDir, pageIsPlugin = false) {
338
+ const parts = [];
339
+ if (pageServerModule) {
340
+ parts.push({
341
+ modulePath: pageServerModule,
342
+ exportName: pageExportName,
343
+ source: pageIsPlugin ? "npm" : "local"
344
+ });
345
+ }
346
+ for (const info of pageParts.headlessModuleInfos) {
347
+ if (info.key) {
348
+ parts.push({
349
+ modulePath: info.isLocal ? path.relative(buildDir, info.modulePath) : info.modulePath,
350
+ exportName: info.exportName,
351
+ source: info.isLocal ? "local" : "npm",
352
+ key: info.key,
353
+ contractInfo: info.contractInfo
354
+ });
355
+ }
356
+ }
357
+ const instanceComponents = [];
358
+ for (const info of pageParts.headlessModuleInfos) {
359
+ if (info.contractName) {
360
+ instanceComponents.push({
361
+ modulePath: info.isLocal ? path.relative(buildDir, info.modulePath) : info.modulePath,
362
+ exportName: info.exportName,
363
+ source: info.isLocal ? "local" : "npm",
364
+ contractName: info.contractName,
365
+ propNames: info.propNames ?? []
366
+ });
367
+ }
368
+ }
369
+ return {
370
+ parts,
371
+ instanceComponents,
372
+ forEachInstances: pageParts.forEachInstances.map((fi) => ({
373
+ contractName: fi.contractName,
374
+ forEachPath: fi.forEachPath,
375
+ trackBy: fi.trackBy,
376
+ propBindings: fi.propBindings,
377
+ coordinateSuffix: fi.coordinateSuffix
378
+ }))
379
+ };
380
+ }
381
+ async function loadPagePartsFromConfig(configPath, buildDir) {
382
+ const config = JSON.parse(await fs.readFile(configPath, "utf-8"));
383
+ async function importModule(entry) {
384
+ if (!entry.modulePath) {
385
+ throw new Error(
386
+ `Empty modulePath in page-parts.json for "${entry.exportName}" (source: ${entry.source}). Rebuild required.`
387
+ );
388
+ }
389
+ if (entry.source === "local") {
390
+ return import(path.join(buildDir, entry.modulePath));
391
+ }
392
+ return import(entry.modulePath);
393
+ }
394
+ const parts = [];
395
+ for (const entry of config.parts) {
396
+ const mod = await importModule(entry);
397
+ parts.push({
398
+ compDefinition: mod[entry.exportName] ?? mod.default,
399
+ key: entry.key,
400
+ clientImport: "",
401
+ clientPart: "",
402
+ contractInfo: entry.contractInfo
403
+ });
404
+ }
405
+ const headlessInstanceComponents = [];
406
+ for (const entry of config.instanceComponents) {
407
+ const mod = await importModule(entry);
408
+ const serveTimeContract = {
409
+ props: entry.propNames.map((name) => ({ name }))
410
+ };
411
+ headlessInstanceComponents.push({
412
+ contractName: entry.contractName,
413
+ compDefinition: mod[entry.exportName] ?? mod.default,
414
+ contract: serveTimeContract
415
+ });
416
+ }
417
+ return {
418
+ parts,
419
+ headlessContracts: [],
420
+ headlessInstanceComponents,
421
+ discoveredInstances: [],
422
+ forEachInstances: config.forEachInstances,
423
+ keyedPartModules: [],
424
+ headlessModuleInfos: []
425
+ };
426
+ }
322
427
  async function compileServerElement(jayHtmlContent, jayHtmlFilename, jayHtmlDir, outputPath, projectRoot, tsConfigFilePath, sourceDir) {
323
428
  const jayFile = await parseJayFile(
324
429
  jayHtmlContent,
@@ -482,7 +587,7 @@ function resolvePackageNameForRoute(compPath) {
482
587
  }
483
588
  return void 0;
484
589
  }
485
- function hashParams(params) {
590
+ function hashParams(params, suffix) {
486
591
  const sorted = Object.keys(params).sort().reduce(
487
592
  (acc, key) => {
488
593
  acc[key] = params[key];
@@ -491,20 +596,23 @@ function hashParams(params) {
491
596
  {}
492
597
  );
493
598
  const json = JSON.stringify(sorted);
494
- if (json === "{}")
599
+ if (json === "{}" && !suffix)
495
600
  return "";
496
- return "_" + crypto.createHash("md5").update(json).digest("hex").substring(0, 8);
601
+ const input = suffix ? json + ":" + suffix : json;
602
+ return "_" + crypto.createHash("md5").update(input).digest("hex").substring(0, 8);
497
603
  }
498
604
  async function buildInstance(route, params, pageModule, ctx) {
499
605
  const logger = getLogger();
500
606
  const routeDir = route.rawRoute.replace(/^\//, "") || "index";
501
- const paramHash = hashParams(params);
607
+ const paramHash = hashParams(params, ctx.rebuildSuffix);
502
608
  const instanceId = `page${paramHash}`;
503
- const instanceDir = path.join(ctx.buildDir, "pre-rendered", routeDir);
504
- await fs.mkdir(instanceDir, { recursive: true });
609
+ const backendInstanceDir = path.join(ctx.backendDir, "pre-rendered", routeDir);
610
+ const frontendInstanceDir = path.join(ctx.frontendDir, "pages", routeDir);
611
+ await fs.mkdir(backendInstanceDir, { recursive: true });
612
+ await fs.mkdir(frontendInstanceDir, { recursive: true });
505
613
  const jayHtmlContent = await fs.readFile(route.jayHtmlPath, "utf-8");
506
614
  const sourceDir = path.dirname(route.jayHtmlPath);
507
- const serverBuildDir = path.join(ctx.buildDir, "server");
615
+ const serverBuildDir = path.join(ctx.backendDir, "server");
508
616
  const pageParts = await loadProductionPageParts(
509
617
  route,
510
618
  pageModule,
@@ -513,6 +621,38 @@ async function buildInstance(route, params, pageModule, ctx) {
513
621
  ctx.tsConfigFilePath,
514
622
  serverBuildDir
515
623
  );
624
+ const contracts = [
625
+ .../* @__PURE__ */ new Set([
626
+ ...pageParts.headlessInstanceComponents.map((c) => c.contractName),
627
+ ...pageParts.parts.filter((p) => p.contractInfo?.contractName).map((p) => p.contractInfo.contractName)
628
+ ])
629
+ ];
630
+ const pagePartsConfigPath = path.join(backendInstanceDir, "page-parts.json");
631
+ try {
632
+ await fs.access(pagePartsConfigPath);
633
+ } catch {
634
+ const exportName = route.componentExport || "page";
635
+ let pageServerModule = "";
636
+ let pageIsPlugin = false;
637
+ if (route.compPath) {
638
+ if (route.componentExport) {
639
+ pageServerModule = route.compPath;
640
+ pageIsPlugin = true;
641
+ } else {
642
+ const relativePath = path.relative(ctx.projectRoot, route.compPath);
643
+ pageServerModule = relativePath.replace(/^src\//, "server/").replace(/\.ts$/, ".js").replace(/\[/g, "_").replace(/\]/g, "_");
644
+ }
645
+ }
646
+ const config = buildPagePartsConfig(
647
+ pageParts,
648
+ pageServerModule,
649
+ exportName,
650
+ ctx.backendDir,
651
+ pageIsPlugin
652
+ );
653
+ await fs.writeFile(pagePartsConfigPath, JSON.stringify(config, null, 2));
654
+ logger.info(`[Build] Page parts config: ${routeDir}/page-parts.json`);
655
+ }
516
656
  const slowPhase = new DevSlowlyChangingPhase();
517
657
  const slowResult = await slowPhase.runSlowlyForPage(
518
658
  params,
@@ -619,39 +759,62 @@ async function buildInstance(route, params, pageModule, ctx) {
619
759
  }
620
760
  }
621
761
  }
622
- const preRenderedPath = path.join(instanceDir, `${instanceId}.jay-html`);
762
+ preRenderedJayHtml = preRenderedJayHtml.replace(
763
+ /(<script\s+type="application\/jay-headfull"[^>]*\s)(src="([^"]*)")/g,
764
+ (_match, prefix, _srcAttr, srcVal) => {
765
+ if (path.isAbsolute(srcVal))
766
+ return prefix + `src="${srcVal}"`;
767
+ const abs = path.resolve(sourceDir, srcVal);
768
+ let rel = path.relative(backendInstanceDir, abs);
769
+ if (!rel.startsWith("."))
770
+ rel = "./" + rel;
771
+ return prefix + `src="${rel}"`;
772
+ }
773
+ );
774
+ preRenderedJayHtml = preRenderedJayHtml.replace(
775
+ /(<script\s+type="application\/jay-headfull"[^>]*\s)(contract="([^"]*)")/g,
776
+ (_match, prefix, _contractAttr, contractVal) => {
777
+ if (path.isAbsolute(contractVal))
778
+ return prefix + `contract="${contractVal}"`;
779
+ const abs = path.resolve(sourceDir, contractVal);
780
+ let rel = path.relative(backendInstanceDir, abs);
781
+ if (!rel.startsWith("."))
782
+ rel = "./" + rel;
783
+ return prefix + `contract="${rel}"`;
784
+ }
785
+ );
786
+ const preRenderedPath = path.join(backendInstanceDir, `${instanceId}.jay-html`);
623
787
  await fs.writeFile(preRenderedPath, preRenderedJayHtml, "utf-8");
624
- const cacheMetadataPath = path.join(instanceDir, `${instanceId}.cache.json`);
788
+ const cacheMetadataPath = path.join(backendInstanceDir, `${instanceId}.cache.json`);
625
789
  await fs.writeFile(
626
790
  cacheMetadataPath,
627
791
  JSON.stringify({
628
792
  slowViewState,
629
- carryForward,
630
- sourcePath: route.jayHtmlPath
793
+ carryForward
631
794
  }),
632
795
  "utf-8"
633
796
  );
634
797
  logger.info(`[Build] Pre-rendered: ${routeDir}/${instanceId}`);
635
- const serverElementPath = path.join(instanceDir, `${instanceId}.server-element.js`);
798
+ const serverElementPath = path.join(backendInstanceDir, `${instanceId}.server-element.js`);
636
799
  const serverElementResult = await compileServerElement(
637
800
  preRenderedJayHtml,
638
801
  `${instanceId}.jay-html`,
639
- instanceDir,
802
+ backendInstanceDir,
640
803
  serverElementPath,
641
804
  ctx.projectRoot,
642
805
  ctx.tsConfigFilePath,
643
806
  sourceDir
644
807
  );
645
- const hydrateEntryPath = path.join(instanceDir, `${instanceId}.hydrate-entry.ts`);
646
- const relativeJayHtmlPath = path.relative(instanceDir, preRenderedPath);
808
+ const hydrateEntryPath = path.join(backendInstanceDir, `${instanceId}.hydrate-entry.ts`);
809
+ const relativeJayHtmlPath = path.relative(backendInstanceDir, preRenderedPath);
647
810
  let pageModulePath;
648
811
  let pageExportName;
649
812
  if (route.componentExport) {
650
813
  const pkgName = resolvePackageNameForRoute(route.compPath);
651
- pageModulePath = pkgName ? `${pkgName}/client` : "./" + path.relative(instanceDir, route.compPath);
814
+ pageModulePath = pkgName ? `${pkgName}/client` : "./" + path.relative(backendInstanceDir, route.compPath);
652
815
  pageExportName = route.componentExport;
653
816
  } else if (route.compPath) {
654
- pageModulePath = "./" + path.relative(instanceDir, route.compPath);
817
+ pageModulePath = "./" + path.relative(backendInstanceDir, route.compPath);
655
818
  pageExportName = "page";
656
819
  } else {
657
820
  pageModulePath = "";
@@ -675,7 +838,7 @@ async function buildInstance(route, params, pageModule, ctx) {
675
838
  const clientResult = await buildInstanceClient(
676
839
  hydrateEntryPath,
677
840
  instanceId,
678
- instanceDir,
841
+ frontendInstanceDir,
679
842
  ctx.projectRoot,
680
843
  ctx.jayOptions,
681
844
  ctx.minify ?? true,
@@ -684,14 +847,27 @@ async function buildInstance(route, params, pageModule, ctx) {
684
847
  );
685
848
  await fs.rm(hydrateEntryPath, { force: true });
686
849
  const cssFile = clientResult.cssFile || serverElementResult.cssFile;
850
+ if (serverElementResult.cssFile && !clientResult.cssFile) {
851
+ const srcCss = path.join(backendInstanceDir, serverElementResult.cssFile);
852
+ const dstCss = path.join(frontendInstanceDir, serverElementResult.cssFile);
853
+ try {
854
+ await fs.rename(srcCss, dstCss);
855
+ } catch {
856
+ await fs.copyFile(srcCss, dstCss);
857
+ await fs.rm(srcCss, { force: true });
858
+ }
859
+ }
687
860
  const instanceEntry = {
688
861
  params,
689
- preRenderedPath: path.relative(ctx.buildDir, preRenderedPath),
690
- serverElementPath: path.relative(ctx.buildDir, serverElementPath),
691
- clientBundlePath: path.relative(ctx.buildDir, path.join(instanceDir, clientResult.jsFile)),
692
- clientCssPath: cssFile ? path.relative(ctx.buildDir, path.join(instanceDir, cssFile)) : void 0
862
+ preRenderedPath: path.relative(ctx.backendDir, preRenderedPath),
863
+ serverElementPath: path.relative(ctx.backendDir, serverElementPath),
864
+ clientBundlePath: path.relative(
865
+ ctx.frontendDir,
866
+ path.join(frontendInstanceDir, clientResult.jsFile)
867
+ ),
868
+ clientCssPath: cssFile ? path.relative(ctx.frontendDir, path.join(frontendInstanceDir, cssFile)) : void 0
693
869
  };
694
- return { status: "success", instanceEntry, slowViewState, carryForward };
870
+ return { status: "success", instanceEntry, slowViewState, carryForward, contracts };
695
871
  }
696
872
  function convertSegments(segments) {
697
873
  return segments.map((s) => {
@@ -713,7 +889,6 @@ function buildRouteEntry(route, serverModulePath) {
713
889
  pattern: route.rawRoute,
714
890
  segments: convertSegments(route.segments),
715
891
  serverModule: serverModulePath,
716
- jayHtmlPath: route.jayHtmlPath,
717
892
  componentExport: route.componentExport,
718
893
  instances: []
719
894
  };
@@ -1006,11 +1181,14 @@ async function discoverPluginClientPackages(projectRoot) {
1006
1181
  async function buildVersion(options) {
1007
1182
  const logger = getLogger();
1008
1183
  const buildDir = path.join(options.buildRoot, `v${options.version}`);
1184
+ const backendDir = path.join(buildDir, "backend");
1185
+ const frontendDir = path.join(buildDir, "frontend");
1009
1186
  logger.important(`[Build] Starting production build v${options.version}`);
1010
1187
  logger.important(`[Build] Project: ${options.projectRoot}`);
1011
- await fs.mkdir(buildDir, { recursive: true });
1188
+ await fs.mkdir(backendDir, { recursive: true });
1189
+ await fs.mkdir(frontendDir, { recursive: true });
1012
1190
  const { entries, routes } = await discoverServerEntries(options.projectRoot, options.pagesRoot);
1013
- const serverOutputDir = path.join(buildDir, "server");
1191
+ const serverOutputDir = path.join(backendDir, "server");
1014
1192
  await buildServerCode(
1015
1193
  entries,
1016
1194
  { tsConfigFilePath: options.tsConfigFilePath },
@@ -1021,7 +1199,7 @@ async function buildVersion(options) {
1021
1199
  if (pluginClientPackages.length > 0) {
1022
1200
  logger.important(`[Build] Plugin client packages: ${pluginClientPackages.join(", ")}`);
1023
1201
  }
1024
- const sharedOutputDir = path.join(buildDir, "shared");
1202
+ const sharedOutputDir = path.join(frontendDir, "shared");
1025
1203
  const { manifest: sharedManifest } = await buildSharedChunks(
1026
1204
  sharedOutputDir,
1027
1205
  options.projectRoot,
@@ -1031,7 +1209,7 @@ async function buildVersion(options) {
1031
1209
  const { actions, plugins } = await discoverActions(
1032
1210
  entries.actions,
1033
1211
  serverOutputDir,
1034
- buildDir,
1212
+ backendDir,
1035
1213
  options.projectRoot
1036
1214
  );
1037
1215
  const { discoverPluginsWithInit, sortPluginsByDependencies } = await import("@jay-framework/stack-server-runtime");
@@ -1106,6 +1284,8 @@ async function buildVersion(options) {
1106
1284
  projectRoot: options.projectRoot,
1107
1285
  pagesRoot: options.pagesRoot,
1108
1286
  buildDir,
1287
+ backendDir,
1288
+ frontendDir,
1109
1289
  jayOptions: { tsConfigFilePath: options.tsConfigFilePath },
1110
1290
  tsConfigFilePath: options.tsConfigFilePath,
1111
1291
  minify: options.minify ?? true,
@@ -1141,7 +1321,7 @@ async function buildVersion(options) {
1141
1321
  return {};
1142
1322
  if (entry.isPlugin)
1143
1323
  return import(entry.serverModule);
1144
- return import(path.join(serverOutputDir, entry.serverModule.replace("server/", "")));
1324
+ return import(path.join(backendDir, entry.serverModule));
1145
1325
  }
1146
1326
  const routeInfos = routeEntries.map((re) => {
1147
1327
  const optionalNames = re.route.segments.filter((s) => typeof s !== "string" && s.type === JayRouteParamType.optional).map((s) => s.name);
@@ -1172,7 +1352,7 @@ async function buildVersion(options) {
1172
1352
  await fs.readFile(route.jayHtmlPath, "utf-8"),
1173
1353
  options.projectRoot,
1174
1354
  options.tsConfigFilePath,
1175
- path.join(buildDir, "server")
1355
+ serverOutputDir
1176
1356
  );
1177
1357
  const partsWithLoadParams = pageParts.parts.filter((p) => p.compDefinition?.loadParams);
1178
1358
  if (partsWithLoadParams.length === 0)
@@ -1228,6 +1408,9 @@ async function buildVersion(options) {
1228
1408
  const result = await buildInstance(route, params, pageModule, instanceCtx);
1229
1409
  if (result.status === "success") {
1230
1410
  entry.instances.push(result.instanceEntry);
1411
+ if (result.contracts.length > 0 && !entry.contracts) {
1412
+ entry.contracts = result.contracts;
1413
+ }
1231
1414
  logInstance(route.rawRoute || "/", params);
1232
1415
  } else {
1233
1416
  logger.warn(
@@ -1248,13 +1431,12 @@ async function buildVersion(options) {
1248
1431
  buildTimestamp: (/* @__PURE__ */ new Date()).toISOString(),
1249
1432
  sourceHash: "",
1250
1433
  projectRoot: options.projectRoot,
1251
- publicBasePath: options.publicBasePath,
1252
1434
  sharedManifest,
1253
1435
  routes: routeEntries.map((r) => r.entry),
1254
1436
  actions,
1255
1437
  plugins
1256
1438
  };
1257
- await writeRouteManifest(manifest, buildDir);
1439
+ await writeRouteManifest(manifest, backendDir);
1258
1440
  const metadata = {
1259
1441
  version: options.version,
1260
1442
  sourceHash: "",
@@ -1263,9 +1445,16 @@ async function buildVersion(options) {
1263
1445
  instanceCount
1264
1446
  };
1265
1447
  await fs.writeFile(
1266
- path.join(buildDir, "build-metadata.json"),
1448
+ path.join(backendDir, "build-metadata.json"),
1267
1449
  JSON.stringify(metadata, null, 2)
1268
1450
  );
1451
+ const publicFolder = path.join(options.projectRoot, "public");
1452
+ try {
1453
+ await fs.access(publicFolder);
1454
+ await fs.cp(publicFolder, path.join(frontendDir, "public"), { recursive: true });
1455
+ logger.info("[Build] Copied public/ to frontend/public/");
1456
+ } catch {
1457
+ }
1269
1458
  logger.important(`[Build] Done! ${instanceCount} instances built in ${buildDir}`);
1270
1459
  return manifest;
1271
1460
  }
@@ -1274,17 +1463,24 @@ const CACHE_TAG_END = "<\/script>";
1274
1463
  class FilesystemArtifactStore {
1275
1464
  constructor(basePath) {
1276
1465
  __publicField(this, "manifestCache");
1466
+ __publicField(this, "metadataMtime");
1277
1467
  __publicField(this, "moduleCache", /* @__PURE__ */ new Map());
1278
1468
  this.basePath = basePath;
1279
1469
  }
1280
1470
  async readManifest() {
1471
+ const metadataPath = path.join(this.basePath, "build-metadata.json");
1281
1472
  const manifestPath = path.join(this.basePath, "route-manifest.json");
1282
- const stat = await fs.stat(manifestPath);
1283
- if (this.manifestCache && stat.mtimeMs === this.manifestCache.mtime) {
1284
- return this.manifestCache.manifest;
1473
+ try {
1474
+ const metaStat = await fs.stat(metadataPath);
1475
+ if (this.manifestCache && this.metadataMtime === metaStat.mtimeMs) {
1476
+ return this.manifestCache.manifest;
1477
+ }
1478
+ this.metadataMtime = metaStat.mtimeMs;
1479
+ } catch {
1285
1480
  }
1286
1481
  const manifest = JSON.parse(await fs.readFile(manifestPath, "utf-8"));
1287
- this.manifestCache = { manifest, mtime: stat.mtimeMs };
1482
+ const manifestStat = await fs.stat(manifestPath);
1483
+ this.manifestCache = { manifest, mtime: manifestStat.mtimeMs };
1288
1484
  return manifest;
1289
1485
  }
1290
1486
  async readPreRenderedHtml(relativePath) {
@@ -1315,6 +1511,9 @@ class FilesystemArtifactStore {
1315
1511
  getAssetPath(relativePath) {
1316
1512
  return path.join(this.basePath, relativePath);
1317
1513
  }
1514
+ getBuildDir() {
1515
+ return this.basePath;
1516
+ }
1318
1517
  async loadModule(relativePath) {
1319
1518
  const fullPath = path.join(this.basePath, relativePath);
1320
1519
  const stat = await fs.stat(fullPath);
@@ -1421,56 +1620,23 @@ function buildImportMap(sharedManifest, publicBasePath, sharedDir = "shared") {
1421
1620
  return imports;
1422
1621
  }
1423
1622
  const pagePartsCache = /* @__PURE__ */ new Map();
1424
- async function getPageParts(route, pageModule, artifacts, preRenderedPath, manifest) {
1623
+ async function getPageParts(route, artifacts, preRenderedPath) {
1425
1624
  const cacheKey = route.pattern;
1426
1625
  const cached = pagePartsCache.get(cacheKey);
1427
1626
  if (cached)
1428
1627
  return cached;
1429
- if (!route.jayHtmlPath) {
1430
- return {
1431
- parts: [
1432
- {
1433
- compDefinition: pageModule.page ?? pageModule.default,
1434
- clientImport: "",
1435
- clientPart: ""
1436
- }
1437
- ],
1438
- headlessContracts: [],
1439
- headlessInstanceComponents: [],
1440
- discoveredInstances: [],
1441
- forEachInstances: [],
1442
- keyedPartModules: []
1443
- };
1444
- }
1445
- const jayHtmlContent = await artifacts.readRawFile(preRenderedPath);
1446
- const serverBuildDir = artifacts.getAssetPath("server");
1447
- const parts = await loadProductionPageParts(
1448
- { jayHtmlPath: route.jayHtmlPath, componentExport: route.componentExport },
1449
- pageModule,
1450
- jayHtmlContent,
1451
- manifest.projectRoot,
1452
- void 0,
1453
- serverBuildDir
1454
- );
1628
+ const routeDir = path.dirname(preRenderedPath);
1629
+ const configPath = artifacts.getAssetPath(path.join(routeDir, "page-parts.json"));
1630
+ const buildDir = artifacts.getBuildDir();
1631
+ const parts = await loadPagePartsFromConfig(configPath, buildDir);
1455
1632
  pagePartsCache.set(cacheKey, parts);
1456
1633
  return parts;
1457
1634
  }
1458
- async function handlePageRequest(res, match, manifest, artifacts) {
1635
+ async function fetchPageRequest(match, manifest, requestUrl, artifacts, staticBaseUrl, cookies = {}) {
1459
1636
  const { route, instance } = match;
1460
1637
  const preRendered = await artifacts.readPreRenderedHtml(instance.preRenderedPath);
1461
- let pageModule = {};
1462
- if (route.serverModule) {
1463
- pageModule = route.isPlugin ? await import(route.serverModule) : await artifacts.loadPageModule(route.serverModule);
1464
- }
1465
- const pageParts = await getPageParts(
1466
- route,
1467
- pageModule,
1468
- artifacts,
1469
- instance.preRenderedPath,
1470
- manifest
1471
- );
1472
- const url = new URL(`http://localhost${match.pathname}`);
1473
- const query = Object.fromEntries(url.searchParams.entries());
1638
+ const pageParts = await getPageParts(route, artifacts, instance.preRenderedPath);
1639
+ const query = Object.fromEntries(requestUrl.searchParams.entries());
1474
1640
  const fastResult = await renderFastChangingData(
1475
1641
  match.params,
1476
1642
  { params: match.params, query },
@@ -1480,17 +1646,19 @@ async function handlePageRequest(res, match, manifest, artifacts) {
1480
1646
  pageParts.forEachInstances,
1481
1647
  pageParts.headlessInstanceComponents,
1482
1648
  preRendered.slowViewState,
1483
- query
1649
+ query,
1650
+ cookies
1484
1651
  );
1485
1652
  if (fastResult.kind === "Redirect3xx") {
1486
- res.writeHead(fastResult.status, { Location: fastResult.location });
1487
- res.end();
1488
- return;
1653
+ return new Response(null, {
1654
+ status: fastResult.status,
1655
+ headers: { Location: fastResult.location }
1656
+ });
1489
1657
  }
1490
1658
  if (fastResult.kind === "ServerError5xx" || fastResult.kind === "ClientError4xx") {
1491
- res.writeHead(fastResult.status);
1492
- res.end(fastResult.message || "Error");
1493
- return;
1659
+ return new Response(fastResult.message || "Error", {
1660
+ status: fastResult.status
1661
+ });
1494
1662
  }
1495
1663
  const fastViewState = fastResult.rendered || {};
1496
1664
  const fastCarryForward = fastResult.carryForward || {};
@@ -1503,7 +1671,6 @@ async function handlePageRequest(res, match, manifest, artifacts) {
1503
1671
  headTagSources.push(fastHeadTags);
1504
1672
  const headTags = headTagSources.length > 0 ? mergeHeadTags(headTagSources) : [];
1505
1673
  const headTagsHtml = headTags.length > 0 ? serializeHeadTags(headTags) + "\n" : "";
1506
- headTags.some((t) => t.tag.toLowerCase() === "title");
1507
1674
  const fullViewState = deepMergeViewStates(
1508
1675
  preRendered.slowViewState,
1509
1676
  fastViewState,
@@ -1511,15 +1678,15 @@ async function handlePageRequest(res, match, manifest, artifacts) {
1511
1678
  );
1512
1679
  const serverElement = await artifacts.loadServerElement(instance.serverElementPath);
1513
1680
  const asyncPromises = [];
1514
- const importMap = buildImportMap(manifest.sharedManifest, manifest.publicBasePath);
1515
- const modulePreloads = Object.values(importMap).map((url2) => ` <link rel="modulepreload" href="${url2}" />`).join("\n");
1516
- const cssLink = instance.clientCssPath ? ` <link rel="stylesheet" href="${manifest.publicBasePath}${instance.clientCssPath}" />` : "";
1517
- res.writeHead(200, {
1518
- "Content-Type": "text/html; charset=utf-8",
1519
- "Transfer-Encoding": "chunked"
1520
- });
1521
- const headParts = [headTagsHtml, modulePreloads, cssLink].filter(Boolean).join("\n");
1522
- res.write(`<!doctype html>
1681
+ const importMap = buildImportMap(manifest.sharedManifest, staticBaseUrl);
1682
+ const modulePreloads = Object.values(importMap).map((url) => ` <link rel="modulepreload" href="${url}" />`).join("\n");
1683
+ const cssLink = instance.clientCssPath ? ` <link rel="stylesheet" href="${staticBaseUrl}${instance.clientCssPath}" />` : "";
1684
+ const encoder = new TextEncoder();
1685
+ const stream = new ReadableStream({
1686
+ async start(controller) {
1687
+ const write = (s) => controller.enqueue(encoder.encode(s));
1688
+ const headParts = [headTagsHtml, modulePreloads, cssLink].filter(Boolean).join("\n");
1689
+ write(`<!doctype html>
1523
1690
  <html lang="en">
1524
1691
  <head>
1525
1692
  <meta charset="UTF-8" />
@@ -1529,82 +1696,47 @@ ${headParts}
1529
1696
  </head>
1530
1697
  <body>
1531
1698
  <div id="target">`);
1532
- serverElement.renderToStream(fullViewState, {
1533
- write: (chunk) => res.write(chunk),
1534
- onAsync: (promise, id, templates) => {
1535
- asyncPromises.push(
1536
- promise.then(
1537
- (val) => asyncSwapScript(id, templates.resolved(val)),
1538
- (err) => asyncSwapScript(id, templates.rejected(err))
1539
- )
1540
- );
1541
- }
1542
- });
1543
- res.write("</div>");
1544
- const asyncScripts = (await Promise.all(asyncPromises)).filter((s) => s).join("");
1545
- if (asyncScripts)
1546
- res.write(asyncScripts);
1547
- const clientInitData = getClientInitData();
1548
- const clientBundleUrl = `${manifest.publicBasePath}${instance.clientBundlePath}`;
1549
- res.write(`
1699
+ serverElement.renderToStream(fullViewState, {
1700
+ write: (chunk) => write(chunk),
1701
+ onAsync: (promise, id, templates) => {
1702
+ asyncPromises.push(
1703
+ promise.then(
1704
+ (val) => asyncSwapScript(id, templates.resolved(val)),
1705
+ (err) => asyncSwapScript(id, templates.rejected(err))
1706
+ )
1707
+ );
1708
+ }
1709
+ });
1710
+ write("</div>");
1711
+ const asyncScripts = (await Promise.all(asyncPromises)).filter((s) => s).join("");
1712
+ if (asyncScripts)
1713
+ write(asyncScripts);
1714
+ const clientInitData = getClientInitData();
1715
+ const clientBundleUrl = `${staticBaseUrl}${instance.clientBundlePath}`;
1716
+ write(`
1550
1717
  <script type="module">
1551
1718
  import { init } from '${clientBundleUrl}';
1552
1719
  await init(${JSON.stringify(fastViewState)}, ${JSON.stringify(fastCarryForward)}, ${JSON.stringify(clientInitData)});
1553
1720
  <\/script>
1554
1721
  </body>
1555
1722
  </html>`);
1556
- res.end();
1557
- }
1558
- const MIME_TYPES = {
1559
- ".js": "application/javascript",
1560
- ".css": "text/css",
1561
- ".json": "application/json",
1562
- ".html": "text/html",
1563
- ".svg": "image/svg+xml",
1564
- ".png": "image/png",
1565
- ".jpg": "image/jpeg",
1566
- ".woff2": "font/woff2",
1567
- ".woff": "font/woff"
1568
- };
1569
- async function handleStaticRequest(req, res, basePath, urlPrefix) {
1570
- const url = new URL(req.url || "/", `http://${req.headers.host}`);
1571
- if (!url.pathname.startsWith(urlPrefix))
1572
- return false;
1573
- const relativePath = url.pathname.slice(urlPrefix.length);
1574
- const filePath = path.join(basePath, relativePath);
1575
- const normalizedBase = path.resolve(basePath);
1576
- const normalizedFile = path.resolve(filePath);
1577
- if (!normalizedFile.startsWith(normalizedBase)) {
1578
- res.writeHead(403);
1579
- res.end("Forbidden");
1580
- return true;
1581
- }
1582
- try {
1583
- const content = await fs.readFile(filePath);
1584
- const ext = path.extname(filePath);
1585
- const contentType = MIME_TYPES[ext] || "application/octet-stream";
1586
- const isHashed = /[-][a-zA-Z0-9_-]{6,}\./.test(path.basename(filePath));
1587
- const cacheControl = isHashed ? "public, max-age=31536000, immutable" : "public, max-age=3600";
1588
- res.writeHead(200, {
1589
- "Content-Type": contentType,
1590
- "Content-Length": content.length,
1591
- "Cache-Control": cacheControl
1592
- });
1593
- res.end(content);
1594
- return true;
1595
- } catch {
1596
- return false;
1597
- }
1723
+ controller.close();
1724
+ }
1725
+ });
1726
+ const responseHeaders = fastResult.responseHeaders || {};
1727
+ return new Response(stream, {
1728
+ headers: { "Content-Type": "text/html; charset=utf-8", ...responseHeaders }
1729
+ });
1598
1730
  }
1599
1731
  const ACTION_PREFIX = "/_jay/actions/";
1600
1732
  function isActionRequest(pathname) {
1601
1733
  return pathname.startsWith(ACTION_PREFIX);
1602
1734
  }
1603
- async function handleActionRequest(req, res, registry = actionRegistry) {
1604
- const url = new URL(req.url || "/", `http://${req.headers.host}`);
1735
+ async function fetchActionRequest(request, registry = actionRegistry) {
1736
+ const url = new URL(request.url);
1605
1737
  const actionName = url.pathname.slice(ACTION_PREFIX.length);
1606
1738
  if (!actionName) {
1607
- jsonResponse(res, 400, {
1739
+ return jsonResponse(400, {
1608
1740
  success: false,
1609
1741
  error: {
1610
1742
  code: "MISSING_ACTION_NAME",
@@ -1612,11 +1744,10 @@ async function handleActionRequest(req, res, registry = actionRegistry) {
1612
1744
  isActionError: false
1613
1745
  }
1614
1746
  });
1615
- return;
1616
1747
  }
1617
1748
  const action = registry.get(actionName);
1618
1749
  if (!action) {
1619
- jsonResponse(res, 404, {
1750
+ return jsonResponse(404, {
1620
1751
  success: false,
1621
1752
  error: {
1622
1753
  code: "ACTION_NOT_FOUND",
@@ -1624,11 +1755,10 @@ async function handleActionRequest(req, res, registry = actionRegistry) {
1624
1755
  isActionError: false
1625
1756
  }
1626
1757
  });
1627
- return;
1628
1758
  }
1629
- const requestMethod = (req.method || "GET").toUpperCase();
1759
+ const requestMethod = request.method.toUpperCase();
1630
1760
  if (requestMethod !== action.method) {
1631
- jsonResponse(res, 405, {
1761
+ return jsonResponse(405, {
1632
1762
  success: false,
1633
1763
  error: {
1634
1764
  code: "METHOD_NOT_ALLOWED",
@@ -1636,7 +1766,6 @@ async function handleActionRequest(req, res, registry = actionRegistry) {
1636
1766
  isActionError: false
1637
1767
  }
1638
1768
  });
1639
- return;
1640
1769
  }
1641
1770
  let input;
1642
1771
  try {
@@ -1649,10 +1778,11 @@ async function handleActionRequest(req, res, registry = actionRegistry) {
1649
1778
  delete input._input;
1650
1779
  }
1651
1780
  } else {
1652
- input = await parseBody(req);
1781
+ const text = await request.text();
1782
+ input = text ? JSON.parse(text) : {};
1653
1783
  }
1654
1784
  } catch {
1655
- jsonResponse(res, 400, {
1785
+ return jsonResponse(400, {
1656
1786
  success: false,
1657
1787
  error: {
1658
1788
  code: "INVALID_INPUT",
@@ -1660,39 +1790,50 @@ async function handleActionRequest(req, res, registry = actionRegistry) {
1660
1790
  isActionError: false
1661
1791
  }
1662
1792
  });
1663
- return;
1664
1793
  }
1665
1794
  if (registry.isStreaming(actionName)) {
1666
- res.writeHead(200, {
1667
- "Content-Type": "application/x-ndjson",
1668
- "Transfer-Encoding": "chunked"
1669
- });
1670
- try {
1671
- const generator = registry.executeStream(actionName, input);
1672
- for await (const chunk of generator) {
1673
- res.write(JSON.stringify({ chunk }) + "\n");
1795
+ const encoder = new TextEncoder();
1796
+ const stream = new ReadableStream({
1797
+ async start(controller) {
1798
+ try {
1799
+ const generator = registry.executeStream(actionName, input);
1800
+ for await (const chunk of generator) {
1801
+ controller.enqueue(encoder.encode(JSON.stringify({ chunk }) + "\n"));
1802
+ }
1803
+ controller.enqueue(encoder.encode(JSON.stringify({ done: true }) + "\n"));
1804
+ } catch (err) {
1805
+ controller.enqueue(
1806
+ encoder.encode(JSON.stringify({ error: err.message }) + "\n")
1807
+ );
1808
+ }
1809
+ controller.close();
1674
1810
  }
1675
- res.write(JSON.stringify({ done: true }) + "\n");
1676
- } catch (err) {
1677
- res.write(JSON.stringify({ error: err.message }) + "\n");
1678
- }
1679
- res.end();
1680
- return;
1811
+ });
1812
+ return new Response(stream, {
1813
+ headers: { "Content-Type": "application/x-ndjson" }
1814
+ });
1681
1815
  }
1682
1816
  const result = await registry.execute(actionName, input);
1683
1817
  if (result.success) {
1818
+ const headers = {};
1684
1819
  if (requestMethod === "GET") {
1685
1820
  const cacheHeaders = registry.getCacheHeaders(actionName);
1686
1821
  if (cacheHeaders) {
1687
- res.setHeader("Cache-Control", cacheHeaders);
1822
+ headers["Cache-Control"] = cacheHeaders;
1688
1823
  }
1689
1824
  }
1690
- jsonResponse(res, 200, { success: true, data: result.data });
1825
+ return jsonResponse(200, { success: true, data: result.data }, headers);
1691
1826
  } else {
1692
1827
  const statusCode = getStatusCode(result.error.code, result.error.isActionError);
1693
- jsonResponse(res, statusCode, { success: false, error: result.error });
1828
+ return jsonResponse(statusCode, { success: false, error: result.error });
1694
1829
  }
1695
1830
  }
1831
+ function jsonResponse(status, body, extraHeaders = {}) {
1832
+ return new Response(JSON.stringify(body), {
1833
+ status,
1834
+ headers: { "Content-Type": "application/json", ...extraHeaders }
1835
+ });
1836
+ }
1696
1837
  async function registerActionsFromManifest(actions, buildDir, registry = actionRegistry) {
1697
1838
  const logger = getLogger();
1698
1839
  let count = 0;
@@ -1717,33 +1858,6 @@ async function registerActionsFromManifest(actions, buildDir, registry = actionR
1717
1858
  }
1718
1859
  logger.info(`[Server] Registered ${count} actions`);
1719
1860
  }
1720
- function jsonResponse(res, status, body) {
1721
- const json = JSON.stringify(body);
1722
- res.writeHead(status, {
1723
- "Content-Type": "application/json",
1724
- "Content-Length": Buffer.byteLength(json)
1725
- });
1726
- res.end(json);
1727
- }
1728
- function parseBody(req) {
1729
- return new Promise((resolve, reject) => {
1730
- const chunks = [];
1731
- req.on("data", (chunk) => chunks.push(chunk));
1732
- req.on("end", () => {
1733
- const raw = Buffer.concat(chunks).toString("utf-8");
1734
- if (!raw) {
1735
- resolve({});
1736
- return;
1737
- }
1738
- try {
1739
- resolve(JSON.parse(raw));
1740
- } catch {
1741
- reject(new Error("Invalid JSON body"));
1742
- }
1743
- });
1744
- req.on("error", reject);
1745
- });
1746
- }
1747
1861
  function getStatusCode(code, isActionError) {
1748
1862
  if (isActionError)
1749
1863
  return 422;
@@ -1761,71 +1875,186 @@ function getStatusCode(code, isActionError) {
1761
1875
  return 500;
1762
1876
  }
1763
1877
  }
1764
- async function startMainServer(options) {
1878
+ const MIME_TYPES = {
1879
+ ".js": "application/javascript",
1880
+ ".css": "text/css",
1881
+ ".json": "application/json",
1882
+ ".html": "text/html",
1883
+ ".svg": "image/svg+xml",
1884
+ ".png": "image/png",
1885
+ ".jpg": "image/jpeg",
1886
+ ".jpeg": "image/jpeg",
1887
+ ".gif": "image/gif",
1888
+ ".ico": "image/x-icon",
1889
+ ".woff2": "font/woff2",
1890
+ ".woff": "font/woff",
1891
+ ".ttf": "font/ttf",
1892
+ ".webp": "image/webp"
1893
+ };
1894
+ async function fetchStaticFile(pathname, frontendDir) {
1895
+ const normalizedBase = path.resolve(frontendDir);
1896
+ for (const candidate of [
1897
+ path.join(frontendDir, pathname),
1898
+ path.join(frontendDir, "public", pathname)
1899
+ ]) {
1900
+ const normalizedFile = path.resolve(candidate);
1901
+ if (!normalizedFile.startsWith(normalizedBase))
1902
+ continue;
1903
+ try {
1904
+ const content = await fs.readFile(candidate);
1905
+ const ext = path.extname(candidate);
1906
+ const contentType = MIME_TYPES[ext] || "application/octet-stream";
1907
+ const isHashed = /[-][a-zA-Z0-9_-]{6,}\./.test(path.basename(candidate));
1908
+ const cacheControl = isHashed ? "public, max-age=31536000, immutable" : "public, max-age=3600";
1909
+ return new Response(content, {
1910
+ headers: {
1911
+ "Content-Type": contentType,
1912
+ "Content-Length": String(content.length),
1913
+ "Cache-Control": cacheControl
1914
+ }
1915
+ });
1916
+ } catch {
1917
+ }
1918
+ }
1919
+ return null;
1920
+ }
1921
+ async function initializeServices(buildDir, projectRoot, label) {
1765
1922
  const logger = getLogger();
1766
- const buildDir = path.join(options.buildRoot, `v${options.version}`);
1767
- const artifacts = new FilesystemArtifactStore(buildDir);
1768
- const manifest = await artifacts.readManifest();
1769
- logger.important(
1770
- `[Server] Loaded manifest: ${manifest.routes.length} routes, v${manifest.version}`
1771
- );
1772
1923
  const { discoverPluginsWithInit, sortPluginsByDependencies } = await import("@jay-framework/stack-server-runtime");
1773
1924
  try {
1774
1925
  const pluginsWithInit = sortPluginsByDependencies(
1775
- await discoverPluginsWithInit({ projectRoot: manifest.projectRoot })
1926
+ await discoverPluginsWithInit({ projectRoot })
1776
1927
  );
1777
1928
  for (const pluginInit of pluginsWithInit) {
1778
1929
  try {
1779
- const pluginModule = await import(pluginInit.packageName);
1930
+ let modulePath;
1931
+ if (pluginInit.isLocal) {
1932
+ const pluginDirName = path.basename(pluginInit.pluginPath);
1933
+ const initModule = pluginInit.initModule || "index";
1934
+ modulePath = path.join(
1935
+ buildDir,
1936
+ "server",
1937
+ "plugins",
1938
+ pluginDirName,
1939
+ initModule + ".js"
1940
+ );
1941
+ } else {
1942
+ modulePath = pluginInit.packageName;
1943
+ }
1944
+ const pluginModule = await import(modulePath);
1780
1945
  const init = pluginModule.init || pluginModule[pluginInit.initExport || "init"];
1781
1946
  if (init?._serverInit) {
1782
- logger.info(`[Server] Running plugin init: ${pluginInit.name}`);
1947
+ logger.info(`[${label}] Running plugin init: ${pluginInit.name}`);
1783
1948
  const data = await init._serverInit();
1784
1949
  if (data)
1785
1950
  setClientInitData(pluginInit.name, data);
1786
1951
  }
1787
1952
  } catch (err) {
1788
- logger.warn(`[Server] Plugin init failed: ${pluginInit.name}: ${err.message}`);
1953
+ logger.warn(`[${label}] Plugin init failed: ${pluginInit.name}: ${err.message}`);
1789
1954
  }
1790
1955
  }
1791
1956
  } catch {
1792
1957
  }
1793
- const initModule = await artifacts.loadPageModule("server/init.js").catch(() => null);
1794
- if (initModule) {
1958
+ const initModulePath = path.join(buildDir, "server", "init.js");
1959
+ try {
1960
+ const initModule = await import(initModulePath);
1795
1961
  const init = initModule.init || initModule.default;
1796
1962
  if (init?._serverInit) {
1797
- logger.important("[Server] Running server init...");
1963
+ logger.info(`[${label}] Running server init...`);
1798
1964
  const data = await init._serverInit();
1799
1965
  if (data)
1800
1966
  setClientInitData("project", data);
1801
1967
  }
1968
+ } catch {
1969
+ }
1970
+ }
1971
+ function toFetchRequest(req) {
1972
+ const url = new URL(req.url || "/", `http://${req.headers.host}`);
1973
+ const headers = new Headers();
1974
+ for (const [key, value] of Object.entries(req.headers)) {
1975
+ if (value)
1976
+ headers.set(key, Array.isArray(value) ? value.join(", ") : value);
1977
+ }
1978
+ const init = { method: req.method, headers };
1979
+ if (req.method !== "GET" && req.method !== "HEAD") {
1980
+ init.body = Readable.toWeb(req);
1981
+ init.duplex = "half";
1802
1982
  }
1983
+ return new Request(url, init);
1984
+ }
1985
+ async function pipeFetchResponse(response, res) {
1986
+ const headers = {};
1987
+ response.headers.forEach((value, key) => {
1988
+ headers[key] = value;
1989
+ });
1990
+ res.writeHead(response.status, headers);
1991
+ if (response.body) {
1992
+ const reader = response.body.getReader();
1993
+ try {
1994
+ while (true) {
1995
+ const { done, value } = await reader.read();
1996
+ if (done)
1997
+ break;
1998
+ res.write(value);
1999
+ }
2000
+ } finally {
2001
+ reader.releaseLock();
2002
+ }
2003
+ }
2004
+ res.end();
2005
+ }
2006
+ async function startMainServer(options) {
2007
+ const logger = getLogger();
2008
+ const buildDir = path.join(options.buildRoot, `v${options.version}`);
2009
+ const backendDir = path.join(buildDir, "backend");
2010
+ const frontendDir = path.join(buildDir, "frontend");
2011
+ const artifacts = new FilesystemArtifactStore(backendDir);
2012
+ const manifest = await artifacts.readManifest();
2013
+ logger.important(
2014
+ `[Server] Loaded manifest: ${manifest.routes.length} routes, v${manifest.version}`
2015
+ );
2016
+ await initializeServices(backendDir, process.cwd(), "Server");
1803
2017
  if (manifest.actions.length > 0) {
1804
- await registerActionsFromManifest(manifest.actions, buildDir);
2018
+ await registerActionsFromManifest(manifest.actions, backendDir);
1805
2019
  }
2020
+ const staticBaseUrl = options.publicBasePath ?? "/";
2021
+ const startTime = Date.now();
1806
2022
  const server = http.createServer(async (req, res) => {
1807
2023
  const url = new URL(req.url || "/", `http://${req.headers.host}`);
1808
2024
  try {
1809
- if (isActionRequest(url.pathname)) {
1810
- await handleActionRequest(req, res);
2025
+ if (options.testMode && url.pathname === "/_jay/health") {
2026
+ res.writeHead(200, { "Content-Type": "application/json" });
2027
+ res.end(
2028
+ JSON.stringify({
2029
+ status: "ready",
2030
+ port: options.port,
2031
+ uptime: (Date.now() - startTime) / 1e3
2032
+ })
2033
+ );
1811
2034
  return;
1812
2035
  }
1813
- const handled = await handleStaticRequest(
1814
- req,
1815
- res,
1816
- path.join(buildDir, "shared"),
1817
- "/shared/"
1818
- );
1819
- if (handled)
2036
+ if (options.testMode && url.pathname === "/_jay/shutdown" && req.method === "POST") {
2037
+ res.writeHead(200, { "Content-Type": "application/json" });
2038
+ res.end(JSON.stringify({ status: "shutting_down" }));
2039
+ setTimeout(() => {
2040
+ server.close();
2041
+ process.exit(0);
2042
+ }, 100);
1820
2043
  return;
1821
- const handledInstances = await handleStaticRequest(
1822
- req,
1823
- res,
1824
- path.join(buildDir, "pre-rendered"),
1825
- "/pre-rendered/"
1826
- );
1827
- if (handledInstances)
2044
+ }
2045
+ if (isActionRequest(url.pathname)) {
2046
+ const fetchReq = toFetchRequest(req);
2047
+ const response2 = await fetchActionRequest(fetchReq);
2048
+ await pipeFetchResponse(response2, res);
1828
2049
  return;
2050
+ }
2051
+ if (options.serveStatic !== false) {
2052
+ const staticResponse = await fetchStaticFile(url.pathname, frontendDir);
2053
+ if (staticResponse) {
2054
+ await pipeFetchResponse(staticResponse, res);
2055
+ return;
2056
+ }
2057
+ }
1829
2058
  const currentManifest = await artifacts.readManifest();
1830
2059
  const match = matchRequest(currentManifest, url.pathname);
1831
2060
  if (!match) {
@@ -1833,9 +2062,20 @@ async function startMainServer(options) {
1833
2062
  res.end("Not Found");
1834
2063
  return;
1835
2064
  }
1836
- await handlePageRequest(res, match, currentManifest, artifacts);
2065
+ const cookies = parseCookies(req.headers.cookie);
2066
+ const response = await fetchPageRequest(
2067
+ match,
2068
+ currentManifest,
2069
+ url,
2070
+ artifacts,
2071
+ staticBaseUrl,
2072
+ cookies
2073
+ );
2074
+ await pipeFetchResponse(response, res);
1837
2075
  } catch (err) {
1838
2076
  logger.error(`[Server] Error handling ${url.pathname}: ${err.message}`);
2077
+ if (err.stack)
2078
+ logger.error(err.stack);
1839
2079
  if (!res.headersSent) {
1840
2080
  res.writeHead(500);
1841
2081
  res.end("Internal Server Error");
@@ -1846,9 +2086,505 @@ async function startMainServer(options) {
1846
2086
  logger.important(
1847
2087
  `[Server] Production server listening on http://localhost:${options.port}`
1848
2088
  );
2089
+ if (options.testMode) {
2090
+ logger.important(`[Server] Test mode enabled`);
2091
+ logger.important(` Health: http://localhost:${options.port}/_jay/health`);
2092
+ logger.important(
2093
+ ` Shutdown: curl -X POST http://localhost:${options.port}/_jay/shutdown`
2094
+ );
2095
+ }
2096
+ });
2097
+ }
2098
+ async function discoverWebhooks(projectRoot, serverBuildDir) {
2099
+ const logger = getLogger();
2100
+ const webhooks = [];
2101
+ try {
2102
+ const plugins = await scanPlugins({ projectRoot });
2103
+ for (const [packageName, plugin] of plugins) {
2104
+ if (plugin.isLocal)
2105
+ continue;
2106
+ const declaredWebhooks = plugin.manifest.webhooks;
2107
+ if (!declaredWebhooks || declaredWebhooks.length === 0)
2108
+ continue;
2109
+ try {
2110
+ const pluginModule = await import(packageName);
2111
+ for (const entry of declaredWebhooks) {
2112
+ const exportName = typeof entry === "string" ? entry : entry.name;
2113
+ const value = pluginModule[exportName];
2114
+ if (isJayWebhook(value)) {
2115
+ webhooks.push({
2116
+ name: value.webhookName,
2117
+ webhook: value,
2118
+ source: packageName
2119
+ });
2120
+ logger.info(
2121
+ `[Renderer] Webhook "${value.webhookName}" from ${plugin.manifest.name}`
2122
+ );
2123
+ } else {
2124
+ logger.warn(
2125
+ `[Renderer] plugin.yaml declares webhook "${exportName}" but export is not a JayWebhook in ${packageName}`
2126
+ );
2127
+ }
2128
+ }
2129
+ } catch (err) {
2130
+ logger.warn(
2131
+ `[Renderer] Failed to load webhooks from ${packageName}: ${err.message}`
2132
+ );
2133
+ }
2134
+ }
2135
+ } catch {
2136
+ }
2137
+ try {
2138
+ const plugins = await scanPlugins({ projectRoot });
2139
+ for (const [, plugin] of plugins) {
2140
+ if (!plugin.isLocal)
2141
+ continue;
2142
+ const declaredWebhooks = plugin.manifest.webhooks;
2143
+ if (!declaredWebhooks || declaredWebhooks.length === 0)
2144
+ continue;
2145
+ const pluginDirName = path.basename(plugin.pluginPath);
2146
+ for (const entry of declaredWebhooks) {
2147
+ const exportName = typeof entry === "string" ? entry : entry.name;
2148
+ const modulePath = path.join(serverBuildDir, "plugins", pluginDirName, "index.js");
2149
+ try {
2150
+ const mod = await import(modulePath);
2151
+ const value = mod[exportName];
2152
+ if (isJayWebhook(value)) {
2153
+ webhooks.push({
2154
+ name: value.webhookName,
2155
+ webhook: value,
2156
+ source: `local:${plugin.manifest.name}`
2157
+ });
2158
+ logger.info(
2159
+ `[Renderer] Webhook "${value.webhookName}" from local plugin ${plugin.manifest.name}`
2160
+ );
2161
+ }
2162
+ } catch (err) {
2163
+ logger.warn(
2164
+ `[Renderer] Failed to load webhook "${exportName}" from local plugin ${plugin.manifest.name}: ${err.message}`
2165
+ );
2166
+ }
2167
+ }
2168
+ }
2169
+ } catch {
2170
+ }
2171
+ const webhooksDir = path.join(serverBuildDir, "webhooks");
2172
+ try {
2173
+ const files = await fs.readdir(webhooksDir);
2174
+ for (const file of files) {
2175
+ if (!file.endsWith(".js"))
2176
+ continue;
2177
+ try {
2178
+ const mod = await import(path.join(webhooksDir, file));
2179
+ for (const [, value] of Object.entries(mod)) {
2180
+ if (isJayWebhook(value)) {
2181
+ webhooks.push({
2182
+ name: value.webhookName,
2183
+ webhook: value,
2184
+ source: `project:${file}`
2185
+ });
2186
+ logger.info(`[Renderer] Webhook "${value.webhookName}" from project`);
2187
+ }
2188
+ }
2189
+ } catch (err) {
2190
+ logger.warn(`[Renderer] Failed to load webhook ${file}: ${err.message}`);
2191
+ }
2192
+ }
2193
+ } catch {
2194
+ }
2195
+ return webhooks;
2196
+ }
2197
+ function resolveContractToRoutes(manifest, contractName) {
2198
+ return manifest.routes.filter((r) => r.contracts && r.contracts.includes(contractName));
2199
+ }
2200
+ async function rebuild(options) {
2201
+ const logger = getLogger();
2202
+ const buildDir = path.join(options.buildRoot, `v${options.version}`);
2203
+ const manifestPath = path.join(buildDir, "backend", "route-manifest.json");
2204
+ const manifest = JSON.parse(await fs.readFile(manifestPath, "utf-8"));
2205
+ const {
2206
+ routes: affectedRoutes,
2207
+ params: targetParams,
2208
+ label
2209
+ } = resolveTarget(manifest, options.target);
2210
+ logger.important(`[Rebuild] ${label} in v${options.version}`);
2211
+ if (affectedRoutes.length === 0) {
2212
+ logger.warn(`[Rebuild] No routes found for ${label}`);
2213
+ return { affected: 0, rebuilt: 0, errors: [] };
2214
+ }
2215
+ logger.important(
2216
+ `[Rebuild] Found ${affectedRoutes.length} route(s): ${affectedRoutes.map((r) => r.pattern).join(", ")}`
2217
+ );
2218
+ const backendDir = path.join(buildDir, "backend");
2219
+ const frontendDir = path.join(buildDir, "frontend");
2220
+ await initializeServices(backendDir, options.projectRoot, "Rebuild");
2221
+ const rebuildSuffix = Date.now().toString(36);
2222
+ const instanceCtx = {
2223
+ projectRoot: options.projectRoot,
2224
+ pagesRoot: options.pagesRoot,
2225
+ buildDir,
2226
+ backendDir,
2227
+ frontendDir,
2228
+ jayOptions: { tsConfigFilePath: options.tsConfigFilePath },
2229
+ tsConfigFilePath: options.tsConfigFilePath,
2230
+ minify: options.minify ?? true,
2231
+ rebuildSuffix
2232
+ };
2233
+ const result = { affected: 0, rebuilt: 0, errors: [] };
2234
+ const orphanedFiles = [];
2235
+ for (const route of affectedRoutes) {
2236
+ const instancesToRebuild = targetParams ? route.instances.filter((i) => paramsMatch(i.params, targetParams)) : [...route.instances];
2237
+ if (instancesToRebuild.length === 0 && targetParams) {
2238
+ instancesToRebuild.push({ params: targetParams });
2239
+ }
2240
+ for (const instance of instancesToRebuild) {
2241
+ result.affected++;
2242
+ const params = instance.params;
2243
+ const oldFiles = collectInstanceFiles(instance);
2244
+ let pageModule;
2245
+ try {
2246
+ pageModule = await loadRouteModule(route, buildDir);
2247
+ } catch (err) {
2248
+ result.errors.push({
2249
+ route: route.pattern,
2250
+ params,
2251
+ error: `Failed to load module: ${err.message}`
2252
+ });
2253
+ continue;
2254
+ }
2255
+ const jayRoute = await resolveJayRouteFromManifest(route, options);
2256
+ try {
2257
+ const buildResult = await buildInstance(jayRoute, params, pageModule, instanceCtx);
2258
+ if (buildResult.status !== "success") {
2259
+ result.errors.push({
2260
+ route: route.pattern,
2261
+ params,
2262
+ error: buildResult.reason
2263
+ });
2264
+ continue;
2265
+ }
2266
+ const existingIdx = route.instances.findIndex((i) => paramsMatch(i.params, params));
2267
+ if (existingIdx >= 0) {
2268
+ route.instances[existingIdx] = buildResult.instanceEntry;
2269
+ orphanedFiles.push(...oldFiles);
2270
+ } else {
2271
+ route.instances.push(buildResult.instanceEntry);
2272
+ }
2273
+ result.rebuilt++;
2274
+ logger.important(`[Rebuild] ${route.pattern} (${JSON.stringify(params)}): rebuilt`);
2275
+ } catch (err) {
2276
+ result.errors.push({
2277
+ route: route.pattern,
2278
+ params,
2279
+ error: err.message
2280
+ });
2281
+ }
2282
+ }
2283
+ }
2284
+ if (result.rebuilt > 0) {
2285
+ const tempPath = manifestPath + ".tmp";
2286
+ await fs.writeFile(tempPath, JSON.stringify(manifest, null, 2));
2287
+ await fs.rename(tempPath, manifestPath);
2288
+ const metadataPath = path.join(buildDir, "backend", "build-metadata.json");
2289
+ try {
2290
+ const metadata = JSON.parse(await fs.readFile(metadataPath, "utf-8"));
2291
+ metadata.buildTimestamp = (/* @__PURE__ */ new Date()).toISOString();
2292
+ metadata.instanceCount = manifest.routes.reduce((n, r) => n + r.instances.length, 0);
2293
+ await fs.writeFile(metadataPath, JSON.stringify(metadata, null, 2));
2294
+ } catch {
2295
+ await fs.writeFile(
2296
+ metadataPath,
2297
+ JSON.stringify(
2298
+ {
2299
+ version: options.version,
2300
+ sourceHash: "",
2301
+ buildTimestamp: (/* @__PURE__ */ new Date()).toISOString(),
2302
+ nodeVersion: process.version,
2303
+ instanceCount: manifest.routes.reduce((n, r) => n + r.instances.length, 0)
2304
+ },
2305
+ null,
2306
+ 2
2307
+ )
2308
+ );
2309
+ }
2310
+ logger.important(`[Rebuild] Manifest and metadata updated`);
2311
+ if (orphanedFiles.length > 0) {
2312
+ await appendCleanupManifest(buildDir, orphanedFiles);
2313
+ logger.info(`[Rebuild] ${orphanedFiles.length} orphaned file(s) queued for cleanup`);
2314
+ }
2315
+ }
2316
+ logger.important(
2317
+ `[Rebuild] Done: ${result.affected} affected, ${result.rebuilt} rebuilt, ${result.errors.length} errors`
2318
+ );
2319
+ return result;
2320
+ }
2321
+ function resolveTarget(manifest, target) {
2322
+ switch (target.mode) {
2323
+ case "contract": {
2324
+ const routes = resolveContractToRoutes(manifest, target.contractName);
2325
+ return {
2326
+ routes,
2327
+ params: target.params,
2328
+ label: `contract "${target.contractName}"${target.params ? ` (${JSON.stringify(target.params)})` : ""}`
2329
+ };
2330
+ }
2331
+ case "route": {
2332
+ const route = manifest.routes.find((r) => r.pattern === target.routePattern);
2333
+ return {
2334
+ routes: route ? [route] : [],
2335
+ params: target.params,
2336
+ label: `route "${target.routePattern}"${target.params ? ` (${JSON.stringify(target.params)})` : ""}`
2337
+ };
2338
+ }
2339
+ case "url": {
2340
+ const match = matchRequest(manifest, target.url);
2341
+ if (!match) {
2342
+ return { routes: [], label: `url "${target.url}"` };
2343
+ }
2344
+ return {
2345
+ routes: [match.route],
2346
+ params: match.params,
2347
+ label: `url "${target.url}" → ${match.route.pattern} (${JSON.stringify(match.params)})`
2348
+ };
2349
+ }
2350
+ }
2351
+ }
2352
+ async function rebuildContract(options) {
2353
+ return rebuild({
2354
+ ...options,
2355
+ target: { mode: "contract", contractName: options.contractName, params: options.params }
2356
+ });
2357
+ }
2358
+ async function loadRouteModule(route, buildDir) {
2359
+ if (!route.serverModule)
2360
+ return {};
2361
+ if (route.isPlugin)
2362
+ return import(route.serverModule);
2363
+ return import(path.join(buildDir, route.serverModule));
2364
+ }
2365
+ async function resolveJayRouteFromManifest(route, options) {
2366
+ const routeDir = route.pattern.replace(/^\//, "") || "index";
2367
+ const jayHtmlPath = path.join(options.pagesRoot, routeDir, "page.jay-html");
2368
+ let resolvedJayHtmlPath = jayHtmlPath;
2369
+ if (route.isPlugin && route.serverModule) {
2370
+ try {
2371
+ const pluginModule = await import(route.serverModule);
2372
+ const comp = pluginModule[route.componentExport || "page"];
2373
+ if (comp?.jayHtmlPath) {
2374
+ resolvedJayHtmlPath = comp.jayHtmlPath;
2375
+ }
2376
+ } catch {
2377
+ }
2378
+ }
2379
+ return {
2380
+ rawRoute: route.pattern,
2381
+ segments: route.segments.map((s) => {
2382
+ if (s.type === "static")
2383
+ return s.value;
2384
+ return { name: s.value, type: segmentTypeMap[s.type] };
2385
+ }),
2386
+ jayHtmlPath: resolvedJayHtmlPath,
2387
+ compPath: route.isPlugin ? route.serverModule : void 0,
2388
+ componentExport: route.componentExport
2389
+ };
2390
+ }
2391
+ const segmentTypeMap = {
2392
+ param: 0,
2393
+ catchAll: 1,
2394
+ optional: 2
2395
+ };
2396
+ function paramsMatch(instanceParams, targetParams) {
2397
+ return Object.entries(targetParams).every(([key, value]) => instanceParams[key] === value);
2398
+ }
2399
+ function collectInstanceFiles(instance) {
2400
+ if (!instance.preRenderedPath)
2401
+ return [];
2402
+ const files = [
2403
+ instance.preRenderedPath,
2404
+ instance.preRenderedPath.replace(".jay-html", ".cache.json"),
2405
+ instance.serverElementPath,
2406
+ instance.clientBundlePath
2407
+ ];
2408
+ if (instance.clientCssPath)
2409
+ files.push(instance.clientCssPath);
2410
+ return files.filter(Boolean);
2411
+ }
2412
+ async function appendCleanupManifest(buildDir, files) {
2413
+ const cleanupPath = path.join(buildDir, "cleanup-manifest.json");
2414
+ let existing = [];
2415
+ try {
2416
+ existing = JSON.parse(await fs.readFile(cleanupPath, "utf-8"));
2417
+ } catch {
2418
+ }
2419
+ existing.push(...files);
2420
+ await fs.writeFile(cleanupPath, JSON.stringify(existing, null, 2));
2421
+ }
2422
+ async function cleanupOrphanedFiles(buildRoot, version) {
2423
+ const logger = getLogger();
2424
+ const buildDir = path.join(buildRoot, `v${version}`);
2425
+ const cleanupPath = path.join(buildDir, "cleanup-manifest.json");
2426
+ let files;
2427
+ try {
2428
+ files = JSON.parse(await fs.readFile(cleanupPath, "utf-8"));
2429
+ } catch {
2430
+ logger.info("[Cleanup] No cleanup manifest found");
2431
+ return 0;
2432
+ }
2433
+ let deleted = 0;
2434
+ for (const file of files) {
2435
+ try {
2436
+ await fs.unlink(path.join(buildDir, file));
2437
+ deleted++;
2438
+ } catch {
2439
+ }
2440
+ }
2441
+ await fs.unlink(cleanupPath);
2442
+ logger.important(`[Cleanup] Deleted ${deleted}/${files.length} orphaned files`);
2443
+ return deleted;
2444
+ }
2445
+ async function startRendererServer(options) {
2446
+ const logger = getLogger();
2447
+ const buildDir = path.join(options.buildRoot, `v${options.version}`);
2448
+ const serverBuildDir = path.join(buildDir, "server");
2449
+ logger.important(`[Renderer] Starting renderer server v${options.version}`);
2450
+ await initializeServices(buildDir, options.projectRoot, "Renderer");
2451
+ const webhooks = await discoverWebhooks(options.projectRoot, serverBuildDir);
2452
+ const webhookMap = /* @__PURE__ */ new Map();
2453
+ for (const wh of webhooks) {
2454
+ webhookMap.set(wh.name, wh);
2455
+ }
2456
+ logger.important(
2457
+ `[Renderer] ${webhookMap.size} webhook(s) registered: ${[...webhookMap.keys()].join(", ") || "none"}`
2458
+ );
2459
+ const createInvalidateForWebhook = () => {
2460
+ return async (contractName, params) => {
2461
+ await rebuildContract({
2462
+ projectRoot: options.projectRoot,
2463
+ pagesRoot: options.pagesRoot,
2464
+ buildRoot: options.buildRoot,
2465
+ version: options.version,
2466
+ contractName,
2467
+ params,
2468
+ tsConfigFilePath: options.tsConfigFilePath,
2469
+ minify: options.minify
2470
+ });
2471
+ };
2472
+ };
2473
+ const startTime = Date.now();
2474
+ let lastWebhook;
2475
+ const server = http.createServer(async (req, res) => {
2476
+ const url = new URL(req.url || "/", `http://${req.headers.host}`);
2477
+ try {
2478
+ const webhookMatch = url.pathname.match(/^\/_jay\/webhooks\/(.+)$/);
2479
+ if (webhookMatch && req.method === "POST") {
2480
+ const webhookName = webhookMatch[1];
2481
+ const discovered = webhookMap.get(webhookName);
2482
+ if (!discovered) {
2483
+ res.writeHead(404, { "Content-Type": "application/json" });
2484
+ res.end(JSON.stringify({ error: `Unknown webhook: ${webhookName}` }));
2485
+ return;
2486
+ }
2487
+ const body = await readBody(req);
2488
+ const event = {
2489
+ type: webhookName,
2490
+ payload: JSON.parse(body || "{}"),
2491
+ headers: req.headers
2492
+ };
2493
+ const invalidate = createInvalidateForWebhook();
2494
+ const resolver = globalThis.__JAY_SERVICE_RESOLVER__;
2495
+ const services = resolver ? resolver(discovered.webhook.services) : [];
2496
+ await discovered.webhook.handler(event, invalidate, ...services);
2497
+ lastWebhook = { name: webhookName, timestamp: (/* @__PURE__ */ new Date()).toISOString() };
2498
+ res.writeHead(200, { "Content-Type": "application/json" });
2499
+ res.end(JSON.stringify({ ok: true }));
2500
+ return;
2501
+ }
2502
+ if (url.pathname === "/_jay/rebuild" && req.method === "POST") {
2503
+ const body = JSON.parse(await readBody(req) || "{}");
2504
+ const { contract, route, url: rebuildUrl, params } = body;
2505
+ let target;
2506
+ if (contract) {
2507
+ target = { mode: "contract", contractName: contract, params };
2508
+ } else if (route) {
2509
+ target = { mode: "route", routePattern: route, params };
2510
+ } else if (rebuildUrl) {
2511
+ target = { mode: "url", url: rebuildUrl };
2512
+ } else {
2513
+ res.writeHead(400, { "Content-Type": "application/json" });
2514
+ res.end(
2515
+ JSON.stringify({
2516
+ error: 'One of "contract", "route", or "url" is required'
2517
+ })
2518
+ );
2519
+ return;
2520
+ }
2521
+ const result = await rebuild({
2522
+ projectRoot: options.projectRoot,
2523
+ pagesRoot: options.pagesRoot,
2524
+ buildRoot: options.buildRoot,
2525
+ version: options.version,
2526
+ target,
2527
+ tsConfigFilePath: options.tsConfigFilePath,
2528
+ minify: options.minify
2529
+ });
2530
+ res.writeHead(200, { "Content-Type": "application/json" });
2531
+ res.end(JSON.stringify(result));
2532
+ return;
2533
+ }
2534
+ if (url.pathname === "/_jay/status" && req.method === "GET") {
2535
+ const manifest = JSON.parse(
2536
+ await fs.readFile(path.join(buildDir, "route-manifest.json"), "utf-8")
2537
+ );
2538
+ const status = {
2539
+ version: options.version,
2540
+ buildTimestamp: manifest.buildTimestamp,
2541
+ instanceCount: manifest.routes.reduce((n, r) => n + r.instances.length, 0),
2542
+ uptime: Math.floor((Date.now() - startTime) / 1e3),
2543
+ webhooks: [...webhookMap.keys()],
2544
+ lastWebhook
2545
+ };
2546
+ res.writeHead(200, { "Content-Type": "application/json" });
2547
+ res.end(JSON.stringify(status, null, 2));
2548
+ return;
2549
+ }
2550
+ res.writeHead(404);
2551
+ res.end("Not Found");
2552
+ } catch (err) {
2553
+ logger.error(`[Renderer] Error: ${err.message}`);
2554
+ if (!res.headersSent) {
2555
+ res.writeHead(500, { "Content-Type": "application/json" });
2556
+ res.end(JSON.stringify({ error: err.message }));
2557
+ }
2558
+ }
2559
+ });
2560
+ server.listen(options.port, () => {
2561
+ logger.important(
2562
+ `[Renderer] Renderer server listening on http://localhost:${options.port}`
2563
+ );
2564
+ });
2565
+ }
2566
+ function readBody(req) {
2567
+ return new Promise((resolve, reject) => {
2568
+ const chunks = [];
2569
+ req.on("data", (chunk) => chunks.push(chunk));
2570
+ req.on("end", () => resolve(Buffer.concat(chunks).toString()));
2571
+ req.on("error", reject);
1849
2572
  });
1850
2573
  }
1851
2574
  export {
2575
+ FilesystemArtifactStore,
1852
2576
  buildVersion,
1853
- startMainServer
2577
+ cleanupOrphanedFiles,
2578
+ fetchActionRequest,
2579
+ fetchPageRequest,
2580
+ fetchStaticFile,
2581
+ initializeServices,
2582
+ isActionRequest,
2583
+ matchRequest,
2584
+ rebuild,
2585
+ rebuildContract,
2586
+ registerActionsFromManifest,
2587
+ resolveContractToRoutes,
2588
+ startMainServer,
2589
+ startRendererServer
1854
2590
  };