@pyreon/router 0.2.1 → 0.3.1

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.
@@ -5386,7 +5386,7 @@ var drawChart = (function (exports) {
5386
5386
  </script>
5387
5387
  <script>
5388
5388
  /*<!--*/
5389
- const data = {"version":2,"tree":{"name":"root","children":[{"name":"index.js","children":[{"name":"src","children":[{"uid":"a1485d5b-1","name":"loader.ts"},{"uid":"a1485d5b-3","name":"match.ts"},{"uid":"a1485d5b-5","name":"scroll.ts"},{"uid":"a1485d5b-7","name":"types.ts"},{"uid":"a1485d5b-9","name":"router.ts"},{"uid":"a1485d5b-11","name":"components.tsx"},{"uid":"a1485d5b-13","name":"index.ts"}]}]}],"isRoot":true},"nodeParts":{"a1485d5b-1":{"renderedLength":2855,"gzipLength":1243,"brotliLength":0,"metaUid":"a1485d5b-0"},"a1485d5b-3":{"renderedLength":6655,"gzipLength":2084,"brotliLength":0,"metaUid":"a1485d5b-2"},"a1485d5b-5":{"renderedLength":1367,"gzipLength":576,"brotliLength":0,"metaUid":"a1485d5b-4"},"a1485d5b-7":{"renderedLength":385,"gzipLength":246,"brotliLength":0,"metaUid":"a1485d5b-6"},"a1485d5b-9":{"renderedLength":8965,"gzipLength":2657,"brotliLength":0,"metaUid":"a1485d5b-8"},"a1485d5b-11":{"renderedLength":6631,"gzipLength":2488,"brotliLength":0,"metaUid":"a1485d5b-10"},"a1485d5b-13":{"renderedLength":0,"gzipLength":0,"brotliLength":0,"metaUid":"a1485d5b-12"}},"nodeMetas":{"a1485d5b-0":{"id":"/src/loader.ts","moduleParts":{"index.js":"a1485d5b-1"},"imported":[{"uid":"a1485d5b-14"}],"importedBy":[{"uid":"a1485d5b-12"},{"uid":"a1485d5b-10"}]},"a1485d5b-2":{"id":"/src/match.ts","moduleParts":{"index.js":"a1485d5b-3"},"imported":[],"importedBy":[{"uid":"a1485d5b-12"},{"uid":"a1485d5b-8"}]},"a1485d5b-4":{"id":"/src/scroll.ts","moduleParts":{"index.js":"a1485d5b-5"},"imported":[],"importedBy":[{"uid":"a1485d5b-8"}]},"a1485d5b-6":{"id":"/src/types.ts","moduleParts":{"index.js":"a1485d5b-7"},"imported":[],"importedBy":[{"uid":"a1485d5b-12"},{"uid":"a1485d5b-8"}]},"a1485d5b-8":{"id":"/src/router.ts","moduleParts":{"index.js":"a1485d5b-9"},"imported":[{"uid":"a1485d5b-14"},{"uid":"a1485d5b-15"},{"uid":"a1485d5b-2"},{"uid":"a1485d5b-4"},{"uid":"a1485d5b-6"}],"importedBy":[{"uid":"a1485d5b-12"},{"uid":"a1485d5b-10"}]},"a1485d5b-10":{"id":"/src/components.tsx","moduleParts":{"index.js":"a1485d5b-11"},"imported":[{"uid":"a1485d5b-14"},{"uid":"a1485d5b-0"},{"uid":"a1485d5b-8"}],"importedBy":[{"uid":"a1485d5b-12"}]},"a1485d5b-12":{"id":"/src/index.ts","moduleParts":{"index.js":"a1485d5b-13"},"imported":[{"uid":"a1485d5b-10"},{"uid":"a1485d5b-0"},{"uid":"a1485d5b-2"},{"uid":"a1485d5b-8"},{"uid":"a1485d5b-6"}],"importedBy":[],"isEntry":true},"a1485d5b-14":{"id":"@pyreon/core","moduleParts":{},"imported":[],"importedBy":[{"uid":"a1485d5b-10"},{"uid":"a1485d5b-0"},{"uid":"a1485d5b-8"}]},"a1485d5b-15":{"id":"@pyreon/reactivity","moduleParts":{},"imported":[],"importedBy":[{"uid":"a1485d5b-8"}]}},"env":{"rollup":"4.23.0"},"options":{"gzip":true,"brotli":false,"sourcemap":false}};
5389
+ const data = {"version":2,"tree":{"name":"root","children":[{"name":"index.js","children":[{"name":"src","children":[{"uid":"ffb5fd41-1","name":"loader.ts"},{"uid":"ffb5fd41-3","name":"match.ts"},{"uid":"ffb5fd41-5","name":"scroll.ts"},{"uid":"ffb5fd41-7","name":"types.ts"},{"uid":"ffb5fd41-9","name":"router.ts"},{"uid":"ffb5fd41-11","name":"components.tsx"},{"uid":"ffb5fd41-13","name":"index.ts"}]}]}],"isRoot":true},"nodeParts":{"ffb5fd41-1":{"renderedLength":2855,"gzipLength":1243,"brotliLength":0,"metaUid":"ffb5fd41-0"},"ffb5fd41-3":{"renderedLength":10804,"gzipLength":3328,"brotliLength":0,"metaUid":"ffb5fd41-2"},"ffb5fd41-5":{"renderedLength":1367,"gzipLength":576,"brotliLength":0,"metaUid":"ffb5fd41-4"},"ffb5fd41-7":{"renderedLength":385,"gzipLength":246,"brotliLength":0,"metaUid":"ffb5fd41-6"},"ffb5fd41-9":{"renderedLength":8965,"gzipLength":2657,"brotliLength":0,"metaUid":"ffb5fd41-8"},"ffb5fd41-11":{"renderedLength":6631,"gzipLength":2488,"brotliLength":0,"metaUid":"ffb5fd41-10"},"ffb5fd41-13":{"renderedLength":0,"gzipLength":0,"brotliLength":0,"metaUid":"ffb5fd41-12"}},"nodeMetas":{"ffb5fd41-0":{"id":"/src/loader.ts","moduleParts":{"index.js":"ffb5fd41-1"},"imported":[{"uid":"ffb5fd41-14"}],"importedBy":[{"uid":"ffb5fd41-12"},{"uid":"ffb5fd41-10"}]},"ffb5fd41-2":{"id":"/src/match.ts","moduleParts":{"index.js":"ffb5fd41-3"},"imported":[],"importedBy":[{"uid":"ffb5fd41-12"},{"uid":"ffb5fd41-8"}]},"ffb5fd41-4":{"id":"/src/scroll.ts","moduleParts":{"index.js":"ffb5fd41-5"},"imported":[],"importedBy":[{"uid":"ffb5fd41-8"}]},"ffb5fd41-6":{"id":"/src/types.ts","moduleParts":{"index.js":"ffb5fd41-7"},"imported":[],"importedBy":[{"uid":"ffb5fd41-12"},{"uid":"ffb5fd41-8"}]},"ffb5fd41-8":{"id":"/src/router.ts","moduleParts":{"index.js":"ffb5fd41-9"},"imported":[{"uid":"ffb5fd41-14"},{"uid":"ffb5fd41-15"},{"uid":"ffb5fd41-2"},{"uid":"ffb5fd41-4"},{"uid":"ffb5fd41-6"}],"importedBy":[{"uid":"ffb5fd41-12"},{"uid":"ffb5fd41-10"}]},"ffb5fd41-10":{"id":"/src/components.tsx","moduleParts":{"index.js":"ffb5fd41-11"},"imported":[{"uid":"ffb5fd41-14"},{"uid":"ffb5fd41-0"},{"uid":"ffb5fd41-8"}],"importedBy":[{"uid":"ffb5fd41-12"}]},"ffb5fd41-12":{"id":"/src/index.ts","moduleParts":{"index.js":"ffb5fd41-13"},"imported":[{"uid":"ffb5fd41-10"},{"uid":"ffb5fd41-0"},{"uid":"ffb5fd41-2"},{"uid":"ffb5fd41-8"},{"uid":"ffb5fd41-6"}],"importedBy":[],"isEntry":true},"ffb5fd41-14":{"id":"@pyreon/core","moduleParts":{},"imported":[],"importedBy":[{"uid":"ffb5fd41-10"},{"uid":"ffb5fd41-0"},{"uid":"ffb5fd41-8"}]},"ffb5fd41-15":{"id":"@pyreon/reactivity","moduleParts":{},"imported":[],"importedBy":[{"uid":"ffb5fd41-8"}]}},"env":{"rollup":"4.23.0"},"options":{"gzip":true,"brotli":false,"sourcemap":false}};
5390
5390
 
5391
5391
  const run = () => {
5392
5392
  const width = window.innerWidth;
package/lib/index.js CHANGED
@@ -135,69 +135,213 @@ function stringifyQuery(query) {
135
135
  for (const [k, v] of Object.entries(query)) parts.push(v ? `${encodeURIComponent(k)}=${encodeURIComponent(v)}` : encodeURIComponent(k));
136
136
  return parts.length ? `?${parts.join("&")}` : "";
137
137
  }
138
+ /** WeakMap cache: compile each RouteRecord[] once */
139
+ const _compiledCache = /* @__PURE__ */ new WeakMap();
140
+ function compileSegment(raw) {
141
+ if (raw.endsWith("*") && raw.startsWith(":")) return {
142
+ raw,
143
+ isParam: true,
144
+ isSplat: true,
145
+ paramName: raw.slice(1, -1)
146
+ };
147
+ if (raw.startsWith(":")) return {
148
+ raw,
149
+ isParam: true,
150
+ isSplat: false,
151
+ paramName: raw.slice(1)
152
+ };
153
+ return {
154
+ raw,
155
+ isParam: false,
156
+ isSplat: false,
157
+ paramName: ""
158
+ };
159
+ }
160
+ function compileRoute(route) {
161
+ const pattern = route.path;
162
+ if (pattern === "(.*)" || pattern === "*") return {
163
+ route,
164
+ isWildcard: true,
165
+ segments: [],
166
+ segmentCount: 0,
167
+ isStatic: false,
168
+ staticPath: null,
169
+ children: null,
170
+ firstSegment: null
171
+ };
172
+ const segments = pattern.split("/").filter(Boolean).map(compileSegment);
173
+ const isStatic = segments.every((s) => !s.isParam);
174
+ const staticPath = isStatic ? `/${segments.map((s) => s.raw).join("/")}` : null;
175
+ const first = segments.length > 0 ? segments[0] : void 0;
176
+ const firstSegment = first && !first.isParam ? first.raw : null;
177
+ return {
178
+ route,
179
+ isWildcard: false,
180
+ segments,
181
+ segmentCount: segments.length,
182
+ isStatic,
183
+ staticPath,
184
+ children: null,
185
+ firstSegment
186
+ };
187
+ }
188
+ function compileRoutes(routes) {
189
+ const cached = _compiledCache.get(routes);
190
+ if (cached) return cached;
191
+ const compiled = routes.map((r) => {
192
+ const c = compileRoute(r);
193
+ if (r.children && r.children.length > 0) c.children = compileRoutes(r.children);
194
+ return c;
195
+ });
196
+ _compiledCache.set(routes, compiled);
197
+ return compiled;
198
+ }
199
+ /** Extract first static segment from a segment list, or null if dynamic/empty */
200
+ function getFirstSegment(segments) {
201
+ const first = segments[0];
202
+ if (first && !first.isParam) return first.raw;
203
+ return null;
204
+ }
205
+ /** Build a FlattenedRoute from segments + metadata */
206
+ function makeFlatEntry(segments, chain, meta, isWildcard) {
207
+ const isStatic = !isWildcard && segments.every((s) => !s.isParam);
208
+ return {
209
+ segments,
210
+ segmentCount: segments.length,
211
+ matchedChain: chain,
212
+ isStatic,
213
+ staticPath: isStatic ? `/${segments.map((s) => s.raw).join("/")}` : null,
214
+ meta,
215
+ firstSegment: getFirstSegment(segments),
216
+ hasSplat: segments.some((s) => s.isSplat),
217
+ isWildcard
218
+ };
219
+ }
138
220
  /**
139
- * Match a single route pattern against a path segment.
140
- * Returns extracted params or null if no match.
141
- *
142
- * Supports:
143
- * - Exact segments: "/about"
144
- * - Param segments: "/user/:id"
145
- * - Wildcard: "(.*)" matches everything
221
+ * Flatten nested routes into leaf entries with pre-joined segments.
222
+ * This eliminates recursion during matching for the common case.
146
223
  */
147
- function matchPath(pattern, path) {
148
- if (pattern === "(.*)" || pattern === "*") return {};
149
- const patternParts = pattern.split("/").filter(Boolean);
150
- const pathParts = path.split("/").filter(Boolean);
224
+ function flattenRoutes(compiled) {
225
+ const result = [];
226
+ flattenWalk(result, compiled, [], [], {});
227
+ return result;
228
+ }
229
+ function flattenWalk(result, routes, parentSegments, parentChain, parentMeta) {
230
+ for (const c of routes) flattenOne(result, c, parentSegments, [...parentChain, c.route], c.route.meta ? {
231
+ ...parentMeta,
232
+ ...c.route.meta
233
+ } : { ...parentMeta });
234
+ }
235
+ function flattenOne(result, c, parentSegments, chain, meta) {
236
+ if (c.isWildcard) {
237
+ result.push(makeFlatEntry(parentSegments, chain, meta, true));
238
+ if (c.children && c.children.length > 0) flattenWalk(result, c.children, parentSegments, chain, meta);
239
+ return;
240
+ }
241
+ const joined = [...parentSegments, ...c.segments];
242
+ if (c.children && c.children.length > 0) flattenWalk(result, c.children, joined, chain, meta);
243
+ result.push(makeFlatEntry(joined, chain, meta, false));
244
+ }
245
+ const _indexCache = /* @__PURE__ */ new WeakMap();
246
+ /** Classify a single flattened route into the appropriate index bucket */
247
+ function indexFlatRoute(f, staticMap, segmentMap, dynamicFirst, wildcards) {
248
+ if (f.isStatic && f.staticPath && !staticMap.has(f.staticPath)) staticMap.set(f.staticPath, f);
249
+ if (f.isWildcard) {
250
+ wildcards.push(f);
251
+ return;
252
+ }
253
+ if (f.segmentCount === 0) return;
254
+ if (f.firstSegment) {
255
+ let bucket = segmentMap.get(f.firstSegment);
256
+ if (!bucket) {
257
+ bucket = [];
258
+ segmentMap.set(f.firstSegment, bucket);
259
+ }
260
+ bucket.push(f);
261
+ } else dynamicFirst.push(f);
262
+ }
263
+ function buildRouteIndex(routes, compiled) {
264
+ const cached = _indexCache.get(routes);
265
+ if (cached) return cached;
266
+ const flattened = flattenRoutes(compiled);
267
+ const staticMap = /* @__PURE__ */ new Map();
268
+ const segmentMap = /* @__PURE__ */ new Map();
269
+ const dynamicFirst = [];
270
+ const wildcards = [];
271
+ for (const f of flattened) indexFlatRoute(f, staticMap, segmentMap, dynamicFirst, wildcards);
272
+ const index = {
273
+ staticMap,
274
+ segmentMap,
275
+ dynamicFirst,
276
+ wildcards
277
+ };
278
+ _indexCache.set(routes, index);
279
+ return index;
280
+ }
281
+ /** Split path into segments without allocating a filtered array */
282
+ function splitPath(path) {
283
+ if (path === "/") return [];
284
+ const start = path.charCodeAt(0) === 47 ? 1 : 0;
285
+ const end = path.length;
286
+ if (start >= end) return [];
287
+ const parts = [];
288
+ let segStart = start;
289
+ for (let i = start; i <= end; i++) if (i === end || path.charCodeAt(i) === 47) {
290
+ if (i > segStart) parts.push(path.substring(segStart, i));
291
+ segStart = i + 1;
292
+ }
293
+ return parts;
294
+ }
295
+ /** Decode only if the segment contains a `%` character */
296
+ function decodeSafe(s) {
297
+ return s.indexOf("%") >= 0 ? decodeURIComponent(s) : s;
298
+ }
299
+ /** Collect remaining path segments as a decoded splat value */
300
+ function captureSplat(pathParts, from, pathLen) {
301
+ const remaining = [];
302
+ for (let j = from; j < pathLen; j++) {
303
+ const p = pathParts[j];
304
+ if (p !== void 0) remaining.push(decodeSafe(p));
305
+ }
306
+ return remaining.join("/");
307
+ }
308
+ /** Try to match a flattened route against path parts */
309
+ function matchFlattened(f, pathParts, pathLen) {
310
+ if (f.segmentCount !== pathLen) {
311
+ if (!f.hasSplat || pathLen < f.segmentCount) return null;
312
+ }
151
313
  const params = {};
152
- for (let i = 0; i < patternParts.length; i++) {
153
- const pp = patternParts[i];
314
+ const segments = f.segments;
315
+ const count = f.segmentCount;
316
+ for (let i = 0; i < count; i++) {
317
+ const seg = segments[i];
154
318
  const pt = pathParts[i];
155
- if (pp.endsWith("*") && pp.startsWith(":")) {
156
- const paramName = pp.slice(1, -1);
157
- params[paramName] = pathParts.slice(i).map(decodeURIComponent).join("/");
319
+ if (!seg || pt === void 0) return null;
320
+ if (seg.isSplat) {
321
+ params[seg.paramName] = captureSplat(pathParts, i, pathLen);
158
322
  return params;
159
323
  }
160
- if (pp.startsWith(":")) params[pp.slice(1)] = decodeURIComponent(pt);
161
- else if (pp !== pt) return null;
324
+ if (seg.isParam) params[seg.paramName] = decodeSafe(pt);
325
+ else if (seg.raw !== pt) return null;
162
326
  }
163
- if (patternParts.length !== pathParts.length) return null;
164
327
  return params;
165
328
  }
166
- /**
167
- * Check if a path starts with a route's prefix (for nested route matching).
168
- * Returns the remaining path suffix, or null if no match.
169
- */
170
- function matchPrefix(pattern, path) {
171
- if (pattern === "(.*)" || pattern === "*") return {
172
- params: {},
173
- rest: path
174
- };
175
- const patternParts = pattern.split("/").filter(Boolean);
176
- const pathParts = path.split("/").filter(Boolean);
177
- if (pathParts.length < patternParts.length) return null;
178
- const params = {};
179
- for (let i = 0; i < patternParts.length; i++) {
180
- const pp = patternParts[i];
181
- const pt = pathParts[i];
182
- if (pp.endsWith("*") && pp.startsWith(":")) {
183
- const paramName = pp.slice(1, -1);
184
- params[paramName] = pathParts.slice(i).map(decodeURIComponent).join("/");
185
- return {
186
- params,
187
- rest: "/"
188
- };
189
- }
190
- if (pp.startsWith(":")) params[pp.slice(1)] = decodeURIComponent(pt);
191
- else if (pp !== pt) return null;
329
+ /** Search a list of flattened candidates for a match */
330
+ function searchCandidates(candidates, pathParts, pathLen) {
331
+ for (let i = 0; i < candidates.length; i++) {
332
+ const f = candidates[i];
333
+ if (!f) continue;
334
+ const params = matchFlattened(f, pathParts, pathLen);
335
+ if (params) return {
336
+ params,
337
+ matched: f.matchedChain
338
+ };
192
339
  }
193
- return {
194
- params,
195
- rest: `/${pathParts.slice(patternParts.length).join("/")}`
196
- };
340
+ return null;
197
341
  }
198
342
  /**
199
343
  * Resolve a raw path (including query string and hash) against the route tree.
200
- * Handles nested routes recursively.
344
+ * Uses flattened index for O(1) static lookup and first-segment dispatch.
201
345
  */
202
346
  function resolveRoute(rawPath, routes) {
203
347
  const qIdx = rawPath.indexOf("?");
@@ -207,14 +351,50 @@ function resolveRoute(rawPath, routes) {
207
351
  const cleanPath = hIdx >= 0 ? pathAndHash.slice(0, hIdx) : pathAndHash;
208
352
  const hash = hIdx >= 0 ? pathAndHash.slice(hIdx + 1) : "";
209
353
  const query = parseQuery(queryPart);
210
- const match = matchRoutes(cleanPath, routes, []);
211
- if (match) return {
354
+ const index = buildRouteIndex(routes, compileRoutes(routes));
355
+ const staticMatch = index.staticMap.get(cleanPath);
356
+ if (staticMatch) return {
357
+ path: cleanPath,
358
+ params: {},
359
+ query,
360
+ hash,
361
+ matched: staticMatch.matchedChain,
362
+ meta: staticMatch.meta
363
+ };
364
+ const pathParts = splitPath(cleanPath);
365
+ const pathLen = pathParts.length;
366
+ if (pathLen > 0) {
367
+ const first = pathParts[0];
368
+ const bucket = index.segmentMap.get(first);
369
+ if (bucket) {
370
+ const match = searchCandidates(bucket, pathParts, pathLen);
371
+ if (match) return {
372
+ path: cleanPath,
373
+ params: match.params,
374
+ query,
375
+ hash,
376
+ matched: match.matched,
377
+ meta: mergeMeta(match.matched)
378
+ };
379
+ }
380
+ }
381
+ const dynMatch = searchCandidates(index.dynamicFirst, pathParts, pathLen);
382
+ if (dynMatch) return {
212
383
  path: cleanPath,
213
- params: match.params,
384
+ params: dynMatch.params,
214
385
  query,
215
386
  hash,
216
- matched: match.matched,
217
- meta: mergeMeta(match.matched)
387
+ matched: dynMatch.matched,
388
+ meta: mergeMeta(dynMatch.matched)
389
+ };
390
+ const w = index.wildcards[0];
391
+ if (w) return {
392
+ path: cleanPath,
393
+ params: {},
394
+ query,
395
+ hash,
396
+ matched: w.matchedChain,
397
+ meta: w.meta
218
398
  };
219
399
  return {
220
400
  path: cleanPath,
@@ -225,44 +405,6 @@ function resolveRoute(rawPath, routes) {
225
405
  meta: {}
226
406
  };
227
407
  }
228
- function matchRoutes(path, routes, parentMatched, parentParams = {}) {
229
- for (const route of routes) {
230
- const result = matchSingleRoute(path, route, parentMatched, parentParams);
231
- if (result) return result;
232
- }
233
- return null;
234
- }
235
- function matchSingleRoute(path, route, parentMatched, parentParams) {
236
- if (!route.children || route.children.length === 0) {
237
- const params = matchPath(route.path, path);
238
- if (params === null) return null;
239
- return {
240
- params: {
241
- ...parentParams,
242
- ...params
243
- },
244
- matched: [...parentMatched, route]
245
- };
246
- }
247
- const prefix = matchPrefix(route.path, path);
248
- if (prefix === null) return null;
249
- const allParams = {
250
- ...parentParams,
251
- ...prefix.params
252
- };
253
- const matched = [...parentMatched, route];
254
- const childMatch = matchRoutes(prefix.rest, route.children, matched, allParams);
255
- if (childMatch) return childMatch;
256
- const exactParams = matchPath(route.path, path);
257
- if (exactParams === null) return null;
258
- return {
259
- params: {
260
- ...parentParams,
261
- ...exactParams
262
- },
263
- matched
264
- };
265
- }
266
408
  /** Merge meta from matched routes (leaf takes precedence) */
267
409
  function mergeMeta(matched) {
268
410
  const meta = {};