@davidwells/extract-deps 0.0.5 → 0.0.6

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 (32) hide show
  1. package/.claude/settings.local.json +7 -1
  2. package/examples/verify-basic.js +2 -0
  3. package/examples/verify-package-entries.js +74 -0
  4. package/package.json +2 -2
  5. package/src/dep-graph.js +72 -12
  6. package/src/get-cjs-exports.js +112 -7
  7. package/src/get-imports.js +1 -0
  8. package/src/utils.js +1 -1
  9. package/src/vendor/inspect-parameters-declaration.js +282 -0
  10. package/src/verify-package-entries.js +228 -0
  11. package/tests/dep-graph.test.js +145 -0
  12. package/tests/fixtures/cjs-broken-example/index.js +8 -0
  13. package/tests/fixtures/cjs-broken-example/one.js +9 -0
  14. package/tests/fixtures/cjs-broken-example/package.json +7 -0
  15. package/tests/fixtures/cjs-broken-example/sub-folder/sub-sub-folder/three.js +6 -0
  16. package/tests/fixtures/cjs-broken-example/sub-folder/two.js +8 -0
  17. package/tests/fixtures/cjs-deep-example/package.json +15 -0
  18. package/tests/fixtures/cjs-example/package.json +7 -0
  19. package/tests/fixtures/esm-broken-fixture/index.js +8 -0
  20. package/tests/fixtures/esm-broken-fixture/one.js +9 -0
  21. package/tests/fixtures/esm-broken-fixture/package.json +15 -0
  22. package/tests/fixtures/esm-broken-fixture/sub-folder/sub-sub-folder/three.js +6 -0
  23. package/tests/fixtures/esm-broken-fixture/sub-folder/sub-sub-folder-two/deep.js +6 -0
  24. package/tests/fixtures/esm-broken-fixture/sub-folder/sub-sub-folder-two/even-deeper/wow-deep.js +5 -0
  25. package/tests/fixtures/esm-broken-fixture/sub-folder/two.js +8 -0
  26. package/tests/fixtures/esm-broken-fixture/valid.js +7 -0
  27. package/tests/fixtures/esm-fixture/package.json +11 -0
  28. package/tests/get-cjs-exports.test.js +171 -0
  29. package/tests/get-imports.test.js +4 -1
  30. package/tests/manual/broken-cjs.js +38 -0
  31. package/tests/manual/broken-esm.js +37 -0
  32. package/tests/verify-package-entries.test.js +272 -0
