@react-router/dev 0.0.0-experimental-fbbd4fd81 → 0.0.0-experimental-cb25a21e1

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.
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @react-router/dev v0.0.0-experimental-fbbd4fd81
2
+ * @react-router/dev v0.0.0-experimental-cb25a21e1
3
3
  *
4
4
  * Copyright (c) Remix Software Inc.
5
5
  *
@@ -128,7 +128,7 @@ async function generateEntry(entry, reactRouterRoot, flags = {}) {
128
128
  }
129
129
  let defaultsDirectory = path__namespace.resolve(__dirname, "..", "config", "defaults");
130
130
  let defaultEntryClient = path__namespace.resolve(defaultsDirectory, "entry.client.tsx");
131
- let defaultEntryServer = path__namespace.resolve(defaultsDirectory, (ctx === null || ctx === void 0 ? void 0 : ctx.reactRouterConfig.ssr) === false && (ctx === null || ctx === void 0 ? void 0 : ctx.reactRouterConfig.future.unstable_singleFetch) !== true ? `entry.server.spa.tsx` : `entry.server.${serverRuntime}.tsx`);
131
+ let defaultEntryServer = path__namespace.resolve(defaultsDirectory, (ctx === null || ctx === void 0 ? void 0 : ctx.reactRouterConfig.ssr) === false ? `entry.server.spa.tsx` : `entry.server.${serverRuntime}.tsx`);
132
132
  let isServerEntry = entry === "entry.server";
133
133
  let contents = isServerEntry ? await createServerEntry(rootDirectory, appDirectory, defaultEntryServer) : await createClientEntry(rootDirectory, appDirectory, defaultEntryClient);
134
134
  let useTypeScript = flags.typescript ?? true;
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @react-router/dev v0.0.0-experimental-fbbd4fd81
2
+ * @react-router/dev v0.0.0-experimental-cb25a21e1
3
3
  *
4
4
  * Copyright (c) Remix Software Inc.
5
5
  *
package/dist/cli/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @react-router/dev v0.0.0-experimental-fbbd4fd81
2
+ * @react-router/dev v0.0.0-experimental-cb25a21e1
3
3
  *
4
4
  * Copyright (c) Remix Software Inc.
5
5
  *
package/dist/cli/run.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @react-router/dev v0.0.0-experimental-fbbd4fd81
2
+ * @react-router/dev v0.0.0-experimental-cb25a21e1
3
3
  *
4
4
  * Copyright (c) Remix Software Inc.
5
5
  *
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @react-router/dev v0.0.0-experimental-fbbd4fd81
2
+ * @react-router/dev v0.0.0-experimental-cb25a21e1
3
3
  *
4
4
  * Copyright (c) Remix Software Inc.
5
5
  *
package/dist/cli.js CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * @react-router/dev v0.0.0-experimental-fbbd4fd81
3
+ * @react-router/dev v0.0.0-experimental-cb25a21e1
4
4
  *
5
5
  * Copyright (c) Remix Software Inc.
6
6
  *
package/dist/colors.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @react-router/dev v0.0.0-experimental-fbbd4fd81
2
+ * @react-router/dev v0.0.0-experimental-cb25a21e1
3
3
  *
4
4
  * Copyright (c) Remix Software Inc.
5
5
  *
@@ -1,20 +1,73 @@
1
- import type { EntryContext } from "@react-router/node";
1
+ import { PassThrough } from "node:stream";
2
+
3
+ import type { AppLoadContext, EntryContext } from "@react-router/node";
4
+ import { createReadableStreamFromReadable } from "@react-router/node";
2
5
  import { RemixServer } from "react-router";
3
- import * as React from "react";
4
- import { renderToString } from "react-dom/server";
6
+ import { renderToPipeableStream } from "react-dom/server";
7
+
8
+ const ABORT_DELAY = 5_000;
5
9
 
