gonia 0.1.0 → 0.1.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.
@@ -26,7 +26,7 @@ export declare function setElementContext(el: Element, ctx: Context): void;
26
26
  * this directive and processes them immediately.
27
27
  *
28
28
  * @param registry - The directive registry
29
- * @param name - Directive name (without c- prefix)
29
+ * @param name - Directive name (without g- prefix)
30
30
  * @param fn - The directive function
31
31
  */
32
32
  export declare function registerDirective(registry: DirectiveRegistry, name: string, fn: Directive): void;
@@ -252,7 +252,7 @@ function processNode(node, selector, registry) {
252
252
  * this directive and processes them immediately.
253
253
  *
254
254
  * @param registry - The directive registry
255
- * @param name - Directive name (without c- prefix)
255
+ * @param name - Directive name (without g- prefix)
256
256
  * @param fn - The directive function
257
257
  */
258
258
  export function registerDirective(registry, name, fn) {
@@ -56,14 +56,12 @@ export const slot = function slot($expr, $element, $eval) {
56
56
  return;
57
57
  }
58
58
  const content = getSavedContent(templateEl);
59
+ if (!content) {
60
+ return;
61
+ }
59
62
  const slotContent = content.slots.get(name);
60
63
  if (slotContent) {
61
64
  $element.innerHTML = slotContent;
62
- // MutationObserver will process the new content
63
- }
64
- else {
65
- // No content for this slot - could show fallback
66
- // For now, leave any default content in the slot
67
65
  }
68
66
  };
69
67
  // If expression is provided, make it reactive
@@ -92,6 +90,9 @@ export function processNativeSlot(el) {
92
90
  return;
93
91
  }
94
92
  const content = getSavedContent(templateEl);
93
+ if (!content) {
94
+ return;
95
+ }
95
96
  const slotContent = content.slots.get(name);
96
97
  if (slotContent) {
97
98
  el.outerHTML = slotContent;
@@ -52,4 +52,4 @@ export declare const template: Directive<['$expr', '$element', '$templates']>;
52
52
  *
53
53
  * @internal
54
54
  */
55
- export declare function getElementScope(el: Element): EffectScope | undefined;
55
+ export declare function getEffectScope(el: Element): EffectScope | undefined;
@@ -142,6 +142,6 @@ directive('g-template', template);
142
142
  *
143
143
  * @internal
144
144
  */
145
- export function getElementScope(el) {
145
+ export function getEffectScope(el) {
146
146
  return elementScopes.get(el);
147
147
  }
@@ -41,7 +41,7 @@ export interface Segment {
41
41
  *
42
42
  * @remarks
43
43
  * Splits a template string with `{{ expr }}` markers into segments
44
- * of static text and expressions. Used by directives like c-href
44
+ * of static text and expressions. Useful for directives
45
45
  * that support inline interpolation.
46
46
  *
47
47
  * @param template - The template string with interpolation markers
@@ -52,7 +52,7 @@ export function findRoots(expr) {
52
52
  *
53
53
  * @remarks
54
54
  * Splits a template string with `{{ expr }}` markers into segments
55
- * of static text and expressions. Used by directives like c-href
55
+ * of static text and expressions. Useful for directives
56
56
  * that support inline interpolation.
57
57
  *
58
58
  * @param template - The template string with interpolation markers
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Cubist.js - A modern framework inspired by AngularJS 1.
2
+ * Gonia - A lightweight, SSR-first reactive UI library.
3
3
  *
4
4
  * @remarks
5
5
  * SSR-first design with HTML attributes as directives.
@@ -16,4 +16,5 @@ export { createTemplateRegistry, createMemoryRegistry, createServerRegistry } fr
16
16
  export type { TemplateRegistry } from './templates.js';
17
17
  export { findRoots, parseInterpolation } from './expression.js';
18
18
  export { getInjectables } from './inject.js';
19
+ export { getRootScope, clearRootScope } from './scope.js';
19
20
  export * as directives from './directives/index.js';
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Cubist.js - A modern framework inspired by AngularJS 1.
2
+ * Gonia - A lightweight, SSR-first reactive UI library.
3
3
  *
4
4
  * @remarks
5
5
  * SSR-first design with HTML attributes as directives.
@@ -13,4 +13,5 @@ export { reactive, effect, createScope, createEffectScope } from './reactivity.j
13
13
  export { createTemplateRegistry, createMemoryRegistry, createServerRegistry } from './templates.js';
14
14
  export { findRoots, parseInterpolation } from './expression.js';
15
15
  export { getInjectables } from './inject.js';
16
+ export { getRootScope, clearRootScope } from './scope.js';
16
17
  export * as directives from './directives/index.js';
package/dist/scope.d.ts CHANGED
@@ -4,6 +4,24 @@
4
4
  * @packageDocumentation
5
5
  */
6
6
  import { Directive, DirectiveOptions } from './types.js';
7
+ /**
8
+ * Get or create the root scope.
9
+ *
10
+ * @remarks
11
+ * The root scope is used as a fallback for directives that aren't
12
+ * inside an element with `scope: true`. This allows top-level
13
+ * directives like `g-model` to work without requiring a parent scope.
14
+ *
15
+ * @returns The root reactive scope
16
+ */
17
+ export declare function getRootScope(): Record<string, unknown>;
18
+ /**
19
+ * Clear the root scope.
20
+ *
21
+ * @remarks
22
+ * Primarily useful for testing.
23
+ */
24
+ export declare function clearRootScope(): void;
7
25
  /**
8
26
  * Create a new scope for an element.
9
27
  *
@@ -24,9 +42,10 @@ export declare function getElementScope(el: Element): Record<string, unknown> |
24
42
  *
25
43
  * @param el - The element to start from
26
44
  * @param includeSelf - Whether to check the element itself (default: false)
27
- * @returns The nearest scope, or undefined if none found
45
+ * @param useRootFallback - Whether to return root scope if no parent found (default: true)
46
+ * @returns The nearest scope, or root scope if none found and fallback enabled
28
47
  */
29
- export declare function findParentScope(el: Element, includeSelf?: boolean): Record<string, unknown> | undefined;
48
+ export declare function findParentScope(el: Element, includeSelf?: boolean, useRootFallback?: boolean): Record<string, unknown> | undefined;
30
49
  /**
31
50
  * Remove scope for an element (cleanup).
32
51
  *
package/dist/scope.js CHANGED
@@ -9,6 +9,33 @@ import { Mode } from './types.js';
9
9
  import { getInjectables } from './inject.js';
10
10
  /** WeakMap to store element scopes */
11
11
  const elementScopes = new WeakMap();
12
+ /** Root scope for top-level directives without explicit parent scope */
13
+ let rootScope = null;
14
+ /**
15
+ * Get or create the root scope.
16
+ *
17
+ * @remarks
18
+ * The root scope is used as a fallback for directives that aren't
19
+ * inside an element with `scope: true`. This allows top-level
20
+ * directives like `g-model` to work without requiring a parent scope.
21
+ *
22
+ * @returns The root reactive scope
23
+ */
24
+ export function getRootScope() {
25
+ if (!rootScope) {
26
+ rootScope = reactive({});
27
+ }
28
+ return rootScope;
29
+ }
30
+ /**
31
+ * Clear the root scope.
32
+ *
33
+ * @remarks
34
+ * Primarily useful for testing.
35
+ */
36
+ export function clearRootScope() {
37
+ rootScope = null;
38
+ }
12
39
  /**
13
40
  * Create a new scope for an element.
14
41
  *
@@ -42,9 +69,10 @@ export function getElementScope(el) {
42
69
  *
43
70
  * @param el - The element to start from
44
71
  * @param includeSelf - Whether to check the element itself (default: false)
45
- * @returns The nearest scope, or undefined if none found
72
+ * @param useRootFallback - Whether to return root scope if no parent found (default: true)
73
+ * @returns The nearest scope, or root scope if none found and fallback enabled
46
74
  */
47
- export function findParentScope(el, includeSelf = false) {
75
+ export function findParentScope(el, includeSelf = false, useRootFallback = true) {
48
76
  let current = includeSelf ? el : el.parentElement;
49
77
  while (current) {
50
78
  const scope = elementScopes.get(current);
@@ -53,7 +81,8 @@ export function findParentScope(el, includeSelf = false) {
53
81
  }
54
82
  current = current.parentElement;
55
83
  }
56
- return undefined;
84
+ // Fall back to root scope for top-level directives
85
+ return useRootFallback ? getRootScope() : undefined;
57
86
  }
58
87
  /**
59
88
  * Remove scope for an element (cleanup).
@@ -19,7 +19,7 @@ export type ServiceRegistry = Map<string, unknown>;
19
19
  * Invalidates the cached selector so it will be rebuilt on next render.
20
20
  *
21
21
  * @param registry - The directive registry
22
- * @param name - Directive name (without c- prefix)
22
+ * @param name - Directive name (without g- prefix)
23
23
  * @param fn - The directive function
24
24
  */
25
25
  export declare function registerDirective(registry: DirectiveRegistry, name: string, fn: Directive): void;
@@ -36,7 +36,7 @@ function getSelector(registry) {
36
36
  * Invalidates the cached selector so it will be rebuilt on next render.
37
37
  *
38
38
  * @param registry - The directive registry
39
- * @param name - Directive name (without c- prefix)
39
+ * @param name - Directive name (without g- prefix)
40
40
  * @param fn - The directive function
41
41
  */
42
42
  export function registerDirective(registry, name, fn) {
@@ -3,6 +3,7 @@
3
3
  *
4
4
  * @remarks
5
5
  * - Auto-detects directive usage in templates and injects imports
6
+ * - Scans custom directive sources to build import map
6
7
  * - Transforms directive functions to add $inject arrays at build time
7
8
  * - Configures Vite for SSR with gonia.js
8
9
  *
@@ -20,21 +21,40 @@ export interface GoniaPluginOptions {
20
21
  autoDirectives?: boolean;
21
22
  /**
22
23
  * Additional directives to include (for runtime/dynamic cases).
23
- * Use directive names without the 'g-' prefix.
24
- * @example ['text', 'for', 'if']
24
+ * Use full directive names (e.g., 'g-text', 'my-component').
25
+ * @example ['g-text', 'g-for', 'app-header']
25
26
  */
26
27
  includeDirectives?: string[];
27
28
  /**
28
29
  * Directives to exclude from auto-detection.
29
- * Use directive names without the 'g-' prefix.
30
+ * Use full directive names.
30
31
  */
31
32
  excludeDirectives?: string[];
33
+ /**
34
+ * Glob patterns for files containing custom directive definitions.
35
+ * The plugin scans these files at build start to discover directive() calls.
36
+ * @example ['src/directives/**\/*.ts']
37
+ */
38
+ directiveSources?: string[];
39
+ /**
40
+ * Prefixes for attribute directives.
41
+ * @defaultValue ['g-']
42
+ * @example ['g-', 'v-', 'x-']
43
+ */
44
+ directiveAttributePrefixes?: string[];
45
+ /**
46
+ * Prefixes for element/component directives.
47
+ * Defaults to directiveAttributePrefixes if not specified.
48
+ * @example ['app-', 'my-', 'ui-']
49
+ */
50
+ directiveElementPrefixes?: string[];
32
51
  }
33
52
  /**
34
53
  * Gonia Vite plugin.
35
54
  *
36
55
  * @remarks
37
56
  * - Auto-detects directive usage and injects imports
57
+ * - Scans custom directive sources to discover user directives
38
58
  * - Adds $inject arrays to directive functions for minification safety
39
59
  * - Configures Vite for SSR with gonia.js
40
60
  *
@@ -51,12 +71,12 @@ export interface GoniaPluginOptions {
51
71
  *
52
72
  * @example
53
73
  * ```ts
54
- * // With options
74
+ * // With custom directives
55
75
  * export default defineConfig({
56
76
  * plugins: [gonia({
57
- * autoDirectives: true,
58
- * includeDirectives: ['text', 'for'], // For dynamic/runtime HTML
59
- * excludeDirectives: ['model'], // Never include these
77
+ * directiveSources: ['src/directives/**\/*.ts'],
78
+ * directiveAttributePrefixes: ['g-'],
79
+ * directiveElementPrefixes: ['app-', 'ui-'],
60
80
  * })]
61
81
  * });
62
82
  * ```
@@ -3,90 +3,170 @@
3
3
  *
4
4
  * @remarks
5
5
  * - Auto-detects directive usage in templates and injects imports
6
+ * - Scans custom directive sources to build import map
6
7
  * - Transforms directive functions to add $inject arrays at build time
7
8
  * - Configures Vite for SSR with gonia.js
8
9
  *
9
10
  * @packageDocumentation
10
11
  */
12
+ import { readFileSync } from 'fs';
13
+ import { glob } from 'tinyglobby';
14
+ import { relative } from 'path';
11
15
  /**
12
- * Map of directive names to their export names from gonia.
16
+ * Map of built-in directive names to their export names from gonia.
13
17
  */
14
- const DIRECTIVE_MAP = {
15
- text: 'text',
16
- html: 'html',
17
- show: 'show',
18
- template: 'template',
19
- slot: 'slot',
20
- class: 'cclass',
21
- model: 'model',
22
- on: 'on',
23
- for: 'cfor',
24
- if: 'cif',
18
+ const BUILTIN_DIRECTIVES = {
19
+ 'g-text': { exportName: 'text', module: 'gonia/directives' },
20
+ 'g-html': { exportName: 'html', module: 'gonia/directives' },
21
+ 'g-show': { exportName: 'show', module: 'gonia/directives' },
22
+ 'g-template': { exportName: 'template', module: 'gonia/directives' },
23
+ 'g-slot': { exportName: 'slot', module: 'gonia/directives' },
24
+ 'g-class': { exportName: 'cclass', module: 'gonia/directives' },
25
+ 'g-model': { exportName: 'model', module: 'gonia/directives' },
26
+ 'g-on': { exportName: 'on', module: 'gonia/directives' },
27
+ 'g-for': { exportName: 'cfor', module: 'gonia/directives' },
28
+ 'g-if': { exportName: 'cif', module: 'gonia/directives' },
25
29
  };
30
+ /**
31
+ * Scan a file for directive() calls and extract directive names.
32
+ */
33
+ function scanFileForDirectives(filePath) {
34
+ const directives = new Map();
35
+ try {
36
+ const content = readFileSync(filePath, 'utf-8');
37
+ // Match directive('name', ...) or directive("name", ...)
38
+ const pattern = /directive\s*\(\s*(['"`])([^'"`]+)\1/g;
39
+ let match;
40
+ while ((match = pattern.exec(content)) !== null) {
41
+ const name = match[2];
42
+ directives.set(name, {
43
+ name,
44
+ exportName: null,
45
+ module: filePath,
46
+ isBuiltin: false,
47
+ });
48
+ }
49
+ }
50
+ catch {
51
+ // File read error - skip silently
52
+ }
53
+ return directives;
54
+ }
55
+ /**
56
+ * Build regex pattern for matching directive attributes.
57
+ */
58
+ function buildAttributePattern(prefixes) {
59
+ const escaped = prefixes.map(p => p.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'));
60
+ return new RegExp(`(?:${escaped.join('|')})([a-zA-Z][a-zA-Z0-9-]*)`, 'g');
61
+ }
62
+ /**
63
+ * Build regex pattern for matching directive elements (custom elements).
64
+ */
65
+ function buildElementPattern(prefixes) {
66
+ const escaped = prefixes.map(p => p.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'));
67
+ return new RegExp(`<((?:${escaped.join('|')})[a-zA-Z][a-zA-Z0-9-]*)`, 'g');
68
+ }
26
69
  /**
27
70
  * Detect directive names used in source code.
28
71
  */
29
- function detectDirectives(code, id, isDev) {
72
+ function detectDirectives(code, id, isDev, attributePrefixes, elementPrefixes, customDirectives) {
30
73
  const found = new Set();
31
- // Pattern 1: g-name as attribute in template literals or strings
32
- // Matches: g-text, g-for, g-if, etc.
33
- const attrPattern = /g-([a-z]+)/g;
74
+ // Pattern for attribute directives (e.g., g-text, v-if)
75
+ const attrPattern = buildAttributePattern(attributePrefixes);
34
76
  let match;
35
77
  while ((match = attrPattern.exec(code)) !== null) {
36
- const name = match[1];
37
- if (DIRECTIVE_MAP[name]) {
38
- found.add(name);
39
- }
78
+ // Reconstruct full attribute name
79
+ const fullMatch = match[0];
80
+ found.add(fullMatch);
40
81
  }
41
- // Pattern 2: Dynamic directive names we can resolve
42
- // Matches: `g-${expr}` where expr is a string literal or simple variable
43
- const dynamicPattern = /`g-\$\{([^}]+)\}`/g;
44
- while ((match = dynamicPattern.exec(code)) !== null) {
45
- const expr = match[1].trim();
46
- // Try to resolve simple string literals
47
- if (/^['"]([a-z]+)['"]$/.test(expr)) {
48
- const name = expr.slice(1, -1);
49
- if (DIRECTIVE_MAP[name]) {
50
- found.add(name);
51
- }
52
- }
53
- else if (/^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(expr)) {
54
- // It's a variable - try to find its value
55
- const varPattern = new RegExp(`(?:const|let|var)\\s+${expr}\\s*=\\s*['"]([a-z]+)['"]`);
56
- const varMatch = code.match(varPattern);
57
- if (varMatch && DIRECTIVE_MAP[varMatch[1]]) {
58
- found.add(varMatch[1]);
82
+ // Pattern for element directives (e.g., <app-header>, <my-component>)
83
+ const elemPattern = buildElementPattern(elementPrefixes);
84
+ while ((match = elemPattern.exec(code)) !== null) {
85
+ const elementName = match[1];
86
+ found.add(elementName);
87
+ }
88
+ // Handle dynamic directive names we can resolve
89
+ for (const prefix of attributePrefixes) {
90
+ const escaped = prefix.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
91
+ const dynamicPattern = new RegExp(`\`${escaped}\\$\\{([^}]+)\\}\``, 'g');
92
+ while ((match = dynamicPattern.exec(code)) !== null) {
93
+ const expr = match[1].trim();
94
+ // Try to resolve simple string literals
95
+ const literalMatch = expr.match(/^['"]([a-zA-Z][a-zA-Z0-9-]*)['"]$/);
96
+ if (literalMatch) {
97
+ found.add(prefix + literalMatch[1]);
98
+ continue;
59
99
  }
60
- else if (isDev) {
61
- console.warn(`[gonia] Could not resolve directive name in \`g-\${${expr}}\` at ${id}\n` +
62
- ` Add to vite config: includeDirectives: ['expected-directive']`);
100
+ // Try to resolve variable
101
+ if (/^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(expr)) {
102
+ const varPattern = new RegExp(`(?:const|let|var)\\s+${expr}\\s*=\\s*['"]([a-zA-Z][a-zA-Z0-9-]*)['"]`);
103
+ const varMatch = code.match(varPattern);
104
+ if (varMatch) {
105
+ found.add(prefix + varMatch[1]);
106
+ }
107
+ else if (isDev) {
108
+ console.warn(`[gonia] Could not resolve directive name in \`${prefix}\${${expr}}\` at ${id}\n` +
109
+ ` Add to includeDirectives: ['${prefix}expected-name']`);
110
+ }
63
111
  }
64
112
  }
65
113
  }
66
114
  return found;
67
115
  }
68
116
  /**
69
- * Generate import statement for detected directives.
117
+ * Generate import statements for detected directives.
70
118
  */
71
- function generateDirectiveImports(directives) {
119
+ function generateImports(directives, customDirectives, currentFile, rootDir) {
72
120
  if (directives.size === 0)
73
121
  return '';
74
- const imports = [];
122
+ // Group by module
123
+ const moduleImports = new Map();
75
124
  for (const name of directives) {
76
- const exportName = DIRECTIVE_MAP[name];
77
- if (exportName) {
78
- imports.push(exportName);
125
+ // Check built-in first
126
+ const builtin = BUILTIN_DIRECTIVES[name];
127
+ if (builtin) {
128
+ const imports = moduleImports.get(builtin.module) ?? [];
129
+ if (!imports.includes(builtin.exportName)) {
130
+ imports.push(builtin.exportName);
131
+ }
132
+ moduleImports.set(builtin.module, imports);
133
+ continue;
134
+ }
135
+ // Check custom directives
136
+ const custom = customDirectives.get(name);
137
+ if (custom) {
138
+ // For custom directives, we import the whole module (side effect)
139
+ // since the directive() call registers it
140
+ let modulePath = custom.module;
141
+ // Make path relative to current file
142
+ if (modulePath.startsWith('/') || modulePath.match(/^[a-zA-Z]:\\/)) {
143
+ const relPath = relative(currentFile.replace(/[/\\][^/\\]+$/, ''), modulePath);
144
+ modulePath = relPath.startsWith('.') ? relPath : './' + relPath;
145
+ // Normalize to forward slashes and remove .ts extension for import
146
+ modulePath = modulePath.replace(/\\/g, '/').replace(/\.tsx?$/, '.js');
147
+ }
148
+ // Side-effect import
149
+ const imports = moduleImports.get(modulePath) ?? [];
150
+ moduleImports.set(modulePath, imports);
79
151
  }
80
152
  }
81
- if (imports.length === 0)
82
- return '';
83
- // Import from gonia/directives which auto-registers
84
- return `import { ${imports.join(', ')} } from 'gonia/directives';\n`;
153
+ // Generate import statements
154
+ const statements = [];
155
+ for (const [module, imports] of moduleImports) {
156
+ if (imports.length > 0) {
157
+ statements.push(`import { ${imports.join(', ')} } from '${module}';`);
158
+ }
159
+ else {
160
+ // Side-effect import for custom directives
161
+ statements.push(`import '${module}';`);
162
+ }
163
+ }
164
+ return statements.length > 0 ? statements.join('\n') + '\n' : '';
85
165
  }
86
166
  /**
87
167
  * Transform source code to add $inject arrays to directive functions.
88
168
  */
89
- function transformInject(code, id) {
169
+ function transformInject(code) {
90
170
  if (!code.includes('directive(')) {
91
171
  return { code, modified: false };
92
172
  }
@@ -131,6 +211,7 @@ function transformInject(code, id) {
131
211
  *
132
212
  * @remarks
133
213
  * - Auto-detects directive usage and injects imports
214
+ * - Scans custom directive sources to discover user directives
134
215
  * - Adds $inject arrays to directive functions for minification safety
135
216
  * - Configures Vite for SSR with gonia.js
136
217
  *
@@ -147,26 +228,48 @@ function transformInject(code, id) {
147
228
  *
148
229
  * @example
149
230
  * ```ts
150
- * // With options
231
+ * // With custom directives
151
232
  * export default defineConfig({
152
233
  * plugins: [gonia({
153
- * autoDirectives: true,
154
- * includeDirectives: ['text', 'for'], // For dynamic/runtime HTML
155
- * excludeDirectives: ['model'], // Never include these
234
+ * directiveSources: ['src/directives/**\/*.ts'],
235
+ * directiveAttributePrefixes: ['g-'],
236
+ * directiveElementPrefixes: ['app-', 'ui-'],
156
237
  * })]
157
238
  * });
158
239
  * ```
159
240
  */
160
241
  export function gonia(options = {}) {
161
- const { autoDirectives = true, includeDirectives = [], excludeDirectives = [], } = options;
242
+ const { autoDirectives = true, includeDirectives = [], excludeDirectives = [], directiveSources = [], directiveAttributePrefixes = ['g-'], directiveElementPrefixes = options.directiveElementPrefixes ?? options.directiveAttributePrefixes ?? ['g-'], } = options;
162
243
  let isDev = false;
163
- // Track which directives have been injected per chunk
244
+ let rootDir = process.cwd();
245
+ // Map of custom directive name -> DirectiveInfo
246
+ const customDirectives = new Map();
247
+ // Track which modules have been processed
164
248
  const injectedModules = new Set();
165
249
  return {
166
250
  name: 'gonia',
167
251
  enforce: 'pre',
168
252
  configResolved(config) {
169
253
  isDev = config.command === 'serve';
254
+ rootDir = config.root;
255
+ },
256
+ async buildStart() {
257
+ // Scan directive sources to discover custom directives
258
+ if (directiveSources.length > 0) {
259
+ const files = await glob(directiveSources, {
260
+ cwd: rootDir,
261
+ absolute: true,
262
+ });
263
+ for (const file of files) {
264
+ const directives = scanFileForDirectives(file);
265
+ for (const [name, info] of directives) {
266
+ customDirectives.set(name, info);
267
+ }
268
+ }
269
+ if (isDev && customDirectives.size > 0) {
270
+ console.log(`[gonia] Discovered ${customDirectives.size} custom directive(s)`);
271
+ }
272
+ }
170
273
  },
171
274
  transform(code, id) {
172
275
  // Skip node_modules (except for $inject transform in gonia itself)
@@ -182,11 +285,11 @@ export function gonia(options = {}) {
182
285
  const detected = new Set();
183
286
  // Auto-detect directives if enabled
184
287
  if (autoDirectives) {
185
- for (const name of detectDirectives(code, id, isDev)) {
288
+ for (const name of detectDirectives(code, id, isDev, directiveAttributePrefixes, directiveElementPrefixes, customDirectives)) {
186
289
  detected.add(name);
187
290
  }
188
291
  }
189
- // Add explicitly included directives (always, regardless of autoDirectives)
292
+ // Add explicitly included directives
190
293
  for (const name of includeDirectives) {
191
294
  detected.add(name);
192
295
  }
@@ -197,18 +300,19 @@ export function gonia(options = {}) {
197
300
  // Generate imports if we found directives and haven't already
198
301
  if (detected.size > 0 && !injectedModules.has(id)) {
199
302
  // Check if this file already imports from gonia/directives
200
- if (!code.includes("from 'gonia/directives'") && !code.includes('from "gonia/directives"')) {
201
- const importStatement = generateDirectiveImports(detected);
202
- if (importStatement) {
203
- result = importStatement + result;
204
- modified = true;
205
- injectedModules.add(id);
206
- }
303
+ const hasGoniaImport = code.includes("from 'gonia/directives'") ||
304
+ code.includes('from "gonia/directives"');
305
+ // For custom directives, check if already imported
306
+ const importStatement = generateImports(detected, customDirectives, id, rootDir);
307
+ if (importStatement && !hasGoniaImport) {
308
+ result = importStatement + result;
309
+ modified = true;
310
+ injectedModules.add(id);
207
311
  }
208
312
  }
209
313
  }
210
314
  // Transform $inject arrays
211
- const injectResult = transformInject(result, id);
315
+ const injectResult = transformInject(result);
212
316
  if (injectResult.modified) {
213
317
  result = injectResult.code;
214
318
  modified = true;
@@ -216,7 +320,7 @@ export function gonia(options = {}) {
216
320
  if (modified) {
217
321
  return {
218
322
  code: result,
219
- map: null // TODO: proper source map support
323
+ map: null
220
324
  };
221
325
  }
222
326
  return null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gonia",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "A lightweight, SSR-first reactive UI library with declarative directives",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -42,7 +42,8 @@
42
42
  }
43
43
  },
44
44
  "dependencies": {
45
- "linkedom": "^0.18.0"
45
+ "linkedom": "^0.18.0",
46
+ "tinyglobby": "^0.2.15"
46
47
  },
47
48
  "peerDependencies": {
48
49
  "vite": ">=5.0.0"
@@ -53,10 +54,11 @@
53
54
  }
54
55
  },
55
56
  "devDependencies": {
57
+ "@types/node": "^25.0.10",
56
58
  "@vitest/coverage-v8": "^4.0.17",
57
59
  "jsdom": "^27.4.0",
58
60
  "typescript": "^5.7.0",
59
- "vite": "^6.0.0",
61
+ "vite": "^6.4.0",
60
62
  "vitest": "^4.0.17"
61
63
  },
62
64
  "scripts": {