@adukiorg/anza 0.2.0 → 0.2.3
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/CHANGELOG.md +90 -4
- package/README.md +97 -133
- package/bin/anza/anza-linux-arm64 +0 -0
- package/bin/anza/anza-linux-x64 +0 -0
- package/bin/anza/anza-macos-arm64 +0 -0
- package/bin/anza/anza-macos-x64 +0 -0
- package/bin/anza/anza-windows-x64.exe +0 -0
- package/bin/anza/find.js +35 -0
- package/bin/anza/index.js +34 -0
- package/bin/anza/launch.js +19 -0
- package/bin/common/index.js +7 -0
- package/bin/common/logs.js +62 -0
- package/bin/create/copy.js +18 -0
- package/bin/create/index.js +45 -0
- package/bin/create/run.js +210 -0
- package/bin/create/write.js +19 -0
- package/importmap.json +4 -0
- package/package.json +16 -10
- package/src/core/offline/{usage.md → notes/usage.md} +11 -1
- package/src/core/router/boot.js +82 -0
- package/src/core/router/cascade.js +76 -0
- package/src/core/router/container.js +63 -72
- package/src/core/router/graph.js +144 -0
- package/src/core/router/index.js +12 -2
- package/src/core/router/intercept.js +26 -7
- package/src/core/router/lca.js +58 -0
- package/src/core/router/match.js +49 -36
- package/src/core/router/notes/audit-old.md +887 -0
- package/src/core/router/notes/audti.md +773 -0
- package/src/core/router/notes/tasks.md +473 -0
- package/src/core/router/{usage.md → notes/usage.md} +57 -35
- package/src/core/router/sync/tab.js +6 -4
- package/src/core/router/transitions.js +35 -8
- package/src/core/router/trie.js +130 -0
- package/src/core/security/{usage.md → notes/usage.md} +1 -2
- package/src/core/storage/{usage.md → notes/usage.md} +6 -6
- package/src/core/theme/index.js +78 -0
- package/src/core/ui/define/index.js +2 -1
- package/src/core/ui/define/orchestrator.js +10 -4
- package/src/core/ui/defs/dock.js +134 -0
- package/src/core/ui/defs/index.js +20 -0
- package/src/core/ui/defs/page.js +89 -0
- package/src/core/ui/defs/part.js +28 -0
- package/src/core/ui/defs/spec.js +96 -0
- package/src/core/ui/defs/view.js +23 -0
- package/src/core/ui/index.js +16 -3
- package/src/core/ui/notes/definations.md +979 -0
- package/src/tokens/index.css +1 -0
- package/src/tokens/semantic/contrast.css +18 -0
- package/src/tokens/semantic/transitions.css +32 -0
- package/types/core/platform/index.d.ts +39 -10
- package/types/core/router/index.d.ts +9 -0
- package/types/core/theme/index.d.ts +18 -0
- package/types/core/ui/index.d.ts +11 -0
- package/types/index.d.ts +1 -0
- package/bin/anza.js +0 -63
- package/bin/create.js +0 -150
- package/src/core/api/plan.md +0 -209
- package/src/core/events/missing.md +0 -103
- package/src/core/events/plan.md +0 -177
- package/src/core/offline/missing.md +0 -89
- package/src/core/offline/plan.md +0 -143
- package/src/core/platform/missing.md +0 -119
- package/src/core/platform/platform.d.ts +0 -88
- package/src/core/router/missing.md +0 -716
- package/src/core/router/outlet.js +0 -139
- package/src/core/router/plan.md +0 -370
- package/src/core/security/missing.md +0 -97
- package/src/core/state/missing.md +0 -165
- package/src/core/storage/missing.md +0 -165
- package/src/core/storage/plan.md +0 -69
- package/src/core/ui/implementation.md +0 -170
- package/src/core/ui/plan.md +0 -510
- package/src/core/ui/ui.types.md +0 -890
- /package/src/core/animations/{usage.md → notes/usage.md} +0 -0
- /package/src/core/api/{usage.md → notes/usage.md} +0 -0
- /package/src/core/events/{usage.md → notes/usage.md} +0 -0
- /package/src/core/platform/{usage.md → notes/usage.md} +0 -0
- /package/src/core/state/{usage.md → notes/usage.md} +0 -0
- /package/src/core/ui/{usage.md → notes/usage.md} +0 -0
- /package/src/core/ui/{watch.md → notes/watch.md} +0 -0
- /package/src/core/workers/{plan.md → notes/plan.md} +0 -0
- /package/src/core/workers/{usage.md → notes/usage.md} +0 -0
|
@@ -1,121 +1,109 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* src/core/router/container.js
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* Container registry facade. Delegates storage to the hierarchical graph
|
|
5
|
+
* (graph.js) while preserving the historical public API and the CSS-selector
|
|
6
|
+
* fallback for containers that are plain DOM elements rather than registered
|
|
7
|
+
* docks.
|
|
8
8
|
*
|
|
9
|
-
* Source: advance2.md
|
|
9
|
+
* Source: tasks.md Phase 3, advance2.md
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
const containerNodeMap = new Map();
|
|
12
|
+
import { add, remove, element as graphElement, get, clear } from './graph.js';
|
|
14
13
|
|
|
15
|
-
//
|
|
16
|
-
const cleanupRegistry = typeof FinalizationRegistry !== 'undefined'
|
|
17
|
-
? new FinalizationRegistry((name) => {
|
|
18
|
-
if (containerNodeMap.get(name)?.deref() === undefined) {
|
|
19
|
-
containerNodeMap.delete(name);
|
|
20
|
-
}
|
|
21
|
-
})
|
|
22
|
-
: { register() {}, unregister() {} };
|
|
23
|
-
|
|
24
|
-
let observer;
|
|
14
|
+
// Selectors we are still waiting to appear in the DOM.
|
|
25
15
|
const observedSelectors = new Set();
|
|
16
|
+
let observer;
|
|
26
17
|
|
|
27
18
|
/**
|
|
28
|
-
* Boots the MutationObserver
|
|
19
|
+
* Boots the MutationObserver to discover standard (non-dock) containers that
|
|
20
|
+
* match a tracked CSS selector. Uses requestIdleCallback with a 100ms timeout
|
|
21
|
+
* so it still fires under sustained animation load, falling back to
|
|
22
|
+
* requestAnimationFrame where idle callbacks are unavailable (RT bug 8.1).
|
|
29
23
|
*/
|
|
30
24
|
function ensureObserver() {
|
|
31
|
-
if (observer || typeof window === 'undefined' || typeof
|
|
25
|
+
if (observer || typeof window === 'undefined' || typeof MutationObserver === 'undefined') return;
|
|
32
26
|
|
|
33
|
-
|
|
27
|
+
const attach = () => {
|
|
28
|
+
if (observer) return;
|
|
34
29
|
observer = new MutationObserver(() => {
|
|
35
|
-
let
|
|
30
|
+
let stillWaiting = false;
|
|
36
31
|
for (const selector of observedSelectors) {
|
|
37
|
-
if (!
|
|
32
|
+
if (!graphElement(selector)) {
|
|
38
33
|
const el = document.querySelector(selector);
|
|
39
|
-
if (el)
|
|
40
|
-
|
|
41
|
-
} else {
|
|
42
|
-
activeObservationNeeded = true;
|
|
43
|
-
}
|
|
34
|
+
if (el) registerContainer(selector, el);
|
|
35
|
+
else stillWaiting = true;
|
|
44
36
|
}
|
|
45
37
|
}
|
|
46
|
-
|
|
47
|
-
// RT-06: Automatically disconnect MutationObserver if all observed selectors are resolved
|
|
48
|
-
if (!activeObservationNeeded && observer) {
|
|
38
|
+
if (!stillWaiting && observer) {
|
|
49
39
|
observer.disconnect();
|
|
50
40
|
observer = null;
|
|
51
41
|
}
|
|
52
42
|
});
|
|
53
|
-
|
|
54
43
|
observer.observe(document.body, { childList: true, subtree: true });
|
|
55
|
-
}
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
if (typeof requestIdleCallback !== 'undefined') {
|
|
47
|
+
requestIdleCallback(attach, { timeout: 100 });
|
|
48
|
+
} else {
|
|
49
|
+
requestAnimationFrame(attach);
|
|
50
|
+
}
|
|
56
51
|
}
|
|
57
52
|
|
|
58
53
|
/**
|
|
59
54
|
* Registers a layout container as actively mounted in the DOM.
|
|
60
|
-
*
|
|
61
|
-
* @param {
|
|
55
|
+
*
|
|
56
|
+
* @param {string} name - unique registry key (or CSS selector).
|
|
57
|
+
* @param {HTMLElement} element - the DOM element instance.
|
|
58
|
+
* @param {string} [parent='body'] - parent container key in the graph.
|
|
62
59
|
*/
|
|
63
|
-
export function registerContainer(name, element) {
|
|
64
|
-
|
|
65
|
-
if (existing && existing !== element) {
|
|
66
|
-
throw new Error(`ContainerError: Singleton violation — '${name}' is already mounted. A second instance cannot register while the first is active.`);
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
containerNodeMap.set(name, new WeakRef(element));
|
|
70
|
-
cleanupRegistry.register(element, name);
|
|
60
|
+
export function registerContainer(name, element, parent = 'body') {
|
|
61
|
+
add(name, element, parent);
|
|
71
62
|
}
|
|
72
63
|
|
|
73
64
|
/**
|
|
74
65
|
* Unregisters a layout container when it is removed from the DOM.
|
|
75
|
-
*
|
|
76
|
-
* @param {
|
|
66
|
+
*
|
|
67
|
+
* @param {string} name - the registry key.
|
|
68
|
+
* @param {HTMLElement} [element] - element guard for the safety check.
|
|
77
69
|
*/
|
|
78
70
|
export function unregisterContainer(name, element) {
|
|
79
|
-
|
|
80
|
-
if (!element || existing === element) {
|
|
81
|
-
containerNodeMap.delete(name);
|
|
82
|
-
if (existing) {
|
|
83
|
-
try { cleanupRegistry.unregister(existing); } catch(e) {}
|
|
84
|
-
}
|
|
85
|
-
}
|
|
71
|
+
remove(name, element);
|
|
86
72
|
}
|
|
87
73
|
|
|
88
74
|
/**
|
|
89
75
|
* Retrieves an active layout container by name.
|
|
90
|
-
*
|
|
91
|
-
*
|
|
76
|
+
*
|
|
77
|
+
* A plain key (e.g. 'main') is resolved only through the graph. A CSS selector
|
|
78
|
+
* (starts with '#', '.', '[', or contains a combinator) additionally resolves
|
|
79
|
+
* against the document and self-registers on first hit.
|
|
80
|
+
*
|
|
81
|
+
* @param {string} name - the registry key or selector.
|
|
82
|
+
* @returns {HTMLElement|undefined} the element, or undefined if not mounted.
|
|
92
83
|
*/
|
|
93
84
|
export function getContainer(name) {
|
|
94
|
-
let el =
|
|
85
|
+
let el = graphElement(name);
|
|
86
|
+
if (el) return el;
|
|
95
87
|
|
|
96
|
-
|
|
97
|
-
// A CSS selector (starts with '#', '.', '[', or contains a combinator) is
|
|
98
|
-
// resolved against the document. This removes the ambiguity where 'main'
|
|
99
|
-
// could mean a registry key or `querySelector('main')`.
|
|
100
|
-
if (!el && isSelector(name) && typeof document !== 'undefined') {
|
|
88
|
+
if (isSelector(name) && typeof document !== 'undefined') {
|
|
101
89
|
try {
|
|
102
90
|
el = document.querySelector(name);
|
|
103
91
|
if (el) {
|
|
104
92
|
registerContainer(name, el);
|
|
105
|
-
|
|
106
|
-
if (!observedSelectors.has(name)) {
|
|
107
|
-
observedSelectors.add(name);
|
|
108
|
-
}
|
|
109
|
-
ensureObserver();
|
|
93
|
+
return el;
|
|
110
94
|
}
|
|
111
|
-
|
|
112
|
-
|
|
95
|
+
if (!observedSelectors.has(name)) observedSelectors.add(name);
|
|
96
|
+
ensureObserver();
|
|
97
|
+
} catch (_) {
|
|
98
|
+
// Invalid selector string — ignore.
|
|
113
99
|
}
|
|
100
|
+
return el ?? undefined;
|
|
114
101
|
}
|
|
115
102
|
|
|
116
|
-
// Warn
|
|
117
|
-
if (
|
|
118
|
-
|
|
103
|
+
// Warn when a plain key shadows a real DOM element of the same tag name.
|
|
104
|
+
if (typeof name === 'string' && typeof document !== 'undefined') {
|
|
105
|
+
let queryResult = null;
|
|
106
|
+
try { queryResult = document.querySelector(name); } catch (_) {}
|
|
119
107
|
if (queryResult) {
|
|
120
108
|
console.warn(
|
|
121
109
|
`[Router] Container name "${name}" is ambiguous: it exists in the DOM as a selector ` +
|
|
@@ -125,7 +113,7 @@ export function getContainer(name) {
|
|
|
125
113
|
}
|
|
126
114
|
}
|
|
127
115
|
|
|
128
|
-
return el;
|
|
116
|
+
return el ?? undefined;
|
|
129
117
|
}
|
|
130
118
|
|
|
131
119
|
/** Heuristic: does this string look like a CSS selector rather than a key? */
|
|
@@ -134,13 +122,16 @@ function isSelector(name) {
|
|
|
134
122
|
}
|
|
135
123
|
|
|
136
124
|
/**
|
|
137
|
-
* Clears the entire container registry.
|
|
125
|
+
* Clears the entire container registry and stops selector observation.
|
|
138
126
|
*/
|
|
139
127
|
export function clearContainers() {
|
|
140
|
-
|
|
128
|
+
clear();
|
|
141
129
|
observedSelectors.clear();
|
|
142
130
|
if (observer) {
|
|
143
131
|
observer.disconnect();
|
|
144
132
|
observer = null;
|
|
145
133
|
}
|
|
146
134
|
}
|
|
135
|
+
|
|
136
|
+
// Re-export the graph node accessor for modules that need topology directly.
|
|
137
|
+
export { get as getNode };
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/core/router/graph.js
|
|
3
|
+
*
|
|
4
|
+
* Hierarchical container graph. Replaces the flat name->WeakRef registry with a
|
|
5
|
+
* tree that knows parent/child relationships and depth, enabling lowest-common-
|
|
6
|
+
* ancestor traversal (lca.js) and sequential cascade mounting (cascade.js).
|
|
7
|
+
*
|
|
8
|
+
* Every node holds a WeakRef to its element so an unmounted container can be
|
|
9
|
+
* garbage-collected without a manual unregister. A FinalizationRegistry prunes
|
|
10
|
+
* stale nodes. The virtual root 'body' always exists.
|
|
11
|
+
*
|
|
12
|
+
* Source: tasks.md Phase 3
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
class Node {
|
|
16
|
+
constructor(name, ref, parent) {
|
|
17
|
+
this.name = name; // registry key, e.g. 'main' | 'sidebar'
|
|
18
|
+
this.ref = ref; // WeakRef<Element> | null (null for virtual root)
|
|
19
|
+
this.parent = parent; // Node | null
|
|
20
|
+
this.children = new Set(); // Set<Node>
|
|
21
|
+
this.depth = parent ? parent.depth + 1 : 0;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** @returns {boolean} true while the referenced element is still alive. */
|
|
25
|
+
alive() {
|
|
26
|
+
return this.ref ? this.ref.deref() !== undefined : true;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const nodes = new Map();
|
|
31
|
+
const root = new Node('body', null, null);
|
|
32
|
+
nodes.set('body', root);
|
|
33
|
+
|
|
34
|
+
// Explicit unregistrations. element() returns undefined for a name in this set
|
|
35
|
+
// so a just-unmounted container is not confused with a GC'd one (RT bug 8.2).
|
|
36
|
+
const gone = new Set();
|
|
37
|
+
|
|
38
|
+
// Prune stale nodes after their element is collected.
|
|
39
|
+
const finalizer = typeof FinalizationRegistry !== 'undefined'
|
|
40
|
+
? new FinalizationRegistry((name) => {
|
|
41
|
+
const node = nodes.get(name);
|
|
42
|
+
if (node && !node.alive()) detach(name);
|
|
43
|
+
})
|
|
44
|
+
: { register() {}, unregister() {} };
|
|
45
|
+
|
|
46
|
+
/** Removes a node from the tree, reparenting its children to its parent. */
|
|
47
|
+
function detach(name) {
|
|
48
|
+
const node = nodes.get(name);
|
|
49
|
+
if (!node || node === root) return;
|
|
50
|
+
node.parent?.children.delete(node);
|
|
51
|
+
for (const child of node.children) {
|
|
52
|
+
child.parent = node.parent;
|
|
53
|
+
child.depth = child.parent ? child.parent.depth + 1 : 0;
|
|
54
|
+
node.parent?.children.add(child);
|
|
55
|
+
}
|
|
56
|
+
nodes.delete(name);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Inserts (or re-points) a container node under a parent.
|
|
61
|
+
*
|
|
62
|
+
* @param {string} name - unique registry key.
|
|
63
|
+
* @param {Element} el - the container element.
|
|
64
|
+
* @param {string} [parent='body'] - parent registry key.
|
|
65
|
+
* @returns {Node} the inserted node.
|
|
66
|
+
*/
|
|
67
|
+
export function add(name, el, parent = 'body') {
|
|
68
|
+
gone.delete(name);
|
|
69
|
+
|
|
70
|
+
const existing = nodes.get(name);
|
|
71
|
+
if (existing && existing !== root) {
|
|
72
|
+
const prev = existing.ref?.deref();
|
|
73
|
+
if (prev && prev !== el) {
|
|
74
|
+
throw new Error(`ContainerError: Singleton violation — '${name}' is already mounted. A second instance cannot register while the first is active.`);
|
|
75
|
+
}
|
|
76
|
+
// Same element re-registering (e.g. HMR): refresh the ref and return.
|
|
77
|
+
existing.ref = new WeakRef(el);
|
|
78
|
+
return existing;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const parentNode = nodes.get(parent) ?? root;
|
|
82
|
+
const node = new Node(name, new WeakRef(el), parentNode);
|
|
83
|
+
parentNode.children.add(node);
|
|
84
|
+
nodes.set(name, node);
|
|
85
|
+
finalizer.register(el, name);
|
|
86
|
+
return node;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Removes a container node. Marks the name as explicitly gone for one macrotask
|
|
91
|
+
* so a lookup during teardown does not fall back to a stale resolution.
|
|
92
|
+
*
|
|
93
|
+
* @param {string} name - registry key.
|
|
94
|
+
* @param {Element} [el] - element guard; if given, only removes when it matches.
|
|
95
|
+
*/
|
|
96
|
+
export function remove(name, el) {
|
|
97
|
+
const node = nodes.get(name);
|
|
98
|
+
if (!node || node === root) return;
|
|
99
|
+
|
|
100
|
+
const current = node.ref?.deref();
|
|
101
|
+
if (el && current && current !== el) return;
|
|
102
|
+
|
|
103
|
+
if (current) {
|
|
104
|
+
try { finalizer.unregister(current); } catch (_) {}
|
|
105
|
+
}
|
|
106
|
+
detach(name);
|
|
107
|
+
|
|
108
|
+
gone.add(name);
|
|
109
|
+
if (typeof setTimeout !== 'undefined') {
|
|
110
|
+
setTimeout(() => gone.delete(name), 0);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* @param {string} name - registry key.
|
|
116
|
+
* @returns {Node|null} the node, or null if absent.
|
|
117
|
+
*/
|
|
118
|
+
export function get(name) {
|
|
119
|
+
return nodes.get(name) ?? null;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Resolves a node's live element.
|
|
124
|
+
*
|
|
125
|
+
* @param {string} name - registry key.
|
|
126
|
+
* @returns {Element|null|undefined} element, null if absent,
|
|
127
|
+
* or undefined if the name was just explicitly removed.
|
|
128
|
+
*/
|
|
129
|
+
export function element(name) {
|
|
130
|
+
if (gone.has(name)) return undefined;
|
|
131
|
+
const node = nodes.get(name);
|
|
132
|
+
if (!node) return null;
|
|
133
|
+
return node.ref ? (node.ref.deref() ?? null) : null;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** Resets the graph to root-only. */
|
|
137
|
+
export function clear() {
|
|
138
|
+
nodes.clear();
|
|
139
|
+
root.children.clear();
|
|
140
|
+
gone.clear();
|
|
141
|
+
nodes.set('body', root);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export { Node, root };
|
package/src/core/router/index.js
CHANGED
|
@@ -48,8 +48,6 @@ import {
|
|
|
48
48
|
|
|
49
49
|
import { cache, prefetch } from './cache.js';
|
|
50
50
|
|
|
51
|
-
import './outlet.js';
|
|
52
|
-
|
|
53
51
|
export const router = {
|
|
54
52
|
// Registration and boundary hooks
|
|
55
53
|
register,
|
|
@@ -110,6 +108,18 @@ export const router = {
|
|
|
110
108
|
|
|
111
109
|
// Auto-bootstrap client-side navigation listeners on client load
|
|
112
110
|
if (typeof window !== 'undefined') {
|
|
111
|
+
// Expose the router globally so non-module scripts, devtools, and definition
|
|
112
|
+
// helpers (page/dock) can reach it without importing. Non-enumerable and
|
|
113
|
+
// non-configurable so it cannot be accidentally clobbered or redefined.
|
|
114
|
+
if (!('router' in window)) {
|
|
115
|
+
Object.defineProperty(window, 'router', {
|
|
116
|
+
value: router,
|
|
117
|
+
writable: false,
|
|
118
|
+
enumerable: false,
|
|
119
|
+
configurable: false
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
113
123
|
registerNavigator(navigate);
|
|
114
124
|
setup();
|
|
115
125
|
setupTabSync(router);
|
|
@@ -12,6 +12,8 @@ import { match } from './match.js';
|
|
|
12
12
|
import { transitions } from './transitions.js';
|
|
13
13
|
import { getContainer } from './container.js';
|
|
14
14
|
import { isCallback, runCallback } from './handler.js';
|
|
15
|
+
import { boot, reset as resetBoot } from './boot.js';
|
|
16
|
+
import { ensure } from './cascade.js';
|
|
15
17
|
|
|
16
18
|
let guards = [];
|
|
17
19
|
let notFoundHandler = null;
|
|
@@ -147,11 +149,24 @@ export function setup() {
|
|
|
147
149
|
return;
|
|
148
150
|
}
|
|
149
151
|
|
|
150
|
-
//
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
152
|
+
// Layout resolution: ensure the route's container chain is mounted.
|
|
153
|
+
// Routes declare an ordered `via` chain (root-to-leaf) or a single
|
|
154
|
+
// `container`. Missing containers are mounted via cascade rather than
|
|
155
|
+
// throwing — a hard refresh on a deep route can now build its own
|
|
156
|
+
// layout instead of erroring out.
|
|
157
|
+
if (routeMatch) {
|
|
158
|
+
const meta = routeMatch.route?.meta ?? {};
|
|
159
|
+
const chain = Array.isArray(meta.via) && meta.via.length
|
|
160
|
+
? meta.via
|
|
161
|
+
: (meta.container ? [meta.container] : []);
|
|
162
|
+
|
|
163
|
+
try {
|
|
164
|
+
for (let i = 0; i < chain.length; i++) {
|
|
165
|
+
if (!getContainer(chain[i])) {
|
|
166
|
+
await ensure(chain[i], chain[i - 1] ?? 'body');
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
} catch (err) {
|
|
155
170
|
emit('error', { error: err, url: destination.url, route: routeMatch.route, phase: 'container' });
|
|
156
171
|
throw err;
|
|
157
172
|
}
|
|
@@ -244,8 +259,11 @@ export function setup() {
|
|
|
244
259
|
window.navigation.addEventListener('navigatesuccess', successListener);
|
|
245
260
|
window.navigation.addEventListener('navigateerror', errorListener);
|
|
246
261
|
|
|
247
|
-
// Trigger initial on-boot matching
|
|
248
|
-
|
|
262
|
+
// Trigger initial on-boot matching once the DOM is parsed and every gated
|
|
263
|
+
// prerequisite (element definitions registered via boot.gate) has settled.
|
|
264
|
+
// Deferring this past DOMContentLoaded is what survives a hard refresh on a
|
|
265
|
+
// deep route — the first match no longer races element registration.
|
|
266
|
+
boot(async () => {
|
|
249
267
|
const url = window.navigation.currentEntry?.url || window.location.href;
|
|
250
268
|
const routeMatch = await match(url);
|
|
251
269
|
if (routeMatch) {
|
|
@@ -284,6 +302,7 @@ export function destroy() {
|
|
|
284
302
|
|
|
285
303
|
guards = [];
|
|
286
304
|
notFoundHandler = null;
|
|
305
|
+
resetBoot();
|
|
287
306
|
|
|
288
307
|
for (const set of Object.values(listeners)) {
|
|
289
308
|
set.clear();
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/core/router/lca.js
|
|
3
|
+
*
|
|
4
|
+
* Lowest common ancestor over the container graph. Given two container names,
|
|
5
|
+
* computes the deepest node that is an ancestor of both — the pivot for a
|
|
6
|
+
* cross-branch navigation (everything below it on the source side unmounts,
|
|
7
|
+
* everything below it on the target side mounts).
|
|
8
|
+
*
|
|
9
|
+
* O(d) where d is tree depth. For real UI trees d rarely exceeds 5.
|
|
10
|
+
*
|
|
11
|
+
* Source: tasks.md Phase 4
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { get } from './graph.js';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* @param {string} a - first container name.
|
|
18
|
+
* @param {string} b - second container name.
|
|
19
|
+
* @returns {import('./graph.js').Node|null} the lowest common ancestor, or null.
|
|
20
|
+
*/
|
|
21
|
+
export function lca(a, b) {
|
|
22
|
+
let na = get(a);
|
|
23
|
+
let nb = get(b);
|
|
24
|
+
if (!na || !nb) return null;
|
|
25
|
+
|
|
26
|
+
// Phase 1: equalize depth.
|
|
27
|
+
while (na.depth > nb.depth) na = na.parent;
|
|
28
|
+
while (nb.depth > na.depth) nb = nb.parent;
|
|
29
|
+
|
|
30
|
+
// Phase 2: walk up in tandem until the pointers meet.
|
|
31
|
+
while (na && nb && na !== nb) {
|
|
32
|
+
na = na.parent;
|
|
33
|
+
nb = nb.parent;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return na ?? null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Ordered node path from the common ancestor down to `to`.
|
|
41
|
+
*
|
|
42
|
+
* @param {string} from - source container name.
|
|
43
|
+
* @param {string} to - target container name.
|
|
44
|
+
* @returns {import('./graph.js').Node[]|null} ancestor-first node chain, or null.
|
|
45
|
+
*/
|
|
46
|
+
export function path(from, to) {
|
|
47
|
+
const ancestor = lca(from, to);
|
|
48
|
+
if (!ancestor) return null;
|
|
49
|
+
|
|
50
|
+
const segments = [];
|
|
51
|
+
let current = get(to);
|
|
52
|
+
while (current && current !== ancestor) {
|
|
53
|
+
segments.unshift(current);
|
|
54
|
+
current = current.parent;
|
|
55
|
+
}
|
|
56
|
+
segments.unshift(ancestor);
|
|
57
|
+
return segments;
|
|
58
|
+
}
|
package/src/core/router/match.js
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
import { guard } from '../platform/index.js';
|
|
11
11
|
import { resolveTag } from './handler.js';
|
|
12
|
+
import { insert as trieInsert, find as trieFind, clear as trieClear } from './trie.js';
|
|
12
13
|
|
|
13
14
|
const routes = [];
|
|
14
15
|
// Resolve Pattern class at module load if available natively (RT-03)
|
|
@@ -42,12 +43,18 @@ function getSpecificity(patternStr) {
|
|
|
42
43
|
* Registers a route mapping.
|
|
43
44
|
*/
|
|
44
45
|
export function register(patternStr, handler, meta = {}) {
|
|
45
|
-
|
|
46
|
+
const route = {
|
|
46
47
|
patternStr,
|
|
47
48
|
handler,
|
|
48
49
|
meta,
|
|
49
50
|
pattern: null
|
|
50
|
-
}
|
|
51
|
+
};
|
|
52
|
+
routes.push(route);
|
|
53
|
+
|
|
54
|
+
// Index in the radix trie for O(k) matching. Patterns the trie cannot
|
|
55
|
+
// express (regex groups, modifiers, absolute URLs) stay on the URLPattern
|
|
56
|
+
// scan below — trieInsert returns false for those.
|
|
57
|
+
trieInsert(patternStr, route);
|
|
51
58
|
|
|
52
59
|
// Sort routes by specificity (descending) and length (longer first) at registration time (RT-04)
|
|
53
60
|
routes.sort((a, b) => {
|
|
@@ -66,6 +73,15 @@ export async function match(url) {
|
|
|
66
73
|
const P = Pattern || (await getURLPattern());
|
|
67
74
|
const targetUrl = new URL(url, globalThis.location?.href || 'http://localhost');
|
|
68
75
|
|
|
76
|
+
// Fast path: O(k) radix-trie lookup on the pathname. Covers the common case
|
|
77
|
+
// of plain/:param/* patterns without compiling or executing a URLPattern.
|
|
78
|
+
const trieHit = trieFind(targetUrl.pathname);
|
|
79
|
+
if (trieHit) {
|
|
80
|
+
return finalize(trieHit.route, trieHit.params, targetUrl, null);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Fallback: URLPattern scan for patterns the trie cannot express
|
|
84
|
+
// (regex groups, modifiers, absolute-URL patterns).
|
|
69
85
|
for (const route of routes) {
|
|
70
86
|
if (!route.pattern) {
|
|
71
87
|
if (route.patternStr.startsWith('http://') || route.patternStr.startsWith('https://')) {
|
|
@@ -77,51 +93,48 @@ export async function match(url) {
|
|
|
77
93
|
|
|
78
94
|
const result = route.pattern.exec(targetUrl.href);
|
|
79
95
|
if (result) {
|
|
80
|
-
|
|
81
|
-
const tag = await resolveTag(route.handler);
|
|
82
|
-
|
|
83
|
-
const query = Object.fromEntries(targetUrl.searchParams.entries());
|
|
84
|
-
const hash = targetUrl.hash;
|
|
85
|
-
|
|
86
|
-
const chain = [];
|
|
87
|
-
let currentRoute = route;
|
|
88
|
-
while (currentRoute) {
|
|
89
|
-
const currentTag = await resolveTag(currentRoute.handler);
|
|
90
|
-
chain.unshift({
|
|
91
|
-
route: currentRoute,
|
|
92
|
-
tag: currentTag,
|
|
93
|
-
params: result.pathname.groups || {}
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
const parentPattern = currentRoute.meta?.parent;
|
|
97
|
-
if (parentPattern) {
|
|
98
|
-
const parentRoute = routes.find(r => r.patternStr === parentPattern);
|
|
99
|
-
currentRoute = parentRoute;
|
|
100
|
-
} else {
|
|
101
|
-
currentRoute = null;
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
return {
|
|
106
|
-
route,
|
|
107
|
-
tag,
|
|
108
|
-
params: result.pathname.groups || {},
|
|
109
|
-
query,
|
|
110
|
-
hash,
|
|
111
|
-
chain,
|
|
112
|
-
result
|
|
113
|
-
};
|
|
96
|
+
return finalize(route, result.pathname.groups || {}, targetUrl, result);
|
|
114
97
|
}
|
|
115
98
|
}
|
|
116
99
|
|
|
117
100
|
return null;
|
|
118
101
|
}
|
|
119
102
|
|
|
103
|
+
/**
|
|
104
|
+
* Builds the full match result (tag, query, hash, parent chain) for a matched
|
|
105
|
+
* route. The parent chain walk is cycle-guarded so a misconfigured
|
|
106
|
+
* A→B→A parent loop breaks instead of hanging (RT bug 8.3).
|
|
107
|
+
*/
|
|
108
|
+
async function finalize(route, params, targetUrl, result) {
|
|
109
|
+
const tag = await resolveTag(route.handler);
|
|
110
|
+
const query = Object.fromEntries(targetUrl.searchParams.entries());
|
|
111
|
+
const hash = targetUrl.hash;
|
|
112
|
+
|
|
113
|
+
const chain = [];
|
|
114
|
+
const visited = new Set();
|
|
115
|
+
let currentRoute = route;
|
|
116
|
+
while (currentRoute) {
|
|
117
|
+
if (visited.has(currentRoute.patternStr)) break; // cycle detected
|
|
118
|
+
visited.add(currentRoute.patternStr);
|
|
119
|
+
|
|
120
|
+
const currentTag = await resolveTag(currentRoute.handler);
|
|
121
|
+
chain.unshift({ route: currentRoute, tag: currentTag, params });
|
|
122
|
+
|
|
123
|
+
const parentPattern = currentRoute.meta?.parent;
|
|
124
|
+
currentRoute = parentPattern
|
|
125
|
+
? routes.find(r => r.patternStr === parentPattern)
|
|
126
|
+
: null;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return { route, tag, params, query, hash, chain, result };
|
|
130
|
+
}
|
|
131
|
+
|
|
120
132
|
/**
|
|
121
133
|
* Clears the route registry.
|
|
122
134
|
*/
|
|
123
135
|
export function clear() {
|
|
124
136
|
routes.length = 0;
|
|
137
|
+
trieClear();
|
|
125
138
|
}
|
|
126
139
|
|
|
127
140
|
/**
|