fusee-framework 1.9.0 → 1.9.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.
Files changed (62) hide show
  1. package/README.md +1 -1
  2. package/bin/create-fusee-linux +0 -0
  3. package/bin/create-fusee-mac-arm64 +0 -0
  4. package/bin/create-fusee-mac-intel +0 -0
  5. package/bin/create-fusee-win.exe +0 -0
  6. package/bin/create-fusee.exe +0 -0
  7. package/create-fusee-go/internal/assets/embed/framework/auto-import-preset.js +15 -9
  8. package/create-fusee-go/internal/assets/embed/framework/core/async-context.js +27 -0
  9. package/create-fusee-go/internal/assets/embed/framework/core/compiler/ast.js +167 -0
  10. package/create-fusee-go/internal/assets/embed/framework/core/compiler/errors-list.js +47 -0
  11. package/create-fusee-go/internal/assets/embed/framework/core/compiler/errors.js +107 -0
  12. package/create-fusee-go/internal/assets/embed/framework/core/compiler/generator.js +363 -0
  13. package/create-fusee-go/internal/assets/embed/framework/core/compiler/lexer.js +347 -0
  14. package/create-fusee-go/internal/assets/embed/framework/core/compiler/main-compiler.js +115 -0
  15. package/create-fusee-go/internal/assets/embed/framework/core/compiler/parser.js +414 -0
  16. package/create-fusee-go/internal/assets/embed/framework/core/compiler/plugins/actions-plugin.js +263 -0
  17. package/create-fusee-go/internal/assets/embed/framework/core/compiler/plugins/compiler-plugin.d.ts +5 -0
  18. package/create-fusee-go/internal/assets/embed/framework/core/compiler/plugins/compiler-plugin.js +45 -0
  19. package/create-fusee-go/internal/assets/embed/framework/core/compiler/plugins/file-router-plugin.js +301 -0
  20. package/create-fusee-go/internal/assets/embed/framework/core/compiler/plugins/router-plugin.js +196 -0
  21. package/create-fusee-go/internal/assets/embed/framework/core/compiler/transformer.js +398 -0
  22. package/create-fusee-go/internal/assets/embed/framework/core/component.js +61 -23
  23. package/create-fusee-go/internal/assets/embed/framework/core/di.js +187 -0
  24. package/create-fusee-go/internal/assets/embed/framework/core/directives.js +40 -96
  25. package/create-fusee-go/internal/assets/embed/framework/core/evaluator.js +1 -1
  26. package/create-fusee-go/internal/assets/embed/framework/core/h.js +430 -0
  27. package/create-fusee-go/internal/assets/embed/framework/core/hydration.js +83 -0
  28. package/create-fusee-go/internal/assets/embed/framework/core/reconcile.js +151 -0
  29. package/create-fusee-go/internal/assets/embed/framework/core/signal.js +269 -0
  30. package/create-fusee-go/internal/assets/embed/framework/core/store.js +322 -81
  31. package/create-fusee-go/internal/assets/embed/framework/index.js +5 -3
  32. package/create-fusee-go/internal/assets/embed/framework/router/file-router.js +1 -1
  33. package/create-fusee-go/internal/assets/embed/framework/router/router.js +113 -21
  34. package/create-fusee-go/internal/assets/embed/framework/server/actions.js +173 -0
  35. package/create-fusee-go/internal/assets/embed/framework/server/actions.server.js +88 -0
  36. package/create-fusee-go/internal/assets/embed/framework/server/index.js +18 -0
  37. package/create-fusee-go/internal/assets/embed/framework/types/actions.d.ts +50 -0
  38. package/create-fusee-go/internal/assets/embed/framework/types/actions.server.d.ts +36 -0
  39. package/create-fusee-go/internal/assets/embed/framework/types/compiler/ast.d.ts +166 -0
  40. package/create-fusee-go/internal/assets/embed/framework/types/compiler/errors-codes.d.ts +23 -0
  41. package/create-fusee-go/internal/assets/embed/framework/types/compiler/errors.d.ts +43 -0
  42. package/create-fusee-go/internal/assets/embed/framework/types/compiler/generator.d.ts +8 -0
  43. package/create-fusee-go/internal/assets/embed/framework/types/compiler/h.d.ts +46 -0
  44. package/create-fusee-go/internal/assets/embed/framework/types/compiler/lexer.d.ts +27 -0
  45. package/create-fusee-go/internal/assets/embed/framework/types/compiler/main-compiler.d.ts +60 -0
  46. package/create-fusee-go/internal/assets/embed/framework/types/compiler/parser.d.ts +4 -0
  47. package/create-fusee-go/internal/assets/embed/framework/types/compiler/plugins/actions.d.ts +30 -0
  48. package/create-fusee-go/internal/assets/embed/framework/types/compiler/plugins/compiler-plugin.d.ts +5 -0
  49. package/create-fusee-go/internal/assets/embed/framework/types/compiler/plugins/file-router-plugins.d.ts +32 -0
  50. package/create-fusee-go/internal/assets/embed/framework/types/compiler/plugins/router-plugin.d.ts +24 -0
  51. package/create-fusee-go/internal/assets/embed/framework/types/compiler/transformer.d.ts +28 -0
  52. package/create-fusee-go/internal/assets/embed/framework/types/component.d.ts +89 -88
  53. package/create-fusee-go/internal/assets/embed/framework/types/di.d.ts +43 -0
  54. package/create-fusee-go/internal/assets/embed/framework/types/hydration.d.ts +30 -0
  55. package/create-fusee-go/internal/assets/embed/framework/types/index.d.ts +38 -22
  56. package/create-fusee-go/internal/assets/embed/framework/types/reconcile.d.ts +7 -0
  57. package/create-fusee-go/internal/assets/embed/framework/types/router.d.ts +11 -1
  58. package/create-fusee-go/internal/assets/embed/framework/types/server.d.ts +11 -0
  59. package/create-fusee-go/internal/assets/embed/framework/types/signal.d.ts +142 -79
  60. package/create-fusee-go/internal/assets/embed/framework/types/store.d.ts +66 -8
  61. package/package.json +1 -1
  62. package/create-fusee-go/internal/assets/embed/framework/bin/create-fusee.js +0 -199
@@ -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
+ }