@reckona/mreact-router 0.0.97 → 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.
- package/README.md +4 -0
- package/dist/actions.d.ts +7 -0
- package/dist/actions.d.ts.map +1 -1
- package/dist/actions.js +26 -2
- package/dist/actions.js.map +1 -1
- package/dist/adapters/aws-lambda.d.ts.map +1 -1
- package/dist/adapters/aws-lambda.js +10 -7
- package/dist/adapters/aws-lambda.js.map +1 -1
- package/dist/build.d.ts +38 -1
- package/dist/build.d.ts.map +1 -1
- package/dist/build.js +1087 -304
- package/dist/build.js.map +1 -1
- package/dist/bundle-pipeline.d.ts +7 -0
- package/dist/bundle-pipeline.d.ts.map +1 -1
- package/dist/bundle-pipeline.js +121 -87
- package/dist/bundle-pipeline.js.map +1 -1
- package/dist/cache.d.ts.map +1 -1
- package/dist/cache.js +10 -8
- package/dist/cache.js.map +1 -1
- package/dist/cli-options.d.ts +1 -0
- package/dist/cli-options.d.ts.map +1 -1
- package/dist/cli-options.js +16 -0
- package/dist/cli-options.js.map +1 -1
- package/dist/cli.js +1 -0
- package/dist/cli.js.map +1 -1
- package/dist/client.d.ts +6 -1
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +113 -8
- package/dist/client.js.map +1 -1
- package/dist/csrf.d.ts.map +1 -1
- package/dist/csrf.js +7 -3
- package/dist/csrf.js.map +1 -1
- package/dist/http.d.ts.map +1 -1
- package/dist/http.js +18 -1
- package/dist/http.js.map +1 -1
- package/dist/import-policy.d.ts.map +1 -1
- package/dist/import-policy.js +7 -3
- package/dist/import-policy.js.map +1 -1
- package/dist/index.d.ts +4 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/link.d.ts +5 -3
- package/dist/link.d.ts.map +1 -1
- package/dist/link.js +92 -3
- package/dist/link.js.map +1 -1
- package/dist/logger.d.ts +7 -1
- package/dist/logger.d.ts.map +1 -1
- package/dist/logger.js.map +1 -1
- package/dist/module-runner.d.ts +2 -0
- package/dist/module-runner.d.ts.map +1 -1
- package/dist/module-runner.js +1 -0
- package/dist/module-runner.js.map +1 -1
- package/dist/native-route-matcher.d.ts +2 -1
- package/dist/native-route-matcher.d.ts.map +1 -1
- package/dist/native-route-matcher.js +13 -2
- package/dist/native-route-matcher.js.map +1 -1
- package/dist/render.d.ts.map +1 -1
- package/dist/render.js +117 -2
- package/dist/render.js.map +1 -1
- package/dist/route-source.d.ts +4 -4
- package/dist/route-source.d.ts.map +1 -1
- package/dist/route-source.js +8 -8
- package/dist/route-source.js.map +1 -1
- package/dist/route-styles.d.ts +6 -0
- package/dist/route-styles.d.ts.map +1 -1
- package/dist/route-styles.js +10 -1
- package/dist/route-styles.js.map +1 -1
- package/dist/serve.d.ts +5 -0
- package/dist/serve.d.ts.map +1 -1
- package/dist/serve.js +7 -4
- package/dist/serve.js.map +1 -1
- package/package.json +11 -11
- package/src/actions.ts +42 -2
- package/src/adapters/aws-lambda.ts +33 -16
- package/src/build.ts +1578 -397
- package/src/bundle-pipeline.ts +136 -88
- package/src/cache.ts +13 -8
- package/src/cli-options.ts +20 -0
- package/src/cli.ts +1 -0
- package/src/client.ts +145 -14
- package/src/csrf.ts +8 -3
- package/src/http.ts +21 -1
- package/src/import-policy.ts +8 -3
- package/src/index.ts +5 -0
- package/src/link.ts +135 -8
- package/src/logger.ts +9 -1
- package/src/module-runner.ts +4 -0
- package/src/native-route-matcher.ts +13 -2
- package/src/render.ts +177 -2
- package/src/route-source.ts +8 -8
- package/src/route-styles.ts +17 -1
- 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
|
|
package/src/route-source.ts
CHANGED
|
@@ -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(
|
package/src/route-styles.ts
CHANGED
|
@@ -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
|
|
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
|
|
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;
|