@doweljs/vite 0.0.0-stage → 0.1.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/LICENSE +21 -0
- package/README.md +47 -2
- package/dist/analysis/analyze.d.ts +62 -0
- package/dist/analysis/analyze.d.ts.map +1 -0
- package/dist/analysis/analyze.js +396 -0
- package/dist/analysis/analyze.js.map +1 -0
- package/dist/analysis/registry.d.ts +74 -0
- package/dist/analysis/registry.d.ts.map +1 -0
- package/dist/analysis/registry.js +195 -0
- package/dist/analysis/registry.js.map +1 -0
- package/dist/index.d.ts +48 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +112 -0
- package/dist/index.js.map +1 -0
- package/dist/inject.d.ts +21 -0
- package/dist/inject.d.ts.map +1 -0
- package/dist/inject.js +33 -0
- package/dist/inject.js.map +1 -0
- package/dist/overlay.d.ts +15 -0
- package/dist/overlay.d.ts.map +1 -0
- package/dist/overlay.js +19 -0
- package/dist/overlay.js.map +1 -0
- package/package.json +39 -5
- package/src/analysis/analyze.ts +475 -0
- package/src/analysis/registry.ts +239 -0
- package/src/index.ts +155 -0
- package/src/inject.ts +48 -0
- package/src/overlay.ts +27 -0
|
@@ -0,0 +1,475 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Static analysis of a module's dowel declarations.
|
|
3
|
+
*
|
|
4
|
+
* Only top-level `const X = service(...)` / `slot(...)()` declarations are
|
|
5
|
+
* recognized. That is deliberate: a dependency graph declared at the top
|
|
6
|
+
* level of a module is what makes hot replacement and devtools possible without running
|
|
7
|
+
* the code.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { ESTree } from 'vite'
|
|
11
|
+
import { parseSync } from 'vite'
|
|
12
|
+
|
|
13
|
+
/** A reference a static graph can follow: a local identifier, or a member of a namespace import (`ns.Name`). */
|
|
14
|
+
export interface Ref {
|
|
15
|
+
readonly local: string
|
|
16
|
+
readonly member: string | null
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface DepRef {
|
|
20
|
+
readonly key: string
|
|
21
|
+
/** What the dependency refers to; `null` when the edge is kept but cannot be resolved statically. */
|
|
22
|
+
readonly ref: Ref | null
|
|
23
|
+
readonly optional: boolean
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface Declaration {
|
|
27
|
+
readonly local: string
|
|
28
|
+
readonly kind: 'slot' | 'service' | 'scope' | 'execution'
|
|
29
|
+
readonly name: string
|
|
30
|
+
/** The scope name when it is known statically: `root` for a slot or scope declared without one. */
|
|
31
|
+
readonly scope: string | null
|
|
32
|
+
/** A slot's or service's `scope` option, or a scope's parent. */
|
|
33
|
+
readonly scopeRef: Ref | null
|
|
34
|
+
/** Whether a slot is a multi-binding; null when its options cannot be read. */
|
|
35
|
+
readonly many: boolean | null
|
|
36
|
+
readonly deps: readonly DepRef[]
|
|
37
|
+
/** Offsets of the declaring call in the code this plugin received, after earlier transforms. */
|
|
38
|
+
readonly start: number
|
|
39
|
+
readonly end: number
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
interface ImportRef {
|
|
43
|
+
readonly source: string
|
|
44
|
+
/** Exported name in the source module (`default` for default imports). */
|
|
45
|
+
readonly imported: string
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface ModuleAnalysis {
|
|
49
|
+
readonly declarations: readonly Declaration[]
|
|
50
|
+
/** Local name to the import it comes from, for resolving dependency references. */
|
|
51
|
+
readonly imports: ReadonlyMap<string, ImportRef>
|
|
52
|
+
/** Exported name to local name. */
|
|
53
|
+
readonly exports: ReadonlyMap<string, string>
|
|
54
|
+
/** Exported name to what it re-exports: `export { Db } from '../db'`, `export * as ns from`. */
|
|
55
|
+
readonly reexports: ReadonlyMap<string, ImportRef>
|
|
56
|
+
/** Sources of `export * from` statements, searched in order for names not exported otherwise. */
|
|
57
|
+
readonly stars: readonly string[]
|
|
58
|
+
/**
|
|
59
|
+
* True when every export is a dowel declaration, so the module can accept
|
|
60
|
+
* its own hot updates without leaving importers with stale values.
|
|
61
|
+
*/
|
|
62
|
+
readonly selfContained: boolean
|
|
63
|
+
/** True when the module calls `container()`, which makes it an app root. */
|
|
64
|
+
readonly buildsContainer: boolean
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
type Node = ESTree.Node
|
|
68
|
+
|
|
69
|
+
/** The names a module may import from dowel, mapped from their local aliases. */
|
|
70
|
+
type DowelImports = ReadonlyMap<string, DowelExport>
|
|
71
|
+
|
|
72
|
+
const DOWEL_EXPORTS = ['service', 'slot', 'optional', 'container', 'scope', 'execution'] as const
|
|
73
|
+
type DowelExport = (typeof DOWEL_EXPORTS)[number]
|
|
74
|
+
|
|
75
|
+
const isDowelExport = (name: string): name is DowelExport =>
|
|
76
|
+
(DOWEL_EXPORTS as readonly string[]).includes(name)
|
|
77
|
+
|
|
78
|
+
/** Where a declaration lives: a statically known scope name, or a reference to a scope. */
|
|
79
|
+
type Placement = Pick<Declaration, 'scope' | 'scopeRef'>
|
|
80
|
+
|
|
81
|
+
const REEXPORT = /\bexport\s*(?:\*|\{[^}]*\})\s*(?:as\s+\w+\s*)?from\b/
|
|
82
|
+
|
|
83
|
+
/** Cheap pre-check so most modules never reach the parser: they neither use dowel nor re-export. */
|
|
84
|
+
export function mayDeclare(code: string): boolean {
|
|
85
|
+
return code.includes('@doweljs/core') || REEXPORT.test(code)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function analyze(id: string, code: string): ModuleAnalysis | null {
|
|
89
|
+
const { program, errors } = parseSync(id, code)
|
|
90
|
+
if (errors.length > 0) return null
|
|
91
|
+
|
|
92
|
+
const dowel = collectDowelImports(program)
|
|
93
|
+
const declarations: Declaration[] = []
|
|
94
|
+
const imports = new Map<string, ImportRef>()
|
|
95
|
+
const exports = new Map<string, string>()
|
|
96
|
+
const reexports = new Map<string, ImportRef>()
|
|
97
|
+
const stars: string[] = []
|
|
98
|
+
// Whether the module exports anything besides its dowel declarations.
|
|
99
|
+
let mixed = false
|
|
100
|
+
|
|
101
|
+
for (const statement of program.body) {
|
|
102
|
+
switch (statement.type) {
|
|
103
|
+
case 'ImportDeclaration':
|
|
104
|
+
for (const specifier of statement.specifiers) {
|
|
105
|
+
const imported =
|
|
106
|
+
specifier.type === 'ImportSpecifier'
|
|
107
|
+
? moduleExportName(specifier.imported)
|
|
108
|
+
: specifier.type === 'ImportDefaultSpecifier'
|
|
109
|
+
? 'default'
|
|
110
|
+
: '*'
|
|
111
|
+
imports.set(specifier.local.name, { source: statement.source.value, imported })
|
|
112
|
+
}
|
|
113
|
+
break
|
|
114
|
+
case 'VariableDeclaration':
|
|
115
|
+
declarations.push(...declarationsOf(statement, dowel))
|
|
116
|
+
break
|
|
117
|
+
case 'ExportNamedDeclaration': {
|
|
118
|
+
const { declaration } = statement
|
|
119
|
+
if (declaration?.type === 'VariableDeclaration') {
|
|
120
|
+
const found = declarationsOf(declaration, dowel)
|
|
121
|
+
declarations.push(...found)
|
|
122
|
+
for (const declarator of declaration.declarations) {
|
|
123
|
+
for (const name of boundNames(declarator.id)) {
|
|
124
|
+
exports.set(name, name)
|
|
125
|
+
if (!found.some((d) => d.local === name)) mixed = true
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
} else if (declaration !== null) {
|
|
129
|
+
mixed = true
|
|
130
|
+
} else if (statement.source !== null) {
|
|
131
|
+
mixed = true
|
|
132
|
+
for (const specifier of statement.specifiers) {
|
|
133
|
+
reexports.set(moduleExportName(specifier.exported), {
|
|
134
|
+
source: statement.source.value,
|
|
135
|
+
imported: moduleExportName(specifier.local),
|
|
136
|
+
})
|
|
137
|
+
}
|
|
138
|
+
} else {
|
|
139
|
+
for (const specifier of statement.specifiers) {
|
|
140
|
+
exports.set(moduleExportName(specifier.exported), moduleExportName(specifier.local))
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
break
|
|
144
|
+
}
|
|
145
|
+
case 'ExportDefaultDeclaration':
|
|
146
|
+
if (statement.declaration.type === 'Identifier') {
|
|
147
|
+
exports.set('default', statement.declaration.name)
|
|
148
|
+
} else {
|
|
149
|
+
mixed = true
|
|
150
|
+
}
|
|
151
|
+
break
|
|
152
|
+
case 'ExportAllDeclaration':
|
|
153
|
+
mixed = true
|
|
154
|
+
if (statement.exported === null) stars.push(statement.source.value)
|
|
155
|
+
else {
|
|
156
|
+
reexports.set(moduleExportName(statement.exported), {
|
|
157
|
+
source: statement.source.value,
|
|
158
|
+
imported: '*',
|
|
159
|
+
})
|
|
160
|
+
}
|
|
161
|
+
break
|
|
162
|
+
default:
|
|
163
|
+
break
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (dowel.size === 0 && reexports.size === 0 && stars.length === 0) return null
|
|
168
|
+
|
|
169
|
+
const locals = new Set(declarations.map((d) => d.local))
|
|
170
|
+
for (const local of exports.values()) if (!locals.has(local)) mixed = true
|
|
171
|
+
|
|
172
|
+
const containerLocal = [...dowel].find(([, name]) => name === 'container')?.[0]
|
|
173
|
+
const buildsContainer = containerLocal !== undefined && callsIdentifier(program, containerLocal)
|
|
174
|
+
|
|
175
|
+
return {
|
|
176
|
+
declarations,
|
|
177
|
+
imports,
|
|
178
|
+
exports,
|
|
179
|
+
reexports,
|
|
180
|
+
stars,
|
|
181
|
+
selfContained: !mixed && !buildsContainer,
|
|
182
|
+
buildsContainer,
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/** `Name` or `ns.Name`, the references a static graph can follow. */
|
|
187
|
+
function refOf(node: Node): Ref | null {
|
|
188
|
+
const value = unwrap(node)
|
|
189
|
+
if (value.type === 'Identifier') return { local: value.name, member: null }
|
|
190
|
+
if (
|
|
191
|
+
value.type === 'MemberExpression' &&
|
|
192
|
+
!value.computed &&
|
|
193
|
+
value.object.type === 'Identifier' &&
|
|
194
|
+
value.property.type === 'Identifier'
|
|
195
|
+
) {
|
|
196
|
+
return { local: value.object.name, member: value.property.name }
|
|
197
|
+
}
|
|
198
|
+
return null
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* The properties of an options object literal by key: empty when there are
|
|
203
|
+
* no options, null when the analysis cannot read all of them (not a literal,
|
|
204
|
+
* or a literal with spread or computed properties).
|
|
205
|
+
*/
|
|
206
|
+
function optionsOf(node: Node | undefined): Map<string, Node> | null {
|
|
207
|
+
if (node === undefined) return new Map()
|
|
208
|
+
const options = unwrap(node)
|
|
209
|
+
if (options.type !== 'ObjectExpression') return null
|
|
210
|
+
const properties = new Map<string, Node>()
|
|
211
|
+
for (const property of options.properties) {
|
|
212
|
+
if (property.type !== 'Property' || property.computed) return null
|
|
213
|
+
const key = propertyKey(property.key)
|
|
214
|
+
if (key === null) return null
|
|
215
|
+
properties.set(key, unwrap(property.value))
|
|
216
|
+
}
|
|
217
|
+
return properties
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/** `true` or `false` when the option is absent or a boolean literal; null when it cannot be read. */
|
|
221
|
+
function literalFlag(node: Node | undefined): boolean | null {
|
|
222
|
+
if (node === undefined) return false
|
|
223
|
+
return node.type === 'Literal' && typeof node.value === 'boolean' ? node.value : null
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/** A scope argument: absent (the default), a reference to resolve, or an expression that cannot be followed. */
|
|
227
|
+
function scopeArgument(node: Node | undefined, fallback: string | null): Placement {
|
|
228
|
+
if (node === undefined) return { scope: fallback, scopeRef: null }
|
|
229
|
+
return { scope: null, scopeRef: refOf(node) }
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function collectDowelImports(program: ESTree.Program): DowelImports {
|
|
233
|
+
const result = new Map<string, DowelExport>()
|
|
234
|
+
for (const statement of program.body) {
|
|
235
|
+
if (statement.type !== 'ImportDeclaration' || statement.source.value !== '@doweljs/core')
|
|
236
|
+
continue
|
|
237
|
+
for (const specifier of statement.specifiers) {
|
|
238
|
+
if (specifier.type !== 'ImportSpecifier') continue
|
|
239
|
+
const imported = moduleExportName(specifier.imported)
|
|
240
|
+
if (isDowelExport(imported)) result.set(specifier.local.name, imported)
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
return result
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function declarationsOf(statement: ESTree.VariableDeclaration, dowel: DowelImports): Declaration[] {
|
|
247
|
+
const found: Declaration[] = []
|
|
248
|
+
for (const declarator of statement.declarations) {
|
|
249
|
+
if (declarator.id.type !== 'Identifier' || declarator.init === null) continue
|
|
250
|
+
const init = unwrap(declarator.init)
|
|
251
|
+
if (init.type !== 'CallExpression') continue
|
|
252
|
+
const declaration =
|
|
253
|
+
parseService(declarator.id.name, init, dowel) ??
|
|
254
|
+
parseSlot(declarator.id.name, init, dowel) ??
|
|
255
|
+
parseScope(declarator.id.name, init, dowel) ??
|
|
256
|
+
parseExecution(declarator.id.name, init, dowel)
|
|
257
|
+
if (declaration !== null) found.push(declaration)
|
|
258
|
+
}
|
|
259
|
+
return found
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/** `service('Name', deps?, factory, options?)` */
|
|
263
|
+
function parseService(
|
|
264
|
+
local: string,
|
|
265
|
+
call: ESTree.CallExpression,
|
|
266
|
+
dowel: DowelImports,
|
|
267
|
+
): Declaration | null {
|
|
268
|
+
if (call.callee.type !== 'Identifier' || dowel.get(call.callee.name) !== 'service') return null
|
|
269
|
+
const [nameArg, second, third, fourth] = call.arguments
|
|
270
|
+
const name = stringValue(nameArg)
|
|
271
|
+
if (name === null) return null
|
|
272
|
+
const hasDeps = second?.type === 'ObjectExpression'
|
|
273
|
+
const options = hasDeps ? fourth : third
|
|
274
|
+
const deps: DepRef[] = []
|
|
275
|
+
if (hasDeps) {
|
|
276
|
+
for (const property of second.properties) {
|
|
277
|
+
if (property.type !== 'Property' || property.computed) continue
|
|
278
|
+
const key = propertyKey(property.key)
|
|
279
|
+
if (key === null) continue
|
|
280
|
+
const value = unwrap(property.value)
|
|
281
|
+
const isOptional =
|
|
282
|
+
value.type === 'CallExpression' &&
|
|
283
|
+
value.callee.type === 'Identifier' &&
|
|
284
|
+
dowel.get(value.callee.name) === 'optional'
|
|
285
|
+
const target = isOptional ? value.arguments[0] : value
|
|
286
|
+
deps.push({
|
|
287
|
+
key,
|
|
288
|
+
ref: target === undefined || target.type === 'SpreadElement' ? null : refOf(target),
|
|
289
|
+
optional: isOptional,
|
|
290
|
+
})
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
return {
|
|
294
|
+
local,
|
|
295
|
+
kind: 'service',
|
|
296
|
+
name,
|
|
297
|
+
...scopeArgument(optionsOf(options)?.get('scope'), null),
|
|
298
|
+
many: false,
|
|
299
|
+
deps,
|
|
300
|
+
start: call.start,
|
|
301
|
+
end: call.end,
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/** `slot('Name', options?)()` */
|
|
306
|
+
function parseSlot(
|
|
307
|
+
local: string,
|
|
308
|
+
call: ESTree.CallExpression,
|
|
309
|
+
dowel: DowelImports,
|
|
310
|
+
): Declaration | null {
|
|
311
|
+
const inner = unwrap(call.callee)
|
|
312
|
+
if (inner.type !== 'CallExpression') return null
|
|
313
|
+
if (inner.callee.type !== 'Identifier' || dowel.get(inner.callee.name) !== 'slot') return null
|
|
314
|
+
const [nameArg, optionsArg] = inner.arguments
|
|
315
|
+
const name = stringValue(nameArg)
|
|
316
|
+
if (name === null) return null
|
|
317
|
+
const options = optionsOf(optionsArg)
|
|
318
|
+
return {
|
|
319
|
+
local,
|
|
320
|
+
kind: 'slot',
|
|
321
|
+
name,
|
|
322
|
+
// Options the analysis cannot read leave the scope unknown.
|
|
323
|
+
...(options === null
|
|
324
|
+
? { scope: null, scopeRef: null }
|
|
325
|
+
: scopeArgument(options.get('scope'), 'root')),
|
|
326
|
+
many: options === null ? null : literalFlag(options.get('many')),
|
|
327
|
+
deps: [],
|
|
328
|
+
start: call.start,
|
|
329
|
+
end: call.end,
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/** `scope('name', Parent?)` */
|
|
334
|
+
function parseScope(
|
|
335
|
+
local: string,
|
|
336
|
+
call: ESTree.CallExpression,
|
|
337
|
+
dowel: DowelImports,
|
|
338
|
+
): Declaration | null {
|
|
339
|
+
if (call.callee.type !== 'Identifier' || dowel.get(call.callee.name) !== 'scope') return null
|
|
340
|
+
const name = stringValue(call.arguments[0])
|
|
341
|
+
if (name === null) return null
|
|
342
|
+
const parent = call.arguments[1]
|
|
343
|
+
return {
|
|
344
|
+
local,
|
|
345
|
+
kind: 'scope',
|
|
346
|
+
name,
|
|
347
|
+
...scopeArgument(parent?.type === 'SpreadElement' ? undefined : parent, 'root'),
|
|
348
|
+
many: false,
|
|
349
|
+
deps: [],
|
|
350
|
+
start: call.start,
|
|
351
|
+
end: call.end,
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/** `execution('Name', { scope: S, entry: E, inputs: { key: Slot } })` */
|
|
356
|
+
function parseExecution(
|
|
357
|
+
local: string,
|
|
358
|
+
call: ESTree.CallExpression,
|
|
359
|
+
dowel: DowelImports,
|
|
360
|
+
): Declaration | null {
|
|
361
|
+
if (call.callee.type !== 'Identifier' || dowel.get(call.callee.name) !== 'execution') return null
|
|
362
|
+
const name = stringValue(call.arguments[0])
|
|
363
|
+
if (name === null) return null
|
|
364
|
+
// Options it cannot read still declare the execution; its scope and dependencies stay unknown.
|
|
365
|
+
const options = optionsOf(call.arguments[1]) ?? new Map<string, Node>()
|
|
366
|
+
const scope = options.get('scope')
|
|
367
|
+
const entry = options.get('entry')
|
|
368
|
+
const inputs = options.get('inputs')
|
|
369
|
+
const deps: DepRef[] = []
|
|
370
|
+
if (entry !== undefined) deps.push({ key: 'entry', ref: refOf(entry), optional: false })
|
|
371
|
+
// Keyed as at runtime, by the key each run passes the input's value under.
|
|
372
|
+
for (const [key, input] of optionsOf(inputs) ?? []) {
|
|
373
|
+
deps.push({ key, ref: refOf(input), optional: false })
|
|
374
|
+
}
|
|
375
|
+
return {
|
|
376
|
+
local,
|
|
377
|
+
kind: 'execution',
|
|
378
|
+
name,
|
|
379
|
+
// Known once its scope reference resolves: the parent of the scope its runs open containers for.
|
|
380
|
+
scope: null,
|
|
381
|
+
scopeRef: scope === undefined ? null : refOf(scope),
|
|
382
|
+
many: false,
|
|
383
|
+
deps,
|
|
384
|
+
start: call.start,
|
|
385
|
+
end: call.end,
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/** Strips TypeScript wrappers that may survive transpilation, such as `satisfies`. */
|
|
390
|
+
function unwrap(node: Node): Node {
|
|
391
|
+
let current = node
|
|
392
|
+
for (;;) {
|
|
393
|
+
switch (current.type) {
|
|
394
|
+
case 'ParenthesizedExpression':
|
|
395
|
+
case 'TSAsExpression':
|
|
396
|
+
case 'TSSatisfiesExpression':
|
|
397
|
+
case 'TSNonNullExpression':
|
|
398
|
+
case 'TSTypeAssertion':
|
|
399
|
+
current = current.expression
|
|
400
|
+
continue
|
|
401
|
+
default:
|
|
402
|
+
return current
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
function stringValue(node: Node | undefined): string | null {
|
|
408
|
+
if (node === undefined) return null
|
|
409
|
+
const value = unwrap(node)
|
|
410
|
+
if (value.type === 'Literal' && typeof value.value === 'string') return value.value
|
|
411
|
+
if (value.type === 'TemplateLiteral' && value.expressions.length === 0) {
|
|
412
|
+
return value.quasis[0]?.value.cooked ?? null
|
|
413
|
+
}
|
|
414
|
+
return null
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
function propertyKey(key: Node): string | null {
|
|
418
|
+
if (key.type === 'Identifier') return key.name
|
|
419
|
+
if (key.type === 'Literal' && typeof key.value === 'string') return key.value
|
|
420
|
+
return null
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
function moduleExportName(node: ESTree.ModuleExportName): string {
|
|
424
|
+
return node.type === 'Identifier' ? node.name : String(node.value)
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
function boundNames(pattern: ESTree.BindingPattern): string[] {
|
|
428
|
+
switch (pattern.type) {
|
|
429
|
+
case 'Identifier':
|
|
430
|
+
return [pattern.name]
|
|
431
|
+
case 'ObjectPattern':
|
|
432
|
+
return pattern.properties.flatMap((property) =>
|
|
433
|
+
property.type === 'RestElement'
|
|
434
|
+
? boundNames(property.argument)
|
|
435
|
+
: boundNames(property.value),
|
|
436
|
+
)
|
|
437
|
+
case 'ArrayPattern':
|
|
438
|
+
return pattern.elements.flatMap((element) =>
|
|
439
|
+
element === null
|
|
440
|
+
? []
|
|
441
|
+
: element.type === 'RestElement'
|
|
442
|
+
? boundNames(element.argument)
|
|
443
|
+
: boundNames(element),
|
|
444
|
+
)
|
|
445
|
+
case 'AssignmentPattern':
|
|
446
|
+
return boundNames(pattern.left)
|
|
447
|
+
default:
|
|
448
|
+
return []
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
/** Whether any call expression in the tree calls the given identifier. */
|
|
453
|
+
function callsIdentifier(root: Node, name: string): boolean {
|
|
454
|
+
const stack: unknown[] = [root]
|
|
455
|
+
while (stack.length > 0) {
|
|
456
|
+
const current = stack.pop()
|
|
457
|
+
if (typeof current !== 'object' || current === null) continue
|
|
458
|
+
if (Array.isArray(current)) {
|
|
459
|
+
stack.push(...current)
|
|
460
|
+
continue
|
|
461
|
+
}
|
|
462
|
+
const node = current as Node
|
|
463
|
+
if (
|
|
464
|
+
node.type === 'CallExpression' &&
|
|
465
|
+
node.callee.type === 'Identifier' &&
|
|
466
|
+
node.callee.name === name
|
|
467
|
+
) {
|
|
468
|
+
return true
|
|
469
|
+
}
|
|
470
|
+
for (const [key, value] of Object.entries(node)) {
|
|
471
|
+
if (key !== 'parent' && typeof value === 'object' && value !== null) stack.push(value)
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
return false
|
|
475
|
+
}
|