@jay-framework/production-server 0.17.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 +86 -0
- package/dist/index.js +1854 -0
- package/package.json +42 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1854 @@
|
|
|
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
|
+
import { build } from "vite";
|
|
8
|
+
import { jayStackCompiler, extractActionsFromSource } from "@jay-framework/compiler-jay-stack";
|
|
9
|
+
import { scanRoutes, JayRouteParamType, parseRouteSegments } from "@jay-framework/stack-route-scanner";
|
|
10
|
+
import { getLogger } from "@jay-framework/logger";
|
|
11
|
+
import path from "node:path";
|
|
12
|
+
import fs from "node:fs/promises";
|
|
13
|
+
import { createRequire } from "node:module";
|
|
14
|
+
import { parseJayFile, JAY_IMPORT_RESOLVER, injectHeadfullFSTemplates, discoverHeadlessInstances, assignCoordinatesToJayHtml, generateServerElementFile, parseContract, slowRenderTransform, resolveHeadlessInstances } from "@jay-framework/compiler-jay-html";
|
|
15
|
+
import { DevSlowlyChangingPhase, slowRenderInstances, scanPlugins, runLoadParams, renderFastChangingData, mergeHeadTags, serializeHeadTags, getClientInitData, actionRegistry, setClientInitData } from "@jay-framework/stack-server-runtime";
|
|
16
|
+
import { checkValidationErrors } from "@jay-framework/compiler-shared";
|
|
17
|
+
import { jayRuntime } from "@jay-framework/vite-plugin";
|
|
18
|
+
import crypto from "node:crypto";
|
|
19
|
+
import fs$1 from "node:fs";
|
|
20
|
+
import http from "node:http";
|
|
21
|
+
import { deepMergeViewStates } from "@jay-framework/view-state-merge";
|
|
22
|
+
import { asyncSwapScript } from "@jay-framework/ssr-runtime";
|
|
23
|
+
import { isJayAction, isJayStreamAction } from "@jay-framework/fullstack-component";
|
|
24
|
+
async function discoverServerEntries(projectRoot, pagesRoot) {
|
|
25
|
+
const logger = getLogger();
|
|
26
|
+
const routes = await scanRoutes(pagesRoot, {
|
|
27
|
+
jayHtmlFilename: "page.jay-html",
|
|
28
|
+
compFilename: "page.ts"
|
|
29
|
+
});
|
|
30
|
+
const pages = {};
|
|
31
|
+
for (const route of routes) {
|
|
32
|
+
if (route.compPath) {
|
|
33
|
+
const relativePath = path.relative(projectRoot, route.compPath);
|
|
34
|
+
const entryName = relativePath.replace(/^src\//, "").replace(/\.ts$/, "");
|
|
35
|
+
pages[entryName] = route.compPath;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
const actions = {};
|
|
39
|
+
const actionsDir = path.join(projectRoot, "src", "actions");
|
|
40
|
+
try {
|
|
41
|
+
const files = await fs.readdir(actionsDir);
|
|
42
|
+
for (const file of files) {
|
|
43
|
+
if (file.endsWith(".actions.ts")) {
|
|
44
|
+
const entryName = "actions/" + file.replace(/\.ts$/, "");
|
|
45
|
+
actions[entryName] = path.join(actionsDir, file);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
} catch {
|
|
49
|
+
}
|
|
50
|
+
for (const subDir of ["plugins", "components"]) {
|
|
51
|
+
const scanDir = path.join(projectRoot, "src", subDir);
|
|
52
|
+
try {
|
|
53
|
+
const dirs = await fs.readdir(scanDir, { withFileTypes: true });
|
|
54
|
+
for (const dir of dirs) {
|
|
55
|
+
if (!dir.isDirectory())
|
|
56
|
+
continue;
|
|
57
|
+
const dirPath = path.join(scanDir, dir.name);
|
|
58
|
+
const files = await fs.readdir(dirPath);
|
|
59
|
+
for (const file of files) {
|
|
60
|
+
if (file.endsWith(".ts") && !file.endsWith(".d.ts") && file !== "init.ts" && file !== "page.ts") {
|
|
61
|
+
const entryName = `${subDir}/${dir.name}/${file.replace(/\.ts$/, "")}`;
|
|
62
|
+
pages[entryName] = path.join(dirPath, file);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
} catch {
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
let init;
|
|
70
|
+
const initPaths = [
|
|
71
|
+
path.join(projectRoot, "src", "lib", "init.ts"),
|
|
72
|
+
path.join(projectRoot, "src", "init.ts")
|
|
73
|
+
];
|
|
74
|
+
for (const initPath of initPaths) {
|
|
75
|
+
try {
|
|
76
|
+
await fs.access(initPath);
|
|
77
|
+
init = initPath;
|
|
78
|
+
break;
|
|
79
|
+
} catch {
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
const entries = { init, pages, actions };
|
|
83
|
+
logger.info(
|
|
84
|
+
`[Build] Discovered: ${Object.keys(pages).length} pages, ${Object.keys(actions).length} actions, init: ${init ? "yes" : "no"}`
|
|
85
|
+
);
|
|
86
|
+
return { entries, routes };
|
|
87
|
+
}
|
|
88
|
+
async function buildServerCode(entries, jayOptions, outputDir, projectRoot) {
|
|
89
|
+
const logger = getLogger();
|
|
90
|
+
logger.info("[Build] Compiling server code...");
|
|
91
|
+
const input = {};
|
|
92
|
+
if (entries.init) {
|
|
93
|
+
input["init"] = entries.init;
|
|
94
|
+
}
|
|
95
|
+
for (const [name, filePath] of Object.entries(entries.pages)) {
|
|
96
|
+
input[name] = filePath;
|
|
97
|
+
}
|
|
98
|
+
for (const [name, filePath] of Object.entries(entries.actions)) {
|
|
99
|
+
input[name] = filePath;
|
|
100
|
+
}
|
|
101
|
+
if (Object.keys(input).length === 0) {
|
|
102
|
+
logger.info("[Build] No server entries to compile");
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
await build({
|
|
106
|
+
root: projectRoot,
|
|
107
|
+
plugins: [...jayStackCompiler(jayOptions)],
|
|
108
|
+
build: {
|
|
109
|
+
ssr: true,
|
|
110
|
+
outDir: outputDir,
|
|
111
|
+
emptyOutDir: true,
|
|
112
|
+
minify: false,
|
|
113
|
+
rollupOptions: {
|
|
114
|
+
input,
|
|
115
|
+
external: [
|
|
116
|
+
/^node:/,
|
|
117
|
+
/^@jay-framework\//,
|
|
118
|
+
// Plugin packages are pre-compiled, externalize them
|
|
119
|
+
/^@wix\//
|
|
120
|
+
],
|
|
121
|
+
output: {
|
|
122
|
+
entryFileNames: "[name].js",
|
|
123
|
+
chunkFileNames: "chunks/[name]-[hash].js",
|
|
124
|
+
format: "es"
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
},
|
|
128
|
+
logLevel: "warn"
|
|
129
|
+
});
|
|
130
|
+
logger.info(`[Build] Server code compiled to ${outputDir}`);
|
|
131
|
+
}
|
|
132
|
+
createRequire(import.meta.url);
|
|
133
|
+
const FRAMEWORK_PACKAGES = [
|
|
134
|
+
"@jay-framework/stack-client-runtime",
|
|
135
|
+
"@jay-framework/component",
|
|
136
|
+
"@jay-framework/reactive",
|
|
137
|
+
"@jay-framework/runtime",
|
|
138
|
+
"@jay-framework/view-state-merge",
|
|
139
|
+
"@jay-framework/fullstack-component"
|
|
140
|
+
];
|
|
141
|
+
async function buildSharedChunks(outputDir, _projectRoot, minify = true, pluginClientPackages = []) {
|
|
142
|
+
const logger = getLogger();
|
|
143
|
+
logger.info("[Build] Building shared client chunks...");
|
|
144
|
+
await fs.mkdir(outputDir, { recursive: true });
|
|
145
|
+
const allPackages = [...FRAMEWORK_PACKAGES, ...pluginClientPackages];
|
|
146
|
+
const entries = {};
|
|
147
|
+
for (const pkg of allPackages) {
|
|
148
|
+
const varName = pkgToVarName(pkg);
|
|
149
|
+
const entryPath = path.join(outputDir, `_shared_${varName}.js`);
|
|
150
|
+
await fs.writeFile(entryPath, `export * from '${pkg}';
|
|
151
|
+
`);
|
|
152
|
+
entries[varName] = entryPath;
|
|
153
|
+
}
|
|
154
|
+
const dedupePackages = [...allPackages, "@jay-framework/list-compare"];
|
|
155
|
+
await build({
|
|
156
|
+
build: {
|
|
157
|
+
outDir: outputDir,
|
|
158
|
+
emptyOutDir: true,
|
|
159
|
+
minify,
|
|
160
|
+
manifest: "vite-manifest.json",
|
|
161
|
+
rollupOptions: {
|
|
162
|
+
input: entries,
|
|
163
|
+
output: {
|
|
164
|
+
entryFileNames: "[name]-[hash].js",
|
|
165
|
+
chunkFileNames: "[name]-[hash].js",
|
|
166
|
+
format: "es"
|
|
167
|
+
},
|
|
168
|
+
preserveEntrySignatures: "exports-only"
|
|
169
|
+
}
|
|
170
|
+
},
|
|
171
|
+
resolve: {
|
|
172
|
+
dedupe: dedupePackages
|
|
173
|
+
},
|
|
174
|
+
logLevel: "warn"
|
|
175
|
+
});
|
|
176
|
+
for (const pkg of allPackages) {
|
|
177
|
+
const varName = pkgToVarName(pkg);
|
|
178
|
+
await fs.rm(path.join(outputDir, `_shared_${varName}.js`), { force: true });
|
|
179
|
+
}
|
|
180
|
+
const manifest = await parseViteManifest(outputDir, allPackages);
|
|
181
|
+
logger.info(`[Build] Shared chunks built: ${Object.keys(manifest).length} entries`);
|
|
182
|
+
return { manifest, outputDir };
|
|
183
|
+
}
|
|
184
|
+
function pkgToVarName(pkg) {
|
|
185
|
+
return pkg.replace("@jay-framework/", "").replace(/[/-]/g, "_");
|
|
186
|
+
}
|
|
187
|
+
async function parseViteManifest(outputDir, packages) {
|
|
188
|
+
const viteManifestPath = path.join(outputDir, "vite-manifest.json");
|
|
189
|
+
const raw = JSON.parse(await fs.readFile(viteManifestPath, "utf-8"));
|
|
190
|
+
const varNameToPackage = /* @__PURE__ */ new Map();
|
|
191
|
+
for (const pkg of packages) {
|
|
192
|
+
varNameToPackage.set(pkgToVarName(pkg), pkg);
|
|
193
|
+
}
|
|
194
|
+
const manifest = {};
|
|
195
|
+
for (const [, entry] of Object.entries(raw)) {
|
|
196
|
+
if (!entry.isEntry)
|
|
197
|
+
continue;
|
|
198
|
+
const outputBase = path.basename(entry.file, ".js");
|
|
199
|
+
for (const [varName, pkg] of varNameToPackage) {
|
|
200
|
+
if (outputBase.startsWith(varName)) {
|
|
201
|
+
manifest[pkg] = entry.file;
|
|
202
|
+
break;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
const sharedManifestPath = path.join(outputDir, "shared-manifest.json");
|
|
207
|
+
await fs.writeFile(sharedManifestPath, JSON.stringify(manifest, null, 2));
|
|
208
|
+
await fs.rm(viteManifestPath, { force: true });
|
|
209
|
+
return manifest;
|
|
210
|
+
}
|
|
211
|
+
const require2 = createRequire(import.meta.url);
|
|
212
|
+
async function loadProductionPageParts(route, pageModule, jayHtmlContent, projectRoot, tsConfigFilePath, serverBuildDir) {
|
|
213
|
+
const exportName = route.componentExport || "page";
|
|
214
|
+
const compDefinition = pageModule[exportName] ?? pageModule.default;
|
|
215
|
+
const parts = compDefinition ? [{ compDefinition, clientImport: "", clientPart: "" }] : [];
|
|
216
|
+
const dirName = path.dirname(route.jayHtmlPath);
|
|
217
|
+
const fileName = path.basename(route.jayHtmlPath);
|
|
218
|
+
const jayHtmlWithValidations = await parseJayFile(
|
|
219
|
+
jayHtmlContent,
|
|
220
|
+
fileName,
|
|
221
|
+
dirName,
|
|
222
|
+
{ relativePath: tsConfigFilePath },
|
|
223
|
+
JAY_IMPORT_RESOLVER,
|
|
224
|
+
projectRoot
|
|
225
|
+
);
|
|
226
|
+
const jayHtml = checkValidationErrors(jayHtmlWithValidations);
|
|
227
|
+
const headlessInstanceComponents = [];
|
|
228
|
+
const keyedPartModules = [];
|
|
229
|
+
const headlessImports = jayHtml.headlessImports ?? [];
|
|
230
|
+
getLogger().info(
|
|
231
|
+
`[Build] headlessImports for ${fileName}: ${headlessImports.length}, keys: ${Object.keys(jayHtml).join(",")}`
|
|
232
|
+
);
|
|
233
|
+
for (const headlessImport of headlessImports) {
|
|
234
|
+
const module = headlessImport.codeLink.module;
|
|
235
|
+
const name = headlessImport.codeLink.names[0].name;
|
|
236
|
+
const isLocalModule = module[0] === "." || module[0] === "/";
|
|
237
|
+
let modulePath;
|
|
238
|
+
if (isLocalModule) {
|
|
239
|
+
const sourcePath = path.resolve(dirName, module);
|
|
240
|
+
if (serverBuildDir) {
|
|
241
|
+
const relativeToSrc = path.relative(path.join(projectRoot, "src"), sourcePath);
|
|
242
|
+
let compiledPath = path.join(serverBuildDir, relativeToSrc);
|
|
243
|
+
compiledPath = compiledPath.replace(/\.ts$/, ".js");
|
|
244
|
+
if (!compiledPath.endsWith(".js")) {
|
|
245
|
+
const indexPath = path.join(compiledPath, "index.js");
|
|
246
|
+
try {
|
|
247
|
+
await fs.access(indexPath);
|
|
248
|
+
compiledPath = indexPath;
|
|
249
|
+
} catch {
|
|
250
|
+
compiledPath += ".js";
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
modulePath = compiledPath;
|
|
254
|
+
} else {
|
|
255
|
+
modulePath = sourcePath;
|
|
256
|
+
}
|
|
257
|
+
} else {
|
|
258
|
+
modulePath = require2.resolve(module, { paths: [dirName] });
|
|
259
|
+
}
|
|
260
|
+
const headlessModule = await import(modulePath);
|
|
261
|
+
const headlessCompDef = headlessModule[name];
|
|
262
|
+
if (headlessImport.key) {
|
|
263
|
+
const clientModulePath = isLocalModule ? path.resolve(dirName, module) : `${module}/client`;
|
|
264
|
+
parts.push({
|
|
265
|
+
key: headlessImport.key,
|
|
266
|
+
compDefinition: headlessCompDef,
|
|
267
|
+
clientImport: "",
|
|
268
|
+
clientPart: "",
|
|
269
|
+
contractInfo: headlessImport.contract ? {
|
|
270
|
+
contractName: headlessImport.contract.name,
|
|
271
|
+
metadata: headlessImport.metadata
|
|
272
|
+
} : void 0
|
|
273
|
+
});
|
|
274
|
+
keyedPartModules.push({
|
|
275
|
+
key: headlessImport.key,
|
|
276
|
+
modulePath: clientModulePath,
|
|
277
|
+
exportName: name
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
if (!headlessImport.key && headlessImport.contract) {
|
|
281
|
+
headlessInstanceComponents.push({
|
|
282
|
+
contractName: headlessImport.contractName,
|
|
283
|
+
compDefinition: headlessCompDef,
|
|
284
|
+
contract: headlessImport.contract
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
const headlessContracts = (jayHtml.headlessImports ?? []).filter((hi) => hi.contract && hi.key).map((hi) => ({
|
|
289
|
+
key: hi.key,
|
|
290
|
+
contract: hi.contract,
|
|
291
|
+
contractPath: hi.contractPath
|
|
292
|
+
}));
|
|
293
|
+
const jayHtmlForDiscovery = injectHeadfullFSTemplates(
|
|
294
|
+
jayHtmlContent,
|
|
295
|
+
dirName,
|
|
296
|
+
JAY_IMPORT_RESOLVER
|
|
297
|
+
);
|
|
298
|
+
let discoveredInstances = [];
|
|
299
|
+
let forEachInstances = [];
|
|
300
|
+
if (headlessInstanceComponents.length > 0) {
|
|
301
|
+
const firstDiscovery = discoverHeadlessInstances(jayHtmlForDiscovery);
|
|
302
|
+
const contractNames = new Set(headlessInstanceComponents.map((c) => c.contractName));
|
|
303
|
+
const withCoords = assignCoordinatesToJayHtml(
|
|
304
|
+
firstDiscovery.preRenderedJayHtml,
|
|
305
|
+
contractNames
|
|
306
|
+
);
|
|
307
|
+
const finalDiscovery = discoverHeadlessInstances(withCoords);
|
|
308
|
+
discoveredInstances = finalDiscovery.instances;
|
|
309
|
+
forEachInstances = finalDiscovery.forEachInstances;
|
|
310
|
+
}
|
|
311
|
+
return {
|
|
312
|
+
parts,
|
|
313
|
+
headlessContracts,
|
|
314
|
+
headlessInstanceComponents,
|
|
315
|
+
discoveredInstances,
|
|
316
|
+
forEachInstances,
|
|
317
|
+
keyedPartModules,
|
|
318
|
+
serverTrackByMap: jayHtml.serverTrackByMap,
|
|
319
|
+
clientTrackByMap: jayHtml.clientTrackByMap
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
async function compileServerElement(jayHtmlContent, jayHtmlFilename, jayHtmlDir, outputPath, projectRoot, tsConfigFilePath, sourceDir) {
|
|
323
|
+
const jayFile = await parseJayFile(
|
|
324
|
+
jayHtmlContent,
|
|
325
|
+
jayHtmlFilename,
|
|
326
|
+
jayHtmlDir,
|
|
327
|
+
{ relativePath: tsConfigFilePath },
|
|
328
|
+
JAY_IMPORT_RESOLVER,
|
|
329
|
+
projectRoot,
|
|
330
|
+
sourceDir
|
|
331
|
+
);
|
|
332
|
+
const parsedJayFile = checkValidationErrors(jayFile);
|
|
333
|
+
const serverElementCode = checkValidationErrors(generateServerElementFile(parsedJayFile));
|
|
334
|
+
const outputDir = path.dirname(outputPath);
|
|
335
|
+
await fs.mkdir(outputDir, { recursive: true });
|
|
336
|
+
const tsPath = outputPath.replace(/\.js$/, ".ts");
|
|
337
|
+
await fs.writeFile(tsPath, serverElementCode, "utf-8");
|
|
338
|
+
const jayOptions = { tsConfigFilePath };
|
|
339
|
+
await build({
|
|
340
|
+
root: projectRoot,
|
|
341
|
+
plugins: [jayRuntime(jayOptions)],
|
|
342
|
+
build: {
|
|
343
|
+
outDir: outputDir,
|
|
344
|
+
emptyOutDir: false,
|
|
345
|
+
minify: false,
|
|
346
|
+
ssr: true,
|
|
347
|
+
rollupOptions: {
|
|
348
|
+
input: { [path.basename(outputPath, ".js")]: tsPath },
|
|
349
|
+
external: [/^node:/, /^@jay-framework\//],
|
|
350
|
+
output: {
|
|
351
|
+
entryFileNames: "[name].js",
|
|
352
|
+
format: "es"
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
},
|
|
356
|
+
logLevel: "warn"
|
|
357
|
+
});
|
|
358
|
+
await fs.rm(tsPath, { force: true });
|
|
359
|
+
let cssFile;
|
|
360
|
+
const css = parsedJayFile.css;
|
|
361
|
+
if (css) {
|
|
362
|
+
const cssFilename = path.basename(outputPath, ".server-element.js") + ".css";
|
|
363
|
+
const cssPath = path.join(outputDir, cssFilename);
|
|
364
|
+
await fs.writeFile(cssPath, css, "utf-8");
|
|
365
|
+
cssFile = cssFilename;
|
|
366
|
+
}
|
|
367
|
+
getLogger().info(`[Build] Compiled server element: ${path.basename(outputPath)}`);
|
|
368
|
+
return { cssFile };
|
|
369
|
+
}
|
|
370
|
+
async function generateHydrationEntry(options) {
|
|
371
|
+
const {
|
|
372
|
+
jayHtmlPath,
|
|
373
|
+
pageModulePath,
|
|
374
|
+
pageExportName = "page",
|
|
375
|
+
slowViewState,
|
|
376
|
+
trackByMap,
|
|
377
|
+
outputPath,
|
|
378
|
+
keyedParts = [],
|
|
379
|
+
clientInits = []
|
|
380
|
+
} = options;
|
|
381
|
+
const hydrateImport = `${jayHtmlPath}?jay-hydrate`;
|
|
382
|
+
const partImports = keyedParts.map((p, i) => `import { ${p.exportName} as keyedPart${i} } from '${p.modulePath}';`).join("\n");
|
|
383
|
+
const hasPageModule = pageModulePath && pageExportName;
|
|
384
|
+
const pagePartExpr = hasPageModule ? `pagePart && pagePart.comp ? { comp: pagePart.comp, contextMarkers: pagePart.contexts || [] } : null` : `null`;
|
|
385
|
+
const partsArray = [
|
|
386
|
+
pagePartExpr,
|
|
387
|
+
...keyedParts.map(
|
|
388
|
+
(p, i) => `keyedPart${i} && keyedPart${i}.comp ? { comp: keyedPart${i}.comp, contextMarkers: keyedPart${i}.contexts || [], key: '${p.key}' } : null`
|
|
389
|
+
)
|
|
390
|
+
];
|
|
391
|
+
const pageImport = hasPageModule ? `import { ${pageExportName} as pagePart } from '${pageModulePath}';` : "";
|
|
392
|
+
const initImports = clientInits.map((ci, i) => `import { ${ci.exportName} as clientInit${i} } from '${ci.modulePath}';`).join("\n");
|
|
393
|
+
const initCalls = clientInits.map(
|
|
394
|
+
(ci, i) => ` if (clientInit${i}?._clientInit) await clientInit${i}._clientInit(clientInitData['${ci.key}'] || {});`
|
|
395
|
+
).join("\n");
|
|
396
|
+
const hasClientInit = clientInits.length > 0;
|
|
397
|
+
const code = `import { hydrateCompositeJayComponent } from '@jay-framework/stack-client-runtime';
|
|
398
|
+
import { deepMergeViewStates } from '@jay-framework/view-state-merge';
|
|
399
|
+
import { hydrate } from '${hydrateImport}';
|
|
400
|
+
${pageImport}
|
|
401
|
+
${partImports}
|
|
402
|
+
${initImports}
|
|
403
|
+
|
|
404
|
+
const slowViewState = ${JSON.stringify(slowViewState)};
|
|
405
|
+
const trackByMap = ${JSON.stringify(trackByMap)};
|
|
406
|
+
|
|
407
|
+
export async function init(fastViewState, fastCarryForward${hasClientInit ? ", clientInitData" : ""}) {
|
|
408
|
+
${initCalls}
|
|
409
|
+
const viewState = deepMergeViewStates(slowViewState, fastViewState, trackByMap);
|
|
410
|
+
const target = document.getElementById('target');
|
|
411
|
+
const rootElement = target.firstElementChild;
|
|
412
|
+
const parts = [
|
|
413
|
+
${partsArray.join(",\n ")}
|
|
414
|
+
].filter(p => p !== null);
|
|
415
|
+
const pageComp = hydrateCompositeJayComponent(
|
|
416
|
+
hydrate, viewState, fastCarryForward,
|
|
417
|
+
parts, trackByMap, rootElement
|
|
418
|
+
);
|
|
419
|
+
return pageComp({});
|
|
420
|
+
}
|
|
421
|
+
`;
|
|
422
|
+
const outputDir = path.dirname(outputPath);
|
|
423
|
+
await fs.mkdir(outputDir, { recursive: true });
|
|
424
|
+
await fs.writeFile(outputPath, code, "utf-8");
|
|
425
|
+
getLogger().info(`[Build] Generated hydration entry: ${path.basename(outputPath)}`);
|
|
426
|
+
}
|
|
427
|
+
async function buildInstanceClient(hydrateEntryPath, instanceId, outputDir, projectRoot, jayOptions, minify = true, pagesRoot, buildDir) {
|
|
428
|
+
const logger = getLogger();
|
|
429
|
+
await fs.mkdir(outputDir, { recursive: true });
|
|
430
|
+
const fullJayOptions = {
|
|
431
|
+
...jayOptions,
|
|
432
|
+
...pagesRoot && buildDir ? { pagesRoot, buildFolder: buildDir } : {}
|
|
433
|
+
};
|
|
434
|
+
await build({
|
|
435
|
+
root: projectRoot,
|
|
436
|
+
plugins: [...jayStackCompiler(fullJayOptions)],
|
|
437
|
+
build: {
|
|
438
|
+
outDir: outputDir,
|
|
439
|
+
emptyOutDir: false,
|
|
440
|
+
minify,
|
|
441
|
+
manifest: `${instanceId}-manifest.json`,
|
|
442
|
+
rollupOptions: {
|
|
443
|
+
input: { [instanceId]: hydrateEntryPath },
|
|
444
|
+
external: (id) => id.startsWith("@jay-framework/"),
|
|
445
|
+
output: {
|
|
446
|
+
entryFileNames: "[name]-[hash].js",
|
|
447
|
+
chunkFileNames: "chunks/[name]-[hash].js",
|
|
448
|
+
assetFileNames: "[name]-[hash].[ext]",
|
|
449
|
+
format: "es"
|
|
450
|
+
},
|
|
451
|
+
preserveEntrySignatures: "exports-only"
|
|
452
|
+
}
|
|
453
|
+
},
|
|
454
|
+
logLevel: "warn"
|
|
455
|
+
});
|
|
456
|
+
const manifestPath = path.join(outputDir, `${instanceId}-manifest.json`);
|
|
457
|
+
const manifest = JSON.parse(await fs.readFile(manifestPath, "utf-8"));
|
|
458
|
+
await fs.rm(manifestPath, { force: true });
|
|
459
|
+
const entryKey = Object.keys(manifest).find((k) => manifest[k].isEntry);
|
|
460
|
+
if (!entryKey) {
|
|
461
|
+
throw new Error(`No entry found in instance build manifest for ${instanceId}`);
|
|
462
|
+
}
|
|
463
|
+
const entry = manifest[entryKey];
|
|
464
|
+
const result = {
|
|
465
|
+
jsFile: entry.file,
|
|
466
|
+
cssFile: entry.css?.[0]
|
|
467
|
+
};
|
|
468
|
+
logger.info(`[Build] Client bundle: ${result.jsFile}`);
|
|
469
|
+
return result;
|
|
470
|
+
}
|
|
471
|
+
function resolvePackageNameForRoute(compPath) {
|
|
472
|
+
const dir = path.dirname(compPath);
|
|
473
|
+
for (const candidate of [dir, path.join(dir, "..")]) {
|
|
474
|
+
try {
|
|
475
|
+
const pkgJson = JSON.parse(
|
|
476
|
+
fs$1.readFileSync(path.join(candidate, "package.json"), "utf-8")
|
|
477
|
+
);
|
|
478
|
+
if (pkgJson.name)
|
|
479
|
+
return pkgJson.name;
|
|
480
|
+
} catch {
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
return void 0;
|
|
484
|
+
}
|
|
485
|
+
function hashParams(params) {
|
|
486
|
+
const sorted = Object.keys(params).sort().reduce(
|
|
487
|
+
(acc, key) => {
|
|
488
|
+
acc[key] = params[key];
|
|
489
|
+
return acc;
|
|
490
|
+
},
|
|
491
|
+
{}
|
|
492
|
+
);
|
|
493
|
+
const json = JSON.stringify(sorted);
|
|
494
|
+
if (json === "{}")
|
|
495
|
+
return "";
|
|
496
|
+
return "_" + crypto.createHash("md5").update(json).digest("hex").substring(0, 8);
|
|
497
|
+
}
|
|
498
|
+
async function buildInstance(route, params, pageModule, ctx) {
|
|
499
|
+
const logger = getLogger();
|
|
500
|
+
const routeDir = route.rawRoute.replace(/^\//, "") || "index";
|
|
501
|
+
const paramHash = hashParams(params);
|
|
502
|
+
const instanceId = `page${paramHash}`;
|
|
503
|
+
const instanceDir = path.join(ctx.buildDir, "pre-rendered", routeDir);
|
|
504
|
+
await fs.mkdir(instanceDir, { recursive: true });
|
|
505
|
+
const jayHtmlContent = await fs.readFile(route.jayHtmlPath, "utf-8");
|
|
506
|
+
const sourceDir = path.dirname(route.jayHtmlPath);
|
|
507
|
+
const serverBuildDir = path.join(ctx.buildDir, "server");
|
|
508
|
+
const pageParts = await loadProductionPageParts(
|
|
509
|
+
route,
|
|
510
|
+
pageModule,
|
|
511
|
+
jayHtmlContent,
|
|
512
|
+
ctx.projectRoot,
|
|
513
|
+
ctx.tsConfigFilePath,
|
|
514
|
+
serverBuildDir
|
|
515
|
+
);
|
|
516
|
+
const slowPhase = new DevSlowlyChangingPhase();
|
|
517
|
+
const slowResult = await slowPhase.runSlowlyForPage(
|
|
518
|
+
params,
|
|
519
|
+
{ params },
|
|
520
|
+
pageParts.parts,
|
|
521
|
+
pageParts.discoveredInstances,
|
|
522
|
+
pageParts.headlessInstanceComponents,
|
|
523
|
+
route.jayHtmlPath
|
|
524
|
+
);
|
|
525
|
+
if (slowResult.kind !== "PhaseOutput") {
|
|
526
|
+
if (slowResult.kind === "ClientError" || slowResult.kind === "Redirect") {
|
|
527
|
+
return {
|
|
528
|
+
status: "skipped",
|
|
529
|
+
reason: `${slowResult.kind} ${slowResult.status ?? ""} ${slowResult.message ?? ""}`.trim()
|
|
530
|
+
};
|
|
531
|
+
}
|
|
532
|
+
throw new Error(
|
|
533
|
+
`Slow render failed for ${route.rawRoute} with params ${JSON.stringify(params)}: ${slowResult.kind}`
|
|
534
|
+
);
|
|
535
|
+
}
|
|
536
|
+
const slowViewState = slowResult.rendered;
|
|
537
|
+
const carryForward = slowResult.carryForward;
|
|
538
|
+
let contract;
|
|
539
|
+
const contractPath = route.jayHtmlPath.replace(".jay-html", ".jay-contract");
|
|
540
|
+
try {
|
|
541
|
+
const contractContent = await fs.readFile(contractPath, "utf-8");
|
|
542
|
+
const parseResult = parseContract(contractContent, path.basename(contractPath));
|
|
543
|
+
if (parseResult.val)
|
|
544
|
+
contract = parseResult.val;
|
|
545
|
+
} catch {
|
|
546
|
+
}
|
|
547
|
+
const jayHtmlWithTemplates = injectHeadfullFSTemplates(
|
|
548
|
+
jayHtmlContent,
|
|
549
|
+
sourceDir,
|
|
550
|
+
JAY_IMPORT_RESOLVER
|
|
551
|
+
);
|
|
552
|
+
const transformResult = slowRenderTransform({
|
|
553
|
+
jayHtmlContent: jayHtmlWithTemplates,
|
|
554
|
+
slowViewState,
|
|
555
|
+
contract,
|
|
556
|
+
headlessContracts: pageParts.headlessContracts,
|
|
557
|
+
sourceDir,
|
|
558
|
+
importResolver: JAY_IMPORT_RESOLVER
|
|
559
|
+
});
|
|
560
|
+
if (!transformResult.val) {
|
|
561
|
+
throw new Error(
|
|
562
|
+
`Slow render transform failed for ${route.rawRoute}: ${transformResult.validations.join(", ")}`
|
|
563
|
+
);
|
|
564
|
+
}
|
|
565
|
+
let preRenderedJayHtml = transformResult.val.preRenderedJayHtml;
|
|
566
|
+
if (pageParts.headlessInstanceComponents.length > 0) {
|
|
567
|
+
const discoveryResult = discoverHeadlessInstances(preRenderedJayHtml);
|
|
568
|
+
const htmlWithRefs = discoveryResult.preRenderedJayHtml;
|
|
569
|
+
const contractNames = new Set(
|
|
570
|
+
pageParts.headlessInstanceComponents.map((c) => c.contractName)
|
|
571
|
+
);
|
|
572
|
+
preRenderedJayHtml = assignCoordinatesToJayHtml(htmlWithRefs, contractNames);
|
|
573
|
+
const finalDiscovery = discoverHeadlessInstances(preRenderedJayHtml);
|
|
574
|
+
if (finalDiscovery.instances.length > 0) {
|
|
575
|
+
const slowResult2 = await slowRenderInstances(
|
|
576
|
+
finalDiscovery.instances,
|
|
577
|
+
pageParts.headlessInstanceComponents
|
|
578
|
+
);
|
|
579
|
+
if (slowResult2) {
|
|
580
|
+
const existingInstances = carryForward.__instances || {
|
|
581
|
+
discovered: [],
|
|
582
|
+
carryForwards: {}
|
|
583
|
+
};
|
|
584
|
+
carryForward.__instances = {
|
|
585
|
+
discovered: [
|
|
586
|
+
...existingInstances.discovered,
|
|
587
|
+
...slowResult2.instancePhaseData.discovered
|
|
588
|
+
],
|
|
589
|
+
carryForwards: {
|
|
590
|
+
...existingInstances.carryForwards,
|
|
591
|
+
...slowResult2.instancePhaseData.carryForwards
|
|
592
|
+
},
|
|
593
|
+
slowViewStates: {
|
|
594
|
+
...existingInstances.slowViewStates || {},
|
|
595
|
+
...slowResult2.instancePhaseData.slowViewStates
|
|
596
|
+
}
|
|
597
|
+
};
|
|
598
|
+
carryForward.__instanceSlowViewStates = {
|
|
599
|
+
...carryForward.__instanceSlowViewStates || {},
|
|
600
|
+
...Object.fromEntries(
|
|
601
|
+
slowResult2.resolvedData.map((d) => [
|
|
602
|
+
d.coordinate.join("/"),
|
|
603
|
+
d.slowViewState
|
|
604
|
+
])
|
|
605
|
+
)
|
|
606
|
+
};
|
|
607
|
+
carryForward.__instanceResolvedData = [
|
|
608
|
+
...carryForward.__instanceResolvedData || [],
|
|
609
|
+
...slowResult2.resolvedData
|
|
610
|
+
];
|
|
611
|
+
const pass2Result = resolveHeadlessInstances(
|
|
612
|
+
preRenderedJayHtml,
|
|
613
|
+
slowResult2.resolvedData,
|
|
614
|
+
JAY_IMPORT_RESOLVER
|
|
615
|
+
);
|
|
616
|
+
if (pass2Result.val) {
|
|
617
|
+
preRenderedJayHtml = pass2Result.val;
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
const preRenderedPath = path.join(instanceDir, `${instanceId}.jay-html`);
|
|
623
|
+
await fs.writeFile(preRenderedPath, preRenderedJayHtml, "utf-8");
|
|
624
|
+
const cacheMetadataPath = path.join(instanceDir, `${instanceId}.cache.json`);
|
|
625
|
+
await fs.writeFile(
|
|
626
|
+
cacheMetadataPath,
|
|
627
|
+
JSON.stringify({
|
|
628
|
+
slowViewState,
|
|
629
|
+
carryForward,
|
|
630
|
+
sourcePath: route.jayHtmlPath
|
|
631
|
+
}),
|
|
632
|
+
"utf-8"
|
|
633
|
+
);
|
|
634
|
+
logger.info(`[Build] Pre-rendered: ${routeDir}/${instanceId}`);
|
|
635
|
+
const serverElementPath = path.join(instanceDir, `${instanceId}.server-element.js`);
|
|
636
|
+
const serverElementResult = await compileServerElement(
|
|
637
|
+
preRenderedJayHtml,
|
|
638
|
+
`${instanceId}.jay-html`,
|
|
639
|
+
instanceDir,
|
|
640
|
+
serverElementPath,
|
|
641
|
+
ctx.projectRoot,
|
|
642
|
+
ctx.tsConfigFilePath,
|
|
643
|
+
sourceDir
|
|
644
|
+
);
|
|
645
|
+
const hydrateEntryPath = path.join(instanceDir, `${instanceId}.hydrate-entry.ts`);
|
|
646
|
+
const relativeJayHtmlPath = path.relative(instanceDir, preRenderedPath);
|
|
647
|
+
let pageModulePath;
|
|
648
|
+
let pageExportName;
|
|
649
|
+
if (route.componentExport) {
|
|
650
|
+
const pkgName = resolvePackageNameForRoute(route.compPath);
|
|
651
|
+
pageModulePath = pkgName ? `${pkgName}/client` : "./" + path.relative(instanceDir, route.compPath);
|
|
652
|
+
pageExportName = route.componentExport;
|
|
653
|
+
} else if (route.compPath) {
|
|
654
|
+
pageModulePath = "./" + path.relative(instanceDir, route.compPath);
|
|
655
|
+
pageExportName = "page";
|
|
656
|
+
} else {
|
|
657
|
+
pageModulePath = "";
|
|
658
|
+
pageExportName = "";
|
|
659
|
+
}
|
|
660
|
+
if (pageParts.keyedPartModules.length > 0) {
|
|
661
|
+
logger.info(
|
|
662
|
+
`[Build] Keyed parts for ${routeDir}: ${pageParts.keyedPartModules.map((p) => p.key).join(", ")}`
|
|
663
|
+
);
|
|
664
|
+
}
|
|
665
|
+
await generateHydrationEntry({
|
|
666
|
+
jayHtmlPath: "./" + relativeJayHtmlPath,
|
|
667
|
+
pageModulePath,
|
|
668
|
+
pageExportName,
|
|
669
|
+
slowViewState,
|
|
670
|
+
trackByMap: pageParts.clientTrackByMap || {},
|
|
671
|
+
outputPath: hydrateEntryPath,
|
|
672
|
+
keyedParts: pageParts.keyedPartModules,
|
|
673
|
+
clientInits: ctx.clientInits
|
|
674
|
+
});
|
|
675
|
+
const clientResult = await buildInstanceClient(
|
|
676
|
+
hydrateEntryPath,
|
|
677
|
+
instanceId,
|
|
678
|
+
instanceDir,
|
|
679
|
+
ctx.projectRoot,
|
|
680
|
+
ctx.jayOptions,
|
|
681
|
+
ctx.minify ?? true,
|
|
682
|
+
ctx.pagesRoot,
|
|
683
|
+
ctx.buildDir
|
|
684
|
+
);
|
|
685
|
+
await fs.rm(hydrateEntryPath, { force: true });
|
|
686
|
+
const cssFile = clientResult.cssFile || serverElementResult.cssFile;
|
|
687
|
+
const instanceEntry = {
|
|
688
|
+
params,
|
|
689
|
+
preRenderedPath: path.relative(ctx.buildDir, preRenderedPath),
|
|
690
|
+
serverElementPath: path.relative(ctx.buildDir, serverElementPath),
|
|
691
|
+
clientBundlePath: path.relative(ctx.buildDir, path.join(instanceDir, clientResult.jsFile)),
|
|
692
|
+
clientCssPath: cssFile ? path.relative(ctx.buildDir, path.join(instanceDir, cssFile)) : void 0
|
|
693
|
+
};
|
|
694
|
+
return { status: "success", instanceEntry, slowViewState, carryForward };
|
|
695
|
+
}
|
|
696
|
+
function convertSegments(segments) {
|
|
697
|
+
return segments.map((s) => {
|
|
698
|
+
if (typeof s === "string") {
|
|
699
|
+
return { type: "static", value: s };
|
|
700
|
+
}
|
|
701
|
+
switch (s.type) {
|
|
702
|
+
case JayRouteParamType.single:
|
|
703
|
+
return { type: "param", value: s.name };
|
|
704
|
+
case JayRouteParamType.catchAll:
|
|
705
|
+
return { type: "catchAll", value: s.name };
|
|
706
|
+
case JayRouteParamType.optional:
|
|
707
|
+
return { type: "optional", value: s.name };
|
|
708
|
+
}
|
|
709
|
+
});
|
|
710
|
+
}
|
|
711
|
+
function buildRouteEntry(route, serverModulePath) {
|
|
712
|
+
return {
|
|
713
|
+
pattern: route.rawRoute,
|
|
714
|
+
segments: convertSegments(route.segments),
|
|
715
|
+
serverModule: serverModulePath,
|
|
716
|
+
jayHtmlPath: route.jayHtmlPath,
|
|
717
|
+
componentExport: route.componentExport,
|
|
718
|
+
instances: []
|
|
719
|
+
};
|
|
720
|
+
}
|
|
721
|
+
async function discoverActions(actionPaths, serverOutputDir, buildDir, projectRoot) {
|
|
722
|
+
const actions = [];
|
|
723
|
+
const plugins = [];
|
|
724
|
+
for (const [entryName, sourcePath] of Object.entries(actionPaths)) {
|
|
725
|
+
try {
|
|
726
|
+
const code = await fs.readFile(sourcePath, "utf-8");
|
|
727
|
+
const extracted = extractActionsFromSource(code, sourcePath);
|
|
728
|
+
if (extracted.length > 0) {
|
|
729
|
+
actions.push({
|
|
730
|
+
serverModule: path.relative(
|
|
731
|
+
buildDir,
|
|
732
|
+
path.join(serverOutputDir, entryName + ".js")
|
|
733
|
+
),
|
|
734
|
+
isPlugin: false,
|
|
735
|
+
actionNames: extracted.map((a) => a.actionName)
|
|
736
|
+
});
|
|
737
|
+
}
|
|
738
|
+
} catch {
|
|
739
|
+
getLogger().warn(`[Build] Could not extract actions from ${sourcePath}`);
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
try {
|
|
743
|
+
const scannedPlugins = await scanPlugins({ projectRoot });
|
|
744
|
+
for (const [packageName, plugin] of scannedPlugins) {
|
|
745
|
+
if (plugin.isLocal)
|
|
746
|
+
continue;
|
|
747
|
+
plugins.push({ name: plugin.manifest.name, packageName });
|
|
748
|
+
const pluginActions = plugin.manifest.actions;
|
|
749
|
+
if (pluginActions && pluginActions.length > 0) {
|
|
750
|
+
actions.push({
|
|
751
|
+
serverModule: "",
|
|
752
|
+
packageName,
|
|
753
|
+
isPlugin: true,
|
|
754
|
+
actionNames: pluginActions.map(
|
|
755
|
+
(a) => typeof a === "string" ? a : a.name
|
|
756
|
+
)
|
|
757
|
+
});
|
|
758
|
+
getLogger().info(
|
|
759
|
+
`[Build] Plugin actions from ${packageName}: ${pluginActions.length}`
|
|
760
|
+
);
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
} catch (err) {
|
|
764
|
+
getLogger().warn(`[Build] Plugin action scan failed: ${err.message}`);
|
|
765
|
+
}
|
|
766
|
+
return { actions, plugins };
|
|
767
|
+
}
|
|
768
|
+
async function writeRouteManifest(manifest, buildDir) {
|
|
769
|
+
const manifestPath = path.join(buildDir, "route-manifest.json");
|
|
770
|
+
await fs.writeFile(manifestPath, JSON.stringify(manifest, null, 2));
|
|
771
|
+
getLogger().info(
|
|
772
|
+
`[Build] Route manifest written: ${manifest.routes.length} routes, ${manifest.routes.reduce((n, r) => n + r.instances.length, 0)} instances`
|
|
773
|
+
);
|
|
774
|
+
}
|
|
775
|
+
createRequire(import.meta.url);
|
|
776
|
+
async function scanPluginRoutes(projectRoot, projectRoutes) {
|
|
777
|
+
const logger = getLogger();
|
|
778
|
+
const plugins = await scanPlugins({ projectRoot });
|
|
779
|
+
const projectPaths = new Set(projectRoutes.map((r) => r.rawRoute));
|
|
780
|
+
const pluginRoutes = [];
|
|
781
|
+
for (const [, plugin] of plugins) {
|
|
782
|
+
if (plugin.isLocal)
|
|
783
|
+
continue;
|
|
784
|
+
if (!plugin.manifest.routes)
|
|
785
|
+
continue;
|
|
786
|
+
for (const route of plugin.manifest.routes) {
|
|
787
|
+
if (projectPaths.has(route.path)) {
|
|
788
|
+
logger.info(
|
|
789
|
+
`[Routes] Plugin "${plugin.manifest.name}" route ${route.path} skipped — project route takes precedence`
|
|
790
|
+
);
|
|
791
|
+
continue;
|
|
792
|
+
}
|
|
793
|
+
const jayHtmlPath = resolvePluginExport(plugin.pluginPath, route.jayHtml);
|
|
794
|
+
if (!jayHtmlPath) {
|
|
795
|
+
logger.warn(
|
|
796
|
+
`[Routes] Plugin "${plugin.manifest.name}" route ${route.path}: jayHtml "${route.jayHtml}" not found`
|
|
797
|
+
);
|
|
798
|
+
continue;
|
|
799
|
+
}
|
|
800
|
+
const compPath = resolvePluginModule(plugin.pluginPath);
|
|
801
|
+
const componentExport = route.component;
|
|
802
|
+
pluginRoutes.push({
|
|
803
|
+
segments: parseRouteSegments(route.path),
|
|
804
|
+
rawRoute: route.path,
|
|
805
|
+
jayHtmlPath,
|
|
806
|
+
compPath,
|
|
807
|
+
componentExport
|
|
808
|
+
});
|
|
809
|
+
logger.info(`[Routes] Plugin "${plugin.manifest.name}" provides route ${route.path}`);
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
return pluginRoutes;
|
|
813
|
+
}
|
|
814
|
+
function resolvePluginExport(pluginPath, exportSubpath) {
|
|
815
|
+
const normalized = exportSubpath.replace(/^\.\//, "");
|
|
816
|
+
const packageJsonPath = path.join(pluginPath, "package.json");
|
|
817
|
+
try {
|
|
818
|
+
const packageJson = JSON.parse(fs$1.readFileSync(packageJsonPath, "utf-8"));
|
|
819
|
+
if (packageJson.exports) {
|
|
820
|
+
const exportKey = "./" + normalized;
|
|
821
|
+
const exportValue = packageJson.exports[exportKey];
|
|
822
|
+
if (exportValue) {
|
|
823
|
+
const resolved = typeof exportValue === "string" ? exportValue : exportValue.default || exportValue.import || exportValue.require;
|
|
824
|
+
if (resolved)
|
|
825
|
+
return path.join(pluginPath, resolved);
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
} catch {
|
|
829
|
+
}
|
|
830
|
+
for (const dir of ["dist", "lib", ""]) {
|
|
831
|
+
const candidate = path.join(pluginPath, dir, normalized);
|
|
832
|
+
try {
|
|
833
|
+
fs$1.accessSync(candidate);
|
|
834
|
+
return candidate;
|
|
835
|
+
} catch {
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
return void 0;
|
|
839
|
+
}
|
|
840
|
+
function resolvePluginModule(pluginPath) {
|
|
841
|
+
const pkgJsonPath = path.join(pluginPath, "package.json");
|
|
842
|
+
try {
|
|
843
|
+
const pkg = JSON.parse(fs$1.readFileSync(pkgJsonPath, "utf-8"));
|
|
844
|
+
const mainExport = pkg.exports?.["."];
|
|
845
|
+
const mainPath = typeof mainExport === "string" ? mainExport : mainExport?.default || mainExport?.import || pkg.main;
|
|
846
|
+
if (mainPath) {
|
|
847
|
+
const resolved = path.join(pluginPath, mainPath);
|
|
848
|
+
if (fs$1.existsSync(resolved))
|
|
849
|
+
return resolved;
|
|
850
|
+
}
|
|
851
|
+
} catch {
|
|
852
|
+
}
|
|
853
|
+
return path.join(pluginPath, "dist", "index.js");
|
|
854
|
+
}
|
|
855
|
+
function crossProductParams(parts) {
|
|
856
|
+
if (parts.length === 0)
|
|
857
|
+
return [];
|
|
858
|
+
if (parts.length === 1)
|
|
859
|
+
return parts[0].values;
|
|
860
|
+
const logger = getLogger();
|
|
861
|
+
for (let i = 0; i < parts.length; i++) {
|
|
862
|
+
for (let j = i + 1; j < parts.length; j++) {
|
|
863
|
+
for (const key of parts[i].keys) {
|
|
864
|
+
if (parts[j].keys.has(key)) {
|
|
865
|
+
logger.warn(
|
|
866
|
+
`[Build] Multiple loadParams provide key "${key}" — using first provider`
|
|
867
|
+
);
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
let result = parts[0].values;
|
|
873
|
+
for (let i = 1; i < parts.length; i++) {
|
|
874
|
+
const next = parts[i].values;
|
|
875
|
+
const combined = [];
|
|
876
|
+
for (const a of result) {
|
|
877
|
+
for (const b of next) {
|
|
878
|
+
combined.push({ ...a, ...b });
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
result = combined;
|
|
882
|
+
}
|
|
883
|
+
return result;
|
|
884
|
+
}
|
|
885
|
+
function paramsMatchInferred(params, inferredParams, optionalSegments) {
|
|
886
|
+
return Object.entries(inferredParams).every(([k, v]) => {
|
|
887
|
+
if (optionalSegments?.has(k))
|
|
888
|
+
return true;
|
|
889
|
+
return params[k] === v;
|
|
890
|
+
});
|
|
891
|
+
}
|
|
892
|
+
function computeSpecificity(route) {
|
|
893
|
+
const dynamicCount = (route.rawRoute.match(/\[/g) || []).length;
|
|
894
|
+
const inferredCount = route.inferredParams ? Object.keys(route.inferredParams).length : 0;
|
|
895
|
+
const unresolvedCount = Math.max(0, dynamicCount - inferredCount);
|
|
896
|
+
return 0 - unresolvedCount;
|
|
897
|
+
}
|
|
898
|
+
function buildUrl(route, params) {
|
|
899
|
+
return route.rawRoute.replace(/\[\[(\w+)\]\]/g, (_, name) => {
|
|
900
|
+
const value = params[name];
|
|
901
|
+
if (!value)
|
|
902
|
+
return "";
|
|
903
|
+
if (route.inferredParams?.[name] === value)
|
|
904
|
+
return "";
|
|
905
|
+
return value;
|
|
906
|
+
}).replace(/\[(\w+)\]/g, (_, name) => params[name] || "").replace(/\/\/+/g, "/").replace(/\/$/, "") || "/";
|
|
907
|
+
}
|
|
908
|
+
function materializeRouteParams(routes, loadParamsResults) {
|
|
909
|
+
const entries = [];
|
|
910
|
+
for (const route of routes) {
|
|
911
|
+
const specificity = computeSpecificity(route);
|
|
912
|
+
if (!route.hasDynamicParams) {
|
|
913
|
+
const params = route.inferredParams || {};
|
|
914
|
+
entries.push({ route, params, url: route.rawRoute, specificity });
|
|
915
|
+
continue;
|
|
916
|
+
}
|
|
917
|
+
const allParams = loadParamsResults.get(route) || [];
|
|
918
|
+
for (const params of allParams) {
|
|
919
|
+
if (route.inferredParams && !paramsMatchInferred(params, route.inferredParams, route.optionalSegments)) {
|
|
920
|
+
continue;
|
|
921
|
+
}
|
|
922
|
+
let mergedParams = params;
|
|
923
|
+
if (route.inferredParams) {
|
|
924
|
+
mergedParams = { ...params };
|
|
925
|
+
for (const [k, v] of Object.entries(route.inferredParams)) {
|
|
926
|
+
if (!(k in mergedParams)) {
|
|
927
|
+
mergedParams[k] = v;
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
const url = buildUrl(route, mergedParams);
|
|
932
|
+
entries.push({ route, params: mergedParams, url, specificity });
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
return entries;
|
|
936
|
+
}
|
|
937
|
+
function dedupeByUrl(entries) {
|
|
938
|
+
const logger = getLogger();
|
|
939
|
+
const byUrl = /* @__PURE__ */ new Map();
|
|
940
|
+
for (const entry of entries) {
|
|
941
|
+
const existing = byUrl.get(entry.url);
|
|
942
|
+
if (!existing) {
|
|
943
|
+
byUrl.set(entry.url, entry);
|
|
944
|
+
} else if (entry.specificity > existing.specificity) {
|
|
945
|
+
byUrl.set(entry.url, entry);
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
const deduped = [...byUrl.values()];
|
|
949
|
+
if (deduped.length < entries.length) {
|
|
950
|
+
logger.info(
|
|
951
|
+
`[Build] Deduplication: ${entries.length} materialized → ${deduped.length} unique URLs`
|
|
952
|
+
);
|
|
953
|
+
}
|
|
954
|
+
return deduped;
|
|
955
|
+
}
|
|
956
|
+
async function discoverPluginClientPackages(projectRoot) {
|
|
957
|
+
const projectRequire = createRequire(path.join(projectRoot, "package.json"));
|
|
958
|
+
const seen = /* @__PURE__ */ new Set();
|
|
959
|
+
const result = [];
|
|
960
|
+
async function walk(pkgName) {
|
|
961
|
+
if (seen.has(pkgName))
|
|
962
|
+
return;
|
|
963
|
+
seen.add(pkgName);
|
|
964
|
+
try {
|
|
965
|
+
const mainPath = projectRequire.resolve(pkgName);
|
|
966
|
+
let pkgDir = path.dirname(mainPath);
|
|
967
|
+
while (pkgDir !== path.dirname(pkgDir)) {
|
|
968
|
+
const candidate = path.join(pkgDir, "package.json");
|
|
969
|
+
try {
|
|
970
|
+
const pkgJson = JSON.parse(await fs.readFile(candidate, "utf-8"));
|
|
971
|
+
if (pkgJson.name === pkgName) {
|
|
972
|
+
if (pkgJson.exports?.["./client"]) {
|
|
973
|
+
result.push(`${pkgName}/client`);
|
|
974
|
+
}
|
|
975
|
+
for (const dep of Object.keys(pkgJson.dependencies || {})) {
|
|
976
|
+
if (dep.startsWith("@jay-framework/")) {
|
|
977
|
+
await walk(dep);
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
break;
|
|
981
|
+
}
|
|
982
|
+
} catch {
|
|
983
|
+
}
|
|
984
|
+
pkgDir = path.dirname(pkgDir);
|
|
985
|
+
}
|
|
986
|
+
} catch {
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
try {
|
|
990
|
+
const projectPkg = JSON.parse(
|
|
991
|
+
await fs.readFile(path.join(projectRoot, "package.json"), "utf-8")
|
|
992
|
+
);
|
|
993
|
+
const allDeps = {
|
|
994
|
+
...projectPkg.dependencies,
|
|
995
|
+
...projectPkg.devDependencies
|
|
996
|
+
};
|
|
997
|
+
for (const dep of Object.keys(allDeps)) {
|
|
998
|
+
if (dep.startsWith("@jay-framework/")) {
|
|
999
|
+
await walk(dep);
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
} catch {
|
|
1003
|
+
}
|
|
1004
|
+
return result;
|
|
1005
|
+
}
|
|
1006
|
+
async function buildVersion(options) {
|
|
1007
|
+
const logger = getLogger();
|
|
1008
|
+
const buildDir = path.join(options.buildRoot, `v${options.version}`);
|
|
1009
|
+
logger.important(`[Build] Starting production build v${options.version}`);
|
|
1010
|
+
logger.important(`[Build] Project: ${options.projectRoot}`);
|
|
1011
|
+
await fs.mkdir(buildDir, { recursive: true });
|
|
1012
|
+
const { entries, routes } = await discoverServerEntries(options.projectRoot, options.pagesRoot);
|
|
1013
|
+
const serverOutputDir = path.join(buildDir, "server");
|
|
1014
|
+
await buildServerCode(
|
|
1015
|
+
entries,
|
|
1016
|
+
{ tsConfigFilePath: options.tsConfigFilePath },
|
|
1017
|
+
serverOutputDir,
|
|
1018
|
+
options.projectRoot
|
|
1019
|
+
);
|
|
1020
|
+
const pluginClientPackages = await discoverPluginClientPackages(options.projectRoot);
|
|
1021
|
+
if (pluginClientPackages.length > 0) {
|
|
1022
|
+
logger.important(`[Build] Plugin client packages: ${pluginClientPackages.join(", ")}`);
|
|
1023
|
+
}
|
|
1024
|
+
const sharedOutputDir = path.join(buildDir, "shared");
|
|
1025
|
+
const { manifest: sharedManifest } = await buildSharedChunks(
|
|
1026
|
+
sharedOutputDir,
|
|
1027
|
+
options.projectRoot,
|
|
1028
|
+
options.minify ?? true,
|
|
1029
|
+
pluginClientPackages
|
|
1030
|
+
);
|
|
1031
|
+
const { actions, plugins } = await discoverActions(
|
|
1032
|
+
entries.actions,
|
|
1033
|
+
serverOutputDir,
|
|
1034
|
+
buildDir,
|
|
1035
|
+
options.projectRoot
|
|
1036
|
+
);
|
|
1037
|
+
const { discoverPluginsWithInit, sortPluginsByDependencies } = await import("@jay-framework/stack-server-runtime");
|
|
1038
|
+
try {
|
|
1039
|
+
const pluginsWithInit = sortPluginsByDependencies(
|
|
1040
|
+
await discoverPluginsWithInit({ projectRoot: options.projectRoot })
|
|
1041
|
+
);
|
|
1042
|
+
for (const pluginInit of pluginsWithInit) {
|
|
1043
|
+
try {
|
|
1044
|
+
const pluginModule = await import(pluginInit.packageName);
|
|
1045
|
+
const init = pluginModule.init || pluginModule[pluginInit.initExport || "init"];
|
|
1046
|
+
if (init?._serverInit) {
|
|
1047
|
+
logger.info(`[Build] Running plugin init: ${pluginInit.name}`);
|
|
1048
|
+
await init._serverInit();
|
|
1049
|
+
}
|
|
1050
|
+
} catch (err) {
|
|
1051
|
+
logger.warn(`[Build] Plugin init failed: ${pluginInit.name}: ${err.message}`);
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
} catch {
|
|
1055
|
+
}
|
|
1056
|
+
if (entries.init) {
|
|
1057
|
+
const initModulePath = path.join(serverOutputDir, "init.js");
|
|
1058
|
+
try {
|
|
1059
|
+
const initModule = await import(initModulePath);
|
|
1060
|
+
const init = initModule.init || initModule.default;
|
|
1061
|
+
if (init?._serverInit) {
|
|
1062
|
+
logger.info("[Build] Running server init...");
|
|
1063
|
+
await init._serverInit();
|
|
1064
|
+
}
|
|
1065
|
+
} catch (err) {
|
|
1066
|
+
logger.error(`[Build] Failed to run init: ${err}`);
|
|
1067
|
+
throw err;
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
const clientInits = [];
|
|
1071
|
+
try {
|
|
1072
|
+
const allPluginsWithInit = sortPluginsByDependencies(
|
|
1073
|
+
await discoverPluginsWithInit({ projectRoot: options.projectRoot })
|
|
1074
|
+
);
|
|
1075
|
+
for (const pluginInit of allPluginsWithInit) {
|
|
1076
|
+
if (pluginInit.isLocal)
|
|
1077
|
+
continue;
|
|
1078
|
+
const clientImportPath = `${pluginInit.packageName}/client`;
|
|
1079
|
+
try {
|
|
1080
|
+
const clientModule = await import(clientImportPath);
|
|
1081
|
+
const init = clientModule[pluginInit.initExport || "init"] || clientModule.init;
|
|
1082
|
+
if (init?._clientInit) {
|
|
1083
|
+
clientInits.push({
|
|
1084
|
+
modulePath: clientImportPath,
|
|
1085
|
+
exportName: pluginInit.initExport || "init",
|
|
1086
|
+
key: pluginInit.name
|
|
1087
|
+
});
|
|
1088
|
+
}
|
|
1089
|
+
} catch {
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
} catch (err) {
|
|
1093
|
+
logger.warn(`[Build] Client init discovery failed: ${err.message}`);
|
|
1094
|
+
}
|
|
1095
|
+
if (clientInits.length > 0) {
|
|
1096
|
+
logger.important(`[Build] Client inits: ${clientInits.map((ci) => ci.key).join(", ")}`);
|
|
1097
|
+
}
|
|
1098
|
+
if (entries.init) {
|
|
1099
|
+
clientInits.push({
|
|
1100
|
+
modulePath: entries.init,
|
|
1101
|
+
exportName: "init",
|
|
1102
|
+
key: "project"
|
|
1103
|
+
});
|
|
1104
|
+
}
|
|
1105
|
+
const instanceCtx = {
|
|
1106
|
+
projectRoot: options.projectRoot,
|
|
1107
|
+
pagesRoot: options.pagesRoot,
|
|
1108
|
+
buildDir,
|
|
1109
|
+
jayOptions: { tsConfigFilePath: options.tsConfigFilePath },
|
|
1110
|
+
tsConfigFilePath: options.tsConfigFilePath,
|
|
1111
|
+
minify: options.minify ?? true,
|
|
1112
|
+
clientInits
|
|
1113
|
+
};
|
|
1114
|
+
const pluginRoutes = await scanPluginRoutes(options.projectRoot, routes);
|
|
1115
|
+
const allRoutes = [...routes, ...pluginRoutes];
|
|
1116
|
+
const routeEntries = allRoutes.map((route) => {
|
|
1117
|
+
let serverModule = "";
|
|
1118
|
+
if (route.compPath) {
|
|
1119
|
+
if (route.componentExport) {
|
|
1120
|
+
serverModule = route.compPath;
|
|
1121
|
+
} else {
|
|
1122
|
+
const relativePath = path.relative(options.projectRoot, route.compPath);
|
|
1123
|
+
serverModule = relativePath.replace(/^src\//, "server/").replace(/\.ts$/, ".js").replace(/\[/g, "_").replace(/\]/g, "_");
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
const entry = buildRouteEntry(route, serverModule);
|
|
1127
|
+
if (route.componentExport) {
|
|
1128
|
+
entry.isPlugin = true;
|
|
1129
|
+
}
|
|
1130
|
+
return { route, entry };
|
|
1131
|
+
});
|
|
1132
|
+
let instanceCount = 0;
|
|
1133
|
+
let totalExpected = 0;
|
|
1134
|
+
function logInstance(routeName, params) {
|
|
1135
|
+
instanceCount++;
|
|
1136
|
+
const paramStr = Object.keys(params).length > 0 ? ` (${Object.entries(params).map(([k, v]) => `${k}=${v}`).join(", ")})` : "";
|
|
1137
|
+
logger.important(`[Build] ${instanceCount}/${totalExpected} ${routeName}${paramStr}`);
|
|
1138
|
+
}
|
|
1139
|
+
async function loadPageModule(entry) {
|
|
1140
|
+
if (!entry.serverModule)
|
|
1141
|
+
return {};
|
|
1142
|
+
if (entry.isPlugin)
|
|
1143
|
+
return import(entry.serverModule);
|
|
1144
|
+
return import(path.join(serverOutputDir, entry.serverModule.replace("server/", "")));
|
|
1145
|
+
}
|
|
1146
|
+
const routeInfos = routeEntries.map((re) => {
|
|
1147
|
+
const optionalNames = re.route.segments.filter((s) => typeof s !== "string" && s.type === JayRouteParamType.optional).map((s) => s.name);
|
|
1148
|
+
return {
|
|
1149
|
+
rawRoute: re.route.rawRoute,
|
|
1150
|
+
inferredParams: re.route.inferredParams,
|
|
1151
|
+
optionalSegments: optionalNames.length > 0 ? new Set(optionalNames) : void 0,
|
|
1152
|
+
hasDynamicParams: re.route.segments.some((s) => typeof s !== "string"),
|
|
1153
|
+
routeEntry: re
|
|
1154
|
+
};
|
|
1155
|
+
});
|
|
1156
|
+
const loadParamsCache = /* @__PURE__ */ new Map();
|
|
1157
|
+
const loadParamsResults = /* @__PURE__ */ new Map();
|
|
1158
|
+
for (const info of routeInfos) {
|
|
1159
|
+
if (!info.hasDynamicParams)
|
|
1160
|
+
continue;
|
|
1161
|
+
const { route, entry } = info.routeEntry;
|
|
1162
|
+
let pageModule;
|
|
1163
|
+
try {
|
|
1164
|
+
pageModule = await loadPageModule(entry);
|
|
1165
|
+
} catch (err) {
|
|
1166
|
+
logger.error(`[Build] Failed to load page module ${entry.serverModule}: ${err}`);
|
|
1167
|
+
continue;
|
|
1168
|
+
}
|
|
1169
|
+
const pageParts = await loadProductionPageParts(
|
|
1170
|
+
route,
|
|
1171
|
+
pageModule,
|
|
1172
|
+
await fs.readFile(route.jayHtmlPath, "utf-8"),
|
|
1173
|
+
options.projectRoot,
|
|
1174
|
+
options.tsConfigFilePath,
|
|
1175
|
+
path.join(buildDir, "server")
|
|
1176
|
+
);
|
|
1177
|
+
const partsWithLoadParams = pageParts.parts.filter((p) => p.compDefinition?.loadParams);
|
|
1178
|
+
if (partsWithLoadParams.length === 0)
|
|
1179
|
+
continue;
|
|
1180
|
+
const paramParts = [];
|
|
1181
|
+
for (const part of partsWithLoadParams) {
|
|
1182
|
+
const fn = part.compDefinition.loadParams;
|
|
1183
|
+
if (!loadParamsCache.has(fn)) {
|
|
1184
|
+
logger.important(`[Build] Loading params for ${route.rawRoute}...`);
|
|
1185
|
+
const partParams = [];
|
|
1186
|
+
let batchIndex = 0;
|
|
1187
|
+
for await (const batch of runLoadParams([part])) {
|
|
1188
|
+
partParams.push(...batch);
|
|
1189
|
+
batchIndex++;
|
|
1190
|
+
if (batchIndex > 1) {
|
|
1191
|
+
logger.important(`[Build] ...${partParams.length} params so far`);
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1194
|
+
loadParamsCache.set(fn, partParams);
|
|
1195
|
+
}
|
|
1196
|
+
const cached = loadParamsCache.get(fn);
|
|
1197
|
+
const keys = new Set(cached.flatMap((p) => Object.keys(p)));
|
|
1198
|
+
paramParts.push({ keys, values: cached });
|
|
1199
|
+
}
|
|
1200
|
+
loadParamsResults.set(info, crossProductParams(paramParts));
|
|
1201
|
+
}
|
|
1202
|
+
const materialized = materializeRouteParams(routeInfos, loadParamsResults);
|
|
1203
|
+
const deduped = dedupeByUrl(materialized);
|
|
1204
|
+
totalExpected = deduped.length;
|
|
1205
|
+
const byRoute = /* @__PURE__ */ new Map();
|
|
1206
|
+
for (const materialized2 of deduped) {
|
|
1207
|
+
const info = materialized2.route;
|
|
1208
|
+
if (!byRoute.has(info))
|
|
1209
|
+
byRoute.set(info, []);
|
|
1210
|
+
byRoute.get(info).push(materialized2.params);
|
|
1211
|
+
}
|
|
1212
|
+
for (const [info, paramsList] of byRoute) {
|
|
1213
|
+
const { route, entry } = info.routeEntry;
|
|
1214
|
+
let pageModule;
|
|
1215
|
+
try {
|
|
1216
|
+
pageModule = await loadPageModule(entry);
|
|
1217
|
+
} catch (err) {
|
|
1218
|
+
logger.error(`[Build] Failed to load page module ${entry.serverModule}: ${err}`);
|
|
1219
|
+
continue;
|
|
1220
|
+
}
|
|
1221
|
+
if (paramsList.length > 1 || info.hasDynamicParams) {
|
|
1222
|
+
logger.important(
|
|
1223
|
+
`[Build] Route ${route.rawRoute}: ${paramsList.length} param combinations`
|
|
1224
|
+
);
|
|
1225
|
+
}
|
|
1226
|
+
for (const params of paramsList) {
|
|
1227
|
+
try {
|
|
1228
|
+
const result = await buildInstance(route, params, pageModule, instanceCtx);
|
|
1229
|
+
if (result.status === "success") {
|
|
1230
|
+
entry.instances.push(result.instanceEntry);
|
|
1231
|
+
logInstance(route.rawRoute || "/", params);
|
|
1232
|
+
} else {
|
|
1233
|
+
logger.warn(
|
|
1234
|
+
`[Build] Skipped ${route.rawRoute} (${JSON.stringify(params)}): ${result.reason}`
|
|
1235
|
+
);
|
|
1236
|
+
totalExpected--;
|
|
1237
|
+
}
|
|
1238
|
+
} catch (err) {
|
|
1239
|
+
instanceCount++;
|
|
1240
|
+
logger.error(
|
|
1241
|
+
`[Build] ${instanceCount}/${totalExpected} FAILED ${route.rawRoute || "/"} (${JSON.stringify(params)}): ${err.message}`
|
|
1242
|
+
);
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
}
|
|
1246
|
+
const manifest = {
|
|
1247
|
+
version: options.version,
|
|
1248
|
+
buildTimestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1249
|
+
sourceHash: "",
|
|
1250
|
+
projectRoot: options.projectRoot,
|
|
1251
|
+
publicBasePath: options.publicBasePath,
|
|
1252
|
+
sharedManifest,
|
|
1253
|
+
routes: routeEntries.map((r) => r.entry),
|
|
1254
|
+
actions,
|
|
1255
|
+
plugins
|
|
1256
|
+
};
|
|
1257
|
+
await writeRouteManifest(manifest, buildDir);
|
|
1258
|
+
const metadata = {
|
|
1259
|
+
version: options.version,
|
|
1260
|
+
sourceHash: "",
|
|
1261
|
+
buildTimestamp: manifest.buildTimestamp,
|
|
1262
|
+
nodeVersion: process.version,
|
|
1263
|
+
instanceCount
|
|
1264
|
+
};
|
|
1265
|
+
await fs.writeFile(
|
|
1266
|
+
path.join(buildDir, "build-metadata.json"),
|
|
1267
|
+
JSON.stringify(metadata, null, 2)
|
|
1268
|
+
);
|
|
1269
|
+
logger.important(`[Build] Done! ${instanceCount} instances built in ${buildDir}`);
|
|
1270
|
+
return manifest;
|
|
1271
|
+
}
|
|
1272
|
+
const CACHE_TAG_START = '<script type="application/jay-cache">';
|
|
1273
|
+
const CACHE_TAG_END = "<\/script>";
|
|
1274
|
+
class FilesystemArtifactStore {
|
|
1275
|
+
constructor(basePath) {
|
|
1276
|
+
__publicField(this, "manifestCache");
|
|
1277
|
+
__publicField(this, "moduleCache", /* @__PURE__ */ new Map());
|
|
1278
|
+
this.basePath = basePath;
|
|
1279
|
+
}
|
|
1280
|
+
async readManifest() {
|
|
1281
|
+
const manifestPath = path.join(this.basePath, "route-manifest.json");
|
|
1282
|
+
const stat = await fs.stat(manifestPath);
|
|
1283
|
+
if (this.manifestCache && stat.mtimeMs === this.manifestCache.mtime) {
|
|
1284
|
+
return this.manifestCache.manifest;
|
|
1285
|
+
}
|
|
1286
|
+
const manifest = JSON.parse(await fs.readFile(manifestPath, "utf-8"));
|
|
1287
|
+
this.manifestCache = { manifest, mtime: stat.mtimeMs };
|
|
1288
|
+
return manifest;
|
|
1289
|
+
}
|
|
1290
|
+
async readPreRenderedHtml(relativePath) {
|
|
1291
|
+
const fullPath = path.join(this.basePath, relativePath);
|
|
1292
|
+
const content = await fs.readFile(fullPath, "utf-8");
|
|
1293
|
+
const cachePath = fullPath.replace(/\.jay-html$/, ".cache.json");
|
|
1294
|
+
try {
|
|
1295
|
+
const cacheData = JSON.parse(await fs.readFile(cachePath, "utf-8"));
|
|
1296
|
+
return {
|
|
1297
|
+
content,
|
|
1298
|
+
slowViewState: cacheData.slowViewState || {},
|
|
1299
|
+
carryForward: cacheData.carryForward || {}
|
|
1300
|
+
};
|
|
1301
|
+
} catch {
|
|
1302
|
+
return extractCacheMetadata(content);
|
|
1303
|
+
}
|
|
1304
|
+
}
|
|
1305
|
+
async loadServerElement(relativePath) {
|
|
1306
|
+
return this.loadModule(relativePath);
|
|
1307
|
+
}
|
|
1308
|
+
async loadPageModule(relativePath) {
|
|
1309
|
+
return this.loadModule(relativePath);
|
|
1310
|
+
}
|
|
1311
|
+
async readRawFile(relativePath) {
|
|
1312
|
+
const fullPath = path.join(this.basePath, relativePath);
|
|
1313
|
+
return await fs.readFile(fullPath, "utf-8");
|
|
1314
|
+
}
|
|
1315
|
+
getAssetPath(relativePath) {
|
|
1316
|
+
return path.join(this.basePath, relativePath);
|
|
1317
|
+
}
|
|
1318
|
+
async loadModule(relativePath) {
|
|
1319
|
+
const fullPath = path.join(this.basePath, relativePath);
|
|
1320
|
+
const stat = await fs.stat(fullPath);
|
|
1321
|
+
const cached = this.moduleCache.get(relativePath);
|
|
1322
|
+
if (cached && stat.mtimeMs === cached.mtime) {
|
|
1323
|
+
return cached.module;
|
|
1324
|
+
}
|
|
1325
|
+
const mod = await import(fullPath + "?t=" + stat.mtimeMs);
|
|
1326
|
+
this.moduleCache.set(relativePath, { module: mod, mtime: stat.mtimeMs });
|
|
1327
|
+
return mod;
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1330
|
+
function extractCacheMetadata(fileContent) {
|
|
1331
|
+
const startIdx = fileContent.indexOf(CACHE_TAG_START);
|
|
1332
|
+
if (startIdx === -1) {
|
|
1333
|
+
return { content: fileContent, slowViewState: {}, carryForward: {} };
|
|
1334
|
+
}
|
|
1335
|
+
const jsonStart = startIdx + CACHE_TAG_START.length;
|
|
1336
|
+
const endIdx = fileContent.indexOf(CACHE_TAG_END, jsonStart);
|
|
1337
|
+
if (endIdx === -1) {
|
|
1338
|
+
return { content: fileContent, slowViewState: {}, carryForward: {} };
|
|
1339
|
+
}
|
|
1340
|
+
const jsonStr = fileContent.substring(jsonStart, endIdx);
|
|
1341
|
+
const metadata = JSON.parse(jsonStr);
|
|
1342
|
+
const tagEnd = endIdx + CACHE_TAG_END.length;
|
|
1343
|
+
const afterTag = fileContent[tagEnd] === "\n" ? tagEnd + 1 : tagEnd;
|
|
1344
|
+
const content = fileContent.substring(0, startIdx) + fileContent.substring(afterTag);
|
|
1345
|
+
return {
|
|
1346
|
+
content,
|
|
1347
|
+
slowViewState: metadata.slowViewState || {},
|
|
1348
|
+
carryForward: metadata.carryForward || {}
|
|
1349
|
+
};
|
|
1350
|
+
}
|
|
1351
|
+
function matchRequest(manifest, pathname) {
|
|
1352
|
+
const urlSegments = pathname.split("/").filter((s) => s.length > 0);
|
|
1353
|
+
for (const route of manifest.routes) {
|
|
1354
|
+
const params = matchSegments(route.segments, urlSegments);
|
|
1355
|
+
if (params) {
|
|
1356
|
+
const instance = findInstance(route, params);
|
|
1357
|
+
if (instance) {
|
|
1358
|
+
return { route, instance, params, pathname };
|
|
1359
|
+
}
|
|
1360
|
+
}
|
|
1361
|
+
}
|
|
1362
|
+
return void 0;
|
|
1363
|
+
}
|
|
1364
|
+
function matchSegments(routeSegments, urlSegments) {
|
|
1365
|
+
const params = {};
|
|
1366
|
+
let urlIdx = 0;
|
|
1367
|
+
for (let i = 0; i < routeSegments.length; i++) {
|
|
1368
|
+
const seg = routeSegments[i];
|
|
1369
|
+
if (seg.type === "static") {
|
|
1370
|
+
if (urlIdx >= urlSegments.length || urlSegments[urlIdx] !== seg.value) {
|
|
1371
|
+
return void 0;
|
|
1372
|
+
}
|
|
1373
|
+
urlIdx++;
|
|
1374
|
+
} else if (seg.type === "param") {
|
|
1375
|
+
if (urlIdx >= urlSegments.length)
|
|
1376
|
+
return void 0;
|
|
1377
|
+
params[seg.value] = urlSegments[urlIdx];
|
|
1378
|
+
urlIdx++;
|
|
1379
|
+
} else if (seg.type === "optional") {
|
|
1380
|
+
if (urlIdx < urlSegments.length) {
|
|
1381
|
+
params[seg.value] = urlSegments[urlIdx];
|
|
1382
|
+
urlIdx++;
|
|
1383
|
+
}
|
|
1384
|
+
} else if (seg.type === "catchAll") {
|
|
1385
|
+
if (urlIdx >= urlSegments.length)
|
|
1386
|
+
return void 0;
|
|
1387
|
+
params[seg.value] = urlSegments.slice(urlIdx).join("/");
|
|
1388
|
+
urlIdx = urlSegments.length;
|
|
1389
|
+
} else if (seg.type === "optionalCatchAll") {
|
|
1390
|
+
if (urlIdx < urlSegments.length) {
|
|
1391
|
+
params[seg.value] = urlSegments.slice(urlIdx).join("/");
|
|
1392
|
+
urlIdx = urlSegments.length;
|
|
1393
|
+
}
|
|
1394
|
+
}
|
|
1395
|
+
}
|
|
1396
|
+
if (urlIdx !== urlSegments.length)
|
|
1397
|
+
return void 0;
|
|
1398
|
+
return params;
|
|
1399
|
+
}
|
|
1400
|
+
function findInstance(route, params) {
|
|
1401
|
+
const paramNames = new Set(
|
|
1402
|
+
route.segments.filter((s) => s.type !== "static").map((s) => s.value)
|
|
1403
|
+
);
|
|
1404
|
+
return route.instances.find((instance) => {
|
|
1405
|
+
for (const name of paramNames) {
|
|
1406
|
+
const urlVal = params[name];
|
|
1407
|
+
const instVal = instance.params[name];
|
|
1408
|
+
if (urlVal === void 0 && instVal === void 0)
|
|
1409
|
+
continue;
|
|
1410
|
+
if (urlVal !== instVal)
|
|
1411
|
+
return false;
|
|
1412
|
+
}
|
|
1413
|
+
return true;
|
|
1414
|
+
});
|
|
1415
|
+
}
|
|
1416
|
+
function buildImportMap(sharedManifest, publicBasePath, sharedDir = "shared") {
|
|
1417
|
+
const imports = {};
|
|
1418
|
+
for (const [pkgName, hashedFile] of Object.entries(sharedManifest)) {
|
|
1419
|
+
imports[pkgName] = `${publicBasePath}${sharedDir}/${hashedFile}`;
|
|
1420
|
+
}
|
|
1421
|
+
return imports;
|
|
1422
|
+
}
|
|
1423
|
+
const pagePartsCache = /* @__PURE__ */ new Map();
|
|
1424
|
+
async function getPageParts(route, pageModule, artifacts, preRenderedPath, manifest) {
|
|
1425
|
+
const cacheKey = route.pattern;
|
|
1426
|
+
const cached = pagePartsCache.get(cacheKey);
|
|
1427
|
+
if (cached)
|
|
1428
|
+
return cached;
|
|
1429
|
+
if (!route.jayHtmlPath) {
|
|
1430
|
+
return {
|
|
1431
|
+
parts: [
|
|
1432
|
+
{
|
|
1433
|
+
compDefinition: pageModule.page ?? pageModule.default,
|
|
1434
|
+
clientImport: "",
|
|
1435
|
+
clientPart: ""
|
|
1436
|
+
}
|
|
1437
|
+
],
|
|
1438
|
+
headlessContracts: [],
|
|
1439
|
+
headlessInstanceComponents: [],
|
|
1440
|
+
discoveredInstances: [],
|
|
1441
|
+
forEachInstances: [],
|
|
1442
|
+
keyedPartModules: []
|
|
1443
|
+
};
|
|
1444
|
+
}
|
|
1445
|
+
const jayHtmlContent = await artifacts.readRawFile(preRenderedPath);
|
|
1446
|
+
const serverBuildDir = artifacts.getAssetPath("server");
|
|
1447
|
+
const parts = await loadProductionPageParts(
|
|
1448
|
+
{ jayHtmlPath: route.jayHtmlPath, componentExport: route.componentExport },
|
|
1449
|
+
pageModule,
|
|
1450
|
+
jayHtmlContent,
|
|
1451
|
+
manifest.projectRoot,
|
|
1452
|
+
void 0,
|
|
1453
|
+
serverBuildDir
|
|
1454
|
+
);
|
|
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;
|
|
1494
|
+
}
|
|
1495
|
+
const fastViewState = fastResult.rendered || {};
|
|
1496
|
+
const fastCarryForward = fastResult.carryForward || {};
|
|
1497
|
+
const headTagSources = [];
|
|
1498
|
+
const slowHeadTags = preRendered.carryForward.__slowHeadTags;
|
|
1499
|
+
if (slowHeadTags)
|
|
1500
|
+
headTagSources.push(...slowHeadTags);
|
|
1501
|
+
const fastHeadTags = fastResult.headTags;
|
|
1502
|
+
if (fastHeadTags)
|
|
1503
|
+
headTagSources.push(fastHeadTags);
|
|
1504
|
+
const headTags = headTagSources.length > 0 ? mergeHeadTags(headTagSources) : [];
|
|
1505
|
+
const headTagsHtml = headTags.length > 0 ? serializeHeadTags(headTags) + "\n" : "";
|
|
1506
|
+
headTags.some((t) => t.tag.toLowerCase() === "title");
|
|
1507
|
+
const fullViewState = deepMergeViewStates(
|
|
1508
|
+
preRendered.slowViewState,
|
|
1509
|
+
fastViewState,
|
|
1510
|
+
route.trackByMap || {}
|
|
1511
|
+
);
|
|
1512
|
+
const serverElement = await artifacts.loadServerElement(instance.serverElementPath);
|
|
1513
|
+
const asyncPromises = [];
|
|
1514
|
+
const importMap = buildImportMap(manifest.sharedManifest, manifest.publicBasePath);
|
|
1515
|
+
const modulePreloads = Object.values(importMap).map((url2) => ` <link rel="modulepreload" href="${url2}" />`).join("\n");
|
|
1516
|
+
const cssLink = instance.clientCssPath ? ` <link rel="stylesheet" href="${manifest.publicBasePath}${instance.clientCssPath}" />` : "";
|
|
1517
|
+
res.writeHead(200, {
|
|
1518
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
1519
|
+
"Transfer-Encoding": "chunked"
|
|
1520
|
+
});
|
|
1521
|
+
const headParts = [headTagsHtml, modulePreloads, cssLink].filter(Boolean).join("\n");
|
|
1522
|
+
res.write(`<!doctype html>
|
|
1523
|
+
<html lang="en">
|
|
1524
|
+
<head>
|
|
1525
|
+
<meta charset="UTF-8" />
|
|
1526
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
1527
|
+
${headParts}
|
|
1528
|
+
<script type="importmap">${JSON.stringify({ imports: importMap })}<\/script>
|
|
1529
|
+
</head>
|
|
1530
|
+
<body>
|
|
1531
|
+
<div id="target">`);
|
|
1532
|
+
serverElement.renderToStream(fullViewState, {
|
|
1533
|
+
write: (chunk) => res.write(chunk),
|
|
1534
|
+
onAsync: (promise, id, templates) => {
|
|
1535
|
+
asyncPromises.push(
|
|
1536
|
+
promise.then(
|
|
1537
|
+
(val) => asyncSwapScript(id, templates.resolved(val)),
|
|
1538
|
+
(err) => asyncSwapScript(id, templates.rejected(err))
|
|
1539
|
+
)
|
|
1540
|
+
);
|
|
1541
|
+
}
|
|
1542
|
+
});
|
|
1543
|
+
res.write("</div>");
|
|
1544
|
+
const asyncScripts = (await Promise.all(asyncPromises)).filter((s) => s).join("");
|
|
1545
|
+
if (asyncScripts)
|
|
1546
|
+
res.write(asyncScripts);
|
|
1547
|
+
const clientInitData = getClientInitData();
|
|
1548
|
+
const clientBundleUrl = `${manifest.publicBasePath}${instance.clientBundlePath}`;
|
|
1549
|
+
res.write(`
|
|
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;
|
|
1581
|
+
}
|
|
1582
|
+
try {
|
|
1583
|
+
const content = await fs.readFile(filePath);
|
|
1584
|
+
const ext = path.extname(filePath);
|
|
1585
|
+
const contentType = MIME_TYPES[ext] || "application/octet-stream";
|
|
1586
|
+
const isHashed = /[-][a-zA-Z0-9_-]{6,}\./.test(path.basename(filePath));
|
|
1587
|
+
const cacheControl = isHashed ? "public, max-age=31536000, immutable" : "public, max-age=3600";
|
|
1588
|
+
res.writeHead(200, {
|
|
1589
|
+
"Content-Type": contentType,
|
|
1590
|
+
"Content-Length": content.length,
|
|
1591
|
+
"Cache-Control": cacheControl
|
|
1592
|
+
});
|
|
1593
|
+
res.end(content);
|
|
1594
|
+
return true;
|
|
1595
|
+
} catch {
|
|
1596
|
+
return false;
|
|
1597
|
+
}
|
|
1598
|
+
}
|
|
1599
|
+
const ACTION_PREFIX = "/_jay/actions/";
|
|
1600
|
+
function isActionRequest(pathname) {
|
|
1601
|
+
return pathname.startsWith(ACTION_PREFIX);
|
|
1602
|
+
}
|
|
1603
|
+
async function handleActionRequest(req, res, registry = actionRegistry) {
|
|
1604
|
+
const url = new URL(req.url || "/", `http://${req.headers.host}`);
|
|
1605
|
+
const actionName = url.pathname.slice(ACTION_PREFIX.length);
|
|
1606
|
+
if (!actionName) {
|
|
1607
|
+
jsonResponse(res, 400, {
|
|
1608
|
+
success: false,
|
|
1609
|
+
error: {
|
|
1610
|
+
code: "MISSING_ACTION_NAME",
|
|
1611
|
+
message: "Action name is required",
|
|
1612
|
+
isActionError: false
|
|
1613
|
+
}
|
|
1614
|
+
});
|
|
1615
|
+
return;
|
|
1616
|
+
}
|
|
1617
|
+
const action = registry.get(actionName);
|
|
1618
|
+
if (!action) {
|
|
1619
|
+
jsonResponse(res, 404, {
|
|
1620
|
+
success: false,
|
|
1621
|
+
error: {
|
|
1622
|
+
code: "ACTION_NOT_FOUND",
|
|
1623
|
+
message: `Action '${actionName}' is not registered`,
|
|
1624
|
+
isActionError: false
|
|
1625
|
+
}
|
|
1626
|
+
});
|
|
1627
|
+
return;
|
|
1628
|
+
}
|
|
1629
|
+
const requestMethod = (req.method || "GET").toUpperCase();
|
|
1630
|
+
if (requestMethod !== action.method) {
|
|
1631
|
+
jsonResponse(res, 405, {
|
|
1632
|
+
success: false,
|
|
1633
|
+
error: {
|
|
1634
|
+
code: "METHOD_NOT_ALLOWED",
|
|
1635
|
+
message: `Action '${actionName}' expects ${action.method}, got ${requestMethod}`,
|
|
1636
|
+
isActionError: false
|
|
1637
|
+
}
|
|
1638
|
+
});
|
|
1639
|
+
return;
|
|
1640
|
+
}
|
|
1641
|
+
let input;
|
|
1642
|
+
try {
|
|
1643
|
+
if (requestMethod === "GET") {
|
|
1644
|
+
const inputParam = url.searchParams.get("_input");
|
|
1645
|
+
if (inputParam) {
|
|
1646
|
+
input = JSON.parse(inputParam);
|
|
1647
|
+
} else {
|
|
1648
|
+
input = Object.fromEntries(url.searchParams.entries());
|
|
1649
|
+
delete input._input;
|
|
1650
|
+
}
|
|
1651
|
+
} else {
|
|
1652
|
+
input = await parseBody(req);
|
|
1653
|
+
}
|
|
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
|
+
}
|
|
1665
|
+
if (registry.isStreaming(actionName)) {
|
|
1666
|
+
res.writeHead(200, {
|
|
1667
|
+
"Content-Type": "application/x-ndjson",
|
|
1668
|
+
"Transfer-Encoding": "chunked"
|
|
1669
|
+
});
|
|
1670
|
+
try {
|
|
1671
|
+
const generator = registry.executeStream(actionName, input);
|
|
1672
|
+
for await (const chunk of generator) {
|
|
1673
|
+
res.write(JSON.stringify({ chunk }) + "\n");
|
|
1674
|
+
}
|
|
1675
|
+
res.write(JSON.stringify({ done: true }) + "\n");
|
|
1676
|
+
} catch (err) {
|
|
1677
|
+
res.write(JSON.stringify({ error: err.message }) + "\n");
|
|
1678
|
+
}
|
|
1679
|
+
res.end();
|
|
1680
|
+
return;
|
|
1681
|
+
}
|
|
1682
|
+
const result = await registry.execute(actionName, input);
|
|
1683
|
+
if (result.success) {
|
|
1684
|
+
if (requestMethod === "GET") {
|
|
1685
|
+
const cacheHeaders = registry.getCacheHeaders(actionName);
|
|
1686
|
+
if (cacheHeaders) {
|
|
1687
|
+
res.setHeader("Cache-Control", cacheHeaders);
|
|
1688
|
+
}
|
|
1689
|
+
}
|
|
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
|
+
}
|
|
1695
|
+
}
|
|
1696
|
+
async function registerActionsFromManifest(actions, buildDir, registry = actionRegistry) {
|
|
1697
|
+
const logger = getLogger();
|
|
1698
|
+
let count = 0;
|
|
1699
|
+
for (const entry of actions) {
|
|
1700
|
+
try {
|
|
1701
|
+
const modulePath = entry.isPlugin ? entry.packageName : `${buildDir}/${entry.serverModule}`;
|
|
1702
|
+
const mod = await import(modulePath);
|
|
1703
|
+
for (const [, exported] of Object.entries(mod)) {
|
|
1704
|
+
if (isJayAction(exported)) {
|
|
1705
|
+
registry.register(exported);
|
|
1706
|
+
count++;
|
|
1707
|
+
} else if (isJayStreamAction(exported)) {
|
|
1708
|
+
registry.registerStream(exported);
|
|
1709
|
+
count++;
|
|
1710
|
+
}
|
|
1711
|
+
}
|
|
1712
|
+
} catch (err) {
|
|
1713
|
+
logger.error(
|
|
1714
|
+
`[Server] Failed to load action module ${entry.serverModule}: ${err.message}`
|
|
1715
|
+
);
|
|
1716
|
+
}
|
|
1717
|
+
}
|
|
1718
|
+
logger.info(`[Server] Registered ${count} actions`);
|
|
1719
|
+
}
|
|
1720
|
+
function jsonResponse(res, status, body) {
|
|
1721
|
+
const json = JSON.stringify(body);
|
|
1722
|
+
res.writeHead(status, {
|
|
1723
|
+
"Content-Type": "application/json",
|
|
1724
|
+
"Content-Length": Buffer.byteLength(json)
|
|
1725
|
+
});
|
|
1726
|
+
res.end(json);
|
|
1727
|
+
}
|
|
1728
|
+
function parseBody(req) {
|
|
1729
|
+
return new Promise((resolve, reject) => {
|
|
1730
|
+
const chunks = [];
|
|
1731
|
+
req.on("data", (chunk) => chunks.push(chunk));
|
|
1732
|
+
req.on("end", () => {
|
|
1733
|
+
const raw = Buffer.concat(chunks).toString("utf-8");
|
|
1734
|
+
if (!raw) {
|
|
1735
|
+
resolve({});
|
|
1736
|
+
return;
|
|
1737
|
+
}
|
|
1738
|
+
try {
|
|
1739
|
+
resolve(JSON.parse(raw));
|
|
1740
|
+
} catch {
|
|
1741
|
+
reject(new Error("Invalid JSON body"));
|
|
1742
|
+
}
|
|
1743
|
+
});
|
|
1744
|
+
req.on("error", reject);
|
|
1745
|
+
});
|
|
1746
|
+
}
|
|
1747
|
+
function getStatusCode(code, isActionError) {
|
|
1748
|
+
if (isActionError)
|
|
1749
|
+
return 422;
|
|
1750
|
+
switch (code) {
|
|
1751
|
+
case "ACTION_NOT_FOUND":
|
|
1752
|
+
return 404;
|
|
1753
|
+
case "INVALID_INPUT":
|
|
1754
|
+
case "VALIDATION_ERROR":
|
|
1755
|
+
return 400;
|
|
1756
|
+
case "UNAUTHORIZED":
|
|
1757
|
+
return 401;
|
|
1758
|
+
case "FORBIDDEN":
|
|
1759
|
+
return 403;
|
|
1760
|
+
default:
|
|
1761
|
+
return 500;
|
|
1762
|
+
}
|
|
1763
|
+
}
|
|
1764
|
+
async function startMainServer(options) {
|
|
1765
|
+
const logger = getLogger();
|
|
1766
|
+
const buildDir = path.join(options.buildRoot, `v${options.version}`);
|
|
1767
|
+
const artifacts = new FilesystemArtifactStore(buildDir);
|
|
1768
|
+
const manifest = await artifacts.readManifest();
|
|
1769
|
+
logger.important(
|
|
1770
|
+
`[Server] Loaded manifest: ${manifest.routes.length} routes, v${manifest.version}`
|
|
1771
|
+
);
|
|
1772
|
+
const { discoverPluginsWithInit, sortPluginsByDependencies } = await import("@jay-framework/stack-server-runtime");
|
|
1773
|
+
try {
|
|
1774
|
+
const pluginsWithInit = sortPluginsByDependencies(
|
|
1775
|
+
await discoverPluginsWithInit({ projectRoot: manifest.projectRoot })
|
|
1776
|
+
);
|
|
1777
|
+
for (const pluginInit of pluginsWithInit) {
|
|
1778
|
+
try {
|
|
1779
|
+
const pluginModule = await import(pluginInit.packageName);
|
|
1780
|
+
const init = pluginModule.init || pluginModule[pluginInit.initExport || "init"];
|
|
1781
|
+
if (init?._serverInit) {
|
|
1782
|
+
logger.info(`[Server] Running plugin init: ${pluginInit.name}`);
|
|
1783
|
+
const data = await init._serverInit();
|
|
1784
|
+
if (data)
|
|
1785
|
+
setClientInitData(pluginInit.name, data);
|
|
1786
|
+
}
|
|
1787
|
+
} catch (err) {
|
|
1788
|
+
logger.warn(`[Server] Plugin init failed: ${pluginInit.name}: ${err.message}`);
|
|
1789
|
+
}
|
|
1790
|
+
}
|
|
1791
|
+
} catch {
|
|
1792
|
+
}
|
|
1793
|
+
const initModule = await artifacts.loadPageModule("server/init.js").catch(() => null);
|
|
1794
|
+
if (initModule) {
|
|
1795
|
+
const init = initModule.init || initModule.default;
|
|
1796
|
+
if (init?._serverInit) {
|
|
1797
|
+
logger.important("[Server] Running server init...");
|
|
1798
|
+
const data = await init._serverInit();
|
|
1799
|
+
if (data)
|
|
1800
|
+
setClientInitData("project", data);
|
|
1801
|
+
}
|
|
1802
|
+
}
|
|
1803
|
+
if (manifest.actions.length > 0) {
|
|
1804
|
+
await registerActionsFromManifest(manifest.actions, buildDir);
|
|
1805
|
+
}
|
|
1806
|
+
const server = http.createServer(async (req, res) => {
|
|
1807
|
+
const url = new URL(req.url || "/", `http://${req.headers.host}`);
|
|
1808
|
+
try {
|
|
1809
|
+
if (isActionRequest(url.pathname)) {
|
|
1810
|
+
await handleActionRequest(req, res);
|
|
1811
|
+
return;
|
|
1812
|
+
}
|
|
1813
|
+
const handled = await handleStaticRequest(
|
|
1814
|
+
req,
|
|
1815
|
+
res,
|
|
1816
|
+
path.join(buildDir, "shared"),
|
|
1817
|
+
"/shared/"
|
|
1818
|
+
);
|
|
1819
|
+
if (handled)
|
|
1820
|
+
return;
|
|
1821
|
+
const handledInstances = await handleStaticRequest(
|
|
1822
|
+
req,
|
|
1823
|
+
res,
|
|
1824
|
+
path.join(buildDir, "pre-rendered"),
|
|
1825
|
+
"/pre-rendered/"
|
|
1826
|
+
);
|
|
1827
|
+
if (handledInstances)
|
|
1828
|
+
return;
|
|
1829
|
+
const currentManifest = await artifacts.readManifest();
|
|
1830
|
+
const match = matchRequest(currentManifest, url.pathname);
|
|
1831
|
+
if (!match) {
|
|
1832
|
+
res.writeHead(404);
|
|
1833
|
+
res.end("Not Found");
|
|
1834
|
+
return;
|
|
1835
|
+
}
|
|
1836
|
+
await handlePageRequest(res, match, currentManifest, artifacts);
|
|
1837
|
+
} catch (err) {
|
|
1838
|
+
logger.error(`[Server] Error handling ${url.pathname}: ${err.message}`);
|
|
1839
|
+
if (!res.headersSent) {
|
|
1840
|
+
res.writeHead(500);
|
|
1841
|
+
res.end("Internal Server Error");
|
|
1842
|
+
}
|
|
1843
|
+
}
|
|
1844
|
+
});
|
|
1845
|
+
server.listen(options.port, () => {
|
|
1846
|
+
logger.important(
|
|
1847
|
+
`[Server] Production server listening on http://localhost:${options.port}`
|
|
1848
|
+
);
|
|
1849
|
+
});
|
|
1850
|
+
}
|
|
1851
|
+
export {
|
|
1852
|
+
buildVersion,
|
|
1853
|
+
startMainServer
|
|
1854
|
+
};
|