@reckona/mreact-router 0.0.97 → 0.0.99

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 (92) 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/build.d.ts +38 -1
  10. package/dist/build.d.ts.map +1 -1
  11. package/dist/build.js +1087 -304
  12. package/dist/build.js.map +1 -1
  13. package/dist/bundle-pipeline.d.ts +7 -0
  14. package/dist/bundle-pipeline.d.ts.map +1 -1
  15. package/dist/bundle-pipeline.js +121 -87
  16. package/dist/bundle-pipeline.js.map +1 -1
  17. package/dist/cache.d.ts.map +1 -1
  18. package/dist/cache.js +10 -8
  19. package/dist/cache.js.map +1 -1
  20. package/dist/cli-options.d.ts +1 -0
  21. package/dist/cli-options.d.ts.map +1 -1
  22. package/dist/cli-options.js +16 -0
  23. package/dist/cli-options.js.map +1 -1
  24. package/dist/cli.js +1 -0
  25. package/dist/cli.js.map +1 -1
  26. package/dist/client.d.ts +6 -1
  27. package/dist/client.d.ts.map +1 -1
  28. package/dist/client.js +113 -8
  29. package/dist/client.js.map +1 -1
  30. package/dist/csrf.d.ts.map +1 -1
  31. package/dist/csrf.js +7 -3
  32. package/dist/csrf.js.map +1 -1
  33. package/dist/http.d.ts.map +1 -1
  34. package/dist/http.js +18 -1
  35. package/dist/http.js.map +1 -1
  36. package/dist/import-policy.d.ts.map +1 -1
  37. package/dist/import-policy.js +7 -3
  38. package/dist/import-policy.js.map +1 -1
  39. package/dist/index.d.ts +4 -3
  40. package/dist/index.d.ts.map +1 -1
  41. package/dist/index.js.map +1 -1
  42. package/dist/link.d.ts +5 -2
  43. package/dist/link.d.ts.map +1 -1
  44. package/dist/link.js +92 -3
  45. package/dist/link.js.map +1 -1
  46. package/dist/logger.d.ts +7 -1
  47. package/dist/logger.d.ts.map +1 -1
  48. package/dist/logger.js.map +1 -1
  49. package/dist/module-runner.d.ts +2 -0
  50. package/dist/module-runner.d.ts.map +1 -1
  51. package/dist/module-runner.js +1 -0
  52. package/dist/module-runner.js.map +1 -1
  53. package/dist/native-route-matcher.d.ts +2 -1
  54. package/dist/native-route-matcher.d.ts.map +1 -1
  55. package/dist/native-route-matcher.js +13 -2
  56. package/dist/native-route-matcher.js.map +1 -1
  57. package/dist/render.d.ts.map +1 -1
  58. package/dist/render.js +117 -2
  59. package/dist/render.js.map +1 -1
  60. package/dist/route-source.d.ts +4 -4
  61. package/dist/route-source.d.ts.map +1 -1
  62. package/dist/route-source.js +8 -8
  63. package/dist/route-source.js.map +1 -1
  64. package/dist/route-styles.d.ts +6 -0
  65. package/dist/route-styles.d.ts.map +1 -1
  66. package/dist/route-styles.js +10 -1
  67. package/dist/route-styles.js.map +1 -1
  68. package/dist/serve.d.ts +5 -0
  69. package/dist/serve.d.ts.map +1 -1
  70. package/dist/serve.js +7 -4
  71. package/dist/serve.js.map +1 -1
  72. package/package.json +11 -11
  73. package/src/actions.ts +42 -2
  74. package/src/adapters/aws-lambda.ts +33 -16
  75. package/src/build.ts +1578 -397
  76. package/src/bundle-pipeline.ts +136 -88
  77. package/src/cache.ts +13 -8
  78. package/src/cli-options.ts +20 -0
  79. package/src/cli.ts +1 -0
  80. package/src/client.ts +145 -14
  81. package/src/csrf.ts +8 -3
  82. package/src/http.ts +21 -1
  83. package/src/import-policy.ts +8 -3
  84. package/src/index.ts +5 -0
  85. package/src/link.ts +129 -8
  86. package/src/logger.ts +9 -1
  87. package/src/module-runner.ts +4 -0
  88. package/src/native-route-matcher.ts +13 -2
  89. package/src/render.ts +177 -2
  90. package/src/route-source.ts +8 -8
  91. package/src/route-styles.ts +17 -1
  92. package/src/serve.ts +11 -6
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({
@@ -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
 
@@ -132,8 +132,8 @@ export function stripRouteConfigExports(code: string, filename?: string | undefi
132
132
  });
133
133
  }
134
134
 
135
- export function isStreamRouteSource(code: string): boolean {
136
- return hasTopLevelExportDeclaration({ code, names: ["stream"] });
135
+ export function isStreamRouteSource(code: string, filename?: string | undefined): boolean {
136
+ return hasTopLevelExportDeclaration({ code, filename, names: ["stream"] });
137
137
  }
138
138
 
139
139
  export function mayUseAwaitBoundarySource(code: string): boolean {
@@ -206,16 +206,16 @@ export function routeClosureMayUseAwaitBoundary(options: {
206
206
  }
207
207
  }
208
208
 
209
- export function hasPrerenderExport(code: string): boolean {
210
- return hasTopLevelExportDeclaration({ code, names: ["prerender"] });
209
+ export function hasPrerenderExport(code: string, filename?: string | undefined): boolean {
210
+ return hasTopLevelExportDeclaration({ code, filename, names: ["prerender"] });
211
211
  }
212
212
 
213
- export function hasGenerateStaticParamsExport(code: string): boolean {
214
- return hasTopLevelExportDeclaration({ code, names: ["generateStaticParams"] });
213
+ export function hasGenerateStaticParamsExport(code: string, filename?: string | undefined): boolean {
214
+ return hasTopLevelExportDeclaration({ code, filename, names: ["generateStaticParams"] });
215
215
  }
216
216
 
217
- export function hasLoaderExport(code: string): boolean {
218
- return hasTopLevelExportDeclaration({ code, names: ["loader"] });
217
+ export function hasLoaderExport(code: string, filename?: string | undefined): boolean {
218
+ return hasTopLevelExportDeclaration({ code, filename, names: ["loader"] });
219
219
  }
220
220
 
221
221
  function demoteRouteHelperExports(
@@ -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;