@reckona/mreact-router 0.0.181 → 0.0.183
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/dist/actions.d.ts.map +1 -1
- package/dist/actions.js +20 -8
- package/dist/actions.js.map +1 -1
- package/dist/build.d.ts +3 -0
- package/dist/build.d.ts.map +1 -1
- package/dist/build.js +263 -5
- package/dist/build.js.map +1 -1
- package/dist/built-assets.d.ts.map +1 -1
- package/dist/built-assets.js +2 -2
- package/dist/built-assets.js.map +1 -1
- package/dist/cache-config.d.ts +1 -1
- package/dist/cache-config.d.ts.map +1 -1
- package/dist/cache-config.js.map +1 -1
- package/dist/cache.d.ts.map +1 -1
- package/dist/cache.js +18 -1
- package/dist/cache.js.map +1 -1
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +16 -6
- package/dist/client.js.map +1 -1
- package/dist/config.d.ts +2 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +6 -0
- package/dist/config.js.map +1 -1
- package/dist/csrf.js +10 -6
- package/dist/csrf.js.map +1 -1
- package/dist/navigation.d.ts.map +1 -1
- package/dist/navigation.js +14 -1
- package/dist/navigation.js.map +1 -1
- package/dist/render.d.ts +4 -0
- package/dist/render.d.ts.map +1 -1
- package/dist/render.js +96 -46
- package/dist/render.js.map +1 -1
- package/dist/vite.d.ts.map +1 -1
- package/dist/vite.js +35 -4
- package/dist/vite.js.map +1 -1
- package/package.json +11 -11
- package/src/actions.ts +26 -9
- package/src/build.ts +398 -5
- package/src/built-assets.ts +2 -3
- package/src/cache-config.ts +1 -0
- package/src/cache.ts +20 -1
- package/src/client.ts +16 -6
- package/src/config.ts +8 -0
- package/src/csrf.ts +13 -6
- package/src/navigation.ts +16 -1
- package/src/render.ts +194 -78
- package/src/vite.ts +41 -4
package/src/build.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
3
|
import type { Dirent } from "node:fs";
|
|
3
4
|
import {
|
|
4
5
|
copyFile,
|
|
@@ -108,7 +109,8 @@ const nativeEscapeTransform = {
|
|
|
108
109
|
batchImportSource: "@reckona/mreact-router/native-escape",
|
|
109
110
|
} as const;
|
|
110
111
|
|
|
111
|
-
const
|
|
112
|
+
const maxDefaultBuildConcurrency = 8;
|
|
113
|
+
const buildConcurrencyStorage = new AsyncLocalStorage<number>();
|
|
112
114
|
const serverArtifactFilesystemConcurrency = 2;
|
|
113
115
|
|
|
114
116
|
type ServerTransformOutput = ReturnType<typeof transform>;
|
|
@@ -219,6 +221,29 @@ export interface BuildAppResult {
|
|
|
219
221
|
routes: AppRoute[];
|
|
220
222
|
}
|
|
221
223
|
|
|
224
|
+
interface IncrementalBuildCacheManifest {
|
|
225
|
+
fingerprint: string;
|
|
226
|
+
version: 1;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
interface IncrementalBuildServerManifestOutputs {
|
|
230
|
+
serverModuleFiles?: Record<string, string>;
|
|
231
|
+
serverModuleRenderFiles?: Record<string, string>;
|
|
232
|
+
serverModuleRequestFiles?: Record<string, string>;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
interface IncrementalBuildClientManifestOutputs {
|
|
236
|
+
assets?: readonly string[];
|
|
237
|
+
publicAssets?: readonly string[];
|
|
238
|
+
routes: readonly {
|
|
239
|
+
css?: readonly string[];
|
|
240
|
+
imports?: readonly string[];
|
|
241
|
+
navigationScript?: string | undefined;
|
|
242
|
+
script?: string | undefined;
|
|
243
|
+
sourceMap?: string | undefined;
|
|
244
|
+
}[];
|
|
245
|
+
}
|
|
246
|
+
|
|
222
247
|
/**
|
|
223
248
|
* Describes the generated import policy artifact consumed by built request handlers.
|
|
224
249
|
*/
|
|
@@ -377,10 +402,24 @@ type StaticParams = Record<string, string | number | boolean | readonly string[]
|
|
|
377
402
|
* Use this from custom build scripts when the CLI is too coarse-grained; the returned manifest paths describe the files written under `outDir`. The build reads route files, loaders, middleware, metadata, server actions, and client references, so callers should pass the same project root and source allow-list they expect to deploy.
|
|
378
403
|
*/
|
|
379
404
|
export async function buildApp(options: BuildAppOptions): Promise<BuildAppResult> {
|
|
405
|
+
const project = resolveAppRouterProjectOptions(options);
|
|
406
|
+
const buildConcurrency = resolveBuildConcurrency(
|
|
407
|
+
options.buildConcurrency ?? project.buildConcurrency,
|
|
408
|
+
);
|
|
409
|
+
|
|
410
|
+
return await buildConcurrencyStorage.run(
|
|
411
|
+
buildConcurrency,
|
|
412
|
+
async () => await buildAppWithResolvedProject(options, project),
|
|
413
|
+
);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
async function buildAppWithResolvedProject(
|
|
417
|
+
options: BuildAppOptions,
|
|
418
|
+
project: ResolvedAppRouterProject,
|
|
419
|
+
): Promise<BuildAppResult> {
|
|
380
420
|
const timingSink = options.onBuildPhaseTiming;
|
|
381
421
|
const progressSink = options.onBuildProgress;
|
|
382
422
|
const shouldTrackBuildPhases = timingSink !== undefined || progressSink !== undefined;
|
|
383
|
-
const project = resolveAppRouterProjectOptions(options);
|
|
384
423
|
const buildTargets = resolveBuildTargets(options.targets ?? project.buildTargets);
|
|
385
424
|
const shouldBuildCloudflare = buildTargets.includes("cloudflare");
|
|
386
425
|
const shouldBuildAwsLambda = buildTargets.includes("aws-lambda");
|
|
@@ -405,6 +444,14 @@ export async function buildApp(options: BuildAppOptions): Promise<BuildAppResult
|
|
|
405
444
|
const serverDir = join(options.outDir, "server");
|
|
406
445
|
const clientDir = join(options.outDir, "client");
|
|
407
446
|
const cloudflareDir = join(options.outDir, "cloudflare");
|
|
447
|
+
const incrementalBuildFingerprint = await createIncrementalBuildCacheFingerprint({
|
|
448
|
+
buildTargets,
|
|
449
|
+
files,
|
|
450
|
+
project,
|
|
451
|
+
routes,
|
|
452
|
+
viteDefine,
|
|
453
|
+
vitePlugins,
|
|
454
|
+
});
|
|
408
455
|
const sourceAnalysis =
|
|
409
456
|
shouldTrackBuildPhases === false
|
|
410
457
|
? await analyzeBuildRouteSources({
|
|
@@ -452,6 +499,17 @@ export async function buildApp(options: BuildAppOptions): Promise<BuildAppResult
|
|
|
452
499
|
);
|
|
453
500
|
}
|
|
454
501
|
|
|
502
|
+
if (
|
|
503
|
+
incrementalBuildFingerprint !== undefined &&
|
|
504
|
+
(await isIncrementalBuildCacheHit({
|
|
505
|
+
buildTargets,
|
|
506
|
+
fingerprint: incrementalBuildFingerprint,
|
|
507
|
+
outDir: options.outDir,
|
|
508
|
+
}))
|
|
509
|
+
) {
|
|
510
|
+
return { routes };
|
|
511
|
+
}
|
|
512
|
+
|
|
455
513
|
if (shouldTrackBuildPhases === false) {
|
|
456
514
|
await rm(options.outDir, { force: true, recursive: true });
|
|
457
515
|
await Promise.all([
|
|
@@ -783,9 +841,284 @@ export async function buildApp(options: BuildAppOptions): Promise<BuildAppResult
|
|
|
783
841
|
});
|
|
784
842
|
}
|
|
785
843
|
|
|
844
|
+
if (incrementalBuildFingerprint !== undefined) {
|
|
845
|
+
await writeIncrementalBuildCacheManifest(options.outDir, incrementalBuildFingerprint);
|
|
846
|
+
}
|
|
847
|
+
|
|
786
848
|
return { routes };
|
|
787
849
|
}
|
|
788
850
|
|
|
851
|
+
const incrementalBuildCacheFilename = "build-cache.json";
|
|
852
|
+
const incrementalBuildCacheVersion = 1;
|
|
853
|
+
|
|
854
|
+
async function createIncrementalBuildCacheFingerprint(options: {
|
|
855
|
+
buildTargets: readonly AppRouterBuildTarget[];
|
|
856
|
+
files: Record<string, string>;
|
|
857
|
+
project: ResolvedAppRouterProject;
|
|
858
|
+
routes: readonly AppRoute[];
|
|
859
|
+
viteDefine: UserConfig["define"] | undefined;
|
|
860
|
+
vitePlugins: readonly PluginOption[] | undefined;
|
|
861
|
+
}): Promise<string | undefined> {
|
|
862
|
+
if (options.vitePlugins !== undefined && options.vitePlugins.length > 0) {
|
|
863
|
+
return undefined;
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
let define: string;
|
|
867
|
+
try {
|
|
868
|
+
const serializedDefine = JSON.stringify(options.viteDefine ?? null);
|
|
869
|
+
if (serializedDefine === undefined) {
|
|
870
|
+
return undefined;
|
|
871
|
+
}
|
|
872
|
+
define = serializedDefine;
|
|
873
|
+
} catch {
|
|
874
|
+
return undefined;
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
const [publicAssets, appConventionAssets] = await Promise.all([
|
|
878
|
+
collectBuildInputFileHashes(options.project.publicDir, options.project.projectRoot),
|
|
879
|
+
collectAppConventionAssetInputHashes(options.project.routesDir, options.project.projectRoot),
|
|
880
|
+
]);
|
|
881
|
+
const sourceFiles = Object.keys(options.files)
|
|
882
|
+
.sort()
|
|
883
|
+
.map((file) => [normalizeBuildInputPath(file), hashText(options.files[file] ?? "")] as const);
|
|
884
|
+
const routes = options.routes
|
|
885
|
+
.map((route) => ({
|
|
886
|
+
file: normalizeBuildInputPath(relative(options.project.projectRoot, route.file)),
|
|
887
|
+
kind: route.kind,
|
|
888
|
+
path: route.path,
|
|
889
|
+
}))
|
|
890
|
+
.sort((left, right) => left.file.localeCompare(right.file));
|
|
891
|
+
const payload = {
|
|
892
|
+
appDir: normalizeBuildInputPath(relative(options.project.projectRoot, options.project.routesDir)),
|
|
893
|
+
assetBaseUrl: options.project.assetBaseUrl ?? null,
|
|
894
|
+
clientConsolePureFunctions: options.project.clientConsolePureFunctions ?? [],
|
|
895
|
+
clientSourceMaps: options.project.clientSourceMaps,
|
|
896
|
+
define,
|
|
897
|
+
nodeEnv: process.env.NODE_ENV ?? null,
|
|
898
|
+
publicAssetBaseUrl: options.project.publicAssetBaseUrl ?? null,
|
|
899
|
+
publicDir: normalizeBuildInputPath(relative(options.project.projectRoot, options.project.publicDir)),
|
|
900
|
+
routes,
|
|
901
|
+
sourceDirs: options.project.allowedSourceDirs
|
|
902
|
+
.map((directory) => normalizeBuildInputPath(relative(options.project.projectRoot, directory)))
|
|
903
|
+
.sort(),
|
|
904
|
+
sourceFiles,
|
|
905
|
+
targets: [...options.buildTargets].sort(),
|
|
906
|
+
version: incrementalBuildCacheVersion,
|
|
907
|
+
publicAssets,
|
|
908
|
+
appConventionAssets,
|
|
909
|
+
};
|
|
910
|
+
|
|
911
|
+
return createHash("sha256").update(JSON.stringify(payload)).digest("hex");
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
async function isIncrementalBuildCacheHit(options: {
|
|
915
|
+
buildTargets: readonly AppRouterBuildTarget[];
|
|
916
|
+
fingerprint: string;
|
|
917
|
+
outDir: string;
|
|
918
|
+
}): Promise<boolean> {
|
|
919
|
+
const cache = await readJsonBuildOutput<IncrementalBuildCacheManifest>(
|
|
920
|
+
join(options.outDir, incrementalBuildCacheFilename),
|
|
921
|
+
);
|
|
922
|
+
if (cache === undefined) {
|
|
923
|
+
return false;
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
return (
|
|
927
|
+
cache.version === incrementalBuildCacheVersion &&
|
|
928
|
+
cache.fingerprint === options.fingerprint &&
|
|
929
|
+
(await hasRequiredIncrementalBuildOutputs(options.outDir, options.buildTargets))
|
|
930
|
+
);
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
async function hasRequiredIncrementalBuildOutputs(
|
|
934
|
+
outDir: string,
|
|
935
|
+
buildTargets: readonly AppRouterBuildTarget[],
|
|
936
|
+
): Promise<boolean> {
|
|
937
|
+
const serverManifestFile = join(outDir, "server", "manifest.json");
|
|
938
|
+
const clientManifestFile = join(outDir, "client", "manifest.json");
|
|
939
|
+
const requiredFiles = [
|
|
940
|
+
serverManifestFile,
|
|
941
|
+
join(outDir, "server", "import-policy.json"),
|
|
942
|
+
clientManifestFile,
|
|
943
|
+
join(outDir, "client", ".vite", "manifest.json"),
|
|
944
|
+
join(outDir, "routes.d.ts"),
|
|
945
|
+
join(outDir, "public-assets.d.ts"),
|
|
946
|
+
...(buildTargets.includes("aws-lambda")
|
|
947
|
+
? [join(outDir, "aws-lambda", "mreact-handler.mjs")]
|
|
948
|
+
: []),
|
|
949
|
+
...(buildTargets.includes("cloudflare") ? [join(outDir, "cloudflare", "worker.mjs")] : []),
|
|
950
|
+
];
|
|
951
|
+
const [serverManifest, clientManifest] = await Promise.all([
|
|
952
|
+
readJsonBuildOutput<IncrementalBuildServerManifestOutputs>(serverManifestFile),
|
|
953
|
+
readJsonBuildOutput<IncrementalBuildClientManifestOutputs>(clientManifestFile),
|
|
954
|
+
]);
|
|
955
|
+
if (serverManifest === undefined || clientManifest === undefined) {
|
|
956
|
+
return false;
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
for (const file of [
|
|
960
|
+
...Object.values(serverManifest.serverModuleFiles ?? {}),
|
|
961
|
+
...Object.values(serverManifest.serverModuleRenderFiles ?? {}),
|
|
962
|
+
...Object.values(serverManifest.serverModuleRequestFiles ?? {}),
|
|
963
|
+
]) {
|
|
964
|
+
requiredFiles.push(join(outDir, "server", file));
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
for (const file of collectClientManifestOutputFiles(clientManifest)) {
|
|
968
|
+
requiredFiles.push(join(outDir, "client", file));
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
for (const file of requiredFiles) {
|
|
972
|
+
if (!(await isExistingFile(file))) {
|
|
973
|
+
return false;
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
return true;
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
function collectClientManifestOutputFiles(
|
|
981
|
+
manifest: IncrementalBuildClientManifestOutputs,
|
|
982
|
+
): string[] {
|
|
983
|
+
const files = new Set<string>();
|
|
984
|
+
|
|
985
|
+
for (const asset of manifest.assets ?? []) {
|
|
986
|
+
files.add(asset);
|
|
987
|
+
}
|
|
988
|
+
for (const asset of manifest.publicAssets ?? []) {
|
|
989
|
+
files.add(asset);
|
|
990
|
+
files.add(`public/${asset}`);
|
|
991
|
+
}
|
|
992
|
+
for (const route of manifest.routes) {
|
|
993
|
+
for (const file of [
|
|
994
|
+
route.script,
|
|
995
|
+
route.sourceMap,
|
|
996
|
+
route.navigationScript,
|
|
997
|
+
...(route.css ?? []),
|
|
998
|
+
...(route.imports ?? []),
|
|
999
|
+
]) {
|
|
1000
|
+
if (file !== undefined) {
|
|
1001
|
+
files.add(file);
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
return [...files];
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
async function readJsonBuildOutput<T>(file: string): Promise<T | undefined> {
|
|
1010
|
+
try {
|
|
1011
|
+
return JSON.parse(await readFile(file, "utf8")) as T;
|
|
1012
|
+
} catch (error) {
|
|
1013
|
+
if (
|
|
1014
|
+
(isNodeError(error) && error.code === "ENOENT") ||
|
|
1015
|
+
error instanceof SyntaxError
|
|
1016
|
+
) {
|
|
1017
|
+
return undefined;
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
throw error;
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
async function writeIncrementalBuildCacheManifest(
|
|
1025
|
+
outDir: string,
|
|
1026
|
+
fingerprint: string,
|
|
1027
|
+
): Promise<void> {
|
|
1028
|
+
const cache = {
|
|
1029
|
+
fingerprint,
|
|
1030
|
+
version: incrementalBuildCacheVersion,
|
|
1031
|
+
} satisfies IncrementalBuildCacheManifest;
|
|
1032
|
+
|
|
1033
|
+
await writeFile(
|
|
1034
|
+
join(outDir, incrementalBuildCacheFilename),
|
|
1035
|
+
`${JSON.stringify(cache, null, 2)}\n`,
|
|
1036
|
+
);
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
async function collectBuildInputFileHashes(
|
|
1040
|
+
directory: string,
|
|
1041
|
+
projectRoot: string,
|
|
1042
|
+
): Promise<readonly (readonly [string, string])[]> {
|
|
1043
|
+
if (!(await isPublicAssetDirectory(directory))) {
|
|
1044
|
+
return [];
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
const files = await collectFiles(directory);
|
|
1048
|
+
const hashes = await mapWithBuildConcurrency(
|
|
1049
|
+
files,
|
|
1050
|
+
async (file) =>
|
|
1051
|
+
[
|
|
1052
|
+
normalizeBuildInputPath(relative(projectRoot, file)),
|
|
1053
|
+
hashBuffer(await readFile(file)),
|
|
1054
|
+
] as const,
|
|
1055
|
+
);
|
|
1056
|
+
|
|
1057
|
+
return hashes.sort(([left], [right]) => left.localeCompare(right));
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
async function collectAppConventionAssetInputHashes(
|
|
1061
|
+
appDir: string,
|
|
1062
|
+
projectRoot: string,
|
|
1063
|
+
): Promise<readonly (readonly [string, string])[]> {
|
|
1064
|
+
const entries = await readdir(appDir, { withFileTypes: true });
|
|
1065
|
+
const assetFiles = entries
|
|
1066
|
+
.filter((entry) => entry.isFile())
|
|
1067
|
+
.map((entry) => join(appDir, entry.name))
|
|
1068
|
+
.filter((file) => isAppFileConventionAsset(file, appDir));
|
|
1069
|
+
const hashes = await mapWithBuildConcurrency(
|
|
1070
|
+
assetFiles,
|
|
1071
|
+
async (file) =>
|
|
1072
|
+
[
|
|
1073
|
+
normalizeBuildInputPath(relative(projectRoot, file)),
|
|
1074
|
+
hashBuffer(await readFile(file)),
|
|
1075
|
+
] as const,
|
|
1076
|
+
);
|
|
1077
|
+
|
|
1078
|
+
return hashes.sort(([left], [right]) => left.localeCompare(right));
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
async function isExistingFile(file: string): Promise<boolean> {
|
|
1082
|
+
try {
|
|
1083
|
+
return (await stat(file)).isFile();
|
|
1084
|
+
} catch (error) {
|
|
1085
|
+
if (isNodeError(error) && error.code === "ENOENT") {
|
|
1086
|
+
return false;
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
throw error;
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
function normalizeBuildInputPath(path: string): string {
|
|
1094
|
+
return path.split(sep).join("/");
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
function hashBuffer(buffer: Buffer): string {
|
|
1098
|
+
return createHash("sha256").update(buffer).digest("hex").slice(0, 16);
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
function defaultBuildConcurrency(): number {
|
|
1102
|
+
return Math.max(1, Math.min(maxDefaultBuildConcurrency, availableParallelism()));
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
function currentBuildConcurrency(): number {
|
|
1106
|
+
return buildConcurrencyStorage.getStore() ?? defaultBuildConcurrency();
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
function resolveBuildConcurrency(
|
|
1110
|
+
value: number | undefined,
|
|
1111
|
+
cores = availableParallelism(),
|
|
1112
|
+
): number {
|
|
1113
|
+
const resolved = value ?? Math.max(1, Math.min(maxDefaultBuildConcurrency, cores));
|
|
1114
|
+
|
|
1115
|
+
if (!Number.isSafeInteger(resolved) || resolved < 1) {
|
|
1116
|
+
throw new Error("mreactRouter buildConcurrency must be a positive integer.");
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
return resolved;
|
|
1120
|
+
}
|
|
1121
|
+
|
|
789
1122
|
function typedRoutesDeclaration(routes: readonly AppRoute[]): string {
|
|
790
1123
|
const routePaths = Array.from(
|
|
791
1124
|
new Set(
|
|
@@ -803,21 +1136,60 @@ function typedRoutesDeclaration(routes: readonly AppRoute[]): string {
|
|
|
803
1136
|
return left.localeCompare(right);
|
|
804
1137
|
});
|
|
805
1138
|
const routeUnion = routePaths.map((routePath) => JSON.stringify(routePath)).join(" | ");
|
|
1139
|
+
const routeParamEntries = routePaths.map(
|
|
1140
|
+
(routePath) => ` readonly ${JSON.stringify(routePath)}: ${routeParamsType(routePath)};`,
|
|
1141
|
+
);
|
|
806
1142
|
return [
|
|
807
|
-
`import type { AppRouteLinkHref as MreactAppRouteLinkHref } from "@reckona/mreact-router";`,
|
|
1143
|
+
`import type { AppRouteLinkHref as MreactAppRouteLinkHref, RouteParamsFor as MreactRouteParamsFor } from "@reckona/mreact-router";`,
|
|
808
1144
|
``,
|
|
809
1145
|
`export type AppRoutePath = ${routeUnion === "" ? "never" : routeUnion};`,
|
|
810
1146
|
`export type AppRouteHref = MreactAppRouteLinkHref<AppRoutePath>;`,
|
|
1147
|
+
`export type AppRouteParams<Path extends AppRoutePath> = MreactRouteParamsFor<Path>;`,
|
|
1148
|
+
`export interface AppRouteParamMap {`,
|
|
1149
|
+
...(routeParamEntries.length === 0 ? [` readonly [path: string]: never;`] : routeParamEntries),
|
|
1150
|
+
`}`,
|
|
811
1151
|
``,
|
|
812
1152
|
`declare module "@reckona/mreact-router/link" {`,
|
|
813
1153
|
` interface AppRouteDeclarations {`,
|
|
814
1154
|
` readonly path: AppRoutePath;`,
|
|
1155
|
+
` readonly params: AppRouteParamMap;`,
|
|
815
1156
|
` }`,
|
|
816
1157
|
`}`,
|
|
817
1158
|
``,
|
|
818
1159
|
].join("\n");
|
|
819
1160
|
}
|
|
820
1161
|
|
|
1162
|
+
function routeParamsType(routePath: string): string {
|
|
1163
|
+
const params = routePath
|
|
1164
|
+
.split("/")
|
|
1165
|
+
.flatMap((segment): Array<{ catchAll: boolean; name: string }> => {
|
|
1166
|
+
if (segment.startsWith(":...")) {
|
|
1167
|
+
return [{ catchAll: true, name: segment.slice(4) }];
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
if (segment.startsWith(":")) {
|
|
1171
|
+
return [{ catchAll: false, name: segment.slice(1) }];
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
return [];
|
|
1175
|
+
});
|
|
1176
|
+
|
|
1177
|
+
if (params.length === 0) {
|
|
1178
|
+
return "Record<never, never>";
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
return `{ ${params
|
|
1182
|
+
.map(
|
|
1183
|
+
(param) =>
|
|
1184
|
+
`readonly ${propertyKeyForType(param.name)}: ${param.catchAll ? "readonly string[]" : "string"}`,
|
|
1185
|
+
)
|
|
1186
|
+
.join("; ")} }`;
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
function propertyKeyForType(value: string): string {
|
|
1190
|
+
return /^[A-Za-z_$][\w$]*$/.test(value) ? value : JSON.stringify(value);
|
|
1191
|
+
}
|
|
1192
|
+
|
|
821
1193
|
function typedPublicAssetsDeclaration(publicAssets: readonly string[]): string {
|
|
822
1194
|
const assetUnion = publicAssets.map((assetPath) => JSON.stringify(assetPath)).join(" | ");
|
|
823
1195
|
|
|
@@ -861,7 +1233,7 @@ function roundBuildPhaseMs(value: number): number {
|
|
|
861
1233
|
async function mapWithBuildConcurrency<T, R>(
|
|
862
1234
|
items: readonly T[],
|
|
863
1235
|
map: (item: T, index: number) => Promise<R>,
|
|
864
|
-
concurrency =
|
|
1236
|
+
concurrency = currentBuildConcurrency(),
|
|
865
1237
|
): Promise<R[]> {
|
|
866
1238
|
if (items.length === 0) {
|
|
867
1239
|
return [];
|
|
@@ -897,6 +1269,27 @@ export async function __mapWithBuildConcurrencyForTests<T, R>(
|
|
|
897
1269
|
return await mapWithBuildConcurrency(items, map, concurrency);
|
|
898
1270
|
}
|
|
899
1271
|
|
|
1272
|
+
async function mapServerOutputsWithBuildConcurrency<R>(
|
|
1273
|
+
serverOutputs: readonly ServerOutputMode[],
|
|
1274
|
+
map: (serverOutput: ServerOutputMode, index: number) => Promise<R>,
|
|
1275
|
+
): Promise<R[]> {
|
|
1276
|
+
return await mapWithBuildConcurrency(serverOutputs, map, 1);
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1279
|
+
export async function __mapServerOutputsWithBuildConcurrencyForTests<R>(
|
|
1280
|
+
serverOutputs: readonly ServerOutputMode[],
|
|
1281
|
+
map: (serverOutput: ServerOutputMode, index: number) => Promise<R>,
|
|
1282
|
+
): Promise<R[]> {
|
|
1283
|
+
return await mapServerOutputsWithBuildConcurrency(serverOutputs, map);
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1286
|
+
export function __resolveBuildConcurrencyForTests(
|
|
1287
|
+
value: number | undefined,
|
|
1288
|
+
cores: number,
|
|
1289
|
+
): number {
|
|
1290
|
+
return resolveBuildConcurrency(value, cores);
|
|
1291
|
+
}
|
|
1292
|
+
|
|
900
1293
|
async function analyzeBuildRouteSources(options: {
|
|
901
1294
|
clientRouteInferenceCache: ClientRouteInferenceCache;
|
|
902
1295
|
files: Record<string, string>;
|
|
@@ -2479,7 +2872,7 @@ async function buildServerModuleArtifacts(options: {
|
|
|
2479
2872
|
});
|
|
2480
2873
|
}
|
|
2481
2874
|
|
|
2482
|
-
const serverOutputArtifacts = await
|
|
2875
|
+
const serverOutputArtifacts = await mapServerOutputsWithBuildConcurrency(
|
|
2483
2876
|
serverOutputs,
|
|
2484
2877
|
async (serverOutput) => {
|
|
2485
2878
|
const output = await transformServerRouteSource({
|
package/src/built-assets.ts
CHANGED
|
@@ -87,9 +87,8 @@ export async function readBuiltPublicAsset(
|
|
|
87
87
|
return undefined;
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
-
const normalized =
|
|
91
|
-
|
|
92
|
-
if (isAbsolute(normalized) || normalized === ".." || normalized.startsWith("../")) {
|
|
90
|
+
const normalized = safeBuiltClientAssetPath(relativePath);
|
|
91
|
+
if (normalized === undefined) {
|
|
93
92
|
return undefined;
|
|
94
93
|
}
|
|
95
94
|
|
package/src/cache-config.ts
CHANGED
package/src/cache.ts
CHANGED
|
@@ -203,6 +203,11 @@ export function routeCachePolicyFromSource(code: string): RouteCachePolicy | und
|
|
|
203
203
|
const seconds = match?.groups?.seconds === undefined ? undefined : Number(match.groups.seconds);
|
|
204
204
|
|
|
205
205
|
if (seconds === undefined || !Number.isFinite(seconds)) {
|
|
206
|
+
if (/^\s*export\s+const\s+revalidate\s*=/m.test(code)) {
|
|
207
|
+
console.warn(
|
|
208
|
+
"export const revalidate must be a plain integer literal, e.g. export const revalidate = 3600",
|
|
209
|
+
);
|
|
210
|
+
}
|
|
206
211
|
return undefined;
|
|
207
212
|
}
|
|
208
213
|
|
|
@@ -362,8 +367,10 @@ function requestCarriesCredentials(request: Request | undefined): boolean {
|
|
|
362
367
|
lower === "x-api-key" ||
|
|
363
368
|
lower === "cf-access-jwt-assertion" ||
|
|
364
369
|
lower === "proxy-authorization" ||
|
|
370
|
+
lower === "x-access-token" ||
|
|
365
371
|
lower === "x-auth-token" ||
|
|
366
372
|
lower === "x-authenticated-user" ||
|
|
373
|
+
lower === "x-id-token" ||
|
|
367
374
|
lower === "x-forwarded-user" ||
|
|
368
375
|
lower === "x-session-id" ||
|
|
369
376
|
lower === "x-user-email" ||
|
|
@@ -380,7 +387,7 @@ function requestCarriesCredentials(request: Request | undefined): boolean {
|
|
|
380
387
|
return true;
|
|
381
388
|
}
|
|
382
389
|
|
|
383
|
-
|
|
390
|
+
continue;
|
|
384
391
|
}
|
|
385
392
|
|
|
386
393
|
return false;
|
|
@@ -391,12 +398,18 @@ const PUBLIC_ROUTE_CACHE_REQUEST_HEADERS = new Set([
|
|
|
391
398
|
"accept-encoding",
|
|
392
399
|
"accept-language",
|
|
393
400
|
"cache-control",
|
|
401
|
+
"cf-connecting-ip",
|
|
402
|
+
"cf-ipcountry",
|
|
403
|
+
"cf-ray",
|
|
394
404
|
"connection",
|
|
395
405
|
"dnt",
|
|
396
406
|
"host",
|
|
407
|
+
"if-none-match",
|
|
397
408
|
"pragma",
|
|
409
|
+
"priority",
|
|
398
410
|
"purpose",
|
|
399
411
|
"referer",
|
|
412
|
+
"save-data",
|
|
400
413
|
"sec-ch-prefers-color-scheme",
|
|
401
414
|
"sec-ch-prefers-reduced-motion",
|
|
402
415
|
"sec-ch-ua",
|
|
@@ -408,8 +421,14 @@ const PUBLIC_ROUTE_CACHE_REQUEST_HEADERS = new Set([
|
|
|
408
421
|
"sec-fetch-user",
|
|
409
422
|
"upgrade-insecure-requests",
|
|
410
423
|
"user-agent",
|
|
424
|
+
"via",
|
|
411
425
|
"x-mreact-navigation",
|
|
412
426
|
"x-mreact-navigation-cache",
|
|
427
|
+
"x-real-ip",
|
|
428
|
+
"x-request-id",
|
|
429
|
+
"x-forwarded-for",
|
|
430
|
+
"x-forwarded-host",
|
|
431
|
+
"x-forwarded-proto",
|
|
413
432
|
]);
|
|
414
433
|
|
|
415
434
|
/**
|
package/src/client.ts
CHANGED
|
@@ -3849,6 +3849,20 @@ function __mreactObserveViewportPrefetchAnchors(root) {
|
|
|
3849
3849
|
|
|
3850
3850
|
function __mreactApplyOutOfOrderFragments(root) {
|
|
3851
3851
|
const fragments = Array.from(root.querySelectorAll("template[data-mreact-oob-fragment]"));
|
|
3852
|
+
const completionMarkers = new Map();
|
|
3853
|
+
for (const marker of root.querySelectorAll("[data-mreact-oob-complete]")) {
|
|
3854
|
+
const id = marker.getAttribute("data-mreact-oob-complete");
|
|
3855
|
+
if (!completionMarkers.has(id)) {
|
|
3856
|
+
completionMarkers.set(id, marker);
|
|
3857
|
+
}
|
|
3858
|
+
}
|
|
3859
|
+
const placeholders = new Map();
|
|
3860
|
+
for (const placeholder of root.querySelectorAll("[data-mreact-oob-placeholder]")) {
|
|
3861
|
+
const id = placeholder.getAttribute("data-mreact-oob-placeholder");
|
|
3862
|
+
if (!placeholders.has(id)) {
|
|
3863
|
+
placeholders.set(id, placeholder);
|
|
3864
|
+
}
|
|
3865
|
+
}
|
|
3852
3866
|
|
|
3853
3867
|
for (const fragment of fragments) {
|
|
3854
3868
|
const id = fragment.getAttribute("data-mreact-oob-fragment");
|
|
@@ -3857,16 +3871,12 @@ function __mreactApplyOutOfOrderFragments(root) {
|
|
|
3857
3871
|
continue;
|
|
3858
3872
|
}
|
|
3859
3873
|
|
|
3860
|
-
const completionMarker =
|
|
3861
|
-
.find((candidate) => candidate.getAttribute("data-mreact-oob-complete") === id);
|
|
3862
|
-
|
|
3874
|
+
const completionMarker = completionMarkers.get(id);
|
|
3863
3875
|
if (completionMarker === undefined) {
|
|
3864
3876
|
continue;
|
|
3865
3877
|
}
|
|
3866
3878
|
|
|
3867
|
-
const placeholder =
|
|
3868
|
-
.find((candidate) => candidate.getAttribute("data-mreact-oob-placeholder") === id);
|
|
3869
|
-
|
|
3879
|
+
const placeholder = placeholders.get(id);
|
|
3870
3880
|
if (placeholder === undefined) {
|
|
3871
3881
|
continue;
|
|
3872
3882
|
}
|
package/src/config.ts
CHANGED
|
@@ -26,6 +26,7 @@ export interface AppRouterProductionOptions {
|
|
|
26
26
|
|
|
27
27
|
export interface AppRouterProjectOptions {
|
|
28
28
|
assetBaseUrl?: string | undefined;
|
|
29
|
+
buildConcurrency?: number | undefined;
|
|
29
30
|
buildTargets?: readonly AppRouterBuildTarget[] | undefined;
|
|
30
31
|
clientSourceMaps?: AppRouterClientSourceMapOption | undefined;
|
|
31
32
|
/**
|
|
@@ -48,6 +49,7 @@ export interface AppRouterProjectOptions {
|
|
|
48
49
|
export interface ResolvedAppRouterProject {
|
|
49
50
|
allowedSourceDirs: readonly string[];
|
|
50
51
|
assetBaseUrl?: string | undefined;
|
|
52
|
+
buildConcurrency?: number | undefined;
|
|
51
53
|
buildTargets: readonly AppRouterBuildTarget[];
|
|
52
54
|
clientSourceMaps: AppRouterClientSourceMapMode;
|
|
53
55
|
clientConsolePureFunctions?: readonly string[] | undefined;
|
|
@@ -76,6 +78,9 @@ export function resolveAppRouterProjectOptions(
|
|
|
76
78
|
resolveProjectPath(appDir, directory, "allowedSourceDirs"),
|
|
77
79
|
),
|
|
78
80
|
...(options.assetBaseUrl === undefined ? {} : { assetBaseUrl: options.assetBaseUrl }),
|
|
81
|
+
...(options.buildConcurrency === undefined
|
|
82
|
+
? {}
|
|
83
|
+
: { buildConcurrency: options.buildConcurrency }),
|
|
79
84
|
buildTargets: resolveBuildTargets(options.buildTargets),
|
|
80
85
|
clientSourceMaps: resolveClientSourceMapMode(options.clientSourceMaps),
|
|
81
86
|
...(clientConsolePureFunctions === undefined ? {} : { clientConsolePureFunctions }),
|
|
@@ -98,6 +103,9 @@ export function resolveAppRouterProjectOptions(
|
|
|
98
103
|
resolveProjectPath(projectRoot, directory, "allowedSourceDirs"),
|
|
99
104
|
),
|
|
100
105
|
...(options.assetBaseUrl === undefined ? {} : { assetBaseUrl: options.assetBaseUrl }),
|
|
106
|
+
...(options.buildConcurrency === undefined
|
|
107
|
+
? {}
|
|
108
|
+
: { buildConcurrency: options.buildConcurrency }),
|
|
101
109
|
buildTargets: resolveBuildTargets(options.buildTargets),
|
|
102
110
|
clientSourceMaps: resolveClientSourceMapMode(options.clientSourceMaps),
|
|
103
111
|
...(clientConsolePureFunctions === undefined ? {} : { clientConsolePureFunctions }),
|
package/src/csrf.ts
CHANGED
|
@@ -8,7 +8,7 @@ const formFieldCsrf = "__mreact_csrf";
|
|
|
8
8
|
export const formCsrfFieldName = formFieldCsrf;
|
|
9
9
|
|
|
10
10
|
export function serverActionCookie(csrfToken: string): string {
|
|
11
|
-
const
|
|
11
|
+
const secure = shouldUseSecureCsrfCookie();
|
|
12
12
|
const parts = [
|
|
13
13
|
`${currentCsrfCookieName()}=${encodeURIComponent(csrfToken)}`,
|
|
14
14
|
"Path=/",
|
|
@@ -16,7 +16,7 @@ export function serverActionCookie(csrfToken: string): string {
|
|
|
16
16
|
"HttpOnly",
|
|
17
17
|
];
|
|
18
18
|
|
|
19
|
-
if (
|
|
19
|
+
if (secure) {
|
|
20
20
|
parts.push("Secure");
|
|
21
21
|
}
|
|
22
22
|
|
|
@@ -97,16 +97,23 @@ function randomToken(): string {
|
|
|
97
97
|
return `${hex.slice(0, 4).join("")}-${hex.slice(4, 6).join("")}-${hex.slice(6, 8).join("")}-${hex.slice(8, 10).join("")}-${hex.slice(10).join("")}`;
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
-
function
|
|
101
|
-
return
|
|
100
|
+
function isLocalCsrfEnvironment(): boolean {
|
|
101
|
+
return (
|
|
102
|
+
typeof process !== "undefined" &&
|
|
103
|
+
(process.env?.NODE_ENV === "development" || process.env?.NODE_ENV === "test")
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function shouldUseSecureCsrfCookie(): boolean {
|
|
108
|
+
return !isLocalCsrfEnvironment();
|
|
102
109
|
}
|
|
103
110
|
|
|
104
111
|
function currentCsrfCookieName(): string {
|
|
105
|
-
return
|
|
112
|
+
return shouldUseSecureCsrfCookie() ? csrfCookieNameProduction : csrfCookieNameDevelopment;
|
|
106
113
|
}
|
|
107
114
|
|
|
108
115
|
function csrfCookieNamesRead(): readonly string[] {
|
|
109
|
-
return
|
|
116
|
+
return shouldUseSecureCsrfCookie()
|
|
110
117
|
? [csrfCookieNameProduction]
|
|
111
118
|
: [csrfCookieNameProduction, csrfCookieNameDevelopment];
|
|
112
119
|
}
|