@jay-framework/jay-stack-cli 0.25.1 → 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/designer/INSTRUCTIONS.md +1 -0
- package/agent-kit-template/designer/error-pages-guide.md +109 -0
- package/agent-kit-template/designer/routing.md +7 -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-BQbkAEhi.js → index-B5oavmfA.js} +739 -552
- package/dist/index.d.ts +34 -1
- package/dist/index.js +428 -18
- package/package.json +14 -14
|
@@ -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,123 +152,310 @@ 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
|
-
|
|
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);
|
|
265
459
|
compiledPath = indexPath;
|
|
266
460
|
} catch {
|
|
267
461
|
compiledPath += ".js";
|
|
@@ -411,69 +605,287 @@ function buildPagePartsConfig(pageParts, pageServerModule, pageExportName, build
|
|
|
411
605
|
}))
|
|
412
606
|
};
|
|
413
607
|
}
|
|
414
|
-
function
|
|
415
|
-
if (parts.length === 0) return [];
|
|
416
|
-
if (parts.length === 1) return parts[0].values;
|
|
608
|
+
async function buildErrorPages(errorPages, ctx, projectRoot, pagesRoot, tsConfigFilePath, minify, sharedManifest, buildDir) {
|
|
417
609
|
const logger = getLogger();
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
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;
|
|
615
|
+
}
|
|
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 });
|
|
425
643
|
}
|
|
644
|
+
entry.routeCssPath = path__default.relative(ctx.frontendDir, dstCss);
|
|
426
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;
|
|
427
655
|
}
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
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 {
|
|
436
677
|
}
|
|
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;
|
|
437
685
|
}
|
|
438
|
-
|
|
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;
|
|
439
744
|
}
|
|
440
745
|
return result;
|
|
441
746
|
}
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
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"
|
|
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 };
|
|
799
|
+
}
|
|
800
|
+
function pkgToVarName(pkg) {
|
|
801
|
+
return pkg.replace("@jay-framework/", "").replace(/[/-]/g, "_");
|
|
802
|
+
}
|
|
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;
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
}
|
|
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
|
+
);
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
}
|
|
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;
|
|
853
|
+
}
|
|
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
|
+
});
|
|
859
|
+
}
|
|
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)) {
|
|
886
|
+
continue;
|
|
887
|
+
}
|
|
888
|
+
let mergedParams = params;
|
|
477
889
|
if (route.inferredParams) {
|
|
478
890
|
mergedParams = { ...params };
|
|
479
891
|
for (const [k, v] of Object.entries(route.inferredParams)) {
|
|
@@ -675,406 +1087,140 @@ function buildRouteEntry(route, serverModulePath) {
|
|
|
675
1087
|
...route.devOnly && { devOnly: true }
|
|
676
1088
|
};
|
|
677
1089
|
}
|
|
678
|
-
async function discoverActions(actionPaths, serverOutputDir, buildDir, projectRoot) {
|
|
679
|
-
const actions = [];
|
|
680
|
-
const plugins = [];
|
|
681
|
-
for (const [entryName, sourcePath] of Object.entries(actionPaths)) {
|
|
682
|
-
try {
|
|
683
|
-
const code = await fs.readFile(sourcePath, "utf-8");
|
|
684
|
-
const extracted = extractActionsFromSource(code, sourcePath);
|
|
685
|
-
if (extracted.length > 0) {
|
|
686
|
-
actions.push({
|
|
687
|
-
serverModule: path__default.relative(
|
|
688
|
-
buildDir,
|
|
689
|
-
path__default.join(serverOutputDir, entryName + ".js")
|
|
690
|
-
),
|
|
691
|
-
isPlugin: false,
|
|
692
|
-
actionNames: extracted.map((a) => a.actionName)
|
|
693
|
-
});
|
|
694
|
-
}
|
|
695
|
-
} catch {
|
|
696
|
-
getLogger().warn(`[Build] Could not extract actions from ${sourcePath}`);
|
|
697
|
-
}
|
|
698
|
-
}
|
|
699
|
-
try {
|
|
700
|
-
const scannedPlugins = await scanPlugins({ projectRoot });
|
|
701
|
-
for (const [packageName, plugin] of scannedPlugins) {
|
|
702
|
-
if (plugin.isLocal) continue;
|
|
703
|
-
plugins.push({ name: plugin.manifest.name, packageName });
|
|
704
|
-
const pluginActions = plugin.manifest.actions;
|
|
705
|
-
if (pluginActions && pluginActions.length > 0) {
|
|
706
|
-
const productionActions = pluginActions.map(normalizeActionEntry).filter((a) => !a.devOnly);
|
|
707
|
-
const devOnlyCount = pluginActions.length - productionActions.length;
|
|
708
|
-
if (productionActions.length > 0) {
|
|
709
|
-
actions.push({
|
|
710
|
-
serverModule: "",
|
|
711
|
-
packageName,
|
|
712
|
-
isPlugin: true,
|
|
713
|
-
actionNames: productionActions.map((a) => a.name)
|
|
714
|
-
});
|
|
715
|
-
}
|
|
716
|
-
getLogger().info(
|
|
717
|
-
`[Build] Plugin actions from ${packageName}: ${productionActions.length}` + (devOnlyCount > 0 ? ` (excluded ${devOnlyCount} devOnly)` : "")
|
|
718
|
-
);
|
|
719
|
-
}
|
|
720
|
-
}
|
|
721
|
-
} catch (err) {
|
|
722
|
-
getLogger().warn(`[Build] Plugin action scan failed: ${err.message}`);
|
|
723
|
-
}
|
|
724
|
-
return { actions, plugins };
|
|
725
|
-
}
|
|
726
|
-
async function writeRouteManifest(manifest, buildDir) {
|
|
727
|
-
const manifestPath = path__default.join(buildDir, "route-manifest.json");
|
|
728
|
-
await fs.writeFile(manifestPath, JSON.stringify(manifest, null, 2));
|
|
729
|
-
getLogger().info(
|
|
730
|
-
`[Build] Route manifest written: ${manifest.routes.length} routes, ${manifest.routes.reduce((n, r) => n + r.instances.length, 0)} instances`
|
|
731
|
-
);
|
|
732
|
-
}
|
|
733
|
-
createRequire(import.meta.url);
|
|
734
|
-
async function scanPluginRoutes(projectRoot, projectRoutes) {
|
|
735
|
-
const logger = getLogger();
|
|
736
|
-
const plugins = await scanPlugins({ projectRoot });
|
|
737
|
-
const projectPaths = new Set(projectRoutes.map((r) => r.rawRoute));
|
|
738
|
-
const pluginRoutes = [];
|
|
739
|
-
for (const [, plugin] of plugins) {
|
|
740
|
-
if (plugin.isLocal) continue;
|
|
741
|
-
if (!plugin.manifest.routes) continue;
|
|
742
|
-
for (const route of plugin.manifest.routes) {
|
|
743
|
-
if (projectPaths.has(route.path)) {
|
|
744
|
-
logger.info(
|
|
745
|
-
`[Routes] Plugin "${plugin.manifest.name}" route ${route.path} skipped — project route takes precedence`
|
|
746
|
-
);
|
|
747
|
-
continue;
|
|
748
|
-
}
|
|
749
|
-
const jayHtmlPath = resolvePluginExport(plugin.pluginPath, route.jayHtml);
|
|
750
|
-
if (!jayHtmlPath) {
|
|
751
|
-
logger.warn(
|
|
752
|
-
`[Routes] Plugin "${plugin.manifest.name}" route ${route.path}: jayHtml "${route.jayHtml}" not found`
|
|
753
|
-
);
|
|
754
|
-
continue;
|
|
755
|
-
}
|
|
756
|
-
const compPath = resolvePluginModule(plugin.pluginPath);
|
|
757
|
-
const componentExport = route.component;
|
|
758
|
-
pluginRoutes.push({
|
|
759
|
-
segments: parseRouteSegments(route.path),
|
|
760
|
-
rawRoute: route.path,
|
|
761
|
-
jayHtmlPath,
|
|
762
|
-
compPath,
|
|
763
|
-
componentExport,
|
|
764
|
-
packageName: plugin.packageName,
|
|
765
|
-
...route.devOnly === true && { devOnly: true }
|
|
766
|
-
});
|
|
767
|
-
logger.info(
|
|
768
|
-
`[Routes] Plugin "${plugin.manifest.name}" provides route ${route.path}${route.devOnly ? " (dev-only)" : ""}`
|
|
769
|
-
);
|
|
770
|
-
}
|
|
771
|
-
}
|
|
772
|
-
return pluginRoutes;
|
|
773
|
-
}
|
|
774
|
-
function resolvePluginExport(pluginPath, exportSubpath) {
|
|
775
|
-
const normalized = exportSubpath.replace(/^\.\//, "");
|
|
776
|
-
const packageJsonPath = path__default.join(pluginPath, "package.json");
|
|
777
|
-
try {
|
|
778
|
-
const packageJson = JSON.parse(fs__default.readFileSync(packageJsonPath, "utf-8"));
|
|
779
|
-
if (packageJson.exports) {
|
|
780
|
-
const exportKey = "./" + normalized;
|
|
781
|
-
const exportValue = packageJson.exports[exportKey];
|
|
782
|
-
if (exportValue) {
|
|
783
|
-
const resolved = typeof exportValue === "string" ? exportValue : exportValue.default || exportValue.import || exportValue.require;
|
|
784
|
-
if (resolved) return path__default.join(pluginPath, resolved);
|
|
785
|
-
}
|
|
786
|
-
}
|
|
787
|
-
} catch {
|
|
788
|
-
}
|
|
789
|
-
for (const dir of ["dist", "lib", ""]) {
|
|
790
|
-
const candidate = path__default.join(pluginPath, dir, normalized);
|
|
791
|
-
try {
|
|
792
|
-
fs__default.accessSync(candidate);
|
|
793
|
-
return candidate;
|
|
794
|
-
} catch {
|
|
795
|
-
}
|
|
796
|
-
}
|
|
797
|
-
return void 0;
|
|
798
|
-
}
|
|
799
|
-
function resolvePluginModule(pluginPath) {
|
|
800
|
-
const pkgJsonPath = path__default.join(pluginPath, "package.json");
|
|
801
|
-
try {
|
|
802
|
-
const pkg = JSON.parse(fs__default.readFileSync(pkgJsonPath, "utf-8"));
|
|
803
|
-
const mainExport = pkg.exports?.["."];
|
|
804
|
-
const mainPath = typeof mainExport === "string" ? mainExport : mainExport?.default || mainExport?.import || pkg.main;
|
|
805
|
-
if (mainPath) {
|
|
806
|
-
const resolved = path__default.join(pluginPath, mainPath);
|
|
807
|
-
if (fs__default.existsSync(resolved)) return resolved;
|
|
808
|
-
}
|
|
809
|
-
} catch {
|
|
810
|
-
}
|
|
811
|
-
return path__default.join(pluginPath, "dist", "index.js");
|
|
812
|
-
}
|
|
813
|
-
async function compileServerElement(jayHtmlContent, jayHtmlFilename, jayHtmlDir, outputPath, projectRoot, tsConfigFilePath, sourceDir, minifyCss = true) {
|
|
814
|
-
const jayFile = await parseJayFile(
|
|
815
|
-
jayHtmlContent,
|
|
816
|
-
jayHtmlFilename,
|
|
817
|
-
jayHtmlDir,
|
|
818
|
-
{ relativePath: tsConfigFilePath },
|
|
819
|
-
JAY_IMPORT_RESOLVER,
|
|
820
|
-
projectRoot,
|
|
821
|
-
sourceDir
|
|
822
|
-
);
|
|
823
|
-
const parsedJayFile = checkValidationErrors(jayFile);
|
|
824
|
-
const serverElementCode = checkValidationErrors(generateServerElementFile(parsedJayFile));
|
|
825
|
-
const outputDir = path__default.dirname(outputPath);
|
|
826
|
-
await fs.mkdir(outputDir, { recursive: true });
|
|
827
|
-
const tsPath = outputPath.replace(/\.js$/, ".ts");
|
|
828
|
-
await fs.writeFile(tsPath, serverElementCode, "utf-8");
|
|
829
|
-
const jayOptions = { tsConfigFilePath };
|
|
830
|
-
await build({
|
|
831
|
-
root: projectRoot,
|
|
832
|
-
publicDir: false,
|
|
833
|
-
plugins: [jayRuntime(jayOptions)],
|
|
834
|
-
build: {
|
|
835
|
-
outDir: outputDir,
|
|
836
|
-
emptyOutDir: false,
|
|
837
|
-
minify: false,
|
|
838
|
-
ssr: true,
|
|
839
|
-
rollupOptions: {
|
|
840
|
-
input: { [path__default.basename(outputPath, ".js")]: tsPath },
|
|
841
|
-
external: [/^node:/, /^@jay-framework\//],
|
|
842
|
-
output: {
|
|
843
|
-
entryFileNames: "[name].js",
|
|
844
|
-
format: "es"
|
|
845
|
-
}
|
|
846
|
-
}
|
|
847
|
-
},
|
|
848
|
-
logLevel: "warn"
|
|
849
|
-
});
|
|
850
|
-
await fs.rm(tsPath, { force: true });
|
|
851
|
-
let cssFile;
|
|
852
|
-
let cssImports;
|
|
853
|
-
const css = parsedJayFile.css;
|
|
854
|
-
if (css) {
|
|
855
|
-
cssImports = extractCssImportUrls(css);
|
|
856
|
-
const cssContent = minifyCss ? (await transform(css, { loader: "css", minify: true })).code : css;
|
|
857
|
-
const hash = createHash("sha256").update(cssContent).digest("hex").slice(0, 8);
|
|
858
|
-
const baseName = path__default.basename(outputPath, ".server-element.js");
|
|
859
|
-
const cssFilename = `${baseName}-${hash}.css`;
|
|
860
|
-
await fs.writeFile(path__default.join(outputDir, cssFilename), cssContent, "utf-8");
|
|
861
|
-
cssFile = cssFilename;
|
|
862
|
-
}
|
|
863
|
-
getLogger().info(`[Build] Compiled server element: ${path__default.basename(outputPath)}`);
|
|
864
|
-
return {
|
|
865
|
-
cssFile,
|
|
866
|
-
cssImports,
|
|
867
|
-
headMeta: parsedJayFile.headMeta,
|
|
868
|
-
scripts: parsedJayFile.scripts
|
|
869
|
-
};
|
|
870
|
-
}
|
|
871
|
-
async function compileRouteServerElement(jayHtmlPath, outputPath, projectRoot, tsConfigFilePath, minifyCss = true) {
|
|
872
|
-
const jayHtmlContent = await fs.readFile(jayHtmlPath, "utf-8");
|
|
873
|
-
const sourceDir = path__default.dirname(jayHtmlPath);
|
|
874
|
-
const outputDir = path__default.dirname(outputPath);
|
|
875
|
-
const jayHtml = resolveJayHtmlPaths(jayHtmlContent, sourceDir, outputDir);
|
|
876
|
-
return compileServerElement(
|
|
877
|
-
jayHtml,
|
|
878
|
-
path__default.basename(jayHtmlPath),
|
|
879
|
-
outputDir,
|
|
880
|
-
outputPath,
|
|
881
|
-
projectRoot,
|
|
882
|
-
tsConfigFilePath,
|
|
883
|
-
sourceDir,
|
|
884
|
-
minifyCss
|
|
885
|
-
);
|
|
886
|
-
}
|
|
887
|
-
async function compileRouteHydrateScript(jayHtmlPath, outputDir, projectRoot, tsConfigFilePath, minify = true) {
|
|
888
|
-
const jayHtmlContent = await fs.readFile(jayHtmlPath, "utf-8");
|
|
889
|
-
const sourceDir = path__default.dirname(jayHtmlPath);
|
|
890
|
-
const jayHtml = resolveJayHtmlPaths(jayHtmlContent, sourceDir, outputDir);
|
|
891
|
-
const jayFile = await parseJayFile(
|
|
892
|
-
jayHtml,
|
|
893
|
-
path__default.basename(jayHtmlPath),
|
|
894
|
-
outputDir,
|
|
895
|
-
{ relativePath: tsConfigFilePath },
|
|
896
|
-
JAY_IMPORT_RESOLVER,
|
|
897
|
-
projectRoot,
|
|
898
|
-
sourceDir
|
|
899
|
-
);
|
|
900
|
-
const parsedJayFile = checkValidationErrors(jayFile);
|
|
901
|
-
const hydrateCode = checkValidationErrors(
|
|
902
|
-
generateElementHydrateFile(parsedJayFile, RuntimeMode.MainTrusted)
|
|
903
|
-
);
|
|
904
|
-
await fs.mkdir(outputDir, { recursive: true });
|
|
905
|
-
const tsPath = path__default.join(outputDir, "route.hydrate.ts");
|
|
906
|
-
await fs.writeFile(tsPath, hydrateCode, "utf-8");
|
|
907
|
-
const jayOptions = { tsConfigFilePath };
|
|
908
|
-
await build({
|
|
909
|
-
root: projectRoot,
|
|
910
|
-
publicDir: false,
|
|
911
|
-
plugins: [...jayStackCompiler(jayOptions)],
|
|
912
|
-
build: {
|
|
913
|
-
outDir: outputDir,
|
|
914
|
-
emptyOutDir: false,
|
|
915
|
-
minify,
|
|
916
|
-
manifest: "route-hydrate-manifest.json",
|
|
917
|
-
rollupOptions: {
|
|
918
|
-
input: { "route.hydrate": tsPath },
|
|
919
|
-
external: (id) => id.startsWith("@jay-framework/"),
|
|
920
|
-
output: {
|
|
921
|
-
entryFileNames: "[name]-[hash].js",
|
|
922
|
-
format: "es"
|
|
923
|
-
},
|
|
924
|
-
preserveEntrySignatures: "exports-only"
|
|
925
|
-
}
|
|
926
|
-
},
|
|
927
|
-
logLevel: "warn"
|
|
928
|
-
});
|
|
929
|
-
await fs.rm(tsPath, { force: true });
|
|
930
|
-
const manifestPath = path__default.join(outputDir, "route-hydrate-manifest.json");
|
|
931
|
-
const manifest = JSON.parse(await fs.readFile(manifestPath, "utf-8"));
|
|
932
|
-
await fs.rm(manifestPath, { force: true });
|
|
933
|
-
const entryKey = Object.keys(manifest).find((k) => manifest[k].isEntry);
|
|
934
|
-
if (!entryKey) throw new Error("No entry in route hydrate manifest");
|
|
935
|
-
const jsFile = manifest[entryKey].file;
|
|
936
|
-
getLogger().info(`[Build] Compiled route hydrate script: ${jsFile}`);
|
|
937
|
-
return { jsFile };
|
|
938
|
-
}
|
|
939
|
-
function resolveJayHtmlPaths(html, sourceDir, targetDir) {
|
|
940
|
-
const root = parse(html, {
|
|
941
|
-
comment: true,
|
|
942
|
-
blockTextElements: { script: true, style: true }
|
|
943
|
-
});
|
|
944
|
-
const rewrite = (el, attr) => {
|
|
945
|
-
const val = el.getAttribute(attr);
|
|
946
|
-
if (val && (val.startsWith("./") || val.startsWith("../"))) {
|
|
947
|
-
const abs = path__default.resolve(sourceDir, val);
|
|
948
|
-
let rel = path__default.relative(targetDir, abs);
|
|
949
|
-
if (!rel.startsWith(".")) rel = "./" + rel;
|
|
950
|
-
el.setAttribute(attr, rel);
|
|
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
|
+
});
|
|
1106
|
+
}
|
|
1107
|
+
} catch {
|
|
1108
|
+
getLogger().warn(`[Build] Could not extract actions from ${sourcePath}`);
|
|
951
1109
|
}
|
|
952
|
-
};
|
|
953
|
-
for (const el of root.querySelectorAll('script[type="application/jay-data"]')) {
|
|
954
|
-
rewrite(el, "contract");
|
|
955
|
-
}
|
|
956
|
-
for (const el of root.querySelectorAll('script[type="application/jay-headless"]')) {
|
|
957
|
-
rewrite(el, "src");
|
|
958
|
-
rewrite(el, "contract");
|
|
959
|
-
}
|
|
960
|
-
for (const el of root.querySelectorAll('script[type="application/jay-headfull"]')) {
|
|
961
|
-
rewrite(el, "src");
|
|
962
|
-
rewrite(el, "contract");
|
|
963
|
-
}
|
|
964
|
-
for (const el of root.querySelectorAll('link[rel="stylesheet"]')) {
|
|
965
|
-
rewrite(el, "href");
|
|
966
1110
|
}
|
|
967
|
-
|
|
968
|
-
}
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
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
|
+
);
|
|
1131
|
+
}
|
|
977
1132
|
}
|
|
1133
|
+
} catch (err) {
|
|
1134
|
+
getLogger().warn(`[Build] Plugin action scan failed: ${err.message}`);
|
|
978
1135
|
}
|
|
979
|
-
return
|
|
980
|
-
}
|
|
981
|
-
async function generateRouteHydrationEntry(options) {
|
|
982
|
-
const {
|
|
983
|
-
hydrateImport,
|
|
984
|
-
pageModulePath,
|
|
985
|
-
pageExportName = "page",
|
|
986
|
-
trackByMap,
|
|
987
|
-
outputPath,
|
|
988
|
-
keyedParts = [],
|
|
989
|
-
clientInits = []
|
|
990
|
-
} = options;
|
|
991
|
-
const partImports = keyedParts.map((p, i) => `import { ${p.exportName} as keyedPart${i} } from '${p.modulePath}';`).join("\n");
|
|
992
|
-
const hasPageModule = pageModulePath && pageExportName;
|
|
993
|
-
const pagePartExpr = hasPageModule ? `pagePart && pagePart.comp ? { comp: pagePart.comp, contextMarkers: pagePart.contexts || [] } : null` : `null`;
|
|
994
|
-
const partsArray = [
|
|
995
|
-
pagePartExpr,
|
|
996
|
-
...keyedParts.map(
|
|
997
|
-
(p, i) => `keyedPart${i} && keyedPart${i}.comp ? { comp: keyedPart${i}.comp, contextMarkers: keyedPart${i}.contexts || [], key: '${p.key}' } : null`
|
|
998
|
-
)
|
|
999
|
-
];
|
|
1000
|
-
const pageImport = hasPageModule ? `import { ${pageExportName} as pagePart } from '${pageModulePath}';` : "";
|
|
1001
|
-
const initImports = clientInits.map((ci, i) => `import { ${ci.exportName} as clientInit${i} } from '${ci.modulePath}';`).join("\n");
|
|
1002
|
-
const initCalls = clientInits.map(
|
|
1003
|
-
(ci, i) => ` if (clientInit${i}?._clientInit) await clientInit${i}._clientInit(clientInitData['${ci.key}'] || {});`
|
|
1004
|
-
).join("\n");
|
|
1005
|
-
const code = `import { hydrateCompositeJayComponent } from '@jay-framework/stack-client-runtime';
|
|
1006
|
-
import { deepMergeViewStates } from '@jay-framework/view-state-merge';
|
|
1007
|
-
import { hydrate } from '${hydrateImport}';
|
|
1008
|
-
${pageImport}
|
|
1009
|
-
${partImports}
|
|
1010
|
-
${initImports}
|
|
1011
|
-
|
|
1012
|
-
const trackByMap = ${JSON.stringify(trackByMap)};
|
|
1013
|
-
|
|
1014
|
-
export async function init(slowViewState, fastViewState, fastCarryForward, clientInitData) {
|
|
1015
|
-
${initCalls}
|
|
1016
|
-
const viewState = deepMergeViewStates(slowViewState, fastViewState, trackByMap);
|
|
1017
|
-
const target = document.getElementById('target');
|
|
1018
|
-
const rootElement = target.firstElementChild;
|
|
1019
|
-
const parts = [
|
|
1020
|
-
${partsArray.join(",\n ")}
|
|
1021
|
-
].filter(p => p !== null);
|
|
1022
|
-
const pageComp = hydrateCompositeJayComponent(
|
|
1023
|
-
hydrate, viewState, fastCarryForward,
|
|
1024
|
-
parts, trackByMap, rootElement
|
|
1025
|
-
);
|
|
1026
|
-
return pageComp({});
|
|
1136
|
+
return { actions, plugins };
|
|
1027
1137
|
}
|
|
1028
|
-
|
|
1029
|
-
const
|
|
1030
|
-
await fs.
|
|
1031
|
-
|
|
1032
|
-
|
|
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
|
+
);
|
|
1033
1144
|
}
|
|
1034
|
-
|
|
1145
|
+
createRequire(import.meta.url);
|
|
1146
|
+
async function scanPluginRoutes(projectRoot, projectRoutes) {
|
|
1035
1147
|
const logger = getLogger();
|
|
1036
|
-
await
|
|
1037
|
-
const
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
minify,
|
|
1049
|
-
manifest: `${instanceId}-manifest.json`,
|
|
1050
|
-
rollupOptions: {
|
|
1051
|
-
input: { [instanceId]: hydrateEntryPath },
|
|
1052
|
-
external: (id) => id.startsWith("@jay-framework/") || id === "jay-route-hydrate",
|
|
1053
|
-
output: {
|
|
1054
|
-
entryFileNames: "[name]-[hash].js",
|
|
1055
|
-
chunkFileNames: "chunks/[name]-[hash].js",
|
|
1056
|
-
assetFileNames: "[name]-[hash].[ext]",
|
|
1057
|
-
format: "es"
|
|
1058
|
-
},
|
|
1059
|
-
preserveEntrySignatures: "exports-only"
|
|
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
|
+
);
|
|
1159
|
+
continue;
|
|
1060
1160
|
}
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
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;
|
|
1167
|
+
}
|
|
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
|
+
);
|
|
1182
|
+
}
|
|
1070
1183
|
}
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1184
|
+
return pluginRoutes;
|
|
1185
|
+
}
|
|
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
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
} catch {
|
|
1200
|
+
}
|
|
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
|
+
}
|
|
1208
|
+
}
|
|
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");
|
|
1078
1224
|
}
|
|
1079
1225
|
async function discoverPluginClientPackages(projectRoot) {
|
|
1080
1226
|
const projectRequire = createRequire(path__default.join(projectRoot, "package.json"));
|
|
@@ -1153,7 +1299,10 @@ async function buildVersion(options) {
|
|
|
1153
1299
|
logger.important(`[Build] Project: ${options.projectRoot}`);
|
|
1154
1300
|
await fs.mkdir(backendDir, { recursive: true });
|
|
1155
1301
|
await fs.mkdir(frontendDir, { recursive: true });
|
|
1156
|
-
const { entries, routes } = await discoverServerEntries(
|
|
1302
|
+
const { entries, routes, errorPages } = await discoverServerEntries(
|
|
1303
|
+
options.projectRoot,
|
|
1304
|
+
options.pagesRoot
|
|
1305
|
+
);
|
|
1157
1306
|
const serverOutputDir = path__default.join(backendDir, "server");
|
|
1158
1307
|
await buildServerCode(
|
|
1159
1308
|
entries,
|
|
@@ -1554,13 +1703,33 @@ async function buildVersion(options) {
|
|
|
1554
1703
|
}
|
|
1555
1704
|
}
|
|
1556
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);
|
|
1557
1724
|
const manifest = {
|
|
1558
1725
|
version: options.version,
|
|
1559
1726
|
projectRoot: options.projectRoot,
|
|
1560
1727
|
sharedManifest,
|
|
1561
1728
|
routes: routeEntries.map((r) => r.entry),
|
|
1562
1729
|
actions,
|
|
1563
|
-
plugins
|
|
1730
|
+
plugins,
|
|
1731
|
+
...siteHeadTags.length > 0 && { siteHeadTags },
|
|
1732
|
+
...errorPageEntries && Object.keys(errorPageEntries).length > 0 && { errorPages: errorPageEntries }
|
|
1564
1733
|
};
|
|
1565
1734
|
await writeRouteManifest(manifest, backendDir);
|
|
1566
1735
|
const publicFolder = path__default.join(options.projectRoot, "public");
|
|
@@ -1575,6 +1744,24 @@ async function buildVersion(options) {
|
|
|
1575
1744
|
const urlCount = await generateSitemap(manifest, options.siteBaseUrl, sitemapPath);
|
|
1576
1745
|
logger.important(`[Build] Sitemap generated: ${urlCount} URLs`);
|
|
1577
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
|
+
}
|
|
1578
1765
|
const sourceHash = await computeBuildHash(buildDir);
|
|
1579
1766
|
const metadata = {
|
|
1580
1767
|
version: options.version,
|