@reckona/mreact-router 0.0.168 → 0.0.170

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/src/routes.ts CHANGED
@@ -80,6 +80,25 @@ export interface RouteMatcher {
80
80
  match(pathname: string): MatchedRoute | undefined;
81
81
  }
82
82
 
83
+ export type CompiledRouteMatcherSegment =
84
+ | { kind: "static"; value: string }
85
+ | { kind: "dynamic"; name: string }
86
+ | { kind: "catch-all"; name: string };
87
+
88
+ export interface CompiledRouteMatcherEntry {
89
+ catchAllIndex: number;
90
+ exactLength?: number;
91
+ minimumLength: number;
92
+ routeIndex: number;
93
+ segments: readonly CompiledRouteMatcherSegment[];
94
+ suffixLength?: number;
95
+ }
96
+
97
+ export interface CompiledRouteMatcherArtifact {
98
+ routes: readonly CompiledRouteMatcherEntry[];
99
+ version: 1;
100
+ }
101
+
83
102
  /**
84
103
  * Scans an app directory and returns sorted app-router route definitions.
85
104
  */
@@ -121,7 +140,33 @@ export function matchRoute(
121
140
  return createRouteMatcher(routes).match(pathname);
122
141
  }
123
142
 
124
- export function createRouteMatcher(routes: readonly AppRoute[]): RouteMatcher {
143
+ export function compileRouteMatcherArtifact(
144
+ routes: readonly AppRoute[],
145
+ ): CompiledRouteMatcherArtifact {
146
+ const sortedRoutes = routes
147
+ .map((route, routeIndex) => ({ route, routeIndex }))
148
+ .sort((left, right) => compareRoutes(left.route, right.route));
149
+
150
+ return {
151
+ version: 1,
152
+ routes: sortedRoutes.map(({ route, routeIndex }) =>
153
+ compileRouteMatcherEntry(route, routeIndex),
154
+ ),
155
+ };
156
+ }
157
+
158
+ export function createRouteMatcher(
159
+ routes: readonly AppRoute[],
160
+ artifact?: CompiledRouteMatcherArtifact | undefined,
161
+ ): RouteMatcher {
162
+ if (artifact?.version === 1) {
163
+ return {
164
+ match(pathname) {
165
+ return matchCompiledRoutes(routes, artifact, pathname);
166
+ },
167
+ };
168
+ }
169
+
125
170
  const sortedRoutes = [...routes].sort(compareRoutes);
126
171
  const nativeMatcher = createNativeRouteMatcher(sortedRoutes);
127
172
 
@@ -136,6 +181,143 @@ export function createRouteMatcher(routes: readonly AppRoute[]): RouteMatcher {
136
181
  };
137
182
  }
138
183
 
184
+ function compileRouteMatcherEntry(
185
+ route: AppRoute,
186
+ routeIndex: number,
187
+ ): CompiledRouteMatcherEntry {
188
+ const catchAllIndex = route.segments.findIndex((segment) => segment.kind === "catch-all");
189
+ const shared = {
190
+ catchAllIndex,
191
+ minimumLength: catchAllIndex === -1 ? route.segments.length : catchAllIndex + 1,
192
+ routeIndex,
193
+ segments: route.segments.map((segment) => ({ ...segment })),
194
+ };
195
+
196
+ return catchAllIndex === -1
197
+ ? {
198
+ ...shared,
199
+ exactLength: route.segments.length,
200
+ }
201
+ : {
202
+ ...shared,
203
+ suffixLength: route.segments.length - catchAllIndex - 1,
204
+ };
205
+ }
206
+
207
+ function matchCompiledRoutes(
208
+ routes: readonly AppRoute[],
209
+ artifact: CompiledRouteMatcherArtifact,
210
+ pathname: string,
211
+ ): MatchedRoute | undefined {
212
+ const normalized = normalizePath(pathname);
213
+ const pathnameSegments = normalized === "/" ? [] : normalized.slice(1).split("/");
214
+
215
+ for (const compiledRoute of artifact.routes) {
216
+ const route = routes[compiledRoute.routeIndex];
217
+ if (route === undefined) {
218
+ continue;
219
+ }
220
+
221
+ if (
222
+ compiledRoute.exactLength !== undefined &&
223
+ compiledRoute.exactLength !== pathnameSegments.length
224
+ ) {
225
+ continue;
226
+ }
227
+
228
+ if (pathnameSegments.length < compiledRoute.minimumLength) {
229
+ continue;
230
+ }
231
+
232
+ const params: Record<string, readonly string[] | string> = {};
233
+ let matched = true;
234
+
235
+ for (const [index, segment] of compiledRoute.segments.entries()) {
236
+ const value = pathnameSegments[index];
237
+
238
+ if (value === undefined) {
239
+ matched = false;
240
+ break;
241
+ }
242
+
243
+ if (segment.kind === "static" && segment.value !== value) {
244
+ matched = false;
245
+ break;
246
+ }
247
+
248
+ if (segment.kind === "dynamic") {
249
+ const decoded = safeDecodeURIComponent(value);
250
+ if (decoded === undefined) {
251
+ matched = false;
252
+ break;
253
+ }
254
+ params[segment.name] = decoded;
255
+ }
256
+
257
+ if (segment.kind === "catch-all") {
258
+ const suffixLength = compiledRoute.suffixLength ?? 0;
259
+ const catchAllEnd = pathnameSegments.length - suffixLength;
260
+
261
+ if (catchAllEnd <= index) {
262
+ matched = false;
263
+ break;
264
+ }
265
+
266
+ const decodedParts: string[] = [];
267
+ for (let partIndex = index; partIndex < catchAllEnd; partIndex += 1) {
268
+ const decoded = safeDecodeURIComponent(pathnameSegments[partIndex] ?? "");
269
+ if (decoded === undefined) {
270
+ matched = false;
271
+ break;
272
+ }
273
+ decodedParts.push(decoded);
274
+ }
275
+ if (!matched) {
276
+ break;
277
+ }
278
+ params[segment.name] = decodedParts;
279
+
280
+ for (let suffixIndex = 0; suffixIndex < suffixLength; suffixIndex += 1) {
281
+ const suffixSegment = compiledRoute.segments[index + 1 + suffixIndex];
282
+ const suffixValue = pathnameSegments[catchAllEnd + suffixIndex];
283
+
284
+ if (suffixSegment === undefined || suffixValue === undefined) {
285
+ matched = false;
286
+ break;
287
+ }
288
+
289
+ if (suffixSegment.kind === "static" && suffixSegment.value !== suffixValue) {
290
+ matched = false;
291
+ break;
292
+ }
293
+
294
+ if (suffixSegment.kind === "dynamic") {
295
+ const decoded = safeDecodeURIComponent(suffixValue);
296
+ if (decoded === undefined) {
297
+ matched = false;
298
+ break;
299
+ }
300
+ params[suffixSegment.name] = decoded;
301
+ }
302
+
303
+ if (suffixSegment.kind === "catch-all") {
304
+ matched = false;
305
+ break;
306
+ }
307
+ }
308
+
309
+ break;
310
+ }
311
+ }
312
+
313
+ if (matched) {
314
+ return { route, params };
315
+ }
316
+ }
317
+
318
+ return undefined;
319
+ }
320
+
139
321
  function matchSortedRoutes(
140
322
  routes: readonly AppRoute[],
141
323
  pathname: string,
package/src/serve.ts CHANGED
@@ -975,7 +975,7 @@ async function materializeBuiltRuntime(options: {
975
975
  references,
976
976
  ]),
977
977
  );
978
- const routeMatcher = createRouteMatcher(routes);
978
+ const routeMatcher = createRouteMatcher(routes, serverManifest.routeMatcher);
979
979
  const clientScripts = new Map(
980
980
  clientManifest.routes.flatMap((route) =>
981
981
  route.client && route.script !== undefined ? [[route.path, route.script]] : [],