fusee-framework 1.8.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 +64 -3
- package/app/benchmark/vite.config.js +23 -0
- package/auto-imports.d.ts +4 -0
- package/framework/auto-import-preset.js +3 -1
- 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 +34 -10
- package/framework/core/evaluator.js +1 -1
- package/framework/core/h.js +430 -0
- package/framework/core/hydration.js +83 -0
- package/framework/core/signal.js +17 -15
- 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/hydration.d.ts +30 -0
- package/framework/types/index.d.ts +6 -0
- package/framework/types/server.d.ts +11 -0
- package/framework/types/signal.d.ts +2 -7
- 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 -53
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
|
|
4
|
+
export function routerPlugin(options = {}) {
|
|
5
|
+
const {
|
|
6
|
+
typesOutput = 'src/types/router.d.ts',
|
|
7
|
+
pagesDir = 'src/pages',
|
|
8
|
+
strict = false,
|
|
9
|
+
} = options
|
|
10
|
+
|
|
11
|
+
return {
|
|
12
|
+
name: 'vite-plugin-fusee-router',
|
|
13
|
+
|
|
14
|
+
async generateBundle() {
|
|
15
|
+
let routes = []
|
|
16
|
+
|
|
17
|
+
try {
|
|
18
|
+
const { fileRouterPlugin, compileFileRoutes } = await import('./file-router-plugin.js')
|
|
19
|
+
const { globSync } = await import('fast-glob')
|
|
20
|
+
const files = globSync(`${pagesDir}/**/*.{js,ts,jsx,tsx}`, {
|
|
21
|
+
cwd: process.cwd()
|
|
22
|
+
})
|
|
23
|
+
routes = compileFileRoutes(files, { pagesDir })
|
|
24
|
+
} catch {
|
|
25
|
+
return
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const { warnings, errors } = validateRoutes(routes)
|
|
29
|
+
for (const w of warnings) this.warn(`[fusee router] ${w}`)
|
|
30
|
+
for (const e of errors) {
|
|
31
|
+
if (strict) this.error(`[fusee router] ${e}`)
|
|
32
|
+
else this.warn(`[fusee router] ${e}`)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (typesOutput) {
|
|
36
|
+
const types = generateRouterTypes(routes)
|
|
37
|
+
const absPath = path.resolve(process.cwd(), typesOutput)
|
|
38
|
+
fs.mkdirSync(path.dirname(absPath), { recursive: true })
|
|
39
|
+
fs.writeFileSync(absPath, types, 'utf8')
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function validateRoutes(routes) {
|
|
46
|
+
const warnings = []
|
|
47
|
+
const errors = []
|
|
48
|
+
_validateLevel(routes, '', warnings, errors)
|
|
49
|
+
return { warnings, errors }
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function _validateLevel(routes, prefix, warnings, errors) {
|
|
53
|
+
const seenStatic = new Map()
|
|
54
|
+
let dynamicSeg = null
|
|
55
|
+
let hasWildcard = false
|
|
56
|
+
|
|
57
|
+
for (let i = 0; i < routes.length; i++) {
|
|
58
|
+
const route = routes[i]
|
|
59
|
+
const fullPath = (prefix + '/' + route.path).replace(/\/+/g, '/') || '/'
|
|
60
|
+
|
|
61
|
+
if (hasWildcard && route.path !== '*') {
|
|
62
|
+
errors.push(`Route "${fullPath}" is defined after a wildcard "*" at the same level — it will never be reached`)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (route.path === '*') {
|
|
66
|
+
if (hasWildcard) {
|
|
67
|
+
errors.push(`Duplicate wildcard "*" route at level "${prefix || '/'}"`)
|
|
68
|
+
}
|
|
69
|
+
hasWildcard = true
|
|
70
|
+
continue
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const segment = route.path.split('/').filter(Boolean)[0] ?? ''
|
|
74
|
+
|
|
75
|
+
if (!segment.startsWith(':') && !segment.startsWith('*')) {
|
|
76
|
+
if (seenStatic.has(route.path)) {
|
|
77
|
+
errors.push(`Duplicate route path "${fullPath}" — defined more than once at the same level`)
|
|
78
|
+
} else {
|
|
79
|
+
seenStatic.set(route.path, route)
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (segment.startsWith(':')) {
|
|
84
|
+
if (dynamicSeg && dynamicSeg !== segment) {
|
|
85
|
+
warnings.push(`Conflicting dynamic segments at level "${prefix || '/'}": ` + `"${dynamicSeg}" and "${segment}" — only one dynamic segment per level is predictable. ` + `Consider renaming them consistently.`)
|
|
86
|
+
}
|
|
87
|
+
dynamicSeg = segment
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (route.children !== undefined && route.children.length === 0) {
|
|
91
|
+
warnings.push(`Layout route "${fullPath}" has no child routes — ` + `<router-view> inside it will never render`)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (route.path === '' || route.path === '/') {
|
|
95
|
+
const conflict = routes.find(
|
|
96
|
+
r => r !== route && (r.path === '' || r.path === '/')
|
|
97
|
+
)
|
|
98
|
+
if (conflict) {
|
|
99
|
+
errors.push(`Duplicate index route at level "${prefix || '/'}" — ` + `both "" and "/" resolve to the same path`)
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (route.children?.length) {
|
|
104
|
+
_validateLevel(route.children, fullPath, warnings, errors)
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function generateRouterTypes(routes) {
|
|
110
|
+
const allPaths = []
|
|
111
|
+
const paramMap = {}
|
|
112
|
+
|
|
113
|
+
_collectPaths(routes, '', allPaths, paramMap)
|
|
114
|
+
|
|
115
|
+
const staticPaths = allPaths.filter(p => !p.includes(':') && p !== '*')
|
|
116
|
+
const dynamicPaths = allPaths.filter(p => p.includes(':'))
|
|
117
|
+
|
|
118
|
+
const pathUnion = [...staticPaths, ...dynamicPaths]
|
|
119
|
+
.map(p => JSON.stringify(p))
|
|
120
|
+
.join('\n | ')
|
|
121
|
+
|
|
122
|
+
const paramEntries = Object.entries(paramMap)
|
|
123
|
+
.filter(([, params]) => Object.keys(params).length > 0)
|
|
124
|
+
.map(([p, params]) => {
|
|
125
|
+
const fields = Object.entries(params)
|
|
126
|
+
.map(([k]) => ` ${k}: string`)
|
|
127
|
+
.join('\n')
|
|
128
|
+
return ` ${JSON.stringify(p)}: {\n${fields}\n }`
|
|
129
|
+
})
|
|
130
|
+
.join('\n')
|
|
131
|
+
|
|
132
|
+
return [
|
|
133
|
+
'// [fusée] Auto-generated router types — do not edit',
|
|
134
|
+
'// Generated by compiler/plugins/router-plugin.js',
|
|
135
|
+
'',
|
|
136
|
+
'/**',
|
|
137
|
+
' * All valid application route paths.',
|
|
138
|
+
' * Auto-completed in navigate(), <a href>, and useRoute().',
|
|
139
|
+
' */',
|
|
140
|
+
'export type AppRoute =',
|
|
141
|
+
` | ${pathUnion || "'/'"}`,
|
|
142
|
+
'',
|
|
143
|
+
'/**',
|
|
144
|
+
' * Dynamic route params per path.',
|
|
145
|
+
' * Only paths with `:param` segments appear here.',
|
|
146
|
+
' */',
|
|
147
|
+
'export type RouteParams = {',
|
|
148
|
+
paramEntries,
|
|
149
|
+
'}',
|
|
150
|
+
'',
|
|
151
|
+
'/**',
|
|
152
|
+
' * Extract params type for a specific route.',
|
|
153
|
+
' * @example',
|
|
154
|
+
' * type Params = ParamsFor<\'/users/:id\'> // { id: string }',
|
|
155
|
+
' */',
|
|
156
|
+
'export type ParamsFor<T extends AppRoute> =',
|
|
157
|
+
' T extends keyof RouteParams ? RouteParams[T] : Record<string, never>',
|
|
158
|
+
'',
|
|
159
|
+
'/**',
|
|
160
|
+
' * Type-safe navigate() — accepts only valid AppRoute paths.',
|
|
161
|
+
' * Import from fusee/router to get autocomplete.',
|
|
162
|
+
' */',
|
|
163
|
+
'export declare function navigate(path: AppRoute): void',
|
|
164
|
+
'',
|
|
165
|
+
'/**',
|
|
166
|
+
' * Current route params, typed as a record.',
|
|
167
|
+
' * Use ParamsFor<T> for specific route param shapes.',
|
|
168
|
+
' */',
|
|
169
|
+
'export declare function useRouteParams<T extends AppRoute = AppRoute>(): ParamsFor<T>',
|
|
170
|
+
'',
|
|
171
|
+
].join('\n')
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function _collectPaths(routes, prefix, allPaths, paramMap) {
|
|
175
|
+
for (const route of routes) {
|
|
176
|
+
const seg = route.path
|
|
177
|
+
const fullPath = seg === '' ? (prefix || '/') : prefix + (prefix.endsWith('/') ? '' : '/') + seg
|
|
178
|
+
const normalized = fullPath.replace(/\/+/g, '/') || '/'
|
|
179
|
+
|
|
180
|
+
if (normalized && !allPaths.includes(normalized)) {
|
|
181
|
+
allPaths.push(normalized)
|
|
182
|
+
|
|
183
|
+
const params = {}
|
|
184
|
+
const re = /:([^/]+)/g
|
|
185
|
+
let m
|
|
186
|
+
while ((m = re.exec(normalized)) !== null) {
|
|
187
|
+
params[m[1]] = 'string'
|
|
188
|
+
}
|
|
189
|
+
paramMap[normalized] = params
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (route.children?.length) {
|
|
193
|
+
_collectPaths(route.children, normalized === '/' ? '' : normalized, allPaths, paramMap)
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
import {NodeType, createExpression, isDirective, findDirective, isStaticText, cloneNode,} from './ast.js'
|
|
2
|
+
import { CompileWarning } from './errors.js'
|
|
3
|
+
import { ErrorCode } from './errors-list.js'
|
|
4
|
+
|
|
5
|
+
export function transform(ast, options = {}) {
|
|
6
|
+
const ctx = {
|
|
7
|
+
components: options.components ?? new Set(),
|
|
8
|
+
source: options.source ?? '',
|
|
9
|
+
hoisted: [],
|
|
10
|
+
warnings: [],
|
|
11
|
+
scope: new Set(options.scope ?? []),
|
|
12
|
+
scopeStack: [],
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
markStaticPass(ast, ctx)
|
|
16
|
+
walkChildren(ast, node => {
|
|
17
|
+
if (node.children) chainConditionalsInChildren(node.children, ctx)
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
walkChildren(ast, node => {
|
|
21
|
+
validateFor(node, ctx)
|
|
22
|
+
validateModel(node, ctx)
|
|
23
|
+
validateComponents(node, ctx)
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
if (ctx.scope.size > 0) {
|
|
27
|
+
analyseScope(ast, ctx, new Set(ctx.scope))
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
ast._hoisted = ctx.hoisted
|
|
31
|
+
|
|
32
|
+
return { ast, warnings: ctx.warnings }
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Step 1 & 2: static marking + hoisting
|
|
36
|
+
|
|
37
|
+
function markStaticPass(node, ctx) {
|
|
38
|
+
if (!node) return false
|
|
39
|
+
|
|
40
|
+
if (node.type === NodeType.TEXT) {
|
|
41
|
+
node.isStatic = true
|
|
42
|
+
return true
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (node.type === NodeType.EXPRESSION) {
|
|
46
|
+
node.isStatic = isLiteralExpression(node.content)
|
|
47
|
+
return node.isStatic
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (node.type === NodeType.INTERPOLATION) {
|
|
51
|
+
node.isStatic = markStaticPass(node.expression, ctx)
|
|
52
|
+
return node.isStatic
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (node.type === NodeType.ELEMENT) {
|
|
56
|
+
const hasOnce = node.props.some(p => isDirective(p, 'once'))
|
|
57
|
+
if (hasOnce) {
|
|
58
|
+
node.isStatic = true
|
|
59
|
+
node.hoisted = true
|
|
60
|
+
ctx.hoisted.push(node)
|
|
61
|
+
return true
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const hasDynamic = node.props.some(p => p.type === NodeType.DIRECTIVE || p.type === NodeType.BINDING || p.type === NodeType.EVENT)
|
|
65
|
+
|
|
66
|
+
if (hasDynamic) {
|
|
67
|
+
node.isStatic = false
|
|
68
|
+
if (node.children) node.children.forEach(c => markStaticPass(c, ctx))
|
|
69
|
+
return false
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const hasInterpolatedAttr = node.props.some(p => p.type === NodeType.ATTRIBUTE && typeof p.value === 'string' && p.value.includes('{{'))
|
|
73
|
+
if (hasInterpolatedAttr) {
|
|
74
|
+
node.isStatic = false
|
|
75
|
+
if (node.children) node.children.forEach(c => markStaticPass(c, ctx))
|
|
76
|
+
return false
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const childrenStatic = node.children ? node.children.every(c => markStaticPass(c, ctx)) : true
|
|
80
|
+
|
|
81
|
+
node.isStatic = childrenStatic
|
|
82
|
+
|
|
83
|
+
if (node.isStatic && node.children && node.children.length >= 2) {
|
|
84
|
+
node.hoisted = true
|
|
85
|
+
ctx.hoisted.push(node)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return node.isStatic
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (node.type === NodeType.COMPONENT) {
|
|
92
|
+
node.isStatic = false
|
|
93
|
+
if (node.slots) {
|
|
94
|
+
for (const slotChildren of Object.values(node.slots)) {
|
|
95
|
+
if (Array.isArray(slotChildren))
|
|
96
|
+
slotChildren.forEach(c => markStaticPass(c, ctx))
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return false
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (node.type === NodeType.ROOT) {
|
|
103
|
+
if (node.children) node.children.forEach(c => markStaticPass(c, ctx))
|
|
104
|
+
return false
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return false
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Step 3: chain f-if / f-else-if / f-else
|
|
111
|
+
|
|
112
|
+
function chainConditionalsInChildren(children, ctx) {
|
|
113
|
+
for (let i = 0; i < children.length; i++) {
|
|
114
|
+
const node = children[i]
|
|
115
|
+
if (!isElementOrComponent(node)) continue
|
|
116
|
+
|
|
117
|
+
const ifDir = findDirective(node, 'if')
|
|
118
|
+
if (!ifDir) {
|
|
119
|
+
const elseDir = findDirective(node, 'else') ?? findDirective(node, 'else-if')
|
|
120
|
+
if (elseDir) {
|
|
121
|
+
ctx.warnings.push(new CompileWarning(ErrorCode.V_ELSE_NO_IF, elseDir.loc, ctx.source))
|
|
122
|
+
}
|
|
123
|
+
continue
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const branches = [{ condition: ifDir.expression, node }]
|
|
127
|
+
let j = i + 1
|
|
128
|
+
|
|
129
|
+
while (j < children.length) {
|
|
130
|
+
const next = children[j]
|
|
131
|
+
if (!isElementOrComponent(next)) {
|
|
132
|
+
j++
|
|
133
|
+
continue
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const elseIfDir = findDirective(next, 'else-if')
|
|
137
|
+
const elseDir = findDirective(next, 'else')
|
|
138
|
+
|
|
139
|
+
if (elseIfDir) {
|
|
140
|
+
branches.push({ condition: elseIfDir.expression, node: next })
|
|
141
|
+
j++
|
|
142
|
+
} else if (elseDir) {
|
|
143
|
+
branches.push({ condition: null, node: next })
|
|
144
|
+
j++
|
|
145
|
+
break
|
|
146
|
+
} else {
|
|
147
|
+
break
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const ifNode = {
|
|
152
|
+
type: 'If',
|
|
153
|
+
branches,
|
|
154
|
+
loc: node.loc,
|
|
155
|
+
isStatic: false,
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
children.splice(i, j - i, ifNode)
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Step 4: f-for :key validation
|
|
163
|
+
|
|
164
|
+
function validateFor(node, ctx) {
|
|
165
|
+
if (node.type !== NodeType.ELEMENT && node.type !== NodeType.COMPONENT) return
|
|
166
|
+
|
|
167
|
+
const forDir = findDirective(node, 'for')
|
|
168
|
+
if (!forDir) return
|
|
169
|
+
|
|
170
|
+
const hasKey = node.props.some(p => p.type === NodeType.BINDING && p.name === 'key')
|
|
171
|
+
if (!hasKey) {
|
|
172
|
+
ctx.warnings.push(new CompileWarning(ErrorCode.FOR_MISSING_KEY, forDir.loc, ctx.source, node.tag ?? node.name))
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Step 5: f-model element validation
|
|
177
|
+
|
|
178
|
+
const MODEL_ALLOWED = new Set(['input', 'textarea', 'select'])
|
|
179
|
+
|
|
180
|
+
function validateModel(node, ctx) {
|
|
181
|
+
if (node.type !== NodeType.ELEMENT) return
|
|
182
|
+
const modelDir = findDirective(node, 'model')
|
|
183
|
+
if (!modelDir) return
|
|
184
|
+
if (!MODEL_ALLOWED.has(node.tag.toLowerCase())) {
|
|
185
|
+
ctx.warnings.push(new CompileWarning(ErrorCode.MODEL_ON_NON_INPUT, modelDir.loc, ctx.source, node.tag))
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Step 6: component registration validation
|
|
190
|
+
|
|
191
|
+
function validateComponents(node, ctx) {
|
|
192
|
+
if (node.type !== NodeType.COMPONENT) return
|
|
193
|
+
if (!ctx.components.has(node.name)) {
|
|
194
|
+
ctx.warnings.push(new CompileWarning(ErrorCode.COMPONENT_NOT_REGISTERED, node.loc, ctx.source, node.name))
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// ─── Step 7 & 8: Semantic analysis — scope + expression validation ────────────
|
|
199
|
+
|
|
200
|
+
function analyseScope(node, ctx, localScope) {
|
|
201
|
+
if (!node) return
|
|
202
|
+
|
|
203
|
+
switch (node.type) {
|
|
204
|
+
case NodeType.ROOT:
|
|
205
|
+
for (const child of node.children ?? []) {
|
|
206
|
+
analyseScope(child, ctx, localScope)
|
|
207
|
+
}
|
|
208
|
+
break
|
|
209
|
+
|
|
210
|
+
case NodeType.ELEMENT:
|
|
211
|
+
case NodeType.COMPONENT: {
|
|
212
|
+
const forDir = findDirective(node, 'for')
|
|
213
|
+
const childScope = forDir ? _extendScopeFromFor(localScope, forDir.arg) : localScope
|
|
214
|
+
|
|
215
|
+
for (const prop of node.props ?? []) {
|
|
216
|
+
_validateProp(prop, ctx, childScope)
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
for (const child of node.children ?? []) {
|
|
220
|
+
analyseScope(child, ctx, childScope)
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (node.slots) {
|
|
224
|
+
for (const slotChildren of Object.values(node.slots)) {
|
|
225
|
+
if (Array.isArray(slotChildren)) {
|
|
226
|
+
for (const c of slotChildren)
|
|
227
|
+
analyseScope(c, ctx, childScope)
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
break
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
case NodeType.INTERPOLATION:
|
|
235
|
+
_validateExpression(node.expression, ctx, localScope)
|
|
236
|
+
break
|
|
237
|
+
|
|
238
|
+
case 'If':
|
|
239
|
+
for (const branch of node.branches ?? []) {
|
|
240
|
+
if (branch.condition) _validateExpression(branch.condition, ctx, localScope)
|
|
241
|
+
analyseScope(branch.node, ctx, localScope)
|
|
242
|
+
}
|
|
243
|
+
break
|
|
244
|
+
|
|
245
|
+
case NodeType.TEXT:
|
|
246
|
+
case NodeType.EXPRESSION:
|
|
247
|
+
case NodeType.SLOT_OUTLET:
|
|
248
|
+
case NodeType.SLOT_CONTENT:
|
|
249
|
+
break
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function _validateProp(prop, ctx, scope) {
|
|
254
|
+
switch (prop.type) {
|
|
255
|
+
case NodeType.BINDING:
|
|
256
|
+
_validateExpression(prop.expression, ctx, scope)
|
|
257
|
+
break
|
|
258
|
+
|
|
259
|
+
case NodeType.EVENT:
|
|
260
|
+
_validateExpression(prop.expression, ctx, scope, { isHandler: true })
|
|
261
|
+
break
|
|
262
|
+
|
|
263
|
+
case NodeType.DIRECTIVE:
|
|
264
|
+
if (prop.name === 'for')
|
|
265
|
+
break
|
|
266
|
+
if (prop.expression)
|
|
267
|
+
_validateExpression(prop.expression, ctx, scope)
|
|
268
|
+
break
|
|
269
|
+
|
|
270
|
+
case NodeType.ATTRIBUTE:
|
|
271
|
+
break
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function _validateExpression(exprNode, ctx, scope, opts = {}) {
|
|
276
|
+
if (!exprNode || !exprNode.content) return
|
|
277
|
+
if (exprNode.isStatic) return
|
|
278
|
+
|
|
279
|
+
const raw = exprNode.content.trim()
|
|
280
|
+
if (!raw) return
|
|
281
|
+
|
|
282
|
+
const identifiers = _extractIdentifiers(raw)
|
|
283
|
+
|
|
284
|
+
for (const id of identifiers) {
|
|
285
|
+
if (JS_GLOBALS.has(id)) continue
|
|
286
|
+
if (opts.isHandler && id === '$event') continue
|
|
287
|
+
|
|
288
|
+
if (!scope.has(id)) {
|
|
289
|
+
ctx.warnings.push(new CompileWarning(
|
|
290
|
+
ErrorCode.UNKNOWN_IDENTIFIER,
|
|
291
|
+
exprNode.loc,
|
|
292
|
+
ctx.source,
|
|
293
|
+
id,
|
|
294
|
+
raw
|
|
295
|
+
))
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
if (exprNode._isForKey && isLiteralExpression(raw)) {
|
|
300
|
+
ctx.warnings.push(new CompileWarning(
|
|
301
|
+
ErrorCode.STATIC_FOR_KEY,
|
|
302
|
+
exprNode.loc,
|
|
303
|
+
ctx.source,
|
|
304
|
+
raw
|
|
305
|
+
))
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function _extractIdentifiers(expr) {
|
|
310
|
+
const stripped = expr
|
|
311
|
+
.replace(/'[^'\\]*(?:\\.[^'\\]*)*'/g, '""')
|
|
312
|
+
.replace(/"[^"\\]*(?:\\.[^"\\]*)*"/g, '""')
|
|
313
|
+
.replace(/`[^`\\]*(?:\\.[^`\\]*)*`/g, '""')
|
|
314
|
+
|
|
315
|
+
const found = new Set()
|
|
316
|
+
const RE = /(?<![a-zA-Z0-9_$])([a-zA-Z_$][a-zA-Z0-9_$]*)(?![a-zA-Z0-9_$])/g
|
|
317
|
+
let m
|
|
318
|
+
|
|
319
|
+
while ((m = RE.exec(stripped)) !== null) {
|
|
320
|
+
const id = m[1]
|
|
321
|
+
const idx = m.index
|
|
322
|
+
|
|
323
|
+
if (JS_KEYWORDS.has(id)) continue
|
|
324
|
+
if (idx > 0 && stripped[idx - 1] === '.') continue
|
|
325
|
+
|
|
326
|
+
found.add(id)
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
return found
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function _extendScopeFromFor(parentScope, forArg) {
|
|
333
|
+
if (!forArg)
|
|
334
|
+
return parentScope
|
|
335
|
+
|
|
336
|
+
const extended = new Set(parentScope)
|
|
337
|
+
|
|
338
|
+
if (forArg.item)
|
|
339
|
+
extended.add(forArg.item)
|
|
340
|
+
if (forArg.index)
|
|
341
|
+
extended.add(forArg.index)
|
|
342
|
+
|
|
343
|
+
return extended
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
const JS_GLOBALS = new Set([
|
|
347
|
+
'Math', 'Object', 'Array', 'String', 'Number', 'Boolean', 'Symbol',
|
|
348
|
+
'Date', 'RegExp', 'Map', 'Set', 'WeakMap', 'WeakSet', 'WeakRef',
|
|
349
|
+
'Promise', 'Proxy', 'Reflect', 'JSON', 'Error', 'TypeError',
|
|
350
|
+
'RangeError', 'parseInt', 'parseFloat', 'isNaN', 'isFinite',
|
|
351
|
+
'encodeURIComponent', 'decodeURIComponent', 'encodeURI', 'decodeURI',
|
|
352
|
+
'console', 'window', 'document', 'globalThis', 'undefined', 'null',
|
|
353
|
+
'Infinity', 'NaN', 'setTimeout', 'clearTimeout', 'setInterval',
|
|
354
|
+
'clearInterval', 'queueMicrotask', 'fetch', 'true', 'false'
|
|
355
|
+
])
|
|
356
|
+
|
|
357
|
+
const JS_KEYWORDS = new Set([
|
|
358
|
+
'if', 'else', 'for', 'while', 'do', 'switch', 'case', 'break',
|
|
359
|
+
'continue', 'return', 'throw', 'try', 'catch', 'finally',
|
|
360
|
+
'new', 'delete', 'typeof', 'instanceof', 'in', 'of', 'void',
|
|
361
|
+
'const', 'let', 'var', 'function', 'class', 'import', 'export',
|
|
362
|
+
'default', 'extends', 'super', 'this', 'yield', 'await', 'async',
|
|
363
|
+
'true', 'false', 'null', 'undefined'
|
|
364
|
+
])
|
|
365
|
+
|
|
366
|
+
function walkChildren(node, visitor) {
|
|
367
|
+
if (!node) return
|
|
368
|
+
visitor(node)
|
|
369
|
+
|
|
370
|
+
if (node.children) {
|
|
371
|
+
for (const child of node.children)
|
|
372
|
+
walkChildren(child, visitor)
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
if (node.branches) {
|
|
376
|
+
for (const branch of node.branches)
|
|
377
|
+
walkChildren(branch.node, visitor)
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
if (node.slots) {
|
|
381
|
+
for (const slotChildren of Object.values(node.slots)) {
|
|
382
|
+
if (Array.isArray(slotChildren)) {
|
|
383
|
+
for (const c of slotChildren)
|
|
384
|
+
walkChildren(c, visitor)
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function isElementOrComponent(node) {
|
|
391
|
+
return node?.type === NodeType.ELEMENT || node?.type === NodeType.COMPONENT
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
const LITERAL_RE = /^(?:true|false|null|undefined|-?\d[\d._]*(?:n)?|'[^']*'|"[^"]*"|`[^`]*`)$/
|
|
395
|
+
|
|
396
|
+
function isLiteralExpression(expr) {
|
|
397
|
+
return LITERAL_RE.test(expr.trim())
|
|
398
|
+
}
|
|
@@ -157,17 +157,41 @@ export function defineComponent(options) {
|
|
|
157
157
|
function render(container) {
|
|
158
158
|
instance._element = container
|
|
159
159
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
160
|
+
if (options.render) {
|
|
161
|
+
const nodes = options.render(result, options.components || {})
|
|
162
|
+
container.innerHTML = ''
|
|
163
|
+
|
|
164
|
+
function mountNode(fnode) {
|
|
165
|
+
if (!fnode || !fnode.node) return
|
|
166
|
+
container.appendChild(fnode.node)
|
|
167
|
+
|
|
168
|
+
if (fnode.effects) {
|
|
169
|
+
instance._effects.push(...fnode.effects)
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (fnode.children) {
|
|
173
|
+
for (const child of fnode.children) {
|
|
174
|
+
mountNode(child)
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
for (const node of nodes) {
|
|
180
|
+
mountNode(node)
|
|
181
|
+
}
|
|
182
|
+
} else {
|
|
183
|
+
const resolvedTemplate = resolveSlots(result.template, slots)
|
|
184
|
+
const { effects } = mountTemplate(
|
|
185
|
+
resolvedTemplate,
|
|
186
|
+
container,
|
|
187
|
+
result,
|
|
188
|
+
options.components || {}
|
|
189
|
+
)
|
|
190
|
+
instance._effects.push(...effects)
|
|
191
|
+
}
|
|
168
192
|
|
|
169
|
-
instance.
|
|
170
|
-
|
|
193
|
+
for (const hook of instance._mountHooks)
|
|
194
|
+
hook()
|
|
171
195
|
|
|
172
196
|
return instance
|
|
173
197
|
}
|
|
@@ -37,7 +37,7 @@ export function evaluateExpression(expr, context, extraContext = {}, unwrapSigna
|
|
|
37
37
|
).join('\n')
|
|
38
38
|
let wrappedExpr = expr
|
|
39
39
|
for (const id of keysToUnwrap) {
|
|
40
|
-
wrappedExpr = wrappedExpr.replace(new RegExp('\\b' + id + '\\b(
|
|
40
|
+
wrappedExpr = wrappedExpr.replace(new RegExp('\\b' + id + '\\b(?!\\(|\\.loading\\b|\\.error\\b|\\.isFetching\\b)', 'g'), '__' + id)
|
|
41
41
|
}
|
|
42
42
|
body = (unwrapStatements ? unwrapStatements + '\n' : '') + `return ${wrappedExpr}`
|
|
43
43
|
}
|