depvitals 0.0.1 → 0.1.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.
- package/README.md +67 -10
- package/dist/cli/index.js +13 -7
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/parseCliArgs.d.ts +37 -0
- package/dist/cli/parseCliArgs.d.ts.map +1 -0
- package/dist/cli/parseCliArgs.js +85 -0
- package/dist/cli/parseCliArgs.js.map +1 -0
- package/dist/cli/runCli.d.ts +31 -13
- package/dist/cli/runCli.d.ts.map +1 -1
- package/dist/cli/runCli.js +76 -84
- package/dist/cli/runCli.js.map +1 -1
- package/dist/core/analyzeProject.d.ts +92 -0
- package/dist/core/analyzeProject.d.ts.map +1 -0
- package/dist/core/analyzeProject.js +175 -0
- package/dist/core/analyzeProject.js.map +1 -0
- package/dist/reporters/terminalReporter.d.ts +49 -0
- package/dist/reporters/terminalReporter.d.ts.map +1 -0
- package/dist/reporters/terminalReporter.js +98 -0
- package/dist/reporters/terminalReporter.js.map +1 -0
- package/dist/scanner/moduleSpecifiers.d.ts +60 -0
- package/dist/scanner/moduleSpecifiers.d.ts.map +1 -0
- package/dist/scanner/moduleSpecifiers.js +824 -0
- package/dist/scanner/moduleSpecifiers.js.map +1 -0
- package/dist/scanner/packageSpecifier.d.ts +45 -0
- package/dist/scanner/packageSpecifier.d.ts.map +1 -0
- package/dist/scanner/packageSpecifier.js +98 -0
- package/dist/scanner/packageSpecifier.js.map +1 -0
- package/dist/scanner/projectManifest.d.ts +72 -0
- package/dist/scanner/projectManifest.d.ts.map +1 -0
- package/dist/scanner/projectManifest.js +176 -0
- package/dist/scanner/projectManifest.js.map +1 -0
- package/dist/scanner/scriptUsage.d.ts +54 -0
- package/dist/scanner/scriptUsage.d.ts.map +1 -0
- package/dist/scanner/scriptUsage.js +490 -0
- package/dist/scanner/scriptUsage.js.map +1 -0
- package/dist/scanner/sourceFiles.d.ts +52 -0
- package/dist/scanner/sourceFiles.d.ts.map +1 -0
- package/dist/scanner/sourceFiles.js +190 -0
- package/dist/scanner/sourceFiles.js.map +1 -0
- package/package.json +3 -1
|
@@ -0,0 +1,824 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Static module specifier extraction.
|
|
3
|
+
*
|
|
4
|
+
* This module reads JavaScript/TypeScript source text and returns the statically knowable module
|
|
5
|
+
* specifiers it contains, without executing the code and without a heavyweight parser dependency.
|
|
6
|
+
*
|
|
7
|
+
* It deliberately does not:
|
|
8
|
+
* - decide whether a specifier is an external package, a Node builtin, relative, missing or used
|
|
9
|
+
* (that belongs to later steps);
|
|
10
|
+
* - normalize specifiers, sort or deduplicate them;
|
|
11
|
+
* - resolve anything on disk or through path aliases / bundler aliases.
|
|
12
|
+
*
|
|
13
|
+
* The occurrences are returned in source encounter order and are not deduplicated: deduplication
|
|
14
|
+
* belongs to the analysis step, not to extraction.
|
|
15
|
+
*
|
|
16
|
+
* Instead of a few global regexes over raw source, this module runs a small lexical scanner with
|
|
17
|
+
* just enough JavaScript awareness to identify the required forms while ignoring comments, strings,
|
|
18
|
+
* template literal text and regex literals so they never produce false findings. JavaScript inside
|
|
19
|
+
* template interpolations (`${...}`) is still lexed, so specifiers there are found.
|
|
20
|
+
*/
|
|
21
|
+
/**
|
|
22
|
+
* Operational error for module specifier extraction.
|
|
23
|
+
*
|
|
24
|
+
* It is thrown only when the source cannot be scanned safely, so callers (later steps) can treat a
|
|
25
|
+
* file as un-analysable rather than trusting a partial result.
|
|
26
|
+
*/
|
|
27
|
+
export class ModuleSpecifierError extends Error {
|
|
28
|
+
code;
|
|
29
|
+
offset;
|
|
30
|
+
constructor(message, code, offset) {
|
|
31
|
+
super(message);
|
|
32
|
+
this.name = 'ModuleSpecifierError';
|
|
33
|
+
this.code = code;
|
|
34
|
+
this.offset = offset;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Extract the statically knowable module specifiers from `source`.
|
|
39
|
+
*
|
|
40
|
+
* The returned occurrences preserve source order and are not deduplicated.
|
|
41
|
+
*
|
|
42
|
+
* @throws {ModuleSpecifierError} when the source reaches an unterminated or malformed lexical state
|
|
43
|
+
* from which the scanner cannot safely continue.
|
|
44
|
+
*/
|
|
45
|
+
export function extractModuleSpecifiers(source) {
|
|
46
|
+
return interpret(tokenize(source));
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Turn source into a stream of significant tokens, skipping whitespace, line/block comments, regex
|
|
50
|
+
* literals and template literals.
|
|
51
|
+
*/
|
|
52
|
+
function tokenize(source) {
|
|
53
|
+
const tokens = [];
|
|
54
|
+
const n = source.length;
|
|
55
|
+
let i = 0;
|
|
56
|
+
// Whether the previous identifier token is an expression-starting keyword (return, typeof, ...).
|
|
57
|
+
// Such keywords start an expression, so a following `/` begins a regex rather than division.
|
|
58
|
+
let prevIsExpressionKeyword = false;
|
|
59
|
+
// Hashbang: only at offset 0. Ignore until the first line terminator.
|
|
60
|
+
if (source.startsWith('#!')) {
|
|
61
|
+
i = 2;
|
|
62
|
+
while (i < n && !isLineTerminator(source[i])) {
|
|
63
|
+
i++;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
// The previous significant token drives the regex-vs-division decision for `/`.
|
|
67
|
+
let prevKind = 'start';
|
|
68
|
+
// The value of the previous punctuation token, when the previous token was punctuation.
|
|
69
|
+
let prevPunct;
|
|
70
|
+
// The value of the previous identifier token, when the previous token was an identifier. Which
|
|
71
|
+
// identifier it was matters for one decision: `return {` opens an object literal, `else {` not.
|
|
72
|
+
let prevIdentifier;
|
|
73
|
+
// Template literal state. `inText` is true while scanning template text that must not be lexed as
|
|
74
|
+
// JavaScript; a backtick toggles it, so nested templates are handled by the toggle. Every open
|
|
75
|
+
// `${...}` expression pushes its own brace depth onto `braceDepths`, which is what distinguishes
|
|
76
|
+
// the `}` that closes the interpolation from a `}` belonging to an object literal, a block or a
|
|
77
|
+
// function body inside it. JavaScript inside `${...}` is lexed like ordinary source.
|
|
78
|
+
const braceDepths = [];
|
|
79
|
+
let inText = false;
|
|
80
|
+
// Offset of the opening backtick of the current template, for unterminated-template errors.
|
|
81
|
+
let templateStart = 0;
|
|
82
|
+
// Brace context. Each `{` pushes whether it opened an expression (an object literal or a
|
|
83
|
+
// destructuring pattern) or a statement block / function body, and its matching `}` pops that
|
|
84
|
+
// entry. A `}` only ends an expression when it closed an object literal, which is what separates
|
|
85
|
+
// `{}` in `{} / require('pkg') / 2`, where both slashes divide, from the `}` of
|
|
86
|
+
// `if (ok) { ... }`, after which a `/` may legitimately start a regex. Anything unrecognized is
|
|
87
|
+
// treated as a block.
|
|
88
|
+
const braceContexts = [];
|
|
89
|
+
// Whether the most recent `}` closed an object literal. It is only consulted while that `}` is
|
|
90
|
+
// the previous token, exactly like `prevPunct`.
|
|
91
|
+
let closedObjectBrace = false;
|
|
92
|
+
// Regex-after-`)` heuristic. `afterControlHead` is true when the most recent `)` closed the head
|
|
93
|
+
// of a control statement (if/while/for/with), so a following `/` may start a regex literal.
|
|
94
|
+
let afterControlHead = false;
|
|
95
|
+
// The control keyword pending a `(` (if/while/for/with), so the matching `)` can be recognised.
|
|
96
|
+
// Only whitespace and comments may appear between the keyword and the `(`.
|
|
97
|
+
let pendingControlKeyword;
|
|
98
|
+
// How many `(` are currently open, and the depth at which the open control-statement head sits,
|
|
99
|
+
// so only the `)` that closes the head itself (not a nested one) enables the heuristic.
|
|
100
|
+
let parenDepth = 0;
|
|
101
|
+
let controlParenDepth;
|
|
102
|
+
// Reset the expression-start state at the beginning of a fresh `${...}` expression.
|
|
103
|
+
const resetExpressionState = () => {
|
|
104
|
+
prevKind = 'start';
|
|
105
|
+
prevPunct = undefined;
|
|
106
|
+
prevIdentifier = undefined;
|
|
107
|
+
prevIsExpressionKeyword = false;
|
|
108
|
+
afterControlHead = false;
|
|
109
|
+
pendingControlKeyword = undefined;
|
|
110
|
+
closedObjectBrace = false;
|
|
111
|
+
};
|
|
112
|
+
while (i < n) {
|
|
113
|
+
const ch = source[i];
|
|
114
|
+
if (ch === undefined) {
|
|
115
|
+
i++;
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
// Template literal text: skip everything until `${`, a nested `` ` ``, or the closing `` ` ``.
|
|
119
|
+
// A backtick toggles out of text mode; the matching toggle happens below in code mode.
|
|
120
|
+
if (inText) {
|
|
121
|
+
if (ch === '\\') {
|
|
122
|
+
i += 2;
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
if (ch === '`') {
|
|
126
|
+
// Closing backtick of the current (or nested) template literal.
|
|
127
|
+
inText = false;
|
|
128
|
+
prevKind = 'template';
|
|
129
|
+
i++;
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
if (ch === '$' && source[i + 1] === '{') {
|
|
133
|
+
// Start of a `${...}` expression: lex the JavaScript inside like ordinary source. The frame
|
|
134
|
+
// starts at brace depth 0, so the next unmatched `}` closes the interpolation.
|
|
135
|
+
braceDepths.push(0);
|
|
136
|
+
inText = false;
|
|
137
|
+
resetExpressionState();
|
|
138
|
+
i += 2;
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
i++;
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
if (isWhitespace(ch)) {
|
|
145
|
+
i++;
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
// Line comment: it ends at any ECMAScript line terminator (LF, CR, LS or PS), not only `\n`.
|
|
149
|
+
if (ch === '/' && source[i + 1] === '/') {
|
|
150
|
+
i += 2;
|
|
151
|
+
while (i < n && !isLineTerminator(source[i])) {
|
|
152
|
+
i++;
|
|
153
|
+
}
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
// Block comment. Comments are transparent to the regex/division heuristic, so a comment between
|
|
157
|
+
// a control keyword and its `(` behaves exactly like whitespace.
|
|
158
|
+
if (ch === '/' && source[i + 1] === '*') {
|
|
159
|
+
i = skipBlockComment(source, i);
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
// A backtick in code mode opens a (possibly nested) template literal.
|
|
163
|
+
if (ch === '`') {
|
|
164
|
+
templateStart = i;
|
|
165
|
+
inText = true;
|
|
166
|
+
prevKind = 'template';
|
|
167
|
+
i++;
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
// A `}` in code mode closes the innermost open `${...}` expression only when that expression has
|
|
171
|
+
// no open braces of its own; otherwise it is an ordinary brace inside the expression.
|
|
172
|
+
if (ch === '}') {
|
|
173
|
+
const depth = braceDepths[braceDepths.length - 1];
|
|
174
|
+
if (depth !== undefined) {
|
|
175
|
+
if (depth === 0) {
|
|
176
|
+
braceDepths.pop();
|
|
177
|
+
i++;
|
|
178
|
+
inText = true;
|
|
179
|
+
resetExpressionState();
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
braceDepths[braceDepths.length - 1] = depth - 1;
|
|
183
|
+
}
|
|
184
|
+
// Otherwise `}` is an ordinary brace: it ends an expression only when it closed an object
|
|
185
|
+
// literal. An unmatched `}` (an empty context) keeps the statement-block behaviour.
|
|
186
|
+
closedObjectBrace = braceContexts.pop() === true;
|
|
187
|
+
}
|
|
188
|
+
// `/` is a regex when it can start an expression, otherwise it is division.
|
|
189
|
+
if (ch === '/') {
|
|
190
|
+
if (isRegexStart(prevKind, prevPunct, prevIsExpressionKeyword, afterControlHead, closedObjectBrace)) {
|
|
191
|
+
i = skipRegex(source, i);
|
|
192
|
+
prevKind = 'regex';
|
|
193
|
+
afterControlHead = false;
|
|
194
|
+
pendingControlKeyword = undefined;
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
tokens.push({ kind: 'punct', value: '/', offset: i });
|
|
198
|
+
prevKind = 'punct';
|
|
199
|
+
prevPunct = '/';
|
|
200
|
+
afterControlHead = false;
|
|
201
|
+
pendingControlKeyword = undefined;
|
|
202
|
+
i++;
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
// String literals (single or double quoted).
|
|
206
|
+
if (ch === "'" || ch === '"') {
|
|
207
|
+
const { value, next } = readString(source, i);
|
|
208
|
+
tokens.push({ kind: 'string', value, offset: i });
|
|
209
|
+
prevKind = 'string';
|
|
210
|
+
afterControlHead = false;
|
|
211
|
+
pendingControlKeyword = undefined;
|
|
212
|
+
i = next;
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
// Identifiers.
|
|
216
|
+
if (isIdentifierStart(ch)) {
|
|
217
|
+
const start = i;
|
|
218
|
+
i++;
|
|
219
|
+
while (i < n) {
|
|
220
|
+
const part = source[i];
|
|
221
|
+
if (part === undefined || !isIdentifierPart(part)) {
|
|
222
|
+
break;
|
|
223
|
+
}
|
|
224
|
+
i++;
|
|
225
|
+
}
|
|
226
|
+
const value = source.slice(start, i);
|
|
227
|
+
prevIdentifier = value;
|
|
228
|
+
// A keyword after `.` is a property name (e.g. `x.return`), not an expression start.
|
|
229
|
+
prevIsExpressionKeyword =
|
|
230
|
+
REGEX_PRECEDING_KEYWORDS.has(value) && !(prevKind === 'punct' && prevPunct === '.');
|
|
231
|
+
// Remember a control-statement keyword until its `(` is seen so only that head's `)` may be
|
|
232
|
+
// followed by a regex literal.
|
|
233
|
+
pendingControlKeyword = pendingControlKeywordAfter(value,
|
|
234
|
+
// After `.` the identifier is a property name (e.g. `obj.if(...)`), never a control head.
|
|
235
|
+
prevKind === 'punct' && prevPunct === '.', pendingControlKeyword);
|
|
236
|
+
tokens.push({ kind: 'identifier', value, offset: start });
|
|
237
|
+
prevKind = 'identifier';
|
|
238
|
+
afterControlHead = false;
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
241
|
+
// Numbers end an expression, so they affect the regex/division decision.
|
|
242
|
+
if (isNumberStart(ch)) {
|
|
243
|
+
const start = i;
|
|
244
|
+
i++;
|
|
245
|
+
while (i < n) {
|
|
246
|
+
const part = source[i];
|
|
247
|
+
if (part === undefined || !(isIdentifierPart(part) || part === '.')) {
|
|
248
|
+
break;
|
|
249
|
+
}
|
|
250
|
+
i++;
|
|
251
|
+
}
|
|
252
|
+
tokens.push({ kind: 'number', offset: start });
|
|
253
|
+
prevKind = 'number';
|
|
254
|
+
afterControlHead = false;
|
|
255
|
+
pendingControlKeyword = undefined;
|
|
256
|
+
continue;
|
|
257
|
+
}
|
|
258
|
+
// Any other single character is punctuation.
|
|
259
|
+
if (ch === '{') {
|
|
260
|
+
// Record what this brace opens, so its `}` knows whether an expression or a statement ended.
|
|
261
|
+
// The enclosing entry is inspected before pushing, so a `:` knows if it separates a property.
|
|
262
|
+
braceContexts.push(opensObjectLiteral(prevKind, prevPunct, prevIdentifier, prevIsExpressionKeyword, braceDepths.length > 0, braceContexts[braceContexts.length - 1] === true));
|
|
263
|
+
// Count the brace inside the innermost open `${...}` expression so its closing `}` is found.
|
|
264
|
+
const depth = braceDepths[braceDepths.length - 1];
|
|
265
|
+
if (depth !== undefined) {
|
|
266
|
+
braceDepths[braceDepths.length - 1] = depth + 1;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
else if (ch === '(') {
|
|
270
|
+
// A `(` after a control keyword opens a control head; only its matching `)` may then be
|
|
271
|
+
// followed by a regex literal, so the nesting depth of every `(` has to be tracked.
|
|
272
|
+
parenDepth++;
|
|
273
|
+
if (pendingControlKeyword !== undefined) {
|
|
274
|
+
controlParenDepth = parenDepth;
|
|
275
|
+
pendingControlKeyword = undefined;
|
|
276
|
+
}
|
|
277
|
+
afterControlHead = false;
|
|
278
|
+
}
|
|
279
|
+
else if (ch === ')') {
|
|
280
|
+
afterControlHead = controlParenDepth !== undefined && parenDepth === controlParenDepth;
|
|
281
|
+
if (afterControlHead) {
|
|
282
|
+
controlParenDepth = undefined;
|
|
283
|
+
}
|
|
284
|
+
if (parenDepth > 0) {
|
|
285
|
+
parenDepth--;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
tokens.push({ kind: 'punct', value: ch, offset: i });
|
|
289
|
+
prevKind = 'punct';
|
|
290
|
+
prevPunct = ch;
|
|
291
|
+
i++;
|
|
292
|
+
}
|
|
293
|
+
// Reaching the end of the source while inside a template text or an open `${...}` expression means
|
|
294
|
+
// the template literal was never closed.
|
|
295
|
+
if (inText || braceDepths.length > 0) {
|
|
296
|
+
throw new ModuleSpecifierError('Unterminated template literal', 'invalid-source', templateStart);
|
|
297
|
+
}
|
|
298
|
+
return tokens;
|
|
299
|
+
}
|
|
300
|
+
/**
|
|
301
|
+
* Significant token kinds that clearly terminate an expression. After one of these, `/` is
|
|
302
|
+
* division; otherwise it may start a regex literal.
|
|
303
|
+
*/
|
|
304
|
+
const EXPRESSION_ENDING = new Set([
|
|
305
|
+
'identifier',
|
|
306
|
+
'number',
|
|
307
|
+
'string',
|
|
308
|
+
'template',
|
|
309
|
+
'regex',
|
|
310
|
+
]);
|
|
311
|
+
/**
|
|
312
|
+
* Expression-starting keywords. After one of these, a `/` begins a regex literal even though the
|
|
313
|
+
* keyword itself is tokenized as an identifier. `of`, `yield` and `await` are intentionally
|
|
314
|
+
* excluded: they are valid identifiers in some contexts, so `/` after them may be division.
|
|
315
|
+
* `default` covers `export default /re/.test(value)`, where the regex literal is the exported
|
|
316
|
+
* value, and, together with the brace stack, `export default {} / require('pkg') / 2`, where the
|
|
317
|
+
* object literal is.
|
|
318
|
+
*/
|
|
319
|
+
const REGEX_PRECEDING_KEYWORDS = new Set([
|
|
320
|
+
'return',
|
|
321
|
+
'default',
|
|
322
|
+
'typeof',
|
|
323
|
+
'instanceof',
|
|
324
|
+
'in',
|
|
325
|
+
'new',
|
|
326
|
+
'delete',
|
|
327
|
+
'void',
|
|
328
|
+
'throw',
|
|
329
|
+
'case',
|
|
330
|
+
'do',
|
|
331
|
+
'else',
|
|
332
|
+
]);
|
|
333
|
+
// Control-statement heads whose closing `)` may be followed by a regex literal.
|
|
334
|
+
const CONTROL_HEAD_KEYWORDS = new Set(['if', 'while', 'for', 'with']);
|
|
335
|
+
/**
|
|
336
|
+
* The control keyword still pending a `(` after reading the identifier `value`, given what was
|
|
337
|
+
* pending before it.
|
|
338
|
+
*
|
|
339
|
+
* - `if`/`while`/`for`/`with` start a control head.
|
|
340
|
+
* - `await` keeps a pending `for` alive, because `for await (...)` puts it between the keyword and
|
|
341
|
+
* its `(`. `await` is not a control keyword on its own.
|
|
342
|
+
* - Anything else (including a keyword used as a property name after `.`) clears the pending state.
|
|
343
|
+
*/
|
|
344
|
+
function pendingControlKeywordAfter(value, isMemberName, pending) {
|
|
345
|
+
if (isMemberName) {
|
|
346
|
+
return undefined;
|
|
347
|
+
}
|
|
348
|
+
if (CONTROL_HEAD_KEYWORDS.has(value)) {
|
|
349
|
+
return value;
|
|
350
|
+
}
|
|
351
|
+
return value === 'await' && pending === 'for' ? pending : undefined;
|
|
352
|
+
}
|
|
353
|
+
/**
|
|
354
|
+
* Punctuation after which a `{` opens an expression, because the token before it needs an
|
|
355
|
+
* expression on its right: assignment (`= {`), an argument or element list (`f({`, `[{`) and the
|
|
356
|
+
* separators and binary/logical operators. `>` is absent because it usually ends an arrow function
|
|
357
|
+
* (`() => {}`), whose `{` opens a function body, and `)`/`}` are absent because they close
|
|
358
|
+
* statement heads and blocks. `:` is decided separately, in {@link opensObjectLiteral}, because it
|
|
359
|
+
* is a property separator inside an object literal but a label or `case`/`default` separator
|
|
360
|
+
* otherwise.
|
|
361
|
+
*/
|
|
362
|
+
const EXPRESSION_BEFORE_BRACE = new Set([
|
|
363
|
+
'=',
|
|
364
|
+
'(',
|
|
365
|
+
'[',
|
|
366
|
+
',',
|
|
367
|
+
'?',
|
|
368
|
+
'&',
|
|
369
|
+
'|',
|
|
370
|
+
'^',
|
|
371
|
+
'+',
|
|
372
|
+
'-',
|
|
373
|
+
'*',
|
|
374
|
+
'%',
|
|
375
|
+
'!',
|
|
376
|
+
'~',
|
|
377
|
+
]);
|
|
378
|
+
// Keywords that start an expression but are still followed by a statement block.
|
|
379
|
+
const BLOCK_AFTER_KEYWORD = new Set(['else', 'do']);
|
|
380
|
+
/**
|
|
381
|
+
* Whether a `{` opens an expression (an object literal or a destructuring pattern) instead of a
|
|
382
|
+
* statement block or function body. `enclosingIsObjectLiteral` reports what the enclosing `{`, if
|
|
383
|
+
* any, opened. Statement position is the default, so an unknown context keeps the existing
|
|
384
|
+
* behaviour of allowing a regex literal after the matching `}`.
|
|
385
|
+
*/
|
|
386
|
+
function opensObjectLiteral(prevKind, prevPunct, prevIdentifier, prevIsExpressionKeyword, inTemplateExpression, enclosingIsObjectLiteral) {
|
|
387
|
+
if (prevKind === 'punct') {
|
|
388
|
+
// Inside an object literal a `:` separates a property from its value (`{ scale: {} }`, where a
|
|
389
|
+
// following `/` divides), while at statement level the same `:` follows a label
|
|
390
|
+
// (`label: {}`) or a `case`/`default` clause, whose `{` opens a block.
|
|
391
|
+
if (prevPunct === ':') {
|
|
392
|
+
return enclosingIsObjectLiteral;
|
|
393
|
+
}
|
|
394
|
+
return EXPRESSION_BEFORE_BRACE.has(prevPunct ?? '');
|
|
395
|
+
}
|
|
396
|
+
if (prevKind === 'identifier') {
|
|
397
|
+
// `return {` is an expression, `else {`/`do {` are blocks and every other identifier
|
|
398
|
+
// (`function f`, `class C`) sits in statement position.
|
|
399
|
+
return prevIsExpressionKeyword && !BLOCK_AFTER_KEYWORD.has(prevIdentifier ?? '');
|
|
400
|
+
}
|
|
401
|
+
// Right after `${` an expression is expected, so a `{` opens an object literal.
|
|
402
|
+
return inTemplateExpression && prevKind === 'start';
|
|
403
|
+
}
|
|
404
|
+
/**
|
|
405
|
+
* Whether a `/` (not already a comment) may begin a regex literal, given the previous significant
|
|
406
|
+
* token. `)` and `]` punctuation also end an expression (division), so any other punctuation (or
|
|
407
|
+
* the start of the file) may start a regex. The one exception is a `/` after the `)` that closes a
|
|
408
|
+
* control-statement head (if/while/for/with), which may start a regex literal. A `}` depends on
|
|
409
|
+
* what it closed: an object literal ends an expression, so the `/` divides, while a statement block
|
|
410
|
+
* leaves the expression-start position, where a regex literal may begin.
|
|
411
|
+
*/
|
|
412
|
+
function isRegexStart(prevKind, prevPunct, prevIsExpressionKeyword, afterControlHead, closedObjectBrace) {
|
|
413
|
+
if (prevKind === 'identifier' && prevIsExpressionKeyword) {
|
|
414
|
+
return true;
|
|
415
|
+
}
|
|
416
|
+
if (EXPRESSION_ENDING.has(prevKind)) {
|
|
417
|
+
return false;
|
|
418
|
+
}
|
|
419
|
+
if (prevKind === 'punct') {
|
|
420
|
+
if (afterControlHead && prevPunct === ')') {
|
|
421
|
+
return true;
|
|
422
|
+
}
|
|
423
|
+
if (prevPunct === '}') {
|
|
424
|
+
return !closedObjectBrace;
|
|
425
|
+
}
|
|
426
|
+
return prevPunct !== ')' && prevPunct !== ']';
|
|
427
|
+
}
|
|
428
|
+
return true;
|
|
429
|
+
}
|
|
430
|
+
/* -------------------------------------------------------------------------- */
|
|
431
|
+
/* Interpretation */
|
|
432
|
+
/* -------------------------------------------------------------------------- */
|
|
433
|
+
/**
|
|
434
|
+
* Walk the significant tokens and record specifiers for the required import/export/require forms.
|
|
435
|
+
*/
|
|
436
|
+
function interpret(tokens) {
|
|
437
|
+
const occurrences = [];
|
|
438
|
+
const n = tokens.length;
|
|
439
|
+
let i = 0;
|
|
440
|
+
while (i < n) {
|
|
441
|
+
const token = tokens[i];
|
|
442
|
+
if (token?.kind !== 'identifier') {
|
|
443
|
+
i++;
|
|
444
|
+
continue;
|
|
445
|
+
}
|
|
446
|
+
if (token.value === 'import') {
|
|
447
|
+
i = handleImport(tokens, i, occurrences);
|
|
448
|
+
continue;
|
|
449
|
+
}
|
|
450
|
+
if (token.value === 'export') {
|
|
451
|
+
i = handleExport(tokens, i, occurrences);
|
|
452
|
+
continue;
|
|
453
|
+
}
|
|
454
|
+
if (token.value === 'require') {
|
|
455
|
+
i = handleRequire(tokens, i, occurrences);
|
|
456
|
+
continue;
|
|
457
|
+
}
|
|
458
|
+
i++;
|
|
459
|
+
}
|
|
460
|
+
return occurrences;
|
|
461
|
+
}
|
|
462
|
+
/**
|
|
463
|
+
* Handle an `import` identifier. Returns the next index to scan from.
|
|
464
|
+
*
|
|
465
|
+
* - `obj.import(...)` is ignored (a member call, preceded by `.`).
|
|
466
|
+
* - `import.meta` is ignored (treated as `import` followed by `.`).
|
|
467
|
+
* - `import 'pkg'` and `import ... from 'pkg'` are `import`.
|
|
468
|
+
* - `import('pkg')` is `dynamic-import` only when the sole argument is a static string.
|
|
469
|
+
*/
|
|
470
|
+
function handleImport(tokens, i, occurrences) {
|
|
471
|
+
const prev = tokens[i - 1];
|
|
472
|
+
const next = tokens[i + 1];
|
|
473
|
+
// `obj.import(...)`: a member call, not the import keyword.
|
|
474
|
+
if (prev?.kind === 'punct' && prev.value === '.') {
|
|
475
|
+
return i + 1;
|
|
476
|
+
}
|
|
477
|
+
// `import.meta` / `import .foo`: not a module import.
|
|
478
|
+
if (next?.kind === 'punct' && next.value === '.') {
|
|
479
|
+
return i + 1;
|
|
480
|
+
}
|
|
481
|
+
// `import 'pkg'`
|
|
482
|
+
if (next?.kind === 'string') {
|
|
483
|
+
occurrences.push({ specifier: next.value, kind: 'import' });
|
|
484
|
+
return i + 2;
|
|
485
|
+
}
|
|
486
|
+
// `import('pkg')` / `import ('pkg')`: only a sole string argument closed by `)` is static.
|
|
487
|
+
if (next?.kind === 'punct' && next.value === '(') {
|
|
488
|
+
const specifier = soleStringSpecifier(tokens, i + 2);
|
|
489
|
+
if (specifier !== undefined) {
|
|
490
|
+
occurrences.push({ specifier, kind: 'dynamic-import' });
|
|
491
|
+
return i + 3;
|
|
492
|
+
}
|
|
493
|
+
return i + 1;
|
|
494
|
+
}
|
|
495
|
+
// Static `import ... from 'pkg'`: scan forward for `from` followed by a string.
|
|
496
|
+
return scanImportSource(tokens, i, occurrences, 'import');
|
|
497
|
+
}
|
|
498
|
+
/**
|
|
499
|
+
* Handle an `export` identifier. `export ... from 'pkg'` is `export-from`.
|
|
500
|
+
*/
|
|
501
|
+
function handleExport(tokens, i, occurrences) {
|
|
502
|
+
return scanImportSource(tokens, i, occurrences, 'export-from');
|
|
503
|
+
}
|
|
504
|
+
/**
|
|
505
|
+
* Scan forward from `i` looking for the `from` keyword followed by a string, recording it with
|
|
506
|
+
* `kind`. Stops early at a nested `import`/`export`/`require` so a following construct is handled on
|
|
507
|
+
* its own, except for `import`/`export` used as binding names inside the `{ ... }` clause of the
|
|
508
|
+
* current statement (e.g. `import { export as v } from 'pkg'`).
|
|
509
|
+
*/
|
|
510
|
+
function scanImportSource(tokens, i, occurrences, kind) {
|
|
511
|
+
const n = tokens.length;
|
|
512
|
+
let j = i + 1;
|
|
513
|
+
// How many `{ ... }` named clauses of the current statement are open. `import`/`export` are legal
|
|
514
|
+
// binding names inside such a clause (e.g. `import { export as v } from 'pkg'`), so they may only
|
|
515
|
+
// start a new construct at depth 0, otherwise the search stops before the real `from` source.
|
|
516
|
+
let braceDepth = 0;
|
|
517
|
+
while (j < n) {
|
|
518
|
+
const token = tokens[j];
|
|
519
|
+
if (token?.kind === 'punct') {
|
|
520
|
+
if (token.value === '{') {
|
|
521
|
+
braceDepth++;
|
|
522
|
+
}
|
|
523
|
+
else if (token.value === '}') {
|
|
524
|
+
// Clamped so an unmatched `}` cannot leave a negative depth that hides later boundaries.
|
|
525
|
+
braceDepth = braceDepth > 0 ? braceDepth - 1 : 0;
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
if (token?.kind === 'identifier') {
|
|
529
|
+
const next = tokens[j + 1];
|
|
530
|
+
// `import`/`export` only start a new construct outside the named clause. A `require`
|
|
531
|
+
// identifier only starts one when it begins a direct call: a binding named `require` (e.g.
|
|
532
|
+
// `import { require } from 'pkg'`) must not stop the search for the source that follows
|
|
533
|
+
// `from`.
|
|
534
|
+
if ((braceDepth === 0 && (token.value === 'import' || token.value === 'export')) ||
|
|
535
|
+
(token.value === 'require' && next?.kind === 'punct' && next.value === '(')) {
|
|
536
|
+
break;
|
|
537
|
+
}
|
|
538
|
+
if (token.value === 'from' && next?.kind === 'string') {
|
|
539
|
+
occurrences.push({ specifier: next.value, kind });
|
|
540
|
+
return j + 2;
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
j++;
|
|
544
|
+
}
|
|
545
|
+
// No source found; keep scanning from the next token.
|
|
546
|
+
return i + 1;
|
|
547
|
+
}
|
|
548
|
+
/**
|
|
549
|
+
* Handle a `require` identifier. `obj.require(...)` is ignored (preceded by `.`).
|
|
550
|
+
* `require('pkg')` is `require` only when the sole argument is a static string.
|
|
551
|
+
*/
|
|
552
|
+
function handleRequire(tokens, i, occurrences) {
|
|
553
|
+
const prev = tokens[i - 1];
|
|
554
|
+
// `obj.require(...)`: not the global CommonJS require.
|
|
555
|
+
if (prev?.kind === 'punct' && prev.value === '.') {
|
|
556
|
+
return i + 1;
|
|
557
|
+
}
|
|
558
|
+
const next = tokens[i + 1];
|
|
559
|
+
// `require('pkg')`: only a sole string argument closed by `)` is static.
|
|
560
|
+
if (next?.kind === 'punct' && next.value === '(') {
|
|
561
|
+
const specifier = soleStringSpecifier(tokens, i + 2);
|
|
562
|
+
if (specifier !== undefined) {
|
|
563
|
+
occurrences.push({ specifier, kind: 'require' });
|
|
564
|
+
return i + 3;
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
return i + 1;
|
|
568
|
+
}
|
|
569
|
+
/**
|
|
570
|
+
* The decoded string at `argIndex` when it is the only argument of the surrounding call, that is
|
|
571
|
+
* when it is a string and the very next token closes the call with `)`. Returns undefined otherwise,
|
|
572
|
+
* so a dynamic argument (e.g. `'pkg-' + name`) or a call with extra arguments (e.g.
|
|
573
|
+
* `require('pkg', ctx)`) never yields a static specifier.
|
|
574
|
+
*/
|
|
575
|
+
function soleStringSpecifier(tokens, argIndex) {
|
|
576
|
+
const arg = tokens[argIndex];
|
|
577
|
+
const closing = tokens[argIndex + 1];
|
|
578
|
+
if (arg?.kind !== 'string') {
|
|
579
|
+
return undefined;
|
|
580
|
+
}
|
|
581
|
+
if (closing?.kind !== 'punct' || closing.value !== ')') {
|
|
582
|
+
return undefined;
|
|
583
|
+
}
|
|
584
|
+
return arg.value;
|
|
585
|
+
}
|
|
586
|
+
/* -------------------------------------------------------------------------- */
|
|
587
|
+
/* Lexical constructs */
|
|
588
|
+
/* -------------------------------------------------------------------------- */
|
|
589
|
+
function skipBlockComment(source, start) {
|
|
590
|
+
const n = source.length;
|
|
591
|
+
let i = start + 2;
|
|
592
|
+
while (i < n) {
|
|
593
|
+
if (source[i] === '*' && source[i + 1] === '/') {
|
|
594
|
+
return i + 2;
|
|
595
|
+
}
|
|
596
|
+
i++;
|
|
597
|
+
}
|
|
598
|
+
throw new ModuleSpecifierError('Unterminated block comment', 'invalid-source', start);
|
|
599
|
+
}
|
|
600
|
+
function skipRegex(source, start) {
|
|
601
|
+
const n = source.length;
|
|
602
|
+
let i = start + 1;
|
|
603
|
+
let inClass = false;
|
|
604
|
+
while (i < n) {
|
|
605
|
+
const ch = source[i];
|
|
606
|
+
// A regex literal cannot contain a raw line terminator. Rejecting it here stops a wrong regex
|
|
607
|
+
// start from swallowing the following lines.
|
|
608
|
+
if (ch === '\n' || ch === '\r' || ch === '\u2028' || ch === '\u2029') {
|
|
609
|
+
throw new ModuleSpecifierError('Unterminated regex literal', 'invalid-source', start);
|
|
610
|
+
}
|
|
611
|
+
if (ch === '\\') {
|
|
612
|
+
i += 2;
|
|
613
|
+
continue;
|
|
614
|
+
}
|
|
615
|
+
if (inClass) {
|
|
616
|
+
if (ch === ']') {
|
|
617
|
+
inClass = false;
|
|
618
|
+
}
|
|
619
|
+
i++;
|
|
620
|
+
continue;
|
|
621
|
+
}
|
|
622
|
+
if (ch === '[') {
|
|
623
|
+
inClass = true;
|
|
624
|
+
i++;
|
|
625
|
+
continue;
|
|
626
|
+
}
|
|
627
|
+
if (ch === '/') {
|
|
628
|
+
return skipRegexFlags(source, i + 1);
|
|
629
|
+
}
|
|
630
|
+
i++;
|
|
631
|
+
}
|
|
632
|
+
throw new ModuleSpecifierError('Unterminated regex literal', 'invalid-source', start);
|
|
633
|
+
}
|
|
634
|
+
function skipRegexFlags(source, i) {
|
|
635
|
+
const n = source.length;
|
|
636
|
+
while (i < n) {
|
|
637
|
+
const ch = source[i];
|
|
638
|
+
if (ch === undefined || !/[a-z]/.test(ch)) {
|
|
639
|
+
break;
|
|
640
|
+
}
|
|
641
|
+
i++;
|
|
642
|
+
}
|
|
643
|
+
return i;
|
|
644
|
+
}
|
|
645
|
+
/**
|
|
646
|
+
* Read a single- or double-quoted string literal starting at `start`, decoding ordinary JS string
|
|
647
|
+
* escapes to the specifier's semantic value. Returns the decoded value and the index just past the
|
|
648
|
+
* closing quote.
|
|
649
|
+
*
|
|
650
|
+
* @throws {ModuleSpecifierError} on an unterminated string or a malformed escape sequence.
|
|
651
|
+
*/
|
|
652
|
+
function readString(source, start) {
|
|
653
|
+
const quote = source[start];
|
|
654
|
+
const n = source.length;
|
|
655
|
+
let i = start + 1;
|
|
656
|
+
let out = '';
|
|
657
|
+
while (i < n) {
|
|
658
|
+
const ch = source[i];
|
|
659
|
+
if (ch === '\\') {
|
|
660
|
+
const decoded = decodeEscape(source, i);
|
|
661
|
+
i = decoded.next;
|
|
662
|
+
out += decoded.value;
|
|
663
|
+
continue;
|
|
664
|
+
}
|
|
665
|
+
if (ch === quote) {
|
|
666
|
+
return { value: out, next: i + 1 };
|
|
667
|
+
}
|
|
668
|
+
// A raw LF/CR inside a quoted string is invalid JavaScript; reject it instead of continuing to
|
|
669
|
+
// scan (which would otherwise swallow later code as part of the string). Raw U+2028 and U+2029
|
|
670
|
+
// are valid inside strings since ES2019, so they are kept as part of the value.
|
|
671
|
+
if (ch === '\n' || ch === '\r') {
|
|
672
|
+
throw new ModuleSpecifierError('Unterminated string literal', 'invalid-source', start);
|
|
673
|
+
}
|
|
674
|
+
if (ch !== undefined) {
|
|
675
|
+
out += ch;
|
|
676
|
+
}
|
|
677
|
+
i++;
|
|
678
|
+
}
|
|
679
|
+
throw new ModuleSpecifierError('Unterminated string literal', 'invalid-source', start);
|
|
680
|
+
}
|
|
681
|
+
/**
|
|
682
|
+
* Decode one escape sequence starting with a backslash at `start`.
|
|
683
|
+
*
|
|
684
|
+
* Returns the decoded character(s) and the index just past the escape. Line continuations are
|
|
685
|
+
* dropped. A malformed escape throws.
|
|
686
|
+
*/
|
|
687
|
+
function decodeEscape(source, start) {
|
|
688
|
+
const n = source.length;
|
|
689
|
+
const backslash = start;
|
|
690
|
+
const i = start + 1; // past the backslash
|
|
691
|
+
if (i >= n) {
|
|
692
|
+
throw new ModuleSpecifierError('Unterminated string literal', 'invalid-source', backslash);
|
|
693
|
+
}
|
|
694
|
+
const e = source[i] ?? '';
|
|
695
|
+
// Line continuation: a backslash before a line break (including the Unicode line separators
|
|
696
|
+
// U+2028 and U+2029) is dropped from the decoded value.
|
|
697
|
+
if (e === '\n' || e === '\u2028' || e === '\u2029') {
|
|
698
|
+
return { value: '', next: i + 1 };
|
|
699
|
+
}
|
|
700
|
+
if (e === '\r') {
|
|
701
|
+
return { value: '', next: source[i + 1] === '\n' ? i + 2 : i + 1 };
|
|
702
|
+
}
|
|
703
|
+
// Legacy octal escapes (`\0`-`\7` plus the following octal digits) are accepted and decoded: they
|
|
704
|
+
// are valid in sloppy-mode CommonJS, and rejecting them would make a whole source un-analysable
|
|
705
|
+
// even though the string has nothing to do with dependency detection. `\8` and `\9` fall through
|
|
706
|
+
// to the identity rule below, which is exactly what Annex-B specifies for them.
|
|
707
|
+
if (e >= '0' && e <= '7') {
|
|
708
|
+
return decodeLegacyOctal(source, i);
|
|
709
|
+
}
|
|
710
|
+
const simple = {
|
|
711
|
+
'\\': '\\',
|
|
712
|
+
n: '\n',
|
|
713
|
+
r: '\r',
|
|
714
|
+
t: '\t',
|
|
715
|
+
b: '\b',
|
|
716
|
+
f: '\f',
|
|
717
|
+
v: '\v',
|
|
718
|
+
};
|
|
719
|
+
if (e === "'" || e === '"' || e === '`') {
|
|
720
|
+
return { value: e, next: i + 1 };
|
|
721
|
+
}
|
|
722
|
+
const single = simple[e];
|
|
723
|
+
if (single !== undefined) {
|
|
724
|
+
return { value: single, next: i + 1 };
|
|
725
|
+
}
|
|
726
|
+
if (e === 'x') {
|
|
727
|
+
return decodeHex(source, i + 1, 2);
|
|
728
|
+
}
|
|
729
|
+
if (e === 'u') {
|
|
730
|
+
if (source[i + 1] === '{') {
|
|
731
|
+
return decodeUnicodeBraced(source, i + 2);
|
|
732
|
+
}
|
|
733
|
+
return decodeHex(source, i + 1, 4);
|
|
734
|
+
}
|
|
735
|
+
// Any other character after a backslash evaluates to itself (e.g. `\q` is `q`, `\/` is `/`).
|
|
736
|
+
return { value: e, next: i + 1 };
|
|
737
|
+
}
|
|
738
|
+
/**
|
|
739
|
+
* Decode a legacy octal escape whose first digit (`0`-`7`) sits at `start`.
|
|
740
|
+
*
|
|
741
|
+
* The first digit bounds how many octal digits the escape may consume: `0`-`3` allow up to three
|
|
742
|
+
* digits, `4`-`7` up to two. A lone `\0` decodes to NUL.
|
|
743
|
+
*/
|
|
744
|
+
function decodeLegacyOctal(source, start) {
|
|
745
|
+
const first = source[start] ?? '';
|
|
746
|
+
const maxDigits = first >= '4' ? 2 : 3;
|
|
747
|
+
let i = start;
|
|
748
|
+
let digits = '';
|
|
749
|
+
while (digits.length < maxDigits) {
|
|
750
|
+
const digit = source[i];
|
|
751
|
+
if (digit === undefined || digit < '0' || digit > '7') {
|
|
752
|
+
break;
|
|
753
|
+
}
|
|
754
|
+
digits += digit;
|
|
755
|
+
i++;
|
|
756
|
+
}
|
|
757
|
+
return { value: String.fromCharCode(parseInt(digits, 8)), next: i };
|
|
758
|
+
}
|
|
759
|
+
function decodeHex(source, start, length) {
|
|
760
|
+
const digits = source.slice(start, start + length);
|
|
761
|
+
if (digits.length !== length || !/^[0-9a-fA-F]+$/.test(digits)) {
|
|
762
|
+
throw new ModuleSpecifierError('Invalid hex escape', 'invalid-source', start);
|
|
763
|
+
}
|
|
764
|
+
return { value: String.fromCharCode(parseInt(digits, 16)), next: start + length };
|
|
765
|
+
}
|
|
766
|
+
function decodeUnicodeBraced(source, start) {
|
|
767
|
+
let i = start;
|
|
768
|
+
let code = 0;
|
|
769
|
+
while (i < source.length) {
|
|
770
|
+
const hex = source[i];
|
|
771
|
+
if (hex === undefined || !/[0-9a-fA-F]/.test(hex)) {
|
|
772
|
+
break;
|
|
773
|
+
}
|
|
774
|
+
code = code * 16 + parseInt(hex, 16);
|
|
775
|
+
// Stop accumulating once the value exceeds the maximum code point to avoid unbounded growth.
|
|
776
|
+
if (code > 0x10ffff) {
|
|
777
|
+
break;
|
|
778
|
+
}
|
|
779
|
+
i++;
|
|
780
|
+
}
|
|
781
|
+
// Reject empty braced escapes (`\u{}`) and code points above the maximum Unicode value.
|
|
782
|
+
if (source[i] !== '}' || i === start || code > 0x10ffff) {
|
|
783
|
+
throw new ModuleSpecifierError('Invalid \\u{...} escape', 'invalid-source', start);
|
|
784
|
+
}
|
|
785
|
+
return { value: String.fromCodePoint(code), next: i + 1 };
|
|
786
|
+
}
|
|
787
|
+
/* -------------------------------------------------------------------------- */
|
|
788
|
+
/* Character helpers */
|
|
789
|
+
/* -------------------------------------------------------------------------- */
|
|
790
|
+
// The Unicode `Zs` (`Space_Separator`) category, so every one of those characters separates tokens
|
|
791
|
+
// instead of being read as punctuation.
|
|
792
|
+
const SPACE_SEPARATOR = /\p{Zs}/u;
|
|
793
|
+
/**
|
|
794
|
+
* The characters that separate tokens, that is the ECMAScript `WhiteSpace` production: the Unicode
|
|
795
|
+
* `Space_Separator` (`Zs`) characters (plain space, NBSP, EM SPACE, IDEOGRAPHIC SPACE, ...), tab,
|
|
796
|
+
* vertical tab, form feed, ZWNBSP/BOM, plus every `LineTerminator`. Only used for the gap between
|
|
797
|
+
* tokens; string contents are handled by `readString`, so a raw U+2028/U+2029 inside a quoted string
|
|
798
|
+
* stays valid source while a raw LF/CR there stays an error.
|
|
799
|
+
*/
|
|
800
|
+
function isWhitespace(ch) {
|
|
801
|
+
return (ch === '\t' ||
|
|
802
|
+
ch === '\v' ||
|
|
803
|
+
ch === '\f' ||
|
|
804
|
+
ch === '\uFEFF' ||
|
|
805
|
+
isLineTerminator(ch) ||
|
|
806
|
+
SPACE_SEPARATOR.test(ch));
|
|
807
|
+
}
|
|
808
|
+
/**
|
|
809
|
+
* The ECMAScript `LineTerminator` characters. Line comments and the hashbang end at any of them,
|
|
810
|
+
* not only at `\n`. `undefined` (end of the source indexed past its end) is not a terminator.
|
|
811
|
+
*/
|
|
812
|
+
function isLineTerminator(ch) {
|
|
813
|
+
return ch === '\n' || ch === '\r' || ch === '\u2028' || ch === '\u2029';
|
|
814
|
+
}
|
|
815
|
+
function isIdentifierStart(ch) {
|
|
816
|
+
return /[A-Za-z_$]/.test(ch);
|
|
817
|
+
}
|
|
818
|
+
function isIdentifierPart(ch) {
|
|
819
|
+
return /[A-Za-z0-9_$]/.test(ch);
|
|
820
|
+
}
|
|
821
|
+
function isNumberStart(ch) {
|
|
822
|
+
return /[0-9]/.test(ch);
|
|
823
|
+
}
|
|
824
|
+
//# sourceMappingURL=moduleSpecifiers.js.map
|