├── .gitignore ├── .gitattributes ├── screenshot.png ├── lib ├── messages │ ├── node-version.twig │ ├── global-config-misc.twig │ ├── node-path-npm-failure.twig │ ├── yo-version-out-of-date.twig │ ├── global-config-syntax.twig │ ├── environment-version-out-of-date.twig │ ├── yo-rc-home-file-exists.twig │ ├── bowerrc-home-file-exists.twig │ ├── npm-version.twig │ ├── node-path-path-mismatch.twig │ └── node-path-path-mismatch-windows.twig ├── cli.js ├── rules │ ├── node-version.js │ ├── index.js │ ├── npm-version.js │ ├── yo-version.js │ ├── yo-rc-home.js │ ├── bowerrc-home.js │ ├── global-config.js │ ├── environment-version.js │ └── node-path.js ├── message.js └── index.js ├── .editorconfig ├── xo.config.mjs ├── .github └── workflows │ ├── dependency-review.yml │ ├── ci.yml │ ├── codeQL.yml │ └── scorecard.yml ├── readme.md ├── test ├── rule-npm-version.js ├── rule-node-version.js ├── rule-bowerrc-home.js ├── rule-yo-rc-home.js ├── rule-yo-version.js ├── rule-node-path.js ├── rule-environment-version.js └── rule-global-config.js ├── license └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | yarn.lock 3 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | *.js text eol=lf 3 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yeoman/doctor/HEAD/screenshot.png -------------------------------------------------------------------------------- /lib/messages/node-version.twig: -------------------------------------------------------------------------------- 1 | 2 | {{ 'Your Node.js version is outdated.' | red }} 3 | Upgrade to the latest version: {{ 'https://nodejs.org' | blue | underline }} 4 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | -------------------------------------------------------------------------------- /lib/messages/global-config-misc.twig: -------------------------------------------------------------------------------- 1 | 2 | Unable to read {{ path | magenta }} 3 | Make sure your user have the right permissions to read the file. (look {{ "chmod" | magenta }}) 4 | -------------------------------------------------------------------------------- /lib/messages/node-path-npm-failure.twig: -------------------------------------------------------------------------------- 1 | 2 | {{ "Unable to find the npm root, something went wrong." | red }} 3 | Try to execute {{ 'npm -g root --silent' | magenta }} on your command-line 4 | -------------------------------------------------------------------------------- /lib/messages/yo-version-out-of-date.twig: -------------------------------------------------------------------------------- 1 | 2 | {{ 'Your yo version is outdated.' | red }} 3 | 4 | Upgrade to the latest version by running: 5 | {{ 'npm install -g yo@latest' | magenta }} 6 | -------------------------------------------------------------------------------- /lib/messages/global-config-syntax.twig: -------------------------------------------------------------------------------- 1 | 2 | Your global config file is not a valid JSON. 3 | It contains the following syntax error: 4 | {{ message }} 5 | Please open {{ path | magenta }} and fix it manually. 6 | -------------------------------------------------------------------------------- /lib/cli.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import globalAgent from 'global-agent'; 3 | import app from './index.js'; 4 | 5 | app(); 6 | 7 | // Override http networking to go through a proxy if one is configured. 8 | globalAgent.bootstrap(); 9 | -------------------------------------------------------------------------------- /lib/messages/environment-version-out-of-date.twig: -------------------------------------------------------------------------------- 1 | 2 | {{ 'Your environment version is outdated.' | red }} 3 | 4 | Upgrade to the latest version by reinstalling yo: 5 | {{ 'npm uninstall -g yo; npm install -g yo@latest' | magenta }} 6 | -------------------------------------------------------------------------------- /lib/messages/yo-rc-home-file-exists.twig: -------------------------------------------------------------------------------- 1 | 2 | Found a {{ yorc | cyan }} file in your home directory. Delete it otherwise 3 | Yeoman will generate everything in your home rather then your project folder. 4 | 5 | To delete the file, run: {{ command | magenta }} 6 | -------------------------------------------------------------------------------- /lib/messages/bowerrc-home-file-exists.twig: -------------------------------------------------------------------------------- 1 | 2 | We found a {{ bowerrc | cyan }} file in your home directory. This can cause 3 | issues by overriding expected default config. Prefer setting up one `.bowerrc` per 4 | project. 5 | 6 | To delete the file, run: {{ command | magenta }} 7 | -------------------------------------------------------------------------------- /lib/messages/npm-version.twig: -------------------------------------------------------------------------------- 1 | 2 | {{ 'Your npm version is outdated.' | red }} 3 | 4 | Upgrade to the latest version by running: 5 | {{ 'npm install -g npm' | magenta }} 6 | {% if isWin %} 7 | 8 | See this guide if you're having trouble upgrading: 9 | {{ 'https://github.com/npm/npm/wiki/Troubleshooting#upgrading-on-windows' | blue | underline }} 10 | {% endif %} 11 | -------------------------------------------------------------------------------- /xo.config.mjs: -------------------------------------------------------------------------------- 1 | import globals from 'globals'; 2 | 3 | const xoConfig = [{ 4 | space: true, 5 | languageOptions: { 6 | globals: {...globals.node, ...globals.mocha}, 7 | }, 8 | rules: { 9 | 'node/no-deprecated-api': 'off', 10 | 'unicorn/prefer-module': 'off', 11 | 'unicorn/prefer-node-protocol': 'off', 12 | }, 13 | }]; 14 | 15 | export default xoConfig; 16 | -------------------------------------------------------------------------------- /.github/workflows/dependency-review.yml: -------------------------------------------------------------------------------- 1 | name: 'Dependency Review' 2 | on: [pull_request] 3 | permissions: 4 | contents: read 5 | jobs: 6 | dependency-review: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - name: 'Checkout Repository' 10 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 11 | - name: 'Dependency Review' 12 | uses: actions/dependency-review-action@3b139cfc5fae8b618d3eae3675e383bb1769c019 # v4.5.0 -------------------------------------------------------------------------------- /lib/rules/node-version.js: -------------------------------------------------------------------------------- 1 | import process from 'node:process'; 2 | import semver from 'semver'; 3 | import getMessage from '../message.js'; 4 | 5 | const rule = { 6 | OLDEST_NODE_VERSION: '4.2.0', 7 | description: 'Node.js version', 8 | errors: { 9 | oldNodeVersion() { 10 | return getMessage('node-version'); 11 | }, 12 | }, 13 | async verify() { 14 | return semver.lt(process.version, this.OLDEST_NODE_VERSION) ? this.errors.oldNodeVersion() : null; 15 | }, 16 | }; 17 | 18 | export default rule; 19 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Yeoman Doctor [![CI](https://github.com/yeoman/doctor/actions/workflows/ci.yml/badge.svg)](https://github.com/yeoman/doctor/actions/workflows/ci.yml) 2 | 3 | > Detect potential issues with users system that could prevent Yeoman from working correctly 4 | 5 | ![](screenshot.png) 6 | 7 | ## Usage 8 | 9 | Use as part of [`yo`](https://github.com/yeoman/yo): 10 | 11 | ``` 12 | $ yo doctor 13 | ``` 14 | 15 | Can also be run with `yo-doctor` if installed globally. 16 | 17 | ## License 18 | 19 | BSD-2-Clause © Google 20 | -------------------------------------------------------------------------------- /test/rule-npm-version.js: -------------------------------------------------------------------------------- 1 | import assert from 'node:assert'; 2 | import rule from '../lib/rules/npm-version.js'; 3 | 4 | describe('npm version', () => { 5 | it('pass if it\'s new enough', async () => { 6 | rule.OLDEST_NPM_VERSION = 'v1.0.0'; 7 | 8 | const error = await rule.verify(); 9 | assert.ok(!error, error); 10 | }); 11 | 12 | it('fail if it\'s too old', async () => { 13 | rule.OLDEST_NPM_VERSION = 'v100.0.0'; 14 | 15 | const error = await rule.verify(); 16 | assert.ok(error, error); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /lib/messages/node-path-path-mismatch.twig: -------------------------------------------------------------------------------- 1 | {{ "npm global root value is not in your NODE_PATH" | red }} 2 | 3 | [{{ "Info" | cyan }}] 4 | NODE_PATH = {{ path }} 5 | npm root = {{ npmroot }} 6 | 7 | [{{ "Fix" | cyan}}] Append the npm root value to your NODE_PATH variable 8 | Add this line to your .bashrc 9 | {{ "export NODE_PATH=$NODE_PATH:" | magenta }}{{ npmroot | magenta }} 10 | Or run this command 11 | {{ 'echo "export NODE_PATH=$NODE_PATH:' | magenta }}{{ npmroot | magenta }}{{ '" >> ~/.bashrc && source ~/.bashrc' | magenta }} 12 | -------------------------------------------------------------------------------- /lib/rules/index.js: -------------------------------------------------------------------------------- 1 | import bowerrcHome from './bowerrc-home.js'; 2 | import globalConfig from './global-config.js'; 3 | import nodeCath from './node-path.js'; 4 | import yoRcHome from './yo-rc-home.js'; 5 | import nodeVersion from './node-version.js'; 6 | import npmVersion from './npm-version.js'; 7 | import yoVersion from './yo-version.js'; 8 | import environmentVersion from './environment-version.js'; 9 | 10 | const rules = { 11 | bowerrcHome, globalConfig, nodeCath, yoRcHome, nodeVersion, npmVersion, yoVersion, environmentVersion, 12 | }; 13 | 14 | export default rules; 15 | -------------------------------------------------------------------------------- /lib/messages/node-path-path-mismatch-windows.twig: -------------------------------------------------------------------------------- 1 | {{ "npm global root value is not in your NODE_PATH" | red }} 2 | 3 | [{{ "Info" | cyan }}] 4 | NODE_PATH = {{ path }} 5 | npm root = {{ npmroot }} 6 | 7 | [{{ "Fix" | cyan}}] Append the npm root value to your NODE_PATH variable 8 | If you're using cmd.exe, run this command to fix the issue: 9 | {{ 'setx NODE_PATH "%NODE_PATH%;' | magenta }}{{ npmroot | magenta }}{{ '"' | magenta }} 10 | Then restart your command-line. Otherwise, you can setup NODE_PATH manually: 11 | {{ "https://github.com/sindresorhus/guides/blob/master/set-environment-variables.md#windows" | magenta }} 12 | -------------------------------------------------------------------------------- /lib/rules/npm-version.js: -------------------------------------------------------------------------------- 1 | import process from 'node:process'; 2 | import binaryVersionCheck from 'binary-version-check'; 3 | import getMessage from '../message.js'; 4 | 5 | const rule = { 6 | OLDEST_NPM_VERSION: '3.3.0', 7 | description: 'npm version', 8 | errors: { 9 | oldNpmVersion() { 10 | return getMessage('npm-version', { 11 | isWin: process.platform === 'win32', 12 | }); 13 | }, 14 | }, 15 | async verify() { 16 | try { 17 | await binaryVersionCheck('npm', `>=${this.OLDEST_NPM_VERSION}`); 18 | } catch { 19 | return this.errors.oldNpmVersion(); 20 | } 21 | }, 22 | }; 23 | 24 | export default rule; 25 | -------------------------------------------------------------------------------- /lib/message.js: -------------------------------------------------------------------------------- 1 | import path from 'node:path'; 2 | import fs from 'node:fs'; 3 | import {fileURLToPath} from 'node:url'; 4 | import chalk from 'chalk'; 5 | import ansiStyles from 'ansi-styles'; 6 | import Twig from 'twig'; 7 | 8 | const __dirname = path.dirname(fileURLToPath(import.meta.url)); 9 | 10 | // Add Chalk to Twig 11 | for (const style of Object.keys(ansiStyles)) { 12 | Twig.extendFilter(style, input => chalk[style](input)); 13 | } 14 | 15 | export default function getMessage(message, data) { 16 | const fileTemplate = fs.readFileSync(path.join(__dirname, 'messages', `${message}.twig`), 'utf8'); 17 | return Twig.twig({data: fileTemplate}).render(data); 18 | } 19 | -------------------------------------------------------------------------------- /lib/rules/yo-version.js: -------------------------------------------------------------------------------- 1 | import latestVersion from 'latest-version'; 2 | import binaryVersionCheck from 'binary-version-check'; 3 | import getMessage from '../message.js'; 4 | 5 | const rule = { 6 | description: 'yo version', 7 | errors: { 8 | oldYoVersion() { 9 | return getMessage('yo-version-out-of-date', {}); 10 | }, 11 | }, 12 | async verify() { 13 | try { 14 | const version = await latestVersion('yo'); 15 | await binaryVersionCheck('yo', `>=${version}`); 16 | } catch (error) { 17 | if (error.name === 'InvalidBinaryVersion') { 18 | return this.errors.oldYoVersion(); 19 | } 20 | 21 | console.log(error); 22 | return error; 23 | } 24 | }, 25 | }; 26 | 27 | export default rule; 28 | -------------------------------------------------------------------------------- /lib/rules/yo-rc-home.js: -------------------------------------------------------------------------------- 1 | import fs from 'node:fs'; 2 | import path from 'node:path'; 3 | import process from 'node:process'; 4 | import os from 'node:os'; 5 | import getMessage from '../message.js'; 6 | 7 | const rule = { 8 | description: 'No .yo-rc.json file in home directory', 9 | errors: { 10 | fileExists() { 11 | const rm = process.platform === 'win32' ? 'del' : 'rm'; 12 | return getMessage('yo-rc-home-file-exists', { 13 | yorc: '.yo-rc.json', 14 | command: rm + ' ~/.yo-rc.json', 15 | }); 16 | }, 17 | }, 18 | yorcPath: path.join(os.homedir(), '.yo-rc.json'), 19 | async verify() { 20 | const exists = fs.existsSync(this.yorcPath); 21 | return exists ? this.errors.fileExists() : null; 22 | }, 23 | }; 24 | 25 | export default rule; 26 | -------------------------------------------------------------------------------- /lib/rules/bowerrc-home.js: -------------------------------------------------------------------------------- 1 | import fs from 'node:fs'; 2 | import path from 'node:path'; 3 | import process from 'node:process'; 4 | import os from 'node:os'; 5 | import getMessage from '../message.js'; 6 | 7 | const rule = { 8 | description: 'No .bowerrc file in home directory', 9 | errors: { 10 | fileExists() { 11 | const rm = process.platform === 'win32' ? 'del' : 'rm'; 12 | return getMessage('bowerrc-home-file-exists', { 13 | bowerrc: '.bowerrc', 14 | command: rm + ' ~/.bowerrc', 15 | }); 16 | }, 17 | }, 18 | bowerrcPath: path.join(os.homedir(), '.bowerrc'), 19 | async verify() { 20 | const exists = fs.existsSync(this.bowerrcPath); 21 | return exists ? this.errors.fileExists() : null; 22 | }, 23 | }; 24 | 25 | export default rule; 26 | -------------------------------------------------------------------------------- /test/rule-node-version.js: -------------------------------------------------------------------------------- 1 | import assert from 'node:assert'; 2 | import process from 'process'; 3 | import rule from '../lib/rules/node-version.js'; 4 | 5 | let _processVersion; 6 | 7 | before(() => { 8 | _processVersion = process.version; 9 | Object.defineProperty(process, 'version', {writable: true}); 10 | }); 11 | 12 | after(() => { 13 | process.version = _processVersion; 14 | }); 15 | 16 | describe('Node.js version', () => { 17 | it('pass if it\'s new enough', async () => { 18 | process.version = 'v100.0.0'; 19 | 20 | const error = await rule.verify(); 21 | assert.ok(!error, error); 22 | }); 23 | 24 | it('fail if it\'s too old', async () => { 25 | process.version = 'v0.10.0'; 26 | 27 | const error = await rule.verify(); 28 | assert.ok(error, error); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | import symbols from 'log-symbols'; 3 | import rules from './rules/index.js'; 4 | 5 | export default function doctor() { 6 | let errorCount = 0; 7 | 8 | console.log('\n' + chalk.underline.blue('Yeoman Doctor')); 9 | console.log('Running sanity checks on your system\n'); 10 | 11 | (async () => { 12 | await Promise.all(Object.values(rules).map(rule => 13 | // eslint-disable-next-line promise/prefer-await-to-then 14 | rule.verify().then(error => { 15 | console.log((error ? symbols.error : symbols.success) + ' ' + rule.description); 16 | 17 | if (error) { 18 | errorCount++; 19 | console.log(error); 20 | } 21 | }))); 22 | 23 | if (errorCount === 0) { 24 | console.log(chalk.green('\nEverything looks all right!')); 25 | } else { 26 | console.log(chalk.red('\nFound potential issues on your machine :(')); 27 | } 28 | })(); 29 | } 30 | -------------------------------------------------------------------------------- /test/rule-bowerrc-home.js: -------------------------------------------------------------------------------- 1 | import fs from 'node:fs'; 2 | import assert from 'node:assert'; 3 | import sinon from 'sinon'; 4 | import rule from '../lib/rules/bowerrc-home.js'; 5 | 6 | describe('global .bowerrc rule', () => { 7 | beforeEach(function () { 8 | this.sandbox = sinon.createSandbox(); 9 | }); 10 | 11 | afterEach(function () { 12 | this.sandbox.restore(); 13 | }); 14 | 15 | it('pass if there is no .bowerrc file in user home', async function () { 16 | const mock = this.sandbox.mock(fs); 17 | mock.expects('existsSync').once().withArgs(rule.bowerrcPath).returns(false); 18 | const error = await rule.verify(); 19 | assert.ok(!error); 20 | mock.verify(); 21 | }); 22 | 23 | it('fail if there is a .bowerrc file in user home', async function () { 24 | const mock = this.sandbox.mock(fs); 25 | mock.expects('existsSync').once().withArgs(rule.bowerrcPath).returns(true); 26 | const error = await rule.verify(); 27 | assert.equal(error, rule.errors.fileExists()); 28 | mock.verify(); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /test/rule-yo-rc-home.js: -------------------------------------------------------------------------------- 1 | import fs from 'node:fs'; 2 | import assert from 'node:assert'; 3 | import sinon from 'sinon'; 4 | import rule from '../lib/rules/yo-rc-home.js'; 5 | 6 | describe('global .yo-rc.json rule', () => { 7 | beforeEach(function () { 8 | this.sandbox = sinon.createSandbox(); 9 | }); 10 | 11 | afterEach(function () { 12 | this.sandbox.restore(); 13 | }); 14 | 15 | it('pass if there is no .yo-rc.json file in user home', async function () { 16 | const mock = this.sandbox.mock(fs); 17 | mock.expects('existsSync').once().withArgs(rule.yorcPath).returns(false); 18 | const error = await rule.verify(); 19 | assert.ok(!error); 20 | mock.verify(); 21 | }); 22 | 23 | it('fail if there is a .yo-rc.json file in user home', async function () { 24 | const mock = this.sandbox.mock(fs); 25 | mock.expects('existsSync').once().withArgs(rule.yorcPath).returns(true); 26 | const error = await rule.verify(); 27 | assert.equal(error, rule.errors.fileExists()); 28 | mock.verify(); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /lib/rules/global-config.js: -------------------------------------------------------------------------------- 1 | import path from 'node:path'; 2 | import fs from 'node:fs'; 3 | import os from 'node:os'; 4 | import getMessage from '../message.js'; 5 | 6 | const rule = { 7 | description: 'Global configuration file is valid', 8 | errors: { 9 | syntax(error, configPath) { 10 | return getMessage('global-config-syntax', { 11 | message: error.message, 12 | path: configPath, 13 | }); 14 | }, 15 | 16 | misc(configPath) { 17 | return getMessage('global-config-misc', { 18 | path: configPath, 19 | }); 20 | }, 21 | }, 22 | configPath: path.join(os.homedir(), '.yo-rc-global.json'), 23 | async verify() { 24 | if (!fs.existsSync(this.configPath)) { 25 | return null; 26 | } 27 | 28 | try { 29 | JSON.parse(fs.readFileSync(this.configPath, 'utf8')); 30 | } catch (error) { 31 | if (error instanceof SyntaxError) { 32 | return this.errors.syntax(error, this.configPath); 33 | } 34 | 35 | return this.errors.misc(this.configPath); 36 | } 37 | 38 | return null; 39 | }, 40 | }; 41 | 42 | export default rule; 43 | -------------------------------------------------------------------------------- /lib/rules/environment-version.js: -------------------------------------------------------------------------------- 1 | import latestVersion from 'latest-version'; 2 | import binaryVersionCheck from 'binary-version-check'; 3 | import getMessage from '../message.js'; 4 | 5 | const rule = { 6 | description: 'environment version', 7 | errors: { 8 | oldEnvironmentVersion() { 9 | return getMessage('environment-version-out-of-date', {}); 10 | }, 11 | oldYoVersion() { 12 | return getMessage('yo-version-out-of-date', {}); 13 | }, 14 | }, 15 | async verify() { 16 | try { 17 | await binaryVersionCheck('yo', '>=6.0.0'); 18 | } catch (error) { 19 | if (error.name === 'InvalidBinaryVersion') { 20 | return this.errors.oldYoVersion(); 21 | } 22 | 23 | console.log(error); 24 | return error; 25 | } 26 | 27 | try { 28 | const version = await latestVersion('yeoman-environment'); 29 | await binaryVersionCheck('yo', `>=${version}`, {args: ['--environment-version']}); 30 | } catch (error) { 31 | if (error.name === 'InvalidBinaryVersion') { 32 | return this.errors.oldEnvironmentVersion(); 33 | } 34 | 35 | console.log(error); 36 | return error; 37 | } 38 | }, 39 | }; 40 | 41 | export default rule; 42 | -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | Copyright Google 2 | 3 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 4 | 5 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 6 | 7 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 8 | 9 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "yeoman-doctor", 3 | "version": "6.0.0", 4 | "description": "Detect potential issues with users system that could prevent Yeoman from working correctly", 5 | "license": "BSD-2-Clause", 6 | "author": "Yeoman", 7 | "repository": "yeoman/doctor", 8 | "type": "module", 9 | "exports": "./lib/index.js", 10 | "bin": { 11 | "yodoctor": "./lib/cli.js", 12 | "yo-doctor": "./lib/cli.js" 13 | }, 14 | "engines": { 15 | "node": ">=20" 16 | }, 17 | "scripts": { 18 | "lint": "xo", 19 | "test": "esmocha test/** -R spec --parallel" 20 | }, 21 | "files": [ 22 | "lib" 23 | ], 24 | "keywords": [ 25 | "cli-app", 26 | "cli", 27 | "yeoman", 28 | "yo", 29 | "doctor", 30 | "system", 31 | "health", 32 | "report", 33 | "check" 34 | ], 35 | "dependencies": { 36 | "ansi-styles": "^6.2.3", 37 | "binary-version-check": "^6.1.0", 38 | "chalk": "^5.6.2", 39 | "global-agent": "^3.0.0", 40 | "latest-version": "^9.0.0", 41 | "log-symbols": "^7.0.1", 42 | "semver": "^7.7.2", 43 | "twig": "^1.17.1" 44 | }, 45 | "devDependencies": { 46 | "esmocha": "^4.0.0", 47 | "globals": "^16.4.0", 48 | "sinon": "^21.0.0", 49 | "xo": "^1.2.2" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /test/rule-yo-version.js: -------------------------------------------------------------------------------- 1 | 2 | import assert from 'node:assert'; 3 | import {esmocha} from 'esmocha'; 4 | 5 | const {default: binaryVersion} = await esmocha.mock('binary-version', {default: esmocha.fn()}); 6 | const {default: latestVersion} = await esmocha.mock('latest-version', {default: esmocha.fn()}); 7 | 8 | const {default: rule} = await import('../lib/rules/yo-version.js'); 9 | 10 | describe('yo version', () => { 11 | it('pass if it\'s new enough', async () => { 12 | // Mock installed yo 13 | binaryVersion.mockResolvedValueOnce('6.0.0'); 14 | 15 | // Mock latest yo 16 | latestVersion.mockResolvedValueOnce('1.8.4'); 17 | 18 | const error = await rule.verify(); 19 | assert.ok(!error, error); 20 | }); 21 | 22 | it('fail if it\'s too old', async () => { 23 | // Mock installed yo 24 | binaryVersion.mockResolvedValueOnce('6.0.0'); 25 | 26 | // Mock latest yo 27 | latestVersion.mockResolvedValueOnce('999.999.999'); 28 | 29 | const error = await rule.verify(); 30 | assert.ok(error, error); 31 | }); 32 | 33 | it('fail if it\'s invalid version range', async () => { 34 | // Mock installed yo 35 | binaryVersion.mockResolvedValueOnce('6.0.0'); 36 | 37 | // Mock latest yo 38 | latestVersion.mockResolvedValueOnce('-1'); 39 | 40 | const error = await rule.verify(); 41 | assert.ok(error, error); 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | permissions: 10 | contents: read 11 | 12 | jobs: 13 | lint: 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - name: Checkout repository 18 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 19 | 20 | - name: Setup Node.js 21 | uses: actions/setup-node@1a4442cacd436585916779262731d5b162bc6ec7 # v3.8.2 22 | with: 23 | node-version: 20 24 | 25 | - name: Install dependencies 26 | run: npm install 27 | 28 | - name: Run lint 29 | run: npm run lint 30 | 31 | test: 32 | runs-on: ${{ matrix.os }} 33 | 34 | strategy: 35 | fail-fast: false 36 | matrix: 37 | os: [ubuntu-latest, macos-latest, windows-latest] 38 | node: [20, 22, 23, 24] 39 | 40 | steps: 41 | - name: Checkout repository 42 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 43 | 44 | - name: Setup Node.js 45 | uses: actions/setup-node@1a4442cacd436585916779262731d5b162bc6ec7 # v3.8.2 46 | with: 47 | node-version: ${{ matrix.node }} 48 | 49 | - name: Install dependencies 50 | run: npm install 51 | 52 | - name: Install YO 53 | run: npm install -g yo 54 | 55 | - name: Run tests 56 | run: npm test 57 | -------------------------------------------------------------------------------- /test/rule-node-path.js: -------------------------------------------------------------------------------- 1 | import assert from 'node:assert'; 2 | import path from 'node:path'; 3 | import childProcess from 'node:child_process'; 4 | import process from 'node:process'; 5 | import sinon from 'sinon'; 6 | import rule from '../lib/rules/node-path.js'; 7 | 8 | describe('NODE_PATH rule', () => { 9 | beforeEach(function () { 10 | this.sandbox = sinon.createSandbox(); 11 | this.beforePath = process.env.NODE_PATH; 12 | }); 13 | 14 | afterEach(function () { 15 | this.sandbox.restore(); 16 | process.env.NODE_PATH = this.beforePath; 17 | }); 18 | 19 | it('pass if npm root is contained in NODE_PATH', async function () { 20 | this.sandbox.stub(childProcess, 'execSync').returns('node-fake-path/foo\n'); 21 | process.env.NODE_PATH = 'node-fake-path/foo'; 22 | const error = await rule.verify(); 23 | assert.ok(!error); 24 | }); 25 | 26 | it('pass if NODE_PATH is undefined', async () => { 27 | delete process.env.NODE_PATH; 28 | const error = await rule.verify(); 29 | assert.ok(!error); 30 | }); 31 | 32 | it('fail if the npm call throw', async function () { 33 | this.sandbox.stub(childProcess, 'execSync').returns(new Error('Child Process failure')); 34 | process.env.NODE_PATH = 'some-path'; 35 | const error = await rule.verify(); 36 | assert.equal(error, rule.errors.npmFailure()); 37 | }); 38 | 39 | it('fail if the paths mismatch', async function () { 40 | this.sandbox.stub(childProcess, 'execSync').returns('node-fake-path/foo'); 41 | process.env.NODE_PATH = 'node-fake-path/bar'; 42 | const error = await rule.verify(); 43 | assert.equal(error, rule.errors.pathMismatch(path.resolve('node-fake-path/foo'))); 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /lib/rules/node-path.js: -------------------------------------------------------------------------------- 1 | import fs from 'node:fs'; 2 | import path from 'node:path'; 3 | import process from 'node:process'; 4 | import childProcess from 'node:child_process'; 5 | import getMessage from '../message.js'; 6 | 7 | function fixPath(filepath) { 8 | let fixedPath = path.resolve(path.normalize(filepath.trim())); 9 | 10 | try { 11 | fixedPath = fs.realpathSync(fixedPath); 12 | } catch (error) { 13 | if (error.code !== 'ENOENT' && error.code !== 'ENOTDIR') { 14 | throw error; 15 | } 16 | } 17 | 18 | return fixedPath; 19 | } 20 | 21 | const rule = { 22 | description: 'NODE_PATH matches the npm root', 23 | errors: { 24 | npmFailure() { 25 | return getMessage('node-path-npm-failure', {}); 26 | }, 27 | pathMismatch(npmRoot) { 28 | let messagePath = 'node-path-path-mismatch'; 29 | 30 | if (process.platform === 'win32') { 31 | messagePath += '-windows'; 32 | } 33 | 34 | return getMessage(messagePath, { 35 | path: process.env.NODE_PATH, 36 | npmroot: npmRoot, 37 | }); 38 | }, 39 | }, 40 | async verify() { 41 | if (process.env.NODE_PATH === undefined) { 42 | return null; 43 | } 44 | 45 | const nodePaths = (process.env.NODE_PATH || '').split(path.delimiter).map(segment => fixPath(segment)); 46 | 47 | try { 48 | const stdout = childProcess.execSync('npm -g root --silent'); 49 | const npmRoot = fixPath(stdout); 50 | 51 | if (!nodePaths.includes(npmRoot)) { 52 | return this.errors.pathMismatch(npmRoot); 53 | } 54 | 55 | return null; 56 | } catch { 57 | return this.errors.npmFailure(); 58 | } 59 | }, 60 | }; 61 | 62 | export default rule; 63 | -------------------------------------------------------------------------------- /test/rule-environment-version.js: -------------------------------------------------------------------------------- 1 | 2 | import assert from 'node:assert'; 3 | import {esmocha} from 'esmocha'; 4 | 5 | const {default: binaryVersion} = await esmocha.mock('binary-version', {default: esmocha.fn()}); 6 | const {default: latestVersion} = await esmocha.mock('latest-version', {default: esmocha.fn()}); 7 | 8 | const {default: rule} = await import('../lib/rules/environment-version.js'); 9 | 10 | describe('environment version', () => { 11 | beforeEach(() => { 12 | esmocha.resetAllMocks(); 13 | }); 14 | 15 | it('pass if it\'s new enough', async () => { 16 | // Mock installed yo 17 | binaryVersion.mockResolvedValueOnce('6.0.0'); 18 | 19 | // Mock latest yeoman-environment 20 | latestVersion.mockResolvedValueOnce('1.8.4'); 21 | // Mock installed yeoman-environment 22 | binaryVersion.mockResolvedValueOnce('2.0.0'); 23 | 24 | const error = await rule.verify(); 25 | assert.ok(!error, error); 26 | }); 27 | 28 | it('fail if it\'s too old', async () => { 29 | // Mock installed yo 30 | binaryVersion.mockResolvedValueOnce('6.0.0'); 31 | 32 | // Mock latest yeoman-environment 33 | latestVersion.mockResolvedValueOnce('999.999.999'); 34 | // Mock installed yeoman-environment 35 | binaryVersion.mockResolvedValueOnce('2.0.0'); 36 | 37 | const error = await rule.verify(); 38 | assert.ok(error, error); 39 | }); 40 | 41 | it('fail if it\'s invalid version range', async () => { 42 | // Mock installed yo 43 | binaryVersion.mockResolvedValueOnce('6.0.0'); 44 | 45 | // Mock latest yeoman-environment 46 | latestVersion.mockResolvedValueOnce('-1'); 47 | // Mock installed yeoman-environment 48 | binaryVersion.mockResolvedValueOnce('2.0.0'); 49 | 50 | const error = await rule.verify(); 51 | assert.ok(error, error); 52 | }); 53 | }); 54 | -------------------------------------------------------------------------------- /.github/workflows/codeQL.yml: -------------------------------------------------------------------------------- 1 | name: "CodeQL" 2 | on: 3 | push: 4 | branches: ["main"] 5 | pull_request: 6 | branches: ["main"] 7 | schedule: 8 | - cron: "0 0 * * 1" 9 | permissions: 10 | contents: read 11 | jobs: 12 | analyze: 13 | name: Analyze 14 | runs-on: ubuntu-latest 15 | permissions: 16 | actions: read 17 | contents: read 18 | security-events: write 19 | strategy: 20 | fail-fast: false 21 | matrix: 22 | language: ["javascript"] 23 | steps: 24 | - name: Checkout repository 25 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 26 | # Initializes the CodeQL tools for scanning. 27 | - name: Initialize CodeQL 28 | uses: github/codeql-action/init@b6a472f63d85b9c78a3ac5e89422239fc15e9b3c # v3.28.1 29 | with: 30 | languages: ${{ matrix.language }} 31 | # If you wish to specify custom queries, you can do so here or in a config file. 32 | # By default, queries listed here will override any specified in a config file. 33 | # Prefix the list here with "+" to use these queries and those in the config file. 34 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 35 | # If this step fails, then you should remove it and run the build manually (see below) 36 | - name: Autobuild 37 | uses: github/codeql-action/autobuild@b6a472f63d85b9c78a3ac5e89422239fc15e9b3c # v3.28.1 38 | # ℹ️ Command-line programs to run using the OS shell. 39 | # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun 40 | # If the Autobuild fails above, remove it and uncomment the following three lines. 41 | # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. 42 | # - run: | 43 | # echo "Run, Build Application using script" 44 | # ./location_of_script_within_repo/buildscript.sh 45 | - name: Perform CodeQL Analysis 46 | uses: github/codeql-action/analyze@b6a472f63d85b9c78a3ac5e89422239fc15e9b3c # v3.28.1 47 | with: 48 | category: "/language:${{matrix.language}}" -------------------------------------------------------------------------------- /test/rule-global-config.js: -------------------------------------------------------------------------------- 1 | import assert from 'node:assert'; 2 | import fs from 'node:fs'; 3 | import {fileURLToPath} from 'node:url'; 4 | import path from 'node:path'; 5 | import sinon from 'sinon'; 6 | import rule from '../lib/rules/global-config.js'; 7 | 8 | const __dirname = path.dirname(fileURLToPath(import.meta.url)); 9 | 10 | // Setting the message paths & files before fs is stubbed 11 | const messageSyntaxPath = path.join(__dirname, '../lib/messages', 'global-config-syntax.twig'); 12 | const messageSyntaxFile = fs.readFileSync(messageSyntaxPath, 'utf8'); 13 | const messageMiscPath = path.join(__dirname, '../lib/messages', 'global-config-misc.twig'); 14 | const messageMiscFile = fs.readFileSync(messageMiscPath, 'utf8'); 15 | 16 | describe('global config rule', () => { 17 | beforeEach(function () { 18 | this.sandbox = sinon.createSandbox(); 19 | }); 20 | 21 | afterEach(function () { 22 | this.sandbox.restore(); 23 | }); 24 | 25 | it('pass if there is no global config', async function () { 26 | this.sandbox.stub(fs, 'existsSync').returns(false); 27 | const error = await rule.verify(); 28 | assert.ok(!error); 29 | }); 30 | 31 | it('pass if the config content is valid JSON', async function () { 32 | this.sandbox.stub(fs, 'existsSync').returns(true); 33 | this.sandbox.stub(fs, 'readFileSync').returns('{ "foo": 1 }'); 34 | const error = await rule.verify(); 35 | assert.ok(!error); 36 | }); 37 | 38 | it('fails if JSON is invalid', async function () { 39 | this.sandbox.stub(fs, 'existsSync').withArgs(rule.configPath).returns(true); 40 | 41 | const fsStub = this.sandbox.stub(fs, 'readFileSync'); 42 | fsStub.withArgs(rule.configPath).returns('@#'); 43 | fsStub.withArgs(messageSyntaxPath).returns(messageSyntaxFile); 44 | 45 | const error = await rule.verify(); 46 | // Assert(error instanceof SyntaxError); 47 | assert.ok(/Unexpected token '?@/.test(error)); 48 | // Assert.equal(error, rule.errors.syntax(new SyntaxError('Unexpected token @'), rule.configPath)); 49 | }); 50 | 51 | it('fails if file is unreadable', async function () { 52 | this.sandbox.stub(fs, 'existsSync').withArgs(rule.configPath).returns(true); 53 | 54 | const fsStub = this.sandbox.stub(fs, 'readFileSync'); 55 | fsStub.withArgs(rule.configPath).throws(new Error('nope')); 56 | fsStub.withArgs(messageMiscPath).returns(messageMiscFile); 57 | 58 | const error = await rule.verify(); 59 | assert.equal(error, rule.errors.misc(rule.configPath)); 60 | }); 61 | }); 62 | -------------------------------------------------------------------------------- /.github/workflows/scorecard.yml: -------------------------------------------------------------------------------- 1 | name: Scorecard supply-chain security 2 | on: 3 | # For Branch-Protection check. Only the default branch is supported. See 4 | # https://github.com/ossf/scorecard/blob/main/docs/checks.md#branch-protection 5 | branch_protection_rule: 6 | # To guarantee Maintained check is occasionally updated. See 7 | # https://github.com/ossf/scorecard/blob/main/docs/checks.md#maintained 8 | schedule: 9 | - cron: '20 7 * * 2' 10 | workflow_dispatch: 11 | push: 12 | branches: ["main"] 13 | # Declare default permissions as read only. 14 | permissions: read-all 15 | jobs: 16 | analysis: 17 | name: Scorecard analysis 18 | runs-on: ubuntu-latest 19 | permissions: 20 | # Needed to upload the results to code-scanning dashboard. 21 | security-events: write 22 | # Needed to publish results and get a badge (see publish_results below). 23 | id-token: write 24 | contents: read 25 | actions: read 26 | # To allow GraphQL ListCommits to work 27 | issues: read 28 | pull-requests: read 29 | # To detect SAST tools 30 | checks: read 31 | steps: 32 | - name: "Checkout code" 33 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 34 | with: 35 | persist-credentials: false 36 | - name: "Run analysis" 37 | uses: ossf/scorecard-action@dc50aa9510b46c811795eb24b2f1ba02a914e534 # v2.3.3 38 | with: 39 | results_file: results.sarif 40 | results_format: sarif 41 | # (Optional) "write" PAT token. Uncomment the `repo_token` line below if: 42 | # - you want to enable the Branch-Protection check on a *public* repository, or 43 | # - you are installing Scorecards on a *private* repository 44 | # To create the PAT, follow the steps in https://github.com/ossf/scorecard-action#authentication-with-pat. 45 | # repo_token: ${{ secrets.SCORECARD_TOKEN }} 46 | # Public repositories: 47 | # - Publish results to OpenSSF REST API for easy access by consumers 48 | # - Allows the repository to include the Scorecard badge. 49 | # - See https://github.com/ossf/scorecard-action#publishing-results. 50 | # For private repositories: 51 | # - `publish_results` will always be set to `false`, regardless 52 | # of the value entered here. 53 | publish_results: true 54 | # Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF 55 | # format to the repository Actions tab. 56 | - name: "Upload artifact" 57 | uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 58 | with: 59 | name: SARIF file 60 | path: results.sarif 61 | retention-days: 5 62 | # Upload the results to GitHub's code scanning dashboard. 63 | - name: "Upload to code-scanning" 64 | uses: github/codeql-action/upload-sarif@b6a472f63d85b9c78a3ac5e89422239fc15e9b3c # v3.28.1 65 | with: 66 | sarif_file: results.sarif --------------------------------------------------------------------------------