@reckona/mreact-router 0.0.96 → 0.0.98

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.
Files changed (100) hide show
  1. package/README.md +4 -0
  2. package/dist/actions.d.ts +7 -0
  3. package/dist/actions.d.ts.map +1 -1
  4. package/dist/actions.js +26 -2
  5. package/dist/actions.js.map +1 -1
  6. package/dist/adapters/aws-lambda.d.ts.map +1 -1
  7. package/dist/adapters/aws-lambda.js +10 -7
  8. package/dist/adapters/aws-lambda.js.map +1 -1
  9. package/dist/adapters/static.d.ts.map +1 -1
  10. package/dist/adapters/static.js +13 -0
  11. package/dist/adapters/static.js.map +1 -1
  12. package/dist/build.d.ts +38 -1
  13. package/dist/build.d.ts.map +1 -1
  14. package/dist/build.js +1093 -307
  15. package/dist/build.js.map +1 -1
  16. package/dist/bundle-pipeline.d.ts +7 -0
  17. package/dist/bundle-pipeline.d.ts.map +1 -1
  18. package/dist/bundle-pipeline.js +121 -69
  19. package/dist/bundle-pipeline.js.map +1 -1
  20. package/dist/cache.d.ts.map +1 -1
  21. package/dist/cache.js +10 -8
  22. package/dist/cache.js.map +1 -1
  23. package/dist/cli-options.d.ts +1 -0
  24. package/dist/cli-options.d.ts.map +1 -1
  25. package/dist/cli-options.js +16 -0
  26. package/dist/cli-options.js.map +1 -1
  27. package/dist/cli.js +1 -0
  28. package/dist/cli.js.map +1 -1
  29. package/dist/client.d.ts +6 -1
  30. package/dist/client.d.ts.map +1 -1
  31. package/dist/client.js +120 -9
  32. package/dist/client.js.map +1 -1
  33. package/dist/csrf.d.ts.map +1 -1
  34. package/dist/csrf.js +7 -3
  35. package/dist/csrf.js.map +1 -1
  36. package/dist/http.d.ts.map +1 -1
  37. package/dist/http.js +18 -1
  38. package/dist/http.js.map +1 -1
  39. package/dist/import-policy.d.ts.map +1 -1
  40. package/dist/import-policy.js +7 -3
  41. package/dist/import-policy.js.map +1 -1
  42. package/dist/index.d.ts +4 -3
  43. package/dist/index.d.ts.map +1 -1
  44. package/dist/index.js.map +1 -1
  45. package/dist/link.d.ts +5 -3
  46. package/dist/link.d.ts.map +1 -1
  47. package/dist/link.js +92 -3
  48. package/dist/link.js.map +1 -1
  49. package/dist/logger.d.ts +7 -1
  50. package/dist/logger.d.ts.map +1 -1
  51. package/dist/logger.js.map +1 -1
  52. package/dist/module-runner.d.ts +2 -0
  53. package/dist/module-runner.d.ts.map +1 -1
  54. package/dist/module-runner.js +1 -0
  55. package/dist/module-runner.js.map +1 -1
  56. package/dist/native-route-matcher.d.ts +2 -1
  57. package/dist/native-route-matcher.d.ts.map +1 -1
  58. package/dist/native-route-matcher.js +13 -2
  59. package/dist/native-route-matcher.js.map +1 -1
  60. package/dist/render.d.ts.map +1 -1
  61. package/dist/render.js +121 -6
  62. package/dist/render.js.map +1 -1
  63. package/dist/route-source.d.ts +11 -11
  64. package/dist/route-source.d.ts.map +1 -1
  65. package/dist/route-source.js +32 -25
  66. package/dist/route-source.js.map +1 -1
  67. package/dist/route-styles.d.ts +6 -0
  68. package/dist/route-styles.d.ts.map +1 -1
  69. package/dist/route-styles.js +10 -1
  70. package/dist/route-styles.js.map +1 -1
  71. package/dist/serve.d.ts +5 -0
  72. package/dist/serve.d.ts.map +1 -1
  73. package/dist/serve.js +7 -4
  74. package/dist/serve.js.map +1 -1
  75. package/dist/vite.d.ts.map +1 -1
  76. package/dist/vite.js +32 -5
  77. package/dist/vite.js.map +1 -1
  78. package/package.json +11 -11
  79. package/src/actions.ts +42 -2
  80. package/src/adapters/aws-lambda.ts +33 -16
  81. package/src/adapters/static.ts +25 -0
  82. package/src/build.ts +1605 -421
  83. package/src/bundle-pipeline.ts +136 -70
  84. package/src/cache.ts +13 -8
  85. package/src/cli-options.ts +20 -0
  86. package/src/cli.ts +1 -0
  87. package/src/client.ts +152 -15
  88. package/src/csrf.ts +8 -3
  89. package/src/http.ts +21 -1
  90. package/src/import-policy.ts +8 -3
  91. package/src/index.ts +5 -0
  92. package/src/link.ts +135 -8
  93. package/src/logger.ts +9 -1
  94. package/src/module-runner.ts +4 -0
  95. package/src/native-route-matcher.ts +13 -2
  96. package/src/render.ts +181 -6
  97. package/src/route-source.ts +33 -20
  98. package/src/route-styles.ts +17 -1
  99. package/src/serve.ts +11 -6
  100. package/src/vite.ts +41 -5
