├── .eslintrc.cjs ├── .github ├── ISSUE_TEMPLATE │ ├── BUG_REPORT.yml │ └── FEATURE_REQUEST.yml └── actions │ └── ci.yml ├── .gitignore ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── bin └── depngn.cjs ├── jest.config.ts ├── package-lock.json ├── package.json ├── scripts └── build.ts ├── src ├── cli │ ├── index.ts │ ├── log.ts │ ├── parse.ts │ ├── usage.ts │ └── validate.ts ├── core │ ├── getCompatData.ts │ ├── getDependencies.ts │ ├── getEngines.ts │ ├── getPackageData.ts │ ├── getPackageManager.ts │ └── index.ts ├── index.ts ├── report │ ├── html.ts │ ├── index.ts │ ├── json.ts │ └── table.ts ├── types │ └── index.ts └── utils │ └── index.ts ├── tests ├── integration │ ├── cwd-option.spec.ts │ ├── mocks │ │ └── compat-data.ts │ └── report.spec.ts └── unit │ ├── cli │ ├── usage.spec.ts │ └── validate.spec.ts │ ├── core │ ├── getDependencies │ │ ├── get-dependencies.spec.ts │ │ └── mocks │ │ │ ├── withPackageJson │ │ │ └── package.json │ │ │ └── withoutPackageJson │ │ │ └── notPackage.json │ ├── getEngines │ │ ├── get-engines.spec.ts │ │ └── mocks │ │ │ ├── npm │ │ │ ├── lockfileVersion1 │ │ │ │ ├── package-lock.json │ │ │ │ └── package.json │ │ │ └── lockfileVersion2 │ │ │ │ ├── package-lock.json │ │ │ │ └── package.json │ │ │ └── yarn │ │ │ ├── node_modules │ │ │ ├── test-package-1 │ │ │ │ └── package.json │ │ │ └── test-package-2 │ │ │ │ └── package.json │ │ │ ├── package.json │ │ │ └── yarn.lock │ ├── getPackageData │ │ └── get-package-data.spec.ts │ └── getPackageManager │ │ ├── get-package-manager.spec.ts │ │ └── mocks │ │ ├── npm │ │ └── package-lock.json │ │ └── yarn │ │ └── yarn.lock │ └── report │ └── create.spec.ts └── tsconfig.json /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | es2021: true, 5 | node: true, 6 | }, 7 | extends: [ 8 | 'eslint:recommended', 9 | 'plugin:@typescript-eslint/recommended', 10 | 'plugin:prettier/recommended', 11 | ], 12 | parser: '@typescript-eslint/parser', 13 | parserOptions: { 14 | ecmaVersion: 12, 15 | sourceType: 'module', 16 | }, 17 | plugins: ['@typescript-eslint', 'prettier'], 18 | rules: { 19 | 'prettier/prettier': [ 20 | 'error', 21 | { 22 | printWidth: 100, 23 | trailingComma: 'es5', 24 | semi: true, 25 | singleQuote: true, 26 | }, 27 | ], 28 | '@typescript-eslint/ban-ts-comment': 'off', 29 | '@typescript-eslint/no-var-requires': 'off', 30 | '@typescript-eslint/no-explicit-any': 'off', 31 | '@typescript-eslint/no-non-null-assertion': 'off', 32 | }, 33 | ignorePatterns: ['dist', 'node_modules'], 34 | }; 35 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/BUG_REPORT.yml: -------------------------------------------------------------------------------- 1 | name: Bug Report 2 | description: File a bug report 3 | title: "[Bug]: " 4 | labels: ["bug"] 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: | 9 | Thanks for taking the time to fill out this bug report! 10 | validations: 11 | required: false 12 | - type: textarea 13 | id: expected-behavior 14 | attributes: 15 | label: Expected Behavior 16 | description: What did you expect to happen? 17 | placeholder: I expected... 18 | validations: 19 | required: true 20 | - type: textarea 21 | id: actual-behavior 22 | attributes: 23 | label: Actual Behavior 24 | description: But what actually happened? 25 | placeholder: But I got... 26 | validations: 27 | required: true 28 | - type: textarea 29 | id: reproduce 30 | attributes: 31 | label: Steps to Reproduce 32 | description: What lead to this bug? 33 | placeholder: But I got... 34 | value: | 35 | 1. 36 | 2. 37 | 3. 38 | 4. 39 | validations: 40 | required: true 41 | - type: textarea 42 | id: additional-info 43 | attributes: 44 | label: Additional Information 45 | description: Anything else that's relevant. 46 | validations: 47 | required: false 48 | - type: dropdown 49 | id: browsers 50 | attributes: 51 | label: What package manager are you seeing the problem with? 52 | multiple: true 53 | options: 54 | - npm 55 | - yarn 56 | - type: textarea 57 | id: logs 58 | attributes: 59 | label: Relevant log output 60 | description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks. 61 | render: shell 62 | - type: checkboxes 63 | id: terms 64 | attributes: 65 | label: Code of Conduct 66 | description: By submitting this issue, you agree to follow our [Code of Conduct](https://github.com/ombulabs/depngn/blob/main/CODE_OF_CONDUCT.md) 67 | options: 68 | - label: I agree to follow this project's Code of Conduct 69 | required: true 70 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/FEATURE_REQUEST.yml: -------------------------------------------------------------------------------- 1 | name: Feature Request 2 | description: Request a new feature or change to an existing feature 3 | title: "[Feature Request]: " 4 | labels: ["enhancement"] 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: | 9 | Thanks for taking the time to fill out this feature request! 10 | validations: 11 | required: false 12 | - type: textarea 13 | id: feature-request 14 | attributes: 15 | label: Describe your request 16 | description: Also, please describe why you'd like to see this change. 17 | placeholder: I would like to request... 18 | validations: 19 | required: true 20 | - type: textarea 21 | id: possible-implementation 22 | attributes: 23 | label: Possible Implementation 24 | description: If you can, describe how this might be implemented. 25 | validations: 26 | required: false 27 | - type: checkboxes 28 | id: terms 29 | attributes: 30 | label: Code of Conduct 31 | description: By submitting this issue, you agree to follow our [Code of Conduct](https://github.com/ombulabs/depngn/blob/main/CODE_OF_CONDUCT.md) 32 | options: 33 | - label: I agree to follow this project's Code of Conduct 34 | required: true 35 | -------------------------------------------------------------------------------- /.github/actions/ci.yml: -------------------------------------------------------------------------------- 1 | name: Node.js CI 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: ubuntu-latest 13 | 14 | strategy: 15 | matrix: 16 | node-version: [10.x, 12.x, 14.x, 16.x, 18.x] 17 | 18 | steps: 19 | - uses: actions/checkout@v3 20 | - name: Use Node.js ${{ matrix.node-version }} 21 | uses: actions/setup-node@v3 22 | with: 23 | node-version: ${{ matrix.node-version }} 24 | - run: npm run build --if-present 25 | - run: npm install 26 | - run: npm test -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | *dist 13 | dist-ssr 14 | *.local 15 | 16 | # Editor directories and files 17 | .vscode/* 18 | !.vscode/extensions.json 19 | .idea 20 | .DS_Store 21 | *.suo 22 | *.ntvs* 23 | *.njsproj 24 | *.sln 25 | *.sw? 26 | 27 | # testing 28 | coverage 29 | !tests/**/node_modules 30 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # main (unreleased) 2 | 3 | # 0.6.0 4 | - [[feature]: add the ability to generate a report file in the desired path and with desired name](https://github.com/upgradejs/depngn/pull/46) 5 | 6 | # 0.5.0 7 | - [[feature]: export types and migrate from `rollup` to `tsup` for the build purposes](https://github.com/upgradejs/depngn/pull/41) 8 | - [[feature]: add tests](https://github.com/upgradejs/depngn/pull/40) 9 | 10 | # 0.4.0 11 | - [[feature]: refactor configuration files to export the types and declaration](https://github.com/upgradejs/depngn/pull/41) 12 | - [[feature]: refactor `depngn` internals to read files directly, instead of using `npm`/`yarn` commands](https://github.com/upgradejs/depngn/pull/39) 13 | - [[bugfix]: filter out `__ngcc_entry_points__.json` file](https://github.com/upgradejs/depngn/pull/38) 14 | - [[bugfix]: remove whitespace from certain malformed version ranges](https://github.com/upgradejs/depngn/pull/37) 15 | - [[bugfix]: support Node versions 10-14](https://github.com/upgradejs/depngn/pull/35) 16 | 17 | # 0.3.0 18 | - [[feature]: add the ability to perform the check in the specified path using `--cwd` option](https://github.com/upgradejs/depngn/pull/30) 19 | 20 | # 0.2.0 21 | - [[feature]: add links to the GitHub repo to be able to navigate from the npm package page](https://github.com/upgradejs/depngn/pull/27) 22 | - [[bugfix]: keep README.md, CONTRIBUTING.md and CHANGELOG.md current and up to date](https://github.com/upgradejs/depngn/pull/27) 23 | - [[feature]: add support for packages that specify * versions](https://github.com/upgradejs/depngn/pull/19) 24 | - [[feature]: add support for html reporter](https://github.com/upgradejs/depngn/pull/21) 25 | - [[feature]: start using GitHub Actions for CI](https://github.com/upgradejs/depngn/pull/23) 26 | - [[feature]: allow us to use node 14](https://github.com/upgradejs/depngn/pull/24) 27 | 28 | # 0.1.4 29 | - [[feature]: use `process.versions.node`](https://github.com/upgradejs/depngn/pull/9) 30 | 31 | # 0.1.3 32 | - [[chore]: add jest config](https://github.com/upgradejs/depngn/pull/6) 33 | - [[feature]: refactor logs, refactor package manager logic, fix RegEx bug](https://github.com/upgradejs/depngn/pull/7) 34 | 35 | # 0.1.2 36 | - [[bugfix]: update shape of data returned from yarn](https://github.com/ombulabs/depngn/pull/5) 37 | 38 | # 0.1.1 39 | - [[bugfix]: specify depth in npm ls to avoid errors from uninstalled peerDeps](https://github.com/ombulabs/depngn/pull/1) 40 | - [[feature]: refactor and disable logging for standalone package](https://github.com/ombulabs/depngn/pull/2) 41 | - [[feature]: revert logging change](https://github.com/ombulabs/depngn/pull/3) 42 | 43 | # 0.1.0 44 | - initial commit 45 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to make participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies within all project spaces, and it also applies when 49 | an individual is representing the project or its community in public spaces. 50 | Examples of representing a project or community include using an official 51 | project e-mail address, posting via an official social media account, or acting 52 | as an appointed representative at an online or offline event. Representation of 53 | a project may be further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at oss@ombulabs.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | ## Getting Started 4 | 5 | ```bash 6 | git clone git@github.com:upgradejs/depngn.git 7 | cd depngn 8 | npm install 9 | ``` 10 | 11 | To be able to test running the CLI manually: 12 | 13 | ```bash 14 | npm run build 15 | npm link 16 | depngn --help 17 | ``` 18 | 19 | ## How it works 20 | 21 | The standalone `depngn` package essentially does 4 things: 22 | 23 | - determines a user's package manager by looking in the CWD for a lock file (`package-lock.json`/`yarn.lock`) 24 | - reads a user's top-level dependencies from `package.json` (`dependencies`, `devDependencies`, `peerDependencies`) 25 | - reads each dependency's `engines.node` field (if present) from either `package-lock.json` (`npm`) or traversing `node_modules` and reading each dependency's `package.json` (`yarn`) 26 | - determines whether the Node version in question is supported by each dependency 27 | 28 | If run with the CLI, you can receive your data in the following formats: 29 | - table (printed to the console) 30 | - json (written to the CWD) 31 | - html (written to the CWD) 32 | 33 | The project is split into two directories -- `depngn`, where the modules for reading and parsing dependency information live. And `cli`, where the functionality of the CLI lives. 34 | 35 | ## Tests 36 | 37 | Tests live in the aptly named `tests` directory (which is split into `unit` and `integration` directories). Some tests require reading from the filesystem and so we've added some mock directories with/without expected files so we can test functions. Inside `tests/depngn` you'll see some directories that contain a `mocks` directory with `package.json`, `package-lock.json`, or `yarn.lock` files that are set up for specific test cases. If you are adding tests for `getDepdencies` or `getEngines`, you may need to add a new directory inside `mocks` and use `process.chdir` to make sure the function is being executed inside it. 38 | 39 | ```typescript 40 | const passingCaseDir = 'tests/depngn/path/to/test/dir'; 41 | const failingCaseDir = 'tests/depngn/path/to/test/dir'; 42 | 43 | // save original cwd 44 | const originalCwd = process.cwd(); 45 | 46 | describe('the function', () => { 47 | afterAll(() => { 48 | // make sure you return to original cwd after tests run 49 | process.chdir(path.resolve(originalCwd)); 50 | }); 51 | 52 | it('does the thing i want', () => { 53 | // change to relevent directory 54 | process.chdir(path.resolve(originalCwd, passingCaseDir)); 55 | // run tests and assert stuff 56 | }); 57 | 58 | it('does not do the thing i want', () => { 59 | // change to relevent directory 60 | process.chdir(path.resolve(originalCwd, failingCaseDir)); 61 | // run tests and assert stuff 62 | }); 63 | }); 64 | ``` 65 | 66 | ## When Submitting a Pull Request: 67 | 68 | - If your PR closes any open GitHub issues, please include `Closes #XXXX` in your comment. 69 | - Please include a line in the CHANGELOG.md so that it's easier to release new versions. 70 | - Please include a summary of the change and which issue is fixed or which feature is introduced. 71 | - If changes to the behavior are made, clearly describe what are the changes and why. 72 | - If changes to the UI are made, please include screenshots of the before and after. 73 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 OmbuLabs | The Lean Software Boutique 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 | # depngn (short for dependency engine) 2 | 3 | A CLI tool to find out if your dependencies support a given version of `node`. 4 | It fetches the `engines` field of your dependencies' `package.json` file and, 5 | if it's present, determines whether or not the version of `node` satisfies the 6 | range of supported versions. 7 | 8 | ## CLI 9 | 10 | ### Usage 11 | 12 | ```bash 13 | npx depngn [options] 14 | 15 | # examples 16 | npx depngn 10.0.0 17 | 18 | npx depngn 14.17.6 --reporter=json 19 | ``` 20 | 21 | ### Node Version 22 | 23 | `depngn` will accept any single value version of `node` as an argument (ie, not a range). If no version is given, it will attempt to determine your current `node` version and use that. 24 | 25 | ### Options 26 | 27 | `depngn` supports these options: 28 | 29 | - `--help` 30 | - `--cwd` 31 | - `--reporter` 32 | - `--reportDir` 33 | - `--reportFileName` 34 | 35 | #### `--cwd` 36 | 37 | Specify the path where you want the check to be performed 38 | 39 | #### `--reporter` 40 | 41 | These are the valid values for `--reporter`: 42 | 43 | - `terminal` (**default**): It will output a table to the terminal. 44 | - `html`: It will generate an HTML file named `compat.html` to the directory the 45 | command is executed in. 46 | - `json`: It will write a file named `compat.json` to the directory the command 47 | is executed in. It uses the following format: 48 | 49 | ```javascript 50 | [package_name]: { 51 | compatible: boolean // whether or not this package will work with the given Node version 52 | range: string // the range of supported Node versions 53 | } 54 | ``` 55 | 56 | #### `--reportDir` 57 | This allows you to specify the path where you want the report to be generated. If no path is specified, it will default to the current working directory. 58 | 59 | #### `--reportFileName` 60 | This allows you to specify the name of the report file. If no name is specified, it will default to `compat`. 61 | 62 | ### A Note on The Engines Field 63 | 64 | The `engines` field in `package.json` is optional and many libraries don't include it. If that's the case, the output for that package will be: 65 | 66 | ```javascript 67 | { 68 | compatible: undefined, 69 | range: 'n/a' 70 | } 71 | ``` 72 | 73 | ## Standalone Package 74 | 75 | You can also import `depngn` as a standalone function to use in your own CLI 76 | tools. It takes an object as an argument: 77 | 78 | ```typescript 79 | interface Options { 80 | version: string; 81 | cwd: string | undefined; 82 | } 83 | ``` 84 | 85 | And it returns a promise that resolves to: 86 | 87 | ```typescript 88 | type DepngnReturn = Record; 89 | 90 | interface CompatData { 91 | compatible: boolean | 'invalid' | undefined; 92 | range: string; 93 | } 94 | ``` 95 | 96 | ### Usage 97 | 98 | ```javascript 99 | import { depngn } from 'depngn'; 100 | 101 | const generateReport = async () => { 102 | return await depngn({ version: '10.0.0' }); 103 | }; 104 | ``` 105 | 106 | There's also a chance there *is* an `engines` field specified in the package, but the range is invalid in some way. Since RegEx for SemVer can be tricky, we return the following, if that's the case: 107 | 108 | ```javascript 109 | { 110 | compatible: 'invalid', 111 | range: '1 .2 . 0not-a-valid-range' 112 | } 113 | ``` 114 | 115 | ## Report module 116 | 117 | You can import `report` (the function that generates a report file when using CLI) as a standalone function to use in your tools to create reports exactly when you need them. It takes two arguments - the first is a result of the `depngn` function, and the second is an object with options: 118 | 119 | ```typescript 120 | interface CliOptions { 121 | version: string; 122 | cwd: string | undefined; 123 | reporter: 'terminal' | 'html' | 'json' | undefined; 124 | reportDir: string | undefined; 125 | reportFileName: string | undefined; 126 | } 127 | ``` 128 | 129 | It returns a promise that resolves as a report file of the given type (`html`, `json`) or prints the result to the console if the report is not provided or is `terminal`. 130 | 131 | ### Usage 132 | 133 | ```javascript 134 | import { report } from 'depngn/report'; 135 | 136 | const createReport = async () => { 137 | const desiredVersion = '10.0.0'; 138 | const result = await depngn({ version: desiredVersion }); 139 | await report(result, { version: desiredVersion, reportDir: './dependencies-reports', reportFileName: 'depngn' }); 140 | }; 141 | ``` 142 | 143 | ## Supported Package Managers 144 | 145 | For now, this package supports `npm` and `yarn`. If you want support for 146 | your favorite package manager, feel free to open a PR! 147 | 148 | ## Development 149 | 150 | In order to start contributing to `depngn`, you can follow these steps: [CONTRIBUTING.md](CONTRIBUTING.md) 151 | 152 | ## CHANGELOG 153 | 154 | If you want to see what changed between versions: [CHANGELOG.md](CHANGELOG.md) 155 | 156 | ## Possible future features 157 | - Support the ability to sort and/or filter output 158 | - Ignore irrelevant dependencies (ie, `@types/`) 159 | - Support all `node` versions (pretty sure this should work going back to `node` version `10`, but if we wrote our own versions of some dependencies, we could support further back. the main offender is `table` (`>=10.0.0`), but a lot of modern cli table packages seem to only support `node` `10` or `12` and above). 160 | - Support attempting to determine support for dependencies that don't include `engines` field (not sure if it's worth it, since we'd have to fetch the `engines` of the dependency's dependencies and make an educated guess on what the supported version range is) 161 | - Support `pnpm` 162 | -------------------------------------------------------------------------------- /bin/depngn.cjs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | "use strict"; 4 | 5 | const { cli } = require('../dist/cli/index.js'); 6 | 7 | cli(); 8 | -------------------------------------------------------------------------------- /jest.config.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * For a detailed explanation regarding each configuration property and type check, visit: 3 | * https://jestjs.io/docs/configuration 4 | */ 5 | 6 | export default { 7 | // All imported modules in your tests should be mocked automatically 8 | // automock: false, 9 | 10 | // Stop running tests after `n` failures 11 | // bail: 0, 12 | 13 | // The directory where Jest should store its cached dependency information 14 | // cacheDirectory: "/tmp/jest_rs", 15 | 16 | // Automatically clear mock calls, instances, contexts and results before every test 17 | clearMocks: true, 18 | 19 | // Indicates whether the coverage information should be collected while executing the test 20 | collectCoverage: true, 21 | 22 | // An array of glob patterns indicating a set of files for which coverage information should be collected 23 | collectCoverageFrom: [ 24 | 'src/**/*.ts', 25 | '!src/**/index.ts', 26 | '!src/**/types.ts', 27 | '!src/**/log.ts', 28 | '!src/**/parse.ts', 29 | ], 30 | 31 | // The directory where Jest should output its coverage files 32 | coverageDirectory: 'coverage', 33 | 34 | // An array of regexp pattern strings used to skip coverage collection 35 | // coveragePathIgnorePatterns: [ 36 | // "/node_modules/" 37 | // ], 38 | 39 | // Indicates which provider should be used to instrument code for coverage 40 | coverageProvider: 'v8', 41 | 42 | // A list of reporter names that Jest uses when writing coverage reports 43 | // coverageReporters: [ 44 | // "json", 45 | // "text", 46 | // "lcov", 47 | // "clover" 48 | // ], 49 | 50 | // An object that configures minimum threshold enforcement for coverage results 51 | // coverageThreshold: undefined, 52 | 53 | // A path to a custom dependency extractor 54 | // dependencyExtractor: undefined, 55 | 56 | // Make calling deprecated APIs throw helpful error messages 57 | // errorOnDeprecated: false, 58 | 59 | // The default configuration for fake timers 60 | // fakeTimers: { 61 | // "enableGlobally": false 62 | // }, 63 | 64 | // Force coverage collection from ignored files using an array of glob patterns 65 | // forceCoverageMatch: [], 66 | 67 | // A path to a module which exports an async function that is triggered once before all test suites 68 | // globalSetup: undefined, 69 | 70 | // A path to a module which exports an async function that is triggered once after all test suites 71 | // globalTeardown: undefined, 72 | 73 | // A set of global variables that need to be available in all test environments 74 | // globals: {}, 75 | 76 | // The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers. 77 | // maxWorkers: "50%", 78 | 79 | // An array of directory names to be searched recursively up from the requiring module's location 80 | // moduleDirectories: [ 81 | // "node_modules" 82 | // ], 83 | 84 | // An array of file extensions your modules use 85 | // moduleFileExtensions: [ 86 | // "js", 87 | // "mjs", 88 | // "cjs", 89 | // "jsx", 90 | // "ts", 91 | // "tsx", 92 | // "json", 93 | // "node" 94 | // ], 95 | 96 | // A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module 97 | moduleNameMapper: { 98 | '^src(.*)': '/src/$1', 99 | '^core(.*)': '/src/core/$1', 100 | '^cli(.*)': '/src/cli/$1', 101 | }, 102 | 103 | // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader 104 | // modulePathIgnorePatterns: [], 105 | 106 | // Activates notifications for test results 107 | // notify: false, 108 | 109 | // An enum that specifies notification mode. Requires { notify: true } 110 | // notifyMode: "failure-change", 111 | 112 | // A preset that is used as a base for Jest's configuration 113 | preset: 'ts-jest', 114 | 115 | // Run tests from one or more projects 116 | // projects: undefined, 117 | 118 | // Use this configuration option to add custom reporters to Jest 119 | // reporters: undefined, 120 | 121 | // Automatically reset mock state before every test 122 | // resetMocks: false, 123 | 124 | // Reset the module registry before running each individual test 125 | // resetModules: false, 126 | 127 | // A path to a custom resolver 128 | // resolver: undefined, 129 | 130 | // Automatically restore mock state and implementation before every test 131 | // restoreMocks: false, 132 | 133 | // The root directory that Jest should scan for tests and modules within 134 | // rootDir: undefined, 135 | 136 | // A list of paths to directories that Jest should use to search for files in 137 | // roots: [ 138 | // "" 139 | // ], 140 | 141 | // Allows you to use a custom runner instead of Jest's default test runner 142 | // runner: "jest-runner", 143 | 144 | // The paths to modules that run some code to configure or set up the testing environment before each test 145 | // setupFiles: [], 146 | 147 | // A list of paths to modules that run some code to configure or set up the testing framework before each test 148 | // setupFilesAfterEnv: [], 149 | 150 | // The number of seconds after which a test is considered as slow and reported as such in the results. 151 | // slowTestThreshold: 5, 152 | 153 | // A list of paths to snapshot serializer modules Jest should use for snapshot testing 154 | // snapshotSerializers: [], 155 | 156 | // The test environment that will be used for testing 157 | testEnvironment: 'jest-environment-node', 158 | 159 | // Options that will be passed to the testEnvironment 160 | // testEnvironmentOptions: {}, 161 | 162 | // Adds a location field to test results 163 | // testLocationInResults: false, 164 | 165 | // The glob patterns Jest uses to detect test files 166 | // testMatch: [ 167 | // "**/__tests__/**/*.[jt]s?(x)", 168 | // "**/?(*.)+(spec|test).[tj]s?(x)" 169 | // ], 170 | 171 | // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped 172 | // testPathIgnorePatterns: [ 173 | // "/node_modules/" 174 | // ], 175 | 176 | // The regexp pattern or array of patterns that Jest uses to detect test files 177 | // testRegex: [], 178 | 179 | // This option allows the use of a custom results processor 180 | // testResultsProcessor: undefined, 181 | 182 | // This option allows use of a custom test runner 183 | // testRunner: "jest-circus/runner", 184 | 185 | // A map from regular expressions to paths to transformers 186 | // transform: undefined, 187 | 188 | // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation 189 | // transformIgnorePatterns: [ 190 | // "/node_modules/", 191 | // "\\.pnp\\.[^\\/]+$" 192 | // ], 193 | 194 | // An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them 195 | // unmockedModulePathPatterns: undefined, 196 | 197 | // Indicates whether each individual test should be reported during the run 198 | // verbose: undefined, 199 | 200 | // An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode 201 | // watchPathIgnorePatterns: [], 202 | 203 | // Whether to use watchman for file crawling 204 | // watchman: true, 205 | }; 206 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "depngn", 3 | "version": "0.6.0", 4 | "description": "Determine the compatibility of your packages with a given Node version", 5 | "author": "Lewis D'Avanzo", 6 | "license": "MIT", 7 | "bugs": "https://github.com/upgradejs/depngn/issues", 8 | "repository": "github:upgradejs/depngn", 9 | "keywords": [ 10 | "engines", 11 | "dependencies", 12 | "upgrade", 13 | "version", 14 | "semver", 15 | "cli" 16 | ], 17 | "types": "dist/index.d.ts", 18 | "module": "dist/index.mjs", 19 | "main": "dist/index.js", 20 | "exports": { 21 | ".": { 22 | "types": "./dist/index.d.ts", 23 | "import": "./dist/index.mjs", 24 | "default": "./dist/index.js" 25 | }, 26 | "./report": { 27 | "import": "./dist/report/index.mjs", 28 | "default": "./dist/report/index.js" 29 | } 30 | }, 31 | "bin": { 32 | "depngn": "bin/depngn.cjs" 33 | }, 34 | "files": [ 35 | "dist", 36 | "bin", 37 | "src" 38 | ], 39 | "scripts": { 40 | "predev": "npm run build", 41 | "dev": "./bin/depngn.cjs", 42 | "clean": "rimraf dist", 43 | "prebuild": "npm run clean", 44 | "build": "npx ts-node scripts/build.ts", 45 | "prepublishOnly": "npm run build", 46 | "test": "jest", 47 | "test:watch": "jest --watch" 48 | }, 49 | "engines": { 50 | "node": ">=10.0.0" 51 | }, 52 | "dependencies": { 53 | "arg": "^5.0.2", 54 | "compare-versions": "^5.0.1", 55 | "fancy-log": "^2.0.0", 56 | "kleur": "^4.1.5", 57 | "table": "^6.8.0" 58 | }, 59 | "devDependencies": { 60 | "@swc/core": "^1.3.42", 61 | "@types/fancy-log": "^2.0.0", 62 | "@types/jest": "^29.1.1", 63 | "@types/node": "^18.7.19", 64 | "@types/semver": "^7.3.12", 65 | "@types/signal-exit": "^3.0.1", 66 | "@typescript-eslint/eslint-plugin": "^5.38.0", 67 | "@typescript-eslint/parser": "^5.38.1", 68 | "eslint": "^8.24.0", 69 | "eslint-config-prettier": "^8.5.0", 70 | "eslint-plugin-prettier": "^4.2.1", 71 | "prettier": "^2.7.1", 72 | "rimraf": "^3.0.2", 73 | "signal-exit": "^3.0.7", 74 | "ts-jest": "^29.0.3", 75 | "ts-node": "^10.9.1", 76 | "tsup": "^6.7.0", 77 | "typescript": "^4.8.3" 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /scripts/build.ts: -------------------------------------------------------------------------------- 1 | import { Options, build } from 'tsup'; 2 | 3 | const baseCoreConfig: Options = { 4 | entry: ['./src/core/index.ts'], 5 | platform: 'node', 6 | outDir: 'dist', 7 | }; 8 | 9 | Promise.all([ 10 | build({ 11 | ...baseCoreConfig, 12 | format: 'esm', 13 | dts: true, 14 | }), 15 | build({ 16 | ...baseCoreConfig, 17 | format: 'cjs', 18 | }), 19 | build({ 20 | ...baseCoreConfig, 21 | format: 'cjs', 22 | }), 23 | build({ 24 | ...baseCoreConfig, 25 | entry: ['./src/report/index.ts'], 26 | format: 'esm', 27 | outDir: 'dist/report', 28 | }), 29 | build({ 30 | ...baseCoreConfig, 31 | entry: ['./src/report/index.ts'], 32 | format: 'cjs', 33 | outDir: 'dist/report', 34 | }), 35 | build({ 36 | ...baseCoreConfig, 37 | entry: ['./src/cli/index.ts'], 38 | outDir: 'dist/cli', 39 | format: 'cjs', 40 | }), 41 | ]); 42 | -------------------------------------------------------------------------------- /src/cli/index.ts: -------------------------------------------------------------------------------- 1 | import log from 'fancy-log'; 2 | import { execWithLog } from './log'; 3 | import { parseCliArgs } from './parse'; 4 | import { createUsage } from './usage'; 5 | import { validateArgs } from './validate'; 6 | import { depngn } from 'src/core'; 7 | import { report } from 'src/report'; 8 | import { Reporter } from 'src/types'; 9 | 10 | export async function cli() { 11 | try { 12 | const { version, reporter, help, cwd, reportFileName, reportDir } = parseCliArgs(); 13 | if (help) { 14 | createUsage(); 15 | } else { 16 | const typedReporter = reporter as Reporter | undefined; 17 | await validateArgs({ 18 | version, 19 | reporter: typedReporter, 20 | cwd, 21 | reportFileName, 22 | reportDir, 23 | }); 24 | const compatData = await execWithLog( 25 | 'Parsing engine data', 26 | async () => await depngn({ version, cwd }) 27 | ); 28 | await report(compatData, { 29 | version, 30 | reporter: typedReporter, 31 | reportFileName, 32 | reportDir, 33 | }); 34 | } 35 | } catch (error) { 36 | log.error(error); 37 | process.exitCode = 1; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/cli/log.ts: -------------------------------------------------------------------------------- 1 | import process from 'process'; 2 | import { green } from 'kleur/colors'; 3 | import onExit from 'signal-exit'; 4 | 5 | export async function execWithLog(text: string, callback: () => Promise) { 6 | // this is necessary because the 7 | // cursor could remain hidden when 8 | // exited with `^C` 9 | onExit(() => { 10 | showCursor(); 11 | }); 12 | 13 | const dots = ['', '.', '..', '...']; 14 | let index = 0; 15 | 16 | hideCursor(); 17 | const loadingAnimation = setInterval(() => { 18 | clearLog(); 19 | logUpdate(`${text}${dots[index]}`); 20 | if (index === dots.length - 1) { 21 | index = 0; 22 | } else { 23 | index += 1; 24 | } 25 | }, 200); 26 | try { 27 | return await callback(); 28 | // eslint-disable-next-line no-useless-catch 29 | } catch (error) { 30 | throw error; 31 | } finally { 32 | clearInterval(loadingAnimation); 33 | clearLog(); 34 | showCursor(); 35 | } 36 | } 37 | 38 | export function hideCursor() { 39 | process.stdout.write('\x1B[?25l'); 40 | } 41 | 42 | export function showCursor() { 43 | process.stdout.write('\x1B[?25h'); 44 | } 45 | 46 | export function logUpdate(text: string) { 47 | process.stdout.write(green(text)); 48 | } 49 | 50 | export function clearLog() { 51 | process.stdout.clearLine(0); 52 | process.stdout.cursorTo(0); 53 | } 54 | -------------------------------------------------------------------------------- /src/cli/parse.ts: -------------------------------------------------------------------------------- 1 | import arg from 'arg'; 2 | import { versions } from 'process'; 3 | import { Reporter } from 'src/types'; 4 | 5 | export function parseCliArgs() { 6 | const args = arg({ 7 | '--help': Boolean, 8 | '--reporter': String, 9 | '--cwd': String, 10 | '--reportDir': String, 11 | '--reportFileName': String, 12 | '-h': '--help', 13 | '-r': '--reporter', 14 | '-d': '--reportDir', 15 | '-f': '--reportFileName', 16 | }); 17 | const version = args._[0] ?? versions.node; 18 | const reporter = args['--reporter']; 19 | const help = args['--help']; 20 | const cwd = args['--cwd']; 21 | const reportDir = args['--reportDir']; 22 | const reportFileName = args['--reportFileName']; 23 | 24 | return { version, reporter, help, cwd, reportDir, reportFileName }; 25 | } 26 | -------------------------------------------------------------------------------- /src/cli/usage.ts: -------------------------------------------------------------------------------- 1 | export function createUsage() { 2 | const usage = ` 3 | Usage: 4 | depngn [options] 5 | 6 | Options: 7 | -h, --help output usage information 8 | -r, --reporter which reporter for output. options are: terminal (default), json, html 9 | --cwd override the current working directory where to perform dependencies check 10 | -d, --reportDir specifies the directory where to write the report 11 | -f, --reportFileName specifies the name of the report file 12 | 13 | Example: 14 | depngn 12.0.0 --reporter=json 15 | `; 16 | 17 | console.log(usage); 18 | } 19 | -------------------------------------------------------------------------------- /src/cli/validate.ts: -------------------------------------------------------------------------------- 1 | import {validate} from 'compare-versions'; 2 | import {green, red, yellow} from 'kleur/colors'; 3 | import {CliOptions, Reporter} from 'src/types'; 4 | import {pathExists} from 'src/utils'; 5 | 6 | const REPORTERS = [Reporter.Terminal, Reporter.Json, Reporter.Html]; 7 | 8 | export async function validateArgs({ 9 | version, 10 | reporter, 11 | cwd, 12 | reportDir, 13 | reportFileName, 14 | }: CliOptions) { 15 | validateNodeVersion(version); 16 | if (reporter) validateReporter(reporter); 17 | if (cwd) await validateCwd(cwd); 18 | if (reportDir) validateReportDir(reporter); 19 | if (reportFileName) validateReportFileName(reporter); 20 | } 21 | 22 | function validateReportDir(reporter?: Reporter) { 23 | if (!reporter) { 24 | throw new Error( 25 | `When using ${green('--reportDir')} you must also specify ${yellow('--reporter')}.` 26 | ); 27 | } 28 | 29 | if (![Reporter.Html, Reporter.Json].includes(reporter)) { 30 | throw new Error( 31 | `Both ${yellow('--reporterDir')} and ${yellow( 32 | '--reporter' 33 | )} were specified. Either remove one of these options or change the ${yellow( 34 | '--reporter' 35 | )} to ${green(Reporter.Json)} or ${green(Reporter.Html)}.` 36 | ); 37 | } 38 | } 39 | 40 | function validateReportFileName(reporter?: Reporter) { 41 | if (!reporter) { 42 | throw new Error( 43 | `When using ${green('--reportFileName')} you must also specify ${yellow('--reporter')}.` 44 | ); 45 | } 46 | 47 | if (![Reporter.Html, Reporter.Json].includes(reporter)) { 48 | throw new Error( 49 | `Both ${yellow('--reportFileName')} and ${yellow( 50 | '--reporter' 51 | )} were specified. Either remove one of these options or change the ${yellow( 52 | '--reporter' 53 | )} to ${green(Reporter.Json)} or ${green(Reporter.Html)}.` 54 | ); 55 | } 56 | } 57 | 58 | function validateNodeVersion(nodeVersion: string) { 59 | if (!validate(nodeVersion)) { 60 | throw new Error(`Invalid Node version: ${red(nodeVersion)}.`); 61 | } 62 | } 63 | 64 | function validateReporter(reporter: Reporter) { 65 | if (!REPORTERS.includes(reporter)) { 66 | throw new Error( 67 | `Invalid reporter: ${red(reporter)}. Valid options are: ${green(REPORTERS.join(', '))}.` 68 | ); 69 | } 70 | } 71 | 72 | async function validateCwd(cwd: string) { 73 | if (!(await pathExists(cwd))) { 74 | throw new Error(`Invalid cwd: ${red(cwd)}. This directory does not exist.`); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/core/getCompatData.ts: -------------------------------------------------------------------------------- 1 | import { getDependencies } from './getDependencies'; 2 | import { getEngines } from './getEngines'; 3 | import { getPackageData } from './getPackageData'; 4 | import { getPackageManager } from './getPackageManager'; 5 | import { CompatData, EnginesDataArray } from 'src/types'; 6 | 7 | export async function getCompatData(version: string) { 8 | const manager = await getPackageManager(); 9 | const deps = await getDependencies(); 10 | const engines = await getEngines(deps, manager); 11 | return createCompatData(engines, version); 12 | } 13 | 14 | export function createCompatData(compatData: EnginesDataArray, version: string) { 15 | const compatObject: Record = {}; 16 | compatData.forEach((dep) => { 17 | compatObject[dep.package] = getPackageData(dep, version); 18 | }); 19 | return compatObject; 20 | } 21 | -------------------------------------------------------------------------------- /src/core/getDependencies.ts: -------------------------------------------------------------------------------- 1 | import { readJsonFile } from 'src/utils'; 2 | import { PackageJson } from 'src/types'; 3 | 4 | const AUTO_EXCLUDE = [ 5 | // automatically added to `node_modules` in Angular projects 6 | '__ngcc_entry_points__.json', 7 | ]; 8 | 9 | export async function getDependencies(): Promise> { 10 | const cwd = process.cwd(); 11 | const pkg = await readJsonFile(cwd, 'package.json'); 12 | if (!pkg) { 13 | throw new Error(`Unable to find package.json in ${cwd}`); 14 | } 15 | const deps = Object.keys(pkg.dependencies || {}); 16 | const devDeps = Object.keys(pkg.devDependencies || {}); 17 | const peerDeps = Object.keys(pkg.peerDependencies || {}); 18 | 19 | return [...deps, ...devDeps, ...peerDeps].filter((dep) => !AUTO_EXCLUDE.includes(dep)); 20 | } 21 | -------------------------------------------------------------------------------- /src/core/getEngines.ts: -------------------------------------------------------------------------------- 1 | import { readJsonFile } from 'src/utils'; 2 | import { EnginesDataArray, Manager, PackageJson, PackageLock, PackageManagerName } from 'src/types'; 3 | 4 | export async function getEngines(deps: Array, manager: Manager): Promise { 5 | switch (manager.name) { 6 | case PackageManagerName.Npm: { 7 | // eslint-disable-next-line no-useless-catch 8 | try { 9 | return await getNpmEngines(deps, manager); 10 | } catch (error) { 11 | throw error; 12 | } 13 | } 14 | 15 | case PackageManagerName.Yarn: { 16 | // eslint-disable-next-line no-useless-catch 17 | try { 18 | return await getYarnEngines(deps); 19 | } catch (error) { 20 | throw error; 21 | } 22 | } 23 | 24 | default: { 25 | const wrong = manager.name as never; 26 | throw new Error( 27 | `This error shouldn't happen, but somehow an invalid package manager made it through checks: ${wrong}.` 28 | ); 29 | } 30 | } 31 | } 32 | 33 | async function getNpmEngines(deps: Array, manager: Manager) { 34 | // at this point, we know `package-lock.json` exists 35 | // as that's how we determined that `npm` is the package manager 36 | const pkgLock = await readJsonFile(process.cwd(), manager.lockFile); 37 | // `npm` version 7 onwards uses lockfileVersion: 2 38 | // it's JSON keys are named using the full file path. 39 | // in lockfileVersion: 1, it was just the name of the package 40 | const prefix = pkgLock?.lockfileVersion === 2 ? 'node_modules/' : ''; 41 | return deps.map((dep) => { 42 | const range = pkgLock?.packages[`${prefix}${dep}`]?.engines?.node || ''; 43 | return { 44 | package: dep, 45 | range, 46 | }; 47 | }); 48 | } 49 | 50 | async function getYarnEngines(deps: Array) { 51 | const cwd = process.cwd(); 52 | // `yarn.lock` doesn't contain all the same information as package-lock.json, 53 | // so we have to traverse `node_modules` to read each package's `package.json`. 54 | // still faster than fetching from `npm` 55 | return await Promise.all( 56 | deps.map(async (dep) => { 57 | const pkg = await readJsonFile(cwd, 'node_modules', dep, 'package.json'); 58 | const range = pkg?.engines?.node || ''; 59 | return { 60 | package: dep, 61 | range, 62 | }; 63 | }) 64 | ); 65 | } 66 | -------------------------------------------------------------------------------- /src/core/getPackageData.ts: -------------------------------------------------------------------------------- 1 | import { satisfies } from 'compare-versions'; 2 | import { CompatData, EnginesData } from 'src/types'; 3 | 4 | export function getPackageData(dep: EnginesData, version: string): CompatData { 5 | const range = dep.range ? dep.range : 'n/a'; 6 | const compatible = isCompatible(version, dep.range); 7 | return { compatible, range }; 8 | } 9 | 10 | function isCompatible(nodeVersion: string, depRange: string) { 11 | if (!depRange) return undefined; 12 | 13 | // if a dependency has `*` for the node version, it's always compatible 14 | if (['x', '*'].includes(depRange)) return true; 15 | 16 | try { 17 | return depRange 18 | .split('||') 19 | .map((range) => removeWhitespace(range)) 20 | .some((range) => safeSatisfies(nodeVersion, range)); 21 | } catch (error) { 22 | if ((error as Error).message.match(/Invalid argument not valid semver/)) { 23 | return 'invalid'; 24 | } 25 | throw error; 26 | } 27 | } 28 | 29 | // accounts for `AND` ranges -- ie, `'>=1.2.9 <2.0.0'` 30 | function safeSatisfies(nodeVersion: string, range: string) { 31 | return ( 32 | range 33 | .split(' ') 34 | // filter out any whitespace we may have missed with the RegEx -- ie, `'>4 <8'` 35 | .filter((r) => !!r) 36 | .every((r) => satisfies(nodeVersion, r)) 37 | ); 38 | } 39 | 40 | // trims leading and trailing whitespace and whitespace 41 | // between the comparator operators and the actual version number 42 | // version number. ie, ' > = 12.0.0 ' becomes '>=12.0.0' 43 | function removeWhitespace(range: string) { 44 | const comparatorWhitespace = /((?<=(<|>))(\s+)(?=(=)))/g; 45 | const comparatorAndVersionWhiteSpace = /(?<=(<|>|=|\^|~))(\s+)(?=\d)/g; 46 | return range.trim().replace(comparatorWhitespace, '').replace(comparatorAndVersionWhiteSpace, ''); 47 | } 48 | -------------------------------------------------------------------------------- /src/core/getPackageManager.ts: -------------------------------------------------------------------------------- 1 | import { pathExists } from 'src/utils'; 2 | import { Manager, PackageManagerName } from 'src/types'; 3 | 4 | export const MANAGERS: Record = { 5 | [PackageManagerName.Npm]: { 6 | name: PackageManagerName.Npm, 7 | lockFile: 'package-lock.json', 8 | }, 9 | [PackageManagerName.Yarn]: { 10 | name: PackageManagerName.Yarn, 11 | lockFile: 'yarn.lock', 12 | }, 13 | }; 14 | 15 | export async function getPackageManager(): Promise { 16 | const managerChecks = [ 17 | pathExists(MANAGERS[PackageManagerName.Npm].lockFile), 18 | pathExists(MANAGERS[PackageManagerName.Yarn].lockFile), 19 | ]; 20 | const packageManager: PackageManagerName | undefined = await Promise.all(managerChecks).then( 21 | ([isNpm, isYarn]) => { 22 | let manager: PackageManagerName | undefined; 23 | if (isNpm) { 24 | manager = PackageManagerName.Npm; 25 | } else if (isYarn) { 26 | manager = PackageManagerName.Yarn; 27 | } 28 | return manager; 29 | } 30 | ); 31 | if (!packageManager) { 32 | const currentCwd = process.cwd(); 33 | throw new Error( 34 | `Could not determine package manager. You may be missing a lock file or using an unsupported package manager.\nThe search was performed on the path - ${currentCwd}` 35 | ); 36 | } 37 | return MANAGERS[packageManager]; 38 | } 39 | -------------------------------------------------------------------------------- /src/core/index.ts: -------------------------------------------------------------------------------- 1 | import { getCompatData } from './getCompatData'; 2 | import { Options } from 'src/types'; 3 | import path from 'path'; 4 | 5 | export async function depngn({ cwd, version }: Options) { 6 | const originalCwd = process.cwd(); 7 | try { 8 | if (cwd && originalCwd !== cwd) { 9 | // There is no other way to get dependencies while using npm 10 | // rather than being in that particular directory and running a command 11 | // So it is irrelevant to pass the cwd argument down to be further used 12 | // to resolve the path 13 | process.chdir(path.resolve(cwd)); 14 | } 15 | return await getCompatData(version); 16 | } finally { 17 | process.chdir(originalCwd); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './core'; 2 | export * from './types'; 3 | -------------------------------------------------------------------------------- /src/report/html.ts: -------------------------------------------------------------------------------- 1 | import log from 'fancy-log'; 2 | import { writeFileWithFolder } from 'src/utils'; 3 | import { CompatData } from 'src/types'; 4 | 5 | export async function createHtml( 6 | compatData: Record, 7 | version: string, 8 | path: string 9 | ) { 10 | const compatDataKeys = Object.keys(compatData); 11 | const classGreen = 'green'; 12 | const classRed = 'red'; 13 | const classYellow = 'yellow'; 14 | 15 | const style = ` 16 | h1{ 17 | font-family: arial, sans-serif; 18 | } 19 | table { 20 | font-family: arial, sans-serif; 21 | border-collapse: collapse; 22 | width: 100%; 23 | } 24 | td, th { 25 | border: 1px solid #dddddd; 26 | text-align: left; 27 | padding: 8px; 28 | } 29 | tr:nth-child(even) { 30 | background-color: #dddddd; 31 | } 32 | .${classRed}{ 33 | color: #ff0000; 34 | } 35 | .${classGreen}{ 36 | color: #0f9b4e; 37 | } 38 | .${classYellow}{ 39 | color: #ce8d02; 40 | }`; 41 | 42 | const tableData = compatDataKeys 43 | .map((key) => { 44 | const compatible = compatData[key].compatible; 45 | const compatibleClass = 46 | compatible === undefined || compatible === 'invalid' 47 | ? classYellow 48 | : compatible 49 | ? classGreen 50 | : classRed; 51 | return ` 52 | 53 | ${key} 54 | ${compatible} 55 | ${compatData[key].range} 56 | 57 | `; 58 | }) 59 | .join(''); 60 | 61 | const out = ` 62 | 63 | 64 | 67 | depngn 68 | 69 | 70 |

