@agent-native/core 0.79.11 → 0.79.12

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.
@@ -133,7 +133,8 @@ export function addImmutableAssetRouteRulesForClientBuild(routeRules, clientDir,
133
133
  * `@agent-native/core/server`. This is the middle layer of the three-layer
134
134
  * inheritance model: app local > workspace core > framework default.
135
135
  */
136
- export function generateWorkerEntry(routes, pluginPaths, defaultPluginStems = [], actions = [], workspaceCore = null, immutableAssetPaths = [], builtAppBasePath = normalizeConfiguredAppBasePath()) {
136
+ export function generateWorkerEntry(routes, pluginPaths, defaultPluginStems = [], actions = [], workspaceCore = null, immutableAssetPaths = [], builtAppBasePath = normalizeConfiguredAppBasePath(), options = {}) {
137
+ const includeReactRouterSsr = options.includeReactRouterSsr ?? true;
137
138
  const routeImports = [];
138
139
  const routeRegistrations = [];
139
140
  for (let i = 0; i < routes.length; i++) {
@@ -230,8 +231,8 @@ export function generateWorkerEntry(routes, pluginPaths, defaultPluginStems = []
230
231
  return `
231
232
  // Auto-generated worker entry point for ${preset}
232
233
  import { H3, defineEventHandler, readBody, toResponse } from "h3";
233
- import { createRequestHandler } from "react-router";
234
- import * as serverBuild from "./server-build.js";
234
+ ${includeReactRouterSsr ? 'import { createRequestHandler } from "react-router";' : ""}
235
+ ${includeReactRouterSsr ? 'import * as serverBuild from "./server-build.js";' : ""}
235
236
 
236
237
  function normalizeAppBasePath(value) {
237
238
  if (!value || value === "/") return "";
@@ -547,6 +548,52 @@ function requestWithPathname(request, pathname) {
547
548
  return new Request(url, request);
548
549
  }
549
550
 
551
+ function isStaticAppShellRequest(request) {
552
+ if (request.method !== "GET" && request.method !== "HEAD") return false;
553
+ const p = stripAppBasePath(new URL(request.url).pathname);
554
+ if (
555
+ p.startsWith("/.well-known/") ||
556
+ p.startsWith("/_agent-native/") ||
557
+ isApiPath(p) ||
558
+ p === "/favicon.ico" ||
559
+ p === "/favicon.png" ||
560
+ /\\.\\w+$/.test(p)
561
+ ) {
562
+ return false;
563
+ }
564
+ return true;
565
+ }
566
+
567
+ async function fetchStaticAppShell(request, env) {
568
+ if (!env?.ASSETS || !isStaticAppShellRequest(request)) return null;
569
+ const basePath = getAppBasePath();
570
+ const p = stripAppBasePath(new URL(request.url).pathname);
571
+ const shellRequest = requestWithPathname(
572
+ requestWithMethod(request, "GET"),
573
+ "/index.html",
574
+ );
575
+ let response;
576
+ try {
577
+ response = await env.ASSETS.fetch(shellRequest);
578
+ } catch {
579
+ return null;
580
+ }
581
+ if (response.status === 404) return null;
582
+ if (request.method === "HEAD") {
583
+ return rewriteMountedResponse(
584
+ new Response(null, {
585
+ status: response.status,
586
+ statusText: response.statusText,
587
+ headers: response.headers,
588
+ }),
589
+ basePath,
590
+ p,
591
+ request,
592
+ );
593
+ }
594
+ return rewriteMountedResponse(response, basePath, p, request);
595
+ }
596
+
550
597
  // API route handlers
551
598
  ${routeImports.join("\n")}
552
599
 
@@ -602,7 +649,8 @@ ${routeRegistrations.join("\n")}
602
649
  // Register action routes (/_agent-native/actions/*)
603
650
  ${actionRegistrations.join("\n")}
604
651
 
605
- // SSR catch-all for React Router
652
+ ${includeReactRouterSsr
653
+ ? ` // SSR catch-all for React Router
606
654
  const rrHandler = createRequestHandler(() => serverBuild);
607
655
  app.all("/**", defineEventHandler(async (event) => {
608
656
  const basePath = getAppBasePath();
@@ -633,7 +681,8 @@ ${actionRegistrations.join("\n")}
633
681
  );
634
682
  }
635
683
  return rewriteMountedResponse(await rrHandler(request), basePath, p, request);
636
- }));
684
+ }));`
685
+ : ""}
637
686
 
638
687
  _handler = app.fetch.bind(app);
639
688
  return _handler;
@@ -672,11 +721,166 @@ export default {
672
721
  }
673
722
 
674
723
  const handler = await getHandler();
675
- return handler(requestWithMountedApiPrefixStripped(request));
724
+ const response = await handler(requestWithMountedApiPrefixStripped(request));
725
+ ${includeReactRouterSsr
726
+ ? " return response;"
727
+ : ` if (response.status === 404) {
728
+ const shellResponse = await fetchStaticAppShell(request, env);
729
+ if (shellResponse) return shellResponse;
730
+ }
731
+ return response;`}
676
732
  }
677
733
  };
678
734
  `;
