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