Node version: ${version}

71 | 72 | 73 | 74 | 75 | 76 | 77 | ${tableData} 78 |
packagecompatiblerange
79 | 80 | `; 81 | 82 | await writeFileWithFolder(path, out); 83 | log.info(`File generated at ${path}`); 84 | } 85 | -------------------------------------------------------------------------------- /src/report/index.ts: -------------------------------------------------------------------------------- 1 | import { join } from 'path'; 2 | import { createJson } from './json'; 3 | import { createTable } from './table'; 4 | import { createHtml } from './html'; 5 | import { CliOptions, CompatDataMap, Reporter } from 'src/types'; 6 | 7 | export async function report( 8 | compatData: CompatDataMap, 9 | { reporter = Reporter.Terminal, version, reportDir, reportFileName }: CliOptions 10 | ) { 11 | const finalOutputFilePath = join(reportDir ?? '', `${reportFileName ?? 'compat'}.${reporter}`); 12 | switch (reporter) { 13 | case Reporter.Terminal: 14 | return createTable(compatData, version); 15 | case Reporter.Json: 16 | return await createJson(compatData, version, finalOutputFilePath); 17 | case Reporter.Html: 18 | return await createHtml(compatData, version, finalOutputFilePath); 19 | default: { 20 | const wrong = reporter as never; 21 | throw new Error( 22 | `This error shouldn't happen, but somehow you entered an invalid reporter and it made it past the first check: ${wrong}.` 23 | ); 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/report/json.ts: -------------------------------------------------------------------------------- 1 | import log from 'fancy-log'; 2 | import { writeFileWithFolder } from 'src/utils'; 3 | import { CompatData } from 'src/types'; 4 | 5 | export async function createJson( 6 | compatData: Record, 7 | version: string, 8 | path: string 9 | ) { 10 | const out = JSON.stringify( 11 | { 12 | node: version, 13 | dependencies: compatData, 14 | }, 15 | null, 16 | 2 17 | ); 18 | 19 | await writeFileWithFolder(path, out); 20 | log.info(`File generated at ${path}`); 21 | } 22 | -------------------------------------------------------------------------------- /src/report/table.ts: -------------------------------------------------------------------------------- 1 | import { blue, green, red, yellow } from 'kleur/colors'; 2 | import { table, TableUserConfig } from 'table'; 3 | import { CompatData } from 'src/types'; 4 | 5 | export function createTable(compatData: Record, version: string) { 6 | const titles = ['package', 'compatible', 'range'].map((title) => blue(title)); 7 | const out = Object.keys(compatData).map((dep) => { 8 | const { compatible, range } = compatData[dep]; 9 | return [dep, toColorString(compatible), range]; 10 | }); 11 | out.unshift(titles); 12 | 13 | const config: TableUserConfig = { 14 | header: { 15 | alignment: 'center', 16 | content: green(`\nNode version: ${version}\n`), 17 | }, 18 | }; 19 | console.log(table(out, config)); 20 | } 21 | 22 | function toColorString(value: boolean | 'invalid' | undefined) { 23 | if (value === undefined || value === 'invalid') return yellow(`${value}`); 24 | const outputColor = value ? green : red; 25 | return outputColor(value.toString()); 26 | } 27 | -------------------------------------------------------------------------------- /src/types/index.ts: -------------------------------------------------------------------------------- 1 | export interface EnginesData { 2 | package: string; 3 | range: string; 4 | } 5 | 6 | export type EnginesDataArray = Array; 7 | 8 | export interface CompatData { 9 | compatible: boolean | 'invalid' | undefined; 10 | range: string; 11 | } 12 | 13 | export enum Reporter { 14 | Terminal = 'terminal', 15 | Json = 'json', 16 | Html = 'html', 17 | } 18 | 19 | export enum PackageManagerName { 20 | Npm = 'npm', 21 | Yarn = 'yarn', 22 | } 23 | 24 | export interface Manager { 25 | name: PackageManagerName; 26 | lockFile: string; 27 | } 28 | 29 | export interface PackageInfo { 30 | version: string; 31 | resolved: string; 32 | integrity: string; 33 | dev: boolean; 34 | dependencies: Record; 35 | engines?: { 36 | node?: string; 37 | }; 38 | } 39 | 40 | export interface PackageLock { 41 | name: string; 42 | version: string; 43 | lockfileVersion: number; 44 | requires: boolean; 45 | packages: Record; 46 | } 47 | 48 | interface RecursiveJsonObject { 49 | [key: string]: string | RecursiveJsonObject; 50 | } 51 | 52 | export interface PackageJson { 53 | name: string; 54 | version: string; 55 | description?: string; 56 | homepage?: string; 57 | author?: string | { name: string; email: string; url: string }; 58 | funding?: string | { type: string; url: string } | Array; 59 | license?: string; 60 | bugs?: string | { email: string; url: string }; 61 | repository?: 62 | | string 63 | | { 64 | type?: string; 65 | url?: string; 66 | directory?: string; 67 | }; 68 | keywords?: string; 69 | type?: string; 70 | main?: string; 71 | module?: string; 72 | exports?: Record | Record>; 73 | bin?: string | Record; 74 | browser?: string; 75 | files?: Array; 76 | scripts?: Record; 77 | engines?: Record; 78 | os?: Array; 79 | cpu?: Array; 80 | private?: boolean; 81 | workspaces?: Array; 82 | dependencies?: Record; 83 | devDependencies?: Record; 84 | peerDependencies?: Record; 85 | peerDependenciesMeta?: RecursiveJsonObject; 86 | optionalDependencies?: Record; 87 | overrides?: RecursiveJsonObject; 88 | resolutions?: RecursiveJsonObject; 89 | } 90 | 91 | export interface Options { 92 | version: string; 93 | cwd?: string; 94 | } 95 | 96 | export interface CliOptions extends Options { 97 | reporter?: Reporter; 98 | reportDir?: string; 99 | reportFileName?: string; 100 | } 101 | 102 | export type CompatDataMap = Record; 103 | -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | import { promisify } from 'util'; 2 | import { 3 | readFile as syncReadFile, 4 | writeFile as syncWriteFile, 5 | stat as syncStat, 6 | mkdir as syncMkdir, 7 | rm as syncRm, 8 | } from 'fs'; 9 | import path from 'path'; 10 | 11 | // `fs/promises` is only available from Node v14 onwards 12 | // so we import the sync versions of `writeFile` and `access` and transform them into 13 | // async versions using `promisify` (available from Node v8 onwards) 14 | // if we ever decided to drop support for Node (...filepath: Array): Promise => { 22 | try { 23 | const resolvedPath = path.resolve(...filepath); 24 | const file = await readFile(resolvedPath, { encoding: 'utf-8' }); 25 | return JSON.parse(file); 26 | } catch (error) { 27 | if ((error as NodeJS.ErrnoException).code === 'ENOENT') { 28 | // this means the file we're looking for doesn't exist. 29 | // return `undefined` so we can handle this in the function 30 | // it's called in 31 | return; 32 | } 33 | // just throw if the error is unexpected 34 | throw error; 35 | } 36 | }; 37 | 38 | export async function pathExists(path: string) { 39 | try { 40 | await stat(path); 41 | return true; 42 | } catch { 43 | return false; 44 | } 45 | } 46 | 47 | export async function writeFileWithFolder(filePath: string, data: string) { 48 | const dirname = path.dirname(filePath); 49 | const exist = await pathExists(dirname); 50 | if (!exist) { 51 | await mkdir(dirname, { recursive: true }); 52 | } 53 | await writeFile(filePath, data); 54 | } 55 | -------------------------------------------------------------------------------- /tests/integration/cwd-option.spec.ts: -------------------------------------------------------------------------------- 1 | import { depngn } from 'src/core'; 2 | 3 | describe('cwd option', () => { 4 | it('possible to perform the check in the existing directory passing a relative path', async () => { 5 | const checkResult = await depngn({ version: '18.0.0', cwd: '.' }); 6 | expect(checkResult!.typescript.compatible).toBe(true); 7 | }); 8 | 9 | it('possible to perform the check in the existing directory passing an absolute path', async () => { 10 | const checkResult = await depngn({ version: '18.0.0', cwd: process.cwd() }); 11 | expect(checkResult).not.toBeNull(); 12 | }); 13 | 14 | it('impossible to perform the check in the non-existing directory', async () => { 15 | const cwd = './x'; 16 | try { 17 | await depngn({ version: '18.0.0', cwd }); 18 | } catch (e) { 19 | expect((e as Error).message).toBe( 20 | `ENOENT: no such file or directory, chdir '${process.cwd()}' -> '${process.cwd()}/x'` 21 | ); 22 | } 23 | }); 24 | 25 | it('impossible to perform the check in the directory without a lock file', async () => { 26 | try { 27 | await depngn({ version: '18.0.0', cwd: './src' }); 28 | } catch (e) { 29 | expect((e as Error).message).toBe( 30 | `Could not determine package manager. You may be missing a lock file or using an unsupported package manager.\nThe search was performed on the path - ${process.cwd()}/src` 31 | ); 32 | } 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /tests/integration/mocks/compat-data.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'test-package-1': { compatible: false, range: '1.0.0' }, 3 | 'test-package-2': { compatible: undefined, range: 'n/a' }, 4 | }; 5 | -------------------------------------------------------------------------------- /tests/integration/report.spec.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import { report } from 'src/report'; 3 | import { Reporter } from 'src/types'; 4 | import { table } from 'table'; 5 | import { blue, green, red, yellow } from 'kleur/colors'; 6 | import { mkdir, rm, stat } from 'src/utils'; 7 | import compatData from './mocks/compat-data'; 8 | 9 | jest.mock('table', () => ({ 10 | table: jest.fn(), 11 | })); 12 | 13 | describe('report', () => { 14 | const cwd = process.cwd(); 15 | const testOutputPath = path.join(cwd, 'testOutput'); 16 | 17 | beforeEach(async () => { 18 | await mkdir(testOutputPath); 19 | }); 20 | 21 | afterEach(async () => { 22 | await rm(testOutputPath, { recursive: true }); 23 | }); 24 | 25 | test('should generate a report in desired path', async () => { 26 | const reportOutputPath = path.join(testOutputPath, 'compat.html'); 27 | await report(compatData, { 28 | reporter: Reporter.Html, 29 | reportDir: testOutputPath, 30 | version: '18.0.0', 31 | }); 32 | 33 | const reportExists = !!(await stat(reportOutputPath).catch(() => false)); 34 | 35 | expect(reportExists).toBe(true); 36 | }); 37 | 38 | test('should generate a report in desired path with a desired name', async () => { 39 | const reportFileName = 'compat'; 40 | const reportOutputPath = path.join(testOutputPath, `${reportFileName}.html`); 41 | await report(compatData, { 42 | reporter: Reporter.Html, 43 | reportFileName, 44 | reportDir: testOutputPath, 45 | version: '18.0.0', 46 | }); 47 | 48 | const reportExists = !!(await stat(reportOutputPath).catch(() => false)); 49 | 50 | expect(reportExists).toBe(true); 51 | }); 52 | 53 | test('should generate a report to the console when the reporter is specified as terminal', async () => { 54 | const reporter = Reporter.Terminal; 55 | const npmDir = 'tests/unit/core/getEngines/mocks/npm/lockfileVersion2'; 56 | process.chdir(npmDir); 57 | await report(compatData, { reporter, version: '18.0.0' }); 58 | process.chdir(cwd); 59 | expect(table).toHaveBeenCalledWith( 60 | [ 61 | ['package', 'compatible', 'range'].map((title) => blue(title)), 62 | ['test-package-1', red('false'), '1.0.0'], 63 | ['test-package-2', yellow('undefined'), 'n/a'], 64 | ], 65 | { 66 | header: { 67 | alignment: 'center', 68 | content: green('\nNode version: 18.0.0\n'), 69 | }, 70 | } 71 | ); 72 | }); 73 | }); 74 | -------------------------------------------------------------------------------- /tests/unit/cli/usage.spec.ts: -------------------------------------------------------------------------------- 1 | import { createUsage } from 'cli/usage'; 2 | 3 | const originalConsoleLog = console.log; 4 | 5 | describe('createUsage', () => { 6 | beforeEach(() => { 7 | console.log = jest.fn(); 8 | }); 9 | 10 | afterEach(() => { 11 | console.log = originalConsoleLog; 12 | }); 13 | 14 | it('uses console.log', () => { 15 | createUsage(); 16 | expect(console.log).toHaveBeenCalledTimes(1); 17 | }); 18 | 19 | it('returns an usage message', () => { 20 | createUsage(); 21 | 22 | const usage = ` 23 | Usage: 24 | depngn [options] 25 | 26 | Options: 27 | -h, --help output usage information 28 | -r, --reporter which reporter for output. options are: terminal (default), json, html 29 | --cwd override the current working directory where to perform dependencies check 30 | -d, --reportDir specifies the directory where to write the report 31 | -f, --reportFileName specifies the name of the report file 32 | 33 | Example: 34 | depngn 12.0.0 --reporter=json 35 | `; 36 | expect(console.log).toHaveBeenCalledWith(usage); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /tests/unit/cli/validate.spec.ts: -------------------------------------------------------------------------------- 1 | import { validateArgs } from 'cli/validate'; 2 | import { Reporter } from 'src/types'; 3 | import {green, red, yellow} from 'kleur/colors'; 4 | 5 | describe('validate', () => { 6 | it('throws the correct error when node version is invalid', async () => { 7 | try { 8 | await validateArgs({ 9 | version: 'foo', 10 | reporter: Reporter.Terminal, 11 | }); 12 | } catch (error) { 13 | expect((error as Error).message).toBe(`Invalid Node version: ${red('foo')}.`); 14 | } 15 | }); 16 | it('throws the correct error when reporter is invalid', async () => { 17 | try { 18 | await validateArgs({ 19 | version: '1.0.0', 20 | reporter: 'foo' as Reporter.Terminal, 21 | }); 22 | } catch (error) { 23 | expect((error as Error).message).toBe( 24 | `Invalid reporter: ${red('foo')}. Valid options are: ${green('terminal, json, html')}.` 25 | ); 26 | } 27 | }); 28 | it('throws when cwd is invalid', async () => { 29 | try { 30 | await validateArgs({ 31 | version: '1.0.0', 32 | reporter: Reporter.Terminal, 33 | cwd: './foo', 34 | }); 35 | } catch (error) { 36 | expect((error as Error).message).toBe( 37 | `Invalid cwd: ${red('./foo')}. This directory does not exist.` 38 | ); 39 | } 40 | }); 41 | 42 | it('throws when reportDir is specified without a proper reporter', async () => { 43 | try { 44 | await validateArgs({ 45 | version: '1.0.0', 46 | reportDir: './foo', 47 | }); 48 | } catch (error) { 49 | expect((error as Error).message).toBe( 50 | `When using ${green('--reportDir')} you must also specify ${yellow('--reporter')}.` 51 | ); 52 | } 53 | }); 54 | 55 | it('throws when reportDir is specified with a wrong reporter', async () => { 56 | try { 57 | await validateArgs({ 58 | version: '1.0.0', 59 | reporter: Reporter.Terminal, 60 | reportDir: './foo', 61 | }); 62 | } catch (error) { 63 | expect((error as Error).message).toBe( 64 | `Both ${yellow('--reporterDir')} and ${yellow( 65 | '--reporter' 66 | )} were specified. Either remove one of these options or change the ${yellow( 67 | '--reporter' 68 | )} to ${green(Reporter.Json)} or ${green(Reporter.Html)}.` 69 | ); 70 | } 71 | }); 72 | 73 | it('throws when reportFileName is specified without a proper reporter', async () => { 74 | try { 75 | await validateArgs({ 76 | version: '1.0.0', 77 | reportFileName: 'bar', 78 | }); 79 | } catch (error) { 80 | expect((error as Error).message).toBe( 81 | `When using ${green('--reportFileName')} you must also specify ${yellow('--reporter')}.` 82 | ); 83 | } 84 | }); 85 | 86 | it('throws when reportFileName is specified with a wrong reporter', async () => { 87 | try { 88 | await validateArgs({ 89 | version: '1.0.0', 90 | reporter: Reporter.Terminal, 91 | reportFileName: 'bar', 92 | }); 93 | } catch (error) { 94 | expect((error as Error).message).toBe( 95 | `Both ${yellow('--reportFileName')} and ${yellow( 96 | '--reporter' 97 | )} were specified. Either remove one of these options or change the ${yellow( 98 | '--reporter' 99 | )} to ${green(Reporter.Json)} or ${green(Reporter.Html)}.` 100 | ); 101 | } 102 | }); 103 | }); 104 | -------------------------------------------------------------------------------- /tests/unit/core/getDependencies/get-dependencies.spec.ts: -------------------------------------------------------------------------------- 1 | import { getDependencies } from 'core/getDependencies'; 2 | import path from 'path'; 3 | 4 | const withPackageTestDir = 'tests/unit/core/getDependencies/mocks/withPackageJson'; 5 | const withoutPackageTestDir = 'tests/unit/core/getDependencies/mocks/withoutPackageJson'; 6 | 7 | const originalCwd = process.cwd(); 8 | 9 | describe('getDependencies', () => { 10 | afterAll(() => { 11 | process.chdir(path.resolve(originalCwd)); 12 | }); 13 | 14 | it('reads dependencies from `package.json`', async () => { 15 | process.chdir(path.resolve(originalCwd, withPackageTestDir)); 16 | const output = await getDependencies(); 17 | expect(output).toStrictEqual(['test-package-1', `test-package-2`, `test-package-3`]); 18 | }); 19 | 20 | it('throws when no `package.json` is present', async () => { 21 | process.chdir(path.resolve(originalCwd, withoutPackageTestDir)); 22 | await expect(async () => { 23 | await getDependencies(); 24 | }).rejects.toEqual(new Error(`Unable to find package.json in ${process.cwd()}`)); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /tests/unit/core/getDependencies/mocks/withPackageJson/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "get-dependencies-spec", 3 | "type": "module", 4 | "engines": { 5 | "node": ">=10.0.0" 6 | }, 7 | "dependencies": { 8 | "test-package-1": "^1.0.0" 9 | }, 10 | "devDependencies": { 11 | "test-package-2": "^1.0.0" 12 | }, 13 | "peerDependencies": { 14 | "test-package-3": "^1.0.0" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /tests/unit/core/getDependencies/mocks/withoutPackageJson/notPackage.json: -------------------------------------------------------------------------------- 1 | { 2 | "field": "test file." 3 | } 4 | -------------------------------------------------------------------------------- /tests/unit/core/getEngines/get-engines.spec.ts: -------------------------------------------------------------------------------- 1 | import { getEngines } from 'core/getEngines'; 2 | import { MANAGERS } from 'core/getPackageManager'; 3 | import path from 'path'; 4 | import { PackageManagerName } from 'src/types'; 5 | 6 | const npmDir = 'tests/unit/core/getEngines/mocks/npm'; 7 | const yarnDir = 'tests/unit/core/getEngines/mocks/yarn'; 8 | const lockfileVersion1 = `${npmDir}/lockfileVersion1`; 9 | const lockfileVersion2 = `${npmDir}/lockfileVersion2`; 10 | 11 | const originalCwd = process.cwd(); 12 | 13 | const mockDeps = ['test-package-1', 'test-package-2']; 14 | 15 | describe('getEngines', () => { 16 | afterAll(() => { 17 | process.chdir(path.resolve(originalCwd)); 18 | }); 19 | 20 | describe('reads from package-lock.json when package manager is npm', () => { 21 | it('and lockfileVersion is 1', async () => { 22 | process.chdir(path.resolve(originalCwd, lockfileVersion1)); 23 | const output = await getEngines(mockDeps, MANAGERS[PackageManagerName.Npm]); 24 | expect(output).toStrictEqual([ 25 | { package: 'test-package-1', range: '1.0.0' }, 26 | { package: 'test-package-2', range: '' }, 27 | ]); 28 | }); 29 | 30 | it('and lockfileVersion is 2', async () => { 31 | process.chdir(path.resolve(originalCwd, lockfileVersion2)); 32 | const output = await getEngines(mockDeps, MANAGERS[PackageManagerName.Npm]); 33 | expect(output).toStrictEqual([ 34 | { package: 'test-package-1', range: '1.0.0' }, 35 | { package: 'test-package-2', range: '' }, 36 | ]); 37 | }); 38 | }); 39 | 40 | it('reads from node_modules when package manager is yarn', async () => { 41 | process.chdir(path.resolve(originalCwd, yarnDir)); 42 | const output = await getEngines(mockDeps, MANAGERS[PackageManagerName.Yarn]); 43 | expect(output).toStrictEqual([ 44 | { package: 'test-package-1', range: '1.0.0' }, 45 | { package: 'test-package-2', range: '' }, 46 | ]); 47 | }); 48 | }); 49 | -------------------------------------------------------------------------------- /tests/unit/core/getEngines/mocks/npm/lockfileVersion1/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "get-engines-spec-lockfile-1", 3 | "lockfileVersion": 1, 4 | "packages": { 5 | "test-package-1": { 6 | "engines": { 7 | "node": "1.0.0" 8 | } 9 | }, 10 | "test-package-2": { 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /tests/unit/core/getEngines/mocks/npm/lockfileVersion1/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "get-engines-spec-npm-1", 3 | "type": "module", 4 | "engines": { 5 | "node": ">=10.0.0" 6 | }, 7 | "dependencies": { 8 | "test-package-1": "^1.0.0", 9 | "test-package-2": "^1.0.0" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /tests/unit/core/getEngines/mocks/npm/lockfileVersion2/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "get-engines-spec-lockfile-2", 3 | "lockfileVersion": 2, 4 | "packages": { 5 | "node_modules/test-package-1": { 6 | "engines": { 7 | "node": "1.0.0" 8 | } 9 | }, 10 | "node_modules/test-package-2": { 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /tests/unit/core/getEngines/mocks/npm/lockfileVersion2/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "get-engines-spec-npm-2", 3 | "type": "module", 4 | "engines": { 5 | "node": ">=10.0.0" 6 | }, 7 | "dependencies": { 8 | "test-package-1": "^1.0.0", 9 | "test-package-2": "^1.0.0" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /tests/unit/core/getEngines/mocks/yarn/node_modules/test-package-1/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test-package-1", 3 | "type": "module", 4 | "engines": { 5 | "node": "1.0.0" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /tests/unit/core/getEngines/mocks/yarn/node_modules/test-package-2/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test-package-1", 3 | "type": "module" 4 | } 5 | -------------------------------------------------------------------------------- /tests/unit/core/getEngines/mocks/yarn/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "get-engines-spec-yarn", 3 | "type": "module", 4 | "engines": { 5 | "node": ">=10.0.0" 6 | }, 7 | "dependencies": { 8 | "test-package-1": "^1.0.0", 9 | "test-package-2": "^1.0.0" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /tests/unit/core/getEngines/mocks/yarn/yarn.lock: -------------------------------------------------------------------------------- 1 | "get-engine-spec-yarn": 2 | version: 1.0.0 3 | -------------------------------------------------------------------------------- /tests/unit/core/getPackageData/get-package-data.spec.ts: -------------------------------------------------------------------------------- 1 | import { getPackageData } from 'core/getPackageData'; 2 | 3 | describe('getPackageData', () => { 4 | it('has correct output when package is compatible', async () => { 5 | const output = getPackageData({ package: 'test-package', range: '>=8.0.0' }, '8.0.0'); 6 | expect(output).toStrictEqual({ compatible: true, range: '>=8.0.0' }); 7 | }); 8 | 9 | it('has correct output when package is incompatible', async () => { 10 | const output = getPackageData({ package: 'test-package', range: '^8.0.0' }, '9.0.0'); 11 | expect(output).toStrictEqual({ compatible: false, range: '^8.0.0' }); 12 | }); 13 | 14 | it('has correct output when package has no engine data', async () => { 15 | const output = getPackageData({ package: 'test-package', range: '' }, '8.0.0'); 16 | expect(output).toStrictEqual({ compatible: undefined, range: 'n/a' }); 17 | }); 18 | 19 | it('has correct output when package has invalid range', async () => { 20 | const output = getPackageData({ package: 'test-package', range: 'not-a-range' }, '8.0.0'); 21 | expect(output).toStrictEqual({ 22 | compatible: 'invalid', 23 | range: 'not-a-range', 24 | }); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /tests/unit/core/getPackageManager/get-package-manager.spec.ts: -------------------------------------------------------------------------------- 1 | import { getPackageManager } from 'core/getPackageManager'; 2 | import path from 'path'; 3 | import { PackageManagerName } from 'src/types'; 4 | 5 | const npmDir = 'tests/unit/core/getPackageManager/mocks/npm'; 6 | const yarnDir = 'tests/unit/core/getPackageManager/mocks/yarn'; 7 | const testDir = 'tests/unit/core/getPackageManager'; 8 | 9 | const originalCwd = process.cwd(); 10 | 11 | describe('getPackageManager', () => { 12 | afterAll(() => { 13 | process.chdir(path.resolve(originalCwd)); 14 | }); 15 | 16 | it('determines npm as package manager', async () => { 17 | process.chdir(path.resolve(originalCwd, npmDir)); 18 | const output = await getPackageManager(); 19 | expect(output.name).toStrictEqual(PackageManagerName.Npm); 20 | }); 21 | 22 | it('determines yarn as package manager', async () => { 23 | process.chdir(path.resolve(originalCwd, yarnDir)); 24 | const output = await getPackageManager(); 25 | expect(output.name).toStrictEqual(PackageManagerName.Yarn); 26 | }); 27 | 28 | it('throws when no lock file is present', async () => { 29 | process.chdir(path.resolve(originalCwd, testDir)); 30 | await expect(async () => { 31 | await getPackageManager(); 32 | }).rejects.toEqual( 33 | new Error( 34 | `Could not determine package manager. You may be missing a lock file or using an unsupported package manager.\nThe search was performed on the path - ${process.cwd()}` 35 | ) 36 | ); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /tests/unit/core/getPackageManager/mocks/npm/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "get-package-manager-spec" 3 | } 4 | -------------------------------------------------------------------------------- /tests/unit/core/getPackageManager/mocks/yarn/yarn.lock: -------------------------------------------------------------------------------- 1 | "get-package-manager-spec": 2 | version: 1.0.0 -------------------------------------------------------------------------------- /tests/unit/report/create.spec.ts: -------------------------------------------------------------------------------- 1 | import { report } from 'src/report'; 2 | import { Reporter } from 'src/types'; 3 | import { writeFileWithFolder } from 'src/utils'; 4 | import { table } from 'table'; 5 | import { blue, green, red, yellow } from 'kleur/colors'; 6 | 7 | const mockCompatData = { 8 | 'test-package-1': { 9 | compatible: true, 10 | range: '>=8.0.0', 11 | }, 12 | 'test-package-2': { 13 | compatible: false, 14 | range: '^6.0.0', 15 | }, 16 | 'test-package-3': { 17 | compatible: undefined, 18 | range: 'n/a', 19 | }, 20 | }; 21 | 22 | jest.mock('src/utils', () => ({ 23 | writeFileWithFolder: jest.fn(), 24 | })); 25 | 26 | jest.mock('table', () => ({ 27 | table: jest.fn(), 28 | })); 29 | 30 | const htmlExpected = 31 | 'depngn

