@davidwells/extract-deps 0.1.13 → 0.1.14

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 (35) hide show
  1. package/package.json +1 -1
  2. package/src/dep-graph.js +148 -37
  3. package/src/get-cjs-exports.js +7 -0
  4. package/src/get-exports.js +2 -3
  5. package/src/get-imports.js +5 -4
  6. package/src/get-requires.js +2 -3
  7. package/src/utils.js +21 -0
  8. package/tests/dep-graph-cycles.test.js +111 -0
  9. package/tests/fixtures/cycle-fixtures/cycle-entry/index.js +2 -0
  10. package/tests/fixtures/cycle-fixtures/cycle-entry/leaf.js +1 -0
  11. package/tests/fixtures/cycle-fixtures/cycle-entry/peer.js +3 -0
  12. package/tests/fixtures/cycle-fixtures/diamond/a.js +2 -0
  13. package/tests/fixtures/cycle-fixtures/diamond/b.js +2 -0
  14. package/tests/fixtures/cycle-fixtures/diamond/c.js +2 -0
  15. package/tests/fixtures/cycle-fixtures/diamond/d.js +1 -0
  16. package/tests/fixtures/cycle-fixtures/diamond/index.js +3 -0
  17. package/tests/fixtures/cycle-fixtures/duplicate-edge/index.js +5 -0
  18. package/tests/fixtures/cycle-fixtures/duplicate-edge/leaf.js +1 -0
  19. package/tests/fixtures/cycle-fixtures/duplicate-edge/other.js +1 -0
  20. package/tests/fixtures/cycle-fixtures/duplicate-edge/util.js +2 -0
  21. package/tests/fixtures/cycle-fixtures/mixed-duplicate/base.mjs +1 -0
  22. package/tests/fixtures/cycle-fixtures/mixed-duplicate/helper.mjs +2 -0
  23. package/tests/fixtures/cycle-fixtures/mixed-duplicate/index.mjs +3 -0
  24. package/tests/fixtures/cycle-fixtures/self-import/index.js +2 -0
  25. package/tests/fixtures/cycle-fixtures/self-import/leaf.js +1 -0
  26. package/tests/fixtures/cycle-fixtures/self-import/self.js +3 -0
  27. package/tests/fixtures/cycle-fixtures/three-cycle/index.js +3 -0
  28. package/tests/fixtures/cycle-fixtures/three-cycle/leaf.js +1 -0
  29. package/tests/fixtures/cycle-fixtures/three-cycle/w.js +2 -0
  30. package/tests/fixtures/cycle-fixtures/three-cycle/x.js +2 -0
  31. package/tests/fixtures/cycle-fixtures/three-cycle/y.js +2 -0
  32. package/tests/fixtures/cycle-fixtures/three-cycle/z.js +3 -0
  33. package/tests/fixtures/cycle-fixtures/two-cycle/a.js +2 -0
  34. package/tests/fixtures/cycle-fixtures/two-cycle/b.js +2 -0
  35. package/tests/fixtures/cycle-fixtures/two-cycle/index.js +2 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@davidwells/extract-deps",
