gonia 0.1.2 → 0.2.0
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 +24 -7
- package/dist/client/hydrate.js +94 -59
- package/dist/context-registry.d.ts +130 -0
- package/dist/context-registry.js +153 -0
- package/dist/directives/for.d.ts +1 -1
- package/dist/directives/for.js +12 -76
- package/dist/directives/if.d.ts +4 -1
- package/dist/directives/if.js +69 -66
- package/dist/directives/slot.d.ts +4 -2
- package/dist/directives/slot.js +11 -16
- package/dist/directives/template.d.ts +9 -2
- package/dist/directives/template.js +14 -13
- package/dist/dom.d.ts +29 -0
- package/dist/dom.js +39 -0
- package/dist/expression.js +17 -2
- package/dist/index.d.ts +8 -2
- package/dist/index.js +5 -2
- package/dist/inject.d.ts +47 -6
- package/dist/inject.js +66 -4
- package/dist/process.d.ts +61 -0
- package/dist/process.js +215 -0
- package/dist/providers.js +9 -12
- package/dist/scope.js +17 -22
- package/dist/server/render.js +70 -41
- package/dist/types.d.ts +130 -20
- package/dist/types.js +48 -2
- package/dist/vite/plugin.d.ts +27 -0
- package/dist/vite/plugin.js +56 -3
- package/package.json +9 -2
package/dist/server/render.js
CHANGED
|
@@ -3,13 +3,15 @@
|
|
|
3
3
|
*
|
|
4
4
|
* @packageDocumentation
|
|
5
5
|
*/
|
|
6
|
-
import { parseHTML } from 'linkedom';
|
|
7
|
-
import { Mode, DirectivePriority } from '../types.js';
|
|
6
|
+
import { parseHTML } from 'linkedom/worker';
|
|
7
|
+
import { Mode, DirectivePriority, getDirective } from '../types.js';
|
|
8
8
|
import { createContext } from '../context.js';
|
|
9
9
|
import { processNativeSlot } from '../directives/slot.js';
|
|
10
10
|
import { getLocalState, registerProvider, resolveFromProviders, resolveFromDIProviders } from '../providers.js';
|
|
11
11
|
import { FOR_PROCESSED_ATTR, FOR_TEMPLATE_ATTR } from '../directives/for.js';
|
|
12
12
|
import { IF_PROCESSED_ATTR } from '../directives/if.js';
|
|
13
|
+
import { resolveDependencies as resolveInjectables } from '../inject.js';
|
|
14
|
+
import { resolveContext } from '../context-registry.js';
|
|
13
15
|
/** Registered services */
|
|
14
16
|
let services = new Map();
|
|
15
17
|
const selectorCache = new WeakMap();
|
|
@@ -24,11 +26,26 @@ function getSelector(registry) {
|
|
|
24
26
|
const directiveSelectors = [...registry.keys()].map(n => `[g-${n}]`);
|
|
25
27
|
// Also match native <slot> elements
|
|
26
28
|
directiveSelectors.push('slot');
|
|
29
|
+
// Match g-scope for inline scope initialization
|
|
30
|
+
directiveSelectors.push('[g-scope]');
|
|
27
31
|
selector = directiveSelectors.join(',');
|
|
28
32
|
selectorCache.set(registry, selector);
|
|
29
33
|
}
|
|
30
34
|
return selector;
|
|
31
35
|
}
|
|
36
|
+
/**
|
|
37
|
+
* Check if element has any g-bind:* attributes.
|
|
38
|
+
*
|
|
39
|
+
* @internal
|
|
40
|
+
*/
|
|
41
|
+
function hasBindAttributes(el) {
|
|
42
|
+
for (const attr of el.attributes) {
|
|
43
|
+
if (attr.name.startsWith('g-bind:')) {
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
32
49
|
/**
|
|
33
50
|
* Register a directive in the registry.
|
|
34
51
|
*
|
|
@@ -53,46 +70,29 @@ export function registerService(name, service) {
|
|
|
53
70
|
services.set(name, service);
|
|
54
71
|
}
|
|
55
72
|
/**
|
|
56
|
-
*
|
|
73
|
+
* Create resolver config for server-side dependency resolution.
|
|
57
74
|
*
|
|
58
75
|
* @internal
|
|
59
76
|
*/
|
|
60
|
-
function
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
return
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
return
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
-
});
|
|
77
|
+
function createServerResolverConfig(el, rootState) {
|
|
78
|
+
return {
|
|
79
|
+
resolveContext: (key) => resolveContext(el, key),
|
|
80
|
+
resolveState: () => getLocalState(el) ?? rootState,
|
|
81
|
+
resolveRootState: () => rootState,
|
|
82
|
+
resolveCustom: (name) => {
|
|
83
|
+
// Look up in ancestor DI providers first (provide option)
|
|
84
|
+
const diProvided = resolveFromDIProviders(el, name);
|
|
85
|
+
if (diProvided !== undefined)
|
|
86
|
+
return diProvided;
|
|
87
|
+
// Look up in global services registry
|
|
88
|
+
const service = services.get(name);
|
|
89
|
+
if (service !== undefined)
|
|
90
|
+
return service;
|
|
91
|
+
// Look up in ancestor context providers ($context)
|
|
92
|
+
return resolveFromProviders(el, name);
|
|
93
|
+
},
|
|
94
|
+
mode: 'server'
|
|
95
|
+
};
|
|
96
96
|
}
|
|
97
97
|
/**
|
|
98
98
|
* Render HTML with directives on the server.
|
|
@@ -135,6 +135,10 @@ export async function render(html, state, registry) {
|
|
|
135
135
|
const matches = el.matches(selector) ? [el] : [];
|
|
136
136
|
const descendants = [...el.querySelectorAll(selector)];
|
|
137
137
|
for (const match of [...matches, ...descendants]) {
|
|
138
|
+
// Skip elements inside template content (used as placeholders)
|
|
139
|
+
if (match.closest('template')) {
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
138
142
|
// Handle native <slot> elements
|
|
139
143
|
if (match.tagName === 'SLOT') {
|
|
140
144
|
index.push({
|
|
@@ -150,12 +154,15 @@ export async function render(html, state, registry) {
|
|
|
150
154
|
for (const [name, directive] of registry) {
|
|
151
155
|
const attr = match.getAttribute(`g-${name}`);
|
|
152
156
|
if (attr !== null) {
|
|
157
|
+
// Look up options from the global directive registry
|
|
158
|
+
const registration = getDirective(`g-${name}`);
|
|
153
159
|
index.push({
|
|
154
160
|
el: match,
|
|
155
161
|
name,
|
|
156
162
|
directive,
|
|
157
163
|
expr: attr,
|
|
158
|
-
priority: directive.priority ?? DirectivePriority.NORMAL
|
|
164
|
+
priority: directive.priority ?? DirectivePriority.NORMAL,
|
|
165
|
+
using: registration?.options.using
|
|
159
166
|
});
|
|
160
167
|
}
|
|
161
168
|
}
|
|
@@ -216,6 +223,27 @@ export async function render(html, state, registry) {
|
|
|
216
223
|
const directives = byElement.get(el);
|
|
217
224
|
// Sort directives on this element by priority (higher first)
|
|
218
225
|
directives.sort((a, b) => b.priority - a.priority);
|
|
226
|
+
// Process g-scope first (inline scope initialization)
|
|
227
|
+
const scopeAttr = el.getAttribute('g-scope');
|
|
228
|
+
if (scopeAttr) {
|
|
229
|
+
const scopeValues = ctx.eval(scopeAttr);
|
|
230
|
+
if (scopeValues && typeof scopeValues === 'object') {
|
|
231
|
+
Object.assign(state, scopeValues);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
// Process g-bind:* attributes (dynamic attribute binding)
|
|
235
|
+
for (const attr of [...el.attributes]) {
|
|
236
|
+
if (attr.name.startsWith('g-bind:')) {
|
|
237
|
+
const targetAttr = attr.name.slice('g-bind:'.length);
|
|
238
|
+
const value = ctx.eval(attr.value);
|
|
239
|
+
if (value === null || value === undefined) {
|
|
240
|
+
el.removeAttribute(targetAttr);
|
|
241
|
+
}
|
|
242
|
+
else {
|
|
243
|
+
el.setAttribute(targetAttr, String(value));
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
219
247
|
for (const item of directives) {
|
|
220
248
|
// Check if element was disconnected by a previous directive (e.g., g-for replacing it)
|
|
221
249
|
if (!item.el.isConnected) {
|
|
@@ -225,7 +253,8 @@ export async function render(html, state, registry) {
|
|
|
225
253
|
processNativeSlot(item.el);
|
|
226
254
|
}
|
|
227
255
|
else {
|
|
228
|
-
const
|
|
256
|
+
const config = createServerResolverConfig(item.el, state);
|
|
257
|
+
const args = resolveInjectables(item.directive, item.expr, item.el, ctx.eval.bind(ctx), config, item.using);
|
|
229
258
|
await item.directive(...args);
|
|
230
259
|
// Register as context provider if directive declares $context
|
|
231
260
|
if (item.directive.$context?.length) {
|
package/dist/types.d.ts
CHANGED
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
*
|
|
4
4
|
* @packageDocumentation
|
|
5
5
|
*/
|
|
6
|
+
import type { ContextKey } from './context-registry.js';
|
|
7
|
+
import type { Injectable } from './inject.js';
|
|
6
8
|
/**
|
|
7
9
|
* Execution mode for the framework.
|
|
8
10
|
*/
|
|
@@ -48,7 +50,7 @@ export interface InjectableRegistry {
|
|
|
48
50
|
/** Function to evaluate expressions against state */
|
|
49
51
|
$eval: EvalFn;
|
|
50
52
|
/** Local reactive state object (isolated per element) */
|
|
51
|
-
$
|
|
53
|
+
$scope: Record<string, unknown>;
|
|
52
54
|
/** Root reactive state object (shared across all elements) */
|
|
53
55
|
$rootState: Record<string, unknown>;
|
|
54
56
|
/** Template registry for g-template directive */
|
|
@@ -59,23 +61,28 @@ export interface InjectableRegistry {
|
|
|
59
61
|
$mode: Mode;
|
|
60
62
|
}
|
|
61
63
|
/**
|
|
62
|
-
*
|
|
64
|
+
* Extract the value type from a ContextKey.
|
|
65
|
+
*/
|
|
66
|
+
type ContextKeyValue<K> = K extends ContextKey<infer V> ? V : never;
|
|
67
|
+
/**
|
|
68
|
+
* Maps a tuple of injectable names or context keys to their corresponding types.
|
|
63
69
|
*
|
|
64
|
-
* @typeParam K - Tuple of injectable
|
|
70
|
+
* @typeParam K - Tuple of injectable names (strings) or ContextKey objects
|
|
65
71
|
* @typeParam T - Type map (defaults to InjectableRegistry)
|
|
66
72
|
*
|
|
67
73
|
* @example
|
|
68
74
|
* ```ts
|
|
69
|
-
* type Args = MapInjectables<['$element', '$
|
|
75
|
+
* type Args = MapInjectables<['$element', '$scope']>;
|
|
70
76
|
* // => [Element, Record<string, unknown>]
|
|
71
77
|
*
|
|
72
|
-
* // With
|
|
73
|
-
*
|
|
74
|
-
*
|
|
78
|
+
* // With context keys
|
|
79
|
+
* const MyContext = createContextKey<{ value: number }>('MyContext');
|
|
80
|
+
* type Args2 = MapInjectables<['$element', typeof MyContext]>;
|
|
81
|
+
* // => [Element, { value: number }]
|
|
75
82
|
* ```
|
|
76
83
|
*/
|
|
77
|
-
export type MapInjectables<K extends readonly string[], T = InjectableRegistry> = {
|
|
78
|
-
[I in keyof K]: K[I] extends keyof T ? T[K[I]] : unknown;
|
|
84
|
+
export type MapInjectables<K extends readonly (string | ContextKey<unknown>)[], T = InjectableRegistry> = {
|
|
85
|
+
[I in keyof K]: K[I] extends ContextKey<unknown> ? ContextKeyValue<K[I]> : K[I] extends keyof T ? T[K[I]] : unknown;
|
|
79
86
|
};
|
|
80
87
|
/**
|
|
81
88
|
* Evaluation context passed to directives.
|
|
@@ -146,25 +153,35 @@ export interface DirectiveMeta<T = InjectableRegistry> {
|
|
|
146
153
|
* - `$expr`: The expression string from the attribute
|
|
147
154
|
* - `$element`: The target DOM element
|
|
148
155
|
* - `$eval`: Function to evaluate expressions: `(expr) => value`
|
|
149
|
-
* - `$
|
|
156
|
+
* - `$scope`: Local reactive state object (isolated per element)
|
|
150
157
|
* - Any registered service names
|
|
158
|
+
* - Any `ContextKey` for typed context resolution
|
|
151
159
|
* - Any names provided by ancestor directives via `$context`
|
|
160
|
+
*
|
|
161
|
+
* @example
|
|
162
|
+
* ```ts
|
|
163
|
+
* // String-based injection
|
|
164
|
+
* myDirective.$inject = ['$element', '$scope'];
|
|
165
|
+
*
|
|
166
|
+
* // With typed context keys
|
|
167
|
+
* myDirective.$inject = ['$element', SlotContentContext];
|
|
168
|
+
* ```
|
|
152
169
|
*/
|
|
153
|
-
$inject?: readonly
|
|
170
|
+
$inject?: readonly Injectable[];
|
|
154
171
|
/**
|
|
155
172
|
* Names this directive exposes as context to descendants.
|
|
156
173
|
*
|
|
157
174
|
* @remarks
|
|
158
|
-
* When a directive declares `$context`, its `$
|
|
175
|
+
* When a directive declares `$context`, its `$scope` becomes
|
|
159
176
|
* available to descendant directives under those names.
|
|
160
177
|
* Useful for passing state through isolate scope boundaries.
|
|
161
178
|
*
|
|
162
179
|
* @example
|
|
163
180
|
* ```ts
|
|
164
|
-
* const themeProvider: Directive = ($
|
|
165
|
-
* $
|
|
181
|
+
* const themeProvider: Directive = ($scope) => {
|
|
182
|
+
* $scope.mode = 'dark';
|
|
166
183
|
* };
|
|
167
|
-
* themeProvider.$inject = ['$
|
|
184
|
+
* themeProvider.$inject = ['$scope'];
|
|
168
185
|
* themeProvider.$context = ['theme'];
|
|
169
186
|
*
|
|
170
187
|
* // Descendants can inject 'theme'
|
|
@@ -187,9 +204,9 @@ export interface DirectiveMeta<T = InjectableRegistry> {
|
|
|
187
204
|
*
|
|
188
205
|
* @remarks
|
|
189
206
|
* Use this type annotation to get contextual typing for directive parameters.
|
|
190
|
-
* The tuple of keys maps to parameter types from InjectableRegistry.
|
|
207
|
+
* The tuple of keys maps to parameter types from InjectableRegistry or ContextKey types.
|
|
191
208
|
*
|
|
192
|
-
* @typeParam K - Tuple of injectable key names
|
|
209
|
+
* @typeParam K - Tuple of injectable key names or ContextKey objects
|
|
193
210
|
* @typeParam T - Optional custom type map to extend InjectableRegistry
|
|
194
211
|
*
|
|
195
212
|
* @example
|
|
@@ -204,6 +221,12 @@ export interface DirectiveMeta<T = InjectableRegistry> {
|
|
|
204
221
|
* $element.textContent = String($eval($expr) ?? '');
|
|
205
222
|
* };
|
|
206
223
|
*
|
|
224
|
+
* // With typed context keys
|
|
225
|
+
* const slot: Directive<['$element', typeof SlotContentContext]> = ($element, content) => {
|
|
226
|
+
* // content is typed as SlotContent
|
|
227
|
+
* console.log(content.slots);
|
|
228
|
+
* };
|
|
229
|
+
*
|
|
207
230
|
* // With custom types (extend InjectableRegistry first)
|
|
208
231
|
* declare module 'gonia' {
|
|
209
232
|
* interface InjectableRegistry {
|
|
@@ -215,7 +238,7 @@ export interface DirectiveMeta<T = InjectableRegistry> {
|
|
|
215
238
|
* };
|
|
216
239
|
* ```
|
|
217
240
|
*/
|
|
218
|
-
export type Directive<K extends readonly (string
|
|
241
|
+
export type Directive<K extends readonly (string | ContextKey<unknown>)[] = readonly (string & keyof InjectableRegistry)[], T extends Record<string, unknown> = {}> = ((...args: MapInjectables<K, InjectableRegistry & T>) => void | Promise<void>) & DirectiveMeta<InjectableRegistry & T>;
|
|
219
242
|
/**
|
|
220
243
|
* Attributes passed to template functions.
|
|
221
244
|
*/
|
|
@@ -297,6 +320,67 @@ export interface DirectiveOptions {
|
|
|
297
320
|
* ```
|
|
298
321
|
*/
|
|
299
322
|
provide?: Record<string, unknown>;
|
|
323
|
+
/**
|
|
324
|
+
* Context keys this directive uses.
|
|
325
|
+
*
|
|
326
|
+
* @remarks
|
|
327
|
+
* Declares which contexts the directive depends on. The resolved
|
|
328
|
+
* context values are appended to the directive function parameters
|
|
329
|
+
* in the order they appear in this array.
|
|
330
|
+
*
|
|
331
|
+
* This enables:
|
|
332
|
+
* - Static analysis of context dependencies
|
|
333
|
+
* - Automatic `$inject` generation by the Vite plugin
|
|
334
|
+
* - Type-safe context access without manual `resolveContext` calls
|
|
335
|
+
*
|
|
336
|
+
* @example
|
|
337
|
+
* ```ts
|
|
338
|
+
* const ThemeContext = createContextKey<{ mode: 'light' | 'dark' }>('Theme');
|
|
339
|
+
* const UserContext = createContextKey<{ name: string }>('User');
|
|
340
|
+
*
|
|
341
|
+
* directive('themed-greeting', ($element, $scope, theme, user) => {
|
|
342
|
+
* // theme and user are resolved from the using array
|
|
343
|
+
* $element.textContent = `Hello ${user.name}!`;
|
|
344
|
+
* $element.className = theme.mode;
|
|
345
|
+
* }, {
|
|
346
|
+
* using: [ThemeContext, UserContext]
|
|
347
|
+
* });
|
|
348
|
+
* ```
|
|
349
|
+
*/
|
|
350
|
+
using?: ContextKey<unknown>[];
|
|
351
|
+
/**
|
|
352
|
+
* Values to assign to the directive's scope.
|
|
353
|
+
*
|
|
354
|
+
* @remarks
|
|
355
|
+
* Requires `scope: true`. Assigns the provided values to the
|
|
356
|
+
* directive's scope, making them available in expressions.
|
|
357
|
+
*
|
|
358
|
+
* Useful for injecting external values (like styles) that should
|
|
359
|
+
* be accessible in templates without manual `$scope` assignment.
|
|
360
|
+
*
|
|
361
|
+
* @example
|
|
362
|
+
* ```ts
|
|
363
|
+
* import styles from './button.css';
|
|
364
|
+
*
|
|
365
|
+
* directive('my-button', handler, {
|
|
366
|
+
* scope: true,
|
|
367
|
+
* assign: { $styles: styles }
|
|
368
|
+
* });
|
|
369
|
+
*
|
|
370
|
+
* // In template:
|
|
371
|
+
* // <div g-class="$styles.container">...</div>
|
|
372
|
+
* ```
|
|
373
|
+
*/
|
|
374
|
+
assign?: Record<string, unknown>;
|
|
375
|
+
/**
|
|
376
|
+
* Index signature for custom options.
|
|
377
|
+
*
|
|
378
|
+
* @remarks
|
|
379
|
+
* Allows libraries to pass additional options that gonia
|
|
380
|
+
* doesn't process directly. Libraries can create typed
|
|
381
|
+
* wrapper functions around `directive()` for type safety.
|
|
382
|
+
*/
|
|
383
|
+
[key: string]: unknown;
|
|
300
384
|
}
|
|
301
385
|
/** Registered directive with options */
|
|
302
386
|
export interface DirectiveRegistration {
|
|
@@ -321,8 +405,8 @@ export interface DirectiveRegistration {
|
|
|
321
405
|
* @example
|
|
322
406
|
* ```ts
|
|
323
407
|
* // Directive with behavior
|
|
324
|
-
* directive('todo-app', ($element, $
|
|
325
|
-
* $
|
|
408
|
+
* directive('todo-app', ($element, $scope) => {
|
|
409
|
+
* $scope.todos = [];
|
|
326
410
|
* }, { scope: true });
|
|
327
411
|
*
|
|
328
412
|
* // Template-only directive
|
|
@@ -359,4 +443,30 @@ export declare function getDirectiveNames(): string[];
|
|
|
359
443
|
* Primarily useful for testing.
|
|
360
444
|
*/
|
|
361
445
|
export declare function clearDirectives(): void;
|
|
446
|
+
/**
|
|
447
|
+
* Configure options for an existing directive.
|
|
448
|
+
*
|
|
449
|
+
* @remarks
|
|
450
|
+
* Merges the provided options with any existing options for the directive.
|
|
451
|
+
* If the directive hasn't been registered yet, stores the options to be
|
|
452
|
+
* applied when it is registered.
|
|
453
|
+
*
|
|
454
|
+
* This is useful for configuring built-in or third-party directives
|
|
455
|
+
* without needing access to the directive function.
|
|
456
|
+
*
|
|
457
|
+
* @param name - The directive name
|
|
458
|
+
* @param options - Options to merge
|
|
459
|
+
*
|
|
460
|
+
* @example
|
|
461
|
+
* ```ts
|
|
462
|
+
* // Add scope to a built-in directive
|
|
463
|
+
* configureDirective('g-text', { scope: true });
|
|
464
|
+
*
|
|
465
|
+
* // Add template to a custom element
|
|
466
|
+
* configureDirective('app-header', {
|
|
467
|
+
* template: '<header><slot></slot></header>'
|
|
468
|
+
* });
|
|
469
|
+
* ```
|
|
470
|
+
*/
|
|
471
|
+
export declare function configureDirective(name: string, options: Partial<DirectiveOptions>): void;
|
|
362
472
|
export {};
|
package/dist/types.js
CHANGED
|
@@ -49,8 +49,8 @@ const directiveRegistry = new Map();
|
|
|
49
49
|
* @example
|
|
50
50
|
* ```ts
|
|
51
51
|
* // Directive with behavior
|
|
52
|
-
* directive('todo-app', ($element, $
|
|
53
|
-
* $
|
|
52
|
+
* directive('todo-app', ($element, $scope) => {
|
|
53
|
+
* $scope.todos = [];
|
|
54
54
|
* }, { scope: true });
|
|
55
55
|
*
|
|
56
56
|
* // Template-only directive
|
|
@@ -61,6 +61,11 @@ const directiveRegistry = new Map();
|
|
|
61
61
|
*/
|
|
62
62
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
63
63
|
export function directive(name, fn, options = {}) {
|
|
64
|
+
// Validate: assign requires scope: true
|
|
65
|
+
if (options.assign && !options.scope) {
|
|
66
|
+
throw new Error(`Directive '${name}': 'assign' requires 'scope: true'. ` +
|
|
67
|
+
`To modify parent scope, use $scope in your directive function.`);
|
|
68
|
+
}
|
|
64
69
|
directiveRegistry.set(name, { fn, options });
|
|
65
70
|
// Register as custom element if name contains hyphen and scope is true
|
|
66
71
|
if (fn && name.includes('-') && options.scope && typeof customElements !== 'undefined') {
|
|
@@ -108,3 +113,44 @@ export function getDirectiveNames() {
|
|
|
108
113
|
export function clearDirectives() {
|
|
109
114
|
directiveRegistry.clear();
|
|
110
115
|
}
|
|
116
|
+
/**
|
|
117
|
+
* Configure options for an existing directive.
|
|
118
|
+
*
|
|
119
|
+
* @remarks
|
|
120
|
+
* Merges the provided options with any existing options for the directive.
|
|
121
|
+
* If the directive hasn't been registered yet, stores the options to be
|
|
122
|
+
* applied when it is registered.
|
|
123
|
+
*
|
|
124
|
+
* This is useful for configuring built-in or third-party directives
|
|
125
|
+
* without needing access to the directive function.
|
|
126
|
+
*
|
|
127
|
+
* @param name - The directive name
|
|
128
|
+
* @param options - Options to merge
|
|
129
|
+
*
|
|
130
|
+
* @example
|
|
131
|
+
* ```ts
|
|
132
|
+
* // Add scope to a built-in directive
|
|
133
|
+
* configureDirective('g-text', { scope: true });
|
|
134
|
+
*
|
|
135
|
+
* // Add template to a custom element
|
|
136
|
+
* configureDirective('app-header', {
|
|
137
|
+
* template: '<header><slot></slot></header>'
|
|
138
|
+
* });
|
|
139
|
+
* ```
|
|
140
|
+
*/
|
|
141
|
+
export function configureDirective(name, options) {
|
|
142
|
+
const existing = directiveRegistry.get(name);
|
|
143
|
+
if (existing) {
|
|
144
|
+
directiveRegistry.set(name, {
|
|
145
|
+
fn: existing.fn,
|
|
146
|
+
options: { ...existing.options, ...options }
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
else {
|
|
150
|
+
// Store options for later - directive will be registered soon
|
|
151
|
+
directiveRegistry.set(name, {
|
|
152
|
+
fn: null,
|
|
153
|
+
options: options
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
}
|
package/dist/vite/plugin.d.ts
CHANGED
|
@@ -10,6 +10,14 @@
|
|
|
10
10
|
* @packageDocumentation
|
|
11
11
|
*/
|
|
12
12
|
import type { Plugin } from 'vite';
|
|
13
|
+
/**
|
|
14
|
+
* Options for directive registration.
|
|
15
|
+
*/
|
|
16
|
+
export interface DirectiveOptionsConfig {
|
|
17
|
+
scope?: boolean;
|
|
18
|
+
template?: string | ((attrs: Record<string, string>) => string | Promise<string>);
|
|
19
|
+
provide?: Record<string, unknown>;
|
|
20
|
+
}
|
|
13
21
|
/**
|
|
14
22
|
* Plugin options.
|
|
15
23
|
*/
|
|
@@ -48,6 +56,25 @@ export interface GoniaPluginOptions {
|
|
|
48
56
|
* @example ['app-', 'my-', 'ui-']
|
|
49
57
|
*/
|
|
50
58
|
directiveElementPrefixes?: string[];
|
|
59
|
+
/**
|
|
60
|
+
* Options to apply to directives.
|
|
61
|
+
*
|
|
62
|
+
* Can be an object mapping directive names to options, or a function
|
|
63
|
+
* that receives the directive name and returns options.
|
|
64
|
+
*
|
|
65
|
+
* @example
|
|
66
|
+
* ```ts
|
|
67
|
+
* // Object form - explicit per-directive
|
|
68
|
+
* directiveOptions: {
|
|
69
|
+
* 'g-text': { scope: true },
|
|
70
|
+
* 'app-card': { template: '<div class="card"><slot></slot></div>' }
|
|
71
|
+
* }
|
|
72
|
+
*
|
|
73
|
+
* // Function form - dynamic/pattern-based
|
|
74
|
+
* directiveOptions: (name) => name.startsWith('app-') ? { scope: true } : undefined
|
|
75
|
+
* ```
|
|
76
|
+
*/
|
|
77
|
+
directiveOptions?: Record<string, DirectiveOptionsConfig> | ((name: string) => DirectiveOptionsConfig | undefined);
|
|
51
78
|
}
|
|
52
79
|
/**
|
|
53
80
|
* Gonia Vite plugin.
|
package/dist/vite/plugin.js
CHANGED
|
@@ -113,15 +113,56 @@ function detectDirectives(code, id, isDev, attributePrefixes, elementPrefixes, c
|
|
|
113
113
|
}
|
|
114
114
|
return found;
|
|
115
115
|
}
|
|
116
|
+
/**
|
|
117
|
+
* Get options for a directive from the directiveOptions config.
|
|
118
|
+
*/
|
|
119
|
+
function getDirectiveOptions(name, directiveOptions) {
|
|
120
|
+
if (!directiveOptions)
|
|
121
|
+
return undefined;
|
|
122
|
+
if (typeof directiveOptions === 'function') {
|
|
123
|
+
return directiveOptions(name);
|
|
124
|
+
}
|
|
125
|
+
return directiveOptions[name];
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Serialize directive options to a string for code generation.
|
|
129
|
+
*/
|
|
130
|
+
function serializeOptions(options) {
|
|
131
|
+
const parts = [];
|
|
132
|
+
if (options.scope !== undefined) {
|
|
133
|
+
parts.push(`scope: ${options.scope}`);
|
|
134
|
+
}
|
|
135
|
+
if (options.template !== undefined) {
|
|
136
|
+
if (typeof options.template === 'string') {
|
|
137
|
+
parts.push(`template: ${JSON.stringify(options.template)}`);
|
|
138
|
+
}
|
|
139
|
+
else {
|
|
140
|
+
// Function templates can't be serialized - warn and skip
|
|
141
|
+
console.warn('[gonia] Function templates in directiveOptions are not supported. Use a string template.');
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
if (options.provide !== undefined) {
|
|
145
|
+
// provide can contain functions which can't be serialized
|
|
146
|
+
console.warn('[gonia] "provide" in directiveOptions is not supported via plugin config.');
|
|
147
|
+
}
|
|
148
|
+
return `{ ${parts.join(', ')} }`;
|
|
149
|
+
}
|
|
116
150
|
/**
|
|
117
151
|
* Generate import statements for detected directives.
|
|
118
152
|
*/
|
|
119
|
-
function generateImports(directives, customDirectives, currentFile, rootDir) {
|
|
153
|
+
function generateImports(directives, customDirectives, currentFile, rootDir, directiveOptions) {
|
|
120
154
|
if (directives.size === 0)
|
|
121
155
|
return '';
|
|
122
156
|
// Group by module
|
|
123
157
|
const moduleImports = new Map();
|
|
158
|
+
// Track which directives need configuration
|
|
159
|
+
const directivesToConfigure = [];
|
|
124
160
|
for (const name of directives) {
|
|
161
|
+
// Check if this directive has options
|
|
162
|
+
const options = getDirectiveOptions(name, directiveOptions);
|
|
163
|
+
if (options) {
|
|
164
|
+
directivesToConfigure.push({ name, options });
|
|
165
|
+
}
|
|
125
166
|
// Check built-in first
|
|
126
167
|
const builtin = BUILTIN_DIRECTIVES[name];
|
|
127
168
|
if (builtin) {
|
|
@@ -152,6 +193,14 @@ function generateImports(directives, customDirectives, currentFile, rootDir) {
|
|
|
152
193
|
}
|
|
153
194
|
// Generate import statements
|
|
154
195
|
const statements = [];
|
|
196
|
+
// Add configureDirective import if we have options to apply
|
|
197
|
+
if (directivesToConfigure.length > 0) {
|
|
198
|
+
const goniaImports = moduleImports.get('gonia') ?? [];
|
|
199
|
+
if (!goniaImports.includes('configureDirective')) {
|
|
200
|
+
goniaImports.push('configureDirective');
|
|
201
|
+
}
|
|
202
|
+
moduleImports.set('gonia', goniaImports);
|
|
203
|
+
}
|
|
155
204
|
for (const [module, imports] of moduleImports) {
|
|
156
205
|
if (imports.length > 0) {
|
|
157
206
|
statements.push(`import { ${imports.join(', ')} } from '${module}';`);
|
|
@@ -161,6 +210,10 @@ function generateImports(directives, customDirectives, currentFile, rootDir) {
|
|
|
161
210
|
statements.push(`import '${module}';`);
|
|
162
211
|
}
|
|
163
212
|
}
|
|
213
|
+
// Add configureDirective calls
|
|
214
|
+
for (const { name, options } of directivesToConfigure) {
|
|
215
|
+
statements.push(`configureDirective('${name}', ${serializeOptions(options)});`);
|
|
216
|
+
}
|
|
164
217
|
return statements.length > 0 ? statements.join('\n') + '\n' : '';
|
|
165
218
|
}
|
|
166
219
|
/**
|
|
@@ -239,7 +292,7 @@ function transformInject(code) {
|
|
|
239
292
|
* ```
|
|
240
293
|
*/
|
|
241
294
|
export function gonia(options = {}) {
|
|
242
|
-
const { autoDirectives = true, includeDirectives = [], excludeDirectives = [], directiveSources = [], directiveAttributePrefixes = ['g-'], directiveElementPrefixes = options.directiveElementPrefixes ?? options.directiveAttributePrefixes ?? ['g-'], } = options;
|
|
295
|
+
const { autoDirectives = true, includeDirectives = [], excludeDirectives = [], directiveSources = [], directiveAttributePrefixes = ['g-'], directiveElementPrefixes = options.directiveElementPrefixes ?? options.directiveAttributePrefixes ?? ['g-'], directiveOptions, } = options;
|
|
243
296
|
let isDev = false;
|
|
244
297
|
let rootDir = process.cwd();
|
|
245
298
|
// Map of custom directive name -> DirectiveInfo
|
|
@@ -303,7 +356,7 @@ export function gonia(options = {}) {
|
|
|
303
356
|
const hasGoniaImport = code.includes("from 'gonia/directives'") ||
|
|
304
357
|
code.includes('from "gonia/directives"');
|
|
305
358
|
// For custom directives, check if already imported
|
|
306
|
-
const importStatement = generateImports(detected, customDirectives, id, rootDir);
|
|
359
|
+
const importStatement = generateImports(detected, customDirectives, id, rootDir, directiveOptions);
|
|
307
360
|
if (importStatement && !hasGoniaImport) {
|
|
308
361
|
result = importStatement + result;
|
|
309
362
|
modified = true;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gonia",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "A lightweight, SSR-first reactive UI library with declarative directives",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -36,6 +36,10 @@
|
|
|
36
36
|
"types": "./dist/server/index.d.ts",
|
|
37
37
|
"import": "./dist/server/index.js"
|
|
38
38
|
},
|
|
39
|
+
"./directives": {
|
|
40
|
+
"types": "./dist/directives/index.d.ts",
|
|
41
|
+
"import": "./dist/directives/index.js"
|
|
42
|
+
},
|
|
39
43
|
"./vite": {
|
|
40
44
|
"types": "./dist/vite/index.d.ts",
|
|
41
45
|
"import": "./dist/vite/index.js"
|
|
@@ -56,6 +60,7 @@
|
|
|
56
60
|
"devDependencies": {
|
|
57
61
|
"@types/node": "^25.0.10",
|
|
58
62
|
"@vitest/coverage-v8": "^4.0.17",
|
|
63
|
+
"conventional-changelog-cli": "^5.0.0",
|
|
59
64
|
"jsdom": "^27.4.0",
|
|
60
65
|
"typescript": "^5.7.0",
|
|
61
66
|
"vite": "^6.4.0",
|
|
@@ -64,6 +69,8 @@
|
|
|
64
69
|
"scripts": {
|
|
65
70
|
"build": "tsc",
|
|
66
71
|
"test": "vitest run",
|
|
67
|
-
"test:watch": "vitest"
|
|
72
|
+
"test:watch": "vitest",
|
|
73
|
+
"changelog": "conventional-changelog -p angular -i CHANGELOG.md -s",
|
|
74
|
+
"changelog:all": "conventional-changelog -p angular -i CHANGELOG.md -s -r 0"
|
|
68
75
|
}
|
|
69
76
|
}
|