@jay-framework/jay-stack-cli 0.25.0 → 0.25.2
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/agent-kit-template/contracts/GUIDE.md +8 -8
- package/agent-kit-template/contracts/linked-contracts.md +1 -1
- package/agent-kit-template/designer/INSTRUCTIONS.md +1 -0
- package/agent-kit-template/designer/cli-commands.md +7 -0
- package/agent-kit-template/designer/contracts-and-plugins.md +1 -1
- package/agent-kit-template/designer/error-pages-guide.md +109 -0
- package/agent-kit-template/designer/jay-html-template-syntax.md +1 -1
- package/agent-kit-template/designer/routing.md +7 -0
- package/agent-kit-template/designer/validation-guide.md +27 -3
- package/agent-kit-template/designer/validation-tier-2-guide.md +64 -0
- package/agent-kit-template/developer/INSTRUCTIONS.md +1 -0
- package/agent-kit-template/developer/error-pages-guide.md +86 -0
- package/agent-kit-template/developer/render-results.md +16 -0
- package/agent-kit-template/developer/seo-guide.md +110 -1
- package/agent-kit-template/plugin/validation.md +9 -0
- package/dist/{index-l6QVpvnY.js → index-B5oavmfA.js} +906 -710
- package/dist/index.d.ts +34 -1
- package/dist/index.js +1023 -21
- package/package.json +14 -13
|
@@ -5,18 +5,18 @@ import { JayRouteParamType, scanRoutes, parseRouteSegments } from "@jay-framewor
|
|
|
5
5
|
import { getLogger } from "@jay-framework/logger";
|
|
6
6
|
import path__default from "node:path";
|
|
7
7
|
import fs from "node:fs/promises";
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
8
|
+
import crypto, { createHash } from "node:crypto";
|
|
9
|
+
import { jayRuntime } from "@jay-framework/vite-plugin";
|
|
10
10
|
import { parseJayFile, JAY_IMPORT_RESOLVER, assignCoordinatesToJayHtml, discoverHeadlessInstances, generateElementHydrateFile, generateServerElementFile } from "@jay-framework/compiler-jay-html";
|
|
11
11
|
import { normalizeActionEntry, checkValidationErrors, RuntimeMode } from "@jay-framework/compiler-shared";
|
|
12
|
-
import
|
|
12
|
+
import { parse } from "node-html-parser";
|
|
13
|
+
import { transform } from "esbuild";
|
|
14
|
+
import { createRequire } from "node:module";
|
|
15
|
+
import { runLoadParams, scanPlugins, makePassthroughInstanceComponent, DevSlowlyChangingPhase, slowRenderInstances } from "@jay-framework/stack-server-runtime";
|
|
13
16
|
import * as productionServer from "@jay-framework/production-server";
|
|
14
|
-
import { generateSitemap } from "@jay-framework/production-server";
|
|
17
|
+
import { generateSiteJsonLd, generateSitemap, generateRobotsTxtIfAbsent, generateLlmsTxt } from "@jay-framework/production-server";
|
|
15
18
|
export * from "@jay-framework/production-server";
|
|
16
19
|
import fs__default from "node:fs";
|
|
17
|
-
import { jayRuntime } from "@jay-framework/vite-plugin";
|
|
18
|
-
import { parse } from "node-html-parser";
|
|
19
|
-
import { transform } from "esbuild";
|
|
20
20
|
function isCompilableTypeScriptFile(fileName) {
|
|
21
21
|
return fileName.endsWith(".ts") && !fileName.endsWith(".d.ts") && fileName !== "page.ts";
|
|
22
22
|
}
|
|
@@ -40,7 +40,7 @@ async function collectTypeScriptEntries(rootDir, entryPrefix, pages) {
|
|
|
40
40
|
}
|
|
41
41
|
async function discoverServerEntries(projectRoot, pagesRoot) {
|
|
42
42
|
const logger = getLogger();
|
|
43
|
-
const routes = await scanRoutes(pagesRoot, {
|
|
43
|
+
const { routes, errorPages } = await scanRoutes(pagesRoot, {
|
|
44
44
|
jayHtmlFilename: "page.jay-html",
|
|
45
45
|
compFilename: "page.ts"
|
|
46
46
|
});
|
|
@@ -52,6 +52,13 @@ async function discoverServerEntries(projectRoot, pagesRoot) {
|
|
|
52
52
|
pages[entryName] = route.compPath;
|
|
53
53
|
}
|
|
54
54
|
}
|
|
55
|
+
for (const errorPage of errorPages) {
|
|
56
|
+
if (errorPage.compPath) {
|
|
57
|
+
const relativePath = path__default.relative(projectRoot, errorPage.compPath);
|
|
58
|
+
const entryName = relativePath.replace(/^src\//, "").replace(/\.ts$/, "");
|
|
59
|
+
pages[entryName] = errorPage.compPath;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
55
62
|
const actions = {};
|
|
56
63
|
const actionsDir = path__default.join(projectRoot, "src", "actions");
|
|
57
64
|
try {
|
|
@@ -96,9 +103,9 @@ async function discoverServerEntries(projectRoot, pagesRoot) {
|
|
|
96
103
|
}
|
|
97
104
|
const entries = { init, pages, actions };
|
|
98
105
|
logger.info(
|
|
99
|
-
`[Build] Discovered: ${Object.keys(pages).length} pages, ${Object.keys(actions).length} actions, init: ${init ? "yes" : "no"}`
|
|
106
|
+
`[Build] Discovered: ${Object.keys(pages).length} pages, ${Object.keys(actions).length} actions, ${errorPages.length} error pages, init: ${init ? "yes" : "no"}`
|
|
100
107
|
);
|
|
101
|
-
return { entries, routes };
|
|
108
|
+
return { entries, routes, errorPages };
|
|
102
109
|
}
|
|
103
110
|
async function buildServerCode(entries, jayOptions, outputDir, projectRoot) {
|
|
104
111
|
const logger = getLogger();
|
|
@@ -145,127 +152,314 @@ async function buildServerCode(entries, jayOptions, outputDir, projectRoot) {
|
|
|
145
152
|
});
|
|
146
153
|
logger.info(`[Build] Server code compiled to ${outputDir}`);
|
|
147
154
|
}
|
|
148
|
-
|
|
149
|
-
const
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
const
|
|
159
|
-
|
|
155
|
+
async function compileServerElement(jayHtmlContent, jayHtmlFilename, jayHtmlDir, outputPath, projectRoot, tsConfigFilePath, sourceDir, minifyCss = true) {
|
|
156
|
+
const jayFile = await parseJayFile(
|
|
157
|
+
jayHtmlContent,
|
|
158
|
+
jayHtmlFilename,
|
|
159
|
+
jayHtmlDir,
|
|
160
|
+
{ relativePath: tsConfigFilePath },
|
|
161
|
+
JAY_IMPORT_RESOLVER,
|
|
162
|
+
projectRoot,
|
|
163
|
+
sourceDir
|
|
164
|
+
);
|
|
165
|
+
const parsedJayFile = checkValidationErrors(jayFile);
|
|
166
|
+
const serverElementCode = checkValidationErrors(generateServerElementFile(parsedJayFile));
|
|
167
|
+
const outputDir = path__default.dirname(outputPath);
|
|
160
168
|
await fs.mkdir(outputDir, { recursive: true });
|
|
161
|
-
const
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
+
const tsPath = outputPath.replace(/\.js$/, ".ts");
|
|
170
|
+
await fs.writeFile(tsPath, serverElementCode, "utf-8");
|
|
171
|
+
const jayOptions = { tsConfigFilePath };
|
|
172
|
+
await build({
|
|
173
|
+
root: projectRoot,
|
|
174
|
+
publicDir: false,
|
|
175
|
+
plugins: [jayRuntime(jayOptions)],
|
|
176
|
+
build: {
|
|
177
|
+
outDir: outputDir,
|
|
178
|
+
emptyOutDir: false,
|
|
179
|
+
minify: false,
|
|
180
|
+
ssr: true,
|
|
181
|
+
rollupOptions: {
|
|
182
|
+
input: { [path__default.basename(outputPath, ".js")]: tsPath },
|
|
183
|
+
external: [/^node:/, /^@jay-framework\//],
|
|
184
|
+
output: {
|
|
185
|
+
entryFileNames: "[name].js",
|
|
186
|
+
format: "es"
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
},
|
|
190
|
+
logLevel: "warn"
|
|
191
|
+
});
|
|
192
|
+
await fs.rm(tsPath, { force: true });
|
|
193
|
+
let cssFile;
|
|
194
|
+
let cssImports;
|
|
195
|
+
const css = parsedJayFile.css;
|
|
196
|
+
if (css) {
|
|
197
|
+
cssImports = extractCssImportUrls(css);
|
|
198
|
+
const cssContent = minifyCss ? (await transform(css, { loader: "css", minify: true })).code : css;
|
|
199
|
+
const hash = createHash("sha256").update(cssContent).digest("hex").slice(0, 8);
|
|
200
|
+
const baseName = path__default.basename(outputPath, ".server-element.js");
|
|
201
|
+
const cssFilename = `${baseName}-${hash}.css`;
|
|
202
|
+
await fs.writeFile(path__default.join(outputDir, cssFilename), cssContent, "utf-8");
|
|
203
|
+
cssFile = cssFilename;
|
|
169
204
|
}
|
|
170
|
-
|
|
205
|
+
getLogger().info(`[Build] Compiled server element: ${path__default.basename(outputPath)}`);
|
|
206
|
+
return {
|
|
207
|
+
cssFile,
|
|
208
|
+
cssImports,
|
|
209
|
+
headMeta: parsedJayFile.headMeta,
|
|
210
|
+
scripts: parsedJayFile.scripts
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
async function compileRouteServerElement(jayHtmlPath, outputPath, projectRoot, tsConfigFilePath, minifyCss = true) {
|
|
214
|
+
const jayHtmlContent = await fs.readFile(jayHtmlPath, "utf-8");
|
|
215
|
+
const sourceDir = path__default.dirname(jayHtmlPath);
|
|
216
|
+
const outputDir = path__default.dirname(outputPath);
|
|
217
|
+
const jayHtml = resolveJayHtmlPaths(jayHtmlContent, sourceDir, outputDir);
|
|
218
|
+
return compileServerElement(
|
|
219
|
+
jayHtml,
|
|
220
|
+
path__default.basename(jayHtmlPath),
|
|
221
|
+
outputDir,
|
|
222
|
+
outputPath,
|
|
223
|
+
projectRoot,
|
|
224
|
+
tsConfigFilePath,
|
|
225
|
+
sourceDir,
|
|
226
|
+
minifyCss
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
async function compileRouteHydrateScript(jayHtmlPath, outputDir, projectRoot, tsConfigFilePath, minify = true) {
|
|
230
|
+
const jayHtmlContent = await fs.readFile(jayHtmlPath, "utf-8");
|
|
231
|
+
const sourceDir = path__default.dirname(jayHtmlPath);
|
|
232
|
+
const jayHtml = resolveJayHtmlPaths(jayHtmlContent, sourceDir, outputDir);
|
|
233
|
+
const jayFile = await parseJayFile(
|
|
234
|
+
jayHtml,
|
|
235
|
+
path__default.basename(jayHtmlPath),
|
|
236
|
+
outputDir,
|
|
237
|
+
{ relativePath: tsConfigFilePath },
|
|
238
|
+
JAY_IMPORT_RESOLVER,
|
|
239
|
+
projectRoot,
|
|
240
|
+
sourceDir
|
|
241
|
+
);
|
|
242
|
+
const parsedJayFile = checkValidationErrors(jayFile);
|
|
243
|
+
const hydrateCode = checkValidationErrors(
|
|
244
|
+
generateElementHydrateFile(parsedJayFile, RuntimeMode.MainTrusted)
|
|
245
|
+
);
|
|
246
|
+
await fs.mkdir(outputDir, { recursive: true });
|
|
247
|
+
const tsPath = path__default.join(outputDir, "route.hydrate.ts");
|
|
248
|
+
await fs.writeFile(tsPath, hydrateCode, "utf-8");
|
|
249
|
+
const jayOptions = { tsConfigFilePath };
|
|
171
250
|
await build({
|
|
251
|
+
root: projectRoot,
|
|
172
252
|
publicDir: false,
|
|
253
|
+
plugins: [...jayStackCompiler(jayOptions)],
|
|
173
254
|
build: {
|
|
174
255
|
outDir: outputDir,
|
|
175
|
-
emptyOutDir:
|
|
256
|
+
emptyOutDir: false,
|
|
176
257
|
minify,
|
|
177
|
-
manifest: "
|
|
258
|
+
manifest: "route-hydrate-manifest.json",
|
|
178
259
|
rollupOptions: {
|
|
179
|
-
input:
|
|
260
|
+
input: { "route.hydrate": tsPath },
|
|
261
|
+
external: (id) => id.startsWith("@jay-framework/"),
|
|
180
262
|
output: {
|
|
181
263
|
entryFileNames: "[name]-[hash].js",
|
|
182
|
-
chunkFileNames: "[name]-[hash].js",
|
|
183
264
|
format: "es"
|
|
184
265
|
},
|
|
185
266
|
preserveEntrySignatures: "exports-only"
|
|
186
267
|
}
|
|
187
268
|
},
|
|
188
|
-
resolve: {
|
|
189
|
-
dedupe: dedupePackages
|
|
190
|
-
},
|
|
191
269
|
logLevel: "warn"
|
|
192
270
|
});
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
}
|
|
197
|
-
const
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
return pkg.replace("@jay-framework/", "").replace(/[/-]/g, "_");
|
|
271
|
+
await fs.rm(tsPath, { force: true });
|
|
272
|
+
const manifestPath = path__default.join(outputDir, "route-hydrate-manifest.json");
|
|
273
|
+
const manifest = JSON.parse(await fs.readFile(manifestPath, "utf-8"));
|
|
274
|
+
await fs.rm(manifestPath, { force: true });
|
|
275
|
+
const entryKey = Object.keys(manifest).find((k) => manifest[k].isEntry);
|
|
276
|
+
if (!entryKey) throw new Error("No entry in route hydrate manifest");
|
|
277
|
+
const jsFile = manifest[entryKey].file;
|
|
278
|
+
getLogger().info(`[Build] Compiled route hydrate script: ${jsFile}`);
|
|
279
|
+
return { jsFile };
|
|
203
280
|
}
|
|
204
|
-
|
|
205
|
-
const
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
281
|
+
function resolveJayHtmlPaths(html, sourceDir, targetDir) {
|
|
282
|
+
const root = parse(html, {
|
|
283
|
+
comment: true,
|
|
284
|
+
blockTextElements: { script: true, style: true }
|
|
285
|
+
});
|
|
286
|
+
const rewrite = (el, attr) => {
|
|
287
|
+
const val = el.getAttribute(attr);
|
|
288
|
+
if (val && (val.startsWith("./") || val.startsWith("../"))) {
|
|
289
|
+
const abs = path__default.resolve(sourceDir, val);
|
|
290
|
+
let rel = path__default.relative(targetDir, abs);
|
|
291
|
+
if (!rel.startsWith(".")) rel = "./" + rel;
|
|
292
|
+
el.setAttribute(attr, rel);
|
|
293
|
+
}
|
|
294
|
+
};
|
|
295
|
+
for (const el of root.querySelectorAll('script[type="application/jay-data"]')) {
|
|
296
|
+
rewrite(el, "contract");
|
|
210
297
|
}
|
|
211
|
-
const
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
298
|
+
for (const el of root.querySelectorAll('script[type="application/jay-headless"]')) {
|
|
299
|
+
rewrite(el, "src");
|
|
300
|
+
rewrite(el, "contract");
|
|
301
|
+
}
|
|
302
|
+
for (const el of root.querySelectorAll('script[type="application/jay-headfull"]')) {
|
|
303
|
+
rewrite(el, "src");
|
|
304
|
+
rewrite(el, "contract");
|
|
305
|
+
}
|
|
306
|
+
for (const el of root.querySelectorAll('link[rel="stylesheet"]')) {
|
|
307
|
+
rewrite(el, "href");
|
|
308
|
+
}
|
|
309
|
+
return root.toString();
|
|
310
|
+
}
|
|
311
|
+
function extractCssImportUrls(css) {
|
|
312
|
+
const imports = [];
|
|
313
|
+
const re = /@import\s*(?:url\(\s*['"]?([^'")]+)['"]?\s*\)|['"]([^'"]+)['"])/g;
|
|
314
|
+
let match;
|
|
315
|
+
while ((match = re.exec(css)) !== null) {
|
|
316
|
+
const url = match[1] || match[2];
|
|
317
|
+
if (url.startsWith("https://")) {
|
|
318
|
+
imports.push(url);
|
|
220
319
|
}
|
|
221
320
|
}
|
|
222
|
-
|
|
223
|
-
await fs.writeFile(sharedManifestPath, JSON.stringify(manifest, null, 2));
|
|
224
|
-
await fs.rm(viteManifestPath, { force: true });
|
|
225
|
-
return manifest;
|
|
321
|
+
return imports;
|
|
226
322
|
}
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
const
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
323
|
+
async function generateRouteHydrationEntry(options) {
|
|
324
|
+
const {
|
|
325
|
+
hydrateImport,
|
|
326
|
+
pageModulePath,
|
|
327
|
+
pageExportName = "page",
|
|
328
|
+
trackByMap,
|
|
329
|
+
outputPath,
|
|
330
|
+
keyedParts = [],
|
|
331
|
+
clientInits = []
|
|
332
|
+
} = options;
|
|
333
|
+
const partImports = keyedParts.map((p, i) => `import { ${p.exportName} as keyedPart${i} } from '${p.modulePath}';`).join("\n");
|
|
334
|
+
const hasPageModule = pageModulePath && pageExportName;
|
|
335
|
+
const pagePartExpr = hasPageModule ? `pagePart && pagePart.comp ? { comp: pagePart.comp, contextMarkers: pagePart.contexts || [] } : null` : `null`;
|
|
336
|
+
const partsArray = [
|
|
337
|
+
pagePartExpr,
|
|
338
|
+
...keyedParts.map(
|
|
339
|
+
(p, i) => `keyedPart${i} && keyedPart${i}.comp ? { comp: keyedPart${i}.comp, contextMarkers: keyedPart${i}.contexts || [], key: '${p.key}' } : null`
|
|
340
|
+
)
|
|
341
|
+
];
|
|
342
|
+
const pageImport = hasPageModule ? `import { ${pageExportName} as pagePart } from '${pageModulePath}';` : "";
|
|
343
|
+
const initImports = clientInits.map((ci, i) => `import { ${ci.exportName} as clientInit${i} } from '${ci.modulePath}';`).join("\n");
|
|
344
|
+
const initCalls = clientInits.map(
|
|
345
|
+
(ci, i) => ` if (clientInit${i}?._clientInit) await clientInit${i}._clientInit(clientInitData['${ci.key}'] || {});`
|
|
346
|
+
).join("\n");
|
|
347
|
+
const code = `import { hydrateCompositeJayComponent } from '@jay-framework/stack-client-runtime';
|
|
348
|
+
import { deepMergeViewStates } from '@jay-framework/view-state-merge';
|
|
349
|
+
import { hydrate } from '${hydrateImport}';
|
|
350
|
+
${pageImport}
|
|
351
|
+
${partImports}
|
|
352
|
+
${initImports}
|
|
353
|
+
|
|
354
|
+
const trackByMap = ${JSON.stringify(trackByMap)};
|
|
355
|
+
|
|
356
|
+
export async function init(slowViewState, fastViewState, fastCarryForward, clientInitData) {
|
|
357
|
+
${initCalls}
|
|
358
|
+
const viewState = deepMergeViewStates(slowViewState, fastViewState, trackByMap);
|
|
359
|
+
const target = document.getElementById('target');
|
|
360
|
+
const rootElement = target.firstElementChild;
|
|
361
|
+
const parts = [
|
|
362
|
+
${partsArray.join(",\n ")}
|
|
363
|
+
].filter(p => p !== null);
|
|
364
|
+
const pageComp = hydrateCompositeJayComponent(
|
|
365
|
+
hydrate, viewState, fastCarryForward,
|
|
366
|
+
parts, trackByMap, rootElement
|
|
367
|
+
);
|
|
368
|
+
return pageComp({});
|
|
369
|
+
}
|
|
370
|
+
`;
|
|
371
|
+
const outputDir = path__default.dirname(outputPath);
|
|
372
|
+
await fs.mkdir(outputDir, { recursive: true });
|
|
373
|
+
await fs.writeFile(outputPath, code, "utf-8");
|
|
374
|
+
getLogger().info(`[Build] Generated route hydration entry: ${path__default.basename(outputPath)}`);
|
|
375
|
+
}
|
|
376
|
+
async function buildInstanceClient(hydrateEntryPath, instanceId, outputDir, projectRoot, jayOptions, minify = true, pagesRoot, buildDir) {
|
|
377
|
+
const logger = getLogger();
|
|
378
|
+
await fs.mkdir(outputDir, { recursive: true });
|
|
379
|
+
const fullJayOptions = {
|
|
380
|
+
...jayOptions,
|
|
381
|
+
...pagesRoot && buildDir ? { pagesRoot, buildFolder: buildDir } : {}
|
|
382
|
+
};
|
|
383
|
+
await build({
|
|
384
|
+
root: projectRoot,
|
|
385
|
+
publicDir: false,
|
|
386
|
+
plugins: [...jayStackCompiler(fullJayOptions)],
|
|
387
|
+
build: {
|
|
388
|
+
outDir: outputDir,
|
|
389
|
+
emptyOutDir: false,
|
|
390
|
+
minify,
|
|
391
|
+
manifest: `${instanceId}-manifest.json`,
|
|
392
|
+
rollupOptions: {
|
|
393
|
+
input: { [instanceId]: hydrateEntryPath },
|
|
394
|
+
external: (id) => id.startsWith("@jay-framework/") || id === "jay-route-hydrate",
|
|
395
|
+
output: {
|
|
396
|
+
entryFileNames: "[name]-[hash].js",
|
|
397
|
+
chunkFileNames: "chunks/[name]-[hash].js",
|
|
398
|
+
assetFileNames: "[name]-[hash].[ext]",
|
|
399
|
+
format: "es"
|
|
400
|
+
},
|
|
401
|
+
preserveEntrySignatures: "exports-only"
|
|
402
|
+
}
|
|
403
|
+
},
|
|
404
|
+
logLevel: "warn"
|
|
405
|
+
});
|
|
406
|
+
const manifestPath = path__default.join(outputDir, `${instanceId}-manifest.json`);
|
|
407
|
+
const manifest = JSON.parse(await fs.readFile(manifestPath, "utf-8"));
|
|
408
|
+
await fs.rm(manifestPath, { force: true });
|
|
409
|
+
const entryKey = Object.keys(manifest).find((k) => manifest[k].isEntry);
|
|
410
|
+
if (!entryKey) {
|
|
411
|
+
throw new Error(`No entry found in instance build manifest for ${instanceId}`);
|
|
412
|
+
}
|
|
413
|
+
const entry = manifest[entryKey];
|
|
414
|
+
const result = {
|
|
415
|
+
jsFile: entry.file,
|
|
416
|
+
cssFile: entry.css?.[0]
|
|
417
|
+
};
|
|
418
|
+
logger.info(`[Build] Client bundle: ${result.jsFile}`);
|
|
419
|
+
return result;
|
|
420
|
+
}
|
|
421
|
+
const require$1 = createRequire(import.meta.url);
|
|
422
|
+
async function loadProductionPageParts(route, pageModule, jayHtmlContent, projectRoot, tsConfigFilePath, serverBuildDir) {
|
|
423
|
+
const exportName = route.componentExport || "page";
|
|
424
|
+
const compDefinition = pageModule[exportName] ?? pageModule.default;
|
|
425
|
+
const parts = compDefinition ? [{ compDefinition, clientImport: "", clientPart: "" }] : [];
|
|
426
|
+
const dirName = path__default.dirname(route.jayHtmlPath);
|
|
427
|
+
const fileName = path__default.basename(route.jayHtmlPath);
|
|
428
|
+
const jayHtmlWithValidations = await parseJayFile(
|
|
429
|
+
jayHtmlContent,
|
|
430
|
+
fileName,
|
|
431
|
+
dirName,
|
|
432
|
+
{ relativePath: tsConfigFilePath },
|
|
433
|
+
JAY_IMPORT_RESOLVER,
|
|
434
|
+
projectRoot
|
|
435
|
+
);
|
|
436
|
+
const jayHtml = checkValidationErrors(jayHtmlWithValidations);
|
|
437
|
+
const headlessInstanceComponents = [];
|
|
438
|
+
const keyedPartModules = [];
|
|
439
|
+
const headlessModuleInfos = [];
|
|
440
|
+
const headlessImports = jayHtml.headlessImports ?? [];
|
|
441
|
+
getLogger().info(
|
|
442
|
+
`[Build] headlessImports for ${fileName}: ${headlessImports.length}, keys: ${Object.keys(jayHtml).join(",")}`
|
|
443
|
+
);
|
|
444
|
+
for (const headlessImport of headlessImports) {
|
|
445
|
+
const module = headlessImport.codeLink.module;
|
|
446
|
+
const name = headlessImport.codeLink.names[0].name;
|
|
447
|
+
const isLocalModule = module[0] === "." || module[0] === "/";
|
|
448
|
+
let modulePath;
|
|
449
|
+
if (isLocalModule) {
|
|
450
|
+
const sourcePath = path__default.resolve(dirName, module);
|
|
451
|
+
if (serverBuildDir) {
|
|
452
|
+
const relativeToSrc = path__default.relative(path__default.join(projectRoot, "src"), sourcePath);
|
|
453
|
+
let compiledPath = path__default.join(serverBuildDir, relativeToSrc);
|
|
454
|
+
compiledPath = compiledPath.replace(/\.ts$/, ".js");
|
|
455
|
+
if (!compiledPath.endsWith(".js")) {
|
|
456
|
+
const indexPath = path__default.join(compiledPath, "index.js");
|
|
457
|
+
try {
|
|
458
|
+
await fs.access(indexPath);
|
|
459
|
+
compiledPath = indexPath;
|
|
460
|
+
} catch {
|
|
461
|
+
compiledPath += ".js";
|
|
462
|
+
}
|
|
269
463
|
}
|
|
270
464
|
modulePath = compiledPath;
|
|
271
465
|
} else {
|
|
@@ -411,664 +605,622 @@ function buildPagePartsConfig(pageParts, pageServerModule, pageExportName, build
|
|
|
411
605
|
}))
|
|
412
606
|
};
|
|
413
607
|
}
|
|
414
|
-
function
|
|
415
|
-
const sorted = Object.keys(params).sort().reduce(
|
|
416
|
-
(acc, key) => {
|
|
417
|
-
acc[key] = params[key];
|
|
418
|
-
return acc;
|
|
419
|
-
},
|
|
420
|
-
{}
|
|
421
|
-
);
|
|
422
|
-
const json = JSON.stringify(sorted);
|
|
423
|
-
if (json === "{}" && !suffix) return "";
|
|
424
|
-
const input = suffix ? json + ":" + suffix : json;
|
|
425
|
-
return "_" + crypto.createHash("md5").update(input).digest("hex").substring(0, 8);
|
|
426
|
-
}
|
|
427
|
-
async function buildInstance(route, params, pageModule, ctx, routeServerElementPath, routeCssPath, routeHydratePath, routeClientBundlePath) {
|
|
608
|
+
async function buildErrorPages(errorPages, ctx, projectRoot, pagesRoot, tsConfigFilePath, minify, sharedManifest, buildDir) {
|
|
428
609
|
const logger = getLogger();
|
|
429
|
-
const
|
|
430
|
-
const
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
await fs.mkdir(backendInstanceDir, { recursive: true });
|
|
435
|
-
await fs.mkdir(frontendInstanceDir, { recursive: true });
|
|
436
|
-
const jayHtmlContent = await fs.readFile(route.jayHtmlPath, "utf-8");
|
|
437
|
-
path__default.dirname(route.jayHtmlPath);
|
|
438
|
-
const serverBuildDir = path__default.join(ctx.backendDir, "server");
|
|
439
|
-
const pageParts = await loadProductionPageParts(
|
|
440
|
-
route,
|
|
441
|
-
pageModule,
|
|
442
|
-
jayHtmlContent,
|
|
443
|
-
ctx.projectRoot,
|
|
444
|
-
ctx.tsConfigFilePath,
|
|
445
|
-
serverBuildDir
|
|
446
|
-
);
|
|
447
|
-
const contracts = [
|
|
448
|
-
.../* @__PURE__ */ new Set([
|
|
449
|
-
...pageParts.headlessInstanceComponents.map((c) => c.contractName),
|
|
450
|
-
...pageParts.parts.filter((p) => p.contractInfo?.contractName).map((p) => p.contractInfo.contractName)
|
|
451
|
-
])
|
|
452
|
-
];
|
|
453
|
-
const pagePartsConfigPath = path__default.join(backendInstanceDir, "page-parts.json");
|
|
454
|
-
const exportName = route.componentExport || "page";
|
|
455
|
-
let pageServerModule = "";
|
|
456
|
-
let pageIsPlugin = false;
|
|
457
|
-
if (route.compPath) {
|
|
458
|
-
if (route.componentExport) {
|
|
459
|
-
pageServerModule = route.packageName || route.compPath;
|
|
460
|
-
pageIsPlugin = true;
|
|
461
|
-
} else {
|
|
462
|
-
const relativePath = path__default.relative(ctx.projectRoot, route.compPath);
|
|
463
|
-
pageServerModule = relativePath.replace(/^src\//, "server/").replace(/\.ts$/, ".js").replace(/\[/g, "_").replace(/\]/g, "_");
|
|
610
|
+
const result = {};
|
|
611
|
+
for (const errorPage of errorPages) {
|
|
612
|
+
if (!errorPage.jayHtmlPath) {
|
|
613
|
+
logger.warn(`[Build] Error page ${errorPage.token} has no page.jay-html — skipped`);
|
|
614
|
+
continue;
|
|
464
615
|
}
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
616
|
+
const outputDirName = path__default.join("_errors", errorPage.token);
|
|
617
|
+
const backendPageDir = path__default.join(ctx.backendDir, "pre-rendered", outputDirName);
|
|
618
|
+
const frontendPageDir = path__default.join(ctx.frontendDir, "pages", outputDirName);
|
|
619
|
+
await fs.mkdir(backendPageDir, { recursive: true });
|
|
620
|
+
await fs.mkdir(frontendPageDir, { recursive: true });
|
|
621
|
+
const entry = {
|
|
622
|
+
token: errorPage.token,
|
|
623
|
+
serverElementPath: ""
|
|
624
|
+
};
|
|
625
|
+
const serverElementPath = path__default.join(backendPageDir, "route.server-element.js");
|
|
626
|
+
try {
|
|
627
|
+
const seResult = await compileRouteServerElement(
|
|
628
|
+
errorPage.jayHtmlPath,
|
|
629
|
+
serverElementPath,
|
|
630
|
+
projectRoot,
|
|
631
|
+
tsConfigFilePath,
|
|
632
|
+
minify
|
|
633
|
+
);
|
|
634
|
+
entry.serverElementPath = path__default.relative(ctx.backendDir, serverElementPath);
|
|
635
|
+
if (seResult.cssFile) {
|
|
636
|
+
const srcCss = path__default.join(backendPageDir, seResult.cssFile);
|
|
637
|
+
const dstCss = path__default.join(frontendPageDir, seResult.cssFile);
|
|
638
|
+
try {
|
|
639
|
+
await fs.rename(srcCss, dstCss);
|
|
640
|
+
} catch {
|
|
641
|
+
await fs.copyFile(srcCss, dstCss);
|
|
642
|
+
await fs.rm(srcCss, { force: true });
|
|
643
|
+
}
|
|
644
|
+
entry.routeCssPath = path__default.relative(ctx.frontendDir, dstCss);
|
|
645
|
+
}
|
|
646
|
+
if (seResult.cssImports?.length) entry.cssImports = seResult.cssImports;
|
|
647
|
+
if (seResult.headMeta) entry.headMeta = seResult.headMeta;
|
|
648
|
+
if (seResult.scripts?.length) entry.scripts = seResult.scripts;
|
|
649
|
+
logger.important(`[Build] Error page server element: ${errorPage.token}`);
|
|
650
|
+
} catch (err) {
|
|
651
|
+
logger.error(
|
|
652
|
+
`[Build] Error page server element FAILED ${errorPage.token}: ${err.message}`
|
|
653
|
+
);
|
|
654
|
+
continue;
|
|
490
655
|
}
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
656
|
+
try {
|
|
657
|
+
const hydrateResult = await compileRouteHydrateScript(
|
|
658
|
+
errorPage.jayHtmlPath,
|
|
659
|
+
frontendPageDir,
|
|
660
|
+
projectRoot,
|
|
661
|
+
tsConfigFilePath,
|
|
662
|
+
minify
|
|
663
|
+
);
|
|
664
|
+
entry.routeHydratePath = path__default.relative(
|
|
665
|
+
ctx.frontendDir,
|
|
666
|
+
path__default.join(frontendPageDir, hydrateResult.jsFile)
|
|
667
|
+
);
|
|
668
|
+
try {
|
|
669
|
+
const hydrateCode = await fs.readFile(
|
|
670
|
+
path__default.join(frontendPageDir, hydrateResult.jsFile),
|
|
671
|
+
"utf-8"
|
|
672
|
+
);
|
|
673
|
+
const importMatches = hydrateCode.match(/from\s*"([^"]+)"/g) || [];
|
|
674
|
+
const deps = importMatches.map((m) => m.replace(/^from\s*"/, "").replace(/"$/, "")).filter((dep) => sharedManifest[dep]);
|
|
675
|
+
if (deps.length > 0) entry.sharedDeps = deps;
|
|
676
|
+
} catch {
|
|
505
677
|
}
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
discovered: [
|
|
514
|
-
...existingInstances.discovered,
|
|
515
|
-
...slowResult2.instancePhaseData.discovered
|
|
516
|
-
],
|
|
517
|
-
carryForwards: {
|
|
518
|
-
...existingInstances.carryForwards,
|
|
519
|
-
...slowResult2.instancePhaseData.carryForwards
|
|
520
|
-
},
|
|
521
|
-
slowViewStates: {
|
|
522
|
-
...existingInstances.slowViewStates || {},
|
|
523
|
-
...slowResult2.instancePhaseData.slowViewStates
|
|
524
|
-
}
|
|
525
|
-
};
|
|
678
|
+
logger.important(`[Build] Error page hydrate script: ${errorPage.token}`);
|
|
679
|
+
} catch (err) {
|
|
680
|
+
logger.error(
|
|
681
|
+
`[Build] Error page hydrate script FAILED ${errorPage.token}: ${err.message}`
|
|
682
|
+
);
|
|
683
|
+
result[errorPage.token] = entry;
|
|
684
|
+
continue;
|
|
526
685
|
}
|
|
686
|
+
try {
|
|
687
|
+
const ROUTE_HYDRATE_KEY = "jay-route-hydrate";
|
|
688
|
+
let pageModulePath = "";
|
|
689
|
+
if (errorPage.compPath) {
|
|
690
|
+
pageModulePath = "./" + path__default.relative(frontendPageDir, errorPage.compPath);
|
|
691
|
+
}
|
|
692
|
+
const pageParts = await loadProductionPageParts(
|
|
693
|
+
{ jayHtmlPath: errorPage.jayHtmlPath },
|
|
694
|
+
{},
|
|
695
|
+
await fs.readFile(errorPage.jayHtmlPath, "utf-8"),
|
|
696
|
+
projectRoot,
|
|
697
|
+
tsConfigFilePath,
|
|
698
|
+
path__default.join(ctx.backendDir, "server")
|
|
699
|
+
);
|
|
700
|
+
entry.trackByMap = pageParts.serverTrackByMap || pageParts.clientTrackByMap;
|
|
701
|
+
const entryPath = path__default.join(frontendPageDir, "route.entry.ts");
|
|
702
|
+
await generateRouteHydrationEntry({
|
|
703
|
+
hydrateImport: ROUTE_HYDRATE_KEY,
|
|
704
|
+
pageModulePath,
|
|
705
|
+
pageExportName: "page",
|
|
706
|
+
trackByMap: pageParts.clientTrackByMap || {},
|
|
707
|
+
outputPath: entryPath,
|
|
708
|
+
keyedParts: pageParts.keyedPartModules,
|
|
709
|
+
clientInits: ctx.clientInits
|
|
710
|
+
});
|
|
711
|
+
const clientResult = await buildInstanceClient(
|
|
712
|
+
entryPath,
|
|
713
|
+
"route.client",
|
|
714
|
+
frontendPageDir,
|
|
715
|
+
projectRoot,
|
|
716
|
+
{ tsConfigFilePath },
|
|
717
|
+
minify,
|
|
718
|
+
pagesRoot,
|
|
719
|
+
buildDir
|
|
720
|
+
);
|
|
721
|
+
await fs.rm(entryPath, { force: true });
|
|
722
|
+
entry.routeClientBundlePath = path__default.relative(
|
|
723
|
+
ctx.frontendDir,
|
|
724
|
+
path__default.join(frontendPageDir, clientResult.jsFile)
|
|
725
|
+
);
|
|
726
|
+
try {
|
|
727
|
+
const clientCode = await fs.readFile(
|
|
728
|
+
path__default.join(frontendPageDir, clientResult.jsFile),
|
|
729
|
+
"utf-8"
|
|
730
|
+
);
|
|
731
|
+
const clientImports = (clientCode.match(/from\s*"([^"]+)"/g) || []).map((m) => m.replace(/^from\s*"/, "").replace(/"$/, "")).filter((dep) => sharedManifest[dep]);
|
|
732
|
+
const existing = new Set(entry.sharedDeps || []);
|
|
733
|
+
for (const dep of clientImports) existing.add(dep);
|
|
734
|
+
if (existing.size > 0) entry.sharedDeps = [...existing];
|
|
735
|
+
} catch {
|
|
736
|
+
}
|
|
737
|
+
logger.important(`[Build] Error page client bundle: ${errorPage.token}`);
|
|
738
|
+
} catch (err) {
|
|
739
|
+
logger.error(
|
|
740
|
+
`[Build] Error page client bundle FAILED ${errorPage.token}: ${err.message}`
|
|
741
|
+
);
|
|
742
|
+
}
|
|
743
|
+
result[errorPage.token] = entry;
|
|
527
744
|
}
|
|
528
|
-
|
|
529
|
-
const existingInstances = carryForward.__instances || {
|
|
530
|
-
discovered: [],
|
|
531
|
-
carryForwards: {}
|
|
532
|
-
};
|
|
533
|
-
existingInstances.forEachInstances = pageParts.forEachInstances;
|
|
534
|
-
carryForward.__instances = existingInstances;
|
|
535
|
-
}
|
|
536
|
-
const cachePath = path__default.join(backendInstanceDir, `${instanceId}.cache.json`);
|
|
537
|
-
await fs.writeFile(
|
|
538
|
-
cachePath,
|
|
539
|
-
JSON.stringify({
|
|
540
|
-
slowViewState,
|
|
541
|
-
carryForward
|
|
542
|
-
}),
|
|
543
|
-
"utf-8"
|
|
544
|
-
);
|
|
545
|
-
logger.info(`[Build] Instance data: ${routeDir}/${instanceId}`);
|
|
546
|
-
const serverElementPath = routeServerElementPath ? path__default.join(ctx.backendDir, routeServerElementPath) : path__default.join(backendInstanceDir, `${instanceId}.server-element.js`);
|
|
547
|
-
const instanceEntry = {
|
|
548
|
-
params,
|
|
549
|
-
cachePath: path__default.relative(ctx.backendDir, cachePath),
|
|
550
|
-
serverElementPath: path__default.relative(ctx.backendDir, serverElementPath),
|
|
551
|
-
clientBundlePath: routeClientBundlePath || "",
|
|
552
|
-
clientCssPath: routeCssPath
|
|
553
|
-
};
|
|
554
|
-
return { status: "success", instanceEntry, slowViewState, carryForward, contracts };
|
|
745
|
+
return result;
|
|
555
746
|
}
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
747
|
+
createRequire(import.meta.url);
|
|
748
|
+
const FRAMEWORK_PACKAGES = [
|
|
749
|
+
"@jay-framework/stack-client-runtime",
|
|
750
|
+
"@jay-framework/component",
|
|
751
|
+
"@jay-framework/reactive",
|
|
752
|
+
"@jay-framework/runtime",
|
|
753
|
+
"@jay-framework/view-state-merge",
|
|
754
|
+
"@jay-framework/fullstack-component"
|
|
755
|
+
];
|
|
756
|
+
async function buildSharedChunks(outputDir, _projectRoot, minify = true, pluginClientPackages = []) {
|
|
757
|
+
const logger = getLogger();
|
|
758
|
+
logger.info("[Build] Building shared client chunks...");
|
|
759
|
+
await fs.mkdir(outputDir, { recursive: true });
|
|
760
|
+
const allPackages = [...FRAMEWORK_PACKAGES, ...pluginClientPackages];
|
|
761
|
+
const entries = {};
|
|
762
|
+
for (const pkg of allPackages) {
|
|
763
|
+
const varName = pkgToVarName(pkg);
|
|
764
|
+
const entryPath = path__default.join(outputDir, `_shared_${varName}.js`);
|
|
765
|
+
await fs.writeFile(entryPath, `export * from '${pkg}';
|
|
766
|
+
`);
|
|
767
|
+
entries[varName] = entryPath;
|
|
768
|
+
}
|
|
769
|
+
const dedupePackages = [...allPackages, "@jay-framework/list-compare"];
|
|
770
|
+
await build({
|
|
771
|
+
publicDir: false,
|
|
772
|
+
build: {
|
|
773
|
+
outDir: outputDir,
|
|
774
|
+
emptyOutDir: true,
|
|
775
|
+
minify,
|
|
776
|
+
manifest: "vite-manifest.json",
|
|
777
|
+
rollupOptions: {
|
|
778
|
+
input: entries,
|
|
779
|
+
output: {
|
|
780
|
+
entryFileNames: "[name]-[hash].js",
|
|
781
|
+
chunkFileNames: "[name]-[hash].js",
|
|
782
|
+
format: "es"
|
|
783
|
+
},
|
|
784
|
+
preserveEntrySignatures: "exports-only"
|
|
785
|
+
}
|
|
786
|
+
},
|
|
787
|
+
resolve: {
|
|
788
|
+
dedupe: dedupePackages
|
|
789
|
+
},
|
|
790
|
+
logLevel: "warn"
|
|
569
791
|
});
|
|
792
|
+
for (const pkg of allPackages) {
|
|
793
|
+
const varName = pkgToVarName(pkg);
|
|
794
|
+
await fs.rm(path__default.join(outputDir, `_shared_${varName}.js`), { force: true });
|
|
795
|
+
}
|
|
796
|
+
const manifest = await parseViteManifest(outputDir, allPackages);
|
|
797
|
+
logger.info(`[Build] Shared chunks built: ${Object.keys(manifest).length} entries`);
|
|
798
|
+
return { manifest, outputDir };
|
|
570
799
|
}
|
|
571
|
-
function
|
|
572
|
-
return
|
|
573
|
-
pattern: route.rawRoute,
|
|
574
|
-
segments: convertSegments(route.segments),
|
|
575
|
-
serverModule: serverModulePath,
|
|
576
|
-
componentExport: route.componentExport,
|
|
577
|
-
instances: [],
|
|
578
|
-
...route.devOnly && { devOnly: true }
|
|
579
|
-
};
|
|
800
|
+
function pkgToVarName(pkg) {
|
|
801
|
+
return pkg.replace("@jay-framework/", "").replace(/[/-]/g, "_");
|
|
580
802
|
}
|
|
581
|
-
async function
|
|
582
|
-
const
|
|
583
|
-
const
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
});
|
|
803
|
+
async function parseViteManifest(outputDir, packages) {
|
|
804
|
+
const viteManifestPath = path__default.join(outputDir, "vite-manifest.json");
|
|
805
|
+
const raw = JSON.parse(await fs.readFile(viteManifestPath, "utf-8"));
|
|
806
|
+
const varNameToPackage = /* @__PURE__ */ new Map();
|
|
807
|
+
for (const pkg of packages) {
|
|
808
|
+
varNameToPackage.set(pkgToVarName(pkg), pkg);
|
|
809
|
+
}
|
|
810
|
+
const manifest = {};
|
|
811
|
+
for (const [, entry] of Object.entries(raw)) {
|
|
812
|
+
if (!entry.isEntry) continue;
|
|
813
|
+
const outputBase = path__default.basename(entry.file, ".js");
|
|
814
|
+
for (const [varName, pkg] of varNameToPackage) {
|
|
815
|
+
if (outputBase.startsWith(varName)) {
|
|
816
|
+
manifest[pkg] = entry.file;
|
|
817
|
+
break;
|
|
597
818
|
}
|
|
598
|
-
} catch {
|
|
599
|
-
getLogger().warn(`[Build] Could not extract actions from ${sourcePath}`);
|
|
600
819
|
}
|
|
601
820
|
}
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
821
|
+
const sharedManifestPath = path__default.join(outputDir, "shared-manifest.json");
|
|
822
|
+
await fs.writeFile(sharedManifestPath, JSON.stringify(manifest, null, 2));
|
|
823
|
+
await fs.rm(viteManifestPath, { force: true });
|
|
824
|
+
return manifest;
|
|
825
|
+
}
|
|
826
|
+
function crossProductParams(parts) {
|
|
827
|
+
if (parts.length === 0) return [];
|
|
828
|
+
if (parts.length === 1) return parts[0].values;
|
|
829
|
+
const logger = getLogger();
|
|
830
|
+
for (let i = 0; i < parts.length; i++) {
|
|
831
|
+
for (let j = i + 1; j < parts.length; j++) {
|
|
832
|
+
for (const key of parts[i].keys) {
|
|
833
|
+
if (parts[j].keys.has(key)) {
|
|
834
|
+
logger.warn(
|
|
835
|
+
`[Build] Multiple loadParams provide key "${key}" — using first provider`
|
|
836
|
+
);
|
|
618
837
|
}
|
|
619
|
-
getLogger().info(
|
|
620
|
-
`[Build] Plugin actions from ${packageName}: ${productionActions.length}` + (devOnlyCount > 0 ? ` (excluded ${devOnlyCount} devOnly)` : "")
|
|
621
|
-
);
|
|
622
838
|
}
|
|
623
839
|
}
|
|
624
|
-
} catch (err) {
|
|
625
|
-
getLogger().warn(`[Build] Plugin action scan failed: ${err.message}`);
|
|
626
840
|
}
|
|
627
|
-
|
|
841
|
+
let result = parts[0].values;
|
|
842
|
+
for (let i = 1; i < parts.length; i++) {
|
|
843
|
+
const next = parts[i].values;
|
|
844
|
+
const combined = [];
|
|
845
|
+
for (const a of result) {
|
|
846
|
+
for (const b of next) {
|
|
847
|
+
combined.push({ ...a, ...b });
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
result = combined;
|
|
851
|
+
}
|
|
852
|
+
return result;
|
|
628
853
|
}
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
);
|
|
854
|
+
function paramsMatchInferred(params, inferredParams, optionalSegments) {
|
|
855
|
+
return Object.entries(inferredParams).every(([k, v]) => {
|
|
856
|
+
if (optionalSegments?.has(k)) return true;
|
|
857
|
+
return params[k] === v;
|
|
858
|
+
});
|
|
635
859
|
}
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
const
|
|
639
|
-
const
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
860
|
+
function computeSpecificity(route) {
|
|
861
|
+
const dynamicCount = (route.rawRoute.match(/\[/g) || []).length;
|
|
862
|
+
const inferredCount = route.inferredParams ? Object.keys(route.inferredParams).length : 0;
|
|
863
|
+
const unresolvedCount = Math.max(0, dynamicCount - inferredCount);
|
|
864
|
+
return 0 - unresolvedCount;
|
|
865
|
+
}
|
|
866
|
+
function buildUrl(route, params) {
|
|
867
|
+
return route.rawRoute.replace(/\[\[(\w+)\]\]/g, (_, name) => {
|
|
868
|
+
const value = params[name];
|
|
869
|
+
if (!value) return "";
|
|
870
|
+
if (route.inferredParams?.[name] === value) return "";
|
|
871
|
+
return value;
|
|
872
|
+
}).replace(/\[(\w+)\]/g, (_, name) => params[name] || "").replace(/\/\/+/g, "/").replace(/\/$/, "") || "/";
|
|
873
|
+
}
|
|
874
|
+
function materializeRouteParams(routes, loadParamsResults) {
|
|
875
|
+
const entries = [];
|
|
876
|
+
for (const route of routes) {
|
|
877
|
+
const specificity = computeSpecificity(route);
|
|
878
|
+
if (!route.hasDynamicParams) {
|
|
879
|
+
const params = route.inferredParams || {};
|
|
880
|
+
entries.push({ route, params, url: route.rawRoute, specificity });
|
|
881
|
+
continue;
|
|
882
|
+
}
|
|
883
|
+
const allParams = loadParamsResults.get(route) || [];
|
|
884
|
+
for (const params of allParams) {
|
|
885
|
+
if (route.inferredParams && !paramsMatchInferred(params, route.inferredParams, route.optionalSegments)) {
|
|
650
886
|
continue;
|
|
651
887
|
}
|
|
652
|
-
|
|
653
|
-
if (
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
888
|
+
let mergedParams = params;
|
|
889
|
+
if (route.inferredParams) {
|
|
890
|
+
mergedParams = { ...params };
|
|
891
|
+
for (const [k, v] of Object.entries(route.inferredParams)) {
|
|
892
|
+
if (!(k in mergedParams)) {
|
|
893
|
+
mergedParams[k] = v;
|
|
894
|
+
}
|
|
895
|
+
}
|
|
658
896
|
}
|
|
659
|
-
const
|
|
660
|
-
|
|
661
|
-
pluginRoutes.push({
|
|
662
|
-
segments: parseRouteSegments(route.path),
|
|
663
|
-
rawRoute: route.path,
|
|
664
|
-
jayHtmlPath,
|
|
665
|
-
compPath,
|
|
666
|
-
componentExport,
|
|
667
|
-
packageName: plugin.packageName,
|
|
668
|
-
...route.devOnly === true && { devOnly: true }
|
|
669
|
-
});
|
|
670
|
-
logger.info(
|
|
671
|
-
`[Routes] Plugin "${plugin.manifest.name}" provides route ${route.path}${route.devOnly ? " (dev-only)" : ""}`
|
|
672
|
-
);
|
|
897
|
+
const url = buildUrl(route, mergedParams);
|
|
898
|
+
entries.push({ route, params: mergedParams, url, specificity });
|
|
673
899
|
}
|
|
674
900
|
}
|
|
675
|
-
return
|
|
901
|
+
return entries;
|
|
676
902
|
}
|
|
677
|
-
function
|
|
678
|
-
const
|
|
679
|
-
const
|
|
680
|
-
|
|
681
|
-
const
|
|
682
|
-
if (
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
const resolved = typeof exportValue === "string" ? exportValue : exportValue.default || exportValue.import || exportValue.require;
|
|
687
|
-
if (resolved) return path__default.join(pluginPath, resolved);
|
|
688
|
-
}
|
|
689
|
-
}
|
|
690
|
-
} catch {
|
|
691
|
-
}
|
|
692
|
-
for (const dir of ["dist", "lib", ""]) {
|
|
693
|
-
const candidate = path__default.join(pluginPath, dir, normalized);
|
|
694
|
-
try {
|
|
695
|
-
fs__default.accessSync(candidate);
|
|
696
|
-
return candidate;
|
|
697
|
-
} catch {
|
|
903
|
+
function dedupeByUrl(entries) {
|
|
904
|
+
const logger = getLogger();
|
|
905
|
+
const byUrl = /* @__PURE__ */ new Map();
|
|
906
|
+
for (const entry of entries) {
|
|
907
|
+
const existing = byUrl.get(entry.url);
|
|
908
|
+
if (!existing) {
|
|
909
|
+
byUrl.set(entry.url, entry);
|
|
910
|
+
} else if (entry.specificity > existing.specificity) {
|
|
911
|
+
byUrl.set(entry.url, entry);
|
|
698
912
|
}
|
|
699
913
|
}
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
const pkg = JSON.parse(fs__default.readFileSync(pkgJsonPath, "utf-8"));
|
|
706
|
-
const mainExport = pkg.exports?.["."];
|
|
707
|
-
const mainPath = typeof mainExport === "string" ? mainExport : mainExport?.default || mainExport?.import || pkg.main;
|
|
708
|
-
if (mainPath) {
|
|
709
|
-
const resolved = path__default.join(pluginPath, mainPath);
|
|
710
|
-
if (fs__default.existsSync(resolved)) return resolved;
|
|
711
|
-
}
|
|
712
|
-
} catch {
|
|
914
|
+
const deduped = [...byUrl.values()];
|
|
915
|
+
if (deduped.length < entries.length) {
|
|
916
|
+
logger.info(
|
|
917
|
+
`[Build] Deduplication: ${entries.length} materialized → ${deduped.length} unique URLs`
|
|
918
|
+
);
|
|
713
919
|
}
|
|
714
|
-
return
|
|
920
|
+
return deduped;
|
|
715
921
|
}
|
|
716
|
-
|
|
717
|
-
const
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
{ relativePath: tsConfigFilePath },
|
|
722
|
-
JAY_IMPORT_RESOLVER,
|
|
723
|
-
projectRoot,
|
|
724
|
-
sourceDir
|
|
725
|
-
);
|
|
726
|
-
const parsedJayFile = checkValidationErrors(jayFile);
|
|
727
|
-
const serverElementCode = checkValidationErrors(generateServerElementFile(parsedJayFile));
|
|
728
|
-
const outputDir = path__default.dirname(outputPath);
|
|
729
|
-
await fs.mkdir(outputDir, { recursive: true });
|
|
730
|
-
const tsPath = outputPath.replace(/\.js$/, ".ts");
|
|
731
|
-
await fs.writeFile(tsPath, serverElementCode, "utf-8");
|
|
732
|
-
const jayOptions = { tsConfigFilePath };
|
|
733
|
-
await build({
|
|
734
|
-
root: projectRoot,
|
|
735
|
-
publicDir: false,
|
|
736
|
-
plugins: [jayRuntime(jayOptions)],
|
|
737
|
-
build: {
|
|
738
|
-
outDir: outputDir,
|
|
739
|
-
emptyOutDir: false,
|
|
740
|
-
minify: false,
|
|
741
|
-
ssr: true,
|
|
742
|
-
rollupOptions: {
|
|
743
|
-
input: { [path__default.basename(outputPath, ".js")]: tsPath },
|
|
744
|
-
external: [/^node:/, /^@jay-framework\//],
|
|
745
|
-
output: {
|
|
746
|
-
entryFileNames: "[name].js",
|
|
747
|
-
format: "es"
|
|
748
|
-
}
|
|
749
|
-
}
|
|
922
|
+
function hashParams(params, suffix) {
|
|
923
|
+
const sorted = Object.keys(params).sort().reduce(
|
|
924
|
+
(acc, key) => {
|
|
925
|
+
acc[key] = params[key];
|
|
926
|
+
return acc;
|
|
750
927
|
},
|
|
751
|
-
|
|
752
|
-
});
|
|
753
|
-
await fs.rm(tsPath, { force: true });
|
|
754
|
-
let cssFile;
|
|
755
|
-
let cssImports;
|
|
756
|
-
const css = parsedJayFile.css;
|
|
757
|
-
if (css) {
|
|
758
|
-
cssImports = extractCssImportUrls(css);
|
|
759
|
-
const cssContent = minifyCss ? (await transform(css, { loader: "css", minify: true })).code : css;
|
|
760
|
-
const hash = createHash("sha256").update(cssContent).digest("hex").slice(0, 8);
|
|
761
|
-
const baseName = path__default.basename(outputPath, ".server-element.js");
|
|
762
|
-
const cssFilename = `${baseName}-${hash}.css`;
|
|
763
|
-
await fs.writeFile(path__default.join(outputDir, cssFilename), cssContent, "utf-8");
|
|
764
|
-
cssFile = cssFilename;
|
|
765
|
-
}
|
|
766
|
-
getLogger().info(`[Build] Compiled server element: ${path__default.basename(outputPath)}`);
|
|
767
|
-
return { cssFile, cssImports, headMeta: parsedJayFile.headMeta };
|
|
768
|
-
}
|
|
769
|
-
async function compileRouteServerElement(jayHtmlPath, outputPath, projectRoot, tsConfigFilePath, minifyCss = true) {
|
|
770
|
-
const jayHtmlContent = await fs.readFile(jayHtmlPath, "utf-8");
|
|
771
|
-
const sourceDir = path__default.dirname(jayHtmlPath);
|
|
772
|
-
const outputDir = path__default.dirname(outputPath);
|
|
773
|
-
const jayHtml = resolveJayHtmlPaths(jayHtmlContent, sourceDir, outputDir);
|
|
774
|
-
return compileServerElement(
|
|
775
|
-
jayHtml,
|
|
776
|
-
path__default.basename(jayHtmlPath),
|
|
777
|
-
outputDir,
|
|
778
|
-
outputPath,
|
|
779
|
-
projectRoot,
|
|
780
|
-
tsConfigFilePath,
|
|
781
|
-
sourceDir,
|
|
782
|
-
minifyCss
|
|
928
|
+
{}
|
|
783
929
|
);
|
|
930
|
+
const json = JSON.stringify(sorted);
|
|
931
|
+
if (json === "{}" && !suffix) return "";
|
|
932
|
+
const input = suffix ? json + ":" + suffix : json;
|
|
933
|
+
return "_" + crypto.createHash("md5").update(input).digest("hex").substring(0, 8);
|
|
784
934
|
}
|
|
785
|
-
async function
|
|
786
|
-
const
|
|
787
|
-
const
|
|
788
|
-
const
|
|
789
|
-
const
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
935
|
+
async function buildInstance(route, params, pageModule, ctx, routeServerElementPath, routeCssPath, routeHydratePath, routeClientBundlePath) {
|
|
936
|
+
const logger = getLogger();
|
|
937
|
+
const routeDir = route.rawRoute.replace(/^\//, "") || "index";
|
|
938
|
+
const paramHash = hashParams(params, ctx.rebuildSuffix);
|
|
939
|
+
const instanceId = `page${paramHash}`;
|
|
940
|
+
const backendInstanceDir = path__default.join(ctx.backendDir, "pre-rendered", routeDir);
|
|
941
|
+
const frontendInstanceDir = path__default.join(ctx.frontendDir, "pages", routeDir);
|
|
942
|
+
await fs.mkdir(backendInstanceDir, { recursive: true });
|
|
943
|
+
await fs.mkdir(frontendInstanceDir, { recursive: true });
|
|
944
|
+
const jayHtmlContent = await fs.readFile(route.jayHtmlPath, "utf-8");
|
|
945
|
+
path__default.dirname(route.jayHtmlPath);
|
|
946
|
+
const serverBuildDir = path__default.join(ctx.backendDir, "server");
|
|
947
|
+
const pageParts = await loadProductionPageParts(
|
|
948
|
+
route,
|
|
949
|
+
pageModule,
|
|
950
|
+
jayHtmlContent,
|
|
951
|
+
ctx.projectRoot,
|
|
952
|
+
ctx.tsConfigFilePath,
|
|
953
|
+
serverBuildDir
|
|
801
954
|
);
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
entryFileNames: "[name]-[hash].js",
|
|
820
|
-
format: "es"
|
|
821
|
-
},
|
|
822
|
-
preserveEntrySignatures: "exports-only"
|
|
823
|
-
}
|
|
824
|
-
},
|
|
825
|
-
logLevel: "warn"
|
|
826
|
-
});
|
|
827
|
-
await fs.rm(tsPath, { force: true });
|
|
828
|
-
const manifestPath = path__default.join(outputDir, "route-hydrate-manifest.json");
|
|
829
|
-
const manifest = JSON.parse(await fs.readFile(manifestPath, "utf-8"));
|
|
830
|
-
await fs.rm(manifestPath, { force: true });
|
|
831
|
-
const entryKey = Object.keys(manifest).find((k) => manifest[k].isEntry);
|
|
832
|
-
if (!entryKey) throw new Error("No entry in route hydrate manifest");
|
|
833
|
-
const jsFile = manifest[entryKey].file;
|
|
834
|
-
getLogger().info(`[Build] Compiled route hydrate script: ${jsFile}`);
|
|
835
|
-
return { jsFile };
|
|
836
|
-
}
|
|
837
|
-
function resolveJayHtmlPaths(html, sourceDir, targetDir) {
|
|
838
|
-
const root = parse(html, {
|
|
839
|
-
comment: true,
|
|
840
|
-
blockTextElements: { script: true, style: true }
|
|
841
|
-
});
|
|
842
|
-
const rewrite = (el, attr) => {
|
|
843
|
-
const val = el.getAttribute(attr);
|
|
844
|
-
if (val && (val.startsWith("./") || val.startsWith("../"))) {
|
|
845
|
-
const abs = path__default.resolve(sourceDir, val);
|
|
846
|
-
let rel = path__default.relative(targetDir, abs);
|
|
847
|
-
if (!rel.startsWith(".")) rel = "./" + rel;
|
|
848
|
-
el.setAttribute(attr, rel);
|
|
955
|
+
const contracts = [
|
|
956
|
+
.../* @__PURE__ */ new Set([
|
|
957
|
+
...pageParts.headlessInstanceComponents.map((c) => c.contractName),
|
|
958
|
+
...pageParts.parts.filter((p) => p.contractInfo?.contractName).map((p) => p.contractInfo.contractName)
|
|
959
|
+
])
|
|
960
|
+
];
|
|
961
|
+
const pagePartsConfigPath = path__default.join(backendInstanceDir, "page-parts.json");
|
|
962
|
+
const exportName = route.componentExport || "page";
|
|
963
|
+
let pageServerModule = "";
|
|
964
|
+
let pageIsPlugin = false;
|
|
965
|
+
if (route.compPath) {
|
|
966
|
+
if (route.componentExport) {
|
|
967
|
+
pageServerModule = route.packageName || route.compPath;
|
|
968
|
+
pageIsPlugin = true;
|
|
969
|
+
} else {
|
|
970
|
+
const relativePath = path__default.relative(ctx.projectRoot, route.compPath);
|
|
971
|
+
pageServerModule = relativePath.replace(/^src\//, "server/").replace(/\.ts$/, ".js").replace(/\[/g, "_").replace(/\]/g, "_");
|
|
849
972
|
}
|
|
850
|
-
};
|
|
851
|
-
for (const el of root.querySelectorAll('script[type="application/jay-data"]')) {
|
|
852
|
-
rewrite(el, "contract");
|
|
853
|
-
}
|
|
854
|
-
for (const el of root.querySelectorAll('script[type="application/jay-headless"]')) {
|
|
855
|
-
rewrite(el, "src");
|
|
856
|
-
rewrite(el, "contract");
|
|
857
973
|
}
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
}
|
|
867
|
-
|
|
868
|
-
const
|
|
869
|
-
const
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
974
|
+
const config = buildPagePartsConfig(
|
|
975
|
+
pageParts,
|
|
976
|
+
pageServerModule,
|
|
977
|
+
exportName,
|
|
978
|
+
ctx.backendDir,
|
|
979
|
+
pageIsPlugin
|
|
980
|
+
);
|
|
981
|
+
await fs.writeFile(pagePartsConfigPath, JSON.stringify(config, null, 2));
|
|
982
|
+
logger.info(`[Build] Page parts config: ${routeDir}/page-parts.json`);
|
|
983
|
+
const pageUrl = buildUrl(route, params);
|
|
984
|
+
const slowPhase = new DevSlowlyChangingPhase();
|
|
985
|
+
const slowResult = await slowPhase.runSlowlyForPage(
|
|
986
|
+
params,
|
|
987
|
+
{ language: "en", url: pageUrl },
|
|
988
|
+
pageParts.parts,
|
|
989
|
+
pageParts.discoveredInstances,
|
|
990
|
+
pageParts.headlessInstanceComponents,
|
|
991
|
+
route.jayHtmlPath
|
|
992
|
+
);
|
|
993
|
+
if (slowResult.kind !== "PhaseOutput") {
|
|
994
|
+
if (slowResult.kind === "ClientError" || slowResult.kind === "Redirect") {
|
|
995
|
+
return {
|
|
996
|
+
status: "skipped",
|
|
997
|
+
reason: `${slowResult.kind} ${slowResult.status ?? ""} ${slowResult.message ?? ""}`.trim()
|
|
998
|
+
};
|
|
875
999
|
}
|
|
1000
|
+
throw new Error(
|
|
1001
|
+
`Slow render failed for ${route.rawRoute} with params ${JSON.stringify(params)}: ${slowResult.kind}`
|
|
1002
|
+
);
|
|
876
1003
|
}
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
} = options;
|
|
889
|
-
const partImports = keyedParts.map((p, i) => `import { ${p.exportName} as keyedPart${i} } from '${p.modulePath}';`).join("\n");
|
|
890
|
-
const hasPageModule = pageModulePath && pageExportName;
|
|
891
|
-
const pagePartExpr = hasPageModule ? `pagePart && pagePart.comp ? { comp: pagePart.comp, contextMarkers: pagePart.contexts || [] } : null` : `null`;
|
|
892
|
-
const partsArray = [
|
|
893
|
-
pagePartExpr,
|
|
894
|
-
...keyedParts.map(
|
|
895
|
-
(p, i) => `keyedPart${i} && keyedPart${i}.comp ? { comp: keyedPart${i}.comp, contextMarkers: keyedPart${i}.contexts || [], key: '${p.key}' } : null`
|
|
896
|
-
)
|
|
897
|
-
];
|
|
898
|
-
const pageImport = hasPageModule ? `import { ${pageExportName} as pagePart } from '${pageModulePath}';` : "";
|
|
899
|
-
const initImports = clientInits.map((ci, i) => `import { ${ci.exportName} as clientInit${i} } from '${ci.modulePath}';`).join("\n");
|
|
900
|
-
const initCalls = clientInits.map(
|
|
901
|
-
(ci, i) => ` if (clientInit${i}?._clientInit) await clientInit${i}._clientInit(clientInitData['${ci.key}'] || {});`
|
|
902
|
-
).join("\n");
|
|
903
|
-
const code = `import { hydrateCompositeJayComponent } from '@jay-framework/stack-client-runtime';
|
|
904
|
-
import { deepMergeViewStates } from '@jay-framework/view-state-merge';
|
|
905
|
-
import { hydrate } from '${hydrateImport}';
|
|
906
|
-
${pageImport}
|
|
907
|
-
${partImports}
|
|
908
|
-
${initImports}
|
|
909
|
-
|
|
910
|
-
const trackByMap = ${JSON.stringify(trackByMap)};
|
|
911
|
-
|
|
912
|
-
export async function init(slowViewState, fastViewState, fastCarryForward, clientInitData) {
|
|
913
|
-
${initCalls}
|
|
914
|
-
const viewState = deepMergeViewStates(slowViewState, fastViewState, trackByMap);
|
|
915
|
-
const target = document.getElementById('target');
|
|
916
|
-
const rootElement = target.firstElementChild;
|
|
917
|
-
const parts = [
|
|
918
|
-
${partsArray.join(",\n ")}
|
|
919
|
-
].filter(p => p !== null);
|
|
920
|
-
const pageComp = hydrateCompositeJayComponent(
|
|
921
|
-
hydrate, viewState, fastCarryForward,
|
|
922
|
-
parts, trackByMap, rootElement
|
|
1004
|
+
const slowViewState = slowResult.rendered;
|
|
1005
|
+
const carryForward = slowResult.carryForward;
|
|
1006
|
+
if (pageParts.discoveredInstances.length > 0 && pageParts.headlessInstanceComponents.length > 0) {
|
|
1007
|
+
const slowResult2 = await slowRenderInstances(
|
|
1008
|
+
pageParts.discoveredInstances,
|
|
1009
|
+
pageParts.headlessInstanceComponents,
|
|
1010
|
+
{
|
|
1011
|
+
pageViewState: slowViewState,
|
|
1012
|
+
pageParams: params,
|
|
1013
|
+
pageProps: { language: "en", url: pageUrl }
|
|
1014
|
+
}
|
|
923
1015
|
);
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
...pagesRoot && buildDir ? { pagesRoot, buildFolder: buildDir } : {}
|
|
938
|
-
};
|
|
939
|
-
await build({
|
|
940
|
-
root: projectRoot,
|
|
941
|
-
publicDir: false,
|
|
942
|
-
plugins: [...jayStackCompiler(fullJayOptions)],
|
|
943
|
-
build: {
|
|
944
|
-
outDir: outputDir,
|
|
945
|
-
emptyOutDir: false,
|
|
946
|
-
minify,
|
|
947
|
-
manifest: `${instanceId}-manifest.json`,
|
|
948
|
-
rollupOptions: {
|
|
949
|
-
input: { [instanceId]: hydrateEntryPath },
|
|
950
|
-
external: (id) => id.startsWith("@jay-framework/") || id === "jay-route-hydrate",
|
|
951
|
-
output: {
|
|
952
|
-
entryFileNames: "[name]-[hash].js",
|
|
953
|
-
chunkFileNames: "chunks/[name]-[hash].js",
|
|
954
|
-
assetFileNames: "[name]-[hash].[ext]",
|
|
955
|
-
format: "es"
|
|
1016
|
+
if (slowResult2) {
|
|
1017
|
+
const existingInstances = carryForward.__instances || {
|
|
1018
|
+
discovered: [],
|
|
1019
|
+
carryForwards: {}
|
|
1020
|
+
};
|
|
1021
|
+
carryForward.__instances = {
|
|
1022
|
+
discovered: [
|
|
1023
|
+
...existingInstances.discovered,
|
|
1024
|
+
...slowResult2.instancePhaseData.discovered
|
|
1025
|
+
],
|
|
1026
|
+
carryForwards: {
|
|
1027
|
+
...existingInstances.carryForwards,
|
|
1028
|
+
...slowResult2.instancePhaseData.carryForwards
|
|
956
1029
|
},
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
1030
|
+
slowViewStates: {
|
|
1031
|
+
...existingInstances.slowViewStates || {},
|
|
1032
|
+
...slowResult2.instancePhaseData.slowViewStates
|
|
1033
|
+
}
|
|
1034
|
+
};
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
if (pageParts.forEachInstances.length > 0) {
|
|
1038
|
+
const existingInstances = carryForward.__instances || {
|
|
1039
|
+
discovered: [],
|
|
1040
|
+
carryForwards: {}
|
|
1041
|
+
};
|
|
1042
|
+
existingInstances.forEachInstances = pageParts.forEachInstances;
|
|
1043
|
+
carryForward.__instances = existingInstances;
|
|
1044
|
+
}
|
|
1045
|
+
const cachePath = path__default.join(backendInstanceDir, `${instanceId}.cache.json`);
|
|
1046
|
+
await fs.writeFile(
|
|
1047
|
+
cachePath,
|
|
1048
|
+
JSON.stringify({
|
|
1049
|
+
slowViewState,
|
|
1050
|
+
carryForward
|
|
1051
|
+
}),
|
|
1052
|
+
"utf-8"
|
|
1053
|
+
);
|
|
1054
|
+
logger.info(`[Build] Instance data: ${routeDir}/${instanceId}`);
|
|
1055
|
+
const serverElementPath = routeServerElementPath ? path__default.join(ctx.backendDir, routeServerElementPath) : path__default.join(backendInstanceDir, `${instanceId}.server-element.js`);
|
|
1056
|
+
const instanceEntry = {
|
|
1057
|
+
params,
|
|
1058
|
+
cachePath: path__default.relative(ctx.backendDir, cachePath),
|
|
1059
|
+
serverElementPath: path__default.relative(ctx.backendDir, serverElementPath),
|
|
1060
|
+
clientBundlePath: routeClientBundlePath || "",
|
|
1061
|
+
clientCssPath: routeCssPath
|
|
1062
|
+
};
|
|
1063
|
+
return { status: "success", instanceEntry, slowViewState, carryForward, contracts };
|
|
1064
|
+
}
|
|
1065
|
+
function convertSegments(segments) {
|
|
1066
|
+
return segments.map((s) => {
|
|
1067
|
+
if (typeof s === "string") {
|
|
1068
|
+
return { type: "static", value: s };
|
|
1069
|
+
}
|
|
1070
|
+
switch (s.type) {
|
|
1071
|
+
case JayRouteParamType.single:
|
|
1072
|
+
return { type: "param", value: s.name };
|
|
1073
|
+
case JayRouteParamType.catchAll:
|
|
1074
|
+
return { type: "catchAll", value: s.name };
|
|
1075
|
+
case JayRouteParamType.optional:
|
|
1076
|
+
return { type: "optional", value: s.name };
|
|
1077
|
+
}
|
|
961
1078
|
});
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
jsFile: entry.file,
|
|
972
|
-
cssFile: entry.css?.[0]
|
|
1079
|
+
}
|
|
1080
|
+
function buildRouteEntry(route, serverModulePath) {
|
|
1081
|
+
return {
|
|
1082
|
+
pattern: route.rawRoute,
|
|
1083
|
+
segments: convertSegments(route.segments),
|
|
1084
|
+
serverModule: serverModulePath,
|
|
1085
|
+
componentExport: route.componentExport,
|
|
1086
|
+
instances: [],
|
|
1087
|
+
...route.devOnly && { devOnly: true }
|
|
973
1088
|
};
|
|
974
|
-
logger.info(`[Build] Client bundle: ${result.jsFile}`);
|
|
975
|
-
return result;
|
|
976
1089
|
}
|
|
977
|
-
function
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
const
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
1090
|
+
async function discoverActions(actionPaths, serverOutputDir, buildDir, projectRoot) {
|
|
1091
|
+
const actions = [];
|
|
1092
|
+
const plugins = [];
|
|
1093
|
+
for (const [entryName, sourcePath] of Object.entries(actionPaths)) {
|
|
1094
|
+
try {
|
|
1095
|
+
const code = await fs.readFile(sourcePath, "utf-8");
|
|
1096
|
+
const extracted = extractActionsFromSource(code, sourcePath);
|
|
1097
|
+
if (extracted.length > 0) {
|
|
1098
|
+
actions.push({
|
|
1099
|
+
serverModule: path__default.relative(
|
|
1100
|
+
buildDir,
|
|
1101
|
+
path__default.join(serverOutputDir, entryName + ".js")
|
|
1102
|
+
),
|
|
1103
|
+
isPlugin: false,
|
|
1104
|
+
actionNames: extracted.map((a) => a.actionName)
|
|
1105
|
+
});
|
|
989
1106
|
}
|
|
1107
|
+
} catch {
|
|
1108
|
+
getLogger().warn(`[Build] Could not extract actions from ${sourcePath}`);
|
|
990
1109
|
}
|
|
991
1110
|
}
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
const
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
1111
|
+
try {
|
|
1112
|
+
const scannedPlugins = await scanPlugins({ projectRoot });
|
|
1113
|
+
for (const [packageName, plugin] of scannedPlugins) {
|
|
1114
|
+
if (plugin.isLocal) continue;
|
|
1115
|
+
plugins.push({ name: plugin.manifest.name, packageName });
|
|
1116
|
+
const pluginActions = plugin.manifest.actions;
|
|
1117
|
+
if (pluginActions && pluginActions.length > 0) {
|
|
1118
|
+
const productionActions = pluginActions.map(normalizeActionEntry).filter((a) => !a.devOnly);
|
|
1119
|
+
const devOnlyCount = pluginActions.length - productionActions.length;
|
|
1120
|
+
if (productionActions.length > 0) {
|
|
1121
|
+
actions.push({
|
|
1122
|
+
serverModule: "",
|
|
1123
|
+
packageName,
|
|
1124
|
+
isPlugin: true,
|
|
1125
|
+
actionNames: productionActions.map((a) => a.name)
|
|
1126
|
+
});
|
|
1127
|
+
}
|
|
1128
|
+
getLogger().info(
|
|
1129
|
+
`[Build] Plugin actions from ${packageName}: ${productionActions.length}` + (devOnlyCount > 0 ? ` (excluded ${devOnlyCount} devOnly)` : "")
|
|
1130
|
+
);
|
|
999
1131
|
}
|
|
1000
1132
|
}
|
|
1001
|
-
|
|
1133
|
+
} catch (err) {
|
|
1134
|
+
getLogger().warn(`[Build] Plugin action scan failed: ${err.message}`);
|
|
1002
1135
|
}
|
|
1003
|
-
return
|
|
1004
|
-
}
|
|
1005
|
-
function paramsMatchInferred(params, inferredParams, optionalSegments) {
|
|
1006
|
-
return Object.entries(inferredParams).every(([k, v]) => {
|
|
1007
|
-
if (optionalSegments?.has(k)) return true;
|
|
1008
|
-
return params[k] === v;
|
|
1009
|
-
});
|
|
1010
|
-
}
|
|
1011
|
-
function computeSpecificity(route) {
|
|
1012
|
-
const dynamicCount = (route.rawRoute.match(/\[/g) || []).length;
|
|
1013
|
-
const inferredCount = route.inferredParams ? Object.keys(route.inferredParams).length : 0;
|
|
1014
|
-
const unresolvedCount = Math.max(0, dynamicCount - inferredCount);
|
|
1015
|
-
return 0 - unresolvedCount;
|
|
1136
|
+
return { actions, plugins };
|
|
1016
1137
|
}
|
|
1017
|
-
function
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
}).replace(/\[(\w+)\]/g, (_, name) => params[name] || "").replace(/\/\/+/g, "/").replace(/\/$/, "") || "/";
|
|
1138
|
+
async function writeRouteManifest(manifest, buildDir) {
|
|
1139
|
+
const manifestPath = path__default.join(buildDir, "route-manifest.json");
|
|
1140
|
+
await fs.writeFile(manifestPath, JSON.stringify(manifest, null, 2));
|
|
1141
|
+
getLogger().info(
|
|
1142
|
+
`[Build] Route manifest written: ${manifest.routes.length} routes, ${manifest.routes.reduce((n, r) => n + r.instances.length, 0)} instances`
|
|
1143
|
+
);
|
|
1024
1144
|
}
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
const
|
|
1035
|
-
|
|
1036
|
-
|
|
1145
|
+
createRequire(import.meta.url);
|
|
1146
|
+
async function scanPluginRoutes(projectRoot, projectRoutes) {
|
|
1147
|
+
const logger = getLogger();
|
|
1148
|
+
const plugins = await scanPlugins({ projectRoot });
|
|
1149
|
+
const projectPaths = new Set(projectRoutes.map((r) => r.rawRoute));
|
|
1150
|
+
const pluginRoutes = [];
|
|
1151
|
+
for (const [, plugin] of plugins) {
|
|
1152
|
+
if (plugin.isLocal) continue;
|
|
1153
|
+
if (!plugin.manifest.routes) continue;
|
|
1154
|
+
for (const route of plugin.manifest.routes) {
|
|
1155
|
+
if (projectPaths.has(route.path)) {
|
|
1156
|
+
logger.info(
|
|
1157
|
+
`[Routes] Plugin "${plugin.manifest.name}" route ${route.path} skipped — project route takes precedence`
|
|
1158
|
+
);
|
|
1037
1159
|
continue;
|
|
1038
1160
|
}
|
|
1039
|
-
|
|
1040
|
-
if (
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
}
|
|
1046
|
-
}
|
|
1161
|
+
const jayHtmlPath = resolvePluginExport(plugin.pluginPath, route.jayHtml);
|
|
1162
|
+
if (!jayHtmlPath) {
|
|
1163
|
+
logger.warn(
|
|
1164
|
+
`[Routes] Plugin "${plugin.manifest.name}" route ${route.path}: jayHtml "${route.jayHtml}" not found`
|
|
1165
|
+
);
|
|
1166
|
+
continue;
|
|
1047
1167
|
}
|
|
1048
|
-
const
|
|
1049
|
-
|
|
1168
|
+
const compPath = resolvePluginModule(plugin.pluginPath);
|
|
1169
|
+
const componentExport = route.component;
|
|
1170
|
+
pluginRoutes.push({
|
|
1171
|
+
segments: parseRouteSegments(route.path),
|
|
1172
|
+
rawRoute: route.path,
|
|
1173
|
+
jayHtmlPath,
|
|
1174
|
+
compPath,
|
|
1175
|
+
componentExport,
|
|
1176
|
+
packageName: plugin.packageName,
|
|
1177
|
+
...route.devOnly === true && { devOnly: true }
|
|
1178
|
+
});
|
|
1179
|
+
logger.info(
|
|
1180
|
+
`[Routes] Plugin "${plugin.manifest.name}" provides route ${route.path}${route.devOnly ? " (dev-only)" : ""}`
|
|
1181
|
+
);
|
|
1050
1182
|
}
|
|
1051
1183
|
}
|
|
1052
|
-
return
|
|
1184
|
+
return pluginRoutes;
|
|
1053
1185
|
}
|
|
1054
|
-
function
|
|
1055
|
-
const
|
|
1056
|
-
const
|
|
1057
|
-
|
|
1058
|
-
const
|
|
1059
|
-
if (
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1186
|
+
function resolvePluginExport(pluginPath, exportSubpath) {
|
|
1187
|
+
const normalized = exportSubpath.replace(/^\.\//, "");
|
|
1188
|
+
const packageJsonPath = path__default.join(pluginPath, "package.json");
|
|
1189
|
+
try {
|
|
1190
|
+
const packageJson = JSON.parse(fs__default.readFileSync(packageJsonPath, "utf-8"));
|
|
1191
|
+
if (packageJson.exports) {
|
|
1192
|
+
const exportKey = "./" + normalized;
|
|
1193
|
+
const exportValue = packageJson.exports[exportKey];
|
|
1194
|
+
if (exportValue) {
|
|
1195
|
+
const resolved = typeof exportValue === "string" ? exportValue : exportValue.default || exportValue.import || exportValue.require;
|
|
1196
|
+
if (resolved) return path__default.join(pluginPath, resolved);
|
|
1197
|
+
}
|
|
1063
1198
|
}
|
|
1199
|
+
} catch {
|
|
1064
1200
|
}
|
|
1065
|
-
const
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1201
|
+
for (const dir of ["dist", "lib", ""]) {
|
|
1202
|
+
const candidate = path__default.join(pluginPath, dir, normalized);
|
|
1203
|
+
try {
|
|
1204
|
+
fs__default.accessSync(candidate);
|
|
1205
|
+
return candidate;
|
|
1206
|
+
} catch {
|
|
1207
|
+
}
|
|
1070
1208
|
}
|
|
1071
|
-
return
|
|
1209
|
+
return void 0;
|
|
1210
|
+
}
|
|
1211
|
+
function resolvePluginModule(pluginPath) {
|
|
1212
|
+
const pkgJsonPath = path__default.join(pluginPath, "package.json");
|
|
1213
|
+
try {
|
|
1214
|
+
const pkg = JSON.parse(fs__default.readFileSync(pkgJsonPath, "utf-8"));
|
|
1215
|
+
const mainExport = pkg.exports?.["."];
|
|
1216
|
+
const mainPath = typeof mainExport === "string" ? mainExport : mainExport?.default || mainExport?.import || pkg.main;
|
|
1217
|
+
if (mainPath) {
|
|
1218
|
+
const resolved = path__default.join(pluginPath, mainPath);
|
|
1219
|
+
if (fs__default.existsSync(resolved)) return resolved;
|
|
1220
|
+
}
|
|
1221
|
+
} catch {
|
|
1222
|
+
}
|
|
1223
|
+
return path__default.join(pluginPath, "dist", "index.js");
|
|
1072
1224
|
}
|
|
1073
1225
|
async function discoverPluginClientPackages(projectRoot) {
|
|
1074
1226
|
const projectRequire = createRequire(path__default.join(projectRoot, "package.json"));
|
|
@@ -1147,7 +1299,10 @@ async function buildVersion(options) {
|
|
|
1147
1299
|
logger.important(`[Build] Project: ${options.projectRoot}`);
|
|
1148
1300
|
await fs.mkdir(backendDir, { recursive: true });
|
|
1149
1301
|
await fs.mkdir(frontendDir, { recursive: true });
|
|
1150
|
-
const { entries, routes } = await discoverServerEntries(
|
|
1302
|
+
const { entries, routes, errorPages } = await discoverServerEntries(
|
|
1303
|
+
options.projectRoot,
|
|
1304
|
+
options.pagesRoot
|
|
1305
|
+
);
|
|
1151
1306
|
const serverOutputDir = path__default.join(backendDir, "server");
|
|
1152
1307
|
await buildServerCode(
|
|
1153
1308
|
entries,
|
|
@@ -1404,6 +1559,9 @@ async function buildVersion(options) {
|
|
|
1404
1559
|
entry.noIndex = true;
|
|
1405
1560
|
}
|
|
1406
1561
|
}
|
|
1562
|
+
if (seResult.scripts?.length) {
|
|
1563
|
+
entry.scripts = seResult.scripts;
|
|
1564
|
+
}
|
|
1407
1565
|
logger.important(`[Build] Route server element: ${routeDir}`);
|
|
1408
1566
|
} catch (err) {
|
|
1409
1567
|
logger.error(`[Build] Route server element FAILED ${route.rawRoute}: ${err.message}`);
|
|
@@ -1545,13 +1703,33 @@ async function buildVersion(options) {
|
|
|
1545
1703
|
}
|
|
1546
1704
|
}
|
|
1547
1705
|
}
|
|
1706
|
+
let errorPageEntries;
|
|
1707
|
+
if (errorPages.length > 0) {
|
|
1708
|
+
errorPageEntries = await buildErrorPages(
|
|
1709
|
+
errorPages,
|
|
1710
|
+
instanceCtx,
|
|
1711
|
+
options.projectRoot,
|
|
1712
|
+
options.pagesRoot,
|
|
1713
|
+
options.tsConfigFilePath,
|
|
1714
|
+
options.minify ?? true,
|
|
1715
|
+
sharedManifest,
|
|
1716
|
+
buildDir
|
|
1717
|
+
);
|
|
1718
|
+
logger.important(
|
|
1719
|
+
`[Build] Error pages compiled: ${Object.keys(errorPageEntries).join(", ")}`
|
|
1720
|
+
);
|
|
1721
|
+
}
|
|
1722
|
+
const siteConfig = options.site ?? (options.siteBaseUrl ? { baseUrl: options.siteBaseUrl } : void 0);
|
|
1723
|
+
const siteHeadTags = generateSiteJsonLd(siteConfig);
|
|
1548
1724
|
const manifest = {
|
|
1549
1725
|
version: options.version,
|
|
1550
1726
|
projectRoot: options.projectRoot,
|
|
1551
1727
|
sharedManifest,
|
|
1552
1728
|
routes: routeEntries.map((r) => r.entry),
|
|
1553
1729
|
actions,
|
|
1554
|
-
plugins
|
|
1730
|
+
plugins,
|
|
1731
|
+
...siteHeadTags.length > 0 && { siteHeadTags },
|
|
1732
|
+
...errorPageEntries && Object.keys(errorPageEntries).length > 0 && { errorPages: errorPageEntries }
|
|
1555
1733
|
};
|
|
1556
1734
|
await writeRouteManifest(manifest, backendDir);
|
|
1557
1735
|
const publicFolder = path__default.join(options.projectRoot, "public");
|
|
@@ -1566,6 +1744,24 @@ async function buildVersion(options) {
|
|
|
1566
1744
|
const urlCount = await generateSitemap(manifest, options.siteBaseUrl, sitemapPath);
|
|
1567
1745
|
logger.important(`[Build] Sitemap generated: ${urlCount} URLs`);
|
|
1568
1746
|
}
|
|
1747
|
+
const robotsWritten = await generateRobotsTxtIfAbsent(
|
|
1748
|
+
path__default.join(frontendDir, "robots.txt"),
|
|
1749
|
+
siteConfig?.baseUrl
|
|
1750
|
+
);
|
|
1751
|
+
if (robotsWritten) logger.important("[Build] robots.txt generated");
|
|
1752
|
+
if (siteConfig?.name) {
|
|
1753
|
+
const llmsPath = path__default.join(frontendDir, "llms.txt");
|
|
1754
|
+
let authored = false;
|
|
1755
|
+
try {
|
|
1756
|
+
await fs.access(llmsPath);
|
|
1757
|
+
authored = true;
|
|
1758
|
+
} catch {
|
|
1759
|
+
}
|
|
1760
|
+
if (!authored) {
|
|
1761
|
+
const count = await generateLlmsTxt(manifest, siteConfig, backendDir, llmsPath);
|
|
1762
|
+
logger.important(`[Build] llms.txt generated: ${count} pages`);
|
|
1763
|
+
}
|
|
1764
|
+
}
|
|
1569
1765
|
const sourceHash = await computeBuildHash(buildDir);
|
|
1570
1766
|
const metadata = {
|
|
1571
1767
|
version: options.version,
|