@neutron-build/cli 0.0.1

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.
Files changed (39) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +27 -0
  3. package/dist/commands/build.d.ts +2 -0
  4. package/dist/commands/build.d.ts.map +1 -0
  5. package/dist/commands/build.js +1251 -0
  6. package/dist/commands/build.js.map +1 -0
  7. package/dist/commands/deploy-check.d.ts +2 -0
  8. package/dist/commands/deploy-check.d.ts.map +1 -0
  9. package/dist/commands/deploy-check.js +157 -0
  10. package/dist/commands/deploy-check.js.map +1 -0
  11. package/dist/commands/dev.d.ts +2 -0
  12. package/dist/commands/dev.d.ts.map +1 -0
  13. package/dist/commands/dev.js +111 -0
  14. package/dist/commands/dev.js.map +1 -0
  15. package/dist/commands/preview.d.ts +2 -0
  16. package/dist/commands/preview.d.ts.map +1 -0
  17. package/dist/commands/preview.js +223 -0
  18. package/dist/commands/preview.js.map +1 -0
  19. package/dist/commands/release-check.d.ts +2 -0
  20. package/dist/commands/release-check.d.ts.map +1 -0
  21. package/dist/commands/release-check.js +9 -0
  22. package/dist/commands/release-check.js.map +1 -0
  23. package/dist/commands/start.d.ts +4 -0
  24. package/dist/commands/start.d.ts.map +1 -0
  25. package/dist/commands/start.js +72 -0
  26. package/dist/commands/start.js.map +1 -0
  27. package/dist/commands/worker.d.ts +2 -0
  28. package/dist/commands/worker.d.ts.map +1 -0
  29. package/dist/commands/worker.js +178 -0
  30. package/dist/commands/worker.js.map +1 -0
  31. package/dist/index.d.ts +3 -0
  32. package/dist/index.d.ts.map +1 -0
  33. package/dist/index.js +65 -0
  34. package/dist/index.js.map +1 -0
  35. package/dist/index.test.d.ts +2 -0
  36. package/dist/index.test.d.ts.map +1 -0
  37. package/dist/index.test.js +685 -0
  38. package/dist/index.test.js.map +1 -0
  39. package/package.json +57 -0