3
- "version": "0.1.13",
3
+ "version": "0.1.14",
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,7 +90,10 @@ function fsExistsSync(path) {
90
90
 
91
91
  function isFileSync(filePath) {
92
92
  try {
93
- return fs.statSync(filePath).isFile()
93
+ // Most probes miss (each import tries several extensions): a missing path returns
94
+ // undefined instead of building and throwing an ENOENT error.
95
+ const stat = fs.statSync(filePath, { throwIfNoEntry: false })
96
+ return stat ? stat.isFile() : false
94
97
  } catch (error) {
95
98
  return false
96
99
  }
@@ -448,7 +451,149 @@ function depGraph(filePath, options = {}) {
448
451
 
449
452
 
450
453
  /**
451
- * Get all dependency file paths in build order (dependencies first), deduplicated
454
+ * Order every file in the graph so dependencies come before dependents, each file once.
455
+ *
456
+ * Kahn's algorithm over the strongly connected components (Tarjan) of the file graph:
457
+ * an import cycle (or self-import) is one component, emitted as a unit with its members
458
+ * in discovery order. Duplicate edges (one file importing a module twice) count once.
459
+ * For an acyclic graph every component is a single file, and the queue seeding and
460
+ * dependent visiting orders match a plain per-file Kahn's sort over the graph's
461
+ * insertion order. O(V + E).
462
+ *
463
+ * @param {Map<string, string[]>} graph - file -> deps (file paths and package names)
464
+ * @returns {string[]}
465
+ */
466
+ function buildOrder(graph) {
467
+ const nodes = Array.from(graph.keys())
468
+ const n = nodes.length
469
+ /** @type {Map<string, number>} */
470
+ const indexOf = new Map()
471
+ for (let i = 0; i < n; i++) indexOf.set(nodes[i], i)
472
+
473
+ // Unique file deps per node (first-occurrence order) and reverse edges. Dependents
474
+ // are appended while walking nodes in graph order, so each reverse list is in graph order.
475
+ /** @type {number[][]} */
476
+ const deps = new Array(n)
477
+ /** @type {number[][]} */
478
+ const dependents = new Array(n)
479
+ for (let i = 0; i < n; i++) dependents[i] = []
480
+ const seenStamp = new Int32Array(n).fill(-1)
481
+ for (let i = 0; i < n; i++) {
482
+ const out = []
483
+ for (const dep of graph.get(nodes[i])) {
484
+ const j = indexOf.get(dep) // package names (and unvisited paths) are not nodes
485
+ if (j === undefined || seenStamp[j] === i) continue
486
+ seenStamp[j] = i
487
+ out.push(j)
488
+ dependents[j].push(i)
489
+ }
490
+ deps[i] = out
491
+ }
492
+
493
+ // Tarjan's SCC, iterative (deep import chains must not overflow the call stack)
494
+ const UNVISITED = -1
495
+ const order = new Int32Array(n).fill(UNVISITED)
496
+ const low = new Int32Array(n)
497
+ const onStack = new Uint8Array(n)
498
+ const comp = new Int32Array(n)
499
+ const sccStack = []
500
+ const callStack = [] // [node, next dep position]
501
+ let counter = 0
502
+ let compCount = 0
503
+ for (let root = 0; root < n; root++) {
504
+ if (order[root] !== UNVISITED) continue
505
+ callStack.push(root, 0)
506
+ order[root] = low[root] = counter++
507
+ sccStack.push(root)
508
+ onStack[root] = 1
509
+ while (callStack.length > 0) {
510
+ const top = callStack.length - 2
511
+ const v = callStack[top]
512
+ const edges = deps[v]
513
+ const pos = callStack[top + 1]
514
+ if (pos < edges.length) {
515
+ callStack[top + 1] = pos + 1
516
+ const w = edges[pos]
517
+ if (order[w] === UNVISITED) {
518
+ order[w] = low[w] = counter++
519
+ sccStack.push(w)
520
+ onStack[w] = 1
521
+ callStack.push(w, 0)
522
+ } else if (onStack[w] && order[w] < low[v]) {
523
+ low[v] = order[w]
524
+ }
525
+ continue
526
+ }
527
+ callStack.length = top
528
+ if (top > 0) {
529
+ const parent = callStack[top - 2]
530
+ if (low[v] < low[parent]) low[parent] = low[v]
531
+ }
532
+ if (low[v] === order[v]) {
533
+ let w
534
+ do {
535
+ w = sccStack.pop()
536
+ onStack[w] = 0
537
+ comp[w] = compCount
538
+ } while (w !== v)
539
+ compCount++
540
+ }
541
+ }
542
+ }
543
+
544
+ // Members per component in graph (discovery) order
545
+ /** @type {number[][]} */
546
+ const members = new Array(compCount)
547
+ for (let c = 0; c < compCount; c++) members[c] = []
548
+ for (let i = 0; i < n; i++) members[comp[i]].push(i)
549
+
550
+ // Component in-degree: distinct other components it depends on
551
+ const compInDegree = new Int32Array(compCount)
552
+ const compStamp = new Int32Array(compCount).fill(-1)
553
+ for (let c = 0; c < compCount; c++) {
554
+ for (const v of members[c]) {
555
+ for (const w of deps[v]) {
556
+ const cw = comp[w]
557
+ if (cw === c || compStamp[cw] === c) continue
558
+ compStamp[cw] = c
559
+ compInDegree[c]++
560
+ }
561
+ }
562
+ }
563
+
564
+ // Kahn's over components, seeded in graph order
565
+ const queue = []
566
+ const queued = new Uint8Array(compCount)
567
+ for (let i = 0; i < n; i++) {
568
+ const c = comp[i]
569
+ if (compInDegree[c] === 0 && !queued[c]) {
570
+ queued[c] = 1
571
+ queue.push(c)
572
+ }
573
+ }
574
+ const result = []
575
+ compStamp.fill(-1)
576
+ for (let head = 0; head < queue.length; head++) {
577
+ const c = queue[head]
578
+ for (const v of members[c]) result.push(nodes[v])
579
+ for (const v of members[c]) {
580
+ for (const d of dependents[v]) {
581
+ const cd = comp[d]
582
+ if (cd === c || compStamp[cd] === c) continue
583
+ compStamp[cd] = c
584
+ if (--compInDegree[cd] === 0) {
585
+ queued[cd] = 1
586
+ queue.push(cd)
587
+ }
588
+ }
589
+ }
590
+ }
591
+ return result
592
+ }
593
+
594
+ /**
595
+ * Get all dependency file paths in build order (dependencies first), deduplicated.
596
+ * Files in an import cycle are kept together, in discovery order.
452
597
  * @param {string} filePath - Entry file path
453
598
  * @param {object} options - Options to pass to depGraph
454
599
  * @param {boolean} [options.verifyGraph] - If true, verify imports/exports. Default: true
@@ -492,42 +637,8 @@ function resolveDepPaths(filePath, options = {}) {
492
637
  }
493
638
  }
494
639
 
495
- // Calculate in-degree (number of file dependencies) for each file
496
- const inDegree = new Map()
497
- for (const [file, deps] of graph) {
498
- // Only count file deps, not node_modules
499
- const fileDeps = deps.filter(d => d.startsWith('/'))
500
- inDegree.set(file, fileDeps.length)
501
- }
502
-
503
- // Queue of files with no dependencies (can be processed first)
504
- const queue = []
505
- for (const [file, degree] of inDegree) {
506
- if (degree === 0) {
507
- queue.push(file)
508
- }
509
- }
510
-
511
- const buildOrder = []
512
-
513
- while (queue.length > 0) {
514
- const file = queue.shift()
515
- buildOrder.push(file)
516
-
517
- // Find all files that depend on this file and decrement their in-degree
518
- for (const [dependent, deps] of graph) {
519
- if (deps.includes(file)) {
520
- const newDegree = inDegree.get(dependent) - 1
521
- inDegree.set(dependent, newDegree)
522
- if (newDegree === 0) {
523
- queue.push(dependent)
524
- }
525
- }
526
- }
527
- }
528
-
529
640
  const result = {
530
- files: buildOrder,
641
+ files: buildOrder(graph),
531
642
  nodeModules: Array.from(nodeModulesSet),
532
643
  builtIns: Array.from(builtInsSet)
533
644
  }
@@ -34,7 +34,14 @@ function stripComments(code) {
34
34
  return result
35
35
  }
36
36
 
37
+ // Every scanner below is gated on one of these in the sanitized text. Sanitizing only
38
+ // blanks characters to spaces (length-preserving), so a needle without whitespace that
39
+ // isn't in the source can't be in the sanitized text either.
40
+ const CJS_NEEDLES = ['exports', 'Object.assign', 'Object.defineProperty', 'Object.keys(', '__exportStar', '__reExport']
41
+
37
42
  function getCjsExports(code) {
43
+ // Most ESM/TS files mention none of them: skip the sanitize pass entirely.
44
+ if (!CJS_NEEDLES.some(needle => code.includes(needle))) return []
38
45
  // Single tokenizer-based pass that blanks comments and code-bearing
39
46
  // strings/templates together. Length is preserved so the position-based
40
47
  // logic below (which slices into the original `code` for full export
@@ -1,4 +1,4 @@
1
- const { getNames, extractExportName, stripComments, stripStringContents } = require('./utils')
1
+ const { getNames, extractExportName, stripCommentsThenStrings } = require('./utils')
2
2
 
3
3
  // https://regex101.com/r/example/1
4
4
  const EXPORT_DEFAULT_REGEX = /export\s+default\s+(?:(?:async\s+)?function\s+([\w$]+)|class\s+([\w$]+)|([\w$]+|\([^)]*\)\s*=>|{[\s\S]*?}|\[[\s\S]*?\]|'(?:[^'\\]|\\.)*'|"(?:[^"\\]|\\.)*"|`(?:[^`\\]|\\.)*`|[\w$]+(?:\s*[+\-*/%]\s*[\w$]+)*))/gm
