├── .gitignore ├── .gitattributes ├── .c8rc.json ├── .github ├── codeql │ └── codeql-config.yml ├── dependabot.yml └── workflows │ ├── lint.yml │ ├── ci.yml │ └── codeql.yml ├── .mocharc.json ├── .editorconfig ├── LICENSE ├── README.md ├── index.js ├── package.json └── test └── test.js /.gitignore: -------------------------------------------------------------------------------- 1 | /coverage/ 2 | /node_modules/ 3 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Enforce Unix newlines 2 | * text=auto eol=lf 3 | -------------------------------------------------------------------------------- /.c8rc.json: -------------------------------------------------------------------------------- 1 | { 2 | "reporter": [ 3 | "html", 4 | "lcovonly", 5 | "text" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /.github/codeql/codeql-config.yml: -------------------------------------------------------------------------------- 1 | name: "CodeQL config" 2 | paths-ignore: 3 | - "test/fixtures/**" 4 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: monthly 7 | -------------------------------------------------------------------------------- /.mocharc.json: -------------------------------------------------------------------------------- 1 | { 2 | "check-leaks": true, 3 | "throw-deprecation": true, 4 | "trace-deprecation": true, 5 | "trace-warnings": true, 6 | "use-strict": true 7 | } 8 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | charset = utf-8 7 | end_of_line = lf 8 | indent_size = 2 9 | indent_style = space 10 | insert_final_newline = true 11 | trim_trailing_whitespace = true 12 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Lint 2 | 3 | on: 4 | push: 5 | branches: 6 | - "main" 7 | pull_request: 8 | workflow_dispatch: 9 | 10 | env: 11 | FORCE_COLOR: 2 12 | NODE: 20 13 | 14 | permissions: 15 | contents: read 16 | 17 | jobs: 18 | lint: 19 | runs-on: ubuntu-latest 20 | 21 | steps: 22 | - name: Clone repository 23 | uses: actions/checkout@v4 24 | with: 25 | persist-credentials: false 26 | 27 | - name: Set up Node.js 28 | uses: actions/setup-node@v4 29 | with: 30 | node-version: ${{ env.NODE }} 31 | cache: npm 32 | 33 | - name: Install npm dependencies 34 | run: npm ci 35 | 36 | - name: Lint 37 | run: npm run lint 38 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - "main" 7 | pull_request: 8 | workflow_dispatch: 9 | 10 | env: 11 | FORCE_COLOR: 2 12 | 13 | permissions: 14 | contents: read 15 | 16 | jobs: 17 | test: 18 | name: Node ${{ matrix.node }} on ${{ matrix.os }} 19 | runs-on: ${{ matrix.os }} 20 | 21 | strategy: 22 | fail-fast: false 23 | matrix: 24 | os: [ubuntu-latest, windows-latest] 25 | node: [18, 20, 22] 26 | 27 | steps: 28 | - name: Clone repository 29 | uses: actions/checkout@v4 30 | with: 31 | persist-credentials: false 32 | 33 | - name: Set up Node.js 34 | uses: actions/setup-node@v4 35 | with: 36 | node-version: ${{ matrix.node }} 37 | cache: npm 38 | 39 | - name: Install npm dependencies 40 | run: npm ci 41 | 42 | - name: Run unit tests 43 | run: npm run test:ci 44 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | name: "CodeQL" 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | schedule: 11 | - cron: "0 0 * * 0" 12 | workflow_dispatch: 13 | 14 | jobs: 15 | analyze: 16 | name: Analyze 17 | runs-on: ubuntu-latest 18 | permissions: 19 | actions: read 20 | contents: read 21 | security-events: write 22 | 23 | steps: 24 | - name: Clone repository 25 | uses: actions/checkout@v4 26 | with: 27 | persist-credentials: false 28 | 29 | - name: Initialize CodeQL 30 | uses: github/codeql-action/init@v3 31 | with: 32 | config-file: ./.github/codeql/codeql-config.yml 33 | languages: "javascript" 34 | queries: +security-and-quality 35 | 36 | - name: Autobuild 37 | uses: github/codeql-action/autobuild@v3 38 | 39 | - name: Perform CodeQL Analysis 40 | uses: github/codeql-action/analyze@v3 41 | with: 42 | category: "/language:javascript" 43 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Dependents 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # detective-es6 2 | 3 | [![CI](https://img.shields.io/github/actions/workflow/status/dependents/node-detective-es6/ci.yml?branch=main&label=CI&logo=github)](https://github.com/dependents/node-detective-es6/actions/workflows/ci.yml?query=branch%3Amain) 4 | [![npm version](https://img.shields.io/npm/v/detective-es6?logo=npm&logoColor=fff)](https://www.npmjs.com/package/detective-es6) 5 | [![npm downloads](https://img.shields.io/npm/dm/detective-es6)](https://www.npmjs.com/package/detective-es6) 6 | 7 | > Get the dependencies of an ES6 module 8 | 9 | ```sh 10 | npm install detective-es6 11 | ``` 12 | 13 | ## Usage 14 | 15 | ```js 16 | const fs = require('fs'); 17 | const detective = require('detective-es6'); 18 | 19 | const mySourceCode = fs.readFileSync('myfile.js', 'utf8'); 20 | 21 | // Pass in a file's content or an AST 22 | const dependencies = detective(mySourceCode); 23 | ``` 24 | 25 | * Supports JSX, Flow, and any other features that [node-source-walk](https://github.com/dependents/node-source-walk) supports. 26 | 27 | You may also (optionally) configure the detective via a second object argument detective(src, options) that supports the following options: 28 | 29 | - `skipTypeImports`: (Boolean) whether or not to omit type imports (`import type {foo} from "mylib";`) in the list of extracted dependencies. 30 | - `skipAsyncImports`: (Boolean) whether or not to omit async imports (`import('foo')`) in the list of extracted dependencies. 31 | 32 | ## License 33 | 34 | [MIT](LICENSE) 35 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Walker = require('node-source-walk'); 4 | 5 | /** 6 | * Extracts the dependencies of the supplied es6 module 7 | * 8 | * @param {String|Object} src - File's content or AST 9 | * @param {Object} options - optional extra settings 10 | * @return {String[]} 11 | */ 12 | module.exports = function(src, options = {}) { 13 | if (src === undefined) throw new Error('src not given'); 14 | if (src === '') return []; 15 | 16 | const walker = new Walker(); 17 | const dependencies = []; 18 | 19 | walker.walk(src, node => { 20 | switch (node.type) { 21 | case 'ImportDeclaration': { 22 | if (options.skipTypeImports && node.importKind === 'type') { 23 | break; 24 | } 25 | 26 | if (node.source?.value) { 27 | dependencies.push(node.source.value); 28 | } 29 | 30 | break; 31 | } 32 | 33 | case 'ExportNamedDeclaration': 34 | case 'ExportAllDeclaration': { 35 | if (node.source?.value) { 36 | dependencies.push(node.source.value); 37 | } 38 | 39 | break; 40 | } 41 | 42 | case 'CallExpression': { 43 | if (options.skipAsyncImports) { 44 | break; 45 | } 46 | 47 | if (node.callee.type === 'Import' && node.arguments?.[0].value) { 48 | dependencies.push(node.arguments?.[0].value); 49 | } 50 | 51 | break; 52 | } 53 | 54 | default: 55 | // nothing 56 | } 57 | }); 58 | 59 | return dependencies; 60 | }; 61 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "detective-es6", 3 | "version": "5.0.1", 4 | "description": "Get the dependencies of an ES6 module", 5 | "main": "index.js", 6 | "files": [ 7 | "index.js" 8 | ], 9 | "scripts": { 10 | "lint": "xo", 11 | "fix": "xo --fix", 12 | "mocha": "mocha", 13 | "test": "npm run lint && npm run mocha", 14 | "test:ci": "c8 npm run mocha" 15 | }, 16 | "repository": { 17 | "type": "git", 18 | "url": "git+https://github.com/dependents/node-detective-es6.git" 19 | }, 20 | "keywords": [ 21 | "detective", 22 | "es6", 23 | "es2015", 24 | "dependencies", 25 | "module", 26 | "ast", 27 | "import" 28 | ], 29 | "author": "Joel Kemp ", 30 | "license": "MIT", 31 | "bugs": { 32 | "url": "https://github.com/dependents/node-detective-es6/issues" 33 | }, 34 | "homepage": "https://github.com/dependents/node-detective-es6", 35 | "engines": { 36 | "node": ">=18" 37 | }, 38 | "dependencies": { 39 | "node-source-walk": "^7.0.1" 40 | }, 41 | "devDependencies": { 42 | "c8": "^10.1.3", 43 | "mocha": "^11.1.0", 44 | "xo": "^0.60.0" 45 | }, 46 | "xo": { 47 | "space": true, 48 | "ignores": [ 49 | "test/fixtures/*" 50 | ], 51 | "rules": { 52 | "arrow-body-style": "off", 53 | "capitalized-comments": "off", 54 | "comma-dangle": [ 55 | "error", 56 | "never" 57 | ], 58 | "curly": [ 59 | "error", 60 | "multi-line" 61 | ], 62 | "operator-linebreak": [ 63 | "error", 64 | "after" 65 | ], 66 | "object-curly-spacing": [ 67 | "error", 68 | "always" 69 | ], 70 | "space-before-function-paren": [ 71 | "error", 72 | "never" 73 | ], 74 | "unicorn/no-anonymous-default-export": "off", 75 | "unicorn/prefer-module": "off", 76 | "unicorn/prefer-node-protocol": "off", 77 | "unicorn/prefer-top-level-await": "off", 78 | "unicorn/prevent-abbreviations": "off" 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | 3 | 'use strict'; 4 | 5 | const assert = require('assert').strict; 6 | const detective = require('../index.js'); 7 | 8 | const ast = { 9 | type: 'Program', 10 | body: [{ 11 | type: 'VariableDeclaration', 12 | declarations: [{ 13 | type: 'VariableDeclarator', 14 | id: { 15 | type: 'Identifier', 16 | name: 'x' 17 | }, 18 | init: { 19 | type: 'Literal', 20 | value: 4, 21 | raw: '4' 22 | } 23 | }], 24 | kind: 'let' 25 | }] 26 | }; 27 | 28 | describe('detective-es6', () => { 29 | it('accepts an ast', () => { 30 | const deps = detective(ast); 31 | assert.equal(deps.length, 0); 32 | }); 33 | 34 | it('retrieves the dependencies of es6 modules', () => { 35 | const deps = detective('import {foo, bar} from "mylib";'); 36 | assert.equal(deps.length, 1); 37 | assert.equal(deps[0], 'mylib'); 38 | }); 39 | 40 | it('retrieves the re-export dependencies of es6 modules', () => { 41 | const deps = detective('export {foo, bar} from "mylib";'); 42 | assert.equal(deps.length, 1); 43 | assert.equal(deps[0], 'mylib'); 44 | }); 45 | 46 | it('retrieves the re-export * dependencies of es6 modules', () => { 47 | const deps = detective('export * from "mylib";'); 48 | assert.equal(deps.length, 1); 49 | assert.equal(deps[0], 'mylib'); 50 | }); 51 | 52 | it('handles multiple imports', () => { 53 | const deps = detective('import {foo, bar} from "mylib";\nimport "mylib2"'); 54 | 55 | assert.equal(deps.length, 2); 56 | assert.equal(deps[0], 'mylib'); 57 | assert.equal(deps[1], 'mylib2'); 58 | }); 59 | 60 | it('handles default imports', () => { 61 | const deps = detective('import foo from "foo";'); 62 | 63 | assert.equal(deps.length, 1); 64 | assert.equal(deps[0], 'foo'); 65 | }); 66 | 67 | it('handles dynamic imports', () => { 68 | const deps = detective('import("foo").then(foo => foo());'); 69 | 70 | assert.equal(deps.length, 1); 71 | assert.equal(deps[0], 'foo'); 72 | }); 73 | 74 | it('returns an empty list for non-es6 modules', () => { 75 | const deps = detective('var foo = require("foo");'); 76 | assert.equal(deps.length, 0); 77 | }); 78 | 79 | it('returns an empty list for empty files', () => { 80 | const deps = detective(''); 81 | assert.equal(deps.length, 0); 82 | }); 83 | 84 | it('throws when content is not provided', () => { 85 | assert.throws(() => { 86 | detective(); 87 | }, /^Error: src not given$/); 88 | }); 89 | 90 | it('does not throw with jsx in a module', () => { 91 | assert.doesNotThrow(() => { 92 | detective('import foo from "foo"; var templ = ;'); 93 | }); 94 | }); 95 | 96 | it('does not throw on an async ES7 function', () => { 97 | assert.doesNotThrow(() => { 98 | detective('import foo from "foo"; export default async function baz() {}'); 99 | }); 100 | }); 101 | 102 | it('respects settings for type imports', () => { 103 | const source = 'import type {foo} from "mylib";'; 104 | const depsWithTypes = detective(source); 105 | const depsWithoutTypes = detective(source, { skipTypeImports: true }); 106 | assert.deepEqual(depsWithTypes, ['mylib']); 107 | assert.deepEqual(depsWithoutTypes, []); 108 | }); 109 | 110 | it('respects settings for async imports', () => { 111 | const source = 'import("myLib")'; 112 | const depsWithAsync = detective(source); 113 | const depsWithoutAsync = detective(source, { skipAsyncImports: true }); 114 | assert.deepEqual(depsWithAsync, ['myLib']); 115 | assert.deepEqual(depsWithoutAsync, []); 116 | }); 117 | 118 | it('respects settings for async imports with multiple imports', () => { 119 | const source = 'import("myLib");\nimport foo from "foo"'; 120 | const depsWithoutAsync = detective(source, { skipAsyncImports: true }); 121 | assert.deepEqual(depsWithoutAsync, ['foo']); 122 | }); 123 | 124 | it('skips variable imports', () => { 125 | const deps = detective('var baz = "bar"; import(baz);'); 126 | assert.equal(deps.length, 0); 127 | }); 128 | }); 129 | --------------------------------------------------------------------------------