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.
- package/dist/client/hydrate.d.ts +1 -1
- package/dist/client/hydrate.js +1 -1
- package/dist/directives/slot.js +6 -5
- package/dist/directives/template.d.ts +1 -1
- package/dist/directives/template.js +1 -1
- package/dist/expression.d.ts +1 -1
- package/dist/expression.js +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -1
- package/dist/scope.d.ts +21 -2
- package/dist/scope.js +32 -3
- package/dist/server/render.d.ts +1 -1
- package/dist/server/render.js +1 -1
- package/dist/vite/plugin.d.ts +27 -7
- package/dist/vite/plugin.js +173 -69
- package/package.json +5 -3
package/dist/client/hydrate.d.ts
CHANGED
|
@@ -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
|
|
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;
|
package/dist/client/hydrate.js
CHANGED
|
@@ -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
|
|
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) {
|
package/dist/directives/slot.js
CHANGED
|
@@ -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;
|
package/dist/expression.d.ts
CHANGED
|
@@ -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.
|
|
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
|
package/dist/expression.js
CHANGED
|
@@ -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.
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
* @
|
|
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
|
-
* @
|
|
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
|
-
|
|
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).
|
package/dist/server/render.d.ts
CHANGED
|
@@ -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
|
|
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;
|
package/dist/server/render.js
CHANGED
|
@@ -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
|
|
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) {
|
package/dist/vite/plugin.d.ts
CHANGED
|
@@ -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
|
|
24
|
-
* @example ['text', 'for', '
|
|
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
|
|
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
|
|
74
|
+
* // With custom directives
|
|
55
75
|
* export default defineConfig({
|
|
56
76
|
* plugins: [gonia({
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
77
|
+
* directiveSources: ['src/directives/**\/*.ts'],
|
|
78
|
+
* directiveAttributePrefixes: ['g-'],
|
|
79
|
+
* directiveElementPrefixes: ['app-', 'ui-'],
|
|
60
80
|
* })]
|
|
61
81
|
* });
|
|
62
82
|
* ```
|
package/dist/vite/plugin.js
CHANGED
|
@@ -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
|
|
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
|
|
32
|
-
|
|
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
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
}
|
|
78
|
+
// Reconstruct full attribute name
|
|
79
|
+
const fullMatch = match[0];
|
|
80
|
+
found.add(fullMatch);
|
|
40
81
|
}
|
|
41
|
-
// Pattern
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
|
117
|
+
* Generate import statements for detected directives.
|
|
70
118
|
*/
|
|
71
|
-
function
|
|
119
|
+
function generateImports(directives, customDirectives, currentFile, rootDir) {
|
|
72
120
|
if (directives.size === 0)
|
|
73
121
|
return '';
|
|
74
|
-
|
|
122
|
+
// Group by module
|
|
123
|
+
const moduleImports = new Map();
|
|
75
124
|
for (const name of directives) {
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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
|
|
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
|
|
231
|
+
* // With custom directives
|
|
151
232
|
* export default defineConfig({
|
|
152
233
|
* plugins: [gonia({
|
|
153
|
-
*
|
|
154
|
-
*
|
|
155
|
-
*
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
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
|
|
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
|
|
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.
|
|
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.
|
|
61
|
+
"vite": "^6.4.0",
|
|
60
62
|
"vitest": "^4.0.17"
|
|
61
63
|
},
|
|
62
64
|
"scripts": {
|