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.
- package/README.md +206 -139
- package/app/benchmark/benchmark.js +2 -2
- package/app/benchmark/vite.config.js +23 -0
- package/app/main.js +24 -3
- package/app/main.ts +7 -1
- package/app/pages/_layout.js +27 -0
- package/app/pages/async-work.js +65 -0
- package/app/pages/index.js +25 -5
- package/app/pages/index.ts +9 -0
- package/app/pages/router-example.js +24 -1
- package/app/pages/scroll-demo.ts +102 -0
- package/app/providers/ChildComponent.js +8 -2
- package/app/providers/ChildComponent.ts +47 -47
- package/app/providers/GrandchildComponent.js +19 -0
- package/app/providers/ParentComponent.js +5 -1
- package/app/routes/RouterExample.js +23 -1
- package/app/routes/TestResource.js +47 -0
- package/app/services.js +15 -0
- package/auto-imports.d.ts +12 -0
- package/framework/auto-import-preset.js +15 -9
- package/framework/core/async-context.js +27 -0
- package/framework/core/compiler/ast.js +167 -0
- package/framework/core/compiler/errors-list.js +47 -0
- package/framework/core/compiler/errors.js +107 -0
- package/framework/core/compiler/generator.js +363 -0
- package/framework/core/compiler/lexer.js +347 -0
- package/framework/core/compiler/main-compiler.js +115 -0
- package/framework/core/compiler/parser.js +414 -0
- package/framework/core/compiler/plugins/actions-plugin.js +263 -0
- package/framework/core/compiler/plugins/compiler-plugin.d.ts +5 -0
- package/framework/core/compiler/plugins/compiler-plugin.js +45 -0
- package/framework/core/compiler/plugins/file-router-plugin.js +301 -0
- package/framework/core/compiler/plugins/router-plugin.js +196 -0
- package/framework/core/compiler/transformer.js +398 -0
- package/framework/core/component.js +61 -23
- package/framework/core/di.js +187 -0
- package/framework/core/directives.js +40 -96
- package/framework/core/evaluator.js +1 -1
- package/framework/core/h.js +430 -0
- package/framework/core/hydration.js +83 -0
- package/framework/core/reconcile.js +151 -0
- package/framework/core/signal.js +269 -0
- package/framework/core/store.js +322 -81
- package/framework/index.js +5 -3
- package/framework/router/file-router.js +1 -1
- package/framework/router/router.js +113 -21
- package/framework/server/actions.js +173 -0
- package/framework/server/actions.server.js +88 -0
- package/framework/server/index.js +18 -0
- package/framework/types/actions.d.ts +50 -0
- package/framework/types/actions.server.d.ts +36 -0
- package/framework/types/compiler/ast.d.ts +166 -0
- package/framework/types/compiler/errors-codes.d.ts +23 -0
- package/framework/types/compiler/errors.d.ts +43 -0
- package/framework/types/compiler/generator.d.ts +8 -0
- package/framework/types/compiler/h.d.ts +46 -0
- package/framework/types/compiler/lexer.d.ts +27 -0
- package/framework/types/compiler/main-compiler.d.ts +60 -0
- package/framework/types/compiler/parser.d.ts +4 -0
- package/framework/types/compiler/plugins/actions.d.ts +30 -0
- package/framework/types/compiler/plugins/compiler-plugin.d.ts +5 -0
- package/framework/types/compiler/plugins/file-router-plugins.d.ts +32 -0
- package/framework/types/compiler/plugins/router-plugin.d.ts +24 -0
- package/framework/types/compiler/transformer.d.ts +28 -0
- package/framework/types/component.d.ts +89 -88
- package/framework/types/di.d.ts +43 -0
- package/framework/types/hydration.d.ts +30 -0
- package/framework/types/index.d.ts +38 -22
- package/framework/types/reconcile.d.ts +7 -0
- package/framework/types/router.d.ts +11 -1
- package/framework/types/server.d.ts +11 -0
- package/framework/types/signal.d.ts +142 -79
- package/framework/types/store.d.ts +66 -8
- package/ghid_go_fusee_integrat_go1_26.txt +276 -0
- package/ghid_rust_fusee_rust_1_95_0.txt +1118 -0
- package/package.json +44 -36
- 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
|
+
}
|