litzjs 0.0.1 → 0.1.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/vite.mjs CHANGED
@@ -1,10 +1,10 @@
1
- import { a as matchPathname, i as extractRouteLikeParams, o as sortByPathSpecificity } from "./internal-transport-dsMykcNK.mjs";
2
- import { n as parseInternalRequestBody, t as createInternalHandlerHeaders } from "./request-headers-DepZ5tjg.mjs";
1
+ import { a as matchPathname, i as extractRouteLikeParams, o as sortByPathSpecificity } from "./internal-transport-ceutGxl7.mjs";
2
+ import { n as parseInternalRequestBody, t as createInternalHandlerHeaders } from "./request-headers-B-mkIVuc.mjs";
3
3
  import vitePluginRsc from "@vitejs/plugin-rsc";
4
- import { spawnSync } from "node:child_process";
5
- import { mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs";
4
+ import { readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs";
6
5
  import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
7
6
  import path from "node:path";
7
+ import picomatch from "picomatch";
8
8
  import { glob } from "tinyglobby";
9
9
  import ts from "typescript";
10
10
  //#region src/client-projection.ts
@@ -269,6 +269,11 @@ const LITZ_BROWSER_ENTRY_ID = "virtual:litzjs:browser-entry";
269
269
  const RESOLVED_LITZ_BROWSER_ENTRY_ID = "\0virtual:litzjs:browser-entry";
270
270
  const LITZ_RSC_RENDERER_ID = "virtual:litzjs:rsc-renderer";
271
271
  const RESOLVED_LITZ_RSC_RENDERER_ID = "\0virtual:litzjs:rsc-renderer";
272
+ /**
273
+ * Creates the Litz Vite plugin array. Returns the `@vitejs/plugin-rsc` plugins
274
+ * plus the core Litz plugin. The mutable state variables below are populated
275
+ * during `configResolved` and kept in sync during dev via file watching.
276
+ */
272
277
  function litz(options = {}) {
273
278
  let root = process.cwd();
274
279
  let browserEntryPath = "src/main.tsx";
@@ -282,6 +287,7 @@ function litz(options = {}) {
282
287
  let resourceManifest = [];
283
288
  let apiManifest = [];
284
289
  let clientProjectedFiles = /* @__PURE__ */ new Set();
290
+ let closeBundlePassCount = 0;
285
291
  let hasFinalizedServerArtifacts = false;
286
292
  let hasRegisteredExitFinalizer = false;
287
293
  const routePatterns = options.routes ?? [
@@ -292,6 +298,7 @@ function litz(options = {}) {
292
298
  const resourcePatterns = options.resources ?? ["src/routes/resources/**/*.{ts,tsx}"];
293
299
  const apiPatterns = options.api ?? ["src/routes/api/**/*.{ts,tsx}"];
294
300
  const rscPlugins = vitePluginRsc({
301
+ ...options.rsc,
295
302
  entries: {
296
303
  client: LITZ_BROWSER_ENTRY_ID,
297
304
  rsc: LITZ_RSC_ENTRY_ID
@@ -300,6 +307,12 @@ function litz(options = {}) {
300
307
  });
301
308
  const litzPlugin = {
302
309
  name: "litzjs/vite",
310
+ buildStart() {
311
+ if (this.environment?.name === "rsc") {
312
+ closeBundlePassCount = 0;
313
+ hasFinalizedServerArtifacts = false;
314
+ }
315
+ },
303
316
  config(userConfig) {
304
317
  const baseOutDir = userConfig.build?.outDir ?? "dist";
305
318
  return { environments: {
@@ -307,7 +320,14 @@ function litz(options = {}) {
307
320
  outDir: path.join(baseOutDir, "client"),
308
321
  manifest: true
309
322
  } },
310
- rsc: { build: { outDir: path.join(baseOutDir, "server") } },
323
+ rsc: { build: {
324
+ outDir: path.join(baseOutDir, "server"),
325
+ rollupOptions: { output: {
326
+ entryFileNames: "index.js",
327
+ format: "es",
328
+ codeSplitting: false
329
+ } }
330
+ } },
311
331
  ssr: { build: {
312
332
  outDir: path.join(baseOutDir, "server"),
313
333
  emptyOutDir: false
@@ -319,6 +339,12 @@ function litz(options = {}) {
319
339
  outputRootDir = path.resolve(root, config.build.outDir || "dist");
320
340
  clientOutDir = path.resolve(root, config.environments.client?.build.outDir || path.join("dist", "client"));
321
341
  serverOutDir = path.resolve(root, config.environments.rsc?.build.outDir || path.join("dist", "server"));
342
+ if (config.command === "build") {
343
+ const rscOutput = config.environments.rsc?.build.rollupOptions?.output;
344
+ const rscOutputs = Array.isArray(rscOutput) ? rscOutput : rscOutput ? [rscOutput] : [];
345
+ if (rscOutputs.length === 0) throw new Error("litz: could not find a rollupOptions.output entry for the RSC environment. This is an internal configuration error.");
346
+ for (const output of rscOutputs) if (output.codeSplitting !== false) throw new Error("litz: the RSC environment must have codeSplitting disabled (rollupOptions.output.codeSplitting: false). The server build pipeline requires a single entry file.");
347
+ }
322
348
  browserEntryPath = await discoverBrowserEntry(root);
323
349
  serverEntryPath = await discoverServerEntry(root, options.server);
324
350
  serverEntryFilePath = serverEntryPath ? path.resolve(root, serverEntryPath) : null;
@@ -373,26 +399,138 @@ export async function renderView(node, metadata = {}) {
373
399
  return null;
374
400
  },
375
401
  configureServer(server) {
376
- const refreshManifests = async (changedFile) => {
377
- if (changedFile && !/\.(ts|tsx)$/.test(changedFile)) return;
378
- const next = await discoverAllManifests(root, routePatterns, resourcePatterns, apiPatterns);
379
- const changed = JSON.stringify(routeManifest) !== JSON.stringify(next.routeManifest) || JSON.stringify(layoutManifest) !== JSON.stringify(next.layoutManifest) || JSON.stringify(resourceManifest) !== JSON.stringify(next.resourceManifest) || JSON.stringify(apiManifest) !== JSON.stringify(next.apiManifest);
380
- routeManifest = next.routeManifest;
381
- layoutManifest = next.layoutManifest;
382
- resourceManifest = next.resourceManifest;
383
- apiManifest = next.apiManifest;
384
- clientProjectedFiles = createClientProjectedFileSet(root, routeManifest, layoutManifest, resourceManifest, apiManifest);
385
- if (!changed) return;
386
- invalidateVirtualModule(server, RESOLVED_ROUTE_MANIFEST_ID);
387
- invalidateVirtualModule(server, RESOLVED_RESOURCE_MANIFEST_ID);
388
- server.ws.send({ type: "full-reload" });
402
+ const isRouteCandidate = picomatch(routePatterns);
403
+ const isResourceCandidate = picomatch(resourcePatterns);
404
+ const isApiCandidate = picomatch(apiPatterns);
405
+ const isManifestCandidate = (p) => isRouteCandidate(p) || isResourceCandidate(p) || isApiCandidate(p);
406
+ let debounceTimer = null;
407
+ let pendingFullDiscovery = false;
408
+ let manifestGeneration = 0;
409
+ const pendingRetry = /* @__PURE__ */ new Set();
410
+ const inFlightSingleFile = /* @__PURE__ */ new Set();
411
+ const flushManifestRefresh = async () => {
412
+ if (pendingFullDiscovery) {
413
+ pendingFullDiscovery = false;
414
+ manifestGeneration++;
415
+ const generation = manifestGeneration;
416
+ const next = await discoverAllManifests(root, routePatterns, resourcePatterns, apiPatterns);
417
+ if (manifestGeneration !== generation || pendingFullDiscovery) return;
418
+ const changed = JSON.stringify(routeManifest) !== JSON.stringify(next.routeManifest) || JSON.stringify(layoutManifest) !== JSON.stringify(next.layoutManifest) || JSON.stringify(resourceManifest) !== JSON.stringify(next.resourceManifest) || JSON.stringify(apiManifest) !== JSON.stringify(next.apiManifest);
419
+ routeManifest = next.routeManifest;
420
+ layoutManifest = next.layoutManifest;
421
+ resourceManifest = next.resourceManifest;
422
+ apiManifest = next.apiManifest;
423
+ clientProjectedFiles = createClientProjectedFileSet(root, routeManifest, layoutManifest, resourceManifest, apiManifest);
424
+ if (changed) {
425
+ invalidateVirtualModule(server, RESOLVED_ROUTE_MANIFEST_ID);
426
+ invalidateVirtualModule(server, RESOLVED_RESOURCE_MANIFEST_ID);
427
+ server.ws.send({ type: "full-reload" });
428
+ }
429
+ }
430
+ };
431
+ const scheduleRefresh = () => {
432
+ if (debounceTimer !== null) clearTimeout(debounceTimer);
433
+ debounceTimer = setTimeout(() => {
434
+ debounceTimer = null;
435
+ flushManifestRefresh().catch((err) => {
436
+ console.error("[litzjs] error during manifest discovery:", err);
437
+ });
438
+ }, 50);
439
+ };
440
+ const updateManifestEntry = (manifest, entry, file, sort) => {
441
+ const modulePath = normalizeRelativePath(root, file);
442
+ const idx = manifest.findIndex((r) => r.modulePath === modulePath);
443
+ if (entry && idx >= 0) {
444
+ if (JSON.stringify(manifest[idx]) !== JSON.stringify(entry)) {
445
+ const next = manifest.map((item, i) => i === idx ? entry : item);
446
+ return {
447
+ manifest: sort ? sort(next) : next,
448
+ changed: true
449
+ };
450
+ }
451
+ } else if (entry && idx < 0) {
452
+ const next = [...manifest, entry];
453
+ return {
454
+ manifest: sort ? sort(next) : next,
455
+ changed: true
456
+ };
457
+ } else if (!entry && idx >= 0) return {
458
+ manifest: manifest.filter((_, i) => i !== idx),
459
+ changed: true
460
+ };
461
+ return {
462
+ manifest,
463
+ changed: false
464
+ };
465
+ };
466
+ const refreshSingleFile = async (file) => {
467
+ if (pendingFullDiscovery) return;
468
+ const generation = manifestGeneration;
469
+ const relativePath = normalizeRelativePath(root, file);
470
+ let changed = false;
471
+ if (isRouteCandidate(relativePath)) {
472
+ const entry = await discoverRouteFromFile(root, file);
473
+ if (manifestGeneration !== generation) return;
474
+ const result = updateManifestEntry(routeManifest, entry, file, sortByPathSpecificity);
475
+ routeManifest = result.manifest;
476
+ changed = changed || result.changed;
477
+ const layoutEntry = await discoverLayoutFromFile(root, file);
478
+ if (manifestGeneration !== generation) return;
479
+ const layoutResult = updateManifestEntry(layoutManifest, layoutEntry, file);
480
+ layoutManifest = layoutResult.manifest;
481
+ changed = changed || layoutResult.changed;
482
+ }
483
+ if (isResourceCandidate(relativePath)) {
484
+ const entry = await discoverResourceFromFile(root, file);
485
+ if (manifestGeneration !== generation) return;
486
+ const result = updateManifestEntry(resourceManifest, entry, file);
487
+ resourceManifest = result.manifest;
488
+ changed = changed || result.changed;
489
+ }
490
+ if (isApiCandidate(relativePath)) {
491
+ const entry = await discoverApiRouteFromFile(root, file);
492
+ if (manifestGeneration !== generation) return;
493
+ const result = updateManifestEntry(apiManifest, entry, file, sortByPathSpecificity);
494
+ apiManifest = result.manifest;
495
+ changed = changed || result.changed;
496
+ }
497
+ if (changed) {
498
+ clientProjectedFiles = createClientProjectedFileSet(root, routeManifest, layoutManifest, resourceManifest, apiManifest);
499
+ invalidateVirtualModule(server, RESOLVED_ROUTE_MANIFEST_ID);
500
+ invalidateVirtualModule(server, RESOLVED_RESOURCE_MANIFEST_ID);
501
+ server.ws.send({ type: "full-reload" });
502
+ }
389
503
  };
390
- const onFsChange = (file) => {
391
- refreshManifests(file);
504
+ const runSingleFileRefresh = (file) => {
505
+ inFlightSingleFile.add(file);
506
+ refreshSingleFile(file).catch((err) => {
507
+ if (err?.code !== "ENOENT") console.error("[litzjs] error refreshing file manifest:", err);
508
+ }).finally(() => {
509
+ inFlightSingleFile.delete(file);
510
+ if (pendingRetry.has(file)) {
511
+ pendingRetry.delete(file);
512
+ if (!pendingFullDiscovery) runSingleFileRefresh(file);
513
+ }
514
+ });
515
+ };
516
+ const onFileAddOrUnlink = (file) => {
517
+ if (!/\.(ts|tsx)$/.test(file)) return;
518
+ if (!isManifestCandidate(normalizeRelativePath(root, file))) return;
519
+ pendingFullDiscovery = true;
520
+ scheduleRefresh();
521
+ };
522
+ const onFileChange = (file) => {
523
+ if (!/\.(ts|tsx)$/.test(file)) return;
524
+ if (!isManifestCandidate(normalizeRelativePath(root, file))) return;
525
+ if (inFlightSingleFile.has(file)) {
526
+ pendingRetry.add(file);
527
+ return;
528
+ }
529
+ runSingleFileRefresh(file);
392
530
  };
393
- server.watcher.on("add", onFsChange);
394
- server.watcher.on("change", onFsChange);
395
- server.watcher.on("unlink", onFsChange);
531
+ server.watcher.on("add", onFileAddOrUnlink);
532
+ server.watcher.on("change", onFileChange);
533
+ server.watcher.on("unlink", onFileAddOrUnlink);
396
534
  server.middlewares.use((request, response, next) => {
397
535
  handleLitzResourceRequest(server, resourceManifest, request, response, next);
398
536
  });
@@ -430,6 +568,7 @@ export async function renderView(node, metadata = {}) {
430
568
  };
431
569
  },
432
570
  async closeBundle() {
571
+ closeBundlePassCount++;
433
572
  await Promise.all([writeProductionIndexHtml(root, clientOutDir), removeLegacyBuildArtifacts(outputRootDir)]);
434
573
  if (hasFinalizedServerArtifacts) return;
435
574
  if (!hasRegisteredExitFinalizer) {
@@ -437,9 +576,15 @@ export async function renderView(node, metadata = {}) {
437
576
  process.once("exit", () => {
438
577
  if (hasFinalizedServerArtifacts) return;
439
578
  hasFinalizedServerArtifacts = finalizeServerArtifacts(serverOutDir, clientOutDir, options.embedAssets ?? false);
579
+ if (!hasFinalizedServerArtifacts) {
580
+ process.exitCode = 1;
581
+ console.error("litz: failed to finalize server artifacts. The assets manifest import could not be inlined — the production server bundle may be broken.");
582
+ }
440
583
  });
441
584
  }
442
- hasFinalizedServerArtifacts = finalizeServerArtifacts(serverOutDir, clientOutDir, options.embedAssets ?? false);
585
+ const inlineClientAssets = options.embedAssets ?? false;
586
+ if (inlineClientAssets && closeBundlePassCount <= 1) return;
587
+ hasFinalizedServerArtifacts = finalizeServerArtifacts(serverOutDir, clientOutDir, inlineClientAssets);
443
588
  }
444
589
  };
445
590
  return [...rscPlugins, litzPlugin];
@@ -595,6 +740,13 @@ function createClientProjectedFileSet(root, routes, layouts, resources, apiRoute
595
740
  ...apiRoutes
596
741
  ].map((entry) => path.resolve(root, entry.modulePath)));
597
742
  }
743
+ /**
744
+ * Build-time transform that injects the server manifest into the user's server
745
+ * entry. Uses the TypeScript compiler API to find all `createServer()` calls
746
+ * (imported from `litzjs/server`) and wraps their options argument with a
747
+ * helper that merges in the route/resource/API manifest. Returns `null` if no
748
+ * `createServer` import is found.
749
+ */
598
750
  function injectServerManifestIntoServerEntry(filePath, source) {
599
751
  const scriptKind = filePath.endsWith(".tsx") ? ts.ScriptKind.TSX : ts.ScriptKind.TS;
600
752
  const sourceFile = ts.createSourceFile(filePath, source, ts.ScriptTarget.Latest, true, scriptKind);
@@ -712,7 +864,9 @@ async function handleLitzResourceRequest(server, manifest, request, response, ne
712
864
  return;
713
865
  }
714
866
  const normalizedRequest = normalizeInternalResourceRequest(internalRequest, resourcePath, body.request, body.payload);
715
- const signal = new AbortController().signal;
867
+ const controller = new AbortController();
868
+ request.once("close", () => controller.abort());
869
+ const signal = controller.signal;
716
870
  await sendServerResult(server, response, await runDevMiddlewareChain({
717
871
  middleware: resource.middleware ?? [],
718
872
  request: normalizedRequest.request,
@@ -730,9 +884,10 @@ async function handleLitzResourceRequest(server, manifest, request, response, ne
730
884
  }), `${entry.path}#${operation}`);
731
885
  } catch (error) {
732
886
  server.ssrFixStacktrace(error);
887
+ console.error(error);
733
888
  sendLitzJson(response, 500, {
734
889
  kind: "fault",
735
- message: error instanceof Error ? error.message : "Resource request failed."
890
+ message: "Resource request failed."
736
891
  });
737
892
  }
738
893
  }
@@ -791,7 +946,9 @@ async function handleLitzRouteRequest(server, manifest, request, response, next)
791
946
  return;
792
947
  }
793
948
  const normalizedRequest = normalizeInternalResourceRequest(internalRequest, routePath, body.request, body.payload);
794
- const signal = new AbortController().signal;
949
+ const controller = new AbortController();
950
+ request.once("close", () => controller.abort());
951
+ const signal = controller.signal;
795
952
  await sendServerResult(server, response, await runDevMiddlewareChain({
796
953
  middleware: chain.slice(0, targetIndex + 1).flatMap((candidate) => candidate.middleware),
797
954
  request: normalizedRequest.request,
@@ -810,9 +967,10 @@ async function handleLitzRouteRequest(server, manifest, request, response, next)
810
967
  }), `${target.id}#${operation}`);
811
968
  } catch (error) {
812
969
  server.ssrFixStacktrace(error);
970
+ console.error(error);
813
971
  sendLitzJson(response, 500, {
814
972
  kind: "fault",
815
- message: error instanceof Error ? error.message : "Route request failed."
973
+ message: "Route request failed."
816
974
  });
817
975
  }
818
976
  }
@@ -868,7 +1026,9 @@ async function handleLitzApiRequest(server, manifest, request, response, next) {
868
1026
  return;
869
1027
  }
870
1028
  const apiRequest = await createNodeRequest(request, requestUrl);
871
- const signal = new AbortController().signal;
1029
+ const controller = new AbortController();
1030
+ request.once("close", () => controller.abort());
1031
+ const signal = controller.signal;
872
1032
  await writeFetchResponseToNode(response, await runDevMiddlewareChain({
873
1033
  middleware: api?.middleware ?? [],
874
1034
  request: apiRequest,
@@ -886,8 +1046,9 @@ async function handleLitzApiRequest(server, manifest, request, response, next) {
886
1046
  }));
887
1047
  } catch (error) {
888
1048
  server.ssrFixStacktrace(error);
1049
+ console.error(error);
889
1050
  response.statusCode = 500;
890
- response.end(error instanceof Error ? error.message : "API route failed.");
1051
+ response.end("API route failed.");
891
1052
  }
892
1053
  }
893
1054
  async function readRequestBuffer(request) {
@@ -1078,6 +1239,13 @@ function isDevLayout(value) {
1078
1239
  function findDevTargetRouteMatch(chain, targetId) {
1079
1240
  return chain.find((entry) => entry.id === targetId);
1080
1241
  }
1242
+ /**
1243
+ * Koa-style middleware dispatch. Recursively walks through `middleware` handlers
1244
+ * — each receives the current context and a `next()` function. Calling `next()`
1245
+ * invokes the next handler in the chain; the final handler calls `execute()`.
1246
+ * A guard prevents `next()` from being called more than once per handler.
1247
+ * Handlers can override the context for downstream middleware via `next({ context })`.
1248
+ */
1081
1249
  async function runDevMiddlewareChain(options) {
1082
1250
  const dispatch = async (index, context) => {
1083
1251
  const middleware = options.middleware[index];
@@ -1123,15 +1291,25 @@ async function readJson(filePath) {
1123
1291
  async function removeLegacyBuildArtifacts(outputRootDir) {
1124
1292
  await rm(path.join(outputRootDir, "index.html"), { force: true });
1125
1293
  }
1294
+ /**
1295
+ * Post-build finalization pipeline:
1296
+ * 1. Read the single-file RSC bundle (`index.js`) and assets manifest
1297
+ * 2. Inline the manifest by replacing its import with the manifest source
1298
+ * 3. Wrap in a server module (or an inline-asset wrapper if `embedAssets`)
1299
+ * 4. Write the final `index.js` and clean up RSC plugin artifacts
1300
+ *
1301
+ * Returns `false` if the manifest import replacement doesn't match — this
1302
+ * signals that the RSC plugin hasn't written its artifacts yet, and the
1303
+ * caller should retry (see the `closeBundle` hook).
1304
+ */
1126
1305
  function finalizeServerArtifacts(serverOutDir, clientOutDir, inlineClientAssets) {
1127
- const rscIndexPath = path.join(serverOutDir, "index.js");
1128
- const rscAssetsManifestPath = path.join(serverOutDir, "__vite_rsc_assets_manifest.js");
1129
1306
  let rscEntrySource;
1130
1307
  let rscAssetsManifestSource;
1131
1308
  let documentHtml = "";
1132
1309
  let clientAssets = [];
1133
1310
  try {
1134
- [rscEntrySource, rscAssetsManifestSource] = [readFileSync(rscIndexPath, "utf8"), readFileSync(rscAssetsManifestPath, "utf8")];
1311
+ rscEntrySource = readFileSync(path.join(serverOutDir, "index.js"), "utf8");
1312
+ rscAssetsManifestSource = readFileSync(path.join(serverOutDir, "__vite_rsc_assets_manifest.js"), "utf8");
1135
1313
  if (inlineClientAssets) {
1136
1314
  documentHtml = readFileSync(path.join(clientOutDir, "index.html"), "utf8");
1137
1315
  clientAssets = collectEmbeddedClientAssets(clientOutDir);
@@ -1140,36 +1318,25 @@ function finalizeServerArtifacts(serverOutDir, clientOutDir, inlineClientAssets)
1140
1318
  return false;
1141
1319
  }
1142
1320
  const manifestBindingSource = rscAssetsManifestSource.replace(/^export default\s+/, "const assetsManifest = ");
1143
- const asyncLocalStorageShimSource = [
1144
- "class __LitzAsyncLocalStorage {",
1145
- " run(store, callback, ...args) {",
1146
- " const previousStore = this.store;",
1147
- " this.store = store;",
1148
- " try {",
1149
- " return callback(...args);",
1150
- " } finally {",
1151
- " this.store = previousStore;",
1152
- " }",
1153
- " }",
1154
- "",
1155
- " getStore() {",
1156
- " return this.store;",
1157
- " }",
1158
- "",
1159
- " enterWith(store) {",
1160
- " this.store = store;",
1161
- " }",
1162
- "}",
1163
- "",
1164
- "globalThis.AsyncLocalStorage ??= __LitzAsyncLocalStorage;"
1165
- ].join("\n");
1166
- const inlinedServerSource = rscEntrySource.replace("import assetsManifest from \"./__vite_rsc_assets_manifest.js\";", manifestBindingSource).replace(/import \* as __viteRscAsyncHooks from "node:async_hooks";\s*/g, "").replace(/const __viteRscAsyncHooks = require\("node:async_hooks"\);\s*/g, "").replace(/globalThis\.AsyncLocalStorage = __viteRscAsyncHooks\.AsyncLocalStorage;/g, asyncLocalStorageShimSource);
1167
- const bundledWrapperSource = bundleServerWrapper(serverOutDir, inlineClientAssets ? createInlineAssetServerWrapper(inlinedServerSource, documentHtml, clientAssets) : createServerModuleWrapper(inlinedServerSource));
1168
- if (!bundledWrapperSource) return false;
1169
- mkdirSync(serverOutDir, { recursive: true });
1170
- writeFileSync(path.join(serverOutDir, "index.js"), bundledWrapperSource, "utf8");
1321
+ if (manifestBindingSource === rscAssetsManifestSource) return false;
1322
+ const inlinedServerSource = rscEntrySource.replace("import assetsManifest from \"./__vite_rsc_assets_manifest.js\";", manifestBindingSource);
1323
+ if (inlinedServerSource === rscEntrySource) return false;
1324
+ let wrapperSource;
1325
+ try {
1326
+ wrapperSource = inlineClientAssets ? createInlineAssetServerWrapper(inlinedServerSource, documentHtml, clientAssets) : createServerModuleWrapper(inlinedServerSource);
1327
+ } catch {
1328
+ return false;
1329
+ }
1330
+ writeFileSync(path.join(serverOutDir, "index.js"), wrapperSource, "utf8");
1331
+ cleanupRscPluginArtifacts(serverOutDir);
1171
1332
  return true;
1172
1333
  }
1334
+ function cleanupRscPluginArtifacts(serverOutDir) {
1335
+ for (const entry of readdirSync(serverOutDir)) if (entry.startsWith("__vite_rsc_")) rmSync(path.join(serverOutDir, entry), {
1336
+ force: true,
1337
+ recursive: true
1338
+ });
1339
+ }
1173
1340
  function createServerModuleWrapper(serverModuleSource) {
1174
1341
  const { source, handlerName } = transformServerModuleSource(serverModuleSource);
1175
1342
  return [
@@ -1179,6 +1346,19 @@ function createServerModuleWrapper(serverModuleSource) {
1179
1346
  ""
1180
1347
  ].join("\n");
1181
1348
  }
1349
+ /**
1350
+ * Strips the default export from the bundled RSC server module and rebinds it
1351
+ * to `__litzjsServerHandler` so the wrapper can re-export it. Uses the
1352
+ * TypeScript compiler API to handle four export patterns:
1353
+ *
1354
+ * 1. Named export lists — `export { handler as default }` → extract the binding
1355
+ * 2. Export assignments — `export default expr` → `const __litzjsServerHandler = expr`
1356
+ * 3. Default function declarations — strip modifiers, keep name, set binding
1357
+ * 4. Default class declarations — same treatment as functions
1358
+ *
1359
+ * If no handler was directly declared (pattern 1), a final `const` binding is
1360
+ * appended from the recorded default expression.
1361
+ */
1182
1362
  function transformServerModuleSource(serverModuleSource) {
1183
1363
  const handlerName = "__litzjsServerHandler";
1184
1364
  const sourceFile = ts.createSourceFile("server/index.js", serverModuleSource, ts.ScriptTarget.Latest, true, ts.ScriptKind.JS);
@@ -1257,6 +1437,13 @@ function transformServerModuleSource(serverModuleSource) {
1257
1437
  handlerName
1258
1438
  };
1259
1439
  }
1440
+ /**
1441
+ * Generates a self-contained server module for edge runtimes. Embeds all client
1442
+ * assets (JS, CSS, images, fonts) and the document HTML as data literals. The
1443
+ * generated `handle(request)` function serves static assets directly, returns
1444
+ * `index.html` for document requests, and delegates everything else to the RSC
1445
+ * server handler.
1446
+ */
1260
1447
  function createInlineAssetServerWrapper(serverModuleSource, documentHtml, clientAssets) {
1261
1448
  const serializedClientAssets = JSON.stringify(clientAssets);
1262
1449
  const serializedDocumentHtml = JSON.stringify(documentHtml);
@@ -1334,78 +1521,6 @@ function createInlineAssetServerWrapper(serverModuleSource, documentHtml, client
1334
1521
  ""
1335
1522
  ].join("\n");
1336
1523
  }
1337
- function bundleServerWrapper(serverOutDir, wrapperSource) {
1338
- const wrapperEntryPath = path.join(serverOutDir, "__litzjs_wrapped_server_entry.mjs");
1339
- const bundleOutDir = path.join(serverOutDir, "__litzjs_bundle_out");
1340
- const bundleScript = `
1341
- const entryPath = process.env.LITZ_SERVER_ENTRY_PATH;
1342
- const outDir = process.env.LITZ_SERVER_OUT_DIR;
1343
-
1344
- if (!entryPath || !outDir) {
1345
- throw new Error("Missing Litz server bundle paths.");
1346
- }
1347
-
1348
- (async () => {
1349
- const { build } = await import("vite");
1350
-
1351
- await build({
1352
- appType: "custom",
1353
- configFile: false,
1354
- publicDir: false,
1355
- logLevel: "silent",
1356
- build: {
1357
- ssr: entryPath,
1358
- copyPublicDir: false,
1359
- emptyOutDir: false,
1360
- minify: false,
1361
- outDir,
1362
- target: "esnext",
1363
- write: true,
1364
- rollupOptions: {
1365
- output: {
1366
- entryFileNames: "index.js",
1367
- format: "es",
1368
- inlineDynamicImports: true,
1369
- },
1370
- },
1371
- },
1372
- });
1373
- })().catch((error) => {
1374
- console.error(error);
1375
- process.exit(1);
1376
- });
1377
- `;
1378
- writeFileSync(wrapperEntryPath, wrapperSource, "utf8");
1379
- rmSync(bundleOutDir, {
1380
- force: true,
1381
- recursive: true
1382
- });
1383
- mkdirSync(bundleOutDir, { recursive: true });
1384
- const result = spawnSync(process.execPath, ["-e", bundleScript], {
1385
- cwd: serverOutDir,
1386
- encoding: "utf8",
1387
- env: {
1388
- ...process.env,
1389
- LITZ_SERVER_ENTRY_PATH: wrapperEntryPath,
1390
- LITZ_SERVER_OUT_DIR: bundleOutDir
1391
- }
1392
- });
1393
- try {
1394
- if (result.status !== 0) {
1395
- const stderr = result.stderr.trim();
1396
- const stdout = result.stdout.trim();
1397
- const failureMessage = stderr || stdout || "Failed to bundle the production Litz server.";
1398
- throw new Error(failureMessage);
1399
- }
1400
- return readFileSync(path.join(bundleOutDir, "index.js"), "utf8");
1401
- } finally {
1402
- rmSync(wrapperEntryPath, { force: true });
1403
- rmSync(bundleOutDir, {
1404
- force: true,
1405
- recursive: true
1406
- });
1407
- }
1408
- }
1409
1524
  function collectEmbeddedClientAssets(clientOutDir) {
1410
1525
  const assets = [];
1411
1526
  walkClientAssets(clientOutDir, clientOutDir, assets);
@@ -1455,4 +1570,4 @@ function getContentType(filePath) {
1455
1570
  }
1456
1571
  }
1457
1572
  //#endregion
1458
- export { discoverServerEntry, litz, transformServerModuleSource };
1573
+ export { cleanupRscPluginArtifacts, discoverAllManifests, discoverApiRouteFromFile, discoverLayoutFromFile, discoverResourceFromFile, discoverRouteFromFile, discoverServerEntry, handleLitzApiRequest, handleLitzResourceRequest, handleLitzRouteRequest, litz, transformServerModuleSource };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "litzjs",
3
- "version": "0.0.1",
3
+ "version": "0.1.0",
4
4
  "description": "Client-first React framework with RSC support built on Vite",
5
5
  "keywords": [
6
6
  "framework",
@@ -63,11 +63,13 @@
63
63
  "fixture:build": "vite build --config fixtures/rsc-smoke/vite.config.ts"
64
64
  },
65
65
  "dependencies": {
66
+ "picomatch": "4.0.4",
66
67
  "tinyglobby": "0.2.15"
67
68
  },
68
69
  "devDependencies": {
69
70
  "@changesets/cli": "2.30.0",
70
71
  "@types/node": "25.5.0",
72
+ "@types/picomatch": "4.0.2",
71
73
  "@types/react": "19.2.14",
72
74
  "@types/react-dom": "19.2.3",
73
75
  "@vitejs/plugin-rsc": "0.5.21",
@@ -79,7 +81,8 @@
79
81
  "react": "19.2.4",
80
82
  "react-dom": "19.2.4",
81
83
  "tsdown": "0.21.4",
82
- "vite": "8.0.1"
84
+ "vite": "8.0.1",
85
+ "vite-plugin-pwa": "1.2.0"
83
86
  },
84
87
  "peerDependencies": {
85
88
  "@vitejs/plugin-rsc": "^0.5.21",