├── .editorconfig ├── .eslintignore ├── .eslintrc ├── .github ├── dependabot.yml ├── funding.yml └── workflows │ ├── codeql-analysis.yml │ ├── dependency-review.yml │ ├── exit-silently-on-unsupported.yml │ ├── lint.yml │ └── nodejs.yml ├── .gitignore ├── .husky └── pre-push ├── .npmrc ├── SECURITY.md ├── cli.cjs ├── collaborators.md ├── declaration.tsconfig.json ├── index.js ├── lib ├── check.js ├── cli-engine.js ├── compare.js ├── extensions.js ├── parse.js ├── path-helpers.js ├── resolve-dependency.js ├── resolve-paths.js └── resolve-target.js ├── package.json ├── readme.md ├── test ├── .eslintrc ├── check.spec.js ├── extensions.spec.js ├── extra.spec.js ├── functional.spec.js ├── missing.spec.js ├── mock-missing-local-file │ ├── index.mjs │ └── package.json ├── mock-negative │ ├── abc.ts │ ├── bar.cjs │ ├── foo.js │ ├── index.mjs │ └── package.json ├── mock-positive │ ├── abc.ts │ ├── donkey │ │ └── index.js │ ├── foo.js │ ├── index.js │ ├── package.json │ ├── pizza.js │ └── scoped.js ├── mocks.js └── parse.spec.js └── tsconfig.json /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = true 6 | indent_style = space 7 | indent_size = 2 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | /coverage/**/* 2 | /test/*/**/* 3 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@voxpelli/eslint-config/esm", 3 | "root": true, 4 | "rules": { 5 | "semi": ["error", "never"] 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "npm" 9 | directory: "/" 10 | open-pull-requests-limit: 10 11 | schedule: 12 | interval: "weekly" 13 | -------------------------------------------------------------------------------- /.github/funding.yml: -------------------------------------------------------------------------------- 1 | github: voxpelli 2 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | name: "CodeQL" 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | schedule: 9 | - cron: '26 8 * * 4' 10 | 11 | permissions: 12 | actions: read 13 | contents: read 14 | security-events: write 15 | 16 | jobs: 17 | analyze: 18 | uses: voxpelli/ghatemplates/.github/workflows/codeql-analysis.yml@main 19 | -------------------------------------------------------------------------------- /.github/workflows/dependency-review.yml: -------------------------------------------------------------------------------- 1 | name: 'Dependency Review' 2 | 3 | on: [pull_request] 4 | 5 | permissions: 6 | contents: read 7 | 8 | jobs: 9 | dependency-review: 10 | uses: voxpelli/ghatemplates/.github/workflows/dependency-review.yml@main 11 | 12 | -------------------------------------------------------------------------------- /.github/workflows/exit-silently-on-unsupported.yml: -------------------------------------------------------------------------------- 1 | name: Exit silently on unsupported 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | tags: 8 | - '*' 9 | pull_request: 10 | branches: 11 | - main 12 | 13 | permissions: 14 | contents: read 15 | 16 | jobs: 17 | lint: 18 | uses: voxpelli/ghatemplates/.github/workflows/exit-silently-on-unsupported.yml@main 19 | with: 20 | files: './cli.cjs' 21 | node-versions: '0.10.48,12' 22 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Linting 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | tags: 8 | - '*' 9 | pull_request: 10 | branches: 11 | - main 12 | 13 | permissions: 14 | contents: read 15 | 16 | jobs: 17 | lint: 18 | uses: voxpelli/ghatemplates/.github/workflows/lint.yml@main 19 | -------------------------------------------------------------------------------- /.github/workflows/nodejs.yml: -------------------------------------------------------------------------------- 1 | name: Node CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | tags: 8 | - '*' 9 | pull_request: 10 | branches: 11 | - main 12 | 13 | permissions: 14 | contents: read 15 | 16 | jobs: 17 | test: 18 | uses: voxpelli/ghatemplates/.github/workflows/test.yml@main 19 | with: 20 | node-versions: '14,16,18,19' 21 | os: 'ubuntu-latest,windows-latest' 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Basic ones 2 | .DS_Store 3 | /node_modules 4 | /*.tgz 5 | /coverage 6 | /.nyc_output 7 | 8 | # We're a library, so please, no lock files 9 | /package-lock.json 10 | /yarn.lock 11 | 12 | # Generated types 13 | *.d.ts 14 | *.d.ts.map 15 | *.d.cts 16 | *.d.cts.map 17 | 18 | # Library specific ones 19 | -------------------------------------------------------------------------------- /.husky/pre-push: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npm test 5 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | | Version | Supported | 6 | | ------- | ------------------ | 7 | | 5.x.x | :white_check_mark: | 8 | | 4.x.x | :white_check_mark: | 9 | | 3.x.x | :warning: | 10 | | 2.10.x | :warning: | 11 | | < 2.10 | :x: | 12 | 13 | ## Reporting a Vulnerability 14 | 15 | Send an e-mail to the maintainers, eg. @voxpelli through pelle@kodfabrik.se Start the subject line with `SECURITY:` 16 | 17 | The maintainers will get back to you as soon as possible and work with you to evaluate and handle the vulnerability. 18 | 19 | As none of the maintainers are maintaining this module as part of their day jobs, no promises can be made in how fast a fix can be made. 20 | 21 | Whenever feasible a patch version fixing the security vulnerability will be released and the reporting user, unless it wishes to stay anonymous, will be credited for their contribution. 22 | -------------------------------------------------------------------------------- /cli.cjs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | 'use strict' 4 | 5 | require('version-guard')('./lib/cli-engine.js', 14, 18) 6 | -------------------------------------------------------------------------------- /collaborators.md: -------------------------------------------------------------------------------- 1 | ## Collaborators 2 | 3 | dependency-check is only possible due to the excellent work of the following collaborators: 4 | 5 | | Nickname | Profile | 6 | |-------------|------------------------------------------------------| 7 | | blakeembrey | [GitHub/blakeembrey](https://github.com/blakeembrey) | 8 | | voxpelli | [GitHub/voxpelli](https://github.com/voxpelli) | 9 | | maxogden | [GitHub/maxogden](https://github.com/maxogden) | 10 | -------------------------------------------------------------------------------- /declaration.tsconfig.json: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "extends": "./tsconfig", 4 | "exclude": [ 5 | "test/**/*.js" 6 | ], 7 | "compilerOptions": { 8 | "declaration": true, 9 | "declarationMap": true, 10 | "noEmit": false, 11 | "emitDeclarationOnly": true 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | export { check } from './lib/check.js' 2 | export { 3 | findUnused as extra, 4 | findMissing as missing 5 | } from './lib/compare.js' 6 | -------------------------------------------------------------------------------- /lib/check.js: -------------------------------------------------------------------------------- 1 | 2 | import { getExtensions } from './extensions.js' 3 | import { parse } from './parse.js' 4 | 5 | import { resolveEntryTarget, resolveModuleTarget } from './resolve-target.js' 6 | 7 | /** 8 | * @typedef CheckOptions 9 | * @property {string} path 10 | * @property {string[]} [entries] 11 | * @property {boolean} [noDefaultEntries] 12 | * @property {boolean} [ignoreUnknownExtensions] 13 | * @property {import('./extensions').ExtensionsInput|string[]} [extensions] 14 | * @property {import('./extensions').Detective|string|undefined} [detective] 15 | * @property {boolean} [builtins] 16 | */ 17 | 18 | /** 19 | * @param {CheckOptions} opts 20 | * @returns {Promise} 21 | */ 22 | export async function check (opts) { 23 | if (!opts) throw new Error('Requires an opts argument to be set') 24 | 25 | const { 26 | builtins, 27 | detective, 28 | entries, 29 | extensions, 30 | ignoreUnknownExtensions, 31 | noDefaultEntries, 32 | path: targetPath 33 | } = opts 34 | 35 | if (!targetPath) throw new Error('Requires a path to be set') 36 | if (typeof targetPath !== 'string') throw new TypeError(`Requires path to be a string, got: ${typeof targetPath}`) 37 | 38 | const { 39 | pkg, 40 | pkgPath, 41 | targetEntries 42 | } = await resolveModuleTarget(targetPath) || await resolveEntryTarget(targetPath) || {} 43 | 44 | if (!pkg || !pkgPath) { 45 | throw new Error('Failed to find a package.json') 46 | } 47 | 48 | return parse({ 49 | builtins, 50 | entries: [...(targetEntries || []), ...(entries || [])], 51 | extensions: await getExtensions(extensions, detective), 52 | ignoreUnknownExtensions, 53 | noDefaultEntries: noDefaultEntries || (targetEntries && targetEntries.length !== 0), 54 | 'package': pkg, 55 | path: pkgPath 56 | }) 57 | } 58 | -------------------------------------------------------------------------------- /lib/cli-engine.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console, unicorn/no-process-exit */ 2 | 3 | import meow from 'meow' 4 | 5 | import { check } from './check.js' 6 | import { findMissing, findUnused } from './compare.js' 7 | 8 | const cli = meow(` 9 | Usage 10 | $ dependency-check 11 | 12 | Entry paths supports globbing for easy adding of eg. entire folders. 13 | 14 | Options 15 | --detective Requireable path containing an alternative implementation of the detective module that supports alternate syntaxes 16 | --extensions, -e List of file extensions with detective to use when resolving require paths. Eg. 'js,jsx:detective-es6' 17 | --help Print this help and exits. 18 | --ignore To always exit with code 0 pass --ignore 19 | --ignore-module, -i Won't tell you about these module names when missing or unused. Supports globbing 20 | --ignore-unknown-extension, -u Won't fail on file extensions that are missing detectives 21 | --json, -j Format the output as json object 22 | --missing Only check to make sure that all modules in your code are listed in your package.json 23 | --no-default-entries Won't parse your main and bin entries from package.json even when a package.json or module folder has been defined 24 | --no-dev Won't tell you about devDependencies that are missing or unused 25 | --no-peer Won't tell you about peerDependencies that are missing or unused 26 | --unused Only check which modules listed in your package.json *are not* used in your code 27 | --verbose Enable logging of eg. success message 28 | --version Prints current version and exits. 29 | 30 | Examples 31 | $ dependency-check . 32 | `, { 33 | flags: { 34 | defaultEntries: { type: 'boolean', 'default': true }, 35 | detective: { type: 'string' }, 36 | dev: { type: 'boolean', 'default': true }, 37 | extensions: { alias: 'e', type: 'string', isMultiple: true }, 38 | ignore: { type: 'boolean' }, 39 | ignoreModule: { alias: 'i', type: 'string', isMultiple: true }, 40 | ignoreUnknownExtensions: { alias: 'u', type: 'boolean', 'default': false }, 41 | json: { alias: 'j', type: 'boolean', 'default': false }, 42 | missing: { type: 'boolean', 'default': false }, 43 | peer: { type: 'boolean', 'default': true }, 44 | unused: { type: 'boolean', 'default': false }, 45 | verbose: { type: 'boolean', 'default': false }, 46 | }, 47 | importMeta: import.meta 48 | }) 49 | 50 | // windows leaves leading/trailing quotes on strings needed on unix to 51 | // stop shells from doing path expansion, so strip them if present 52 | const entries = cli.input.map((string) => { 53 | if (string.startsWith("'") || string.startsWith('"')) { 54 | string = string.slice(1) 55 | } 56 | 57 | if (string.endsWith("'") || string.endsWith('"')) { 58 | string = string.slice(0, -1) 59 | } 60 | 61 | return string 62 | }) 63 | 64 | /** 65 | * @param {string|string[]|undefined} arg 66 | * @returns {import('./extensions').ExtensionsInput} 67 | */ 68 | function resolveExtensionsArgument (arg) { 69 | if (!arg) return {} 70 | 71 | /** @type {import('./extensions').ExtensionsInput} */ 72 | const extensions = {} 73 | 74 | if (typeof arg === 'string') { 75 | arg = [arg] 76 | } 77 | 78 | for (const value of arg) { 79 | const parts = value.trim().split(':', 2) 80 | 81 | for (const ext of (parts[0] || '').split(',')) { 82 | extensions[ext.charAt(0) === '.' ? ext : '.' + ext] = parts[1] 83 | } 84 | } 85 | 86 | return extensions 87 | } 88 | 89 | const path = entries.shift() 90 | 91 | if (!path) { 92 | console.error('Requires a path') 93 | process.exit(1) 94 | } 95 | 96 | const { 97 | defaultEntries: addDefaultEntries, 98 | detective, 99 | dev: includeDev, 100 | extensions, 101 | ignore, 102 | ignoreModule = [], 103 | ignoreUnknownExtensions, 104 | json, 105 | missing, 106 | peer: includePeer, 107 | unused, 108 | verbose, 109 | } = cli.flags 110 | 111 | const { 112 | 'package': pkg, 113 | used: deps, 114 | } = await check({ 115 | path, 116 | entries, 117 | ignoreUnknownExtensions, 118 | noDefaultEntries: !addDefaultEntries, 119 | extensions: resolveExtensionsArgument(extensions), 120 | detective, 121 | }) 122 | 123 | const options = { 124 | excludeDev: !includeDev, 125 | excludePeer: !includePeer, 126 | ignore: ignoreModule, 127 | } 128 | 129 | const runAllTests = !unused && !missing 130 | 131 | let failed = 0 132 | /** @type {string[]|undefined} */ 133 | let unusedDependencies 134 | /** @type {string[]|undefined} */ 135 | let missingDependencies 136 | 137 | if (runAllTests || unused) { 138 | unusedDependencies = findUnused(pkg, deps, options) 139 | failed += unusedDependencies.length 140 | } 141 | 142 | if (runAllTests || missing) { 143 | const optionsForMissingCheck = runAllTests 144 | ? { 145 | ...options, 146 | excludeDev: false, 147 | excludePeer: false 148 | } 149 | : options 150 | 151 | missingDependencies = findMissing(pkg, deps, optionsForMissingCheck) 152 | 153 | failed += missingDependencies.length 154 | } 155 | 156 | // print the result 157 | 158 | if (json) { 159 | console.log(JSON.stringify({ 160 | missing: missingDependencies, 161 | unused: unusedDependencies 162 | })) 163 | } else { 164 | if (unusedDependencies) { 165 | if (unusedDependencies.length) { 166 | console.error('Fail! Modules in package.json not used in code: ' + unusedDependencies.join(', ')) 167 | } else if (verbose) { 168 | console.log('Success! All dependencies in package.json are used in the code') 169 | } 170 | } 171 | 172 | if (missingDependencies) { 173 | if (missingDependencies.length) { 174 | console.error('Fail! Dependencies not listed in package.json: ' + missingDependencies.join(', ')) 175 | } else if (verbose) { 176 | console.log('Success! All dependencies used in the code are listed in package.json') 177 | } 178 | } 179 | } 180 | 181 | process.exit((failed && !ignore) ? 1 : 0) 182 | -------------------------------------------------------------------------------- /lib/compare.js: -------------------------------------------------------------------------------- 1 | import picomatch from 'picomatch' 2 | 3 | /** 4 | * @typedef DependencyOptions 5 | * @property {boolean} [excludeDev] 6 | * @property {boolean} [excludePeer] 7 | * @property {string|string[]} [ignore] 8 | */ 9 | 10 | /** 11 | * @param {import('read-pkg').NormalizedPackageJson} pkg 12 | * @param {DependencyOptions} [options] 13 | * @returns {{ allDeps: string[], ignore: string[] }} 14 | */ 15 | function configure (pkg, options = {}) { 16 | if (!pkg || typeof pkg !== 'object') throw new TypeError('Expected a pkg object') 17 | 18 | const allDeps = [ 19 | ...Object.keys(pkg.dependencies || {}), 20 | ...(options.excludePeer ? [] : Object.keys(pkg.peerDependencies || {})), 21 | ...(options.excludeDev ? [] : Object.keys(pkg.devDependencies || {})) 22 | ] 23 | const ignore = typeof options.ignore === 'string' 24 | ? [options.ignore] 25 | : (options.ignore || []) 26 | 27 | return { 28 | allDeps, 29 | ignore 30 | } 31 | } 32 | 33 | /** 34 | * @param {import('read-pkg').NormalizedPackageJson} pkg 35 | * @param {string[]} deps 36 | * @param {DependencyOptions} [options] 37 | * @returns {string[]} 38 | */ 39 | export function findMissing (pkg, deps, options) { 40 | /** @type {string[]} */ 41 | const missing = [] 42 | const { allDeps, ignore } = configure(pkg, options) 43 | const isMatch = picomatch(ignore) 44 | 45 | if (!Array.isArray(deps)) throw new TypeError('Expected a deps array') 46 | 47 | for (const used of deps) { 48 | if (!allDeps.includes(used) && !isMatch(used)) { 49 | missing.push(used) 50 | } 51 | } 52 | 53 | return missing 54 | } 55 | 56 | /** 57 | * @param {import('read-pkg').NormalizedPackageJson} pkg 58 | * @param {string[]} deps 59 | * @param {DependencyOptions} [options] 60 | * @returns {string[]} 61 | */ 62 | export function findUnused (pkg, deps, options) { 63 | /** @type {string[]} */ 64 | const missing = [] 65 | const { allDeps, ignore } = configure(pkg, options) 66 | const isMatch = picomatch(ignore) 67 | 68 | if (!Array.isArray(deps)) throw new TypeError('Expected a deps array') 69 | 70 | for (const dep of allDeps) { 71 | if (!deps.includes(dep) && !isMatch(dep)) { 72 | missing.push(dep) 73 | } 74 | } 75 | 76 | return missing 77 | } 78 | -------------------------------------------------------------------------------- /lib/extensions.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable unicorn/no-await-expression-member */ 2 | 3 | import { ErrorWithCause } from 'pony-cause' 4 | 5 | /** @type {Record} */ 6 | const extensionMapping = { 7 | '.js': 'precinct/es6', 8 | '.jsx': 'precinct/es6', 9 | '.mjs': 'precinct/es6', 10 | '.cjs': 'precinct/commonjs', 11 | '.ts': 'precinct/ts', 12 | '.tsx': 'precinct/tsx', 13 | } 14 | 15 | const noopExtensions = new Set([ 16 | '.node', 17 | '.json' 18 | ]) 19 | 20 | /** @typedef {(contents: string) => string[]} Detective */ 21 | /** @typedef {{ [extension: string]: Detective | string | undefined }} ExtensionsInput */ 22 | /** @typedef {{ [extension: string]: Detective | undefined }} Extensions */ 23 | 24 | /** @type {Detective} */ 25 | const noopDetective = () => [] 26 | 27 | /** 28 | * @param {string|Detective} [name] 29 | * @returns {Promise} 30 | */ 31 | async function getDetective (name) { 32 | if (name === '-') return noopDetective 33 | 34 | /** @type {string|undefined} */ 35 | let precinctType 36 | 37 | // Allows the use of precinct/foo to add a precinct detective with type 'foo' to a custom extension 38 | if (typeof name === 'string' && name.startsWith('precinct/')) { 39 | precinctType = name.slice('precinct/'.length) 40 | name = undefined 41 | } 42 | 43 | if (name) { 44 | try { 45 | return typeof name === 'string' ? (await import(name)).default : name 46 | } catch (err) { 47 | throw new ErrorWithCause(`Failed to load detective '${name}'`, { cause: err }) 48 | } 49 | } 50 | 51 | /** @type {(contents: string, options: { type?: string, es6?: { mixedImports: boolean }|undefined}) => string[]} */ 52 | // @ts-ignore There is no declaration for the precinct module 53 | const precinct = (await import('precinct')).default 54 | 55 | if (!precinctType) throw new Error('Expected a "precinct/something", but got "precinct/"') 56 | 57 | return (contents) => (precinctType && precinct(contents, { 58 | type: precinctType, 59 | es6: precinctType && ['es6', 'commonjs'].includes(precinctType) ? { mixedImports: true } : undefined 60 | })) || [] 61 | } 62 | 63 | /** 64 | * @param {string} extension 65 | * @param {Detective | string | undefined} detective 66 | * @returns {Promise} 67 | */ 68 | async function getDetectiveForExtension (extension, detective) { 69 | if (noopExtensions.has(extension)) { 70 | return getDetective('-') 71 | } 72 | 73 | return getDetective(detective || extensionMapping[extension]) 74 | } 75 | 76 | /** 77 | * @param {string[]|ExtensionsInput|undefined} extensions 78 | * @param {string|Detective|undefined} detective 79 | * @returns {Promise} 80 | */ 81 | export async function getExtensions (extensions, detective) { 82 | /** @type {Extensions} */ 83 | const result = {} 84 | 85 | if (Array.isArray(extensions)) { 86 | for (const extension of extensions) { 87 | result[extension] = await getDetectiveForExtension(extension, detective) 88 | } 89 | } else if (typeof extensions === 'object') { 90 | for (const extension in extensions) { 91 | if (extensions[extension]) { 92 | result[extension] = await getDetective(extensions[extension]) 93 | } else { 94 | result[extension] = await getDetectiveForExtension(extension, detective) 95 | } 96 | } 97 | } else if (extensions) { 98 | throw new TypeError('Requires extensions argument to be an array or object') 99 | } 100 | 101 | if (detective && typeof detective !== 'function' && typeof detective !== 'string') { 102 | throw new TypeError('Requires detective to be a string or a function') 103 | } 104 | 105 | if (Object.keys(result).length === 0) { 106 | for (const extension of [...Object.keys(extensionMapping), ...noopExtensions]) { 107 | result[extension] = await getDetectiveForExtension(extension, detective) 108 | } 109 | } 110 | 111 | return result 112 | } 113 | -------------------------------------------------------------------------------- /lib/parse.js: -------------------------------------------------------------------------------- 1 | import createDebug from 'debug' 2 | 3 | import { resolveDep } from './resolve-dependency.js' 4 | import { resolvePaths } from './resolve-paths.js' 5 | 6 | const debug = createDebug('dependency-check') 7 | 8 | /** 9 | * @typedef ParseOptions 10 | * @property {string} path 11 | * @property {import('read-pkg').NormalizedPackageJson} package 12 | * @property {import('./extensions').Extensions} extensions 13 | * @property {undefined|boolean} builtins 14 | * @property {undefined|boolean} ignoreUnknownExtensions 15 | * @property {undefined|boolean} noDefaultEntries 16 | * @property {undefined|string[]} entries 17 | */ 18 | 19 | /** 20 | * @typedef ParseResult 21 | * @property {import('read-pkg').NormalizedPackageJson} package 22 | * @property {string[]} used 23 | * @property {string[]} [builtins] 24 | */ 25 | 26 | /** 27 | * @param {ParseOptions} opts 28 | * @returns {Promise} 29 | */ 30 | export async function parse (opts) { 31 | const { 32 | builtins, 33 | entries, 34 | extensions, 35 | ignoreUnknownExtensions = false, 36 | noDefaultEntries, 37 | 'package': pkg, 38 | path: basePath, 39 | } = opts 40 | 41 | /** @type {Set} */ 42 | const deps = new Set() 43 | /** @type {Set} */ 44 | const seen = new Set() 45 | /** @type {Set} */ 46 | const core = new Set() 47 | 48 | const paths = await resolvePaths({ 49 | path: basePath, 50 | 'package': pkg, 51 | entries, 52 | noDefaultEntries, 53 | }) 54 | 55 | debug('entry paths', paths) 56 | 57 | if (paths.length === 0) { 58 | throw new Error('No entry paths found') 59 | } 60 | 61 | /** @type {Array>} */ 62 | const lookups = [] 63 | 64 | for (const file of paths) { 65 | lookups.push(resolveDep(file, extensions, { core, deps, ignoreUnknownExtensions, seen })) 66 | } 67 | 68 | await Promise.all(lookups) 69 | 70 | return { 71 | 'package': pkg, 72 | used: [...deps], 73 | ...(builtins ? { builtins: [...core] } : {}) 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /lib/path-helpers.js: -------------------------------------------------------------------------------- 1 | 2 | import { resolve } from 'node:path' 3 | import createDebug from 'debug' 4 | import { globby } from 'globby' 5 | 6 | const debug = createDebug('dependency-check') 7 | 8 | /** 9 | * @param {string|string[]} entries 10 | * @param {string} cwd 11 | * @returns {Promise} 12 | */ 13 | export async function resolveGlobbedPath (entries, cwd) { 14 | if (typeof entries === 'string') entries = [entries] 15 | 16 | // replace backslashes for forward slashes for windows 17 | entries = unixifyPaths(entries) 18 | 19 | debug('globby resolving', entries) 20 | 21 | const resolvedEntries = await globby(entries, { 22 | cwd: unixifyPath(cwd), 23 | absolute: true, 24 | expandDirectories: false 25 | }) 26 | 27 | /** @type {Set} */ 28 | const paths = new Set() 29 | 30 | for (const entry of resolvedEntries) { 31 | // Globby yields unix-style paths. 32 | const normalized = resolve(entry) 33 | paths.add(normalized) 34 | } 35 | 36 | const pathsArray = [...paths] 37 | 38 | debug('globby resolved', pathsArray) 39 | 40 | return pathsArray 41 | } 42 | 43 | /** 44 | * @param {string[]} values 45 | * @returns {string[]} 46 | */ 47 | export function unixifyPaths (values) { 48 | return values.map(value => unixifyPath(value)) 49 | } 50 | 51 | /** 52 | * @param {string} value 53 | * @returns {string} 54 | */ 55 | export function unixifyPath (value) { 56 | return value.replace(/\\/g, '/') 57 | } 58 | -------------------------------------------------------------------------------- /lib/resolve-dependency.js: -------------------------------------------------------------------------------- 1 | 2 | import { extname, resolve, dirname } from 'node:path' 3 | import { readFile } from 'node:fs/promises' 4 | import { builtinModules as builtins } from 'node:module' 5 | 6 | import createDebug from 'debug' 7 | import isRelative from 'is-relative' 8 | import resolveModule from 'resolve' 9 | 10 | const debug = createDebug('dependency-check') 11 | 12 | /** 13 | * @typedef DependencyContext 14 | * @property {Set} core 15 | * @property {Set} deps 16 | * @property {boolean} ignoreUnknownExtensions 17 | * @property {Set} seen 18 | */ 19 | 20 | /** 21 | * @param {string} file 22 | * @param {import('resolve').AsyncOpts} options 23 | * @returns {Promise} 24 | */ 25 | const promisedResolveModule = (file, options) => new Promise((resolve, reject) => { 26 | resolveModule(file, options, (err, path) => { 27 | if (err) return reject(err) 28 | if (path === undefined) return reject(new Error('Could not resolve a module path')) 29 | resolve(path) 30 | }) 31 | }) 32 | 33 | /** 34 | * @param {string} file 35 | * @returns {boolean} 36 | */ 37 | const isNotRelative = (file) => isRelative(file) && file[0] !== '.' 38 | 39 | /** 40 | * @param {string} file 41 | * @param {import('./extensions').Extensions} extensions 42 | * @param {DependencyContext} context 43 | * @returns {Promise} 44 | */ 45 | async function getDeps (file, extensions, { core, deps, ignoreUnknownExtensions, seen }) { 46 | const ext = extname(file) 47 | const detective = extensions[ext] 48 | 49 | if (typeof detective !== 'function') { 50 | if (ignoreUnknownExtensions) { 51 | return 52 | } else { 53 | throw new TypeError('Detective function missing for "' + file + '"') 54 | } 55 | } 56 | 57 | const contents = await readFile(file, 'utf8') 58 | const requires = detective(contents) 59 | /** @type {string[]} */ 60 | const relatives = [] 61 | 62 | for (let req of requires) { 63 | const isCore = builtins.includes(req.startsWith('node:') ? req.slice(5) : req) 64 | 65 | if (isNotRelative(req) && !isCore) { 66 | // require('foo/bar') -> require('foo') 67 | if (req[0] !== '@' && req.includes('/')) req = req.split('/')[0] || '' 68 | else if (req[0] === '@') req = req.split('/').slice(0, 2).join('/') 69 | debug('require("' + req + '")' + ' is a dependency') 70 | deps.add(req) 71 | } else if (isCore) { 72 | debug('require("' + req + '")' + ' is core') 73 | core.add(req) 74 | } else { 75 | debug('require("' + req + '")' + ' is relative') 76 | req = resolve(dirname(file), req) 77 | if (!seen.has(req)) { 78 | seen.add(req) 79 | relatives.push(req) 80 | } 81 | } 82 | } 83 | 84 | await Promise.all(relatives.map(name => resolveDep(name, extensions, { core, deps, ignoreUnknownExtensions, seen }))) 85 | } 86 | 87 | /** 88 | * @param {string} file 89 | * @param {import('./extensions').Extensions} extensions 90 | * @param {DependencyContext} context 91 | * @returns {Promise} 92 | */ 93 | export async function resolveDep (file, extensions, { core, deps, ignoreUnknownExtensions, seen }) { 94 | if (isNotRelative(file)) return 95 | 96 | const resolvedPath = await promisedResolveModule(file, { 97 | basedir: dirname(file), 98 | extensions: Object.keys(extensions) 99 | }) 100 | 101 | return getDeps(resolvedPath, extensions, { core, deps, ignoreUnknownExtensions, seen }) 102 | } 103 | -------------------------------------------------------------------------------- /lib/resolve-paths.js: -------------------------------------------------------------------------------- 1 | 2 | import { resolve, join, dirname } from 'node:path' 3 | import { access as promisedFsAccess } from 'node:fs/promises' 4 | 5 | import { resolveGlobbedPath } from './path-helpers.js' 6 | 7 | /** 8 | * @param {string} basePath 9 | * @param {string} targetPath 10 | * @returns {string} 11 | */ 12 | const joinAndResolvePath = (basePath, targetPath) => resolve(join(basePath, targetPath)) 13 | 14 | /** 15 | * @typedef ResolveDefaultEntriesPathsOptions 16 | * @property {string} path 17 | * @property {import('read-pkg').NormalizedPackageJson} package 18 | */ 19 | 20 | /** 21 | * @param {ResolveDefaultEntriesPathsOptions} opts 22 | * @returns {Promise} 23 | */ 24 | async function resolveDefaultEntriesPaths (opts) { 25 | const pkgPath = opts.path 26 | const pkgDir = dirname(pkgPath) 27 | const pkg = opts.package 28 | 29 | const mainPath = joinAndResolvePath(pkgDir, pkg.main || 'index.js') 30 | 31 | /** @type {string[]} */ 32 | const paths = [] 33 | 34 | // Add the path of the main file 35 | try { 36 | await promisedFsAccess(mainPath) 37 | paths.push(mainPath) 38 | } catch {} 39 | 40 | // Add the path of binaries 41 | if (pkg.bin) { 42 | const binPaths = typeof pkg.bin === 'string' 43 | ? [pkg.bin] 44 | : Object.values(pkg.bin) 45 | 46 | for (const cmd of binPaths) { 47 | paths.push(joinAndResolvePath(pkgDir, cmd)) 48 | } 49 | } 50 | 51 | // TODO: Add browser field, styles field, es6 module style ones etc 52 | 53 | return paths 54 | } 55 | 56 | /** 57 | * @typedef ResolvePathsOptions 58 | * @property {string} path 59 | * @property {import('read-pkg').NormalizedPackageJson} package 60 | * @property {undefined|boolean} noDefaultEntries 61 | * @property {undefined|string[]} entries 62 | */ 63 | 64 | /** 65 | * @param {ResolvePathsOptions} opts 66 | * @returns {Promise} 67 | */ 68 | export async function resolvePaths (opts) { 69 | const [ 70 | defaultEntries, 71 | globbedPaths 72 | ] = await Promise.all([ 73 | !opts.noDefaultEntries ? resolveDefaultEntriesPaths(opts) : [], 74 | opts.entries ? resolveGlobbedPath(opts.entries, dirname(opts.path)) : [] 75 | ]) 76 | 77 | return [ 78 | ...defaultEntries, 79 | ...globbedPaths 80 | ] 81 | } 82 | -------------------------------------------------------------------------------- /lib/resolve-target.js: -------------------------------------------------------------------------------- 1 | import { dirname, join } from 'node:path' 2 | 3 | import { pkgUp } from 'pkg-up' 4 | import { ErrorWithCause } from 'pony-cause' 5 | 6 | import { resolveGlobbedPath, unixifyPath } from './path-helpers.js' 7 | 8 | /** 9 | * @param {string} targetPath 10 | * @returns {Promise} 11 | */ 12 | export async function resolveModuleTarget (targetPath) { 13 | /** @type {string} */ 14 | let cwd 15 | 16 | targetPath = unixifyPath(targetPath) 17 | 18 | if (targetPath.endsWith('/package.json')) { 19 | cwd = dirname(targetPath) 20 | } else if (targetPath === 'package.json') { 21 | cwd = process.cwd() 22 | } else { 23 | cwd = targetPath 24 | } 25 | 26 | const { readPackage } = await import('read-pkg') 27 | 28 | try { 29 | const pkg = await readPackage({ cwd }) 30 | const pkgPath = join(cwd, 'package.json') 31 | 32 | return { 33 | pkgPath, 34 | pkg 35 | } 36 | } catch (err) { 37 | if (targetPath.endsWith('/package.json') || targetPath === 'package.json') { 38 | throw new ErrorWithCause('Failed to read package.json', { cause: err }) 39 | } 40 | // Else just fail silently so we can fall back to next lookup method 41 | } 42 | } 43 | 44 | /** 45 | * @param {string} targetPath 46 | * @returns {Promise} 47 | */ 48 | export async function resolveEntryTarget (targetPath) { 49 | // We've been given an entry path pattern as the target rather than a package.json or module folder 50 | // We'll resolve those entries and then finds us the package.json from the location of those 51 | const targetEntries = await resolveGlobbedPath(targetPath, process.cwd()) 52 | 53 | if (!targetEntries[0]) { 54 | throw new Error(`Failed to find package.json, path "${targetPath}" does not resolve to any file for "${process.cwd()}"`) 55 | } 56 | 57 | const pkgPath = await pkgUp({ cwd: dirname(targetEntries[0]) }) 58 | 59 | const resolved = pkgPath && await resolveModuleTarget(pkgPath) 60 | 61 | if (!resolved) return 62 | 63 | return { 64 | ...resolved, 65 | targetEntries 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dependency-check", 3 | "version": "5.0.0-7", 4 | "description": "checks which modules you have used in your code and then makes sure they are listed as dependencies in your package.json", 5 | "bin": { 6 | "dependency-check": "cli.cjs" 7 | }, 8 | "type": "module", 9 | "exports": "./index.js", 10 | "types": "index.d.ts", 11 | "files": [ 12 | "lib/**/*.cjs", 13 | "lib/**/*.js", 14 | "lib/**/*.d.ts", 15 | "lib/**/*.d.ts.map", 16 | "cli.cjs", 17 | "index.js", 18 | "index.d.ts", 19 | "index.d.ts.map" 20 | ], 21 | "scripts": { 22 | "build:0": "run-s clean", 23 | "build:1-declaration": "tsc -p declaration.tsconfig.json", 24 | "build": "run-s build:*", 25 | "check:dependencies": "node cli.cjs index.js cli.cjs 'lib/**/*.js' --no-dev", 26 | "check:node-versions": "installed-check -i precinct -i eslint-plugin-jsdoc", 27 | "check:lint": "eslint --report-unused-disable-directives .", 28 | "check:tsc": "tsc", 29 | "check:type-coverage": "type-coverage --detail --strict --at-least 95", 30 | "check": "run-s clean && run-p -c --aggregate-output check:*", 31 | "clean:declarations": "rm -rf $(find . -maxdepth 2 -type f -name '*.d.*ts*')", 32 | "clean": "run-p clean:*", 33 | "prepare": "husky install", 34 | "prepublishOnly": "run-s build", 35 | "test-ci": "run-s test:*", 36 | "test:cli:positive:custom-detective": "node cli.cjs test/mock-positive/ -e js:detective-cjs", 37 | "test:cli:positive:glob": "node cli.cjs 'test/mock-positive/**/*.js' --no-default-entries", 38 | "test:cli:positive:multi-glob": "node cli.cjs test/mock-positive/foo.js 'test/mock-positive/*.js' \"test/mock-positive/donkey/*.js\" --no-default-entries", 39 | "test:cli:positive:ts": "node cli.cjs test/mock-positive/abc.ts", 40 | "test:cli:positive:main-as-file": "node cli.cjs test/mock-positive/index.js", 41 | "test:cli:positive:simple": "node cli.cjs test/mock-positive/", 42 | "test:cli": "run-p test:cli:**", 43 | "test:mocha": "c8 --reporter=lcov --reporter text mocha 'test/**/*.spec.js'", 44 | "test": "run-s check test:*" 45 | }, 46 | "engines": { 47 | "node": "^14.18.0 || >=16.0.0" 48 | }, 49 | "author": "max ogden", 50 | "dependencies": { 51 | "debug": "^4.3.1", 52 | "globby": "^13.1.2", 53 | "is-relative": "^1.0.0", 54 | "meow": "^11.0.0", 55 | "picomatch": "^2.3.1", 56 | "pkg-up": "^4.0.0", 57 | "pony-cause": "^2.1.4", 58 | "precinct": "^8.2.0", 59 | "read-pkg": "^7.0.0", 60 | "resolve": "^1.19.0", 61 | "version-guard": "^1.1.0" 62 | }, 63 | "devDependencies": { 64 | "@types/chai": "^4.3.4", 65 | "@types/chai-as-promised": "^7.1.4", 66 | "@types/debug": "^4.1.5", 67 | "@types/is-relative": "^1.0.0", 68 | "@types/mocha": "^10.0.0", 69 | "@types/node": "^14.18.38", 70 | "@types/picomatch": "^2.3.0", 71 | "@types/resolve": "^1.20.2", 72 | "@voxpelli/eslint-config": "^15.2.0", 73 | "@voxpelli/tsconfig": "^7.0.0", 74 | "c8": "^7.12.0", 75 | "chai": "^4.3.7", 76 | "chai-as-promised": "^7.1.1", 77 | "detective-cjs": "^3.0.0", 78 | "eslint": "^8.27.0", 79 | "eslint-config-standard": "^17.0.0", 80 | "eslint-plugin-es": "^4.1.0", 81 | "eslint-plugin-import": "^2.26.0", 82 | "eslint-plugin-jsdoc": "^39.6.2", 83 | "eslint-plugin-mocha": "^10.1.0", 84 | "eslint-plugin-n": "^15.5.1", 85 | "eslint-plugin-promise": "^6.1.1", 86 | "eslint-plugin-security": "^1.5.0", 87 | "eslint-plugin-sort-destructure-keys": "^1.4.0", 88 | "eslint-plugin-unicorn": "^43.0.2", 89 | "husky": "^8.0.2", 90 | "installed-check": "^6.0.4", 91 | "mocha": "^10.1.0", 92 | "npm-run-all2": "^6.0.4", 93 | "type-coverage": "^2.25.0", 94 | "typescript": "~5.0.2" 95 | }, 96 | "directories": { 97 | "test": "test" 98 | }, 99 | "repository": { 100 | "type": "git", 101 | "url": "git+https://github.com/dependency-check-team/dependency-check.git" 102 | }, 103 | "bugs": { 104 | "url": "https://github.com/dependency-check-team/dependency-check/issues" 105 | }, 106 | "homepage": "https://github.com/dependency-check-team/dependency-check", 107 | "license": "BSD-3-Clause" 108 | } 109 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # dependency-check 2 | 3 | **Spring 2024: Deprecated in favor of [`knip`](https://github.com/webpro/knip), which is a way more powerful and comprehensive approach** 4 | 5 | checks which modules you have used in your code and then makes sure they are listed as dependencies in your package.json, or vice-versa 6 | 7 | [![npm version](https://img.shields.io/npm/v/dependency-check.svg?style=flat)](https://www.npmjs.com/package/dependency-check) 8 | [![npm version, next](https://img.shields.io/npm/v/dependency-check/next)](https://www.npmjs.com/package/dependency-check) 9 | [![npm downloads](https://img.shields.io/npm/dm/dependency-check.svg?style=flat)](https://www.npmjs.com/package/dependency-check) 10 | [![ES Module Ready Badge](https://img.shields.io/badge/es%20module%20ready-yes-success.svg)](https://esmodules.dev/) 11 | [![Types in JS](https://img.shields.io/badge/types_in_js-yes-brightgreen)](https://github.com/voxpelli/types-in-js) 12 | [![JavaScript Style Guide](https://img.shields.io/badge/code_style-standard-brightgreen.svg)](https://standardjs.com) 13 | 14 | ## requirements for maintained majors 15 | 16 | If used on an older Node.js version `dependency-check` will fail silently rather than crashing your tests. 17 | 18 | * `5.x` supports Node.js 14 and later 19 | * `4.x` supports Node.js 10 and later 20 | * `3.x` supports Node.js 6 and later 21 | * `2.x` supports Node.js 0.10 and later (Dev note: published using the `legacy` tag) 22 | 23 | For more info on maintenance status, see [SECURITY.md](./SECURITY.md). 24 | 25 | ## how it works 26 | 27 | `dependency-check` parses your module code starting from the default entry files (e.g. `index.js` or `main` and any `bin` commands defined in package.json or if specific files has been defined, then those) and traverses through all relatively required JS files, ultimately producing a list of non-relative modules 28 | 29 | * **relative** - e.g. `require('./a-relative-file.js')`, if one of these are encountered the required file will be recursively parsed by the `dependency-check` algorithm 30 | * **non-relative** - e.g. `require('a-module')`, if one of these are encountered it will get added to the list of dependencies, but sub-dependencies of the module will not get recursively parsed 31 | 32 | the goal of this module is to simply check that all non-relative modules that get `require()`'d are in package.json, which prevents people from getting 'module not found' errors when they install your module that has missing deps which was accidentally published to NPM (happened to me all the time, hence the impetus to write this module). 33 | 34 | ## cli usage 35 | 36 | ``` 37 | $ npm install dependency-check -g 38 | $ dependency-check 39 | 40 | # e.g. 41 | 42 | $ dependency-check ./package.json --verbose 43 | Success! All dependencies used in the code are listed in package.json 44 | Success! All dependencies in package.json are used in the code 45 | $ dependency-check ./package.json --missing --verbose 46 | Success! All dependencies used in the code are listed in package.json 47 | $ dependency-check ./package.json --unused --verbose 48 | Success! All dependencies in package.json are used in the code 49 | 50 | # or with file input instead: 51 | 52 | $ dependency-check ./index.js 53 | 54 | # even with globs and multiple inputs: 55 | 56 | $ dependency-check ./test/**/*.js ./lib/*.js 57 | ``` 58 | 59 | `dependency-check` exits with code 1 if there are discrepancies, in addition to printing them out 60 | 61 | To always exit with code 0 pass `--ignore` 62 | 63 | ### --missing 64 | 65 | running `dependency-check ./package.json --missing` will only do the check to make sure that all modules in your code are listed in your package.json 66 | 67 | ### --unused 68 | 69 | running `dependency-check ./package.json --unused` will only do the inverse of the missing check and will tell you which modules in your package.json dependencies **were not used** in your code 70 | 71 | ### --no-dev 72 | 73 | running `dependency-check ./package.json --unused --no-dev` will not tell you if any devDependencies in your package.json were missing or unused 74 | 75 | ### --no-peer 76 | 77 | running `dependency-check ./package.json --unused --no-peer` will not tell you if any peerDependencies in your package.json were missing or unused 78 | 79 | ### --ignore-module, -i 80 | 81 | ignores a module. This works for both `--unused` and `--missing`. You can specify as many separate `--ignore-module` arguments as you want. For example running `dependency-check ./package.json --unused --ignore-module foo` will not tell you if the `foo` module was not used in your code. Supports globbing patterns through the use of [micromatch](https://www.npmjs.com/package/micromatch), so eg. `--ignore-module "@types/*"` is possible 82 | 83 | ### --ignore-unknown-extensions, -u 84 | 85 | won't fail when finding imported files with file extensions that no detectives has been registered for 86 | 87 | ### --no-default-entries 88 | 89 | running eg. `dependency-check package.json tests.js --no-default-entries` won't add any default entries despite the main path given being one to a package.json or module folder. So only the `tests.js` file will be checked 90 | 91 | ### --extensions, -e 92 | 93 | running `dependency-check ./package.json -e js,cjs:detective` will resolve require paths to `.js` and `.cjs` paths, and parse using [`detective`](https://www.npmjs.com/package/detective). Specifying any extension will disable the default detectives. Specify just `-e js,cjs` to use the standard detective, `-e foo::` to use ignore the extension, `-e js:precinct/es6` to specify a specific `precinct` setting 94 | 95 | ### --detective 96 | 97 | running `dependency-check ./package.json --detective detective` will `require()` the local `detective` as the default parser. This can be set per-extension using using `-e`. Defaults to parsing with [`precinct`](https://www.npmjs.com/package/precinct). 98 | 99 | ### --json, -j 100 | 101 | formats the output as a json object 102 | 103 | ### --verbose 104 | 105 | running with `--verbose` will enable a log message on success, otherwise dependency-check only logs on failure. 106 | 107 | ### --help 108 | 109 | shows above options and all other available options 110 | 111 | ## auto check before every npm publish 112 | 113 | add this to your `.bash_profile`/`.bashrc` 114 | 115 | ```sh 116 | # originally from https://gist.github.com/mafintosh/405048d304fbabb830b2 117 | npm () { 118 | ([ "$1" != "publish" ] || dependency-check .) && command npm "$@" 119 | } 120 | ``` 121 | 122 | now when you do `npm publish` and you have missing dependencies it won't publish, e.g.: 123 | 124 | ``` 125 | $ npm publish 126 | Fail! Dependencies not listed in package.json: siblings 127 | $ npm install --save siblings 128 | $ npm publish # works this time 129 | ``` 130 | 131 | ## protips 132 | 133 | - [detective](https://www.npmjs.org/package/detective)-style packages are used for parsing `require()` statements, which means it only does **static requires**. this means you should convert things like `var foo = "bar"; require(foo)` to be static, e.g. `require("bar")` 134 | - use globbing to effectively add all the files you want to check 135 | -------------------------------------------------------------------------------- /test/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "mocha": true 4 | }, 5 | "rules": { 6 | "no-unused-expressions": 0 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /test/check.spec.js: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | /// 4 | 5 | import { sep } from 'node:path' 6 | 7 | import chai from 'chai' 8 | import chaiAsPromised from 'chai-as-promised' 9 | 10 | import { check } from '../index.js' 11 | import { mockPkg, mockUsed } from './mocks.js' 12 | 13 | chai.use(chaiAsPromised) 14 | 15 | const should = chai.should() 16 | 17 | describe('check()', () => { 18 | it('should throw on missing input', async () => { 19 | // @ts-ignore 20 | await check() 21 | .should.be.rejectedWith(Error, 'Requires an opts argument to be set') 22 | }) 23 | 24 | it('should throw on invalid input', async () => { 25 | // @ts-ignore 26 | await check({}) 27 | .should.be.rejectedWith(Error, 'Requires a path to be set') 28 | }) 29 | 30 | it('should throw on invalid path', async () => { 31 | // @ts-ignore 32 | await check({ path: true }) 33 | .should.be.rejectedWith(TypeError, 'Requires path to be a string, got: boolean') 34 | }) 35 | 36 | it('should throw when finding no package.json', async () => { 37 | await check({ path: './yet/another/missing/path' }) 38 | .should.be.rejectedWith(Error, `Failed to find package.json, path "./yet/another/missing/path" does not resolve to any file for "${process.cwd()}"`) 39 | }) 40 | 41 | it('should resolve when given given proper input', async () => { 42 | const result = await check({ path: 'test/mock-positive/' }) 43 | 44 | should.exist(result) 45 | result.should.have.property('package').that.deep.equals(mockPkg()) 46 | result.should.have.property('used').with.members(mockUsed()) 47 | result.should.not.have.property('builtins') 48 | }) 49 | 50 | it('should return used builtins when requested', async () => { 51 | const result = await check({ 52 | path: 'test/mock-positive/', 53 | builtins: true 54 | }) 55 | 56 | should.exist(result) 57 | result.should.have.property('package').that.deep.equals(mockPkg()) 58 | result.should.have.property('used').with.members(mockUsed()) 59 | result.should.have.property('builtins').which.deep.equals(['path']) 60 | }) 61 | 62 | it('should be able to skip default entries', async () => { 63 | const result = await check({ 64 | path: 'test/mock-positive/', 65 | noDefaultEntries: true, 66 | entries: ['scoped.js'] 67 | }) 68 | 69 | should.exist(result) 70 | result.should.have.property('package').that.deep.equals(mockPkg()) 71 | result.should.have.property('used').with.members([ 72 | '@scope/test1', 73 | '@scope/test2' 74 | ]) 75 | }) 76 | 77 | it('should throw when finding no entries', async () => { 78 | await check({ 79 | path: 'test/mock-positive/', 80 | noDefaultEntries: true 81 | }) 82 | .should.be.rejectedWith(Error, 'No entry paths found') 83 | }) 84 | 85 | it('should add bin files defined in package.json as default entry', async () => { 86 | const result = await check({ 87 | path: './', 88 | entries: ['./lib/cli-engine.js'] 89 | }) 90 | 91 | should.exist(result) 92 | result.should.have.nested.property('package.name', 'dependency-check') 93 | result.should.have.nested.property('package.bin').which.deep.equals({ 94 | 'dependency-check': 'cli.cjs' 95 | }) 96 | result.should.have.property('used').which.includes('meow') 97 | }) 98 | 99 | it('should handle path simply set to "package.json"', async () => { 100 | const result = await check({ 101 | path: 'package.json', 102 | }) 103 | 104 | should.exist(result) 105 | result.should.have.nested.property('package.name', 'dependency-check') 106 | }) 107 | 108 | it('should throw when file has no detective', async () => { 109 | await check({ 110 | path: process.cwd(), 111 | noDefaultEntries: true, 112 | entries: ['readme.md'] 113 | }) 114 | .should.be.rejectedWith(Error, `Detective function missing for "${process.cwd() + sep}readme.md"`) 115 | }) 116 | 117 | it('should throw when encountering local file that does not exist', async () => { 118 | await check({ 119 | path: 'test/mock-missing-local-file/' 120 | }) 121 | .should.be.rejectedWith(Error, `Cannot find module '${process.cwd() + sep}test${sep}mock-missing-local-file${sep}not-found.js' from '${process.cwd() + sep}test${sep}mock-missing-local-file'`) 122 | }) 123 | }) 124 | -------------------------------------------------------------------------------- /test/extensions.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable unicorn/no-useless-undefined */ 2 | /// 3 | /// 4 | /// 5 | 6 | import chai from 'chai' 7 | import chaiAsPromised from 'chai-as-promised' 8 | 9 | import { ErrorWithCause } from 'pony-cause' 10 | 11 | import { getExtensions } from '../lib/extensions.js' 12 | 13 | chai.use(chaiAsPromised) 14 | 15 | const should = chai.should() 16 | 17 | const defaultDetective = () => [] 18 | const specificDetective = () => [] 19 | 20 | describe('getExtensions()', () => { 21 | it('should throw on invalid extensions argument', async () => { 22 | // @ts-ignore 23 | await getExtensions(true).should.be.rejectedWith( 24 | TypeError, 25 | 'Requires extensions argument to be an array or object' 26 | ) 27 | }) 28 | 29 | it('should throw on invalid detective argument', async () => { 30 | // @ts-ignore 31 | await getExtensions(undefined, true).should.be.rejectedWith( 32 | TypeError, 33 | 'Requires detective to be a string or a function' 34 | ) 35 | }) 36 | 37 | it('should return default setup on no input', async () => { 38 | const result = await getExtensions(undefined, undefined) 39 | 40 | should.exist(should) 41 | 42 | result.should.have.all.keys([ 43 | '.cjs', 44 | '.js', 45 | '.json', 46 | '.jsx', 47 | '.mjs', 48 | '.node', 49 | '.ts', 50 | '.tsx', 51 | ]) 52 | 53 | result.should.have.property('.cjs').which.is.a('function') 54 | }) 55 | 56 | it('should only return requested extensions', async () => { 57 | const result = await getExtensions(['.js', '.json'], undefined) 58 | 59 | should.exist(should) 60 | 61 | result.should.have.all.keys([ 62 | '.js', 63 | '.json', 64 | ]) 65 | 66 | result.should.have.property('.js').which.is.a('function') 67 | }) 68 | 69 | it('should use provided detectives', async () => { 70 | const extensions = { 71 | '.js': undefined, 72 | '.json': specificDetective 73 | } 74 | 75 | const result = await getExtensions(extensions, defaultDetective) 76 | 77 | should.exist(should) 78 | 79 | result.should.deep.equal({ 80 | '.js': defaultDetective, 81 | '.json': specificDetective, 82 | }) 83 | }) 84 | 85 | it('should default to noop-detective for .json and .node', async () => { 86 | const extensions = { 87 | '.js': specificDetective, 88 | '.cjs': undefined, 89 | '.json': undefined, 90 | '.node': undefined 91 | } 92 | 93 | const result = await getExtensions(extensions, defaultDetective) 94 | 95 | should.exist(should) 96 | 97 | result.should.have.all.keys([ 98 | '.js', 99 | '.cjs', 100 | '.json', 101 | '.node', 102 | ]) 103 | 104 | result.should.have.property('.js', specificDetective) 105 | result.should.have.property('.cjs', defaultDetective) 106 | result.should.have.property('.json').which.is.a('function').and.is.not.equal(defaultDetective) 107 | result.should.have.property('.node').which.is.a('function').and.is.not.equal(defaultDetective) 108 | }) 109 | 110 | it('should use named detective', async () => { 111 | const extensions = { 112 | '.js': undefined, 113 | '.mjs': undefined, 114 | '.cjs': 'detective-cjs' 115 | } 116 | 117 | const result = await getExtensions(extensions, defaultDetective) 118 | 119 | should.exist(should) 120 | 121 | result.should.have.all.keys([ 122 | '.js', 123 | '.mjs', 124 | '.cjs', 125 | ]) 126 | 127 | result.should.have.property('.js').which.is.a('function').and.equals(defaultDetective) 128 | result.should.have.property('.mjs').which.is.a('function').and.equals(result['.js']) 129 | result.should.have.property('.cjs').which.is.a('function').and.is.not.equal(result['.js']) 130 | }) 131 | 132 | it('should throw on missing precinct type', async () => { 133 | await getExtensions(undefined, 'precinct/').should.be.rejectedWith( 134 | Error, 135 | 'Expected a "precinct/something", but got "precinct/"' 136 | ) 137 | }) 138 | 139 | it('should throw on missing detective module', async () => { 140 | await getExtensions(undefined, '@dependency-check-team/yet-another-missing-module').should.be.rejectedWith( 141 | ErrorWithCause, 142 | "Failed to load detective '@dependency-check-team/yet-another-missing-module'" 143 | ) 144 | }) 145 | }) 146 | -------------------------------------------------------------------------------- /test/extra.spec.js: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | /// 4 | 5 | import chai from 'chai' 6 | 7 | import { extra } from '../index.js' 8 | import { 9 | mockDevPeerPkg, 10 | mockPkg, 11 | mockUsed, 12 | } from './mocks.js' 13 | 14 | const should = chai.should() 15 | 16 | describe('extra()', () => { 17 | it('should throw on missing pkg', () => { 18 | should.throw(() => { 19 | // @ts-ignore 20 | extra() 21 | }, TypeError, 'Expected a pkg object') 22 | }) 23 | 24 | it('should throw on non-array deps', () => { 25 | should.throw(() => { 26 | // @ts-ignore 27 | extra(mockPkg()) 28 | }, TypeError, 'Expected a deps array') 29 | }) 30 | 31 | it('should pass when given empty package', () => { 32 | // @ts-ignore 33 | const result = extra({}, mockUsed()) 34 | 35 | should.exist(result) 36 | result.should.be.an('array').that.is.empty 37 | }) 38 | 39 | it('should pass when given proper input', () => { 40 | const result = extra(mockPkg(), mockUsed()) 41 | 42 | should.exist(result) 43 | result.should.be.an('array').that.is.empty 44 | }) 45 | 46 | it('should return unused dependencies from pkg', () => { 47 | const count = 2 48 | const deps = mockUsed() 49 | const removed = deps.splice(0, count) 50 | 51 | const result = extra(mockPkg(), deps) 52 | 53 | should.exist(result) 54 | result.should.be.an('array').of.length(count).with.members(removed) 55 | }) 56 | 57 | it('should ignore used dependencies not in pkg', () => { 58 | const deps = [...mockUsed(), 'an-extra-dependency'] 59 | 60 | const result = extra(mockPkg(), deps) 61 | 62 | should.exist(result) 63 | result.should.be.an('array').that.is.empty 64 | }) 65 | 66 | it('should ignore explicitly ignored dependencies', () => { 67 | const result = extra({ 68 | '_id': 'test@0.0.1', 69 | 'dependencies': { 70 | '@scope/test1': '*', 71 | '@scope/test2': '*', 72 | 'async': '*', 73 | 'minimist': '*', 74 | 'resolve': '*', 75 | }, 76 | 'name': 'test', 77 | 'readme': 'ERROR: No README data found!', 78 | 'version': '0.0.1', 79 | }, [ 80 | 'minimist', 81 | 'resolve' 82 | ], { 83 | ignore: '@scope/test1' 84 | }) 85 | 86 | should.exist(result) 87 | result.should.deep.equal(['@scope/test2', 'async']) 88 | }) 89 | 90 | it('should support wildcards in explicitly ignored dependencies', () => { 91 | const result = extra({ 92 | '_id': 'test@0.0.1', 93 | 'dependencies': { 94 | '@scope/test1': '*', 95 | '@scope/test2': '*', 96 | 'async': '*', 97 | 'minimist': '*', 98 | 'resolve': '*', 99 | }, 100 | 'name': 'test', 101 | 'readme': 'ERROR: No README data found!', 102 | 'version': '0.0.1', 103 | }, [ 104 | 'minimist', 105 | 'resolve' 106 | ], { 107 | ignore: '@scope/*' 108 | }) 109 | 110 | should.exist(result) 111 | result.should.deep.equal(['async']) 112 | }) 113 | 114 | it('should support multiple explicitly ignored dependencies', () => { 115 | const result = extra({ 116 | '_id': 'test@0.0.1', 117 | 'dependencies': { 118 | '@scope/test1': '*', 119 | '@scope/test2': '*', 120 | 'async': '*', 121 | 'minimist': '*', 122 | 'resolve': '*', 123 | }, 124 | 'name': 'test', 125 | 'readme': 'ERROR: No README data found!', 126 | 'version': '0.0.1', 127 | }, [ 128 | 'resolve' 129 | ], { 130 | ignore: ['@scope/*', 'async'] 131 | }) 132 | 133 | should.exist(result) 134 | result.should.deep.equal(['minimist']) 135 | }) 136 | 137 | it('should return unused dependencies from all dependency types', () => { 138 | const deps = mockUsed() 139 | 140 | const result = extra(mockDevPeerPkg(), []) 141 | 142 | should.exist(result) 143 | result.should.be.an('array').of.length(deps.length).with.members(deps) 144 | }) 145 | 146 | it('should ignore dev dependencies when requested', () => { 147 | const result = extra(mockDevPeerPkg(), [], { excludeDev: true }) 148 | 149 | should.exist(result) 150 | result.should.be.an('array').of.length(3).with.members([ 151 | '@scope/test1', 152 | 'minimist', 153 | 'resolve', 154 | ]) 155 | }) 156 | 157 | it('should ignore peer dependencies when requested', () => { 158 | const result = extra(mockDevPeerPkg(), [], { excludePeer: true }) 159 | 160 | should.exist(result) 161 | result.should.be.an('array').of.length(3).with.members([ 162 | '@scope/test1', 163 | '@scope/test2', 164 | 'async', 165 | ]) 166 | }) 167 | 168 | it('should ignore both dev and peer dependencies when requested', () => { 169 | const result = extra(mockDevPeerPkg(), [], { excludeDev: true, excludePeer: true }) 170 | 171 | should.exist(result) 172 | result.should.be.an('array').of.length(1).with.members([ 173 | '@scope/test1', 174 | ]) 175 | }) 176 | }) 177 | -------------------------------------------------------------------------------- /test/functional.spec.js: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | /// 4 | 5 | import chai from 'chai' 6 | 7 | import { check, extra, missing } from '../index.js' 8 | 9 | chai.should() 10 | 11 | describe('functional', () => { 12 | it('should correctly handle a simple case', async () => { 13 | const result = await check({ path: 'test/mock-negative/' }) 14 | 15 | extra(result.package, result.used).should.deep.equal(['@scope/test1']) 16 | missing(result.package, result.used).should.deep.equal(['node:foobar', 'example', 'example2']) 17 | }) 18 | 19 | it('should correctly handle a typescript case', async () => { 20 | const result = await check({ path: 'test/mock-negative/abc.ts' }) 21 | 22 | extra(result.package, result.used).should.deep.equal(['@scope/test1']) 23 | missing(result.package, result.used).should.deep.equal(['example3', 'example2']) 24 | }) 25 | }) 26 | -------------------------------------------------------------------------------- /test/missing.spec.js: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | /// 4 | 5 | import chai from 'chai' 6 | 7 | import { missing } from '../index.js' 8 | import { 9 | mockDevPeerPkg, 10 | mockPkg, 11 | mockUsed, 12 | } from './mocks.js' 13 | 14 | const should = chai.should() 15 | 16 | describe('missing()', () => { 17 | it('should throw on missing pkg', () => { 18 | should.throw(() => { 19 | // @ts-ignore 20 | missing() 21 | }, TypeError, 'Expected a pkg object') 22 | }) 23 | 24 | it('should throw on non-array deps', () => { 25 | should.throw(() => { 26 | // @ts-ignore 27 | missing(mockPkg()) 28 | }, TypeError, 'Expected a deps array') 29 | }) 30 | 31 | it('should pass when given empty package', () => { 32 | // @ts-ignore 33 | const result = missing({}, mockUsed()) 34 | 35 | should.exist(result) 36 | result.should.deep.equal(mockUsed()) 37 | }) 38 | 39 | it('should pass when given proper input', () => { 40 | const result = missing(mockPkg(), mockUsed()) 41 | 42 | should.exist(result) 43 | result.should.be.an('array').that.is.empty 44 | }) 45 | 46 | it('should ignore unused dependencies from pkg', () => { 47 | const result = missing(mockPkg(), []) 48 | 49 | should.exist(result) 50 | result.should.be.an('array').which.is.empty 51 | }) 52 | 53 | it('should return used dependencies not in pkg', () => { 54 | const deps = [...mockUsed(), 'an-extra-dependency'] 55 | 56 | const result = missing(mockPkg(), deps) 57 | 58 | should.exist(result) 59 | result.should.be.an('array').of.length(1).with.members(['an-extra-dependency']) 60 | }) 61 | 62 | it('should ignore explicitly ignored dependencies', () => { 63 | const result = missing({ 64 | '_id': 'test@0.0.1', 65 | 'dependencies': { 66 | 'minimist': '*', 67 | 'resolve': '*', 68 | }, 69 | 'name': 'test', 70 | 'readme': 'ERROR: No README data found!', 71 | 'version': '0.0.1', 72 | }, [ 73 | 'async', 74 | 'resolve', 75 | '@scope/test1', 76 | '@scope/test2', 77 | 'minimist', 78 | ], { 79 | ignore: '@scope/test1' 80 | }) 81 | 82 | should.exist(result) 83 | result.should.deep.equal(['async', '@scope/test2']) 84 | }) 85 | 86 | it('should support wildcards in explicitly ignored dependencies', () => { 87 | const result = missing({ 88 | '_id': 'test@0.0.1', 89 | 'dependencies': { 90 | 'minimist': '*', 91 | 'resolve': '*', 92 | }, 93 | 'name': 'test', 94 | 'readme': 'ERROR: No README data found!', 95 | 'version': '0.0.1', 96 | }, [ 97 | 'async', 98 | 'resolve', 99 | '@scope/test1', 100 | '@scope/test2', 101 | 'minimist', 102 | ], { 103 | ignore: '@scope/*' 104 | }) 105 | 106 | should.exist(result) 107 | result.should.deep.equal(['async']) 108 | }) 109 | 110 | it('should support multiple explicitly ignored dependencies', () => { 111 | const result = missing({ 112 | '_id': 'test@0.0.1', 113 | 'dependencies': { 114 | 'resolve': '*', 115 | }, 116 | 'name': 'test', 117 | 'readme': 'ERROR: No README data found!', 118 | 'version': '0.0.1', 119 | }, [ 120 | 'async', 121 | 'resolve', 122 | '@scope/test1', 123 | '@scope/test2', 124 | 'minimist', 125 | ], { 126 | ignore: ['@scope/*', 'async'] 127 | }) 128 | 129 | should.exist(result) 130 | result.should.deep.equal(['minimist']) 131 | }) 132 | 133 | it('should ignore dev dependencies when requested', () => { 134 | const result = missing(mockDevPeerPkg(), mockUsed(), { excludeDev: true }) 135 | 136 | should.exist(result) 137 | result.should.be.an('array').of.length(2).with.members([ 138 | '@scope/test2', 139 | 'async', 140 | ]) 141 | }) 142 | 143 | it('should ignore peer dependencies when requested', () => { 144 | const result = missing(mockDevPeerPkg(), mockUsed(), { excludePeer: true }) 145 | 146 | should.exist(result) 147 | result.should.be.an('array').of.length(2).with.members([ 148 | 'minimist', 149 | 'resolve', 150 | ]) 151 | }) 152 | 153 | it('should ignore both dev and peer dependencies when requested', () => { 154 | const result = missing(mockDevPeerPkg(), mockUsed(), { excludeDev: true, excludePeer: true }) 155 | 156 | should.exist(result) 157 | result.should.be.an('array').of.length(4).with.members([ 158 | '@scope/test2', 159 | 'async', 160 | 'minimist', 161 | 'resolve', 162 | ]) 163 | }) 164 | }) 165 | -------------------------------------------------------------------------------- /test/mock-missing-local-file/index.mjs: -------------------------------------------------------------------------------- 1 | import foo from './not-found.js' 2 | import example from 'example' 3 | -------------------------------------------------------------------------------- /test/mock-missing-local-file/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test-missing-local-dep", 3 | "version": "0.0.1", 4 | "main": "index.mjs", 5 | "dependencies": { 6 | "async": "*", 7 | "@scope/test1": "*" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /test/mock-negative/abc.ts: -------------------------------------------------------------------------------- 1 | // @ts-ignore 2 | import example from 'example3' 3 | // @ts-ignore 4 | import bar from './bar.cjs' 5 | -------------------------------------------------------------------------------- /test/mock-negative/bar.cjs: -------------------------------------------------------------------------------- 1 | const example2 = require('example2'); 2 | const async = require('async'); 3 | -------------------------------------------------------------------------------- /test/mock-negative/foo.js: -------------------------------------------------------------------------------- 1 | const example2 = require('example2/foo'); 2 | const async = require('async'); 3 | -------------------------------------------------------------------------------- /test/mock-negative/index.mjs: -------------------------------------------------------------------------------- 1 | import http from 'http' 2 | import path from 'node:path' 3 | import foobar from 'node:foobar' 4 | import example from 'example' 5 | import foo from './foo.js' 6 | -------------------------------------------------------------------------------- /test/mock-negative/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test-negative", 3 | "version": "0.0.1", 4 | "main": "index.mjs", 5 | "dependencies": { 6 | "async": "*", 7 | "@scope/test1": "*" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /test/mock-positive/abc.ts: -------------------------------------------------------------------------------- 1 | import foo from './foo.js' 2 | -------------------------------------------------------------------------------- /test/mock-positive/donkey/index.js: -------------------------------------------------------------------------------- 1 | require('minimist') 2 | module.exports = {} 3 | -------------------------------------------------------------------------------- /test/mock-positive/foo.js: -------------------------------------------------------------------------------- 1 | module.exports = {} 2 | require('async') 3 | 4 | require('./pizza') 5 | require('./donkey') 6 | require('./scoped') 7 | -------------------------------------------------------------------------------- /test/mock-positive/index.js: -------------------------------------------------------------------------------- 1 | const foo = require('./foo.js') // eslint-disable-line no-unused-vars 2 | const path = require('path') // eslint-disable-line no-unused-vars 3 | -------------------------------------------------------------------------------- /test/mock-positive/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test", 3 | "version": "0.0.1", 4 | "dependencies": { 5 | "async": "*", 6 | "resolve": "*", 7 | "minimist": "*", 8 | "@scope/test1": "*", 9 | "@scope/test2": "*" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /test/mock-positive/pizza.js: -------------------------------------------------------------------------------- 1 | require('./foo') 2 | require('resolve') 3 | -------------------------------------------------------------------------------- /test/mock-positive/scoped.js: -------------------------------------------------------------------------------- 1 | require('@scope/test1') 2 | require('@scope/test2/something') 3 | -------------------------------------------------------------------------------- /test/mocks.js: -------------------------------------------------------------------------------- 1 | 2 | /** @returns {import('read-pkg').NormalizedPackageJson} */ 3 | export const mockPkg = () => ({ 4 | '_id': 'test@0.0.1', 5 | 'dependencies': { 6 | '@scope/test1': '*', 7 | '@scope/test2': '*', 8 | 'async': '*', 9 | 'minimist': '*', 10 | 'resolve': '*', 11 | }, 12 | 'name': 'test', 13 | 'readme': 'ERROR: No README data found!', 14 | 'version': '0.0.1', 15 | }) 16 | 17 | export const mockUsed = () => ([ 18 | 'async', 19 | 'resolve', 20 | '@scope/test1', 21 | '@scope/test2', 22 | 'minimist', 23 | ]) 24 | 25 | /** @returns {import('read-pkg').NormalizedPackageJson} */ 26 | export const mockDevPeerPkg = () => ({ 27 | '_id': 'test@0.0.1', 28 | 'dependencies': { 29 | '@scope/test1': '*', 30 | }, 31 | 'devDependencies': { 32 | '@scope/test2': '*', 33 | 'async': '*', 34 | }, 35 | 'peerDependencies': { 36 | 'minimist': '*', 37 | 'resolve': '*', 38 | }, 39 | 'name': 'test', 40 | 'readme': 'ERROR: No README data found!', 41 | 'version': '0.0.1', 42 | }) 43 | -------------------------------------------------------------------------------- /test/parse.spec.js: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | /// 4 | 5 | import chai from 'chai' 6 | import chaiAsPromised from 'chai-as-promised' 7 | 8 | import { parse } from '../lib/parse.js' 9 | import { mockPkg } from './mocks.js' 10 | 11 | chai.use(chaiAsPromised) 12 | chai.should() 13 | 14 | describe('parse()', () => { 15 | it('should throw on missing files', async () => { 16 | await parse({ 17 | path: 'foo/**/*', 18 | 'package': mockPkg(), 19 | extensions: {}, 20 | builtins: undefined, 21 | ignoreUnknownExtensions: undefined, 22 | noDefaultEntries: undefined, 23 | entries: undefined, 24 | }) 25 | .should.be.rejectedWith(Error, 'No entry paths found') 26 | }) 27 | }) 28 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@voxpelli/tsconfig/node14.json", 3 | "files": [ 4 | "cli.cjs", 5 | "index.js" 6 | ], 7 | "include": [ 8 | "lib/*.js", 9 | "test/*.js" 10 | ], 11 | "compilerOptions": { 12 | "module": "es2022", 13 | "moduleResolution": "node" 14 | } 15 | } 16 | --------------------------------------------------------------------------------