gonia 0.0.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.
- package/README.md +119 -0
- package/dist/client/hydrate.d.ts +54 -0
- package/dist/client/hydrate.js +445 -0
- package/dist/client/index.d.ts +7 -0
- package/dist/client/index.js +6 -0
- package/dist/context.d.ts +40 -0
- package/dist/context.js +69 -0
- package/dist/directives/class.d.ts +21 -0
- package/dist/directives/class.js +42 -0
- package/dist/directives/for.d.ts +29 -0
- package/dist/directives/for.js +265 -0
- package/dist/directives/html.d.ts +16 -0
- package/dist/directives/html.js +19 -0
- package/dist/directives/if.d.ts +25 -0
- package/dist/directives/if.js +133 -0
- package/dist/directives/index.d.ts +15 -0
- package/dist/directives/index.js +15 -0
- package/dist/directives/model.d.ts +27 -0
- package/dist/directives/model.js +134 -0
- package/dist/directives/on.d.ts +21 -0
- package/dist/directives/on.js +54 -0
- package/dist/directives/show.d.ts +15 -0
- package/dist/directives/show.js +19 -0
- package/dist/directives/slot.d.ts +48 -0
- package/dist/directives/slot.js +99 -0
- package/dist/directives/template.d.ts +55 -0
- package/dist/directives/template.js +147 -0
- package/dist/directives/text.d.ts +15 -0
- package/dist/directives/text.js +18 -0
- package/dist/expression.d.ts +60 -0
- package/dist/expression.js +96 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.js +16 -0
- package/dist/inject.d.ts +42 -0
- package/dist/inject.js +63 -0
- package/dist/providers.d.ts +96 -0
- package/dist/providers.js +146 -0
- package/dist/reactivity.d.ts +95 -0
- package/dist/reactivity.js +219 -0
- package/dist/scope.d.ts +43 -0
- package/dist/scope.js +112 -0
- package/dist/server/index.d.ts +7 -0
- package/dist/server/index.js +6 -0
- package/dist/server/render.d.ts +61 -0
- package/dist/server/render.js +243 -0
- package/dist/templates.d.ts +92 -0
- package/dist/templates.js +124 -0
- package/dist/types.d.ts +362 -0
- package/dist/types.js +110 -0
- package/dist/vite/index.d.ts +6 -0
- package/dist/vite/index.js +6 -0
- package/dist/vite/plugin.d.ts +30 -0
- package/dist/vite/plugin.js +127 -0
- package/package.json +67 -0
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fine-grained reactivity system using Proxies.
|
|
3
|
+
*
|
|
4
|
+
* @remarks
|
|
5
|
+
* Each directive becomes its own effect, tracking only the state it accesses.
|
|
6
|
+
* Changes trigger only the affected effects - no component re-renders, no diffing.
|
|
7
|
+
*
|
|
8
|
+
* @packageDocumentation
|
|
9
|
+
*/
|
|
10
|
+
let activeEffect = null;
|
|
11
|
+
let activeScope = null;
|
|
12
|
+
const targetMap = new WeakMap();
|
|
13
|
+
const effectDeps = new Map();
|
|
14
|
+
export function createEffectScope() {
|
|
15
|
+
const scope = {
|
|
16
|
+
_effects: [],
|
|
17
|
+
active: true,
|
|
18
|
+
run(fn) {
|
|
19
|
+
const prevScope = activeScope;
|
|
20
|
+
activeScope = scope;
|
|
21
|
+
try {
|
|
22
|
+
return fn();
|
|
23
|
+
}
|
|
24
|
+
finally {
|
|
25
|
+
activeScope = prevScope;
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
stop() {
|
|
29
|
+
if (scope.active) {
|
|
30
|
+
for (const stopFn of scope._effects) {
|
|
31
|
+
stopFn();
|
|
32
|
+
}
|
|
33
|
+
scope._effects.length = 0;
|
|
34
|
+
scope.active = false;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
return scope;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Register an effect's stop function with the active scope.
|
|
42
|
+
*
|
|
43
|
+
* @internal
|
|
44
|
+
*/
|
|
45
|
+
function registerWithScope(stopFn) {
|
|
46
|
+
if (activeScope && activeScope.active) {
|
|
47
|
+
activeScope._effects.push(stopFn);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Make an object deeply reactive.
|
|
52
|
+
*
|
|
53
|
+
* @remarks
|
|
54
|
+
* Property access is tracked when inside an effect. Mutations trigger
|
|
55
|
+
* all effects that depend on the changed property.
|
|
56
|
+
*
|
|
57
|
+
* @typeParam T - Object type
|
|
58
|
+
* @param target - The object to make reactive
|
|
59
|
+
* @returns A reactive proxy of the object
|
|
60
|
+
*
|
|
61
|
+
* @example
|
|
62
|
+
* ```ts
|
|
63
|
+
* const state = reactive({ count: 0 });
|
|
64
|
+
* effect(() => console.log(state.count));
|
|
65
|
+
* state.count = 1; // logs: 1
|
|
66
|
+
* ```
|
|
67
|
+
*/
|
|
68
|
+
export function reactive(target) {
|
|
69
|
+
return new Proxy(target, {
|
|
70
|
+
get(obj, key, receiver) {
|
|
71
|
+
track(obj, key);
|
|
72
|
+
const value = Reflect.get(obj, key, receiver);
|
|
73
|
+
if (value !== null && typeof value === 'object') {
|
|
74
|
+
return reactive(value);
|
|
75
|
+
}
|
|
76
|
+
return value;
|
|
77
|
+
},
|
|
78
|
+
set(obj, key, value, receiver) {
|
|
79
|
+
const oldValue = Reflect.get(obj, key, receiver);
|
|
80
|
+
const result = Reflect.set(obj, key, value, receiver);
|
|
81
|
+
if (oldValue !== value) {
|
|
82
|
+
trigger(obj, key);
|
|
83
|
+
}
|
|
84
|
+
return result;
|
|
85
|
+
},
|
|
86
|
+
deleteProperty(obj, key) {
|
|
87
|
+
const hadKey = key in obj;
|
|
88
|
+
const result = Reflect.deleteProperty(obj, key);
|
|
89
|
+
if (hadKey) {
|
|
90
|
+
trigger(obj, key);
|
|
91
|
+
}
|
|
92
|
+
return result;
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Track a dependency: the active effect depends on target[key].
|
|
98
|
+
*
|
|
99
|
+
* @internal
|
|
100
|
+
*/
|
|
101
|
+
function track(target, key) {
|
|
102
|
+
if (!activeEffect)
|
|
103
|
+
return;
|
|
104
|
+
let depsMap = targetMap.get(target);
|
|
105
|
+
if (!depsMap) {
|
|
106
|
+
depsMap = new Map();
|
|
107
|
+
targetMap.set(target, depsMap);
|
|
108
|
+
}
|
|
109
|
+
let deps = depsMap.get(key);
|
|
110
|
+
if (!deps) {
|
|
111
|
+
deps = new Set();
|
|
112
|
+
depsMap.set(key, deps);
|
|
113
|
+
}
|
|
114
|
+
deps.add(activeEffect);
|
|
115
|
+
let trackedDeps = effectDeps.get(activeEffect);
|
|
116
|
+
if (!trackedDeps) {
|
|
117
|
+
trackedDeps = new Set();
|
|
118
|
+
effectDeps.set(activeEffect, trackedDeps);
|
|
119
|
+
}
|
|
120
|
+
trackedDeps.add(deps);
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Trigger effects that depend on target[key].
|
|
124
|
+
*
|
|
125
|
+
* @internal
|
|
126
|
+
*/
|
|
127
|
+
function trigger(target, key) {
|
|
128
|
+
const depsMap = targetMap.get(target);
|
|
129
|
+
if (!depsMap)
|
|
130
|
+
return;
|
|
131
|
+
const deps = depsMap.get(key);
|
|
132
|
+
if (deps) {
|
|
133
|
+
const effectsToRun = [...deps];
|
|
134
|
+
effectsToRun.forEach(effect => effect());
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Create a reactive effect.
|
|
139
|
+
*
|
|
140
|
+
* @remarks
|
|
141
|
+
* The function runs immediately, tracking dependencies.
|
|
142
|
+
* It re-runs automatically whenever those dependencies change.
|
|
143
|
+
*
|
|
144
|
+
* @param fn - The effect function to run
|
|
145
|
+
* @returns A cleanup function to stop the effect
|
|
146
|
+
*
|
|
147
|
+
* @example
|
|
148
|
+
* ```ts
|
|
149
|
+
* const state = reactive({ count: 0 });
|
|
150
|
+
* const stop = effect(() => {
|
|
151
|
+
* console.log('Count:', state.count);
|
|
152
|
+
* });
|
|
153
|
+
* state.count = 1; // logs: Count: 1
|
|
154
|
+
* stop(); // effect no longer runs
|
|
155
|
+
* ```
|
|
156
|
+
*/
|
|
157
|
+
export function effect(fn) {
|
|
158
|
+
const run = () => {
|
|
159
|
+
cleanup(run);
|
|
160
|
+
activeEffect = run;
|
|
161
|
+
fn();
|
|
162
|
+
activeEffect = null;
|
|
163
|
+
};
|
|
164
|
+
run();
|
|
165
|
+
const stopFn = () => cleanup(run);
|
|
166
|
+
registerWithScope(stopFn);
|
|
167
|
+
return stopFn;
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Remove an effect from all dependency sets.
|
|
171
|
+
*
|
|
172
|
+
* @internal
|
|
173
|
+
*/
|
|
174
|
+
function cleanup(effectFn) {
|
|
175
|
+
const trackedDeps = effectDeps.get(effectFn);
|
|
176
|
+
if (trackedDeps) {
|
|
177
|
+
for (const deps of trackedDeps) {
|
|
178
|
+
deps.delete(effectFn);
|
|
179
|
+
}
|
|
180
|
+
trackedDeps.clear();
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Create a child reactive scope.
|
|
185
|
+
*
|
|
186
|
+
* @remarks
|
|
187
|
+
* Used by structural directives like c-for to create per-item contexts.
|
|
188
|
+
* The child scope inherits from the parent, with additions taking precedence.
|
|
189
|
+
*
|
|
190
|
+
* @typeParam T - Parent object type
|
|
191
|
+
* @param parent - The parent reactive object
|
|
192
|
+
* @param additions - Additional properties for this scope
|
|
193
|
+
* @returns A new reactive scope that inherits from parent
|
|
194
|
+
*
|
|
195
|
+
* @example
|
|
196
|
+
* ```ts
|
|
197
|
+
* const parent = reactive({ items: [1, 2, 3] });
|
|
198
|
+
* const child = createScope(parent, { item: 1, index: 0 });
|
|
199
|
+
* child.item; // 1
|
|
200
|
+
* child.items; // [1, 2, 3] (from parent)
|
|
201
|
+
* ```
|
|
202
|
+
*/
|
|
203
|
+
export function createScope(parent, additions) {
|
|
204
|
+
const scope = reactive({ ...additions });
|
|
205
|
+
return new Proxy(scope, {
|
|
206
|
+
get(target, key, receiver) {
|
|
207
|
+
if (key in target) {
|
|
208
|
+
return Reflect.get(target, key, receiver);
|
|
209
|
+
}
|
|
210
|
+
return parent[key];
|
|
211
|
+
},
|
|
212
|
+
set(target, key, value, receiver) {
|
|
213
|
+
if (key in target) {
|
|
214
|
+
return Reflect.set(target, key, value, receiver);
|
|
215
|
+
}
|
|
216
|
+
return Reflect.set(parent, key, value);
|
|
217
|
+
}
|
|
218
|
+
});
|
|
219
|
+
}
|
package/dist/scope.d.ts
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scope management for element state.
|
|
3
|
+
*
|
|
4
|
+
* @packageDocumentation
|
|
5
|
+
*/
|
|
6
|
+
import { Directive, DirectiveOptions } from './types.js';
|
|
7
|
+
/**
|
|
8
|
+
* Create a new scope for an element.
|
|
9
|
+
*
|
|
10
|
+
* @param el - The element to create scope for
|
|
11
|
+
* @param parentScope - Optional parent scope to inherit from via prototype
|
|
12
|
+
* @returns The new reactive scope
|
|
13
|
+
*/
|
|
14
|
+
export declare function createElementScope(el: Element, parentScope?: Record<string, unknown>): Record<string, unknown>;
|
|
15
|
+
/**
|
|
16
|
+
* Get the scope for an element.
|
|
17
|
+
*
|
|
18
|
+
* @param el - The element
|
|
19
|
+
* @returns The element's scope, or undefined if none
|
|
20
|
+
*/
|
|
21
|
+
export declare function getElementScope(el: Element): Record<string, unknown> | undefined;
|
|
22
|
+
/**
|
|
23
|
+
* Find the nearest ancestor scope by walking up the DOM tree.
|
|
24
|
+
*
|
|
25
|
+
* @param el - The element to start from
|
|
26
|
+
* @param includeSelf - Whether to check the element itself (default: false)
|
|
27
|
+
* @returns The nearest scope, or undefined if none found
|
|
28
|
+
*/
|
|
29
|
+
export declare function findParentScope(el: Element, includeSelf?: boolean): Record<string, unknown> | undefined;
|
|
30
|
+
/**
|
|
31
|
+
* Remove scope for an element (cleanup).
|
|
32
|
+
*
|
|
33
|
+
* @param el - The element
|
|
34
|
+
*/
|
|
35
|
+
export declare function removeElementScope(el: Element): void;
|
|
36
|
+
/**
|
|
37
|
+
* Register a directive as a custom element.
|
|
38
|
+
*
|
|
39
|
+
* @param name - The custom element name (must contain hyphen)
|
|
40
|
+
* @param fn - The directive function
|
|
41
|
+
* @param options - Directive options
|
|
42
|
+
*/
|
|
43
|
+
export declare function registerDirectiveElement(name: string, fn: Directive<any>, options: DirectiveOptions): void;
|
package/dist/scope.js
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scope management for element state.
|
|
3
|
+
*
|
|
4
|
+
* @packageDocumentation
|
|
5
|
+
*/
|
|
6
|
+
import { reactive } from './reactivity.js';
|
|
7
|
+
import { createContext } from './context.js';
|
|
8
|
+
import { Mode } from './types.js';
|
|
9
|
+
import { getInjectables } from './inject.js';
|
|
10
|
+
/** WeakMap to store element scopes */
|
|
11
|
+
const elementScopes = new WeakMap();
|
|
12
|
+
/**
|
|
13
|
+
* Create a new scope for an element.
|
|
14
|
+
*
|
|
15
|
+
* @param el - The element to create scope for
|
|
16
|
+
* @param parentScope - Optional parent scope to inherit from via prototype
|
|
17
|
+
* @returns The new reactive scope
|
|
18
|
+
*/
|
|
19
|
+
export function createElementScope(el, parentScope) {
|
|
20
|
+
let scope;
|
|
21
|
+
if (parentScope) {
|
|
22
|
+
// Create new object with parent as prototype
|
|
23
|
+
scope = reactive(Object.create(parentScope));
|
|
24
|
+
}
|
|
25
|
+
else {
|
|
26
|
+
scope = reactive({});
|
|
27
|
+
}
|
|
28
|
+
elementScopes.set(el, scope);
|
|
29
|
+
return scope;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Get the scope for an element.
|
|
33
|
+
*
|
|
34
|
+
* @param el - The element
|
|
35
|
+
* @returns The element's scope, or undefined if none
|
|
36
|
+
*/
|
|
37
|
+
export function getElementScope(el) {
|
|
38
|
+
return elementScopes.get(el);
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Find the nearest ancestor scope by walking up the DOM tree.
|
|
42
|
+
*
|
|
43
|
+
* @param el - The element to start from
|
|
44
|
+
* @param includeSelf - Whether to check the element itself (default: false)
|
|
45
|
+
* @returns The nearest scope, or undefined if none found
|
|
46
|
+
*/
|
|
47
|
+
export function findParentScope(el, includeSelf = false) {
|
|
48
|
+
let current = includeSelf ? el : el.parentElement;
|
|
49
|
+
while (current) {
|
|
50
|
+
const scope = elementScopes.get(current);
|
|
51
|
+
if (scope) {
|
|
52
|
+
return scope;
|
|
53
|
+
}
|
|
54
|
+
current = current.parentElement;
|
|
55
|
+
}
|
|
56
|
+
return undefined;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Remove scope for an element (cleanup).
|
|
60
|
+
*
|
|
61
|
+
* @param el - The element
|
|
62
|
+
*/
|
|
63
|
+
export function removeElementScope(el) {
|
|
64
|
+
elementScopes.delete(el);
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Register a directive as a custom element.
|
|
68
|
+
*
|
|
69
|
+
* @param name - The custom element name (must contain hyphen)
|
|
70
|
+
* @param fn - The directive function
|
|
71
|
+
* @param options - Directive options
|
|
72
|
+
*/
|
|
73
|
+
export function registerDirectiveElement(name,
|
|
74
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
75
|
+
fn, options) {
|
|
76
|
+
// Don't re-register if already defined
|
|
77
|
+
if (customElements.get(name)) {
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
customElements.define(name, class extends HTMLElement {
|
|
81
|
+
connectedCallback() {
|
|
82
|
+
// Find parent scope for prototype chain
|
|
83
|
+
const parentScope = findParentScope(this);
|
|
84
|
+
// Create this element's scope
|
|
85
|
+
const scope = createElementScope(this, parentScope);
|
|
86
|
+
// Create context for expression evaluation
|
|
87
|
+
const ctx = createContext(Mode.CLIENT, scope);
|
|
88
|
+
// Resolve dependencies and call directive
|
|
89
|
+
const inject = getInjectables(fn);
|
|
90
|
+
const args = inject.map((dep) => {
|
|
91
|
+
switch (dep) {
|
|
92
|
+
case '$element':
|
|
93
|
+
return this;
|
|
94
|
+
case '$state':
|
|
95
|
+
return scope;
|
|
96
|
+
case '$eval':
|
|
97
|
+
return ctx.eval.bind(ctx);
|
|
98
|
+
default:
|
|
99
|
+
return undefined;
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
const result = fn(...args);
|
|
103
|
+
// Handle async directives
|
|
104
|
+
if (result instanceof Promise) {
|
|
105
|
+
result.catch(err => console.error(`Error in ${name}:`, err));
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
disconnectedCallback() {
|
|
109
|
+
removeElementScope(this);
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server-side rendering with MutationObserver-based directive indexing.
|
|
3
|
+
*
|
|
4
|
+
* @packageDocumentation
|
|
5
|
+
*/
|
|
6
|
+
import { Directive } from '../types.js';
|
|
7
|
+
/**
|
|
8
|
+
* Registry of directives by name.
|
|
9
|
+
*/
|
|
10
|
+
export type DirectiveRegistry = Map<string, Directive>;
|
|
11
|
+
/**
|
|
12
|
+
* Service registry for dependency injection.
|
|
13
|
+
*/
|
|
14
|
+
export type ServiceRegistry = Map<string, unknown>;
|
|
15
|
+
/**
|
|
16
|
+
* Register a directive in the registry.
|
|
17
|
+
*
|
|
18
|
+
* @remarks
|
|
19
|
+
* Invalidates the cached selector so it will be rebuilt on next render.
|
|
20
|
+
*
|
|
21
|
+
* @param registry - The directive registry
|
|
22
|
+
* @param name - Directive name (without c- prefix)
|
|
23
|
+
* @param fn - The directive function
|
|
24
|
+
*/
|
|
25
|
+
export declare function registerDirective(registry: DirectiveRegistry, name: string, fn: Directive): void;
|
|
26
|
+
/**
|
|
27
|
+
* Register a service for dependency injection.
|
|
28
|
+
*
|
|
29
|
+
* @param name - Service name (used in $inject arrays)
|
|
30
|
+
* @param service - The service instance
|
|
31
|
+
*/
|
|
32
|
+
export declare function registerService(name: string, service: unknown): void;
|
|
33
|
+
/**
|
|
34
|
+
* Render HTML with directives on the server.
|
|
35
|
+
*
|
|
36
|
+
* @remarks
|
|
37
|
+
* Uses MutationObserver to index elements with directive attributes
|
|
38
|
+
* as they are parsed, then executes directives to produce the final HTML.
|
|
39
|
+
* Directive attributes are preserved in output for client hydration.
|
|
40
|
+
* Directives are processed in tree order (parents before children),
|
|
41
|
+
* with priority used only for multiple directives on the same element.
|
|
42
|
+
*
|
|
43
|
+
* @param html - The HTML template string
|
|
44
|
+
* @param state - The state object to use for expression evaluation
|
|
45
|
+
* @param registry - The directive registry
|
|
46
|
+
* @returns The rendered HTML string
|
|
47
|
+
*
|
|
48
|
+
* @example
|
|
49
|
+
* ```ts
|
|
50
|
+
* const registry = new Map();
|
|
51
|
+
* registry.set('text', textDirective);
|
|
52
|
+
*
|
|
53
|
+
* const html = await render(
|
|
54
|
+
* '<span c-text="user.name"></span>',
|
|
55
|
+
* { user: { name: 'Alice' } },
|
|
56
|
+
* registry
|
|
57
|
+
* );
|
|
58
|
+
* // '<span c-text="user.name">Alice</span>'
|
|
59
|
+
* ```
|
|
60
|
+
*/
|
|
61
|
+
export declare function render(html: string, state: Record<string, unknown>, registry: DirectiveRegistry): Promise<string>;
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server-side rendering with MutationObserver-based directive indexing.
|
|
3
|
+
*
|
|
4
|
+
* @packageDocumentation
|
|
5
|
+
*/
|
|
6
|
+
import { parseHTML } from 'linkedom';
|
|
7
|
+
import { Mode, DirectivePriority } from '../types.js';
|
|
8
|
+
import { createContext } from '../context.js';
|
|
9
|
+
import { processNativeSlot } from '../directives/slot.js';
|
|
10
|
+
import { getLocalState, registerProvider, resolveFromProviders, resolveFromDIProviders } from '../providers.js';
|
|
11
|
+
import { FOR_PROCESSED_ATTR, FOR_TEMPLATE_ATTR } from '../directives/for.js';
|
|
12
|
+
import { IF_PROCESSED_ATTR } from '../directives/if.js';
|
|
13
|
+
/** Registered services */
|
|
14
|
+
let services = new Map();
|
|
15
|
+
const selectorCache = new WeakMap();
|
|
16
|
+
/**
|
|
17
|
+
* Build a CSS selector for all registered directives.
|
|
18
|
+
*
|
|
19
|
+
* @internal
|
|
20
|
+
*/
|
|
21
|
+
function getSelector(registry) {
|
|
22
|
+
let selector = selectorCache.get(registry);
|
|
23
|
+
if (!selector) {
|
|
24
|
+
const directiveSelectors = [...registry.keys()].map(n => `[c-${n}]`);
|
|
25
|
+
// Also match native <slot> elements
|
|
26
|
+
directiveSelectors.push('slot');
|
|
27
|
+
selector = directiveSelectors.join(',');
|
|
28
|
+
selectorCache.set(registry, selector);
|
|
29
|
+
}
|
|
30
|
+
return selector;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Register a directive in the registry.
|
|
34
|
+
*
|
|
35
|
+
* @remarks
|
|
36
|
+
* Invalidates the cached selector so it will be rebuilt on next render.
|
|
37
|
+
*
|
|
38
|
+
* @param registry - The directive registry
|
|
39
|
+
* @param name - Directive name (without c- prefix)
|
|
40
|
+
* @param fn - The directive function
|
|
41
|
+
*/
|
|
42
|
+
export function registerDirective(registry, name, fn) {
|
|
43
|
+
registry.set(name, fn);
|
|
44
|
+
selectorCache.delete(registry);
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Register a service for dependency injection.
|
|
48
|
+
*
|
|
49
|
+
* @param name - Service name (used in $inject arrays)
|
|
50
|
+
* @param service - The service instance
|
|
51
|
+
*/
|
|
52
|
+
export function registerService(name, service) {
|
|
53
|
+
services.set(name, service);
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Resolve dependencies for a directive based on its $inject array.
|
|
57
|
+
*
|
|
58
|
+
* @internal
|
|
59
|
+
*/
|
|
60
|
+
function resolveDependencies(directive, expr, el, ctx, rootState) {
|
|
61
|
+
const inject = directive.$inject ?? ['$expr', '$element', '$eval'];
|
|
62
|
+
return inject.map(name => {
|
|
63
|
+
switch (name) {
|
|
64
|
+
case '$expr':
|
|
65
|
+
return expr;
|
|
66
|
+
case '$element':
|
|
67
|
+
return el;
|
|
68
|
+
case '$eval':
|
|
69
|
+
return ctx.eval.bind(ctx);
|
|
70
|
+
case '$state':
|
|
71
|
+
return getLocalState(el) ?? rootState;
|
|
72
|
+
case '$rootState':
|
|
73
|
+
return rootState;
|
|
74
|
+
case '$mode':
|
|
75
|
+
return Mode.SERVER;
|
|
76
|
+
default: {
|
|
77
|
+
// Look up in ancestor DI providers first (provide option)
|
|
78
|
+
const diProvided = resolveFromDIProviders(el, name);
|
|
79
|
+
if (diProvided !== undefined) {
|
|
80
|
+
return diProvided;
|
|
81
|
+
}
|
|
82
|
+
// Look up in global services registry
|
|
83
|
+
const service = services.get(name);
|
|
84
|
+
if (service !== undefined) {
|
|
85
|
+
return service;
|
|
86
|
+
}
|
|
87
|
+
// Look up in ancestor context providers ($context)
|
|
88
|
+
const contextProvided = resolveFromProviders(el, name);
|
|
89
|
+
if (contextProvided !== undefined) {
|
|
90
|
+
return contextProvided;
|
|
91
|
+
}
|
|
92
|
+
throw new Error(`Unknown injectable: ${name}`);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Render HTML with directives on the server.
|
|
99
|
+
*
|
|
100
|
+
* @remarks
|
|
101
|
+
* Uses MutationObserver to index elements with directive attributes
|
|
102
|
+
* as they are parsed, then executes directives to produce the final HTML.
|
|
103
|
+
* Directive attributes are preserved in output for client hydration.
|
|
104
|
+
* Directives are processed in tree order (parents before children),
|
|
105
|
+
* with priority used only for multiple directives on the same element.
|
|
106
|
+
*
|
|
107
|
+
* @param html - The HTML template string
|
|
108
|
+
* @param state - The state object to use for expression evaluation
|
|
109
|
+
* @param registry - The directive registry
|
|
110
|
+
* @returns The rendered HTML string
|
|
111
|
+
*
|
|
112
|
+
* @example
|
|
113
|
+
* ```ts
|
|
114
|
+
* const registry = new Map();
|
|
115
|
+
* registry.set('text', textDirective);
|
|
116
|
+
*
|
|
117
|
+
* const html = await render(
|
|
118
|
+
* '<span c-text="user.name"></span>',
|
|
119
|
+
* { user: { name: 'Alice' } },
|
|
120
|
+
* registry
|
|
121
|
+
* );
|
|
122
|
+
* // '<span c-text="user.name">Alice</span>'
|
|
123
|
+
* ```
|
|
124
|
+
*/
|
|
125
|
+
export async function render(html, state, registry) {
|
|
126
|
+
const { document, MutationObserver } = parseHTML('<!DOCTYPE html><html><body></body></html>');
|
|
127
|
+
const index = [];
|
|
128
|
+
const selector = getSelector(registry);
|
|
129
|
+
const observer = new MutationObserver((mutations) => {
|
|
130
|
+
for (const mutation of mutations) {
|
|
131
|
+
for (const node of mutation.addedNodes) {
|
|
132
|
+
if (node.nodeType !== 1)
|
|
133
|
+
continue;
|
|
134
|
+
const el = node;
|
|
135
|
+
const matches = el.matches(selector) ? [el] : [];
|
|
136
|
+
const descendants = [...el.querySelectorAll(selector)];
|
|
137
|
+
for (const match of [...matches, ...descendants]) {
|
|
138
|
+
// Handle native <slot> elements
|
|
139
|
+
if (match.tagName === 'SLOT') {
|
|
140
|
+
index.push({
|
|
141
|
+
el: match,
|
|
142
|
+
name: 'slot',
|
|
143
|
+
directive: null,
|
|
144
|
+
expr: '',
|
|
145
|
+
priority: DirectivePriority.NORMAL,
|
|
146
|
+
isNativeSlot: true
|
|
147
|
+
});
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
for (const [name, directive] of registry) {
|
|
151
|
+
const attr = match.getAttribute(`c-${name}`);
|
|
152
|
+
if (attr !== null) {
|
|
153
|
+
index.push({
|
|
154
|
+
el: match,
|
|
155
|
+
name,
|
|
156
|
+
directive,
|
|
157
|
+
expr: attr,
|
|
158
|
+
priority: directive.priority ?? DirectivePriority.NORMAL
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
observer.observe(document.body, { childList: true, subtree: true });
|
|
167
|
+
document.body.innerHTML = html;
|
|
168
|
+
await new Promise(r => setTimeout(r, 0));
|
|
169
|
+
const ctx = createContext(Mode.SERVER, state);
|
|
170
|
+
const processed = new Set();
|
|
171
|
+
// Process directives in rounds until no new elements are added
|
|
172
|
+
let hasMore = true;
|
|
173
|
+
while (hasMore) {
|
|
174
|
+
// Group by element, preserving tree order from the index
|
|
175
|
+
const elementOrder = [];
|
|
176
|
+
const byElement = new Map();
|
|
177
|
+
for (const item of index) {
|
|
178
|
+
if (processed.has(item.el))
|
|
179
|
+
continue;
|
|
180
|
+
if (!byElement.has(item.el)) {
|
|
181
|
+
elementOrder.push(item.el);
|
|
182
|
+
byElement.set(item.el, []);
|
|
183
|
+
}
|
|
184
|
+
byElement.get(item.el).push(item);
|
|
185
|
+
}
|
|
186
|
+
if (elementOrder.length === 0) {
|
|
187
|
+
hasMore = false;
|
|
188
|
+
break;
|
|
189
|
+
}
|
|
190
|
+
// Process elements in tree order
|
|
191
|
+
for (const el of elementOrder) {
|
|
192
|
+
// Skip elements that were removed (e.g., by c-for cloning)
|
|
193
|
+
if (!el.isConnected) {
|
|
194
|
+
processed.add(el);
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
// Skip elements already processed by structural directives (c-for, c-if)
|
|
198
|
+
// These elements have their own scoped processing
|
|
199
|
+
if (el.hasAttribute(FOR_PROCESSED_ATTR) || el.hasAttribute(IF_PROCESSED_ATTR)) {
|
|
200
|
+
processed.add(el);
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
// Skip template elements with c-for - these are template wrappers created by c-for
|
|
204
|
+
// and should not be processed as directives (they're for client hydration)
|
|
205
|
+
if (el.tagName === 'TEMPLATE' && el.hasAttribute('c-for')) {
|
|
206
|
+
processed.add(el);
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
// Skip template content elements - these are inside template wrappers
|
|
210
|
+
// and their directives are processed by c-for when rendering items
|
|
211
|
+
if (el.hasAttribute(FOR_TEMPLATE_ATTR)) {
|
|
212
|
+
processed.add(el);
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
processed.add(el);
|
|
216
|
+
const directives = byElement.get(el);
|
|
217
|
+
// Sort directives on this element by priority (higher first)
|
|
218
|
+
directives.sort((a, b) => b.priority - a.priority);
|
|
219
|
+
for (const item of directives) {
|
|
220
|
+
// Check if element was disconnected by a previous directive (e.g., c-for replacing it)
|
|
221
|
+
if (!item.el.isConnected) {
|
|
222
|
+
break;
|
|
223
|
+
}
|
|
224
|
+
if (item.isNativeSlot) {
|
|
225
|
+
processNativeSlot(item.el);
|
|
226
|
+
}
|
|
227
|
+
else {
|
|
228
|
+
const args = resolveDependencies(item.directive, item.expr, item.el, ctx, state);
|
|
229
|
+
await item.directive(...args);
|
|
230
|
+
// Register as context provider if directive declares $context
|
|
231
|
+
if (item.directive.$context?.length) {
|
|
232
|
+
const localState = getLocalState(item.el);
|
|
233
|
+
registerProvider(item.el, item.directive, localState);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
// Let observer catch new elements
|
|
238
|
+
await new Promise(r => setTimeout(r, 0));
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
observer.disconnect();
|
|
242
|
+
return document.body.innerHTML;
|
|
243
|
+
}
|