/
analyze.ts
211 lines (179 loc) · 6.49 KB
/
analyze.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
import { tokenizer } from 'acorn'
import { matchAll } from './_utils'
import { resolvePath, ResolveOptions } from './resolve'
import { loadURL } from './utils'
export interface ESMImport {
type: 'static' | 'dynamic'
code: string
start: number
end: number
}
export interface StaticImport extends ESMImport {
type: 'static'
imports: string
specifier: string
}
export interface ParsedStaticImport extends StaticImport {
defaultImport?: string
namespacedImport?: string
namedImports?: { [name: string]: string }
}
export interface DynamicImport extends ESMImport {
type: 'dynamic'
expression: string
}
export interface ESMExport {
_type?: 'declaration' | 'named' | 'default' | 'star',
type: 'declaration' | 'named' | 'default' | 'star',
code: string
start: number
end: number
name?: string
names: string[]
specifier?: string
}
export interface DeclarationExport extends ESMExport {
type: 'declaration'
declaration: string
name: string
}
export interface NamedExport extends ESMExport {
type: 'named'
exports: string
names: string[]
specifier?: string
}
export interface DefaultExport extends ESMExport {
type: 'default'
}
export const ESM_STATIC_IMPORT_RE = /(?<=\s|^|;)import\s*(["'\s]*(?<imports>[\w*${}\n\r\t, /]+)from\s*)?["']\s*(?<specifier>(?<="\s*)[^"]*[^"\s](?=\s*")|(?<='\s*)[^']*[^'\s](?=\s*'))\s*["'][\s;]*/gm
export const DYNAMIC_IMPORT_RE = /import\s*\((?<expression>(?:[^)(]+|\((?:[^)(]+|\([^)(]*\))*\))*)\)/gm
export const EXPORT_DECAL_RE = /\bexport\s+(?<declaration>(async function|function|let|const enum|const|enum|var|class))\s+(?<name>[\w$_]+)/g
const EXPORT_NAMED_RE = /\bexport\s+{(?<exports>[^}]+?)(?:[,\s]*)}(\s*from\s*["']\s*(?<specifier>(?<="\s*)[^"]*[^"\s](?=\s*")|(?<='\s*)[^']*[^'\s](?=\s*'))\s*["'][^\n]*)?/g
const EXPORT_STAR_RE = /\bexport\s*(\*)(\s*as\s+(?<name>[\w$_]+)\s+)?\s*(\s*from\s*["']\s*(?<specifier>(?<="\s*)[^"]*[^"\s](?=\s*")|(?<='\s*)[^']*[^'\s](?=\s*'))\s*["'][^\n]*)?/g
const EXPORT_DEFAULT_RE = /\bexport\s+default\s+/g
export function findStaticImports (code: string): StaticImport[] {
return matchAll(ESM_STATIC_IMPORT_RE, code, { type: 'static' })
}
export function findDynamicImports (code: string): DynamicImport[] {
return matchAll(DYNAMIC_IMPORT_RE, code, { type: 'dynamic' })
}
export function parseStaticImport (matched: StaticImport): ParsedStaticImport {
const cleanedImports = (matched.imports || '')
.replace(/(\/\/[^\n]*\n|\/\*.*\*\/)/g, '')
.replace(/\s+/g, ' ')
const namedImports = {}
for (const namedImport of cleanedImports.match(/\{([^}]*)\}/)?.[1]?.split(',') || []) {
const [, source = namedImport.trim(), importName = source] = namedImport.match(/^\s*([^\s]*) as ([^\s]*)\s*$/) || []
if (source) {
namedImports[source] = importName
}
}
const topLevelImports = cleanedImports.replace(/\{([^}]*)\}/, '')
const namespacedImport = topLevelImports.match(/\* as \s*([^\s]*)/)?.[1]
const defaultImport = topLevelImports.split(',').find(i => !i.match(/[*{}]/))?.trim() || undefined
return {
...matched,
defaultImport,
namespacedImport,
namedImports
} as ParsedStaticImport
}
export function findExports (code: string): ESMExport[] {
// Find declarations like export const foo = 'bar'
const declaredExports: DeclarationExport[] = matchAll(EXPORT_DECAL_RE, code, { type: 'declaration' })
// Find named exports
const namedExports: NamedExport[] = matchAll(EXPORT_NAMED_RE, code, { type: 'named' })
for (const namedExport of namedExports) {
namedExport.names = namedExport.exports.split(/\s*,\s*/g).map(name => name.replace(/^.*?\sas\s/, '').trim())
}
// Find export default
const defaultExport: DefaultExport[] = matchAll(EXPORT_DEFAULT_RE, code, { type: 'default', name: 'default' })
// Find export star
const starExports: ESMExport[] = matchAll(EXPORT_STAR_RE, code, { type: 'star' })
// Merge and normalize exports
const exports: ESMExport[] = [].concat(declaredExports, namedExports, defaultExport, starExports)
for (const exp of exports) {
if (!exp.name && exp.names && exp.names.length === 1) {
exp.name = exp.names[0]
}
if (exp.name === 'default' && exp.type !== 'default') {
exp._type = exp.type
exp.type = 'default'
}
if (!exp.names && exp.name) {
exp.names = [exp.name]
}
}
// Return early when there is no export statement
if (!exports.length) {
return []
}
const exportLocations = _tryGetExportLocations(code)
if (exportLocations && !exportLocations.length) {
return []
}
return exports.filter((exp, index, exports) => {
// Filter false positive export matches
if (exportLocations && !_isExportStatement(exportLocations, exp)) {
return false
}
// Prevent multiple exports of same function, only keep latest iteration of signatures
const nextExport = exports[index + 1]
return !nextExport || exp.type !== nextExport.type || !exp.name || exp.name !== nextExport.name
})
}
export function findExportNames (code: string): string[] {
return findExports(code).flatMap(exp => exp.names).filter(Boolean)
}
export async function resolveModuleExportNames (id: string, opts?: ResolveOptions): Promise<string[]> {
const url = await resolvePath(id, opts)
const code = await loadURL(url)
const exports = findExports(code)
// Explicit named exports
const exportNames = new Set(exports.flatMap(exp => exp.names).filter(Boolean))
// Recursive * exports
for (const exp of exports) {
if (exp.type === 'star') {
const subExports = await resolveModuleExportNames(exp.specifier, { ...opts, url })
for (const subExport of subExports) {
exportNames.add(subExport)
}
}
}
return Array.from(exportNames)
}
// --- Internal ---
interface TokenLocation {
start: number
end: number
}
function _isExportStatement (exportsLocation: TokenLocation[], exp: ESMExport) {
return exportsLocation.some(location => exp.start <= location.start && exp.end >= location.end)
}
function _tryGetExportLocations (code: string) {
try {
return _getExportLocations(code)
} catch (err) {
return null
}
}
function _getExportLocations (code: string) {
const tokens = tokenizer(code, {
ecmaVersion: 'latest',
sourceType: 'module',
allowHashBang: true,
allowAwaitOutsideFunction: true,
allowImportExportEverywhere: true
})
const locations: TokenLocation[] = []
for (const token of tokens) {
if (token.type.label === 'export') {
locations.push({
start: token.start,
end: token.end
})
}
}
return locations
}