679
735
  }
736
+ function escapeHtmlAttribute(value) {
737
+ return value
738
+ .replaceAll("&", "&amp;")
739
+ .replaceAll('"', "&quot;")
740
+ .replaceAll("<", "&lt;")
741
+ .replaceAll(">", "&gt;");
742
+ }
743
+ function findReactRouterManifest(distDir) {
744
+ const assetsDir = path.join(distDir, "assets");
745
+ const manifestFile = fs
746
+ .readdirSync(assetsDir)
747
+ .find((file) => /^manifest-[\w-]+\.js$/.test(file));
748
+ if (!manifestFile) {
749
+ throw new Error(`React Router client manifest not found in ${assetsDir}`);
750
+ }
751
+ const source = fs.readFileSync(path.join(assetsDir, manifestFile), "utf8");
752
+ const match = source.match(/^window\.__reactRouterManifest=(.*);?\s*$/);
753
+ if (!match) {
754
+ throw new Error(`Could not parse React Router manifest ${manifestFile}`);
755
+ }
756
+ return JSON.parse(match[1].replace(/;$/, ""));
757
+ }
758
+ function collectModulePreloads(manifest, route) {
759
+ const paths = new Set();
760
+ const add = (value) => {
761
+ if (value)
762
+ paths.add(value);
763
+ };
764
+ add(manifest.url);
765
+ add(manifest.entry.module);
766
+ manifest.entry.imports?.forEach(add);
767
+ add(route.module);
768
+ route.imports?.forEach(add);
769
+ add(route.clientActionModule);
770
+ add(route.clientLoaderModule);
771
+ add(route.clientMiddlewareModule);
772
+ add(route.hydrateFallbackModule);
773
+ return [...paths];
774
+ }
775
+ function collectStylesheetLinks(manifest, route) {
776
+ return [...new Set([...(manifest.entry.css ?? []), ...(route.css ?? [])])];
777
+ }
778
+ function generateRouteModuleImportScript(manifest, route) {
779
+ const modules = [
780
+ ["route0", route.module],
781
+ ["route0_clientAction", route.clientActionModule],
782
+ ["route0_clientLoader", route.clientLoaderModule],
783
+ ["route0_clientMiddleware", route.clientMiddlewareModule],
784
+ ["route0_hydrateFallback", route.hydrateFallbackModule],
785
+ ];
786
+ const imports = modules
787
+ .filter(([, modulePath]) => modulePath)
788
+ .map(([name, modulePath]) => `import * as ${name} from ${JSON.stringify(modulePath)};`);
789
+ const parts = modules
790
+ .filter(([, modulePath]) => modulePath)
791
+ .map(([name]) => `...${name}`);
792
+ return [
793
+ `import ${JSON.stringify(manifest.url)};`,
794
+ ...imports,
795
+ `window.__reactRouterRouteModules = {${JSON.stringify(route.id)}:{${parts.join(",")}}};`,
796
+ `import(${JSON.stringify(manifest.entry.module)});`,
797
+ ].join("\n");
798
+ }
799
+ const EMPTY_REACT_ROUTER_TURBO_STREAM = '[{"_1":2,"_3":-5,"_4":-5},"loaderData",{},"actionData","errors"]\n';
800
+ // Manifest fallbacks cannot execute server loaders, so root loaders get the
801
+ // framework's default locale shape to keep hydration from reading undefined.
802
+ const DEFAULT_ROOT_LOADER_REACT_ROUTER_TURBO_STREAM = '[{"_1":2,"_3":-5,"_4":-5},"loaderData",{"_5":6},"actionData","errors","root",{"_7":8,"_9":10,"_11":12,"_13":14},"locale","en-US","preference",{"_7":15},"dir","ltr","messages",{},"system"]\n';
803
+ export function generateCloudflarePagesStaticShellFromManifest(manifest, basePath = normalizeConfiguredAppBasePath()) {
804
+ const rootRoute = manifest.routes.root;
805
+ if (!rootRoute) {
806
+ throw new Error("React Router manifest is missing the root route");
807
+ }
808
+ const modulePreloads = collectModulePreloads(manifest, rootRoute)
809
+ .map((href) => `<link rel="modulepreload" href="${escapeHtmlAttribute(href)}"/>`)
810
+ .join("");
811
+ const stylesheets = collectStylesheetLinks(manifest, rootRoute)
812
+ .map((href) => `<link rel="stylesheet" href="${escapeHtmlAttribute(href)}"/>`)
813
+ .join("");
814
+ const routeModuleScript = generateRouteModuleImportScript(manifest, rootRoute);
815
+ const context = {
816
+ basename: basePath || "/",
817
+ future: { unstable_optimizeDeps: false },
818
+ routeDiscovery: { mode: "initial" },
819
+ ssr: true,
820
+ isSpaMode: true,
821
+ };
822
+ const encodedInitialState = rootRoute.hasLoader
823
+ ? DEFAULT_ROOT_LOADER_REACT_ROUTER_TURBO_STREAM
824
+ : EMPTY_REACT_ROUTER_TURBO_STREAM;
825
+ return `<!DOCTYPE html><html lang="en"><head><meta charSet="utf-8"/><meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no"/><link rel="manifest" href="/manifest.json"/><link rel="icon" type="image/svg+xml" href="/favicon.svg"/>${modulePreloads}${stylesheets}</head><body><div style="display:flex;align-items:center;justify-content:center;height:100vh;width:100%"><svg role="status" aria-label="Loading" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="animation:an-spin 1s linear infinite;opacity:0.7"><path d="M21 12a9 9 0 1 1-6.219-8.56"></path></svg><style>@keyframes an-spin { to { transform: rotate(360deg) } } @media (prefers-color-scheme: dark) { html { background: #09090b; color: #fafafa } }</style></div><script>window.__reactRouterContext = ${JSON.stringify(context)};window.__reactRouterContext.stream = new ReadableStream({start(controller){window.__reactRouterContext.streamController = controller;}}).pipeThrough(new TextEncoderStream());</script><script type="module" async="">${routeModuleScript}</script><!--$--><script>window.__reactRouterContext.streamController.enqueue(${JSON.stringify(encodedInitialState)});</script><!--$--><script>window.__reactRouterContext.streamController.close();</script><!--/$--><!--/$--></body></html>`;
826
+ }
827
+ function writeCloudflarePagesStaticShell({ serverDir, distDir, tmpDir, }) {
828
+ const serverEntry = path.join(serverDir, "index.js");
829
+ if (!fs.existsSync(serverEntry)) {
830
+ throw new Error(`React Router server build not found at ${serverEntry}`);
831
+ }
832
+ const outFile = path.join(distDir, "index.html");
833
+ const renderScript = path.join(tmpDir, "render-cloudflare-static-shell.mjs");
834
+ const basePath = normalizeConfiguredAppBasePath();
835
+ fs.writeFileSync(renderScript, `
836
+ import fs from "node:fs";
837
+ import { createRequire } from "node:module";
838
+ import { pathToFileURL } from "node:url";
839
+
840
+ const cwd = ${JSON.stringify(cwd)};
841
+ const serverEntry = ${JSON.stringify(serverEntry)};
842
+ const outFile = ${JSON.stringify(outFile)};
843
+ const basePath = ${JSON.stringify(basePath)};
844
+
845
+ const requireFromApp = createRequire(cwd + "/package.json");
846
+ const reactRouterEntry = requireFromApp.resolve("react-router");
847
+ const { createRequestHandler } = await import(pathToFileURL(reactRouterEntry).href);
848
+ const serverBuild = await import(pathToFileURL(serverEntry).href);
849
+ const handler = createRequestHandler(serverBuild, "production");
850
+ const pathname = basePath ? basePath + "/" : "/";
851
+ const response = await handler(
852
+ new Request(new URL(pathname, "https://agent-native.local"), {
853
+ headers: { "X-React-Router-SPA-Mode": "yes" },
854
+ }),
855
+ );
856
+ const html = await response.text();
857
+
858
+ if (!html || !html.includes("__reactRouterContext") || !html.includes("entry.client")) {
859
+ throw new Error("React Router did not render a usable Cloudflare Pages static shell");
860
+ }
861
+
862
+ fs.writeFileSync(outFile, html);
863
+ process.exit(0);
864
+ `);
865
+ try {
866
+ execFileSync(process.execPath, [renderScript], {
867
+ cwd,
868
+ env: {
869
+ ...process.env,
870
+ NODE_ENV: process.env.NODE_ENV || "production",
871
+ IS_RR_BUILD_REQUEST: "yes",
872
+ },
873
+ stdio: "inherit",
874
+ });
875
+ console.log("[deploy] Wrote Cloudflare Pages static app shell.");
876
+ }
877
+ catch (error) {
878
+ const message = error instanceof Error ? error.message : String(error);
879
+ console.warn(`[deploy] React Router static shell render failed; using manifest fallback. ${message}`);
880
+ fs.writeFileSync(outFile, generateCloudflarePagesStaticShellFromManifest(findReactRouterManifest(distDir), basePath));
881
+ console.log("[deploy] Wrote Cloudflare Pages static app shell fallback.");
882
+ }
883
+ }
680
884
  /**
681
885
  * Build for Cloudflare Pages.
682
886
  * Output structure:
@@ -702,6 +906,9 @@ async function buildCloudflarePages() {
702
906
  fs.mkdirSync(distDir, { recursive: true });
703
907
  // Copy client assets to dist/
704
908
  copyDir(clientDir, distDir);
909
+ const tmpDir = path.join(cwd, ".deploy-tmp");
910
+ fs.mkdirSync(tmpDir, { recursive: true });
911
+ writeCloudflarePagesStaticShell({ serverDir, distDir, tmpDir });
705
912
  // Exclude _worker.js from being served as a public asset
706
913
  fs.writeFileSync(path.join(distDir, ".assetsignore"), "_worker.js\n");
707
914
  // Write a package.json inside _worker.js/ to tell wrangler this is a
@@ -718,30 +925,35 @@ async function buildCloudflarePages() {
718
925
  const actions = await discoverActionFiles(cwd);
719
926
  const missingDefaults = await getMissingDefaultPlugins(cwd);
720
927
  const workspaceCore = await getWorkspaceCoreExports(cwd);
928
+ const includeReactRouterSsr = false;
721
929
  const workspaceSlotCount = workspaceCore
722
930
  ? Object.keys(workspaceCore.plugins).length
723
931
  : 0;
724
932
  console.log(`[deploy] ${routes.length} API routes, ${actions.length} actions, ${plugins.length} plugins (${plugins.filter((p) => isNodeOnlyPlugin(p)).length} skipped as Node-only), ${missingDefaults.length} auto-mounted defaults${workspaceCore ? `, workspace-core ${workspaceCore.packageName} (${workspaceSlotCount} plugin slots)` : ""}`);
725
933
  // Generate the worker entry
726
934
  const immutableAssetPaths = collectImmutableAssetPaths(clientDir);
727
- const entrySource = generateWorkerEntry(routes, plugins, missingDefaults, actions, workspaceCore, immutableAssetPaths);
935
+ const entrySource = generateWorkerEntry(routes, plugins, missingDefaults, actions, workspaceCore, immutableAssetPaths, normalizeConfiguredAppBasePath(), { includeReactRouterSsr });
728
936
  // Create _worker.js output directory
729
937
  const workerOutDir = path.join(distDir, "_worker.js");
730
938
  fs.mkdirSync(workerOutDir, { recursive: true });
731
939
  // Write the worker entry
732
940
  const entryFile = path.join(workerOutDir, "index.js");
733
- // Rewrite the server-build import to point at the copied files
734
- const adjustedEntry = entrySource.replace(`import * as serverBuild from "./server-build.js";`, `import * as serverBuild from "./server/index.js";`);
941
+ // Rewrite the server-build import to point at the copied files when this
942
+ // worker intentionally includes React Router SSR.
943
+ const adjustedEntry = includeReactRouterSsr
944
+ ? entrySource.replace(`import * as serverBuild from "./server-build.js";`, `import * as serverBuild from "./server/index.js";`)
945
+ : entrySource;
735
946
  // Write a temp file for esbuild to bundle everything into a single worker entry.
736
- // The server build (React Router SSR) is copied to tmp so esbuild can resolve it.
737
- const tmpDir = path.join(cwd, ".deploy-tmp");
738
- fs.mkdirSync(tmpDir, { recursive: true });
947
+ // When React Router SSR is enabled, the server build is copied to tmp so
948
+ // esbuild can resolve it. Cloudflare Pages currently uses a static app shell
949
+ // instead so the worker stays under the platform bundle size limit.
739
950
  // Name the entry "index.js" so esbuild outputs index.js in the outdir,
740
951
  // matching the _worker.js/index.js entry point that Cloudflare Pages expects.
741
952
  const tmpEntry = path.join(tmpDir, "index.js");
742
953
  fs.writeFileSync(tmpEntry, adjustedEntry);
743
- // Copy server build files so esbuild can resolve the import
744
- copyDir(serverDir, path.join(tmpDir, "server"));
954
+ if (includeReactRouterSsr) {
955
+ copyDir(serverDir, path.join(tmpDir, "server"));
956
+ }
745
957
  // Create a require shim so CJS require("fs") calls resolve via ESM imports.
746
958
  // This is injected via esbuild --inject to replace its broken __require shim.
747
959
  fs.writeFileSync(path.join(tmpDir, "_require-shim.js"), generateRequireShim());