@davidwells/extract-deps 0.1.5 → 0.1.7

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@davidwells/extract-deps",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
4
4
  "description": "Get imports and requires from a codebase without AST",
5
5
  "main": "src/index.js",
6
6
  "exports": {
package/src/dep-graph.js CHANGED
@@ -90,8 +90,14 @@ function verifyImportedValuesExist(dep, targetDeps, targetPath, sourcePath, erro
90
90
  }
91
91
 
92
92
  // Check ESM exports
93
+ let hasReExportAll = false
93
94
  if (targetDeps.exports && targetDeps.exports.length > 0) {
94
95
  for (const exp of targetDeps.exports) {
96
+ if (exp.exportType === 'all') {
97
+ // export * from './module' — can't know what names are re-exported without
98
+ // following the chain, so skip verification for this module
99
+ hasReExportAll = true
100
+ }
95
101
  const exportedName = exp.exportAlias || exp.exportName
96
102
  if (exportedName) {
97
103
  exportedNames.add(exportedName)
@@ -99,6 +105,11 @@ function verifyImportedValuesExist(dep, targetDeps, targetPath, sourcePath, erro
99
105
  }
100
106
  }
101
107
 
108
+ // If module uses export * from, we can't verify names — skip
109
+ if (hasReExportAll) {
110
+ return
111
+ }
112
+
102
113
  // Verify each imported name is exported
103
114
  for (const importedName of importedNames) {
104
115
  if (!exportedNames.has(importedName)) {
@@ -121,6 +132,10 @@ function verifyImportedValuesExist(dep, targetDeps, targetPath, sourcePath, erro
121
132
  // Only applies to ESM imports - CJS require() gets whatever module.exports is
122
133
  else {
123
134
  if (dep.isESM) {
135
+ // Namespace imports (import * as X) don't need a default export
136
+ if (dep.varName && dep.varName.startsWith('*')) {
137
+ return
138
+ }
124
139
  // ESM import X from 'pkg' requires a default export
125
140
  const hasModuleExports = targetDeps.moduleExports && targetDeps.moduleExports.length > 0
126
141
  const hasESMExports = targetDeps.exports && targetDeps.exports.length > 0
@@ -1,13 +1,13 @@
1
1
  const { isBuiltIn, isRelativeImport, isRelativeImportWithQuotes, getNames, stripComments } = require('./utils')
2
2
 
3
3
  // https://regex101.com/r/cfEmlm/1
4
- const IMPORT_REGEX = /import(?:["'\s]*([\w*{}\n\r\t, ]+)from\s*)?["'\s](.*[@\w_-]+)["'\s]+;?$/gm
4
+ const IMPORT_REGEX = /import(?:["'\s]*([\w*{}\n\r\t, $]+)from\s*)?["'\s](.*[@\w_-]+)["'\s]+;?$/gm
5
5
  // https://regex101.com/r/UDmXlb/1
6
6
  const IMPORT_TYPE_REGEX = /import\(['"](.*)['"]\)\.?([A-Za-z_0-9.]*)?/gm
7
7
  // https://regex101.com/r/UDjMoV/1
8
- const DYNAMIC_IMPORT_REGEX = /(?:(const|var|let)\s+([\w*{}\n\r\t,:= ]+)\s*=\s*)?await\s+import\(([^()]+(?:\([^()]*\)[^()]*)*)\)/gm
8
+ const DYNAMIC_IMPORT_REGEX = /(?:(const|var|let)\s+([\w*{}\n\r\t,:= $]+)\s*=\s*)?await\s+import\(([^()]+(?:\([^()]*\)[^()]*)*)\)/gm
9
9
  // https://regex101.com/r/example/1
10
- const ASSERTION_IMPORT_REGEX = /(?:^|\n)\s*import\s+(?:["'\s]*([\w*{}\n\r\t, ]+)from\s*)?["'\s](.*[@\w_-]+)["'\s]+(assert|with)\s*\{([^}]+)\};?$/gm
10
+ const ASSERTION_IMPORT_REGEX = /(?:^|\n)\s*import\s+(?:["'\s]*([\w*{}\n\r\t, $]+)from\s*)?["'\s](.*[@\w_-]+)["'\s]+(assert|with)\s*\{([^}]+)\};?$/gm
11
11
 
12
12
  function getImports(code = '', options = {}) {
13
13
  // Strip comments to avoid false positives
@@ -27,7 +27,19 @@ function getStaticImports(targetText = '') {
27
27
  var result;
28
28
  while((result = IMPORT_REGEX.exec(targetText)) !== null) {
29
29
  const [ line, names, from ] = result
30
- const namesValues = (names || '').trim()
30
+ // Skip TypeScript type-only imports — erased at compile time, no runtime resolution needed
31
+ if (/^\s*import\s+type\s/.test(line)) {
32
+ continue
33
+ }
34
+ // Skip false matches where `import` appears inside a string literal (e.g. `= 'import' | 'internal'`)
35
+ // A valid module path won't contain operators like | = ( )
36
+ if (/[|=()]/.test(from)) {
37
+ continue
38
+ }
39
+ // Remove inline `type` imports from destructured names — they're erased at compile time
40
+ // e.g. `{ foo, type Bar, type Baz }` → `{ foo }` (Bar and Baz are type-only)
41
+ const rawNames = (names || '').trim()
42
+ const namesValues = rawNames.replace(/,?\s*\btype\s+[\w$]+/g, '').replace(/^,\s*/, '').trim()
31
43
  importStatements.push({
32
44
  line,
33
45
  varType: 'const',
@@ -67,19 +79,21 @@ function getDynamicImports(targetText = '') {
67
79
  let importStatements = []
68
80
  var result;
69
81
  while((result = DYNAMIC_IMPORT_REGEX.exec(targetText)) !== null) {
70
- const [ line, varType, names, from ] = result
82
+ const [ line, varType, names, rawFrom ] = result
71
83
  let namesValues = (names || '').trim()
72
-
84
+ // Strip surrounding quotes from dynamic import paths (e.g. './foo.js' → ./foo.js)
85
+ const from = rawFrom.trim().replace(/^["'`]|["'`]$/g, '')
86
+
73
87
  importStatements.push({
74
88
  line,
75
89
  varType: varType || '',
76
90
  varName: namesValues,
77
91
  definitions: names ? getNames(namesValues) : null,
78
- from: from.trim(),
92
+ from,
79
93
  index: result.index,
80
94
  isImport: true,
81
- isRelative: isRelativeImport(from) || isRelativeImportWithQuotes(from),
82
- isBuiltIn: isBuiltIn(from.trim()),
95
+ isRelative: isRelativeImport(from),
96
+ isBuiltIn: isBuiltIn(from),
83
97
  isDynamic: true
84
98
  })
85
99
  }
@@ -1,9 +1,11 @@
1
1
  const { isBuiltIn, isRelativeImport, getNames } = require('./utils')
2
2
 
3
3
  // Updated regex to exclude ES6 dynamic imports
4
- const REQUIRE_REGEX = /(?!.*await\s+import)(const|var|let)(?:["'\s]*([\w*{}:=\n\r\t,:= ]+)\s*=\s*require\(\s*)?["'\s](.*[@\w_-]+)["'\s]+\);?$|(?!.*await\s+import)require\(["'\s](.*[@\w_-]+)["'\s]\)/gm
4
+ // require() must always be present — previous version made it optional, causing false matches
5
+ // on any const/let/var line containing string literals (e.g. `const x = arr.filter(e => e.type === 'foo')`)
6
+ const REQUIRE_REGEX = /(?!.*await\s+import)(?:(const|var|let)\s+([\w*{}:=\n\r\t,:= $]+)\s*=\s*)?require\(\s*["'](.*[@\w_-]+)["']\s*\);?$/gm
5
7
  // https://regex101.com/r/kZ5xCk/3
6
- const DYNAMIC_REQUIRE_REGEX = /(?:(const|var|let)\s+([\w*{}\n\r\t,:= ]+)\s*=\s*)?require\(([^()'"]+(?:\([^()]*\)[^()'"]*)*)\)/gm
8
+ const DYNAMIC_REQUIRE_REGEX = /(?:(const|var|let)\s+([\w*{}\n\r\t,:= $]+)\s*=\s*)?require\(([^()'"]+(?:\([^()]*\)[^()'"]*)*)\)/gm
7
9
 
8
10
  function getDynamicRequires(code) {
9
11
  const results = []
@@ -41,9 +43,9 @@ function getStaticRequires(targetText = '') {
41
43
  let requireStatements = []
42
44
  var result;
43
45
  while((result = REQUIRE_REGEX.exec(targetText)) !== null) {
44
- const [ line, varType, names, from, fromTwo ] = result
46
+ const [ line, varType, names, from ] = result
45
47
  // console.log('getStaticRequires result', result)
46
- const importLocation = from || fromTwo
48
+ const importLocation = from
47
49
  const importItems = (names || '').trim()
48
50
  requireStatements.push({
49
51
  line,
@@ -550,4 +550,94 @@ test('verifyNodeModules with returnErrors should collect export mismatch errors'
550
550
  assert.ok(result.errors[0].message.includes('does not have a default export'), 'Should be default export error')
551
551
  })
552
552
 
553
+ // Re-export (export * from) tests
554
+ const ESM_REEXPORT_FIXTURE = path.resolve(__dirname, './fixtures/esm-reexport-fixture/index.js')
555
+
556
+ test('import from barrel with export * should not fail verification', () => {
557
+ assert.not.throws(() => {
558
+ depGraph(ESM_REEXPORT_FIXTURE, { verifyGraph: true })
559
+ }, 'Importing named exports through export * barrel should not fail')
560
+ })
561
+
562
+ test('import from barrel with export * should build valid graph', () => {
563
+ const graph = depGraph(ESM_REEXPORT_FIXTURE, { verifyGraph: true })
564
+
565
+ assert.ok(graph instanceof Map, 'Should return a Map')
566
+ const indexPath = Array.from(graph.keys()).find(key => key.includes('esm-reexport-fixture/index.js'))
567
+ assert.ok(indexPath, 'Should have index.js in graph')
568
+
569
+ const deps = graph.get(indexPath)
570
+ assert.ok(deps.some(d => d.includes('barrel.js')), 'Should have barrel.js as dependency')
571
+ })
572
+
573
+ // Namespace import (import * as X) tests
574
+ const ESM_NAMESPACE_FIXTURE = path.resolve(__dirname, './fixtures/esm-namespace-fixture/index.js')
575
+
576
+ test('namespace import (import * as X) should not require default export', () => {
577
+ assert.not.throws(() => {
578
+ depGraph(ESM_NAMESPACE_FIXTURE, { verifyGraph: true })
579
+ }, 'import * as X should not fail when target has no default export')
580
+ })
581
+
582
+ test('namespace import should build valid graph', () => {
583
+ const graph = depGraph(ESM_NAMESPACE_FIXTURE, { verifyGraph: true })
584
+
585
+ assert.ok(graph instanceof Map, 'Should return a Map')
586
+ assert.ok(graph.size > 0, 'Graph should not be empty')
587
+
588
+ const indexPath = Array.from(graph.keys()).find(key => key.includes('esm-namespace-fixture/index.js'))
589
+ assert.ok(indexPath, 'Should have index.js in graph')
590
+
591
+ const deps = graph.get(indexPath)
592
+ assert.ok(deps.some(d => d.includes('utils.js')), 'Should have utils.js as dependency')
593
+ })
594
+
595
+ // TypeScript patterns fixture — exercises all fixed false positive patterns
596
+ const TS_PATTERNS_FIXTURE = path.resolve(__dirname, './fixtures/ts-patterns-fixture/index.ts')
597
+
598
+ test('TS patterns: import type, inline type, import * as, $-prefixed names should all pass', () => {
599
+ // This fixture contains:
600
+ // - import type { Context } from 'hono' (should be skipped)
601
+ // - import { helper, type HelperOptions } from './utils.js' (inline type stripped)
602
+ // - import * as Constants from './constants.js' (namespace import, no default needed)
603
+ // - import { $transform } from './transforms.js' ($ in name)
604
+ // - export type = 'import' | 'internal' (type literal, not an import)
605
+ // - c.req.valid('param'), .filter(), .some(), c.req.header() (not requires)
606
+ assert.not.throws(() => {
607
+ depGraph(TS_PATTERNS_FIXTURE, { verifyGraph: true })
608
+ }, 'All TS patterns should pass verification without errors')
609
+ })
610
+
611
+ test('TS patterns: should build correct dependency graph', () => {
612
+ const graph = depGraph(TS_PATTERNS_FIXTURE, { verifyGraph: true })
613
+
614
+ assert.ok(graph instanceof Map, 'Should return a Map')
615
+
616
+ const indexPath = Array.from(graph.keys()).find(key => key.includes('ts-patterns-fixture/index.ts'))
617
+ assert.ok(indexPath, 'Should have index.ts in graph')
618
+
619
+ const deps = graph.get(indexPath)
620
+ // Should have utils.ts, constants.ts, transforms.ts as deps
621
+ assert.ok(deps.some(d => d.includes('utils.ts')), 'Should depend on utils.ts')
622
+ assert.ok(deps.some(d => d.includes('constants.ts')), 'Should depend on constants.ts')
623
+ assert.ok(deps.some(d => d.includes('transforms.ts')), 'Should depend on transforms.ts')
624
+ })
625
+
626
+ test('TS patterns: resolveDepPaths should not include import type packages', () => {
627
+ const { nodeModules } = resolveDepPaths(TS_PATTERNS_FIXTURE)
628
+
629
+ // 'hono' was import type — should NOT appear in nodeModules
630
+ assert.ok(!nodeModules.includes('hono'), 'import type packages should not appear in nodeModules')
631
+ })
632
+
633
+ test('TS patterns: resolveDepPaths with returnErrors should have zero errors', () => {
634
+ const result = resolveDepPaths(TS_PATTERNS_FIXTURE, { returnErrors: true })
635
+
636
+ assert.ok(Array.isArray(result.errors), 'Should have errors array')
637
+ if (result.errors.length > 0) {
638
+ console.log('Unexpected errors:', result.errors.map(e => e.message))
639
+ }
640
+ assert.is(result.errors.length, 0, 'Should have zero verification errors')
641
+ })
642
+
553
643
  test.run()
@@ -0,0 +1,5 @@
1
+ import * as Utils from './utils.js'
2
+
3
+ export function main() {
4
+ return Utils.helper()
5
+ }
@@ -0,0 +1,4 @@
1
+ {
2
+ "name": "esm-namespace-fixture",
3
+ "type": "module"
4
+ }
@@ -0,0 +1,7 @@
1
+ export function helper() {
2
+ return 'hello'
3
+ }
4
+
5
+ export function other() {
6
+ return 'world'
7
+ }
@@ -0,0 +1 @@
1
+ export * from './impl.js'
@@ -0,0 +1,7 @@
1
+ export function helper() {
2
+ return 'hello'
3
+ }
4
+
5
+ export function other() {
6
+ return 'world'
7
+ }
@@ -0,0 +1,5 @@
1
+ import { helper, other } from './barrel.js'
2
+
3
+ export function main() {
4
+ return helper() + other()
5
+ }
@@ -0,0 +1,4 @@
1
+ {
2
+ "name": "esm-reexport-fixture",
3
+ "type": "module"
4
+ }
@@ -0,0 +1,2 @@
1
+ export const DEFAULT_NAME = 'world'
2
+ export const MAX_RETRIES = 3
@@ -0,0 +1,17 @@
1
+ // This fixture exercises TypeScript patterns that previously caused false positives
2
+ import type { Context } from 'hono'
3
+ import { helper, type HelperOptions } from './utils.js'
4
+ import * as Constants from './constants.js'
5
+ import { $transform } from './transforms.js'
6
+
7
+ export type EnrollmentSource = 'import' | 'internal' | 'manual'
8
+
9
+ export async function handler(c: Context) {
10
+ const { email } = c.req.valid('param')
11
+ const active = items.filter((e) => e.status === 'active')
12
+ const replied = timeline.some((e) => e.type === 'reply' || e.type === 'stop')
13
+ const key = c.req.header('Idempotency-Key')
14
+
15
+ const result = helper({ name: Constants.DEFAULT_NAME })
16
+ return $transform(result)
17
+ }
@@ -0,0 +1,4 @@
1
+ {
2
+ "name": "ts-patterns-fixture",
3
+ "type": "module"
4
+ }
@@ -0,0 +1,7 @@
1
+ export function $transform(value: string): string {
2
+ return value.toUpperCase()
3
+ }
4
+
5
+ export function $validate(value: string): boolean {
6
+ return value.length > 0
7
+ }
@@ -0,0 +1,7 @@
1
+ export interface HelperOptions {
2
+ name: string
3
+ }
4
+
5
+ export function helper(opts: HelperOptions): string {
6
+ return opts.name
7
+ }
@@ -170,7 +170,7 @@ test('getDynamicImports', async () => {
170
170
  varType: 'const',
171
171
  varName: 'module',
172
172
  definitions: { declaration: 'module', name: 'module' },
173
- from: "'./module.js'",
173
+ from: './module.js',
174
174
  isImport: true,
175
175
  isRelative: true,
176
176
  isBuiltIn: false,
@@ -192,9 +192,9 @@ test('getDynamicImports', async () => {
192
192
  varType: 'const',
193
193
  varName: 'component',
194
194
  definitions: { declaration: 'component', name: 'component' },
195
- from: '`./src/${folder}/Button.js`',
195
+ from: './src/${folder}/Button.js',
196
196
  isImport: true,
197
- isRelative: false,
197
+ isRelative: true,
198
198
  isBuiltIn: false,
199
199
  isDynamic: true
200
200
  },
@@ -226,7 +226,7 @@ test('getDynamicImports', async () => {
226
226
  ],
227
227
  name: '{ default: Component, helper }'
228
228
  },
229
- from: "'./Component.js'",
229
+ from: './Component.js',
230
230
  isImport: true,
231
231
  isRelative: true,
232
232
  isBuiltIn: false,
@@ -428,6 +428,103 @@ import realImport from 'real'
428
428
  assert.equal(importsFound[0].from, 'real', 'Should find real import')
429
429
  })
430
430
 
431
+ test('should handle $ in import names', () => {
432
+ const code = `
433
+ import { $add } from 'dynamodb-toolbox/entity/actions/update'
434
+ import { UpdateItemCommand, $add } from 'dynamodb-toolbox/entity/actions/update'
435
+ import { $remove } from 'dynamodb-toolbox/entity/actions/update'
436
+ `
437
+
438
+ const importsFound = getImports(code)
439
+
440
+ assert.equal(importsFound.length, 3, 'Should find 3 imports')
441
+ assert.equal(importsFound[0].from, 'dynamodb-toolbox/entity/actions/update')
442
+ assert.equal(importsFound[0].varName, '{ $add }')
443
+ assert.equal(importsFound[1].varName, '{ UpdateItemCommand, $add }')
444
+ assert.equal(importsFound[2].varName, '{ $remove }')
445
+ })
446
+
447
+ test('dynamic import should strip quotes from path', () => {
448
+ const code = `
449
+ const { findContactEmail } = await import('./sfdc-reverse-lookup.js')
450
+ const { invokeAsync } = await import('../../lib/invoke-async.js')
451
+ const { SNSClient } = await import('@aws-sdk/client-sns')
452
+ `
453
+
454
+ const importsFound = getDynamicImports(code)
455
+
456
+ assert.equal(importsFound.length, 3, 'Should find 3 dynamic imports')
457
+ assert.equal(importsFound[0].from, './sfdc-reverse-lookup.js', 'Should strip quotes from relative path')
458
+ assert.is(importsFound[0].isRelative, true, 'Should detect as relative')
459
+ assert.equal(importsFound[1].from, '../../lib/invoke-async.js', 'Should strip quotes from relative path')
460
+ assert.is(importsFound[1].isRelative, true, 'Should detect as relative')
461
+ assert.equal(importsFound[2].from, '@aws-sdk/client-sns', 'Should strip quotes from package path')
462
+ })
463
+
464
+ test('should skip type literal unions containing import keyword', () => {
465
+ const code = `
466
+ export type EnrollmentSource = 'import' | 'internal' | 'manual'
467
+ export type ActionType = 'import' | 'export'
468
+ const x: 'import' | 'require' = getType()
469
+ `
470
+
471
+ const importsFound = getImports(code)
472
+ assert.equal(importsFound.length, 0, 'Should not match type literal unions')
473
+ })
474
+
475
+ test('should remove inline type-only names from destructured imports', () => {
476
+ const code = `
477
+ import { listReviewActions, type ReviewActionChannel, type ReviewActionStatus } from '../lib/action-review.js'
478
+ import { normalizeWorkflowNodes, type WorkflowNode } from '../../lib/workflow-node.js'
479
+ import { type Foo, bar, type Baz } from 'some-pkg'
480
+ `
481
+
482
+ const importsFound = getImports(code)
483
+ assert.equal(importsFound.length, 3, 'Should find 3 imports')
484
+
485
+ // Type-only names should be removed entirely (not just renamed)
486
+ const firstDefs = importsFound[0].definitions.destructuredParameters.map(p => p.name)
487
+ assert.equal(firstDefs.length, 1, 'Should only have 1 runtime import')
488
+ assert.ok(firstDefs.includes('listReviewActions'), 'Should keep listReviewActions')
489
+ assert.ok(!firstDefs.includes('ReviewActionChannel'), 'type ReviewActionChannel should be removed')
490
+ assert.ok(!firstDefs.includes('ReviewActionStatus'), 'type ReviewActionStatus should be removed')
491
+
492
+ const secondDefs = importsFound[1].definitions.destructuredParameters.map(p => p.name)
493
+ assert.equal(secondDefs.length, 1, 'Should only have 1 runtime import')
494
+ assert.ok(secondDefs.includes('normalizeWorkflowNodes'), 'Should keep normalizeWorkflowNodes')
495
+ assert.ok(!secondDefs.includes('WorkflowNode'), 'type WorkflowNode should be removed')
496
+
497
+ // Third: type Foo and type Baz removed, only bar remains
498
+ const thirdDefs = importsFound[2].definitions.destructuredParameters.map(p => p.name)
499
+ assert.equal(thirdDefs.length, 1, 'Should only have 1 runtime import')
500
+ assert.ok(thirdDefs.includes('bar'), 'Should keep bar')
501
+ })
502
+
503
+ test('should handle all-type destructured imports', () => {
504
+ const code = `import { type Foo, type Bar } from 'pkg'`
505
+
506
+ const importsFound = getImports(code)
507
+ assert.equal(importsFound.length, 1, 'Should still find the import statement')
508
+ // No runtime names to verify — definitions may be empty
509
+ const defs = importsFound[0].definitions?.destructuredParameters || []
510
+ const runtimeNames = defs.filter(p => !p.name.startsWith('type '))
511
+ assert.equal(runtimeNames.length, 0, 'Should have no runtime import names')
512
+ })
513
+
514
+ test('should skip TypeScript import type statements', () => {
515
+ const code = `
516
+ import type { Context } from 'hono'
517
+ import type { APIGatewayProxyEventV2 } from 'aws-lambda'
518
+ import type { WsEventType } from './ws-events.js'
519
+ import { realImport } from 'real-package'
520
+ `
521
+
522
+ const importsFound = getImports(code)
523
+
524
+ assert.equal(importsFound.length, 1, 'Should only find 1 real import')
525
+ assert.equal(importsFound[0].from, 'real-package', 'Should find real-package import')
526
+ })
527
+
431
528
  test('should preserve URLs with :// in strings', () => {
432
529
  const code = `
433
530
  import foo from 'foo' // http://example.com
@@ -208,4 +208,64 @@ test('getRequires with dynamic requires', async () => {
208
208
  ])
209
209
  })
210
210
 
211
+ test('should not match non-require patterns with string literals', async () => {
212
+ const code = `
213
+ const { email: bdrEmail } = c.req.valid('param')
214
+ const activeBdrs = allActive.filter((u) => u.email !== 'CONFIG')
215
+ const connections = await Connection.getByMember(bdr.email, 'gmail')
216
+ const replied = timeline.some((e) => e.type === 'reply' || e.type === 'stop')
217
+ const lastSent = timeline.find((e) => e.type === 'email_sent')
218
+ const key = c.req.header('Idempotency-Key')
219
+ const { enabled, endDate, handoff } = c.req.valid('json')
220
+ const active = enrollmentItems.filter((e) => e.status === 'active')
221
+ const { limit } = c.req.valid('query')
222
+ const vacationPaused = enrollmentItems.filter((e) => e.status === 'paused' && e.pauseReason === 'vacation')
223
+ const activeEnrollments = enrollmentItems.filter((e) => e.status === 'active' || e.status === 'pending_review')
224
+ `
225
+
226
+ const requiresFound = getRequires(code)
227
+ assert.equal(requiresFound.length, 0, 'Should not match any non-require patterns')
228
+ })
229
+
230
+ test('should handle $ and _ in variable names', async () => {
231
+ const code = `
232
+ const { $add } = require('dynamodb-toolbox')
233
+ const $ref = require('reactive')
234
+ const _lodash = require('lodash')
235
+ const { _private, __double } = require('internal')
236
+ const { $$ref, _0x1234 } = require('edge-case')
237
+ `
238
+
239
+ const requiresFound = getRequires(code).filter(r => !r.isDynamic)
240
+ assert.equal(requiresFound.length, 5, 'Should find 5 requires with special chars')
241
+ assert.equal(requiresFound[0].from, 'dynamodb-toolbox')
242
+ assert.equal(requiresFound[0].varName, '{ $add }')
243
+ assert.equal(requiresFound[1].from, 'reactive')
244
+ assert.equal(requiresFound[1].varName, '$ref')
245
+ assert.equal(requiresFound[2].from, 'lodash')
246
+ assert.equal(requiresFound[2].varName, '_lodash')
247
+ assert.equal(requiresFound[3].from, 'internal')
248
+ assert.equal(requiresFound[3].varName, '{ _private, __double }')
249
+ assert.equal(requiresFound[4].from, 'edge-case')
250
+ assert.equal(requiresFound[4].varName, '{ $$ref, _0x1234 }')
251
+ })
252
+
253
+ test('should still match real requires mixed with non-require code', async () => {
254
+ const code = `
255
+ const path = require('path')
256
+ const active = items.filter((e) => e.status === 'active')
257
+ const { foo, bar } = require('some-pkg')
258
+ const key = c.req.header('Idempotency-Key')
259
+ require('side-effect-only')
260
+ `
261
+
262
+ const requiresFound = getRequires(code)
263
+ // Should only match the 3 real requires, not the filter or req.header
264
+ const staticRequires = requiresFound.filter(r => !r.isDynamic)
265
+ assert.equal(staticRequires.length, 3, 'Should find exactly 3 real requires')
266
+ assert.equal(staticRequires[0].from, 'path')
267
+ assert.equal(staticRequires[1].from, 'some-pkg')
268
+ assert.equal(staticRequires[2].from, 'side-effect-only')
269
+ })
270
+
211
271
  test.run()