6
10
  export default function handleRequest(
7
11
  request: Request,
8
12
  responseStatusCode: number,
9
13
  responseHeaders: Headers,
10
- remixContext: EntryContext
14
+ remixContext: EntryContext,
15
+ loadContext: AppLoadContext
11
16
  ) {
12
- let html = renderToString(
13
- <RemixServer context={remixContext} url={request.url} />
17
+ return handleBotRequest(
18
+ request,
19
+ responseStatusCode,
20
+ responseHeaders,
21
+ remixContext
14
22
  );
15
- html = "<!DOCTYPE html>\n" + html;
16
- return new Response(html, {
17
- headers: { "Content-Type": "text/html" },
18
- status: responseStatusCode,
23
+ }
24
+
25
+ function handleBotRequest(
26
+ request: Request,
27
+ responseStatusCode: number,
28
+ responseHeaders: Headers,
29
+ remixContext: EntryContext
30
+ ) {
31
+ return new Promise((resolve, reject) => {
32
+ let shellRendered = false;
33
+ const { pipe, abort } = renderToPipeableStream(
34
+ <RemixServer
35
+ context={remixContext}
36
+ url={request.url}
37
+ abortDelay={ABORT_DELAY}
38
+ />,
39
+ {
40
+ onAllReady() {
41
+ shellRendered = true;
42
+ const body = new PassThrough();
43
+ const stream = createReadableStreamFromReadable(body);
44
+
45
+ responseHeaders.set("Content-Type", "text/html");
46
+
47
+ resolve(
48
+ new Response(stream, {
49
+ headers: responseHeaders,
50
+ status: responseStatusCode,
51
+ })
52
+ );
53
+
54
+ pipe(body);
55
+ },
56
+ onShellError(error: unknown) {
57
+ reject(error);
58
+ },
59
+ onError(error: unknown) {
60
+ responseStatusCode = 500;
61
+ // Log streaming rendering errors from inside the shell. Don't log
62
+ // errors encountered during initial shell rendering since they'll
63
+ // reject and get logged in handleDocumentRequest.
64
+ if (shellRendered) {
65
+ console.error(error);
66
+ }
67
+ },
68
+ }
69
+ );
70
+
71
+ setTimeout(abort, ABORT_DELAY);
19
72
  });
20
73
  }
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @react-router/dev v0.0.0-experimental-fbbd4fd81
2
+ * @react-router/dev v0.0.0-experimental-cb25a21e1
3
3
  *
4
4
  * Copyright (c) Remix Software Inc.
5
5
  *
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @react-router/dev v0.0.0-experimental-fbbd4fd81
2
+ * @react-router/dev v0.0.0-experimental-cb25a21e1
3
3
  *
4
4
  * Copyright (c) Remix Software Inc.
5
5
  *
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @react-router/dev v0.0.0-experimental-fbbd4fd81
2
+ * @react-router/dev v0.0.0-experimental-cb25a21e1
3
3
  *
4
4
  * Copyright (c) Remix Software Inc.
5
5
  *
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @react-router/dev v0.0.0-experimental-fbbd4fd81
2
+ * @react-router/dev v0.0.0-experimental-cb25a21e1
3
3
  *
4
4
  * Copyright (c) Remix Software Inc.
5
5
  *
package/dist/config.d.ts CHANGED
@@ -39,7 +39,6 @@ interface FutureConfig {
39
39
  v3_fetcherPersist: boolean;
40
40
  v3_relativeSplatPath: boolean;
41
41
  v3_throwAbortReason: boolean;
42
- unstable_singleFetch: boolean;
43
42
  }
44
43
  export type BuildManifest = DefaultBuildManifest | ServerBundlesBuildManifest;
45
44
  type BuildEndHook = (args: {
@@ -93,6 +92,11 @@ export type VitePluginConfig = {
93
92
  * Defaults to `false`.
94
93
  */
95
94
  manifest?: boolean;
95
+ /**
96
+ * An array of URLs to prerender to HTML files at build time. Can also be a
97
+ * function returning an array to dynamically generate URLs.
98
+ */
99
+ prerender?: Array<string> | (() => Array<string> | Promise<Array<string>>);
96
100
  /**
97
101
  * An array of React Router plugin config presets to ease integration with
98
102
  * other platforms and tools.
@@ -144,6 +148,10 @@ export type ResolvedVitePluginConfig = Readonly<{
144
148
  * Defaults to `false`.
145
149
  */
146
150
  manifest: boolean;
151
+ /**
152
+ * An array of URLs to prerender to HTML files at build time.
153
+ */
154
+ prerender: Array<string> | null;
147
155
  /**
148
156
  * Derived from Vite's `base` config
149
157
  * */
@@ -207,6 +215,10 @@ export declare function resolveReactRouterConfig({ rootDirectory, reactRouterUse
207
215
  * Defaults to `false`.
208
216
  */
209
217
  manifest: boolean;
218
+ /**
219
+ * An array of URLs to prerender to HTML files at build time.
220
+ */
221
+ prerender: string[] | null;
210
222
  /**
211
223
  * Derived from Vite's `base` config
212
224
  * */
package/dist/config.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @react-router/dev v0.0.0-experimental-fbbd4fd81
2
+ * @react-router/dev v0.0.0-experimental-cb25a21e1
3
3
  *
4
4
  * Copyright (c) Remix Software Inc.
5
5
  *
@@ -124,6 +124,7 @@ async function resolveReactRouterConfig({
124
124
  ignoredRouteFiles,
125
125
  manifest,
126
126
  routes: userRoutesFunction,
127
+ prerender: prerenderConfig,
127
128
  serverBuildFile,
128
129
  serverBundles,
129
130
  serverModuleFormat,
@@ -133,12 +134,21 @@ async function resolveReactRouterConfig({
133
134
  // Default values should be completely overridden by user/preset config, not merged
134
135
  ...mergeReactRouterConfig(...presets, reactRouterUserConfig)
135
136
  };
136
- let isSpaMode = !ssr;
137
137
  // Log warning for incompatible vite config flags
138
- if (isSpaMode && serverBundles) {
138
+ if (!ssr && serverBundles) {
139
139
  console.warn(colors__default["default"].yellow(colors__default["default"].bold("⚠️ SPA Mode: ") + "the `serverBundles` config is invalid with " + "`ssr:false` and will be ignored`"));
140
140
  serverBundles = undefined;
141
141
  }
142
+ let prerender = null;
143
+ if (prerenderConfig) {
144
+ if (Array.isArray(prerenderConfig)) {
145
+ prerender = prerenderConfig;
146
+ } else if (typeof prerenderConfig === "function") {
147
+ prerender = await prerenderConfig();
148
+ } else {
149
+ throw new Error("The `prerender` config must be an array of string paths, or a function " + "returning an array of string paths");
150
+ }
151
+ }
142
152
  let appDirectory = path__default["default"].resolve(rootDirectory, userAppDirectory || "app");
143
153
  let buildDirectory = path__default["default"].resolve(rootDirectory, userBuildDirectory);
144
154
  let publicPath = viteUserConfig.base ?? "/";
@@ -177,8 +187,7 @@ async function resolveReactRouterConfig({
177
187
  let future = {
178
188
  v3_fetcherPersist: (userFuture === null || userFuture === void 0 ? void 0 : userFuture.v3_fetcherPersist) === true,
179
189
  v3_relativeSplatPath: (userFuture === null || userFuture === void 0 ? void 0 : userFuture.v3_relativeSplatPath) === true,
180
- v3_throwAbortReason: (userFuture === null || userFuture === void 0 ? void 0 : userFuture.v3_throwAbortReason) === true,
181
- unstable_singleFetch: (userFuture === null || userFuture === void 0 ? void 0 : userFuture.unstable_singleFetch) === true
190
+ v3_throwAbortReason: (userFuture === null || userFuture === void 0 ? void 0 : userFuture.v3_throwAbortReason) === true
182
191
  };
183
192
  let reactRouterConfig = deepFreeze({
184
193
  appDirectory,
@@ -187,6 +196,7 @@ async function resolveReactRouterConfig({
187
196
  buildEnd,
188
197
  future,
189
198
  manifest,
199
+ prerender,
190
200
  publicPath,
191
201
  routes: routes$1,
192
202
  serverBuildFile,
@@ -210,7 +220,6 @@ async function resolveEntryFiles({
210
220
  appDirectory,
211
221
  future
212
222
  } = reactRouterConfig;
213
- let isSpaMode = !reactRouterConfig.ssr;
214
223
  let defaultsDirectory = path__default["default"].resolve(__dirname, "config", "defaults");
215
224
  let userEntryClientFile = findEntry(appDirectory, "entry.client");
216
225
  let userEntryServerFile = findEntry(appDirectory, "entry.server");
@@ -218,7 +227,7 @@ async function resolveEntryFiles({
218
227
  let entryClientFile = userEntryClientFile || "entry.client.tsx";
219
228
  let pkgJson = await PackageJson__default["default"].load(rootDirectory);
220
229
  let deps = pkgJson.content.dependencies ?? {};
221
- if (isSpaMode && (future === null || future === void 0 ? void 0 : future.unstable_singleFetch) != true) {
230
+ if (!reactRouterConfig.ssr) {
222
231
  // This is a super-simple default since we don't need streaming in SPA Mode.
223
232
  // We can include this in a remix-spa template, but right now `npx remix reveal`
224
233
  // will still expose the streaming template since that command doesn't have
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @react-router/dev v0.0.0-experimental-fbbd4fd81
2
+ * @react-router/dev v0.0.0-experimental-cb25a21e1
3
3
  *
4
4
  * Copyright (c) Remix Software Inc.
5
5
  *
package/dist/invariant.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @react-router/dev v0.0.0-experimental-fbbd4fd81
2
+ * @react-router/dev v0.0.0-experimental-cb25a21e1
3
3
  *
4
4
  * Copyright (c) Remix Software Inc.
5
5
  *
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @react-router/dev v0.0.0-experimental-fbbd4fd81
2
+ * @react-router/dev v0.0.0-experimental-cb25a21e1
3
3
  *
4
4
  * Copyright (c) Remix Software Inc.
5
5
  *
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @react-router/dev v0.0.0-experimental-fbbd4fd81
2
+ * @react-router/dev v0.0.0-experimental-cb25a21e1
3
3
  *
4
4
  * Copyright (c) Remix Software Inc.
5
5
  *
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @react-router/dev v0.0.0-experimental-fbbd4fd81
2
+ * @react-router/dev v0.0.0-experimental-cb25a21e1
3
3
  *
4
4
  * Copyright (c) Remix Software Inc.
5
5
  *
package/dist/vite/dev.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @react-router/dev v0.0.0-experimental-fbbd4fd81
2
+ * @react-router/dev v0.0.0-experimental-cb25a21e1
3
3
  *
4
4
  * Copyright (c) Remix Software Inc.
5
5
  *
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @react-router/dev v0.0.0-experimental-fbbd4fd81
2
+ * @react-router/dev v0.0.0-experimental-cb25a21e1
3
3
  *
4
4
  * Copyright (c) Remix Software Inc.
5
5
  *
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @react-router/dev v0.0.0-experimental-fbbd4fd81
2
+ * @react-router/dev v0.0.0-experimental-cb25a21e1
3
3
  *
4
4
  * Copyright (c) Remix Software Inc.
5
5
  *
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @react-router/dev v0.0.0-experimental-fbbd4fd81
2
+ * @react-router/dev v0.0.0-experimental-cb25a21e1
3
3
  *
4
4
  * Copyright (c) Remix Software Inc.
5
5
  *
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @react-router/dev v0.0.0-experimental-fbbd4fd81
2
+ * @react-router/dev v0.0.0-experimental-cb25a21e1
3
3
  *
4
4
  * Copyright (c) Remix Software Inc.
5
5
  *
@@ -18,6 +18,7 @@ var url = require('node:url');
18
18
  var fse = require('fs-extra');
19
19
  var babel = require('@babel/core');
20
20
  var serverRuntime = require('@react-router/server-runtime');
21
+ var reactRouter = require('react-router');
21
22
  var esModuleLexer = require('es-module-lexer');
22
23
  var jsesc = require('jsesc');
23
24
  var colors = require('picocolors');
@@ -107,9 +108,11 @@ async function loadPluginContext({
107
108
  }
108
109
  const SERVER_ONLY_ROUTE_EXPORTS = ["loader", "action", "headers"];
109
110
  const CLIENT_ROUTE_EXPORTS = ["clientAction", "clientLoader", "default", "ErrorBoundary", "handle", "HydrateFallback", "Layout", "links", "meta", "shouldRevalidate"];
110
- // The "=1" suffix ensures client route requests can be processed before hitting
111
- // the Vite plugin since "?client-route" can be serialized as "?client-route="
112
- const CLIENT_ROUTE_QUERY_STRING = "?client-route=1";
111
+ /** This is used to manage a build optimization to remove unused route exports
112
+ from the client build output. This is important in cases where custom route
113
+ exports are only ever used on the server. Without this optimization we can't
114
+ tree-shake any unused custom exports because routes are entry points. */
115
+ const BUILD_CLIENT_ROUTE_QUERY_STRING = "?__remix-build-client-route";
113
116
  let serverBuildId = vmod.id("server-build");
114
117
  let serverManifestId = vmod.id("server-manifest");
115
118
  let browserManifestId = vmod.id("browser-manifest");
@@ -134,13 +137,10 @@ const getHash = (source, maxLength) => {
134
137
  let hash = node_crypto.createHash("sha256").update(source).digest("hex");
135
138
  return typeof maxLength === "number" ? hash.slice(0, maxLength) : hash;
136
139
  };
137
- const isClientRoute = id => {
138
- return id.endsWith(CLIENT_ROUTE_QUERY_STRING);
139
- };
140
140
  const resolveChunk = (ctx, viteManifest, absoluteFilePath) => {
141
141
  let vite = importViteEsmSync.importViteEsmSync();
142
142
  let rootRelativeFilePath = vite.normalizePath(path__namespace.relative(ctx.rootDirectory, absoluteFilePath));
143
- let entryChunk = viteManifest[rootRelativeFilePath + CLIENT_ROUTE_QUERY_STRING] ?? viteManifest[rootRelativeFilePath];
143
+ let entryChunk = viteManifest[rootRelativeFilePath + BUILD_CLIENT_ROUTE_QUERY_STRING] ?? viteManifest[rootRelativeFilePath];
144
144
  if (!entryChunk) {
145
145
  let knownManifestKeys = Object.keys(viteManifest).map(key => '"' + key + '"').join(", ");
146
146
  throw new Error(`No manifest entry found for "${rootRelativeFilePath}". Known manifest keys: ${knownManifestKeys}`);
@@ -324,7 +324,7 @@ const reactRouterVitePlugin = _config => {
324
324
  export const assetsBuildDirectory = ${JSON.stringify(path__namespace.relative(ctx.rootDirectory, getClientBuildDirectory(ctx.reactRouterConfig)))};
325
325
  export const basename = ${JSON.stringify(ctx.reactRouterConfig.basename)};
326
326
  export const future = ${JSON.stringify(ctx.reactRouterConfig.future)};
327
- export const isSpaMode = ${!ctx.reactRouterConfig.ssr};
327
+ export const isSpaMode = ${!ctx.reactRouterConfig.ssr && ctx.reactRouterConfig.prerender == null};
328
328
  export const publicPath = ${JSON.stringify(ctx.reactRouterConfig.publicPath)};
329
329
  export const entry = { module: entryServer };
330
330
  export const routes = {
@@ -432,7 +432,7 @@ const reactRouterVitePlugin = _config => {
432
432
  path: route.path,
433
433
  index: route.index,
434
434
  caseSensitive: route.caseSensitive,
435
- module: path__namespace.posix.join(ctx.reactRouterConfig.publicPath, `${resolveFileUrl.resolveFileUrl(ctx, resolveRelativeRouteFilePath(route, ctx.reactRouterConfig))}${CLIENT_ROUTE_QUERY_STRING}`),
435
+ module: path__namespace.posix.join(ctx.reactRouterConfig.publicPath, `${resolveFileUrl.resolveFileUrl(ctx, resolveRelativeRouteFilePath(route, ctx.reactRouterConfig))}`),
436
436
  hasAction: sourceExports.includes("action"),
437
437
  hasLoader: sourceExports.includes("loader"),
438
438
  hasClientAction: sourceExports.includes("clientAction"),
@@ -538,7 +538,7 @@ const reactRouterVitePlugin = _config => {
538
538
  rollupOptions: {
539
539
  ...baseRollupOptions,
540
540
  preserveEntrySignatures: "exports-only",
541
- input: [ctx.entryClientFilePath, ...Object.values(ctx.reactRouterConfig.routes).map(route => `${path__namespace.resolve(ctx.reactRouterConfig.appDirectory, route.file)}${CLIENT_ROUTE_QUERY_STRING}`)]
541
+ input: [ctx.entryClientFilePath, ...Object.values(ctx.reactRouterConfig.routes).map(route => `${path__namespace.resolve(ctx.reactRouterConfig.appDirectory, route.file)}${BUILD_CLIENT_ROUTE_QUERY_STRING}`)]
542
542
  }
543
543
  } : {
544
544
  // We move SSR-only assets to client assets. Note that the
@@ -635,8 +635,8 @@ const reactRouterVitePlugin = _config => {
635
635
  if (styles.isCssModulesFile(id)) {
636
636
  cssModulesManifest[id] = code;
637
637
  }
638
- if (isClientRoute(id)) {
639
- let routeModuleId = id.replace(CLIENT_ROUTE_QUERY_STRING, "");
638
+ if (id.endsWith(BUILD_CLIENT_ROUTE_QUERY_STRING)) {
639
+ let routeModuleId = id.replace(BUILD_CLIENT_ROUTE_QUERY_STRING, "");
640
640
  let sourceExports = await getRouteModuleExports(viteChildCompiler, ctx, routeModuleId);
641
641
  let routeFileName = path__namespace.basename(routeModuleId);
642
642
  let clientExports = sourceExports.filter(exportName => CLIENT_ROUTE_EXPORTS.includes(exportName)).join(", ");
@@ -748,8 +748,19 @@ const reactRouterVitePlugin = _config => {
748
748
  if (movedAssetPaths.length) {
749
749
  viteConfig.logger.info(["", `${colors__default["default"].green("✓")} ${movedAssetPaths.length} asset${movedAssetPaths.length > 1 ? "s" : ""} moved from React Router server build to client assets.`, ...movedAssetPaths.map(movedAssetPath => colors__default["default"].dim(path__namespace.relative(ctx.rootDirectory, movedAssetPath))), ""].join("\n"));
750
750
  }
751
+ if (ctx.reactRouterConfig.prerender != null) {
752
+ // If we have prerender routes, that takes precedence over SPA mode
753
+ // which is ssr:false and only the rot route being rendered
754
+ await handlePrerender(viteConfig, ctx.reactRouterConfig, serverBuildDirectory, clientBuildDirectory);
755
+ } else if (!ctx.reactRouterConfig.ssr) {
756
+ await handleSpaMode(viteConfig, ctx.reactRouterConfig, serverBuildDirectory, clientBuildDirectory);
757
+ }
758
+ // For both SPA mode and prerendering, we can remove the server builds
759
+ // if ssr:false is set
751
760
  if (!ctx.reactRouterConfig.ssr) {
752
- await handleSpaMode(serverBuildDirectory, ctx.reactRouterConfig.serverBuildFile, clientBuildDirectory, viteConfig, ctx.reactRouterConfig.basename);
761
+ // Cleanup - we no longer need the server build assets
762
+ viteConfig.logger.info(["Removing the server build in", colors__default["default"].green(serverBuildDirectory), "due to ssr:false"].join(" "));
763
+ fse__namespace.removeSync(serverBuildDirectory);
753
764
  }
754
765
  }
755
766
  },
@@ -902,11 +913,6 @@ const reactRouterVitePlugin = _config => {
902
913
  let isJSX = filepath.endsWith("x");
903
914
  let useFastRefresh = !ssr && (isJSX || code.includes(devRuntime));
904
915
  if (!useFastRefresh) return;
905
- if (isClientRoute(id)) {
906
- return {
907
- code: addRefreshWrapper(ctx.reactRouterConfig, code, id)
908
- };
909
- }
910
916
  let result = await babel__default["default"].transformAsync(code, {
911
917
  babelrc: false,
912
918
  configFile: false,
@@ -975,7 +981,7 @@ function isEqualJson(v1, v2) {
975
981
  }
976
982
  function addRefreshWrapper(reactRouterConfig, code, id) {
977
983
  let route = getRoute(reactRouterConfig, id);
978
- let acceptExports = route || isClientRoute(id) ? ["clientAction", "clientLoader", "handle", "meta", "links", "shouldRevalidate"] : [];
984
+ let acceptExports = route ? ["clientAction", "clientLoader", "handle", "meta", "links", "shouldRevalidate"] : [];
979
985
  return REACT_REFRESH_HEADER.replaceAll("__SOURCE__", JSON.stringify(id)) + code + REACT_REFRESH_FOOTER.replaceAll("__SOURCE__", JSON.stringify(id)).replaceAll("__ACCEPT_EXPORTS__", JSON.stringify(acceptExports)).replaceAll("__ROUTE_ID__", JSON.stringify(route === null || route === void 0 ? void 0 : route.id));
980
986
  }
981
987
  const REACT_REFRESH_HEADER = `
@@ -1039,29 +1045,125 @@ async function getRouteMetadata(ctx, viteChildCompiler, route, readRouteFile) {
1039
1045
  };
1040
1046
  return info;
1041
1047
  }
1042
- async function handleSpaMode(serverBuildDirectoryPath, serverBuildFile, clientBuildDirectory, viteConfig, basename) {
1043
- // Create a handler and call it for the `/` path - rendering down to the
1044
- // proper HydrateFallback ... or not! Maybe they have a static landing page
1045
- // generated from routes/_index.tsx.
1046
- let serverBuildPath = path__namespace.join(serverBuildDirectoryPath, serverBuildFile);
1048
+ async function getPrerenderBuildAndHandler(viteConfig, reactRouterConfig, serverBuildDirectory) {
1049
+ let serverBuildPath = path__namespace.join(serverBuildDirectory, reactRouterConfig.serverBuildFile);
1047
1050
  let build = await import(url__namespace.pathToFileURL(serverBuildPath).toString());
1048
1051
  let {
1049
1052
  createRequestHandler: createHandler
1050
1053
  } = await import('@react-router/node');
1051
- let handler = createHandler(build, viteConfig.mode);
1052
- let response = await handler(new Request(`http://localhost${basename}`));
1054
+ return {
1055
+ build,
1056
+ handler: createHandler(build, viteConfig.mode)
1057
+ };
1058
+ }
1059
+ async function handleSpaMode(viteConfig, reactRouterConfig, serverBuildDirectory, clientBuildDirectory) {
1060
+ let {
1061
+ handler
1062
+ } = await getPrerenderBuildAndHandler(viteConfig, reactRouterConfig, serverBuildDirectory);
1063
+ let request = new Request(`http://localhost${reactRouterConfig.basename}`);
1064
+ let response = await handler(request);
1053
1065
  let html = await response.text();
1066
+ validatePrerenderedResponse(response, html, "SPA Mode", "/");
1067
+ validatePrerenderedHtml(html, "SPA Mode");
1068
+ // Write out the index.html file for the SPA
1069
+ await fse__namespace.writeFile(path__namespace.join(clientBuildDirectory, "index.html"), html);
1070
+ viteConfig.logger.info("SPA Mode: index.html has been written to your " + colors__default["default"].bold(path__namespace.relative(process.cwd(), clientBuildDirectory)) + " directory");
1071
+ }
1072
+ async function handlePrerender(viteConfig, reactRouterConfig, serverBuildDirectory, clientBuildDirectory) {
1073
+ let {
1074
+ build,
1075
+ handler
1076
+ } = await getPrerenderBuildAndHandler(viteConfig, reactRouterConfig, serverBuildDirectory);
1077
+ let routes = createPrerenderRoutes(build.routes);
1078
+ let routesToPrerender = reactRouterConfig.prerender || ["/"];
1079
+ let requestInit = {
1080
+ headers: {
1081
+ // Header that can be used in the loader to know if you're running at
1082
+ // build time or runtime
1083
+ "X-React-Router-Prerender": "yes"
1084
+ }
1085
+ };
1086
+ for (let path of routesToPrerender) {
1087
+ var _matchRoutes;
1088
+ let hasLoaders = (_matchRoutes = reactRouter.matchRoutes(routes, path)) === null || _matchRoutes === void 0 ? void 0 : _matchRoutes.some(m => m.route.loader);
1089
+ if (hasLoaders) {
1090
+ await prerenderData(handler, path, clientBuildDirectory, reactRouterConfig, viteConfig, requestInit);
1091
+ }
1092
+ await prerenderRoute(handler, path, clientBuildDirectory, reactRouterConfig, viteConfig, requestInit);
1093
+ }
1094
+ async function prerenderData(handler, prerenderPath, clientBuildDirectory, reactRouterConfig, viteConfig, requestInit) {
1095
+ let normalizedPath = `${reactRouterConfig.basename}${prerenderPath === "/" ? "/_root.data" : `${prerenderPath.replace(/\/$/, "")}.data`}`.replace(/\/\/+/g, "/");
1096
+ let request = new Request(`http://localhost${normalizedPath}`, requestInit);
1097
+ let response = await handler(request);
1098
+ let data = await response.text();
1099
+ validatePrerenderedResponse(response, data, "Prerender", normalizedPath);
1100
+ // Write out the .data file
1101
+ let outdir = path__namespace.relative(process.cwd(), clientBuildDirectory);
1102
+ let outfile = path__namespace.join(outdir, normalizedPath.split("/").join(path__namespace.sep));
1103
+ await fse__namespace.ensureDir(path__namespace.dirname(outfile));
1104
+ await fse__namespace.outputFile(outfile, data);
1105
+ viteConfig.logger.info(`Prerender: Generated ${colors__default["default"].bold(outfile)}`);
1106
+ }
1107
+ }
1108
+ async function prerenderRoute(handler, prerenderPath, clientBuildDirectory, reactRouterConfig, viteConfig, requestInit) {
1109
+ let normalizedPath = `${reactRouterConfig.basename}${prerenderPath}/`.replace(/\/\/+/g, "/");
1110
+ let request = new Request(`http://localhost${normalizedPath}`, requestInit);
1111
+ let response = await handler(request);
1112
+ let html = await response.text();
1113
+ validatePrerenderedResponse(response, html, "Prerender", normalizedPath);
1114
+ if (!reactRouterConfig.ssr) {
1115
+ validatePrerenderedHtml(html, "Prerender");
1116
+ }
1117
+ // Write out the HTML file
1118
+ let outdir = path__namespace.relative(process.cwd(), clientBuildDirectory);
1119
+ let outfile = path__namespace.join(outdir, ...normalizedPath.split("/"), "index.html");
1120
+ await fse__namespace.ensureDir(path__namespace.dirname(outfile));
1121
+ await fse__namespace.outputFile(outfile, html);
1122
+ viteConfig.logger.info(`Prerender: Generated ${colors__default["default"].bold(outfile)}`);
1123
+ }
1124
+ function validatePrerenderedResponse(response, html, prefix, path) {
1054
1125
  if (response.status !== 200) {
1055
- throw new Error(`SPA Mode: Received a ${response.status} status code from ` + `\`entry.server.tsx\` while generating the \`index.html\` file.\n${html}`);
1126
+ throw new Error(`${prefix}: Received a ${response.status} status code from ` + `\`entry.server.tsx\` while prerendering the \`${path}\` ` + `path.\n${html}`);
1056
1127
  }
1128
+ }
1129
+ function validatePrerenderedHtml(html, prefix) {
1057
1130
  if (!html.includes("window.__remixContext =") || !html.includes("window.__remixRouteModules =")) {
1058
- throw new Error("SPA Mode: Did you forget to include <Scripts/> in your `root.tsx` " + "`HydrateFallback` component? Your `index.html` file cannot hydrate " + "into a SPA without `<Scripts />`.");
1131
+ throw new Error(`${prefix}: Did you forget to include <Scripts/> in your root route? ` + "Your pre-rendered HTML files cannot hydrate without `<Scripts />`.");
1059
1132
  }
1060
- // Write out the index.html file for the SPA
1061
- await fse__namespace.writeFile(path__namespace.join(clientBuildDirectory, "index.html"), html);
1062
- viteConfig.logger.info("SPA Mode: index.html has been written to your " + colors__default["default"].bold(path__namespace.relative(process.cwd(), clientBuildDirectory)) + " directory");
1063
- // Cleanup - we no longer need the server build assets
1064
- fse__namespace.removeSync(serverBuildDirectoryPath);
1133
+ }
1134
+ // Note: Duplicated from remix-server-runtime
1135
+ function groupRoutesByParentId(manifest) {
1136
+ let routes = {};
1137
+ Object.values(manifest).forEach(route => {
1138
+ let parentId = route.parentId || "";
1139
+ if (!routes[parentId]) {
1140
+ routes[parentId] = [];
1141
+ }
1142
+ routes[parentId].push(route);
1143
+ });
1144
+ return routes;
1145
+ }
1146
+ // Note: Duplicated from remix-server-runtime
1147
+ function createPrerenderRoutes(manifest, parentId = "", routesByParentId = groupRoutesByParentId(manifest)) {
1148
+ return (routesByParentId[parentId] || []).map(route => {
1149
+ let commonRoute = {
1150
+ // Always include root due to default boundaries
1151
+ hasErrorBoundary: route.id === "root" || route.module.ErrorBoundary != null,
1152
+ id: route.id,
1153
+ path: route.path,
1154
+ loader: route.module.loader ? () => null : undefined,
1155
+ action: undefined,
1156
+ handle: route.module.handle
1157
+ };
1158
+ return route.index ? {
1159
+ index: true,
1160
+ ...commonRoute
1161
+ } : {
1162
+ caseSensitive: route.caseSensitive,
1163
+ children: createPrerenderRoutes(manifest, route.id, routesByParentId),
1164
+ ...commonRoute
1165
+ };
1166
+ });
1065
1167
  }
1066
1168
 
1067
1169
  exports.extractPluginContext = extractPluginContext;
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @react-router/dev v0.0.0-experimental-fbbd4fd81
2
+ * @react-router/dev v0.0.0-experimental-cb25a21e1
3
3
  *
4
4
  * Copyright (c) Remix Software Inc.
5
5
  *
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @react-router/dev v0.0.0-experimental-fbbd4fd81
2
+ * @react-router/dev v0.0.0-experimental-cb25a21e1
3
3
  *
4
4
  * Copyright (c) Remix Software Inc.
5
5
  *
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @react-router/dev v0.0.0-experimental-fbbd4fd81
2
+ * @react-router/dev v0.0.0-experimental-cb25a21e1
3
3
  *
4
4
  * Copyright (c) Remix Software Inc.
5
5
  *
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @react-router/dev v0.0.0-experimental-fbbd4fd81
2
+ * @react-router/dev v0.0.0-experimental-cb25a21e1
3
3
  *
4
4
  * Copyright (c) Remix Software Inc.
5
5
  *
package/dist/vite/vmod.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @react-router/dev v0.0.0-experimental-fbbd4fd81
2
+ * @react-router/dev v0.0.0-experimental-cb25a21e1
3
3
  *
4
4
  * Copyright (c) Remix Software Inc.
5
5
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@react-router/dev",
3
- "version": "0.0.0-experimental-fbbd4fd81",
3
+ "version": "0.0.0-experimental-cb25a21e1",
4
4
  "description": "Dev tools and CLI for React Router",
5
5
  "homepage": "https://reactrouter.com",
6
6
  "bugs": {
@@ -42,9 +42,9 @@
42
42
  "react-refresh": "^0.14.0",
43
43
  "semver": "^7.3.7",
44
44
  "set-cookie-parser": "^2.6.0",
45
- "@react-router/node": "0.0.0-experimental-fbbd4fd81",
46
- "@react-router/server-runtime": "0.0.0-experimental-fbbd4fd81",
47
- "react-router": "0.0.0-experimental-fbbd4fd81"
45
+ "@react-router/node": "0.0.0-experimental-cb25a21e1",
46
+ "@react-router/server-runtime": "0.0.0-experimental-cb25a21e1",
47
+ "react-router": "0.0.0-experimental-cb25a21e1"
48
48
  },
49
49
  "devDependencies": {
50
50
  "@types/babel__core": "^7.20.5",
@@ -69,14 +69,14 @@
69
69
  "tiny-invariant": "^1.2.0",
70
70
  "vite": "^5.1.0",
71
71
  "wrangler": "^3.28.2",
72
- "@react-router/serve": "0.0.0-experimental-fbbd4fd81"
72
+ "@react-router/serve": "0.0.0-experimental-cb25a21e1"
73
73
  },
74
74
  "peerDependencies": {
75
75
  "typescript": "^5.1.0",
76
76
  "vite": "^5.1.0",
77
77
  "wrangler": "^3.28.2",
78
- "react-router": "^0.0.0-experimental-fbbd4fd81",
79
- "@react-router/serve": "^0.0.0-experimental-fbbd4fd81"
78
+ "react-router": "^0.0.0-experimental-cb25a21e1",
79
+ "@react-router/serve": "^0.0.0-experimental-cb25a21e1"
80
80
  },
81
81
  "peerDependenciesMeta": {
82
82
  "@react-router/serve": {