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.
@@ -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
- * Resolve dependencies for a directive based on its $inject array.
73
+ * Create resolver config for server-side dependency resolution.
57
74
  *
58
75
  * @internal
59
76
  */
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
- });
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 args = resolveDependencies(item.directive, item.expr, item.el, ctx, state);
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
- $state: Record<string, unknown>;
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
- * Maps a tuple of injectable names to their corresponding types.
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 name strings
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', '$state']>;
75
+ * type Args = MapInjectables<['$element', '$scope']>;
70
76
  * // => [Element, Record<string, unknown>]
71
77
  *
72
- * // With custom type map
73
- * type Args2 = MapInjectables<['$element', 'custom'], { $element: Element; custom: MyType }>;
74
- * // => [Element, MyType]
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
- * - `$state`: Local reactive state object (isolated per element)
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 string[];
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 `$state` becomes
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 = ($state) => {
165
- * $state.mode = 'dark';
181
+ * const themeProvider: Directive = ($scope) => {
182
+ * $scope.mode = 'dark';
166
183
  * };
167
- * themeProvider.$inject = ['$state'];
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 & keyof (InjectableRegistry & T))[] = readonly (string & keyof InjectableRegistry)[], T extends Record<string, unknown> = {}> = ((...args: MapInjectables<K, InjectableRegistry & T>) => void | Promise<void>) & DirectiveMeta<InjectableRegistry & T>;
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, $state) => {
325
- * $state.todos = [];
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, $state) => {
53
- * $state.todos = [];
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
+ }
@@ -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.
@@ -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.1.2",
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
  }