@jay-framework/production-server 0.17.3 → 0.18.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.
package/dist/index.js CHANGED
@@ -1,9 +1,3 @@
1
- var __defProp = Object.defineProperty;
2
- var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
3
- var __publicField = (obj, key, value) => {
4
- __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
5
- return value;
6
- };
7
1
  import { build } from "vite";
8
2
  import { jayStackCompiler, extractActionsFromSource } from "@jay-framework/compiler-jay-stack";
9
3
  import { scanRoutes, JayRouteParamType, parseRouteSegments } from "@jay-framework/stack-route-scanner";
@@ -11,16 +5,20 @@ import { getLogger } from "@jay-framework/logger";
11
5
  import path from "node:path";
12
6
  import fs from "node:fs/promises";
13
7
  import { createRequire } from "node:module";
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";
16
- import { checkValidationErrors } from "@jay-framework/compiler-shared";
17
- import { jayRuntime } from "@jay-framework/vite-plugin";
8
+ import { DevSlowlyChangingPhase, slowRenderInstances, scanPlugins, runLoadParams, parseCookies } from "@jay-framework/stack-server-runtime";
9
+ import { l as loadProductionPageParts, g as buildPagePartsConfig, d as initializeServices, r as registerActionsFromManifest, i as isActionRequest, a as fetchActionRequest, c as fetchStaticFile, m as matchRequest, f as fetchPageRequest, F as FilesystemArtifactStore } from "./init-services-BKVwxzYb.js";
10
+ import { e, b } from "./init-services-BKVwxzYb.js";
18
11
  import crypto from "node:crypto";
19
12
  import fs$1 from "node:fs";
13
+ import { jayRuntime } from "@jay-framework/vite-plugin";
14
+ import { injectHeadfullFSTemplates, JAY_IMPORT_RESOLVER, parseJayFile, generateElementHydrateFile, generateServerElementFile } from "@jay-framework/compiler-jay-html";
15
+ import { checkValidationErrors, RuntimeMode } from "@jay-framework/compiler-shared";
16
+ import { parse } from "node-html-parser";
20
17
  import http from "node:http";
