alabjs 0.2.6 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/analytics/handler.d.ts +5 -1
- package/dist/analytics/handler.d.ts.map +1 -1
- package/dist/analytics/handler.js +14 -10
- package/dist/analytics/handler.js.map +1 -1
- package/dist/cli.js +1 -1
- package/dist/cli.js.map +1 -1
- package/dist/client/hooks.d.ts +9 -1
- package/dist/client/hooks.d.ts.map +1 -1
- package/dist/client/hooks.js +26 -2
- package/dist/client/hooks.js.map +1 -1
- package/dist/commands/build.d.ts.map +1 -1
- package/dist/commands/build.js +279 -65
- package/dist/commands/build.js.map +1 -1
- package/dist/commands/dev.d.ts.map +1 -1
- package/dist/commands/dev.js +225 -2
- package/dist/commands/dev.js.map +1 -1
- package/dist/commands/start.js +1 -1
- package/dist/commands/start.js.map +1 -1
- package/dist/components/Image.d.ts +0 -12
- package/dist/components/Image.d.ts.map +1 -1
- package/dist/components/Image.js +2 -29
- package/dist/components/Image.js.map +1 -1
- package/dist/components/ImageServer.d.ts +20 -0
- package/dist/components/ImageServer.d.ts.map +1 -0
- package/dist/components/ImageServer.js +37 -0
- package/dist/components/ImageServer.js.map +1 -0
- package/dist/components/Link.d.ts +16 -0
- package/dist/components/Link.d.ts.map +1 -1
- package/dist/components/Link.js +10 -0
- package/dist/components/Link.js.map +1 -1
- package/dist/components/index.d.ts +3 -3
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/index.js +2 -2
- package/dist/components/index.js.map +1 -1
- package/dist/live/broadcaster.d.ts +64 -0
- package/dist/live/broadcaster.d.ts.map +1 -0
- package/dist/live/broadcaster.js +78 -0
- package/dist/live/broadcaster.js.map +1 -0
- package/dist/live/registry.d.ts +34 -0
- package/dist/live/registry.d.ts.map +1 -0
- package/dist/live/registry.js +33 -0
- package/dist/live/registry.js.map +1 -0
- package/dist/live/renderer.d.ts +22 -0
- package/dist/live/renderer.d.ts.map +1 -0
- package/dist/live/renderer.js +45 -0
- package/dist/live/renderer.js.map +1 -0
- package/dist/router/manifest.d.ts +1 -1
- package/dist/router/manifest.d.ts.map +1 -1
- package/dist/server/app.d.ts.map +1 -1
- package/dist/server/app.js +339 -43
- package/dist/server/app.js.map +1 -1
- package/dist/server/cache.d.ts.map +1 -1
- package/dist/server/cache.js +23 -1
- package/dist/server/cache.js.map +1 -1
- package/dist/server/csrf.d.ts.map +1 -1
- package/dist/server/csrf.js +5 -0
- package/dist/server/csrf.js.map +1 -1
- package/dist/server/index.d.ts +1 -0
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +1 -0
- package/dist/server/index.js.map +1 -1
- package/dist/server/revalidate.d.ts.map +1 -1
- package/dist/server/revalidate.js +3 -0
- package/dist/server/revalidate.js.map +1 -1
- package/dist/ssr/html.d.ts.map +1 -1
- package/dist/ssr/html.js +15 -0
- package/dist/ssr/html.js.map +1 -1
- package/dist/ssr/ppr.d.ts.map +1 -1
- package/dist/ssr/ppr.js +2 -1
- package/dist/ssr/ppr.js.map +1 -1
- package/package.json +8 -3
- package/src/analytics/handler.ts +15 -10
- package/src/cli.ts +3 -1
- package/src/client/hooks.ts +30 -2
- package/src/commands/build.ts +316 -69
- package/src/commands/dev.ts +246 -3
- package/src/commands/start.ts +1 -1
- package/src/components/Image.tsx +2 -35
- package/src/components/ImageServer.ts +43 -0
- package/src/components/Link.tsx +20 -0
- package/src/components/index.ts +3 -3
- package/src/live/broadcaster.ts +83 -0
- package/src/live/registry.ts +56 -0
- package/src/live/renderer.ts +54 -0
- package/src/router/manifest.ts +1 -1
- package/src/server/app.ts +369 -44
- package/src/server/cache.ts +23 -1
- package/src/server/csrf.ts +5 -0
- package/src/server/index.ts +1 -0
- package/src/server/revalidate.ts +3 -0
- package/src/ssr/html.ts +15 -0
- package/src/ssr/ppr.ts +2 -1
- package/tsconfig.tsbuildinfo +1 -1
package/src/commands/build.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { build as viteBuild, type PluginOption } from "vite";
|
|
2
|
-
import { resolve } from "node:path";
|
|
2
|
+
import { resolve, relative, isAbsolute } from "node:path";
|
|
3
|
+
import { pathToFileURL } from "node:url";
|
|
3
4
|
import { spawn, execSync } from "node:child_process";
|
|
4
5
|
import { existsSync, writeFileSync, readFileSync, mkdirSync } from "node:fs";
|
|
5
6
|
import { loadUserConfig } from "../config.js";
|
|
@@ -94,12 +95,13 @@ async function buildSpa(cwd: string): Promise<void> {
|
|
|
94
95
|
export async function build({ cwd, skipTypecheck = false, mode = "ssr", analyze = false }: BuildOptions) {
|
|
95
96
|
if (mode === "spa") {
|
|
96
97
|
console.log(" alab building SPA (client-only)...\n");
|
|
97
|
-
|
|
98
|
+
await buildSpa(cwd);
|
|
99
|
+
// SPA has no route manifest, but still type-check after the build so any
|
|
100
|
+
// generated types from the Vite plugin are available to tsc.
|
|
98
101
|
if (!skipTypecheck) {
|
|
99
102
|
console.log(" alab type-checking...");
|
|
100
|
-
|
|
103
|
+
await runTypecheck(cwd);
|
|
101
104
|
}
|
|
102
|
-
await Promise.all(tasks);
|
|
103
105
|
return;
|
|
104
106
|
}
|
|
105
107
|
|
|
@@ -143,35 +145,52 @@ export async function build({ cwd, skipTypecheck = false, mode = "ssr", analyze
|
|
|
143
145
|
}
|
|
144
146
|
}
|
|
145
147
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
148
|
+
await viteBuild({
|
|
149
|
+
root: cwd,
|
|
150
|
+
plugins: [
|
|
151
|
+
(await import("alabjs-vite-plugin")).alabPlugin({ mode: "build" }),
|
|
152
|
+
...(visualizerPlugin ? [visualizerPlugin] : []),
|
|
153
|
+
],
|
|
154
|
+
build: {
|
|
155
|
+
// Output client assets to .alabjs/dist/client/ so the production server's
|
|
156
|
+
// static handler (which serves from distDir/client/) can find them.
|
|
157
|
+
outDir: resolve(cwd, ".alabjs/dist/client"),
|
|
158
|
+
manifest: true,
|
|
159
|
+
ssrManifest: true,
|
|
160
|
+
rolldownOptions: {
|
|
161
|
+
// In SSR mode, we don't use an index.html as the entry point.
|
|
162
|
+
// Instead, we bundle the virtual client module as the main browser asset.
|
|
163
|
+
input: "/@alabjs/client",
|
|
161
164
|
},
|
|
162
|
-
}
|
|
163
|
-
|
|
165
|
+
},
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
const distDir = resolve(cwd, ".alabjs/dist");
|
|
164
169
|
|
|
170
|
+
// Scan the app/ directory with the Rust router, normalize paths, and write
|
|
171
|
+
// route-manifest.json. Must run before writeBuildId (hash) and buildPPRShells.
|
|
172
|
+
// Also writes .alabjs/routes.d.ts — type-checking runs AFTER this so tsc can
|
|
173
|
+
// resolve the AlabRoutes union that routes.d.ts exports.
|
|
174
|
+
const manifest = await buildRouteManifest(cwd, distDir);
|
|
175
|
+
|
|
176
|
+
// Type-check after route types are written so `AlabRoutes` is resolvable.
|
|
165
177
|
if (!skipTypecheck) {
|
|
166
178
|
console.log(" alab type-checking...");
|
|
167
|
-
|
|
179
|
+
await runTypecheck(cwd);
|
|
168
180
|
}
|
|
169
181
|
|
|
170
|
-
|
|
182
|
+
// Validate all RouteLink/Link/navigate path references against the manifest.
|
|
183
|
+
// Runs after manifest generation but before the SSR bundle so type-safe route
|
|
184
|
+
// errors abort the build early with clear file + offset info.
|
|
185
|
+
await checkRouteReferences(cwd, manifest);
|
|
171
186
|
|
|
172
|
-
//
|
|
173
|
-
//
|
|
174
|
-
|
|
187
|
+
// Compile all app pages, layouts, and server functions to .alabjs/dist/server/.
|
|
188
|
+
// Must run after buildRouteManifest so we have the entry list, and before
|
|
189
|
+
// buildPPRShells which imports the compiled modules.
|
|
190
|
+
await buildSsrBundle(cwd, distDir, manifest);
|
|
191
|
+
|
|
192
|
+
// Write a stable build ID for skew protection (must run after the route
|
|
193
|
+
// manifest is in place for the content-hash fallback path).
|
|
175
194
|
await writeBuildId(distDir, cwd);
|
|
176
195
|
await buildPPRShells(distDir, cwd);
|
|
177
196
|
|
|
@@ -242,52 +261,64 @@ async function buildPPRShells(distDir: string, cwd: string): Promise<void> {
|
|
|
242
261
|
const pprCacheDir = resolve(cwd, PPR_CACHE_SUBDIR);
|
|
243
262
|
let count = 0;
|
|
244
263
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
// Dynamic import — module is compiled ESM, importable by Node directly.
|
|
250
|
-
const mod = await import(modulePath) as {
|
|
251
|
-
default?: unknown;
|
|
252
|
-
ppr?: unknown;
|
|
253
|
-
metadata?: Record<string, unknown>;
|
|
254
|
-
};
|
|
264
|
+
// Signal to useServerData that it must not make network calls — return
|
|
265
|
+
// empty placeholders instead so components can render their static shell.
|
|
266
|
+
process.env["ALAB_PPR_PRERENDER"] = "1";
|
|
255
267
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
268
|
+
try {
|
|
269
|
+
for (const route of pageRoutes) {
|
|
270
|
+
// esbuild compiles .tsx/.ts → .js; use the compiled path.
|
|
271
|
+
const modulePath = resolve(distDir, "server", route.file.replace(/\.(tsx?)$/, ".js"));
|
|
272
|
+
if (!existsSync(modulePath)) continue;
|
|
273
|
+
|
|
274
|
+
// Dynamic import — on Windows absolute paths need a file:// URL.
|
|
275
|
+
const mod = await import(pathToFileURL(modulePath).href) as {
|
|
276
|
+
default?: unknown;
|
|
277
|
+
ppr?: unknown;
|
|
278
|
+
metadata?: Record<string, unknown>;
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
if (mod.ppr !== true) continue;
|
|
282
|
+
if (typeof mod.default !== "function") {
|
|
283
|
+
console.warn(` alab ppr: ${route.file} has no default export — skipping.`);
|
|
284
|
+
continue;
|
|
285
|
+
}
|
|
261
286
|
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
287
|
+
// Load layout modules (outermost → innermost).
|
|
288
|
+
const layoutPaths = findBuildLayoutFiles(route.file, distDir);
|
|
289
|
+
const layoutMods = await Promise.all(
|
|
290
|
+
layoutPaths.map((p) => import(pathToFileURL(resolve(distDir, "server", p)).href)),
|
|
291
|
+
);
|
|
292
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
293
|
+
const layouts = layoutMods.map((m: any) => m.default).filter((c: unknown) => typeof c === "function");
|
|
269
294
|
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
295
|
+
try {
|
|
296
|
+
await preRenderPPRShell({
|
|
297
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
298
|
+
Page: mod.default as any,
|
|
299
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
300
|
+
layouts: layouts as any[],
|
|
301
|
+
shellOpts: {
|
|
302
|
+
metadata: (mod.metadata as never) ?? {},
|
|
303
|
+
paramsJson: "{}",
|
|
304
|
+
searchParamsJson: "{}",
|
|
305
|
+
routeFile: route.file,
|
|
306
|
+
// PPR shells are static snapshots — client mounts via CSR (createRoot)
|
|
307
|
+
// rather than hydration to avoid mismatches with the pre-rendered HTML.
|
|
308
|
+
ssr: false,
|
|
309
|
+
layoutsJson: JSON.stringify(layoutPaths.map(p => p.replace(/\.js$/, ".tsx"))),
|
|
310
|
+
},
|
|
311
|
+
pprCacheDir,
|
|
312
|
+
routePath: route.path,
|
|
313
|
+
});
|
|
314
|
+
count++;
|
|
315
|
+
} catch (err) {
|
|
316
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
317
|
+
console.warn(` alab ppr: failed to pre-render ${route.path}: ${msg}`);
|
|
318
|
+
}
|
|
290
319
|
}
|
|
320
|
+
} finally {
|
|
321
|
+
delete process.env["ALAB_PPR_PRERENDER"];
|
|
291
322
|
}
|
|
292
323
|
|
|
293
324
|
if (count > 0) {
|
|
@@ -443,6 +474,219 @@ async function buildFederationExposes(
|
|
|
443
474
|
);
|
|
444
475
|
}
|
|
445
476
|
|
|
477
|
+
/**
|
|
478
|
+
* Validate all `<RouteLink to>`, `<Link href>`, and `navigate()` path
|
|
479
|
+
* references in `app/` against the compiled route manifest.
|
|
480
|
+
*
|
|
481
|
+
* Fails the build with a formatted error list when unknown paths or literal
|
|
482
|
+
* bracket segments are found. Gracefully skips when the napi binary is absent.
|
|
483
|
+
*/
|
|
484
|
+
async function checkRouteReferences(cwd: string, manifest: RouteManifest): Promise<void> {
|
|
485
|
+
const appDir = resolve(cwd, "app");
|
|
486
|
+
|
|
487
|
+
type NapiChecker = { checkRouteRefs(appDir: string, manifestJson: string): string };
|
|
488
|
+
let napi: NapiChecker;
|
|
489
|
+
try {
|
|
490
|
+
const mod = await import("@alabjs/compiler") as unknown as { default?: NapiChecker } & NapiChecker;
|
|
491
|
+
napi = (mod.default ?? mod) as NapiChecker;
|
|
492
|
+
if (typeof napi.checkRouteRefs !== "function") return; // napi binary predates route checker
|
|
493
|
+
} catch {
|
|
494
|
+
return; // napi binary not available — skip silently
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
const manifestJson = JSON.stringify(manifest);
|
|
498
|
+
const violationsJson = napi.checkRouteRefs(appDir, manifestJson);
|
|
499
|
+
const violations = JSON.parse(violationsJson) as Array<{
|
|
500
|
+
file: string;
|
|
501
|
+
offset: number;
|
|
502
|
+
kind: "unknown_path" | "literal_segment";
|
|
503
|
+
path: string;
|
|
504
|
+
suggestion?: string;
|
|
505
|
+
}>;
|
|
506
|
+
|
|
507
|
+
if (violations.length === 0) return;
|
|
508
|
+
|
|
509
|
+
const lines: string[] = [
|
|
510
|
+
`\n alab ${violations.length} route violation${violations.length === 1 ? "" : "s"} found:\n`,
|
|
511
|
+
];
|
|
512
|
+
|
|
513
|
+
for (const v of violations) {
|
|
514
|
+
const relFile = relative(cwd, v.file);
|
|
515
|
+
const kindLabel =
|
|
516
|
+
v.kind === "unknown_path"
|
|
517
|
+
? "unknown path"
|
|
518
|
+
: "literal bracket — use params prop";
|
|
519
|
+
lines.push(` ✗ ${relFile} "${v.path}" (${kindLabel})`);
|
|
520
|
+
if (v.suggestion) {
|
|
521
|
+
lines.push(` → suggestion: ${v.suggestion}`);
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
lines.push("");
|
|
526
|
+
console.error(lines.join("\n"));
|
|
527
|
+
throw new Error(`[alabjs] Build failed: ${violations.length} route violation(s). Fix the paths above.`);
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
/**
|
|
531
|
+
* Scan `app/` with the Rust router napi, normalize absolute file paths to
|
|
532
|
+
* cwd-relative, and write `route-manifest.json` to `distDir`.
|
|
533
|
+
*
|
|
534
|
+
* Returns the in-memory manifest so callers can use it immediately without
|
|
535
|
+
* reading the file back from disk.
|
|
536
|
+
*/
|
|
537
|
+
async function buildRouteManifest(cwd: string, distDir: string): Promise<RouteManifest> {
|
|
538
|
+
const appDir = resolve(cwd, "app");
|
|
539
|
+
let manifest: RouteManifest = { routes: [] };
|
|
540
|
+
|
|
541
|
+
try {
|
|
542
|
+
type NapiRoutes = { buildRoutes(appDir: string): string };
|
|
543
|
+
const mod = await import("@alabjs/compiler") as unknown as { default?: NapiRoutes } & NapiRoutes;
|
|
544
|
+
const napi = (mod.default ?? mod) as NapiRoutes;
|
|
545
|
+
const json = napi.buildRoutes(appDir);
|
|
546
|
+
manifest = JSON.parse(json) as RouteManifest;
|
|
547
|
+
|
|
548
|
+
// The Rust scanner stores absolute paths; normalize to cwd-relative so the
|
|
549
|
+
// production server can construct `distDir/server/<file>` paths correctly.
|
|
550
|
+
for (const route of manifest.routes) {
|
|
551
|
+
if (isAbsolute(route.file)) {
|
|
552
|
+
route.file = relative(cwd, route.file);
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
} catch {
|
|
556
|
+
console.warn(
|
|
557
|
+
" alab warning: Rust compiler unavailable — route manifest will be empty.\n" +
|
|
558
|
+
" Run `cargo build --release -p alab-napi && bash scripts/copy-napi-binary.sh`.",
|
|
559
|
+
);
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
mkdirSync(distDir, { recursive: true });
|
|
563
|
+
writeFileSync(resolve(distDir, "route-manifest.json"), JSON.stringify(manifest, null, 2), "utf8");
|
|
564
|
+
|
|
565
|
+
const pages = manifest.routes.filter((r) => r.kind === "page").length;
|
|
566
|
+
const apis = manifest.routes.filter((r) => r.kind === "api").length;
|
|
567
|
+
console.log(` alab routes → ${pages} page(s), ${apis} api route(s)`);
|
|
568
|
+
|
|
569
|
+
// Emit auto-generated route types so <RouteLink to="..."> and navigate() are
|
|
570
|
+
// type-safe without any manual setup. Written to .alabjs/routes.d.ts.
|
|
571
|
+
writeRouteTypes(manifest, distDir);
|
|
572
|
+
|
|
573
|
+
return manifest;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
/**
|
|
577
|
+
* Write `.alabjs/routes.d.ts` containing the `AlabRoutes` union type and
|
|
578
|
+
* a typed `navigate` overload, auto-derived from the route manifest.
|
|
579
|
+
*
|
|
580
|
+
* Example output:
|
|
581
|
+
* ```ts
|
|
582
|
+
* export type AlabRoutes = "/" | "/about" | `/users/${string}`;
|
|
583
|
+
* ```
|
|
584
|
+
*
|
|
585
|
+
* Add `".alabjs/routes.d.ts"` to `tsconfig.json` `include` to enable
|
|
586
|
+
* type-checking on `<RouteLink to>`, `<Link href>`, and `navigate()`.
|
|
587
|
+
*/
|
|
588
|
+
function writeRouteTypes(manifest: RouteManifest, distDir: string): void {
|
|
589
|
+
const pageRoutes = manifest.routes.filter((r) => r.kind === "page");
|
|
590
|
+
|
|
591
|
+
// Convert alab `[param]` syntax → TypeScript template literal `${string}`.
|
|
592
|
+
const routeTypes = pageRoutes.map((r) => {
|
|
593
|
+
const tsPath = r.path.replace(/\[([^\]]+)\]/g, "${string}");
|
|
594
|
+
// Static path → plain string literal; dynamic path → template literal.
|
|
595
|
+
return tsPath.includes("${") ? `\`${tsPath}\`` : JSON.stringify(tsPath);
|
|
596
|
+
});
|
|
597
|
+
|
|
598
|
+
const unionType = routeTypes.length > 0 ? routeTypes.join(" | ") : "string";
|
|
599
|
+
|
|
600
|
+
const content = [
|
|
601
|
+
"// AUTO-GENERATED by `alab build` — do not edit manually.",
|
|
602
|
+
"// Add \".alabjs/routes.d.ts\" to your tsconfig.json `include` array to enable",
|
|
603
|
+
"// type-checking on <RouteLink to>, <Link href>, and navigate().",
|
|
604
|
+
"",
|
|
605
|
+
`export type AlabRoutes = ${unionType};`,
|
|
606
|
+
"",
|
|
607
|
+
"declare module \"alabjs/router\" {",
|
|
608
|
+
" export function navigate(path: AlabRoutes, opts?: { replace?: boolean }): void;",
|
|
609
|
+
"}",
|
|
610
|
+
"",
|
|
611
|
+
"declare module \"alabjs/components\" {",
|
|
612
|
+
" import type { ComponentProps } from \"react\";",
|
|
613
|
+
" interface RouteLinkProps extends Omit<ComponentProps<\"a\">, \"href\"> {",
|
|
614
|
+
" to: AlabRoutes;",
|
|
615
|
+
" replace?: boolean;",
|
|
616
|
+
" }",
|
|
617
|
+
" export function RouteLink(props: RouteLinkProps): JSX.Element;",
|
|
618
|
+
" export function Link(props: RouteLinkProps): JSX.Element;",
|
|
619
|
+
"}",
|
|
620
|
+
"",
|
|
621
|
+
].join("\n");
|
|
622
|
+
|
|
623
|
+
writeFileSync(resolve(distDir, "routes.d.ts"), content, "utf8");
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
/**
|
|
627
|
+
* Compile all SSR route files to `distDir/server/` using esbuild's per-file
|
|
628
|
+
* transpilation mode.
|
|
629
|
+
*
|
|
630
|
+
* We use esbuild directly (bundled with Vite) rather than a second viteBuild
|
|
631
|
+
* call because:
|
|
632
|
+
* 1. Preserves directory structure via `outbase` without needing
|
|
633
|
+
* `preserveModules` (which hangs with some Rolldown versions).
|
|
634
|
+
* 2. `packages: "external"` externalizes all npm specifiers while inlining
|
|
635
|
+
* local relative imports — avoids Node ESM extensionless-import failures.
|
|
636
|
+
* 3. Much faster: no second Vite startup overhead.
|
|
637
|
+
*/
|
|
638
|
+
async function buildSsrBundle(cwd: string, distDir: string, manifest: RouteManifest): Promise<void> {
|
|
639
|
+
const entryFiles = manifest.routes.map((r) => resolve(cwd, r.file));
|
|
640
|
+
|
|
641
|
+
// Include top-level middleware.ts if present.
|
|
642
|
+
const middlewarePath = resolve(cwd, "middleware.ts");
|
|
643
|
+
if (existsSync(middlewarePath)) entryFiles.push(middlewarePath);
|
|
644
|
+
|
|
645
|
+
if (entryFiles.length === 0) return;
|
|
646
|
+
|
|
647
|
+
// Build the import.meta.env replacement object for esbuild.
|
|
648
|
+
// Node.js never defines import.meta.env (it's a Vite-only concept), so if
|
|
649
|
+
// we leave it undefined the compiled server modules throw at runtime on any
|
|
650
|
+
// reference to import.meta.env.*. We mirror exactly what Vite inlines for
|
|
651
|
+
// the client build: standard constants + ALAB_PUBLIC_* / VITE_* vars.
|
|
652
|
+
const publicEnv: Record<string, string> = {};
|
|
653
|
+
for (const [key, val] of Object.entries(process.env)) {
|
|
654
|
+
if (key.startsWith("ALAB_PUBLIC_") || key.startsWith("VITE_")) {
|
|
655
|
+
publicEnv[key] = val ?? "";
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
const metaEnv = {
|
|
659
|
+
PROD: true,
|
|
660
|
+
DEV: false,
|
|
661
|
+
SSR: true,
|
|
662
|
+
MODE: "production",
|
|
663
|
+
BASE_URL: "/",
|
|
664
|
+
...publicEnv,
|
|
665
|
+
};
|
|
666
|
+
|
|
667
|
+
const { build: esbuild } = await import("esbuild");
|
|
668
|
+
await esbuild({
|
|
669
|
+
entryPoints: entryFiles,
|
|
670
|
+
outbase: cwd, // preserve directory structure: app/page.tsx → server/app/page.js
|
|
671
|
+
outdir: resolve(distDir, "server"),
|
|
672
|
+
format: "esm",
|
|
673
|
+
platform: "node",
|
|
674
|
+
target: "node22",
|
|
675
|
+
bundle: true, // bundle local imports (resolves extensionless paths)
|
|
676
|
+
packages: "external", // keep all npm specifiers (react, alabjs/*…) as-is
|
|
677
|
+
jsx: "automatic",
|
|
678
|
+
jsxImportSource: "react",
|
|
679
|
+
logLevel: "warning",
|
|
680
|
+
define: {
|
|
681
|
+
// Replace the entire import.meta.env expression so property accesses,
|
|
682
|
+
// destructuring, and optional-chaining all resolve correctly at runtime.
|
|
683
|
+
"import.meta.env": JSON.stringify(metaEnv),
|
|
684
|
+
},
|
|
685
|
+
});
|
|
686
|
+
|
|
687
|
+
console.log(" alab SSR bundle → .alabjs/dist/server");
|
|
688
|
+
}
|
|
689
|
+
|
|
446
690
|
/** Compile the offline service worker to a standalone iife bundle. */
|
|
447
691
|
async function buildOfflineSw(cwd: string): Promise<void> {
|
|
448
692
|
const swEntry = new URL("../client/offline-sw.js", import.meta.url).pathname;
|
|
@@ -461,7 +705,10 @@ async function buildOfflineSw(cwd: string): Promise<void> {
|
|
|
461
705
|
fileName: () => "offline-sw.js",
|
|
462
706
|
},
|
|
463
707
|
minify: true,
|
|
464
|
-
|
|
708
|
+
// Note: do NOT set rolldownOptions.output.inlineDynamicImports here.
|
|
709
|
+
// iife format sets codeSplitting:false which already implies
|
|
710
|
+
// inlineDynamicImports:true in Rolldown. Setting it explicitly
|
|
711
|
+
// produces a warning that can cause the build to stall in Rolldown/Vite 8+.
|
|
465
712
|
},
|
|
466
713
|
});
|
|
467
714
|
} catch (err) {
|