@@ -22,8 +22,7 @@ function getExports(code = '', options = {}) {
22
22
  // alone would mis-pair an apostrophe inside a JSDoc comment (`Netlify's …`)
23
23
  // with a later quote and shift subsequent backtick pairing, which could
24
24
  // swallow real `export` declarations a few lines later.
25
- code = stripComments(code)
26
- code = stripStringContents(code)
25
+ code = stripCommentsThenStrings(code)
27
26
  const defaultExports = getDefaultExports(code)
28
27
  const namedExports = getNamedExports(code)
29
28
  const namedDeclarationExports = getNamedDeclarationExports(code)
@@ -1,4 +1,4 @@
1
- const { isBuiltIn, isRelativeImport, isRelativeImportWithQuotes, getNames, stripComments, stripStringContents } = require('./utils')
1
+ const { isBuiltIn, isRelativeImport, isRelativeImportWithQuotes, getNames, stripCommentsThenStrings } = require('./utils')
2
2
 
3
3
  // https://regex101.com/r/cfEmlm/1
4
4
  // Path excludes quotes, newlines, and `;` so multi-statement lines like
@@ -43,10 +43,11 @@ function getImports(code = '', options = {}) {
43
43
  // removed before `stripStringContents` looks for string literals.
44
44
  // (Running the two in the opposite order let comment chars masquerade as
45
45
  // string openers and swallow real `import`/`export` lines further down.)
46
- code = stripComments(code)
47
- code = stripStringContents(code)
46
+ code = stripCommentsThenStrings(code)
48
47
  const staticImports = getStaticImports(code)
49
- const dynamicImports = getDynamicImports(code)
48
+ // Every dynamic-import match contains `import(` (most files have none; the regex's
49
+ // optional declaration prefix makes a miss scan the whole text).
50
+ const dynamicImports = code.includes('import(') ? getDynamicImports(code) : []
50
51
  if (options.includeAssertionImports) {
51
52
  const assertionImports = getAssertionImports(code)
52
53
  return staticImports.concat(dynamicImports).concat(assertionImports).sort((a, b) => a.index - b.index)
@@ -1,4 +1,4 @@
1
- const { isBuiltIn, isRelativeImport, getNames, stripComments, stripStringContents } = require('./utils')
1
+ const { isBuiltIn, isRelativeImport, getNames, stripCommentsThenStrings } = require('./utils')
2
2
 
3
3
  // Updated regex to exclude ES6 dynamic imports
4
4
  // require() must always be present — previous version made it optional, causing false matches
@@ -111,8 +111,7 @@ function getRequires(code) {
111
111
  // alone would mis-eat `//` inside strings; running it first instead means
112
112
  // comment chars (incl. apostrophes/backticks inside JSDoc) disappear
113
113
  // before `stripStringContents` looks for real string literals.
114
- code = stripComments(code)
115
- code = stripStringContents(code)
114
+ code = stripCommentsThenStrings(code)
116
115
  const staticRequires = getStaticRequires(code)
117
116
  const dynamicRequires = getDynamicRequires(code)
118
117
  return staticRequires.concat(dynamicRequires).sort((a, b) => a.index - b.index)
package/src/utils.js CHANGED
@@ -291,6 +291,26 @@ function stripComments(code) {
291
291
  return stripCommentsImpl(code)
292
292
  }
293
293
 
294
+ // Last input/output of stripCommentsThenStrings: one parse runs the import,
295
+ // require and export scanners over the same source text in a row.
296
+ let lastStripInput = null
297
+ let lastStripOutput = ''
298
+
299
+ /**
300
+ * `stripStringContents(stripComments(code))`, the prelude of the import, require
301
+ * and export scanners (comments first, then strings: see getImports). Pure in
302
+ * `code`; the latest result is kept for the next scanner of the same text.
303
+ * @param {string} code
304
+ * @returns {string}
305
+ */
306
+ function stripCommentsThenStrings(code) {
307
+ if (code === lastStripInput) return lastStripOutput
308
+ const output = stripStringContents(stripCommentsImpl(code))
309
+ lastStripInput = code
310
+ lastStripOutput = output
311
+ return output
312
+ }
313
+
294
314
  /**
295
315
  * Check if a module specifier is a Node.js built-in
296
316
  * @param {string} moduleName - The module specifier (e.g., 'fs', 'node:fs/promises')
@@ -504,6 +524,7 @@ module.exports = {
504
524
  stripComments,
505
525
  stripStringContents,
506
526
  stripCommentsAndStrings,
527
+ stripCommentsThenStrings,
507
528
  isBuiltIn,
508
529
  isRelativeImport,
509
530
  isRelativeImportWithQuotes,
@@ -0,0 +1,111 @@
1
+ // resolveDepPaths must return every reachable file exactly once, including
2
+ // import cycles, files that depend on a cycle, self-imports and files that
3
+ // import the same module more than once (duplicate edges).
4
+ const { test } = require('uvu')
5
+ const assert = require('uvu/assert')
6
+ const path = require('path')
7
+ const { depGraph, resolveDepPaths } = require('../src/dep-graph.js')
8
+
9
+ const FIXTURES = path.resolve(__dirname, './fixtures/cycle-fixtures')
10
+
11
+ /** @param {string} rel */
12
+ const fixture = (rel) => path.join(FIXTURES, rel)
13
+
14
+ /**
15
+ * Resolve deps and return basenames (fixtures use unique names per directory)
16
+ * @param {string} entry
17
+ */
18
+ function names(entry) {
19
+ const result = resolveDepPaths(entry, { returnErrors: true })
20
+ assert.equal(result.errors, [], 'fixture should resolve without errors')
21
+ return result.files.map(f => path.basename(f))
22
+ }
23
+
24
+ /**
25
+ * Every file in the graph (= everything reachable from the entry) appears exactly once
26
+ * @param {string} entry
27
+ */
28
+ function assertComplete(entry) {
29
+ const graph = depGraph(entry)
30
+ const { files } = resolveDepPaths(entry)
31
+ assert.is(new Set(files).size, files.length, 'no duplicates')
32
+ assert.equal([...files].sort(), [...graph.keys()].sort(), 'files equal the reachable set')
33
+ }
34
+
35
+ test('2-file cycle: both cycle members and their dependent are returned', () => {
36
+ const entry = fixture('two-cycle/index.js')
37
+ assertComplete(entry)
38
+ assert.equal(names(entry), ['a.js', 'b.js', 'index.js'])
39
+ })
40
+
41
+ test('cycle through the entry file: entry and its cycle peer are returned', () => {
42
+ const entry = fixture('cycle-entry/index.js')
43
+ assertComplete(entry)
44
+ assert.equal(names(entry), ['leaf.js', 'index.js', 'peer.js'])
45
+ })
46
+
47
+ test('3-file cycle with a dependent outside it and a leaf below it', () => {
48
+ const entry = fixture('three-cycle/index.js')
49
+ assertComplete(entry)
50
+ // leaf first (dependency of the cycle), cycle members in discovery order,
51
+ // then w (depends on the cycle), then the entry
52
+ assert.equal(names(entry), ['leaf.js', 'x.js', 'y.js', 'z.js', 'w.js', 'index.js'])
53
+ })
54
+
55
+ test('self-import does not drop the file or its dependents', () => {
56
+ const entry = fixture('self-import/index.js')
57
+ assertComplete(entry)
58
+ assert.equal(names(entry), ['leaf.js', 'self.js', 'index.js'])
59
+ })
60
+
61
+ test('same module required twice (same and different specifiers)', () => {
62
+ const entry = fixture('duplicate-edge/index.js')
63
+ assertComplete(entry)
64
+ assert.equal(names(entry), ['leaf.js', 'other.js', 'util.js', 'index.js'])
65
+ })
66
+
67
+ test('same module imported (ESM) and required (CJS) in one file', () => {
68
+ const entry = fixture('mixed-duplicate/index.mjs')
69
+ assertComplete(entry)
70
+ assert.equal(names(entry), ['base.mjs', 'helper.mjs', 'index.mjs'])
71
+ })
72
+
73
+ test('diamond (acyclic): shared dependency once, dependencies before dependents', () => {
74
+ const entry = fixture('diamond/index.js')
75
+ assertComplete(entry)
76
+ assert.equal(names(entry), ['d.js', 'c.js', 'a.js', 'b.js', 'index.js'])
77
+ })
78
+
79
+ test('acyclic graphs keep dependencies-first order', () => {
80
+ for (const rel of ['diamond/index.js', 'duplicate-edge/index.js', 'mixed-duplicate/index.mjs']) {
81
+ const entry = fixture(rel)
82
+ const graph = depGraph(entry)
83
+ const { files } = resolveDepPaths(entry)
84
+ const pos = new Map(files.map((f, i) => [f, i]))
85
+ for (const [file, deps] of graph) {
86
+ for (const dep of deps) {
87
+ assert.ok(pos.get(dep) < pos.get(file), `${path.basename(dep)} before ${path.basename(file)} in ${rel}`)
88
+ }
89
+ }
90
+ }
91
+ })
92
+
93
+ test('files outside a cycle still come after everything they depend on', () => {
94
+ const entry = fixture('three-cycle/index.js')
95
+ const { files } = resolveDepPaths(entry)
96
+ const pos = new Map(files.map(f => [path.basename(f), files.indexOf(f)]))
97
+ for (const member of ['x.js', 'y.js', 'z.js']) {
98
+ assert.ok(pos.get(member) < pos.get('w.js'), `${member} before w.js`)
99
+ assert.ok(pos.get(member) > pos.get('leaf.js'), `leaf.js before ${member}`)
100
+ }
101
+ assert.is(files[files.length - 1], entry, 'entry last when it is not in a cycle')
102
+ })
103
+
104
+ test('output is deterministic across calls', () => {
105
+ for (const rel of ['two-cycle/index.js', 'three-cycle/index.js', 'cycle-entry/index.js', 'self-import/index.js']) {
106
+ const entry = fixture(rel)
107
+ assert.equal(resolveDepPaths(entry).files, resolveDepPaths(entry).files)
108
+ }
109
+ })
110
+
111
+ test.run()
@@ -0,0 +1,2 @@
1
+ const peer = require('./peer')
2
+ module.exports = { run: () => peer }
@@ -0,0 +1 @@
1
+ module.exports = { leaf: true }
@@ -0,0 +1,3 @@
1
+ const index = require('./index')
2
+ const leaf = require('./leaf')
3
+ module.exports = { index, leaf }
@@ -0,0 +1,2 @@
1
+ const c = require('./c')
2
+ module.exports = { a: () => c }
@@ -0,0 +1,2 @@
1
+ const c = require('./c')
2
+ module.exports = { b: () => c }
@@ -0,0 +1,2 @@
1
+ const d = require('./d')
2
+ module.exports = { c: () => d }
@@ -0,0 +1 @@
1
+ module.exports = { d: true }
@@ -0,0 +1,3 @@
1
+ const a = require('./a')
2
+ const b = require('./b')
3
+ module.exports = { a, b }
@@ -0,0 +1,5 @@
1
+ const a = require('./util')
2
+ const b = require('./util.js')
3
+ const c = require('./util')
4
+ const other = require('./other')
5
+ module.exports = { a, b, c, other }
@@ -0,0 +1 @@
1
+ module.exports = { leaf: true }
@@ -0,0 +1 @@
1
+ module.exports = { other: true }
@@ -0,0 +1,2 @@
1
+ const leaf = require('./leaf')
2
+ module.exports = { util: () => leaf }
@@ -0,0 +1 @@
1
+ export const base = 1
@@ -0,0 +1,2 @@
1
+ import { base } from './base.mjs'
2
+ export const helper = () => base
@@ -0,0 +1,3 @@
1
+ import { helper } from './helper.mjs'
2
+ const again = require('./helper.mjs')
3
+ export const run = () => [helper, again]
@@ -0,0 +1,2 @@
1
+ const self = require('./self')
2
+ module.exports = { self }
@@ -0,0 +1 @@
1
+ module.exports = { leaf: true }
@@ -0,0 +1,3 @@
1
+ const me = require('./self')
2
+ const leaf = require('./leaf')
3
+ module.exports = { me, leaf }
@@ -0,0 +1,3 @@
1
+ const w = require('./w')
2
+ const x = require('./x')
3
+ module.exports = { w, x }
@@ -0,0 +1 @@
1
+ module.exports = { leaf: true }
@@ -0,0 +1,2 @@
1
+ const x = require('./x')
2
+ module.exports = { w: () => x }
@@ -0,0 +1,2 @@
1
+ const y = require('./y')
2
+ module.exports = { x: () => y }
@@ -0,0 +1,2 @@
1
+ const z = require('./z')
2
+ module.exports = { y: () => z }
@@ -0,0 +1,3 @@
1
+ const x = require('./x')
2
+ const leaf = require('./leaf')
3
+ module.exports = { z: () => [x, leaf] }
@@ -0,0 +1,2 @@
1
+ const b = require('./b')
2
+ module.exports = { ping: () => b.pong() }
@@ -0,0 +1,2 @@
1
+ const a = require('./a')
2
+ module.exports = { pong: () => a }
@@ -0,0 +1,2 @@
1
+ const a = require('./a')
2
+ module.exports = { run: () => a.ping() }