dalila 1.9.8 → 1.9.10
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/README.md +2 -0
- package/dist/core/for.js +25 -21
- package/dist/core/signal.js +25 -64
- package/dist/router/route-tables.d.ts +5 -0
- package/dist/router/route-tables.js +98 -10
- package/dist/router/router.js +39 -14
- package/dist/runtime/bind.js +334 -45
- package/dist/runtime/boundary.d.ts +104 -0
- package/dist/runtime/boundary.js +304 -0
- package/dist/runtime/index.d.ts +4 -0
- package/dist/runtime/index.js +2 -0
- package/dist/runtime/lazy.d.ts +109 -0
- package/dist/runtime/lazy.js +254 -0
- package/package.json +1 -1
- package/dist/routes.generated.d.ts +0 -2
- package/dist/routes.generated.js +0 -76
- package/dist/routes.generated.manifest.d.ts +0 -4
- package/dist/routes.generated.manifest.js +0 -32
- package/dist/routes.generated.types.d.ts +0 -11
- package/dist/routes.generated.types.js +0 -37
package/README.md
CHANGED
|
@@ -60,6 +60,8 @@ bind(document.getElementById('app')!, ctx);
|
|
|
60
60
|
|
|
61
61
|
- [Template Binding](./docs/runtime/bind.md) — `bind()`, `mount()`, `configure()`, transitions, portal, text interpolation, events
|
|
62
62
|
- [Components](./docs/runtime/component.md) — `defineComponent`, typed props/emits/refs, slots
|
|
63
|
+
- [Lazy Loading](./docs/runtime/lazy.md) — `createLazyComponent`, `d-lazy`, `createSuspense` wrapper, code splitting
|
|
64
|
+
- [Error Boundary](./docs/runtime/boundary.md) — `createErrorBoundary`, `createErrorBoundaryState`, `withErrorBoundary`, `d-boundary`
|
|
63
65
|
- [FOUC Prevention](./docs/runtime/fouc-prevention.md) — Automatic token hiding
|
|
64
66
|
|
|
65
67
|
### Routing
|
package/dist/core/for.js
CHANGED
|
@@ -178,6 +178,8 @@ export function forEach(items, template, keyFn) {
|
|
|
178
178
|
}
|
|
179
179
|
};
|
|
180
180
|
let hasValidatedOnce = false;
|
|
181
|
+
const reusableOldMap = new Map();
|
|
182
|
+
const reusableSeenNextKeys = new Set();
|
|
181
183
|
const throwDuplicateKey = (key, scheduleFatal) => {
|
|
182
184
|
const error = new Error(`[Dalila] Duplicate key "${key}" detected in forEach. ` +
|
|
183
185
|
`Keys must be unique within the same list. Check your keyFn implementation.`);
|
|
@@ -231,43 +233,42 @@ export function forEach(items, template, keyFn) {
|
|
|
231
233
|
const newItems = items();
|
|
232
234
|
// Validate again on updates (will be caught by effect error handler)
|
|
233
235
|
validateNoDuplicateKeys(newItems);
|
|
234
|
-
|
|
235
|
-
currentItems.forEach(item =>
|
|
236
|
+
reusableOldMap.clear();
|
|
237
|
+
currentItems.forEach(item => reusableOldMap.set(item.key, item));
|
|
236
238
|
const nextItems = [];
|
|
237
|
-
|
|
238
|
-
const seenNextKeys = new Set();
|
|
239
|
+
reusableSeenNextKeys.clear();
|
|
239
240
|
// Phase 1: Build next list + detect updates/new
|
|
240
241
|
newItems.forEach((item, index) => {
|
|
241
242
|
const key = getKey(item, index);
|
|
242
|
-
if (
|
|
243
|
+
if (reusableSeenNextKeys.has(key))
|
|
243
244
|
return; // prod-mode: ignore dup keys silently
|
|
244
|
-
|
|
245
|
-
const existing =
|
|
245
|
+
reusableSeenNextKeys.add(key);
|
|
246
|
+
const existing = reusableOldMap.get(key);
|
|
246
247
|
if (existing) {
|
|
248
|
+
reusableOldMap.delete(key);
|
|
247
249
|
if (existing.value !== item) {
|
|
248
|
-
itemsToUpdate.add(key);
|
|
249
250
|
existing.value = item;
|
|
251
|
+
existing.dirty = true;
|
|
250
252
|
}
|
|
251
253
|
nextItems.push(existing);
|
|
252
254
|
}
|
|
253
255
|
else {
|
|
254
|
-
|
|
255
|
-
nextItems.push({
|
|
256
|
+
const created = {
|
|
256
257
|
key,
|
|
257
258
|
value: item,
|
|
258
259
|
start: document.createComment(`for:${key}:start`),
|
|
259
260
|
end: document.createComment(`for:${key}:end`),
|
|
260
261
|
scope: null,
|
|
261
|
-
indexSignal: signal(index)
|
|
262
|
-
|
|
262
|
+
indexSignal: signal(index),
|
|
263
|
+
dirty: true,
|
|
264
|
+
};
|
|
265
|
+
nextItems.push(created);
|
|
263
266
|
}
|
|
264
267
|
});
|
|
265
268
|
// Phase 2: Remove items no longer present
|
|
266
|
-
const
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
removeRange(item);
|
|
270
|
-
});
|
|
269
|
+
for (const staleItem of reusableOldMap.values()) {
|
|
270
|
+
removeRange(staleItem);
|
|
271
|
+
}
|
|
271
272
|
// Phase 3: Move/insert items to correct positions
|
|
272
273
|
const parent = end.parentNode;
|
|
273
274
|
if (parent) {
|
|
@@ -288,18 +289,20 @@ export function forEach(items, template, keyFn) {
|
|
|
288
289
|
}
|
|
289
290
|
// Phase 4: Dispose scopes and clear content for changed items
|
|
290
291
|
nextItems.forEach(item => {
|
|
291
|
-
if (!
|
|
292
|
+
if (!item.dirty)
|
|
292
293
|
return;
|
|
293
294
|
disposeItemScope(item);
|
|
294
295
|
clearBetween(item.start, item.end);
|
|
295
296
|
});
|
|
296
|
-
// Phase 5: Update reactive indices
|
|
297
|
+
// Phase 5: Update reactive indices only when index actually changes
|
|
297
298
|
nextItems.forEach((item, index) => {
|
|
298
|
-
item.indexSignal.
|
|
299
|
+
if (item.indexSignal.peek() !== index) {
|
|
300
|
+
item.indexSignal.set(index);
|
|
301
|
+
}
|
|
299
302
|
});
|
|
300
303
|
// Phase 6: Render changed items
|
|
301
304
|
nextItems.forEach(item => {
|
|
302
|
-
if (!
|
|
305
|
+
if (!item.dirty)
|
|
303
306
|
return;
|
|
304
307
|
item.scope = createScope();
|
|
305
308
|
withScope(item.scope, () => {
|
|
@@ -308,6 +311,7 @@ export function forEach(items, template, keyFn) {
|
|
|
308
311
|
const nodes = Array.isArray(templateResult) ? templateResult : [templateResult];
|
|
309
312
|
item.end.before(...nodes);
|
|
310
313
|
});
|
|
314
|
+
item.dirty = false;
|
|
311
315
|
});
|
|
312
316
|
currentItems = nextItems;
|
|
313
317
|
};
|
package/dist/core/signal.js
CHANGED
|
@@ -44,16 +44,6 @@ let activeScope = null;
|
|
|
44
44
|
* Per-tick dedupe for async effects.
|
|
45
45
|
* Multiple writes in the same tick schedule an effect only once.
|
|
46
46
|
*/
|
|
47
|
-
const pendingEffects = new Set();
|
|
48
|
-
/**
|
|
49
|
-
* Stable runner per effect (function identity).
|
|
50
|
-
*
|
|
51
|
-
* Why:
|
|
52
|
-
* - when batching, we enqueue a function into the batch queue
|
|
53
|
-
* - dedupe uses function identity
|
|
54
|
-
* - each effect needs a stable runner function across schedules
|
|
55
|
-
*/
|
|
56
|
-
const effectRunners = new WeakMap();
|
|
57
47
|
/**
|
|
58
48
|
* Schedule an effect with correct semantics:
|
|
59
49
|
* - computed invalidations run synchronously (mark dirty immediately)
|
|
@@ -73,15 +63,12 @@ function scheduleEffect(eff) {
|
|
|
73
63
|
}
|
|
74
64
|
return;
|
|
75
65
|
}
|
|
76
|
-
|
|
77
|
-
if (pendingEffects.has(eff))
|
|
66
|
+
if (eff.queued)
|
|
78
67
|
return;
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
runEffect = () => {
|
|
84
|
-
pendingEffects.delete(eff);
|
|
68
|
+
eff.queued = true;
|
|
69
|
+
if (!eff.runner) {
|
|
70
|
+
eff.runner = () => {
|
|
71
|
+
eff.queued = false;
|
|
85
72
|
if (eff.disposed)
|
|
86
73
|
return;
|
|
87
74
|
try {
|
|
@@ -91,14 +78,27 @@ function scheduleEffect(eff) {
|
|
|
91
78
|
reportEffectError(error, 'effect');
|
|
92
79
|
}
|
|
93
80
|
};
|
|
94
|
-
effectRunners.set(eff, runEffect);
|
|
95
81
|
}
|
|
96
82
|
// During batch: defer scheduling into the batch queue (no microtask overhead).
|
|
97
83
|
// Outside batch: schedule in a microtask (coalescing across multiple writes).
|
|
98
84
|
if (isBatching())
|
|
99
|
-
queueInBatch(
|
|
85
|
+
queueInBatch(eff.runner);
|
|
100
86
|
else
|
|
101
|
-
scheduleMicrotask(
|
|
87
|
+
scheduleMicrotask(eff.runner);
|
|
88
|
+
}
|
|
89
|
+
function trySubscribeActiveEffect(subscribers, signalRef) {
|
|
90
|
+
if (!activeEffect || activeEffect.disposed)
|
|
91
|
+
return;
|
|
92
|
+
if (activeScope) {
|
|
93
|
+
const current = getCurrentScope();
|
|
94
|
+
if (activeScope !== current)
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
if (subscribers.has(activeEffect))
|
|
98
|
+
return;
|
|
99
|
+
subscribers.add(activeEffect);
|
|
100
|
+
(activeEffect.deps ?? (activeEffect.deps = new Set())).add(subscribers);
|
|
101
|
+
trackDependency(signalRef, activeEffect);
|
|
102
102
|
}
|
|
103
103
|
/**
|
|
104
104
|
* Create a signal: a mutable value with automatic dependency tracking.
|
|
@@ -121,28 +121,7 @@ export function signal(initialValue) {
|
|
|
121
121
|
let signalRef;
|
|
122
122
|
const read = () => {
|
|
123
123
|
trackSignalRead(signalRef);
|
|
124
|
-
|
|
125
|
-
// Scope-aware subscription guard (best effort):
|
|
126
|
-
// - if the active effect is not scoped, allow subscription
|
|
127
|
-
// - if scoped, only subscribe when currently executing inside that scope
|
|
128
|
-
if (!activeScope) {
|
|
129
|
-
if (!subscribers.has(activeEffect)) {
|
|
130
|
-
subscribers.add(activeEffect);
|
|
131
|
-
(activeEffect.deps ?? (activeEffect.deps = new Set())).add(subscribers);
|
|
132
|
-
trackDependency(signalRef, activeEffect);
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
else {
|
|
136
|
-
const current = getCurrentScope();
|
|
137
|
-
if (activeScope === current) {
|
|
138
|
-
if (!subscribers.has(activeEffect)) {
|
|
139
|
-
subscribers.add(activeEffect);
|
|
140
|
-
(activeEffect.deps ?? (activeEffect.deps = new Set())).add(subscribers);
|
|
141
|
-
trackDependency(signalRef, activeEffect);
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
}
|
|
124
|
+
trySubscribeActiveEffect(subscribers, signalRef);
|
|
146
125
|
return value;
|
|
147
126
|
};
|
|
148
127
|
const notify = () => {
|
|
@@ -223,7 +202,7 @@ export function effect(fn) {
|
|
|
223
202
|
return;
|
|
224
203
|
run.disposed = true;
|
|
225
204
|
cleanupDeps();
|
|
226
|
-
|
|
205
|
+
run.queued = false;
|
|
227
206
|
trackEffectDispose(run);
|
|
228
207
|
};
|
|
229
208
|
const run = (() => {
|
|
@@ -302,25 +281,7 @@ export function computed(fn) {
|
|
|
302
281
|
const read = () => {
|
|
303
282
|
trackSignalRead(signalRef);
|
|
304
283
|
// Allow effects to subscribe to this computed (same rules as signal()).
|
|
305
|
-
|
|
306
|
-
if (!activeScope) {
|
|
307
|
-
if (!subscribers.has(activeEffect)) {
|
|
308
|
-
subscribers.add(activeEffect);
|
|
309
|
-
(activeEffect.deps ?? (activeEffect.deps = new Set())).add(subscribers);
|
|
310
|
-
trackDependency(signalRef, activeEffect);
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
|
-
else {
|
|
314
|
-
const current = getCurrentScope();
|
|
315
|
-
if (activeScope === current) {
|
|
316
|
-
if (!subscribers.has(activeEffect)) {
|
|
317
|
-
subscribers.add(activeEffect);
|
|
318
|
-
(activeEffect.deps ?? (activeEffect.deps = new Set())).add(subscribers);
|
|
319
|
-
trackDependency(signalRef, activeEffect);
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
}
|
|
323
|
-
}
|
|
284
|
+
trySubscribeActiveEffect(subscribers, signalRef);
|
|
324
285
|
if (dirty) {
|
|
325
286
|
cleanupDeps();
|
|
326
287
|
const prevEffect = activeEffect;
|
|
@@ -426,7 +387,7 @@ export function effectAsync(fn) {
|
|
|
426
387
|
controller?.abort();
|
|
427
388
|
controller = null;
|
|
428
389
|
cleanupDeps();
|
|
429
|
-
|
|
390
|
+
run.queued = false;
|
|
430
391
|
trackEffectDispose(run);
|
|
431
392
|
};
|
|
432
393
|
const run = (() => {
|
|
@@ -121,6 +121,11 @@ export interface RouteStackResult {
|
|
|
121
121
|
export interface CompiledRoute {
|
|
122
122
|
route: RouteTable;
|
|
123
123
|
fullPath: string;
|
|
124
|
+
normalizedFullPath: string;
|
|
125
|
+
/** Static first segment of fullPath (null for dynamic/wildcard/root-first paths). */
|
|
126
|
+
firstStaticSegment: string | null;
|
|
127
|
+
staticExactPath: string | null;
|
|
128
|
+
staticPrefixPath: string | null;
|
|
124
129
|
exactPattern: RegExp;
|
|
125
130
|
/** null means root "/" which always matches as prefix */
|
|
126
131
|
prefixPattern: RegExp | null;
|
|
@@ -92,6 +92,46 @@ function sortRoutes(routes) {
|
|
|
92
92
|
function escapeRegexSegment(segment) {
|
|
93
93
|
return segment.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
94
94
|
}
|
|
95
|
+
const routeLevelIndexCache = new WeakMap();
|
|
96
|
+
function resolveFirstStaticSegment(path) {
|
|
97
|
+
const normalized = normalizePath(path);
|
|
98
|
+
const segments = normalized.split('/').filter(Boolean);
|
|
99
|
+
const first = segments[0];
|
|
100
|
+
if (!first)
|
|
101
|
+
return null;
|
|
102
|
+
if (first.startsWith(':') || first.includes('*'))
|
|
103
|
+
return null;
|
|
104
|
+
return first;
|
|
105
|
+
}
|
|
106
|
+
function isFullyStaticPath(path) {
|
|
107
|
+
const normalized = normalizePath(path);
|
|
108
|
+
const segments = normalized.split('/').filter(Boolean);
|
|
109
|
+
for (const segment of segments) {
|
|
110
|
+
if (segment.startsWith(':') || segment.includes('*'))
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
return true;
|
|
114
|
+
}
|
|
115
|
+
function buildRouteLevelIndex(compiled) {
|
|
116
|
+
const staticByFirstSegment = new Map();
|
|
117
|
+
const genericRoutes = [];
|
|
118
|
+
const staticParentRoutesWithChildren = [];
|
|
119
|
+
for (const entry of compiled) {
|
|
120
|
+
if (entry.firstStaticSegment) {
|
|
121
|
+
const bucket = staticByFirstSegment.get(entry.firstStaticSegment);
|
|
122
|
+
if (bucket)
|
|
123
|
+
bucket.push(entry);
|
|
124
|
+
else
|
|
125
|
+
staticByFirstSegment.set(entry.firstStaticSegment, [entry]);
|
|
126
|
+
if (entry.children.length > 0) {
|
|
127
|
+
staticParentRoutesWithChildren.push(entry);
|
|
128
|
+
}
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
genericRoutes.push(entry);
|
|
132
|
+
}
|
|
133
|
+
return { staticByFirstSegment, genericRoutes, staticParentRoutesWithChildren };
|
|
134
|
+
}
|
|
95
135
|
function parseDynamicSegment(segment) {
|
|
96
136
|
if (!segment.startsWith(':'))
|
|
97
137
|
return null;
|
|
@@ -196,6 +236,7 @@ export function compileRoutes(routes, parentPath = '') {
|
|
|
196
236
|
return sortRoutes(routes).map(route => {
|
|
197
237
|
const fullPath = joinPaths(parentPath, route.path);
|
|
198
238
|
const normalizedFull = normalizePath(fullPath);
|
|
239
|
+
const staticPath = isFullyStaticPath(fullPath);
|
|
199
240
|
const exact = parsePath(fullPath);
|
|
200
241
|
const prefix = normalizedFull === '/'
|
|
201
242
|
? null
|
|
@@ -203,6 +244,10 @@ export function compileRoutes(routes, parentPath = '') {
|
|
|
203
244
|
return {
|
|
204
245
|
route,
|
|
205
246
|
fullPath,
|
|
247
|
+
normalizedFullPath: normalizedFull,
|
|
248
|
+
firstStaticSegment: resolveFirstStaticSegment(fullPath),
|
|
249
|
+
staticExactPath: staticPath ? normalizedFull : null,
|
|
250
|
+
staticPrefixPath: staticPath && normalizedFull !== '/' ? normalizedFull : null,
|
|
206
251
|
exactPattern: exact.pattern,
|
|
207
252
|
prefixPattern: prefix?.pattern ?? null,
|
|
208
253
|
paramCaptures: exact.paramCaptures,
|
|
@@ -214,6 +259,9 @@ export function compileRoutes(routes, parentPath = '') {
|
|
|
214
259
|
}
|
|
215
260
|
/** Test a pathname against a compiled route's exact pattern. */
|
|
216
261
|
function matchCompiled(pathname, compiled) {
|
|
262
|
+
if (compiled.staticExactPath !== null) {
|
|
263
|
+
return pathname === compiled.staticExactPath ? {} : null;
|
|
264
|
+
}
|
|
217
265
|
const match = pathname.match(compiled.exactPattern);
|
|
218
266
|
if (!match)
|
|
219
267
|
return null;
|
|
@@ -223,6 +271,12 @@ function matchCompiled(pathname, compiled) {
|
|
|
223
271
|
function matchCompiledPrefix(pathname, compiled) {
|
|
224
272
|
if (!compiled.prefixPattern)
|
|
225
273
|
return {};
|
|
274
|
+
if (compiled.staticPrefixPath !== null) {
|
|
275
|
+
if (pathname === compiled.staticPrefixPath || pathname.startsWith(`${compiled.staticPrefixPath}/`)) {
|
|
276
|
+
return {};
|
|
277
|
+
}
|
|
278
|
+
return null;
|
|
279
|
+
}
|
|
226
280
|
const match = pathname.match(compiled.prefixPattern);
|
|
227
281
|
if (!match)
|
|
228
282
|
return null;
|
|
@@ -236,11 +290,17 @@ function matchCompiledPrefix(pathname, compiled) {
|
|
|
236
290
|
*/
|
|
237
291
|
export function findCompiledRouteStackResult(pathname, compiled, stack = []) {
|
|
238
292
|
const normalizedPathname = normalizePath(pathname);
|
|
239
|
-
|
|
293
|
+
const firstPathSegment = normalizedPathname.split('/').filter(Boolean)[0] ?? '';
|
|
294
|
+
return findCompiledRouteStackResultNormalized(normalizedPathname, firstPathSegment, compiled, stack);
|
|
240
295
|
}
|
|
241
|
-
function findCompiledRouteStackResultNormalized(pathname, compiled, stack) {
|
|
296
|
+
function findCompiledRouteStackResultNormalized(pathname, firstPathSegment, compiled, stack) {
|
|
297
|
+
let levelIndex = routeLevelIndexCache.get(compiled);
|
|
298
|
+
if (!levelIndex) {
|
|
299
|
+
levelIndex = buildRouteLevelIndex(compiled);
|
|
300
|
+
routeLevelIndexCache.set(compiled, levelIndex);
|
|
301
|
+
}
|
|
242
302
|
let bestPartial = null;
|
|
243
|
-
|
|
303
|
+
const tryCandidate = (entry) => {
|
|
244
304
|
const exactParams = matchCompiled(pathname, entry);
|
|
245
305
|
const prefixParams = !exactParams && (entry.route.children || entry.route.layout)
|
|
246
306
|
? matchCompiledPrefix(pathname, entry)
|
|
@@ -253,29 +313,34 @@ function findCompiledRouteStackResultNormalized(pathname, compiled, stack) {
|
|
|
253
313
|
path: entry.fullPath,
|
|
254
314
|
params
|
|
255
315
|
};
|
|
256
|
-
|
|
316
|
+
stack.push(match);
|
|
257
317
|
if (entry.children.length > 0) {
|
|
258
|
-
const childResult = findCompiledRouteStackResultNormalized(pathname, entry.children,
|
|
318
|
+
const childResult = findCompiledRouteStackResultNormalized(pathname, firstPathSegment, entry.children, stack);
|
|
259
319
|
if (childResult) {
|
|
260
|
-
if (childResult.exact)
|
|
320
|
+
if (childResult.exact) {
|
|
321
|
+
stack.pop();
|
|
261
322
|
return childResult;
|
|
323
|
+
}
|
|
262
324
|
if (!bestPartial || childResult.stack.length > bestPartial.length) {
|
|
263
325
|
bestPartial = childResult.stack;
|
|
264
326
|
}
|
|
265
327
|
}
|
|
266
328
|
}
|
|
267
329
|
if (isExact && (entry.route.view || entry.route.redirect)) {
|
|
268
|
-
|
|
330
|
+
const exactStack = stack.slice();
|
|
331
|
+
stack.pop();
|
|
332
|
+
return { stack: exactStack, exact: true };
|
|
269
333
|
}
|
|
270
334
|
if (!isExact && (entry.route.layout || entry.route.children)) {
|
|
271
|
-
if (!bestPartial ||
|
|
272
|
-
bestPartial =
|
|
335
|
+
if (!bestPartial || stack.length > bestPartial.length) {
|
|
336
|
+
bestPartial = stack.slice();
|
|
273
337
|
}
|
|
274
338
|
}
|
|
339
|
+
stack.pop();
|
|
275
340
|
}
|
|
276
341
|
else if (entry.children.length > 0) {
|
|
277
342
|
// Parent didn't match but children may have absolute-like paths
|
|
278
|
-
const childResult = findCompiledRouteStackResultNormalized(pathname, entry.children, stack);
|
|
343
|
+
const childResult = findCompiledRouteStackResultNormalized(pathname, firstPathSegment, entry.children, stack);
|
|
279
344
|
if (childResult) {
|
|
280
345
|
if (childResult.exact)
|
|
281
346
|
return childResult;
|
|
@@ -284,6 +349,29 @@ function findCompiledRouteStackResultNormalized(pathname, compiled, stack) {
|
|
|
284
349
|
}
|
|
285
350
|
}
|
|
286
351
|
}
|
|
352
|
+
return null;
|
|
353
|
+
};
|
|
354
|
+
const specific = levelIndex.staticByFirstSegment.get(firstPathSegment);
|
|
355
|
+
if (specific) {
|
|
356
|
+
for (const entry of specific) {
|
|
357
|
+
const exact = tryCandidate(entry);
|
|
358
|
+
if (exact)
|
|
359
|
+
return exact;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
for (const entry of levelIndex.genericRoutes) {
|
|
363
|
+
const exact = tryCandidate(entry);
|
|
364
|
+
if (exact)
|
|
365
|
+
return exact;
|
|
366
|
+
}
|
|
367
|
+
// Preserve support for absolute-like child paths under static parents.
|
|
368
|
+
// Example: parent "/admin" with child "/login" must still resolve "/login".
|
|
369
|
+
for (const entry of levelIndex.staticParentRoutesWithChildren) {
|
|
370
|
+
if (entry.firstStaticSegment === firstPathSegment)
|
|
371
|
+
continue;
|
|
372
|
+
const exact = tryCandidate(entry);
|
|
373
|
+
if (exact)
|
|
374
|
+
return exact;
|
|
287
375
|
}
|
|
288
376
|
return bestPartial ? { stack: bestPartial, exact: false } : null;
|
|
289
377
|
}
|
package/dist/router/router.js
CHANGED
|
@@ -148,17 +148,42 @@ export function createRouter(config) {
|
|
|
148
148
|
function applyBase(fullPath) {
|
|
149
149
|
if (!basePrefix)
|
|
150
150
|
return fullPath;
|
|
151
|
-
const
|
|
152
|
-
const pathname =
|
|
153
|
-
return `${pathname}${
|
|
151
|
+
const location = parseLocation(fullPath);
|
|
152
|
+
const pathname = location.pathname === '/' ? basePrefix : normalizePath(`${basePrefix}${location.pathname}`);
|
|
153
|
+
return `${pathname}${location.queryString ? `?${location.queryString}` : ''}${location.hash ? `#${location.hash}` : ''}`;
|
|
154
|
+
}
|
|
155
|
+
function parseRelativePath(to) {
|
|
156
|
+
let rest = to;
|
|
157
|
+
let hash = '';
|
|
158
|
+
const hashIdx = rest.indexOf('#');
|
|
159
|
+
if (hashIdx >= 0) {
|
|
160
|
+
hash = rest.slice(hashIdx + 1);
|
|
161
|
+
rest = rest.slice(0, hashIdx);
|
|
162
|
+
}
|
|
163
|
+
let queryString = '';
|
|
164
|
+
const queryIdx = rest.indexOf('?');
|
|
165
|
+
if (queryIdx >= 0) {
|
|
166
|
+
queryString = rest.slice(queryIdx + 1);
|
|
167
|
+
rest = rest.slice(0, queryIdx);
|
|
168
|
+
}
|
|
169
|
+
const pathname = stripBase(rest || '/');
|
|
170
|
+
const fullPath = `${pathname}${queryString ? `?${queryString}` : ''}${hash ? `#${hash}` : ''}`;
|
|
171
|
+
return { pathname, queryString, hash, fullPath };
|
|
172
|
+
}
|
|
173
|
+
function getLocationQuery(location) {
|
|
174
|
+
if (!location.query) {
|
|
175
|
+
location.query = new URLSearchParams(location.queryString);
|
|
176
|
+
}
|
|
177
|
+
return location.query;
|
|
154
178
|
}
|
|
155
179
|
function parseLocation(to) {
|
|
180
|
+
// Fast path for most app navigations (absolute pathname within same origin).
|
|
181
|
+
if (to.startsWith('/')) {
|
|
182
|
+
return parseRelativePath(to);
|
|
183
|
+
}
|
|
184
|
+
// Fallback for relative paths and absolute URLs.
|
|
156
185
|
const url = new URL(to, window.location.href);
|
|
157
|
-
|
|
158
|
-
const query = new URLSearchParams(url.search);
|
|
159
|
-
const hash = url.hash ? url.hash.slice(1) : '';
|
|
160
|
-
const fullPath = `${pathname}${url.search}${hash ? `#${hash}` : ''}`;
|
|
161
|
-
return { pathname, query, hash, fullPath };
|
|
186
|
+
return parseRelativePath(`${url.pathname}${url.search}${url.hash}`);
|
|
162
187
|
}
|
|
163
188
|
function joinRoutePaths(parent, child) {
|
|
164
189
|
if (!child || child === '.')
|
|
@@ -194,7 +219,7 @@ export function createRouter(config) {
|
|
|
194
219
|
path: location.pathname,
|
|
195
220
|
fullPath: location.fullPath,
|
|
196
221
|
params: match.params,
|
|
197
|
-
queryString: location.
|
|
222
|
+
queryString: location.queryString,
|
|
198
223
|
hash: location.hash
|
|
199
224
|
});
|
|
200
225
|
}
|
|
@@ -223,7 +248,7 @@ export function createRouter(config) {
|
|
|
223
248
|
}
|
|
224
249
|
function resolvePreloadKey(match, location) {
|
|
225
250
|
const routeId = resolvePreloadRouteId(match);
|
|
226
|
-
const search = location.
|
|
251
|
+
const search = location.queryString;
|
|
227
252
|
const urlKey = search ? `${location.pathname}?${search}` : location.pathname;
|
|
228
253
|
return `${routeId}::${match.path}::${urlKey}`;
|
|
229
254
|
}
|
|
@@ -495,7 +520,7 @@ export function createRouter(config) {
|
|
|
495
520
|
routePath: match.path,
|
|
496
521
|
routeId: manifest?.id,
|
|
497
522
|
params: { ...match.params },
|
|
498
|
-
queryString: location.
|
|
523
|
+
queryString: location.queryString,
|
|
499
524
|
tags: resolveMatchTags(match),
|
|
500
525
|
score: resolveMatchScore(match)
|
|
501
526
|
};
|
|
@@ -656,7 +681,7 @@ export function createRouter(config) {
|
|
|
656
681
|
path: location.pathname,
|
|
657
682
|
fullPath: location.fullPath,
|
|
658
683
|
params: {},
|
|
659
|
-
queryString: location.
|
|
684
|
+
queryString: location.queryString,
|
|
660
685
|
hash: location.hash
|
|
661
686
|
});
|
|
662
687
|
statusSignal.set({ state: 'loading', to: toState });
|
|
@@ -941,7 +966,7 @@ export function createRouter(config) {
|
|
|
941
966
|
// Validate all matches first, then load data in parallel
|
|
942
967
|
let dataStack;
|
|
943
968
|
try {
|
|
944
|
-
const queryValues = createRouteQueryValues(location
|
|
969
|
+
const queryValues = createRouteQueryValues(getLocationQuery(location));
|
|
945
970
|
// Phase 1: Validate (sequential -- fail fast on first error)
|
|
946
971
|
for (const match of matchStack) {
|
|
947
972
|
const paramsValues = createRouteParamsValues(match.params);
|
|
@@ -1394,7 +1419,7 @@ export function createRouter(config) {
|
|
|
1394
1419
|
}
|
|
1395
1420
|
}
|
|
1396
1421
|
}
|
|
1397
|
-
const queryValues = createRouteQueryValues(location
|
|
1422
|
+
const queryValues = createRouteQueryValues(getLocationQuery(location));
|
|
1398
1423
|
const pending = [];
|
|
1399
1424
|
for (const match of stack) {
|
|
1400
1425
|
const preloadFn = match.route.preload ?? match.route.loader;
|