gonia 0.3.0 → 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.
- package/dist/client/hydrate.js +61 -6
- package/dist/directive-utils.d.ts +50 -0
- package/dist/directive-utils.js +101 -0
- package/dist/server/render.js +36 -28
- package/package.json +1 -1
package/dist/client/hydrate.js
CHANGED
|
@@ -12,6 +12,7 @@ import { findParentScope, createElementScope, getElementScope } from '../scope.j
|
|
|
12
12
|
import { resolveDependencies as resolveInjectables } from '../inject.js';
|
|
13
13
|
import { resolveContext } from '../context-registry.js';
|
|
14
14
|
import { effect } from '../reactivity.js';
|
|
15
|
+
import { applyAssigns, directiveNeedsScope } from '../directive-utils.js';
|
|
15
16
|
// Built-in directives
|
|
16
17
|
import { text } from '../directives/text.js';
|
|
17
18
|
import { show } from '../directives/show.js';
|
|
@@ -56,15 +57,22 @@ function getDefaultRegistry() {
|
|
|
56
57
|
function getSelector(registry) {
|
|
57
58
|
if (!cachedSelector) {
|
|
58
59
|
const directiveSelectors = [];
|
|
60
|
+
// Include directives from passed registry
|
|
59
61
|
for (const name of registry.keys()) {
|
|
60
62
|
directiveSelectors.push(`[g-${name}]`);
|
|
61
63
|
}
|
|
64
|
+
// Include directives from global registry
|
|
65
|
+
for (const name of getDirectiveNames()) {
|
|
66
|
+
directiveSelectors.push(`[${name}]`);
|
|
67
|
+
}
|
|
62
68
|
// Also match native <slot> elements
|
|
63
69
|
directiveSelectors.push('slot');
|
|
64
70
|
// Match template placeholders from SSR (g-if with false condition)
|
|
65
71
|
directiveSelectors.push('template[data-g-if]');
|
|
66
72
|
// Match g-scope for inline scope initialization
|
|
67
73
|
directiveSelectors.push('[g-scope]');
|
|
74
|
+
// Match g-bind:* for attribute bindings
|
|
75
|
+
directiveSelectors.push('[g-bind\\:class]');
|
|
68
76
|
cachedSelector = directiveSelectors.join(',');
|
|
69
77
|
}
|
|
70
78
|
return cachedSelector;
|
|
@@ -76,10 +84,10 @@ function getSelector(registry) {
|
|
|
76
84
|
*/
|
|
77
85
|
function getDirectivesForElement(el, registry) {
|
|
78
86
|
const directives = [];
|
|
87
|
+
// Check local registry (built-in directives with g- prefix)
|
|
79
88
|
for (const [name, directive] of registry) {
|
|
80
89
|
const attr = el.getAttribute(`g-${name}`);
|
|
81
90
|
if (attr !== null) {
|
|
82
|
-
// Look up options from the global directive registry
|
|
83
91
|
const registration = getDirective(`g-${name}`);
|
|
84
92
|
directives.push({
|
|
85
93
|
name,
|
|
@@ -89,6 +97,24 @@ function getDirectivesForElement(el, registry) {
|
|
|
89
97
|
});
|
|
90
98
|
}
|
|
91
99
|
}
|
|
100
|
+
// Check global registry (custom directives)
|
|
101
|
+
for (const name of getDirectiveNames()) {
|
|
102
|
+
// Skip if already matched via local registry
|
|
103
|
+
if (directives.some(d => `g-${d.name}` === name))
|
|
104
|
+
continue;
|
|
105
|
+
const attr = el.getAttribute(name);
|
|
106
|
+
if (attr !== null) {
|
|
107
|
+
const registration = getDirective(name);
|
|
108
|
+
if (registration?.fn) {
|
|
109
|
+
directives.push({
|
|
110
|
+
name: name.replace(/^g-/, ''),
|
|
111
|
+
directive: registration.fn,
|
|
112
|
+
expr: attr,
|
|
113
|
+
using: registration.options.using
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
92
118
|
// Sort by priority (higher first)
|
|
93
119
|
directives.sort((a, b) => {
|
|
94
120
|
const priorityA = a.directive.priority ?? DirectivePriority.NORMAL;
|
|
@@ -199,8 +225,31 @@ function processElement(el, registry) {
|
|
|
199
225
|
// Skip if nothing to process
|
|
200
226
|
if (directives.length === 0 && !hasScopeAttr && !hasBindAttrs)
|
|
201
227
|
return;
|
|
202
|
-
|
|
203
|
-
|
|
228
|
+
// Check if any directive needs a scope
|
|
229
|
+
let scope = findParentScope(el, true) ?? {};
|
|
230
|
+
let directiveCreatedScope = false;
|
|
231
|
+
// Collect full directive names for conflict detection
|
|
232
|
+
const directiveFullNames = [];
|
|
233
|
+
for (const { name } of directives) {
|
|
234
|
+
const fullName = `g-${name}`;
|
|
235
|
+
directiveFullNames.push(fullName);
|
|
236
|
+
const registration = getDirective(fullName);
|
|
237
|
+
if (!directiveCreatedScope && directiveNeedsScope(fullName)) {
|
|
238
|
+
// Create a new scope that inherits from parent
|
|
239
|
+
scope = createElementScope(el, scope);
|
|
240
|
+
directiveCreatedScope = true;
|
|
241
|
+
}
|
|
242
|
+
// Register DI providers if present
|
|
243
|
+
if (registration?.options.provide) {
|
|
244
|
+
registerDIProviders(el, registration.options.provide);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
// Apply assigns with conflict detection
|
|
248
|
+
if (directiveCreatedScope) {
|
|
249
|
+
applyAssigns(scope, directiveFullNames);
|
|
250
|
+
}
|
|
251
|
+
const ctx = createContext(Mode.CLIENT, scope);
|
|
252
|
+
contextCache.set(el, ctx);
|
|
204
253
|
// Process g-scope first (inline scope initialization)
|
|
205
254
|
if (hasScopeAttr) {
|
|
206
255
|
const scopeAttr = el.getAttribute('g-scope');
|
|
@@ -384,10 +433,16 @@ async function processDirectiveElements() {
|
|
|
384
433
|
if (options.scope) {
|
|
385
434
|
const parentScope = findParentScope(el);
|
|
386
435
|
scope = createElementScope(el, parentScope);
|
|
387
|
-
//
|
|
388
|
-
|
|
389
|
-
|
|
436
|
+
// Collect all directive names on this element for conflict detection
|
|
437
|
+
const directiveNames = [name];
|
|
438
|
+
for (const attr of el.attributes) {
|
|
439
|
+
const attrReg = getDirective(attr.name);
|
|
440
|
+
if (attrReg) {
|
|
441
|
+
directiveNames.push(attr.name);
|
|
442
|
+
}
|
|
390
443
|
}
|
|
444
|
+
// Apply assigns with conflict detection
|
|
445
|
+
applyAssigns(scope, directiveNames);
|
|
391
446
|
}
|
|
392
447
|
else {
|
|
393
448
|
scope = findParentScope(el, true) ?? {};
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared utilities for directive processing (client and server).
|
|
3
|
+
*
|
|
4
|
+
* @packageDocumentation
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Information about an assign conflict.
|
|
8
|
+
*/
|
|
9
|
+
export interface AssignConflict {
|
|
10
|
+
key: string;
|
|
11
|
+
directives: string[];
|
|
12
|
+
priorities: number[];
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Result of resolving assigns from multiple directives.
|
|
16
|
+
*/
|
|
17
|
+
export interface ResolvedAssigns {
|
|
18
|
+
/** The merged assign values (higher priority wins) */
|
|
19
|
+
values: Record<string, unknown>;
|
|
20
|
+
/** Warnings for different-priority conflicts */
|
|
21
|
+
warnings: string[];
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Resolve assign values from multiple directives on the same element.
|
|
25
|
+
*
|
|
26
|
+
* @param directiveNames - Names of directives on the element
|
|
27
|
+
* @throws Error if same-priority directives conflict on an assign key
|
|
28
|
+
* @returns Resolved assigns and any warnings
|
|
29
|
+
*/
|
|
30
|
+
export declare function resolveAssigns(directiveNames: string[]): ResolvedAssigns;
|
|
31
|
+
/**
|
|
32
|
+
* Apply resolved assigns to a scope, logging any warnings.
|
|
33
|
+
*
|
|
34
|
+
* @param scope - The scope to apply assigns to
|
|
35
|
+
* @param directiveNames - Names of directives on the element
|
|
36
|
+
* @returns The scope with assigns applied
|
|
37
|
+
*/
|
|
38
|
+
export declare function applyAssigns(scope: Record<string, unknown>, directiveNames: string[]): Record<string, unknown>;
|
|
39
|
+
/**
|
|
40
|
+
* Check if a directive should create/use a scope based on its options.
|
|
41
|
+
*/
|
|
42
|
+
export declare function directiveNeedsScope(name: string): boolean;
|
|
43
|
+
/**
|
|
44
|
+
* Get directive options with defaults.
|
|
45
|
+
*/
|
|
46
|
+
export declare function getDirectiveOptions(name: string): import("./types.js").DirectiveOptions;
|
|
47
|
+
/**
|
|
48
|
+
* Get directive priority.
|
|
49
|
+
*/
|
|
50
|
+
export declare function getDirectivePriority(name: string): number;
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared utilities for directive processing (client and server).
|
|
3
|
+
*
|
|
4
|
+
* @packageDocumentation
|
|
5
|
+
*/
|
|
6
|
+
import { getDirective, DirectivePriority } from './types.js';
|
|
7
|
+
/**
|
|
8
|
+
* Resolve assign values from multiple directives on the same element.
|
|
9
|
+
*
|
|
10
|
+
* @param directiveNames - Names of directives on the element
|
|
11
|
+
* @throws Error if same-priority directives conflict on an assign key
|
|
12
|
+
* @returns Resolved assigns and any warnings
|
|
13
|
+
*/
|
|
14
|
+
export function resolveAssigns(directiveNames) {
|
|
15
|
+
const assignsByKey = new Map();
|
|
16
|
+
// Collect all assigns grouped by key
|
|
17
|
+
for (const name of directiveNames) {
|
|
18
|
+
const registration = getDirective(name);
|
|
19
|
+
if (!registration?.options.assign)
|
|
20
|
+
continue;
|
|
21
|
+
const priority = registration.fn?.priority ?? DirectivePriority.NORMAL;
|
|
22
|
+
for (const [key, value] of Object.entries(registration.options.assign)) {
|
|
23
|
+
if (!assignsByKey.has(key)) {
|
|
24
|
+
assignsByKey.set(key, []);
|
|
25
|
+
}
|
|
26
|
+
assignsByKey.get(key).push({ directive: name, priority, value });
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
const values = {};
|
|
30
|
+
const warnings = [];
|
|
31
|
+
// Check for conflicts and resolve
|
|
32
|
+
for (const [key, sources] of assignsByKey) {
|
|
33
|
+
if (sources.length === 1) {
|
|
34
|
+
values[key] = sources[0].value;
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
// Group by priority
|
|
38
|
+
const byPriority = new Map();
|
|
39
|
+
for (const source of sources) {
|
|
40
|
+
if (!byPriority.has(source.priority)) {
|
|
41
|
+
byPriority.set(source.priority, []);
|
|
42
|
+
}
|
|
43
|
+
byPriority.get(source.priority).push(source);
|
|
44
|
+
}
|
|
45
|
+
// Check for same-priority conflicts
|
|
46
|
+
for (const [priority, group] of byPriority) {
|
|
47
|
+
if (group.length > 1) {
|
|
48
|
+
const names = group.map(s => s.directive).join(', ');
|
|
49
|
+
throw new Error(`Conflicting assign key "${key}" at same priority (${priority}) between directives: ${names}`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
// Different priorities - highest wins, emit warning
|
|
53
|
+
const sorted = sources.sort((a, b) => b.priority - a.priority);
|
|
54
|
+
const winner = sorted[0];
|
|
55
|
+
const losers = sorted.slice(1);
|
|
56
|
+
values[key] = winner.value;
|
|
57
|
+
for (const loser of losers) {
|
|
58
|
+
warnings.push(`Directive "${winner.directive}" (priority ${winner.priority}) overrides assign key "${key}" from "${loser.directive}" (priority ${loser.priority})`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return { values, warnings };
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Apply resolved assigns to a scope, logging any warnings.
|
|
65
|
+
*
|
|
66
|
+
* @param scope - The scope to apply assigns to
|
|
67
|
+
* @param directiveNames - Names of directives on the element
|
|
68
|
+
* @returns The scope with assigns applied
|
|
69
|
+
*/
|
|
70
|
+
export function applyAssigns(scope, directiveNames) {
|
|
71
|
+
const { values, warnings } = resolveAssigns(directiveNames);
|
|
72
|
+
for (const warning of warnings) {
|
|
73
|
+
console.warn(`[gonia] ${warning}`);
|
|
74
|
+
}
|
|
75
|
+
Object.assign(scope, values);
|
|
76
|
+
return scope;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Check if a directive should create/use a scope based on its options.
|
|
80
|
+
*/
|
|
81
|
+
export function directiveNeedsScope(name) {
|
|
82
|
+
const registration = getDirective(name);
|
|
83
|
+
if (!registration)
|
|
84
|
+
return false;
|
|
85
|
+
const { options, fn } = registration;
|
|
86
|
+
return !!(options.scope || options.assign || fn?.$context?.length);
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Get directive options with defaults.
|
|
90
|
+
*/
|
|
91
|
+
export function getDirectiveOptions(name) {
|
|
92
|
+
const registration = getDirective(name);
|
|
93
|
+
return registration?.options ?? {};
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Get directive priority.
|
|
97
|
+
*/
|
|
98
|
+
export function getDirectivePriority(name) {
|
|
99
|
+
const registration = getDirective(name);
|
|
100
|
+
return registration?.fn?.priority ?? DirectivePriority.NORMAL;
|
|
101
|
+
}
|
package/dist/server/render.js
CHANGED
|
@@ -14,6 +14,7 @@ import { IF_PROCESSED_ATTR } from '../directives/if.js';
|
|
|
14
14
|
import { PROCESSED_ATTR } from '../process.js';
|
|
15
15
|
import { resolveDependencies as resolveInjectables } from '../inject.js';
|
|
16
16
|
import { resolveContext } from '../context-registry.js';
|
|
17
|
+
import { applyAssigns, directiveNeedsScope } from '../directive-utils.js';
|
|
17
18
|
/**
|
|
18
19
|
* Decode HTML entities that happy-dom doesn't decode.
|
|
19
20
|
*
|
|
@@ -354,10 +355,13 @@ export async function render(html, state, registry) {
|
|
|
354
355
|
}
|
|
355
356
|
}
|
|
356
357
|
// Process g-bind:* attributes (dynamic attribute binding)
|
|
358
|
+
// Use the nearest ancestor scope for evaluation
|
|
359
|
+
const bindScope = findServerScope(el, state);
|
|
360
|
+
const bindCtx = createContext(Mode.SERVER, bindScope);
|
|
357
361
|
for (const attr of [...el.attributes]) {
|
|
358
362
|
if (attr.name.startsWith('g-bind:')) {
|
|
359
363
|
const targetAttr = attr.name.slice('g-bind:'.length);
|
|
360
|
-
const value =
|
|
364
|
+
const value = bindCtx.eval(decodeHTMLEntities(attr.value));
|
|
361
365
|
if (value === null || value === undefined) {
|
|
362
366
|
el.removeAttribute(targetAttr);
|
|
363
367
|
}
|
|
@@ -366,6 +370,24 @@ export async function render(html, state, registry) {
|
|
|
366
370
|
}
|
|
367
371
|
}
|
|
368
372
|
}
|
|
373
|
+
// Collect all directive names for conflict detection
|
|
374
|
+
const directiveNames = [];
|
|
375
|
+
for (const item of directives) {
|
|
376
|
+
if (!item.isNativeSlot && item.directive !== null) {
|
|
377
|
+
directiveNames.push(item.name);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
// Check if any directive needs scope - create once if so
|
|
381
|
+
let elementScope = null;
|
|
382
|
+
for (const name of directiveNames) {
|
|
383
|
+
if (directiveNeedsScope(name)) {
|
|
384
|
+
const parentScope = findServerScope(el, state);
|
|
385
|
+
elementScope = createElementScope(el, parentScope);
|
|
386
|
+
// Apply all assigns with conflict detection
|
|
387
|
+
applyAssigns(elementScope, directiveNames);
|
|
388
|
+
break;
|
|
389
|
+
}
|
|
390
|
+
}
|
|
369
391
|
for (const item of directives) {
|
|
370
392
|
// Check if element was disconnected by a previous directive (e.g., g-for replacing it)
|
|
371
393
|
if (!item.el.isConnected) {
|
|
@@ -382,24 +404,13 @@ export async function render(html, state, registry) {
|
|
|
382
404
|
if (!registration)
|
|
383
405
|
continue;
|
|
384
406
|
const { fn, options } = registration;
|
|
385
|
-
//
|
|
386
|
-
|
|
387
|
-
if
|
|
388
|
-
const parentScope = findServerScope(item.el, state);
|
|
389
|
-
scopeState = createElementScope(item.el, parentScope);
|
|
390
|
-
// Apply assigned values to scope
|
|
391
|
-
if (options.assign) {
|
|
392
|
-
Object.assign(scopeState, options.assign);
|
|
393
|
-
}
|
|
394
|
-
}
|
|
395
|
-
else {
|
|
396
|
-
scopeState = findServerScope(item.el, state);
|
|
397
|
-
}
|
|
398
|
-
// 2. Register DI providers if present
|
|
407
|
+
// Use pre-created scope or find existing
|
|
408
|
+
const scopeState = elementScope ?? findServerScope(item.el, state);
|
|
409
|
+
// Register DI providers if present
|
|
399
410
|
if (options.provide) {
|
|
400
411
|
registerDIProviders(item.el, options.provide);
|
|
401
412
|
}
|
|
402
|
-
//
|
|
413
|
+
// Call directive function if present (initializes state)
|
|
403
414
|
if (fn) {
|
|
404
415
|
const config = createServerResolverConfig(item.el, scopeState, state);
|
|
405
416
|
const args = resolveInjectables(fn, item.expr, item.el, ctx.eval.bind(ctx), config, options.using);
|
|
@@ -409,7 +420,7 @@ export async function render(html, state, registry) {
|
|
|
409
420
|
registerProvider(item.el, fn, scopeState);
|
|
410
421
|
}
|
|
411
422
|
}
|
|
412
|
-
//
|
|
423
|
+
// Render template if present
|
|
413
424
|
if (options.template) {
|
|
414
425
|
const attrs = getTemplateAttrs(item.el);
|
|
415
426
|
let html;
|
|
@@ -428,17 +439,14 @@ export async function render(html, state, registry) {
|
|
|
428
439
|
continue;
|
|
429
440
|
}
|
|
430
441
|
else {
|
|
431
|
-
// Attribute directive
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
else {
|
|
440
|
-
// Other directives use nearest ancestor scope or root
|
|
441
|
-
scopeState = findServerScope(item.el, state);
|
|
442
|
+
// Attribute directive - use pre-created scope or find existing
|
|
443
|
+
const scopeState = elementScope ?? findServerScope(item.el, state);
|
|
444
|
+
// Get registration options
|
|
445
|
+
const registration = getDirective(item.name);
|
|
446
|
+
const options = registration?.options ?? {};
|
|
447
|
+
// Register DI providers if present
|
|
448
|
+
if (options.provide) {
|
|
449
|
+
registerDIProviders(item.el, options.provide);
|
|
442
450
|
}
|
|
443
451
|
const config = createServerResolverConfig(item.el, scopeState, state);
|
|
444
452
|
const args = resolveInjectables(item.directive, item.expr, item.el, ctx.eval.bind(ctx), config, item.using);
|