fusee-framework 1.7.0 → 1.9.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.
Files changed (77) hide show
  1. package/README.md +206 -139
  2. package/app/benchmark/benchmark.js +2 -2
  3. package/app/benchmark/vite.config.js +23 -0
  4. package/app/main.js +24 -3
  5. package/app/main.ts +7 -1
  6. package/app/pages/_layout.js +27 -0
  7. package/app/pages/async-work.js +65 -0
  8. package/app/pages/index.js +25 -5
  9. package/app/pages/index.ts +9 -0
  10. package/app/pages/router-example.js +24 -1
  11. package/app/pages/scroll-demo.ts +102 -0
  12. package/app/providers/ChildComponent.js +8 -2
  13. package/app/providers/ChildComponent.ts +47 -47
  14. package/app/providers/GrandchildComponent.js +19 -0
  15. package/app/providers/ParentComponent.js +5 -1
  16. package/app/routes/RouterExample.js +23 -1
  17. package/app/routes/TestResource.js +47 -0
  18. package/app/services.js +15 -0
  19. package/auto-imports.d.ts +12 -0
  20. package/framework/auto-import-preset.js +15 -9
  21. package/framework/core/async-context.js +27 -0
  22. package/framework/core/compiler/ast.js +167 -0
  23. package/framework/core/compiler/errors-list.js +47 -0
  24. package/framework/core/compiler/errors.js +107 -0
  25. package/framework/core/compiler/generator.js +363 -0
  26. package/framework/core/compiler/lexer.js +347 -0
  27. package/framework/core/compiler/main-compiler.js +115 -0
  28. package/framework/core/compiler/parser.js +414 -0
  29. package/framework/core/compiler/plugins/actions-plugin.js +263 -0
  30. package/framework/core/compiler/plugins/compiler-plugin.d.ts +5 -0
  31. package/framework/core/compiler/plugins/compiler-plugin.js +45 -0
  32. package/framework/core/compiler/plugins/file-router-plugin.js +301 -0
  33. package/framework/core/compiler/plugins/router-plugin.js +196 -0
  34. package/framework/core/compiler/transformer.js +398 -0
  35. package/framework/core/component.js +61 -23
  36. package/framework/core/di.js +187 -0
  37. package/framework/core/directives.js +40 -96
  38. package/framework/core/evaluator.js +1 -1
  39. package/framework/core/h.js +430 -0
  40. package/framework/core/hydration.js +83 -0
  41. package/framework/core/reconcile.js +151 -0
  42. package/framework/core/signal.js +269 -0
  43. package/framework/core/store.js +322 -81
  44. package/framework/index.js +5 -3
  45. package/framework/router/file-router.js +1 -1
  46. package/framework/router/router.js +113 -21
  47. package/framework/server/actions.js +173 -0
  48. package/framework/server/actions.server.js +88 -0
  49. package/framework/server/index.js +18 -0
  50. package/framework/types/actions.d.ts +50 -0
  51. package/framework/types/actions.server.d.ts +36 -0
  52. package/framework/types/compiler/ast.d.ts +166 -0
  53. package/framework/types/compiler/errors-codes.d.ts +23 -0
  54. package/framework/types/compiler/errors.d.ts +43 -0
  55. package/framework/types/compiler/generator.d.ts +8 -0
  56. package/framework/types/compiler/h.d.ts +46 -0
  57. package/framework/types/compiler/lexer.d.ts +27 -0
  58. package/framework/types/compiler/main-compiler.d.ts +60 -0
  59. package/framework/types/compiler/parser.d.ts +4 -0
  60. package/framework/types/compiler/plugins/actions.d.ts +30 -0
  61. package/framework/types/compiler/plugins/compiler-plugin.d.ts +5 -0
  62. package/framework/types/compiler/plugins/file-router-plugins.d.ts +32 -0
  63. package/framework/types/compiler/plugins/router-plugin.d.ts +24 -0
  64. package/framework/types/compiler/transformer.d.ts +28 -0
  65. package/framework/types/component.d.ts +89 -88
  66. package/framework/types/di.d.ts +43 -0
  67. package/framework/types/hydration.d.ts +30 -0
  68. package/framework/types/index.d.ts +38 -22
  69. package/framework/types/reconcile.d.ts +7 -0
  70. package/framework/types/router.d.ts +11 -1
  71. package/framework/types/server.d.ts +11 -0
  72. package/framework/types/signal.d.ts +142 -79
  73. package/framework/types/store.d.ts +66 -8
  74. package/ghid_go_fusee_integrat_go1_26.txt +276 -0
  75. package/ghid_rust_fusee_rust_1_95_0.txt +1118 -0
  76. package/package.json +44 -36
  77. package/vite.config.js +65 -47
