@reckona/mreact-router 0.0.182 → 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/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/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/render.d.ts +2 -0
- package/dist/render.d.ts.map +1 -1
- package/dist/render.js +15 -8
- package/dist/render.js.map +1 -1
- package/dist/vite.d.ts.map +1 -1
- package/dist/vite.js +30 -1
- package/dist/vite.js.map +1 -1
- package/package.json +11 -11
- package/src/build.ts +398 -5
- package/src/config.ts +8 -0
- package/src/render.ts +26 -8
- package/src/vite.ts +35 -1
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/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/render.ts
CHANGED
|
@@ -195,6 +195,7 @@ export interface RenderAppRequestOptions {
|
|
|
195
195
|
instrumentation?: RouterInstrumentation | undefined;
|
|
196
196
|
logger?: AppRouterLogger | undefined;
|
|
197
197
|
navigationScripts?: ReadonlyMap<string, string> | undefined;
|
|
198
|
+
onRenderError?: ((error: unknown) => void) | undefined;
|
|
198
199
|
onResponse?: AppRouterResponseHook | undefined;
|
|
199
200
|
queryClient?: QueryClient | undefined;
|
|
200
201
|
request: Request;
|
|
@@ -794,6 +795,7 @@ export type AppRouterMiddlewareResult =
|
|
|
794
795
|
export async function resolveAppRouterMiddleware(options: {
|
|
795
796
|
appDir: string;
|
|
796
797
|
define?: UserConfig["define"] | undefined;
|
|
798
|
+
devServerModuleCacheVersion?: string | undefined;
|
|
797
799
|
importPolicy?: AppRouterImportPolicy | undefined;
|
|
798
800
|
instrumentation?: RouterInstrumentation | undefined;
|
|
799
801
|
middlewareControl?: RouteMiddlewareControl | undefined;
|
|
@@ -862,6 +864,7 @@ async function renderAppRequestInternal(
|
|
|
862
864
|
? ({ request: options.request, type: "continue" } satisfies AppRouterMiddlewareResult)
|
|
863
865
|
: await resolveAppRouterMiddleware({
|
|
864
866
|
appDir: options.appDir,
|
|
867
|
+
devServerModuleCacheVersion: options.devServerModuleCacheVersion,
|
|
865
868
|
importPolicy: options.importPolicy,
|
|
866
869
|
instrumentation: options.instrumentation,
|
|
867
870
|
middlewareControl,
|
|
@@ -1097,6 +1100,7 @@ async function renderAppRequestInternal(
|
|
|
1097
1100
|
filename: matched.route.file,
|
|
1098
1101
|
importPolicy: options.importPolicy,
|
|
1099
1102
|
instrumentation: options.instrumentation,
|
|
1103
|
+
devServerModuleCacheVersion: options.devServerModuleCacheVersion,
|
|
1100
1104
|
request: options.request,
|
|
1101
1105
|
routeId: routeIdForPath(matched.route.path),
|
|
1102
1106
|
routePath: matched.route.path,
|
|
@@ -1629,6 +1633,7 @@ async function renderAppRequestInternal(
|
|
|
1629
1633
|
filename: "error.mreact.tsx",
|
|
1630
1634
|
pageFile: matched.route.file,
|
|
1631
1635
|
});
|
|
1636
|
+
options.onRenderError?.(error);
|
|
1632
1637
|
|
|
1633
1638
|
const response = await renderSpecialRoute({
|
|
1634
1639
|
appDir: options.appDir,
|
|
@@ -2370,6 +2375,7 @@ function devExternalSourceDirs(
|
|
|
2370
2375
|
async function runMiddleware(options: {
|
|
2371
2376
|
appDir: string;
|
|
2372
2377
|
define?: UserConfig["define"] | undefined;
|
|
2378
|
+
devServerModuleCacheVersion?: string | undefined;
|
|
2373
2379
|
importPolicy?: AppRouterImportPolicy | undefined;
|
|
2374
2380
|
instrumentation?: RouterInstrumentation | undefined;
|
|
2375
2381
|
middlewareControl?: RouteMiddlewareControl | undefined;
|
|
@@ -2440,6 +2446,7 @@ async function runMiddleware(options: {
|
|
|
2440
2446
|
serverModules: options.serverModules,
|
|
2441
2447
|
serverModuleCacheVersion: options.serverModuleCacheVersion,
|
|
2442
2448
|
serverSourceFiles: options.serverSourceFiles,
|
|
2449
|
+
devServerModuleCacheVersion: options.devServerModuleCacheVersion,
|
|
2443
2450
|
vitePlugins: options.vitePlugins,
|
|
2444
2451
|
});
|
|
2445
2452
|
} finally {
|
|
@@ -2538,6 +2545,7 @@ async function loadMiddlewareModule(options: {
|
|
|
2538
2545
|
appDir: string;
|
|
2539
2546
|
code?: string | undefined;
|
|
2540
2547
|
define?: UserConfig["define"] | undefined;
|
|
2548
|
+
devServerModuleCacheVersion?: string | undefined;
|
|
2541
2549
|
file: string;
|
|
2542
2550
|
importPolicy?: AppRouterImportPolicy | undefined;
|
|
2543
2551
|
serverModules?: ReadonlyMap<string, BuiltServerModuleArtifact> | undefined;
|
|
@@ -2552,10 +2560,12 @@ async function loadMiddlewareModule(options: {
|
|
|
2552
2560
|
options.serverModuleCacheVersion,
|
|
2553
2561
|
options.serverSourceFiles,
|
|
2554
2562
|
));
|
|
2563
|
+
const moduleCacheVersion =
|
|
2564
|
+
options.serverModuleCacheVersion ?? options.devServerModuleCacheVersion;
|
|
2555
2565
|
const cacheKey =
|
|
2556
|
-
|
|
2566
|
+
moduleCacheVersion === undefined
|
|
2557
2567
|
? undefined
|
|
2558
|
-
: `middleware\0${options.
|
|
2568
|
+
: `middleware\0${options.serverModuleCacheVersion === undefined ? "dev" : "build"}\0${options.appDir}\0${options.file}\0${moduleCacheVersion}\0${memoizedHashText(code)}\0${importPolicyCacheKey(options.importPolicy)}\0${viteDefineCacheKey(options.define)}\0${vitePluginsCacheKey(options.vitePlugins)}`;
|
|
2559
2569
|
|
|
2560
2570
|
if (cacheKey !== undefined) {
|
|
2561
2571
|
const cached = readRouterRuntimeCacheEntry(
|
|
@@ -2576,6 +2586,7 @@ async function loadMiddlewareModule(options: {
|
|
|
2576
2586
|
file: options.file,
|
|
2577
2587
|
importPolicy: options.importPolicy,
|
|
2578
2588
|
prebuiltArtifact: prebuiltRequestModuleArtifact(options.serverModules, options.file, code),
|
|
2589
|
+
devServerModuleCacheVersion: options.devServerModuleCacheVersion,
|
|
2579
2590
|
serverModuleCacheVersion: options.serverModuleCacheVersion,
|
|
2580
2591
|
vitePlugins: options.vitePlugins,
|
|
2581
2592
|
}).catch((error) => {
|
|
@@ -2637,6 +2648,7 @@ async function loadBundledMiddlewareModule(options: {
|
|
|
2637
2648
|
appDir: string;
|
|
2638
2649
|
code: string;
|
|
2639
2650
|
define?: UserConfig["define"] | undefined;
|
|
2651
|
+
devServerModuleCacheVersion?: string | undefined;
|
|
2640
2652
|
file: string;
|
|
2641
2653
|
importPolicy?: AppRouterImportPolicy | undefined;
|
|
2642
2654
|
prebuiltArtifact?: BuiltServerModuleOutputLike | undefined;
|
|
@@ -2663,10 +2675,10 @@ async function loadBundledMiddlewareModule(options: {
|
|
|
2663
2675
|
}));
|
|
2664
2676
|
|
|
2665
2677
|
return importAppRouterSourceModule<MiddlewareModule>({
|
|
2666
|
-
...(options.serverModuleCacheVersion === undefined
|
|
2678
|
+
...((options.serverModuleCacheVersion ?? options.devServerModuleCacheVersion) === undefined
|
|
2667
2679
|
? {}
|
|
2668
2680
|
: {
|
|
2669
|
-
cacheKey: `middleware:${options.file}:${options.serverModuleCacheVersion}:${memoizedHashText(compiled)}:${viteDefineCacheKey(options.define)}:${vitePluginsCacheKey(options.vitePlugins)}`,
|
|
2681
|
+
cacheKey: `middleware:${options.serverModuleCacheVersion === undefined ? "dev" : "build"}:${options.file}:${options.serverModuleCacheVersion ?? options.devServerModuleCacheVersion}:${memoizedHashText(compiled)}:${viteDefineCacheKey(options.define)}:${vitePluginsCacheKey(options.vitePlugins)}`,
|
|
2670
2682
|
}),
|
|
2671
2683
|
code: compiled,
|
|
2672
2684
|
define: options.define,
|
|
@@ -4447,6 +4459,7 @@ async function loadRouteData(options: {
|
|
|
4447
4459
|
code: string;
|
|
4448
4460
|
context: RouteDataContext;
|
|
4449
4461
|
define?: UserConfig["define"] | undefined;
|
|
4462
|
+
devServerModuleCacheVersion?: string | undefined;
|
|
4450
4463
|
filename: string;
|
|
4451
4464
|
importPolicy?: AppRouterImportPolicy | undefined;
|
|
4452
4465
|
serverModules?: ReadonlyMap<string, BuiltServerModuleArtifact> | undefined;
|
|
@@ -4474,6 +4487,7 @@ async function loadRouteDataWithInstrumentation(options: {
|
|
|
4474
4487
|
code: string;
|
|
4475
4488
|
context: RouteDataContext;
|
|
4476
4489
|
define?: UserConfig["define"] | undefined;
|
|
4490
|
+
devServerModuleCacheVersion?: string | undefined;
|
|
4477
4491
|
filename: string;
|
|
4478
4492
|
importPolicy?: AppRouterImportPolicy | undefined;
|
|
4479
4493
|
instrumentation?: RouterInstrumentation | undefined;
|
|
@@ -4514,16 +4528,19 @@ async function loadRouteLoaderModule(options: {
|
|
|
4514
4528
|
appDir: string;
|
|
4515
4529
|
code: string;
|
|
4516
4530
|
define?: UserConfig["define"] | undefined;
|
|
4531
|
+
devServerModuleCacheVersion?: string | undefined;
|
|
4517
4532
|
filename: string;
|
|
4518
4533
|
importPolicy?: AppRouterImportPolicy | undefined;
|
|
4519
4534
|
serverModules?: ReadonlyMap<string, BuiltServerModuleArtifact> | undefined;
|
|
4520
4535
|
serverModuleCacheVersion?: string | undefined;
|
|
4521
4536
|
vitePlugins?: readonly PluginOption[] | undefined;
|
|
4522
4537
|
}): Promise<RouteLoaderModule> {
|
|
4538
|
+
const moduleCacheVersion =
|
|
4539
|
+
options.serverModuleCacheVersion ?? options.devServerModuleCacheVersion;
|
|
4523
4540
|
const cacheKey =
|
|
4524
|
-
|
|
4541
|
+
moduleCacheVersion === undefined
|
|
4525
4542
|
? undefined
|
|
4526
|
-
: `${options.
|
|
4543
|
+
: `${options.serverModuleCacheVersion === undefined ? "dev" : "build"}\0${options.appDir}\0${options.filename}\0${moduleCacheVersion}\0${memoizedHashText(options.code)}\0${importPolicyCacheKey(options.importPolicy)}\0${viteDefineCacheKey(options.define)}\0${vitePluginsCacheKey(options.vitePlugins)}`;
|
|
4527
4544
|
|
|
4528
4545
|
if (cacheKey !== undefined) {
|
|
4529
4546
|
const cached = readRouterRuntimeCacheEntry(
|
|
@@ -4602,6 +4619,7 @@ async function loadBundledRouteLoaderModule(options: {
|
|
|
4602
4619
|
appDir: string;
|
|
4603
4620
|
code: string;
|
|
4604
4621
|
define?: UserConfig["define"] | undefined;
|
|
4622
|
+
devServerModuleCacheVersion?: string | undefined;
|
|
4605
4623
|
filename: string;
|
|
4606
4624
|
importPolicy?: AppRouterImportPolicy | undefined;
|
|
4607
4625
|
prebuiltArtifact?: BuiltServerModuleOutputLike | undefined;
|
|
@@ -4632,10 +4650,10 @@ async function loadBundledRouteLoaderModule(options: {
|
|
|
4632
4650
|
}));
|
|
4633
4651
|
|
|
4634
4652
|
return await importAppRouterSourceModule<RouteLoaderModule>({
|
|
4635
|
-
...(options.serverModuleCacheVersion === undefined
|
|
4653
|
+
...((options.serverModuleCacheVersion ?? options.devServerModuleCacheVersion) === undefined
|
|
4636
4654
|
? {}
|
|
4637
4655
|
: {
|
|
4638
|
-
cacheKey: `loader:${options.filename}:${options.serverModuleCacheVersion}:${memoizedHashText(code)}:${viteDefineCacheKey(options.define)}`,
|
|
4656
|
+
cacheKey: `loader:${options.serverModuleCacheVersion === undefined ? "dev" : "build"}:${options.filename}:${options.serverModuleCacheVersion ?? options.devServerModuleCacheVersion}:${memoizedHashText(code)}:${viteDefineCacheKey(options.define)}:${vitePluginsCacheKey(options.vitePlugins)}`,
|
|
4639
4657
|
}),
|
|
4640
4658
|
code,
|
|
4641
4659
|
define: options.define,
|