depvitals 0.0.1 → 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.
Files changed (40) hide show
  1. package/README.md +67 -10
  2. package/dist/cli/index.js +13 -7
  3. package/dist/cli/index.js.map +1 -1
  4. package/dist/cli/parseCliArgs.d.ts +37 -0
  5. package/dist/cli/parseCliArgs.d.ts.map +1 -0
  6. package/dist/cli/parseCliArgs.js +85 -0
  7. package/dist/cli/parseCliArgs.js.map +1 -0
  8. package/dist/cli/runCli.d.ts +31 -13
  9. package/dist/cli/runCli.d.ts.map +1 -1
  10. package/dist/cli/runCli.js +76 -84
  11. package/dist/cli/runCli.js.map +1 -1
  12. package/dist/core/analyzeProject.d.ts +92 -0
  13. package/dist/core/analyzeProject.d.ts.map +1 -0
  14. package/dist/core/analyzeProject.js +175 -0
  15. package/dist/core/analyzeProject.js.map +1 -0
  16. package/dist/reporters/terminalReporter.d.ts +49 -0
  17. package/dist/reporters/terminalReporter.d.ts.map +1 -0
  18. package/dist/reporters/terminalReporter.js +98 -0
  19. package/dist/reporters/terminalReporter.js.map +1 -0
  20. package/dist/scanner/moduleSpecifiers.d.ts +60 -0
  21. package/dist/scanner/moduleSpecifiers.d.ts.map +1 -0
  22. package/dist/scanner/moduleSpecifiers.js +824 -0
  23. package/dist/scanner/moduleSpecifiers.js.map +1 -0
  24. package/dist/scanner/packageSpecifier.d.ts +45 -0
  25. package/dist/scanner/packageSpecifier.d.ts.map +1 -0
  26. package/dist/scanner/packageSpecifier.js +98 -0
  27. package/dist/scanner/packageSpecifier.js.map +1 -0
  28. package/dist/scanner/projectManifest.d.ts +72 -0
  29. package/dist/scanner/projectManifest.d.ts.map +1 -0
  30. package/dist/scanner/projectManifest.js +176 -0
  31. package/dist/scanner/projectManifest.js.map +1 -0
  32. package/dist/scanner/scriptUsage.d.ts +50 -0
  33. package/dist/scanner/scriptUsage.d.ts.map +1 -0
  34. package/dist/scanner/scriptUsage.js +461 -0
  35. package/dist/scanner/scriptUsage.js.map +1 -0
  36. package/dist/scanner/sourceFiles.d.ts +52 -0
  37. package/dist/scanner/sourceFiles.d.ts.map +1 -0
  38. package/dist/scanner/sourceFiles.js +190 -0
  39. package/dist/scanner/sourceFiles.js.map +1 -0
  40. 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