@@ -0,0 +1,167 @@
1
+ export const NodeType = {
2
+ ROOT: 'Root',
3
+ ELEMENT: 'Element',
4
+ COMPONENT: 'Component',
5
+ TEXT: 'Text',
6
+ INTERPOLATION: 'Interpolation',
7
+ EXPRESSION: 'Expression',
8
+ ATTRIBUTE: 'Attribute',
9
+ BINDING: 'Binding',
10
+ EVENT: 'Event',
11
+ DIRECTIVE: 'Directive',
12
+ SLOT_OUTLET: 'SlotOutlet',
13
+ SLOT_CONTENT: 'SlotContent'
14
+ }
15
+
16
+ export function createLoc(start, end, source) {
17
+ return { start, end, source }
18
+ }
19
+
20
+ export function createPos(line, col, offset) {
21
+ return { line, col, offset }
22
+ }
23
+
24
+ export function createRoot(children = [], loc = null) {
25
+ return { type: NodeType.ROOT, children, loc }
26
+ }
27
+
28
+ export function createElement(tag, props = [], children = [], selfClosing = false, loc = null) {
29
+ return {
30
+ type: NodeType.ELEMENT,
31
+ tag,
32
+ props,
33
+ children,
34
+ selfClosing,
35
+ isStatic: false,
36
+ hoisted: false,
37
+ loc
38
+ }
39
+ }
40
+
41
+ export function createComponent(name, props = [], slots = {}, loc = null) {
42
+ return {
43
+ type: NodeType.COMPONENT,
44
+ name,
45
+ props,
46
+ slots,
47
+ loc
48
+ }
49
+ }
50
+
51
+ export function createText(content, loc = null) {
52
+ return { type: NodeType.TEXT, content, loc }
53
+ }
54
+
55
+ export function createInterpolation(expression, loc = null) {
56
+ return {
57
+ type: NodeType.INTERPOLATION,
58
+ expression,
59
+ loc
60
+ }
61
+ }
62
+
63
+ export function createExpression(content, isStatic = false, loc = null) {
64
+ return {
65
+ type: NodeType.EXPRESSION,
66
+ content,
67
+ isStatic,
68
+ loc
69
+ }
70
+ }
71
+
72
+ export function createAttribute(name, value, loc = null) {
73
+ return {
74
+ type: NodeType.ATTRIBUTE,
75
+ name,
76
+ value,
77
+ loc
78
+ }
79
+ }
80
+
81
+ export function createBinding(name, expression, isProp = false, loc = null) {
82
+ return {
83
+ type: NodeType.BINDING,
84
+ name,
85
+ expression,
86
+ isProp,
87
+ loc
88
+ }
89
+ }
90
+
91
+ export function createEvent(name, expression, modifiers = [], loc = null) {
92
+ return {
93
+ type: NodeType.EVENT,
94
+ name,
95
+ expression,
96
+ modifiers,
97
+ loc
98
+ }
99
+ }
100
+
101
+ export function createDirective(name, expression = null, arg = null, loc = null) {
102
+ return {
103
+ type: NodeType.DIRECTIVE,
104
+ name,
105
+ expression,
106
+ arg,
107
+ loc
108
+ }
109
+ }
110
+
111
+ export function createSlotOutlet(slotName = 'default', fallback = [], loc = null) {
112
+ return {
113
+ type: NodeType.SLOT_OUTLET,
114
+ slotName,
115
+ fallback,
116
+ loc
117
+ }
118
+ }
119
+
120
+ export function createSlotContent(slotName = 'default', children = [], loc = null) {
121
+ return {
122
+ type: NodeType.SLOT_CONTENT,
123
+ slotName,
124
+ children,
125
+ loc
126
+ }
127
+ }
128
+
129
+ export function createForArg(item, source, index = null, loc = null) {
130
+ return { item, source, index, loc }
131
+ }
132
+
133
+ export function isElement(node) {
134
+ return node?.type === NodeType.ELEMENT
135
+ }
136
+
137
+ export function isComponent(node) {
138
+ return node?.type === NodeType.COMPONENT
139
+ }
140
+
141
+ export function isText(node) {
142
+ return node?.type === NodeType.TEXT
143
+ }
144
+
145
+ export function isInterpolation(node) {
146
+ return node?.type === NodeType.INTERPOLATION
147
+ }
148
+
149
+ export function isDirective(node, name) {
150
+ return node?.type === NodeType.DIRECTIVE && (name == null || node.name === name)
151
+ }
152
+
153
+ export function isStaticText(node) {
154
+ return node?.type === NodeType.TEXT || (node?.type === NodeType.INTERPOLATION && node.expression?.isStatic)
155
+ }
156
+
157
+ export function findDirective(element, name) {
158
+ return element.props.find(p => isDirective(p, name)) ?? null
159
+ }
160
+
161
+ export function getEvents(element) {
162
+ return element.props.filter(p => p.type === NodeType.EVENT)
163
+ }
164
+
165
+ export function cloneNode(node) {
166
+ return { ...node, props: node.props ? [...node.props] : undefined, children: node.children ? [...node.children] : undefined }
167
+ }
@@ -0,0 +1,47 @@
1
+ export const ErrorCode = {
2
+ UNEXPECTED_CHAR: 'E001',
3
+ UNTERMINATED_STRING: 'E002',
4
+ UNTERMINATED_MUSTACHE: 'E003',
5
+ UNTERMINATED_TAG: 'E004',
6
+ MALFORMED_COMMENT: 'E005',
7
+ UNEXPECTED_TOKEN: 'P001',
8
+ MISSING_CLOSING_TAG: 'P002',
9
+ UNEXPECTED_CLOSING_TAG: 'P003',
10
+ INVALID_DIRECTIVE_SYNTAX: 'P004',
11
+ INVALID_FOR_SYNTAX: 'P005',
12
+ DUPLICATE_KEY: 'P006',
13
+ V_ELSE_NO_IF: 'P007',
14
+ EMPTY_EXPRESSION: 'P008',
15
+ INVALID_SLOT_NAME: 'P009',
16
+ DUPLICATE_DIRECTIVE: 'T001',
17
+ FOR_MISSING_KEY: 'T002',
18
+ MODEL_ON_NON_INPUT: 'T003',
19
+ COMPONENT_NOT_REGISTERED: 'T004',
20
+ UNKNOWN_IDENTIFIER: 'S001',
21
+ STATIC_FOR_KEY: 'S002',
22
+ UNKNOWN_NODE_TYPE: 'G001',
23
+ }
24
+
25
+ export const ERROR_MESSAGES = {
26
+ [ErrorCode.UNEXPECTED_CHAR]: 'Unexpected character "{0}"',
27
+ [ErrorCode.UNTERMINATED_STRING]: 'Unterminated string literal',
28
+ [ErrorCode.UNTERMINATED_MUSTACHE]: 'Unterminated mustache expression "{0}"',
29
+ [ErrorCode.UNTERMINATED_TAG]: 'Unterminated tag "<{0}"',
30
+ [ErrorCode.MALFORMED_COMMENT]: 'Malformed HTML comment',
31
+ [ErrorCode.UNEXPECTED_TOKEN]: 'Unexpected token "{0}", expected "{1}"',
32
+ [ErrorCode.MISSING_CLOSING_TAG]: 'Missing closing tag for <{0}>',
33
+ [ErrorCode.UNEXPECTED_CLOSING_TAG]: 'Unexpected closing tag </{0}>',
34
+ [ErrorCode.INVALID_DIRECTIVE_SYNTAX]: 'Invalid directive syntax: "{0}"',
35
+ [ErrorCode.INVALID_FOR_SYNTAX]: 'Invalid f-for syntax: "{0}". Expected "(item, index) in source" or "item in source"',
36
+ [ErrorCode.DUPLICATE_KEY]: 'Duplicate attribute "{0}" on element <{1}>',
37
+ [ErrorCode.V_ELSE_NO_IF]: 'f-else / f-else-if has no matching f-if',
38
+ [ErrorCode.EMPTY_EXPRESSION]: 'Expression in "{0}" must not be empty',
39
+ [ErrorCode.INVALID_SLOT_NAME]: 'Invalid slot name "{0}"',
40
+ [ErrorCode.DUPLICATE_DIRECTIVE]: 'Duplicate directive "f-{0}" on element <{1}>',
41
+ [ErrorCode.FOR_MISSING_KEY]: 'f-for on <{0}> is missing a :key binding — add :key="<unique expr>"',
42
+ [ErrorCode.MODEL_ON_NON_INPUT]: 'f-model can only be used on <input>, <textarea> or <select>, got <{0}>',
43
+ [ErrorCode.COMPONENT_NOT_REGISTERED]: 'Component "{0}" is used in the template but not registered',
44
+ [ErrorCode.UNKNOWN_NODE_TYPE]: 'Unknown AST node type "{0}" encountered in generator',
45
+ [ErrorCode.UNKNOWN_IDENTIFIER]: 'Unknown identifier "{0}" in expression "{1}" — not found in component scope',
46
+ [ErrorCode.STATIC_FOR_KEY]: ':key="{0}" is a static literal — keys must be unique per item (use a dynamic expression like :key="item.id")',
47
+ }
@@ -0,0 +1,107 @@
1
+ import { ErrorCode, ERROR_MESSAGES } from './errors-list.js'
2
+
3
+ export class CompileError extends Error {
4
+ constructor(code, loc, source, ...args) {
5
+ const template = ERROR_MESSAGES[code] ?? `Unknown compiler error (${code})`
6
+ const message = _interpolate(template, args)
7
+ super(message)
8
+ this.name = 'CompileError'
9
+ this.code = code
10
+ this.loc = loc
11
+ this.source = source
12
+ this._snippet = null
13
+ }
14
+
15
+ format(filePath = '<template>') {
16
+ const { line, col } = this.loc?.start ?? { line: '?', col: '?' }
17
+ const header = `[fusée] ${this.name} ${this.code} at ${filePath}:${line}:${col}`
18
+ const snippet = this._buildSnippet()
19
+ return snippet ? `${header}\n${this.message}\n\n${snippet}` : `${header}\n${this.message}`
20
+ }
21
+
22
+ _buildSnippet() {
23
+ if (this._snippet !== null) return this._snippet
24
+ if (!this.source || !this.loc?.start) {
25
+ this._snippet = ''
26
+ return ''
27
+ }
28
+
29
+ const lines = this.source.split('\n')
30
+ const lineIdx = this.loc.start.line - 1
31
+ const col = this.loc.start.col
32
+ const CONTEXT = 2
33
+ const firstLine = Math.max(0, lineIdx - CONTEXT)
34
+ const lastLine = Math.min(lines.length - 1, lineIdx + CONTEXT)
35
+ const width = String(lastLine + 1).length
36
+ const parts = []
37
+
38
+ for (let i = firstLine; i <= lastLine; i++) {
39
+ const lineNo = String(i + 1).padStart(width, ' ')
40
+ const prefix = i === lineIdx ? '> ' : ' '
41
+ parts.push(`${prefix}${lineNo} | ${lines[i]}`)
42
+ if (i === lineIdx) {
43
+ const caretPad = ' '.repeat(width + 4 + (col - 1))
44
+ const caretLen = Math.max(1, (this.loc.end?.col ?? col) - col)
45
+ parts.push(`${caretPad}${'~'.repeat(caretLen)}`)
46
+ }
47
+ }
48
+
49
+ this._snippet = parts.join('\n')
50
+ return this._snippet
51
+ }
52
+ }
53
+
54
+ export class CompileWarning {
55
+ constructor(code, loc, source, ...args) {
56
+ const template = ERROR_MESSAGES[code] ?? `Unknown compiler warning (${code})`
57
+ this.message = _interpolate(template, args)
58
+ this.code = code
59
+ this.loc = loc
60
+ this.source = source
61
+ }
62
+
63
+ format(filePath = '<template>') {
64
+ const { line, col } = this.loc?.start ?? { line: '?', col: '?' }
65
+ return `[fusée] Warning ${this.code} at ${filePath}:${line}:${col}\n${this.message}`
66
+ }
67
+ }
68
+
69
+ export class ErrorCollector {
70
+ constructor(source) {
71
+ this.source = source
72
+ this.errors = []
73
+ this.warnings = []
74
+ }
75
+
76
+ error(code, loc, ...args) {
77
+ const err = new CompileError(code, loc, this.source, ...args)
78
+ this.errors.push(err)
79
+ return err
80
+ }
81
+
82
+ warn(code, loc, ...args) {
83
+ const w = new CompileWarning(code, loc, this.source, ...args)
84
+ this.warnings.push(w)
85
+ return w
86
+ }
87
+
88
+ throwIfAny() {
89
+ if (this.errors.length > 0) throw this.errors[0]
90
+ }
91
+
92
+ get hasErrors() {
93
+ return this.errors.length > 0
94
+ }
95
+
96
+ get hasWarnings() {
97
+ return this.warnings.length > 0
98
+ }
99
+ }
100
+
101
+ function _interpolate(template, args) {
102
+ return template.replace(/\{(\d+)\}/g, (_, i) => args[i] ?? '')
103
+ }
104
+
105
+ export function throwError(code, loc, source, ...args) {
106
+ throw new CompileError(code, loc, source, ...args)
107
+ }
@@ -0,0 +1,363 @@
1
+ import { NodeType } from "./ast.js";
2
+ import { ErrorCode } from "./errors-list.js";
3
+ import { throwError } from "./errors.js";
4
+
5
+ export function generate(ast, options = {}) {
6
+ const gen = new Generator(ast, options);
7
+ return gen.generate();
8
+ }
9
+
10
+ const RUNTIME = "fusee/runtime/h.js";
11
+
12
+ class Generator {
13
+ constructor(ast, options) {
14
+ this.ast = ast;
15
+ this.source = options.source ?? "";
16
+ this.runtimePath = options.runtimePath ?? RUNTIME;
17
+ this.indent = 0;
18
+ this.lines = [];
19
+ this._hoistedIdx = 0;
20
+ this._hoistedMap = new Map();
21
+ this._imports = new Set([
22
+ "h",
23
+ "hText",
24
+ "createComponent",
25
+ "_effect",
26
+ "_batch",
27
+ ]);
28
+ this.localScopes = [];
29
+ }
30
+
31
+ generate() {
32
+ if (this.ast._hoisted) {
33
+ for (const node of this.ast._hoisted) {
34
+ const name = `_s${this._hoistedIdx++}`;
35
+ this._hoistedMap.set(node, name);
36
+ }
37
+ }
38
+
39
+ const bodyLines = [];
40
+ const children = this._genChildren(this.ast.children);
41
+
42
+ const hoistedDecls = [];
43
+ for (const [node, name] of this._hoistedMap) {
44
+ hoistedDecls.push(`const ${name} = ${this._genNode(node, true)}`);
45
+ }
46
+
47
+ const importList = [...this._imports].join(", ");
48
+ const output = [
49
+ `import { ${importList} } from '${this.runtimePath}'`,
50
+ "",
51
+ ...(hoistedDecls.length ? [...hoistedDecls, ""] : []),
52
+ `export function render(_ctx, _components) {`,
53
+ ` return [`,
54
+ children.map((l) => ` ${l}`).join(",\n"),
55
+ ` ]`,
56
+ `}`,
57
+ ];
58
+
59
+ return output.join("\n");
60
+ }
61
+
62
+ _genChildren(children) {
63
+ if (!children || children.length === 0) return [];
64
+ return children.flatMap((child) => {
65
+ const forDir = child.props?.find?.(
66
+ (p) => p.type === NodeType.DIRECTIVE && p.name === "for",
67
+ );
68
+ if (forDir) return [this._genForNode(child, forDir)];
69
+ return [this._genNode(child)];
70
+ });
71
+ }
72
+
73
+ _genNode(node, inHoist = false) {
74
+ if (!inHoist && this._hoistedMap.has(node)) {
75
+ return this._hoistedMap.get(node);
76
+ }
77
+
78
+ switch (node.type) {
79
+ case NodeType.TEXT:
80
+ return this._genText(node);
81
+ case NodeType.INTERPOLATION:
82
+ return this._genInterpolation(node);
83
+ case NodeType.ELEMENT:
84
+ return this._genElement(node);
85
+ case NodeType.COMPONENT:
86
+ return this._genComponent(node);
87
+ case NodeType.SLOT_OUTLET:
88
+ return this._genSlotOutlet(node);
89
+ case "If":
90
+ return this._genIf(node);
91
+ default:
92
+ throwError(
93
+ ErrorCode.UNKNOWN_NODE_TYPE,
94
+ node.loc,
95
+ this.source,
96
+ node.type,
97
+ );
98
+ }
99
+ }
100
+
101
+ _genText(node) {
102
+ return `hText(${JSON.stringify(node.content)})`;
103
+ }
104
+
105
+ _genInterpolation(node) {
106
+ this._imports.add("_effect");
107
+ const expr = node.expression.content;
108
+ if (node.expression.isStatic) {
109
+ return `hText(String(${expr}))`;
110
+ }
111
+ return `hText(() => String(${this._wrapExpr(expr)}))`;
112
+ }
113
+
114
+ _genElement(node) {
115
+ const tag = JSON.stringify(node.tag);
116
+ const props = this._genProps(node.props);
117
+ const children = this._genChildrenArray(node.children);
118
+ const args = [tag, props, children];
119
+
120
+ if (node.isStatic) args.push("true");
121
+
122
+ return `h(${args.join(", ")})`;
123
+ }
124
+
125
+ _genComponent(node) {
126
+ this._imports.add("createComponent");
127
+
128
+ const name = JSON.stringify(node.name);
129
+ const props = this._genComponentProps(node.props);
130
+ const slots = this._genSlots(node.slots);
131
+ return `createComponent(${name}, _components[${name}], ${props}, ${slots})`;
132
+ }
133
+
134
+ _genSlotOutlet(node) {
135
+ this._imports.add("hSlot");
136
+ const name = JSON.stringify(node.slotName);
137
+ const fallback = this._genChildrenArray(node.fallback);
138
+ return `hSlot(_ctx._slots, ${name}, ${fallback})`;
139
+ }
140
+
141
+ _genIf(node) {
142
+ this._imports.add("hIf");
143
+
144
+ const branches = node.branches.map((branch) => {
145
+ const cond = branch.condition
146
+ ? this._wrapExpr(branch.condition.content)
147
+ : "true";
148
+ const children = this._genChildrenArray(branch.node.children ?? []);
149
+ return `[() => ${cond}, () => ${children}]`;
150
+ });
151
+
152
+ return `hIf([\n ${branches.join(",\n ")}\n ])`;
153
+ }
154
+
155
+ _genProps(props) {
156
+ if (!props || props.length === 0) return "{}";
157
+
158
+ const entries = [];
159
+
160
+ for (const prop of props) {
161
+ switch (prop.type) {
162
+ case NodeType.ATTRIBUTE:
163
+ entries.push(this._genAttrEntry(prop));
164
+ break;
165
+ case NodeType.BINDING:
166
+ entries.push(this._genBindingEntry(prop));
167
+ break;
168
+ case NodeType.EVENT:
169
+ entries.push(this._genEventEntry(prop));
170
+ break;
171
+ case NodeType.DIRECTIVE:
172
+ this._genDirectiveEntries(prop, entries);
173
+ break;
174
+ }
175
+ }
176
+
177
+ return `{\n ${entries.join(",\n ")}\n }`;
178
+ }
179
+
180
+ _genAttrEntry(prop) {
181
+ return `${JSON.stringify(prop.name)}: ${JSON.stringify(prop.value ?? true)}`;
182
+ }
183
+
184
+ _genBindingEntry(prop) {
185
+ const expr = this._wrapExpr(prop.expression.content);
186
+ if (prop.isProp) {
187
+ return `${JSON.stringify("prop:" + prop.name)}: () => ${expr}`;
188
+ }
189
+ return `${JSON.stringify(prop.name)}: () => ${expr}`;
190
+ }
191
+
192
+ _genEventEntry(prop) {
193
+ const key = JSON.stringify("@" + prop.name);
194
+ const expr = prop.expression.content;
195
+ const mods = JSON.stringify(prop.modifiers);
196
+ const handlerJs = this._genHandlerExpr(expr);
197
+ return `${key}: { handler: ${handlerJs}, modifiers: ${mods} }`;
198
+ }
199
+
200
+ _genHandlerExpr(expr) {
201
+ if (/^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(expr.trim())) {
202
+ return `_ctx.${expr.trim()}`;
203
+ }
204
+ return `($event) => { ${expr} }`;
205
+ }
206
+
207
+ _genDirectiveEntries(dir, entries) {
208
+ switch (dir.name) {
209
+ case "if":
210
+ case "else-if":
211
+ case "else":
212
+ break;
213
+ case "for":
214
+ break;
215
+ case "show": {
216
+ const expr = this._wrapExpr(dir.expression.content);
217
+ entries.push(`'f-show': () => ${expr}`);
218
+ break;
219
+ }
220
+
221
+ case "model": {
222
+ const expr = dir.expression.content;
223
+ entries.push(`'f-model': () => _ctx.${expr}`);
224
+ entries.push(
225
+ `'@input': { handler: ($e) => { _ctx.${expr}($e.target.value) }, modifiers: [] }`,
226
+ );
227
+ break;
228
+ }
229
+
230
+ case "html": {
231
+ const expr = this._wrapExpr(dir.expression.content);
232
+ entries.push(`'f-html': () => ${expr}`);
233
+ break;
234
+ }
235
+
236
+ case "ref": {
237
+ const name = dir.expression.content.trim();
238
+ entries.push(`'f-ref': ${JSON.stringify(name)}`);
239
+ break;
240
+ }
241
+
242
+ case "once":
243
+ entries.push(`'f-once': true`);
244
+ break;
245
+
246
+ default:
247
+ entries.push(
248
+ `${JSON.stringify("f-" + dir.name)}: ${dir.expression ? `() => ${this._wrapExpr(dir.expression.content)}` : "true"}`,
249
+ );
250
+ }
251
+ }
252
+
253
+ _genComponentProps(props) {
254
+ if (!props || props.length === 0) return "{}";
255
+
256
+ const entries = [];
257
+
258
+ for (const prop of props) {
259
+ if (prop.type === NodeType.ATTRIBUTE) {
260
+ entries.push(
261
+ `${JSON.stringify(prop.name)}: ${JSON.stringify(prop.value ?? true)}`,
262
+ );
263
+ } else if (prop.type === NodeType.BINDING) {
264
+ entries.push(
265
+ `${JSON.stringify(prop.name)}: () => ${this._wrapExpr(prop.expression.content)}`,
266
+ );
267
+ } else if (prop.type === NodeType.EVENT) {
268
+ const handlerJs = this._genHandlerExpr(prop.expression.content);
269
+ entries.push(`${JSON.stringify("on:" + prop.name)}: ${handlerJs}`);
270
+ }
271
+ }
272
+
273
+ return `{ ${entries.join(", ")} }`;
274
+ }
275
+
276
+ _genSlots(slots) {
277
+ if (!slots || Object.keys(slots).length === 0) return "{}";
278
+
279
+ const entries = Object.entries(slots).map(([name, children]) => {
280
+ const childrenJs = this._genChildrenArray(children);
281
+ return `${JSON.stringify(name)}: () => ${childrenJs}`;
282
+ });
283
+
284
+ return `{ ${entries.join(", ")} }`;
285
+ }
286
+
287
+ _genChildrenArray(children) {
288
+ if (!children || children.length === 0) return "[]";
289
+ const expanded = children.flatMap((child) => {
290
+ const forDir = child.props?.find?.(
291
+ (p) => p.type === NodeType.DIRECTIVE && p.name === "for",
292
+ );
293
+ if (forDir) return [this._genForNode(child, forDir)];
294
+ return [this._genNode(child)];
295
+ });
296
+
297
+ if (expanded.length === 1) return `[${expanded[0]}]`;
298
+ return `[\n ${expanded.join(",\n ")}\n ]`;
299
+ }
300
+
301
+ _genForNode(node, forDir) {
302
+ this._imports.add("hFor");
303
+
304
+ const { item, source, index } = forDir.arg;
305
+ const sourceJs = this._wrapExpr(source);
306
+
307
+ this.localScopes.push(new Set([item, index].filter(Boolean)));
308
+
309
+ const innerProps = node.props.filter(
310
+ (p) => !(p.type === NodeType.DIRECTIVE && p.name === "for"),
311
+ );
312
+ const innerNode = { ...node, props: innerProps };
313
+ const keyBinding = innerProps.find(
314
+ (p) => p.type === NodeType.BINDING && p.name === "key",
315
+ );
316
+ const keyExpr = keyBinding ? keyBinding.expression.content : "undefined";
317
+ const itemParam = index ? `${item}, ${index}` : item;
318
+ const itemParamStr = `(${itemParam})`;
319
+ const innerJs = this._genNode(innerNode);
320
+ const keyJs = this._wrapExpr(keyExpr);
321
+
322
+ this.localScopes.pop();
323
+
324
+ return `hFor(\n () => ${sourceJs},\n ${itemParamStr} => ${innerJs},\n ${itemParamStr} => ${keyJs}\n )`;
325
+ }
326
+
327
+ isLocal(id) {
328
+ return this.localScopes.some((scope) => scope.has(id));
329
+ }
330
+
331
+ _wrapExpr(expr) {
332
+ if (expr.includes("_ctx.")) return expr;
333
+ if (/^[a-zA-Z_$][a-zA-Z0-9_$.]*$/.test(expr.trim())) {
334
+ const id = expr.trim();
335
+ if (this.isLocal(id)) return id;
336
+ return `(typeof _ctx.${id} === 'function' && _ctx.${id}.isSignal ? _ctx.${id}() : _ctx.${id})`;
337
+ }
338
+
339
+ return this._rewriteExpr(expr);
340
+ }
341
+
342
+ _rewriteExpr(expr) {
343
+ const GLOBALS = new Set([
344
+ 'true', 'false', 'null', 'undefined', 'NaN', 'Infinity',
345
+ 'Array', 'Object', 'String', 'Number', 'Boolean', 'Date',
346
+ 'Math', 'JSON', 'Promise', 'Map', 'Set', 'Symbol',
347
+ 'parseInt', 'parseFloat', 'isNaN', 'isFinite', 'console',
348
+ 'typeof', 'instanceof', 'void', 'delete', 'new', 'return',
349
+ 'if', 'else', 'for', 'while', 'do', 'switch', 'case',
350
+ 'break', 'continue', 'function', 'class', 'const', 'let',
351
+ 'var', 'import', 'export', 'default', 'this',
352
+ ]);
353
+
354
+ return expr.replace(
355
+ /(?<![.\w$])([a-zA-Z_$][a-zA-Z0-9_$]*)(?!\s*:)(?=\s*[\(\.\,\)\]\s+\|\&\!\?\+\-\*\/\%\=\<\>]|$)/g,
356
+ (match, id) => {
357
+ if (GLOBALS.has(id)) return match;
358
+ if (this.isLocal(id)) return match;
359
+ return `_ctx.${id}`;
360
+ }
361
+ );
362
+ }
363
+ }