Nodeversion:8.0.0

truefalseundefined
packagecompatiblerange
test-package-1>=8.0.0
test-package-2^6.0.0
test-package-3n/a
'; 32 | 33 | const originalConsoleLog = console.log; 34 | 35 | describe('createReport', () => { 36 | beforeEach(() => { 37 | console.log = jest.fn(); 38 | }); 39 | 40 | afterEach(() => { 41 | console.log = originalConsoleLog; 42 | jest.resetAllMocks(); 43 | }); 44 | 45 | it('outputs correct table', async () => { 46 | await report(mockCompatData, { 47 | version: '8.0.0', 48 | }); 49 | expect(table).toHaveBeenCalledWith( 50 | [ 51 | ['package', 'compatible', 'range'].map((title) => blue(title)), 52 | ['test-package-1', green('true'), '>=8.0.0'], 53 | ['test-package-2', red('false'), '^6.0.0'], 54 | ['test-package-3', yellow('undefined'), 'n/a'], 55 | ], 56 | { 57 | header: { 58 | alignment: 'center', 59 | content: green('\nNode version: 8.0.0\n'), 60 | }, 61 | } 62 | ); 63 | }); 64 | 65 | it('outputs correct json', async () => { 66 | await report(mockCompatData, { 67 | version: '8.0.0', 68 | reporter: Reporter.Json, 69 | }); 70 | expect(writeFileWithFolder).toHaveBeenCalledWith( 71 | 'compat.json', 72 | `{ 73 | \"node\": \"8.0.0\", 74 | \"dependencies\": { 75 | \"test-package-1\": { 76 | \"compatible\": true, 77 | \"range\": \">=8.0.0\" 78 | }, 79 | \"test-package-2\": { 80 | \"compatible\": false, 81 | \"range\": \"^6.0.0\" 82 | }, 83 | \"test-package-3\": { 84 | \"range\": \"n/a\" 85 | } 86 | } 87 | }` 88 | ); 89 | }); 90 | 91 | it('outputs correct html', async () => { 92 | await report(mockCompatData, { 93 | version: '8.0.0', 94 | reporter: Reporter.Html, 95 | }); 96 | expect(writeFileWithFolder).toHaveBeenCalled(); 97 | // this is necessary because whitespace is wonky with template literals 98 | // so we grab the args directly from the mock function's metadata 99 | const [path, htmlInput] = (writeFileWithFolder as jest.Mock).mock.calls[0]; 100 | expect(path).toEqual('compat.html'); 101 | expect(htmlInput.replace(/\s+/g, '')).toEqual(htmlExpected); 102 | }); 103 | }); 104 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": true, 3 | "compilerOptions": { 4 | "target": "es5", 5 | "lib": [ 6 | "ESNext" 7 | ], 8 | "types": [ 9 | "node", 10 | "jest" 11 | ], 12 | "allowJs": true, 13 | "skipLibCheck": true, 14 | "esModuleInterop": true, 15 | "allowSyntheticDefaultImports": true, 16 | "strict": true, 17 | "forceConsistentCasingInFileNames": true, 18 | "module": "commonjs", 19 | "moduleResolution": "Node", 20 | "resolveJsonModule": true, 21 | "isolatedModules": true, 22 | "noEmit": true, 23 | "baseUrl": ".", 24 | "paths": { 25 | "src/*": ["./src/*"], 26 | "core/*": ["./src/core/*"], 27 | "cli/*": ["./src/cli/*"], 28 | "report/*": ["./src/report/*"], 29 | } 30 | }, 31 | "include": [ 32 | "src", 33 | "tests" 34 | ], 35 | "exclude": ["node_modules", "dist"] 36 | } 37 | --------------------------------------------------------------------------------