package/src/render.ts CHANGED
@@ -1124,6 +1124,13 @@ async function renderAppRequestInternal(options: RenderAppRequestOptions): Promi
1124
1124
  vitePlugins: options.vitePlugins,
1125
1125
  });
1126
1126
  html = injectHeadMetadata(html, metadata);
1127
+ warnIfCspNonceWouldBlockInlineTags({
1128
+ html,
1129
+ logger: options.logger,
1130
+ metadata,
1131
+ request: options.request,
1132
+ serverModuleCacheVersion: options.serverModuleCacheVersion,
1133
+ });
1127
1134
  html = injectAuthSessionClaims(
1128
1135
  html,
1129
1136
  originalAnalysis.authIncludesClaims ? currentAuthClaims() : undefined,
@@ -1377,6 +1384,13 @@ async function renderAppRequestInternal(options: RenderAppRequestOptions): Promi
1377
1384
  finishRenderTimingPhase(timing, phaseStartedAt, "metadataMs");
1378
1385
  phaseStartedAt = renderTimingPhaseStartedAt(timing);
1379
1386
  html = injectHeadMetadata(html, metadata);
1387
+ warnIfCspNonceWouldBlockInlineTags({
1388
+ html,
1389
+ logger: options.logger,
1390
+ metadata,
1391
+ request: options.request,
1392
+ serverModuleCacheVersion: options.serverModuleCacheVersion,
1393
+ });
1380
1394
  html = injectAuthSessionClaims(
1381
1395
  html,
1382
1396
  originalAnalysis.authIncludesClaims ? currentAuthClaims() : undefined,
@@ -2319,6 +2333,7 @@ export async function bundleMiddlewareModuleCode(options: {
2319
2333
  code: options.code,
2320
2334
  filename: options.file,
2321
2335
  platform: "node",
2336
+ root: options.importPolicy?.projectRoot,
2322
2337
  plugins: [
2323
2338
  fileImportMetaUrlPlugin(),
2324
2339
  createAppRouterImportPolicyPlugin({
@@ -2525,7 +2540,7 @@ async function analyzeRouteSourceUncached(options: {
2525
2540
  serverSourceFiles?: ReadonlyMap<string, string> | undefined;
2526
2541
  vitePlugins?: readonly PluginOption[] | undefined;
2527
2542
  }): Promise<RouteSourceAnalysis> {
2528
- const routeCode = stripRouteModuleExports(options.code);
2543
+ const routeCode = stripRouteModuleExports(options.code, options.filename);
2529
2544
  const clientInference = await inferClientRouteModule({
2530
2545
  appDir: options.appDir,
2531
2546
  cache: options.clientRouteInferenceCache,
@@ -3464,7 +3479,7 @@ async function renderShellPrefixSuffix(
3464
3479
  ? { client: false, clientBoundaryImports: [], diagnostics: [] }
3465
3480
  : await inferClientRouteModule({
3466
3481
  cache: clientRouteInferenceCache,
3467
- code: stripRouteClientOnlyExports(code),
3482
+ code: stripRouteClientOnlyExports(code, shell.file),
3468
3483
  filename: shell.file,
3469
3484
  vitePlugins,
3470
3485
  });
@@ -3967,7 +3982,7 @@ export async function bundleRouteLoaderModuleCode(options: {
3967
3982
  vitePlugins?: readonly PluginOption[] | undefined;
3968
3983
  }): Promise<string> {
3969
3984
  const output = await bundleRouterModule({
3970
- code: stripRouteLoaderOnlyExports(options.code),
3985
+ code: stripRouteLoaderOnlyExports(options.code, options.filename),
3971
3986
  externalizeAppSourceModuleDirs: options.externalizeAppSourceModuleDirs,
3972
3987
  filename: options.filename,
3973
3988
  platform: "node",
@@ -4111,7 +4126,7 @@ async function bundleRouteMetadataModuleCode(options: {
4111
4126
  vitePlugins?: readonly PluginOption[] | undefined;
4112
4127
  }): Promise<string> {
4113
4128
  const output = await bundleRouterModule({
4114
- code: stripRouteMetadataOnlyExports(options.code),
4129
+ code: stripRouteMetadataOnlyExports(options.code, options.filename),
4115
4130
  filename: options.filename,
4116
4131
  platform: "node",
4117
4132
  vitePlugins: options.vitePlugins,
@@ -4269,6 +4284,166 @@ function usesRuntimeCacheControl(code: string): boolean {
4269
4284
  return /\bcacheControl\s*\(/.test(code);
4270
4285
  }
4271
4286
 
4287
+ function warnIfCspNonceWouldBlockInlineTags(options: {
4288
+ html: string;
4289
+ logger: AppRouterLogger | undefined;
4290
+ metadata: RouteMetadata | undefined;
4291
+ request: Request;
4292
+ serverModuleCacheVersion: string | undefined;
4293
+ }): void {
4294
+ if (
4295
+ options.serverModuleCacheVersion !== undefined ||
4296
+ process.env.NODE_ENV === "production" ||
4297
+ options.metadata?.csp?.disable === true ||
4298
+ options.metadata?.csp?.directives === undefined ||
4299
+ !/<(?:script|style)\b/i.test(options.html)
4300
+ ) {
4301
+ return;
4302
+ }
4303
+
4304
+ const scriptNonces = cspDirectiveNonces(options.metadata.csp, "script-src");
4305
+ const styleNonces = cspDirectiveNonces(options.metadata.csp, "style-src");
4306
+
4307
+ if (scriptNonces.size === 0 && styleNonces.size === 0) {
4308
+ return;
4309
+ }
4310
+
4311
+ for (const tag of inlineCspTags(options.html)) {
4312
+ if (tag.name === "script") {
4313
+ if (
4314
+ scriptNonces.size > 0 &&
4315
+ !scriptHasExternalSourceOrInertType(tag.attributes) &&
4316
+ tag.content.trim() !== "" &&
4317
+ !tagHasMatchingNonce(tag.attributes, scriptNonces)
4318
+ ) {
4319
+ warnCspInlineNonceMismatch(options, "script-src", "script");
4320
+ }
4321
+ continue;
4322
+ }
4323
+
4324
+ if (
4325
+ styleNonces.size > 0 &&
4326
+ tag.content.trim() !== "" &&
4327
+ !tagHasMatchingNonce(tag.attributes, styleNonces)
4328
+ ) {
4329
+ warnCspInlineNonceMismatch(options, "style-src", "style");
4330
+ }
4331
+ }
4332
+ }
4333
+
4334
+ function warnCspInlineNonceMismatch(
4335
+ options: {
4336
+ logger: AppRouterLogger | undefined;
4337
+ request: Request;
4338
+ },
4339
+ directive: "script-src" | "style-src",
4340
+ tag: "script" | "style",
4341
+ ): void {
4342
+ const message =
4343
+ tag === "script"
4344
+ ? "mreact router: CSP script-src uses a nonce, but an inline <script> without a matching nonce will be blocked. Add the script through metadata.head with nonce: true, move it to an external script, or remove script-src for this route."
4345
+ : "mreact router: CSP style-src uses a nonce, but an inline <style> without a matching nonce will be blocked. Add the style through metadata.head with nonce: true, move it to an external stylesheet, or remove style-src for this route.";
4346
+
4347
+ if (options.logger === undefined) {
4348
+ console.warn(message);
4349
+ return;
4350
+ }
4351
+
4352
+ emitRouterLog(options.logger, "warn", {
4353
+ directive,
4354
+ path: new URL(options.request.url).pathname,
4355
+ tag,
4356
+ type: "router:csp:inline-nonce-warning",
4357
+ });
4358
+ }
4359
+
4360
+ function cspDirectiveNonces(
4361
+ csp: NonNullable<RouteMetadata["csp"]>,
4362
+ directive: "script-src" | "style-src",
4363
+ ): ReadonlySet<string> {
4364
+ const values = csp.directives?.[directive];
4365
+ const nonces = new Set<string>();
4366
+
4367
+ if (values !== undefined && csp.nonce !== undefined) {
4368
+ nonces.add(csp.nonce);
4369
+ }
4370
+
4371
+ for (const value of Array.isArray(values) ? values : values === undefined ? [] : [values]) {
4372
+ const match = /^'nonce-([^']+)'$/.exec(value);
4373
+
4374
+ if (match?.[1] !== undefined) {
4375
+ nonces.add(match[1]);
4376
+ }
4377
+ }
4378
+
4379
+ return nonces;
4380
+ }
4381
+
4382
+ interface InlineCspTag {
4383
+ attributes: ReadonlyMap<string, string>;
4384
+ content: string;
4385
+ name: "script" | "style";
4386
+ }
4387
+
4388
+ function inlineCspTags(html: string): InlineCspTag[] {
4389
+ const tags: InlineCspTag[] = [];
4390
+ const pattern = /<(script|style)\b([^>]*)>([\s\S]*?)<\/\1>/gi;
4391
+ let match: RegExpExecArray | null;
4392
+
4393
+ while ((match = pattern.exec(html)) !== null) {
4394
+ const name = match[1]?.toLowerCase();
4395
+
4396
+ if (name !== "script" && name !== "style") {
4397
+ continue;
4398
+ }
4399
+
4400
+ tags.push({
4401
+ attributes: parseTagAttributes(match[2] ?? ""),
4402
+ content: match[3] ?? "",
4403
+ name,
4404
+ });
4405
+ }
4406
+
4407
+ return tags;
4408
+ }
4409
+
4410
+ function parseTagAttributes(source: string): ReadonlyMap<string, string> {
4411
+ const attributes = new Map<string, string>();
4412
+ const pattern = /([^\s=/>]+)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'=<>`]+)))?/g;
4413
+ let match: RegExpExecArray | null;
4414
+
4415
+ while ((match = pattern.exec(source)) !== null) {
4416
+ const name = match[1]?.toLowerCase();
4417
+
4418
+ if (name === undefined) {
4419
+ continue;
4420
+ }
4421
+
4422
+ attributes.set(name, match[2] ?? match[3] ?? match[4] ?? "");
4423
+ }
4424
+
4425
+ return attributes;
4426
+ }
4427
+
4428
+ function scriptHasExternalSourceOrInertType(attributes: ReadonlyMap<string, string>): boolean {
4429
+ if (attributes.has("src")) {
4430
+ return true;
4431
+ }
4432
+
4433
+ const type = attributes.get("type")?.trim().toLowerCase();
4434
+
4435
+ return type === "application/json" || type === "application/ld+json";
4436
+ }
4437
+
4438
+ function tagHasMatchingNonce(
4439
+ attributes: ReadonlyMap<string, string>,
4440
+ expectedNonces: ReadonlySet<string>,
4441
+ ): boolean {
4442
+ const nonce = attributes.get("nonce");
4443
+
4444
+ return nonce !== undefined && expectedNonces.has(nonce);
4445
+ }
4446
+
4272
4447
  function injectQueryState(html: string, state: DehydratedQueryClient): string {
4273
4448
  if (state.queries.length === 0) {
4274
4449
  return html;
@@ -4279,7 +4454,7 @@ function injectQueryState(html: string, state: DehydratedQueryClient): string {
4279
4454
  )}</script>`;
4280
4455
 
4281
4456
  return /<\/body>/i.test(html)
4282
- ? html.replace(/<\/body>/i, `${script}</body>`)
4457
+ ? html.replace(/<\/body>/i, () => `${script}</body>`)
4283
4458
  : `${html}${script}`;
4284
4459
  }
4285
4460
 
@@ -4293,7 +4468,7 @@ function injectAuthSessionClaims(html: string, claims: unknown): string {
4293
4468
  )}</script>`;
4294
4469
 
4295
4470
  return /<\/body>/i.test(html)
4296
- ? html.replace(/<\/body>/i, `${script}</body>`)
4471
+ ? html.replace(/<\/body>/i, () => `${script}</body>`)
4297
4472
  : `${html}${script}`;
4298
4473
  }
4299
4474
 
@@ -37,21 +37,26 @@ const routeRequestExportNames = new Set<string>([
37
37
  const routeLoaderOnlyExportNames = new Set<string>(["loader"]);
38
38
  const routeMetadataOnlyExportNames = new Set<string>(["generateMetadata", "metadata"]);
39
39
 
40
- export function stripRouteModuleExports(code: string): string {
40
+ export function stripRouteModuleExports(code: string, filename?: string | undefined): string {
41
41
  return demoteRouteHelperExports(stripTopLevelExportDeclarations({
42
42
  code,
43
+ filename,
43
44
  names: routeModuleExportNames,
44
- }));
45
+ }), routeRenderExportNames, filename);
45
46
  }
46
47
 
47
- export function stripRouteClientOnlyExports(code: string): string {
48
+ export function stripRouteClientOnlyExports(code: string, filename?: string | undefined): string {
48
49
  return stripUnusedStaticValueImports({
49
50
  code: demoteRouteHelperExports(
50
51
  stripTopLevelExportDeclarations({
51
52
  code,
53
+ filename,
52
54
  names: routeClientOnlyExportNames,
53
55
  }),
56
+ routeRenderExportNames,
57
+ filename,
54
58
  ),
59
+ filename,
55
60
  });
56
61
  }
57
62
 
@@ -60,7 +65,7 @@ export function stripRouteClientSource(input: {
60
65
  filename?: string | undefined;
61
66
  }): string {
62
67
  return stripRouteClientFormActionExpressions({
63
- code: stripRouteClientOnlyExports(input.code),
68
+ code: stripRouteClientOnlyExports(input.code, input.filename),
64
69
  filename: input.filename,
65
70
  });
66
71
  }
@@ -79,49 +84,56 @@ export function stripRouteClientFormActionExpressions(input: {
79
84
  return code;
80
85
  }
81
86
 
82
- export function stripRouteBuildExports(code: string): string {
83
- return stripRouteClientOnlyExports(code);
87
+ export function stripRouteBuildExports(code: string, filename?: string | undefined): string {
88
+ return stripRouteClientOnlyExports(code, filename);
84
89
  }
85
90
 
86
- export function stripRouteRequestOnlyExports(code: string): string {
91
+ export function stripRouteRequestOnlyExports(code: string, filename?: string | undefined): string {
87
92
  return demoteRouteHelperExports(
88
93
  stripTopLevelExportDeclarations({
89
94
  code,
95
+ filename,
90
96
  names: routeRequestRenderExportNames,
91
97
  }),
92
98
  routeRequestExportNames,
99
+ filename,
93
100
  );
94
101
  }
95
102
 
96
- export function stripRouteLoaderOnlyExports(code: string): string {
103
+ export function stripRouteLoaderOnlyExports(code: string, filename?: string | undefined): string {
97
104
  return demoteRouteHelperExports(
98
105
  stripTopLevelExportDeclarations({
99
106
  code,
107
+ filename,
100
108
  names: ["auth", "default", "generateMetadata", "generateStaticParams", "metadata", "middleware", "prerender", "revalidate", "slots", "stream"],
101
109
  }),
102
110
  routeLoaderOnlyExportNames,
111
+ filename,
103
112
  );
104
113
  }
105
114
 
106
- export function stripRouteMetadataOnlyExports(code: string): string {
115
+ export function stripRouteMetadataOnlyExports(code: string, filename?: string | undefined): string {
107
116
  return demoteRouteHelperExports(
108
117
  stripTopLevelExportDeclarations({
109
118
  code,
119
+ filename,
110
120
  names: ["auth", "default", "generateStaticParams", "loader", "middleware", "prerender", "revalidate", "slots", "stream"],
111
121
  }),
112
122
  routeMetadataOnlyExportNames,
123
+ filename,
113
124
  );
114
125
  }
115
126
 
116
- export function stripRouteConfigExports(code: string): string {
127
+ export function stripRouteConfigExports(code: string, filename?: string | undefined): string {
117
128
  return stripTopLevelExportDeclarations({
118
129
  code,
130
+ filename,
119
131
  names: ["auth", "prerender", "revalidate", "stream"],
120
132
  });
121
133
  }
122
134
 
123
- export function isStreamRouteSource(code: string): boolean {
124
- return hasTopLevelExportDeclaration({ code, names: ["stream"] });
135
+ export function isStreamRouteSource(code: string, filename?: string | undefined): boolean {
136
+ return hasTopLevelExportDeclaration({ code, filename, names: ["stream"] });
125
137
  }
126
138
 
127
139
  export function mayUseAwaitBoundarySource(code: string): boolean {
@@ -194,28 +206,29 @@ export function routeClosureMayUseAwaitBoundary(options: {
194
206
  }
195
207
  }
196
208
 
197
- export function hasPrerenderExport(code: string): boolean {
198
- return hasTopLevelExportDeclaration({ code, names: ["prerender"] });
209
+ export function hasPrerenderExport(code: string, filename?: string | undefined): boolean {
210
+ return hasTopLevelExportDeclaration({ code, filename, names: ["prerender"] });
199
211
  }
200
212
 
201
- export function hasGenerateStaticParamsExport(code: string): boolean {
202
- return hasTopLevelExportDeclaration({ code, names: ["generateStaticParams"] });
213
+ export function hasGenerateStaticParamsExport(code: string, filename?: string | undefined): boolean {
214
+ return hasTopLevelExportDeclaration({ code, filename, names: ["generateStaticParams"] });
203
215
  }
204
216
 
205
- export function hasLoaderExport(code: string): boolean {
206
- return hasTopLevelExportDeclaration({ code, names: ["loader"] });
217
+ export function hasLoaderExport(code: string, filename?: string | undefined): boolean {
218
+ return hasTopLevelExportDeclaration({ code, filename, names: ["loader"] });
207
219
  }
208
220
 
209
221
  function demoteRouteHelperExports(
210
222
  code: string,
211
223
  preservedExportNames: ReadonlySet<string> = routeRenderExportNames,
224
+ filename?: string | undefined,
212
225
  ): string {
213
- const helperNames = collectTopLevelValueExportNames({ code })
226
+ const helperNames = collectTopLevelValueExportNames({ code, filename })
214
227
  .filter((name) => !preservedExportNames.has(name) && startsLowercase(name));
215
228
 
216
229
  return helperNames.length === 0
217
230
  ? code
218
- : demoteTopLevelExportDeclarations({ code, names: helperNames });
231
+ : demoteTopLevelExportDeclarations({ code, filename, names: helperNames });
219
232
  }
220
233
 
221
234
  function startsLowercase(value: string): boolean {
@@ -7,6 +7,18 @@ export async function collectRouteCssFiles(options: {
7
7
  appDir: string;
8
8
  pageFile: string;
9
9
  projectRoot: string;
10
+ }): Promise<string[]> {
11
+ return await collectRouteCssFilesFromSources({
12
+ ...options,
13
+ readSource: (file) => readFile(file, "utf8"),
14
+ });
15
+ }
16
+
17
+ export async function collectRouteCssFilesFromSources(options: {
18
+ appDir: string;
19
+ pageFile: string;
20
+ projectRoot: string;
21
+ readSource: (file: string) => Promise<string | undefined> | string | undefined;
10
22
  }): Promise<string[]> {
11
23
  const shellFiles = (await existingRouteShellCandidates(options.appDir, options.pageFile, isFile))
12
24
  .map((candidate) => candidate.file);
@@ -15,7 +27,11 @@ export async function collectRouteCssFiles(options: {
15
27
  const cssFiles: string[] = [];
16
28
 
17
29
  for (const file of files) {
18
- const source = await readFile(file, "utf8");
30
+ const source = await options.readSource(file);
31
+
32
+ if (source === undefined) {
33
+ continue;
34
+ }
19
35
 
20
36
  for (const reference of collectStaticImportReferences({ code: source, filename: file })) {
21
37
  const cssFile = resolveCssImport({
package/src/serve.ts CHANGED
@@ -82,7 +82,7 @@ interface BuiltRuntimeCacheEntry {
82
82
  }
83
83
 
84
84
  const builtRuntimeCache = new Map<string, BuiltRuntimeCacheEntry>();
85
- const builtPublicAssetCache = new Map<string, BuiltPublicAsset | null>();
85
+ const builtPublicAssetCache = new Map<string, BuiltPublicAsset>();
86
86
 
87
87
  interface BuiltPublicAsset {
88
88
  bytes: Uint8Array;
@@ -681,10 +681,6 @@ async function readBuiltPublicAsset(
681
681
  const cacheKey = `${outDir}\0${normalized}`;
682
682
  const cached = builtPublicAssetCache.get(cacheKey);
683
683
 
684
- if (cached === null) {
685
- return undefined;
686
- }
687
-
688
684
  if (cached !== undefined) {
689
685
  return bytesResponse(cached.bytes, {
690
686
  headers: cached.headers,
@@ -701,11 +697,20 @@ async function readBuiltPublicAsset(
701
697
 
702
698
  return bytesResponse(bytes, { headers });
703
699
  } catch {
704
- builtPublicAssetCache.set(`${outDir}\0${normalized}`, null);
705
700
  return undefined;
706
701
  }
707
702
  }
708
703
 
704
+ export const __readBuiltPublicAssetForTest = readBuiltPublicAsset;
705
+
706
+ export function __clearBuiltPublicAssetCacheForTest(): void {
707
+ builtPublicAssetCache.clear();
708
+ }
709
+
710
+ export function __getBuiltPublicAssetCacheSizeForTest(): number {
711
+ return builtPublicAssetCache.size;
712
+ }
713
+
709
714
  async function readBuiltRuntime(options: {
710
715
  immutable?: boolean | undefined;
711
716
  outDir: string;
package/src/vite.ts CHANGED
@@ -176,7 +176,7 @@ export function createAppRouterVitePlugin(options: AppRouterVitePluginOptions):
176
176
  enforce: "pre",
177
177
  name: "mreact-router",
178
178
  config(userConfig) {
179
- userVitePlugins = userConfig.plugins;
179
+ userVitePlugins = routeTransformUserVitePlugins(userConfig.plugins);
180
180
 
181
181
  return {
182
182
  optimizeDeps: {
@@ -197,7 +197,7 @@ export function createAppRouterVitePlugin(options: AppRouterVitePluginOptions):
197
197
  return () => {
198
198
  const middlewareOptions: AppRouterViteRuntimeMiddlewareOptions = {
199
199
  ...options,
200
- navigationScanVitePlugins: userVitePlugins,
200
+ navigationScanVitePlugins: userVitePlugins ?? [],
201
201
  viteDevServer: server,
202
202
  vitePlugins: server.config.plugins,
203
203
  };
@@ -370,6 +370,40 @@ function isCompatSourcePath(filename: string): boolean {
370
370
 
371
371
  export const mreactRouter = createAppRouterVitePlugin;
372
372
 
373
+ function routeTransformUserVitePlugins(
374
+ pluginOptions: readonly PluginOption[] | undefined,
375
+ ): PluginOption[] {
376
+ const plugins: PluginOption[] = [];
377
+ const visit = (option: PluginOption | null | false | undefined): void => {
378
+ if (option === false || option === null || option === undefined) {
379
+ return;
380
+ }
381
+
382
+ if (Array.isArray(option)) {
383
+ for (const child of option) {
384
+ visit(child);
385
+ }
386
+ return;
387
+ }
388
+
389
+ if (typeof option === "object" && "then" in option) {
390
+ return;
391
+ }
392
+
393
+ if (typeof option === "object" && mreactRouterConfigKey in option) {
394
+ return;
395
+ }
396
+
397
+ plugins.push(option);
398
+ };
399
+
400
+ for (const option of pluginOptions ?? []) {
401
+ visit(option);
402
+ }
403
+
404
+ return plugins;
405
+ }
406
+
373
407
  export function mreactRouterConfigFromPlugins(
374
408
  plugins: readonly unknown[],
375
409
  ): ResolvedAppRouterProject | undefined {
@@ -450,6 +484,7 @@ async function handleAppRouterViteRequest(
450
484
  }
451
485
 
452
486
  const request = nodeRequestToWebRequest(incoming, origin);
487
+ const routeTransformPlugins = options.navigationScanVitePlugins ?? options.vitePlugins;
453
488
 
454
489
  await sendResponse(
455
490
  outgoing,
@@ -464,11 +499,12 @@ async function handleAppRouterViteRequest(
464
499
  navigationScripts: await devNavigationScripts(
465
500
  project.routesDir,
466
501
  options.clientRouteInferenceCache,
467
- options.navigationScanVitePlugins,
502
+ routeTransformPlugins,
468
503
  ),
469
504
  request,
470
505
  routeCache: options.routeCache,
471
506
  serverActions: options.serverActions,
507
+ vitePlugins: routeTransformPlugins,
472
508
  }),
473
509
  );
474
510
  } catch (error) {
@@ -501,7 +537,7 @@ export async function renderAppRouterClientAsset(
501
537
  }
502
538
 
503
539
  const code = await readFile(route.file, "utf8");
504
- const clientSource = stripRouteClientOnlyExports(code);
540
+ const clientSource = stripRouteClientOnlyExports(code, route.file);
505
541
  let references: Awaited<ReturnType<typeof collectClientRouteReferences>>;
506
542
 
507
543
  try {
@@ -565,7 +601,7 @@ async function renderAppRouterClientRouteDevModule(
565
601
  }
566
602
 
567
603
  const code = await readFile(route.file, "utf8");
568
- const clientSource = stripRouteClientOnlyExports(code);
604
+ const clientSource = stripRouteClientOnlyExports(code, route.file);
569
605
  const references = await collectClientRouteReferences({
570
606
  appDir,
571
607
  code: clientSource,