gonia 0.0.3 → 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) {
@@ -4,3 +4,4 @@
4
4
  * @packageDocumentation
5
5
  */
6
6
  export { gonia, gonia as default } from './plugin.js';
7
+ export type { GoniaPluginOptions } from './plugin.js';
@@ -2,18 +2,61 @@
2
2
  * Vite plugin for gonia.js.
3
3
  *
4
4
  * @remarks
5
- * Transforms directive functions to add $inject arrays at build time,
6
- * making the code minification-safe without manual annotations.
5
+ * - Auto-detects directive usage in templates and injects imports
6
+ * - Scans custom directive sources to build import map
7
+ * - Transforms directive functions to add $inject arrays at build time
8
+ * - Configures Vite for SSR with gonia.js
7
9
  *
8
10
  * @packageDocumentation
9
11
  */
10
12
  import type { Plugin } from 'vite';
11
13
  /**
12
- * Cubist.js Vite plugin.
14
+ * Plugin options.
15
+ */
16
+ export interface GoniaPluginOptions {
17
+ /**
18
+ * Automatically detect and import directives from source code.
19
+ * @defaultValue true
20
+ */
21
+ autoDirectives?: boolean;
22
+ /**
23
+ * Additional directives to include (for runtime/dynamic cases).
24
+ * Use full directive names (e.g., 'g-text', 'my-component').
25
+ * @example ['g-text', 'g-for', 'app-header']
26
+ */
27
+ includeDirectives?: string[];
28
+ /**
29
+ * Directives to exclude from auto-detection.
30
+ * Use full directive names.
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[];
51
+ }
52
+ /**
53
+ * Gonia Vite plugin.
13
54
  *
14
55
  * @remarks
15
- * Adds $inject arrays to directive functions for minification safety.
16
- * Also configures Vite for SSR with gonia.js.
56
+ * - Auto-detects directive usage and injects imports
57
+ * - Scans custom directive sources to discover user directives
58
+ * - Adds $inject arrays to directive functions for minification safety
59
+ * - Configures Vite for SSR with gonia.js
17
60
  *
18
61
  * @example
19
62
  * ```ts
@@ -25,6 +68,18 @@ import type { Plugin } from 'vite';
25
68
  * plugins: [gonia()]
26
69
  * });
27
70
  * ```
71
+ *
72
+ * @example
73
+ * ```ts
74
+ * // With custom directives
75
+ * export default defineConfig({
76
+ * plugins: [gonia({
77
+ * directiveSources: ['src/directives/**\/*.ts'],
78
+ * directiveAttributePrefixes: ['g-'],
79
+ * directiveElementPrefixes: ['app-', 'ui-'],
80
+ * })]
81
+ * });
82
+ * ```
28
83
  */
29
- export declare function gonia(): Plugin;
84
+ export declare function gonia(options?: GoniaPluginOptions): Plugin;
30
85
  export default gonia;
@@ -2,60 +2,188 @@
2
2
  * Vite plugin for gonia.js.
3
3
  *
4
4
  * @remarks
5
- * Transforms directive functions to add $inject arrays at build time,
6
- * making the code minification-safe without manual annotations.
5
+ * - Auto-detects directive usage in templates and injects imports
6
+ * - Scans custom directive sources to build import map
7
+ * - Transforms directive functions to add $inject arrays at build time
8
+ * - Configures Vite for SSR with gonia.js
7
9
  *
8
10
  * @packageDocumentation
9
11
  */
12
+ import { readFileSync } from 'fs';
13
+ import { glob } from 'tinyglobby';
14
+ import { relative } from 'path';
10
15
  /**
11
- * Parse function parameters from a function string.
16
+ * Map of built-in directive names to their export names from gonia.
12
17
  */
13
- function parseParams(fnStr) {
14
- // Match function parameters: handles regular, arrow, and async functions
15
- const match = fnStr.match(/^[^(]*\(([^)]*)\)/);
16
- if (!match)
17
- return null;
18
- const params = match[1];
19
- if (!params.trim())
20
- return [];
21
- return params
22
- .split(',')
23
- .map(p => p.trim())
24
- // Remove type annotations (: Type)
25
- .map(p => p.replace(/\s*:.*$/, ''))
26
- // Remove default values (= value)
27
- .map(p => p.replace(/\s*=.*$/, ''))
28
- .filter(Boolean);
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' },
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
+ }
69
+ /**
70
+ * Detect directive names used in source code.
71
+ */
72
+ function detectDirectives(code, id, isDev, attributePrefixes, elementPrefixes, customDirectives) {
73
+ const found = new Set();
74
+ // Pattern for attribute directives (e.g., g-text, v-if)
75
+ const attrPattern = buildAttributePattern(attributePrefixes);
76
+ let match;
77
+ while ((match = attrPattern.exec(code)) !== null) {
78
+ // Reconstruct full attribute name
79
+ const fullMatch = match[0];
80
+ found.add(fullMatch);
81
+ }
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;
99
+ }
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
+ }
111
+ }
112
+ }
113
+ }
114
+ return found;
115
+ }
116
+ /**
117
+ * Generate import statements for detected directives.
118
+ */
119
+ function generateImports(directives, customDirectives, currentFile, rootDir) {
120
+ if (directives.size === 0)
121
+ return '';
122
+ // Group by module
123
+ const moduleImports = new Map();
124
+ for (const name of directives) {
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);
151
+ }
152
+ }
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' : '';
29
165
  }