21
- import { deepMergeViewStates } from "@jay-framework/view-state-merge";
22
- import { asyncSwapScript } from "@jay-framework/ssr-runtime";
23
- import { isJayAction, isJayStreamAction } from "@jay-framework/fullstack-component";
18
+ import { Readable } from "node:stream";
19
+ import { isJayWebhook } from "@jay-framework/fullstack-component";
20
+ import "@jay-framework/view-state-merge";
21
+ import "@jay-framework/ssr-runtime";
24
22
  async function discoverServerEntries(projectRoot, pagesRoot) {
25
23
  const logger = getLogger();
26
24
  const routes = await scanRoutes(pagesRoot, {
@@ -57,7 +55,7 @@ async function discoverServerEntries(projectRoot, pagesRoot) {
57
55
  const dirPath = path.join(scanDir, dir.name);
58
56
  const files = await fs.readdir(dirPath);
59
57
  for (const file of files) {
60
- if (file.endsWith(".ts") && !file.endsWith(".d.ts") && file !== "init.ts" && file !== "page.ts") {
58
+ if (file.endsWith(".ts") && !file.endsWith(".d.ts") && file !== "page.ts") {
61
59
  const entryName = `${subDir}/${dir.name}/${file.replace(/\.ts$/, "")}`;
62
60
  pages[entryName] = path.join(dirPath, file);
63
61
  }
@@ -208,281 +206,7 @@ async function parseViteManifest(outputDir, packages) {
208
206
  await fs.rm(viteManifestPath, { force: true });
209
207
  return manifest;
210
208
  }
211
- const require2 = createRequire(import.meta.url);
212
- async function loadProductionPageParts(route, pageModule, jayHtmlContent, projectRoot, tsConfigFilePath, serverBuildDir) {
213
- const exportName = route.componentExport || "page";
214
- const compDefinition = pageModule[exportName] ?? pageModule.default;
215
- const parts = compDefinition ? [{ compDefinition, clientImport: "", clientPart: "" }] : [];
216
- const dirName = path.dirname(route.jayHtmlPath);
217
- const fileName = path.basename(route.jayHtmlPath);
218
- const jayHtmlWithValidations = await parseJayFile(
219
- jayHtmlContent,
220
- fileName,
221
- dirName,
222
- { relativePath: tsConfigFilePath },
223
- JAY_IMPORT_RESOLVER,
224
- projectRoot
225
- );
226
- const jayHtml = checkValidationErrors(jayHtmlWithValidations);
227
- const headlessInstanceComponents = [];
228
- const keyedPartModules = [];
229
- const headlessImports = jayHtml.headlessImports ?? [];
230
- getLogger().info(
231
- `[Build] headlessImports for ${fileName}: ${headlessImports.length}, keys: ${Object.keys(jayHtml).join(",")}`
232
- );
233
- for (const headlessImport of headlessImports) {
234
- const module = headlessImport.codeLink.module;
235
- const name = headlessImport.codeLink.names[0].name;
236
- const isLocalModule = module[0] === "." || module[0] === "/";
237
- let modulePath;
238
- if (isLocalModule) {
239
- const sourcePath = path.resolve(dirName, module);
240
- if (serverBuildDir) {
241
- const relativeToSrc = path.relative(path.join(projectRoot, "src"), sourcePath);
242
- let compiledPath = path.join(serverBuildDir, relativeToSrc);
243
- compiledPath = compiledPath.replace(/\.ts$/, ".js");
244
- if (!compiledPath.endsWith(".js")) {
245
- const indexPath = path.join(compiledPath, "index.js");
246
- try {
247
- await fs.access(indexPath);
248
- compiledPath = indexPath;
249
- } catch {
250
- compiledPath += ".js";
251
- }
252
- }
253
- modulePath = compiledPath;
254
- } else {
255
- modulePath = sourcePath;
256
- }
257
- } else {
258
- modulePath = require2.resolve(module, { paths: [dirName] });
259
- }
260
- const headlessModule = await import(modulePath);
261
- const headlessCompDef = headlessModule[name];
262
- if (headlessImport.key) {
263
- const clientModulePath = isLocalModule ? path.resolve(dirName, module) : `${module}/client`;
264
- parts.push({
265
- key: headlessImport.key,
266
- compDefinition: headlessCompDef,
267
- clientImport: "",
268
- clientPart: "",
269
- contractInfo: headlessImport.contract ? {
270
- contractName: headlessImport.contract.name,
271
- metadata: headlessImport.metadata
272
- } : void 0
273
- });
274
- keyedPartModules.push({
275
- key: headlessImport.key,
276
- modulePath: clientModulePath,
277
- exportName: name
278
- });
279
- }
280
- if (!headlessImport.key && headlessImport.contract) {
281
- headlessInstanceComponents.push({
282
- contractName: headlessImport.contractName,
283
- compDefinition: headlessCompDef,
284
- contract: headlessImport.contract
285
- });
286
- }
287
- }
288
- const headlessContracts = (jayHtml.headlessImports ?? []).filter((hi) => hi.contract && hi.key).map((hi) => ({
289
- key: hi.key,
290
- contract: hi.contract,
291
- contractPath: hi.contractPath
292
- }));
293
- const jayHtmlForDiscovery = injectHeadfullFSTemplates(
294
- jayHtmlContent,
295
- dirName,
296
- JAY_IMPORT_RESOLVER
297
- );
298
- let discoveredInstances = [];
299
- let forEachInstances = [];
300
- if (headlessInstanceComponents.length > 0) {
301
- const firstDiscovery = discoverHeadlessInstances(jayHtmlForDiscovery);
302
- const contractNames = new Set(headlessInstanceComponents.map((c) => c.contractName));
303
- const withCoords = assignCoordinatesToJayHtml(
304
- firstDiscovery.preRenderedJayHtml,
305
- contractNames
306
- );
307
- const finalDiscovery = discoverHeadlessInstances(withCoords);
308
- discoveredInstances = finalDiscovery.instances;
309
- forEachInstances = finalDiscovery.forEachInstances;
310
- }
311
- return {
312
- parts,
313
- headlessContracts,
314
- headlessInstanceComponents,
315
- discoveredInstances,
316
- forEachInstances,
317
- keyedPartModules,
318
- serverTrackByMap: jayHtml.serverTrackByMap,
319
- clientTrackByMap: jayHtml.clientTrackByMap
320
- };
321
- }
322
- async function compileServerElement(jayHtmlContent, jayHtmlFilename, jayHtmlDir, outputPath, projectRoot, tsConfigFilePath, sourceDir) {
323
- const jayFile = await parseJayFile(
324
- jayHtmlContent,
325
- jayHtmlFilename,
326
- jayHtmlDir,
327
- { relativePath: tsConfigFilePath },
328
- JAY_IMPORT_RESOLVER,
329
- projectRoot,
330
- sourceDir
331
- );
332
- const parsedJayFile = checkValidationErrors(jayFile);
333
- const serverElementCode = checkValidationErrors(generateServerElementFile(parsedJayFile));
334
- const outputDir = path.dirname(outputPath);
335
- await fs.mkdir(outputDir, { recursive: true });
336
- const tsPath = outputPath.replace(/\.js$/, ".ts");
337
- await fs.writeFile(tsPath, serverElementCode, "utf-8");
338
- const jayOptions = { tsConfigFilePath };
339
- await build({
340
- root: projectRoot,
341
- plugins: [jayRuntime(jayOptions)],
342
- build: {
343
- outDir: outputDir,
344
- emptyOutDir: false,
345
- minify: false,
346
- ssr: true,
347
- rollupOptions: {
348
- input: { [path.basename(outputPath, ".js")]: tsPath },
349
- external: [/^node:/, /^@jay-framework\//],
350
- output: {
351
- entryFileNames: "[name].js",
352
- format: "es"
353
- }
354
- }
355
- },
356
- logLevel: "warn"
357
- });
358
- await fs.rm(tsPath, { force: true });
359
- let cssFile;
360
- const css = parsedJayFile.css;
361
- if (css) {
362
- const cssFilename = path.basename(outputPath, ".server-element.js") + ".css";
363
- const cssPath = path.join(outputDir, cssFilename);
364
- await fs.writeFile(cssPath, css, "utf-8");
365
- cssFile = cssFilename;
366
- }
367
- getLogger().info(`[Build] Compiled server element: ${path.basename(outputPath)}`);
368
- return { cssFile };
369
- }
370
- async function generateHydrationEntry(options) {
371
- const {
372
- jayHtmlPath,
373
- pageModulePath,
374
- pageExportName = "page",
375
- slowViewState,
376
- trackByMap,
377
- outputPath,
378
- keyedParts = [],
379
- clientInits = []
380
- } = options;
381
- const hydrateImport = `${jayHtmlPath}?jay-hydrate`;
382
- const partImports = keyedParts.map((p, i) => `import { ${p.exportName} as keyedPart${i} } from '${p.modulePath}';`).join("\n");
383
- const hasPageModule = pageModulePath && pageExportName;
384
- const pagePartExpr = hasPageModule ? `pagePart && pagePart.comp ? { comp: pagePart.comp, contextMarkers: pagePart.contexts || [] } : null` : `null`;
385
- const partsArray = [
386
- pagePartExpr,
387
- ...keyedParts.map(
388
- (p, i) => `keyedPart${i} && keyedPart${i}.comp ? { comp: keyedPart${i}.comp, contextMarkers: keyedPart${i}.contexts || [], key: '${p.key}' } : null`
389
- )
390
- ];
391
- const pageImport = hasPageModule ? `import { ${pageExportName} as pagePart } from '${pageModulePath}';` : "";
392
- const initImports = clientInits.map((ci, i) => `import { ${ci.exportName} as clientInit${i} } from '${ci.modulePath}';`).join("\n");
393
- const initCalls = clientInits.map(
394
- (ci, i) => ` if (clientInit${i}?._clientInit) await clientInit${i}._clientInit(clientInitData['${ci.key}'] || {});`
395
- ).join("\n");
396
- const hasClientInit = clientInits.length > 0;
397
- const code = `import { hydrateCompositeJayComponent } from '@jay-framework/stack-client-runtime';
398
- import { deepMergeViewStates } from '@jay-framework/view-state-merge';
399
- import { hydrate } from '${hydrateImport}';
400
- ${pageImport}
401
- ${partImports}
402
- ${initImports}
403
-
404
- const slowViewState = ${JSON.stringify(slowViewState)};
405
- const trackByMap = ${JSON.stringify(trackByMap)};
406
-
407
- export async function init(fastViewState, fastCarryForward${hasClientInit ? ", clientInitData" : ""}) {
408
- ${initCalls}
409
- const viewState = deepMergeViewStates(slowViewState, fastViewState, trackByMap);
410
- const target = document.getElementById('target');
411
- const rootElement = target.firstElementChild;
412
- const parts = [
413
- ${partsArray.join(",\n ")}
414
- ].filter(p => p !== null);
415
- const pageComp = hydrateCompositeJayComponent(
416
- hydrate, viewState, fastCarryForward,
417
- parts, trackByMap, rootElement
418
- );
419
- return pageComp({});
420
- }
421
- `;
422
- const outputDir = path.dirname(outputPath);
423
- await fs.mkdir(outputDir, { recursive: true });
424
- await fs.writeFile(outputPath, code, "utf-8");
425
- getLogger().info(`[Build] Generated hydration entry: ${path.basename(outputPath)}`);
426
- }
427
- async function buildInstanceClient(hydrateEntryPath, instanceId, outputDir, projectRoot, jayOptions, minify = true, pagesRoot, buildDir) {
428
- const logger = getLogger();
429
- await fs.mkdir(outputDir, { recursive: true });
430
- const fullJayOptions = {
431
- ...jayOptions,
432
- ...pagesRoot && buildDir ? { pagesRoot, buildFolder: buildDir } : {}
433
- };
434
- await build({
435
- root: projectRoot,
436
- plugins: [...jayStackCompiler(fullJayOptions)],
437
- build: {
438
- outDir: outputDir,
439
- emptyOutDir: false,
440
- minify,
441
- manifest: `${instanceId}-manifest.json`,
442
- rollupOptions: {
443
- input: { [instanceId]: hydrateEntryPath },
444
- external: (id) => id.startsWith("@jay-framework/"),
445
- output: {
446
- entryFileNames: "[name]-[hash].js",
447
- chunkFileNames: "chunks/[name]-[hash].js",
448
- assetFileNames: "[name]-[hash].[ext]",
449
- format: "es"
450
- },
451
- preserveEntrySignatures: "exports-only"
452
- }
453
- },
454
- logLevel: "warn"
455
- });
456
- const manifestPath = path.join(outputDir, `${instanceId}-manifest.json`);
457
- const manifest = JSON.parse(await fs.readFile(manifestPath, "utf-8"));
458
- await fs.rm(manifestPath, { force: true });
459
- const entryKey = Object.keys(manifest).find((k) => manifest[k].isEntry);
460
- if (!entryKey) {
461
- throw new Error(`No entry found in instance build manifest for ${instanceId}`);
462
- }
463
- const entry = manifest[entryKey];
464
- const result = {
465
- jsFile: entry.file,
466
- cssFile: entry.css?.[0]
467
- };
468
- logger.info(`[Build] Client bundle: ${result.jsFile}`);
469
- return result;
470
- }
471
- function resolvePackageNameForRoute(compPath) {
472
- const dir = path.dirname(compPath);
473
- for (const candidate of [dir, path.join(dir, "..")]) {
474
- try {
475
- const pkgJson = JSON.parse(
476
- fs$1.readFileSync(path.join(candidate, "package.json"), "utf-8")
477
- );
478
- if (pkgJson.name)
479
- return pkgJson.name;
480
- } catch {
481
- }
482
- }
483
- return void 0;
484
- }
485
- function hashParams(params) {
209
+ function hashParams(params, suffix) {
486
210
  const sorted = Object.keys(params).sort().reduce(
487
211
  (acc, key) => {
488
212
  acc[key] = params[key];
@@ -491,20 +215,23 @@ function hashParams(params) {
491
215
  {}
492
216
  );
493
217
  const json = JSON.stringify(sorted);
494
- if (json === "{}")
218
+ if (json === "{}" && !suffix)
495
219
  return "";
496
- return "_" + crypto.createHash("md5").update(json).digest("hex").substring(0, 8);
220
+ const input = suffix ? json + ":" + suffix : json;
221
+ return "_" + crypto.createHash("md5").update(input).digest("hex").substring(0, 8);
497
222
  }
498
- async function buildInstance(route, params, pageModule, ctx) {
223
+ async function buildInstance(route, params, pageModule, ctx, routeServerElementPath, routeCssPath, routeHydratePath, routeClientBundlePath) {
499
224
  const logger = getLogger();
500
225
  const routeDir = route.rawRoute.replace(/^\//, "") || "index";
501
- const paramHash = hashParams(params);
226
+ const paramHash = hashParams(params, ctx.rebuildSuffix);
502
227
  const instanceId = `page${paramHash}`;
503
- const instanceDir = path.join(ctx.buildDir, "pre-rendered", routeDir);
504
- await fs.mkdir(instanceDir, { recursive: true });
228
+ const backendInstanceDir = path.join(ctx.backendDir, "pre-rendered", routeDir);
229
+ const frontendInstanceDir = path.join(ctx.frontendDir, "pages", routeDir);
230
+ await fs.mkdir(backendInstanceDir, { recursive: true });
231
+ await fs.mkdir(frontendInstanceDir, { recursive: true });
505
232
  const jayHtmlContent = await fs.readFile(route.jayHtmlPath, "utf-8");
506
- const sourceDir = path.dirname(route.jayHtmlPath);
507
- const serverBuildDir = path.join(ctx.buildDir, "server");
233
+ path.dirname(route.jayHtmlPath);
234
+ const serverBuildDir = path.join(ctx.backendDir, "server");
508
235
  const pageParts = await loadProductionPageParts(
509
236
  route,
510
237
  pageModule,
@@ -513,6 +240,38 @@ async function buildInstance(route, params, pageModule, ctx) {
513
240
  ctx.tsConfigFilePath,
514
241
  serverBuildDir
515
242
  );
243
+ const contracts = [
244
+ .../* @__PURE__ */ new Set([
245
+ ...pageParts.headlessInstanceComponents.map((c) => c.contractName),
246
+ ...pageParts.parts.filter((p) => p.contractInfo?.contractName).map((p) => p.contractInfo.contractName)
247
+ ])
248
+ ];
249
+ const pagePartsConfigPath = path.join(backendInstanceDir, "page-parts.json");
250
+ try {
251
+ await fs.access(pagePartsConfigPath);
252
+ } catch {
253
+ const exportName = route.componentExport || "page";
254
+ let pageServerModule = "";
255
+ let pageIsPlugin = false;
256
+ if (route.compPath) {
257
+ if (route.componentExport) {
258
+ pageServerModule = route.packageName || route.compPath;
259
+ pageIsPlugin = true;
260
+ } else {
261
+ const relativePath = path.relative(ctx.projectRoot, route.compPath);
262
+ pageServerModule = relativePath.replace(/^src\//, "server/").replace(/\.ts$/, ".js").replace(/\[/g, "_").replace(/\]/g, "_");
263
+ }
264
+ }
265
+ const config = buildPagePartsConfig(
266
+ pageParts,
267
+ pageServerModule,
268
+ exportName,
269
+ ctx.backendDir,
270
+ pageIsPlugin
271
+ );
272
+ await fs.writeFile(pagePartsConfigPath, JSON.stringify(config, null, 2));
273
+ logger.info(`[Build] Page parts config: ${routeDir}/page-parts.json`);
274
+ }
516
275
  const slowPhase = new DevSlowlyChangingPhase();
517
276
  const slowResult = await slowPhase.runSlowlyForPage(
518
277
  params,
@@ -535,163 +294,59 @@ async function buildInstance(route, params, pageModule, ctx) {
535
294
  }
536
295
  const slowViewState = slowResult.rendered;
537
296
  const carryForward = slowResult.carryForward;
538
- let contract;
539
- const contractPath = route.jayHtmlPath.replace(".jay-html", ".jay-contract");
540
- try {
541
- const contractContent = await fs.readFile(contractPath, "utf-8");
542
- const parseResult = parseContract(contractContent, path.basename(contractPath));
543
- if (parseResult.val)
544
- contract = parseResult.val;
545
- } catch {
546
- }
547
- const jayHtmlWithTemplates = injectHeadfullFSTemplates(
548
- jayHtmlContent,
549
- sourceDir,
550
- JAY_IMPORT_RESOLVER
551
- );
552
- const transformResult = slowRenderTransform({
553
- jayHtmlContent: jayHtmlWithTemplates,
554
- slowViewState,
555
- contract,
556
- headlessContracts: pageParts.headlessContracts,
557
- sourceDir,
558
- importResolver: JAY_IMPORT_RESOLVER
559
- });
560
- if (!transformResult.val) {
561
- throw new Error(
562
- `Slow render transform failed for ${route.rawRoute}: ${transformResult.validations.join(", ")}`
297
+ if (pageParts.discoveredInstances.length > 0 && pageParts.headlessInstanceComponents.length > 0) {
298
+ const slowResult2 = await slowRenderInstances(
299
+ pageParts.discoveredInstances,
300
+ pageParts.headlessInstanceComponents
563
301
  );
564
- }
565
- let preRenderedJayHtml = transformResult.val.preRenderedJayHtml;
566
- if (pageParts.headlessInstanceComponents.length > 0) {
567
- const discoveryResult = discoverHeadlessInstances(preRenderedJayHtml);
568
- const htmlWithRefs = discoveryResult.preRenderedJayHtml;
569
- const contractNames = new Set(
570
- pageParts.headlessInstanceComponents.map((c) => c.contractName)
571
- );
572
- preRenderedJayHtml = assignCoordinatesToJayHtml(htmlWithRefs, contractNames);
573
- const finalDiscovery = discoverHeadlessInstances(preRenderedJayHtml);
574
- if (finalDiscovery.instances.length > 0) {
575
- const slowResult2 = await slowRenderInstances(
576
- finalDiscovery.instances,
577
- pageParts.headlessInstanceComponents
578
- );
579
- if (slowResult2) {
580
- const existingInstances = carryForward.__instances || {
581
- discovered: [],
582
- carryForwards: {}
583
- };
584
- carryForward.__instances = {
585
- discovered: [
586
- ...existingInstances.discovered,
587
- ...slowResult2.instancePhaseData.discovered
588
- ],
589
- carryForwards: {
590
- ...existingInstances.carryForwards,
591
- ...slowResult2.instancePhaseData.carryForwards
592
- },
593
- slowViewStates: {
594
- ...existingInstances.slowViewStates || {},
595
- ...slowResult2.instancePhaseData.slowViewStates
596
- }
597
- };
598
- carryForward.__instanceSlowViewStates = {
599
- ...carryForward.__instanceSlowViewStates || {},
600
- ...Object.fromEntries(
601
- slowResult2.resolvedData.map((d) => [
602
- d.coordinate.join("/"),
603
- d.slowViewState
604
- ])
605
- )
606
- };
607
- carryForward.__instanceResolvedData = [
608
- ...carryForward.__instanceResolvedData || [],
609
- ...slowResult2.resolvedData
610
- ];
611
- const pass2Result = resolveHeadlessInstances(
612
- preRenderedJayHtml,
613
- slowResult2.resolvedData,
614
- JAY_IMPORT_RESOLVER
615
- );
616
- if (pass2Result.val) {
617
- preRenderedJayHtml = pass2Result.val;
302
+ if (slowResult2) {
303
+ const existingInstances = carryForward.__instances || {
304
+ discovered: [],
305
+ carryForwards: {}
306
+ };
307
+ carryForward.__instances = {
308
+ discovered: [
309
+ ...existingInstances.discovered,
310
+ ...slowResult2.instancePhaseData.discovered
311
+ ],
312
+ carryForwards: {
313
+ ...existingInstances.carryForwards,
314
+ ...slowResult2.instancePhaseData.carryForwards
315
+ },
316
+ slowViewStates: {
317
+ ...existingInstances.slowViewStates || {},
318
+ ...slowResult2.instancePhaseData.slowViewStates
618
319
  }
619
- }
320
+ };
620
321
  }
621
322
  }
622
- const preRenderedPath = path.join(instanceDir, `${instanceId}.jay-html`);
623
- await fs.writeFile(preRenderedPath, preRenderedJayHtml, "utf-8");
624
- const cacheMetadataPath = path.join(instanceDir, `${instanceId}.cache.json`);
323
+ if (pageParts.forEachInstances.length > 0) {
324
+ const existingInstances = carryForward.__instances || {
325
+ discovered: [],
326
+ carryForwards: {}
327
+ };
328
+ existingInstances.forEachInstances = pageParts.forEachInstances;
329
+ carryForward.__instances = existingInstances;
330
+ }
331
+ const cachePath = path.join(backendInstanceDir, `${instanceId}.cache.json`);
625
332
  await fs.writeFile(
626
- cacheMetadataPath,
333
+ cachePath,
627
334
  JSON.stringify({
628
335
  slowViewState,
629
- carryForward,
630
- sourcePath: route.jayHtmlPath
336
+ carryForward
631
337
  }),
632
338
  "utf-8"
633
339
  );
634
- logger.info(`[Build] Pre-rendered: ${routeDir}/${instanceId}`);
635
- const serverElementPath = path.join(instanceDir, `${instanceId}.server-element.js`);
636
- const serverElementResult = await compileServerElement(
637
- preRenderedJayHtml,
638
- `${instanceId}.jay-html`,
639
- instanceDir,
640
- serverElementPath,
641
- ctx.projectRoot,
642
- ctx.tsConfigFilePath,
643
- sourceDir
644
- );
645
- const hydrateEntryPath = path.join(instanceDir, `${instanceId}.hydrate-entry.ts`);
646
- const relativeJayHtmlPath = path.relative(instanceDir, preRenderedPath);
647
- let pageModulePath;
648
- let pageExportName;
649
- if (route.componentExport) {
650
- const pkgName = resolvePackageNameForRoute(route.compPath);
651
- pageModulePath = pkgName ? `${pkgName}/client` : "./" + path.relative(instanceDir, route.compPath);
652
- pageExportName = route.componentExport;
653
- } else if (route.compPath) {
654
- pageModulePath = "./" + path.relative(instanceDir, route.compPath);
655
- pageExportName = "page";
656
- } else {
657
- pageModulePath = "";
658
- pageExportName = "";
659
- }
660
- if (pageParts.keyedPartModules.length > 0) {
661
- logger.info(
662
- `[Build] Keyed parts for ${routeDir}: ${pageParts.keyedPartModules.map((p) => p.key).join(", ")}`
663
- );
664
- }
665
- await generateHydrationEntry({
666
- jayHtmlPath: "./" + relativeJayHtmlPath,
667
- pageModulePath,
668
- pageExportName,
669
- slowViewState,
670
- trackByMap: pageParts.clientTrackByMap || {},
671
- outputPath: hydrateEntryPath,
672
- keyedParts: pageParts.keyedPartModules,
673
- clientInits: ctx.clientInits
674
- });
675
- const clientResult = await buildInstanceClient(
676
- hydrateEntryPath,
677
- instanceId,
678
- instanceDir,
679
- ctx.projectRoot,
680
- ctx.jayOptions,
681
- ctx.minify ?? true,
682
- ctx.pagesRoot,
683
- ctx.buildDir
684
- );
685
- await fs.rm(hydrateEntryPath, { force: true });
686
- const cssFile = clientResult.cssFile || serverElementResult.cssFile;
340
+ logger.info(`[Build] Instance data: ${routeDir}/${instanceId}`);
341
+ const serverElementPath = routeServerElementPath ? path.join(ctx.backendDir, routeServerElementPath) : path.join(backendInstanceDir, `${instanceId}.server-element.js`);
687
342
  const instanceEntry = {
688
343
  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
344
+ cachePath: path.relative(ctx.backendDir, cachePath),
345
+ serverElementPath: path.relative(ctx.backendDir, serverElementPath),
346
+ clientBundlePath: routeClientBundlePath || "",
347
+ clientCssPath: routeCssPath
693
348
  };
694
- return { status: "success", instanceEntry, slowViewState, carryForward };
349
+ return { status: "success", instanceEntry, slowViewState, carryForward, contracts };
695
350
  }
696
351
  function convertSegments(segments) {
697
352
  return segments.map((s) => {
@@ -713,7 +368,6 @@ function buildRouteEntry(route, serverModulePath) {
713
368
  pattern: route.rawRoute,
714
369
  segments: convertSegments(route.segments),
715
370
  serverModule: serverModulePath,
716
- jayHtmlPath: route.jayHtmlPath,
717
371
  componentExport: route.componentExport,
718
372
  instances: []
719
373
  };
@@ -804,7 +458,8 @@ async function scanPluginRoutes(projectRoot, projectRoutes) {
804
458
  rawRoute: route.path,
805
459
  jayHtmlPath,
806
460
  compPath,
807
- componentExport
461
+ componentExport,
462
+ packageName: plugin.packageName
808
463
  });
809
464
  logger.info(`[Routes] Plugin "${plugin.manifest.name}" provides route ${route.path}`);
810
465
  }
@@ -852,44 +507,289 @@ function resolvePluginModule(pluginPath) {
852
507
  }
853
508
  return path.join(pluginPath, "dist", "index.js");
854
509
  }
855
- function crossProductParams(parts) {
856
- if (parts.length === 0)
857
- return [];
858
- if (parts.length === 1)
859
- return parts[0].values;
860
- const logger = getLogger();
861
- for (let i = 0; i < parts.length; i++) {
862
- for (let j = i + 1; j < parts.length; j++) {
863
- for (const key of parts[i].keys) {
864
- if (parts[j].keys.has(key)) {
865
- logger.warn(
866
- `[Build] Multiple loadParams provide key "${key}" — using first provider`
867
- );
868
- }
869
- }
870
- }
871
- }
872
- let result = parts[0].values;
873
- for (let i = 1; i < parts.length; i++) {
874
- const next = parts[i].values;
875
- const combined = [];
876
- for (const a of result) {
877
- for (const b of next) {
878
- combined.push({ ...a, ...b });
879
- }
880
- }
881
- result = combined;
882
- }
883
- return result;
884
- }
885
- function paramsMatchInferred(params, inferredParams, optionalSegments) {
886
- return Object.entries(inferredParams).every(([k, v]) => {
887
- if (optionalSegments?.has(k))
888
- return true;
889
- return params[k] === v;
890
- });
891
- }
892
- function computeSpecificity(route) {
510
+ async function compileServerElement(jayHtmlContent, jayHtmlFilename, jayHtmlDir, outputPath, projectRoot, tsConfigFilePath, sourceDir) {
511
+ const jayFile = await parseJayFile(
512
+ jayHtmlContent,
513
+ jayHtmlFilename,
514
+ jayHtmlDir,
515
+ { relativePath: tsConfigFilePath },
516
+ JAY_IMPORT_RESOLVER,
517
+ projectRoot,
518
+ sourceDir
519
+ );
520
+ const parsedJayFile = checkValidationErrors(jayFile);
521
+ const serverElementCode = checkValidationErrors(generateServerElementFile(parsedJayFile));
522
+ const outputDir = path.dirname(outputPath);
523
+ await fs.mkdir(outputDir, { recursive: true });
524
+ const tsPath = outputPath.replace(/\.js$/, ".ts");
525
+ await fs.writeFile(tsPath, serverElementCode, "utf-8");
526
+ const jayOptions = { tsConfigFilePath };
527
+ await build({
528
+ root: projectRoot,
529
+ plugins: [jayRuntime(jayOptions)],
530
+ build: {
531
+ outDir: outputDir,
532
+ emptyOutDir: false,
533
+ minify: false,
534
+ ssr: true,
535
+ rollupOptions: {
536
+ input: { [path.basename(outputPath, ".js")]: tsPath },
537
+ external: [/^node:/, /^@jay-framework\//],
538
+ output: {
539
+ entryFileNames: "[name].js",
540
+ format: "es"
541
+ }
542
+ }
543
+ },
544
+ logLevel: "warn"
545
+ });
546
+ await fs.rm(tsPath, { force: true });
547
+ let cssFile;
548
+ const css = parsedJayFile.css;
549
+ if (css) {
550
+ const cssFilename = path.basename(outputPath, ".server-element.js") + ".css";
551
+ const cssPath = path.join(outputDir, cssFilename);
552
+ await fs.writeFile(cssPath, css, "utf-8");
553
+ cssFile = cssFilename;
554
+ }
555
+ getLogger().info(`[Build] Compiled server element: ${path.basename(outputPath)}`);
556
+ return { cssFile };
557
+ }
558
+ async function compileRouteServerElement(jayHtmlPath, outputPath, projectRoot, tsConfigFilePath) {
559
+ const jayHtmlContent = await fs.readFile(jayHtmlPath, "utf-8");
560
+ const sourceDir = path.dirname(jayHtmlPath);
561
+ const outputDir = path.dirname(outputPath);
562
+ let jayHtml = injectHeadfullFSTemplates(jayHtmlContent, sourceDir, JAY_IMPORT_RESOLVER);
563
+ jayHtml = resolveJayHtmlPaths(jayHtml, sourceDir, outputDir);
564
+ return compileServerElement(
565
+ jayHtml,
566
+ path.basename(jayHtmlPath),
567
+ outputDir,
568
+ outputPath,
569
+ projectRoot,
570
+ tsConfigFilePath,
571
+ sourceDir
572
+ );
573
+ }
574
+ async function compileRouteHydrateScript(jayHtmlPath, outputDir, projectRoot, tsConfigFilePath, minify = true) {
575
+ const jayHtmlContent = await fs.readFile(jayHtmlPath, "utf-8");
576
+ const sourceDir = path.dirname(jayHtmlPath);
577
+ let jayHtml = injectHeadfullFSTemplates(jayHtmlContent, sourceDir, JAY_IMPORT_RESOLVER);
578
+ jayHtml = resolveJayHtmlPaths(jayHtml, sourceDir, outputDir);
579
+ const jayFile = await parseJayFile(
580
+ jayHtml,
581
+ path.basename(jayHtmlPath),
582
+ outputDir,
583
+ { relativePath: tsConfigFilePath },
584
+ JAY_IMPORT_RESOLVER,
585
+ projectRoot,
586
+ sourceDir
587
+ );
588
+ const parsedJayFile = checkValidationErrors(jayFile);
589
+ const hydrateCode = checkValidationErrors(
590
+ generateElementHydrateFile(parsedJayFile, RuntimeMode.MainTrusted)
591
+ );
592
+ await fs.mkdir(outputDir, { recursive: true });
593
+ const tsPath = path.join(outputDir, "route.hydrate.ts");
594
+ await fs.writeFile(tsPath, hydrateCode, "utf-8");
595
+ const jayOptions = { tsConfigFilePath };
596
+ await build({
597
+ root: projectRoot,
598
+ plugins: [...jayStackCompiler(jayOptions)],
599
+ build: {
600
+ outDir: outputDir,
601
+ emptyOutDir: false,
602
+ minify,
603
+ manifest: "route-hydrate-manifest.json",
604
+ rollupOptions: {
605
+ input: { "route.hydrate": tsPath },
606
+ external: (id) => id.startsWith("@jay-framework/"),
607
+ output: {
608
+ entryFileNames: "[name]-[hash].js",
609
+ format: "es"
610
+ },
611
+ preserveEntrySignatures: "exports-only"
612
+ }
613
+ },
614
+ logLevel: "warn"
615
+ });
616
+ await fs.rm(tsPath, { force: true });
617
+ const manifestPath = path.join(outputDir, "route-hydrate-manifest.json");
618
+ const manifest = JSON.parse(await fs.readFile(manifestPath, "utf-8"));
619
+ await fs.rm(manifestPath, { force: true });
620
+ const entryKey = Object.keys(manifest).find((k) => manifest[k].isEntry);
621
+ if (!entryKey)
622
+ throw new Error("No entry in route hydrate manifest");
623
+ const jsFile = manifest[entryKey].file;
624
+ getLogger().info(`[Build] Compiled route hydrate script: ${jsFile}`);
625
+ return { jsFile };
626
+ }
627
+ function resolveJayHtmlPaths(html, sourceDir, targetDir) {
628
+ const root = parse(html, {
629
+ comment: true,
630
+ blockTextElements: { script: true, style: true }
631
+ });
632
+ const rewrite = (el, attr) => {
633
+ const val = el.getAttribute(attr);
634
+ if (val && (val.startsWith("./") || val.startsWith("../"))) {
635
+ const abs = path.resolve(sourceDir, val);
636
+ let rel = path.relative(targetDir, abs);
637
+ if (!rel.startsWith("."))
638
+ rel = "./" + rel;
639
+ el.setAttribute(attr, rel);
640
+ }
641
+ };
642
+ for (const el of root.querySelectorAll('script[type="application/jay-data"]')) {
643
+ rewrite(el, "contract");
644
+ }
645
+ for (const el of root.querySelectorAll('script[type="application/jay-headless"]')) {
646
+ rewrite(el, "src");
647
+ rewrite(el, "contract");
648
+ }
649
+ for (const el of root.querySelectorAll('script[type="application/jay-headfull"]')) {
650
+ rewrite(el, "src");
651
+ rewrite(el, "contract");
652
+ }
653
+ for (const el of root.querySelectorAll('link[rel="stylesheet"]')) {
654
+ rewrite(el, "href");
655
+ }
656
+ return root.toString();
657
+ }
658
+ async function generateRouteHydrationEntry(options) {
659
+ const {
660
+ hydrateImport,
661
+ pageModulePath,
662
+ pageExportName = "page",
663
+ trackByMap,
664
+ outputPath,
665
+ keyedParts = [],
666
+ clientInits = []
667
+ } = options;
668
+ const partImports = keyedParts.map((p, i) => `import { ${p.exportName} as keyedPart${i} } from '${p.modulePath}';`).join("\n");
669
+ const hasPageModule = pageModulePath && pageExportName;
670
+ const pagePartExpr = hasPageModule ? `pagePart && pagePart.comp ? { comp: pagePart.comp, contextMarkers: pagePart.contexts || [] } : null` : `null`;
671
+ const partsArray = [
672
+ pagePartExpr,
673
+ ...keyedParts.map(
674
+ (p, i) => `keyedPart${i} && keyedPart${i}.comp ? { comp: keyedPart${i}.comp, contextMarkers: keyedPart${i}.contexts || [], key: '${p.key}' } : null`
675
+ )
676
+ ];
677
+ const pageImport = hasPageModule ? `import { ${pageExportName} as pagePart } from '${pageModulePath}';` : "";
678
+ const initImports = clientInits.map((ci, i) => `import { ${ci.exportName} as clientInit${i} } from '${ci.modulePath}';`).join("\n");
679
+ const initCalls = clientInits.map(
680
+ (ci, i) => ` if (clientInit${i}?._clientInit) await clientInit${i}._clientInit(clientInitData['${ci.key}'] || {});`
681
+ ).join("\n");
682
+ const code = `import { hydrateCompositeJayComponent } from '@jay-framework/stack-client-runtime';
683
+ import { deepMergeViewStates } from '@jay-framework/view-state-merge';
684
+ import { hydrate } from '${hydrateImport}';
685
+ ${pageImport}
686
+ ${partImports}
687
+ ${initImports}
688
+
689
+ const trackByMap = ${JSON.stringify(trackByMap)};
690
+
691
+ export async function init(slowViewState, fastViewState, fastCarryForward, clientInitData) {
692
+ ${initCalls}
693
+ const viewState = deepMergeViewStates(slowViewState, fastViewState, trackByMap);
694
+ const target = document.getElementById('target');
695
+ const rootElement = target.firstElementChild;
696
+ const parts = [
697
+ ${partsArray.join(",\n ")}
698
+ ].filter(p => p !== null);
699
+ const pageComp = hydrateCompositeJayComponent(
700
+ hydrate, viewState, fastCarryForward,
701
+ parts, trackByMap, rootElement
702
+ );
703
+ return pageComp({});
704
+ }
705
+ `;
706
+ const outputDir = path.dirname(outputPath);
707
+ await fs.mkdir(outputDir, { recursive: true });
708
+ await fs.writeFile(outputPath, code, "utf-8");
709
+ getLogger().info(`[Build] Generated route hydration entry: ${path.basename(outputPath)}`);
710
+ }
711
+ async function buildInstanceClient(hydrateEntryPath, instanceId, outputDir, projectRoot, jayOptions, minify = true, pagesRoot, buildDir) {
712
+ const logger = getLogger();
713
+ await fs.mkdir(outputDir, { recursive: true });
714
+ const fullJayOptions = {
715
+ ...jayOptions,
716
+ ...pagesRoot && buildDir ? { pagesRoot, buildFolder: buildDir } : {}
717
+ };
718
+ await build({
719
+ root: projectRoot,
720
+ plugins: [...jayStackCompiler(fullJayOptions)],
721
+ build: {
722
+ outDir: outputDir,
723
+ emptyOutDir: false,
724
+ minify,
725
+ manifest: `${instanceId}-manifest.json`,
726
+ rollupOptions: {
727
+ input: { [instanceId]: hydrateEntryPath },
728
+ external: (id) => id.startsWith("@jay-framework/") || id === "jay-route-hydrate",
729
+ output: {
730
+ entryFileNames: "[name]-[hash].js",
731
+ chunkFileNames: "chunks/[name]-[hash].js",
732
+ assetFileNames: "[name]-[hash].[ext]",
733
+ format: "es"
734
+ },
735
+ preserveEntrySignatures: "exports-only"
736
+ }
737
+ },
738
+ logLevel: "warn"
739
+ });
740
+ const manifestPath = path.join(outputDir, `${instanceId}-manifest.json`);
741
+ const manifest = JSON.parse(await fs.readFile(manifestPath, "utf-8"));
742
+ await fs.rm(manifestPath, { force: true });
743
+ const entryKey = Object.keys(manifest).find((k) => manifest[k].isEntry);
744
+ if (!entryKey) {
745
+ throw new Error(`No entry found in instance build manifest for ${instanceId}`);
746
+ }
747
+ const entry = manifest[entryKey];
748
+ const result = {
749
+ jsFile: entry.file,
750
+ cssFile: entry.css?.[0]
751
+ };
752
+ logger.info(`[Build] Client bundle: ${result.jsFile}`);
753
+ return result;
754
+ }
755
+ function crossProductParams(parts) {
756
+ if (parts.length === 0)
757
+ return [];
758
+ if (parts.length === 1)
759
+ return parts[0].values;
760
+ const logger = getLogger();
761
+ for (let i = 0; i < parts.length; i++) {
762
+ for (let j = i + 1; j < parts.length; j++) {
763
+ for (const key of parts[i].keys) {
764
+ if (parts[j].keys.has(key)) {
765
+ logger.warn(
766
+ `[Build] Multiple loadParams provide key "${key}" — using first provider`
767
+ );
768
+ }
769
+ }
770
+ }
771
+ }
772
+ let result = parts[0].values;
773
+ for (let i = 1; i < parts.length; i++) {
774
+ const next = parts[i].values;
775
+ const combined = [];
776
+ for (const a of result) {
777
+ for (const b2 of next) {
778
+ combined.push({ ...a, ...b2 });
779
+ }
780
+ }
781
+ result = combined;
782
+ }
783
+ return result;
784
+ }
785
+ function paramsMatchInferred(params, inferredParams, optionalSegments) {
786
+ return Object.entries(inferredParams).every(([k, v]) => {
787
+ if (optionalSegments?.has(k))
788
+ return true;
789
+ return params[k] === v;
790
+ });
791
+ }
792
+ function computeSpecificity(route) {
893
793
  const dynamicCount = (route.rawRoute.match(/\[/g) || []).length;
894
794
  const inferredCount = route.inferredParams ? Object.keys(route.inferredParams).length : 0;
895
795
  const unresolvedCount = Math.max(0, dynamicCount - inferredCount);
@@ -1006,11 +906,14 @@ async function discoverPluginClientPackages(projectRoot) {
1006
906
  async function buildVersion(options) {
1007
907
  const logger = getLogger();
1008
908
  const buildDir = path.join(options.buildRoot, `v${options.version}`);
909
+ const backendDir = path.join(buildDir, "backend");
910
+ const frontendDir = path.join(buildDir, "frontend");
1009
911
  logger.important(`[Build] Starting production build v${options.version}`);
1010
912
  logger.important(`[Build] Project: ${options.projectRoot}`);
1011
- await fs.mkdir(buildDir, { recursive: true });
913
+ await fs.mkdir(backendDir, { recursive: true });
914
+ await fs.mkdir(frontendDir, { recursive: true });
1012
915
  const { entries, routes } = await discoverServerEntries(options.projectRoot, options.pagesRoot);
1013
- const serverOutputDir = path.join(buildDir, "server");
916
+ const serverOutputDir = path.join(backendDir, "server");
1014
917
  await buildServerCode(
1015
918
  entries,
1016
919
  { tsConfigFilePath: options.tsConfigFilePath },
@@ -1021,7 +924,7 @@ async function buildVersion(options) {
1021
924
  if (pluginClientPackages.length > 0) {
1022
925
  logger.important(`[Build] Plugin client packages: ${pluginClientPackages.join(", ")}`);
1023
926
  }
1024
- const sharedOutputDir = path.join(buildDir, "shared");
927
+ const sharedOutputDir = path.join(frontendDir, "shared");
1025
928
  const { manifest: sharedManifest } = await buildSharedChunks(
1026
929
  sharedOutputDir,
1027
930
  options.projectRoot,
@@ -1031,7 +934,7 @@ async function buildVersion(options) {
1031
934
  const { actions, plugins } = await discoverActions(
1032
935
  entries.actions,
1033
936
  serverOutputDir,
1034
- buildDir,
937
+ backendDir,
1035
938
  options.projectRoot
1036
939
  );
1037
940
  const { discoverPluginsWithInit, sortPluginsByDependencies } = await import("@jay-framework/stack-server-runtime");
@@ -1106,6 +1009,8 @@ async function buildVersion(options) {
1106
1009
  projectRoot: options.projectRoot,
1107
1010
  pagesRoot: options.pagesRoot,
1108
1011
  buildDir,
1012
+ backendDir,
1013
+ frontendDir,
1109
1014
  jayOptions: { tsConfigFilePath: options.tsConfigFilePath },
1110
1015
  tsConfigFilePath: options.tsConfigFilePath,
1111
1016
  minify: options.minify ?? true,
@@ -1141,7 +1046,7 @@ async function buildVersion(options) {
1141
1046
  return {};
1142
1047
  if (entry.isPlugin)
1143
1048
  return import(entry.serverModule);
1144
- return import(path.join(serverOutputDir, entry.serverModule.replace("server/", "")));
1049
+ return import(path.join(backendDir, entry.serverModule));
1145
1050
  }
1146
1051
  const routeInfos = routeEntries.map((re) => {
1147
1052
  const optionalNames = re.route.segments.filter((s) => typeof s !== "string" && s.type === JayRouteParamType.optional).map((s) => s.name);
@@ -1172,7 +1077,7 @@ async function buildVersion(options) {
1172
1077
  await fs.readFile(route.jayHtmlPath, "utf-8"),
1173
1078
  options.projectRoot,
1174
1079
  options.tsConfigFilePath,
1175
- path.join(buildDir, "server")
1080
+ serverOutputDir
1176
1081
  );
1177
1082
  const partsWithLoadParams = pageParts.parts.filter((p) => p.compDefinition?.loadParams);
1178
1083
  if (partsWithLoadParams.length === 0)
@@ -1209,6 +1114,108 @@ async function buildVersion(options) {
1209
1114
  byRoute.set(info, []);
1210
1115
  byRoute.get(info).push(materialized2.params);
1211
1116
  }
1117
+ for (const [info] of byRoute) {
1118
+ const { route, entry } = info.routeEntry;
1119
+ if (!route.jayHtmlPath)
1120
+ continue;
1121
+ const routeDir = route.rawRoute.replace(/^\//, "") || "index";
1122
+ const frontendSafeRouteDir = routeDir.replace(/\[/g, "%5B").replace(/\]/g, "%5D");
1123
+ const backendRouteDir = path.join(backendDir, "pre-rendered", routeDir);
1124
+ const frontendRouteDir = path.join(frontendDir, "pages", frontendSafeRouteDir);
1125
+ await fs.mkdir(backendRouteDir, { recursive: true });
1126
+ await fs.mkdir(frontendRouteDir, { recursive: true });
1127
+ const serverElementPath = path.join(backendRouteDir, "route.server-element.js");
1128
+ try {
1129
+ const seResult = await compileRouteServerElement(
1130
+ route.jayHtmlPath,
1131
+ serverElementPath,
1132
+ options.projectRoot,
1133
+ options.tsConfigFilePath
1134
+ );
1135
+ entry.serverElementPath = path.relative(backendDir, serverElementPath);
1136
+ if (seResult.cssFile) {
1137
+ const srcCss = path.join(backendRouteDir, seResult.cssFile);
1138
+ const dstCss = path.join(frontendRouteDir, seResult.cssFile);
1139
+ try {
1140
+ await fs.rename(srcCss, dstCss);
1141
+ } catch {
1142
+ await fs.copyFile(srcCss, dstCss);
1143
+ await fs.rm(srcCss, { force: true });
1144
+ }
1145
+ entry.routeCssPath = path.relative(frontendDir, dstCss);
1146
+ }
1147
+ logger.important(`[Build] Route server element: ${routeDir}`);
1148
+ } catch (err) {
1149
+ logger.error(`[Build] Route server element FAILED ${route.rawRoute}: ${err.message}`);
1150
+ }
1151
+ try {
1152
+ const hydrateResult = await compileRouteHydrateScript(
1153
+ route.jayHtmlPath,
1154
+ frontendRouteDir,
1155
+ options.projectRoot,
1156
+ options.tsConfigFilePath,
1157
+ options.minify ?? true
1158
+ );
1159
+ entry.routeHydratePath = path.relative(
1160
+ frontendDir,
1161
+ path.join(frontendRouteDir, hydrateResult.jsFile)
1162
+ );
1163
+ logger.important(`[Build] Route hydrate script: ${routeDir}`);
1164
+ } catch (err) {
1165
+ logger.error(`[Build] Route hydrate script FAILED ${route.rawRoute}: ${err.message}`);
1166
+ continue;
1167
+ }
1168
+ try {
1169
+ const ROUTE_HYDRATE_KEY = "jay-route-hydrate";
1170
+ const exportName = route.componentExport || "page";
1171
+ let pageModulePath = "";
1172
+ if (route.compPath) {
1173
+ if (route.componentExport) {
1174
+ const pkgName = route.packageName || route.compPath;
1175
+ pageModulePath = `${pkgName}/client`;
1176
+ } else {
1177
+ pageModulePath = "./" + path.relative(frontendRouteDir, route.compPath);
1178
+ }
1179
+ }
1180
+ const pageParts = await loadProductionPageParts(
1181
+ route,
1182
+ {},
1183
+ await fs.readFile(route.jayHtmlPath, "utf-8"),
1184
+ options.projectRoot,
1185
+ options.tsConfigFilePath,
1186
+ path.join(backendDir, "server")
1187
+ );
1188
+ entry.trackByMap = pageParts.serverTrackByMap || pageParts.clientTrackByMap;
1189
+ const entryPath = path.join(frontendRouteDir, "route.entry.ts");
1190
+ await generateRouteHydrationEntry({
1191
+ hydrateImport: ROUTE_HYDRATE_KEY,
1192
+ pageModulePath,
1193
+ pageExportName: exportName,
1194
+ trackByMap: pageParts.clientTrackByMap || {},
1195
+ outputPath: entryPath,
1196
+ keyedParts: pageParts.keyedPartModules,
1197
+ clientInits
1198
+ });
1199
+ const clientResult = await buildInstanceClient(
1200
+ entryPath,
1201
+ "route.client",
1202
+ frontendRouteDir,
1203
+ options.projectRoot,
1204
+ { tsConfigFilePath: options.tsConfigFilePath },
1205
+ options.minify ?? true,
1206
+ options.pagesRoot,
1207
+ buildDir
1208
+ );
1209
+ await fs.rm(entryPath, { force: true });
1210
+ entry.routeClientBundlePath = path.relative(
1211
+ frontendDir,
1212
+ path.join(frontendRouteDir, clientResult.jsFile)
1213
+ );
1214
+ logger.important(`[Build] Route client bundle: ${routeDir}`);
1215
+ } catch (err) {
1216
+ logger.error(`[Build] Route client bundle FAILED ${route.rawRoute}: ${err.message}`);
1217
+ }
1218
+ }
1212
1219
  for (const [info, paramsList] of byRoute) {
1213
1220
  const { route, entry } = info.routeEntry;
1214
1221
  let pageModule;
@@ -1225,9 +1232,21 @@ async function buildVersion(options) {
1225
1232
  }
1226
1233
  for (const params of paramsList) {
1227
1234
  try {
1228
- const result = await buildInstance(route, params, pageModule, instanceCtx);
1235
+ const result = await buildInstance(
1236
+ route,
1237
+ params,
1238
+ pageModule,
1239
+ instanceCtx,
1240
+ entry.serverElementPath,
1241
+ entry.routeCssPath,
1242
+ entry.routeHydratePath,
1243
+ entry.routeClientBundlePath
1244
+ );
1229
1245
  if (result.status === "success") {
1230
1246
  entry.instances.push(result.instanceEntry);
1247
+ if (result.contracts.length > 0 && !entry.contracts) {
1248
+ entry.contracts = result.contracts;
1249
+ }
1231
1250
  logInstance(route.rawRoute || "/", params);
1232
1251
  } else {
1233
1252
  logger.warn(
@@ -1248,13 +1267,12 @@ async function buildVersion(options) {
1248
1267
  buildTimestamp: (/* @__PURE__ */ new Date()).toISOString(),
1249
1268
  sourceHash: "",
1250
1269
  projectRoot: options.projectRoot,
1251
- publicBasePath: options.publicBasePath,
1252
1270
  sharedManifest,
1253
1271
  routes: routeEntries.map((r) => r.entry),
1254
1272
  actions,
1255
1273
  plugins
1256
1274
  };
1257
- await writeRouteManifest(manifest, buildDir);
1275
+ await writeRouteManifest(manifest, backendDir);
1258
1276
  const metadata = {
1259
1277
  version: options.version,
1260
1278
  sourceHash: "",
@@ -1263,592 +1281,633 @@ async function buildVersion(options) {
1263
1281
  instanceCount
1264
1282
  };
1265
1283
  await fs.writeFile(
1266
- path.join(buildDir, "build-metadata.json"),
1284
+ path.join(backendDir, "build-metadata.json"),
1267
1285
  JSON.stringify(metadata, null, 2)
1268
1286
  );
1287
+ const publicFolder = path.join(options.projectRoot, "public");
1288
+ try {
1289
+ await fs.access(publicFolder);
1290
+ await fs.cp(publicFolder, path.join(frontendDir, "public"), { recursive: true });
1291
+ logger.info("[Build] Copied public/ to frontend/public/");
1292
+ } catch {
1293
+ }
1269
1294
  logger.important(`[Build] Done! ${instanceCount} instances built in ${buildDir}`);
1270
1295
  return manifest;
1271
1296
  }
1272
- const CACHE_TAG_START = '<script type="application/jay-cache">';
1273
- const CACHE_TAG_END = "<\/script>";
1274
- class FilesystemArtifactStore {
1275
- constructor(basePath) {
1276
- __publicField(this, "manifestCache");
1277
- __publicField(this, "moduleCache", /* @__PURE__ */ new Map());
1278
- this.basePath = basePath;
1279
- }
1280
- async readManifest() {
1281
- 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;
1285
- }
1286
- const manifest = JSON.parse(await fs.readFile(manifestPath, "utf-8"));
1287
- this.manifestCache = { manifest, mtime: stat.mtimeMs };
1288
- return manifest;
1289
- }
1290
- async readPreRenderedHtml(relativePath) {
1291
- const fullPath = path.join(this.basePath, relativePath);
1292
- const content = await fs.readFile(fullPath, "utf-8");
1293
- const cachePath = fullPath.replace(/\.jay-html$/, ".cache.json");
1294
- try {
1295
- const cacheData = JSON.parse(await fs.readFile(cachePath, "utf-8"));
1296
- return {
1297
- content,
1298
- slowViewState: cacheData.slowViewState || {},
1299
- carryForward: cacheData.carryForward || {}
1300
- };
1301
- } catch {
1302
- return extractCacheMetadata(content);
1303
- }
1304
- }
1305
- async loadServerElement(relativePath) {
1306
- return this.loadModule(relativePath);
1307
- }
1308
- async loadPageModule(relativePath) {
1309
- return this.loadModule(relativePath);
1310
- }
1311
- async readRawFile(relativePath) {
1312
- const fullPath = path.join(this.basePath, relativePath);
1313
- return await fs.readFile(fullPath, "utf-8");
1314
- }
1315
- getAssetPath(relativePath) {
1316
- return path.join(this.basePath, relativePath);
1297
+ function toFetchRequest(req) {
1298
+ const url = new URL(req.url || "/", `http://${req.headers.host}`);
1299
+ const headers = new Headers();
1300
+ for (const [key, value] of Object.entries(req.headers)) {
1301
+ if (value)
1302
+ headers.set(key, Array.isArray(value) ? value.join(", ") : value);
1317
1303
  }
1318
- async loadModule(relativePath) {
1319
- const fullPath = path.join(this.basePath, relativePath);
1320
- const stat = await fs.stat(fullPath);
1321
- const cached = this.moduleCache.get(relativePath);
1322
- if (cached && stat.mtimeMs === cached.mtime) {
1323
- return cached.module;
1324
- }
1325
- const mod = await import(fullPath + "?t=" + stat.mtimeMs);
1326
- this.moduleCache.set(relativePath, { module: mod, mtime: stat.mtimeMs });
1327
- return mod;
1304
+ const init = { method: req.method, headers };
1305
+ if (req.method !== "GET" && req.method !== "HEAD") {
1306
+ init.body = Readable.toWeb(req);
1307
+ init.duplex = "half";
1328
1308
  }
1309
+ return new Request(url, init);
1329
1310
  }
1330
- function extractCacheMetadata(fileContent) {
1331
- const startIdx = fileContent.indexOf(CACHE_TAG_START);
1332
- if (startIdx === -1) {
1333
- return { content: fileContent, slowViewState: {}, carryForward: {} };
1334
- }
1335
- const jsonStart = startIdx + CACHE_TAG_START.length;
1336
- const endIdx = fileContent.indexOf(CACHE_TAG_END, jsonStart);
1337
- if (endIdx === -1) {
1338
- return { content: fileContent, slowViewState: {}, carryForward: {} };
1339
- }
1340
- const jsonStr = fileContent.substring(jsonStart, endIdx);
1341
- const metadata = JSON.parse(jsonStr);
1342
- const tagEnd = endIdx + CACHE_TAG_END.length;
1343
- const afterTag = fileContent[tagEnd] === "\n" ? tagEnd + 1 : tagEnd;
1344
- const content = fileContent.substring(0, startIdx) + fileContent.substring(afterTag);
1345
- return {
1346
- content,
1347
- slowViewState: metadata.slowViewState || {},
1348
- carryForward: metadata.carryForward || {}
1349
- };
1350
- }
1351
- function matchRequest(manifest, pathname) {
1352
- const urlSegments = pathname.split("/").filter((s) => s.length > 0);
1353
- for (const route of manifest.routes) {
1354
- const params = matchSegments(route.segments, urlSegments);
1355
- if (params) {
1356
- const instance = findInstance(route, params);
1357
- if (instance) {
1358
- return { route, instance, params, pathname };
1311
+ async function pipeFetchResponse(response, res) {
1312
+ const headers = {};
1313
+ response.headers.forEach((value, key) => {
1314
+ headers[key] = value;
1315
+ });
1316
+ res.writeHead(response.status, headers);
1317
+ if (response.body) {
1318
+ const reader = response.body.getReader();
1319
+ try {
1320
+ while (true) {
1321
+ const { done, value } = await reader.read();
1322
+ if (done)
1323
+ break;
1324
+ res.write(value);
1359
1325
  }
1326
+ } finally {
1327
+ reader.releaseLock();
1360
1328
  }
1361
1329
  }
1362
- return void 0;
1330
+ res.end();
1363
1331
  }
1364
- function matchSegments(routeSegments, urlSegments) {
1365
- const params = {};
1366
- let urlIdx = 0;
1367
- for (let i = 0; i < routeSegments.length; i++) {
1368
- const seg = routeSegments[i];
1369
- if (seg.type === "static") {
1370
- if (urlIdx >= urlSegments.length || urlSegments[urlIdx] !== seg.value) {
1371
- return void 0;
1332
+ async function startMainServer(options) {
1333
+ const logger = getLogger();
1334
+ const buildDir = path.join(options.buildRoot, `v${options.version}`);
1335
+ const backendDir = path.join(buildDir, "backend");
1336
+ const frontendDir = path.join(buildDir, "frontend");
1337
+ const artifacts = new FilesystemArtifactStore(backendDir);
1338
+ const manifest = await artifacts.readManifest();
1339
+ logger.important(
1340
+ `[Server] Loaded manifest: ${manifest.routes.length} routes, v${manifest.version}`
1341
+ );
1342
+ await initializeServices(backendDir, process.cwd(), "Server");
1343
+ if (manifest.actions.length > 0) {
1344
+ await registerActionsFromManifest(manifest.actions, backendDir);
1345
+ }
1346
+ const staticBaseUrl = options.publicBasePath ?? "/";
1347
+ const startTime = Date.now();
1348
+ const server = http.createServer(async (req, res) => {
1349
+ const url = new URL(req.url || "/", `http://${req.headers.host}`);
1350
+ try {
1351
+ if (options.testMode && url.pathname === "/_jay/health") {
1352
+ res.writeHead(200, { "Content-Type": "application/json" });
1353
+ res.end(
1354
+ JSON.stringify({
1355
+ status: "ready",
1356
+ port: options.port,
1357
+ uptime: (Date.now() - startTime) / 1e3
1358
+ })
1359
+ );
1360
+ return;
1372
1361
  }
1373
- urlIdx++;
1374
- } else if (seg.type === "param") {
1375
- if (urlIdx >= urlSegments.length)
1376
- return void 0;
1377
- params[seg.value] = urlSegments[urlIdx];
1378
- urlIdx++;
1379
- } else if (seg.type === "optional") {
1380
- if (urlIdx < urlSegments.length) {
1381
- params[seg.value] = urlSegments[urlIdx];
1382
- urlIdx++;
1362
+ if (options.testMode && url.pathname === "/_jay/shutdown" && req.method === "POST") {
1363
+ res.writeHead(200, { "Content-Type": "application/json" });
1364
+ res.end(JSON.stringify({ status: "shutting_down" }));
1365
+ setTimeout(() => {
1366
+ server.close();
1367
+ process.exit(0);
1368
+ }, 100);
1369
+ return;
1383
1370
  }
1384
- } else if (seg.type === "catchAll") {
1385
- if (urlIdx >= urlSegments.length)
1386
- return void 0;
1387
- params[seg.value] = urlSegments.slice(urlIdx).join("/");
1388
- urlIdx = urlSegments.length;
1389
- } else if (seg.type === "optionalCatchAll") {
1390
- if (urlIdx < urlSegments.length) {
1391
- params[seg.value] = urlSegments.slice(urlIdx).join("/");
1392
- urlIdx = urlSegments.length;
1371
+ if (isActionRequest(url.pathname)) {
1372
+ const fetchReq = toFetchRequest(req);
1373
+ const response2 = await fetchActionRequest(fetchReq);
1374
+ await pipeFetchResponse(response2, res);
1375
+ return;
1376
+ }
1377
+ if (options.serveStatic !== false) {
1378
+ const staticResponse = await fetchStaticFile(url.pathname, frontendDir);
1379
+ if (staticResponse) {
1380
+ await pipeFetchResponse(staticResponse, res);
1381
+ return;
1382
+ }
1383
+ }
1384
+ const currentManifest = await artifacts.readManifest();
1385
+ const match = matchRequest(currentManifest, url.pathname);
1386
+ if (!match) {
1387
+ res.writeHead(404);
1388
+ res.end("Not Found");
1389
+ return;
1390
+ }
1391
+ const cookies = parseCookies(req.headers.cookie);
1392
+ const response = await fetchPageRequest(
1393
+ match,
1394
+ currentManifest,
1395
+ url,
1396
+ artifacts,
1397
+ staticBaseUrl,
1398
+ cookies
1399
+ );
1400
+ await pipeFetchResponse(response, res);
1401
+ } catch (err) {
1402
+ logger.error(`[Server] Error handling ${url.pathname}: ${err.message}`);
1403
+ if (err.stack)
1404
+ logger.error(err.stack);
1405
+ if (!res.headersSent) {
1406
+ res.writeHead(500);
1407
+ res.end("Internal Server Error");
1393
1408
  }
1394
1409
  }
1395
- }
1396
- if (urlIdx !== urlSegments.length)
1397
- return void 0;
1398
- return params;
1399
- }
1400
- function findInstance(route, params) {
1401
- const paramNames = new Set(
1402
- route.segments.filter((s) => s.type !== "static").map((s) => s.value)
1403
- );
1404
- return route.instances.find((instance) => {
1405
- for (const name of paramNames) {
1406
- const urlVal = params[name];
1407
- const instVal = instance.params[name];
1408
- if (urlVal === void 0 && instVal === void 0)
1409
- continue;
1410
- if (urlVal !== instVal)
1411
- return false;
1410
+ });
1411
+ server.listen(options.port, () => {
1412
+ logger.important(
1413
+ `[Server] Production server listening on http://localhost:${options.port}`
1414
+ );
1415
+ if (options.testMode) {
1416
+ logger.important(`[Server] Test mode enabled`);
1417
+ logger.important(` Health: http://localhost:${options.port}/_jay/health`);
1418
+ logger.important(
1419
+ ` Shutdown: curl -X POST http://localhost:${options.port}/_jay/shutdown`
1420
+ );
1412
1421
  }
1413
- return true;
1414
1422
  });
1415
1423
  }
1416
- function buildImportMap(sharedManifest, publicBasePath, sharedDir = "shared") {
1417
- const imports = {};
1418
- for (const [pkgName, hashedFile] of Object.entries(sharedManifest)) {
1419
- imports[pkgName] = `${publicBasePath}${sharedDir}/${hashedFile}`;
1420
- }
1421
- return imports;
1422
- }
1423
- const pagePartsCache = /* @__PURE__ */ new Map();
1424
- async function getPageParts(route, pageModule, artifacts, preRenderedPath, manifest) {
1425
- const cacheKey = route.pattern;
1426
- const cached = pagePartsCache.get(cacheKey);
1427
- if (cached)
1428
- return cached;
1429
- if (!route.jayHtmlPath) {
1430
- return {
1431
- parts: [
1432
- {
1433
- compDefinition: pageModule.page ?? pageModule.default,
1434
- clientImport: "",
1435
- clientPart: ""
1424
+ async function discoverWebhooks(projectRoot, serverBuildDir) {
1425
+ const logger = getLogger();
1426
+ const webhooks = [];
1427
+ try {
1428
+ const plugins = await scanPlugins({ projectRoot });
1429
+ for (const [packageName, plugin] of plugins) {
1430
+ if (plugin.isLocal)
1431
+ continue;
1432
+ const declaredWebhooks = plugin.manifest.webhooks;
1433
+ if (!declaredWebhooks || declaredWebhooks.length === 0)
1434
+ continue;
1435
+ try {
1436
+ const pluginModule = await import(packageName);
1437
+ for (const entry of declaredWebhooks) {
1438
+ const exportName = typeof entry === "string" ? entry : entry.name;
1439
+ const value = pluginModule[exportName];
1440
+ if (isJayWebhook(value)) {
1441
+ webhooks.push({
1442
+ name: value.webhookName,
1443
+ webhook: value,
1444
+ source: packageName
1445
+ });
1446
+ logger.info(
1447
+ `[Renderer] Webhook "${value.webhookName}" from ${plugin.manifest.name}`
1448
+ );
1449
+ } else {
1450
+ logger.warn(
1451
+ `[Renderer] plugin.yaml declares webhook "${exportName}" but export is not a JayWebhook in ${packageName}`
1452
+ );
1453
+ }
1436
1454
  }
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
- );
1455
- pagePartsCache.set(cacheKey, parts);
1456
- return parts;
1457
- }
1458
- async function handlePageRequest(res, match, manifest, artifacts) {
1459
- const { route, instance } = match;
1460
- 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());
1474
- const fastResult = await renderFastChangingData(
1475
- match.params,
1476
- { params: match.params, query },
1477
- preRendered.carryForward,
1478
- pageParts.parts,
1479
- preRendered.carryForward.__instances,
1480
- pageParts.forEachInstances,
1481
- pageParts.headlessInstanceComponents,
1482
- preRendered.slowViewState,
1483
- query
1484
- );
1485
- if (fastResult.kind === "Redirect3xx") {
1486
- res.writeHead(fastResult.status, { Location: fastResult.location });
1487
- res.end();
1488
- return;
1489
- }
1490
- if (fastResult.kind === "ServerError5xx" || fastResult.kind === "ClientError4xx") {
1491
- res.writeHead(fastResult.status);
1492
- res.end(fastResult.message || "Error");
1493
- return;
1455
+ } catch (err) {
1456
+ logger.warn(
1457
+ `[Renderer] Failed to load webhooks from ${packageName}: ${err.message}`
1458
+ );
1459
+ }
1460
+ }
1461
+ } catch {
1494
1462
  }
1495
- const fastViewState = fastResult.rendered || {};
1496
- const fastCarryForward = fastResult.carryForward || {};
1497
- const headTagSources = [];
1498
- const slowHeadTags = preRendered.carryForward.__slowHeadTags;
1499
- if (slowHeadTags)
1500
- headTagSources.push(...slowHeadTags);
1501
- const fastHeadTags = fastResult.headTags;
1502
- if (fastHeadTags)
1503
- headTagSources.push(fastHeadTags);
1504
- const headTags = headTagSources.length > 0 ? mergeHeadTags(headTagSources) : [];
1505
- const headTagsHtml = headTags.length > 0 ? serializeHeadTags(headTags) + "\n" : "";
1506
- headTags.some((t) => t.tag.toLowerCase() === "title");
1507
- const fullViewState = deepMergeViewStates(
1508
- preRendered.slowViewState,
1509
- fastViewState,
1510
- route.trackByMap || {}
1511
- );
1512
- const serverElement = await artifacts.loadServerElement(instance.serverElementPath);
1513
- 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>
1523
- <html lang="en">
1524
- <head>
1525
- <meta charset="UTF-8" />
1526
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
1527
- ${headParts}
1528
- <script type="importmap">${JSON.stringify({ imports: importMap })}<\/script>
1529
- </head>
1530
- <body>
1531
- <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
- );
1463
+ try {
1464
+ const plugins = await scanPlugins({ projectRoot });
1465
+ for (const [, plugin] of plugins) {
1466
+ if (!plugin.isLocal)
1467
+ continue;
1468
+ const declaredWebhooks = plugin.manifest.webhooks;
1469
+ if (!declaredWebhooks || declaredWebhooks.length === 0)
1470
+ continue;
1471
+ const pluginDirName = path.basename(plugin.pluginPath);
1472
+ for (const entry of declaredWebhooks) {
1473
+ const exportName = typeof entry === "string" ? entry : entry.name;
1474
+ const modulePath = path.join(serverBuildDir, "plugins", pluginDirName, "index.js");
1475
+ try {
1476
+ const mod = await import(modulePath);
1477
+ const value = mod[exportName];
1478
+ if (isJayWebhook(value)) {
1479
+ webhooks.push({
1480
+ name: value.webhookName,
1481
+ webhook: value,
1482
+ source: `local:${plugin.manifest.name}`
1483
+ });
1484
+ logger.info(
1485
+ `[Renderer] Webhook "${value.webhookName}" from local plugin ${plugin.manifest.name}`
1486
+ );
1487
+ }
1488
+ } catch (err) {
1489
+ logger.warn(
1490
+ `[Renderer] Failed to load webhook "${exportName}" from local plugin ${plugin.manifest.name}: ${err.message}`
1491
+ );
1492
+ }
1493
+ }
1541
1494
  }
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(`
1550
- <script type="module">
1551
- import { init } from '${clientBundleUrl}';
1552
- await init(${JSON.stringify(fastViewState)}, ${JSON.stringify(fastCarryForward)}, ${JSON.stringify(clientInitData)});
1553
- <\/script>
1554
- </body>
1555
- </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;
1495
+ } catch {
1581
1496
  }
1497
+ const webhooksDir = path.join(serverBuildDir, "webhooks");
1582
1498
  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;
1499
+ const files = await fs.readdir(webhooksDir);
1500
+ for (const file of files) {
1501
+ if (!file.endsWith(".js"))
1502
+ continue;
1503
+ try {
1504
+ const mod = await import(path.join(webhooksDir, file));
1505
+ for (const [, value] of Object.entries(mod)) {
1506
+ if (isJayWebhook(value)) {
1507
+ webhooks.push({
1508
+ name: value.webhookName,
1509
+ webhook: value,
1510
+ source: `project:${file}`
1511
+ });
1512
+ logger.info(`[Renderer] Webhook "${value.webhookName}" from project`);
1513
+ }
1514
+ }
1515
+ } catch (err) {
1516
+ logger.warn(`[Renderer] Failed to load webhook ${file}: ${err.message}`);
1517
+ }
1518
+ }
1595
1519
  } catch {
1596
- return false;
1597
1520
  }
1521
+ return webhooks;
1598
1522
  }
1599
- const ACTION_PREFIX = "/_jay/actions/";
1600
- function isActionRequest(pathname) {
1601
- return pathname.startsWith(ACTION_PREFIX);
1523
+ function resolveContractToRoutes(manifest, contractName) {
1524
+ return manifest.routes.filter((r) => r.contracts && r.contracts.includes(contractName));
1602
1525
  }
1603
- async function handleActionRequest(req, res, registry = actionRegistry) {
1604
- const url = new URL(req.url || "/", `http://${req.headers.host}`);
1605
- const actionName = url.pathname.slice(ACTION_PREFIX.length);
1606
- if (!actionName) {
1607
- jsonResponse(res, 400, {
1608
- success: false,
1609
- error: {
1610
- code: "MISSING_ACTION_NAME",
1611
- message: "Action name is required",
1612
- isActionError: false
1613
- }
1614
- });
1615
- return;
1616
- }
1617
- const action = registry.get(actionName);
1618
- if (!action) {
1619
- jsonResponse(res, 404, {
1620
- success: false,
1621
- error: {
1622
- code: "ACTION_NOT_FOUND",
1623
- message: `Action '${actionName}' is not registered`,
1624
- isActionError: false
1625
- }
1626
- });
1627
- return;
1526
+ async function rebuild(options) {
1527
+ const logger = getLogger();
1528
+ const buildDir = path.join(options.buildRoot, `v${options.version}`);
1529
+ const manifestPath = path.join(buildDir, "backend", "route-manifest.json");
1530
+ const manifest = JSON.parse(await fs.readFile(manifestPath, "utf-8"));
1531
+ const {
1532
+ routes: affectedRoutes,
1533
+ params: targetParams,
1534
+ label
1535
+ } = resolveTarget(manifest, options.target);
1536
+ logger.important(`[Rebuild] ${label} in v${options.version}`);
1537
+ if (affectedRoutes.length === 0) {
1538
+ logger.warn(`[Rebuild] No routes found for ${label}`);
1539
+ return { affected: 0, rebuilt: 0, errors: [] };
1628
1540
  }
1629
- const requestMethod = (req.method || "GET").toUpperCase();
1630
- if (requestMethod !== action.method) {
1631
- jsonResponse(res, 405, {
1632
- success: false,
1633
- error: {
1634
- code: "METHOD_NOT_ALLOWED",
1635
- message: `Action '${actionName}' expects ${action.method}, got ${requestMethod}`,
1636
- isActionError: false
1541
+ logger.important(
1542
+ `[Rebuild] Found ${affectedRoutes.length} route(s): ${affectedRoutes.map((r) => r.pattern).join(", ")}`
1543
+ );
1544
+ const backendDir = path.join(buildDir, "backend");
1545
+ const frontendDir = path.join(buildDir, "frontend");
1546
+ await initializeServices(backendDir, options.projectRoot, "Rebuild");
1547
+ const rebuildSuffix = Date.now().toString(36);
1548
+ const instanceCtx = {
1549
+ projectRoot: options.projectRoot,
1550
+ pagesRoot: options.pagesRoot,
1551
+ buildDir,
1552
+ backendDir,
1553
+ frontendDir,
1554
+ jayOptions: { tsConfigFilePath: options.tsConfigFilePath },
1555
+ tsConfigFilePath: options.tsConfigFilePath,
1556
+ minify: options.minify ?? true,
1557
+ rebuildSuffix
1558
+ };
1559
+ const result = { affected: 0, rebuilt: 0, errors: [] };
1560
+ const orphanedFiles = [];
1561
+ for (const route of affectedRoutes) {
1562
+ const instancesToRebuild = targetParams ? route.instances.filter((i) => paramsMatch(i.params, targetParams)) : [...route.instances];
1563
+ if (instancesToRebuild.length === 0 && targetParams) {
1564
+ instancesToRebuild.push({ params: targetParams });
1565
+ }
1566
+ for (const instance of instancesToRebuild) {
1567
+ result.affected++;
1568
+ const params = instance.params;
1569
+ const oldFiles = collectInstanceFiles(instance);
1570
+ let pageModule;
1571
+ try {
1572
+ pageModule = await loadRouteModule(route, buildDir);
1573
+ } catch (err) {
1574
+ result.errors.push({
1575
+ route: route.pattern,
1576
+ params,
1577
+ error: `Failed to load module: ${err.message}`
1578
+ });
1579
+ continue;
1637
1580
  }
1638
- });
1639
- return;
1640
- }
1641
- let input;
1642
- try {
1643
- if (requestMethod === "GET") {
1644
- const inputParam = url.searchParams.get("_input");
1645
- if (inputParam) {
1646
- input = JSON.parse(inputParam);
1647
- } else {
1648
- input = Object.fromEntries(url.searchParams.entries());
1649
- delete input._input;
1581
+ const jayRoute = await resolveJayRouteFromManifest(route, options);
1582
+ try {
1583
+ const buildResult = await buildInstance(jayRoute, params, pageModule, instanceCtx);
1584
+ if (buildResult.status !== "success") {
1585
+ result.errors.push({
1586
+ route: route.pattern,
1587
+ params,
1588
+ error: buildResult.reason
1589
+ });
1590
+ continue;
1591
+ }
1592
+ const existingIdx = route.instances.findIndex((i) => paramsMatch(i.params, params));
1593
+ if (existingIdx >= 0) {
1594
+ route.instances[existingIdx] = buildResult.instanceEntry;
1595
+ orphanedFiles.push(...oldFiles);
1596
+ } else {
1597
+ route.instances.push(buildResult.instanceEntry);
1598
+ }
1599
+ result.rebuilt++;
1600
+ logger.important(`[Rebuild] ${route.pattern} (${JSON.stringify(params)}): rebuilt`);
1601
+ } catch (err) {
1602
+ result.errors.push({
1603
+ route: route.pattern,
1604
+ params,
1605
+ error: err.message
1606
+ });
1650
1607
  }
1651
- } else {
1652
- input = await parseBody(req);
1653
1608
  }
1654
- } catch {
1655
- jsonResponse(res, 400, {
1656
- success: false,
1657
- error: {
1658
- code: "INVALID_INPUT",
1659
- message: "Failed to parse request input",
1660
- isActionError: false
1661
- }
1662
- });
1663
- return;
1664
1609
  }
1665
- if (registry.isStreaming(actionName)) {
1666
- res.writeHead(200, {
1667
- "Content-Type": "application/x-ndjson",
1668
- "Transfer-Encoding": "chunked"
1669
- });
1610
+ if (result.rebuilt > 0) {
1611
+ const tempPath = manifestPath + ".tmp";
1612
+ await fs.writeFile(tempPath, JSON.stringify(manifest, null, 2));
1613
+ await fs.rename(tempPath, manifestPath);
1614
+ const metadataPath = path.join(buildDir, "backend", "build-metadata.json");
1670
1615
  try {
1671
- const generator = registry.executeStream(actionName, input);
1672
- for await (const chunk of generator) {
1673
- res.write(JSON.stringify({ chunk }) + "\n");
1674
- }
1675
- res.write(JSON.stringify({ done: true }) + "\n");
1676
- } catch (err) {
1677
- res.write(JSON.stringify({ error: err.message }) + "\n");
1616
+ const metadata = JSON.parse(await fs.readFile(metadataPath, "utf-8"));
1617
+ metadata.buildTimestamp = (/* @__PURE__ */ new Date()).toISOString();
1618
+ metadata.instanceCount = manifest.routes.reduce((n, r) => n + r.instances.length, 0);
1619
+ await fs.writeFile(metadataPath, JSON.stringify(metadata, null, 2));
1620
+ } catch {
1621
+ await fs.writeFile(
1622
+ metadataPath,
1623
+ JSON.stringify(
1624
+ {
1625
+ version: options.version,
1626
+ sourceHash: "",
1627
+ buildTimestamp: (/* @__PURE__ */ new Date()).toISOString(),
1628
+ nodeVersion: process.version,
1629
+ instanceCount: manifest.routes.reduce((n, r) => n + r.instances.length, 0)
1630
+ },
1631
+ null,
1632
+ 2
1633
+ )
1634
+ );
1635
+ }
1636
+ logger.important(`[Rebuild] Manifest and metadata updated`);
1637
+ if (orphanedFiles.length > 0) {
1638
+ await appendCleanupManifest(buildDir, orphanedFiles);
1639
+ logger.info(`[Rebuild] ${orphanedFiles.length} orphaned file(s) queued for cleanup`);
1678
1640
  }
1679
- res.end();
1680
- return;
1681
1641
  }
1682
- const result = await registry.execute(actionName, input);
1683
- if (result.success) {
1684
- if (requestMethod === "GET") {
1685
- const cacheHeaders = registry.getCacheHeaders(actionName);
1686
- if (cacheHeaders) {
1687
- res.setHeader("Cache-Control", cacheHeaders);
1642
+ logger.important(
1643
+ `[Rebuild] Done: ${result.affected} affected, ${result.rebuilt} rebuilt, ${result.errors.length} errors`
1644
+ );
1645
+ return result;
1646
+ }
1647
+ function resolveTarget(manifest, target) {
1648
+ switch (target.mode) {
1649
+ case "contract": {
1650
+ const routes = resolveContractToRoutes(manifest, target.contractName);
1651
+ return {
1652
+ routes,
1653
+ params: target.params,
1654
+ label: `contract "${target.contractName}"${target.params ? ` (${JSON.stringify(target.params)})` : ""}`
1655
+ };
1656
+ }
1657
+ case "route": {
1658
+ const route = manifest.routes.find((r) => r.pattern === target.routePattern);
1659
+ return {
1660
+ routes: route ? [route] : [],
1661
+ params: target.params,
1662
+ label: `route "${target.routePattern}"${target.params ? ` (${JSON.stringify(target.params)})` : ""}`
1663
+ };
1664
+ }
1665
+ case "url": {
1666
+ const match = matchRequest(manifest, target.url);
1667
+ if (!match) {
1668
+ return { routes: [], label: `url "${target.url}"` };
1688
1669
  }
1670
+ return {
1671
+ routes: [match.route],
1672
+ params: match.params,
1673
+ label: `url "${target.url}" → ${match.route.pattern} (${JSON.stringify(match.params)})`
1674
+ };
1689
1675
  }
1690
- jsonResponse(res, 200, { success: true, data: result.data });
1691
- } else {
1692
- const statusCode = getStatusCode(result.error.code, result.error.isActionError);
1693
- jsonResponse(res, statusCode, { success: false, error: result.error });
1694
1676
  }
1695
1677
  }
1696
- async function registerActionsFromManifest(actions, buildDir, registry = actionRegistry) {
1697
- const logger = getLogger();
1698
- let count = 0;
1699
- for (const entry of actions) {
1678
+ async function rebuildContract(options) {
1679
+ return rebuild({
1680
+ ...options,
1681
+ target: { mode: "contract", contractName: options.contractName, params: options.params }
1682
+ });
1683
+ }
1684
+ async function loadRouteModule(route, buildDir) {
1685
+ if (!route.serverModule)
1686
+ return {};
1687
+ if (route.isPlugin)
1688
+ return import(route.serverModule);
1689
+ return import(path.join(buildDir, route.serverModule));
1690
+ }
1691
+ async function resolveJayRouteFromManifest(route, options) {
1692
+ const routeDir = route.pattern.replace(/^\//, "") || "index";
1693
+ const jayHtmlPath = path.join(options.pagesRoot, routeDir, "page.jay-html");
1694
+ let resolvedJayHtmlPath = jayHtmlPath;
1695
+ if (route.isPlugin && route.serverModule) {
1700
1696
  try {
1701
- const modulePath = entry.isPlugin ? entry.packageName : `${buildDir}/${entry.serverModule}`;
1702
- const mod = await import(modulePath);
1703
- for (const [, exported] of Object.entries(mod)) {
1704
- if (isJayAction(exported)) {
1705
- registry.register(exported);
1706
- count++;
1707
- } else if (isJayStreamAction(exported)) {
1708
- registry.registerStream(exported);
1709
- count++;
1710
- }
1697
+ const pluginModule = await import(route.serverModule);
1698
+ const comp = pluginModule[route.componentExport || "page"];
1699
+ if (comp?.jayHtmlPath) {
1700
+ resolvedJayHtmlPath = comp.jayHtmlPath;
1711
1701
  }
1712
- } catch (err) {
1713
- logger.error(
1714
- `[Server] Failed to load action module ${entry.serverModule}: ${err.message}`
1715
- );
1702
+ } catch {
1716
1703
  }
1717
1704
  }
1718
- logger.info(`[Server] Registered ${count} actions`);
1705
+ return {
1706
+ rawRoute: route.pattern,
1707
+ segments: route.segments.map((s) => {
1708
+ if (s.type === "static")
1709
+ return s.value;
1710
+ return { name: s.value, type: segmentTypeMap[s.type] };
1711
+ }),
1712
+ jayHtmlPath: resolvedJayHtmlPath,
1713
+ compPath: route.isPlugin ? route.serverModule : void 0,
1714
+ componentExport: route.componentExport
1715
+ };
1719
1716
  }
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);
1717
+ const segmentTypeMap = {
1718
+ param: 0,
1719
+ catchAll: 1,
1720
+ optional: 2
1721
+ };
1722
+ function paramsMatch(instanceParams, targetParams) {
1723
+ return Object.entries(targetParams).every(([key, value]) => instanceParams[key] === value);
1727
1724
  }
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
- });
1725
+ function collectInstanceFiles(instance) {
1726
+ if (!instance.cachePath)
1727
+ return [];
1728
+ const files = [instance.cachePath, instance.serverElementPath, instance.clientBundlePath];
1729
+ if (instance.clientCssPath)
1730
+ files.push(instance.clientCssPath);
1731
+ return files.filter(Boolean);
1746
1732
  }
1747
- function getStatusCode(code, isActionError) {
1748
- if (isActionError)
1749
- return 422;
1750
- switch (code) {
1751
- case "ACTION_NOT_FOUND":
1752
- return 404;
1753
- case "INVALID_INPUT":
1754
- case "VALIDATION_ERROR":
1755
- return 400;
1756
- case "UNAUTHORIZED":
1757
- return 401;
1758
- case "FORBIDDEN":
1759
- return 403;
1760
- default:
1761
- return 500;
1733
+ async function appendCleanupManifest(buildDir, files) {
1734
+ const cleanupPath = path.join(buildDir, "cleanup-manifest.json");
1735
+ let existing = [];
1736
+ try {
1737
+ existing = JSON.parse(await fs.readFile(cleanupPath, "utf-8"));
1738
+ } catch {
1762
1739
  }
1740
+ existing.push(...files);
1741
+ await fs.writeFile(cleanupPath, JSON.stringify(existing, null, 2));
1763
1742
  }
1764
- async function startMainServer(options) {
1743
+ async function cleanupOrphanedFiles(buildRoot, version) {
1765
1744
  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
- const { discoverPluginsWithInit, sortPluginsByDependencies } = await import("@jay-framework/stack-server-runtime");
1745
+ const buildDir = path.join(buildRoot, `v${version}`);
1746
+ const cleanupPath = path.join(buildDir, "cleanup-manifest.json");
1747
+ let files;
1773
1748
  try {
1774
- const pluginsWithInit = sortPluginsByDependencies(
1775
- await discoverPluginsWithInit({ projectRoot: manifest.projectRoot })
1776
- );
1777
- for (const pluginInit of pluginsWithInit) {
1778
- try {
1779
- const pluginModule = await import(pluginInit.packageName);
1780
- const init = pluginModule.init || pluginModule[pluginInit.initExport || "init"];
1781
- if (init?._serverInit) {
1782
- logger.info(`[Server] Running plugin init: ${pluginInit.name}`);
1783
- const data = await init._serverInit();
1784
- if (data)
1785
- setClientInitData(pluginInit.name, data);
1786
- }
1787
- } catch (err) {
1788
- logger.warn(`[Server] Plugin init failed: ${pluginInit.name}: ${err.message}`);
1789
- }
1790
- }
1749
+ files = JSON.parse(await fs.readFile(cleanupPath, "utf-8"));
1791
1750
  } catch {
1751
+ logger.info("[Cleanup] No cleanup manifest found");
1752
+ return 0;
1792
1753
  }
1793
- const initModule = await artifacts.loadPageModule("server/init.js").catch(() => null);
1794
- if (initModule) {
1795
- const init = initModule.init || initModule.default;
1796
- if (init?._serverInit) {
1797
- logger.important("[Server] Running server init...");
1798
- const data = await init._serverInit();
1799
- if (data)
1800
- setClientInitData("project", data);
1754
+ let deleted = 0;
1755
+ for (const file of files) {
1756
+ try {
1757
+ await fs.unlink(path.join(buildDir, file));
1758
+ deleted++;
1759
+ } catch {
1801
1760
  }
1802
1761
  }
1803
- if (manifest.actions.length > 0) {
1804
- await registerActionsFromManifest(manifest.actions, buildDir);
1762
+ await fs.unlink(cleanupPath);
1763
+ logger.important(`[Cleanup] Deleted ${deleted}/${files.length} orphaned files`);
1764
+ return deleted;
1765
+ }
1766
+ async function startRendererServer(options) {
1767
+ const logger = getLogger();
1768
+ const buildDir = path.join(options.buildRoot, `v${options.version}`);
1769
+ const serverBuildDir = path.join(buildDir, "server");
1770
+ logger.important(`[Renderer] Starting renderer server v${options.version}`);
1771
+ await initializeServices(buildDir, options.projectRoot, "Renderer");
1772
+ const webhooks = await discoverWebhooks(options.projectRoot, serverBuildDir);
1773
+ const webhookMap = /* @__PURE__ */ new Map();
1774
+ for (const wh of webhooks) {
1775
+ webhookMap.set(wh.name, wh);
1805
1776
  }
1777
+ logger.important(
1778
+ `[Renderer] ${webhookMap.size} webhook(s) registered: ${[...webhookMap.keys()].join(", ") || "none"}`
1779
+ );
1780
+ const createInvalidateForWebhook = () => {
1781
+ return async (contractName, params) => {
1782
+ await rebuildContract({
1783
+ projectRoot: options.projectRoot,
1784
+ pagesRoot: options.pagesRoot,
1785
+ buildRoot: options.buildRoot,
1786
+ version: options.version,
1787
+ contractName,
1788
+ params,
1789
+ tsConfigFilePath: options.tsConfigFilePath,
1790
+ minify: options.minify
1791
+ });
1792
+ };
1793
+ };
1794
+ const startTime = Date.now();
1795
+ let lastWebhook;
1806
1796
  const server = http.createServer(async (req, res) => {
1807
1797
  const url = new URL(req.url || "/", `http://${req.headers.host}`);
1808
1798
  try {
1809
- if (isActionRequest(url.pathname)) {
1810
- await handleActionRequest(req, res);
1799
+ const webhookMatch = url.pathname.match(/^\/_jay\/webhooks\/(.+)$/);
1800
+ if (webhookMatch && req.method === "POST") {
1801
+ const webhookName = webhookMatch[1];
1802
+ const discovered = webhookMap.get(webhookName);
1803
+ if (!discovered) {
1804
+ res.writeHead(404, { "Content-Type": "application/json" });
1805
+ res.end(JSON.stringify({ error: `Unknown webhook: ${webhookName}` }));
1806
+ return;
1807
+ }
1808
+ const body = await readBody(req);
1809
+ const event = {
1810
+ type: webhookName,
1811
+ payload: JSON.parse(body || "{}"),
1812
+ headers: req.headers
1813
+ };
1814
+ const invalidate = createInvalidateForWebhook();
1815
+ const resolver = globalThis.__JAY_SERVICE_RESOLVER__;
1816
+ const services = resolver ? resolver(discovered.webhook.services) : [];
1817
+ await discovered.webhook.handler(event, invalidate, ...services);
1818
+ lastWebhook = { name: webhookName, timestamp: (/* @__PURE__ */ new Date()).toISOString() };
1819
+ res.writeHead(200, { "Content-Type": "application/json" });
1820
+ res.end(JSON.stringify({ ok: true }));
1811
1821
  return;
1812
1822
  }
1813
- const handled = await handleStaticRequest(
1814
- req,
1815
- res,
1816
- path.join(buildDir, "shared"),
1817
- "/shared/"
1818
- );
1819
- if (handled)
1820
- return;
1821
- const handledInstances = await handleStaticRequest(
1822
- req,
1823
- res,
1824
- path.join(buildDir, "pre-rendered"),
1825
- "/pre-rendered/"
1826
- );
1827
- if (handledInstances)
1823
+ if (url.pathname === "/_jay/rebuild" && req.method === "POST") {
1824
+ const body = JSON.parse(await readBody(req) || "{}");
1825
+ const { contract, route, url: rebuildUrl, params } = body;
1826
+ let target;
1827
+ if (contract) {
1828
+ target = { mode: "contract", contractName: contract, params };
1829
+ } else if (route) {
1830
+ target = { mode: "route", routePattern: route, params };
1831
+ } else if (rebuildUrl) {
1832
+ target = { mode: "url", url: rebuildUrl };
1833
+ } else {
1834
+ res.writeHead(400, { "Content-Type": "application/json" });
1835
+ res.end(
1836
+ JSON.stringify({
1837
+ error: 'One of "contract", "route", or "url" is required'
1838
+ })
1839
+ );
1840
+ return;
1841
+ }
1842
+ const result = await rebuild({
1843
+ projectRoot: options.projectRoot,
1844
+ pagesRoot: options.pagesRoot,
1845
+ buildRoot: options.buildRoot,
1846
+ version: options.version,
1847
+ target,
1848
+ tsConfigFilePath: options.tsConfigFilePath,
1849
+ minify: options.minify
1850
+ });
1851
+ res.writeHead(200, { "Content-Type": "application/json" });
1852
+ res.end(JSON.stringify(result));
1828
1853
  return;
1829
- const currentManifest = await artifacts.readManifest();
1830
- const match = matchRequest(currentManifest, url.pathname);
1831
- if (!match) {
1832
- res.writeHead(404);
1833
- res.end("Not Found");
1854
+ }
1855
+ if (url.pathname === "/_jay/status" && req.method === "GET") {
1856
+ const manifest = JSON.parse(
1857
+ await fs.readFile(path.join(buildDir, "route-manifest.json"), "utf-8")
1858
+ );
1859
+ const status = {
1860
+ version: options.version,
1861
+ buildTimestamp: manifest.buildTimestamp,
1862
+ instanceCount: manifest.routes.reduce((n, r) => n + r.instances.length, 0),
1863
+ uptime: Math.floor((Date.now() - startTime) / 1e3),
1864
+ webhooks: [...webhookMap.keys()],
1865
+ lastWebhook
1866
+ };
1867
+ res.writeHead(200, { "Content-Type": "application/json" });
1868
+ res.end(JSON.stringify(status, null, 2));
1834
1869
  return;
1835
1870
  }
1836
- await handlePageRequest(res, match, currentManifest, artifacts);
1871
+ res.writeHead(404);
1872
+ res.end("Not Found");
1837
1873
  } catch (err) {
1838
- logger.error(`[Server] Error handling ${url.pathname}: ${err.message}`);
1874
+ logger.error(`[Renderer] Error: ${err.message}`);
1839
1875
  if (!res.headersSent) {
1840
- res.writeHead(500);
1841
- res.end("Internal Server Error");
1876
+ res.writeHead(500, { "Content-Type": "application/json" });
1877
+ res.end(JSON.stringify({ error: err.message }));
1842
1878
  }
1843
1879
  }
1844
1880
  });
1845
1881
  server.listen(options.port, () => {
1846
1882
  logger.important(
1847
- `[Server] Production server listening on http://localhost:${options.port}`
1883
+ `[Renderer] Renderer server listening on http://localhost:${options.port}`
1848
1884
  );
1849
1885
  });
1850
1886
  }
1887
+ function readBody(req) {
1888
+ return new Promise((resolve, reject) => {
1889
+ const chunks = [];
1890
+ req.on("data", (chunk) => chunks.push(chunk));
1891
+ req.on("end", () => resolve(Buffer.concat(chunks).toString()));
1892
+ req.on("error", reject);
1893
+ });
1894
+ }
1851
1895
  export {
1896
+ FilesystemArtifactStore,
1852
1897
  buildVersion,
1853
- startMainServer
1898
+ cleanupOrphanedFiles,
1899
+ fetchActionRequest,
1900
+ fetchPageRequest,
1901
+ fetchStaticFile,
1902
+ initializeServices,
1903
+ e as initializeServicesFromModules,
1904
+ isActionRequest,
1905
+ matchRequest,
1906
+ rebuild,
1907
+ rebuildContract,
1908
+ registerActionsFromManifest,
1909
+ b as registerActionsFromModules,
1910
+ resolveContractToRoutes,
1911
+ startMainServer,
1912
+ startRendererServer
1854
1913
  };