@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.d.ts +63 -75
- package/dist/index.js +1061 -1002
- package/dist/init-services-BKVwxzYb.js +796 -0
- package/dist/serve-index-9aAW1pbg.d.ts +179 -0
- package/dist/serve-index.d.ts +3 -0
- package/dist/serve-index.js +23 -0
- package/package.json +24 -13
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 {
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
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 {
|
|
22
|
-
import {
|
|
23
|
-
import
|
|
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 !== "
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
504
|
-
|
|
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
|
-
|
|
507
|
-
const serverBuildDir = path.join(ctx.
|
|
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
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
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
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
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
|
-
|
|
623
|
-
|
|
624
|
-
|
|
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
|
-
|
|
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]
|
|
635
|
-
const serverElementPath = path.join(
|
|
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
|
-
|
|
690
|
-
serverElementPath: path.relative(ctx.
|
|
691
|
-
clientBundlePath:
|
|
692
|
-
clientCssPath:
|
|
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
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
}
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
});
|
|
891
|
-
}
|
|
892
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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,
|
|
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(
|
|
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
|
-
|
|
1273
|
-
const
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
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
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
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
|
|
1331
|
-
const
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
}
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
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
|
-
|
|
1330
|
+
res.end();
|
|
1363
1331
|
}
|
|
1364
|
-
function
|
|
1365
|
-
const
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
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
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
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
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
if (
|
|
1391
|
-
|
|
1392
|
-
|
|
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
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
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
|
|
1417
|
-
const
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
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
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
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
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
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
|
|
1584
|
-
const
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
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
|
-
|
|
1600
|
-
|
|
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
|
|
1604
|
-
const
|
|
1605
|
-
const
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
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
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
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
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
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 (
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
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
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
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
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
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
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
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
|
|
1702
|
-
const
|
|
1703
|
-
|
|
1704
|
-
|
|
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
|
|
1713
|
-
logger.error(
|
|
1714
|
-
`[Server] Failed to load action module ${entry.serverModule}: ${err.message}`
|
|
1715
|
-
);
|
|
1702
|
+
} catch {
|
|
1716
1703
|
}
|
|
1717
1704
|
}
|
|
1718
|
-
|
|
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
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
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
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
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
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
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
|
|
1743
|
+
async function cleanupOrphanedFiles(buildRoot, version) {
|
|
1765
1744
|
const logger = getLogger();
|
|
1766
|
-
const buildDir = path.join(
|
|
1767
|
-
const
|
|
1768
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
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
|
-
|
|
1804
|
-
|
|
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
|
-
|
|
1810
|
-
|
|
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
|
-
|
|
1814
|
-
req
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
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
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
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
|
-
|
|
1871
|
+
res.writeHead(404);
|
|
1872
|
+
res.end("Not Found");
|
|
1837
1873
|
} catch (err) {
|
|
1838
|
-
logger.error(`[
|
|
1874
|
+
logger.error(`[Renderer] Error: ${err.message}`);
|
|
1839
1875
|
if (!res.headersSent) {
|
|
1840
|
-
res.writeHead(500);
|
|
1841
|
-
res.end(
|
|
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
|
-
`[
|
|
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
|
-
|
|
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
|
};
|