@@ -0,0 +1,1251 @@
1
+ import * as path from "node:path";
2
+ import * as fs from "node:fs";
3
+ import { build as viteBuild, loadConfigFromFile, mergeConfig, createServer } from "vite";
4
+ import { neutronPlugin } from "@neutron-build/core/vite";
5
+ import { discoverRoutes, adapterCloudflare, adapterDocker, adapterStatic, adapterVercel, prepareContentCollections, prepareRouteTypes, resolveRuntime, resolveRuntimeAliases, resolveRuntimeNoExternal, mergeSeoMetaInput, renderDocumentHead, } from "@neutron-build/core";
6
+ import { renderToString } from "preact-render-to-string";
7
+ import { h } from "preact";
8
+ export async function build() {
9
+ const cwd = process.cwd();
10
+ const routesDir = path.resolve(cwd, "src/routes");
11
+ const outputDir = path.resolve(cwd, "dist");
12
+ const neutronConfig = await loadNeutronConfig(cwd);
13
+ const runtime = resolveRuntime(neutronConfig);
14
+ const runtimeAliases = resolveRuntimeAliases(runtime);
15
+ const runtimeNoExternal = resolveRuntimeNoExternal(runtime);
16
+ const buildArgs = parseBuildArgs(process.argv.slice(3));
17
+ const selectedAdapter = resolveAdapterForBuild(neutronConfig, buildArgs);
18
+ await prepareContentCollections({
19
+ rootDir: cwd,
20
+ writeManifest: true,
21
+ writeTypes: true,
22
+ });
23
+ await prepareRouteTypes({
24
+ rootDir: cwd,
25
+ routesDir: "src/routes",
26
+ writeTypes: true,
27
+ });
28
+ if (!fs.existsSync(routesDir)) {
29
+ console.error(`Routes directory not found: ${routesDir}`);
30
+ process.exit(1);
31
+ }
32
+ console.log("Building Neutron app...\n");
33
+ const routes = discoverRoutes({ routesDir });
34
+ const pageRoutes = routes.filter((r) => !r.file.includes("_layout"));
35
+ const staticRouteCount = pageRoutes.filter((route) => route.config.mode === "static").length;
36
+ const appRouteCount = pageRoutes.filter((route) => route.config.mode === "app").length;
37
+ console.log(`Found ${routes.length} routes:\n`);
38
+ for (const route of routes) {
39
+ const isStatic = route.config.mode === "static";
40
+ const type = isStatic ? "static" : "app";
41
+ const hasParams = route.params.length > 0;
42
+ const paramNote = hasParams ? " (has params)" : "";
43
+ console.log(` ${route.path} (${type})${paramNote}`);
44
+ }
45
+ console.log("");
46
+ const loadedConfig = await loadConfigFromFile({ command: "build", mode: "production" }, undefined, cwd);
47
+ const userConfig = loadedConfig?.config || {};
48
+ // Build client assets. For static-only sites (no app routes), create a
49
+ // temporary CSS-only entry so Vite still extracts stylesheets without
50
+ // requiring an index.html entry point.
51
+ if (appRouteCount > 0) {
52
+ console.log("Building client bundle...");
53
+ await viteBuild(mergeConfig(userConfig, {
54
+ configFile: false,
55
+ root: cwd,
56
+ plugins: [neutronPlugin({ routesDir, rootDir: cwd, routeRules: neutronConfig.routes })],
57
+ resolve: {
58
+ ...(runtimeAliases ? { alias: runtimeAliases } : {}),
59
+ dedupe: ["preact", "preact/hooks", "preact/compat", "preact/jsx-runtime"],
60
+ },
61
+ build: {
62
+ outDir: outputDir,
63
+ emptyOutDir: true,
64
+ },
65
+ }));
66
+ }
67
+ else {
68
+ console.log("Building CSS bundle (static site)...");
69
+ const cssEntryDir = path.join(cwd, ".neutron");
70
+ fs.mkdirSync(cssEntryDir, { recursive: true });
71
+ const cssEntryPath = path.join(cssEntryDir, "_css-entry.js");
72
+ const cssImports = new Set();
73
+ for (const route of routes) {
74
+ try {
75
+ const src = fs.readFileSync(route.file, "utf-8");
76
+ for (const m of src.matchAll(/import\s+["']([^"']+\.css)["']/g)) {
77
+ cssImports.add(path.resolve(path.dirname(route.file), m[1]));
78
+ }
79
+ }
80
+ catch { }
81
+ }
82
+ fs.writeFileSync(cssEntryPath, [...cssImports].map((p) => `import ${JSON.stringify(p)};`).join("\n") + "\n");
83
+ await viteBuild(mergeConfig(userConfig, {
84
+ configFile: false,
85
+ root: cwd,
86
+ plugins: [],
87
+ build: {
88
+ outDir: outputDir,
89
+ emptyOutDir: true,
90
+ lib: { entry: cssEntryPath, formats: ["es"] },
91
+ rollupOptions: {
92
+ output: { assetFileNames: "assets/[name]-[hash][extname]" },
93
+ },
94
+ cssCodeSplit: false,
95
+ },
96
+ }));
97
+ try {
98
+ fs.unlinkSync(cssEntryPath);
99
+ }
100
+ catch { }
101
+ // Remove the JS lib output — we only need the extracted CSS.
102
+ for (const f of fs.readdirSync(outputDir)) {
103
+ if (f.endsWith(".mjs") || f.endsWith(".js")) {
104
+ try {
105
+ fs.unlinkSync(path.join(outputDir, f));
106
+ }
107
+ catch { }
108
+ }
109
+ }
110
+ }
111
+ const clientEntryScriptSrc = extractClientEntryScriptSrc(outputDir);
112
+ if (clientEntryScriptSrc) {
113
+ writeClientEntryMetadata(outputDir, clientEntryScriptSrc);
114
+ }
115
+ // Collect CSS files produced by the client build for injection into static HTML
116
+ const clientCssFiles = extractClientCssFiles(outputDir);
117
+ const ensureRuntimeBundle = createRuntimeBundleBuilder({
118
+ cwd,
119
+ outputDir,
120
+ routesDir,
121
+ routeRules: neutronConfig.routes,
122
+ routes,
123
+ pageRoutes,
124
+ clientEntryScriptSrc,
125
+ userConfig,
126
+ runtimeAliases,
127
+ runtimeNoExternal,
128
+ });
129
+ // Create a Vite SSR server for rendering
130
+ const server = await createServer(mergeConfig(userConfig, {
131
+ configFile: false,
132
+ root: cwd,
133
+ plugins: [neutronPlugin({ routesDir, rootDir: cwd, routeRules: neutronConfig.routes })],
134
+ ...(runtimeAliases ? { resolve: { alias: runtimeAliases } } : {}),
135
+ ...(runtimeNoExternal.length > 0 ? { ssr: { noExternal: runtimeNoExternal } } : {}),
136
+ server: {
137
+ middlewareMode: true,
138
+ hmr: false,
139
+ ws: false,
140
+ },
141
+ optimizeDeps: {
142
+ noDiscovery: true,
143
+ },
144
+ appType: "custom",
145
+ }));
146
+ // Get layouts map
147
+ const layouts = new Map();
148
+ for (const route of routes) {
149
+ if (route.file.includes("_layout")) {
150
+ layouts.set(route.id, route);
151
+ }
152
+ }
153
+ const moduleCache = new Map();
154
+ const staticHeadersByRoute = {};
155
+ function getLayoutChain(route) {
156
+ const chain = [];
157
+ let currentId = route.parentId;
158
+ while (currentId) {
159
+ const parent = layouts.get(currentId);
160
+ if (parent) {
161
+ chain.push(parent);
162
+ currentId = parent.parentId;
163
+ }
164
+ else {
165
+ break;
166
+ }
167
+ }
168
+ return chain;
169
+ }
170
+ async function loadRouteModule(route) {
171
+ let pending = moduleCache.get(route.file);
172
+ if (!pending) {
173
+ pending = server.ssrLoadModule(route.file).then((loaded) => loaded);
174
+ moduleCache.set(route.file, pending);
175
+ }
176
+ return pending;
177
+ }
178
+ async function resolveRouteHeaders(route, layoutChain, request, context, params, loaderData) {
179
+ const allRoutes = [...layoutChain].reverse();
180
+ allRoutes.push(route);
181
+ const loaderDataMap = {};
182
+ if (loaderData !== undefined) {
183
+ loaderDataMap[route.id] = loaderData;
184
+ }
185
+ const merged = new Headers();
186
+ for (const currentRoute of allRoutes) {
187
+ const currentModule = await loadRouteModule(currentRoute);
188
+ if (!currentModule.headers) {
189
+ continue;
190
+ }
191
+ const args = {
192
+ request,
193
+ params,
194
+ context,
195
+ loaderData: loaderDataMap,
196
+ };
197
+ const resolved = normalizeHeaders(await currentModule.headers(args));
198
+ for (const [name, value] of Object.entries(resolved)) {
199
+ merged.set(name, value);
200
+ }
201
+ }
202
+ return headersToRecord(merged);
203
+ }
204
+ async function resolveRouteHeadHtml(route, layoutChain, request, context, params, loaderData, pathname) {
205
+ const allRoutes = [...layoutChain].reverse();
206
+ allRoutes.push(route);
207
+ const loaderDataMap = {};
208
+ if (loaderData !== undefined) {
209
+ loaderDataMap[route.id] = loaderData;
210
+ }
211
+ let mergedSeo = null;
212
+ const headFragments = [];
213
+ for (const currentRoute of allRoutes) {
214
+ const currentModule = await loadRouteModule(currentRoute);
215
+ if (!currentModule.head) {
216
+ continue;
217
+ }
218
+ const args = {
219
+ request,
220
+ params,
221
+ context,
222
+ loaderData: loaderDataMap,
223
+ pathname,
224
+ };
225
+ // Routes use `head({ data })` — provide `data` as an alias for
226
+ // the current route's loader data so destructuring works.
227
+ const headArgsWithData = {
228
+ ...args,
229
+ data: loaderDataMap[currentRoute.id] ?? loaderData,
230
+ };
231
+ const resolved = await currentModule.head(headArgsWithData);
232
+ if (!resolved) {
233
+ continue;
234
+ }
235
+ if (typeof resolved === "string") {
236
+ headFragments.push(resolved);
237
+ continue;
238
+ }
239
+ mergedSeo = mergeSeoMetaInput(mergedSeo, resolved);
240
+ }
241
+ return renderDocumentHead(pathname, mergedSeo, headFragments);
242
+ }
243
+ // Render static routes
244
+ console.log("\nRendering static routes...");
245
+ const staticRoutes = pageRoutes.filter((r) => r.config.mode === "static");
246
+ let renderedCount = 0;
247
+ let skippedCount = 0;
248
+ for (const route of staticRoutes) {
249
+ try {
250
+ const module = await loadRouteModule(route);
251
+ if (!module?.default) {
252
+ console.log(` Skipping ${route.path} (no component)`);
253
+ skippedCount++;
254
+ continue;
255
+ }
256
+ // Handle dynamic routes with getStaticPaths
257
+ if (route.params.length > 0) {
258
+ if (!module.getStaticPaths) {
259
+ console.log(` Skipping ${route.path} (needs getStaticPaths export)`);
260
+ skippedCount++;
261
+ continue;
262
+ }
263
+ // Get all paths to render — supports both array and { paths: [] } forms
264
+ const result = await module.getStaticPaths();
265
+ const pathList = Array.isArray(result) ? result : result.paths;
266
+ for (const { params, props } of pathList) {
267
+ // Build the actual path by substituting params
268
+ const resolvedPath = resolvePath(route.path, params);
269
+ const context = {};
270
+ const request = new Request("http://localhost" + resolvedPath);
271
+ // Call the route's loader (same as static routes) so content,
272
+ // TOC, pagination, etc. are available. Fall back to props from
273
+ // getStaticPaths if there is no loader.
274
+ let loaderData = props || {};
275
+ if (module.loader) {
276
+ loaderData = await module.loader({
277
+ request,
278
+ params,
279
+ context,
280
+ });
281
+ }
282
+ const layoutChain = getLayoutChain(route);
283
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
284
+ let element = h(module.default, {
285
+ data: loaderData,
286
+ params,
287
+ });
288
+ for (const layoutRoute of [...layoutChain].reverse()) {
289
+ const layoutModule = await loadRouteModule(layoutRoute);
290
+ // Call layout loaders so sidebars, nav trees, etc. are populated.
291
+ let layoutData = {};
292
+ if (layoutModule?.loader) {
293
+ layoutData = await layoutModule.loader({
294
+ request,
295
+ params,
296
+ context,
297
+ });
298
+ }
299
+ if (layoutModule?.default) {
300
+ element = h(layoutModule.default, { data: layoutData }, element);
301
+ }
302
+ }
303
+ const html = renderToString(element);
304
+ const headHtml = await resolveRouteHeadHtml(route, layoutChain, request, context, params, loaderData, resolvedPath);
305
+ const fullHtml = wrapHtml(html, resolvedPath, loaderData, clientEntryScriptSrc, headHtml, clientCssFiles);
306
+ const outPath = getOutputPath(outputDir, resolvedPath);
307
+ fs.mkdirSync(path.dirname(outPath), { recursive: true });
308
+ fs.writeFileSync(outPath, fullHtml);
309
+ const routeHeaders = await resolveRouteHeaders(route, layoutChain, request, context, params, loaderData);
310
+ if (Object.keys(routeHeaders).length > 0) {
311
+ staticHeadersByRoute[resolvedPath] = routeHeaders;
312
+ }
313
+ console.log(` ${resolvedPath} → ${path.relative(outputDir, outPath)}`);
314
+ renderedCount++;
315
+ }
316
+ continue;
317
+ }
318
+ // Static route without params
319
+ const context = {};
320
+ const request = new Request("http://localhost" + route.path);
321
+ let loaderData = undefined;
322
+ if (module.loader) {
323
+ loaderData = await module.loader({
324
+ request,
325
+ params: {},
326
+ context,
327
+ });
328
+ }
329
+ const layoutChain = getLayoutChain(route);
330
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
331
+ let element = h(module.default, {
332
+ data: loaderData,
333
+ params: {},
334
+ });
335
+ for (const layoutRoute of [...layoutChain].reverse()) {
336
+ const layoutModule = await loadRouteModule(layoutRoute);
337
+ let layoutData = {};
338
+ if (layoutModule?.loader) {
339
+ layoutData = await layoutModule.loader({
340
+ request,
341
+ params: {},
342
+ context,
343
+ });
344
+ }
345
+ if (layoutModule?.default) {
346
+ element = h(layoutModule.default, { data: layoutData }, element);
347
+ }
348
+ }
349
+ const html = renderToString(element);
350
+ const headHtml = await resolveRouteHeadHtml(route, layoutChain, request, context, {}, loaderData, route.path);
351
+ const fullHtml = wrapHtml(html, route.path, loaderData, clientEntryScriptSrc, headHtml, clientCssFiles);
352
+ const outPath = getOutputPath(outputDir, route.path);
353
+ fs.mkdirSync(path.dirname(outPath), { recursive: true });
354
+ fs.writeFileSync(outPath, fullHtml);
355
+ const routeHeaders = await resolveRouteHeaders(route, layoutChain, request, context, {}, loaderData);
356
+ if (Object.keys(routeHeaders).length > 0) {
357
+ staticHeadersByRoute[route.path] = routeHeaders;
358
+ }
359
+ console.log(` ${route.path} → ${path.relative(outputDir, outPath)}`);
360
+ renderedCount++;
361
+ }
362
+ catch (error) {
363
+ console.error(` Error rendering ${route.path}:`, error);
364
+ skippedCount++;
365
+ }
366
+ }
367
+ if (Object.keys(staticHeadersByRoute).length > 0) {
368
+ writeStaticHeadersMetadata(outputDir, staticHeadersByRoute);
369
+ }
370
+ // Allow pending module processing to settle before closing middleware server.
371
+ await new Promise((resolve) => setTimeout(resolve, 50));
372
+ await server.close();
373
+ if (selectedAdapter) {
374
+ console.log(`\nRunning adapter: ${selectedAdapter.name}`);
375
+ await selectedAdapter.adapt({
376
+ rootDir: cwd,
377
+ outDir: outputDir,
378
+ routes: {
379
+ total: pageRoutes.length,
380
+ static: staticRouteCount,
381
+ app: appRouteCount,
382
+ },
383
+ clientEntryScriptSrc,
384
+ ensureRuntimeBundle,
385
+ log: (message) => {
386
+ console.log(` [adapter] ${message}`);
387
+ },
388
+ });
389
+ }
390
+ console.log(`\nRendered ${renderedCount} pages, skipped ${skippedCount}.`);
391
+ console.log(`\nBuild complete!`);
392
+ console.log(`Output: ${outputDir}`);
393
+ }
394
+ /**
395
+ * Resolve a route pattern with params to an actual path.
396
+ * Handles both named params and catch-all (splat) params:
397
+ * "/blog/:slug" + { slug: "hello" } → "/blog/hello"
398
+ * "/docs/*" + { "*": "getting-started/intro" } → "/docs/getting-started/intro"
399
+ */
400
+ function resolvePath(pattern, params) {
401
+ let resolved = pattern;
402
+ for (const [key, value] of Object.entries(params)) {
403
+ // Named param — :slug or [slug]
404
+ const bracketReplaced = resolved.replace(`[${key}]`, value);
405
+ const colonReplaced = resolved.replace(`:${key}`, value);
406
+ if (bracketReplaced !== resolved) {
407
+ resolved = bracketReplaced;
408
+ }
409
+ else if (colonReplaced !== resolved) {
410
+ resolved = colonReplaced;
411
+ }
412
+ else {
413
+ // Catch-all — replace *paramName (e.g., *slug) or bare *
414
+ const splatPattern = key === "*" ? "*" : `*${key}`;
415
+ resolved = resolved.replace(splatPattern, value);
416
+ }
417
+ }
418
+ return resolved;
419
+ }
420
+ function wrapHtml(content, routePath, _loaderData, clientEntryScriptSrc = null, headHtml = renderDocumentHead(routePath, null), cssFiles = []) {
421
+ // Detect islands in content — only load client runtime if interactive islands exist
422
+ const hasIslands = content.includes("<neutron-island");
423
+ const clientScript = hasIslands && clientEntryScriptSrc
424
+ ? `<script type="module" src="${escapeHtml(clientEntryScriptSrc)}"></script>`
425
+ : "";
426
+ const cssLinks = cssFiles
427
+ .map((href) => `<link rel="stylesheet" href="${escapeHtml(href)}">`)
428
+ .join("\n");
429
+ return `<!DOCTYPE html>
430
+ <html lang="en">
431
+ <head>
432
+ ${headHtml}
433
+ ${cssLinks}
434
+ </head>
435
+ <body>
436
+ <div id="app">${content}</div>
437
+ ${clientScript}
438
+ </body>
439
+ </html>`;
440
+ }
441
+ function escapeHtml(str) {
442
+ return str
443
+ .replace(/&/g, "&amp;")
444
+ .replace(/</g, "&lt;")
445
+ .replace(/>/g, "&gt;")
446
+ .replace(/"/g, "&quot;")
447
+ .replace(/'/g, "&#039;");
448
+ }
449
+ function getOutputPath(outputDir, routePath) {
450
+ if (routePath === "/") {
451
+ return path.join(outputDir, "index.html");
452
+ }
453
+ const cleanPath = routePath.replace(/\/$/, "");
454
+ return path.join(outputDir, cleanPath, "index.html");
455
+ }
456
+ function extractClientCssFiles(outputDir) {
457
+ const assetsDir = path.join(outputDir, "assets");
458
+ if (!fs.existsSync(assetsDir))
459
+ return [];
460
+ return fs
461
+ .readdirSync(assetsDir)
462
+ .filter((name) => name.endsWith(".css"))
463
+ .map((name) => `/assets/${name}`);
464
+ }
465
+ function extractClientEntryScriptSrc(outputDir) {
466
+ const assetsDir = path.join(outputDir, "assets");
467
+ if (fs.existsSync(assetsDir)) {
468
+ const candidates = fs
469
+ .readdirSync(assetsDir)
470
+ .filter((name) => name.startsWith("index-") && name.endsWith(".js"))
471
+ .sort();
472
+ if (candidates.length > 0) {
473
+ return `/assets/${candidates[candidates.length - 1]}`;
474
+ }
475
+ }
476
+ const indexPath = path.join(outputDir, "index.html");
477
+ if (!fs.existsSync(indexPath)) {
478
+ return null;
479
+ }
480
+ const html = fs.readFileSync(indexPath, "utf-8");
481
+ const match = html.match(/<script[^>]*type="module"[^>]*src="([^"]+)"[^>]*><\/script>/i);
482
+ return match?.[1] || null;
483
+ }
484
+ function writeClientEntryMetadata(outputDir, src) {
485
+ const metadataPath = path.join(outputDir, ".neutron-client-entry.json");
486
+ fs.writeFileSync(metadataPath, JSON.stringify({ src }, null, 2));
487
+ }
488
+ function normalizeHeaders(value) {
489
+ if (!value) {
490
+ return {};
491
+ }
492
+ if (value instanceof Headers) {
493
+ return headersToRecord(value);
494
+ }
495
+ const output = {};
496
+ for (const [name, headerValue] of Object.entries(value)) {
497
+ const lower = name.toLowerCase();
498
+ if (lower === "content-length" || lower === "set-cookie") {
499
+ continue;
500
+ }
501
+ output[name] = String(headerValue);
502
+ }
503
+ return output;
504
+ }
505
+ function headersToRecord(headers) {
506
+ const output = {};
507
+ headers.forEach((value, name) => {
508
+ const lower = name.toLowerCase();
509
+ if (lower === "content-length" || lower === "set-cookie") {
510
+ return;
511
+ }
512
+ output[name] = value;
513
+ });
514
+ return output;
515
+ }
516
+ function writeStaticHeadersMetadata(outputDir, headersByRoute) {
517
+ const metadataPath = path.join(outputDir, ".neutron-static-headers.json");
518
+ fs.writeFileSync(metadataPath, JSON.stringify(headersByRoute, null, 2));
519
+ }
520
+ function parseBuildArgs(argv) {
521
+ let preset = null;
522
+ let cloudflareMode = "pages";
523
+ for (let i = 0; i < argv.length; i++) {
524
+ const arg = argv[i];
525
+ if (arg === "--preset" && argv[i + 1]) {
526
+ const value = argv[++i];
527
+ if (value === "vercel" || value === "cloudflare" || value === "docker" || value === "static") {
528
+ preset = value;
529
+ }
530
+ continue;
531
+ }
532
+ if (arg.startsWith("--preset=")) {
533
+ const value = arg.split("=")[1];
534
+ if (value === "vercel" || value === "cloudflare" || value === "docker" || value === "static") {
535
+ preset = value;
536
+ }
537
+ continue;
538
+ }
539
+ if (arg === "--cloudflare-mode" && argv[i + 1]) {
540
+ const value = argv[++i];
541
+ if (value === "pages" || value === "workers") {
542
+ cloudflareMode = value;
543
+ }
544
+ continue;
545
+ }
546
+ if (arg.startsWith("--cloudflare-mode=")) {
547
+ const value = arg.split("=")[1];
548
+ if (value === "pages" || value === "workers") {
549
+ cloudflareMode = value;
550
+ }
551
+ }
552
+ }
553
+ return { preset, cloudflareMode };
554
+ }
555
+ function resolveAdapterForBuild(config, args) {
556
+ if (args.preset === "vercel") {
557
+ return adapterVercel();
558
+ }
559
+ if (args.preset === "cloudflare") {
560
+ return adapterCloudflare({ mode: args.cloudflareMode });
561
+ }
562
+ if (args.preset === "docker") {
563
+ return adapterDocker();
564
+ }
565
+ if (args.preset === "static") {
566
+ return adapterStatic();
567
+ }
568
+ return config.adapter;
569
+ }
570
+ function createRuntimeBundleBuilder(options) {
571
+ const pending = new Map();
572
+ return async (target) => {
573
+ let bundle = pending.get(target);
574
+ if (!bundle) {
575
+ bundle = buildRuntimeBundle(options, target);
576
+ pending.set(target, bundle);
577
+ }
578
+ return bundle;
579
+ };
580
+ }
581
+ async function buildRuntimeBundle(options, target) {
582
+ const appRoutes = options.pageRoutes.filter((route) => route.config.mode === "app");
583
+ if (appRoutes.length === 0) {
584
+ throw new Error(`No app routes found; cannot build ${target} runtime bundle.`);
585
+ }
586
+ const runtimeRoutes = collectRuntimeRoutes(options.routes, appRoutes);
587
+ const runtimeDir = path.join(options.cwd, ".neutron", "runtime");
588
+ fs.mkdirSync(runtimeDir, { recursive: true });
589
+ const entryPath = path.join(runtimeDir, `entry.${target}.ts`);
590
+ fs.writeFileSync(entryPath, generateRuntimeEntrySource(runtimeRoutes, appRoutes, options.clientEntryScriptSrc, entryPath, options.routeRules), "utf-8");
591
+ const bundleOutDir = path.join(options.outputDir, "server", target);
592
+ await viteBuild(mergeConfig(options.userConfig, {
593
+ configFile: false,
594
+ root: options.cwd,
595
+ plugins: [
596
+ neutronPlugin({
597
+ routesDir: options.routesDir,
598
+ rootDir: options.cwd,
599
+ routeRules: options.routeRules,
600
+ }),
601
+ ],
602
+ ...(options.runtimeAliases ? { resolve: { alias: options.runtimeAliases } } : {}),
603
+ ssr: {
604
+ target: target === "worker" ? "webworker" : "node",
605
+ noExternal: [
606
+ "preact",
607
+ "preact-render-to-string",
608
+ ...(options.runtimeNoExternal || []),
609
+ ],
610
+ },
611
+ build: {
612
+ ssr: entryPath,
613
+ outDir: bundleOutDir,
614
+ emptyOutDir: true,
615
+ rollupOptions: {
616
+ output: {
617
+ format: "esm",
618
+ entryFileNames: "entry.js",
619
+ chunkFileNames: "chunks/[name]-[hash].js",
620
+ assetFileNames: "assets/[name]-[hash][extname]",
621
+ },
622
+ },
623
+ },
624
+ }));
625
+ const entryRelativePath = path.relative(options.outputDir, path.join(bundleOutDir, "entry.js"));
626
+ return {
627
+ target,
628
+ outDir: bundleOutDir,
629
+ entryPath: path.join(bundleOutDir, "entry.js"),
630
+ entryRelativePath: entryRelativePath.split(path.sep).join("/"),
631
+ };
632
+ }
633
+ function collectRuntimeRoutes(routes, appRoutes) {
634
+ const byId = new Map(routes.map((route) => [route.id, route]));
635
+ const includedIds = new Set();
636
+ for (const route of appRoutes) {
637
+ includedIds.add(route.id);
638
+ let parentId = route.parentId;
639
+ while (parentId) {
640
+ includedIds.add(parentId);
641
+ parentId = byId.get(parentId)?.parentId ?? null;
642
+ }
643
+ }
644
+ return routes
645
+ .filter((route) => includedIds.has(route.id))
646
+ .map((route) => ({
647
+ id: route.id,
648
+ path: route.path,
649
+ parentId: route.parentId,
650
+ params: route.params,
651
+ mode: route.config.mode,
652
+ cache: route.config.cache,
653
+ isLayout: route.file.includes("_layout"),
654
+ file: route.file,
655
+ }));
656
+ }
657
+ function generateRuntimeEntrySource(runtimeRoutes, appRoutes, clientEntryScriptSrc, entryPath, routeRules) {
658
+ const imports = [];
659
+ const moduleEntries = [];
660
+ const routeDefs = [];
661
+ const appRouteIds = appRoutes.map((route) => route.id);
662
+ const routeRulesJson = JSON.stringify(routeRules || {});
663
+ runtimeRoutes.forEach((route, index) => {
664
+ const importVar = `routeModule${index}`;
665
+ const relPath = relativeImportPath(path.dirname(entryPath), route.file);
666
+ imports.push(`import * as ${importVar} from "${relPath}";`);
667
+ moduleEntries.push(` "${escapeJsString(route.id)}": ${importVar},`);
668
+ routeDefs.push(` {
669
+ id: "${escapeJsString(route.id)}",
670
+ path: "${escapeJsString(route.path)}",
671
+ parentId: ${route.parentId ? `"${escapeJsString(route.parentId)}"` : "null"},
672
+ params: ${JSON.stringify(route.params)},
673
+ mode: "${route.mode}",
674
+ cache: ${JSON.stringify(route.cache || null)},
675
+ isLayout: ${route.isLayout ? "true" : "false"},
676
+ },`);
677
+ });
678
+ return `import { h } from "preact";
679
+ import { createRouter, runMiddlewareChain, renderToString, encodeSerializedPayloadAsJson, serializeForInlineScript, mergeSeoMetaInput, renderDocumentHead, compileRouteRules, resolveRouteRuleRedirect, resolveRouteRuleRewrite, resolveRouteRuleHeaders } from "@neutron-build/core/runtime-edge";
680
+ ${imports.join("\n")}
681
+
682
+ const CLIENT_ENTRY_SCRIPT_SRC = ${JSON.stringify(clientEntryScriptSrc)};
683
+ const ROUTE_RULES = compileRouteRules(${routeRulesJson});
684
+
685
+ const ROUTE_DEFS = [
686
+ ${routeDefs.join("\n")}
687
+ ];
688
+
689
+ const ROUTE_MODULES = {
690
+ ${moduleEntries.join("\n")}
691
+ };
692
+
693
+ const APP_ROUTE_IDS = new Set(${JSON.stringify(appRouteIds)});
694
+ const ROUTE_DEF_BY_ID = new Map(ROUTE_DEFS.map((route) => [route.id, route]));
695
+ const ROUTES_BY_ID = new Map(ROUTE_DEFS.map((route) => [route.id, toRuntimeRoute(route)]));
696
+ const LOADER_DATA_CACHE = new Map();
697
+ const LOADER_CACHE_MAX_ENTRIES = 4000;
698
+
699
+ const router = createRouter();
700
+ for (const routeDef of ROUTE_DEFS) {
701
+ if (!routeDef.isLayout && APP_ROUTE_IDS.has(routeDef.id)) {
702
+ router.insert(toRuntimeRoute(routeDef));
703
+ }
704
+ }
705
+
706
+ export async function handleNeutronRequest(request) {
707
+ const requestUrl = new URL(request.url);
708
+ const pathname = normalizePathname(requestUrl.pathname);
709
+ if (!pathname) {
710
+ return new Response("Bad Request", { status: 400 });
711
+ }
712
+
713
+ const redirect = resolveRouteRuleRedirect(ROUTE_RULES, pathname, requestUrl.search);
714
+ if (redirect) {
715
+ return new Response(null, {
716
+ status: redirect.status,
717
+ headers: {
718
+ Location: redirect.location,
719
+ },
720
+ });
721
+ }
722
+
723
+ const rewrite = resolveRouteRuleRewrite(ROUTE_RULES, pathname);
724
+ const effectivePathname = rewrite?.pathname || pathname;
725
+
726
+ const match = router.match(effectivePathname);
727
+ if (!match || !APP_ROUTE_IDS.has(match.route.id)) {
728
+ return new Response("Not Found", { status: 404 });
729
+ }
730
+
731
+ const layoutChain = getLayoutChain(match.route);
732
+ const allRoutes = [...layoutChain, match.route];
733
+ const routeModules = new Map();
734
+ for (const route of allRoutes) {
735
+ routeModules.set(route.id, ROUTE_MODULES[route.id] || {});
736
+ }
737
+
738
+ const middlewares = [];
739
+ for (const route of allRoutes) {
740
+ const mod = routeModules.get(route.id);
741
+ if (mod?.middleware) {
742
+ middlewares.push(mod.middleware);
743
+ }
744
+ }
745
+
746
+ const context = {};
747
+ if (isMutationMethod(request.method)) {
748
+ invalidateLoaderDataCacheForPath(effectivePathname);
749
+ }
750
+
751
+ const response = await runMiddlewareChain(middlewares, request, context, async () => {
752
+ let actionData = undefined;
753
+ const pageModule = routeModules.get(match.route.id);
754
+
755
+ if (!pageModule?.default) {
756
+ return new Response("Not Found", { status: 404 });
757
+ }
758
+
759
+ if (isMutationMethod(request.method) && pageModule.action) {
760
+ try {
761
+ const actionResult = await pageModule.action({
762
+ request,
763
+ params: match.params,
764
+ context,
765
+ });
766
+ if (actionResult instanceof Response) {
767
+ return actionResult;
768
+ }
769
+ actionData = actionResult;
770
+ } catch (error) {
771
+ if (error instanceof Response) {
772
+ return error;
773
+ }
774
+ return renderErrorResponse(allRoutes, routeModules, match.route, toError(error));
775
+ }
776
+ }
777
+
778
+ const loaderResults = await Promise.all(
779
+ allRoutes.map(async (route) => {
780
+ const mod = routeModules.get(route.id);
781
+ if (!mod?.loader) {
782
+ return { routeId: route.id, data: null, error: null };
783
+ }
784
+ const routeParams = route.id === match.route.id ? match.params : {};
785
+ const loaderCacheMaxAge = route.config?.cache?.loaderMaxAge || 0;
786
+ const canCacheLoaderData =
787
+ loaderCacheMaxAge > 0 && isLoaderDataCacheableRequest(request);
788
+ const canReadLoaderCache =
789
+ canCacheLoaderData && isLoaderDataCacheReadableMethod(request.method);
790
+ const loaderCacheKey = canCacheLoaderData
791
+ ? buildLoaderDataCacheKey(request, route.id, routeParams)
792
+ : null;
793
+ if (loaderCacheKey && canReadLoaderCache) {
794
+ const cachedData = readCachedLoaderData(loaderCacheKey);
795
+ if (cachedData !== null) {
796
+ return { routeId: route.id, data: cachedData, error: null };
797
+ }
798
+ }
799
+ try {
800
+ const data = await mod.loader({
801
+ request,
802
+ params: routeParams,
803
+ context,
804
+ });
805
+ if (loaderCacheKey) {
806
+ storeLoaderDataCache(loaderCacheKey, data, loaderCacheMaxAge);
807
+ }
808
+ return { routeId: route.id, data, error: null };
809
+ } catch (error) {
810
+ return { routeId: route.id, data: null, error };
811
+ }
812
+ })
813
+ );
814
+
815
+ const loaderData = {};
816
+ for (const result of loaderResults) {
817
+ if (result.error) {
818
+ if (result.error instanceof Response) {
819
+ return result.error;
820
+ }
821
+ const errorRoute = allRoutes.find((route) => route.id === result.routeId) || match.route;
822
+ return renderErrorResponse(allRoutes, routeModules, errorRoute, toError(result.error));
823
+ }
824
+ if (result.data !== null) {
825
+ loaderData[result.routeId] = result.data;
826
+ }
827
+ }
828
+
829
+ const routeHeaders = await resolveRouteHeaders(allRoutes, routeModules, {
830
+ request,
831
+ params: match.params,
832
+ context,
833
+ loaderData,
834
+ actionData,
835
+ });
836
+
837
+ if (isJsonRequest(request)) {
838
+ const payload = { ...loaderData };
839
+ if (actionData !== undefined) {
840
+ payload.__action__ = actionData;
841
+ }
842
+ routeHeaders.set("Content-Type", "application/json");
843
+ return new Response(encodeSerializedPayloadAsJson(payload), { headers: routeHeaders });
844
+ }
845
+
846
+ try {
847
+ let element = h(pageModule.default, {
848
+ data: loaderData[match.route.id],
849
+ params: match.params,
850
+ actionData,
851
+ });
852
+
853
+ for (let i = layoutChain.length - 1; i >= 0; i--) {
854
+ const layoutRoute = layoutChain[i];
855
+ const layoutModule = routeModules.get(layoutRoute.id);
856
+ if (layoutModule?.default) {
857
+ element = h(layoutModule.default, { data: loaderData[layoutRoute.id] }, element);
858
+ }
859
+ }
860
+
861
+ const routeHeadHtml = await resolveRouteHeadHtml(allRoutes, routeModules, {
862
+ request,
863
+ params: match.params,
864
+ context,
865
+ loaderData,
866
+ actionData,
867
+ pathname,
868
+ });
869
+ const html = renderToString(element);
870
+ const fullHtml = wrapHtml(html, pathname, loaderData, actionData, routeHeadHtml);
871
+ return new Response(fullHtml, {
872
+ headers: withDefaultContentType(routeHeaders, "text/html; charset=utf-8"),
873
+ });
874
+ } catch (error) {
875
+ return renderErrorResponse(allRoutes, routeModules, match.route, toError(error));
876
+ }
877
+ });
878
+
879
+ if (isMutationMethod(request.method)) {
880
+ applyMutationInvalidationToLoaderDataCache(effectivePathname, response);
881
+ }
882
+
883
+ applyRouteRuleHeaders(response, pathname);
884
+ return response;
885
+ }
886
+
887
+ function toRuntimeRoute(routeDef) {
888
+ const config = { mode: routeDef.mode };
889
+ if (routeDef.cache) {
890
+ config.cache = routeDef.cache;
891
+ }
892
+
893
+ return {
894
+ id: routeDef.id,
895
+ path: routeDef.path,
896
+ file: routeDef.id,
897
+ pattern: /^$/,
898
+ params: routeDef.params,
899
+ config,
900
+ parentId: routeDef.parentId,
901
+ };
902
+ }
903
+
904
+ function getLayoutChain(route) {
905
+ const layouts = [];
906
+ let parentId = route.parentId;
907
+ while (parentId) {
908
+ const routeDef = ROUTE_DEF_BY_ID.get(parentId);
909
+ if (!routeDef) {
910
+ break;
911
+ }
912
+ if (routeDef.isLayout) {
913
+ const layoutRoute = ROUTES_BY_ID.get(routeDef.id);
914
+ if (layoutRoute) {
915
+ layouts.unshift(layoutRoute);
916
+ }
917
+ }
918
+ parentId = routeDef.parentId;
919
+ }
920
+ return layouts;
921
+ }
922
+
923
+ function normalizePathname(pathname) {
924
+ let decoded;
925
+ try {
926
+ decoded = decodeURIComponent(pathname || "/");
927
+ } catch {
928
+ return null;
929
+ }
930
+
931
+ if (!decoded.startsWith("/") || decoded.includes("..")) {
932
+ return null;
933
+ }
934
+ if (decoded.length > 1 && decoded.endsWith("/")) {
935
+ return decoded.slice(0, -1);
936
+ }
937
+ return decoded;
938
+ }
939
+
940
+ function applyRouteRuleHeaders(response, pathname) {
941
+ const matches = resolveRouteRuleHeaders(ROUTE_RULES, pathname);
942
+ for (const match of matches) {
943
+ for (const [name, value] of Object.entries(match.headers || {})) {
944
+ try {
945
+ if (!response.headers.has(name)) {
946
+ response.headers.set(name, String(value));
947
+ }
948
+ } catch {
949
+ // Ignore immutable Response headers (for example, redirect responses).
950
+ }
951
+ }
952
+ }
953
+ }
954
+
955
+ function isMutationMethod(method) {
956
+ const normalized = String(method || "GET").toUpperCase();
957
+ return normalized === "POST" || normalized === "PUT" || normalized === "PATCH" || normalized === "DELETE";
958
+ }
959
+
960
+ function isJsonRequest(request) {
961
+ const accept = request.headers.get("Accept") || "";
962
+ return accept.includes("application/json");
963
+ }
964
+
965
+ function isLoaderDataCacheableRequest(request) {
966
+ const cacheControl = request.headers.get("Cache-Control") || "";
967
+ if (cacheControl.includes("no-cache") || cacheControl.includes("no-store")) {
968
+ return false;
969
+ }
970
+
971
+ if (request.headers.has("Authorization") || request.headers.has("Cookie")) {
972
+ return false;
973
+ }
974
+
975
+ return true;
976
+ }
977
+
978
+ function isLoaderDataCacheReadableMethod(method) {
979
+ const normalized = String(method || "GET").toUpperCase();
980
+ return normalized === "GET" || normalized === "HEAD";
981
+ }
982
+
983
+ function buildLoaderDataCacheKey(request, routeId, params) {
984
+ const url = new URL(request.url);
985
+ const encodedParams = stableEncodeParams(params);
986
+ return \`\${url.pathname}::\${url.search}::\${routeId}::\${encodedParams}\`;
987
+ }
988
+
989
+ function stableEncodeParams(params) {
990
+ const sortedEntries = Object.entries(params).sort(([left], [right]) =>
991
+ left.localeCompare(right)
992
+ );
993
+ return JSON.stringify(sortedEntries);
994
+ }
995
+
996
+ function readCachedLoaderData(key) {
997
+ const entry = LOADER_DATA_CACHE.get(key);
998
+ if (!entry) {
999
+ return null;
1000
+ }
1001
+ if (entry.expiresAt <= Date.now()) {
1002
+ LOADER_DATA_CACHE.delete(key);
1003
+ return null;
1004
+ }
1005
+ return entry.data;
1006
+ }
1007
+
1008
+ function storeLoaderDataCache(key, data, maxAgeSec) {
1009
+ if (!(maxAgeSec > 0)) {
1010
+ return;
1011
+ }
1012
+
1013
+ if (!LOADER_DATA_CACHE.has(key) && LOADER_DATA_CACHE.size >= LOADER_CACHE_MAX_ENTRIES) {
1014
+ const oldest = LOADER_DATA_CACHE.keys().next().value;
1015
+ if (typeof oldest === "string") {
1016
+ LOADER_DATA_CACHE.delete(oldest);
1017
+ }
1018
+ }
1019
+
1020
+ LOADER_DATA_CACHE.set(key, {
1021
+ data,
1022
+ expiresAt: Date.now() + maxAgeSec * 1000,
1023
+ });
1024
+ }
1025
+
1026
+ function invalidateLoaderDataCacheForPath(pathname) {
1027
+ const normalized = normalizePathname(pathname);
1028
+ if (!normalized) {
1029
+ return;
1030
+ }
1031
+ const prefix = \`\${normalized}::\`;
1032
+ for (const key of LOADER_DATA_CACHE.keys()) {
1033
+ if (key.startsWith(prefix)) {
1034
+ LOADER_DATA_CACHE.delete(key);
1035
+ }
1036
+ }
1037
+ }
1038
+
1039
+ function applyMutationInvalidationToLoaderDataCache(pathname, response) {
1040
+ const directive = response.headers.get("x-neutron-invalidate");
1041
+ if (!directive) {
1042
+ return;
1043
+ }
1044
+
1045
+ const tokens = directive
1046
+ .split(",")
1047
+ .map((token) => token.trim())
1048
+ .filter(Boolean);
1049
+
1050
+ if (tokens.length === 0) {
1051
+ return;
1052
+ }
1053
+
1054
+ for (const token of tokens) {
1055
+ if (token === "*") {
1056
+ LOADER_DATA_CACHE.clear();
1057
+ return;
1058
+ }
1059
+ if (token === "self") {
1060
+ invalidateLoaderDataCacheForPath(pathname);
1061
+ continue;
1062
+ }
1063
+ const normalized = normalizePathname(token);
1064
+ if (normalized) {
1065
+ invalidateLoaderDataCacheForPath(normalized);
1066
+ }
1067
+ }
1068
+ }
1069
+
1070
+ async function resolveRouteHeaders(allRoutes, routeModules, args) {
1071
+ const headers = new Headers();
1072
+ for (const route of allRoutes) {
1073
+ const mod = routeModules.get(route.id);
1074
+ if (!mod?.headers) {
1075
+ continue;
1076
+ }
1077
+ const resolved = await mod.headers(args);
1078
+ const next = toHeaders(resolved);
1079
+ next.forEach((value, name) => {
1080
+ headers.set(name, value);
1081
+ });
1082
+ }
1083
+ return headers;
1084
+ }
1085
+
1086
+ async function resolveRouteHeadHtml(allRoutes, routeModules, args) {
1087
+ let mergedSeo = null;
1088
+ const headFragments = [];
1089
+
1090
+ for (const route of allRoutes) {
1091
+ const mod = routeModules.get(route.id);
1092
+ if (!mod?.head) {
1093
+ continue;
1094
+ }
1095
+
1096
+ const resolved = await mod.head(args);
1097
+ if (!resolved) {
1098
+ continue;
1099
+ }
1100
+
1101
+ if (typeof resolved === "string") {
1102
+ headFragments.push(resolved);
1103
+ continue;
1104
+ }
1105
+
1106
+ mergedSeo = mergeSeoMetaInput(mergedSeo, resolved);
1107
+ }
1108
+
1109
+ return renderDocumentHead(args.pathname, mergedSeo, headFragments);
1110
+ }
1111
+
1112
+ function toHeaders(value) {
1113
+ if (!value) {
1114
+ return new Headers();
1115
+ }
1116
+ if (value instanceof Headers) {
1117
+ return new Headers(value);
1118
+ }
1119
+ const headers = new Headers();
1120
+ for (const [name, headerValue] of Object.entries(value)) {
1121
+ headers.set(name, String(headerValue));
1122
+ }
1123
+ return headers;
1124
+ }
1125
+
1126
+ function withDefaultContentType(headers, fallback) {
1127
+ if (!headers.has("Content-Type")) {
1128
+ headers.set("Content-Type", fallback);
1129
+ }
1130
+ return headers;
1131
+ }
1132
+
1133
+ function renderErrorResponse(allRoutes, modules, route, error) {
1134
+ const boundary = findNearestErrorBoundary(allRoutes, modules, route);
1135
+ if (!boundary) {
1136
+ return new Response(renderDefaultError(error), {
1137
+ status: 500,
1138
+ headers: { "Content-Type": "text/html; charset=utf-8" },
1139
+ });
1140
+ }
1141
+
1142
+ const boundaryElement = h(boundary, { error });
1143
+ const boundaryHtml = renderToString(boundaryElement);
1144
+ return new Response(wrapHtml(boundaryHtml, route.path, {}), {
1145
+ status: 500,
1146
+ headers: { "Content-Type": "text/html; charset=utf-8" },
1147
+ });
1148
+ }
1149
+
1150
+ function findNearestErrorBoundary(allRoutes, modules, route) {
1151
+ const pageModule = modules.get(route.id);
1152
+ if (pageModule?.ErrorBoundary) {
1153
+ return pageModule.ErrorBoundary;
1154
+ }
1155
+
1156
+ for (let i = allRoutes.length - 2; i >= 0; i--) {
1157
+ const layoutModule = modules.get(allRoutes[i].id);
1158
+ if (layoutModule?.ErrorBoundary) {
1159
+ return layoutModule.ErrorBoundary;
1160
+ }
1161
+ }
1162
+
1163
+ return undefined;
1164
+ }
1165
+
1166
+ function renderDefaultError(error) {
1167
+ return \`<!DOCTYPE html>
1168
+ <html lang="en">
1169
+ <head>
1170
+ <meta charset="UTF-8">
1171
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
1172
+ <title>Error - Neutron</title>
1173
+ </head>
1174
+ <body>
1175
+ <h1>Application Error</h1>
1176
+ <pre>\${escapeHtml(error.message || "Unknown error")}</pre>
1177
+ </body>
1178
+ </html>\`;
1179
+ }
1180
+
1181
+ function wrapHtml(content, pathname, loaderData, actionData, headHtml = "") {
1182
+ const allData = { ...loaderData };
1183
+ if (actionData !== undefined) {
1184
+ allData.__action__ = actionData;
1185
+ }
1186
+ const dataScript = Object.keys(allData).length > 0
1187
+ ? \`<script>window.__NEUTRON_DATA_SERIALIZED__=\${serializeForInlineScript(allData)};</script>\`
1188
+ : "";
1189
+ const clientScript = CLIENT_ENTRY_SCRIPT_SRC
1190
+ ? \`<script type="module" src="\${escapeHtml(CLIENT_ENTRY_SCRIPT_SRC)}"></script>\`
1191
+ : "";
1192
+
1193
+ return \`<!DOCTYPE html>
1194
+ <html lang="en">
1195
+ <head>
1196
+ \${headHtml || renderDocumentHead(pathname, null)}
1197
+ </head>
1198
+ <body>
1199
+ <div id="app">\${content}</div>
1200
+ \${dataScript}
1201
+ \${clientScript}
1202
+ </body>
1203
+ </html>\`;
1204
+ }
1205
+
1206
+ function escapeHtml(str) {
1207
+ return String(str)
1208
+ .replace(/&/g, "&amp;")
1209
+ .replace(/</g, "&lt;")
1210
+ .replace(/>/g, "&gt;")
1211
+ .replace(/"/g, "&quot;");
1212
+ }
1213
+
1214
+ function toError(value) {
1215
+ if (value instanceof Error) {
1216
+ return value;
1217
+ }
1218
+ if (typeof value === "string") {
1219
+ return new Error(value);
1220
+ }
1221
+ return new Error("Unknown error");
1222
+ }
1223
+ `;
1224
+ }
1225
+ function relativeImportPath(fromDir, filePath) {
1226
+ const rel = path.relative(fromDir, filePath).split(path.sep).join("/");
1227
+ return rel.startsWith(".") ? rel : `./${rel}`;
1228
+ }
1229
+ function escapeJsString(value) {
1230
+ return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
1231
+ }
1232
+ async function loadNeutronConfig(cwd) {
1233
+ const candidates = [
1234
+ "neutron.config.ts",
1235
+ "neutron.config.js",
1236
+ "neutron.config.mjs",
1237
+ "neutron.config.cjs",
1238
+ ];
1239
+ for (const file of candidates) {
1240
+ const fullPath = path.resolve(cwd, file);
1241
+ if (!fs.existsSync(fullPath)) {
1242
+ continue;
1243
+ }
1244
+ const loaded = await loadConfigFromFile({ command: "build", mode: "production" }, fullPath, cwd);
1245
+ if (loaded?.config) {
1246
+ return loaded.config;
1247
+ }
1248
+ }
1249
+ return {};
1250
+ }
1251
+ //# sourceMappingURL=build.js.map