30
166
  /**
31
167
  * Transform source code to add $inject arrays to directive functions.
32
168
  */
33
- function transformCode(code, id) {
34
- // Skip node_modules and non-JS/TS files
35
- if (id.includes('node_modules'))
36
- return null;
37
- if (!/\.(ts|js|tsx|jsx)$/.test(id))
38
- return null;
39
- // Skip if no directive calls
40
- if (!code.includes('directive('))
41
- return null;
169
+ function transformInject(code) {
170
+ if (!code.includes('directive(')) {
171
+ return { code, modified: false };
172
+ }
42
173
  let result = code;
43
174
  let modified = false;
44
- // Pattern: directive('name', functionName, ...)
45
- // We need to find the function and add $inject to it
46
175
  const directiveCallPattern = /directive\s*\(\s*['"`]([^'"`]+)['"`]\s*,\s*([a-zA-Z_$][a-zA-Z0-9_$]*)/g;
47
176
  const functionNames = new Set();
48
177
  let match;
49
178
  while ((match = directiveCallPattern.exec(code)) !== null) {
50
179
  functionNames.add(match[2]);
51
180
  }
52
- if (functionNames.size === 0)
53
- return null;
181
+ if (functionNames.size === 0) {
182
+ return { code, modified: false };
183
+ }
54
184
  for (const fnName of functionNames) {
55
- // Skip if already has $inject
56
185
  if (code.includes(`${fnName}.$inject`))
57
186
  continue;
58
- // Find function declaration: function fnName(params) or const fnName = (params) =>
59
187
  const fnDeclPattern = new RegExp(`(?:function\\s+${fnName}\\s*\\(([^)]*)\\)|(?:const|let|var)\\s+${fnName}\\s*=\\s*(?:async\\s*)?(?:function\\s*)?\\(([^)]*)\\))`, 'g');
60
188
  const fnMatch = fnDeclPattern.exec(code);
61
189
  if (!fnMatch)
@@ -71,20 +199,21 @@ function transformCode(code, id) {
71
199
  .filter(Boolean);
72
200
  if (params.length === 0)
73
201
  continue;
74
- // Find the directive() call and insert $inject before it
75
202
  const directivePattern = new RegExp(`(directive\\s*\\(\\s*['"\`][^'"\`]+['"\`]\\s*,\\s*${fnName})`, 'g');
76
203
  const injectStatement = `${fnName}.$inject = ${JSON.stringify(params)};\n`;
77
204
  result = result.replace(directivePattern, `${injectStatement}$1`);
78
205
  modified = true;
79
206
  }
80
- return modified ? result : null;
207
+ return { code: result, modified };
81
208
  }
82
209
  /**
83
- * Cubist.js Vite plugin.
210
+ * Gonia Vite plugin.
84
211
  *
85
212
  * @remarks
86
- * Adds $inject arrays to directive functions for minification safety.
87
- * Also configures Vite for SSR with gonia.js.
213
+ * - Auto-detects directive usage and injects imports
214
+ * - Scans custom directive sources to discover user directives
215
+ * - Adds $inject arrays to directive functions for minification safety
216
+ * - Configures Vite for SSR with gonia.js
88
217
  *
89
218
  * @example
90
219
  * ```ts
@@ -96,17 +225,102 @@ function transformCode(code, id) {
96
225
  * plugins: [gonia()]
97
226
  * });
98
227
  * ```
228
+ *
229
+ * @example
230
+ * ```ts
231
+ * // With custom directives
232
+ * export default defineConfig({
233
+ * plugins: [gonia({
234
+ * directiveSources: ['src/directives/**\/*.ts'],
235
+ * directiveAttributePrefixes: ['g-'],
236
+ * directiveElementPrefixes: ['app-', 'ui-'],
237
+ * })]
238
+ * });
239
+ * ```
99
240
  */
100
- export function gonia() {
241
+ export function gonia(options = {}) {
242
+ const { autoDirectives = true, includeDirectives = [], excludeDirectives = [], directiveSources = [], directiveAttributePrefixes = ['g-'], directiveElementPrefixes = options.directiveElementPrefixes ?? options.directiveAttributePrefixes ?? ['g-'], } = options;
243
+ let isDev = false;
244
+ let rootDir = process.cwd();
245
+ // Map of custom directive name -> DirectiveInfo
246
+ const customDirectives = new Map();
247
+ // Track which modules have been processed
248
+ const injectedModules = new Set();
101
249
  return {
102
250
  name: 'gonia',
103
251
  enforce: 'pre',
252
+ configResolved(config) {
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
+ }
273
+ },
104
274
  transform(code, id) {
105
- const result = transformCode(code, id);
106
- if (result) {
275
+ // Skip node_modules (except for $inject transform in gonia itself)
276
+ const isGoniaInternal = id.includes('gonia') && id.includes('node_modules');
277
+ if (id.includes('node_modules') && !isGoniaInternal)
278
+ return null;
279
+ if (!/\.(ts|js|tsx|jsx|html)$/.test(id))
280
+ return null;
281
+ let result = code;
282
+ let modified = false;
283
+ // Collect directives to import
284
+ if (!isGoniaInternal) {
285
+ const detected = new Set();
286
+ // Auto-detect directives if enabled
287
+ if (autoDirectives) {
288
+ for (const name of detectDirectives(code, id, isDev, directiveAttributePrefixes, directiveElementPrefixes, customDirectives)) {
289
+ detected.add(name);
290
+ }
291
+ }
292
+ // Add explicitly included directives
293
+ for (const name of includeDirectives) {
294
+ detected.add(name);
295
+ }
296
+ // Remove excluded directives
297
+ for (const name of excludeDirectives) {
298
+ detected.delete(name);
299
+ }
300
+ // Generate imports if we found directives and haven't already
301
+ if (detected.size > 0 && !injectedModules.has(id)) {
302
+ // Check if this file already imports from gonia/directives
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);
311
+ }
312
+ }
313
+ }
314
+ // Transform $inject arrays
315
+ const injectResult = transformInject(result);
316
+ if (injectResult.modified) {
317
+ result = injectResult.code;
318
+ modified = true;
319
+ }
320
+ if (modified) {
107
321
  return {
108
322
  code: result,
109
- map: null // TODO: proper source map support
323
+ map: null
110
324
  };
111
325
  }
112
326
  return null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gonia",
3
- "version": "0.0.3",
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": {