@@ -1,7 +1,13 @@
1
1
  {
2
2
  "permissions": {
3
3
  "allow": [
4
- "Bash(node:*)"
4
+ "Bash(node:*)",
5
+ "Bash(npm audit:*)",
6
+ "Bash(npm view:*)",
7
+ "Bash(npm pack:*)",
8
+ "Bash(tar:*)",
9
+ "Bash(npm install)",
10
+ "Bash(npm test)"
5
11
  ],
6
12
  "deny": [],
7
13
  "ask": []
@@ -15,6 +15,8 @@ try {
15
15
  console.error(error.message)
16
16
  }
17
17
 
18
+ console.log('foo')
19
+
18
20
  console.log('Verifying ESM fixture...')
19
21
  try {
20
22
  const graph = depGraph(esmFixture)
@@ -0,0 +1,74 @@
1
+ // Verify package.json entry points
2
+ const path = require('path')
3
+ const { getPackageEntryPoints, verifyPackageEntries } = require('../src/verify-package-json-entries')
4
+
5
+ const pkgPath = path.resolve(__dirname, '../package.json')
6
+
7
+ console.log('=== Extract All Package Entry Points ===\n')
8
+ const entryPoints = getPackageEntryPoints(pkgPath)
9
+ console.log(`Found ${entryPoints.length} entry points:\n`)
10
+ entryPoints.forEach(ep => {
11
+ console.log(` ${ep.field}: ${ep.path}`)
12
+ })
13
+
14
+ console.log('\n=== Verify Entries and Collect Dependencies ===\n')
15
+ const results = verifyPackageEntries(pkgPath, {
16
+ collectDeps: true, // Collect dependency paths
17
+ verifyGraph: false // Skip verification (faster, for CI use case)
18
+ })
19
+
20
+ console.log('Results per entry point:\n')
21
+ results.entryPoints.forEach(ep => {
22
+ const status = ep.valid ? '✓' : '✗'
23
+ console.log(`${status} ${ep.field}`)
24
+ if (ep.deps) {
25
+ console.log(` ${ep.deps.length} dependencies`)
26
+ }
27
+ if (ep.error) {
28
+ console.log(` Error: ${ep.error.message}`)
29
+ }
30
+ console.log()
31
+ })
32
+
33
+ console.log(`\n=== Summary ===`)
34
+ console.log(`Total entry points: ${results.entryPoints.length}`)
35
+ console.log(`Valid: ${results.entryPoints.filter(ep => ep.valid).length}`)
36
+ console.log(`Errors: ${results.errors.length}`)
37
+ console.log(`Total unique dependencies across all entry points: ${results.allDeps.size}`)
38
+
39
+ if (results.allDeps.size > 0) {
40
+ console.log('\n=== All Dependencies (for git change tracking) ===')
41
+ const allDeps = Array.from(results.allDeps)
42
+ allDeps.forEach(dep => {
43
+ console.log(` ${path.relative(process.cwd(), dep)}`)
44
+ })
45
+ }
46
+
47
+ console.log('\n=== Example: CI Use Case ===')
48
+ console.log(`
49
+ // In your CI script, you can use this to check if any deps changed:
50
+ const { verifyPackageEntries } = require('extract-deps/verify-package-json-entries')
51
+ const { execSync } = require('child_process')
52
+
53
+ const pkgPath = './package.json'
54
+ const results = verifyPackageEntries(pkgPath, { verifyGraph: false })
55
+
56
+ // Get all unique dependency file paths
57
+ const allDepPaths = Array.from(results.allDeps)
58
+
59
+ // Check which ones changed in git
60
+ const changedFiles = execSync('git diff --name-only HEAD~1 HEAD')
61
+ .toString()
62
+ .split('\\n')
63
+ .filter(Boolean)
64
+
65
+ const changedDeps = allDepPaths.filter(dep => {
66
+ const relativePath = path.relative(process.cwd(), dep)
67
+ return changedFiles.includes(relativePath)
68
+ })
69
+
70
+ console.log(\`Changed dependencies: \${changedDeps.length}\`)
71
+ if (changedDeps.length > 0) {
72
+ console.log('Need to rebuild/retest')
73
+ }
74
+ `)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@davidwells/extract-deps",
3
- "version": "0.0.5",
3
+ "version": "0.0.6",
4
4
  "description": "Get imports and requires from a codebase without AST",
5
5
  "main": "src/index.js",
6
6
  "exports": {
@@ -22,7 +22,7 @@
22
22
  "release:major": "npm version major && npm publish"
23
23
  },
24
24
  "dependencies": {
25
- "inspect-parameters-declaration": "^0.1.0"
25
+ "@dual-bundle/import-meta-resolve": "^4.2.1"
26
26
  },
27
27
  "devDependencies": {
28
28
  "typescript": "^5.7.3",
package/src/dep-graph.js CHANGED
@@ -1,6 +1,8 @@
1
1
  // Build dependency graph and optionally verify imports
2
2
  const path = require('path')
3
3
  const fs = require('fs')
4
+ const { pathToFileURL, fileURLToPath } = require('url')
5
+ const { resolve: resolveImport } = require('@dual-bundle/import-meta-resolve')
4
6
  const { extractDeps } = require('./index')
5
7
 
6
8
  function fsExistsSync(path) {
@@ -80,10 +82,12 @@ function verifyImportedValuesExist(dep, targetDeps, targetPath, sourcePath, erro
80
82
  // Verify each imported name is exported
81
83
  for (const importedName of importedNames) {
82
84
  if (!exportedNames.has(importedName)) {
85
+ const type = (dep.line.indexOf('import') !== -1 ? 'Import' : 'Require')
83
86
  const error = new Error(
84
- `Import not found: "${importedName}" is not exported from "${targetPath}".\n` +
85
- `Import statement: ${dep.line}\n` +
86
- `Importing from: ${sourcePath}\n` +
87
+ `\n${type} "${importedName}" not found from statement:\n` +
88
+ `${dep.line}\n` +
89
+ `in "${sourcePath} file.\n\n` +
90
+ `The "${importedName}" value is not exported from file "${targetPath}".\n\n` +
87
91
  `Available exports: ${Array.from(exportedNames).join(', ') || 'none'}`
88
92
  )
89
93
  if (throwOnFirstError) {
@@ -119,10 +123,19 @@ function verifyImportedValuesExist(dep, targetDeps, targetPath, sourcePath, erro
119
123
  * @param {Array<Error>} errors - Array to collect errors
120
124
  * @param {boolean} throwOnFirstError - Whether to throw immediately on first error
121
125
  * @param {boolean} verify - Whether to verify imports/exports
126
+ * @param {boolean} verifyNodeModules - Whether to verify node_modules can be resolved
122
127
  * @param {Map<string, string[]>|null} graph - Optional map to collect dependency graph
123
128
  * @param {Set} visited - Set of already-visited file paths to avoid cycles
124
129
  */
125
- function buildGraphRecursively(filePath, errors, throwOnFirstError, verify, graph = null, visited = new Set()) {
130
+ function buildGraphRecursively(
131
+ filePath,
132
+ errors,
133
+ throwOnFirstError,
134
+ verify,
135
+ verifyNodeModules,
136
+ graph = null,
137
+ visited = new Set(),
138
+ ) {
126
139
  // Avoid infinite loops
127
140
  const normalizedPath = fs.realpathSync(filePath)
128
141
  if (visited.has(normalizedPath)) {
@@ -148,6 +161,17 @@ function buildGraphRecursively(filePath, errors, throwOnFirstError, verify, grap
148
161
  relativeDeps.push(...deps.imports.filter(imp => imp.isRelative))
149
162
  }
150
163
 
164
+ // Collect all non-relative dependencies (node_modules)
165
+ const nodeModuleDeps = []
166
+ if (verifyNodeModules) {
167
+ if (deps.requires && deps.requires.length > 0) {
168
+ nodeModuleDeps.push(...deps.requires.filter(req => !req.isRelative))
169
+ }
170
+ if (deps.imports && deps.imports.length > 0) {
171
+ nodeModuleDeps.push(...deps.imports.filter(imp => !imp.isRelative))
172
+ }
173
+ }
174
+
151
175
  // Process each relative dependency
152
176
  const fileDir = path.dirname(filePath)
153
177
  for (const dep of relativeDeps) {
@@ -185,7 +209,34 @@ function buildGraphRecursively(filePath, errors, throwOnFirstError, verify, grap
185
209
  }
186
210
 
187
211
  // Recursively process the imported file
188
- buildGraphRecursively(resolvedPath, errors, throwOnFirstError, verify, graph, visited)
212
+ buildGraphRecursively(resolvedPath, errors, throwOnFirstError, verify, verifyNodeModules, graph, visited)
213
+ }
214
+
215
+ // Verify node_modules dependencies if enabled
216
+ if (verifyNodeModules && nodeModuleDeps.length > 0) {
217
+ const parentURL = pathToFileURL(filePath).href
218
+
219
+ for (const dep of nodeModuleDeps) {
220
+ const { from, line } = dep
221
+
222
+ try {
223
+ // Try to resolve the module using Node's resolution algorithm
224
+ const resolved = resolveImport(from, parentURL)
225
+ //console.log('resolved', resolved)
226
+ // If we get here, the module resolved successfully
227
+ } catch (error) {
228
+ const err = new Error(
229
+ `"${line}" could not be resolved in ${filePath}\n\n` +
230
+ `The package "${from}" is not installed or cannot be found.\n` +
231
+ `Run "npm install ${from}" to fix this issue.\n\n` +
232
+ `Resolution error: ${error.message}`
233
+ )
234
+ if (throwOnFirstError) {
235
+ throw err
236
+ }
237
+ errors.push(err)
238
+ }
239
+ }
189
240
  }
190
241
  }
191
242
 
@@ -195,19 +246,27 @@ function buildGraphRecursively(filePath, errors, throwOnFirstError, verify, grap
195
246
  * @param {object} options - Options
196
247
  * @param {boolean} [options.buildGraph] - If true, build and return dependency graph. Default: true
197
248
  * @param {boolean} [options.verifyGraph] - If true, verify imports/exports. Default: true
249
+ * @param {boolean} [options.verifyNodeModules] - If true, verify node_modules packages can be resolved. Default: false
198
250
  * @param {boolean} [options.throwOnFirstError] - If true, throw immediately on first error. Default: false
199
251
  * @returns {Map<string, string[]>|undefined} - Dependency graph where keys are file paths and values are arrays of dependency paths. Returns undefined if buildGraph is false.
200
252
  */
201
253
  function depGraph(filePath, options = {}) {
202
- const { buildGraph = true, verifyGraph = true, throwOnFirstError = false } = options
254
+ const {
255
+ buildGraph = true,
256
+ verifyGraph = true,
257
+ verifyNodeModules = false,
258
+ throwOnFirstError = false,
259
+ } = options
203
260
  const errors = []
204
261
  const graph = buildGraph ? new Map() : null
205
262
 
206
- buildGraphRecursively(filePath, errors, throwOnFirstError, verifyGraph, graph)
263
+ buildGraphRecursively(filePath, errors, throwOnFirstError, verifyGraph, verifyNodeModules, graph)
207
264
 
208
265
  if (errors.length > 0) {
209
- const errorMessage = errors.map((err, idx) => `Error ${idx + 1}:\n${err.message}`).join('\n\n')
210
- throw new Error(`Found ${errors.length} validation error${errors.length === 1 ? '' : 's'}:\n\n${errorMessage}`)
266
+ const errorMessage = errors.map((err, idx) => `Dep graph error:\n ${err.message}`).join('\n')
267
+ throw new Error(
268
+ `Found ${errors.length} validation error${errors.length === 1 ? '' : 's'}:\n\n${errorMessage}`
269
+ )
211
270
  }
212
271
 
213
272
  if (buildGraph) {
@@ -263,10 +322,11 @@ function resolveDepPaths(filePath, options = {}) {
263
322
 
264
323
  if (require.main === module) {
265
324
  const entry = path.resolve(__dirname, '../tests/fixtures/cjs-deep-example/index.js')
266
- const graph = depGraph(entry)
325
+ const graph = depGraph(entry, {
326
+ verifyNodeModules: false,
327
+ throwOnFirstError: true
328
+ })
267
329
  console.log('graph', graph)
268
- const depPaths = Array.from(graph.values()).flat()
269
- console.log('depPaths', depPaths)
270
330
  const depPathsProper = resolveDepPaths(entry)
271
331
  console.log('depPathsProper', depPathsProper)
272
332
  }
@@ -8,17 +8,22 @@ const MODULE_EXPORTS_ASSIGNMENT_REGEX = /^(?!\s*\/\/)(?!\s*\/\*)\s*(?:exports\s*
8
8
 
9
9
  // Matches: module.exports.propertyName = ... or module.exports['propertyName'] = ...
10
10
  // https://regex101.com/r/example/cjs-2
11
- const MODULE_EXPORTS_PROPERTY_REGEX = /module\.exports(?:\.([\w$]+)|\[['"]([^'"]+)['"]\])\s*=\s*(.+?)(?=\s*(?:module\.exports|exports\.|\/\/|\/\*|$))/gs
11
+ const MODULE_EXPORTS_PROPERTY_REGEX = /module\.exports(?:\.([\w$]+)|\[['"]([^'"]+)['"]\])\s*=\s*(.+?)(?=\n\s*\n|(?:module\.exports|exports\.|Object\.defineProperty|\/\/|\/\*|$))/gs
12
12
 
13
13
  // Matches: exports.propertyName = ... or exports['propertyName'] = ...
14
14
  // https://regex101.com/r/example/cjs-3
15
- const EXPORTS_PROPERTY_REGEX = /(?<!module\.)exports(?:\.([\w$]+)|\[['"]([^'"]+)['"]\])\s*=\s*(.+?)(?=\s*(?:module\.exports|exports\.|\/\/|\/\*|$))/gs
15
+ const EXPORTS_PROPERTY_REGEX = /(?<!module\.)exports(?:\.([\w$]+)|\[['"]([^'"]+)['"]\])\s*=\s*(.+?)(?=\n\s*\n|(?:module\.exports|exports\.|Object\.defineProperty|\/\/|\/\*|$))/gs
16
16
 
17
17
  function stripComments(code) {
18
18
  let result = code
19
19
 
20
- // Remove single-line comments but preserve newlines
21
- result = result.replace(/\/\/.*$/gm, '')
20
+ // Remove single-line comments but preserve string positions
21
+ // This is critical: we must preserve the exact length of the string so that
22
+ // indices found in sanitized text match indices in original text
23
+ result = result.replace(/\/\/(.*)$/gm, (match, commentText) => {
24
+ // Replace // and comment text with spaces (same length as original)
25
+ return ' ' + ' '.repeat(commentText.length)
26
+ })
22
27
 
23
28
  // Remove multi-line comments but preserve line structure
24
29
  result = result.replace(/\/\*[\s\S]*?\*\//g, (match) => {
@@ -36,13 +41,22 @@ function getCjsExports(code) {
36
41
  const assignmentExports = getModuleExportsAssignment(code, sanitizedCode)
37
42
  const modulePropertyExports = getModuleExportsProperty(code, sanitizedCode)
38
43
  const propertyExports = getExportsProperty(code, sanitizedCode)
39
- const objectAssignExports = getObjectAssignExports(code, sanitizedCode)
44
+
45
+ // Quick pre-checks to avoid expensive regex operations when not needed
46
+ const objectAssignExports = sanitizedCode.includes('Object.assign')
47
+ ? getObjectAssignExports(code, sanitizedCode)
48
+ : []
49
+
50
+ const definePropertyExports = sanitizedCode.includes('Object.defineProperty')
51
+ ? getDefinePropertyExports(code, sanitizedCode)
52
+ : []
40
53
 
41
54
  /*
42
55
  console.log('assignmentExports', assignmentExports)
43
56
  console.log('modulePropertyExports', modulePropertyExports)
44
57
  console.log('propertyExports', propertyExports)
45
58
  console.log('objectAssignExports', objectAssignExports)
59
+ console.log('definePropertyExports', definePropertyExports)
46
60
  /** */
47
61
 
48
62
  const allExports = []
@@ -50,6 +64,7 @@ function getCjsExports(code) {
50
64
  .concat(modulePropertyExports)
51
65
  .concat(propertyExports)
52
66
  .concat(objectAssignExports)
67
+ .concat(definePropertyExports)
53
68
 
54
69
  return allExports.sort((a, b) => a.index - b.index)
55
70
  }
@@ -283,7 +298,81 @@ function getObjectAssignExports(originalText = '', sanitizedText = originalText)
283
298
  return exportStatements
284
299
  }
285
300
 
286
- if(require.main === module) {
301
+ function getDefinePropertyExports(originalText = '', sanitizedText = originalText) {
302
+ let exportStatements = []
303
+
304
+ // Find all Object.defineProperty calls with module or exports
305
+ // Matches: Object.defineProperty(module, 'exports', ...) or Object.defineProperty(exports, 'propertyName', ...)
306
+ const definePropertyPattern = /Object\.defineProperty\s*\(\s*(module|exports)\s*,\s*['"]([^'"]+)['"]/g
307
+ var result
308
+
309
+ while((result = definePropertyPattern.exec(sanitizedText)) !== null) {
310
+ const target = result[1]
311
+ const propertyName = result[2]
312
+
313
+ // Find the opening parenthesis of Object.defineProperty
314
+ const openParenIndex = result.index + result[0].indexOf('(')
315
+
316
+ // Balance parentheses to find the complete Object.defineProperty call
317
+ let depth = 0
318
+ let i = openParenIndex
319
+ let completeCall = ''
320
+
321
+ while (i < originalText.length) {
322
+ const char = originalText[i]
323
+ if (char === '(') depth++
324
+ else if (char === ')') {
325
+ depth--
326
+ if (depth === 0) {
327
+ completeCall = originalText.substring(result.index, i + 1)
328
+ break
329
+ }
330
+ }
331
+ i++
332
+ }
333
+
334
+ if (!completeCall) {
335
+ continue
336
+ }
337
+
338
+ // Try to extract the getter function if present
339
+ // Matches: get: functionName or get: () => ... or get() { ... }
340
+ const getterMatch = completeCall.match(/get\s*:\s*(\w+)|get\s*:\s*(\([^)]*\)\s*=>)|get\s*\(/)
341
+ let getterValue = ''
342
+ if (getterMatch) {
343
+ if (getterMatch[1]) {
344
+ // Named function: get: functionName
345
+ getterValue = getterMatch[1]
346
+ } else if (getterMatch[2]) {
347
+ // Arrow function: get: () =>
348
+ getterValue = 'arrow function'
349
+ } else {
350
+ // Method: get() { ... }
351
+ getterValue = 'getter method'
352
+ }
353
+ }
354
+
355
+ // Determine if this is a module.exports or property export
356
+ const isModuleExports = target === 'module' && propertyName === 'exports'
357
+
358
+ const exportStatement = {
359
+ line: completeCall.trim(),
360
+ exportType: isModuleExports ? 'default' : 'named',
361
+ exportName: isModuleExports ? '' : propertyName,
362
+ exportValue: completeCall, // Original text with comments
363
+ exportDefinitionName: getterValue || propertyName,
364
+ index: result.index,
365
+ isExport: true,
366
+ isDefineProperty: true
367
+ }
368
+
369
+ exportStatements.push(exportStatement)
370
+ }
371
+
372
+ return exportStatements
373
+ }
374
+
375
+ if (require.main === module) {
287
376
  const code = `
288
377
  // module.exports assignment
289
378
  module.exports = function hello() {}
@@ -309,6 +398,21 @@ module.exports.MyClass = class MyClass {}
309
398
  // exports properties
310
399
  exports.foo = function() {}
311
400
  exports.bar = 'bar'
401
+
402
+ // Object.defineProperty exports
403
+ function assembleStyles() {
404
+ return { color: 'red' }
405
+ }
406
+
407
+ Object.defineProperty(module, 'exports', {
408
+ enumerable: true,
409
+ get: assembleStyles
410
+ })
411
+
412
+ Object.defineProperty(exports, 'myProperty', {
413
+ enumerable: true,
414
+ get: () => 'value'
415
+ })
312
416
  `
313
417
 
314
418
  const exportsFound = getCjsExports(code)
@@ -320,5 +424,6 @@ module.exports = {
320
424
  getModuleExportsAssignment,
321
425
  getModuleExportsProperty,
322
426
  getExportsProperty,
323
- getObjectAssignExports
427
+ getObjectAssignExports,
428
+ getDefinePropertyExports
324
429
  }
@@ -52,6 +52,7 @@ function getTypeImports(targetText = '') {
52
52
  importName: (importName || '').trim(),
53
53
  index: result.index,
54
54
  isImport: true,
55
+ isTypeImport: true,
55
56
  isRelative: isRelativeImport(importPath)
56
57
  })
57
58
  }
package/src/utils.js CHANGED
@@ -1,4 +1,4 @@
1
- const { inspectParameters, getParametersNames } = require('inspect-parameters-declaration')
1
+ const { inspectParameters, getParametersNames } = require('./vendor/inspect-parameters-declaration')
2
2
 
3
3
  function isRelativeImport(importLocation) {
4
4
  return !!importLocation.match(/^\./)