react-bun-ssr 0.1.1 → 0.2.0
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/framework/cli/commands.ts +59 -223
- package/framework/cli/dev-client-watch.ts +281 -0
- package/framework/cli/dev-route-table.ts +71 -0
- package/framework/cli/dev-runtime.ts +382 -0
- package/framework/cli/internal.ts +138 -0
- package/framework/cli/main.ts +27 -31
- package/framework/runtime/build-tools.ts +131 -12
- package/framework/runtime/bun-route-adapter.ts +20 -7
- package/framework/runtime/client-runtime.tsx +73 -132
- package/framework/runtime/client-transition-core.ts +159 -0
- package/framework/runtime/markdown-routes.ts +1 -14
- package/framework/runtime/matcher.ts +11 -11
- package/framework/runtime/module-loader.ts +62 -24
- package/framework/runtime/render.tsx +56 -20
- package/framework/runtime/server.ts +49 -106
- package/framework/runtime/types.ts +12 -1
- package/framework/runtime/utils.ts +3 -0
- package/package.json +2 -1
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
ensureDir,
|
|
6
6
|
existsPath,
|
|
7
7
|
glob,
|
|
8
|
+
removePath,
|
|
8
9
|
statPath,
|
|
9
10
|
writeTextIfChanged,
|
|
10
11
|
} from './io';
|
|
@@ -25,12 +26,20 @@ const BUILD_OPTIMIZE_IMPORTS = [
|
|
|
25
26
|
'@datadog/browser-rum-react',
|
|
26
27
|
];
|
|
27
28
|
|
|
28
|
-
interface ClientEntryFile {
|
|
29
|
+
export interface ClientEntryFile {
|
|
29
30
|
routeId: string;
|
|
30
31
|
entryFilePath: string;
|
|
31
32
|
route: PageRouteDefinition;
|
|
32
33
|
}
|
|
33
34
|
|
|
35
|
+
export interface ClientEntrySyncResult {
|
|
36
|
+
entries: ClientEntryFile[];
|
|
37
|
+
addedEntryPaths: string[];
|
|
38
|
+
changedEntryPaths: string[];
|
|
39
|
+
removedEntryPaths: string[];
|
|
40
|
+
entrySetChanged: boolean;
|
|
41
|
+
}
|
|
42
|
+
|
|
34
43
|
async function walkFiles(rootDir: string): Promise<string[]> {
|
|
35
44
|
if (!(await existsPath(rootDir))) {
|
|
36
45
|
return [];
|
|
@@ -39,6 +48,21 @@ async function walkFiles(rootDir: string): Promise<string[]> {
|
|
|
39
48
|
return glob('**/*', { cwd: rootDir, absolute: true });
|
|
40
49
|
}
|
|
41
50
|
|
|
51
|
+
function buildClientModuleProjectionSource(defaultRef: string, moduleRef: string): string {
|
|
52
|
+
return `projectClientModule(${defaultRef}, ${moduleRef})`;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function toClientEntryFile(options: {
|
|
56
|
+
route: PageRouteDefinition;
|
|
57
|
+
entryFilePath: string;
|
|
58
|
+
}): ClientEntryFile {
|
|
59
|
+
return {
|
|
60
|
+
routeId: options.route.id,
|
|
61
|
+
entryFilePath: options.entryFilePath,
|
|
62
|
+
route: options.route,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
42
66
|
function buildClientEntrySource(options: {
|
|
43
67
|
generatedDir: string;
|
|
44
68
|
route: PageRouteDefinition;
|
|
@@ -54,7 +78,7 @@ function buildClientEntrySource(options: {
|
|
|
54
78
|
const routeImport = toImportPath(generatedDir, route.filePath);
|
|
55
79
|
|
|
56
80
|
imports.push(
|
|
57
|
-
`import { hydrateInitialRoute, registerRouteModules } from "${runtimeImport}";`,
|
|
81
|
+
`import { hydrateInitialRoute, projectClientModule, registerRouteModules } from "${runtimeImport}";`,
|
|
58
82
|
);
|
|
59
83
|
|
|
60
84
|
imports.push(`import RootDefault from "${rootImport}";`);
|
|
@@ -72,16 +96,16 @@ function buildClientEntrySource(options: {
|
|
|
72
96
|
`import * as Layout${index}Module from "${layoutImportPath}";`,
|
|
73
97
|
);
|
|
74
98
|
layoutModuleRefs.push(
|
|
75
|
-
`
|
|
99
|
+
buildClientModuleProjectionSource(`Layout${index}Default`, `Layout${index}Module`),
|
|
76
100
|
);
|
|
77
101
|
}
|
|
78
102
|
|
|
79
103
|
return `${imports.join('\n')}
|
|
80
104
|
|
|
81
105
|
const modules = {
|
|
82
|
-
root: {
|
|
106
|
+
root: ${buildClientModuleProjectionSource('RootDefault', 'RootModule')},
|
|
83
107
|
layouts: [${layoutModuleRefs.join(', ')}],
|
|
84
|
-
route: {
|
|
108
|
+
route: ${buildClientModuleProjectionSource('RouteDefault', 'RouteModule')},
|
|
85
109
|
};
|
|
86
110
|
|
|
87
111
|
registerRouteModules(${JSON.stringify(route.id)}, modules);
|
|
@@ -112,13 +136,84 @@ export async function generateClientEntries(options: {
|
|
|
112
136
|
|
|
113
137
|
await writeTextIfChanged(entryFilePath, source);
|
|
114
138
|
|
|
115
|
-
return {
|
|
116
|
-
|
|
139
|
+
return toClientEntryFile({
|
|
140
|
+
route,
|
|
117
141
|
entryFilePath,
|
|
142
|
+
});
|
|
143
|
+
}),
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export async function syncClientEntries(options: {
|
|
148
|
+
config: ResolvedConfig;
|
|
149
|
+
manifest: RouteManifest;
|
|
150
|
+
generatedDir: string;
|
|
151
|
+
}): Promise<ClientEntrySyncResult> {
|
|
152
|
+
const { config, manifest, generatedDir } = options;
|
|
153
|
+
await ensureDir(generatedDir);
|
|
154
|
+
|
|
155
|
+
const runtimeClientFile = path.resolve(import.meta.dir, 'client-runtime.tsx');
|
|
156
|
+
const desiredEntries = new Map<string, { route: PageRouteDefinition; entryFilePath: string }>();
|
|
157
|
+
for (const route of manifest.pages) {
|
|
158
|
+
const entryFilePath = path.join(generatedDir, `route__${route.id}.tsx`);
|
|
159
|
+
desiredEntries.set(entryFilePath, {
|
|
160
|
+
route,
|
|
161
|
+
entryFilePath,
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const existingEntryPaths = new Set(
|
|
166
|
+
(await glob('route__*.tsx', {
|
|
167
|
+
cwd: generatedDir,
|
|
168
|
+
absolute: true,
|
|
169
|
+
}))
|
|
170
|
+
.map((filePath) => path.resolve(filePath)),
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
const addedEntryPaths: string[] = [];
|
|
174
|
+
const changedEntryPaths: string[] = [];
|
|
175
|
+
const removedEntryPaths: string[] = [];
|
|
176
|
+
|
|
177
|
+
await Promise.all(
|
|
178
|
+
[...desiredEntries.values()].map(async ({ route, entryFilePath }) => {
|
|
179
|
+
const source = buildClientEntrySource({
|
|
180
|
+
generatedDir,
|
|
118
181
|
route,
|
|
119
|
-
|
|
182
|
+
rootModulePath: config.rootModule,
|
|
183
|
+
runtimeClientFile,
|
|
184
|
+
});
|
|
185
|
+
const existed = existingEntryPaths.has(entryFilePath);
|
|
186
|
+
const changed = await writeTextIfChanged(entryFilePath, source);
|
|
187
|
+
if (!existed) {
|
|
188
|
+
addedEntryPaths.push(entryFilePath);
|
|
189
|
+
} else if (changed) {
|
|
190
|
+
changedEntryPaths.push(entryFilePath);
|
|
191
|
+
}
|
|
192
|
+
existingEntryPaths.delete(entryFilePath);
|
|
193
|
+
}),
|
|
194
|
+
);
|
|
195
|
+
|
|
196
|
+
await Promise.all(
|
|
197
|
+
[...existingEntryPaths].map(async (entryFilePath) => {
|
|
198
|
+
removedEntryPaths.push(entryFilePath);
|
|
199
|
+
await removePath(entryFilePath);
|
|
120
200
|
}),
|
|
121
201
|
);
|
|
202
|
+
|
|
203
|
+
const entries = manifest.pages.map((route) => {
|
|
204
|
+
return toClientEntryFile({
|
|
205
|
+
route,
|
|
206
|
+
entryFilePath: path.join(generatedDir, `route__${route.id}.tsx`),
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
return {
|
|
211
|
+
entries,
|
|
212
|
+
addedEntryPaths: addedEntryPaths.sort(),
|
|
213
|
+
changedEntryPaths: changedEntryPaths.sort(),
|
|
214
|
+
removedEntryPaths: removedEntryPaths.sort(),
|
|
215
|
+
entrySetChanged: addedEntryPaths.length > 0 || removedEntryPaths.length > 0,
|
|
216
|
+
};
|
|
122
217
|
}
|
|
123
218
|
|
|
124
219
|
async function mapBuildOutputsByPrefix(options: {
|
|
@@ -155,7 +250,7 @@ async function mapBuildOutputsByPrefix(options: {
|
|
|
155
250
|
return routeAssets;
|
|
156
251
|
}
|
|
157
252
|
|
|
158
|
-
function normalizeMetafilePath(filePath: string): string {
|
|
253
|
+
export function normalizeMetafilePath(filePath: string): string {
|
|
159
254
|
return normalizeSlashes(filePath).replace(/^\.\//, "");
|
|
160
255
|
}
|
|
161
256
|
|
|
@@ -163,7 +258,7 @@ function toPublicBuildPath(publicPrefix: string, filePath: string): string {
|
|
|
163
258
|
return `${publicPrefix}${normalizeMetafilePath(filePath)}`;
|
|
164
259
|
}
|
|
165
260
|
|
|
166
|
-
function mapBuildOutputsFromMetafile(options: {
|
|
261
|
+
export function mapBuildOutputsFromMetafile(options: {
|
|
167
262
|
metafile: Bun.BuildMetafile;
|
|
168
263
|
entries: ClientEntryFile[];
|
|
169
264
|
publicPrefix: string;
|
|
@@ -204,6 +299,21 @@ function mapBuildOutputsFromMetafile(options: {
|
|
|
204
299
|
return routeAssets;
|
|
205
300
|
}
|
|
206
301
|
|
|
302
|
+
export function listBuildOutputFiles(metafile: Bun.BuildMetafile): string[] {
|
|
303
|
+
return Object.keys(metafile.outputs)
|
|
304
|
+
.map(normalizeMetafilePath)
|
|
305
|
+
.sort();
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
export function createClientEntrySetSignature(entries: ClientEntryFile[]): string {
|
|
309
|
+
return stableHash(
|
|
310
|
+
entries
|
|
311
|
+
.map((entry) => normalizeSlashes(path.resolve(entry.entryFilePath)))
|
|
312
|
+
.sort()
|
|
313
|
+
.join('|'),
|
|
314
|
+
);
|
|
315
|
+
}
|
|
316
|
+
|
|
207
317
|
export async function bundleClientEntries(options: {
|
|
208
318
|
entries: ClientEntryFile[];
|
|
209
319
|
outDir: string;
|
|
@@ -299,8 +409,17 @@ export function createBuildManifest(
|
|
|
299
409
|
};
|
|
300
410
|
}
|
|
301
411
|
|
|
302
|
-
export async function discoverFileSignature(
|
|
303
|
-
const
|
|
412
|
+
export async function discoverFileSignature(rootPath: string): Promise<string> {
|
|
413
|
+
const rootStat = await statPath(rootPath);
|
|
414
|
+
if (!rootStat) {
|
|
415
|
+
return stableHash("");
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
const files = (
|
|
419
|
+
rootStat.isFile()
|
|
420
|
+
? [rootPath]
|
|
421
|
+
: await walkFiles(rootPath)
|
|
422
|
+
)
|
|
304
423
|
.filter((file) => !normalizeSlashes(file).includes('/node_modules/'))
|
|
305
424
|
.sort();
|
|
306
425
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
|
-
import {
|
|
2
|
+
import { ensureDir, glob, removePath, writeTextIfChanged } from "./io";
|
|
3
|
+
import { matchRouteBySegments } from "./matcher";
|
|
3
4
|
import { scanRoutes } from "./route-scanner";
|
|
4
5
|
import type {
|
|
5
6
|
ApiRouteDefinition,
|
|
@@ -116,17 +117,30 @@ async function writeProjectionRoutes<T extends PageRouteDefinition | ApiRouteDef
|
|
|
116
117
|
}),
|
|
117
118
|
);
|
|
118
119
|
|
|
120
|
+
const expectedPaths = new Set(writes.map(({ projectedFilePath }) => path.resolve(projectedFilePath)));
|
|
121
|
+
const existingPaths = new Set(await glob(`**/*${extension}`, {
|
|
122
|
+
cwd: outDir,
|
|
123
|
+
absolute: true,
|
|
124
|
+
}));
|
|
125
|
+
|
|
126
|
+
await Promise.all(
|
|
127
|
+
[...existingPaths]
|
|
128
|
+
.filter((projectedFilePath) => !expectedPaths.has(path.resolve(projectedFilePath)))
|
|
129
|
+
.map((projectedFilePath) => removePath(projectedFilePath)),
|
|
130
|
+
);
|
|
131
|
+
|
|
119
132
|
return byProjectedFilePath;
|
|
120
133
|
}
|
|
121
134
|
|
|
122
135
|
function toRouteMatch<T extends PageRouteDefinition | ApiRouteDefinition>(
|
|
136
|
+
orderedRoutes: T[],
|
|
123
137
|
routeByProjectedPath: Map<string, T>,
|
|
124
138
|
pathname: string,
|
|
125
139
|
router: Bun.FileSystemRouter,
|
|
126
140
|
): RouteMatch<T> | null {
|
|
127
141
|
const matched = router.match(pathname);
|
|
128
142
|
if (!matched) {
|
|
129
|
-
return
|
|
143
|
+
return matchRouteBySegments(orderedRoutes, pathname);
|
|
130
144
|
}
|
|
131
145
|
|
|
132
146
|
const matchedSource = normalizeRouteKey(
|
|
@@ -136,7 +150,7 @@ function toRouteMatch<T extends PageRouteDefinition | ApiRouteDefinition>(
|
|
|
136
150
|
);
|
|
137
151
|
const route = routeByProjectedPath.get(matchedSource);
|
|
138
152
|
if (!route) {
|
|
139
|
-
return
|
|
153
|
+
return matchRouteBySegments(orderedRoutes, pathname);
|
|
140
154
|
}
|
|
141
155
|
|
|
142
156
|
return {
|
|
@@ -153,8 +167,7 @@ export async function createBunRouteAdapter(options: {
|
|
|
153
167
|
const manifest = await scanRoutes(options.routesDir, {
|
|
154
168
|
generatedMarkdownRootDir: options.generatedMarkdownRootDir,
|
|
155
169
|
});
|
|
156
|
-
|
|
157
|
-
await ensureCleanDir(options.projectionRootDir);
|
|
170
|
+
await ensureDir(options.projectionRootDir);
|
|
158
171
|
|
|
159
172
|
const pagesProjectionDir = path.join(options.projectionRootDir, "pages");
|
|
160
173
|
const apiProjectionDir = path.join(options.projectionRootDir, "api");
|
|
@@ -191,10 +204,10 @@ export async function createBunRouteAdapter(options: {
|
|
|
191
204
|
return {
|
|
192
205
|
manifest,
|
|
193
206
|
matchPage(pathname) {
|
|
194
|
-
return toRouteMatch(pageRouteByProjectedPath, pathname, pageRouter);
|
|
207
|
+
return toRouteMatch(manifest.pages, pageRouteByProjectedPath, pathname, pageRouter);
|
|
195
208
|
},
|
|
196
209
|
matchApi(pathname) {
|
|
197
|
-
return toRouteMatch(apiRouteByProjectedPath, pathname, apiRouter);
|
|
210
|
+
return toRouteMatch(manifest.api, apiRouteByProjectedPath, pathname, apiRouter);
|
|
198
211
|
},
|
|
199
212
|
};
|
|
200
213
|
}
|
|
@@ -1,4 +1,14 @@
|
|
|
1
1
|
import { hydrateRoot, type Root } from "react-dom/client";
|
|
2
|
+
import {
|
|
3
|
+
consumeTransitionChunkText,
|
|
4
|
+
createTransitionChunkParserState,
|
|
5
|
+
flushTransitionChunkText,
|
|
6
|
+
isStaleNavigationToken,
|
|
7
|
+
matchClientPageRoute,
|
|
8
|
+
sanitizePrefetchCache,
|
|
9
|
+
shouldHardNavigateForRedirectDepth,
|
|
10
|
+
shouldSkipSoftNavigation,
|
|
11
|
+
} from "./client-transition-core";
|
|
2
12
|
import { isDeferredToken } from "./deferred";
|
|
3
13
|
import {
|
|
4
14
|
addNavigationNavigateListener,
|
|
@@ -21,14 +31,12 @@ import {
|
|
|
21
31
|
} from "./tree";
|
|
22
32
|
import { isRouteErrorResponse } from "./route-errors";
|
|
23
33
|
import type {
|
|
24
|
-
ClientRouteSnapshot,
|
|
25
34
|
ClientRouterSnapshot,
|
|
26
|
-
Params,
|
|
27
35
|
RenderPayload,
|
|
28
36
|
RouteModule,
|
|
29
37
|
RouteModuleBundle,
|
|
30
|
-
TransitionChunk,
|
|
31
38
|
TransitionDeferredChunk,
|
|
39
|
+
TransitionDocumentChunk,
|
|
32
40
|
TransitionInitialChunk,
|
|
33
41
|
TransitionRedirectChunk,
|
|
34
42
|
} from "./types";
|
|
@@ -61,7 +69,7 @@ interface NavigateResult {
|
|
|
61
69
|
interface PrefetchEntry {
|
|
62
70
|
createdAt: number;
|
|
63
71
|
modulePromise: Promise<void>;
|
|
64
|
-
initialPromise: Promise<TransitionInitialChunk | TransitionRedirectChunk | null>;
|
|
72
|
+
initialPromise: Promise<TransitionInitialChunk | TransitionRedirectChunk | TransitionDocumentChunk | null>;
|
|
65
73
|
donePromise: Promise<void>;
|
|
66
74
|
}
|
|
67
75
|
|
|
@@ -71,7 +79,7 @@ interface TransitionRequestOptions {
|
|
|
71
79
|
}
|
|
72
80
|
|
|
73
81
|
interface TransitionRequestHandle {
|
|
74
|
-
initialPromise: Promise<TransitionInitialChunk | TransitionRedirectChunk | null>;
|
|
82
|
+
initialPromise: Promise<TransitionInitialChunk | TransitionRedirectChunk | TransitionDocumentChunk | null>;
|
|
75
83
|
donePromise: Promise<void>;
|
|
76
84
|
}
|
|
77
85
|
|
|
@@ -126,7 +134,6 @@ declare global {
|
|
|
126
134
|
}
|
|
127
135
|
}
|
|
128
136
|
|
|
129
|
-
const PREFETCH_TTL_MS = 30_000;
|
|
130
137
|
const NAVIGATION_API_PENDING_TIMEOUT_MS = 1_500;
|
|
131
138
|
const NAVIGATION_API_PENDING_MATCH_WINDOW_MS = 10_000;
|
|
132
139
|
const ROUTE_ANNOUNCER_ID = "__rbssr-route-announcer";
|
|
@@ -137,69 +144,26 @@ let popstateBound = false;
|
|
|
137
144
|
let navigationApiListenerBound = false;
|
|
138
145
|
let navigationApiTransitionCounter = 0;
|
|
139
146
|
|
|
140
|
-
function
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
return
|
|
146
|
-
.replace(/^\/+/, "")
|
|
147
|
-
.replace(/\/+$/, "")
|
|
148
|
-
.split("/")
|
|
149
|
-
.filter(Boolean)
|
|
150
|
-
.map(part => decodeURIComponent(part));
|
|
147
|
+
function pickOptionalClientModuleExport<T>(
|
|
148
|
+
moduleValue: Record<string, unknown>,
|
|
149
|
+
exportName: string,
|
|
150
|
+
): T | undefined {
|
|
151
|
+
const value = moduleValue[exportName];
|
|
152
|
+
return typeof value === "function" ? (value as T) : undefined;
|
|
151
153
|
}
|
|
152
154
|
|
|
153
|
-
function
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
return params;
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
const current = pathParts[j];
|
|
169
|
-
if (current === undefined) {
|
|
170
|
-
return null;
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
if (segment.kind === "static") {
|
|
174
|
-
if (segment.value !== current) {
|
|
175
|
-
return null;
|
|
176
|
-
}
|
|
177
|
-
} else {
|
|
178
|
-
params[segment.value] = current;
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
i += 1;
|
|
182
|
-
j += 1;
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
if (j !== pathParts.length) {
|
|
186
|
-
return null;
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
return params;
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
function matchPageRoute(
|
|
193
|
-
routes: ClientRouteSnapshot[],
|
|
194
|
-
pathname: string,
|
|
195
|
-
): { route: ClientRouteSnapshot; params: Params } | null {
|
|
196
|
-
for (const route of routes) {
|
|
197
|
-
const params = matchSegments(route.segments, pathname);
|
|
198
|
-
if (params) {
|
|
199
|
-
return { route, params };
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
return null;
|
|
155
|
+
export function projectClientModule(
|
|
156
|
+
defaultExport: RouteModule["default"],
|
|
157
|
+
moduleValue: Record<string, unknown>,
|
|
158
|
+
): RouteModule {
|
|
159
|
+
return {
|
|
160
|
+
default: defaultExport,
|
|
161
|
+
Loading: pickOptionalClientModuleExport<RouteModule["Loading"]>(moduleValue, "Loading"),
|
|
162
|
+
ErrorComponent: pickOptionalClientModuleExport<RouteModule["ErrorComponent"]>(moduleValue, "ErrorComponent"),
|
|
163
|
+
CatchBoundary: pickOptionalClientModuleExport<RouteModule["CatchBoundary"]>(moduleValue, "CatchBoundary"),
|
|
164
|
+
ErrorBoundary: pickOptionalClientModuleExport<RouteModule["ErrorBoundary"]>(moduleValue, "ErrorBoundary"),
|
|
165
|
+
NotFound: pickOptionalClientModuleExport<RouteModule["NotFound"]>(moduleValue, "NotFound"),
|
|
166
|
+
};
|
|
203
167
|
}
|
|
204
168
|
|
|
205
169
|
function withVersionQuery(url: string, version?: number): string {
|
|
@@ -402,50 +366,23 @@ function ensureRuntimeState(): RuntimeState {
|
|
|
402
366
|
return runtimeState;
|
|
403
367
|
}
|
|
404
368
|
|
|
405
|
-
function sanitizePrefetchCache(cache: Map<string, PrefetchEntry>): void {
|
|
406
|
-
const now = Date.now();
|
|
407
|
-
for (const [key, entry] of cache.entries()) {
|
|
408
|
-
if (now - entry.createdAt > PREFETCH_TTL_MS) {
|
|
409
|
-
cache.delete(key);
|
|
410
|
-
}
|
|
411
|
-
}
|
|
412
|
-
}
|
|
413
|
-
|
|
414
369
|
function createTransitionUrl(toUrl: URL): URL {
|
|
415
370
|
const transitionUrl = new URL("/__rbssr/transition", window.location.origin);
|
|
416
371
|
transitionUrl.searchParams.set("to", toUrl.pathname + toUrl.search + toUrl.hash);
|
|
417
372
|
return transitionUrl;
|
|
418
373
|
}
|
|
419
374
|
|
|
420
|
-
function splitLines(buffer: string): { lines: string[]; rest: string } {
|
|
421
|
-
const lines: string[] = [];
|
|
422
|
-
let start = 0;
|
|
423
|
-
|
|
424
|
-
for (let index = 0; index < buffer.length; index += 1) {
|
|
425
|
-
if (buffer[index] !== "\n") {
|
|
426
|
-
continue;
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
const line = buffer.slice(start, index).trim();
|
|
430
|
-
if (line.length > 0) {
|
|
431
|
-
lines.push(line);
|
|
432
|
-
}
|
|
433
|
-
start = index + 1;
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
return {
|
|
437
|
-
lines,
|
|
438
|
-
rest: buffer.slice(start),
|
|
439
|
-
};
|
|
440
|
-
}
|
|
441
|
-
|
|
442
375
|
function startTransitionRequest(
|
|
443
376
|
toUrl: URL,
|
|
444
377
|
options: TransitionRequestOptions = {},
|
|
445
378
|
): TransitionRequestHandle {
|
|
446
|
-
let resolveInitial: (
|
|
379
|
+
let resolveInitial: (
|
|
380
|
+
value: TransitionInitialChunk | TransitionRedirectChunk | TransitionDocumentChunk | null,
|
|
381
|
+
) => void = () => undefined;
|
|
447
382
|
let rejectInitial: (reason?: unknown) => void = () => undefined;
|
|
448
|
-
const initialPromise = new Promise<
|
|
383
|
+
const initialPromise = new Promise<
|
|
384
|
+
TransitionInitialChunk | TransitionRedirectChunk | TransitionDocumentChunk | null
|
|
385
|
+
>((resolve, reject) => {
|
|
449
386
|
resolveInitial = resolve;
|
|
450
387
|
rejectInitial = reject;
|
|
451
388
|
});
|
|
@@ -464,8 +401,7 @@ function startTransitionRequest(
|
|
|
464
401
|
|
|
465
402
|
const reader = response.body.getReader();
|
|
466
403
|
const decoder = new TextDecoder();
|
|
467
|
-
let
|
|
468
|
-
let textBuffer = "";
|
|
404
|
+
let parserState = createTransitionChunkParserState();
|
|
469
405
|
|
|
470
406
|
while (true) {
|
|
471
407
|
const { done, value } = await reader.read();
|
|
@@ -473,38 +409,35 @@ function startTransitionRequest(
|
|
|
473
409
|
break;
|
|
474
410
|
}
|
|
475
411
|
|
|
476
|
-
|
|
477
|
-
const
|
|
478
|
-
|
|
412
|
+
const previousInitialChunk = parserState.initialChunk;
|
|
413
|
+
const previousDeferredCount = parserState.deferredChunks.length;
|
|
414
|
+
parserState = consumeTransitionChunkText(
|
|
415
|
+
parserState,
|
|
416
|
+
decoder.decode(value, { stream: true }),
|
|
417
|
+
);
|
|
479
418
|
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
if (!initialChunk) {
|
|
484
|
-
initialChunk = chunk;
|
|
485
|
-
resolveInitial(initialChunk);
|
|
486
|
-
}
|
|
487
|
-
continue;
|
|
488
|
-
}
|
|
419
|
+
if (!previousInitialChunk && parserState.initialChunk) {
|
|
420
|
+
resolveInitial(parserState.initialChunk);
|
|
421
|
+
}
|
|
489
422
|
|
|
423
|
+
for (const chunk of parserState.deferredChunks.slice(previousDeferredCount)) {
|
|
490
424
|
options.onDeferredChunk?.(chunk);
|
|
491
425
|
}
|
|
492
426
|
}
|
|
493
427
|
|
|
494
|
-
const
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
resolveInitial(initialChunk);
|
|
501
|
-
}
|
|
502
|
-
} else {
|
|
503
|
-
options.onDeferredChunk?.(chunk);
|
|
504
|
-
}
|
|
428
|
+
const previousInitialChunk = parserState.initialChunk;
|
|
429
|
+
const previousDeferredCount = parserState.deferredChunks.length;
|
|
430
|
+
parserState = flushTransitionChunkText(parserState);
|
|
431
|
+
|
|
432
|
+
if (!previousInitialChunk && parserState.initialChunk) {
|
|
433
|
+
resolveInitial(parserState.initialChunk);
|
|
505
434
|
}
|
|
506
435
|
|
|
507
|
-
|
|
436
|
+
for (const chunk of parserState.deferredChunks.slice(previousDeferredCount)) {
|
|
437
|
+
options.onDeferredChunk?.(chunk);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
if (!parserState.initialChunk) {
|
|
508
441
|
resolveInitial(null);
|
|
509
442
|
}
|
|
510
443
|
})();
|
|
@@ -917,11 +850,11 @@ async function navigateToInternal(
|
|
|
917
850
|
const currentPath = window.location.pathname + window.location.search + window.location.hash;
|
|
918
851
|
const targetPath = toUrl.pathname + toUrl.search + toUrl.hash;
|
|
919
852
|
|
|
920
|
-
if (currentPath
|
|
853
|
+
if (shouldSkipSoftNavigation(currentPath, targetPath, options)) {
|
|
921
854
|
return null;
|
|
922
855
|
}
|
|
923
856
|
|
|
924
|
-
const matched =
|
|
857
|
+
const matched = matchClientPageRoute(state.routerSnapshot.pages, toUrl.pathname);
|
|
925
858
|
const routeId = matched?.route.id ?? null;
|
|
926
859
|
|
|
927
860
|
if (state.transitionAbortController) {
|
|
@@ -942,7 +875,7 @@ async function navigateToInternal(
|
|
|
942
875
|
|
|
943
876
|
try {
|
|
944
877
|
await prefetchEntry.modulePromise;
|
|
945
|
-
if (
|
|
878
|
+
if (isStaleNavigationToken(state.navigationToken, navigationToken)) {
|
|
946
879
|
return null;
|
|
947
880
|
}
|
|
948
881
|
|
|
@@ -965,7 +898,7 @@ async function navigateToInternal(
|
|
|
965
898
|
}
|
|
966
899
|
|
|
967
900
|
const initialChunk = await prefetchEntry.initialPromise;
|
|
968
|
-
if (
|
|
901
|
+
if (isStaleNavigationToken(state.navigationToken, navigationToken)) {
|
|
969
902
|
return null;
|
|
970
903
|
}
|
|
971
904
|
|
|
@@ -973,6 +906,11 @@ async function navigateToInternal(
|
|
|
973
906
|
throw new Error("Transition response did not include an initial payload.");
|
|
974
907
|
}
|
|
975
908
|
|
|
909
|
+
if (initialChunk.type === "document") {
|
|
910
|
+
hardNavigate(new URL(initialChunk.location, window.location.origin));
|
|
911
|
+
return null;
|
|
912
|
+
}
|
|
913
|
+
|
|
976
914
|
if (initialChunk.type === "redirect") {
|
|
977
915
|
const redirectUrl = new URL(initialChunk.location, window.location.origin);
|
|
978
916
|
if (!isInternalUrl(redirectUrl)) {
|
|
@@ -981,7 +919,7 @@ async function navigateToInternal(
|
|
|
981
919
|
}
|
|
982
920
|
|
|
983
921
|
const depth = (options.redirectDepth ?? 0) + 1;
|
|
984
|
-
if (depth
|
|
922
|
+
if (shouldHardNavigateForRedirectDepth(depth)) {
|
|
985
923
|
hardNavigate(redirectUrl);
|
|
986
924
|
return null;
|
|
987
925
|
}
|
|
@@ -991,6 +929,9 @@ async function navigateToInternal(
|
|
|
991
929
|
replace: true,
|
|
992
930
|
redirected: true,
|
|
993
931
|
redirectDepth: depth,
|
|
932
|
+
// The intercepted navigation has already committed the source URL.
|
|
933
|
+
// The redirected target must update history explicitly.
|
|
934
|
+
historyManagedByNavigationApi: false,
|
|
994
935
|
});
|
|
995
936
|
}
|
|
996
937
|
|
|
@@ -1190,7 +1131,7 @@ export async function prefetchTo(to: string): Promise<void> {
|
|
|
1190
1131
|
return;
|
|
1191
1132
|
}
|
|
1192
1133
|
|
|
1193
|
-
const matched =
|
|
1134
|
+
const matched = matchClientPageRoute(state.routerSnapshot.pages, toUrl.pathname);
|
|
1194
1135
|
const routeId = matched?.route.id ?? null;
|
|
1195
1136
|
getOrCreatePrefetchEntry(toUrl, routeId, state.routerSnapshot);
|
|
1196
1137
|
}
|