├── .c8rc.json ├── .editorconfig ├── .gitattributes ├── .github ├── codeql │ └── codeql-config.yml ├── dependabot.yml └── workflows │ ├── ci.yml │ ├── codeql.yml │ └── lint.yml ├── .gitignore ├── .mocharc.json ├── LICENSE ├── README.md ├── bin └── cli.js ├── index.d.ts ├── index.js ├── package-lock.json ├── package.json └── test ├── fixtures ├── Gruntfile.js ├── amd.js ├── cjsExportLazy.js ├── cjsMixedImport.js ├── commonjs-requiremain.js ├── commonjs.cjs ├── commonjs.js ├── coreModules.js ├── es6.esm ├── es6.js ├── es6.mjs ├── es6DynamicImport.js ├── es6MixedExportLazy.js ├── es6MixedImport.js ├── es6NoImport.js ├── es6WithError.js ├── es7.js ├── exampleAST.js ├── internalNodePrefix.js ├── js.vue ├── jsx.js ├── module.jsx ├── module.tsx ├── none.js ├── requiretest.js ├── styles.css ├── styles.less ├── styles.sass ├── styles.scss ├── styles.styl ├── ts.vue ├── typescript.ts ├── typescriptWithError.ts └── unparseable.js └── test.js /.c8rc.json: -------------------------------------------------------------------------------- 1 | { 2 | "reporter": [ 3 | "html", 4 | "lcovonly", 5 | "text" 6 | ] 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 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Enforce Unix newlines 2 | * text=auto eol=lf 3 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /coverage/ 2 | /node_modules/ 3 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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 | # precinct 2 | 3 | [![CI](https://img.shields.io/github/actions/workflow/status/dependents/node-precinct/ci.yml?branch=main&label=CI&logo=github)](https://github.com/dependents/node-precinct/actions/workflows/ci.yml?query=branch%3Amain) 4 | [![npm version](https://img.shields.io/npm/v/precinct?logo=npm&logoColor=fff)](https://www.npmjs.com/package/precinct) 5 | [![npm downloads](http://img.shields.io/npm/dm/precinct)](https://www.npmjs.com/package/precinct) 6 | 7 | > Unleash the detectives 8 | 9 | ```sh 10 | npm install precinct 11 | ``` 12 | 13 | Uses the appropriate detective to find the dependencies of a file or its AST. 14 | 15 | Supports: 16 | 17 | * JavaScript modules: AMD, CommonJS, and ES6 18 | * TypeScript 19 | * CSS Preprocessors: Sass, Scss, Stylus, and Less 20 | * CSS (PostCSS) 21 | 22 | ## Usage 23 | 24 | ```js 25 | const fs = require('fs'); 26 | const precinct = require('precinct'); 27 | 28 | const content = fs.readFileSync('myFile.js', 'utf8'); 29 | 30 | // Pass in a file's content or an AST 31 | const deps = precinct(content); 32 | ``` 33 | 34 | You may pass options (to individual detectives) based on the module type via an optional second object argument `detective(content, options)`, for example: 35 | 36 | Example call: 37 | 38 | ```js 39 | precinct(content, { 40 | amd: { 41 | skipLazyLoaded: true 42 | }, 43 | type: 'amd' 44 | }); 45 | ``` 46 | 47 | * The supported module type prefixes are: `amd`, `commonjs`, `css`, `es6`, `less`, `sass`, `scss`, `stylus`, `ts`, `tsx`, `vue`. 48 | 49 | Current options: 50 | 51 | * `amd.skipLazyLoaded`: tells the AMD detective to omit lazy-loaded dependencies (i.e., inner requires). 52 | * `es6.mixedImports`: allows for all dependencies to be fetched from a file that contains both CJS and ES6 imports. 53 | * Note: This will work for any file format that contains an ES6 import. 54 | * `css.url`: tells the CSS detective to include `url()` references to images, fonts, etc. 55 | 56 | Finding non-JavaScript (ex: Sass and Stylus) dependencies: 57 | 58 | ```js 59 | const fs = require('fs'); 60 | const content = fs.readFileSync('styles.scss', 'utf8'); 61 | 62 | const sassDeps = precinct(content, { type: 'sass' }); 63 | const stylusDeps = precinct(content, { type: 'stylus' }); 64 | ``` 65 | 66 | Or, if you just want to pass in a filepath and get the dependencies: 67 | 68 | ```js 69 | const { paperwork } = require('precinct'); 70 | 71 | const deps = paperwork('myFile.js'); 72 | const deps2 = paperwork('styles.scss'); 73 | ``` 74 | 75 | ### `precinct.paperwork(filename, options)` 76 | 77 | Supported options: 78 | 79 | * `includeCore`: (default: `true`) set to `false` to exclude core Node.js dependencies from the list of dependencies. 80 | * `fileSystem`: (default: `undefined`) set to an alternative `fs` implementation that will be used to read the file path. 81 | * You may also pass detective-specific configuration like you would to `precinct(content, options)`. 82 | 83 | ### CLI 84 | 85 | Assumes a global install precinct with `npm install -g precinct`. 86 | 87 | ```sh 88 | precinct [options] path/to/file 89 | ``` 90 | 91 | Run `precinct --help` to see all options. 92 | 93 | ## License 94 | 95 | [MIT](LICENSE) 96 | -------------------------------------------------------------------------------- /bin/cli.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | 'use strict'; 4 | 5 | const fs = require('fs'); 6 | const { program } = require('commander'); 7 | const precinct = require('../index.js'); 8 | const { name, description, version } = require('../package.json'); 9 | 10 | program 11 | .name(name) 12 | .description(description) 13 | .version(version) 14 | .argument('', 'the path to file to examine') 15 | .option('--es6-mixed-imports', 'Fetch all dependendies from a file that contains both CJS and ES6 imports') 16 | .option('-t, --type ', 'The type of content being passed in. Useful if you want to use a non-JS detective') 17 | .showHelpAfterError() 18 | .parse(); 19 | 20 | const { es6MixedImports: mixedImports, type } = program.opts(); 21 | const options = { 22 | es6: { 23 | mixedImports: Boolean(mixedImports) 24 | }, 25 | type 26 | }; 27 | 28 | const content = fs.readFileSync(program.args[0], 'utf8'); 29 | 30 | console.log(precinct(content, options).join('\n')); 31 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | export = precinct; 2 | 3 | /** 4 | * Finds the list of dependencies for the given file 5 | * 6 | * @param {String|Object} content - File's content or AST 7 | * @param {Object} [options] 8 | * @param {String} [options.type] - The type of content being passed in. Useful if you want to use a non-js detective 9 | * @return {String[]} 10 | */ 11 | declare function precinct(content: string | any, options?: { 12 | type?: string; 13 | }): string[]; 14 | declare namespace precinct { 15 | /** 16 | * Returns the dependencies for the given file path 17 | * 18 | * @param {String} filename 19 | * @param {Object} [options] 20 | * @param {Boolean} [options.includeCore=true] - Whether or not to include core modules in the dependency list 21 | * @param {Object} [options.fileSystem=undefined] - An alternative fs implementation to use for reading the file path. 22 | * @return {String[]} 23 | */ 24 | function paperwork(filename: string, options?: { 25 | includeCore?: boolean; 26 | fileSystem?: any; 27 | }): string[]; 28 | } 29 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs'); 4 | const path = require('path'); 5 | const process = require('process'); 6 | const { debuglog } = require('util'); 7 | const getModuleType = require('module-definition'); 8 | const Walker = require('node-source-walk'); 9 | const detectiveAmd = require('detective-amd'); 10 | const detectiveCjs = require('detective-cjs'); 11 | const detectiveEs6 = require('detective-es6'); 12 | const detectiveLess = require('@dependents/detective-less'); 13 | const detectivePostcss = require('detective-postcss'); 14 | const detectiveSass = require('detective-sass'); 15 | const detectiveScss = require('detective-scss'); 16 | const detectiveStylus = require('detective-stylus'); 17 | const detectiveTypeScript = require('detective-typescript'); 18 | const detectiveVue = require('detective-vue2'); 19 | 20 | const debug = debuglog('precinct'); 21 | // eslint-disable-next-line n/no-deprecated-api 22 | const natives = process.binding('natives'); 23 | 24 | /** 25 | * Finds the list of dependencies for the given file 26 | * 27 | * @param {String|Object} content - File's content or AST 28 | * @param {Object} [options] 29 | * @param {String} [options.type] - The type of content being passed in. Useful if you want to use a non-js detective 30 | * @return {String[]} 31 | */ 32 | function precinct(content, options = {}) { 33 | debug('options given: %o', options); 34 | 35 | let ast; 36 | 37 | // We assume we're dealing with a JS file 38 | if (!options.type && typeof content !== 'object') { 39 | debug('we assume this is JS'); 40 | const walker = new Walker(); 41 | 42 | try { 43 | // Parse once and distribute the AST to all detectives 44 | ast = walker.parse(content); 45 | debug('parsed the file content into an ast'); 46 | precinct.ast = ast; 47 | } catch (error) { 48 | // In case a previous call had it populated 49 | precinct.ast = null; 50 | debug('could not parse content: %s', error.message); 51 | return []; 52 | } 53 | // SASS files shouldn't be parsed by Acorn 54 | } else { 55 | ast = content; 56 | 57 | if (typeof content === 'object') { 58 | precinct.ast = content; 59 | } 60 | } 61 | 62 | const type = options.type ?? getModuleType.fromSource(ast); 63 | debug('module type: %s', type); 64 | 65 | const detective = getDetective(type, options); 66 | let dependencies = []; 67 | 68 | if (detective) { 69 | dependencies = detective(ast, options[type]); 70 | } else { 71 | debug('no detective found for: %s', type); 72 | } 73 | 74 | // For non-JS files that we don't parse 75 | if (detective?.ast) { 76 | precinct.ast = detective.ast; 77 | } 78 | 79 | return dependencies; 80 | } 81 | 82 | /** 83 | * Returns the dependencies for the given file path 84 | * 85 | * @param {String} filename 86 | * @param {Object} [options] 87 | * @param {Boolean} [options.includeCore=true] - Whether or not to include core modules in the dependency list 88 | * @param {Object} [options.fileSystem=undefined] - An alternative fs implementation to use for reading the file path. 89 | * @return {String[]} 90 | */ 91 | precinct.paperwork = (filename, options = {}) => { 92 | options = { includeCore: true, ...options }; 93 | 94 | const fileSystem = options.fileSystem || fs; 95 | const content = fileSystem.readFileSync(filename, 'utf8'); 96 | const ext = path.extname(filename); 97 | let type; 98 | 99 | if (ext === '.styl') { 100 | debug('paperwork: converting .styl into the stylus type'); 101 | type = 'stylus'; 102 | } else if (ext === '.cjs') { 103 | debug('paperwork: converting .cjs into the commonjs type'); 104 | type = 'commonjs'; 105 | // We need to sniff the JS module to find its type, not by extension. 106 | // Other possible types pass through normally 107 | } else if (!['.js', '.jsx'].includes(ext)) { 108 | debug('paperwork: stripping the dot from the extension to serve as the type'); 109 | type = ext.replace('.', ''); 110 | } 111 | 112 | if (type) { 113 | debug('paperwork: setting the module type'); 114 | options.type = type; 115 | } 116 | 117 | debug('paperwork: invoking precinct'); 118 | const dependencies = precinct(content, options); 119 | 120 | if (!options.includeCore) { 121 | return dependencies.filter(dependency => { 122 | if (dependency.startsWith('node:')) return false; 123 | 124 | // In Node.js 18, node:test is a builtin but shows up under natives["test"], 125 | // but can only be imported by "node:test." We're correcting this so "test" 126 | // isn't unnecessarily stripped from the imports 127 | if (dependency === 'test') { 128 | debug('paperwork: allowing test import to avoid builtin/natives consideration'); 129 | return true; 130 | } 131 | 132 | return !natives[dependency]; 133 | }); 134 | } 135 | 136 | debug('paperwork: got these results\n', dependencies); 137 | return dependencies; 138 | }; 139 | 140 | function getDetective(type, options) { 141 | const mixedMode = options.es6?.mixedImports; 142 | 143 | switch (type) { 144 | case 'cjs': 145 | case 'commonjs': { 146 | return mixedMode ? detectiveEs6Cjs : detectiveCjs; 147 | } 148 | 149 | case 'css': { 150 | return detectivePostcss; 151 | } 152 | 153 | case 'amd': { 154 | return detectiveAmd; 155 | } 156 | 157 | case 'mjs': 158 | case 'esm': 159 | case 'es6': { 160 | return mixedMode ? detectiveEs6Cjs : detectiveEs6; 161 | } 162 | 163 | case 'sass': { 164 | return detectiveSass; 165 | } 166 | 167 | case 'less': { 168 | return detectiveLess; 169 | } 170 | 171 | case 'scss': { 172 | return detectiveScss; 173 | } 174 | 175 | case 'stylus': { 176 | return detectiveStylus; 177 | } 178 | 179 | case 'ts': { 180 | return detectiveTypeScript; 181 | } 182 | 183 | case 'tsx': { 184 | return detectiveTypeScript.tsx; 185 | } 186 | 187 | case 'vue': { 188 | return detectiveVue; 189 | } 190 | 191 | default: 192 | // nothing 193 | } 194 | } 195 | 196 | function detectiveEs6Cjs(ast, detectiveOptions) { 197 | return [ 198 | ...detectiveEs6(ast, detectiveOptions), 199 | ...detectiveCjs(ast, detectiveOptions) 200 | ]; 201 | } 202 | 203 | module.exports = precinct; 204 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "precinct", 3 | "version": "12.2.0", 4 | "description": "Unleash the detectives", 5 | "main": "index.js", 6 | "types": "index.d.ts", 7 | "scripts": { 8 | "lint": "xo", 9 | "fix": "xo --fix", 10 | "mocha": "mocha", 11 | "test": "npm run lint && npm run mocha", 12 | "test:ci": "c8 npm run mocha" 13 | }, 14 | "bin": { 15 | "precinct": "bin/cli.js" 16 | }, 17 | "repository": { 18 | "type": "git", 19 | "url": "git+https://github.com/dependents/node-precinct.git" 20 | }, 21 | "keywords": [ 22 | "modules", 23 | "amd", 24 | "commonjs", 25 | "es6", 26 | "sass", 27 | "less", 28 | "detective", 29 | "dependencies" 30 | ], 31 | "author": "Joel Kemp ", 32 | "license": "MIT", 33 | "bugs": { 34 | "url": "https://github.com/dependents/node-precinct/issues" 35 | }, 36 | "engines": { 37 | "node": ">=18" 38 | }, 39 | "files": [ 40 | "bin/cli.js", 41 | "index.js", 42 | "index.d.ts" 43 | ], 44 | "homepage": "https://github.com/dependents/node-precinct", 45 | "dependencies": { 46 | "@dependents/detective-less": "^5.0.1", 47 | "commander": "^12.1.0", 48 | "detective-amd": "^6.0.1", 49 | "detective-cjs": "^6.0.1", 50 | "detective-es6": "^5.0.1", 51 | "detective-postcss": "^7.0.1", 52 | "detective-sass": "^6.0.1", 53 | "detective-scss": "^5.0.1", 54 | "detective-stylus": "^5.0.1", 55 | "detective-typescript": "^14.0.0", 56 | "detective-vue2": "^2.2.0", 57 | "module-definition": "^6.0.1", 58 | "node-source-walk": "^7.0.1", 59 | "postcss": "^8.5.1", 60 | "typescript": "^5.7.3" 61 | }, 62 | "devDependencies": { 63 | "c8": "^10.1.3", 64 | "mocha": "^11.1.0", 65 | "rewire": "^7.0.0", 66 | "sinon": "^19.0.2", 67 | "xo": "^0.60.0" 68 | }, 69 | "xo": { 70 | "space": true, 71 | "ignores": [ 72 | "test/fixtures/*" 73 | ], 74 | "rules": { 75 | "arrow-body-style": "off", 76 | "capitalized-comments": "off", 77 | "comma-dangle": [ 78 | "error", 79 | "never" 80 | ], 81 | "curly": [ 82 | "error", 83 | "multi-line" 84 | ], 85 | "operator-linebreak": [ 86 | "error", 87 | "after" 88 | ], 89 | "object-curly-spacing": [ 90 | "error", 91 | "always" 92 | ], 93 | "space-before-function-paren": [ 94 | "error", 95 | "never" 96 | ], 97 | "unicorn/prefer-module": "off", 98 | "unicorn/prefer-node-protocol": "off", 99 | "unicorn/prefer-top-level-await": "off", 100 | "unicorn/prevent-abbreviations": "off" 101 | } 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /test/fixtures/Gruntfile.js: -------------------------------------------------------------------------------- 1 | module.exports = function(grunt) { 2 | 3 | grunt.initConfig({ 4 | jshint: { 5 | files: ['Gruntfile.js', 'src/**/*.js', 'test/**/*.js'], 6 | options: { 7 | globals: { 8 | jQuery: true 9 | } 10 | } 11 | }, 12 | watch: { 13 | files: ['<%= jshint.files %>'], 14 | tasks: ['jshint'] 15 | } 16 | }); 17 | 18 | grunt.loadNpmTasks('grunt-contrib-jshint'); 19 | grunt.loadNpmTasks('grunt-contrib-watch'); 20 | 21 | grunt.registerTask('default', ['jshint']); 22 | 23 | }; -------------------------------------------------------------------------------- /test/fixtures/amd.js: -------------------------------------------------------------------------------- 1 | define([ 2 | './a', 3 | './b' 4 | ], function(a, b) { 5 | 6 | }); 7 | -------------------------------------------------------------------------------- /test/fixtures/cjsExportLazy.js: -------------------------------------------------------------------------------- 1 | module.exports = function({ 2 | // Just requiring any files that exist 3 | amd = require('./amd'), 4 | es6 = require('./es6').foo, 5 | es7 = require('./es7'), 6 | }) { 7 | 8 | } 9 | -------------------------------------------------------------------------------- /test/fixtures/cjsMixedImport.js: -------------------------------------------------------------------------------- 1 | var bar = require('./bar'); 2 | import foo from './foo'; 3 | -------------------------------------------------------------------------------- /test/fixtures/commonjs-requiremain.js: -------------------------------------------------------------------------------- 1 | var a = require.main.require('./b'); 2 | -------------------------------------------------------------------------------- /test/fixtures/commonjs.cjs: -------------------------------------------------------------------------------- 1 | var a = require('./a'), 2 | b = require('./b'); 3 | -------------------------------------------------------------------------------- /test/fixtures/commonjs.js: -------------------------------------------------------------------------------- 1 | var a = require('./a'), 2 | b = require('./b'); 3 | -------------------------------------------------------------------------------- /test/fixtures/coreModules.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'); 2 | var path = require('path'); 3 | var fs = require('fs'); -------------------------------------------------------------------------------- /test/fixtures/es6.esm: -------------------------------------------------------------------------------- 1 | import { square, diag } from 'lib'; 2 | console.log(square(11)); // 121 3 | console.log(diag(4, 3)); // 5 -------------------------------------------------------------------------------- /test/fixtures/es6.js: -------------------------------------------------------------------------------- 1 | import { square, diag } from 'lib'; 2 | console.log(square(11)); // 121 3 | console.log(diag(4, 3)); // 5 -------------------------------------------------------------------------------- /test/fixtures/es6.mjs: -------------------------------------------------------------------------------- 1 | import { square, diag } from 'lib'; 2 | console.log(square(11)); // 121 3 | console.log(diag(4, 3)); // 5 -------------------------------------------------------------------------------- /test/fixtures/es6DynamicImport.js: -------------------------------------------------------------------------------- 1 | export default function foo() { 2 | import("./bar"); 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/es6MixedExportLazy.js: -------------------------------------------------------------------------------- 1 | export default function({ 2 | // Just requiring any files that exist 3 | amd = require('./amd'), 4 | es6 = require('./es6'), 5 | es7 = require('./es7') 6 | }) { 7 | 8 | } 9 | -------------------------------------------------------------------------------- /test/fixtures/es6MixedImport.js: -------------------------------------------------------------------------------- 1 | import foo from './foo'; 2 | var bar = require('./bar'); 3 | -------------------------------------------------------------------------------- /test/fixtures/es6NoImport.js: -------------------------------------------------------------------------------- 1 | export const sqrt = Math.sqrt; 2 | export function square(x) { 3 | return x * x; 4 | } 5 | export function diag(x, y) { 6 | return sqrt(square(x) + square(y)); 7 | } -------------------------------------------------------------------------------- /test/fixtures/es6WithError.js: -------------------------------------------------------------------------------- 1 | import { square, diag } from 'lib' // error, semicolon 2 | console.log(square(11)); // 121 3 | console.log(diag(4, 3); // 5, error, missing paren -------------------------------------------------------------------------------- /test/fixtures/es7.js: -------------------------------------------------------------------------------- 1 | import { square, diag } from 'lib'; 2 | async function foo() {}; 3 | -------------------------------------------------------------------------------- /test/fixtures/exampleAST.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | type: 'Program', 3 | body: [{ 4 | type: 'VariableDeclaration', 5 | declarations: [{ 6 | type: 'VariableDeclarator', 7 | id: { 8 | type: 'Identifier', 9 | name: 'a' 10 | }, 11 | init: { 12 | type: 'CallExpression', 13 | callee: { 14 | type: 'Identifier', 15 | name: 'require' 16 | }, 17 | arguments: [{ 18 | type: 'Literal', 19 | value: './a', 20 | raw: './a' 21 | }] 22 | } 23 | }], 24 | kind: 'var' 25 | }] 26 | }; 27 | -------------------------------------------------------------------------------- /test/fixtures/internalNodePrefix.js: -------------------------------------------------------------------------------- 1 | const someModule = require("node:nonexistant") 2 | const anotherModule = require("streams") 3 | -------------------------------------------------------------------------------- /test/fixtures/js.vue: -------------------------------------------------------------------------------- 1 | 4 | 7 | 10 | -------------------------------------------------------------------------------- /test/fixtures/jsx.js: -------------------------------------------------------------------------------- 1 | import { square, diag } from 'lib'; 2 | const tmpl = ; 3 | -------------------------------------------------------------------------------- /test/fixtures/module.jsx: -------------------------------------------------------------------------------- 1 | import es6NoImport from './es6NoImport'; 2 | 3 | export default
Hi
; 4 | -------------------------------------------------------------------------------- /test/fixtures/module.tsx: -------------------------------------------------------------------------------- 1 | import none from './none'; 2 | 3 | export default
Hi
; 4 | -------------------------------------------------------------------------------- /test/fixtures/none.js: -------------------------------------------------------------------------------- 1 | var a = new window.Foo(); 2 | -------------------------------------------------------------------------------- /test/fixtures/requiretest.js: -------------------------------------------------------------------------------- 1 | require("test"); 2 | -------------------------------------------------------------------------------- /test/fixtures/styles.css: -------------------------------------------------------------------------------- 1 | @import "foo.css"; 2 | @import url("baz.css"); 3 | @value a from 'bla.css'; 4 | @value a, b as x from url(another.css); 5 | -------------------------------------------------------------------------------- /test/fixtures/styles.less: -------------------------------------------------------------------------------- 1 | @import "_foo"; 2 | @import "_bar.css"; 3 | @import "baz.less"; 4 | -------------------------------------------------------------------------------- /test/fixtures/styles.sass: -------------------------------------------------------------------------------- 1 | @import _foo 2 | -------------------------------------------------------------------------------- /test/fixtures/styles.scss: -------------------------------------------------------------------------------- 1 | @import "_foo"; 2 | @import "baz.scss"; 3 | -------------------------------------------------------------------------------- /test/fixtures/styles.styl: -------------------------------------------------------------------------------- 1 | @import "mystyles" 2 | @import "styles2.styl" 3 | @require "styles3.styl"; 4 | @require "styles4"; -------------------------------------------------------------------------------- /test/fixtures/ts.vue: -------------------------------------------------------------------------------- 1 | 4 | 7 | 10 | -------------------------------------------------------------------------------- /test/fixtures/typescript.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs'; 2 | import { square, diag } from 'lib'; 3 | import foo from './bar'; 4 | import "./my-module.js"; // Import a module for side-effects only 5 | import zip = require("./ZipCodeValidator"); // needed when importing a module using `export =` syntax 6 | 7 | console.log(square(11)); // 121 8 | console.log(diag(4, 3)); // 5 9 | -------------------------------------------------------------------------------- /test/fixtures/typescriptWithError.ts: -------------------------------------------------------------------------------- 1 | import { square, diag } from 'lib'; 2 | 3 | console.log(diag(4, 3); // error, missing bracket 4 | -------------------------------------------------------------------------------- /test/fixtures/unparseable.js: -------------------------------------------------------------------------------- 1 | { 2 | "very invalid": "javascript", 3 | "this", "is actually json", 4 | "But" not even valid json. 5 | } -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | 3 | 'use strict'; 4 | 5 | const assert = require('assert').strict; 6 | const { readFile } = require('fs/promises'); 7 | const path = require('path'); 8 | const rewire = require('rewire'); 9 | const sinon = require('sinon'); 10 | const ast = require('./fixtures/exampleAST.js'); 11 | 12 | const precinct = rewire('../index.js'); 13 | 14 | async function read(filename) { 15 | return readFile(path.join(__dirname, 'fixtures', filename), 'utf8'); 16 | } 17 | 18 | describe('node-precinct', () => { 19 | it('accepts an AST', () => { 20 | const deps = precinct(ast); 21 | assert.equal(deps.length, 1); 22 | }); 23 | 24 | it('dangles off a given ast', () => { 25 | assert.deepEqual(precinct.ast, ast); 26 | }); 27 | 28 | it('dangles off the parsed ast from a .js file', async() => { 29 | const fixture = await read('amd.js'); 30 | precinct(fixture); 31 | assert.ok(precinct.ast); 32 | assert.notDeepEqual(precinct.ast, ast); 33 | }); 34 | 35 | it('dangles off the parsed ast from a scss detective', async() => { 36 | const fixture = await read('styles.scss'); 37 | precinct(fixture, { type: 'scss' }); 38 | assert.notDeepEqual(precinct.ast, {}); 39 | }); 40 | 41 | it('dangles off the parsed ast from a sass detective', async() => { 42 | const fixture = await read('styles.sass'); 43 | precinct(fixture, { type: 'sass' }); 44 | assert.notDeepEqual(precinct.ast, {}); 45 | }); 46 | 47 | it('grabs dependencies of amd modules', async() => { 48 | const fixture = await read('amd.js'); 49 | const result = precinct(fixture); 50 | assert.equal(result.includes('./a'), true); 51 | assert.equal(result.includes('./b'), true); 52 | assert.equal(result.length, 2); 53 | }); 54 | 55 | it('grabs dependencies of commonjs modules', async() => { 56 | const fixture = await read('commonjs.js'); 57 | const result = precinct(fixture); 58 | assert.equal(result.includes('./a'), true); 59 | assert.equal(result.includes('./b'), true); 60 | assert.equal(result.length, 2); 61 | }); 62 | 63 | it('grabs dependencies of es6 modules', async() => { 64 | const fixture = await read('es6.js'); 65 | const result = precinct(fixture); 66 | assert.equal(result.includes('lib'), true); 67 | assert.equal(result.length, 1); 68 | }); 69 | 70 | it('grabs dependencies of es6 modules with embedded jsx', async() => { 71 | const fixture = await read('jsx.js'); 72 | const result = precinct(fixture); 73 | assert.equal(result.includes('lib'), true); 74 | assert.equal(result.length, 1); 75 | }); 76 | 77 | it('grabs dependencies of es6 modules with embedded es7', async() => { 78 | const fixture = await read('es7.js'); 79 | const result = precinct(fixture); 80 | assert.equal(result.includes('lib'), true); 81 | assert.equal(result.length, 1); 82 | }); 83 | 84 | it('does not grab dependencies of es6 modules with syntax errors', async() => { 85 | const fixture = await read('es6WithError.js'); 86 | const result = precinct(fixture); 87 | assert.equal(result.length, 0); 88 | }); 89 | 90 | it('grabs dependencies of css files', async() => { 91 | const fixture = await read('styles.css'); 92 | const result = precinct(fixture, { type: 'css' }); 93 | assert.deepEqual(result, ['foo.css', 'baz.css', 'bla.css', 'another.css']); 94 | }); 95 | 96 | it('grabs dependencies of scss files', async() => { 97 | const fixture = await read('styles.scss'); 98 | const result = precinct(fixture, { type: 'scss' }); 99 | assert.deepEqual(result, ['_foo', 'baz.scss']); 100 | }); 101 | 102 | it('grabs dependencies of sass files', async() => { 103 | const fixture = await read('styles.sass'); 104 | const result = precinct(fixture, { type: 'sass' }); 105 | assert.deepEqual(result, ['_foo']); 106 | }); 107 | 108 | it('grabs dependencies of stylus files', async() => { 109 | const fixture = await read('styles.styl'); 110 | const result = precinct(fixture, { type: 'stylus' }); 111 | const expected = ['mystyles', 'styles2.styl', 'styles3.styl', 'styles4']; 112 | assert.deepEqual(result, expected); 113 | }); 114 | 115 | it('grabs dependencies of less files', async() => { 116 | const fixture = await read('styles.less'); 117 | const result = precinct(fixture, { type: 'less' }); 118 | const expected = ['_foo', '_bar.css', 'baz.less']; 119 | assert.deepEqual(result, expected); 120 | }); 121 | 122 | it('grabs dependencies of typescript files', async() => { 123 | const fixture = await read('typescript.ts'); 124 | const result = precinct(fixture, { type: 'ts' }); 125 | const expected = ['fs', 'lib', './bar', './my-module.js', './ZipCodeValidator']; 126 | assert.deepEqual(result, expected); 127 | }); 128 | 129 | it('grabs dependencies of typescript tsx files', async() => { 130 | const fixture = await read('module.tsx'); 131 | const result = precinct(fixture, { type: 'tsx' }); 132 | const expected = ['./none']; 133 | assert.deepEqual(result, expected); 134 | }); 135 | 136 | it('does not grab dependencies of typescript modules with syntax errors', async() => { 137 | const fixture = await read('typescriptWithError.ts'); 138 | const result = precinct(fixture); 139 | assert.equal(result.length, 0); 140 | }); 141 | 142 | it('supports the object form of type configuration', async() => { 143 | const fixture = await read('styles.styl'); 144 | const result = precinct(fixture, { type: 'stylus' }); 145 | const expected = ['mystyles', 'styles2.styl', 'styles3.styl', 'styles4']; 146 | assert.deepEqual(result, expected); 147 | }); 148 | 149 | it('yields no dependencies for es6 modules with no imports', async() => { 150 | const fixture = await read('es6NoImport.js'); 151 | const result = precinct(fixture); 152 | assert.equal(result.length, 0); 153 | }); 154 | 155 | it('yields no dependencies for non-modules', async() => { 156 | const fixture = await read('none.js'); 157 | const result = precinct(fixture); 158 | assert.equal(result.length, 0); 159 | }); 160 | 161 | it('ignores unparsable .js files', async() => { 162 | const fixture = await read('unparseable.js'); 163 | const result = precinct(fixture); 164 | assert.equal(result.includes('lib'), false); 165 | assert.equal(result.length, 0); 166 | }); 167 | 168 | it('does not throw on unparsable .js files', async() => { 169 | const fixture = await read('unparseable.js'); 170 | assert.doesNotThrow(() => { 171 | precinct(fixture); 172 | }, SyntaxError); 173 | }); 174 | 175 | it('does not blow up when parsing a gruntfile #2', async() => { 176 | const fixture = await read('Gruntfile.js'); 177 | assert.doesNotThrow(() => { 178 | precinct(fixture); 179 | }); 180 | }); 181 | 182 | describe('paperwork', () => { 183 | it('grabs dependencies of jsx files', () => { 184 | const fixture = path.join(__dirname, '/fixtures/module.jsx'); 185 | const result = precinct.paperwork(fixture); 186 | const expected = ['./es6NoImport']; 187 | assert.deepEqual(result, expected); 188 | }); 189 | 190 | it('uses fileSystem from options if provided', () => { 191 | const fsMock = { 192 | readFileSync(path) { 193 | assert.equal(path, '/foo.js'); 194 | return 'var assert = require("assert");'; 195 | } 196 | }; 197 | 198 | const fixture = '/foo.js'; 199 | const results = precinct.paperwork(fixture, { fileSystem: fsMock }); 200 | assert.equal(results.length, 1); 201 | assert.equal(results[0], 'assert'); 202 | }); 203 | 204 | it('returns the dependencies for the given filepath', () => { 205 | const fixtures = ['es6.js', 'styles.scss', 'typescript.ts', 'styles.css']; 206 | 207 | for (const fixture of fixtures) { 208 | const result = precinct.paperwork(path.join(__dirname, 'fixtures', fixture)); 209 | assert.notEqual(result.length, 0); 210 | } 211 | }); 212 | 213 | it('throws if the file cannot be found', () => { 214 | const fixture = 'foo'; 215 | assert.throws(() => { 216 | precinct.paperwork(fixture); 217 | }); 218 | }); 219 | 220 | it('filters out core modules if options.includeCore is false', () => { 221 | const fixture = path.join(__dirname, '/fixtures/coreModules.js'); 222 | const result = precinct.paperwork(fixture, { includeCore: false }); 223 | assert.equal(result.length, 0); 224 | }); 225 | 226 | it('handles cjs files as commonjs', () => { 227 | const fixture = path.join(__dirname, '/fixtures/commonjs.cjs'); 228 | const result = precinct.paperwork(fixture); 229 | assert.equal(result.includes('./a'), true); 230 | assert.equal(result.includes('./b'), true); 231 | }); 232 | 233 | it('does not filter out core modules by default', () => { 234 | const fixture = path.join(__dirname, '/fixtures/coreModules.js'); 235 | const result = precinct.paperwork(fixture); 236 | assert.notEqual(result.length, 0); 237 | }); 238 | 239 | it('supports passing detective configuration', () => { 240 | const stub = sinon.stub().returns([]); 241 | const revert = precinct.__set__('detectiveAmd', stub); 242 | const config = { 243 | amd: { 244 | skipLazyLoaded: true 245 | } 246 | }; 247 | const fixture = path.join(__dirname, '/fixtures/amd.js'); 248 | 249 | precinct.paperwork(fixture, { 250 | includeCore: false, 251 | amd: config.amd 252 | }); 253 | 254 | assert.deepEqual(stub.args[0][1], config.amd); 255 | revert(); 256 | }); 257 | 258 | describe('when given detective configuration', () => { 259 | it('still does not filter out core module by default', () => { 260 | const stub = sinon.stub().returns([]); 261 | const revert = precinct.__set__('precinct', stub); 262 | const fixture = path.join(__dirname, '/fixtures/amd.js'); 263 | 264 | precinct.paperwork(fixture, { 265 | amd: { 266 | skipLazyLoaded: true 267 | } 268 | }); 269 | 270 | assert.equal(stub.args[0][1].includeCore, true); 271 | revert(); 272 | }); 273 | }); 274 | }); 275 | 276 | describe('when given a configuration object', () => { 277 | it('passes amd config to the amd detective', async() => { 278 | const stub = sinon.stub(); 279 | const revert = precinct.__set__('detectiveAmd', stub); 280 | const config = { 281 | amd: { 282 | skipLazyLoaded: true 283 | } 284 | }; 285 | 286 | const fixture = await read('amd.js'); 287 | precinct(fixture, config); 288 | 289 | assert.deepEqual(stub.args[0][1], config.amd); 290 | revert(); 291 | }); 292 | 293 | describe('that sets mixedImports for es6', () => { 294 | describe('for a file identified as es6', () => { 295 | it('returns both the commonjs and es6 dependencies', async() => { 296 | const fixture = await read('es6MixedImport.js'); 297 | const result = precinct(fixture, { 298 | es6: { 299 | mixedImports: true 300 | } 301 | }); 302 | 303 | assert.equal(result.length, 2); 304 | }); 305 | }); 306 | 307 | describe('for a file identified as cjs', () => { 308 | it('returns both the commonjs and es6 dependencies', async() => { 309 | const fixture = await read('cjsMixedImport.js'); 310 | const result = precinct(fixture, { 311 | es6: { 312 | mixedImports: true 313 | } 314 | }); 315 | 316 | assert.equal(result.length, 2); 317 | }); 318 | }); 319 | }); 320 | }); 321 | 322 | describe('when given vue file', () => { 323 | it('typescript - scss grabs script and style dependencies', () => { 324 | const vueFile = precinct.paperwork(path.join(__dirname, 'fixtures', 'ts.vue')); 325 | 326 | assert.equal(vueFile[0], './typescript'); 327 | assert.equal(vueFile[1], 'styles.scss'); 328 | assert.equal(vueFile.length, 2); 329 | }); 330 | 331 | it('javascript - sass grabs script and style dependencies', () => { 332 | const vueFile = precinct.paperwork(path.join(__dirname, 'fixtures', 'js.vue')); 333 | 334 | assert.equal(vueFile[0], './typescript'); 335 | assert.equal(vueFile[1], 'styles.scss'); 336 | assert.equal(vueFile.length, 2); 337 | }); 338 | }); 339 | 340 | describe('when lazy exported dependencies in CJS', () => { 341 | it('grabs those lazy dependencies', async() => { 342 | const fixture = await read('cjsExportLazy.js'); 343 | const result = precinct(fixture); 344 | assert.equal(result[0], './amd'); 345 | assert.equal(result[1], './es6'); 346 | assert.equal(result[2], './es7'); 347 | assert.equal(result.length, 3); 348 | }); 349 | }); 350 | 351 | describe('when a main require is used', () => { 352 | it('grabs those dependencies', async() => { 353 | const fixture = await read('commonjs-requiremain.js'); 354 | const result = precinct(fixture); 355 | assert.equal(result[0], './b'); 356 | assert.equal(result.length, 1); 357 | }); 358 | }); 359 | 360 | describe('when given an es6 file', () => { 361 | describe('that uses CJS imports for lazy dependencies', () => { 362 | describe('and mixedImport mode is turned on', () => { 363 | it('grabs the lazy imports', async() => { 364 | const fixture = await read('es6MixedExportLazy.js'); 365 | const result = precinct(fixture, { 366 | es6: { 367 | mixedImports: true 368 | } 369 | }); 370 | 371 | assert.equal(result[0], './amd'); 372 | assert.equal(result[1], './es6'); 373 | assert.equal(result[2], './es7'); 374 | assert.equal(result.length, 3); 375 | }); 376 | }); 377 | 378 | describe('and mixedImport mode is turned off', () => { 379 | it('does not grab any imports', async() => { 380 | const fixture = await read('es6MixedExportLazy.js'); 381 | const result = precinct(fixture); 382 | assert.equal(result.length, 0); 383 | }); 384 | }); 385 | }); 386 | 387 | describe('that imports node-internal with node:-prefix', () => { 388 | it('assumes that it exists', () => { 389 | const fixture = path.join(__dirname, 'fixtures', 'internalNodePrefix.js'); 390 | const result = precinct.paperwork(fixture, { 391 | includeCore: false 392 | }); 393 | assert.equal(result.includes('node:nonexistant'), false); 394 | assert.deepEqual(result, ['streams']); 395 | }); 396 | 397 | it('understands quirks around some modules only being addressable via node: prefix', () => { 398 | const fixture = path.join(__dirname, 'fixtures', 'requiretest.js'); 399 | const result = precinct.paperwork(fixture, { 400 | includeCore: false 401 | }); 402 | assert.deepEqual(result, ['test']); 403 | }); 404 | }); 405 | 406 | describe('that uses dynamic imports', () => { 407 | it('grabs the dynamic import', async() => { 408 | const fixture = await read('es6DynamicImport.js'); 409 | const result = precinct(fixture); 410 | assert.equal(result[0], './bar'); 411 | }); 412 | }); 413 | }); 414 | 415 | it('handles the esm extension', async() => { 416 | const fixture = await read('es6.esm'); 417 | const result = precinct(fixture); 418 | assert.equal(result.includes('lib'), true); 419 | assert.equal(result.length, 1); 420 | }); 421 | 422 | it('handles the mjs extension', async() => { 423 | const fixture = await read('es6.mjs'); 424 | const result = precinct(fixture); 425 | assert.equal(result.includes('lib'), true); 426 | assert.equal(result.length, 1); 427 | }); 428 | }); 429 | --------------------------------------------------------------------------------