@jay-framework/production-server 0.24.1 → 0.24.3

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,450 +1,13 @@
1
- import { build } from "vite";
2
- import { jayStackCompiler, extractActionsFromSource } from "@jay-framework/compiler-jay-stack";
3
- import { scanRoutes, JayRouteParamType, parseRouteSegments } from "@jay-framework/stack-route-scanner";
4
- import { getLogger } from "@jay-framework/logger";
5
- import path from "node:path";
6
1
  import fs from "node:fs/promises";
7
- import { createRequire } from "node:module";
8
- import { DevSlowlyChangingPhase, slowRenderInstances, scanPlugins, runLoadParams, parseCookies } from "@jay-framework/stack-server-runtime";
9
- import { l as loadProductionPageParts, g as buildPagePartsConfig, F as FilesystemArtifactStore, i as initializeServices, r as registerActionsFromManifest, d as isActionRequest, f as fetchActionRequest, b as fetchStaticFile, m as matchRequest, a as fetchPageRequest } from "./init-services-Dy2SiHzw.js";
10
- import { c, e } from "./init-services-Dy2SiHzw.js";
11
- import crypto, { createHash } from "node:crypto";
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";
17
- import { transform } from "esbuild";
18
2
  import http from "node:http";
19
3
  import { Readable } from "node:stream";
4
+ import path from "node:path";
5
+ import { getLogger } from "@jay-framework/logger";
6
+ import { F as FilesystemArtifactStore, i as initializeServices, r as registerActionsFromManifest, d as isActionRequest, f as fetchActionRequest, b as fetchStaticFile, m as matchRequest, a as fetchPageRequest, l as loadPagePartsFromConfig } from "./init-services-CnM6IinC.js";
7
+ import { c, e } from "./init-services-CnM6IinC.js";
8
+ import { parseCookies, scanPlugins, DevSlowlyChangingPhase, slowRenderInstances } from "@jay-framework/stack-server-runtime";
20
9
  import { isJayWebhook } from "@jay-framework/fullstack-component";
