@davidwells/extract-deps 0.1.9 → 0.1.11

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.9",
3
+ "version": "0.1.11",
4
4
  "description": "Get imports and requires from a codebase without AST",
5
5
  "main": "src/index.js",
6
6
  "exports": {
@@ -1,6 +1,6 @@
1
1
  // Extracts CommonJS module.exports and exports patterns from JavaScript code
2
2
 
3
- const { getNames, extractExportName, stripStringContents } = require('./utils')
3
+ const { getNames, extractExportName, stripCommentsAndStrings } = require('./utils')
4
4
 
5
5
  // Matches: module.exports = ... or exports = module.exports = ...
6
6
  // https://regex101.com/r/example/cjs-1
@@ -35,13 +35,11 @@ function stripComments(code) {
35
35
  }
36
36
 
37
37
  function getCjsExports(code) {
38
- // Blank string/template literal contents before stripping comments so
39
- // `exports.X = ...` lines embedded inside template literals aren't matched
40
- // as real exports, and so the comment regex doesn't mis-strip `//` inside
41
- // string bodies.
42
- code = stripStringContents(code)
43
- // Strip comments to prevent false positives from commented code
44
- const sanitizedCode = stripComments(code)
38
+ // Single tokenizer-based pass that blanks comments and code-bearing
39
+ // strings/templates together. Length is preserved so the position-based
40
+ // logic below (which slices into the original `code` for full export
41
+ // values) keeps working off the same indices.
42
+ const sanitizedCode = stripCommentsAndStrings(code)
45
43
 
46
44
  const assignmentExports = getModuleExportsAssignment(code, sanitizedCode)
47
45
  const modulePropertyExports = getModuleExportsProperty(code, sanitizedCode)
@@ -16,12 +16,12 @@ const EXPORT_TYPE_REGEX = /export\s+(?:type|interface)\s+([\w$]+)/gm
16
16
  const EXPORT_ASSERTION_REGEX = /export\s+(?:["'\s]*([\w*{}\n\r\t, ]+)from\s*)?["'\s](.*[@\w_-]+)["'\s]+(assert|with)\s*\{([^}]+)\};?$/gm
17
17
 
18
18
  function getExports(code = '', options = {}) {
19
- // Blank string/template contents first so embedded `export` keywords inside
20
- // string literals don't get parsed as real exports, and so `stripComments`
21
- // doesn't mis-strip `//` or `/* */` that appear inside strings.
22
- code = stripStringContents(code)
23
- // Strip comments to avoid false positives
19
+ // Comments first, then strings — order matters. Running stripStringContents
20
+ // alone would mis-pair an apostrophe inside a JSDoc comment (`Netlify's …`)
21
+ // with a later quote and shift subsequent backtick pairing, which could
22
+ // swallow real `export` declarations a few lines later.
24
23
  code = stripComments(code)
24
+ code = stripStringContents(code)
25
25
  const defaultExports = getDefaultExports(code)
26
26
  const namedExports = getNamedExports(code)
27
27
  const namedDeclarationExports = getNamedDeclarationExports(code)
@@ -37,13 +37,12 @@ const DYNAMIC_IMPORT_REGEX = /(?:(const|var|let)\s+([\w*{}\n\r\t,:= $]+)\s*=\s*)
37
37
  const ASSERTION_IMPORT_REGEX = /(?:^|\n)\s*import\s+(?:["'\s]*([\w*{}\n\r\t, $]+)from\s*)?["'\s]+([^"'`\n;\s]+)\s*["']\s+(assert|with)\s*\{([^}]+)\}/gm
38
38
 
39
39
  function getImports(code = '', options = {}) {
40
- // Blank string/template contents first so the comment stripper doesn't
41
- // mis-strip `//` or `/* */` sequences inside literals, and so the regex
42
- // doesn't pick up `import`/`export` keywords that appear inside strings
43
- // (e.g. fenced code in JSDoc-style template literals).
44
- code = stripStringContents(code)
45
- // Strip comments to avoid false positives
40
+ // Strip comments FIRST so apostrophes/backticks inside JSDoc text get
41
+ // removed before `stripStringContents` looks for string literals.
42
+ // (Running the two in the opposite order let comment chars masquerade as
43
+ // string openers and swallow real `import`/`export` lines further down.)
46
44
  code = stripComments(code)
45
+ code = stripStringContents(code)
47
46
  const staticImports = getStaticImports(code)
48
47
  const dynamicImports = getDynamicImports(code)
49
48
  if (options.includeAssertionImports) {
@@ -65,11 +65,12 @@ function getStaticRequires(targetText = '') {
65
65
  }
66
66
 
67
67
  function getRequires(code) {
68
- // Blank string/template contents first so `require()` calls embedded in
69
- // string literals (e.g. fenced code in template strings) aren't matched as
70
- // real requires. Then strip comments so `// require('./fake')` is ignored.
71
- code = stripStringContents(code)
68
+ // Strip comments first, then strings — order matters. `stripComments`
69
+ // alone would mis-eat `//` inside strings; running it first instead means
70
+ // comment chars (incl. apostrophes/backticks inside JSDoc) disappear
71
+ // before `stripStringContents` looks for real string literals.
72
72
  code = stripComments(code)
73
+ code = stripStringContents(code)
73
74
  const staticRequires = getStaticRequires(code)
74
75
  const dynamicRequires = getDynamicRequires(code)
75
76
  return staticRequires.concat(dynamicRequires).sort((a, b) => a.index - b.index)
package/src/utils.js CHANGED
@@ -63,6 +63,59 @@ function stripStringContents(code) {
63
63
  })
64
64
  }
65
65
 
66
+ // Unified regex matching whichever comes first at each position: block
67
+ // comment, line comment, single-quoted string, double-quoted string, or
68
+ // template literal. Matching left-to-right means a comment that starts
69
+ // first consumes its body — including any apostrophes or backticks inside
70
+ // — before they can start a spurious string match a few lines later. That
71
+ // solves the JSDoc-apostrophe-shifts-backtick-pairing class of bugs that
72
+ // arose when comments and strings were stripped in two separate passes.
73
+ const COMMENT_OR_STRING_RE = /\/\*[\s\S]*?\*\/|\/\/[^\n]*|"(?:[^"\\\n]|\\[\s\S])*"|'(?:[^'\\\n]|\\[\s\S])*'|`(?:[^`\\]|\\[\s\S])*`/g
74
+
75
+ /**
76
+ * Single-regex pass that blanks comment bodies and code-bearing
77
+ * string/template bodies together. Solves two bugs that occurred when the
78
+ * string-strip and comment-strip ran in separate passes:
79
+ *
80
+ * - Apostrophes inside JSDoc comments (e.g. `* Netlify's \`...`) were
81
+ * mis-paired with later quotes, which shifted subsequent backtick
82
+ * pairing and could swallow real `export` declarations a few lines
83
+ * later in the same file.
84
+ * - `//` and `/* *\/` sequences inside string bodies were mis-stripped
85
+ * as comments.
86
+ *
87
+ * Length is preserved (each consumed char replaced by a space or newline
88
+ * kept verbatim) so downstream character indices stay valid.
89
+ *
90
+ * Strings whose bodies don't reference `import` / `export` / `require` /
91
+ * `module.exports` pass through unchanged, so module specifiers like
92
+ * `'lodash'` or `'./foo.js'` stay readable by the downstream regexes.
93
+ *
94
+ * Caveat: `${...}` interpolations inside template literals are part of the
95
+ * body, so `${import('./x.js')}` inside a template gets blanked. Rare in
96
+ * practice; a full parser would be needed otherwise.
97
+ *
98
+ * @param {string} code
99
+ * @returns {string}
100
+ */
101
+ function stripCommentsAndStrings(code) {
102
+ return code.replace(COMMENT_OR_STRING_RE, (match) => {
103
+ const first = match[0]
104
+ // Comments (line or block) are fully blanked — they have no extractable
105
+ // content. Newlines are preserved so line counts hold.
106
+ if (first === '/') {
107
+ return match.replace(/[^\n]/g, ' ')
108
+ }
109
+ // Strings/templates — only blank if the body references code keywords;
110
+ // otherwise leave the literal intact so `'lodash'` etc. remain readable.
111
+ const body = match.slice(1, -1)
112
+ if (!EMBEDDED_CODE_RE.test(body)) {
113
+ return match
114
+ }
115
+ return first + body.replace(/[^\n]/g, ' ') + first
116
+ })
117
+ }
118
+
66
119
  /**
67
120
  * Strip comments from code, preserving :// in URLs
68
121
  * @param {string} code
@@ -292,6 +345,7 @@ function extractExportName(exportValue) {
292
345
  module.exports = {
293
346
  stripComments,
294
347
  stripStringContents,
348
+ stripCommentsAndStrings,
295
349
  isBuiltIn,
296
350
  isRelativeImport,
297
351
  isRelativeImportWithQuotes,
@@ -563,6 +563,41 @@ export const real = 'value' // this comment should be stripped
563
563
  assert.equal(foundExports[2].exportName, 'real', 'Should find real export')
564
564
  })
565
565
 
566
+ test('apostrophe inside JSDoc with surrounding backticks does not swallow later exports', () => {
567
+ // Regression: when `stripStringContents` ran BEFORE `stripComments`, an
568
+ // apostrophe inside a JSDoc block (e.g. `Netlify's \`promptType: 'foo'\``)
569
+ // was treated as the opener of a single-quoted string and consumed text up
570
+ // to the next `'`, shifting every subsequent backtick pair by one. The
571
+ // newly mis-paired template literal then spanned multiple lines, including
572
+ // the real `export async function` declaration that followed the JSDoc,
573
+ // and the embedded-code heuristic blanked it. The fix runs `stripComments`
574
+ // first so JSDoc-internal punctuation is gone before string matching.
575
+ //
576
+ // This fixture replicates the structural shape of the AWS SDK adapter file
577
+ // that originally surfaced the bug: a JSDoc block containing both an
578
+ // apostrophe and multiple backticks, followed by an `export async function`,
579
+ // followed by ANOTHER JSDoc with backticks.
580
+ const code = `
581
+ /**
582
+ * Stuff with \`code-one\`. Netlify's \`promptType: 'foo'\`. End \`Error\` on
583
+ * non-2xx upstream responses or malformed bodies.
584
+ */
585
+ export async function generateThing(request) {
586
+ return request
587
+ }
588
+ /** Test seam: override the global \`fetch\` for unit tests. */
589
+ export const helper = 1
590
+ `
591
+
592
+ const found = getExports(code)
593
+ const names = found.map(e => e.exportName).sort()
594
+ assert.equal(
595
+ names,
596
+ ['generateThing', 'helper'],
597
+ 'Both exports must survive past the JSDoc apostrophe+backtick combo'
598
+ )
599
+ })
600
+
566
601
  test('ignores ESM exports embedded in template literals and strings', () => {
567
602
  const code = `
568
603
  export const real = 'real'