diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..2f28772 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,53 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + branches: [main] + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + ci: + name: Node ${{ matrix.node-version }} + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + node-version: [20, 22] + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install pnpm + uses: pnpm/action-setup@v4 + with: + version: 10 + + - name: Setup Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + cache: pnpm + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Lint + run: pnpm lint + + - name: Format check + run: pnpm fmt:check + + - name: Typecheck + run: pnpm tsc --noEmit + + - name: Build + run: pnpm build + + - name: Test + run: pnpm test:coverage diff --git a/README.md b/README.md index fdce189..669a79e 100644 --- a/README.md +++ b/README.md @@ -29,27 +29,27 @@ Measured with [Vitest bench](https://vitest.dev/guide/features.html#benchmarking Each tool brings different strengths — [dependency-tree](https://github.com/dependents/node-dependency-tree) offers robust AST-based analysis via detective, [madge](https://github.com/pahen/madge) supports multiple languages and provides circular dependency detection with visualization. importree trades those features for raw speed through regex-based extraction. -| Scenario | importree | [dependency-tree](https://github.com/dependents/node-dependency-tree) | [madge](https://github.com/pahen/madge) | Manual glob+regex | ts.createProgram | -|----------|-----------|-----------------|-------|-------------------|------------------| -| Small (10 files) | **0.4 ms** | 3.1 ms | 3.7 ms | 0.6 ms | 49.9 ms | -| Medium (100 files) | **2.1 ms** | 14.3 ms | 15.1 ms | 5.2 ms | 48.4 ms | -| Large (500 files) | **12.7 ms** | 44.4 ms | 43.3 ms | 26.5 ms | 50.9 ms | +| Scenario | importree | [dependency-tree](https://github.com/dependents/node-dependency-tree) | [madge](https://github.com/pahen/madge) | Manual glob+regex | ts.createProgram | +| ------------------ | ----------- | --------------------------------------------------------------------- | --------------------------------------- | ----------------- | ---------------- | +| Small (10 files) | **0.4 ms** | 3.1 ms | 3.7 ms | 0.6 ms | 49.9 ms | +| Medium (100 files) | **2.1 ms** | 14.3 ms | 15.1 ms | 5.2 ms | 48.4 ms | +| Large (500 files) | **12.7 ms** | 44.4 ms | 43.3 ms | 26.5 ms | 50.9 ms | ### Full tree build -| Project size | Mean time | Throughput | -|-------------|-----------|------------| -| 10 files | 0.4 ms | ~2,548 ops/s | -| 100 files | 2.5 ms | ~406 ops/s | -| 500 files | 12.1 ms | ~83 ops/s | -| 1,000 files | 26.4 ms | ~38 ops/s | +| Project size | Mean time | Throughput | +| ------------ | --------- | ------------ | +| 10 files | 0.4 ms | ~2,548 ops/s | +| 100 files | 2.5 ms | ~406 ops/s | +| 500 files | 12.1 ms | ~83 ops/s | +| 1,000 files | 26.4 ms | ~38 ops/s | ### Scanner throughput -| Operation | Throughput | -|-----------|-----------| -| `scanImports` (3 imports) | ~661K ops/s | -| `scanImports` (50 imports) | ~41K ops/s | +| Operation | Throughput | +| ----------------------------- | ------------ | +| `scanImports` (3 imports) | ~661K ops/s | +| `scanImports` (50 imports) | ~41K ops/s | | `stripComments` (1,000 lines) | ~2,497 ops/s | > Run `pnpm bench:run` to reproduce locally. @@ -73,10 +73,10 @@ Requires Node.js >= 18. ### Build the tree ```ts -import { importree } from 'importree'; +import { importree } from "importree"; -const tree = await importree('./src/index.ts', { - aliases: { '@': './src' }, +const tree = await importree("./src/index.ts", { + aliases: { "@": "./src" }, }); console.log(tree.files); @@ -92,12 +92,12 @@ console.log(tree.graph); ### Find affected files ```ts -import { importree, getAffectedFiles } from 'importree'; +import { importree, getAffectedFiles } from "importree"; -const tree = await importree('./src/index.ts'); +const tree = await importree("./src/index.ts"); // When utils.ts changes, what needs rebuilding? -const affected = getAffectedFiles(tree, './src/utils.ts'); +const affected = getAffectedFiles(tree, "./src/utils.ts"); console.log(affected); // ['/abs/src/app.ts', '/abs/src/index.ts'] @@ -116,18 +116,18 @@ importree(entry: string, options?: ImportreeOptions): Promise #### Parameters -| Parameter | Type | Required | Description | -|-----------|------|----------|-------------| -| `entry` | `string` | Yes | Path to the entry file (resolved against `cwd`) | -| `options` | `ImportreeOptions` | No | Configuration for resolution behavior | +| Parameter | Type | Required | Description | +| --------- | ------------------ | -------- | ----------------------------------------------- | +| `entry` | `string` | Yes | Path to the entry file (resolved against `cwd`) | +| `options` | `ImportreeOptions` | No | Configuration for resolution behavior | #### `ImportreeOptions` -| Option | Type | Default | Description | -|--------|------|---------|-------------| -| `rootDir` | `string` | `process.cwd()` | Root directory for resolving relative alias paths | -| `aliases` | `Record` | `{}` | Path alias mappings (e.g., `{ '@': './src' }`) | -| `extensions` | `string[]` | `['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs']` | File extensions to try when resolving extensionless imports | +| Option | Type | Default | Description | +| ------------ | ------------------------ | ------------------------------------------------ | ----------------------------------------------------------- | +| `rootDir` | `string` | `process.cwd()` | Root directory for resolving relative alias paths | +| `aliases` | `Record` | `{}` | Path alias mappings (e.g., `{ '@': './src' }`) | +| `extensions` | `string[]` | `['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs']` | File extensions to try when resolving extensionless imports | #### Returns @@ -145,10 +145,10 @@ getAffectedFiles(tree: ImportTree, changedFile: string): string[] #### Parameters -| Parameter | Type | Required | Description | -|-----------|------|----------|-------------| -| `tree` | `ImportTree` | Yes | A tree previously returned by `importree()` | -| `changedFile` | `string` | Yes | Path to the file that changed (resolved to absolute) | +| Parameter | Type | Required | Description | +| ------------- | ------------ | -------- | ---------------------------------------------------- | +| `tree` | `ImportTree` | Yes | A tree previously returned by `importree()` | +| `changedFile` | `string` | Yes | Path to the file that changed (resolved to absolute) | #### Returns @@ -160,13 +160,13 @@ getAffectedFiles(tree: ImportTree, changedFile: string): string[] The result object returned by `importree()`. -| Field | Type | Description | -|-------|------|-------------| -| `entrypoint` | `string` | Absolute path of the entry file | -| `files` | `string[]` | Sorted absolute paths of all local files in the dependency tree | -| `externals` | `string[]` | Sorted unique bare import specifiers — packages like `react`, `lodash`, `node:fs` | -| `graph` | `Record` | Forward adjacency list. Each file maps to its direct local imports. | -| `reverseGraph` | `Record` | Reverse adjacency list. Each file maps to files that import it. | +| Field | Type | Description | +| -------------- | -------------------------- | --------------------------------------------------------------------------------- | +| `entrypoint` | `string` | Absolute path of the entry file | +| `files` | `string[]` | Sorted absolute paths of all local files in the dependency tree | +| `externals` | `string[]` | Sorted unique bare import specifiers — packages like `react`, `lodash`, `node:fs` | +| `graph` | `Record` | Forward adjacency list. Each file maps to its direct local imports. | +| `reverseGraph` | `Record` | Reverse adjacency list. Each file maps to files that import it. | ## What gets detected diff --git a/coverage/base.css b/coverage/base.css new file mode 100644 index 0000000..f418035 --- /dev/null +++ b/coverage/base.css @@ -0,0 +1,224 @@ +body, html { + margin:0; padding: 0; + height: 100%; +} +body { + font-family: Helvetica Neue, Helvetica, Arial; + font-size: 14px; + color:#333; +} +.small { font-size: 12px; } +*, *:after, *:before { + -webkit-box-sizing:border-box; + -moz-box-sizing:border-box; + box-sizing:border-box; + } +h1 { font-size: 20px; margin: 0;} +h2 { font-size: 14px; } +pre { + font: 12px/1.4 Consolas, "Liberation Mono", Menlo, Courier, monospace; + margin: 0; + padding: 0; + -moz-tab-size: 2; + -o-tab-size: 2; + tab-size: 2; +} +a { color:#0074D9; text-decoration:none; } +a:hover { text-decoration:underline; } +.strong { font-weight: bold; } +.space-top1 { padding: 10px 0 0 0; } +.pad2y { padding: 20px 0; } +.pad1y { padding: 10px 0; } +.pad2x { padding: 0 20px; } +.pad2 { padding: 20px; } +.pad1 { padding: 10px; } +.space-left2 { padding-left:55px; } +.space-right2 { padding-right:20px; } +.center { text-align:center; } +.clearfix { display:block; } +.clearfix:after { + content:''; + display:block; + height:0; + clear:both; + visibility:hidden; + } +.fl { float: left; } +@media only screen and (max-width:640px) { + .col3 { width:100%; max-width:100%; } + .hide-mobile { display:none!important; } +} + +.quiet { + color: #7f7f7f; + color: rgba(0,0,0,0.5); +} +.quiet a { opacity: 0.7; } + +.fraction { + font-family: Consolas, 'Liberation Mono', Menlo, Courier, monospace; + font-size: 10px; + color: #555; + background: #E8E8E8; + padding: 4px 5px; + border-radius: 3px; + vertical-align: middle; +} + +div.path a:link, div.path a:visited { color: #333; } +table.coverage { + border-collapse: collapse; + margin: 10px 0 0 0; + padding: 0; +} + +table.coverage td { + margin: 0; + padding: 0; + vertical-align: top; +} +table.coverage td.line-count { + text-align: right; + padding: 0 5px 0 20px; +} +table.coverage td.line-coverage { + text-align: right; + padding-right: 10px; + min-width:20px; +} + +table.coverage td span.cline-any { + display: inline-block; + padding: 0 5px; + width: 100%; +} +.missing-if-branch { + display: inline-block; + margin-right: 5px; + border-radius: 3px; + position: relative; + padding: 0 4px; + background: #333; + color: yellow; +} + +.skip-if-branch { + display: none; + margin-right: 10px; + position: relative; + padding: 0 4px; + background: #ccc; + color: white; +} +.missing-if-branch .typ, .skip-if-branch .typ { + color: inherit !important; +} +.coverage-summary { + border-collapse: collapse; + width: 100%; +} +.coverage-summary tr { border-bottom: 1px solid #bbb; } +.keyline-all { border: 1px solid #ddd; } +.coverage-summary td, .coverage-summary th { padding: 10px; } +.coverage-summary tbody { border: 1px solid #bbb; } +.coverage-summary td { border-right: 1px solid #bbb; } +.coverage-summary td:last-child { border-right: none; } +.coverage-summary th { + text-align: left; + font-weight: normal; + white-space: nowrap; +} +.coverage-summary th.file { border-right: none !important; } +.coverage-summary th.pct { } +.coverage-summary th.pic, +.coverage-summary th.abs, +.coverage-summary td.pct, +.coverage-summary td.abs { text-align: right; } +.coverage-summary td.file { white-space: nowrap; } +.coverage-summary td.pic { min-width: 120px !important; } +.coverage-summary tfoot td { } + +.coverage-summary .sorter { + height: 10px; + width: 7px; + display: inline-block; + margin-left: 0.5em; + background: url(sort-arrow-sprite.png) no-repeat scroll 0 0 transparent; +} +.coverage-summary .sorted .sorter { + background-position: 0 -20px; +} +.coverage-summary .sorted-desc .sorter { + background-position: 0 -10px; +} +.status-line { height: 10px; } +/* yellow */ +.cbranch-no { background: yellow !important; color: #111; } +/* dark red */ +.red.solid, .status-line.low, .low .cover-fill { background:#C21F39 } +.low .chart { border:1px solid #C21F39 } +.highlighted, +.highlighted .cstat-no, .highlighted .fstat-no, .highlighted .cbranch-no{ + background: #C21F39 !important; +} +/* medium red */ +.cstat-no, .fstat-no, .cbranch-no, .cbranch-no { background:#F6C6CE } +/* light red */ +.low, .cline-no { background:#FCE1E5 } +/* light green */ +.high, .cline-yes { background:rgb(230,245,208) } +/* medium green */ +.cstat-yes { background:rgb(161,215,106) } +/* dark green */ +.status-line.high, .high .cover-fill { background:rgb(77,146,33) } +.high .chart { border:1px solid rgb(77,146,33) } +/* dark yellow (gold) */ +.status-line.medium, .medium .cover-fill { background: #f9cd0b; } +.medium .chart { border:1px solid #f9cd0b; } +/* light yellow */ +.medium { background: #fff4c2; } + +.cstat-skip { background: #ddd; color: #111; } +.fstat-skip { background: #ddd; color: #111 !important; } +.cbranch-skip { background: #ddd !important; color: #111; } + +span.cline-neutral { background: #eaeaea; } + +.coverage-summary td.empty { + opacity: .5; + padding-top: 4px; + padding-bottom: 4px; + line-height: 1; + color: #888; +} + +.cover-fill, .cover-empty { + display:inline-block; + height: 12px; +} +.chart { + line-height: 0; +} +.cover-empty { + background: white; +} +.cover-full { + border-right: none !important; +} +pre.prettyprint { + border: none !important; + padding: 0 !important; + margin: 0 !important; +} +.com { color: #999 !important; } +.ignore-none { color: #999; font-weight: normal; } + +.wrapper { + min-height: 100%; + height: auto !important; + height: 100%; + margin: 0 auto -48px; +} +.footer, .push { + height: 48px; +} diff --git a/coverage/block-navigation.js b/coverage/block-navigation.js new file mode 100644 index 0000000..530d1ed --- /dev/null +++ b/coverage/block-navigation.js @@ -0,0 +1,87 @@ +/* eslint-disable */ +var jumpToCode = (function init() { + // Classes of code we would like to highlight in the file view + var missingCoverageClasses = ['.cbranch-no', '.cstat-no', '.fstat-no']; + + // Elements to highlight in the file listing view + var fileListingElements = ['td.pct.low']; + + // We don't want to select elements that are direct descendants of another match + var notSelector = ':not(' + missingCoverageClasses.join('):not(') + ') > '; // becomes `:not(a):not(b) > ` + + // Selector that finds elements on the page to which we can jump + var selector = + fileListingElements.join(', ') + + ', ' + + notSelector + + missingCoverageClasses.join(', ' + notSelector); // becomes `:not(a):not(b) > a, :not(a):not(b) > b` + + // The NodeList of matching elements + var missingCoverageElements = document.querySelectorAll(selector); + + var currentIndex; + + function toggleClass(index) { + missingCoverageElements + .item(currentIndex) + .classList.remove('highlighted'); + missingCoverageElements.item(index).classList.add('highlighted'); + } + + function makeCurrent(index) { + toggleClass(index); + currentIndex = index; + missingCoverageElements.item(index).scrollIntoView({ + behavior: 'smooth', + block: 'center', + inline: 'center' + }); + } + + function goToPrevious() { + var nextIndex = 0; + if (typeof currentIndex !== 'number' || currentIndex === 0) { + nextIndex = missingCoverageElements.length - 1; + } else if (missingCoverageElements.length > 1) { + nextIndex = currentIndex - 1; + } + + makeCurrent(nextIndex); + } + + function goToNext() { + var nextIndex = 0; + + if ( + typeof currentIndex === 'number' && + currentIndex < missingCoverageElements.length - 1 + ) { + nextIndex = currentIndex + 1; + } + + makeCurrent(nextIndex); + } + + return function jump(event) { + if ( + document.getElementById('fileSearch') === document.activeElement && + document.activeElement != null + ) { + // if we're currently focused on the search input, we don't want to navigate + return; + } + + switch (event.which) { + case 78: // n + case 74: // j + goToNext(); + break; + case 66: // b + case 75: // k + case 80: // p + goToPrevious(); + break; + } + }; +})(); +window.addEventListener('keydown', jumpToCode); diff --git a/coverage/clover.xml b/coverage/clover.xml new file mode 100644 index 0000000..87b94fe --- /dev/null +++ b/coverage/clover.xml @@ -0,0 +1,186 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/coverage/coverage-final.json b/coverage/coverage-final.json new file mode 100644 index 0000000..c77dea6 --- /dev/null +++ b/coverage/coverage-final.json @@ -0,0 +1,5 @@ +{"/Users/alexgrozav/conductor/workspaces/importree/new-york/src/index.ts": {"path":"/Users/alexgrozav/conductor/workspaces/importree/new-york/src/index.ts","statementMap":{"0":{"start":{"line":25,"column":2},"end":{"line":25,"column":null}},"1":{"start":{"line":36,"column":8},"end":{"line":36,"column":null}},"2":{"start":{"line":38,"column":2},"end":{"line":38,"column":null}},"3":{"start":{"line":38,"column":36},"end":{"line":38,"column":null}},"4":{"start":{"line":40,"column":19},"end":{"line":40,"column":null}},"5":{"start":{"line":41,"column":16},"end":{"line":41,"column":null}},"6":{"start":{"line":43,"column":2},"end":{"line":54,"column":null}},"7":{"start":{"line":44,"column":20},"end":{"line":44,"column":null}},"8":{"start":{"line":45,"column":23},"end":{"line":45,"column":null}},"9":{"start":{"line":46,"column":4},"end":{"line":46,"column":null}},"10":{"start":{"line":46,"column":21},"end":{"line":46,"column":null}},"11":{"start":{"line":48,"column":4},"end":{"line":53,"column":null}},"12":{"start":{"line":49,"column":6},"end":{"line":52,"column":null}},"13":{"start":{"line":50,"column":8},"end":{"line":50,"column":null}},"14":{"start":{"line":51,"column":8},"end":{"line":51,"column":null}},"15":{"start":{"line":56,"column":2},"end":{"line":56,"column":null}},"16":{"start":{"line":57,"column":2},"end":{"line":57,"column":null}}},"fnMap":{"0":{"name":"importree","decl":{"start":{"line":24,"column":22},"end":{"line":24,"column":32}},"loc":{"start":{"line":24,"column":96},"end":{"line":26,"column":null}},"line":24},"1":{"name":"getAffectedFiles","decl":{"start":{"line":35,"column":16},"end":{"line":35,"column":33}},"loc":{"start":{"line":35,"column":82},"end":{"line":58,"column":null}},"line":35}},"branchMap":{"0":{"loc":{"start":{"line":25,"column":21},"end":{"line":25,"column":34}},"type":"binary-expr","locations":[{"start":{"line":25,"column":21},"end":{"line":25,"column":32}},{"start":{"line":25,"column":32},"end":{"line":25,"column":34}}],"line":25},"1":{"loc":{"start":{"line":38,"column":2},"end":{"line":38,"column":null}},"type":"if","locations":[{"start":{"line":38,"column":2},"end":{"line":38,"column":null}},{"start":{},"end":{}}],"line":38},"2":{"loc":{"start":{"line":46,"column":4},"end":{"line":46,"column":null}},"type":"if","locations":[{"start":{"line":46,"column":4},"end":{"line":46,"column":null}},{"start":{},"end":{}}],"line":46},"3":{"loc":{"start":{"line":49,"column":6},"end":{"line":52,"column":null}},"type":"if","locations":[{"start":{"line":49,"column":6},"end":{"line":52,"column":null}},{"start":{},"end":{}}],"line":49}},"s":{"0":24,"1":9,"2":9,"3":1,"4":8,"5":8,"6":8,"7":23,"8":23,"9":23,"10":1,"11":22,"12":18,"13":15,"14":15,"15":8,"16":8},"f":{"0":24,"1":9},"b":{"0":[24,22],"1":[1,8],"2":[1,22],"3":[15,3]},"meta":{"lastBranch":4,"lastFunction":2,"lastStatement":17,"seen":{"f:24:22:24:32":0,"s:25:2:25:Infinity":0,"b:25:21:25:32:25:32:25:34":0,"f:35:16:35:33":1,"s:36:8:36:Infinity":1,"b:38:2:38:Infinity:undefined:undefined:undefined:undefined":1,"s:38:2:38:Infinity":2,"s:38:36:38:Infinity":3,"s:40:19:40:Infinity":4,"s:41:16:41:Infinity":5,"s:43:2:54:Infinity":6,"s:44:20:44:Infinity":7,"s:45:23:45:Infinity":8,"b:46:4:46:Infinity:undefined:undefined:undefined:undefined":2,"s:46:4:46:Infinity":9,"s:46:21:46:Infinity":10,"s:48:4:53:Infinity":11,"b:49:6:52:Infinity:undefined:undefined:undefined:undefined":3,"s:49:6:52:Infinity":12,"s:50:8:50:Infinity":13,"s:51:8:51:Infinity":14,"s:56:2:56:Infinity":15,"s:57:2:57:Infinity":16}}} +,"/Users/alexgrozav/conductor/workspaces/importree/new-york/src/resolver.ts": {"path":"/Users/alexgrozav/conductor/workspaces/importree/new-york/src/resolver.ts","statementMap":{"0":{"start":{"line":5,"column":27},"end":{"line":5,"column":null}},"1":{"start":{"line":8,"column":8},"end":{"line":8,"column":null}},"2":{"start":{"line":9,"column":2},"end":{"line":9,"column":null}},"3":{"start":{"line":13,"column":8},"end":{"line":13,"column":null}},"4":{"start":{"line":14,"column":2},"end":{"line":14,"column":null}},"5":{"start":{"line":23,"column":2},"end":{"line":26,"column":null}},"6":{"start":{"line":24,"column":18},"end":{"line":24,"column":null}},"7":{"start":{"line":25,"column":4},"end":{"line":25,"column":null}},"8":{"start":{"line":27,"column":2},"end":{"line":27,"column":null}},"9":{"start":{"line":32,"column":2},"end":{"line":32,"column":null}},"10":{"start":{"line":32,"column":28},"end":{"line":32,"column":null}},"11":{"start":{"line":35,"column":2},"end":{"line":38,"column":null}},"12":{"start":{"line":36,"column":20},"end":{"line":36,"column":null}},"13":{"start":{"line":37,"column":4},"end":{"line":37,"column":null}},"14":{"start":{"line":37,"column":29},"end":{"line":37,"column":null}},"15":{"start":{"line":41,"column":2},"end":{"line":46,"column":null}},"16":{"start":{"line":42,"column":4},"end":{"line":45,"column":null}},"17":{"start":{"line":43,"column":12},"end":{"line":43,"column":null}},"18":{"start":{"line":44,"column":6},"end":{"line":44,"column":null}},"19":{"start":{"line":44,"column":33},"end":{"line":44,"column":null}},"20":{"start":{"line":48,"column":2},"end":{"line":48,"column":null}},"21":{"start":{"line":60,"column":21},"end":{"line":60,"column":null}},"22":{"start":{"line":63,"column":18},"end":{"line":65,"column":null}},"23":{"start":{"line":64,"column":53},"end":{"line":64,"column":78}},"24":{"start":{"line":67,"column":30},"end":{"line":69,"column":null}},"25":{"start":{"line":68,"column":22},"end":{"line":68,"column":null}},"26":{"start":{"line":71,"column":16},"end":{"line":71,"column":null}},"27":{"start":{"line":73,"column":2},"end":{"line":114,"column":null}},"28":{"start":{"line":77,"column":10},"end":{"line":77,"column":null}},"29":{"start":{"line":78,"column":21},"end":{"line":78,"column":null}},"30":{"start":{"line":80,"column":4},"end":{"line":80,"column":null}},"31":{"start":{"line":80,"column":29},"end":{"line":80,"column":null}},"32":{"start":{"line":85,"column":4},"end":{"line":110,"column":null}},"33":{"start":{"line":86,"column":27},"end":{"line":86,"column":null}},"34":{"start":{"line":87,"column":6},"end":{"line":89,"column":null}},"35":{"start":{"line":88,"column":8},"end":{"line":88,"column":null}},"36":{"start":{"line":93,"column":20},"end":{"line":93,"column":null}},"37":{"start":{"line":94,"column":6},"end":{"line":104,"column":null}},"38":{"start":{"line":95,"column":8},"end":{"line":103,"column":null}},"39":{"start":{"line":96,"column":23},"end":{"line":96,"column":null}},"40":{"start":{"line":97,"column":31},"end":{"line":97,"column":null}},"41":{"start":{"line":98,"column":10},"end":{"line":100,"column":null}},"42":{"start":{"line":99,"column":12},"end":{"line":99,"column":null}},"43":{"start":{"line":101,"column":10},"end":{"line":101,"column":null}},"44":{"start":{"line":102,"column":10},"end":{"line":102,"column":null}},"45":{"start":{"line":107,"column":6},"end":{"line":109,"column":null}},"46":{"start":{"line":108,"column":8},"end":{"line":108,"column":null}},"47":{"start":{"line":112,"column":4},"end":{"line":112,"column":null}},"48":{"start":{"line":113,"column":4},"end":{"line":113,"column":null}}},"fnMap":{"0":{"name":"fileExists","decl":{"start":{"line":7,"column":9},"end":{"line":7,"column":20}},"loc":{"start":{"line":7,"column":47},"end":{"line":10,"column":null}},"line":7},"1":{"name":"dirExists","decl":{"start":{"line":12,"column":9},"end":{"line":12,"column":19}},"loc":{"start":{"line":12,"column":46},"end":{"line":15,"column":null}},"line":12},"2":{"name":"getBareSpecifier","decl":{"start":{"line":22,"column":9},"end":{"line":22,"column":26}},"loc":{"start":{"line":22,"column":53},"end":{"line":28,"column":null}},"line":22},"3":{"name":"resolveFile","decl":{"start":{"line":30,"column":9},"end":{"line":30,"column":21}},"loc":{"start":{"line":30,"column":81},"end":{"line":49,"column":null}},"line":30},"4":{"name":"createResolver","decl":{"start":{"line":59,"column":16},"end":{"line":59,"column":31}},"loc":{"start":{"line":59,"column":85},"end":{"line":115,"column":null}},"line":59},"5":{"name":"(anonymous_5)","decl":{"start":{"line":64,"column":43},"end":{"line":64,"column":44}},"loc":{"start":{"line":64,"column":53},"end":{"line":64,"column":78}},"line":64},"6":{"name":"(anonymous_6)","decl":{"start":{"line":68,"column":4},"end":{"line":68,"column":5}},"loc":{"start":{"line":68,"column":22},"end":{"line":68,"column":null}},"line":68},"7":{"name":"resolveSpecifier","decl":{"start":{"line":73,"column":18},"end":{"line":73,"column":null}},"loc":{"start":{"line":76,"column":32},"end":{"line":114,"column":null}},"line":76}},"branchMap":{"0":{"loc":{"start":{"line":9,"column":9},"end":{"line":9,"column":null}},"type":"binary-expr","locations":[{"start":{"line":9,"column":9},"end":{"line":9,"column":31}},{"start":{"line":9,"column":31},"end":{"line":9,"column":null}}],"line":9},"1":{"loc":{"start":{"line":14,"column":9},"end":{"line":14,"column":null}},"type":"binary-expr","locations":[{"start":{"line":14,"column":9},"end":{"line":14,"column":31}},{"start":{"line":14,"column":31},"end":{"line":14,"column":null}}],"line":14},"2":{"loc":{"start":{"line":23,"column":2},"end":{"line":26,"column":null}},"type":"if","locations":[{"start":{"line":23,"column":2},"end":{"line":26,"column":null}},{"start":{},"end":{}}],"line":23},"3":{"loc":{"start":{"line":25,"column":11},"end":{"line":25,"column":null}},"type":"cond-expr","locations":[{"start":{"line":25,"column":31},"end":{"line":25,"column":59}},{"start":{"line":25,"column":59},"end":{"line":25,"column":null}}],"line":25},"4":{"loc":{"start":{"line":32,"column":2},"end":{"line":32,"column":null}},"type":"if","locations":[{"start":{"line":32,"column":2},"end":{"line":32,"column":null}},{"start":{},"end":{}}],"line":32},"5":{"loc":{"start":{"line":37,"column":4},"end":{"line":37,"column":null}},"type":"if","locations":[{"start":{"line":37,"column":4},"end":{"line":37,"column":null}},{"start":{},"end":{}}],"line":37},"6":{"loc":{"start":{"line":41,"column":2},"end":{"line":46,"column":null}},"type":"if","locations":[{"start":{"line":41,"column":2},"end":{"line":46,"column":null}},{"start":{},"end":{}}],"line":41},"7":{"loc":{"start":{"line":44,"column":6},"end":{"line":44,"column":null}},"type":"if","locations":[{"start":{"line":44,"column":6},"end":{"line":44,"column":null}},{"start":{},"end":{}}],"line":44},"8":{"loc":{"start":{"line":60,"column":21},"end":{"line":60,"column":null}},"type":"binary-expr","locations":[{"start":{"line":60,"column":21},"end":{"line":60,"column":43}},{"start":{"line":60,"column":43},"end":{"line":60,"column":null}}],"line":60},"9":{"loc":{"start":{"line":63,"column":18},"end":{"line":65,"column":null}},"type":"cond-expr","locations":[{"start":{"line":64,"column":6},"end":{"line":64,"column":null}},{"start":{"line":65,"column":6},"end":{"line":65,"column":null}}],"line":63},"10":{"loc":{"start":{"line":68,"column":23},"end":{"line":68,"column":79}},"type":"cond-expr","locations":[{"start":{"line":68,"column":48},"end":{"line":68,"column":56}},{"start":{"line":68,"column":48},"end":{"line":68,"column":79}}],"line":68},"11":{"loc":{"start":{"line":80,"column":4},"end":{"line":80,"column":null}},"type":"if","locations":[{"start":{"line":80,"column":4},"end":{"line":80,"column":null}},{"start":{},"end":{}}],"line":80},"12":{"loc":{"start":{"line":85,"column":4},"end":{"line":110,"column":null}},"type":"if","locations":[{"start":{"line":85,"column":4},"end":{"line":110,"column":null}},{"start":{"line":92,"column":9},"end":{"line":110,"column":null}}],"line":85},"13":{"loc":{"start":{"line":85,"column":8},"end":{"line":85,"column":67}},"type":"binary-expr","locations":[{"start":{"line":85,"column":8},"end":{"line":85,"column":38}},{"start":{"line":85,"column":38},"end":{"line":85,"column":67}}],"line":85},"14":{"loc":{"start":{"line":87,"column":6},"end":{"line":89,"column":null}},"type":"if","locations":[{"start":{"line":87,"column":6},"end":{"line":89,"column":null}},{"start":{},"end":{}}],"line":87},"15":{"loc":{"start":{"line":95,"column":8},"end":{"line":103,"column":null}},"type":"if","locations":[{"start":{"line":95,"column":8},"end":{"line":103,"column":null}},{"start":{},"end":{}}],"line":95},"16":{"loc":{"start":{"line":95,"column":12},"end":{"line":95,"column":72}},"type":"binary-expr","locations":[{"start":{"line":95,"column":12},"end":{"line":95,"column":36}},{"start":{"line":95,"column":36},"end":{"line":95,"column":72}}],"line":95},"17":{"loc":{"start":{"line":96,"column":23},"end":{"line":96,"column":null}},"type":"cond-expr","locations":[{"start":{"line":96,"column":46},"end":{"line":96,"column":51}},{"start":{"line":96,"column":51},"end":{"line":96,"column":null}}],"line":96},"18":{"loc":{"start":{"line":98,"column":10},"end":{"line":100,"column":null}},"type":"if","locations":[{"start":{"line":98,"column":10},"end":{"line":100,"column":null}},{"start":{},"end":{}}],"line":98},"19":{"loc":{"start":{"line":107,"column":6},"end":{"line":109,"column":null}},"type":"if","locations":[{"start":{"line":107,"column":6},"end":{"line":109,"column":null}},{"start":{},"end":{}}],"line":107}},"s":{"0":3,"1":156,"2":156,"3":7,"4":7,"5":7,"6":2,"7":2,"8":5,"9":57,"10":1,"11":56,"12":91,"13":91,"14":49,"15":7,"16":3,"17":8,"18":8,"19":2,"20":5,"21":38,"22":38,"23":1,"24":38,"25":7,"26":38,"27":38,"28":68,"29":68,"30":68,"31":4,"32":64,"33":51,"34":51,"35":48,"36":13,"37":13,"38":6,"39":6,"40":6,"41":6,"42":4,"43":6,"44":6,"45":13,"46":7,"47":64,"48":64},"f":{"0":156,"1":7,"2":7,"3":57,"4":38,"5":1,"6":7,"7":68},"b":{"0":[156,55],"1":[7,3],"2":[2,5],"3":[1,1],"4":[1,56],"5":[49,42],"6":[3,4],"7":[2,6],"8":[38,38],"9":[6,32],"10":[1,6],"11":[4,64],"12":[51,13],"13":[64,13],"14":[48,3],"15":[6,0],"16":[6,4],"17":[2,4],"18":[4,2],"19":[7,6]},"meta":{"lastBranch":20,"lastFunction":8,"lastStatement":49,"seen":{"s:5:27:5:Infinity":0,"f:7:9:7:20":0,"s:8:8:8:Infinity":1,"s:9:2:9:Infinity":2,"b:9:9:9:31:9:31:9:Infinity":0,"f:12:9:12:19":1,"s:13:8:13:Infinity":3,"s:14:2:14:Infinity":4,"b:14:9:14:31:14:31:14:Infinity":1,"f:22:9:22:26":2,"b:23:2:26:Infinity:undefined:undefined:undefined:undefined":2,"s:23:2:26:Infinity":5,"s:24:18:24:Infinity":6,"s:25:4:25:Infinity":7,"b:25:31:25:59:25:59:25:Infinity":3,"s:27:2:27:Infinity":8,"f:30:9:30:21":3,"b:32:2:32:Infinity:undefined:undefined:undefined:undefined":4,"s:32:2:32:Infinity":9,"s:32:28:32:Infinity":10,"s:35:2:38:Infinity":11,"s:36:20:36:Infinity":12,"b:37:4:37:Infinity:undefined:undefined:undefined:undefined":5,"s:37:4:37:Infinity":13,"s:37:29:37:Infinity":14,"b:41:2:46:Infinity:undefined:undefined:undefined:undefined":6,"s:41:2:46:Infinity":15,"s:42:4:45:Infinity":16,"s:43:12:43:Infinity":17,"b:44:6:44:Infinity:undefined:undefined:undefined:undefined":7,"s:44:6:44:Infinity":18,"s:44:33:44:Infinity":19,"s:48:2:48:Infinity":20,"f:59:16:59:31":4,"s:60:21:60:Infinity":21,"b:60:21:60:43:60:43:60:Infinity":8,"s:63:18:65:Infinity":22,"b:64:6:64:Infinity:65:6:65:Infinity":9,"f:64:43:64:44":5,"s:64:53:64:78":23,"s:67:30:69:Infinity":24,"f:68:4:68:5":6,"s:68:22:68:Infinity":25,"b:68:48:68:56:68:48:68:79":10,"s:71:16:71:Infinity":26,"s:73:2:114:Infinity":27,"f:73:18:73:Infinity":7,"s:77:10:77:Infinity":28,"s:78:21:78:Infinity":29,"b:80:4:80:Infinity:undefined:undefined:undefined:undefined":11,"s:80:4:80:Infinity":30,"s:80:29:80:Infinity":31,"b:85:4:110:Infinity:92:9:110:Infinity":12,"s:85:4:110:Infinity":32,"b:85:8:85:38:85:38:85:67":13,"s:86:27:86:Infinity":33,"b:87:6:89:Infinity:undefined:undefined:undefined:undefined":14,"s:87:6:89:Infinity":34,"s:88:8:88:Infinity":35,"s:93:20:93:Infinity":36,"s:94:6:104:Infinity":37,"b:95:8:103:Infinity:undefined:undefined:undefined:undefined":15,"s:95:8:103:Infinity":38,"b:95:12:95:36:95:36:95:72":16,"s:96:23:96:Infinity":39,"b:96:46:96:51:96:51:96:Infinity":17,"s:97:31:97:Infinity":40,"b:98:10:100:Infinity:undefined:undefined:undefined:undefined":18,"s:98:10:100:Infinity":41,"s:99:12:99:Infinity":42,"s:101:10:101:Infinity":43,"s:102:10:102:Infinity":44,"b:107:6:109:Infinity:undefined:undefined:undefined:undefined":19,"s:107:6:109:Infinity":45,"s:108:8:108:Infinity":46,"s:112:4:112:Infinity":47,"s:113:4:113:Infinity":48}}} +,"/Users/alexgrozav/conductor/workspaces/importree/new-york/src/scanner.ts": {"path":"/Users/alexgrozav/conductor/workspaces/importree/new-york/src/scanner.ts","statementMap":{"0":{"start":{"line":10,"column":14},"end":{"line":10,"column":null}},"1":{"start":{"line":11,"column":27},"end":{"line":11,"column":null}},"2":{"start":{"line":12,"column":10},"end":{"line":12,"column":null}},"3":{"start":{"line":14,"column":2},"end":{"line":103,"column":null}},"4":{"start":{"line":15,"column":15},"end":{"line":15,"column":null}},"5":{"start":{"line":16,"column":17},"end":{"line":16,"column":null}},"6":{"start":{"line":19,"column":4},"end":{"line":26,"column":null}},"7":{"start":{"line":20,"column":6},"end":{"line":20,"column":null}},"8":{"start":{"line":21,"column":6},"end":{"line":21,"column":null}},"9":{"start":{"line":22,"column":6},"end":{"line":24,"column":null}},"10":{"start":{"line":23,"column":8},"end":{"line":23,"column":null}},"11":{"start":{"line":25,"column":6},"end":{"line":25,"column":null}},"12":{"start":{"line":29,"column":4},"end":{"line":41,"column":null}},"13":{"start":{"line":30,"column":6},"end":{"line":30,"column":null}},"14":{"start":{"line":31,"column":6},"end":{"line":31,"column":null}},"15":{"start":{"line":32,"column":6},"end":{"line":35,"column":null}},"16":{"start":{"line":33,"column":8},"end":{"line":33,"column":null}},"17":{"start":{"line":34,"column":8},"end":{"line":34,"column":null}},"18":{"start":{"line":36,"column":6},"end":{"line":39,"column":null}},"19":{"start":{"line":37,"column":8},"end":{"line":37,"column":null}},"20":{"start":{"line":38,"column":8},"end":{"line":38,"column":null}},"21":{"start":{"line":40,"column":6},"end":{"line":40,"column":null}},"22":{"start":{"line":45,"column":4},"end":{"line":65,"column":null}},"23":{"start":{"line":46,"column":20},"end":{"line":46,"column":null}},"24":{"start":{"line":47,"column":6},"end":{"line":47,"column":null}},"25":{"start":{"line":48,"column":6},"end":{"line":48,"column":null}},"26":{"start":{"line":49,"column":6},"end":{"line":59,"column":null}},"27":{"start":{"line":50,"column":8},"end":{"line":58,"column":null}},"28":{"start":{"line":51,"column":10},"end":{"line":51,"column":null}},"29":{"start":{"line":52,"column":10},"end":{"line":52,"column":null}},"30":{"start":{"line":53,"column":10},"end":{"line":53,"column":null}},"31":{"start":{"line":54,"column":10},"end":{"line":54,"column":null}},"32":{"start":{"line":56,"column":10},"end":{"line":56,"column":null}},"33":{"start":{"line":57,"column":10},"end":{"line":57,"column":null}},"34":{"start":{"line":60,"column":6},"end":{"line":63,"column":null}},"35":{"start":{"line":61,"column":8},"end":{"line":61,"column":null}},"36":{"start":{"line":62,"column":8},"end":{"line":62,"column":null}},"37":{"start":{"line":64,"column":6},"end":{"line":64,"column":null}},"38":{"start":{"line":68,"column":4},"end":{"line":98,"column":null}},"39":{"start":{"line":69,"column":6},"end":{"line":69,"column":null}},"40":{"start":{"line":70,"column":6},"end":{"line":70,"column":null}},"41":{"start":{"line":71,"column":18},"end":{"line":71,"column":null}},"42":{"start":{"line":72,"column":6},"end":{"line":96,"column":null}},"43":{"start":{"line":73,"column":8},"end":{"line":95,"column":null}},"44":{"start":{"line":74,"column":10},"end":{"line":74,"column":null}},"45":{"start":{"line":75,"column":10},"end":{"line":75,"column":null}},"46":{"start":{"line":76,"column":10},"end":{"line":76,"column":null}},"47":{"start":{"line":77,"column":10},"end":{"line":77,"column":null}},"48":{"start":{"line":78,"column":8},"end":{"line":95,"column":null}},"49":{"start":{"line":79,"column":10},"end":{"line":79,"column":null}},"50":{"start":{"line":80,"column":10},"end":{"line":80,"column":null}},"51":{"start":{"line":81,"column":10},"end":{"line":81,"column":null}},"52":{"start":{"line":82,"column":10},"end":{"line":82,"column":null}},"53":{"start":{"line":83,"column":10},"end":{"line":83,"column":null}},"54":{"start":{"line":84,"column":8},"end":{"line":95,"column":null}},"55":{"start":{"line":85,"column":10},"end":{"line":85,"column":null}},"56":{"start":{"line":86,"column":10},"end":{"line":86,"column":null}},"57":{"start":{"line":87,"column":10},"end":{"line":87,"column":null}},"58":{"start":{"line":88,"column":8},"end":{"line":95,"column":null}},"59":{"start":{"line":89,"column":10},"end":{"line":89,"column":null}},"60":{"start":{"line":90,"column":10},"end":{"line":90,"column":null}},"61":{"start":{"line":91,"column":10},"end":{"line":91,"column":null}},"62":{"start":{"line":93,"column":10},"end":{"line":93,"column":null}},"63":{"start":{"line":94,"column":10},"end":{"line":94,"column":null}},"64":{"start":{"line":97,"column":6},"end":{"line":97,"column":null}},"65":{"start":{"line":101,"column":4},"end":{"line":101,"column":null}},"66":{"start":{"line":102,"column":4},"end":{"line":102,"column":null}},"67":{"start":{"line":105,"column":2},"end":{"line":105,"column":null}},"68":{"start":{"line":109,"column":15},"end":{"line":109,"column":null}},"69":{"start":{"line":110,"column":21},"end":{"line":110,"column":null}},"70":{"start":{"line":111,"column":18},"end":{"line":111,"column":null}},"71":{"start":{"line":112,"column":18},"end":{"line":112,"column":null}},"72":{"start":{"line":123,"column":19},"end":{"line":123,"column":null}},"73":{"start":{"line":124,"column":21},"end":{"line":124,"column":null}},"74":{"start":{"line":126,"column":2},"end":{"line":126,"column":null}},"75":{"start":{"line":126,"column":45},"end":{"line":126,"column":null}},"76":{"start":{"line":127,"column":2},"end":{"line":127,"column":null}},"77":{"start":{"line":127,"column":51},"end":{"line":127,"column":null}},"78":{"start":{"line":128,"column":2},"end":{"line":128,"column":null}},"79":{"start":{"line":128,"column":48},"end":{"line":128,"column":null}},"80":{"start":{"line":129,"column":2},"end":{"line":129,"column":null}},"81":{"start":{"line":129,"column":48},"end":{"line":129,"column":null}},"82":{"start":{"line":131,"column":2},"end":{"line":131,"column":null}}},"fnMap":{"0":{"name":"stripComments","decl":{"start":{"line":9,"column":16},"end":{"line":9,"column":30}},"loc":{"start":{"line":9,"column":52},"end":{"line":106,"column":null}},"line":9},"1":{"name":"scanImports","decl":{"start":{"line":122,"column":16},"end":{"line":122,"column":28}},"loc":{"start":{"line":122,"column":52},"end":{"line":132,"column":null}},"line":122}},"branchMap":{"0":{"loc":{"start":{"line":16,"column":17},"end":{"line":16,"column":null}},"type":"cond-expr","locations":[{"start":{"line":16,"column":31},"end":{"line":16,"column":45}},{"start":{"line":16,"column":45},"end":{"line":16,"column":null}}],"line":16},"1":{"loc":{"start":{"line":19,"column":4},"end":{"line":26,"column":null}},"type":"if","locations":[{"start":{"line":19,"column":4},"end":{"line":26,"column":null}},{"start":{},"end":{}}],"line":19},"2":{"loc":{"start":{"line":19,"column":8},"end":{"line":19,"column":36}},"type":"binary-expr","locations":[{"start":{"line":19,"column":8},"end":{"line":19,"column":22}},{"start":{"line":19,"column":22},"end":{"line":19,"column":36}}],"line":19},"3":{"loc":{"start":{"line":22,"column":13},"end":{"line":22,"column":42}},"type":"binary-expr","locations":[{"start":{"line":22,"column":13},"end":{"line":22,"column":24}},{"start":{"line":22,"column":24},"end":{"line":22,"column":42}}],"line":22},"4":{"loc":{"start":{"line":29,"column":4},"end":{"line":41,"column":null}},"type":"if","locations":[{"start":{"line":29,"column":4},"end":{"line":41,"column":null}},{"start":{},"end":{}}],"line":29},"5":{"loc":{"start":{"line":29,"column":8},"end":{"line":29,"column":36}},"type":"binary-expr","locations":[{"start":{"line":29,"column":8},"end":{"line":29,"column":22}},{"start":{"line":29,"column":22},"end":{"line":29,"column":36}}],"line":29},"6":{"loc":{"start":{"line":32,"column":13},"end":{"line":32,"column":82}},"type":"binary-expr","locations":[{"start":{"line":32,"column":13},"end":{"line":32,"column":24}},{"start":{"line":32,"column":24},"end":{"line":32,"column":82}}],"line":32},"7":{"loc":{"start":{"line":32,"column":26},"end":{"line":32,"column":82}},"type":"binary-expr","locations":[{"start":{"line":32,"column":26},"end":{"line":32,"column":45}},{"start":{"line":32,"column":45},"end":{"line":32,"column":60}},{"start":{"line":32,"column":60},"end":{"line":32,"column":82}}],"line":32},"8":{"loc":{"start":{"line":33,"column":20},"end":{"line":33,"column":null}},"type":"cond-expr","locations":[{"start":{"line":33,"column":39},"end":{"line":33,"column":46}},{"start":{"line":33,"column":46},"end":{"line":33,"column":null}}],"line":33},"9":{"loc":{"start":{"line":36,"column":6},"end":{"line":39,"column":null}},"type":"if","locations":[{"start":{"line":36,"column":6},"end":{"line":39,"column":null}},{"start":{},"end":{}}],"line":36},"10":{"loc":{"start":{"line":45,"column":4},"end":{"line":65,"column":null}},"type":"if","locations":[{"start":{"line":45,"column":4},"end":{"line":65,"column":null}},{"start":{},"end":{}}],"line":45},"11":{"loc":{"start":{"line":45,"column":8},"end":{"line":45,"column":34}},"type":"binary-expr","locations":[{"start":{"line":45,"column":8},"end":{"line":45,"column":22}},{"start":{"line":45,"column":22},"end":{"line":45,"column":34}}],"line":45},"12":{"loc":{"start":{"line":49,"column":13},"end":{"line":49,"column":43}},"type":"binary-expr","locations":[{"start":{"line":49,"column":13},"end":{"line":49,"column":24}},{"start":{"line":49,"column":24},"end":{"line":49,"column":43}}],"line":49},"13":{"loc":{"start":{"line":50,"column":8},"end":{"line":58,"column":null}},"type":"if","locations":[{"start":{"line":50,"column":8},"end":{"line":58,"column":null}},{"start":{"line":55,"column":15},"end":{"line":58,"column":null}}],"line":50},"14":{"loc":{"start":{"line":50,"column":12},"end":{"line":50,"column":45}},"type":"binary-expr","locations":[{"start":{"line":50,"column":12},"end":{"line":50,"column":32}},{"start":{"line":50,"column":32},"end":{"line":50,"column":45}}],"line":50},"15":{"loc":{"start":{"line":60,"column":6},"end":{"line":63,"column":null}},"type":"if","locations":[{"start":{"line":60,"column":6},"end":{"line":63,"column":null}},{"start":{},"end":{}}],"line":60},"16":{"loc":{"start":{"line":68,"column":4},"end":{"line":98,"column":null}},"type":"if","locations":[{"start":{"line":68,"column":4},"end":{"line":98,"column":null}},{"start":{},"end":{}}],"line":68},"17":{"loc":{"start":{"line":73,"column":8},"end":{"line":95,"column":null}},"type":"if","locations":[{"start":{"line":73,"column":8},"end":{"line":95,"column":null}},{"start":{"line":78,"column":8},"end":{"line":95,"column":null}}],"line":73},"18":{"loc":{"start":{"line":73,"column":12},"end":{"line":73,"column":45}},"type":"binary-expr","locations":[{"start":{"line":73,"column":12},"end":{"line":73,"column":32}},{"start":{"line":73,"column":32},"end":{"line":73,"column":45}}],"line":73},"19":{"loc":{"start":{"line":78,"column":8},"end":{"line":95,"column":null}},"type":"if","locations":[{"start":{"line":78,"column":8},"end":{"line":95,"column":null}},{"start":{"line":84,"column":8},"end":{"line":95,"column":null}}],"line":78},"20":{"loc":{"start":{"line":78,"column":19},"end":{"line":78,"column":74}},"type":"binary-expr","locations":[{"start":{"line":78,"column":19},"end":{"line":78,"column":38}},{"start":{"line":78,"column":38},"end":{"line":78,"column":53}},{"start":{"line":78,"column":53},"end":{"line":78,"column":74}}],"line":78},"21":{"loc":{"start":{"line":84,"column":8},"end":{"line":95,"column":null}},"type":"if","locations":[{"start":{"line":84,"column":8},"end":{"line":95,"column":null}},{"start":{"line":88,"column":8},"end":{"line":95,"column":null}}],"line":84},"22":{"loc":{"start":{"line":84,"column":19},"end":{"line":84,"column":49}},"type":"binary-expr","locations":[{"start":{"line":84,"column":19},"end":{"line":84,"column":38}},{"start":{"line":84,"column":38},"end":{"line":84,"column":49}}],"line":84},"23":{"loc":{"start":{"line":88,"column":8},"end":{"line":95,"column":null}},"type":"if","locations":[{"start":{"line":88,"column":8},"end":{"line":95,"column":null}},{"start":{"line":92,"column":15},"end":{"line":95,"column":null}}],"line":88},"24":{"loc":{"start":{"line":88,"column":19},"end":{"line":88,"column":51}},"type":"binary-expr","locations":[{"start":{"line":88,"column":19},"end":{"line":88,"column":38}},{"start":{"line":88,"column":38},"end":{"line":88,"column":51}}],"line":88}},"s":{"0":95,"1":95,"2":95,"3":95,"4":3716,"5":3716,"6":3716,"7":3,"8":3,"9":3,"10":70,"11":3,"12":3713,"13":6,"14":6,"15":6,"16":174,"17":174,"18":6,"19":5,"20":5,"21":6,"22":3707,"23":98,"24":98,"25":98,"26":98,"27":551,"28":1,"29":1,"30":1,"31":1,"32":550,"33":550,"34":98,"35":97,"36":97,"37":98,"38":3609,"39":5,"40":5,"41":5,"42":5,"43":106,"44":1,"45":1,"46":1,"47":1,"48":105,"49":2,"50":2,"51":2,"52":2,"53":2,"54":103,"55":2,"56":2,"57":2,"58":101,"59":5,"60":5,"61":5,"62":96,"63":96,"64":5,"65":3604,"66":3604,"67":95,"68":3,"69":3,"70":3,"71":3,"72":84,"73":84,"74":84,"75":65,"76":84,"77":2,"78":84,"79":5,"80":84,"81":4,"82":84},"f":{"0":95,"1":84},"b":{"0":[3623,93],"1":[3,3713],"2":[3716,9],"3":[3,73],"4":[6,3707],"5":[3713,6],"6":[6,179],"7":[179,7,7],"8":[4,170],"9":[5,1],"10":[98,3609],"11":[3707,3679],"12":[98,648],"13":[1,550],"14":[551,1],"15":[97,1],"16":[5,3604],"17":[1,105],"18":[106,1],"19":[2,103],"20":[105,2,2],"21":[2,101],"22":[103,3],"23":[5,96],"24":[101,5]},"meta":{"lastBranch":25,"lastFunction":2,"lastStatement":83,"seen":{"f:9:16:9:30":0,"s:10:14:10:Infinity":0,"s:11:27:11:Infinity":1,"s:12:10:12:Infinity":2,"s:14:2:103:Infinity":3,"s:15:15:15:Infinity":4,"s:16:17:16:Infinity":5,"b:16:31:16:45:16:45:16:Infinity":0,"b:19:4:26:Infinity:undefined:undefined:undefined:undefined":1,"s:19:4:26:Infinity":6,"b:19:8:19:22:19:22:19:36":2,"s:20:6:20:Infinity":7,"s:21:6:21:Infinity":8,"s:22:6:24:Infinity":9,"b:22:13:22:24:22:24:22:42":3,"s:23:8:23:Infinity":10,"s:25:6:25:Infinity":11,"b:29:4:41:Infinity:undefined:undefined:undefined:undefined":4,"s:29:4:41:Infinity":12,"b:29:8:29:22:29:22:29:36":5,"s:30:6:30:Infinity":13,"s:31:6:31:Infinity":14,"s:32:6:35:Infinity":15,"b:32:13:32:24:32:24:32:82":6,"b:32:26:32:45:32:45:32:60:32:60:32:82":7,"s:33:8:33:Infinity":16,"b:33:39:33:46:33:46:33:Infinity":8,"s:34:8:34:Infinity":17,"b:36:6:39:Infinity:undefined:undefined:undefined:undefined":9,"s:36:6:39:Infinity":18,"s:37:8:37:Infinity":19,"s:38:8:38:Infinity":20,"s:40:6:40:Infinity":21,"b:45:4:65:Infinity:undefined:undefined:undefined:undefined":10,"s:45:4:65:Infinity":22,"b:45:8:45:22:45:22:45:34":11,"s:46:20:46:Infinity":23,"s:47:6:47:Infinity":24,"s:48:6:48:Infinity":25,"s:49:6:59:Infinity":26,"b:49:13:49:24:49:24:49:43":12,"b:50:8:58:Infinity:55:15:58:Infinity":13,"s:50:8:58:Infinity":27,"b:50:12:50:32:50:32:50:45":14,"s:51:10:51:Infinity":28,"s:52:10:52:Infinity":29,"s:53:10:53:Infinity":30,"s:54:10:54:Infinity":31,"s:56:10:56:Infinity":32,"s:57:10:57:Infinity":33,"b:60:6:63:Infinity:undefined:undefined:undefined:undefined":15,"s:60:6:63:Infinity":34,"s:61:8:61:Infinity":35,"s:62:8:62:Infinity":36,"s:64:6:64:Infinity":37,"b:68:4:98:Infinity:undefined:undefined:undefined:undefined":16,"s:68:4:98:Infinity":38,"s:69:6:69:Infinity":39,"s:70:6:70:Infinity":40,"s:71:18:71:Infinity":41,"s:72:6:96:Infinity":42,"b:73:8:95:Infinity:78:8:95:Infinity":17,"s:73:8:95:Infinity":43,"b:73:12:73:32:73:32:73:45":18,"s:74:10:74:Infinity":44,"s:75:10:75:Infinity":45,"s:76:10:76:Infinity":46,"s:77:10:77:Infinity":47,"b:78:8:95:Infinity:84:8:95:Infinity":19,"s:78:8:95:Infinity":48,"b:78:19:78:38:78:38:78:53:78:53:78:74":20,"s:79:10:79:Infinity":49,"s:80:10:80:Infinity":50,"s:81:10:81:Infinity":51,"s:82:10:82:Infinity":52,"s:83:10:83:Infinity":53,"b:84:8:95:Infinity:88:8:95:Infinity":21,"s:84:8:95:Infinity":54,"b:84:19:84:38:84:38:84:49":22,"s:85:10:85:Infinity":55,"s:86:10:86:Infinity":56,"s:87:10:87:Infinity":57,"b:88:8:95:Infinity:92:15:95:Infinity":23,"s:88:8:95:Infinity":58,"b:88:19:88:38:88:38:88:51":24,"s:89:10:89:Infinity":59,"s:90:10:90:Infinity":60,"s:91:10:91:Infinity":61,"s:93:10:93:Infinity":62,"s:94:10:94:Infinity":63,"s:97:6:97:Infinity":64,"s:101:4:101:Infinity":65,"s:102:4:102:Infinity":66,"s:105:2:105:Infinity":67,"s:109:15:109:Infinity":68,"s:110:21:110:Infinity":69,"s:111:18:111:Infinity":70,"s:112:18:112:Infinity":71,"f:122:16:122:28":1,"s:123:19:123:Infinity":72,"s:124:21:124:Infinity":73,"s:126:2:126:Infinity":74,"s:126:45:126:Infinity":75,"s:127:2:127:Infinity":76,"s:127:51:127:Infinity":77,"s:128:2:128:Infinity":78,"s:128:48:128:Infinity":79,"s:129:2:129:Infinity":80,"s:129:48:129:Infinity":81,"s:131:2:131:Infinity":82}}} +,"/Users/alexgrozav/conductor/workspaces/importree/new-york/src/walker.ts": {"path":"/Users/alexgrozav/conductor/workspaces/importree/new-york/src/walker.ts","statementMap":{"0":{"start":{"line":12,"column":8},"end":{"line":12,"column":null}},"1":{"start":{"line":13,"column":18},"end":{"line":13,"column":null}},"2":{"start":{"line":14,"column":8},"end":{"line":14,"column":null}},"3":{"start":{"line":16,"column":42},"end":{"line":16,"column":null}},"4":{"start":{"line":17,"column":20},"end":{"line":17,"column":null}},"5":{"start":{"line":18,"column":18},"end":{"line":18,"column":null}},"6":{"start":{"line":21,"column":4},"end":{"line":21,"column":null}},"7":{"start":{"line":21,"column":31},"end":{"line":21,"column":null}},"8":{"start":{"line":22,"column":4},"end":{"line":22,"column":null}},"9":{"start":{"line":24,"column":20},"end":{"line":24,"column":null}},"10":{"start":{"line":25,"column":10},"end":{"line":25,"column":null}},"11":{"start":{"line":27,"column":32},"end":{"line":27,"column":null}},"12":{"start":{"line":28,"column":4},"end":{"line":37,"column":null}},"13":{"start":{"line":29,"column":23},"end":{"line":29,"column":null}},"14":{"start":{"line":30,"column":6},"end":{"line":30,"column":null}},"15":{"start":{"line":30,"column":21},"end":{"line":30,"column":null}},"16":{"start":{"line":32,"column":6},"end":{"line":36,"column":null}},"17":{"start":{"line":33,"column":8},"end":{"line":33,"column":null}},"18":{"start":{"line":34,"column":6},"end":{"line":36,"column":null}},"19":{"start":{"line":35,"column":8},"end":{"line":35,"column":null}},"20":{"start":{"line":39,"column":4},"end":{"line":39,"column":null}},"21":{"start":{"line":41,"column":4},"end":{"line":41,"column":null}},"22":{"start":{"line":41,"column":45},"end":{"line":41,"column":55}},"23":{"start":{"line":44,"column":2},"end":{"line":44,"column":null}},"24":{"start":{"line":47,"column":49},"end":{"line":47,"column":null}},"25":{"start":{"line":48,"column":2},"end":{"line":50,"column":null}},"26":{"start":{"line":49,"column":4},"end":{"line":49,"column":null}},"27":{"start":{"line":51,"column":2},"end":{"line":56,"column":null}},"28":{"start":{"line":52,"column":4},"end":{"line":55,"column":null}},"29":{"start":{"line":53,"column":6},"end":{"line":53,"column":null}},"30":{"start":{"line":53,"column":30},"end":{"line":53,"column":null}},"31":{"start":{"line":54,"column":6},"end":{"line":54,"column":null}},"32":{"start":{"line":58,"column":2},"end":{"line":64,"column":null}}},"fnMap":{"0":{"name":"walk","decl":{"start":{"line":11,"column":22},"end":{"line":11,"column":27}},"loc":{"start":{"line":11,"column":94},"end":{"line":65,"column":null}},"line":11},"1":{"name":"visit","decl":{"start":{"line":20,"column":17},"end":{"line":20,"column":23}},"loc":{"start":{"line":20,"column":56},"end":{"line":42,"column":null}},"line":20},"2":{"name":"(anonymous_2)","decl":{"start":{"line":41,"column":36},"end":{"line":41,"column":37}},"loc":{"start":{"line":41,"column":45},"end":{"line":41,"column":55}},"line":41}},"branchMap":{"0":{"loc":{"start":{"line":13,"column":18},"end":{"line":13,"column":null}},"type":"cond-expr","locations":[{"start":{"line":13,"column":26},"end":{"line":13,"column":63}},{"start":{"line":13,"column":63},"end":{"line":13,"column":null}}],"line":13},"1":{"loc":{"start":{"line":21,"column":4},"end":{"line":21,"column":null}},"type":"if","locations":[{"start":{"line":21,"column":4},"end":{"line":21,"column":null}},{"start":{},"end":{}}],"line":21},"2":{"loc":{"start":{"line":30,"column":6},"end":{"line":30,"column":null}},"type":"if","locations":[{"start":{"line":30,"column":6},"end":{"line":30,"column":null}},{"start":{},"end":{}}],"line":30},"3":{"loc":{"start":{"line":32,"column":6},"end":{"line":36,"column":null}},"type":"if","locations":[{"start":{"line":32,"column":6},"end":{"line":36,"column":null}},{"start":{"line":34,"column":6},"end":{"line":36,"column":null}}],"line":32},"4":{"loc":{"start":{"line":32,"column":10},"end":{"line":32,"column":62}},"type":"binary-expr","locations":[{"start":{"line":32,"column":10},"end":{"line":32,"column":42}},{"start":{"line":32,"column":42},"end":{"line":32,"column":62}}],"line":32},"5":{"loc":{"start":{"line":34,"column":6},"end":{"line":36,"column":null}},"type":"if","locations":[{"start":{"line":34,"column":6},"end":{"line":36,"column":null}},{"start":{},"end":{}}],"line":34},"6":{"loc":{"start":{"line":34,"column":17},"end":{"line":34,"column":69}},"type":"binary-expr","locations":[{"start":{"line":34,"column":17},"end":{"line":34,"column":46}},{"start":{"line":34,"column":46},"end":{"line":34,"column":69}}],"line":34},"7":{"loc":{"start":{"line":53,"column":6},"end":{"line":53,"column":null}},"type":"if","locations":[{"start":{"line":53,"column":6},"end":{"line":53,"column":null}},{"start":{},"end":{}}],"line":53}},"s":{"0":24,"1":24,"2":24,"3":24,"4":24,"5":24,"6":72,"7":5,"8":67,"9":67,"10":67,"11":67,"12":67,"13":53,"14":53,"15":2,"16":51,"17":3,"18":48,"19":48,"20":67,"21":67,"22":48,"23":24,"24":24,"25":24,"26":67,"27":24,"28":67,"29":48,"30":0,"31":48,"32":24},"f":{"0":24,"1":72,"2":48},"b":{"0":[1,23],"1":[5,67],"2":[2,51],"3":[3,48],"4":[51,3],"5":[48,0],"6":[48,48],"7":[0,48]},"meta":{"lastBranch":8,"lastFunction":3,"lastStatement":33,"seen":{"f:11:22:11:27":0,"s:12:8:12:Infinity":0,"s:13:18:13:Infinity":1,"b:13:26:13:63:13:63:13:Infinity":0,"s:14:8:14:Infinity":2,"s:16:42:16:Infinity":3,"s:17:20:17:Infinity":4,"s:18:18:18:Infinity":5,"f:20:17:20:23":1,"b:21:4:21:Infinity:undefined:undefined:undefined:undefined":1,"s:21:4:21:Infinity":6,"s:21:31:21:Infinity":7,"s:22:4:22:Infinity":8,"s:24:20:24:Infinity":9,"s:25:10:25:Infinity":10,"s:27:32:27:Infinity":11,"s:28:4:37:Infinity":12,"s:29:23:29:Infinity":13,"b:30:6:30:Infinity:undefined:undefined:undefined:undefined":2,"s:30:6:30:Infinity":14,"s:30:21:30:Infinity":15,"b:32:6:36:Infinity:34:6:36:Infinity":3,"s:32:6:36:Infinity":16,"b:32:10:32:42:32:42:32:62":4,"s:33:8:33:Infinity":17,"b:34:6:36:Infinity:undefined:undefined:undefined:undefined":5,"s:34:6:36:Infinity":18,"b:34:17:34:46:34:46:34:69":6,"s:35:8:35:Infinity":19,"s:39:4:39:Infinity":20,"s:41:4:41:Infinity":21,"f:41:36:41:37":2,"s:41:45:41:55":22,"s:44:2:44:Infinity":23,"s:47:49:47:Infinity":24,"s:48:2:50:Infinity":25,"s:49:4:49:Infinity":26,"s:51:2:56:Infinity":27,"s:52:4:55:Infinity":28,"b:53:6:53:Infinity:undefined:undefined:undefined:undefined":7,"s:53:6:53:Infinity":29,"s:53:30:53:Infinity":30,"s:54:6:54:Infinity":31,"s:58:2:64:Infinity":32}}} +} diff --git a/coverage/favicon.png b/coverage/favicon.png new file mode 100644 index 0000000..c1525b8 Binary files /dev/null and b/coverage/favicon.png differ diff --git a/coverage/index.html b/coverage/index.html new file mode 100644 index 0000000..c75d712 --- /dev/null +++ b/coverage/index.html @@ -0,0 +1,161 @@ + + + + + + Code coverage report for All files + + + + + + + + + +
+
+

All files

+
+ +
+ 99.45% + Statements + 181/182 +
+ + +
+ 97.41% + Branches + 113/116 +
+ + +
+ 100% + Functions + 15/15 +
+ + +
+ 100% + Lines + 168/168 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FileStatementsBranchesFunctionsLines
index.ts +
+
100%17/17100%8/8100%2/2100%15/15
resolver.ts +
+
100%49/4997.5%39/40100%8/8100%45/45
scanner.ts +
+
100%83/83100%52/52100%2/2100%79/79
walker.ts +
+
96.96%32/3387.5%14/16100%3/3100%29/29
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/index.ts.html b/coverage/index.ts.html new file mode 100644 index 0000000..d4a9c57 --- /dev/null +++ b/coverage/index.ts.html @@ -0,0 +1,259 @@ + + + + + + Code coverage report for index.ts + + + + + + + + + +
+
+

All files index.ts

+
+ +
+ 100% + Statements + 17/17 +
+ + +
+ 100% + Branches + 8/8 +
+ + +
+ 100% + Functions + 2/2 +
+ + +
+ 100% + Lines + 15/15 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +24x +  +  +  +  +  +  +  +  +  +  +9x +  +9x +  +8x +8x +  +8x +23x +23x +23x +  +22x +18x +15x +15x +  +  +  +  +8x +8x +  + 
import { resolve } from "node:path";
+import type { ImportreeOptions, ImportTree } from "./types.js";
+import { walk } from "./walker.js";
+ 
+export type { ImportreeOptions, ImportTree } from "./types.js";
+ 
+/**
+ * Builds a full import dependency tree starting from an entry file.
+ *
+ * Recursively resolves all static imports, dynamic imports, require() calls,
+ * and re-exports. Supports path aliases for custom resolution.
+ *
+ * @example
+ * ```ts
+ * const tree = await importree('./src/index.ts', {
+ *   aliases: { '@': './src' },
+ * });
+ *
+ * console.log(tree.files);      // all local dependency file paths
+ * console.log(tree.externals);  // external package names
+ * console.log(tree.graph);      // file → direct dependencies
+ * ```
+ */
+export async function importree(entry: string, options?: ImportreeOptions): Promise<ImportTree> {
+  return walk(entry, options ?? {});
+}
+ 
+/**
+ * Given an import tree and a changed file, returns all files that
+ * transitively depend on the changed file (i.e., files that would
+ * need to be re-evaluated if the changed file is modified).
+ *
+ * The changed file itself is NOT included in the result.
+ */
+export function getAffectedFiles(tree: ImportTree, changedFile: string): string[] {
+  const absolute = resolve(changedFile);
+ 
+  if (!tree.reverseGraph[absolute]) return [];
+ 
+  const affected = new Set<string>();
+  const queue = [absolute];
+ 
+  while (queue.length > 0) {
+    const current = queue.shift()!;
+    const dependents = tree.reverseGraph[current];
+    if (!dependents) continue;
+ 
+    for (const parent of dependents) {
+      if (!affected.has(parent)) {
+        affected.add(parent);
+        queue.push(parent);
+      }
+    }
+  }
+ 
+  affected.delete(absolute);
+  return [...affected].sort();
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/prettify.css b/coverage/prettify.css new file mode 100644 index 0000000..b317a7c --- /dev/null +++ b/coverage/prettify.css @@ -0,0 +1 @@ +.pln{color:#000}@media screen{.str{color:#080}.kwd{color:#008}.com{color:#800}.typ{color:#606}.lit{color:#066}.pun,.opn,.clo{color:#660}.tag{color:#008}.atn{color:#606}.atv{color:#080}.dec,.var{color:#606}.fun{color:red}}@media print,projection{.str{color:#060}.kwd{color:#006;font-weight:bold}.com{color:#600;font-style:italic}.typ{color:#404;font-weight:bold}.lit{color:#044}.pun,.opn,.clo{color:#440}.tag{color:#006;font-weight:bold}.atn{color:#404}.atv{color:#060}}pre.prettyprint{padding:2px;border:1px solid #888}ol.linenums{margin-top:0;margin-bottom:0}li.L0,li.L1,li.L2,li.L3,li.L5,li.L6,li.L7,li.L8{list-style-type:none}li.L1,li.L3,li.L5,li.L7,li.L9{background:#eee} diff --git a/coverage/prettify.js b/coverage/prettify.js new file mode 100644 index 0000000..b322523 --- /dev/null +++ b/coverage/prettify.js @@ -0,0 +1,2 @@ +/* eslint-disable */ +window.PR_SHOULD_USE_CONTINUATION=true;(function(){var h=["break,continue,do,else,for,if,return,while"];var u=[h,"auto,case,char,const,default,double,enum,extern,float,goto,int,long,register,short,signed,sizeof,static,struct,switch,typedef,union,unsigned,void,volatile"];var p=[u,"catch,class,delete,false,import,new,operator,private,protected,public,this,throw,true,try,typeof"];var l=[p,"alignof,align_union,asm,axiom,bool,concept,concept_map,const_cast,constexpr,decltype,dynamic_cast,explicit,export,friend,inline,late_check,mutable,namespace,nullptr,reinterpret_cast,static_assert,static_cast,template,typeid,typename,using,virtual,where"];var x=[p,"abstract,boolean,byte,extends,final,finally,implements,import,instanceof,null,native,package,strictfp,super,synchronized,throws,transient"];var R=[x,"as,base,by,checked,decimal,delegate,descending,dynamic,event,fixed,foreach,from,group,implicit,in,interface,internal,into,is,lock,object,out,override,orderby,params,partial,readonly,ref,sbyte,sealed,stackalloc,string,select,uint,ulong,unchecked,unsafe,ushort,var"];var r="all,and,by,catch,class,else,extends,false,finally,for,if,in,is,isnt,loop,new,no,not,null,of,off,on,or,return,super,then,true,try,unless,until,when,while,yes";var w=[p,"debugger,eval,export,function,get,null,set,undefined,var,with,Infinity,NaN"];var s="caller,delete,die,do,dump,elsif,eval,exit,foreach,for,goto,if,import,last,local,my,next,no,our,print,package,redo,require,sub,undef,unless,until,use,wantarray,while,BEGIN,END";var I=[h,"and,as,assert,class,def,del,elif,except,exec,finally,from,global,import,in,is,lambda,nonlocal,not,or,pass,print,raise,try,with,yield,False,True,None"];var f=[h,"alias,and,begin,case,class,def,defined,elsif,end,ensure,false,in,module,next,nil,not,or,redo,rescue,retry,self,super,then,true,undef,unless,until,when,yield,BEGIN,END"];var H=[h,"case,done,elif,esac,eval,fi,function,in,local,set,then,until"];var A=[l,R,w,s+I,f,H];var e=/^(DIR|FILE|vector|(de|priority_)?queue|list|stack|(const_)?iterator|(multi)?(set|map)|bitset|u?(int|float)\d*)/;var C="str";var z="kwd";var j="com";var O="typ";var G="lit";var L="pun";var F="pln";var m="tag";var E="dec";var J="src";var P="atn";var n="atv";var N="nocode";var M="(?:^^\\.?|[+-]|\\!|\\!=|\\!==|\\#|\\%|\\%=|&|&&|&&=|&=|\\(|\\*|\\*=|\\+=|\\,|\\-=|\\->|\\/|\\/=|:|::|\\;|<|<<|<<=|<=|=|==|===|>|>=|>>|>>=|>>>|>>>=|\\?|\\@|\\[|\\^|\\^=|\\^\\^|\\^\\^=|\\{|\\||\\|=|\\|\\||\\|\\|=|\\~|break|case|continue|delete|do|else|finally|instanceof|return|throw|try|typeof)\\s*";function k(Z){var ad=0;var S=false;var ac=false;for(var V=0,U=Z.length;V122)){if(!(al<65||ag>90)){af.push([Math.max(65,ag)|32,Math.min(al,90)|32])}if(!(al<97||ag>122)){af.push([Math.max(97,ag)&~32,Math.min(al,122)&~32])}}}}af.sort(function(av,au){return(av[0]-au[0])||(au[1]-av[1])});var ai=[];var ap=[NaN,NaN];for(var ar=0;arat[0]){if(at[1]+1>at[0]){an.push("-")}an.push(T(at[1]))}}an.push("]");return an.join("")}function W(al){var aj=al.source.match(new RegExp("(?:\\[(?:[^\\x5C\\x5D]|\\\\[\\s\\S])*\\]|\\\\u[A-Fa-f0-9]{4}|\\\\x[A-Fa-f0-9]{2}|\\\\[0-9]+|\\\\[^ux0-9]|\\(\\?[:!=]|[\\(\\)\\^]|[^\\x5B\\x5C\\(\\)\\^]+)","g"));var ah=aj.length;var an=[];for(var ak=0,am=0;ak=2&&ai==="["){aj[ak]=X(ag)}else{if(ai!=="\\"){aj[ak]=ag.replace(/[a-zA-Z]/g,function(ao){var ap=ao.charCodeAt(0);return"["+String.fromCharCode(ap&~32,ap|32)+"]"})}}}}return aj.join("")}var aa=[];for(var V=0,U=Z.length;V=0;){S[ac.charAt(ae)]=Y}}var af=Y[1];var aa=""+af;if(!ag.hasOwnProperty(aa)){ah.push(af);ag[aa]=null}}ah.push(/[\0-\uffff]/);V=k(ah)})();var X=T.length;var W=function(ah){var Z=ah.sourceCode,Y=ah.basePos;var ad=[Y,F];var af=0;var an=Z.match(V)||[];var aj={};for(var ae=0,aq=an.length;ae=5&&"lang-"===ap.substring(0,5);if(am&&!(ai&&typeof ai[1]==="string")){am=false;ap=J}if(!am){aj[ag]=ap}}var ab=af;af+=ag.length;if(!am){ad.push(Y+ab,ap)}else{var al=ai[1];var ak=ag.indexOf(al);var ac=ak+al.length;if(ai[2]){ac=ag.length-ai[2].length;ak=ac-al.length}var ar=ap.substring(5);B(Y+ab,ag.substring(0,ak),W,ad);B(Y+ab+ak,al,q(ar,al),ad);B(Y+ab+ac,ag.substring(ac),W,ad)}}ah.decorations=ad};return W}function i(T){var W=[],S=[];if(T.tripleQuotedStrings){W.push([C,/^(?:\'\'\'(?:[^\'\\]|\\[\s\S]|\'{1,2}(?=[^\']))*(?:\'\'\'|$)|\"\"\"(?:[^\"\\]|\\[\s\S]|\"{1,2}(?=[^\"]))*(?:\"\"\"|$)|\'(?:[^\\\']|\\[\s\S])*(?:\'|$)|\"(?:[^\\\"]|\\[\s\S])*(?:\"|$))/,null,"'\""])}else{if(T.multiLineStrings){W.push([C,/^(?:\'(?:[^\\\']|\\[\s\S])*(?:\'|$)|\"(?:[^\\\"]|\\[\s\S])*(?:\"|$)|\`(?:[^\\\`]|\\[\s\S])*(?:\`|$))/,null,"'\"`"])}else{W.push([C,/^(?:\'(?:[^\\\'\r\n]|\\.)*(?:\'|$)|\"(?:[^\\\"\r\n]|\\.)*(?:\"|$))/,null,"\"'"])}}if(T.verbatimStrings){S.push([C,/^@\"(?:[^\"]|\"\")*(?:\"|$)/,null])}var Y=T.hashComments;if(Y){if(T.cStyleComments){if(Y>1){W.push([j,/^#(?:##(?:[^#]|#(?!##))*(?:###|$)|.*)/,null,"#"])}else{W.push([j,/^#(?:(?:define|elif|else|endif|error|ifdef|include|ifndef|line|pragma|undef|warning)\b|[^\r\n]*)/,null,"#"])}S.push([C,/^<(?:(?:(?:\.\.\/)*|\/?)(?:[\w-]+(?:\/[\w-]+)+)?[\w-]+\.h|[a-z]\w*)>/,null])}else{W.push([j,/^#[^\r\n]*/,null,"#"])}}if(T.cStyleComments){S.push([j,/^\/\/[^\r\n]*/,null]);S.push([j,/^\/\*[\s\S]*?(?:\*\/|$)/,null])}if(T.regexLiterals){var X=("/(?=[^/*])(?:[^/\\x5B\\x5C]|\\x5C[\\s\\S]|\\x5B(?:[^\\x5C\\x5D]|\\x5C[\\s\\S])*(?:\\x5D|$))+/");S.push(["lang-regex",new RegExp("^"+M+"("+X+")")])}var V=T.types;if(V){S.push([O,V])}var U=(""+T.keywords).replace(/^ | $/g,"");if(U.length){S.push([z,new RegExp("^(?:"+U.replace(/[\s,]+/g,"|")+")\\b"),null])}W.push([F,/^\s+/,null," \r\n\t\xA0"]);S.push([G,/^@[a-z_$][a-z_$@0-9]*/i,null],[O,/^(?:[@_]?[A-Z]+[a-z][A-Za-z_$@0-9]*|\w+_t\b)/,null],[F,/^[a-z_$][a-z_$@0-9]*/i,null],[G,new RegExp("^(?:0x[a-f0-9]+|(?:\\d(?:_\\d+)*\\d*(?:\\.\\d*)?|\\.\\d\\+)(?:e[+\\-]?\\d+)?)[a-z]*","i"),null,"0123456789"],[F,/^\\[\s\S]?/,null],[L,/^.[^\s\w\.$@\'\"\`\/\#\\]*/,null]);return g(W,S)}var K=i({keywords:A,hashComments:true,cStyleComments:true,multiLineStrings:true,regexLiterals:true});function Q(V,ag){var U=/(?:^|\s)nocode(?:\s|$)/;var ab=/\r\n?|\n/;var ac=V.ownerDocument;var S;if(V.currentStyle){S=V.currentStyle.whiteSpace}else{if(window.getComputedStyle){S=ac.defaultView.getComputedStyle(V,null).getPropertyValue("white-space")}}var Z=S&&"pre"===S.substring(0,3);var af=ac.createElement("LI");while(V.firstChild){af.appendChild(V.firstChild)}var W=[af];function ae(al){switch(al.nodeType){case 1:if(U.test(al.className)){break}if("BR"===al.nodeName){ad(al);if(al.parentNode){al.parentNode.removeChild(al)}}else{for(var an=al.firstChild;an;an=an.nextSibling){ae(an)}}break;case 3:case 4:if(Z){var am=al.nodeValue;var aj=am.match(ab);if(aj){var ai=am.substring(0,aj.index);al.nodeValue=ai;var ah=am.substring(aj.index+aj[0].length);if(ah){var ak=al.parentNode;ak.insertBefore(ac.createTextNode(ah),al.nextSibling)}ad(al);if(!ai){al.parentNode.removeChild(al)}}}break}}function ad(ak){while(!ak.nextSibling){ak=ak.parentNode;if(!ak){return}}function ai(al,ar){var aq=ar?al.cloneNode(false):al;var ao=al.parentNode;if(ao){var ap=ai(ao,1);var an=al.nextSibling;ap.appendChild(aq);for(var am=an;am;am=an){an=am.nextSibling;ap.appendChild(am)}}return aq}var ah=ai(ak.nextSibling,0);for(var aj;(aj=ah.parentNode)&&aj.nodeType===1;){ah=aj}W.push(ah)}for(var Y=0;Y=S){ah+=2}if(V>=ap){Z+=2}}}var t={};function c(U,V){for(var S=V.length;--S>=0;){var T=V[S];if(!t.hasOwnProperty(T)){t[T]=U}else{if(window.console){console.warn("cannot override language handler %s",T)}}}}function q(T,S){if(!(T&&t.hasOwnProperty(T))){T=/^\s*]*(?:>|$)/],[j,/^<\!--[\s\S]*?(?:-\->|$)/],["lang-",/^<\?([\s\S]+?)(?:\?>|$)/],["lang-",/^<%([\s\S]+?)(?:%>|$)/],[L,/^(?:<[%?]|[%?]>)/],["lang-",/^]*>([\s\S]+?)<\/xmp\b[^>]*>/i],["lang-js",/^]*>([\s\S]*?)(<\/script\b[^>]*>)/i],["lang-css",/^]*>([\s\S]*?)(<\/style\b[^>]*>)/i],["lang-in.tag",/^(<\/?[a-z][^<>]*>)/i]]),["default-markup","htm","html","mxml","xhtml","xml","xsl"]);c(g([[F,/^[\s]+/,null," \t\r\n"],[n,/^(?:\"[^\"]*\"?|\'[^\']*\'?)/,null,"\"'"]],[[m,/^^<\/?[a-z](?:[\w.:-]*\w)?|\/?>$/i],[P,/^(?!style[\s=]|on)[a-z](?:[\w:-]*\w)?/i],["lang-uq.val",/^=\s*([^>\'\"\s]*(?:[^>\'\"\s\/]|\/(?=\s)))/],[L,/^[=<>\/]+/],["lang-js",/^on\w+\s*=\s*\"([^\"]+)\"/i],["lang-js",/^on\w+\s*=\s*\'([^\']+)\'/i],["lang-js",/^on\w+\s*=\s*([^\"\'>\s]+)/i],["lang-css",/^style\s*=\s*\"([^\"]+)\"/i],["lang-css",/^style\s*=\s*\'([^\']+)\'/i],["lang-css",/^style\s*=\s*([^\"\'>\s]+)/i]]),["in.tag"]);c(g([],[[n,/^[\s\S]+/]]),["uq.val"]);c(i({keywords:l,hashComments:true,cStyleComments:true,types:e}),["c","cc","cpp","cxx","cyc","m"]);c(i({keywords:"null,true,false"}),["json"]);c(i({keywords:R,hashComments:true,cStyleComments:true,verbatimStrings:true,types:e}),["cs"]);c(i({keywords:x,cStyleComments:true}),["java"]);c(i({keywords:H,hashComments:true,multiLineStrings:true}),["bsh","csh","sh"]);c(i({keywords:I,hashComments:true,multiLineStrings:true,tripleQuotedStrings:true}),["cv","py"]);c(i({keywords:s,hashComments:true,multiLineStrings:true,regexLiterals:true}),["perl","pl","pm"]);c(i({keywords:f,hashComments:true,multiLineStrings:true,regexLiterals:true}),["rb"]);c(i({keywords:w,cStyleComments:true,regexLiterals:true}),["js"]);c(i({keywords:r,hashComments:3,cStyleComments:true,multilineStrings:true,tripleQuotedStrings:true,regexLiterals:true}),["coffee"]);c(g([],[[C,/^[\s\S]+/]]),["regex"]);function d(V){var U=V.langExtension;try{var S=a(V.sourceNode);var T=S.sourceCode;V.sourceCode=T;V.spans=S.spans;V.basePos=0;q(U,T)(V);D(V)}catch(W){if("console" in window){console.log(W&&W.stack?W.stack:W)}}}function y(W,V,U){var S=document.createElement("PRE");S.innerHTML=W;if(U){Q(S,U)}var T={langExtension:V,numberLines:U,sourceNode:S};d(T);return S.innerHTML}function b(ad){function Y(af){return document.getElementsByTagName(af)}var ac=[Y("pre"),Y("code"),Y("xmp")];var T=[];for(var aa=0;aa=0){var ah=ai.match(ab);var am;if(!ah&&(am=o(aj))&&"CODE"===am.tagName){ah=am.className.match(ab)}if(ah){ah=ah[1]}var al=false;for(var ak=aj.parentNode;ak;ak=ak.parentNode){if((ak.tagName==="pre"||ak.tagName==="code"||ak.tagName==="xmp")&&ak.className&&ak.className.indexOf("prettyprint")>=0){al=true;break}}if(!al){var af=aj.className.match(/\blinenums\b(?::(\d+))?/);af=af?af[1]&&af[1].length?+af[1]:true:false;if(af){Q(aj,af)}S={langExtension:ah,sourceNode:aj,numberLines:af};d(S)}}}if(X]*(?:>|$)/],[PR.PR_COMMENT,/^<\!--[\s\S]*?(?:-\->|$)/],[PR.PR_PUNCTUATION,/^(?:<[%?]|[%?]>)/],["lang-",/^<\?([\s\S]+?)(?:\?>|$)/],["lang-",/^<%([\s\S]+?)(?:%>|$)/],["lang-",/^]*>([\s\S]+?)<\/xmp\b[^>]*>/i],["lang-handlebars",/^]*type\s*=\s*['"]?text\/x-handlebars-template['"]?\b[^>]*>([\s\S]*?)(<\/script\b[^>]*>)/i],["lang-js",/^]*>([\s\S]*?)(<\/script\b[^>]*>)/i],["lang-css",/^]*>([\s\S]*?)(<\/style\b[^>]*>)/i],["lang-in.tag",/^(<\/?[a-z][^<>]*>)/i],[PR.PR_DECLARATION,/^{{[#^>/]?\s*[\w.][^}]*}}/],[PR.PR_DECLARATION,/^{{&?\s*[\w.][^}]*}}/],[PR.PR_DECLARATION,/^{{{>?\s*[\w.][^}]*}}}/],[PR.PR_COMMENT,/^{{![^}]*}}/]]),["handlebars","hbs"]);PR.registerLangHandler(PR.createSimpleLexer([[PR.PR_PLAIN,/^[ \t\r\n\f]+/,null," \t\r\n\f"]],[[PR.PR_STRING,/^\"(?:[^\n\r\f\\\"]|\\(?:\r\n?|\n|\f)|\\[\s\S])*\"/,null],[PR.PR_STRING,/^\'(?:[^\n\r\f\\\']|\\(?:\r\n?|\n|\f)|\\[\s\S])*\'/,null],["lang-css-str",/^url\(([^\)\"\']*)\)/i],[PR.PR_KEYWORD,/^(?:url|rgb|\!important|@import|@page|@media|@charset|inherit)(?=[^\-\w]|$)/i,null],["lang-css-kw",/^(-?(?:[_a-z]|(?:\\[0-9a-f]+ ?))(?:[_a-z0-9\-]|\\(?:\\[0-9a-f]+ ?))*)\s*:/i],[PR.PR_COMMENT,/^\/\*[^*]*\*+(?:[^\/*][^*]*\*+)*\//],[PR.PR_COMMENT,/^(?:)/],[PR.PR_LITERAL,/^(?:\d+|\d*\.\d+)(?:%|[a-z]+)?/i],[PR.PR_LITERAL,/^#(?:[0-9a-f]{3}){1,2}/i],[PR.PR_PLAIN,/^-?(?:[_a-z]|(?:\\[\da-f]+ ?))(?:[_a-z\d\-]|\\(?:\\[\da-f]+ ?))*/i],[PR.PR_PUNCTUATION,/^[^\s\w\'\"]+/]]),["css"]);PR.registerLangHandler(PR.createSimpleLexer([],[[PR.PR_KEYWORD,/^-?(?:[_a-z]|(?:\\[\da-f]+ ?))(?:[_a-z\d\-]|\\(?:\\[\da-f]+ ?))*/i]]),["css-kw"]);PR.registerLangHandler(PR.createSimpleLexer([],[[PR.PR_STRING,/^[^\)\"\']+/]]),["css-str"]); diff --git a/coverage/resolver.ts.html b/coverage/resolver.ts.html new file mode 100644 index 0000000..e0513b9 --- /dev/null +++ b/coverage/resolver.ts.html @@ -0,0 +1,430 @@ + + + + + + Code coverage report for resolver.ts + + + + + + + + + +
+
+

All files resolver.ts

+
+ +
+ 100% + Statements + 49/49 +
+ + +
+ 97.5% + Branches + 39/40 +
+ + +
+ 100% + Functions + 8/8 +
+ + +
+ 100% + Lines + 45/45 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116  +  +  +  +3x +  +  +156x +156x +  +  +  +7x +7x +  +  +  +  +  +  +  +  +7x +2x +2x +  +5x +  +  +  +  +57x +  +  +56x +91x +91x +  +  +  +7x +3x +8x +8x +  +  +  +5x +  +  +  +  +  +  +  +  +  +  +  +38x +  +  +38x +1x +  +  +38x +7x +  +  +38x +  +38x +  +  +  +68x +68x +  +68x +  +  +  +  +64x +51x +51x +48x +  +  +  +  +13x +13x +6x +6x +6x +6x +4x +  +6x +6x +  +  +  +  +13x +7x +  +  +  +64x +64x +  +  + 
import { statSync } from "node:fs";
+import { dirname, join, resolve, isAbsolute } from "node:path";
+import type { ImportreeOptions, ResolvedImport } from "./types.js";
+ 
+const DEFAULT_EXTENSIONS = [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"];
+ 
+function fileExists(filePath: string): boolean {
+  const stat = statSync(filePath, { throwIfNoEntry: false });
+  return stat !== undefined && stat.isFile();
+}
+ 
+function dirExists(filePath: string): boolean {
+  const stat = statSync(filePath, { throwIfNoEntry: false });
+  return stat !== undefined && stat.isDirectory();
+}
+ 
+/**
+ * Extract the bare package name from an import specifier.
+ * - Scoped: `@scope/pkg/path` → `@scope/pkg`
+ * - Unscoped: `pkg/path` → `pkg`
+ */
+function getBareSpecifier(specifier: string): string {
+  if (specifier.startsWith("@")) {
+    const parts = specifier.split("/");
+    return parts.length >= 2 ? `${parts[0]}/${parts[1]}` : specifier;
+  }
+  return specifier.split("/")[0];
+}
+ 
+function resolveFile(filePath: string, extensions: string[]): string | undefined {
+  // Try exact path
+  if (fileExists(filePath)) return filePath;
+ 
+  // Try with each extension
+  for (const ext of extensions) {
+    const withExt = filePath + ext;
+    if (fileExists(withExt)) return withExt;
+  }
+ 
+  // Try as directory with index file
+  if (dirExists(filePath)) {
+    for (const ext of extensions) {
+      const indexPath = join(filePath, `index${ext}`);
+      if (fileExists(indexPath)) return indexPath;
+    }
+  }
+ 
+  return undefined;
+}
+ 
+export interface Resolver {
+  (specifier: string, fromFile: string): ResolvedImport | undefined;
+}
+ 
+/**
+ * Creates a resolver function that resolves import specifiers to absolute
+ * file paths, with support for aliases and extension probing.
+ */
+export function createResolver(basedir: string, options: ImportreeOptions): Resolver {
+  const extensions = options.extensions ?? DEFAULT_EXTENSIONS;
+ 
+  // Sort aliases by key length descending for longest-prefix matching
+  const aliases = options.aliases
+    ? Object.entries(options.aliases).sort((a, b) => b[0].length - a[0].length)
+    : [];
+ 
+  const resolvedAliasValues = aliases.map(
+    ([key, value]) => [key, isAbsolute(value) ? value : resolve(basedir, value)] as const,
+  );
+ 
+  const cache = new Map<string, ResolvedImport | undefined>();
+ 
+  return function resolveSpecifier(
+    specifier: string,
+    fromFile: string,
+  ): ResolvedImport | undefined {
+    const fromDir = dirname(fromFile);
+    const cacheKey = `${specifier}\0${fromDir}`;
+ 
+    if (cache.has(cacheKey)) return cache.get(cacheKey);
+ 
+    let result: ResolvedImport | undefined;
+ 
+    // Relative import
+    if (specifier.startsWith("./") || specifier.startsWith("../")) {
+      const absolutePath = resolveFile(resolve(fromDir, specifier), extensions);
+      if (absolutePath) {
+        result = { type: "local", absolutePath };
+      }
+    }
+    // Check aliases
+    else {
+      let matched = false;
+      for (const [prefix, replacement] of resolvedAliasValues) {
+        Eif (specifier === prefix || specifier.startsWith(prefix + "/")) {
+          const rest = specifier === prefix ? "" : specifier.slice(prefix.length);
+          const absolutePath = resolveFile(join(replacement, rest), extensions);
+          if (absolutePath) {
+            result = { type: "local", absolutePath };
+          }
+          matched = true;
+          break;
+        }
+      }
+ 
+      // Bare specifier → external
+      if (!matched) {
+        result = { type: "external", specifier: getBareSpecifier(specifier) };
+      }
+    }
+ 
+    cache.set(cacheKey, result);
+    return result;
+  };
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/scanner.ts.html b/coverage/scanner.ts.html new file mode 100644 index 0000000..2d020eb --- /dev/null +++ b/coverage/scanner.ts.html @@ -0,0 +1,481 @@ + + + + + + Code coverage report for scanner.ts + + + + + + + + + +
+
+

All files scanner.ts

+
+ +
+ 100% + Statements + 83/83 +
+ + +
+ 100% + Branches + 52/52 +
+ + +
+ 100% + Functions + 2/2 +
+ + +
+ 100% + Lines + 79/79 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133  +  +  +  +  +  +  +  +  +95x +95x +95x +  +95x +3716x +3716x +  +  +3716x +3x +3x +3x +70x +  +3x +  +  +  +3713x +6x +6x +6x +174x +174x +  +6x +5x +5x +  +6x +  +  +  +  +3707x +98x +98x +98x +98x +551x +1x +1x +1x +1x +  +550x +550x +  +  +98x +97x +97x +  +98x +  +  +  +3609x +5x +5x +5x +5x +106x +1x +1x +1x +1x +105x +2x +2x +2x +2x +2x +103x +2x +2x +2x +101x +5x +5x +5x +  +96x +96x +  +  +5x +  +  +  +3604x +3604x +  +  +95x +  +  +  +3x +3x +3x +3x +  +  +  +  +  +  +  +  +  +  +84x +84x +  +84x +84x +84x +84x +  +84x +  + 
/**
+ * Strips comments from source code while preserving string literals.
+ *
+ * Comments are replaced with spaces (preserving newlines). Strings and
+ * template literals are left intact so that import specifiers inside
+ * `from 'specifier'` remain extractable. The function correctly handles
+ * comment-like sequences inside strings (e.g., `'//'` won't start a comment).
+ */
+export function stripComments(code: string): string {
+  const len = code.length;
+  const result: string[] = new Array(len);
+  let i = 0;
+ 
+  while (i < len) {
+    const ch = code[i];
+    const next = i + 1 < len ? code[i + 1] : "";
+ 
+    // Line comment → blank to end of line
+    if (ch === "/" && next === "/") {
+      result[i++] = " ";
+      result[i++] = " ";
+      while (i < len && code[i] !== "\n") {
+        result[i++] = " ";
+      }
+      continue;
+    }
+ 
+    // Block comment → blank to closing */
+    if (ch === "/" && next === "*") {
+      result[i++] = " ";
+      result[i++] = " ";
+      while (i < len && !(code[i] === "*" && i + 1 < len && code[i + 1] === "/")) {
+        result[i] = code[i] === "\n" ? "\n" : " ";
+        i++;
+      }
+      if (i < len) {
+        result[i++] = " "; // *
+        result[i++] = " "; // /
+      }
+      continue;
+    }
+ 
+    // Single or double quoted string — copy verbatim (skip past to avoid
+    // misidentifying comment markers inside strings)
+    if (ch === "'" || ch === '"') {
+      const quote = ch;
+      result[i] = code[i];
+      i++;
+      while (i < len && code[i] !== quote) {
+        if (code[i] === "\\" && i + 1 < len) {
+          result[i] = code[i];
+          i++;
+          result[i] = code[i];
+          i++;
+        } else {
+          result[i] = code[i];
+          i++;
+        }
+      }
+      if (i < len) {
+        result[i] = code[i];
+        i++;
+      }
+      continue;
+    }
+ 
+    // Template literal — copy verbatim, handling ${} nesting
+    if (ch === "`") {
+      result[i] = code[i];
+      i++;
+      let depth = 0;
+      while (i < len) {
+        if (code[i] === "\\" && i + 1 < len) {
+          result[i] = code[i];
+          i++;
+          result[i] = code[i];
+          i++;
+        } else if (code[i] === "$" && i + 1 < len && code[i + 1] === "{") {
+          result[i] = code[i];
+          i++;
+          result[i] = code[i];
+          i++;
+          depth++;
+        } else if (code[i] === "}" && depth > 0) {
+          result[i] = code[i];
+          i++;
+          depth--;
+        } else if (code[i] === "`" && depth === 0) {
+          result[i] = code[i];
+          i++;
+          break;
+        } else {
+          result[i] = code[i];
+          i++;
+        }
+      }
+      continue;
+    }
+ 
+    // Regular character
+    result[i] = ch;
+    i++;
+  }
+ 
+  return result.join("");
+}
+ 
+// Static regex patterns — compiled once
+const fromRe = /\bfrom\s+['"]([^'"]+)['"]/g;
+const sideEffectRe = /\bimport\s+['"]([^'"]+)['"]/g;
+const dynamicRe = /\bimport\s*\(\s*['"]([^'"]+)['"]\s*\)/g;
+const requireRe = /\brequire\s*\(\s*['"]([^'"]+)['"]\s*\)/g;
+ 
+/**
+ * Scans source code and extracts all import/require specifiers.
+ *
+ * Handles: static imports, dynamic imports, require(), re-exports.
+ * Ignores imports inside comments. Imports inside string literals may
+ * produce false positives, but unresolvable paths are silently skipped
+ * by the resolver.
+ */
+export function scanImports(code: string): string[] {
+  const stripped = stripComments(code);
+  const specifiers = new Set<string>();
+ 
+  for (const m of stripped.matchAll(fromRe)) specifiers.add(m[1]);
+  for (const m of stripped.matchAll(sideEffectRe)) specifiers.add(m[1]);
+  for (const m of stripped.matchAll(dynamicRe)) specifiers.add(m[1]);
+  for (const m of stripped.matchAll(requireRe)) specifiers.add(m[1]);
+ 
+  return [...specifiers];
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/sort-arrow-sprite.png b/coverage/sort-arrow-sprite.png new file mode 100644 index 0000000..6ed6831 Binary files /dev/null and b/coverage/sort-arrow-sprite.png differ diff --git a/coverage/sorter.js b/coverage/sorter.js new file mode 100644 index 0000000..4ed70ae --- /dev/null +++ b/coverage/sorter.js @@ -0,0 +1,210 @@ +/* eslint-disable */ +var addSorting = (function() { + 'use strict'; + var cols, + currentSort = { + index: 0, + desc: false + }; + + // returns the summary table element + function getTable() { + return document.querySelector('.coverage-summary'); + } + // returns the thead element of the summary table + function getTableHeader() { + return getTable().querySelector('thead tr'); + } + // returns the tbody element of the summary table + function getTableBody() { + return getTable().querySelector('tbody'); + } + // returns the th element for nth column + function getNthColumn(n) { + return getTableHeader().querySelectorAll('th')[n]; + } + + function onFilterInput() { + const searchValue = document.getElementById('fileSearch').value; + const rows = document.getElementsByTagName('tbody')[0].children; + + // Try to create a RegExp from the searchValue. If it fails (invalid regex), + // it will be treated as a plain text search + let searchRegex; + try { + searchRegex = new RegExp(searchValue, 'i'); // 'i' for case-insensitive + } catch (error) { + searchRegex = null; + } + + for (let i = 0; i < rows.length; i++) { + const row = rows[i]; + let isMatch = false; + + if (searchRegex) { + // If a valid regex was created, use it for matching + isMatch = searchRegex.test(row.textContent); + } else { + // Otherwise, fall back to the original plain text search + isMatch = row.textContent + .toLowerCase() + .includes(searchValue.toLowerCase()); + } + + row.style.display = isMatch ? '' : 'none'; + } + } + + // loads the search box + function addSearchBox() { + var template = document.getElementById('filterTemplate'); + var templateClone = template.content.cloneNode(true); + templateClone.getElementById('fileSearch').oninput = onFilterInput; + template.parentElement.appendChild(templateClone); + } + + // loads all columns + function loadColumns() { + var colNodes = getTableHeader().querySelectorAll('th'), + colNode, + cols = [], + col, + i; + + for (i = 0; i < colNodes.length; i += 1) { + colNode = colNodes[i]; + col = { + key: colNode.getAttribute('data-col'), + sortable: !colNode.getAttribute('data-nosort'), + type: colNode.getAttribute('data-type') || 'string' + }; + cols.push(col); + if (col.sortable) { + col.defaultDescSort = col.type === 'number'; + colNode.innerHTML = + colNode.innerHTML + ''; + } + } + return cols; + } + // attaches a data attribute to every tr element with an object + // of data values keyed by column name + function loadRowData(tableRow) { + var tableCols = tableRow.querySelectorAll('td'), + colNode, + col, + data = {}, + i, + val; + for (i = 0; i < tableCols.length; i += 1) { + colNode = tableCols[i]; + col = cols[i]; + val = colNode.getAttribute('data-value'); + if (col.type === 'number') { + val = Number(val); + } + data[col.key] = val; + } + return data; + } + // loads all row data + function loadData() { + var rows = getTableBody().querySelectorAll('tr'), + i; + + for (i = 0; i < rows.length; i += 1) { + rows[i].data = loadRowData(rows[i]); + } + } + // sorts the table using the data for the ith column + function sortByIndex(index, desc) { + var key = cols[index].key, + sorter = function(a, b) { + a = a.data[key]; + b = b.data[key]; + return a < b ? -1 : a > b ? 1 : 0; + }, + finalSorter = sorter, + tableBody = document.querySelector('.coverage-summary tbody'), + rowNodes = tableBody.querySelectorAll('tr'), + rows = [], + i; + + if (desc) { + finalSorter = function(a, b) { + return -1 * sorter(a, b); + }; + } + + for (i = 0; i < rowNodes.length; i += 1) { + rows.push(rowNodes[i]); + tableBody.removeChild(rowNodes[i]); + } + + rows.sort(finalSorter); + + for (i = 0; i < rows.length; i += 1) { + tableBody.appendChild(rows[i]); + } + } + // removes sort indicators for current column being sorted + function removeSortIndicators() { + var col = getNthColumn(currentSort.index), + cls = col.className; + + cls = cls.replace(/ sorted$/, '').replace(/ sorted-desc$/, ''); + col.className = cls; + } + // adds sort indicators for current column being sorted + function addSortIndicators() { + getNthColumn(currentSort.index).className += currentSort.desc + ? ' sorted-desc' + : ' sorted'; + } + // adds event listeners for all sorter widgets + function enableUI() { + var i, + el, + ithSorter = function ithSorter(i) { + var col = cols[i]; + + return function() { + var desc = col.defaultDescSort; + + if (currentSort.index === i) { + desc = !currentSort.desc; + } + sortByIndex(i, desc); + removeSortIndicators(); + currentSort.index = i; + currentSort.desc = desc; + addSortIndicators(); + }; + }; + for (i = 0; i < cols.length; i += 1) { + if (cols[i].sortable) { + // add the click event handler on the th so users + // dont have to click on those tiny arrows + el = getNthColumn(i).querySelector('.sorter').parentElement; + if (el.addEventListener) { + el.addEventListener('click', ithSorter(i)); + } else { + el.attachEvent('onclick', ithSorter(i)); + } + } + } + } + // adds sorting functionality to the UI + return function() { + if (!getTable()) { + return; + } + cols = loadColumns(); + loadData(); + addSearchBox(); + addSortIndicators(); + enableUI(); + }; +})(); + +window.addEventListener('load', addSorting); diff --git a/coverage/walker.ts.html b/coverage/walker.ts.html new file mode 100644 index 0000000..0ca63c1 --- /dev/null +++ b/coverage/walker.ts.html @@ -0,0 +1,280 @@ + + + + + + Code coverage report for walker.ts + + + + + + + + + +
+
+

All files walker.ts

+
+ +
+ 96.96% + Statements + 32/33 +
+ + +
+ 87.5% + Branches + 14/16 +
+ + +
+ 100% + Functions + 3/3 +
+ + +
+ 100% + Lines + 29/29 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66  +  +  +  +  +  +  +  +  +  +  +24x +24x +24x +  +24x +24x +24x +  +  +72x +67x +  +67x +67x +  +67x +67x +53x +53x +  +51x +3x +48x +48x +  +  +  +67x +  +67x +  +  +24x +  +  +24x +24x +67x +  +24x +67x +48x +48x +  +  +  +24x +  +  +  +  +  +  +  + 
import { readFile } from "node:fs/promises";
+import { resolve } from "node:path";
+import type { ImportreeOptions, ImportTree } from "./types.js";
+import { scanImports } from "./scanner.js";
+import { createResolver } from "./resolver.js";
+ 
+/**
+ * Recursively walks imports starting from an entry file and builds
+ * the full dependency tree.
+ */
+export async function walk(entryFile: string, options: ImportreeOptions): Promise<ImportTree> {
+  const entrypoint = resolve(entryFile);
+  const basedir = options.rootDir ? resolve(options.rootDir) : process.cwd();
+  const resolveSpecifier = createResolver(basedir, options);
+ 
+  const graph: Record<string, string[]> = {};
+  const externals = new Set<string>();
+  const visited = new Set<string>();
+ 
+  async function visit(filePath: string): Promise<void> {
+    if (visited.has(filePath)) return;
+    visited.add(filePath);
+ 
+    const content = await readFile(filePath, "utf-8");
+    const specifiers = scanImports(content);
+ 
+    const localDeps: string[] = [];
+    for (const spec of specifiers) {
+      const resolved = resolveSpecifier(spec, filePath);
+      if (!resolved) continue;
+ 
+      if (resolved.type === "external" && resolved.specifier) {
+        externals.add(resolved.specifier);
+      E} else if (resolved.type === "local" && resolved.absolutePath) {
+        localDeps.push(resolved.absolutePath);
+      }
+    }
+ 
+    graph[filePath] = localDeps;
+ 
+    await Promise.all(localDeps.map((dep) => visit(dep)));
+  }
+ 
+  await visit(entrypoint);
+ 
+  // Build reverse graph
+  const reverseGraph: Record<string, string[]> = {};
+  for (const file of Object.keys(graph)) {
+    reverseGraph[file] = [];
+  }
+  for (const [file, deps] of Object.entries(graph)) {
+    for (const dep of deps) {
+      Iif (!reverseGraph[dep]) reverseGraph[dep] = [];
+      reverseGraph[dep].push(file);
+    }
+  }
+ 
+  return {
+    entrypoint,
+    files: Object.keys(graph).sort(),
+    externals: [...externals].sort(),
+    graph,
+    reverseGraph,
+  };
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/docs/index.html b/docs/index.html index a5d1497..8946c7a 100644 --- a/docs/index.html +++ b/docs/index.html @@ -1,1388 +1,1957 @@ - + - - - - Import Dependency Trees for TypeScript - importree - - - - - - - - -
-
-
- - - - - -
-
-
-
-
- - v1.0 — Stable Release -
-

- Track every import. - Instantly. -

-

- When a file changes, you need to know what else is affected. importree builds the full import dependency tree for any TypeScript or JavaScript entry point — with zero dependencies and zero AST overhead. -

-

- Built for CI pipelines, build tools, monorepo task runners, and test selectors. -

-
-
- npm install importree - - Copied! -
- - Get Started - - -
+ + + + Import Dependency Trees for TypeScript - importree + + + + + + + +
+
+
+ + + + + +
+
+
+
+
+ + v1.0 — Stable Release +
+

+ Track every import. + Instantly. +

+

+ When a file changes, you need to know what else is affected. importree builds the full + import dependency tree for any TypeScript or JavaScript entry point — with zero + dependencies and zero AST overhead. +

+

+ Built for CI pipelines, build tools, monorepo task runners, and test selectors. +

+
+
+ npm install importree + + + + + Copied! +
+ + Get Started + + + + + +
+
- -
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - index.ts - app.ts - router.ts - auth.ts - db.ts - api.ts - views.ts - utils.ts - + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + index.ts + + + app.ts + + + router.ts + + + auth.ts + + + db.ts + + + api.ts + + + views.ts + + + utils.ts + + +
+
-
-
-
- - -
-
-
-
0
-
Runtime dependencies
-
-
-
<9kb
-
Minified bundle
-
-
-
ESM+CJS
-
Dual module output
-
-
-
100%
-
Test coverage
-
-
-
- - -
-
-
- -

Everything you need.
Nothing you don't.

-

- One job, done right. Resolve every import across your entire codebase with a single function call. -

-
-
-
-
- +
+ + +
+
+
+
0
+
Runtime dependencies
-

Zero Dependencies

-

Built entirely on Node.js built-ins. No native binaries, no WASM, no transitive dependency tree of its own. Just pure TypeScript.

-
-
-
- +
+
<9kb
+
Minified bundle
-

Fast Scanning

-

Regex-based import extraction with concurrent async file traversal. No AST parsing overhead — just the specifiers you need, as fast as reading the file.

-
-
-
- +
+
ESM+CJS
+
Dual module output
-

Path Alias Support

-

Resolve @/components, ~/utils, or any custom alias. Longest-prefix matching with automatic extension probing and index file resolution.

-
-
-
- +
+
100%
+
Test coverage
-

Cache Invalidation

-

Someone edits utils.ts — which test suites, which pages, which build targets need to re-run? The pre-computed reverse graph answers that instantly with getAffectedFiles().

-
-
- - -
-
-
- -

Benchmarks

-

- Compared with excellent tools like madge, dependency-tree, and the TypeScript compiler on a synthetic project with realistic import patterns. -

-
-
- -
-
-
500 files synthetic project
-
-
- importree -
- 12.7 ms -
-
- glob+regex -
- 26.5 ms + +
+
+
+ +

Everything you need.
Nothing you don't.

+

+ One job, done right. Resolve every import across your entire codebase with a single + function call. +

+
+
+
+
+ + + +
-
- madge -
- 43.3 ms +

Zero Dependencies

+

+ Built entirely on Node.js built-ins. No native binaries, no WASM, no transitive + dependency tree of its own. Just pure TypeScript. +

+
+
+
+ + +
-
- dep-tree -
- 44.4 ms +

Fast Scanning

+

+ Regex-based import extraction with concurrent async file traversal. No AST parsing + overhead — just the specifiers you need, as fast as reading the file. +

+
+
+
+ + + +
-
- ts compiler -
- 50.9 ms +

Path Alias Support

+

+ Resolve @/components, ~/utils, or any custom alias. + Longest-prefix matching with automatic extension probing and index file resolution. +

+
+
+
+ + + +
+

Cache Invalidation

+

+ Someone edits utils.ts — which test suites, which pages, which + build targets need to re-run? The pre-computed reverse graph answers that instantly + with getAffectedFiles(). +

- - -
-
-
12.7 ms
-
full dependency tree for 500 files
-
Concurrent async traversal + resolver cache
-
-
-
661K ops/s
-
scanImports throughput on typical source files
-
Regex-based extraction · zero allocation hot path
+
+ + +
+
+
+ +

Benchmarks

+

+ Compared with excellent tools like + madge, + dependency-tree, and the TypeScript compiler on a synthetic project with realistic import patterns. +

-
-
26.4 ms
-
full tree build for 1,000 files
-
~38 ops/s · scales linearly with project size
+ +
+ +
+
+
+ 500 files synthetic project +
+
+
+ importree +
+
+
+ 12.7 ms +
+
+ glob+regex +
+
+
+ 26.5 ms +
+
+ madge +
+
+
+ 43.3 ms +
+
+ dep-tree +
+
+
+ 44.4 ms +
+
+ ts compiler +
+
+
+ 50.9 ms +
+
+
+
+ + +
+
+
12.7 ms
+
+ full dependency tree for 500 files +
+
Concurrent async traversal + resolver cache
+
+
+
661K ops/s
+
+ scanImports throughput on typical source files +
+
+ Regex-based extraction · zero allocation hot path +
+
+
+
26.4 ms
+
+ full tree build for 1,000 files +
+
+ ~38 ops/s · scales linearly with project size +
+
+
+
0 deps
+
+ built entirely on Node.js built-ins +
+
+ No native binaries · no WASM · no AST parsers +
+
+
-
-
0 deps
-
built entirely on Node.js built-ins
-
No native binaries · no WASM · no AST parsers
+ +
-
+
+ + +
+
+
+ +

Two functions.
That's the whole API.

+

Build the tree. Query it. Done.

+
- -
-
- - -
-
-
- -

Two functions.
That's the whole API.

-

- Build the tree. Query it. Done. -

-
- -
-
-

1Build the tree

-
-
-
- build-tree.ts -
-
-
import { importree } from 'importree';
+        
+
+

1Build the tree

+
+
+
+ build-tree.ts +
+
+
import { importree } from 'importree';
 
 const tree = await importree('./src/index.ts', {
   aliases: { '@': './src' },
@@ -1396,19 +1965,19 @@ 

1Build the tree

console.log(tree.graph); // { '/abs/src/index.ts': ['/abs/src/app.ts', ...] }
+
+
-
-
-
-

2Find affected files

-
-
-
- invalidate.ts -
-
-
import { importree, getAffectedFiles } from 'importree';
+          
+

2Find affected files

+
+
+
+ invalidate.ts +
+
+
import { importree, getAffectedFiles } from 'importree';
 
 const tree = await importree('./src/index.ts');
 
@@ -1421,215 +1990,268 @@ 

2Find affected files

console.log(affected); // ['/abs/src/app.ts', '/abs/src/index.ts'] // ^ every file that transitively depends on utils.ts
+
+
-
-
-
- - -
-
-
- -

The complete API.

-

- Two functions, five output fields — fully typed with JSDoc. Nothing hidden, nothing undocumented. -

-
- -
-
-
-
- importree(entry: string, options?: ImportreeOptions): Promise<ImportTree> -
-

Recursively resolves all static imports, dynamic imports, require() calls, and re-exports starting from the entry file. Returns the full dependency graph.

+
+ + +
+
+
+ +

The complete API.

+

+ Two functions, five output fields — fully typed with JSDoc. Nothing hidden, + nothing undocumented. +

-
-
- getAffectedFiles(tree: ImportTree, changedFile: string): string[] -
-

BFS traversal of the reverse dependency graph. Returns all files that transitively depend on the changed file — sorted, deterministic, and without the changed file itself.

-
-
-
-

- ImportTree -

-
-
- entrypoint : string -
-

Absolute path of the entry file.

-
-
-
- files : string[] -
-

Sorted absolute paths of all local files in the dependency tree.

-
-
-
- externals : string[] +
+
+
+
+ importree(entry: string, + options?: ImportreeOptions): Promise<ImportTree> +
+

+ Recursively resolves all static imports, dynamic imports, + require() calls, and re-exports starting from the entry file. Returns + the full dependency graph. +

+
+
+
+ getAffectedFiles(tree: ImportTree, + changedFile: string): string[] +
+

+ BFS traversal of the reverse dependency graph. Returns all files that transitively + depend on the changed file — sorted, deterministic, and without the changed + file itself. +

+
-

Sorted unique bare import specifiers — packages like react, lodash, node:fs.

-
-
-
- graph : Record<string, string[]> + +
+

+ ImportTree +

+
+
+ entrypoint : string +
+

Absolute path of the entry file.

+
+
+
+ files : string[] +
+

Sorted absolute paths of all local files in the dependency tree.

+
+
+
+ externals : string[] +
+

+ Sorted unique bare import specifiers — packages like react, + lodash, node:fs. +

+
+
+
+ graph + : Record<string, string[]> +
+

Forward adjacency list. Each file maps to its direct local imports.

+
+
+
+ reverseGraph + : Record<string, string[]> +
+

+ Reverse adjacency list. Each file maps to files that import it. Pre-computed for + fast cache invalidation. +

+
-

Forward adjacency list. Each file maps to its direct local imports.

-
-
- reverseGraph : Record<string, string[]> +
+
+ + +
+
+
-
-
-
- - - - - + function clearHighlight() { + heroGraph.classList.remove("graph--interactive"); + heroGraph.querySelectorAll(".hl, .hl-source").forEach((el) => { + el.classList.remove("hl", "hl-source"); + }); + } - + let activeNode = null; + heroGraph.querySelectorAll(".graph-node").forEach((node) => { + node.addEventListener("click", (e) => { + e.stopPropagation(); + const id = node.dataset.id; + if (activeNode === id) { + clearHighlight(); + activeNode = null; + } else { + clearHighlight(); + highlightPath(id); + activeNode = id; + } + }); + }); + + heroGraph.addEventListener("click", () => { + clearHighlight(); + activeNode = null; + }); + } + + diff --git a/package.json b/package.json index cb637f9..0a5e672 100644 --- a/package.json +++ b/package.json @@ -1,31 +1,31 @@ { "name": "importree", "version": "1.0.1", - "type": "module", "description": "Build import dependency trees for TypeScript and JavaScript files. Fast, zero-dependency static analysis for dependency detection and cache invalidation.", - "author": "Alex Grozav ", - "license": "ISC", - "repository": { - "type": "git", - "url": "git+https://github.com/alexgrozav/importree.git" - }, - "homepage": "https://importree.js.org", - "bugs": { - "url": "https://github.com/alexgrozav/importree/issues" - }, "keywords": [ - "imports", + "cache-invalidation", "dependencies", "dependency-graph", "import-tree", + "imports", + "javascript", "static-analysis", - "cache-invalidation", - "typescript", - "javascript" + "typescript" ], - "engines": { - "node": ">=18" + "homepage": "https://importree.js.org", + "bugs": { + "url": "https://github.com/alexgrozav/importree/issues" }, + "license": "ISC", + "author": "Alex Grozav ", + "repository": { + "type": "git", + "url": "git+https://github.com/alexgrozav/importree.git" + }, + "files": [ + "dist" + ], + "type": "module", "main": "./dist/index.cjs", "module": "./dist/index.mjs", "types": "./dist/index.d.mts", @@ -41,9 +41,6 @@ } } }, - "files": [ - "dist" - ], "scripts": { "build": "tsdown", "build:docs": "cp -r docs dist-docs", @@ -52,15 +49,26 @@ "dev:docs": "npx http-server docs -p 8765 -o", "bench": "vitest bench", "bench:run": "vitest bench --run", + "lint": "oxlint", + "lint:fix": "oxlint --fix", + "fmt": "oxfmt", + "fmt:check": "oxfmt --check", "test": "vitest run", + "test:coverage": "vitest run --coverage", "test:watch": "vitest" }, "devDependencies": { + "@types/node": "^25.3.5", "@vitest/coverage-v8": "^4.0.18", "dependency-tree": "^11.4.0", "madge": "^8.0.0", + "oxfmt": "^0.36.0", + "oxlint": "^1.51.0", "tsdown": "^0.21.0", "typescript": "^5.7.0", "vitest": "^4.0.18" + }, + "engines": { + "node": ">=18" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0c3bf58..b058917 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,15 +8,24 @@ importers: .: devDependencies: + '@types/node': + specifier: ^25.3.5 + version: 25.3.5 '@vitest/coverage-v8': specifier: ^4.0.18 - version: 4.0.18(vitest@4.0.18) + version: 4.0.18(vitest@4.0.18(@types/node@25.3.5)) dependency-tree: specifier: ^11.4.0 version: 11.4.0 madge: specifier: ^8.0.0 version: 8.0.0(typescript@5.9.3) + oxfmt: + specifier: ^0.36.0 + version: 0.36.0 + oxlint: + specifier: ^1.51.0 + version: 1.51.0 tsdown: specifier: ^0.21.0 version: 0.21.0(typescript@5.9.3) @@ -25,7 +34,7 @@ importers: version: 5.9.3 vitest: specifier: ^4.0.18 - version: 4.0.18 + version: 4.0.18(@types/node@25.3.5) packages: @@ -259,6 +268,250 @@ packages: '@oxc-project/types@0.115.0': resolution: {integrity: sha512-4n91DKnebUS4yjUHl2g3/b2T+IUdCfmoZGhmwsovZCDaJSs+QkVAM+0AqqTxHSsHfeiMuueT75cZaZcT/m0pSw==} + '@oxfmt/binding-android-arm-eabi@0.36.0': + resolution: {integrity: sha512-Z4yVHJWx/swHHjtr0dXrBZb6LxS+qNz1qdza222mWwPTUK4L790+5i3LTgjx3KYGBzcYpjaiZBw4vOx94dH7MQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [android] + + '@oxfmt/binding-android-arm64@0.36.0': + resolution: {integrity: sha512-3ElCJRFNPQl7jexf2CAa9XmAm8eC5JPrIDSjc9jSchkVSFTEqyL0NtZinBB2h1a4i4JgP1oGl/5G5n8YR4FN8Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@oxfmt/binding-darwin-arm64@0.36.0': + resolution: {integrity: sha512-nak4znWCqIExKhYSY/mz/lWsqWIpdsS7o0+SRzXR1Q0m7GrMcG1UrF1pS7TLGZhhkf7nTfEF7q6oZzJiodRDuw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@oxfmt/binding-darwin-x64@0.36.0': + resolution: {integrity: sha512-V4GP96thDnpKx6ADnMDnhIXNdtV+Ql9D4HUU+a37VTeVbs5qQSF/s6hhUP1b3xUqU7iRcwh72jUU2Y12rtGHAw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@oxfmt/binding-freebsd-x64@0.36.0': + resolution: {integrity: sha512-/xapWCADfI5wrhxpEUjhI9fnw7MV5BUZizVa8e24n3VSK6A3Y1TB/ClOP1tfxNspykFKXp4NBWl6NtDJP3osqQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@oxfmt/binding-linux-arm-gnueabihf@0.36.0': + resolution: {integrity: sha512-1lOmv61XMFIH5uNm27620kRRzWt/RK6tdn250BRDoG9W7OXGOQ5UyI1HVT+SFkoOoKztBiinWgi68+NA1MjBVQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@oxfmt/binding-linux-arm-musleabihf@0.36.0': + resolution: {integrity: sha512-vMH23AskdR1ujUS9sPck2Df9rBVoZUnCVY86jisILzIQ/QQ/yKUTi7tgnIvydPx7TyB/48wsQ5QMr5Knq5p/aw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@oxfmt/binding-linux-arm64-gnu@0.36.0': + resolution: {integrity: sha512-Hy1V+zOBHpBiENRx77qrUTt5aPDHeCASRc8K5KwwAHkX2AKP0nV89eL17hsZrE9GmnXFjsNmd80lyf7aRTXsbw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@oxfmt/binding-linux-arm64-musl@0.36.0': + resolution: {integrity: sha512-SPGLJkOIHSIC6ABUQ5V8NqJpvYhMJueJv26NYqfCnwi/Mn6A61amkpJJ9Suy0Nmvs+OWESJpcebrBUbXPGZyQQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@oxfmt/binding-linux-ppc64-gnu@0.36.0': + resolution: {integrity: sha512-3EuoyB8x9x8ysYJjbEO/M9fkSk72zQKnXCvpZMDHXlnY36/1qMp55Nm0PrCwjGO/1pen5hdOVkz9WmP3nAp2IQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@oxfmt/binding-linux-riscv64-gnu@0.36.0': + resolution: {integrity: sha512-MpY3itLwpGh8dnywtrZtaZ604T1m715SydCKy0+qTxetv+IHzuA+aO/AGzrlzUNYZZmtWtmDBrChZGibvZxbRQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@oxfmt/binding-linux-riscv64-musl@0.36.0': + resolution: {integrity: sha512-mmDhe4Vtx+XwQPRPn/V25+APnkApYgZ23q+6GVsNYY98pf3aU0aI3Me96pbRs/AfJ1jIiGC+/6q71FEu8dHcHw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@oxfmt/binding-linux-s390x-gnu@0.36.0': + resolution: {integrity: sha512-AYXhU+DmNWLSnvVwkHM92fuYhogtVHab7UQrPNaDf1sxadugg9gWVmcgJDlIwxJdpk5CVW/TFvwUKwI432zhhA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@oxfmt/binding-linux-x64-gnu@0.36.0': + resolution: {integrity: sha512-H16QhhQ3usoakMleiAAQ2mg0NsBDAdyE9agUgfC8IHHh3jZEbr0rIKwjEqwbOHK5M0EmfhJmr+aGO/MgZPsneA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@oxfmt/binding-linux-x64-musl@0.36.0': + resolution: {integrity: sha512-EFFGkixA39BcmHiCe2ECdrq02D6FCve5ka6ObbvrheXl4V+R0U/E+/uLyVx1X65LW8TA8QQHdnbdDallRekohw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@oxfmt/binding-openharmony-arm64@0.36.0': + resolution: {integrity: sha512-zr/t369wZWFOj1qf06Z5gGNjFymfUNDrxKMmr7FKiDRVI1sNsdKRCuRL4XVjtcptKQ+ao3FfxLN1vrynivmCYg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@oxfmt/binding-win32-arm64-msvc@0.36.0': + resolution: {integrity: sha512-FxO7UksTv8h4olzACgrqAXNF6BP329+H322323iDrMB5V/+a1kcAw07fsOsUmqNrb9iJBsCQgH/zqcqp5903ag==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@oxfmt/binding-win32-ia32-msvc@0.36.0': + resolution: {integrity: sha512-OjoMQ89H01M0oLMfr/CPNH1zi48ZIwxAKObUl57oh7ssUBNDp/2Vjf7E1TQ8M4oj4VFQ/byxl2SmcPNaI2YNDg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ia32] + os: [win32] + + '@oxfmt/binding-win32-x64-msvc@0.36.0': + resolution: {integrity: sha512-MoyeQ9S36ZTz/4bDhOKJgOBIDROd4dQ5AkT9iezhEaUBxAPdNX9Oq0jD8OSnCj3G4wam/XNxVWKMA52kmzmPtQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@oxlint/binding-android-arm-eabi@1.51.0': + resolution: {integrity: sha512-jJYIqbx4sX+suIxWstc4P7SzhEwb4ArWA2KVrmEuu9vH2i0qM6QIHz/ehmbGE4/2fZbpuMuBzTl7UkfNoqiSgw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [android] + + '@oxlint/binding-android-arm64@1.51.0': + resolution: {integrity: sha512-GtXyBCcH4ti98YdiMNCrpBNGitx87EjEWxevnyhcBK12k/Vu4EzSB45rzSC4fGFUD6sQgeaxItRCEEWeVwPafw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@oxlint/binding-darwin-arm64@1.51.0': + resolution: {integrity: sha512-3QJbeYaMHn6Bh2XeBXuITSsbnIctyTjvHf5nRjKYrT9pPeErNIpp5VDEeAXC0CZSwSVTsc8WOSDwgrAI24JolQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@oxlint/binding-darwin-x64@1.51.0': + resolution: {integrity: sha512-NzErhMaTEN1cY0E8C5APy74lw5VwsNfJfVPBMWPVQLqAbO0k4FFLjvHURvkUL+Y18Wu+8Vs1kbqPh2hjXYA4pg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@oxlint/binding-freebsd-x64@1.51.0': + resolution: {integrity: sha512-msAIh3vPAoKoHlOE/oe6Q5C/n9umypv/k81lED82ibrJotn+3YG2Qp1kiR8o/Dg5iOEU97c6tl0utxcyFenpFw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@oxlint/binding-linux-arm-gnueabihf@1.51.0': + resolution: {integrity: sha512-CqQPcvqYyMe9ZBot2stjGogEzk1z8gGAngIX7srSzrzexmXixwVxBdFZyxTVM0CjGfDeV+Ru0w25/WNjlMM2Hw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@oxlint/binding-linux-arm-musleabihf@1.51.0': + resolution: {integrity: sha512-dstrlYQgZMnyOssxSbolGCge/sDbko12N/35RBNuqLpoPbft2aeBidBAb0dvQlyBd9RJ6u8D4o4Eh8Un6iTgyQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@oxlint/binding-linux-arm64-gnu@1.51.0': + resolution: {integrity: sha512-QEjUpXO7d35rP1/raLGGbAsBLLGZIzV3ZbeSjqWlD3oRnxpRIZ6iL4o51XQHkconn3uKssc+1VKdtHJ81BBhDA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@oxlint/binding-linux-arm64-musl@1.51.0': + resolution: {integrity: sha512-YSJua5irtG4DoMAjUapDTPhkQLHhBIY0G9JqlZS6/SZPzqDkPku/1GdWs0D6h/wyx0Iz31lNCfIaWKBQhzP0wQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@oxlint/binding-linux-ppc64-gnu@1.51.0': + resolution: {integrity: sha512-7L4Wj2IEUNDETKssB9IDYt16T6WlF+X2jgC/hBq3diGHda9vJLpAgb09+D3quFq7TdkFtI7hwz/jmuQmQFPc1Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@oxlint/binding-linux-riscv64-gnu@1.51.0': + resolution: {integrity: sha512-cBUHqtOXy76G41lOB401qpFoKx1xq17qYkhWrLSM7eEjiHM9sOtYqpr6ZdqCnN9s6ZpzudX4EkeHOFH2E9q0vA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@oxlint/binding-linux-riscv64-musl@1.51.0': + resolution: {integrity: sha512-WKbg8CysgZcHfZX0ixQFBRSBvFZUHa3SBnEjHY2FVYt2nbNJEjzTxA3ZR5wMU0NOCNKIAFUFvAh5/XJKPRJuJg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@oxlint/binding-linux-s390x-gnu@1.51.0': + resolution: {integrity: sha512-N1QRUvJTxqXNSu35YOufdjsAVmKVx5bkrggOWAhTWBc3J4qjcBwr1IfyLh/6YCg8sYRSR1GraldS9jUgJL/U4A==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@oxlint/binding-linux-x64-gnu@1.51.0': + resolution: {integrity: sha512-e0Mz0DizsCoqNIjeOg6OUKe8JKJWZ5zZlwsd05Bmr51Jo3AOL4UJnPvwKumr4BBtBrDZkCmOLhCvDGm95nJM2g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@oxlint/binding-linux-x64-musl@1.51.0': + resolution: {integrity: sha512-wD8HGTWhYBKXvRDvoBVB1y+fEYV01samhWQSy1Zkxq2vpezvMnjaFKRuiP6tBNITLGuffbNDEXOwcAhJ3gI5Ug==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@oxlint/binding-openharmony-arm64@1.51.0': + resolution: {integrity: sha512-5NSwQ2hDEJ0GPXqikjWtwzgAQCsS7P9aLMNenjjKa+gknN3lTCwwwERsT6lKXSirfU3jLjexA2XQvQALh5h27w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@oxlint/binding-win32-arm64-msvc@1.51.0': + resolution: {integrity: sha512-JEZyah1M0RHMw8d+jjSSJmSmO8sABA1J1RtrHYujGPeCkYg1NeH0TGuClpe2h5QtioRTaF57y/TZfn/2IFV6fA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@oxlint/binding-win32-ia32-msvc@1.51.0': + resolution: {integrity: sha512-q3cEoKH6kwjz/WRyHwSf0nlD2F5Qw536kCXvmlSu+kaShzgrA0ojmh45CA81qL+7udfCaZL2SdKCZlLiGBVFlg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ia32] + os: [win32] + + '@oxlint/binding-win32-x64-msvc@1.51.0': + resolution: {integrity: sha512-Q14+fOGb9T28nWF/0EUsYqERiRA7cl1oy4TJrGmLaqhm+aO2cV+JttboHI3CbdeMCAyDI1+NoSlrM7Melhp/cw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + '@quansync/fs@1.0.0': resolution: {integrity: sha512-4TJ3DFtlf1L5LDMaM6CanJ/0lckGNtJcMjQ1NAV6zDmA0tEHKZtxNKin8EgPaVX1YzljbxckyT2tJrpQKAtngQ==} @@ -532,6 +785,9 @@ packages: '@types/jsesc@2.5.1': resolution: {integrity: sha512-9VN+6yxLOPLOav+7PwjZbxiID2bVaeq0ED4qSQmdQTdjnXJSaCVKTR58t15oqH1H5t8Ng2ZX1SabJVoN9Q34bw==} + '@types/node@25.3.5': + resolution: {integrity: sha512-oX8xrhvpiyRCQkG1MFchB09f+cXftgIXb3a7UUa4Y3wpmZPw5tyZGTLWhlESOLq1Rq6oDlc8npVU2/9xiCuXMA==} + '@typescript-eslint/project-service@8.56.1': resolution: {integrity: sha512-TAdqQTzHNNvlVFfR+hu2PDJrURiwKsUvxFn1M0h95BB8ah5jejas08jUWG4dBA68jDMI988IvtfdAI53JzEHOQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -1021,6 +1277,21 @@ packages: resolution: {integrity: sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==} engines: {node: '>=10'} + oxfmt@0.36.0: + resolution: {integrity: sha512-/ejJ+KoSW6J9bcNT9a9UtJSJNWhJ3yOLSBLbkoFHJs/8CZjmaZVZAJe4YgO1KMJlKpNQasrn/G9JQUEZI3p0EQ==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + + oxlint@1.51.0: + resolution: {integrity: sha512-g6DNPaV9/WI9MoX2XllafxQuxwY1TV++j7hP8fTJByVBuCoVtm3dy9f/2vtH/HU40JztcgWF4G7ua+gkainklQ==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + oxlint-tsgolint: '>=0.15.0' + peerDependenciesMeta: + oxlint-tsgolint: + optional: true + parse-ms@2.1.0: resolution: {integrity: sha512-kHt7kzLoS9VBZfUsiKjv43mr91ea+U05EyKkEtqp7vNbHxmaVuEqN7XxeEVnGrMtYOAxGrDElSi96K7EgO1zCA==} engines: {node: '>=6'} @@ -1212,6 +1483,10 @@ packages: resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} engines: {node: '>=12.0.0'} + tinypool@2.1.0: + resolution: {integrity: sha512-Pugqs6M0m7Lv1I7FtxN4aoyToKg1C4tu+/381vH35y8oENM/Ai7f7C4StcoK4/+BSw9ebcS8jRiVrORFKCALLw==} + engines: {node: ^20.0.0 || >=22.0.0} + tinyrainbow@3.0.3: resolution: {integrity: sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==} engines: {node: '>=14.0.0'} @@ -1273,6 +1548,9 @@ packages: unconfig-core@7.5.0: resolution: {integrity: sha512-Su3FauozOGP44ZmKdHy2oE6LPjk51M/TRRjHv2HNCWiDvfvCoxC2lno6jevMA91MYAdCdwP05QnWdWpSbncX/w==} + undici-types@7.18.2: + resolution: {integrity: sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==} + unrun@0.2.30: resolution: {integrity: sha512-a4W1wDADI0gvDDr14T0ho1FgMhmfjq6M8Iz8q234EnlxgH/9cMHDueUSLwTl1fwSBs5+mHrLFYH+7B8ao36EBA==} engines: {node: '>=20.19.0'} @@ -1533,6 +1811,120 @@ snapshots: '@oxc-project/types@0.115.0': {} + '@oxfmt/binding-android-arm-eabi@0.36.0': + optional: true + + '@oxfmt/binding-android-arm64@0.36.0': + optional: true + + '@oxfmt/binding-darwin-arm64@0.36.0': + optional: true + + '@oxfmt/binding-darwin-x64@0.36.0': + optional: true + + '@oxfmt/binding-freebsd-x64@0.36.0': + optional: true + + '@oxfmt/binding-linux-arm-gnueabihf@0.36.0': + optional: true + + '@oxfmt/binding-linux-arm-musleabihf@0.36.0': + optional: true + + '@oxfmt/binding-linux-arm64-gnu@0.36.0': + optional: true + + '@oxfmt/binding-linux-arm64-musl@0.36.0': + optional: true + + '@oxfmt/binding-linux-ppc64-gnu@0.36.0': + optional: true + + '@oxfmt/binding-linux-riscv64-gnu@0.36.0': + optional: true + + '@oxfmt/binding-linux-riscv64-musl@0.36.0': + optional: true + + '@oxfmt/binding-linux-s390x-gnu@0.36.0': + optional: true + + '@oxfmt/binding-linux-x64-gnu@0.36.0': + optional: true + + '@oxfmt/binding-linux-x64-musl@0.36.0': + optional: true + + '@oxfmt/binding-openharmony-arm64@0.36.0': + optional: true + + '@oxfmt/binding-win32-arm64-msvc@0.36.0': + optional: true + + '@oxfmt/binding-win32-ia32-msvc@0.36.0': + optional: true + + '@oxfmt/binding-win32-x64-msvc@0.36.0': + optional: true + + '@oxlint/binding-android-arm-eabi@1.51.0': + optional: true + + '@oxlint/binding-android-arm64@1.51.0': + optional: true + + '@oxlint/binding-darwin-arm64@1.51.0': + optional: true + + '@oxlint/binding-darwin-x64@1.51.0': + optional: true + + '@oxlint/binding-freebsd-x64@1.51.0': + optional: true + + '@oxlint/binding-linux-arm-gnueabihf@1.51.0': + optional: true + + '@oxlint/binding-linux-arm-musleabihf@1.51.0': + optional: true + + '@oxlint/binding-linux-arm64-gnu@1.51.0': + optional: true + + '@oxlint/binding-linux-arm64-musl@1.51.0': + optional: true + + '@oxlint/binding-linux-ppc64-gnu@1.51.0': + optional: true + + '@oxlint/binding-linux-riscv64-gnu@1.51.0': + optional: true + + '@oxlint/binding-linux-riscv64-musl@1.51.0': + optional: true + + '@oxlint/binding-linux-s390x-gnu@1.51.0': + optional: true + + '@oxlint/binding-linux-x64-gnu@1.51.0': + optional: true + + '@oxlint/binding-linux-x64-musl@1.51.0': + optional: true + + '@oxlint/binding-openharmony-arm64@1.51.0': + optional: true + + '@oxlint/binding-win32-arm64-msvc@1.51.0': + optional: true + + '@oxlint/binding-win32-ia32-msvc@1.51.0': + optional: true + + '@oxlint/binding-win32-x64-msvc@1.51.0': + optional: true + '@quansync/fs@1.0.0': dependencies: quansync: 1.0.0 @@ -1694,6 +2086,10 @@ snapshots: '@types/jsesc@2.5.1': {} + '@types/node@25.3.5': + dependencies: + undici-types: 7.18.2 + '@typescript-eslint/project-service@8.56.1(typescript@5.9.3)': dependencies: '@typescript-eslint/tsconfig-utils': 8.56.1(typescript@5.9.3) @@ -1729,7 +2125,7 @@ snapshots: '@typescript-eslint/types': 8.56.1 eslint-visitor-keys: 5.0.1 - '@vitest/coverage-v8@4.0.18(vitest@4.0.18)': + '@vitest/coverage-v8@4.0.18(vitest@4.0.18(@types/node@25.3.5))': dependencies: '@bcoe/v8-coverage': 1.0.2 '@vitest/utils': 4.0.18 @@ -1741,7 +2137,7 @@ snapshots: obug: 2.1.1 std-env: 3.10.0 tinyrainbow: 3.0.3 - vitest: 4.0.18 + vitest: 4.0.18(@types/node@25.3.5) '@vitest/expect@4.0.18': dependencies: @@ -1752,13 +2148,13 @@ snapshots: chai: 6.2.2 tinyrainbow: 3.0.3 - '@vitest/mocker@4.0.18(vite@7.3.1)': + '@vitest/mocker@4.0.18(vite@7.3.1(@types/node@25.3.5))': dependencies: '@vitest/spy': 4.0.18 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 7.3.1 + vite: 7.3.1(@types/node@25.3.5) '@vitest/pretty-format@4.0.18': dependencies: @@ -2211,6 +2607,52 @@ snapshots: strip-ansi: 6.0.1 wcwidth: 1.0.1 + oxfmt@0.36.0: + dependencies: + tinypool: 2.1.0 + optionalDependencies: + '@oxfmt/binding-android-arm-eabi': 0.36.0 + '@oxfmt/binding-android-arm64': 0.36.0 + '@oxfmt/binding-darwin-arm64': 0.36.0 + '@oxfmt/binding-darwin-x64': 0.36.0 + '@oxfmt/binding-freebsd-x64': 0.36.0 + '@oxfmt/binding-linux-arm-gnueabihf': 0.36.0 + '@oxfmt/binding-linux-arm-musleabihf': 0.36.0 + '@oxfmt/binding-linux-arm64-gnu': 0.36.0 + '@oxfmt/binding-linux-arm64-musl': 0.36.0 + '@oxfmt/binding-linux-ppc64-gnu': 0.36.0 + '@oxfmt/binding-linux-riscv64-gnu': 0.36.0 + '@oxfmt/binding-linux-riscv64-musl': 0.36.0 + '@oxfmt/binding-linux-s390x-gnu': 0.36.0 + '@oxfmt/binding-linux-x64-gnu': 0.36.0 + '@oxfmt/binding-linux-x64-musl': 0.36.0 + '@oxfmt/binding-openharmony-arm64': 0.36.0 + '@oxfmt/binding-win32-arm64-msvc': 0.36.0 + '@oxfmt/binding-win32-ia32-msvc': 0.36.0 + '@oxfmt/binding-win32-x64-msvc': 0.36.0 + + oxlint@1.51.0: + optionalDependencies: + '@oxlint/binding-android-arm-eabi': 1.51.0 + '@oxlint/binding-android-arm64': 1.51.0 + '@oxlint/binding-darwin-arm64': 1.51.0 + '@oxlint/binding-darwin-x64': 1.51.0 + '@oxlint/binding-freebsd-x64': 1.51.0 + '@oxlint/binding-linux-arm-gnueabihf': 1.51.0 + '@oxlint/binding-linux-arm-musleabihf': 1.51.0 + '@oxlint/binding-linux-arm64-gnu': 1.51.0 + '@oxlint/binding-linux-arm64-musl': 1.51.0 + '@oxlint/binding-linux-ppc64-gnu': 1.51.0 + '@oxlint/binding-linux-riscv64-gnu': 1.51.0 + '@oxlint/binding-linux-riscv64-musl': 1.51.0 + '@oxlint/binding-linux-s390x-gnu': 1.51.0 + '@oxlint/binding-linux-x64-gnu': 1.51.0 + '@oxlint/binding-linux-x64-musl': 1.51.0 + '@oxlint/binding-openharmony-arm64': 1.51.0 + '@oxlint/binding-win32-arm64-msvc': 1.51.0 + '@oxlint/binding-win32-ia32-msvc': 1.51.0 + '@oxlint/binding-win32-x64-msvc': 1.51.0 + parse-ms@2.1.0: {} path-parse@1.0.7: {} @@ -2433,6 +2875,8 @@ snapshots: fdir: 6.5.0(picomatch@4.0.3) picomatch: 4.0.3 + tinypool@2.1.0: {} + tinyrainbow@3.0.3: {} tree-kill@1.2.2: {} @@ -2491,13 +2935,15 @@ snapshots: '@quansync/fs': 1.0.0 quansync: 1.0.0 + undici-types@7.18.2: {} + unrun@0.2.30: dependencies: rolldown: 1.0.0-rc.7 util-deprecate@1.0.2: {} - vite@7.3.1: + vite@7.3.1(@types/node@25.3.5): dependencies: esbuild: 0.27.3 fdir: 6.5.0(picomatch@4.0.3) @@ -2506,12 +2952,13 @@ snapshots: rollup: 4.59.0 tinyglobby: 0.2.15 optionalDependencies: + '@types/node': 25.3.5 fsevents: 2.3.3 - vitest@4.0.18: + vitest@4.0.18(@types/node@25.3.5): dependencies: '@vitest/expect': 4.0.18 - '@vitest/mocker': 4.0.18(vite@7.3.1) + '@vitest/mocker': 4.0.18(vite@7.3.1(@types/node@25.3.5)) '@vitest/pretty-format': 4.0.18 '@vitest/runner': 4.0.18 '@vitest/snapshot': 4.0.18 @@ -2528,8 +2975,10 @@ snapshots: tinyexec: 1.0.2 tinyglobby: 0.2.15 tinyrainbow: 3.0.3 - vite: 7.3.1 + vite: 7.3.1(@types/node@25.3.5) why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 25.3.5 transitivePeerDependencies: - jiti - less diff --git a/src/__benchmarks__/affected.bench.ts b/src/__benchmarks__/affected.bench.ts index 99d6eba..0af2ffe 100644 --- a/src/__benchmarks__/affected.bench.ts +++ b/src/__benchmarks__/affected.bench.ts @@ -1,18 +1,18 @@ -import { describe, bench, beforeAll, afterAll } from 'vitest'; -import { join } from 'node:path'; -import { importree, getAffectedFiles, type ImportTree } from '../index.js'; -import { createFixture, type Fixture, type FixtureSize } from './generate-fixtures.js'; +import { describe, bench, beforeAll, afterAll } from "vitest"; +import { join } from "node:path"; +import { importree, getAffectedFiles, type ImportTree } from "../index.js"; +import { createFixture, type Fixture, type FixtureSize } from "./generate-fixtures.js"; const fixtures: Record = {}; const trees: Record = {}; beforeAll(async () => { - const sizes: FixtureSize[] = ['small', 'medium', 'large', 'xlarge']; + const sizes: FixtureSize[] = ["small", "medium", "large", "xlarge"]; for (const size of sizes) { fixtures[size] = createFixture(size); trees[size] = await importree(fixtures[size].entryFile, { rootDir: fixtures[size].rootDir, - aliases: { '@': join(fixtures[size].rootDir, 'modules') }, + aliases: { "@": join(fixtures[size].rootDir, "modules") }, }); } }); @@ -21,66 +21,66 @@ afterAll(() => { for (const f of Object.values(fixtures)) f.cleanup(); }); -describe('getAffectedFiles - small (10 files)', () => { - bench('leaf file (max propagation)', () => { +describe("getAffectedFiles - small (10 files)", () => { + bench("leaf file (max propagation)", () => { const tree = trees.small; getAffectedFiles(tree, tree.files[tree.files.length - 1]); }); - bench('mid-level file', () => { + bench("mid-level file", () => { const tree = trees.small; getAffectedFiles(tree, tree.files[Math.floor(tree.files.length / 2)]); }); - bench('entry file (no dependents)', () => { + bench("entry file (no dependents)", () => { getAffectedFiles(trees.small, trees.small.entrypoint); }); }); -describe('getAffectedFiles - medium (100 files)', () => { - bench('leaf file (max propagation)', () => { +describe("getAffectedFiles - medium (100 files)", () => { + bench("leaf file (max propagation)", () => { const tree = trees.medium; getAffectedFiles(tree, tree.files[tree.files.length - 1]); }); - bench('mid-level file', () => { + bench("mid-level file", () => { const tree = trees.medium; getAffectedFiles(tree, tree.files[Math.floor(tree.files.length / 2)]); }); - bench('entry file (no dependents)', () => { + bench("entry file (no dependents)", () => { getAffectedFiles(trees.medium, trees.medium.entrypoint); }); }); -describe('getAffectedFiles - large (500 files)', () => { - bench('leaf file (max propagation)', () => { +describe("getAffectedFiles - large (500 files)", () => { + bench("leaf file (max propagation)", () => { const tree = trees.large; getAffectedFiles(tree, tree.files[tree.files.length - 1]); }); - bench('mid-level file', () => { + bench("mid-level file", () => { const tree = trees.large; getAffectedFiles(tree, tree.files[Math.floor(tree.files.length / 2)]); }); - bench('entry file (no dependents)', () => { + bench("entry file (no dependents)", () => { getAffectedFiles(trees.large, trees.large.entrypoint); }); }); -describe('getAffectedFiles - xlarge (1000 files)', () => { - bench('leaf file (max propagation)', () => { +describe("getAffectedFiles - xlarge (1000 files)", () => { + bench("leaf file (max propagation)", () => { const tree = trees.xlarge; getAffectedFiles(tree, tree.files[tree.files.length - 1]); }); - bench('mid-level file', () => { + bench("mid-level file", () => { const tree = trees.xlarge; getAffectedFiles(tree, tree.files[Math.floor(tree.files.length / 2)]); }); - bench('entry file (no dependents)', () => { + bench("entry file (no dependents)", () => { getAffectedFiles(trees.xlarge, trees.xlarge.entrypoint); }); }); diff --git a/src/__benchmarks__/comparisons.bench.ts b/src/__benchmarks__/comparisons.bench.ts index 5b146aa..f663d5b 100644 --- a/src/__benchmarks__/comparisons.bench.ts +++ b/src/__benchmarks__/comparisons.bench.ts @@ -1,9 +1,9 @@ -import { describe, bench, beforeAll, afterAll } from 'vitest'; -import { join } from 'node:path'; -import { readdirSync } from 'node:fs'; -import { readFile } from 'node:fs/promises'; -import { importree } from '../index.js'; -import { createFixture, type Fixture, type FixtureSize } from './generate-fixtures.js'; +import { describe, bench, beforeAll, afterAll } from "vitest"; +import { join } from "node:path"; +import { readdirSync } from "node:fs"; +import { readFile } from "node:fs/promises"; +import { importree } from "../index.js"; +import { createFixture, type Fixture, type FixtureSize } from "./generate-fixtures.js"; // Detect available comparison tools let madgeAvailable = false; @@ -11,24 +11,24 @@ let tsAvailable = false; let depTreeAvailable = false; try { - await import('madge'); + await import("madge"); madgeAvailable = true; } catch {} try { - await import('typescript'); + await import("typescript"); tsAvailable = true; } catch {} try { - await import('dependency-tree'); + await import("dependency-tree"); depTreeAvailable = true; } catch {} const fixtures: Record = {}; beforeAll(() => { - const sizes: FixtureSize[] = ['small', 'medium', 'large']; + const sizes: FixtureSize[] = ["small", "medium", "large"]; for (const size of sizes) { fixtures[size] = createFixture(size); } @@ -48,40 +48,40 @@ function walkDir(dir: string): string[] { return results; } -describe('comparison - small (10 files)', () => { - bench('importree', async () => { +describe("comparison - small (10 files)", () => { + bench("importree", async () => { await importree(fixtures.small.entryFile, { rootDir: fixtures.small.rootDir, - aliases: { '@': join(fixtures.small.rootDir, 'modules') }, + aliases: { "@": join(fixtures.small.rootDir, "modules") }, }); }); if (madgeAvailable) { - bench('madge', async () => { - const madge = (await import('madge')).default; + bench("madge", async () => { + const madge = (await import("madge")).default; await madge(fixtures.small.entryFile, { - fileExtensions: ['ts', 'tsx', 'js', 'jsx'], + fileExtensions: ["ts", "tsx", "js", "jsx"], }); }); } - bench('manual glob+regex', async () => { + bench("manual glob+regex", async () => { const files = walkDir(fixtures.small.rootDir); const fromRe = /\bfrom\s+['"]([^'"]+)['"]/g; const graph: Record = {}; for (const file of files) { - const content = await readFile(file, 'utf-8'); + const content = await readFile(file, "utf-8"); const deps: string[] = []; for (const m of content.matchAll(fromRe)) { - if (m[1].startsWith('.')) deps.push(m[1]); + if (m[1].startsWith(".")) deps.push(m[1]); } graph[file] = deps; } }); if (depTreeAvailable) { - bench('dependency-tree', () => { - const depTree = require('dependency-tree'); + bench("dependency-tree", () => { + const depTree = require("dependency-tree"); depTree.toList({ filename: fixtures.small.entryFile, directory: fixtures.small.rootDir, @@ -90,8 +90,8 @@ describe('comparison - small (10 files)', () => { } if (tsAvailable) { - bench('ts.createProgram', () => { - const ts = require('typescript'); + bench("ts.createProgram", () => { + const ts = require("typescript"); const program = ts.createProgram([fixtures.small.entryFile], { target: ts.ScriptTarget.ES2022, module: ts.ModuleKind.ESNext, @@ -104,40 +104,40 @@ describe('comparison - small (10 files)', () => { } }); -describe('comparison - medium (100 files)', () => { - bench('importree', async () => { +describe("comparison - medium (100 files)", () => { + bench("importree", async () => { await importree(fixtures.medium.entryFile, { rootDir: fixtures.medium.rootDir, - aliases: { '@': join(fixtures.medium.rootDir, 'modules') }, + aliases: { "@": join(fixtures.medium.rootDir, "modules") }, }); }); if (madgeAvailable) { - bench('madge', async () => { - const madge = (await import('madge')).default; + bench("madge", async () => { + const madge = (await import("madge")).default; await madge(fixtures.medium.entryFile, { - fileExtensions: ['ts', 'tsx', 'js', 'jsx'], + fileExtensions: ["ts", "tsx", "js", "jsx"], }); }); } - bench('manual glob+regex', async () => { + bench("manual glob+regex", async () => { const files = walkDir(fixtures.medium.rootDir); const fromRe = /\bfrom\s+['"]([^'"]+)['"]/g; const graph: Record = {}; for (const file of files) { - const content = await readFile(file, 'utf-8'); + const content = await readFile(file, "utf-8"); const deps: string[] = []; for (const m of content.matchAll(fromRe)) { - if (m[1].startsWith('.')) deps.push(m[1]); + if (m[1].startsWith(".")) deps.push(m[1]); } graph[file] = deps; } }); if (depTreeAvailable) { - bench('dependency-tree', () => { - const depTree = require('dependency-tree'); + bench("dependency-tree", () => { + const depTree = require("dependency-tree"); depTree.toList({ filename: fixtures.medium.entryFile, directory: fixtures.medium.rootDir, @@ -146,8 +146,8 @@ describe('comparison - medium (100 files)', () => { } if (tsAvailable) { - bench('ts.createProgram', () => { - const ts = require('typescript'); + bench("ts.createProgram", () => { + const ts = require("typescript"); const program = ts.createProgram([fixtures.medium.entryFile], { target: ts.ScriptTarget.ES2022, module: ts.ModuleKind.ESNext, @@ -160,40 +160,40 @@ describe('comparison - medium (100 files)', () => { } }); -describe('comparison - large (500 files)', () => { - bench('importree', async () => { +describe("comparison - large (500 files)", () => { + bench("importree", async () => { await importree(fixtures.large.entryFile, { rootDir: fixtures.large.rootDir, - aliases: { '@': join(fixtures.large.rootDir, 'modules') }, + aliases: { "@": join(fixtures.large.rootDir, "modules") }, }); }); if (madgeAvailable) { - bench('madge', async () => { - const madge = (await import('madge')).default; + bench("madge", async () => { + const madge = (await import("madge")).default; await madge(fixtures.large.entryFile, { - fileExtensions: ['ts', 'tsx', 'js', 'jsx'], + fileExtensions: ["ts", "tsx", "js", "jsx"], }); }); } - bench('manual glob+regex', async () => { + bench("manual glob+regex", async () => { const files = walkDir(fixtures.large.rootDir); const fromRe = /\bfrom\s+['"]([^'"]+)['"]/g; const graph: Record = {}; for (const file of files) { - const content = await readFile(file, 'utf-8'); + const content = await readFile(file, "utf-8"); const deps: string[] = []; for (const m of content.matchAll(fromRe)) { - if (m[1].startsWith('.')) deps.push(m[1]); + if (m[1].startsWith(".")) deps.push(m[1]); } graph[file] = deps; } }); if (depTreeAvailable) { - bench('dependency-tree', () => { - const depTree = require('dependency-tree'); + bench("dependency-tree", () => { + const depTree = require("dependency-tree"); depTree.toList({ filename: fixtures.large.entryFile, directory: fixtures.large.rootDir, @@ -202,8 +202,8 @@ describe('comparison - large (500 files)', () => { } if (tsAvailable) { - bench('ts.createProgram', () => { - const ts = require('typescript'); + bench("ts.createProgram", () => { + const ts = require("typescript"); const program = ts.createProgram([fixtures.large.entryFile], { target: ts.ScriptTarget.ES2022, module: ts.ModuleKind.ESNext, diff --git a/src/__benchmarks__/generate-fixtures.ts b/src/__benchmarks__/generate-fixtures.ts index c895f56..7de7c35 100644 --- a/src/__benchmarks__/generate-fixtures.ts +++ b/src/__benchmarks__/generate-fixtures.ts @@ -1,6 +1,6 @@ -import { mkdirSync, writeFileSync, mkdtempSync, rmSync } from 'node:fs'; -import { join } from 'node:path'; -import { tmpdir } from 'node:os'; +import { mkdirSync, writeFileSync, mkdtempSync, rmSync } from "node:fs"; +import { join } from "node:path"; +import { tmpdir } from "node:os"; export interface Fixture { rootDir: string; @@ -8,13 +8,13 @@ export interface Fixture { cleanup: () => void; } -export type FixtureSize = 'small' | 'medium' | 'large' | 'xlarge'; +export type FixtureSize = "small" | "medium" | "large" | "xlarge"; export const SIZES: Record = { - small: { fileCount: 10, label: 'small (10 files)' }, - medium: { fileCount: 100, label: 'medium (100 files)' }, - large: { fileCount: 500, label: 'large (500 files)' }, - xlarge: { fileCount: 1000, label: 'xlarge (1000 files)' }, + small: { fileCount: 10, label: "small (10 files)" }, + medium: { fileCount: 100, label: "medium (100 files)" }, + large: { fileCount: 500, label: "large (500 files)" }, + xlarge: { fileCount: 1000, label: "xlarge (1000 files)" }, }; /** @@ -25,15 +25,15 @@ function generateFileContent(index: number, imports: string[], useAlias: boolean // JSDoc header lines.push(`/**`); - lines.push(` * Module file-${String(index).padStart(4, '0')}`); + lines.push(` * Module file-${String(index).padStart(4, "0")}`); lines.push(` * Auto-generated benchmark fixture`); lines.push(` */`); - lines.push(''); + lines.push(""); // Import statements — mix static, type, and dynamic styles for (let i = 0; i < imports.length; i++) { const dep = imports[i]; - const prefix = useAlias && Math.random() < 0.1 ? '@/' : './'; + const prefix = useAlias && Math.random() < 0.1 ? "@/" : "./"; const specifier = `${prefix}${dep}`; if (i % 5 === 0 && i > 0) { @@ -51,7 +51,7 @@ function generateFileContent(index: number, imports: string[], useAlias: boolean } } - lines.push(''); + lines.push(""); // Interface lines.push(`interface Config${index} {`); @@ -59,7 +59,7 @@ function generateFileContent(index: number, imports: string[], useAlias: boolean lines.push(` enabled: boolean;`); lines.push(` count: number;`); lines.push(`}`); - lines.push(''); + lines.push(""); // Exported function lines.push(`/** Process data for module ${index} */`); @@ -67,19 +67,19 @@ function generateFileContent(index: number, imports: string[], useAlias: boolean lines.push(` const result = input.toUpperCase();`); lines.push(` return result + '-${index}';`); lines.push(`}`); - lines.push(''); + lines.push(""); // Exported constant and type lines.push(`export const value${index} = ${index};`); lines.push(`export type Type${index} = Config${index} & { id: number };`); - lines.push(''); + lines.push(""); // Filler lines to bring file to realistic size for (let f = 0; f < 10 + (index % 20); f++) { lines.push(`const _internal${f} = '${index}-${f}';`); } - return lines.join('\n'); + return lines.join("\n"); } /** @@ -93,8 +93,8 @@ function generateFileContent(index: number, imports: string[], useAlias: boolean */ export function createFixture(size: FixtureSize): Fixture { const { fileCount } = SIZES[size]; - const rootDir = mkdtempSync(join(tmpdir(), 'importree-bench-')); - const modulesDir = join(rootDir, 'modules'); + const rootDir = mkdtempSync(join(tmpdir(), "importree-bench-")); + const modulesDir = join(rootDir, "modules"); mkdirSync(modulesDir, { recursive: true }); const chainCount = Math.floor(fileCount * 0.3); @@ -102,13 +102,13 @@ export function createFixture(size: FixtureSize): Fixture { const wideCount = Math.floor(fileCount * 0.2); const standaloneCount = fileCount - chainCount - diamondCount - wideCount; - const pad = (n: number) => String(n).padStart(4, '0'); + const pad = (n: number) => String(n).padStart(4, "0"); let fileIndex = 0; // Chain: file-0 → file-1 → ... → file-(chainCount-1) for (let i = 0; i < chainCount; i++) { const deps = i < chainCount - 1 ? [`file-${pad(fileIndex + 1)}`] : []; - const content = generateFileContent(fileIndex, deps, size !== 'small'); + const content = generateFileContent(fileIndex, deps, size !== "small"); writeFileSync(join(modulesDir, `file-${pad(fileIndex)}.ts`), content); fileIndex++; } @@ -121,14 +121,19 @@ export function createFixture(size: FixtureSize): Fixture { const c = fileIndex + 2; const d = fileIndex + 3; - writeFileSync(join(modulesDir, `file-${pad(a)}.ts`), - generateFileContent(a, [`file-${pad(b)}`, `file-${pad(c)}`], false)); - writeFileSync(join(modulesDir, `file-${pad(b)}.ts`), - generateFileContent(b, [`file-${pad(d)}`], false)); - writeFileSync(join(modulesDir, `file-${pad(c)}.ts`), - generateFileContent(c, [`file-${pad(d)}`], false)); - writeFileSync(join(modulesDir, `file-${pad(d)}.ts`), - generateFileContent(d, [], false)); + writeFileSync( + join(modulesDir, `file-${pad(a)}.ts`), + generateFileContent(a, [`file-${pad(b)}`, `file-${pad(c)}`], false), + ); + writeFileSync( + join(modulesDir, `file-${pad(b)}.ts`), + generateFileContent(b, [`file-${pad(d)}`], false), + ); + writeFileSync( + join(modulesDir, `file-${pad(c)}.ts`), + generateFileContent(c, [`file-${pad(d)}`], false), + ); + writeFileSync(join(modulesDir, `file-${pad(d)}.ts`), generateFileContent(d, [], false)); fileIndex += 4; } @@ -138,14 +143,15 @@ export function createFixture(size: FixtureSize): Fixture { // Wide: barrel file imports all wide siblings const wideStart = fileIndex; for (let i = 0; i < wideCount; i++) { - writeFileSync(join(modulesDir, `file-${pad(fileIndex)}.ts`), - generateFileContent(fileIndex, [], false)); + writeFileSync( + join(modulesDir, `file-${pad(fileIndex)}.ts`), + generateFileContent(fileIndex, [], false), + ); fileIndex++; } // Barrel file const barrelImports = Array.from({ length: wideCount }, (_, i) => `file-${pad(wideStart + i)}`); - writeFileSync(join(modulesDir, `barrel.ts`), - generateFileContent(9999, barrelImports, false)); + writeFileSync(join(modulesDir, `barrel.ts`), generateFileContent(9999, barrelImports, false)); // Standalone: random 1-3 deps from already created files const totalStandalone = standaloneCount + diamondRemainder; @@ -156,15 +162,17 @@ export function createFixture(size: FixtureSize): Fixture { const depIdx = (i * 7 + d * 13) % fileIndex; // deterministic pseudo-random deps.push(`file-${pad(depIdx)}`); } - writeFileSync(join(modulesDir, `file-${pad(fileIndex)}.ts`), - generateFileContent(fileIndex, deps, size !== 'small')); + writeFileSync( + join(modulesDir, `file-${pad(fileIndex)}.ts`), + generateFileContent(fileIndex, deps, size !== "small"), + ); fileIndex++; } // Entry file: imports chain head, barrel, first diamond root, a few standalone files const entryImports: string[] = []; - if (chainCount > 0) entryImports.push('file-0000'); - entryImports.push('barrel'); + if (chainCount > 0) entryImports.push("file-0000"); + entryImports.push("barrel"); if (diamondGroups > 0) entryImports.push(`file-${pad(chainCount)}`); // first diamond root // A few standalone files for (let i = 0; i < Math.min(3, totalStandalone); i++) { @@ -172,19 +180,19 @@ export function createFixture(size: FixtureSize): Fixture { } const entryLines: string[] = [ - '/**', - ' * Entry point - benchmark fixture', - ' */', - '', + "/**", + " * Entry point - benchmark fixture", + " */", + "", ...entryImports.map((dep, i) => `import { value${i} } from './modules/${dep}';`), - '', + "", `export const main = 'entry';`, ]; - writeFileSync(join(rootDir, 'index.ts'), entryLines.join('\n')); + writeFileSync(join(rootDir, "index.ts"), entryLines.join("\n")); return { rootDir, - entryFile: join(rootDir, 'index.ts'), + entryFile: join(rootDir, "index.ts"), cleanup: () => rmSync(rootDir, { recursive: true, force: true }), }; } @@ -207,7 +215,7 @@ export function generateHeavyCommentFile(lineCount: number): string { lines.push(`import { thing${i} } from './module-${i}';`); } } - return lines.join('\n'); + return lines.join("\n"); } /** @@ -229,7 +237,7 @@ export function generateMixedImportFile(importCount: number): string { } } // Add filler - lines.push(''); - lines.push('export const main = true;'); - return lines.join('\n'); + lines.push(""); + lines.push("export const main = true;"); + return lines.join("\n"); } diff --git a/src/__benchmarks__/importree.bench.ts b/src/__benchmarks__/importree.bench.ts index 895e02e..a2cd6cd 100644 --- a/src/__benchmarks__/importree.bench.ts +++ b/src/__benchmarks__/importree.bench.ts @@ -1,47 +1,47 @@ -import { describe, bench, beforeAll, afterAll } from 'vitest'; -import { join } from 'node:path'; -import { importree } from '../index.js'; -import { createFixture, type Fixture } from './generate-fixtures.js'; +import { describe, bench, beforeAll, afterAll } from "vitest"; +import { join } from "node:path"; +import { importree } from "../index.js"; +import { createFixture, type Fixture } from "./generate-fixtures.js"; const fixtures: Record = {}; beforeAll(() => { - fixtures.small = createFixture('small'); - fixtures.medium = createFixture('medium'); - fixtures.large = createFixture('large'); - fixtures.xlarge = createFixture('xlarge'); + fixtures.small = createFixture("small"); + fixtures.medium = createFixture("medium"); + fixtures.large = createFixture("large"); + fixtures.xlarge = createFixture("xlarge"); }); afterAll(() => { for (const f of Object.values(fixtures)) f.cleanup(); }); -describe('importree', () => { - bench('small (10 files)', async () => { +describe("importree", () => { + bench("small (10 files)", async () => { await importree(fixtures.small.entryFile, { rootDir: fixtures.small.rootDir, - aliases: { '@': join(fixtures.small.rootDir, 'modules') }, + aliases: { "@": join(fixtures.small.rootDir, "modules") }, }); }); - bench('medium (100 files)', async () => { + bench("medium (100 files)", async () => { await importree(fixtures.medium.entryFile, { rootDir: fixtures.medium.rootDir, - aliases: { '@': join(fixtures.medium.rootDir, 'modules') }, + aliases: { "@": join(fixtures.medium.rootDir, "modules") }, }); }); - bench('large (500 files)', async () => { + bench("large (500 files)", async () => { await importree(fixtures.large.entryFile, { rootDir: fixtures.large.rootDir, - aliases: { '@': join(fixtures.large.rootDir, 'modules') }, + aliases: { "@": join(fixtures.large.rootDir, "modules") }, }); }); - bench('xlarge (1000 files)', async () => { + bench("xlarge (1000 files)", async () => { await importree(fixtures.xlarge.entryFile, { rootDir: fixtures.xlarge.rootDir, - aliases: { '@': join(fixtures.xlarge.rootDir, 'modules') }, + aliases: { "@": join(fixtures.xlarge.rootDir, "modules") }, }); }); }); diff --git a/src/__benchmarks__/scanner.bench.ts b/src/__benchmarks__/scanner.bench.ts index ee4fc0c..9969d63 100644 --- a/src/__benchmarks__/scanner.bench.ts +++ b/src/__benchmarks__/scanner.bench.ts @@ -1,31 +1,45 @@ -import { describe, bench } from 'vitest'; -import { scanImports, stripComments } from '../scanner.js'; -import { generateHeavyCommentFile, generateMixedImportFile } from './generate-fixtures.js'; +import { describe, bench } from "vitest"; +import { scanImports, stripComments } from "../scanner.js"; +import { generateHeavyCommentFile, generateMixedImportFile } from "./generate-fixtures.js"; -describe('stripComments', () => { +describe("stripComments", () => { const file100 = generateHeavyCommentFile(100); const file500 = generateHeavyCommentFile(500); const file1000 = generateHeavyCommentFile(1000); const file5000 = generateHeavyCommentFile(5000); - bench('100 lines', () => { stripComments(file100); }); - bench('500 lines', () => { stripComments(file500); }); - bench('1000 lines', () => { stripComments(file1000); }); - bench('5000 lines', () => { stripComments(file5000); }); + bench("100 lines", () => { + stripComments(file100); + }); + bench("500 lines", () => { + stripComments(file500); + }); + bench("1000 lines", () => { + stripComments(file1000); + }); + bench("5000 lines", () => { + stripComments(file5000); + }); }); -describe('scanImports', () => { +describe("scanImports", () => { const few = [ `import { a } from './a';`, `import { b } from './b';`, `import { c } from './c';`, `export const x = 1;`, - ].join('\n'); + ].join("\n"); const twenty = generateMixedImportFile(20); const fifty = generateMixedImportFile(50); - bench('3 imports', () => { scanImports(few); }); - bench('20 mixed imports', () => { scanImports(twenty); }); - bench('50 mixed imports', () => { scanImports(fifty); }); + bench("3 imports", () => { + scanImports(few); + }); + bench("20 mixed imports", () => { + scanImports(twenty); + }); + bench("50 mixed imports", () => { + scanImports(fifty); + }); }); diff --git a/src/__tests__/fixtures/aliases/src/entry.ts b/src/__tests__/fixtures/aliases/src/entry.ts index 0766084..6953e76 100644 --- a/src/__tests__/fixtures/aliases/src/entry.ts +++ b/src/__tests__/fixtures/aliases/src/entry.ts @@ -1,3 +1,3 @@ -import { utils } from '@/utils'; +import { utils } from "@/utils"; export const main = utils; diff --git a/src/__tests__/fixtures/aliases/src/utils.ts b/src/__tests__/fixtures/aliases/src/utils.ts index 5ebf410..3a5ec53 100644 --- a/src/__tests__/fixtures/aliases/src/utils.ts +++ b/src/__tests__/fixtures/aliases/src/utils.ts @@ -1 +1 @@ -export const utils = 'utils'; +export const utils = "utils"; diff --git a/src/__tests__/fixtures/basic/entry.ts b/src/__tests__/fixtures/basic/entry.ts index 83a6702..c0252d7 100644 --- a/src/__tests__/fixtures/basic/entry.ts +++ b/src/__tests__/fixtures/basic/entry.ts @@ -1,3 +1,3 @@ -import { foo } from './dep'; +import { foo } from "./dep"; export const bar = foo + 1; diff --git a/src/__tests__/fixtures/chain/a.ts b/src/__tests__/fixtures/chain/a.ts index 0eec946..a07068e 100644 --- a/src/__tests__/fixtures/chain/a.ts +++ b/src/__tests__/fixtures/chain/a.ts @@ -1,3 +1,3 @@ -import { b } from './b'; +import { b } from "./b"; export const a = b + 1; diff --git a/src/__tests__/fixtures/chain/b.ts b/src/__tests__/fixtures/chain/b.ts index bf6036d..cd24e08 100644 --- a/src/__tests__/fixtures/chain/b.ts +++ b/src/__tests__/fixtures/chain/b.ts @@ -1,3 +1,3 @@ -import { c } from './c'; +import { c } from "./c"; export const b = c + 1; diff --git a/src/__tests__/fixtures/circular/a.ts b/src/__tests__/fixtures/circular/a.ts index 6e24f5e..fc1656a 100644 --- a/src/__tests__/fixtures/circular/a.ts +++ b/src/__tests__/fixtures/circular/a.ts @@ -1,3 +1,3 @@ -import { b } from './b'; +import { b } from "./b"; -export const a = 'a' + b; +export const a = "a" + b; diff --git a/src/__tests__/fixtures/circular/b.ts b/src/__tests__/fixtures/circular/b.ts index 8562efd..eb7436c 100644 --- a/src/__tests__/fixtures/circular/b.ts +++ b/src/__tests__/fixtures/circular/b.ts @@ -1,3 +1,3 @@ -import { a } from './a'; +import { a } from "./a"; -export const b = 'b' + a; +export const b = "b" + a; diff --git a/src/__tests__/fixtures/comments/entry.ts b/src/__tests__/fixtures/comments/entry.ts index 431e897..1536fa7 100644 --- a/src/__tests__/fixtures/comments/entry.ts +++ b/src/__tests__/fixtures/comments/entry.ts @@ -1,6 +1,6 @@ // import { fake } from './fake'; /* import { alsoFake } from './also-fake'; */ -import { real } from './real'; +import { real } from "./real"; /** * import { commentedOut } from './commented-out'; diff --git a/src/__tests__/fixtures/comments/real.ts b/src/__tests__/fixtures/comments/real.ts index 0b593a7..2cc0b91 100644 --- a/src/__tests__/fixtures/comments/real.ts +++ b/src/__tests__/fixtures/comments/real.ts @@ -1 +1 @@ -export const real = 'real'; +export const real = "real"; diff --git a/src/__tests__/fixtures/diamond/a.ts b/src/__tests__/fixtures/diamond/a.ts index 0b09809..0b3a3a2 100644 --- a/src/__tests__/fixtures/diamond/a.ts +++ b/src/__tests__/fixtures/diamond/a.ts @@ -1,4 +1,4 @@ -import { b } from './b'; -import { c } from './c'; +import { b } from "./b"; +import { c } from "./c"; export const a = b + c; diff --git a/src/__tests__/fixtures/diamond/b.ts b/src/__tests__/fixtures/diamond/b.ts index 92af63e..9463df0 100644 --- a/src/__tests__/fixtures/diamond/b.ts +++ b/src/__tests__/fixtures/diamond/b.ts @@ -1,3 +1,3 @@ -import { d } from './d'; +import { d } from "./d"; export const b = d + 1; diff --git a/src/__tests__/fixtures/diamond/c.ts b/src/__tests__/fixtures/diamond/c.ts index 9b938d2..4075fc2 100644 --- a/src/__tests__/fixtures/diamond/c.ts +++ b/src/__tests__/fixtures/diamond/c.ts @@ -1,3 +1,3 @@ -import { d } from './d'; +import { d } from "./d"; export const c = d + 2; diff --git a/src/__tests__/fixtures/dynamic/entry.ts b/src/__tests__/fixtures/dynamic/entry.ts index 62c0bcd..9622303 100644 --- a/src/__tests__/fixtures/dynamic/entry.ts +++ b/src/__tests__/fixtures/dynamic/entry.ts @@ -1,4 +1,4 @@ export async function loadLazy() { - const lazy = await import('./lazy'); + const lazy = await import("./lazy"); return lazy; } diff --git a/src/__tests__/fixtures/dynamic/lazy.ts b/src/__tests__/fixtures/dynamic/lazy.ts index 8f97c98..9cd62f1 100644 --- a/src/__tests__/fixtures/dynamic/lazy.ts +++ b/src/__tests__/fixtures/dynamic/lazy.ts @@ -1 +1 @@ -export const lazy = 'lazy-loaded'; +export const lazy = "lazy-loaded"; diff --git a/src/__tests__/fixtures/externals/entry.ts b/src/__tests__/fixtures/externals/entry.ts index ab47bf3..ee1faa1 100644 --- a/src/__tests__/fixtures/externals/entry.ts +++ b/src/__tests__/fixtures/externals/entry.ts @@ -1,5 +1,5 @@ -import lodash from 'lodash'; -import { useState } from 'react'; -import { local } from './local'; +import lodash from "lodash"; +import { useState } from "react"; +import { local } from "./local"; export const main = local; diff --git a/src/__tests__/fixtures/externals/local.ts b/src/__tests__/fixtures/externals/local.ts index 35dae12..4dc99d7 100644 --- a/src/__tests__/fixtures/externals/local.ts +++ b/src/__tests__/fixtures/externals/local.ts @@ -1,3 +1,3 @@ -import { join } from 'node:path'; +import { join } from "node:path"; -export const local = 'local'; +export const local = "local"; diff --git a/src/__tests__/fixtures/index-resolution/entry.ts b/src/__tests__/fixtures/index-resolution/entry.ts index 7aad572..93e648b 100644 --- a/src/__tests__/fixtures/index-resolution/entry.ts +++ b/src/__tests__/fixtures/index-resolution/entry.ts @@ -1,3 +1,3 @@ -import { util } from './utils'; +import { util } from "./utils"; export const main = util; diff --git a/src/__tests__/fixtures/index-resolution/utils/helper.ts b/src/__tests__/fixtures/index-resolution/utils/helper.ts index 276ec41..6119c1e 100644 --- a/src/__tests__/fixtures/index-resolution/utils/helper.ts +++ b/src/__tests__/fixtures/index-resolution/utils/helper.ts @@ -1 +1 @@ -export const helper = 'helper'; +export const helper = "helper"; diff --git a/src/__tests__/fixtures/index-resolution/utils/index.ts b/src/__tests__/fixtures/index-resolution/utils/index.ts index 6d1cff4..55f9868 100644 --- a/src/__tests__/fixtures/index-resolution/utils/index.ts +++ b/src/__tests__/fixtures/index-resolution/utils/index.ts @@ -1,3 +1,3 @@ -import { helper } from './helper'; +import { helper } from "./helper"; export const util = helper; diff --git a/src/__tests__/fixtures/mixed/dynamic-dep.ts b/src/__tests__/fixtures/mixed/dynamic-dep.ts index c34b9f5..2d8497a 100644 --- a/src/__tests__/fixtures/mixed/dynamic-dep.ts +++ b/src/__tests__/fixtures/mixed/dynamic-dep.ts @@ -1 +1 @@ -export const dynamicDep = 'dynamic'; +export const dynamicDep = "dynamic"; diff --git a/src/__tests__/fixtures/mixed/entry.ts b/src/__tests__/fixtures/mixed/entry.ts index 9158030..a013e2d 100644 --- a/src/__tests__/fixtures/mixed/entry.ts +++ b/src/__tests__/fixtures/mixed/entry.ts @@ -1,10 +1,10 @@ -import { staticDep } from './static-dep'; -import type { SomeType } from './types'; -import './side-effect'; +import { staticDep } from "./static-dep"; +import type { SomeType } from "./types"; +import "./side-effect"; -const dynamic = import('./dynamic-dep'); -const cjs = require('./cjs-dep'); +const dynamic = import("./dynamic-dep"); +const cjs = require("./cjs-dep"); -export { everything } from './reexport'; +export { everything } from "./reexport"; export const main = staticDep; diff --git a/src/__tests__/fixtures/mixed/reexport.ts b/src/__tests__/fixtures/mixed/reexport.ts index 1b40f14..0935f18 100644 --- a/src/__tests__/fixtures/mixed/reexport.ts +++ b/src/__tests__/fixtures/mixed/reexport.ts @@ -1 +1 @@ -export const everything = 'reexported'; +export const everything = "reexported"; diff --git a/src/__tests__/fixtures/mixed/side-effect.ts b/src/__tests__/fixtures/mixed/side-effect.ts index 75bb07d..1e6e90d 100644 --- a/src/__tests__/fixtures/mixed/side-effect.ts +++ b/src/__tests__/fixtures/mixed/side-effect.ts @@ -1 +1 @@ -console.log('side effect'); +console.log("side effect"); diff --git a/src/__tests__/fixtures/mixed/static-dep.ts b/src/__tests__/fixtures/mixed/static-dep.ts index 66d177a..cd7b6f1 100644 --- a/src/__tests__/fixtures/mixed/static-dep.ts +++ b/src/__tests__/fixtures/mixed/static-dep.ts @@ -1 +1 @@ -export const staticDep = 'static'; +export const staticDep = "static"; diff --git a/src/__tests__/fixtures/reexports/a.ts b/src/__tests__/fixtures/reexports/a.ts index 8d9809e..c60cb36 100644 --- a/src/__tests__/fixtures/reexports/a.ts +++ b/src/__tests__/fixtures/reexports/a.ts @@ -1 +1 @@ -export const everything = 'a'; +export const everything = "a"; diff --git a/src/__tests__/fixtures/reexports/b.ts b/src/__tests__/fixtures/reexports/b.ts index 137b8ce..59d1689 100644 --- a/src/__tests__/fixtures/reexports/b.ts +++ b/src/__tests__/fixtures/reexports/b.ts @@ -1 +1 @@ -export const b = 'b'; +export const b = "b"; diff --git a/src/__tests__/fixtures/reexports/barrel.ts b/src/__tests__/fixtures/reexports/barrel.ts index c43a897..536d8c4 100644 --- a/src/__tests__/fixtures/reexports/barrel.ts +++ b/src/__tests__/fixtures/reexports/barrel.ts @@ -1,2 +1,2 @@ -export * from './a'; -export { b } from './b'; +export * from "./a"; +export { b } from "./b"; diff --git a/src/__tests__/fixtures/reexports/entry.ts b/src/__tests__/fixtures/reexports/entry.ts index 3158a1e..38f8c8a 100644 --- a/src/__tests__/fixtures/reexports/entry.ts +++ b/src/__tests__/fixtures/reexports/entry.ts @@ -1,3 +1,3 @@ -import { everything } from './barrel'; +import { everything } from "./barrel"; export const main = everything; diff --git a/src/__tests__/fixtures/require-cjs/entry.ts b/src/__tests__/fixtures/require-cjs/entry.ts index b16b852..55ee539 100644 --- a/src/__tests__/fixtures/require-cjs/entry.ts +++ b/src/__tests__/fixtures/require-cjs/entry.ts @@ -1,3 +1,3 @@ -const helper = require('./helper'); +const helper = require("./helper"); export const main = helper; diff --git a/src/__tests__/fixtures/strings/entry.ts b/src/__tests__/fixtures/strings/entry.ts index d101fac..4980d79 100644 --- a/src/__tests__/fixtures/strings/entry.ts +++ b/src/__tests__/fixtures/strings/entry.ts @@ -1,4 +1,4 @@ -import { real } from './real'; +import { real } from "./real"; const fakeImport = "import { fake } from './fake'"; const anotherFake = `import { also } from './also-fake'`; diff --git a/src/__tests__/fixtures/strings/real.ts b/src/__tests__/fixtures/strings/real.ts index 0b593a7..2cc0b91 100644 --- a/src/__tests__/fixtures/strings/real.ts +++ b/src/__tests__/fixtures/strings/real.ts @@ -1 +1 @@ -export const real = 'real'; +export const real = "real"; diff --git a/src/__tests__/fixtures/type-imports/entry.ts b/src/__tests__/fixtures/type-imports/entry.ts index 836c4b2..198185e 100644 --- a/src/__tests__/fixtures/type-imports/entry.ts +++ b/src/__tests__/fixtures/type-imports/entry.ts @@ -1,3 +1,3 @@ -import type { Foo } from './types'; +import type { Foo } from "./types"; -export const value: Foo = 'hello'; +export const value: Foo = "hello"; diff --git a/src/index.test.ts b/src/index.test.ts index e83c69c..64414e3 100644 --- a/src/index.test.ts +++ b/src/index.test.ts @@ -1,90 +1,90 @@ -import { describe, it, expect } from 'vitest'; -import { resolve, join } from 'node:path'; -import { importree, getAffectedFiles } from './index.js'; +import { describe, it, expect } from "vitest"; +import { resolve, join } from "node:path"; +import { importree, getAffectedFiles } from "./index.js"; -const fixturesDir = resolve(import.meta.dirname, '__tests__/fixtures'); +const fixturesDir = resolve(import.meta.dirname, "__tests__/fixtures"); const f = (...parts: string[]): string => join(fixturesDir, ...parts); -describe('getAffectedFiles', () => { - it('returns empty array for unknown file', async () => { - const tree = await importree(f('basic', 'entry.ts')); - const affected = getAffectedFiles(tree, '/nonexistent/file.ts'); +describe("getAffectedFiles", () => { + it("returns empty array for unknown file", async () => { + const tree = await importree(f("basic", "entry.ts")); + const affected = getAffectedFiles(tree, "/nonexistent/file.ts"); expect(affected).toEqual([]); }); - it('returns direct dependents', async () => { - const tree = await importree(f('basic', 'entry.ts')); - const affected = getAffectedFiles(tree, f('basic', 'dep.ts')); + it("returns direct dependents", async () => { + const tree = await importree(f("basic", "entry.ts")); + const affected = getAffectedFiles(tree, f("basic", "dep.ts")); - expect(affected).toContain(f('basic', 'entry.ts')); + expect(affected).toContain(f("basic", "entry.ts")); }); - it('returns transitive dependents', async () => { - const tree = await importree(f('chain', 'a.ts')); - const affected = getAffectedFiles(tree, f('chain', 'c.ts')); + it("returns transitive dependents", async () => { + const tree = await importree(f("chain", "a.ts")); + const affected = getAffectedFiles(tree, f("chain", "c.ts")); // c changed → b depends on c → a depends on b - expect(affected).toContain(f('chain', 'b.ts')); - expect(affected).toContain(f('chain', 'a.ts')); + expect(affected).toContain(f("chain", "b.ts")); + expect(affected).toContain(f("chain", "a.ts")); }); - it('handles diamond dependency', async () => { - const tree = await importree(f('diamond', 'a.ts')); - const affected = getAffectedFiles(tree, f('diamond', 'd.ts')); + it("handles diamond dependency", async () => { + const tree = await importree(f("diamond", "a.ts")); + const affected = getAffectedFiles(tree, f("diamond", "d.ts")); // d changed → b, c depend on d → a depends on b and c - expect(affected).toContain(f('diamond', 'b.ts')); - expect(affected).toContain(f('diamond', 'c.ts')); - expect(affected).toContain(f('diamond', 'a.ts')); + expect(affected).toContain(f("diamond", "b.ts")); + expect(affected).toContain(f("diamond", "c.ts")); + expect(affected).toContain(f("diamond", "a.ts")); }); - it('handles circular dependency', async () => { - const tree = await importree(f('circular', 'a.ts')); - const affected = getAffectedFiles(tree, f('circular', 'a.ts')); + it("handles circular dependency", async () => { + const tree = await importree(f("circular", "a.ts")); + const affected = getAffectedFiles(tree, f("circular", "a.ts")); // a changed → b depends on a, but a itself should NOT be included - expect(affected).toContain(f('circular', 'b.ts')); - expect(affected).not.toContain(f('circular', 'a.ts')); + expect(affected).toContain(f("circular", "b.ts")); + expect(affected).not.toContain(f("circular", "a.ts")); }); - it('does not include the changed file itself', async () => { - const tree = await importree(f('chain', 'a.ts')); - const affected = getAffectedFiles(tree, f('chain', 'c.ts')); + it("does not include the changed file itself", async () => { + const tree = await importree(f("chain", "a.ts")); + const affected = getAffectedFiles(tree, f("chain", "c.ts")); - expect(affected).not.toContain(f('chain', 'c.ts')); + expect(affected).not.toContain(f("chain", "c.ts")); }); - it('returns sorted results', async () => { - const tree = await importree(f('diamond', 'a.ts')); - const affected = getAffectedFiles(tree, f('diamond', 'd.ts')); + it("returns sorted results", async () => { + const tree = await importree(f("diamond", "a.ts")); + const affected = getAffectedFiles(tree, f("diamond", "d.ts")); const sorted = [...affected].sort(); expect(affected).toEqual(sorted); }); - it('returns empty for entry file with no dependents', async () => { - const tree = await importree(f('chain', 'a.ts')); - const affected = getAffectedFiles(tree, f('chain', 'a.ts')); + it("returns empty for entry file with no dependents", async () => { + const tree = await importree(f("chain", "a.ts")); + const affected = getAffectedFiles(tree, f("chain", "a.ts")); // a is the root — nothing depends on it expect(affected).toEqual([]); }); - it('skips nodes not present in reverseGraph during BFS', async () => { - const tree: import('./index.js').ImportTree = { - entrypoint: '/a.ts', - files: ['/a.ts', '/b.ts'], + it("skips nodes not present in reverseGraph during BFS", async () => { + const tree: import("./index.js").ImportTree = { + entrypoint: "/a.ts", + files: ["/a.ts", "/b.ts"], externals: [], - graph: { '/a.ts': ['/b.ts'], '/b.ts': [] }, - reverseGraph: { '/a.ts': [], '/b.ts': ['/a.ts'] }, + graph: { "/a.ts": ["/b.ts"], "/b.ts": [] }, + reverseGraph: { "/a.ts": [], "/b.ts": ["/a.ts"] }, }; // Manually add an entry that points to a node not in reverseGraph - tree.reverseGraph['/b.ts'] = ['/a.ts', '/phantom.ts']; - const affected = getAffectedFiles(tree, '/b.ts'); + tree.reverseGraph["/b.ts"] = ["/a.ts", "/phantom.ts"]; + const affected = getAffectedFiles(tree, "/b.ts"); // Should include /a.ts and /phantom.ts, but not crash when /phantom.ts has no reverseGraph entry - expect(affected).toContain('/a.ts'); - expect(affected).toContain('/phantom.ts'); + expect(affected).toContain("/a.ts"); + expect(affected).toContain("/phantom.ts"); }); }); diff --git a/src/index.ts b/src/index.ts index d62b7ac..2028b7a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,8 +1,8 @@ -import { resolve } from 'node:path'; -import type { ImportreeOptions, ImportTree } from './types.js'; -import { walk } from './walker.js'; +import { resolve } from "node:path"; +import type { ImportreeOptions, ImportTree } from "./types.js"; +import { walk } from "./walker.js"; -export type { ImportreeOptions, ImportTree } from './types.js'; +export type { ImportreeOptions, ImportTree } from "./types.js"; /** * Builds a full import dependency tree starting from an entry file. @@ -21,10 +21,7 @@ export type { ImportreeOptions, ImportTree } from './types.js'; * console.log(tree.graph); // file → direct dependencies * ``` */ -export async function importree( - entry: string, - options?: ImportreeOptions, -): Promise { +export async function importree(entry: string, options?: ImportreeOptions): Promise { return walk(entry, options ?? {}); } @@ -35,10 +32,7 @@ export async function importree( * * The changed file itself is NOT included in the result. */ -export function getAffectedFiles( - tree: ImportTree, - changedFile: string, -): string[] { +export function getAffectedFiles(tree: ImportTree, changedFile: string): string[] { const absolute = resolve(changedFile); if (!tree.reverseGraph[absolute]) return []; diff --git a/src/resolver.test.ts b/src/resolver.test.ts index c65c710..8dbd0ca 100644 --- a/src/resolver.test.ts +++ b/src/resolver.test.ts @@ -1,161 +1,159 @@ -import { describe, it, expect } from 'vitest'; -import { resolve, join } from 'node:path'; -import { createResolver } from './resolver.js'; +import { describe, it, expect } from "vitest"; +import { resolve, join } from "node:path"; +import { createResolver } from "./resolver.js"; -const fixturesDir = resolve(import.meta.dirname, '__tests__/fixtures'); +const fixturesDir = resolve(import.meta.dirname, "__tests__/fixtures"); -describe('createResolver', () => { - it('resolves relative import with extension', () => { +describe("createResolver", () => { + it("resolves relative import with extension", () => { const resolver = createResolver(fixturesDir, {}); - const fromFile = join(fixturesDir, 'basic', 'entry.ts'); - const result = resolver('./dep', fromFile); + const fromFile = join(fixturesDir, "basic", "entry.ts"); + const result = resolver("./dep", fromFile); expect(result).toEqual({ - type: 'local', - absolutePath: join(fixturesDir, 'basic', 'dep.ts'), + type: "local", + absolutePath: join(fixturesDir, "basic", "dep.ts"), }); }); - it('resolves relative import trying extensions', () => { + it("resolves relative import trying extensions", () => { const resolver = createResolver(fixturesDir, {}); - const fromFile = join(fixturesDir, 'chain', 'a.ts'); - const result = resolver('./b', fromFile); + const fromFile = join(fixturesDir, "chain", "a.ts"); + const result = resolver("./b", fromFile); - expect(result?.type).toBe('local'); - expect(result?.absolutePath).toBe(join(fixturesDir, 'chain', 'b.ts')); + expect(result?.type).toBe("local"); + expect(result?.absolutePath).toBe(join(fixturesDir, "chain", "b.ts")); }); - it('resolves directory import to index file', () => { + it("resolves directory import to index file", () => { const resolver = createResolver(fixturesDir, {}); - const fromFile = join(fixturesDir, 'index-resolution', 'entry.ts'); - const result = resolver('./utils', fromFile); + const fromFile = join(fixturesDir, "index-resolution", "entry.ts"); + const result = resolver("./utils", fromFile); - expect(result?.type).toBe('local'); - expect(result?.absolutePath).toBe( - join(fixturesDir, 'index-resolution', 'utils', 'index.ts'), - ); + expect(result?.type).toBe("local"); + expect(result?.absolutePath).toBe(join(fixturesDir, "index-resolution", "utils", "index.ts")); }); - it('resolves alias imports', () => { - const aliasBase = join(fixturesDir, 'aliases'); + it("resolves alias imports", () => { + const aliasBase = join(fixturesDir, "aliases"); const resolver = createResolver(aliasBase, { - aliases: { '@': './src' }, + aliases: { "@": "./src" }, }); - const fromFile = join(aliasBase, 'src', 'entry.ts'); - const result = resolver('@/utils', fromFile); + const fromFile = join(aliasBase, "src", "entry.ts"); + const result = resolver("@/utils", fromFile); - expect(result?.type).toBe('local'); - expect(result?.absolutePath).toBe(join(aliasBase, 'src', 'utils.ts')); + expect(result?.type).toBe("local"); + expect(result?.absolutePath).toBe(join(aliasBase, "src", "utils.ts")); }); - it('classifies bare specifiers as external', () => { + it("classifies bare specifiers as external", () => { const resolver = createResolver(fixturesDir, {}); - const fromFile = join(fixturesDir, 'externals', 'entry.ts'); - const result = resolver('lodash', fromFile); + const fromFile = join(fixturesDir, "externals", "entry.ts"); + const result = resolver("lodash", fromFile); expect(result).toEqual({ - type: 'external', - specifier: 'lodash', + type: "external", + specifier: "lodash", }); }); - it('classifies scoped packages as external', () => { + it("classifies scoped packages as external", () => { const resolver = createResolver(fixturesDir, {}); - const fromFile = join(fixturesDir, 'externals', 'entry.ts'); - const result = resolver('@scope/pkg/path', fromFile); + const fromFile = join(fixturesDir, "externals", "entry.ts"); + const result = resolver("@scope/pkg/path", fromFile); expect(result).toEqual({ - type: 'external', - specifier: '@scope/pkg', + type: "external", + specifier: "@scope/pkg", }); }); - it('classifies node: built-ins as external', () => { + it("classifies node: built-ins as external", () => { const resolver = createResolver(fixturesDir, {}); - const fromFile = join(fixturesDir, 'externals', 'local.ts'); - const result = resolver('node:path', fromFile); + const fromFile = join(fixturesDir, "externals", "local.ts"); + const result = resolver("node:path", fromFile); expect(result).toEqual({ - type: 'external', - specifier: 'node:path', + type: "external", + specifier: "node:path", }); }); - it('uses longest-match alias', () => { - const aliasBase = join(fixturesDir, 'aliases'); + it("uses longest-match alias", () => { + const aliasBase = join(fixturesDir, "aliases"); const resolver = createResolver(aliasBase, { aliases: { - '@': './src', - '@/utils': './src/utils', + "@": "./src", + "@/utils": "./src/utils", }, }); - const fromFile = join(aliasBase, 'src', 'entry.ts'); + const fromFile = join(aliasBase, "src", "entry.ts"); // @/utils should match the longer alias first - const result = resolver('@/utils', fromFile); - expect(result?.type).toBe('local'); + const result = resolver("@/utils", fromFile); + expect(result?.type).toBe("local"); }); - it('returns undefined for unresolvable relative import', () => { + it("returns undefined for unresolvable relative import", () => { const resolver = createResolver(fixturesDir, {}); - const fromFile = join(fixturesDir, 'basic', 'entry.ts'); - const result = resolver('./nonexistent', fromFile); + const fromFile = join(fixturesDir, "basic", "entry.ts"); + const result = resolver("./nonexistent", fromFile); expect(result).toBeUndefined(); }); - it('caches resolved paths', () => { + it("caches resolved paths", () => { const resolver = createResolver(fixturesDir, {}); - const fromFile = join(fixturesDir, 'basic', 'entry.ts'); + const fromFile = join(fixturesDir, "basic", "entry.ts"); - const result1 = resolver('./dep', fromFile); - const result2 = resolver('./dep', fromFile); + const result1 = resolver("./dep", fromFile); + const result2 = resolver("./dep", fromFile); // Should return the same object (cached) expect(result1).toBe(result2); }); - it('handles scoped package with no subpath', () => { + it("handles scoped package with no subpath", () => { const resolver = createResolver(fixturesDir, {}); - const fromFile = join(fixturesDir, 'externals', 'entry.ts'); - const result = resolver('@scope', fromFile); + const fromFile = join(fixturesDir, "externals", "entry.ts"); + const result = resolver("@scope", fromFile); expect(result).toEqual({ - type: 'external', - specifier: '@scope', + type: "external", + specifier: "@scope", }); }); - it('returns undefined for alias that matches but file does not exist', () => { - const aliasBase = join(fixturesDir, 'aliases'); + it("returns undefined for alias that matches but file does not exist", () => { + const aliasBase = join(fixturesDir, "aliases"); const resolver = createResolver(aliasBase, { - aliases: { '@': './src' }, + aliases: { "@": "./src" }, }); - const fromFile = join(aliasBase, 'src', 'entry.ts'); - const result = resolver('@/nonexistent', fromFile); + const fromFile = join(aliasBase, "src", "entry.ts"); + const result = resolver("@/nonexistent", fromFile); expect(result).toBeUndefined(); }); - it('resolves exact alias without subpath', () => { - const aliasBase = join(fixturesDir, 'aliases'); + it("resolves exact alias without subpath", () => { + const aliasBase = join(fixturesDir, "aliases"); const resolver = createResolver(aliasBase, { - aliases: { '@': './src' }, + aliases: { "@": "./src" }, }); - const fromFile = join(aliasBase, 'src', 'entry.ts'); + const fromFile = join(aliasBase, "src", "entry.ts"); // '@' exactly matches the alias key, no subpath - const result = resolver('@', fromFile); + const result = resolver("@", fromFile); // ./src is a directory, should resolve to ./src/entry.ts via extension probing expect(result).toBeUndefined(); }); - it('resolves import with explicit extension (exact path match)', () => { + it("resolves import with explicit extension (exact path match)", () => { const resolver = createResolver(fixturesDir, {}); - const fromFile = join(fixturesDir, 'basic', 'entry.ts'); - const result = resolver('./dep.ts', fromFile); + const fromFile = join(fixturesDir, "basic", "entry.ts"); + const result = resolver("./dep.ts", fromFile); - expect(result?.type).toBe('local'); - expect(result?.absolutePath).toBe(join(fixturesDir, 'basic', 'dep.ts')); + expect(result?.type).toBe("local"); + expect(result?.absolutePath).toBe(join(fixturesDir, "basic", "dep.ts")); }); }); diff --git a/src/resolver.ts b/src/resolver.ts index 1094df9..a08b263 100644 --- a/src/resolver.ts +++ b/src/resolver.ts @@ -1,8 +1,8 @@ -import { statSync } from 'node:fs'; -import { dirname, join, resolve, isAbsolute } from 'node:path'; -import type { ImportreeOptions, ResolvedImport } from './types.js'; +import { statSync } from "node:fs"; +import { dirname, join, resolve, isAbsolute } from "node:path"; +import type { ImportreeOptions, ResolvedImport } from "./types.js"; -const DEFAULT_EXTENSIONS = ['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs']; +const DEFAULT_EXTENSIONS = [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"]; function fileExists(filePath: string): boolean { const stat = statSync(filePath, { throwIfNoEntry: false }); @@ -20,17 +20,14 @@ function dirExists(filePath: string): boolean { * - Unscoped: `pkg/path` → `pkg` */ function getBareSpecifier(specifier: string): string { - if (specifier.startsWith('@')) { - const parts = specifier.split('/'); + if (specifier.startsWith("@")) { + const parts = specifier.split("/"); return parts.length >= 2 ? `${parts[0]}/${parts[1]}` : specifier; } - return specifier.split('/')[0]; + return specifier.split("/")[0]; } -function resolveFile( - filePath: string, - extensions: string[], -): string | undefined { +function resolveFile(filePath: string, extensions: string[]): string | undefined { // Try exact path if (fileExists(filePath)) return filePath; @@ -59,10 +56,7 @@ export interface Resolver { * Creates a resolver function that resolves import specifiers to absolute * file paths, with support for aliases and extension probing. */ -export function createResolver( - basedir: string, - options: ImportreeOptions, -): Resolver { +export function createResolver(basedir: string, options: ImportreeOptions): Resolver { const extensions = options.extensions ?? DEFAULT_EXTENSIONS; // Sort aliases by key length descending for longest-prefix matching @@ -70,10 +64,9 @@ export function createResolver( ? Object.entries(options.aliases).sort((a, b) => b[0].length - a[0].length) : []; - const resolvedAliasValues = aliases.map(([key, value]) => [ - key, - isAbsolute(value) ? value : resolve(basedir, value), - ] as const); + const resolvedAliasValues = aliases.map( + ([key, value]) => [key, isAbsolute(value) ? value : resolve(basedir, value)] as const, + ); const cache = new Map(); @@ -89,21 +82,21 @@ export function createResolver( let result: ResolvedImport | undefined; // Relative import - if (specifier.startsWith('./') || specifier.startsWith('../')) { + if (specifier.startsWith("./") || specifier.startsWith("../")) { const absolutePath = resolveFile(resolve(fromDir, specifier), extensions); if (absolutePath) { - result = { type: 'local', absolutePath }; + result = { type: "local", absolutePath }; } } // Check aliases else { let matched = false; for (const [prefix, replacement] of resolvedAliasValues) { - if (specifier === prefix || specifier.startsWith(prefix + '/')) { - const rest = specifier === prefix ? '' : specifier.slice(prefix.length); + if (specifier === prefix || specifier.startsWith(prefix + "/")) { + const rest = specifier === prefix ? "" : specifier.slice(prefix.length); const absolutePath = resolveFile(join(replacement, rest), extensions); if (absolutePath) { - result = { type: 'local', absolutePath }; + result = { type: "local", absolutePath }; } matched = true; break; @@ -112,7 +105,7 @@ export function createResolver( // Bare specifier → external if (!matched) { - result = { type: 'external', specifier: getBareSpecifier(specifier) }; + result = { type: "external", specifier: getBareSpecifier(specifier) }; } } diff --git a/src/scanner.test.ts b/src/scanner.test.ts index fe4f5eb..6926533 100644 --- a/src/scanner.test.ts +++ b/src/scanner.test.ts @@ -1,151 +1,151 @@ -import { describe, it, expect } from 'vitest'; -import { stripComments, scanImports } from './scanner.js'; +import { describe, it, expect } from "vitest"; +import { stripComments, scanImports } from "./scanner.js"; -describe('stripComments', () => { - it('removes single-line comments', () => { - const result = stripComments('const a = 1; // comment\nconst b = 2;'); - expect(result).not.toContain('comment'); - expect(result).toContain('const a = 1;'); - expect(result).toContain('const b = 2;'); +describe("stripComments", () => { + it("removes single-line comments", () => { + const result = stripComments("const a = 1; // comment\nconst b = 2;"); + expect(result).not.toContain("comment"); + expect(result).toContain("const a = 1;"); + expect(result).toContain("const b = 2;"); }); - it('removes block comments', () => { - const result = stripComments('const a = 1; /* block comment */ const b = 2;'); - expect(result).not.toContain('block comment'); - expect(result).toContain('const a = 1;'); - expect(result).toContain('const b = 2;'); + it("removes block comments", () => { + const result = stripComments("const a = 1; /* block comment */ const b = 2;"); + expect(result).not.toContain("block comment"); + expect(result).toContain("const a = 1;"); + expect(result).toContain("const b = 2;"); }); - it('removes multi-line block comments', () => { - const result = stripComments('const a = 1;\n/* line1\nline2\nline3 */\nconst b = 2;'); - expect(result).not.toContain('line1'); - expect(result).toContain('const a = 1;'); - expect(result).toContain('const b = 2;'); + it("removes multi-line block comments", () => { + const result = stripComments("const a = 1;\n/* line1\nline2\nline3 */\nconst b = 2;"); + expect(result).not.toContain("line1"); + expect(result).toContain("const a = 1;"); + expect(result).toContain("const b = 2;"); }); - it('preserves string content', () => { + it("preserves string content", () => { const result = stripComments("const s = 'hello world';"); - expect(result).toContain('hello world'); + expect(result).toContain("hello world"); }); - it('handles escaped quotes in strings', () => { + it("handles escaped quotes in strings", () => { const result = stripComments("const s = 'it\\'s a test'; const a = 1;"); - expect(result).toContain('const a = 1;'); + expect(result).toContain("const a = 1;"); }); - it('does not strip comment-like patterns inside strings', () => { + it("does not strip comment-like patterns inside strings", () => { const code = "const s = '// not a comment'; import { a } from './a';"; const result = stripComments(code); expect(result).toContain("from './a'"); - expect(result).toContain('// not a comment'); + expect(result).toContain("// not a comment"); }); - it('handles template literals with interpolation', () => { - const code = 'const s = `hello ${world} test`;'; + it("handles template literals with interpolation", () => { + const code = "const s = `hello ${world} test`;"; const result = stripComments(code); - expect(result).toContain('`'); - expect(result).toContain('hello'); + expect(result).toContain("`"); + expect(result).toContain("hello"); }); - it('handles comment-like patterns inside template literals', () => { - const code = 'const s = `// not a comment`; const a = 1;'; + it("handles comment-like patterns inside template literals", () => { + const code = "const s = `// not a comment`; const a = 1;"; const result = stripComments(code); - expect(result).toContain('// not a comment'); - expect(result).toContain('const a = 1;'); + expect(result).toContain("// not a comment"); + expect(result).toContain("const a = 1;"); }); - it('handles escaped characters inside template literals', () => { - const code = 'const s = `hello \\n world`; const a = 1;'; + it("handles escaped characters inside template literals", () => { + const code = "const s = `hello \\n world`; const a = 1;"; const result = stripComments(code); - expect(result).toContain('hello \\n world'); - expect(result).toContain('const a = 1;'); + expect(result).toContain("hello \\n world"); + expect(result).toContain("const a = 1;"); }); - it('handles unterminated block comment', () => { - const code = 'const a = 1; /* unterminated'; + it("handles unterminated block comment", () => { + const code = "const a = 1; /* unterminated"; const result = stripComments(code); - expect(result).toContain('const a = 1;'); - expect(result).not.toContain('unterminated'); + expect(result).toContain("const a = 1;"); + expect(result).not.toContain("unterminated"); }); - it('handles unterminated string literal', () => { + it("handles unterminated string literal", () => { const code = "const s = 'unterminated"; const result = stripComments(code); - expect(result).toContain('unterminated'); + expect(result).toContain("unterminated"); }); }); -describe('scanImports', () => { - it('extracts static named import', () => { - expect(scanImports("import { foo } from './foo';")).toContain('./foo'); +describe("scanImports", () => { + it("extracts static named import", () => { + expect(scanImports("import { foo } from './foo';")).toContain("./foo"); }); - it('extracts static default import', () => { - expect(scanImports("import foo from './foo';")).toContain('./foo'); + it("extracts static default import", () => { + expect(scanImports("import foo from './foo';")).toContain("./foo"); }); - it('extracts namespace import', () => { - expect(scanImports("import * as foo from './foo';")).toContain('./foo'); + it("extracts namespace import", () => { + expect(scanImports("import * as foo from './foo';")).toContain("./foo"); }); - it('extracts side-effect import', () => { - expect(scanImports("import './side-effect';")).toContain('./side-effect'); + it("extracts side-effect import", () => { + expect(scanImports("import './side-effect';")).toContain("./side-effect"); }); - it('extracts type import', () => { - expect(scanImports("import type { Foo } from './types';")).toContain('./types'); + it("extracts type import", () => { + expect(scanImports("import type { Foo } from './types';")).toContain("./types"); }); - it('extracts dynamic import', () => { - expect(scanImports("const m = import('./lazy');")).toContain('./lazy'); + it("extracts dynamic import", () => { + expect(scanImports("const m = import('./lazy');")).toContain("./lazy"); }); - it('extracts dynamic import with await', () => { - expect(scanImports("const m = await import('./lazy');")).toContain('./lazy'); + it("extracts dynamic import with await", () => { + expect(scanImports("const m = await import('./lazy');")).toContain("./lazy"); }); - it('extracts require()', () => { - expect(scanImports("const m = require('./cjs');")).toContain('./cjs'); + it("extracts require()", () => { + expect(scanImports("const m = require('./cjs');")).toContain("./cjs"); }); - it('extracts export from', () => { - expect(scanImports("export { foo } from './foo';")).toContain('./foo'); + it("extracts export from", () => { + expect(scanImports("export { foo } from './foo';")).toContain("./foo"); }); - it('extracts export * from', () => { - expect(scanImports("export * from './all';")).toContain('./all'); + it("extracts export * from", () => { + expect(scanImports("export * from './all';")).toContain("./all"); }); - it('extracts export type from', () => { - expect(scanImports("export type { Foo } from './types';")).toContain('./types'); + it("extracts export type from", () => { + expect(scanImports("export type { Foo } from './types';")).toContain("./types"); }); - it('deduplicates specifiers', () => { + it("deduplicates specifiers", () => { const code = "import { a } from './x';\nimport { b } from './x';"; const result = scanImports(code); - expect(result.filter((s) => s === './x')).toHaveLength(1); + expect(result.filter((s) => s === "./x")).toHaveLength(1); }); - it('ignores imports inside line comments', () => { + it("ignores imports inside line comments", () => { const code = "// import { fake } from './fake';\nimport { real } from './real';"; const result = scanImports(code); - expect(result).toContain('./real'); - expect(result).not.toContain('./fake'); + expect(result).toContain("./real"); + expect(result).not.toContain("./fake"); }); - it('ignores imports inside block comments', () => { + it("ignores imports inside block comments", () => { const code = "/* import { fake } from './fake'; */\nimport { real } from './real';"; const result = scanImports(code); - expect(result).toContain('./real'); - expect(result).not.toContain('./fake'); + expect(result).toContain("./real"); + expect(result).not.toContain("./fake"); }); - it('handles multi-line import statement', () => { + it("handles multi-line import statement", () => { const code = "import {\n foo,\n bar,\n} from './multi';"; - expect(scanImports(code)).toContain('./multi'); + expect(scanImports(code)).toContain("./multi"); }); - it('extracts multiple different specifiers', () => { + it("extracts multiple different specifiers", () => { const code = ` import { a } from './a'; import b from './b'; @@ -154,17 +154,17 @@ describe('scanImports', () => { const e = require('./e'); `; const result = scanImports(code); - expect(result).toContain('./a'); - expect(result).toContain('./b'); - expect(result).toContain('./c'); - expect(result).toContain('./d'); - expect(result).toContain('./e'); + expect(result).toContain("./a"); + expect(result).toContain("./b"); + expect(result).toContain("./c"); + expect(result).toContain("./d"); + expect(result).toContain("./e"); }); - it('handles external package specifiers', () => { + it("handles external package specifiers", () => { const code = "import lodash from 'lodash';\nimport { join } from 'node:path';"; const result = scanImports(code); - expect(result).toContain('lodash'); - expect(result).toContain('node:path'); + expect(result).toContain("lodash"); + expect(result).toContain("node:path"); }); }); diff --git a/src/scanner.ts b/src/scanner.ts index 297a1c8..6d596ca 100644 --- a/src/scanner.ts +++ b/src/scanner.ts @@ -13,29 +13,29 @@ export function stripComments(code: string): string { while (i < len) { const ch = code[i]; - const next = i + 1 < len ? code[i + 1] : ''; + const next = i + 1 < len ? code[i + 1] : ""; // Line comment → blank to end of line - if (ch === '/' && next === '/') { - result[i++] = ' '; - result[i++] = ' '; - while (i < len && code[i] !== '\n') { - result[i++] = ' '; + if (ch === "/" && next === "/") { + result[i++] = " "; + result[i++] = " "; + while (i < len && code[i] !== "\n") { + result[i++] = " "; } continue; } // Block comment → blank to closing */ - if (ch === '/' && next === '*') { - result[i++] = ' '; - result[i++] = ' '; - while (i < len && !(code[i] === '*' && i + 1 < len && code[i + 1] === '/')) { - result[i] = code[i] === '\n' ? '\n' : ' '; + if (ch === "/" && next === "*") { + result[i++] = " "; + result[i++] = " "; + while (i < len && !(code[i] === "*" && i + 1 < len && code[i + 1] === "/")) { + result[i] = code[i] === "\n" ? "\n" : " "; i++; } if (i < len) { - result[i++] = ' '; // * - result[i++] = ' '; // / + result[i++] = " "; // * + result[i++] = " "; // / } continue; } @@ -47,7 +47,7 @@ export function stripComments(code: string): string { result[i] = code[i]; i++; while (i < len && code[i] !== quote) { - if (code[i] === '\\' && i + 1 < len) { + if (code[i] === "\\" && i + 1 < len) { result[i] = code[i]; i++; result[i] = code[i]; @@ -65,27 +65,27 @@ export function stripComments(code: string): string { } // Template literal — copy verbatim, handling ${} nesting - if (ch === '`') { + if (ch === "`") { result[i] = code[i]; i++; let depth = 0; while (i < len) { - if (code[i] === '\\' && i + 1 < len) { + if (code[i] === "\\" && i + 1 < len) { result[i] = code[i]; i++; result[i] = code[i]; i++; - } else if (code[i] === '$' && i + 1 < len && code[i + 1] === '{') { + } else if (code[i] === "$" && i + 1 < len && code[i + 1] === "{") { result[i] = code[i]; i++; result[i] = code[i]; i++; depth++; - } else if (code[i] === '}' && depth > 0) { + } else if (code[i] === "}" && depth > 0) { result[i] = code[i]; i++; depth--; - } else if (code[i] === '`' && depth === 0) { + } else if (code[i] === "`" && depth === 0) { result[i] = code[i]; i++; break; @@ -102,7 +102,7 @@ export function stripComments(code: string): string { i++; } - return result.join(''); + return result.join(""); } // Static regex patterns — compiled once diff --git a/src/types.ts b/src/types.ts index 3f1e60a..55d9713 100644 --- a/src/types.ts +++ b/src/types.ts @@ -55,7 +55,7 @@ export interface ImportTree { * @internal */ export interface ResolvedImport { - type: 'local' | 'external'; + type: "local" | "external"; /** Absolute file path (only for local imports). */ absolutePath?: string; /** Bare specifier / package name (only for external imports). */ diff --git a/src/walker.test.ts b/src/walker.test.ts index b3fec89..e6681bc 100644 --- a/src/walker.test.ts +++ b/src/walker.test.ts @@ -1,154 +1,150 @@ -import { describe, it, expect } from 'vitest'; -import { resolve, join } from 'node:path'; -import { importree } from './index.js'; +import { describe, it, expect } from "vitest"; +import { resolve, join } from "node:path"; +import { importree } from "./index.js"; -const fixturesDir = resolve(import.meta.dirname, '__tests__/fixtures'); +const fixturesDir = resolve(import.meta.dirname, "__tests__/fixtures"); const f = (...parts: string[]): string => join(fixturesDir, ...parts); -describe('importree', () => { - it('resolves basic single import', async () => { - const tree = await importree(f('basic', 'entry.ts')); +describe("importree", () => { + it("resolves basic single import", async () => { + const tree = await importree(f("basic", "entry.ts")); - expect(tree.entrypoint).toBe(f('basic', 'entry.ts')); - expect(tree.files).toEqual([f('basic', 'dep.ts'), f('basic', 'entry.ts')]); + expect(tree.entrypoint).toBe(f("basic", "entry.ts")); + expect(tree.files).toEqual([f("basic", "dep.ts"), f("basic", "entry.ts")]); expect(tree.externals).toEqual([]); - expect(tree.graph[f('basic', 'entry.ts')]).toEqual([f('basic', 'dep.ts')]); - expect(tree.graph[f('basic', 'dep.ts')]).toEqual([]); + expect(tree.graph[f("basic", "entry.ts")]).toEqual([f("basic", "dep.ts")]); + expect(tree.graph[f("basic", "dep.ts")]).toEqual([]); }); - it('resolves transitive chain a → b → c', async () => { - const tree = await importree(f('chain', 'a.ts')); + it("resolves transitive chain a → b → c", async () => { + const tree = await importree(f("chain", "a.ts")); - expect(tree.files).toEqual([ - f('chain', 'a.ts'), - f('chain', 'b.ts'), - f('chain', 'c.ts'), - ]); - expect(tree.graph[f('chain', 'a.ts')]).toEqual([f('chain', 'b.ts')]); - expect(tree.graph[f('chain', 'b.ts')]).toEqual([f('chain', 'c.ts')]); - expect(tree.graph[f('chain', 'c.ts')]).toEqual([]); + expect(tree.files).toEqual([f("chain", "a.ts"), f("chain", "b.ts"), f("chain", "c.ts")]); + expect(tree.graph[f("chain", "a.ts")]).toEqual([f("chain", "b.ts")]); + expect(tree.graph[f("chain", "b.ts")]).toEqual([f("chain", "c.ts")]); + expect(tree.graph[f("chain", "c.ts")]).toEqual([]); }); - it('handles circular imports without infinite loop', async () => { - const tree = await importree(f('circular', 'a.ts')); + it("handles circular imports without infinite loop", async () => { + const tree = await importree(f("circular", "a.ts")); expect(tree.files).toHaveLength(2); - expect(tree.files).toContain(f('circular', 'a.ts')); - expect(tree.files).toContain(f('circular', 'b.ts')); - expect(tree.graph[f('circular', 'a.ts')]).toContain(f('circular', 'b.ts')); - expect(tree.graph[f('circular', 'b.ts')]).toContain(f('circular', 'a.ts')); + expect(tree.files).toContain(f("circular", "a.ts")); + expect(tree.files).toContain(f("circular", "b.ts")); + expect(tree.graph[f("circular", "a.ts")]).toContain(f("circular", "b.ts")); + expect(tree.graph[f("circular", "b.ts")]).toContain(f("circular", "a.ts")); }); - it('resolves aliased imports', async () => { - const aliasBase = f('aliases'); - const tree = await importree(f('aliases', 'src', 'entry.ts'), { - aliases: { '@': join(aliasBase, 'src') }, + it("resolves aliased imports", async () => { + const aliasBase = f("aliases"); + const tree = await importree(f("aliases", "src", "entry.ts"), { + aliases: { "@": join(aliasBase, "src") }, }); - expect(tree.files).toContain(f('aliases', 'src', 'utils.ts')); + expect(tree.files).toContain(f("aliases", "src", "utils.ts")); }); - it('resolves relative aliases using rootDir', async () => { - const aliasBase = f('aliases'); - const tree = await importree(f('aliases', 'src', 'entry.ts'), { + it("resolves relative aliases using rootDir", async () => { + const aliasBase = f("aliases"); + const tree = await importree(f("aliases", "src", "entry.ts"), { rootDir: aliasBase, - aliases: { '@': './src' }, + aliases: { "@": "./src" }, }); - expect(tree.files).toContain(f('aliases', 'src', 'utils.ts')); + expect(tree.files).toContain(f("aliases", "src", "utils.ts")); }); - it('classifies external packages', async () => { - const tree = await importree(f('externals', 'entry.ts')); + it("classifies external packages", async () => { + const tree = await importree(f("externals", "entry.ts")); - expect(tree.externals).toContain('lodash'); - expect(tree.externals).toContain('react'); - expect(tree.externals).toContain('node:path'); - expect(tree.files).toContain(f('externals', 'local.ts')); + expect(tree.externals).toContain("lodash"); + expect(tree.externals).toContain("react"); + expect(tree.externals).toContain("node:path"); + expect(tree.files).toContain(f("externals", "local.ts")); }); - it('follows dynamic imports', async () => { - const tree = await importree(f('dynamic', 'entry.ts')); + it("follows dynamic imports", async () => { + const tree = await importree(f("dynamic", "entry.ts")); - expect(tree.files).toContain(f('dynamic', 'lazy.ts')); - expect(tree.graph[f('dynamic', 'entry.ts')]).toContain(f('dynamic', 'lazy.ts')); + expect(tree.files).toContain(f("dynamic", "lazy.ts")); + expect(tree.graph[f("dynamic", "entry.ts")]).toContain(f("dynamic", "lazy.ts")); }); - it('follows require() calls', async () => { - const tree = await importree(f('require-cjs', 'entry.ts')); + it("follows require() calls", async () => { + const tree = await importree(f("require-cjs", "entry.ts")); - expect(tree.files).toContain(f('require-cjs', 'helper.ts')); + expect(tree.files).toContain(f("require-cjs", "helper.ts")); }); - it('follows re-exports', async () => { - const tree = await importree(f('reexports', 'entry.ts')); + it("follows re-exports", async () => { + const tree = await importree(f("reexports", "entry.ts")); - expect(tree.files).toContain(f('reexports', 'barrel.ts')); - expect(tree.files).toContain(f('reexports', 'a.ts')); - expect(tree.files).toContain(f('reexports', 'b.ts')); - expect(tree.graph[f('reexports', 'barrel.ts')]).toContain(f('reexports', 'a.ts')); - expect(tree.graph[f('reexports', 'barrel.ts')]).toContain(f('reexports', 'b.ts')); + expect(tree.files).toContain(f("reexports", "barrel.ts")); + expect(tree.files).toContain(f("reexports", "a.ts")); + expect(tree.files).toContain(f("reexports", "b.ts")); + expect(tree.graph[f("reexports", "barrel.ts")]).toContain(f("reexports", "a.ts")); + expect(tree.graph[f("reexports", "barrel.ts")]).toContain(f("reexports", "b.ts")); }); - it('resolves directory to index file', async () => { - const tree = await importree(f('index-resolution', 'entry.ts')); + it("resolves directory to index file", async () => { + const tree = await importree(f("index-resolution", "entry.ts")); - expect(tree.files).toContain(f('index-resolution', 'utils', 'index.ts')); - expect(tree.files).toContain(f('index-resolution', 'utils', 'helper.ts')); + expect(tree.files).toContain(f("index-resolution", "utils", "index.ts")); + expect(tree.files).toContain(f("index-resolution", "utils", "helper.ts")); }); - it('includes type imports', async () => { - const tree = await importree(f('type-imports', 'entry.ts')); + it("includes type imports", async () => { + const tree = await importree(f("type-imports", "entry.ts")); - expect(tree.files).toContain(f('type-imports', 'types.ts')); + expect(tree.files).toContain(f("type-imports", "types.ts")); }); - it('ignores imports inside comments', async () => { - const tree = await importree(f('comments', 'entry.ts')); + it("ignores imports inside comments", async () => { + const tree = await importree(f("comments", "entry.ts")); - expect(tree.files).toContain(f('comments', 'real.ts')); + expect(tree.files).toContain(f("comments", "real.ts")); // Should not attempt to find ./fake or ./also-fake expect(tree.files).toHaveLength(2); // entry + real only }); - it('ignores imports inside string literals', async () => { - const tree = await importree(f('strings', 'entry.ts')); + it("ignores imports inside string literals", async () => { + const tree = await importree(f("strings", "entry.ts")); - expect(tree.files).toContain(f('strings', 'real.ts')); + expect(tree.files).toContain(f("strings", "real.ts")); expect(tree.files).toHaveLength(2); // entry + real only }); - it('handles diamond dependency correctly', async () => { - const tree = await importree(f('diamond', 'a.ts')); + it("handles diamond dependency correctly", async () => { + const tree = await importree(f("diamond", "a.ts")); expect(tree.files).toEqual([ - f('diamond', 'a.ts'), - f('diamond', 'b.ts'), - f('diamond', 'c.ts'), - f('diamond', 'd.ts'), + f("diamond", "a.ts"), + f("diamond", "b.ts"), + f("diamond", "c.ts"), + f("diamond", "d.ts"), ]); // d appears in both b and c's deps - expect(tree.graph[f('diamond', 'b.ts')]).toContain(f('diamond', 'd.ts')); - expect(tree.graph[f('diamond', 'c.ts')]).toContain(f('diamond', 'd.ts')); + expect(tree.graph[f("diamond", "b.ts")]).toContain(f("diamond", "d.ts")); + expect(tree.graph[f("diamond", "c.ts")]).toContain(f("diamond", "d.ts")); }); - it('computes reverseGraph correctly', async () => { - const tree = await importree(f('chain', 'a.ts')); + it("computes reverseGraph correctly", async () => { + const tree = await importree(f("chain", "a.ts")); - expect(tree.reverseGraph[f('chain', 'c.ts')]).toContain(f('chain', 'b.ts')); - expect(tree.reverseGraph[f('chain', 'b.ts')]).toContain(f('chain', 'a.ts')); - expect(tree.reverseGraph[f('chain', 'a.ts')]).toEqual([]); + expect(tree.reverseGraph[f("chain", "c.ts")]).toContain(f("chain", "b.ts")); + expect(tree.reverseGraph[f("chain", "b.ts")]).toContain(f("chain", "a.ts")); + expect(tree.reverseGraph[f("chain", "a.ts")]).toEqual([]); }); - it('handles mixed import patterns', async () => { - const tree = await importree(f('mixed', 'entry.ts')); + it("handles mixed import patterns", async () => { + const tree = await importree(f("mixed", "entry.ts")); - expect(tree.files).toContain(f('mixed', 'static-dep.ts')); - expect(tree.files).toContain(f('mixed', 'types.ts')); - expect(tree.files).toContain(f('mixed', 'side-effect.ts')); - expect(tree.files).toContain(f('mixed', 'dynamic-dep.ts')); - expect(tree.files).toContain(f('mixed', 'cjs-dep.ts')); - expect(tree.files).toContain(f('mixed', 'reexport.ts')); + expect(tree.files).toContain(f("mixed", "static-dep.ts")); + expect(tree.files).toContain(f("mixed", "types.ts")); + expect(tree.files).toContain(f("mixed", "side-effect.ts")); + expect(tree.files).toContain(f("mixed", "dynamic-dep.ts")); + expect(tree.files).toContain(f("mixed", "cjs-dep.ts")); + expect(tree.files).toContain(f("mixed", "reexport.ts")); expect(tree.files).toHaveLength(7); // entry + 6 deps }); }); diff --git a/src/walker.ts b/src/walker.ts index 70ef262..2f3fb72 100644 --- a/src/walker.ts +++ b/src/walker.ts @@ -1,17 +1,14 @@ -import { readFile } from 'node:fs/promises'; -import { resolve } from 'node:path'; -import type { ImportreeOptions, ImportTree } from './types.js'; -import { scanImports } from './scanner.js'; -import { createResolver } from './resolver.js'; +import { readFile } from "node:fs/promises"; +import { resolve } from "node:path"; +import type { ImportreeOptions, ImportTree } from "./types.js"; +import { scanImports } from "./scanner.js"; +import { createResolver } from "./resolver.js"; /** * Recursively walks imports starting from an entry file and builds * the full dependency tree. */ -export async function walk( - entryFile: string, - options: ImportreeOptions, -): Promise { +export async function walk(entryFile: string, options: ImportreeOptions): Promise { const entrypoint = resolve(entryFile); const basedir = options.rootDir ? resolve(options.rootDir) : process.cwd(); const resolveSpecifier = createResolver(basedir, options); @@ -24,7 +21,7 @@ export async function walk( if (visited.has(filePath)) return; visited.add(filePath); - const content = await readFile(filePath, 'utf-8'); + const content = await readFile(filePath, "utf-8"); const specifiers = scanImports(content); const localDeps: string[] = []; @@ -32,9 +29,9 @@ export async function walk( const resolved = resolveSpecifier(spec, filePath); if (!resolved) continue; - if (resolved.type === 'external' && resolved.specifier) { + if (resolved.type === "external" && resolved.specifier) { externals.add(resolved.specifier); - } else if (resolved.type === 'local' && resolved.absolutePath) { + } else if (resolved.type === "local" && resolved.absolutePath) { localDeps.push(resolved.absolutePath); } } diff --git a/tsconfig.json b/tsconfig.json index 23533a2..160d520 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -13,5 +13,5 @@ "outDir": "./dist" }, "include": ["src"], - "exclude": ["node_modules", "dist"] + "exclude": ["node_modules", "dist", "src/__tests__/fixtures", "src/__benchmarks__"] } diff --git a/tsdown.config.ts b/tsdown.config.ts index e61baa8..2c8e215 100644 --- a/tsdown.config.ts +++ b/tsdown.config.ts @@ -1,9 +1,9 @@ -import { defineConfig } from 'tsdown' +import { defineConfig } from "tsdown"; export default defineConfig({ - entry: 'src/index.ts', - format: ['esm', 'cjs'], + entry: "src/index.ts", + format: ["esm", "cjs"], dts: true, sourcemap: true, clean: true, -}) +}); diff --git a/vitest.config.ts b/vitest.config.ts index d36f1d8..ea761cc 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -1,10 +1,26 @@ -import { defineConfig } from 'vitest/config'; +import { defineConfig } from "vitest/config"; export default defineConfig({ test: { - include: ['src/**/*.test.ts'], + include: ["src/**/*.test.ts"], benchmark: { - include: ['src/**/*.bench.ts'], + include: ["src/**/*.bench.ts"], + }, + coverage: { + provider: "v8", + include: ["src/**/*.ts"], + exclude: [ + "src/**/*.test.ts", + "src/__tests__/**", + "src/__benchmarks__/**", + "src/types.ts", + ], + thresholds: { + lines: 99, + functions: 99, + branches: 95, + statements: 99, + }, }, }, });