21
- function isCompilableTypeScriptFile(fileName) {
22
- return fileName.endsWith(".ts") && !fileName.endsWith(".d.ts") && fileName !== "page.ts";
23
- }
24
- async function collectTypeScriptEntries(rootDir, entryPrefix, pages) {
25
- async function walk(currentDir, relativePath) {
26
- const entries = await fs.readdir(currentDir, { withFileTypes: true });
27
- for (const entry of entries) {
28
- const fullPath = path.join(currentDir, entry.name);
29
- if (entry.isDirectory()) {
30
- const nextRelative = relativePath ? `${relativePath}/${entry.name}` : entry.name;
31
- await walk(fullPath, nextRelative);
32
- continue;
33
- }
34
- if (!isCompilableTypeScriptFile(entry.name)) continue;
35
- const stem = entry.name.replace(/\.ts$/, "");
36
- const entryName = relativePath ? `${entryPrefix}/${relativePath}/${stem}` : `${entryPrefix}/${stem}`;
37
- pages[entryName] = fullPath;
38
- }
39
- }
40
- await walk(rootDir, "");
41
- }
42
- async function discoverServerEntries(projectRoot, pagesRoot) {
43
- const logger = getLogger();
44
- const routes = await scanRoutes(pagesRoot, {
45
- jayHtmlFilename: "page.jay-html",
46
- compFilename: "page.ts"
47
- });
48
- const pages = {};
49
- for (const route of routes) {
50
- if (route.compPath) {
51
- const relativePath = path.relative(projectRoot, route.compPath);
52
- const entryName = relativePath.replace(/^src\//, "").replace(/\.ts$/, "");
53
- pages[entryName] = route.compPath;
54
- }
55
- }
56
- const actions = {};
57
- const actionsDir = path.join(projectRoot, "src", "actions");
58
- try {
59
- const files = await fs.readdir(actionsDir);
60
- for (const file of files) {
61
- if (file.endsWith(".actions.ts")) {
62
- const entryName = "actions/" + file.replace(/\.ts$/, "");
63
- actions[entryName] = path.join(actionsDir, file);
64
- }
65
- }
66
- } catch {
67
- }
68
- for (const subDir of ["plugins", "components"]) {
69
- const scanDir = path.join(projectRoot, "src", subDir);
70
- try {
71
- const entries2 = await fs.readdir(scanDir, { withFileTypes: true });
72
- for (const entry of entries2) {
73
- const entryPath = path.join(scanDir, entry.name);
74
- if (entry.isDirectory()) {
75
- await collectTypeScriptEntries(entryPath, `${subDir}/${entry.name}`, pages);
76
- continue;
77
- }
78
- if (!isCompilableTypeScriptFile(entry.name)) continue;
79
- const stem = entry.name.replace(/\.ts$/, "");
80
- pages[`${subDir}/${stem}`] = entryPath;
81
- }
82
- } catch {
83
- }
84
- }
85
- let init;
86
- const initPaths = [
87
- path.join(projectRoot, "src", "lib", "init.ts"),
88
- path.join(projectRoot, "src", "init.ts")
89
- ];
90
- for (const initPath of initPaths) {
91
- try {
92
- await fs.access(initPath);
93
- init = initPath;
94
- break;
95
- } catch {
96
- }
97
- }
98
- const entries = { init, pages, actions };
99
- logger.info(
100
- `[Build] Discovered: ${Object.keys(pages).length} pages, ${Object.keys(actions).length} actions, init: ${init ? "yes" : "no"}`
101
- );
102
- return { entries, routes };
103
- }
104
- async function buildServerCode(entries, jayOptions, outputDir, projectRoot) {
105
- const logger = getLogger();
106
- logger.info("[Build] Compiling server code...");
107
- const input = {};
108
- if (entries.init) {
109
- input["init"] = entries.init;
110
- }
111
- for (const [name, filePath] of Object.entries(entries.pages)) {
112
- input[name] = filePath;
113
- }
114
- for (const [name, filePath] of Object.entries(entries.actions)) {
115
- input[name] = filePath;
116
- }
117
- if (Object.keys(input).length === 0) {
118
- logger.info("[Build] No server entries to compile");
119
- return;
120
- }
121
- await build({
122
- root: projectRoot,
123
- publicDir: false,
124
- plugins: [...jayStackCompiler(jayOptions)],
125
- build: {
126
- ssr: true,
127
- outDir: outputDir,
128
- emptyOutDir: true,
129
- minify: false,
130
- rollupOptions: {
131
- input,
132
- external: [
133
- /^node:/,
134
- /^@jay-framework\//,
135
- // Plugin packages are pre-compiled, externalize them
136
- /^@wix\//
137
- ],
138
- output: {
139
- entryFileNames: "[name].js",
140
- chunkFileNames: "chunks/[name]-[hash].js",
141
- format: "es"
142
- }
143
- }
144
- },
145
- logLevel: "warn"
146
- });
147
- logger.info(`[Build] Server code compiled to ${outputDir}`);
148
- }
149
- createRequire(import.meta.url);
150
- const FRAMEWORK_PACKAGES = [
151
- "@jay-framework/stack-client-runtime",
152
- "@jay-framework/component",
153
- "@jay-framework/reactive",
154
- "@jay-framework/runtime",
155
- "@jay-framework/view-state-merge",
156
- "@jay-framework/fullstack-component"
157
- ];
158
- async function buildSharedChunks(outputDir, _projectRoot, minify = true, pluginClientPackages = []) {
159
- const logger = getLogger();
160
- logger.info("[Build] Building shared client chunks...");
161
- await fs.mkdir(outputDir, { recursive: true });
162
- const allPackages = [...FRAMEWORK_PACKAGES, ...pluginClientPackages];
163
- const entries = {};
164
- for (const pkg of allPackages) {
165
- const varName = pkgToVarName(pkg);
166
- const entryPath = path.join(outputDir, `_shared_${varName}.js`);
167
- await fs.writeFile(entryPath, `export * from '${pkg}';
168
- `);
169
- entries[varName] = entryPath;
170
- }
171
- const dedupePackages = [...allPackages, "@jay-framework/list-compare"];
172
- await build({
173
- publicDir: false,
174
- build: {
175
- outDir: outputDir,
176
- emptyOutDir: true,
177
- minify,
178
- manifest: "vite-manifest.json",
179
- rollupOptions: {
180
- input: entries,
181
- output: {
182
- entryFileNames: "[name]-[hash].js",
183
- chunkFileNames: "[name]-[hash].js",
184
- format: "es"
185
- },
186
- preserveEntrySignatures: "exports-only"
187
- }
188
- },
189
- resolve: {
190
- dedupe: dedupePackages
191
- },
192
- logLevel: "warn"
193
- });
194
- for (const pkg of allPackages) {
195
- const varName = pkgToVarName(pkg);
196
- await fs.rm(path.join(outputDir, `_shared_${varName}.js`), { force: true });
197
- }
198
- const manifest = await parseViteManifest(outputDir, allPackages);
199
- logger.info(`[Build] Shared chunks built: ${Object.keys(manifest).length} entries`);
200
- return { manifest, outputDir };
201
- }
202
- function pkgToVarName(pkg) {
203
- return pkg.replace("@jay-framework/", "").replace(/[/-]/g, "_");
204
- }
205
- async function parseViteManifest(outputDir, packages) {
206
- const viteManifestPath = path.join(outputDir, "vite-manifest.json");
207
- const raw = JSON.parse(await fs.readFile(viteManifestPath, "utf-8"));
208
- const varNameToPackage = /* @__PURE__ */ new Map();
209
- for (const pkg of packages) {
210
- varNameToPackage.set(pkgToVarName(pkg), pkg);
211
- }
212
- const manifest = {};
213
- for (const [, entry] of Object.entries(raw)) {
214
- if (!entry.isEntry) continue;
215
- const outputBase = path.basename(entry.file, ".js");
216
- for (const [varName, pkg] of varNameToPackage) {
217
- if (outputBase.startsWith(varName)) {
218
- manifest[pkg] = entry.file;
219
- break;
220
- }
221
- }
222
- }
223
- const sharedManifestPath = path.join(outputDir, "shared-manifest.json");
224
- await fs.writeFile(sharedManifestPath, JSON.stringify(manifest, null, 2));
225
- await fs.rm(viteManifestPath, { force: true });
226
- return manifest;
227
- }
228
- function hashParams(params, suffix) {
229
- const sorted = Object.keys(params).sort().reduce(
230
- (acc, key) => {
231
- acc[key] = params[key];
232
- return acc;
233
- },
234
- {}
235
- );
236
- const json = JSON.stringify(sorted);
237
- if (json === "{}" && !suffix) return "";
238
- const input = suffix ? json + ":" + suffix : json;
239
- return "_" + crypto.createHash("md5").update(input).digest("hex").substring(0, 8);
240
- }
241
- async function buildInstance(route, params, pageModule, ctx, routeServerElementPath, routeCssPath, routeHydratePath, routeClientBundlePath) {
242
- const logger = getLogger();
243
- const routeDir = route.rawRoute.replace(/^\//, "") || "index";
244
- const paramHash = hashParams(params, ctx.rebuildSuffix);
245
- const instanceId = `page${paramHash}`;
246
- const backendInstanceDir = path.join(ctx.backendDir, "pre-rendered", routeDir);
247
- const frontendInstanceDir = path.join(ctx.frontendDir, "pages", routeDir);
248
- await fs.mkdir(backendInstanceDir, { recursive: true });
249
- await fs.mkdir(frontendInstanceDir, { recursive: true });
250
- const jayHtmlContent = await fs.readFile(route.jayHtmlPath, "utf-8");
251
- path.dirname(route.jayHtmlPath);
252
- const serverBuildDir = path.join(ctx.backendDir, "server");
253
- const pageParts = await loadProductionPageParts(
254
- route,
255
- pageModule,
256
- jayHtmlContent,
257
- ctx.projectRoot,
258
- ctx.tsConfigFilePath,
259
- serverBuildDir
260
- );
261
- const contracts = [
262
- .../* @__PURE__ */ new Set([
263
- ...pageParts.headlessInstanceComponents.map((c2) => c2.contractName),
264
- ...pageParts.parts.filter((p) => p.contractInfo?.contractName).map((p) => p.contractInfo.contractName)
265
- ])
266
- ];
267
- const pagePartsConfigPath = path.join(backendInstanceDir, "page-parts.json");
268
- const exportName = route.componentExport || "page";
269
- let pageServerModule = "";
270
- let pageIsPlugin = false;
271
- if (route.compPath) {
272
- if (route.componentExport) {
273
- pageServerModule = route.packageName || route.compPath;
274
- pageIsPlugin = true;
275
- } else {
276
- const relativePath = path.relative(ctx.projectRoot, route.compPath);
277
- pageServerModule = relativePath.replace(/^src\//, "server/").replace(/\.ts$/, ".js").replace(/\[/g, "_").replace(/\]/g, "_");
278
- }
279
- }
280
- const config = buildPagePartsConfig(
281
- pageParts,
282
- pageServerModule,
283
- exportName,
284
- ctx.backendDir,
285
- pageIsPlugin
286
- );
287
- await fs.writeFile(pagePartsConfigPath, JSON.stringify(config, null, 2));
288
- logger.info(`[Build] Page parts config: ${routeDir}/page-parts.json`);
289
- const slowPhase = new DevSlowlyChangingPhase();
290
- const slowResult = await slowPhase.runSlowlyForPage(
291
- params,
292
- { params },
293
- pageParts.parts,
294
- pageParts.discoveredInstances,
295
- pageParts.headlessInstanceComponents,
296
- route.jayHtmlPath
297
- );
298
- if (slowResult.kind !== "PhaseOutput") {
299
- if (slowResult.kind === "ClientError" || slowResult.kind === "Redirect") {
300
- return {
301
- status: "skipped",
302
- reason: `${slowResult.kind} ${slowResult.status ?? ""} ${slowResult.message ?? ""}`.trim()
303
- };
304
- }
305
- throw new Error(
306
- `Slow render failed for ${route.rawRoute} with params ${JSON.stringify(params)}: ${slowResult.kind}`
307
- );
308
- }
309
- const slowViewState = slowResult.rendered;
310
- const carryForward = slowResult.carryForward;
311
- if (pageParts.discoveredInstances.length > 0 && pageParts.headlessInstanceComponents.length > 0) {
312
- const slowResult2 = await slowRenderInstances(
313
- pageParts.discoveredInstances,
314
- pageParts.headlessInstanceComponents,
315
- {
316
- pageViewState: slowViewState,
317
- pageParams: params,
318
- pageProps: { language: "en", url: "" }
319
- }
320
- );
321
- if (slowResult2) {
322
- const existingInstances = carryForward.__instances || {
323
- discovered: [],
324
- carryForwards: {}
325
- };
326
- carryForward.__instances = {
327
- discovered: [
328
- ...existingInstances.discovered,
329
- ...slowResult2.instancePhaseData.discovered
330
- ],
331
- carryForwards: {
332
- ...existingInstances.carryForwards,
333
- ...slowResult2.instancePhaseData.carryForwards
334
- },
335
- slowViewStates: {
336
- ...existingInstances.slowViewStates || {},
337
- ...slowResult2.instancePhaseData.slowViewStates
338
- }
339
- };
340
- }
341
- }
342
- if (pageParts.forEachInstances.length > 0) {
343
- const existingInstances = carryForward.__instances || {
344
- discovered: [],
345
- carryForwards: {}
346
- };
347
- existingInstances.forEachInstances = pageParts.forEachInstances;
348
- carryForward.__instances = existingInstances;
349
- }
350
- const cachePath = path.join(backendInstanceDir, `${instanceId}.cache.json`);
351
- await fs.writeFile(
352
- cachePath,
353
- JSON.stringify({
354
- slowViewState,
355
- carryForward
356
- }),
357
- "utf-8"
358
- );
359
- logger.info(`[Build] Instance data: ${routeDir}/${instanceId}`);
360
- const serverElementPath = routeServerElementPath ? path.join(ctx.backendDir, routeServerElementPath) : path.join(backendInstanceDir, `${instanceId}.server-element.js`);
361
- const instanceEntry = {
362
- params,
363
- cachePath: path.relative(ctx.backendDir, cachePath),
364
- serverElementPath: path.relative(ctx.backendDir, serverElementPath),
365
- clientBundlePath: routeClientBundlePath || "",
366
- clientCssPath: routeCssPath
367
- };
368
- return { status: "success", instanceEntry, slowViewState, carryForward, contracts };
369
- }
370
- function convertSegments(segments) {
371
- return segments.map((s) => {
372
- if (typeof s === "string") {
373
- return { type: "static", value: s };
374
- }
375
- switch (s.type) {
376
- case JayRouteParamType.single:
377
- return { type: "param", value: s.name };
378
- case JayRouteParamType.catchAll:
379
- return { type: "catchAll", value: s.name };
380
- case JayRouteParamType.optional:
381
- return { type: "optional", value: s.name };
382
- }
383
- });
384
- }
385
- function buildRouteEntry(route, serverModulePath) {
386
- return {
387
- pattern: route.rawRoute,
388
- segments: convertSegments(route.segments),
389
- serverModule: serverModulePath,
390
- componentExport: route.componentExport,
391
- instances: [],
392
- ...route.devOnly && { devOnly: true }
393
- };
394
- }
395
- async function discoverActions(actionPaths, serverOutputDir, buildDir, projectRoot) {
396
- const actions = [];
397
- const plugins = [];
398
- for (const [entryName, sourcePath] of Object.entries(actionPaths)) {
399
- try {
400
- const code = await fs.readFile(sourcePath, "utf-8");
401
- const extracted = extractActionsFromSource(code, sourcePath);
402
- if (extracted.length > 0) {
403
- actions.push({
404
- serverModule: path.relative(
405
- buildDir,
406
- path.join(serverOutputDir, entryName + ".js")
407
- ),
408
- isPlugin: false,
409
- actionNames: extracted.map((a) => a.actionName)
410
- });
411
- }
412
- } catch {
413
- getLogger().warn(`[Build] Could not extract actions from ${sourcePath}`);
414
- }
415
- }
416
- try {
417
- const scannedPlugins = await scanPlugins({ projectRoot });
418
- for (const [packageName, plugin] of scannedPlugins) {
419
- if (plugin.isLocal) continue;
420
- plugins.push({ name: plugin.manifest.name, packageName });
421
- const pluginActions = plugin.manifest.actions;
422
- if (pluginActions && pluginActions.length > 0) {
423
- actions.push({
424
- serverModule: "",
425
- packageName,
426
- isPlugin: true,
427
- actionNames: pluginActions.map(
428
- (a) => typeof a === "string" ? a : a.name
429
- )
430
- });
431
- getLogger().info(
432
- `[Build] Plugin actions from ${packageName}: ${pluginActions.length}`
433
- );
434
- }
435
- }
436
- } catch (err) {
437
- getLogger().warn(`[Build] Plugin action scan failed: ${err.message}`);
438
- }
439
- return { actions, plugins };
440
- }
441
- async function writeRouteManifest(manifest, buildDir) {
442
- const manifestPath = path.join(buildDir, "route-manifest.json");
443
- await fs.writeFile(manifestPath, JSON.stringify(manifest, null, 2));
444
- getLogger().info(
445
- `[Build] Route manifest written: ${manifest.routes.length} routes, ${manifest.routes.reduce((n, r) => n + r.instances.length, 0)} instances`
446
- );
447
- }
10
+ import crypto from "node:crypto";
448
11
  async function generateSitemap(manifest, baseUrl, outputPath) {
449
12
  const base = baseUrl.replace(/\/$/, "");
450
13
  const urls = [];
@@ -486,929 +49,6 @@ function buildUrlFromManifest(pattern, params) {
486
49
  function escapeXml(str) {
487
50
  return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
488
51
  }
489
- createRequire(import.meta.url);
490
- async function scanPluginRoutes(projectRoot, projectRoutes) {
491
- const logger = getLogger();
492
- const plugins = await scanPlugins({ projectRoot });
493
- const projectPaths = new Set(projectRoutes.map((r) => r.rawRoute));
494
- const pluginRoutes = [];
495
- for (const [, plugin] of plugins) {
496
- if (plugin.isLocal) continue;
497
- if (!plugin.manifest.routes) continue;
498
- for (const route of plugin.manifest.routes) {
499
- if (projectPaths.has(route.path)) {
500
- logger.info(
501
- `[Routes] Plugin "${plugin.manifest.name}" route ${route.path} skipped — project route takes precedence`
502
- );
503
- continue;
504
- }
505
- const jayHtmlPath = resolvePluginExport(plugin.pluginPath, route.jayHtml);
506
- if (!jayHtmlPath) {
507
- logger.warn(
508
- `[Routes] Plugin "${plugin.manifest.name}" route ${route.path}: jayHtml "${route.jayHtml}" not found`
509
- );
510
- continue;
511
- }
512
- const compPath = resolvePluginModule(plugin.pluginPath);
513
- const componentExport = route.component;
514
- pluginRoutes.push({
515
- segments: parseRouteSegments(route.path),
516
- rawRoute: route.path,
517
- jayHtmlPath,
518
- compPath,
519
- componentExport,
520
- packageName: plugin.packageName,
521
- ...route.devOnly === true && { devOnly: true }
522
- });
523
- logger.info(
524
- `[Routes] Plugin "${plugin.manifest.name}" provides route ${route.path}${route.devOnly ? " (dev-only)" : ""}`
525
- );
526
- }
527
- }
528
- return pluginRoutes;
529
- }
530
- function resolvePluginExport(pluginPath, exportSubpath) {
531
- const normalized = exportSubpath.replace(/^\.\//, "");
532
- const packageJsonPath = path.join(pluginPath, "package.json");
533
- try {
534
- const packageJson = JSON.parse(fs$1.readFileSync(packageJsonPath, "utf-8"));
535
- if (packageJson.exports) {
536
- const exportKey = "./" + normalized;
537
- const exportValue = packageJson.exports[exportKey];
538
- if (exportValue) {
539
- const resolved = typeof exportValue === "string" ? exportValue : exportValue.default || exportValue.import || exportValue.require;
540
- if (resolved) return path.join(pluginPath, resolved);
541
- }
542
- }
543
- } catch {
544
- }
545
- for (const dir of ["dist", "lib", ""]) {
546
- const candidate = path.join(pluginPath, dir, normalized);
547
- try {
548
- fs$1.accessSync(candidate);
549
- return candidate;
550
- } catch {
551
- }
552
- }
553
- return void 0;
554
- }
555
- function resolvePluginModule(pluginPath) {
556
- const pkgJsonPath = path.join(pluginPath, "package.json");
557
- try {
558
- const pkg = JSON.parse(fs$1.readFileSync(pkgJsonPath, "utf-8"));
559
- const mainExport = pkg.exports?.["."];
560
- const mainPath = typeof mainExport === "string" ? mainExport : mainExport?.default || mainExport?.import || pkg.main;
561
- if (mainPath) {
562
- const resolved = path.join(pluginPath, mainPath);
563
- if (fs$1.existsSync(resolved)) return resolved;
564
- }
565
- } catch {
566
- }
567
- return path.join(pluginPath, "dist", "index.js");
568
- }
569
- async function compileServerElement(jayHtmlContent, jayHtmlFilename, jayHtmlDir, outputPath, projectRoot, tsConfigFilePath, sourceDir, minifyCss = true) {
570
- const jayFile = await parseJayFile(
571
- jayHtmlContent,
572
- jayHtmlFilename,
573
- jayHtmlDir,
574
- { relativePath: tsConfigFilePath },
575
- JAY_IMPORT_RESOLVER,
576
- projectRoot,
577
- sourceDir
578
- );
579
- const parsedJayFile = checkValidationErrors(jayFile);
580
- const serverElementCode = checkValidationErrors(generateServerElementFile(parsedJayFile));
581
- const outputDir = path.dirname(outputPath);
582
- await fs.mkdir(outputDir, { recursive: true });
583
- const tsPath = outputPath.replace(/\.js$/, ".ts");
584
- await fs.writeFile(tsPath, serverElementCode, "utf-8");
585
- const jayOptions = { tsConfigFilePath };
586
- await build({
587
- root: projectRoot,
588
- publicDir: false,
589
- plugins: [jayRuntime(jayOptions)],
590
- build: {
591
- outDir: outputDir,
592
- emptyOutDir: false,
593
- minify: false,
594
- ssr: true,
595
- rollupOptions: {
596
- input: { [path.basename(outputPath, ".js")]: tsPath },
597
- external: [/^node:/, /^@jay-framework\//],
598
- output: {
599
- entryFileNames: "[name].js",
600
- format: "es"
601
- }
602
- }
603
- },
604
- logLevel: "warn"
605
- });
606
- await fs.rm(tsPath, { force: true });
607
- let cssFile;
608
- let cssImports;
609
- const css = parsedJayFile.css;
610
- if (css) {
611
- cssImports = extractCssImportUrls(css);
612
- const cssContent = minifyCss ? (await transform(css, { loader: "css", minify: true })).code : css;
613
- const hash = createHash("sha256").update(cssContent).digest("hex").slice(0, 8);
614
- const baseName = path.basename(outputPath, ".server-element.js");
615
- const cssFilename = `${baseName}-${hash}.css`;
616
- await fs.writeFile(path.join(outputDir, cssFilename), cssContent, "utf-8");
617
- cssFile = cssFilename;
618
- }
619
- getLogger().info(`[Build] Compiled server element: ${path.basename(outputPath)}`);
620
- return { cssFile, cssImports, headMeta: parsedJayFile.headMeta };
621
- }
622
- async function compileRouteServerElement(jayHtmlPath, outputPath, projectRoot, tsConfigFilePath, minifyCss = true) {
623
- const jayHtmlContent = await fs.readFile(jayHtmlPath, "utf-8");
624
- const sourceDir = path.dirname(jayHtmlPath);
625
- const outputDir = path.dirname(outputPath);
626
- let jayHtml = injectHeadfullFSTemplates(jayHtmlContent, sourceDir, JAY_IMPORT_RESOLVER);
627
- jayHtml = resolveJayHtmlPaths(jayHtml, sourceDir, outputDir);
628
- return compileServerElement(
629
- jayHtml,
630
- path.basename(jayHtmlPath),
631
- outputDir,
632
- outputPath,
633
- projectRoot,
634
- tsConfigFilePath,
635
- sourceDir,
636
- minifyCss
637
- );
638
- }
639
- async function compileRouteHydrateScript(jayHtmlPath, outputDir, projectRoot, tsConfigFilePath, minify = true) {
640
- const jayHtmlContent = await fs.readFile(jayHtmlPath, "utf-8");
641
- const sourceDir = path.dirname(jayHtmlPath);
642
- let jayHtml = injectHeadfullFSTemplates(jayHtmlContent, sourceDir, JAY_IMPORT_RESOLVER);
643
- jayHtml = resolveJayHtmlPaths(jayHtml, sourceDir, outputDir);
644
- const jayFile = await parseJayFile(
645
- jayHtml,
646
- path.basename(jayHtmlPath),
647
- outputDir,
648
- { relativePath: tsConfigFilePath },
649
- JAY_IMPORT_RESOLVER,
650
- projectRoot,
651
- sourceDir
652
- );
653
- const parsedJayFile = checkValidationErrors(jayFile);
654
- const hydrateCode = checkValidationErrors(
655
- generateElementHydrateFile(parsedJayFile, RuntimeMode.MainTrusted)
656
- );
657
- await fs.mkdir(outputDir, { recursive: true });
658
- const tsPath = path.join(outputDir, "route.hydrate.ts");
659
- await fs.writeFile(tsPath, hydrateCode, "utf-8");
660
- const jayOptions = { tsConfigFilePath };
661
- await build({
662
- root: projectRoot,
663
- publicDir: false,
664
- plugins: [...jayStackCompiler(jayOptions)],
665
- build: {
666
- outDir: outputDir,
667
- emptyOutDir: false,
668
- minify,
669
- manifest: "route-hydrate-manifest.json",
670
- rollupOptions: {
671
- input: { "route.hydrate": tsPath },
672
- external: (id) => id.startsWith("@jay-framework/"),
673
- output: {
674
- entryFileNames: "[name]-[hash].js",
675
- format: "es"
676
- },
677
- preserveEntrySignatures: "exports-only"
678
- }
679
- },
680
- logLevel: "warn"
681
- });
682
- await fs.rm(tsPath, { force: true });
683
- const manifestPath = path.join(outputDir, "route-hydrate-manifest.json");
684
- const manifest = JSON.parse(await fs.readFile(manifestPath, "utf-8"));
685
- await fs.rm(manifestPath, { force: true });
686
- const entryKey = Object.keys(manifest).find((k) => manifest[k].isEntry);
687
- if (!entryKey) throw new Error("No entry in route hydrate manifest");
688
- const jsFile = manifest[entryKey].file;
689
- getLogger().info(`[Build] Compiled route hydrate script: ${jsFile}`);
690
- return { jsFile };
691
- }
692
- function resolveJayHtmlPaths(html, sourceDir, targetDir) {
693
- const root = parse(html, {
694
- comment: true,
695
- blockTextElements: { script: true, style: true }
696
- });
697
- const rewrite = (el, attr) => {
698
- const val = el.getAttribute(attr);
699
- if (val && (val.startsWith("./") || val.startsWith("../"))) {
700
- const abs = path.resolve(sourceDir, val);
701
- let rel = path.relative(targetDir, abs);
702
- if (!rel.startsWith(".")) rel = "./" + rel;
703
- el.setAttribute(attr, rel);
704
- }
705
- };
706
- for (const el of root.querySelectorAll('script[type="application/jay-data"]')) {
707
- rewrite(el, "contract");
708
- }
709
- for (const el of root.querySelectorAll('script[type="application/jay-headless"]')) {
710
- rewrite(el, "src");
711
- rewrite(el, "contract");
712
- }
713
- for (const el of root.querySelectorAll('script[type="application/jay-headfull"]')) {
714
- rewrite(el, "src");
715
- rewrite(el, "contract");
716
- }
717
- for (const el of root.querySelectorAll('link[rel="stylesheet"]')) {
718
- rewrite(el, "href");
719
- }
720
- return root.toString();
721
- }
722
- function extractCssImportUrls(css) {
723
- const imports = [];
724
- const re = /@import\s*(?:url\(\s*['"]?([^'")]+)['"]?\s*\)|['"]([^'"]+)['"])/g;
725
- let match;
726
- while ((match = re.exec(css)) !== null) {
727
- const url = match[1] || match[2];
728
- if (url.startsWith("https://")) {
729
- imports.push(url);
730
- }
731
- }
732
- return imports;
733
- }
734
- async function generateRouteHydrationEntry(options) {
735
- const {
736
- hydrateImport,
737
- pageModulePath,
738
- pageExportName = "page",
739
- trackByMap,
740
- outputPath,
741
- keyedParts = [],
742
- clientInits = []
743
- } = options;
744
- const partImports = keyedParts.map((p, i) => `import { ${p.exportName} as keyedPart${i} } from '${p.modulePath}';`).join("\n");
745
- const hasPageModule = pageModulePath && pageExportName;
746
- const pagePartExpr = hasPageModule ? `pagePart && pagePart.comp ? { comp: pagePart.comp, contextMarkers: pagePart.contexts || [] } : null` : `null`;
747
- const partsArray = [
748
- pagePartExpr,
749
- ...keyedParts.map(
750
- (p, i) => `keyedPart${i} && keyedPart${i}.comp ? { comp: keyedPart${i}.comp, contextMarkers: keyedPart${i}.contexts || [], key: '${p.key}' } : null`
751
- )
752
- ];
753
- const pageImport = hasPageModule ? `import { ${pageExportName} as pagePart } from '${pageModulePath}';` : "";
754
- const initImports = clientInits.map((ci, i) => `import { ${ci.exportName} as clientInit${i} } from '${ci.modulePath}';`).join("\n");
755
- const initCalls = clientInits.map(
756
- (ci, i) => ` if (clientInit${i}?._clientInit) await clientInit${i}._clientInit(clientInitData['${ci.key}'] || {});`
757
- ).join("\n");
758
- const code = `import { hydrateCompositeJayComponent } from '@jay-framework/stack-client-runtime';
759
- import { deepMergeViewStates } from '@jay-framework/view-state-merge';
760
- import { hydrate } from '${hydrateImport}';
761
- ${pageImport}
762
- ${partImports}
763
- ${initImports}
764
-
765
- const trackByMap = ${JSON.stringify(trackByMap)};
766
-
767
- export async function init(slowViewState, fastViewState, fastCarryForward, clientInitData) {
768
- ${initCalls}
769
- const viewState = deepMergeViewStates(slowViewState, fastViewState, trackByMap);
770
- const target = document.getElementById('target');
771
- const rootElement = target.firstElementChild;
772
- const parts = [
773
- ${partsArray.join(",\n ")}
774
- ].filter(p => p !== null);
775
- const pageComp = hydrateCompositeJayComponent(
776
- hydrate, viewState, fastCarryForward,
777
- parts, trackByMap, rootElement
778
- );
779
- return pageComp({});
780
- }
781
- `;
782
- const outputDir = path.dirname(outputPath);
783
- await fs.mkdir(outputDir, { recursive: true });
784
- await fs.writeFile(outputPath, code, "utf-8");
785
- getLogger().info(`[Build] Generated route hydration entry: ${path.basename(outputPath)}`);
786
- }
787
- async function buildInstanceClient(hydrateEntryPath, instanceId, outputDir, projectRoot, jayOptions, minify = true, pagesRoot, buildDir) {
788
- const logger = getLogger();
789
- await fs.mkdir(outputDir, { recursive: true });
790
- const fullJayOptions = {
791
- ...jayOptions,
792
- ...pagesRoot && buildDir ? { pagesRoot, buildFolder: buildDir } : {}
793
- };
794
- await build({
795
- root: projectRoot,
796
- publicDir: false,
797
- plugins: [...jayStackCompiler(fullJayOptions)],
798
- build: {
799
- outDir: outputDir,
800
- emptyOutDir: false,
801
- minify,
802
- manifest: `${instanceId}-manifest.json`,
803
- rollupOptions: {
804
- input: { [instanceId]: hydrateEntryPath },
805
- external: (id) => id.startsWith("@jay-framework/") || id === "jay-route-hydrate",
806
- output: {
807
- entryFileNames: "[name]-[hash].js",
808
- chunkFileNames: "chunks/[name]-[hash].js",
809
- assetFileNames: "[name]-[hash].[ext]",
810
- format: "es"
811
- },
812
- preserveEntrySignatures: "exports-only"
813
- }
814
- },
815
- logLevel: "warn"
816
- });
817
- const manifestPath = path.join(outputDir, `${instanceId}-manifest.json`);
818
- const manifest = JSON.parse(await fs.readFile(manifestPath, "utf-8"));
819
- await fs.rm(manifestPath, { force: true });
820
- const entryKey = Object.keys(manifest).find((k) => manifest[k].isEntry);
821
- if (!entryKey) {
822
- throw new Error(`No entry found in instance build manifest for ${instanceId}`);
823
- }
824
- const entry = manifest[entryKey];
825
- const result = {
826
- jsFile: entry.file,
827
- cssFile: entry.css?.[0]
828
- };
829
- logger.info(`[Build] Client bundle: ${result.jsFile}`);
830
- return result;
831
- }
832
- function crossProductParams(parts) {
833
- if (parts.length === 0) return [];
834
- if (parts.length === 1) return parts[0].values;
835
- const logger = getLogger();
836
- for (let i = 0; i < parts.length; i++) {
837
- for (let j = i + 1; j < parts.length; j++) {
838
- for (const key of parts[i].keys) {
839
- if (parts[j].keys.has(key)) {
840
- logger.warn(
841
- `[Build] Multiple loadParams provide key "${key}" — using first provider`
842
- );
843
- }
844
- }
845
- }
846
- }
847
- let result = parts[0].values;
848
- for (let i = 1; i < parts.length; i++) {
849
- const next = parts[i].values;
850
- const combined = [];
851
- for (const a of result) {
852
- for (const b of next) {
853
- combined.push({ ...a, ...b });
854
- }
855
- }
856
- result = combined;
857
- }
858
- return result;
859
- }
860
- function paramsMatchInferred(params, inferredParams, optionalSegments) {
861
- return Object.entries(inferredParams).every(([k, v]) => {
862
- if (optionalSegments?.has(k)) return true;
863
- return params[k] === v;
864
- });
865
- }
866
- function computeSpecificity(route) {
867
- const dynamicCount = (route.rawRoute.match(/\[/g) || []).length;
868
- const inferredCount = route.inferredParams ? Object.keys(route.inferredParams).length : 0;
869
- const unresolvedCount = Math.max(0, dynamicCount - inferredCount);
870
- return 0 - unresolvedCount;
871
- }
872
- function buildUrl(route, params) {
873
- return route.rawRoute.replace(/\[\[(\w+)\]\]/g, (_, name) => {
874
- const value = params[name];
875
- if (!value) return "";
876
- if (route.inferredParams?.[name] === value) return "";
877
- return value;
878
- }).replace(/\[(\w+)\]/g, (_, name) => params[name] || "").replace(/\/\/+/g, "/").replace(/\/$/, "") || "/";
879
- }
880
- function materializeRouteParams(routes, loadParamsResults) {
881
- const entries = [];
882
- for (const route of routes) {
883
- const specificity = computeSpecificity(route);
884
- if (!route.hasDynamicParams) {
885
- const params = route.inferredParams || {};
886
- entries.push({ route, params, url: route.rawRoute, specificity });
887
- continue;
888
- }
889
- const allParams = loadParamsResults.get(route) || [];
890
- for (const params of allParams) {
891
- if (route.inferredParams && !paramsMatchInferred(params, route.inferredParams, route.optionalSegments)) {
892
- continue;
893
- }
894
- let mergedParams = params;
895
- if (route.inferredParams) {
896
- mergedParams = { ...params };
897
- for (const [k, v] of Object.entries(route.inferredParams)) {
898
- if (!(k in mergedParams)) {
899
- mergedParams[k] = v;
900
- }
901
- }
902
- }
903
- const url = buildUrl(route, mergedParams);
904
- entries.push({ route, params: mergedParams, url, specificity });
905
- }
906
- }
907
- return entries;
908
- }
909
- function dedupeByUrl(entries) {
910
- const logger = getLogger();
911
- const byUrl = /* @__PURE__ */ new Map();
912
- for (const entry of entries) {
913
- const existing = byUrl.get(entry.url);
914
- if (!existing) {
915
- byUrl.set(entry.url, entry);
916
- } else if (entry.specificity > existing.specificity) {
917
- byUrl.set(entry.url, entry);
918
- }
919
- }
920
- const deduped = [...byUrl.values()];
921
- if (deduped.length < entries.length) {
922
- logger.info(
923
- `[Build] Deduplication: ${entries.length} materialized → ${deduped.length} unique URLs`
924
- );
925
- }
926
- return deduped;
927
- }
928
- async function discoverPluginClientPackages(projectRoot) {
929
- const projectRequire = createRequire(path.join(projectRoot, "package.json"));
930
- const seen = /* @__PURE__ */ new Set();
931
- const result = [];
932
- async function walk(pkgName) {
933
- if (seen.has(pkgName)) return;
934
- seen.add(pkgName);
935
- try {
936
- const mainPath = projectRequire.resolve(pkgName);
937
- let pkgDir = path.dirname(mainPath);
938
- while (pkgDir !== path.dirname(pkgDir)) {
939
- const candidate = path.join(pkgDir, "package.json");
940
- try {
941
- const pkgJson = JSON.parse(await fs.readFile(candidate, "utf-8"));
942
- if (pkgJson.name === pkgName) {
943
- if (pkgJson.exports?.["./client"]) {
944
- result.push(`${pkgName}/client`);
945
- }
946
- for (const dep of Object.keys(pkgJson.dependencies || {})) {
947
- if (dep.startsWith("@jay-framework/")) {
948
- await walk(dep);
949
- }
950
- }
951
- break;
952
- }
953
- } catch {
954
- }
955
- pkgDir = path.dirname(pkgDir);
956
- }
957
- } catch {
958
- }
959
- }
960
- try {
961
- const projectPkg = JSON.parse(
962
- await fs.readFile(path.join(projectRoot, "package.json"), "utf-8")
963
- );
964
- for (const dep of Object.keys(projectPkg.dependencies || {})) {
965
- if (dep.startsWith("@jay-framework/")) {
966
- await walk(dep);
967
- }
968
- }
969
- } catch {
970
- }
971
- return result;
972
- }
973
- async function collectFiles(dir, base = "") {
974
- const entries = await fs.readdir(dir, { withFileTypes: true });
975
- const paths = [];
976
- for (const entry of entries) {
977
- const rel = base ? `${base}/${entry.name}` : entry.name;
978
- if (entry.isDirectory()) {
979
- paths.push(...await collectFiles(path.join(dir, entry.name), rel));
980
- } else {
981
- paths.push(rel);
982
- }
983
- }
984
- return paths;
985
- }
986
- async function computeBuildHash(buildDir) {
987
- const files = (await collectFiles(buildDir)).filter((f) => f !== "backend/build-metadata.json");
988
- files.sort();
989
- const hash = createHash("sha256");
990
- for (const file of files) {
991
- hash.update(file);
992
- hash.update(await fs.readFile(path.join(buildDir, file)));
993
- }
994
- return hash.digest("hex").slice(0, 12);
995
- }
996
- async function buildVersion(options) {
997
- const logger = getLogger();
998
- const buildDir = path.join(options.buildRoot, `v${options.version}`);
999
- const backendDir = path.join(buildDir, "backend");
1000
- const frontendDir = path.join(buildDir, "frontend");
1001
- logger.important(`[Build] Starting production build v${options.version}`);
1002
- logger.important(`[Build] Project: ${options.projectRoot}`);
1003
- await fs.mkdir(backendDir, { recursive: true });
1004
- await fs.mkdir(frontendDir, { recursive: true });
1005
- const { entries, routes } = await discoverServerEntries(options.projectRoot, options.pagesRoot);
1006
- const serverOutputDir = path.join(backendDir, "server");
1007
- await buildServerCode(
1008
- entries,
1009
- { tsConfigFilePath: options.tsConfigFilePath },
1010
- serverOutputDir,
1011
- options.projectRoot
1012
- );
1013
- const pluginClientPackages = await discoverPluginClientPackages(options.projectRoot);
1014
- if (pluginClientPackages.length > 0) {
1015
- logger.important(`[Build] Plugin client packages: ${pluginClientPackages.join(", ")}`);
1016
- }
1017
- const sharedOutputDir = path.join(frontendDir, "shared");
1018
- const { manifest: sharedManifest } = await buildSharedChunks(
1019
- sharedOutputDir,
1020
- options.projectRoot,
1021
- options.minify ?? true,
1022
- pluginClientPackages
1023
- );
1024
- const { actions, plugins } = await discoverActions(
1025
- entries.actions,
1026
- serverOutputDir,
1027
- backendDir,
1028
- options.projectRoot
1029
- );
1030
- const { discoverPluginsWithInit, sortPluginsByDependencies } = await import("@jay-framework/stack-server-runtime");
1031
- try {
1032
- const pluginsWithInit = sortPluginsByDependencies(
1033
- await discoverPluginsWithInit({ projectRoot: options.projectRoot })
1034
- );
1035
- for (const pluginInit of pluginsWithInit) {
1036
- try {
1037
- let modulePath;
1038
- if (pluginInit.isLocal) {
1039
- const pluginDirName = path.basename(pluginInit.pluginPath);
1040
- modulePath = path.join(
1041
- serverOutputDir,
1042
- "plugins",
1043
- pluginDirName,
1044
- `${pluginInit.initModule}.js`
1045
- );
1046
- } else {
1047
- modulePath = pluginInit.packageName;
1048
- }
1049
- const pluginModule = await import(modulePath);
1050
- const init = pluginModule.init || pluginModule[pluginInit.initExport || "init"];
1051
- if (init?._serverInit) {
1052
- logger.info(`[Build] Running plugin init: ${pluginInit.name}`);
1053
- await init._serverInit();
1054
- }
1055
- } catch (err) {
1056
- logger.warn(`[Build] Plugin init failed: ${pluginInit.name}: ${err.message}`);
1057
- }
1058
- }
1059
- } catch {
1060
- }
1061
- if (entries.init) {
1062
- const initModulePath = path.join(serverOutputDir, "init.js");
1063
- try {
1064
- const initModule = await import(initModulePath);
1065
- const init = initModule.init || initModule.default;
1066
- if (init?._serverInit) {
1067
- logger.info("[Build] Running server init...");
1068
- await init._serverInit();
1069
- }
1070
- } catch (err) {
1071
- logger.error(`[Build] Failed to run init: ${err}`);
1072
- throw err;
1073
- }
1074
- }
1075
- const clientInits = [];
1076
- try {
1077
- const allPluginsWithInit = sortPluginsByDependencies(
1078
- await discoverPluginsWithInit({ projectRoot: options.projectRoot })
1079
- );
1080
- for (const pluginInit of allPluginsWithInit) {
1081
- if (pluginInit.isLocal) continue;
1082
- const clientImportPath = `${pluginInit.packageName}/client`;
1083
- try {
1084
- const clientModule = await import(clientImportPath);
1085
- const init = clientModule[pluginInit.initExport || "init"] || clientModule.init;
1086
- if (init?._clientInit) {
1087
- clientInits.push({
1088
- modulePath: clientImportPath,
1089
- exportName: pluginInit.initExport || "init",
1090
- key: pluginInit.name
1091
- });
1092
- }
1093
- } catch {
1094
- }
1095
- }
1096
- } catch (err) {
1097
- logger.warn(`[Build] Client init discovery failed: ${err.message}`);
1098
- }
1099
- if (clientInits.length > 0) {
1100
- logger.important(`[Build] Client inits: ${clientInits.map((ci) => ci.key).join(", ")}`);
1101
- }
1102
- if (entries.init) {
1103
- clientInits.push({
1104
- modulePath: entries.init,
1105
- exportName: "init",
1106
- key: "project"
1107
- });
1108
- }
1109
- const instanceCtx = {
1110
- projectRoot: options.projectRoot,
1111
- pagesRoot: options.pagesRoot,
1112
- buildDir,
1113
- backendDir,
1114
- frontendDir,
1115
- jayOptions: { tsConfigFilePath: options.tsConfigFilePath },
1116
- tsConfigFilePath: options.tsConfigFilePath,
1117
- minify: options.minify ?? true,
1118
- clientInits
1119
- };
1120
- const pluginRoutes = await scanPluginRoutes(options.projectRoot, routes);
1121
- const allRoutes = [...routes, ...pluginRoutes];
1122
- const routeEntries = allRoutes.map((route) => {
1123
- let serverModule = "";
1124
- if (route.compPath) {
1125
- if (route.componentExport) {
1126
- serverModule = route.compPath;
1127
- } else {
1128
- const relativePath = path.relative(options.projectRoot, route.compPath);
1129
- serverModule = relativePath.replace(/^src\//, "server/").replace(/\.ts$/, ".js").replace(/\[/g, "_").replace(/\]/g, "_");
1130
- }
1131
- }
1132
- const entry = buildRouteEntry(route, serverModule);
1133
- if (route.componentExport) {
1134
- entry.isPlugin = true;
1135
- }
1136
- return { route, entry };
1137
- });
1138
- let instanceCount = 0;
1139
- let totalExpected = 0;
1140
- function logInstance(routeName, params) {
1141
- instanceCount++;
1142
- const paramStr = Object.keys(params).length > 0 ? ` (${Object.entries(params).map(([k, v]) => `${k}=${v}`).join(", ")})` : "";
1143
- logger.important(`[Build] ${instanceCount}/${totalExpected} ${routeName}${paramStr}`);
1144
- }
1145
- async function loadPageModule(entry) {
1146
- if (!entry.serverModule) return {};
1147
- if (entry.isPlugin) return import(entry.serverModule);
1148
- return import(path.join(backendDir, entry.serverModule));
1149
- }
1150
- const routeInfos = routeEntries.map((re) => {
1151
- const optionalNames = re.route.segments.filter((s) => typeof s !== "string" && s.type === JayRouteParamType.optional).map((s) => s.name);
1152
- return {
1153
- rawRoute: re.route.rawRoute,
1154
- inferredParams: re.route.inferredParams,
1155
- optionalSegments: optionalNames.length > 0 ? new Set(optionalNames) : void 0,
1156
- hasDynamicParams: re.route.segments.some((s) => typeof s !== "string"),
1157
- routeEntry: re
1158
- };
1159
- });
1160
- const loadParamsCache = /* @__PURE__ */ new Map();
1161
- const loadParamsResults = /* @__PURE__ */ new Map();
1162
- for (const info of routeInfos) {
1163
- if (!info.hasDynamicParams) continue;
1164
- const { route, entry } = info.routeEntry;
1165
- let pageModule;
1166
- try {
1167
- pageModule = await loadPageModule(entry);
1168
- } catch (err) {
1169
- logger.error(`[Build] Failed to load page module ${entry.serverModule}: ${err}`);
1170
- continue;
1171
- }
1172
- const pageParts = await loadProductionPageParts(
1173
- route,
1174
- pageModule,
1175
- await fs.readFile(route.jayHtmlPath, "utf-8"),
1176
- options.projectRoot,
1177
- options.tsConfigFilePath,
1178
- serverOutputDir
1179
- );
1180
- const partsWithLoadParams = pageParts.parts.filter((p) => p.compDefinition?.loadParams);
1181
- if (partsWithLoadParams.length === 0) continue;
1182
- const paramParts = [];
1183
- for (const part of partsWithLoadParams) {
1184
- const propsKey = JSON.stringify(part.headlessProps ?? {});
1185
- const cacheKey = `${part.key ?? ""}:${propsKey}`;
1186
- if (!loadParamsCache.has(cacheKey)) {
1187
- logger.important(`[Build] Loading params for ${route.rawRoute}...`);
1188
- const partParams = [];
1189
- let batchIndex = 0;
1190
- for await (const batch of runLoadParams([part])) {
1191
- partParams.push(...batch);
1192
- batchIndex++;
1193
- if (batchIndex > 1) {
1194
- logger.important(`[Build] ...${partParams.length} params so far`);
1195
- }
1196
- }
1197
- loadParamsCache.set(cacheKey, partParams);
1198
- }
1199
- const cached = loadParamsCache.get(cacheKey);
1200
- const keys = new Set(cached.flatMap((p) => Object.keys(p)));
1201
- paramParts.push({ keys, values: cached });
1202
- }
1203
- loadParamsResults.set(info, crossProductParams(paramParts));
1204
- }
1205
- const materialized = materializeRouteParams(routeInfos, loadParamsResults);
1206
- const deduped = dedupeByUrl(materialized);
1207
- totalExpected = deduped.length;
1208
- const byRoute = /* @__PURE__ */ new Map();
1209
- for (const materialized2 of deduped) {
1210
- const info = materialized2.route;
1211
- if (!byRoute.has(info)) byRoute.set(info, []);
1212
- byRoute.get(info).push(materialized2.params);
1213
- }
1214
- for (const [info] of byRoute) {
1215
- const { route, entry } = info.routeEntry;
1216
- if (!route.jayHtmlPath) continue;
1217
- const routeDir = route.rawRoute.replace(/^\//, "") || "index";
1218
- const frontendSafeRouteDir = routeDir.replace(/\[/g, "%5B").replace(/\]/g, "%5D");
1219
- const backendRouteDir = path.join(backendDir, "pre-rendered", routeDir);
1220
- const frontendRouteDir = path.join(frontendDir, "pages", frontendSafeRouteDir);
1221
- await fs.mkdir(backendRouteDir, { recursive: true });
1222
- await fs.mkdir(frontendRouteDir, { recursive: true });
1223
- const serverElementPath = path.join(backendRouteDir, "route.server-element.js");
1224
- try {
1225
- const seResult = await compileRouteServerElement(
1226
- route.jayHtmlPath,
1227
- serverElementPath,
1228
- options.projectRoot,
1229
- options.tsConfigFilePath,
1230
- options.minify ?? true
1231
- );
1232
- entry.serverElementPath = path.relative(backendDir, serverElementPath);
1233
- if (seResult.cssFile) {
1234
- const srcCss = path.join(backendRouteDir, seResult.cssFile);
1235
- const dstCss = path.join(frontendRouteDir, seResult.cssFile);
1236
- try {
1237
- await fs.rename(srcCss, dstCss);
1238
- } catch {
1239
- await fs.copyFile(srcCss, dstCss);
1240
- await fs.rm(srcCss, { force: true });
1241
- }
1242
- entry.routeCssPath = path.relative(frontendDir, dstCss);
1243
- }
1244
- if (seResult.cssImports?.length) {
1245
- entry.cssImports = seResult.cssImports;
1246
- }
1247
- if (seResult.headMeta) {
1248
- entry.headMeta = seResult.headMeta;
1249
- const robotsMeta = seResult.headMeta.meta?.find(
1250
- (m) => m.name === "robots" && m.content?.some((p) => p.kind === "static" && p.value.includes("noindex"))
1251
- );
1252
- if (robotsMeta) {
1253
- entry.noIndex = true;
1254
- }
1255
- }
1256
- logger.important(`[Build] Route server element: ${routeDir}`);
1257
- } catch (err) {
1258
- logger.error(`[Build] Route server element FAILED ${route.rawRoute}: ${err.message}`);
1259
- }
1260
- try {
1261
- const hydrateResult = await compileRouteHydrateScript(
1262
- route.jayHtmlPath,
1263
- frontendRouteDir,
1264
- options.projectRoot,
1265
- options.tsConfigFilePath,
1266
- options.minify ?? true
1267
- );
1268
- entry.routeHydratePath = path.relative(
1269
- frontendDir,
1270
- path.join(frontendRouteDir, hydrateResult.jsFile)
1271
- );
1272
- logger.important(`[Build] Route hydrate script: ${routeDir}`);
1273
- } catch (err) {
1274
- logger.error(`[Build] Route hydrate script FAILED ${route.rawRoute}: ${err.message}`);
1275
- continue;
1276
- }
1277
- try {
1278
- const ROUTE_HYDRATE_KEY = "jay-route-hydrate";
1279
- const exportName = route.componentExport || "page";
1280
- let pageModulePath = "";
1281
- if (route.compPath) {
1282
- if (route.componentExport) {
1283
- const pkgName = route.packageName || route.compPath;
1284
- pageModulePath = `${pkgName}/client`;
1285
- } else {
1286
- pageModulePath = "./" + path.relative(frontendRouteDir, route.compPath);
1287
- }
1288
- }
1289
- const pageParts = await loadProductionPageParts(
1290
- route,
1291
- {},
1292
- await fs.readFile(route.jayHtmlPath, "utf-8"),
1293
- options.projectRoot,
1294
- options.tsConfigFilePath,
1295
- path.join(backendDir, "server")
1296
- );
1297
- entry.trackByMap = pageParts.serverTrackByMap || pageParts.clientTrackByMap;
1298
- const entryPath = path.join(frontendRouteDir, "route.entry.ts");
1299
- await generateRouteHydrationEntry({
1300
- hydrateImport: ROUTE_HYDRATE_KEY,
1301
- pageModulePath,
1302
- pageExportName: exportName,
1303
- trackByMap: pageParts.clientTrackByMap || {},
1304
- outputPath: entryPath,
1305
- keyedParts: pageParts.keyedPartModules,
1306
- clientInits
1307
- });
1308
- const clientResult = await buildInstanceClient(
1309
- entryPath,
1310
- "route.client",
1311
- frontendRouteDir,
1312
- options.projectRoot,
1313
- { tsConfigFilePath: options.tsConfigFilePath },
1314
- options.minify ?? true,
1315
- options.pagesRoot,
1316
- buildDir
1317
- );
1318
- await fs.rm(entryPath, { force: true });
1319
- entry.routeClientBundlePath = path.relative(
1320
- frontendDir,
1321
- path.join(frontendRouteDir, clientResult.jsFile)
1322
- );
1323
- logger.important(`[Build] Route client bundle: ${routeDir}`);
1324
- } catch (err) {
1325
- logger.error(`[Build] Route client bundle FAILED ${route.rawRoute}: ${err.message}`);
1326
- }
1327
- }
1328
- for (const [info, paramsList] of byRoute) {
1329
- const { route, entry } = info.routeEntry;
1330
- let pageModule;
1331
- try {
1332
- pageModule = await loadPageModule(entry);
1333
- } catch (err) {
1334
- logger.error(`[Build] Failed to load page module ${entry.serverModule}: ${err}`);
1335
- continue;
1336
- }
1337
- if (paramsList.length > 1 || info.hasDynamicParams) {
1338
- logger.important(
1339
- `[Build] Route ${route.rawRoute}: ${paramsList.length} param combinations`
1340
- );
1341
- }
1342
- for (const params of paramsList) {
1343
- try {
1344
- const result = await buildInstance(
1345
- route,
1346
- params,
1347
- pageModule,
1348
- instanceCtx,
1349
- entry.serverElementPath,
1350
- entry.routeCssPath,
1351
- entry.routeHydratePath,
1352
- entry.routeClientBundlePath
1353
- );
1354
- if (result.status === "success") {
1355
- entry.instances.push(result.instanceEntry);
1356
- if (result.contracts.length > 0 && !entry.contracts) {
1357
- entry.contracts = result.contracts;
1358
- }
1359
- logInstance(route.rawRoute || "/", params);
1360
- } else {
1361
- logger.warn(
1362
- `[Build] Skipped ${route.rawRoute} (${JSON.stringify(params)}): ${result.reason}`
1363
- );
1364
- totalExpected--;
1365
- }
1366
- } catch (err) {
1367
- instanceCount++;
1368
- logger.error(
1369
- `[Build] ${instanceCount}/${totalExpected} FAILED ${route.rawRoute || "/"} (${JSON.stringify(params)}): ${err.message}`
1370
- );
1371
- }
1372
- }
1373
- }
1374
- const manifest = {
1375
- version: options.version,
1376
- projectRoot: options.projectRoot,
1377
- sharedManifest,
1378
- routes: routeEntries.map((r) => r.entry),
1379
- actions,
1380
- plugins
1381
- };
1382
- await writeRouteManifest(manifest, backendDir);
1383
- const publicFolder = path.join(options.projectRoot, "public");
1384
- try {
1385
- await fs.access(publicFolder);
1386
- await fs.cp(publicFolder, frontendDir, { recursive: true });
1387
- logger.info("[Build] Copied public/ contents to frontend/");
1388
- } catch {
1389
- }
1390
- if (options.siteBaseUrl) {
1391
- const sitemapPath = path.join(frontendDir, "sitemap.xml");
1392
- const urlCount = await generateSitemap(manifest, options.siteBaseUrl, sitemapPath);
1393
- logger.important(`[Build] Sitemap generated: ${urlCount} URLs`);
1394
- }
1395
- const sourceHash = await computeBuildHash(buildDir);
1396
- const metadata = {
1397
- version: options.version,
1398
- sourceHash,
1399
- buildTimestamp: (/* @__PURE__ */ new Date()).toISOString(),
1400
- nodeVersion: process.version,
1401
- instanceCount
1402
- };
1403
- await fs.writeFile(
1404
- path.join(backendDir, "build-metadata.json"),
1405
- JSON.stringify(metadata, null, 2)
1406
- );
1407
- logger.important(
1408
- `[Build] Done! ${instanceCount} instances built in ${buildDir} (hash: ${sourceHash})`
1409
- );
1410
- return manifest;
1411
- }
1412
52
  function toFetchRequest(req) {
1413
53
  const url = new URL(req.url || "/", `http://${req.headers.host}`);
1414
54
  const headers = new Headers();
@@ -1627,6 +267,107 @@ async function discoverWebhooks(projectRoot, serverBuildDir) {
1627
267
  }
1628
268
  return webhooks;
1629
269
  }
270
+ function hashParams(params, suffix) {
271
+ const sorted = Object.keys(params).sort().reduce(
272
+ (acc, key) => {
273
+ acc[key] = params[key];
274
+ return acc;
275
+ },
276
+ {}
277
+ );
278
+ const json = JSON.stringify(sorted);
279
+ if (json === "{}" && !suffix) return "";
280
+ const input = suffix ? json + ":" + suffix : json;
281
+ return "_" + crypto.createHash("md5").update(input).digest("hex").substring(0, 8);
282
+ }
283
+ async function rebuildInstance(route, params, backendDir, rebuildSuffix) {
284
+ const logger = getLogger();
285
+ const routeDir = route.pattern.replace(/^\//, "") || "index";
286
+ const paramHash = hashParams(params, rebuildSuffix);
287
+ const instanceId = `page${paramHash}`;
288
+ const backendInstanceDir = path.join(backendDir, "pre-rendered", routeDir);
289
+ await fs.mkdir(backendInstanceDir, { recursive: true });
290
+ const pagePartsConfigPath = path.join(backendInstanceDir, "page-parts.json");
291
+ const artifacts = new FilesystemArtifactStore(backendDir);
292
+ let pageParts;
293
+ try {
294
+ pageParts = await loadPagePartsFromConfig(
295
+ path.relative(backendDir, pagePartsConfigPath),
296
+ artifacts
297
+ );
298
+ } catch (err) {
299
+ return { status: "skipped", reason: `Failed to load page-parts.json: ${err.message}` };
300
+ }
301
+ const slowPhase = new DevSlowlyChangingPhase();
302
+ const slowResult = await slowPhase.runSlowlyForPage(
303
+ params,
304
+ { params },
305
+ pageParts.parts,
306
+ pageParts.discoveredInstances,
307
+ pageParts.headlessInstanceComponents
308
+ );
309
+ if (slowResult.kind !== "PhaseOutput") {
310
+ if (slowResult.kind === "ClientError" || slowResult.kind === "Redirect") {
311
+ return {
312
+ status: "skipped",
313
+ reason: `${slowResult.kind} ${slowResult.status ?? ""} ${slowResult.message ?? ""}`.trim()
314
+ };
315
+ }
316
+ return { status: "skipped", reason: `Slow render returned: ${slowResult.kind}` };
317
+ }
318
+ const slowViewState = slowResult.rendered;
319
+ const carryForward = slowResult.carryForward;
320
+ if (pageParts.discoveredInstances && pageParts.discoveredInstances.length > 0 && pageParts.headlessInstanceComponents.length > 0) {
321
+ const instanceSlowResult = await slowRenderInstances(
322
+ pageParts.discoveredInstances,
323
+ pageParts.headlessInstanceComponents,
324
+ {
325
+ pageViewState: slowViewState,
326
+ pageParams: params,
327
+ pageProps: { language: "en", url: "" }
328
+ }
329
+ );
330
+ if (instanceSlowResult) {
331
+ const existingInstances = carryForward.__instances || {
332
+ discovered: [],
333
+ carryForwards: {}
334
+ };
335
+ carryForward.__instances = {
336
+ discovered: [
337
+ ...existingInstances.discovered,
338
+ ...instanceSlowResult.instancePhaseData.discovered
339
+ ],
340
+ carryForwards: {
341
+ ...existingInstances.carryForwards,
342
+ ...instanceSlowResult.instancePhaseData.carryForwards
343
+ },
344
+ slowViewStates: {
345
+ ...existingInstances.slowViewStates || {},
346
+ ...instanceSlowResult.instancePhaseData.slowViewStates
347
+ }
348
+ };
349
+ }
350
+ }
351
+ if (pageParts.forEachInstances && pageParts.forEachInstances.length > 0) {
352
+ const existingInstances = carryForward.__instances || {
353
+ discovered: [],
354
+ carryForwards: {}
355
+ };
356
+ existingInstances.forEachInstances = pageParts.forEachInstances;
357
+ carryForward.__instances = existingInstances;
358
+ }
359
+ const cachePath = path.join(backendInstanceDir, `${instanceId}.cache.json`);
360
+ await fs.writeFile(cachePath, JSON.stringify({ slowViewState, carryForward }), "utf-8");
361
+ logger.info(`[Rebuild] Instance data: ${routeDir}/${instanceId}`);
362
+ const instanceEntry = {
363
+ params,
364
+ cachePath: path.relative(backendDir, cachePath),
365
+ serverElementPath: route.serverElementPath || "",
366
+ clientBundlePath: route.routeClientBundlePath || "",
367
+ clientCssPath: route.routeCssPath
368
+ };
369
+ return { status: "success", instanceEntry };
370
+ }
1630
371
  function resolveContractToRoutes(manifest, contractName) {
1631
372
  return manifest.routes.filter((r) => r.contracts && r.contracts.includes(contractName));
1632
373
  }
@@ -1652,17 +393,6 @@ async function rebuild(options) {
1652
393
  const frontendDir = path.join(buildDir, "frontend");
1653
394
  await initializeServices(backendDir, options.projectRoot, "Rebuild");
1654
395
  const rebuildSuffix = Date.now().toString(36);
1655
- const instanceCtx = {
1656
- projectRoot: options.projectRoot,
1657
- pagesRoot: options.pagesRoot,
1658
- buildDir,
1659
- backendDir,
1660
- frontendDir,
1661
- jayOptions: { tsConfigFilePath: options.tsConfigFilePath },
1662
- tsConfigFilePath: options.tsConfigFilePath,
1663
- minify: options.minify ?? true,
1664
- rebuildSuffix
1665
- };
1666
396
  const result = { affected: 0, rebuilt: 0, errors: [] };
1667
397
  const orphanedFiles = [];
1668
398
  for (const route of affectedRoutes) {
@@ -1674,25 +404,13 @@ async function rebuild(options) {
1674
404
  result.affected++;
1675
405
  const params = instance.params;
1676
406
  const oldFiles = collectInstanceFiles(instance);
1677
- let pageModule;
1678
- try {
1679
- pageModule = await loadRouteModule(route, buildDir);
1680
- } catch (err) {
1681
- result.errors.push({
1682
- route: route.pattern,
1683
- params,
1684
- error: `Failed to load module: ${err.message}`
1685
- });
1686
- continue;
1687
- }
1688
- const jayRoute = await resolveJayRouteFromManifest(route, options);
1689
407
  try {
1690
- const buildResult = await buildInstance(jayRoute, params, pageModule, instanceCtx);
408
+ const buildResult = await rebuildInstance(route, params, backendDir, rebuildSuffix);
1691
409
  if (buildResult.status !== "success") {
1692
410
  result.errors.push({
1693
411
  route: route.pattern,
1694
412
  params,
1695
- error: buildResult.reason
413
+ error: buildResult.reason || "Unknown error"
1696
414
  });
1697
415
  continue;
1698
416
  }
@@ -1793,41 +511,6 @@ async function rebuildContract(options) {
1793
511
  target: { mode: "contract", contractName: options.contractName, params: options.params }
1794
512
  });
1795
513
  }
1796
- async function loadRouteModule(route, buildDir) {
1797
- if (!route.serverModule) return {};
1798
- if (route.isPlugin) return import(route.serverModule);
1799
- return import(path.join(buildDir, route.serverModule));
1800
- }
1801
- async function resolveJayRouteFromManifest(route, options) {
1802
- const routeDir = route.pattern.replace(/^\//, "") || "index";
1803
- const jayHtmlPath = path.join(options.pagesRoot, routeDir, "page.jay-html");
1804
- let resolvedJayHtmlPath = jayHtmlPath;
1805
- if (route.isPlugin && route.serverModule) {
1806
- try {
1807
- const pluginModule = await import(route.serverModule);
1808
- const comp = pluginModule[route.componentExport || "page"];
1809
- if (comp?.jayHtmlPath) {
1810
- resolvedJayHtmlPath = comp.jayHtmlPath;
1811
- }
1812
- } catch {
1813
- }
1814
- }
1815
- return {
1816
- rawRoute: route.pattern,
1817
- segments: route.segments.map((s) => {
1818
- if (s.type === "static") return s.value;
1819
- return { name: s.value, type: segmentTypeMap[s.type] };
1820
- }),
1821
- jayHtmlPath: resolvedJayHtmlPath,
1822
- compPath: route.isPlugin ? route.serverModule : void 0,
1823
- componentExport: route.componentExport
1824
- };
1825
- }
1826
- const segmentTypeMap = {
1827
- param: 0,
1828
- catchAll: 1,
1829
- optional: 2
1830
- };
1831
514
  function paramsMatch(instanceParams, targetParams) {
1832
515
  return Object.entries(targetParams).every(([key, value]) => instanceParams[key] === value);
1833
516
  }
@@ -2006,7 +689,6 @@ function readBody(req) {
2006
689
  }
2007
690
  export {
2008
691
  FilesystemArtifactStore,
2009
- buildVersion,
2010
692
  cleanupOrphanedFiles,
2011
693
  fetchActionRequest,
2012
694
  fetchPageRequest,
@@ -2015,6 +697,7 @@ export {
2015
697
  initializeServices,
2016
698
  c as initializeServicesFromModules,
2017
699
  isActionRequest,
700
+ loadPagePartsFromConfig,
2018
701
  matchRequest,
2019
702
  rebuild,
2020
703
  rebuildContract,