@reckona/mreact-router 0.0.182 → 0.0.184
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 +271 -8
- package/dist/build.js.map +1 -1
- package/dist/bundle-pipeline.d.ts.map +1 -1
- package/dist/bundle-pipeline.js +9 -0
- package/dist/bundle-pipeline.js.map +1 -1
- package/dist/client.d.ts +1 -0
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +193 -2
- 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/module-runner.d.ts.map +1 -1
- package/dist/module-runner.js +132 -1
- package/dist/module-runner.js.map +1 -1
- package/dist/render.d.ts +2 -0
- package/dist/render.d.ts.map +1 -1
- package/dist/render.js +24 -13
- package/dist/render.js.map +1 -1
- package/dist/vite.d.ts.map +1 -1
- package/dist/vite.js +34 -1
- package/dist/vite.js.map +1 -1
- package/package.json +11 -11
- package/src/build.ts +406 -10
- package/src/bundle-pipeline.ts +9 -0
- package/src/client.ts +195 -2
- package/src/config.ts +8 -0
- package/src/module-runner.ts +185 -1
- package/src/render.ts +37 -15
- package/src/vite.ts +39 -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([
|
|
@@ -601,9 +659,7 @@ export async function buildApp(options: BuildAppOptions): Promise<BuildAppResult
|
|
|
601
659
|
),
|
|
602
660
|
]);
|
|
603
661
|
const clientRoutes = clientBundle.routes;
|
|
604
|
-
const navigationRuntimeScript = clientRoutes.some(
|
|
605
|
-
(route) => route.navigation === true && !route.client,
|
|
606
|
-
)
|
|
662
|
+
const navigationRuntimeScript = clientRoutes.some((route) => route.navigation === true)
|
|
607
663
|
? shouldTrackBuildPhases === false
|
|
608
664
|
? await writeNavigationRuntimeBundle(clientDir, project.clientConsolePureFunctions)
|
|
609
665
|
: await timeBuildPhase(timingSink, progressSink, "navigationRuntime", () =>
|
|
@@ -614,7 +670,7 @@ export async function buildApp(options: BuildAppOptions): Promise<BuildAppResult
|
|
|
614
670
|
navigationRuntimeScript === undefined
|
|
615
671
|
? clientRoutes
|
|
616
672
|
: clientRoutes.map((route) =>
|
|
617
|
-
route.navigation === true
|
|
673
|
+
route.navigation === true
|
|
618
674
|
? { ...route, navigationScript: navigationRuntimeScript }
|
|
619
675
|
: route,
|
|
620
676
|
);
|
|
@@ -783,9 +839,284 @@ export async function buildApp(options: BuildAppOptions): Promise<BuildAppResult
|
|
|
783
839
|
});
|
|
784
840
|
}
|
|
785
841
|
|
|
842
|
+
if (incrementalBuildFingerprint !== undefined) {
|
|
843
|
+
await writeIncrementalBuildCacheManifest(options.outDir, incrementalBuildFingerprint);
|
|
844
|
+
}
|
|
845
|
+
|
|
786
846
|
return { routes };
|
|
787
847
|
}
|
|
788
848
|
|
|
849
|
+
const incrementalBuildCacheFilename = "build-cache.json";
|
|
850
|
+
const incrementalBuildCacheVersion = 1;
|
|
851
|
+
|
|
852
|
+
async function createIncrementalBuildCacheFingerprint(options: {
|
|
853
|
+
buildTargets: readonly AppRouterBuildTarget[];
|
|
854
|
+
files: Record<string, string>;
|
|
855
|
+
project: ResolvedAppRouterProject;
|
|
856
|
+
routes: readonly AppRoute[];
|
|
857
|
+
viteDefine: UserConfig["define"] | undefined;
|
|
858
|
+
vitePlugins: readonly PluginOption[] | undefined;
|
|
859
|
+
}): Promise<string | undefined> {
|
|
860
|
+
if (options.vitePlugins !== undefined && options.vitePlugins.length > 0) {
|
|
861
|
+
return undefined;
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
let define: string;
|
|
865
|
+
try {
|
|
866
|
+
const serializedDefine = JSON.stringify(options.viteDefine ?? null);
|
|
867
|
+
if (serializedDefine === undefined) {
|
|
868
|
+
return undefined;
|
|
869
|
+
}
|
|
870
|
+
define = serializedDefine;
|
|
871
|
+
} catch {
|
|
872
|
+
return undefined;
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
const [publicAssets, appConventionAssets] = await Promise.all([
|
|
876
|
+
collectBuildInputFileHashes(options.project.publicDir, options.project.projectRoot),
|
|
877
|
+
collectAppConventionAssetInputHashes(options.project.routesDir, options.project.projectRoot),
|
|
878
|
+
]);
|
|
879
|
+
const sourceFiles = Object.keys(options.files)
|
|
880
|
+
.sort()
|
|
881
|
+
.map((file) => [normalizeBuildInputPath(file), hashText(options.files[file] ?? "")] as const);
|
|
882
|
+
const routes = options.routes
|
|
883
|
+
.map((route) => ({
|
|
884
|
+
file: normalizeBuildInputPath(relative(options.project.projectRoot, route.file)),
|
|
885
|
+
kind: route.kind,
|
|
886
|
+
path: route.path,
|
|
887
|
+
}))
|
|
888
|
+
.sort((left, right) => left.file.localeCompare(right.file));
|
|
889
|
+
const payload = {
|
|
890
|
+
appDir: normalizeBuildInputPath(relative(options.project.projectRoot, options.project.routesDir)),
|
|
891
|
+
assetBaseUrl: options.project.assetBaseUrl ?? null,
|
|
892
|
+
clientConsolePureFunctions: options.project.clientConsolePureFunctions ?? [],
|
|
893
|
+
clientSourceMaps: options.project.clientSourceMaps,
|
|
894
|
+
define,
|
|
895
|
+
nodeEnv: process.env.NODE_ENV ?? null,
|
|
896
|
+
publicAssetBaseUrl: options.project.publicAssetBaseUrl ?? null,
|
|
897
|
+
publicDir: normalizeBuildInputPath(relative(options.project.projectRoot, options.project.publicDir)),
|
|
898
|
+
routes,
|
|
899
|
+
sourceDirs: options.project.allowedSourceDirs
|
|
900
|
+
.map((directory) => normalizeBuildInputPath(relative(options.project.projectRoot, directory)))
|
|
901
|
+
.sort(),
|
|
902
|
+
sourceFiles,
|
|
903
|
+
targets: [...options.buildTargets].sort(),
|
|
904
|
+
version: incrementalBuildCacheVersion,
|
|
905
|
+
publicAssets,
|
|
906
|
+
appConventionAssets,
|
|
907
|
+
};
|
|
908
|
+
|
|
909
|
+
return createHash("sha256").update(JSON.stringify(payload)).digest("hex");
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
async function isIncrementalBuildCacheHit(options: {
|
|
913
|
+
buildTargets: readonly AppRouterBuildTarget[];
|
|
914
|
+
fingerprint: string;
|
|
915
|
+
outDir: string;
|
|
916
|
+
}): Promise<boolean> {
|
|
917
|
+
const cache = await readJsonBuildOutput<IncrementalBuildCacheManifest>(
|
|
918
|
+
join(options.outDir, incrementalBuildCacheFilename),
|
|
919
|
+
);
|
|
920
|
+
if (cache === undefined) {
|
|
921
|
+
return false;
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
return (
|
|
925
|
+
cache.version === incrementalBuildCacheVersion &&
|
|
926
|
+
cache.fingerprint === options.fingerprint &&
|
|
927
|
+
(await hasRequiredIncrementalBuildOutputs(options.outDir, options.buildTargets))
|
|
928
|
+
);
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
async function hasRequiredIncrementalBuildOutputs(
|
|
932
|
+
outDir: string,
|
|
933
|
+
buildTargets: readonly AppRouterBuildTarget[],
|
|
934
|
+
): Promise<boolean> {
|
|
935
|
+
const serverManifestFile = join(outDir, "server", "manifest.json");
|
|
936
|
+
const clientManifestFile = join(outDir, "client", "manifest.json");
|
|
937
|
+
const requiredFiles = [
|
|
938
|
+
serverManifestFile,
|
|
939
|
+
join(outDir, "server", "import-policy.json"),
|
|
940
|
+
clientManifestFile,
|
|
941
|
+
join(outDir, "client", ".vite", "manifest.json"),
|
|
942
|
+
join(outDir, "routes.d.ts"),
|
|
943
|
+
join(outDir, "public-assets.d.ts"),
|
|
944
|
+
...(buildTargets.includes("aws-lambda")
|
|
945
|
+
? [join(outDir, "aws-lambda", "mreact-handler.mjs")]
|
|
946
|
+
: []),
|
|
947
|
+
...(buildTargets.includes("cloudflare") ? [join(outDir, "cloudflare", "worker.mjs")] : []),
|
|
948
|
+
];
|
|
949
|
+
const [serverManifest, clientManifest] = await Promise.all([
|
|
950
|
+
readJsonBuildOutput<IncrementalBuildServerManifestOutputs>(serverManifestFile),
|
|
951
|
+
readJsonBuildOutput<IncrementalBuildClientManifestOutputs>(clientManifestFile),
|
|
952
|
+
]);
|
|
953
|
+
if (serverManifest === undefined || clientManifest === undefined) {
|
|
954
|
+
return false;
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
for (const file of [
|
|
958
|
+
...Object.values(serverManifest.serverModuleFiles ?? {}),
|
|
959
|
+
...Object.values(serverManifest.serverModuleRenderFiles ?? {}),
|
|
960
|
+
...Object.values(serverManifest.serverModuleRequestFiles ?? {}),
|
|
961
|
+
]) {
|
|
962
|
+
requiredFiles.push(join(outDir, "server", file));
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
for (const file of collectClientManifestOutputFiles(clientManifest)) {
|
|
966
|
+
requiredFiles.push(join(outDir, "client", file));
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
for (const file of requiredFiles) {
|
|
970
|
+
if (!(await isExistingFile(file))) {
|
|
971
|
+
return false;
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
return true;
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
function collectClientManifestOutputFiles(
|
|
979
|
+
manifest: IncrementalBuildClientManifestOutputs,
|
|
980
|
+
): string[] {
|
|
981
|
+
const files = new Set<string>();
|
|
982
|
+
|
|
983
|
+
for (const asset of manifest.assets ?? []) {
|
|
984
|
+
files.add(asset);
|
|
985
|
+
}
|
|
986
|
+
for (const asset of manifest.publicAssets ?? []) {
|
|
987
|
+
files.add(asset);
|
|
988
|
+
files.add(`public/${asset}`);
|
|
989
|
+
}
|
|
990
|
+
for (const route of manifest.routes) {
|
|
991
|
+
for (const file of [
|
|
992
|
+
route.script,
|
|
993
|
+
route.sourceMap,
|
|
994
|
+
route.navigationScript,
|
|
995
|
+
...(route.css ?? []),
|
|
996
|
+
...(route.imports ?? []),
|
|
997
|
+
]) {
|
|
998
|
+
if (file !== undefined) {
|
|
999
|
+
files.add(file);
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
return [...files];
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
async function readJsonBuildOutput<T>(file: string): Promise<T | undefined> {
|
|
1008
|
+
try {
|
|
1009
|
+
return JSON.parse(await readFile(file, "utf8")) as T;
|
|
1010
|
+
} catch (error) {
|
|
1011
|
+
if (
|
|
1012
|
+
(isNodeError(error) && error.code === "ENOENT") ||
|
|
1013
|
+
error instanceof SyntaxError
|
|
1014
|
+
) {
|
|
1015
|
+
return undefined;
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
throw error;
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
async function writeIncrementalBuildCacheManifest(
|
|
1023
|
+
outDir: string,
|
|
1024
|
+
fingerprint: string,
|
|
1025
|
+
): Promise<void> {
|
|
1026
|
+
const cache = {
|
|
1027
|
+
fingerprint,
|
|
1028
|
+
version: incrementalBuildCacheVersion,
|
|
1029
|
+
} satisfies IncrementalBuildCacheManifest;
|
|
1030
|
+
|
|
1031
|
+
await writeFile(
|
|
1032
|
+
join(outDir, incrementalBuildCacheFilename),
|
|
1033
|
+
`${JSON.stringify(cache, null, 2)}\n`,
|
|
1034
|
+
);
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
async function collectBuildInputFileHashes(
|
|
1038
|
+
directory: string,
|
|
1039
|
+
projectRoot: string,
|
|
1040
|
+
): Promise<readonly (readonly [string, string])[]> {
|
|
1041
|
+
if (!(await isPublicAssetDirectory(directory))) {
|
|
1042
|
+
return [];
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
const files = await collectFiles(directory);
|
|
1046
|
+
const hashes = await mapWithBuildConcurrency(
|
|
1047
|
+
files,
|
|
1048
|
+
async (file) =>
|
|
1049
|
+
[
|
|
1050
|
+
normalizeBuildInputPath(relative(projectRoot, file)),
|
|
1051
|
+
hashBuffer(await readFile(file)),
|
|
1052
|
+
] as const,
|
|
1053
|
+
);
|
|
1054
|
+
|
|
1055
|
+
return hashes.sort(([left], [right]) => left.localeCompare(right));
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
async function collectAppConventionAssetInputHashes(
|
|
1059
|
+
appDir: string,
|
|
1060
|
+
projectRoot: string,
|
|
1061
|
+
): Promise<readonly (readonly [string, string])[]> {
|
|
1062
|
+
const entries = await readdir(appDir, { withFileTypes: true });
|
|
1063
|
+
const assetFiles = entries
|
|
1064
|
+
.filter((entry) => entry.isFile())
|
|
1065
|
+
.map((entry) => join(appDir, entry.name))
|
|
1066
|
+
.filter((file) => isAppFileConventionAsset(file, appDir));
|
|
1067
|
+
const hashes = await mapWithBuildConcurrency(
|
|
1068
|
+
assetFiles,
|
|
1069
|
+
async (file) =>
|
|
1070
|
+
[
|
|
1071
|
+
normalizeBuildInputPath(relative(projectRoot, file)),
|
|
1072
|
+
hashBuffer(await readFile(file)),
|
|
1073
|
+
] as const,
|
|
1074
|
+
);
|
|
1075
|
+
|
|
1076
|
+
return hashes.sort(([left], [right]) => left.localeCompare(right));
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
async function isExistingFile(file: string): Promise<boolean> {
|
|
1080
|
+
try {
|
|
1081
|
+
return (await stat(file)).isFile();
|
|
1082
|
+
} catch (error) {
|
|
1083
|
+
if (isNodeError(error) && error.code === "ENOENT") {
|
|
1084
|
+
return false;
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
throw error;
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
function normalizeBuildInputPath(path: string): string {
|
|
1092
|
+
return path.split(sep).join("/");
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
function hashBuffer(buffer: Buffer): string {
|
|
1096
|
+
return createHash("sha256").update(buffer).digest("hex").slice(0, 16);
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
function defaultBuildConcurrency(): number {
|
|
1100
|
+
return Math.max(1, Math.min(maxDefaultBuildConcurrency, availableParallelism()));
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
function currentBuildConcurrency(): number {
|
|
1104
|
+
return buildConcurrencyStorage.getStore() ?? defaultBuildConcurrency();
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
function resolveBuildConcurrency(
|
|
1108
|
+
value: number | undefined,
|
|
1109
|
+
cores = availableParallelism(),
|
|
1110
|
+
): number {
|
|
1111
|
+
const resolved = value ?? Math.max(1, Math.min(maxDefaultBuildConcurrency, cores));
|
|
1112
|
+
|
|
1113
|
+
if (!Number.isSafeInteger(resolved) || resolved < 1) {
|
|
1114
|
+
throw new Error("mreactRouter buildConcurrency must be a positive integer.");
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
return resolved;
|
|
1118
|
+
}
|
|
1119
|
+
|
|
789
1120
|
function typedRoutesDeclaration(routes: readonly AppRoute[]): string {
|
|
790
1121
|
const routePaths = Array.from(
|
|
791
1122
|
new Set(
|
|
@@ -803,21 +1134,60 @@ function typedRoutesDeclaration(routes: readonly AppRoute[]): string {
|
|
|
803
1134
|
return left.localeCompare(right);
|
|
804
1135
|
});
|
|
805
1136
|
const routeUnion = routePaths.map((routePath) => JSON.stringify(routePath)).join(" | ");
|
|
1137
|
+
const routeParamEntries = routePaths.map(
|
|
1138
|
+
(routePath) => ` readonly ${JSON.stringify(routePath)}: ${routeParamsType(routePath)};`,
|
|
1139
|
+
);
|
|
806
1140
|
return [
|
|
807
|
-
`import type { AppRouteLinkHref as MreactAppRouteLinkHref } from "@reckona/mreact-router";`,
|
|
1141
|
+
`import type { AppRouteLinkHref as MreactAppRouteLinkHref, RouteParamsFor as MreactRouteParamsFor } from "@reckona/mreact-router";`,
|
|
808
1142
|
``,
|
|
809
1143
|
`export type AppRoutePath = ${routeUnion === "" ? "never" : routeUnion};`,
|
|
810
1144
|
`export type AppRouteHref = MreactAppRouteLinkHref<AppRoutePath>;`,
|
|
1145
|
+
`export type AppRouteParams<Path extends AppRoutePath> = MreactRouteParamsFor<Path>;`,
|
|
1146
|
+
`export interface AppRouteParamMap {`,
|
|
1147
|
+
...(routeParamEntries.length === 0 ? [` readonly [path: string]: never;`] : routeParamEntries),
|
|
1148
|
+
`}`,
|
|
811
1149
|
``,
|
|
812
1150
|
`declare module "@reckona/mreact-router/link" {`,
|
|
813
1151
|
` interface AppRouteDeclarations {`,
|
|
814
1152
|
` readonly path: AppRoutePath;`,
|
|
1153
|
+
` readonly params: AppRouteParamMap;`,
|
|
815
1154
|
` }`,
|
|
816
1155
|
`}`,
|
|
817
1156
|
``,
|
|
818
1157
|
].join("\n");
|
|
819
1158
|
}
|
|
820
1159
|
|
|
1160
|
+
function routeParamsType(routePath: string): string {
|
|
1161
|
+
const params = routePath
|
|
1162
|
+
.split("/")
|
|
1163
|
+
.flatMap((segment): Array<{ catchAll: boolean; name: string }> => {
|
|
1164
|
+
if (segment.startsWith(":...")) {
|
|
1165
|
+
return [{ catchAll: true, name: segment.slice(4) }];
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
if (segment.startsWith(":")) {
|
|
1169
|
+
return [{ catchAll: false, name: segment.slice(1) }];
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
return [];
|
|
1173
|
+
});
|
|
1174
|
+
|
|
1175
|
+
if (params.length === 0) {
|
|
1176
|
+
return "Record<never, never>";
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
return `{ ${params
|
|
1180
|
+
.map(
|
|
1181
|
+
(param) =>
|
|
1182
|
+
`readonly ${propertyKeyForType(param.name)}: ${param.catchAll ? "readonly string[]" : "string"}`,
|
|
1183
|
+
)
|
|
1184
|
+
.join("; ")} }`;
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
function propertyKeyForType(value: string): string {
|
|
1188
|
+
return /^[A-Za-z_$][\w$]*$/.test(value) ? value : JSON.stringify(value);
|
|
1189
|
+
}
|
|
1190
|
+
|
|
821
1191
|
function typedPublicAssetsDeclaration(publicAssets: readonly string[]): string {
|
|
822
1192
|
const assetUnion = publicAssets.map((assetPath) => JSON.stringify(assetPath)).join(" | ");
|
|
823
1193
|
|
|
@@ -861,7 +1231,7 @@ function roundBuildPhaseMs(value: number): number {
|
|
|
861
1231
|
async function mapWithBuildConcurrency<T, R>(
|
|
862
1232
|
items: readonly T[],
|
|
863
1233
|
map: (item: T, index: number) => Promise<R>,
|
|
864
|
-
concurrency =
|
|
1234
|
+
concurrency = currentBuildConcurrency(),
|
|
865
1235
|
): Promise<R[]> {
|
|
866
1236
|
if (items.length === 0) {
|
|
867
1237
|
return [];
|
|
@@ -897,6 +1267,27 @@ export async function __mapWithBuildConcurrencyForTests<T, R>(
|
|
|
897
1267
|
return await mapWithBuildConcurrency(items, map, concurrency);
|
|
898
1268
|
}
|
|
899
1269
|
|
|
1270
|
+
async function mapServerOutputsWithBuildConcurrency<R>(
|
|
1271
|
+
serverOutputs: readonly ServerOutputMode[],
|
|
1272
|
+
map: (serverOutput: ServerOutputMode, index: number) => Promise<R>,
|
|
1273
|
+
): Promise<R[]> {
|
|
1274
|
+
return await mapWithBuildConcurrency(serverOutputs, map, 1);
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
export async function __mapServerOutputsWithBuildConcurrencyForTests<R>(
|
|
1278
|
+
serverOutputs: readonly ServerOutputMode[],
|
|
1279
|
+
map: (serverOutput: ServerOutputMode, index: number) => Promise<R>,
|
|
1280
|
+
): Promise<R[]> {
|
|
1281
|
+
return await mapServerOutputsWithBuildConcurrency(serverOutputs, map);
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
export function __resolveBuildConcurrencyForTests(
|
|
1285
|
+
value: number | undefined,
|
|
1286
|
+
cores: number,
|
|
1287
|
+
): number {
|
|
1288
|
+
return resolveBuildConcurrency(value, cores);
|
|
1289
|
+
}
|
|
1290
|
+
|
|
900
1291
|
async function analyzeBuildRouteSources(options: {
|
|
901
1292
|
clientRouteInferenceCache: ClientRouteInferenceCache;
|
|
902
1293
|
files: Record<string, string>;
|
|
@@ -2479,7 +2870,7 @@ async function buildServerModuleArtifacts(options: {
|
|
|
2479
2870
|
});
|
|
2480
2871
|
}
|
|
2481
2872
|
|
|
2482
|
-
const serverOutputArtifacts = await
|
|
2873
|
+
const serverOutputArtifacts = await mapServerOutputsWithBuildConcurrency(
|
|
2483
2874
|
serverOutputs,
|
|
2484
2875
|
async (serverOutput) => {
|
|
2485
2876
|
const output = await transformServerRouteSource({
|
|
@@ -5386,6 +5777,10 @@ function cloudflareWorkspaceRuntimePlugin(): RouterCompatPlugin {
|
|
|
5386
5777
|
"@reckona/mreact-compat/scheduler",
|
|
5387
5778
|
packageFile("react-compat", "@reckona/mreact-compat", "scheduler"),
|
|
5388
5779
|
],
|
|
5780
|
+
[
|
|
5781
|
+
"@reckona/mreact-compat/server",
|
|
5782
|
+
packageFile("react-compat", "@reckona/mreact-compat", "server"),
|
|
5783
|
+
],
|
|
5389
5784
|
["@reckona/mreact-query", packageFile("query", "@reckona/mreact-query", "index")],
|
|
5390
5785
|
[
|
|
5391
5786
|
"@reckona/mreact-reactive-core",
|
|
@@ -5775,6 +6170,7 @@ async function writeClientRouteBundles(options: {
|
|
|
5775
6170
|
scriptBasename: routeOutput.chunk.fileName.split("/").pop() ?? "route.js",
|
|
5776
6171
|
sourceMaps: options.sourceMaps,
|
|
5777
6172
|
});
|
|
6173
|
+
const navigation = entry.navigation === true || entry.build.clientNavigation !== false;
|
|
5778
6174
|
|
|
5779
6175
|
return {
|
|
5780
6176
|
bytes: Buffer.byteLength(code),
|
|
@@ -5787,7 +6183,7 @@ async function writeClientRouteBundles(options: {
|
|
|
5787
6183
|
: { clientReferenceManifest: entry.build.clientReferenceManifest }),
|
|
5788
6184
|
...(entry.css.length === 0 ? {} : { css: entry.css }),
|
|
5789
6185
|
...(routeOutput.chunk.imports.length === 0 ? {} : { imports: routeOutput.chunk.imports }),
|
|
5790
|
-
...(
|
|
6186
|
+
...(navigation ? { navigation } : {}),
|
|
5791
6187
|
routeId,
|
|
5792
6188
|
script: routeOutput.chunk.fileName,
|
|
5793
6189
|
...(options.sourceMaps === "linked" ? { sourceMap } : {}),
|
package/src/bundle-pipeline.ts
CHANGED
|
@@ -230,6 +230,15 @@ const mreactJsxRuntimeAliasPaths = new Map([
|
|
|
230
230
|
packageName: "@reckona/mreact-compat",
|
|
231
231
|
}),
|
|
232
232
|
],
|
|
233
|
+
[
|
|
234
|
+
"@reckona/mreact-compat/server",
|
|
235
|
+
workspacePackageFile({
|
|
236
|
+
currentFileUrl: import.meta.url,
|
|
237
|
+
entry: "server",
|
|
238
|
+
monorepoDir: "react-compat",
|
|
239
|
+
packageName: "@reckona/mreact-compat",
|
|
240
|
+
}),
|
|
241
|
+
],
|
|
233
242
|
]);
|
|
234
243
|
|
|
235
244
|
export async function bundleRouterModule(
|