@reckona/mreact-router 0.0.165 → 0.0.167

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/src/actions.ts CHANGED
@@ -43,6 +43,8 @@ const formFieldModuleId = "__mreact_module_id";
43
43
  const formFieldExportName = "__mreact_export_name";
44
44
  const formFieldNonce = "__mreact_action_nonce";
45
45
  const formFieldActionToken = "__mreact_action_token";
46
+ const csrfTokenPlaceholder = "__mreact_action_csrf_placeholder__";
47
+ const actionNoncePlaceholder = "__mreact_action_nonce_placeholder__";
46
48
  const actionTokenSecret = process.env.MREACT_SERVER_ACTION_SECRET ?? defaultActionTokenSecret();
47
49
  // Bounded default replay store for form-action nonces. The previous
48
50
  // implementation was an unbounded Set that grew with every successful
@@ -175,6 +177,7 @@ export interface PreparedRouteActions {
175
177
  code: string;
176
178
  csrfToken?: string;
177
179
  diagnostics?: ServerActionInferenceDiagnostic[] | undefined;
180
+ htmlReplacements?: readonly (readonly [string, string])[] | undefined;
178
181
  // True when the cookie should be (re)set on the response. False means
179
182
  // the incoming request already carried a valid CSRF cookie that the
180
183
  // render is reusing -- skipping Set-Cookie avoids cookie thrash across
@@ -185,6 +188,7 @@ export interface PreparedRouteActions {
185
188
 
186
189
  interface ActionReference {
187
190
  exportName: string;
191
+ expression?: string | undefined;
188
192
  inferred: boolean;
189
193
  moduleId: string;
190
194
  }
@@ -210,6 +214,7 @@ export async function prepareRouteServerActions(options: {
210
214
  appDir: string;
211
215
  code: string;
212
216
  formActionReferences?: readonly PreparedFormActionReference[] | undefined;
217
+ placeholders?: boolean | undefined;
213
218
  pageFile: string;
214
219
  request?: Request | undefined;
215
220
  }): Promise<PreparedRouteActions> {
@@ -248,10 +253,13 @@ export async function prepareRouteServerActions(options: {
248
253
  const csrfToken = existingToken ?? randomUUID();
249
254
  const csrfTokenIsNew = existingToken === undefined;
250
255
  const actionNonce = randomUUID();
256
+ const usePlaceholders =
257
+ options.placeholders === true && canUseStableFormActionPlaceholders(references);
251
258
  const lowered = lowerFormActions({
252
259
  actionNonce,
253
260
  code: options.code,
254
261
  csrfToken,
262
+ placeholders: usePlaceholders,
255
263
  references,
256
264
  });
257
265
 
@@ -271,9 +279,45 @@ export async function prepareRouteServerActions(options: {
271
279
  csrfTokenIsNew,
272
280
  diagnostics,
273
281
  hasFormActions: true,
282
+ ...(usePlaceholders
283
+ ? { htmlReplacements: formActionHtmlReplacements({ actionNonce, csrfToken, references }) }
284
+ : {}),
274
285
  };
275
286
  }
276
287
 
288
+ export function prepareRouteServerActionPlaceholders(options: {
289
+ code: string;
290
+ formActionReferences: readonly PreparedFormActionReference[];
291
+ }): string {
292
+ const references = formActionReferenceMap(options.code, options.formActionReferences);
293
+
294
+ if (references.size === 0) {
295
+ return options.code;
296
+ }
297
+
298
+ if (!canUseStableFormActionPlaceholders(references)) {
299
+ return options.code;
300
+ }
301
+
302
+ return lowerFormActions({
303
+ actionNonce: actionNoncePlaceholder,
304
+ code: options.code,
305
+ csrfToken: csrfTokenPlaceholder,
306
+ placeholders: true,
307
+ references,
308
+ });
309
+ }
310
+
311
+ function canUseStableFormActionPlaceholders(
312
+ references: ReadonlyMap<string, ActionReference>,
313
+ ): boolean {
314
+ return [...references.values()].every(
315
+ (reference) =>
316
+ reference.expression === reference.exportName &&
317
+ /^[A-Za-z_$][\w$]*$/u.test(reference.expression),
318
+ );
319
+ }
320
+
277
321
  function formActionReferenceMap(
278
322
  code: string,
279
323
  references: readonly PreparedFormActionReference[],
@@ -287,6 +331,7 @@ function formActionReferenceMap(
287
331
  formActionOccurrenceKey(reference),
288
332
  {
289
333
  exportName: reference.exportName,
334
+ expression: reference.expression,
290
335
  inferred: reference.inferred,
291
336
  moduleId: reference.moduleId,
292
337
  },
@@ -820,6 +865,7 @@ function lowerFormActions(options: {
820
865
  actionNonce: string;
821
866
  code: string;
822
867
  csrfToken: string;
868
+ placeholders?: boolean | undefined;
823
869
  references: Map<string, ActionReference>;
824
870
  }): string {
825
871
  let code = options.code;
@@ -837,6 +883,7 @@ function lowerFormActions(options: {
837
883
  actionNonce: options.actionNonce,
838
884
  csrfToken: options.csrfToken,
839
885
  opening,
886
+ placeholders: options.placeholders === true,
840
887
  reference,
841
888
  referenceName: formReference.expression,
842
889
  });
@@ -851,6 +898,39 @@ function lowerFormActions(options: {
851
898
  return code;
852
899
  }
853
900
 
901
+ function formActionHtmlReplacements(options: {
902
+ actionNonce: string;
903
+ csrfToken: string;
904
+ references: ReadonlyMap<string, ActionReference>;
905
+ }): readonly (readonly [string, string])[] {
906
+ const replacements: Array<readonly [string, string]> = [
907
+ [csrfTokenPlaceholder, options.csrfToken],
908
+ [actionNoncePlaceholder, options.actionNonce],
909
+ ];
910
+ const seenTokens = new Set<string>();
911
+
912
+ for (const reference of options.references.values()) {
913
+ const placeholder = actionTokenPlaceholder(reference);
914
+
915
+ if (seenTokens.has(placeholder)) {
916
+ continue;
917
+ }
918
+
919
+ seenTokens.add(placeholder);
920
+ replacements.push([
921
+ placeholder,
922
+ formActionToken({
923
+ csrfToken: options.csrfToken,
924
+ exportName: reference.exportName,
925
+ moduleId: reference.moduleId,
926
+ nonce: options.actionNonce,
927
+ }),
928
+ ]);
929
+ }
930
+
931
+ return replacements;
932
+ }
933
+
854
934
  function formActionOccurrenceKey(reference: {
855
935
  end: number;
856
936
  expression: string;
@@ -875,6 +955,7 @@ function lowerFormActionOpening(options: {
875
955
  actionNonce: string;
876
956
  csrfToken: string;
877
957
  opening: string;
958
+ placeholders: boolean;
878
959
  reference: ActionReference;
879
960
  referenceName: string;
880
961
  }): string {
@@ -897,22 +978,39 @@ function lowerFormActionOpening(options: {
897
978
  const hidden = [
898
979
  hiddenInput(formFieldModuleId, options.reference.moduleId),
899
980
  hiddenInput(formFieldExportName, options.reference.exportName),
900
- hiddenInput(formCsrfFieldName, options.csrfToken),
901
- hiddenInput(formFieldNonce, options.actionNonce),
981
+ hiddenInput(
982
+ formCsrfFieldName,
983
+ options.placeholders ? csrfTokenPlaceholder : options.csrfToken,
984
+ ),
985
+ hiddenInput(
986
+ formFieldNonce,
987
+ options.placeholders ? actionNoncePlaceholder : options.actionNonce,
988
+ ),
902
989
  hiddenInput(
903
990
  formFieldActionToken,
904
- formActionToken({
905
- csrfToken: options.csrfToken,
906
- exportName: options.reference.exportName,
907
- moduleId: options.reference.moduleId,
908
- nonce: options.actionNonce,
909
- }),
991
+ options.placeholders
992
+ ? actionTokenPlaceholder(options.reference)
993
+ : formActionToken({
994
+ csrfToken: options.csrfToken,
995
+ exportName: options.reference.exportName,
996
+ moduleId: options.reference.moduleId,
997
+ nonce: options.actionNonce,
998
+ }),
910
999
  ),
911
1000
  ].join("");
912
1001
 
913
1002
  return `<form${attrs} method="post" action="/_mreact/actions">${hidden}`;
914
1003
  }
915
1004
 
1005
+ function actionTokenPlaceholder(reference: ActionReference): string {
1006
+ return `__mreact_action_token_${createHash("sha256")
1007
+ .update(reference.moduleId)
1008
+ .update("\0")
1009
+ .update(reference.exportName)
1010
+ .digest("hex")
1011
+ .slice(0, 16)}__`;
1012
+ }
1013
+
916
1014
  function escapeRegExp(value: string): string {
917
1015
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
918
1016
  }
package/src/build.ts CHANGED
@@ -92,6 +92,7 @@ import { collectRouteCssFilesFromSources, collectSpecialBoundaryFiles } from "./
92
92
  import { existingRouteShellCandidates } from "./route-shells.js";
93
93
  import { sourceModuleCandidates } from "./source-modules.js";
94
94
  import { collectBuildInferredServerActions } from "./server-action-inference.js";
95
+ import { prepareRouteServerActionPlaceholders } from "./actions.js";
95
96
  import { viteDefineCacheKey, vitePluginsCacheKey } from "./vite-plugin-cache-key.js";
96
97
  import { workspacePackageFile } from "./workspace-packages.js";
97
98
  import { prependTailwindSourceDirectives } from "./tailwind-source.js";
@@ -511,15 +512,9 @@ export async function buildApp(options: BuildAppOptions): Promise<BuildAppResult
511
512
  }),
512
513
  ),
513
514
  ]);
514
- const actionRenderBundleExcludedFiles = new Set(
515
- [...serverActionManifest.routeReferences.entries()]
516
- .filter(([, references]) => references.length > 0)
517
- .map(([file]) => file),
518
- );
519
515
  const { artifacts: serverModules, sharedChunks: serverModuleSharedChunks } = await (
520
516
  shouldTrackBuildPhases === false
521
517
  ? buildServerModuleArtifacts({
522
- actionRenderBundleExcludedFiles,
523
518
  bundleRequestRuntimePackages: shouldBuildAwsLambda,
524
519
  bundleCache: new Map(),
525
520
  clientRouteInferenceCache: serverClientRouteInferenceCache,
@@ -529,13 +524,13 @@ export async function buildApp(options: BuildAppOptions): Promise<BuildAppResult
529
524
  project,
530
525
  projectRoot: project.projectRoot,
531
526
  routes,
527
+ serverActionReferencesByFile: serverActionManifest.routeReferences,
532
528
  sourceAnalysis,
533
529
  serverTransformCache,
534
530
  vitePlugins,
535
531
  })
536
532
  : timeBuildPhase(timingSink, progressSink, "serverModules", () =>
537
533
  buildServerModuleArtifacts({
538
- actionRenderBundleExcludedFiles,
539
534
  bundleRequestRuntimePackages: shouldBuildAwsLambda,
540
535
  bundleCache: new Map(),
541
536
  clientRouteInferenceCache: serverClientRouteInferenceCache,
@@ -545,6 +540,7 @@ export async function buildApp(options: BuildAppOptions): Promise<BuildAppResult
545
540
  project,
546
541
  projectRoot: project.projectRoot,
547
542
  routes,
543
+ serverActionReferencesByFile: serverActionManifest.routeReferences,
548
544
  sourceAnalysis,
549
545
  serverTransformCache,
550
546
  vitePlugins,
@@ -990,6 +986,16 @@ function buildSourceAnalysisForFile(
990
986
  return sourceAnalysis.byFile.get(relative(projectRoot, file).split(sep).join("/"));
991
987
  }
992
988
 
989
+ function canUseBuildServerActionPlaceholders(
990
+ references: readonly BuiltServerActionExpressionReference[],
991
+ ): boolean {
992
+ return references.every(
993
+ (reference) =>
994
+ reference.expression === reference.exportName &&
995
+ /^[A-Za-z_$][\w$]*$/u.test(reference.expression),
996
+ );
997
+ }
998
+
993
999
  async function buildPublicAssetManifest(
994
1000
  project: ResolvedAppRouterProject,
995
1001
  clientDir: string,
@@ -2062,7 +2068,6 @@ function routePathFromParams(route: AppRoute, params: StaticParams): string {
2062
2068
  }
2063
2069
 
2064
2070
  async function buildServerModuleArtifacts(options: {
2065
- actionRenderBundleExcludedFiles?: ReadonlySet<string> | undefined;
2066
2071
  bundleRequestRuntimePackages: boolean;
2067
2072
  bundleCache: Map<string, Promise<RouterBundleOutput>>;
2068
2073
  cacheDir?: string | undefined;
@@ -2073,6 +2078,7 @@ async function buildServerModuleArtifacts(options: {
2073
2078
  project: ResolvedAppRouterProject;
2074
2079
  projectRoot: string;
2075
2080
  routes: readonly AppRoute[];
2081
+ serverActionReferencesByFile: ReadonlyMap<string, readonly BuiltServerActionExpressionReference[]>;
2076
2082
  sourceAnalysis: BuildSourceAnalysisScope;
2077
2083
  serverTransformCache: ServerTransformCache;
2078
2084
  vitePlugins?: readonly PluginOption[] | undefined;
@@ -2193,6 +2199,14 @@ async function buildServerModuleArtifacts(options: {
2193
2199
  const route = routeByFile.get(file);
2194
2200
  const routeAnalysis = options.sourceAnalysis.byRouteFile.get(file);
2195
2201
  const artifact: BuiltServerModuleArtifact = {};
2202
+ const routeActionReferences = options.serverActionReferencesByFile.get(file) ?? [];
2203
+ const renderSource =
2204
+ routeActionReferences.length === 0
2205
+ ? source
2206
+ : prepareRouteServerActionPlaceholders({
2207
+ code: source,
2208
+ formActionReferences: routeActionReferences,
2209
+ });
2196
2210
 
2197
2211
  if (
2198
2212
  requestArtifactFiles.has(file) ||
@@ -2282,8 +2296,11 @@ async function buildServerModuleArtifacts(options: {
2282
2296
  ? (["stream", "string"] as const)
2283
2297
  : (["string"] as const);
2284
2298
  const code =
2285
- routeAnalysis?.routeCode ??
2286
- (route === undefined ? source : stripRouteBuildExports(source, absoluteFile));
2299
+ routeActionReferences.length === 0 && routeAnalysis !== undefined
2300
+ ? routeAnalysis.routeCode
2301
+ : route === undefined
2302
+ ? renderSource
2303
+ : stripRouteBuildExports(renderSource, absoluteFile);
2287
2304
  const clientInference =
2288
2305
  routeAnalysis === undefined
2289
2306
  ? await inferClientRouteModule({
@@ -2367,7 +2384,8 @@ async function buildServerModuleArtifacts(options: {
2367
2384
 
2368
2385
  const shouldWriteRenderBundle =
2369
2386
  options.prebundleServerComponents &&
2370
- options.actionRenderBundleExcludedFiles?.has(file) !== true;
2387
+ (routeActionReferences.length === 0 ||
2388
+ canUseBuildServerActionPlaceholders(routeActionReferences));
2371
2389
  const bundleCode = shouldWriteRenderBundle
2372
2390
  ? await buildServerComponentBundleArtifactCode({
2373
2391
  clientRouteInferenceCache: options.clientRouteInferenceCache,
package/src/render.ts CHANGED
@@ -47,6 +47,7 @@ import {
47
47
  type AppRouterServerActionOptions,
48
48
  type PreparedFormActionReference,
49
49
  dispatchServerActionRequest,
50
+ prepareRouteServerActionPlaceholders,
50
51
  prepareRouteServerActions,
51
52
  } from "./actions.js";
52
53
  import { serverActionCookie } from "./csrf.js";
@@ -76,10 +77,12 @@ import {
76
77
  hasLoaderExport,
77
78
  isStreamRouteSource,
78
79
  routeClosureMayUseAwaitBoundary,
80
+ stripRouteBuildExports,
81
+ stripRouteModuleExports,
79
82
  stripRouteClientOnlyExports,
83
+ stripRouteClientSource,
80
84
  stripRouteLoaderOnlyExports,
81
85
  stripRouteMetadataOnlyExports,
82
- stripRouteModuleExports,
83
86
  } from "./route-source.js";
84
87
  import { emitRouterLog, logDurationMs, logNow, type AppRouterLogger } from "./logger.js";
85
88
  import {
@@ -231,6 +234,10 @@ export async function preloadBuiltRequestModules(options: {
231
234
  serverModules?: ReadonlyMap<string, BuiltServerModuleArtifact> | undefined;
232
235
  serverModuleCacheVersion: string;
233
236
  serverSourceFiles: ReadonlyMap<string, string>;
237
+ serverActionReferencesByFile?: ReadonlyMap<
238
+ string,
239
+ readonly PreparedFormActionReference[]
240
+ > | undefined;
234
241
  vitePlugins?: readonly PluginOption[] | undefined;
235
242
  }): Promise<void> {
236
243
  const clientRouteInferenceCache = createClientRouteInferenceCache();
@@ -276,21 +283,31 @@ export async function preloadBuiltRequestModules(options: {
276
283
  }
277
284
 
278
285
  if (options.includeRenderModules !== false) {
286
+ const actionReferences = options.serverActionReferencesByFile?.get(route.file) ?? [];
287
+ const analysisCode =
288
+ actionReferences.length === 0
289
+ ? code
290
+ : prepareRouteServerActionPlaceholders({
291
+ code,
292
+ formActionReferences: actionReferences,
293
+ });
279
294
  const analysis = await analyzeRouteSource({
280
295
  appDir: options.appDir,
281
296
  artifact: options.serverModules?.get(route.file)?.analysis,
282
- code,
297
+ buildRenderCode: analysisCode !== code,
298
+ code: analysisCode,
283
299
  filename: route.file,
284
300
  routePath: route.path,
285
301
  clientRouteInferenceCache,
286
- serverModuleCacheVersion: options.serverModuleCacheVersion,
302
+ serverModuleCacheVersion:
303
+ actionReferences.length === 0 ? options.serverModuleCacheVersion : undefined,
287
304
  serverSourceFiles: options.serverSourceFiles,
288
305
  vitePlugins: options.vitePlugins,
289
306
  });
290
307
  await preloadBuiltPageRouteModules({
291
308
  ...options,
292
309
  analysis,
293
- code,
310
+ code: analysisCode,
294
311
  file: route.file,
295
312
  });
296
313
  }
@@ -1017,6 +1034,7 @@ async function renderAppRequestInternal(options: RenderAppRequestOptions): Promi
1017
1034
  appDir: options.appDir,
1018
1035
  code: originalCode,
1019
1036
  formActionReferences: options.serverActionReferencesByFile?.get(matched.route.file),
1037
+ placeholders: true,
1020
1038
  pageFile: matched.route.file,
1021
1039
  request: options.request,
1022
1040
  });
@@ -1031,6 +1049,7 @@ async function renderAppRequestInternal(options: RenderAppRequestOptions): Promi
1031
1049
  ? originalAnalysis
1032
1050
  : await analyzeRouteSource({
1033
1051
  appDir: options.appDir,
1052
+ buildRenderCode: preparedActions.htmlReplacements !== undefined,
1034
1053
  code,
1035
1054
  filename: matched.route.file,
1036
1055
  routePath: matched.route.path,
@@ -1049,7 +1068,7 @@ async function renderAppRequestInternal(options: RenderAppRequestOptions): Promi
1049
1068
  const dataPromise = routeAnalysis.hasLoader
1050
1069
  ? loadRouteDataWithInstrumentation({
1051
1070
  appDir: options.appDir,
1052
- code,
1071
+ code: originalCode,
1053
1072
  context: {
1054
1073
  env: options.env,
1055
1074
  params: matched.params,
@@ -1226,13 +1245,16 @@ async function renderAppRequestInternal(options: RenderAppRequestOptions): Promi
1226
1245
  html = injectQueryState(html, dehydrate(queryClient));
1227
1246
  const response = withOptionalActionCookie(
1228
1247
  htmlResponse(
1229
- `<!DOCTYPE html>${clientNavigationHeadTags({
1230
- assetBaseUrl: options.assetBaseUrl,
1231
- currentStyleSheets: clientStyleSheets,
1232
- currentScript: clientRoute ? clientScript : undefined,
1233
- currentNavigationScript: clientRoute ? undefined : navigationScript,
1234
- routeScripts: options.clientScripts,
1235
- })}${html}`,
1248
+ applyActionHtmlReplacements(
1249
+ `<!DOCTYPE html>${clientNavigationHeadTags({
1250
+ assetBaseUrl: options.assetBaseUrl,
1251
+ currentStyleSheets: clientStyleSheets,
1252
+ currentScript: clientRoute ? clientScript : undefined,
1253
+ currentNavigationScript: clientRoute ? undefined : navigationScript,
1254
+ routeScripts: options.clientScripts,
1255
+ })}${html}`,
1256
+ preparedActions.htmlReplacements,
1257
+ ),
1236
1258
  {
1237
1259
  headers: responseHeadersForMetadata(metadata, options.request, {
1238
1260
  "x-mreact-stream": "1",
@@ -1249,7 +1271,7 @@ async function renderAppRequestInternal(options: RenderAppRequestOptions): Promi
1249
1271
  let streamData: unknown;
1250
1272
  let streamDataPromise = dataPromise ?? Promise.resolve(undefined);
1251
1273
  phaseStartedAt = renderTimingPhaseStartedAt(timing);
1252
- if (loadingFile === undefined || routeLoaderMayReturnControlResponse(code)) {
1274
+ if (loadingFile === undefined || routeLoaderMayReturnControlResponse(originalCode)) {
1253
1275
  try {
1254
1276
  streamData = dataPromise === undefined ? undefined : await dataPromise;
1255
1277
  } finally {
@@ -1508,13 +1530,16 @@ async function renderAppRequestInternal(options: RenderAppRequestOptions): Promi
1508
1530
 
1509
1531
  const response = withOptionalActionCookie(
1510
1532
  htmlResponse(
1511
- `<!DOCTYPE html>${clientNavigationHeadTags({
1512
- assetBaseUrl: options.assetBaseUrl,
1513
- currentStyleSheets: clientStyleSheets,
1514
- currentScript: clientRoute ? clientScript : undefined,
1515
- currentNavigationScript: clientRoute ? undefined : navigationScript,
1516
- routeScripts: options.clientScripts,
1517
- })}${html}`,
1533
+ applyActionHtmlReplacements(
1534
+ `<!DOCTYPE html>${clientNavigationHeadTags({
1535
+ assetBaseUrl: options.assetBaseUrl,
1536
+ currentStyleSheets: clientStyleSheets,
1537
+ currentScript: clientRoute ? clientScript : undefined,
1538
+ currentNavigationScript: clientRoute ? undefined : navigationScript,
1539
+ routeScripts: options.clientScripts,
1540
+ })}${html}`,
1541
+ preparedActions.htmlReplacements,
1542
+ ),
1518
1543
  {
1519
1544
  headers: responseHeadersForMetadata(metadata, options.request),
1520
1545
  },
@@ -1632,6 +1657,23 @@ function withOptionalActionCookie(
1632
1657
  return response;
1633
1658
  }
1634
1659
 
1660
+ function applyActionHtmlReplacements(
1661
+ html: string,
1662
+ replacements: readonly (readonly [string, string])[] | undefined,
1663
+ ): string {
1664
+ if (replacements === undefined || replacements.length === 0) {
1665
+ return html;
1666
+ }
1667
+
1668
+ let next = html;
1669
+
1670
+ for (const [placeholder, value] of replacements) {
1671
+ next = next.replaceAll(placeholder, escapeHtmlAttribute(value));
1672
+ }
1673
+
1674
+ return next;
1675
+ }
1676
+
1635
1677
  function modulePreloadTags(script: string | undefined, assetBaseUrl: string | undefined): string {
1636
1678
  return script === undefined
1637
1679
  ? ""
@@ -2768,6 +2810,7 @@ function warnProductionDynamicServerTransform(
2768
2810
  async function analyzeRouteSource(options: {
2769
2811
  appDir: string;
2770
2812
  artifact?: BuiltRouteSourceAnalysisSummary | undefined;
2813
+ buildRenderCode?: boolean | undefined;
2771
2814
  clientRouteInferenceCache: ClientRouteInferenceCache;
2772
2815
  code: string;
2773
2816
  define?: UserConfig["define"] | undefined;
@@ -2791,7 +2834,7 @@ async function analyzeRouteSource(options: {
2791
2834
  return analysis;
2792
2835
  }
2793
2836
 
2794
- const cacheKey = `${options.serverModuleCacheVersion ?? "dev"}\0${options.filename}\0${sourceHash}\0${viteDefineCacheKey(options.define)}\0${vitePluginsCacheKey(options.vitePlugins)}`;
2837
+ const cacheKey = `${options.serverModuleCacheVersion ?? "dev"}\0${options.filename}\0${sourceHash}\0${options.buildRenderCode === true ? "build-render" : "render"}\0${viteDefineCacheKey(options.define)}\0${vitePluginsCacheKey(options.vitePlugins)}`;
2795
2838
  const cached = readRouterRuntimeCacheEntry(
2796
2839
  routeSourceAnalysisCache,
2797
2840
  cacheKey,
@@ -2838,6 +2881,7 @@ function routeSourceAnalysisFromArtifact(
2838
2881
 
2839
2882
  async function analyzeRouteSourceUncached(options: {
2840
2883
  appDir: string;
2884
+ buildRenderCode?: boolean | undefined;
2841
2885
  clientRouteInferenceCache: ClientRouteInferenceCache;
2842
2886
  code: string;
2843
2887
  define?: UserConfig["define"] | undefined;
@@ -2846,11 +2890,14 @@ async function analyzeRouteSourceUncached(options: {
2846
2890
  serverSourceFiles?: ReadonlyMap<string, string> | undefined;
2847
2891
  vitePlugins?: readonly PluginOption[] | undefined;
2848
2892
  }): Promise<RouteSourceAnalysis> {
2849
- const routeCode = stripRouteModuleExports(options.code, options.filename);
2893
+ const routeCode =
2894
+ options.buildRenderCode === true
2895
+ ? stripRouteBuildExports(options.code, options.filename)
2896
+ : stripRouteModuleExports(options.code, options.filename);
2850
2897
  const clientInference = await inferClientRouteModule({
2851
2898
  appDir: options.appDir,
2852
2899
  cache: options.clientRouteInferenceCache,
2853
- code: routeCode,
2900
+ code: stripRouteClientSource({ code: options.code, filename: options.filename }),
2854
2901
  filename: options.filename,
2855
2902
  routePath: options.routePath,
2856
2903
  vitePlugins: options.vitePlugins,
@@ -2956,6 +3003,7 @@ async function loadServerModule(
2956
3003
  const artifact = serverModules?.get(sourcefile)?.string;
2957
3004
  const codeHash = memoizedHashText(code);
2958
3005
  const prebuiltCode = prebuiltServerComponentModuleCode(artifact, code, codeHash);
3006
+ const usesPrebuiltCompiledCode = artifact?.code === code;
2959
3007
  if (
2960
3008
  artifact !== undefined &&
2961
3009
  prebuiltServerModuleOutputMatches(artifact, code, codeHash) &&
@@ -2990,7 +3038,7 @@ async function loadServerModule(
2990
3038
  label: "Server component",
2991
3039
  }),
2992
3040
  ],
2993
- ...(prebuiltCode === undefined
3041
+ ...(prebuiltCode === undefined && !usesPrebuiltCompiledCode
2994
3042
  ? {
2995
3043
  resolveDir: dirname(sourcefile),
2996
3044
  serverSourceTransform: {
@@ -3002,6 +3050,7 @@ async function loadServerModule(
3002
3050
  },
3003
3051
  }
3004
3052
  : {}),
3053
+ ...(usesPrebuiltCompiledCode ? { resolveDir: dirname(sourcefile) } : {}),
3005
3054
  sourcefile,
3006
3055
  vitePlugins,
3007
3056
  });
@@ -3611,6 +3660,7 @@ async function loadServerStreamModule(
3611
3660
  const artifactCode = serverModules?.get(sourcefile)?.stream;
3612
3661
  const codeHash = memoizedHashText(code);
3613
3662
  const prebuiltCode = prebuiltServerComponentModuleCode(artifactCode, code, codeHash, outputOptions);
3663
+ const usesPrebuiltCompiledCode = artifactCode?.code === code;
3614
3664
  if (
3615
3665
  artifactCode !== undefined &&
3616
3666
  prebuiltServerModuleOutputOptionsMatch(artifactCode, outputOptions) &&
@@ -3646,7 +3696,7 @@ async function loadServerStreamModule(
3646
3696
  label: "Server stream component",
3647
3697
  }),
3648
3698
  ],
3649
- ...(prebuiltCode === undefined
3699
+ ...(prebuiltCode === undefined && !usesPrebuiltCompiledCode
3650
3700
  ? {
3651
3701
  resolveDir: dirname(sourcefile),
3652
3702
  serverSourceTransform: {
@@ -3658,6 +3708,7 @@ async function loadServerStreamModule(
3658
3708
  },
3659
3709
  }
3660
3710
  : {}),
3711
+ ...(usesPrebuiltCompiledCode ? { resolveDir: dirname(sourcefile) } : {}),
3661
3712
  sourcefile,
3662
3713
  vitePlugins,
3663
3714
  });
package/src/serve.ts CHANGED
@@ -358,6 +358,7 @@ async function preloadBuiltAppRuntimeWithRuntime(options: {
358
358
  serverModules: options.runtime.serverModules,
359
359
  serverModuleCacheVersion: options.runtime.serverModuleCacheVersion,
360
360
  serverSourceFiles: options.runtime.serverSourceFiles,
361
+ serverActionReferencesByFile: options.runtime.serverActionReferencesByFile,
361
362
  includeRenderModules: plan.includeRenderModules,
362
363
  });
363
364
  }