diff --git a/.eslintignore b/.eslintignore index 447742e4312..aafbffa5401 100644 --- a/.eslintignore +++ b/.eslintignore @@ -7,7 +7,6 @@ packages/generators/lib packages/info/lib packages/init/lib packages/migrate/lib -packages/package-utils/lib packages/serve/lib packages/utils/lib packages/webpack-scaffold/lib @@ -28,4 +27,4 @@ test/loader/test-loader/examples/simple/webpack.config.js test/loader/test-loader/examples/simple/src/static-esm-module.js test/loader/test-loader/examples/simple/src/lazy-module.js test/loader/test-loader/examples/simple/example_dist/** -lib/test/loader/error-test/src/index.d.ts \ No newline at end of file +lib/test/loader/error-test/src/index.d.ts diff --git a/.gitignore b/.gitignore index f810ae20a0e..efdfb260487 100644 --- a/.gitignore +++ b/.gitignore @@ -52,6 +52,8 @@ packages/**/*.map # build files packages/**/lib packages/**/yarn.lock +!packages/webpack-cli/lib/utils/__tests__/**/yarn.lock +!packages/webpack-cli/lib/utils/__tests__/**/package-lock.json !packages/webpack-cli/lib # test output files diff --git a/packages/generators/__tests__/addon-generator.test.ts b/packages/generators/__tests__/addon-generator.test.ts index 1c2045567dc..d0958cd59f9 100644 --- a/packages/generators/__tests__/addon-generator.test.ts +++ b/packages/generators/__tests__/addon-generator.test.ts @@ -1,4 +1,4 @@ -jest.setMock('@webpack-cli/package-utils', { +jest.setMock('@webpack-cli/utils', { getPackageManager: jest.fn(), }); @@ -6,7 +6,7 @@ import fs from 'fs'; import path from 'path'; import mkdirp from 'mkdirp'; import rimraf from 'rimraf'; -import { getPackageManager } from '@webpack-cli/package-utils'; +import { getPackageManager } from 'webpack-cli/lib/utils/get-package-manager'; import addonGenerator from '../src/addon-generator'; // TODO: enable after jest release diff --git a/packages/generators/package.json b/packages/generators/package.json index a01d89e1129..a0bb9571678 100644 --- a/packages/generators/package.json +++ b/packages/generators/package.json @@ -14,7 +14,6 @@ "templates" ], "dependencies": { - "@webpack-cli/package-utils": "^1.0.1-rc.1", "@webpack-cli/utils": "^1.0.1-rc.1", "@webpack-cli/webpack-scaffold": "^1.0.1-rc.1", "colorette": "^1.2.1", diff --git a/packages/generators/src/addon-generator.ts b/packages/generators/src/addon-generator.ts index a801f7e591d..3cab6939dc3 100644 --- a/packages/generators/src/addon-generator.ts +++ b/packages/generators/src/addon-generator.ts @@ -2,8 +2,8 @@ import logger from 'webpack-cli/lib/utils/logger'; import mkdirp from 'mkdirp'; import path from 'path'; import Generator from 'yeoman-generator'; -import { getPackageManager } from '@webpack-cli/package-utils'; import { generatorCopy, generatorCopyTpl } from '@webpack-cli/utils'; +import { getPackageManager } from 'webpack-cli/lib/utils/get-package-manager'; /** * Creates a Yeoman Generator that generates a project conforming diff --git a/packages/generators/src/init-generator.ts b/packages/generators/src/init-generator.ts index 5eb9862c41e..3cc7fa85021 100644 --- a/packages/generators/src/init-generator.ts +++ b/packages/generators/src/init-generator.ts @@ -2,7 +2,7 @@ import { blue, green, bold } from 'colorette'; import logger from 'webpack-cli/lib/utils/logger'; import logSymbols from 'log-symbols'; import path from 'path'; -import { getPackageManager } from '@webpack-cli/package-utils'; +import { getPackageManager } from 'webpack-cli/lib/utils/get-package-manager'; import { Confirm, Input, List } from '@webpack-cli/webpack-scaffold'; import { diff --git a/packages/generators/tsconfig.json b/packages/generators/tsconfig.json index 5da888dfbe0..63a4818f8b2 100644 --- a/packages/generators/tsconfig.json +++ b/packages/generators/tsconfig.json @@ -5,5 +5,5 @@ "rootDir": "src" }, "include": ["src"], - "references": [{ "path": "../package-utils" }, { "path": "../utils" }, { "path": "../webpack-scaffold" }] + "references": [{ "path": "../utils" }, { "path": "../webpack-scaffold" }] } diff --git a/packages/package-utils/CHANGELOG.md b/packages/package-utils/CHANGELOG.md deleted file mode 100644 index 444b22aaf22..00000000000 --- a/packages/package-utils/CHANGELOG.md +++ /dev/null @@ -1,34 +0,0 @@ -# Change Log - -All notable changes to this project will be documented in this file. -See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. - -## [1.0.1-rc.1](https://github.com/webpack/webpack-cli/compare/@webpack-cli/package-utils@1.0.1-alpha.4...@webpack-cli/package-utils@1.0.1-rc.1) (2020-10-06) - -### Bug Fixes - -- **utils:** respect package-lock.json ([#1375](https://github.com/webpack/webpack-cli/issues/1375)) ([ce8ec5a](https://github.com/webpack/webpack-cli/commit/ce8ec5a9f56ab5c1ce30742dced56dcbea237600)) -- **webpack-cli:** handle promise rejection with package installation ([#1284](https://github.com/webpack/webpack-cli/issues/1284)) ([eb1112e](https://github.com/webpack/webpack-cli/commit/eb1112edf05b0a1bc83dced0e83987e4f459174c)) -- check webpack installation before running cli ([#1827](https://github.com/webpack/webpack-cli/issues/1827)) ([be509fa](https://github.com/webpack/webpack-cli/commit/be509fac9a03e202e062229484bb10af7876968f)) -- declare default commands as optional peer dependency ([#1816](https://github.com/webpack/webpack-cli/issues/1816)) ([dcc75c8](https://github.com/webpack/webpack-cli/commit/dcc75c8b24d2d170a9274d80ecfb8b2329e4ad2e)) -- use appropriate exit codes ([#1755](https://github.com/webpack/webpack-cli/issues/1755)) ([83f73b0](https://github.com/webpack/webpack-cli/commit/83f73b056e224301b871bee5e9b7254e64e84e95)) - -## [1.0.1-alpha.4](https://github.com/webpack/webpack-cli/compare/@webpack-cli/package-utils@1.0.1-alpha.3...@webpack-cli/package-utils@1.0.1-alpha.4) (2020-03-02) - -**Note:** Version bump only for package @webpack-cli/package-utils - -## [1.0.1-alpha.3](https://github.com/webpack/webpack-cli/compare/@webpack-cli/package-utils@1.0.1-alpha.2...@webpack-cli/package-utils@1.0.1-alpha.3) (2020-02-29) - -**Note:** Version bump only for package @webpack-cli/package-utils - -## [1.0.1-alpha.2](https://github.com/webpack/webpack-cli/compare/@webpack-cli/package-utils@1.0.1-alpha.1...@webpack-cli/package-utils@1.0.1-alpha.2) (2020-02-23) - -**Note:** Version bump only for package @webpack-cli/package-utils - -## [1.0.1-alpha.1](https://github.com/webpack/webpack-cli/compare/@webpack-cli/package-utils@1.0.1-alpha.0...@webpack-cli/package-utils@1.0.1-alpha.1) (2020-02-23) - -**Note:** Version bump only for package @webpack-cli/package-utils - -## 1.0.1-alpha.0 (2020-02-23) - -**Note:** Version bump only for package @webpack-cli/package-utils diff --git a/packages/package-utils/README.md b/packages/package-utils/README.md deleted file mode 100644 index 6b781a5f484..00000000000 --- a/packages/package-utils/README.md +++ /dev/null @@ -1,11 +0,0 @@ -# `@webpack-cli/package-utils` - -> TODO: description - -## Usage - -``` -const packageUtils = require('@webpack-cli/package-utils'); - -// TODO: DEMONSTRATE API -``` diff --git a/packages/package-utils/__tests__/index.test.ts b/packages/package-utils/__tests__/index.test.ts deleted file mode 100644 index 4ceda50a35b..00000000000 --- a/packages/package-utils/__tests__/index.test.ts +++ /dev/null @@ -1,26 +0,0 @@ -'use strict'; - -jest.mock('../lib/packageUtils', () => { - return { - packageExists: jest.fn(), - promptInstallation: jest.fn(), - }; -}); - -import { packageExists, promptInstallation } from '../lib/packageUtils'; -import run from '../../webpack-cli/lib/commands/resolveCommand'; - -describe('@webpack-cli/package-utils', () => { - it('should check existence of package', () => { - (packageExists as jest.Mock).mockImplementation(() => true); - const exists = packageExists('@webpack-cli/info'); - expect(exists).toBeTruthy(); - }); - - it('should not throw if the user interrupts', async () => { - (promptInstallation as jest.Mock).mockImplementation(() => { - throw new Error(); - }); - await expect(run('info')).resolves.not.toThrow(); - }); -}); diff --git a/packages/package-utils/__tests__/packageUtils.test.ts b/packages/package-utils/__tests__/packageUtils.test.ts deleted file mode 100644 index e853fa2ac0a..00000000000 --- a/packages/package-utils/__tests__/packageUtils.test.ts +++ /dev/null @@ -1,183 +0,0 @@ -'use strict'; - -jest.mock('execa'); -jest.mock('cross-spawn'); -const globalModulesNpmValue = 'test-npm'; -jest.setMock('global-modules', globalModulesNpmValue); -jest.setMock('enquirer', { - prompt: jest.fn(), -}); -jest.setMock('../lib/processUtils', { - runCommand: jest.fn(), -}); - -import fs from 'fs'; -import path from 'path'; -import execa from 'execa'; -import spawn from 'cross-spawn'; -import { prompt } from 'enquirer'; -import { getPackageManager, packageExists } from '../lib/packageUtils'; -import { runCommand } from '../lib/processUtils'; - -describe('packageUtils', () => { - describe('getPackageManager', () => { - const testYarnLockPath = path.resolve(__dirname, 'test-yarn-lock'); - const testNpmLockPath = path.resolve(__dirname, 'test-npm-lock'); - const testBothPath = path.resolve(__dirname, 'test-both'); - - const cwdSpy = jest.spyOn(process, 'cwd'); - - beforeAll(() => { - // mock sync - execa.sync = jest.fn(); - - // package-lock.json is ignored by .gitignore, so we simply - // write a lockfile here for testing - if (!fs.existsSync(testNpmLockPath)) { - fs.mkdirSync(testNpmLockPath); - } - fs.writeFileSync(path.resolve(testNpmLockPath, 'package-lock.json'), ''); - fs.writeFileSync(path.resolve(testBothPath, 'package-lock.json'), ''); - }); - - beforeEach(() => { - (execa.sync as jest.Mock).mockClear(); - }); - - it('should find yarn.lock', () => { - cwdSpy.mockReturnValue(testYarnLockPath); - expect(getPackageManager()).toEqual('yarn'); - expect((execa.sync as jest.Mock).mock.calls.length).toEqual(0); - }); - - it('should find package-lock.json', () => { - cwdSpy.mockReturnValue(testNpmLockPath); - expect(getPackageManager()).toEqual('npm'); - expect((execa.sync as jest.Mock).mock.calls.length).toEqual(0); - }); - - it('should prioritize yarn with many lock files', () => { - cwdSpy.mockReturnValue(testBothPath); - expect(getPackageManager()).toEqual('yarn'); - expect((execa.sync as jest.Mock).mock.calls.length).toEqual(0); - }); - - it('should use yarn if yarn command works', () => { - // yarn should output a version number to stdout if - // it is installed - (execa.sync as jest.Mock).mockImplementation(() => { - return { - stdout: '1.0.0', - }; - }); - cwdSpy.mockReturnValue(path.resolve(__dirname)); - expect(getPackageManager()).toEqual('yarn'); - expect((execa.sync as jest.Mock).mock.calls.length).toEqual(1); - }); - - it('should use npm if yarn command fails', () => { - (execa.sync as jest.Mock).mockImplementation(() => { - throw new Error(); - }); - cwdSpy.mockReturnValue(path.resolve(__dirname)); - expect(getPackageManager()).toEqual('npm'); - expect((execa.sync as jest.Mock).mock.calls.length).toEqual(1); - }); - }); - - describe('getPathToGlobalPackages', () => { - let packageUtils; - beforeAll(() => { - packageUtils = require('../lib/packageUtils'); - packageUtils.getPackageManager = jest.fn(); - }); - - it('uses global-modules if package manager is npm', () => { - packageUtils.getPackageManager.mockReturnValue('npm'); - expect(packageUtils.getPathToGlobalPackages()).toEqual(globalModulesNpmValue); - }); - - it('executes a command to find yarn global dir if package manager is yarn', () => { - packageUtils.getPackageManager.mockReturnValue('yarn'); - (spawn.sync as jest.Mock).mockReturnValue({ - stdout: { - toString: (): string => { - return 'test-yarn'; - }, - }, - }); - // after the yarn global dir is found, the node_modules directory - // is added on to the path - expect(packageUtils.getPathToGlobalPackages()).toEqual(`test-yarn${path.sep}node_modules`); - }); - }); - - describe('packageExists', () => { - it('should check existence of package', () => { - // use an actual path relative to the packageUtils file - expect(packageExists('./processUtils')).toBeTruthy(); - expect(packageExists('./nonexistent-package')).toBeFalsy(); - }); - }); - - describe('promptInstallation', () => { - let packageUtils; - beforeAll(() => { - packageUtils = require('../lib/packageUtils'); - packageUtils.getPackageManager = jest.fn(); - packageUtils.packageExists = jest.fn(() => true); - }); - - beforeEach(() => { - (runCommand as jest.Mock).mockClear(); - (prompt as jest.Mock).mockClear(); - }); - - it('should prompt to install using npm if npm is package manager', async () => { - packageUtils.getPackageManager.mockReturnValue('npm'); - (prompt as jest.Mock).mockReturnValue({ - installConfirm: true, - }); - - const preMessage = jest.fn(); - const promptResult = await packageUtils.promptInstallation('test-package', preMessage); - expect(promptResult).toBeTruthy(); - expect(preMessage.mock.calls.length).toEqual(1); - expect((prompt as jest.Mock).mock.calls.length).toEqual(1); - expect((runCommand as jest.Mock).mock.calls.length).toEqual(1); - expect((prompt as jest.Mock).mock.calls[0][0][0].message).toMatch(/Would you like to install test-package\?/); - // install the package using npm - expect((runCommand as jest.Mock).mock.calls[0][0]).toEqual('npm install -D test-package'); - }); - - it('should prompt to install using yarn if yarn is package manager', async () => { - packageUtils.getPackageManager.mockReturnValue('yarn'); - (prompt as jest.Mock).mockReturnValue({ - installConfirm: true, - }); - - const promptResult = await packageUtils.promptInstallation('test-package'); - expect(promptResult).toBeTruthy(); - expect((prompt as jest.Mock).mock.calls.length).toEqual(1); - expect((runCommand as jest.Mock).mock.calls.length).toEqual(1); - expect((prompt as jest.Mock).mock.calls[0][0][0].message).toMatch(/Would you like to install test-package\?/); - // install the package using yarn - expect((runCommand as jest.Mock).mock.calls[0][0]).toEqual('yarn add -D test-package'); - }); - - it('should not install if install is not confirmed', async () => { - packageUtils.getPackageManager.mockReturnValue('npm'); - (prompt as jest.Mock).mockReturnValue({ - installConfirm: false, - }); - - const promptResult = await packageUtils.promptInstallation('test-package'); - expect(promptResult).toBeUndefined(); - expect((prompt as jest.Mock).mock.calls.length).toEqual(1); - // runCommand should not be called, because the installation is not confirmed - expect((runCommand as jest.Mock).mock.calls.length).toEqual(0); - expect((prompt as jest.Mock).mock.calls[0][0][0].message).toMatch(/Would you like to install test-package\?/); - expect(process.exitCode).toEqual(2); - }); - }); -}); diff --git a/packages/package-utils/package.json b/packages/package-utils/package.json deleted file mode 100644 index b89b3259a1a..00000000000 --- a/packages/package-utils/package.json +++ /dev/null @@ -1,57 +0,0 @@ -{ - "name": "@webpack-cli/package-utils", - "version": "1.0.1-rc.1", - "description": "A module to help managing packages and modules inside webpack CLI", - "keywords": [ - "webpack", - "webpack-cli", - "node", - "package", - "utils", - "npm", - "yarn" - ], - "author": "emanuele ", - "homepage": "https://github.com/webpack/webpack-cli#readme", - "license": "MIT", - "main": "lib/index.js", - "types": "lib/index.d.ts", - "directories": { - "test": "__tests__" - }, - "publishConfig": { - "access": "public" - }, - "repository": { - "type": "git", - "url": "git+https://github.com/webpack/webpack-cli.git" - }, - "bugs": { - "url": "https://github.com/webpack/webpack-cli/issues" - }, - "files": [ - "lib" - ], - "dependencies": { - "colorette": "^1.2.1", - "cross-spawn": "^7.0.1", - "enquirer": "^2.3.4", - "execa": "^4.0.0", - "global-modules": "^2.0.0" - }, - "peerDependenciesMeta": { - "webpack": { - "optional": true - }, - "@webpack-cli/info": { - "optional": true - }, - "@webpack-cli/init": { - "optional": true - }, - "@webpack-cli/serve": { - "optional": true - } - }, - "gitHead": "fb50f766851f500ca12867a2aa9de81fa6e368f9" -} diff --git a/packages/package-utils/src/index.ts b/packages/package-utils/src/index.ts deleted file mode 100644 index caffda148a9..00000000000 --- a/packages/package-utils/src/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './packageUtils'; -export * from './processUtils'; diff --git a/packages/package-utils/src/packageUtils.ts b/packages/package-utils/src/packageUtils.ts deleted file mode 100644 index 167d7bee322..00000000000 --- a/packages/package-utils/src/packageUtils.ts +++ /dev/null @@ -1,102 +0,0 @@ -import fs from 'fs'; -import path from 'path'; -import { sync } from 'execa'; -import spawn from 'cross-spawn'; -import { green } from 'colorette'; -import { prompt } from 'enquirer'; -import { runCommand } from './processUtils'; - -/** - * - * Returns the name of package manager to use, - * preferring yarn over npm if available - * - * @returns {String} - The package manager name - */ - -type PackageName = 'npm' | 'yarn'; - -export function getPackageManager(): PackageName { - const hasLocalYarn = fs.existsSync(path.resolve(process.cwd(), 'yarn.lock')); - const hasLocalNpm = fs.existsSync(path.resolve(process.cwd(), 'package-lock.json')); - - if (hasLocalYarn) { - return 'yarn'; - } else if (hasLocalNpm) { - return 'npm'; - } - - try { - // if the sync function below fails because yarn is not installed, - // an error will be thrown - if (sync('yarn', ['--version']).stdout) { - return 'yarn'; - } - } catch (e) { - // Nothing - } - - return 'npm'; -} - -/** - * - * Returns the path to globally installed - * npm packages, depending on the available - * package manager determined by `getPackageManager` - * - * @returns {String} path - Path to global node_modules folder - */ -export function getPathToGlobalPackages(): string { - const manager: string = exports.getPackageManager(); - if (manager === 'yarn') { - try { - const yarnDir = spawn.sync('yarn', ['global', 'dir']).stdout.toString().trim(); - return path.join(yarnDir, 'node_modules'); - } catch (e) { - // Default to the global npm path below - } - } - - return require('global-modules'); -} - -export function packageExists(packageName: string): boolean { - try { - require(packageName); - return true; - } catch (err) { - return false; - } -} - -/** - * - * @param packageName - * @param preMessage Message to show before the question - */ -// eslint-disable-next-line @typescript-eslint/explicit-function-return-type -export async function promptInstallation(packageName: string, preMessage?: Function) { - const packageManager = exports.getPackageManager(); - const options = [packageManager === 'yarn' ? 'add' : 'install', '-D', packageName]; - - const commandToBeRun = `${packageManager} ${options.join(' ')}`; - if (preMessage) { - preMessage(); - } - const question = `Would you like to install ${packageName}? (That will run ${green(commandToBeRun)})`; - const { installConfirm } = await prompt([ - { - type: 'confirm', - name: 'installConfirm', - message: question, - initial: 'Y', - }, - ]); - if (installConfirm) { - await runCommand(commandToBeRun); - return exports.packageExists(packageName); - } - // eslint-disable-next-line require-atomic-updates - process.exitCode = 2; -} diff --git a/packages/package-utils/tsconfig.json b/packages/package-utils/tsconfig.json deleted file mode 100644 index 279b3e923cc..00000000000 --- a/packages/package-utils/tsconfig.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "extends": "../../tsconfig.json", - "compilerOptions": { - "outDir": "./lib", - "rootDir": "./src" - }, - "include": ["./src"] -} diff --git a/packages/utils/__tests__/global-packages-path.test.ts b/packages/utils/__tests__/global-packages-path.test.ts new file mode 100644 index 00000000000..2e3350772e9 --- /dev/null +++ b/packages/utils/__tests__/global-packages-path.test.ts @@ -0,0 +1,34 @@ +'use strict'; +jest.setMock('webpack-cli/lib/utils/get-package-manager', { + getPackageManager: jest.fn(), +}); +import { getPathToGlobalPackages } from '../lib/global-packages-path'; +import { getPackageManager } from 'webpack-cli/lib/utils/get-package-manager'; +jest.mock('execa'); +jest.mock('cross-spawn'); +const globalModulesNpmValue = 'test-npm'; +jest.setMock('global-modules', globalModulesNpmValue); + +import path from 'path'; +import spawn from 'cross-spawn'; + +describe('getPathToGlobalPackages', () => { + it('uses global-modules if package manager is npm', () => { + (getPackageManager as jest.Mock).mockReturnValue('npm'); + expect(getPathToGlobalPackages()).toEqual(globalModulesNpmValue); + }); + + it('executes a command to find yarn global dir if package manager is yarn', () => { + (getPackageManager as jest.Mock).mockReturnValue('yarn'); + (spawn.sync as jest.Mock).mockReturnValue({ + stdout: { + toString: (): string => { + return 'test-yarn'; + }, + }, + }); + // after the yarn global dir is found, the node_modules directory + // is added on to the path + expect(getPathToGlobalPackages()).toEqual(`test-yarn${path.sep}node_modules`); + }); +}); diff --git a/packages/utils/package.json b/packages/utils/package.json index 4a3146f5f24..a77b8ecc641 100644 --- a/packages/utils/package.json +++ b/packages/utils/package.json @@ -12,29 +12,41 @@ "lib" ], "dependencies": { - "@webpack-cli/package-utils": "^1.0.1-rc.1", "colorette": "^1.2.1", + "cross-spawn": "^7.0.3", + "enquirer": "^2.3.6", "execa": "^4.0.0", "findup-sync": "^4.0.0", + "global-modules": "^2.0.0", "got": "^10.7.0", "jscodeshift": "^0.7.0", "p-each-series": "^2.1.0", "yeoman-environment": "^2.8.1", "yeoman-generator": "^4.7.2" }, - "peerDependenciesMeta": { - "prettier": { - "optional": true - } - }, "peerDependencies": { "webpack": "4.x.x || 5.x.x", "webpack-cli": "4.x.x" }, "devDependencies": { + "@types/cross-spawn": "^6.0.2", "@types/got": "9.6.9", "@types/prettier": "1.19.0", "@types/yeoman-generator": "^4.11.2" }, + "peerDependenciesMeta": { + "@webpack-cli/info": { + "optional": true + }, + "@webpack-cli/init": { + "optional": true + }, + "@webpack-cli/serve": { + "optional": true + }, + "prettier": { + "optional": true + } + }, "gitHead": "fb50f766851f500ca12867a2aa9de81fa6e368f9" } diff --git a/packages/utils/src/global-packages-path.ts b/packages/utils/src/global-packages-path.ts new file mode 100644 index 00000000000..acdc7fecc45 --- /dev/null +++ b/packages/utils/src/global-packages-path.ts @@ -0,0 +1,25 @@ +import spawn from 'cross-spawn'; +import path from 'path'; +import { getPackageManager } from 'webpack-cli/lib/utils/get-package-manager'; + +/** + * + * Returns the path to globally installed + * npm packages, depending on the available + * package manager determined by `getPackageManager` + * + * @returns {String} path - Path to global node_modules folder + */ +export function getPathToGlobalPackages(): string { + const manager: string = getPackageManager(); + if (manager === 'yarn') { + try { + const yarnDir = spawn.sync('yarn', ['global', 'dir']).stdout.toString().trim(); + return path.join(yarnDir, 'node_modules'); + } catch (e) { + // Default to the global npm path below + } + } + + return require('global-modules'); +} diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts index e897f093c2a..68fef1ebafe 100644 --- a/packages/utils/src/index.ts +++ b/packages/utils/src/index.ts @@ -9,3 +9,4 @@ export * from './run-prettier'; export * from './scaffold'; export * from './validate-identifier'; export * from './prop-types'; +export * from './global-packages-path'; diff --git a/packages/utils/src/modify-config-helper.ts b/packages/utils/src/modify-config-helper.ts index a36e6696d9b..aba3b571725 100644 --- a/packages/utils/src/modify-config-helper.ts +++ b/packages/utils/src/modify-config-helper.ts @@ -5,7 +5,7 @@ import path from 'path'; import yeoman from 'yeoman-environment'; import Generator from 'yeoman-generator'; import { runTransform } from './scaffold'; -import { getPackageManager } from '@webpack-cli/package-utils'; +import { getPackageManager } from 'webpack-cli/lib/utils/get-package-manager'; export interface Config extends Object { item?: { diff --git a/packages/utils/src/npm-packages-exists.ts b/packages/utils/src/npm-packages-exists.ts index 3233a8027bb..42897c33b2a 100644 --- a/packages/utils/src/npm-packages-exists.ts +++ b/packages/utils/src/npm-packages-exists.ts @@ -4,7 +4,7 @@ import { red, bold } from 'colorette'; import { npmExists } from './npm-exists'; import { isLocalPath } from './path-utils'; import { resolvePackages } from './resolve-packages'; -import { getPathToGlobalPackages } from '@webpack-cli/package-utils'; +import { getPathToGlobalPackages } from './global-packages-path'; const WEBPACK_SCAFFOLD_PREFIX = 'webpack-scaffold'; /** diff --git a/packages/utils/src/resolve-packages.ts b/packages/utils/src/resolve-packages.ts index 773cc0f9d41..9fa19f2ca80 100644 --- a/packages/utils/src/resolve-packages.ts +++ b/packages/utils/src/resolve-packages.ts @@ -2,7 +2,8 @@ import { bold } from 'colorette'; import logger from 'webpack-cli/lib/utils/logger'; import path from 'path'; import { modifyHelperUtil } from './modify-config-helper'; -import { getPathToGlobalPackages, spawnChild } from '@webpack-cli/package-utils'; +import { getPathToGlobalPackages } from './global-packages-path'; +import { spawnChild } from './spawn-child'; import { isLocalPath } from './path-utils'; import { ExecaSyncReturnValue } from 'execa'; diff --git a/packages/utils/src/scaffold.ts b/packages/utils/src/scaffold.ts index a9b2229ca41..18383e30e6a 100644 --- a/packages/utils/src/scaffold.ts +++ b/packages/utils/src/scaffold.ts @@ -3,7 +3,7 @@ import j from 'jscodeshift'; import logger from 'webpack-cli/lib/utils/logger'; import pEachSeries = require('p-each-series'); import path from 'path'; -import { getPackageManager } from '@webpack-cli/package-utils'; +import { getPackageManager } from 'webpack-cli/lib/utils/get-package-manager'; import { findProjectRoot } from './path-utils'; import { Error } from './types'; import { Config, TransformConfig } from './types'; diff --git a/packages/package-utils/src/processUtils.ts b/packages/utils/src/spawn-child.ts similarity index 73% rename from packages/package-utils/src/processUtils.ts rename to packages/utils/src/spawn-child.ts index a85324f5bdc..93b10a9f79e 100644 --- a/packages/package-utils/src/processUtils.ts +++ b/packages/utils/src/spawn-child.ts @@ -1,7 +1,8 @@ -import execa, { ExecaSyncReturnValue, sync } from 'execa'; -import { getPackageManager, getPathToGlobalPackages } from './packageUtils'; import path from 'path'; import fs from 'fs'; +import { ExecaSyncReturnValue, sync } from 'execa'; +import { getPathToGlobalPackages } from './global-packages-path'; +import { getPackageManager } from 'webpack-cli/lib/utils/get-package-manager'; /** * @@ -39,14 +40,3 @@ export function spawnChild(pkg: string): ExecaSyncReturnValue { return spawnWithArg(pkg, isNew); } - -export async function runCommand(command, args = []): Promise { - try { - await execa(command, args, { - stdio: 'inherit', - shell: true, - }); - } catch (e) { - throw new Error(e); - } -} diff --git a/packages/utils/tsconfig.json b/packages/utils/tsconfig.json index e813c20f537..279b3e923cc 100644 --- a/packages/utils/tsconfig.json +++ b/packages/utils/tsconfig.json @@ -4,6 +4,5 @@ "outDir": "./lib", "rootDir": "./src" }, - "include": ["./src"], - "references": [{ "path": "../package-utils" }] + "include": ["./src"] } diff --git a/packages/webpack-cli/bin/cli.js b/packages/webpack-cli/bin/cli.js index 12783d03a18..e591bb25dc5 100755 --- a/packages/webpack-cli/bin/cli.js +++ b/packages/webpack-cli/bin/cli.js @@ -6,7 +6,8 @@ const importLocal = require('import-local'); const runCLI = require('../lib/bootstrap'); const { yellow } = require('colorette'); const { error, success } = require('../lib/utils/logger'); -const { packageExists, promptInstallation } = require('@webpack-cli/package-utils'); +const { packageExists } = require('../lib/utils/package-exists'); +const { promptInstallation } = require('../lib/utils/prompt-installation'); // Prefer the local installation of webpack-cli if (importLocal(__filename)) { diff --git a/packages/webpack-cli/lib/commands/resolveCommand.js b/packages/webpack-cli/lib/commands/resolveCommand.js index 5719aac528c..fae1398cd03 100644 --- a/packages/webpack-cli/lib/commands/resolveCommand.js +++ b/packages/webpack-cli/lib/commands/resolveCommand.js @@ -1,6 +1,7 @@ const { yellow, cyan } = require('colorette'); const logger = require('../utils/logger'); -const { packageExists, promptInstallation } = require('@webpack-cli/package-utils'); +const { packageExists } = require('../utils/package-exists'); +const { promptInstallation } = require('../utils/prompt-installation'); const packagePrefix = '@webpack-cli'; diff --git a/packages/webpack-cli/lib/groups/resolveAdvanced.js b/packages/webpack-cli/lib/groups/resolveAdvanced.js index 02211e847e8..dc7fd01186b 100644 --- a/packages/webpack-cli/lib/groups/resolveAdvanced.js +++ b/packages/webpack-cli/lib/groups/resolveAdvanced.js @@ -1,4 +1,5 @@ -const { packageExists, promptInstallation } = require('@webpack-cli/package-utils'); +const { packageExists } = require('../utils/package-exists'); +const { promptInstallation } = require('../utils/prompt-installation'); const { yellow } = require('colorette'); const { error, success } = require('../utils/logger'); diff --git a/packages/webpack-cli/lib/utils/Compiler.js b/packages/webpack-cli/lib/utils/Compiler.js index 9bd5b4e2c6e..d2cd7949816 100644 --- a/packages/webpack-cli/lib/utils/Compiler.js +++ b/packages/webpack-cli/lib/utils/Compiler.js @@ -1,4 +1,4 @@ -const { packageExists } = require('@webpack-cli/package-utils'); +const { packageExists } = require('./package-exists'); const webpack = packageExists('webpack') ? require('webpack') : undefined; const logger = require('./logger'); const { writeFileSync } = require('fs'); diff --git a/packages/webpack-cli/lib/utils/__tests__/get-package-manager.test.js b/packages/webpack-cli/lib/utils/__tests__/get-package-manager.test.js new file mode 100644 index 00000000000..b11c0113d86 --- /dev/null +++ b/packages/webpack-cli/lib/utils/__tests__/get-package-manager.test.js @@ -0,0 +1,78 @@ +const fs = require('fs'); +const path = require('path'); + +const syncMock = jest.fn(() => { + return { + stdout: '1.0.0', + }; +}); +jest.setMock('execa', { + sync: syncMock, +}); +const { getPackageManager } = require('../get-package-manager'); + +jest.mock('cross-spawn'); +const globalModulesNpmValue = 'test-npm'; +jest.setMock('global-modules', globalModulesNpmValue); +jest.setMock('enquirer', { + prompt: jest.fn(), +}); + +describe('packageUtils', () => { + describe('getPackageManager', () => { + const testYarnLockPath = path.resolve(__dirname, 'test-yarn-lock'); + const testNpmLockPath = path.resolve(__dirname, 'test-npm-lock'); + const testBothPath = path.resolve(__dirname, 'test-both'); + + const cwdSpy = jest.spyOn(process, 'cwd'); + + beforeAll(() => { + // package-lock.json is ignored by .gitignore, so we simply + // write a lockfile here for testing + if (!fs.existsSync(testNpmLockPath)) { + fs.mkdirSync(testNpmLockPath); + } + fs.writeFileSync(path.resolve(testNpmLockPath, 'package-lock.json'), ''); + fs.writeFileSync(path.resolve(testBothPath, 'package-lock.json'), ''); + }); + + beforeEach(() => { + syncMock.mockClear(); + }); + + it('should find yarn.lock', () => { + cwdSpy.mockReturnValue(testYarnLockPath); + expect(getPackageManager()).toEqual('yarn'); + expect(syncMock.mock.calls.length).toEqual(0); + }); + + it('should find package-lock.json', () => { + cwdSpy.mockReturnValue(testNpmLockPath); + expect(getPackageManager()).toEqual('npm'); + expect(syncMock.mock.calls.length).toEqual(0); + }); + + it('should prioritize yarn with many lock files', () => { + cwdSpy.mockReturnValue(testBothPath); + expect(getPackageManager()).toEqual('yarn'); + expect(syncMock.mock.calls.length).toEqual(0); + }); + + it('should use yarn if yarn command works', () => { + // yarn should output a version number to stdout if + // it is installed + cwdSpy.mockReturnValue(path.resolve(__dirname)); + expect(getPackageManager()).toEqual('yarn'); + expect(syncMock.mock.calls.length).toEqual(1); + }); + + it('should use npm if yarn command fails', () => { + syncMock.mockImplementation(() => { + throw new Error(); + }); + cwdSpy.mockReturnValue(path.resolve(__dirname)); + expect(getPackageManager()).toEqual('npm'); + expect(syncMock.mock.calls.length).toEqual(1); + }); + }); +}); diff --git a/packages/webpack-cli/lib/utils/__tests__/package-exists.test.js b/packages/webpack-cli/lib/utils/__tests__/package-exists.test.js new file mode 100644 index 00000000000..309f479c408 --- /dev/null +++ b/packages/webpack-cli/lib/utils/__tests__/package-exists.test.js @@ -0,0 +1,22 @@ +jest.setMock('../prompt-installation', { + promptInstallation: jest.fn(), +}); + +const ExternalCommand = require('../../commands/resolveCommand'); +const { packageExists } = require('../package-exists'); +const { promptInstallation } = require('../prompt-installation'); + +describe('@webpack-cli/utils', () => { + it('should check existence of package', () => { + // use an actual path relative to the packageUtils file + expect(packageExists('./logger')).toBeTruthy(); + expect(packageExists('./nonexistent-package')).toBeFalsy(); + }); + + it('should not throw if the user interrupts', async () => { + promptInstallation.mockImplementation(() => { + throw new Error(); + }); + await expect(ExternalCommand('info')).resolves.not.toThrow(); + }); +}); diff --git a/packages/webpack-cli/lib/utils/__tests__/prompt-installation.test.js b/packages/webpack-cli/lib/utils/__tests__/prompt-installation.test.js new file mode 100644 index 00000000000..2108828c010 --- /dev/null +++ b/packages/webpack-cli/lib/utils/__tests__/prompt-installation.test.js @@ -0,0 +1,82 @@ +'use strict'; + +jest.mock('execa'); +jest.mock('cross-spawn'); +const globalModulesNpmValue = 'test-npm'; +jest.setMock('global-modules', globalModulesNpmValue); +jest.setMock('enquirer', { + prompt: jest.fn(), +}); + +jest.setMock('../run-command', { + runCommand: jest.fn(), +}); + +jest.setMock('../package-exists', { + packageExists: jest.fn(), +}); + +jest.setMock('../get-package-manager', { + getPackageManager: jest.fn(), +}); + +const { getPackageManager } = require('../get-package-manager'); +const { packageExists } = require('../package-exists'); +const { promptInstallation } = require('../prompt-installation'); +const { runCommand } = require('../run-command'); +const { prompt } = require('enquirer'); + +describe('promptInstallation', () => { + beforeAll(() => { + packageExists.mockReturnValue(true); + }); + beforeEach(() => { + runCommand.mockClear(); + prompt.mockClear(); + }); + + it('should prompt to install using npm if npm is package manager', async () => { + prompt.mockReturnValue({ + installConfirm: true, + }); + getPackageManager.mockReturnValue('npm'); + const preMessage = jest.fn(); + const promptResult = await promptInstallation('test-package', preMessage); + expect(promptResult).toBeTruthy(); + expect(preMessage.mock.calls.length).toEqual(1); + expect(prompt.mock.calls.length).toEqual(1); + expect(runCommand.mock.calls.length).toEqual(1); + expect(prompt.mock.calls[0][0][0].message).toMatch(/Would you like to install test-package\?/); + // install the package using npm + expect(runCommand.mock.calls[0][0]).toEqual('npm install -D test-package'); + }); + + it('should prompt to install using yarn if yarn is package manager', async () => { + prompt.mockReturnValue({ + installConfirm: true, + }); + getPackageManager.mockReturnValue('yarn'); + + const promptResult = await promptInstallation('test-package'); + expect(promptResult).toBeTruthy(); + expect(prompt.mock.calls.length).toEqual(1); + expect(runCommand.mock.calls.length).toEqual(1); + expect(prompt.mock.calls[0][0][0].message).toMatch(/Would you like to install test-package\?/); + // install the package using yarn + expect(runCommand.mock.calls[0][0]).toEqual('yarn add -D test-package'); + }); + + it('should not install if install is not confirmed', async () => { + prompt.mockReturnValue({ + installConfirm: false, + }); + + const promptResult = await promptInstallation('test-package'); + expect(promptResult).toBeUndefined(); + expect(prompt.mock.calls.length).toEqual(1); + // runCommand should not be called, because the installation is not confirmed + expect(runCommand.mock.calls.length).toEqual(0); + expect(prompt.mock.calls[0][0][0].message).toMatch(/Would you like to install test-package\?/); + expect(process.exitCode).toEqual(2); + }); +}); diff --git a/packages/package-utils/__tests__/test-both/yarn.lock b/packages/webpack-cli/lib/utils/__tests__/test-both/package-lock.json similarity index 100% rename from packages/package-utils/__tests__/test-both/yarn.lock rename to packages/webpack-cli/lib/utils/__tests__/test-both/package-lock.json diff --git a/packages/package-utils/__tests__/test-yarn-lock/yarn.lock b/packages/webpack-cli/lib/utils/__tests__/test-both/yarn.lock similarity index 100% rename from packages/package-utils/__tests__/test-yarn-lock/yarn.lock rename to packages/webpack-cli/lib/utils/__tests__/test-both/yarn.lock diff --git a/packages/webpack-cli/lib/utils/__tests__/test-npm-lock/package-lock.json b/packages/webpack-cli/lib/utils/__tests__/test-npm-lock/package-lock.json new file mode 100644 index 00000000000..e69de29bb2d diff --git a/packages/webpack-cli/lib/utils/__tests__/test-yarn-lock/yarn.lock b/packages/webpack-cli/lib/utils/__tests__/test-yarn-lock/yarn.lock new file mode 100644 index 00000000000..e69de29bb2d diff --git a/packages/webpack-cli/lib/utils/cli-flags.js b/packages/webpack-cli/lib/utils/cli-flags.js index f5d5c2261e7..0f5f508169a 100644 --- a/packages/webpack-cli/lib/utils/cli-flags.js +++ b/packages/webpack-cli/lib/utils/cli-flags.js @@ -1,4 +1,4 @@ -const { packageExists } = require('@webpack-cli/package-utils'); +const { packageExists } = require('./package-exists'); const cli = packageExists('webpack') ? require('webpack').cli : undefined; const HELP_GROUP = 'help'; diff --git a/packages/webpack-cli/lib/utils/get-package-manager.js b/packages/webpack-cli/lib/utils/get-package-manager.js new file mode 100644 index 00000000000..7ac3efb9c07 --- /dev/null +++ b/packages/webpack-cli/lib/utils/get-package-manager.js @@ -0,0 +1,35 @@ +const fs = require('fs'); +const path = require('path'); +const { sync } = require('execa'); + +/** + * + * Returns the name of package manager to use, + * preferring yarn over npm if available + * + * @returns {String} - The package manager name + */ +function getPackageManager() { + const hasLocalYarn = fs.existsSync(path.resolve(process.cwd(), 'yarn.lock')); + const hasLocalNpm = fs.existsSync(path.resolve(process.cwd(), 'package-lock.json')); + if (hasLocalYarn) { + return 'yarn'; + } else if (hasLocalNpm) { + return 'npm'; + } + try { + // if the sync function below fails because yarn is not installed, + // an error will be thrown + if (sync('yarn', ['--version'])) { + return 'yarn'; + } + } catch (e) { + // Nothing + } + + return 'npm'; +} + +module.exports = { + getPackageManager, +}; diff --git a/packages/webpack-cli/lib/utils/package-exists.js b/packages/webpack-cli/lib/utils/package-exists.js new file mode 100644 index 00000000000..ebb8c3d9321 --- /dev/null +++ b/packages/webpack-cli/lib/utils/package-exists.js @@ -0,0 +1,12 @@ +function packageExists(packageName) { + try { + require(packageName); + return true; + } catch (err) { + return false; + } +} + +module.exports = { + packageExists, +}; diff --git a/packages/webpack-cli/lib/utils/prompt-installation.js b/packages/webpack-cli/lib/utils/prompt-installation.js new file mode 100644 index 00000000000..03259bdbf87 --- /dev/null +++ b/packages/webpack-cli/lib/utils/prompt-installation.js @@ -0,0 +1,40 @@ +const { prompt } = require('enquirer'); +const { green } = require('colorette'); +const { runCommand } = require('./run-command'); +const { getPackageManager } = require('./get-package-manager'); +const { packageExists } = require('./package-exists'); + +/** + * + * @param packageName + * @param preMessage Message to show before the question + */ +// eslint-disable-next-line @typescript-eslint/explicit-function-return-type +async function promptInstallation(packageName, preMessage) { + const packageManager = getPackageManager(); + const options = [packageManager === 'yarn' ? 'add' : 'install', '-D', packageName]; + + const commandToBeRun = `${packageManager} ${options.join(' ')}`; + if (preMessage) { + preMessage(); + } + const question = `Would you like to install ${packageName}? (That will run ${green(commandToBeRun)})`; + const { installConfirm } = await prompt([ + { + type: 'confirm', + name: 'installConfirm', + message: question, + initial: 'Y', + }, + ]); + if (installConfirm) { + await runCommand(commandToBeRun); + return packageExists(packageName); + } + // eslint-disable-next-line require-atomic-updates + process.exitCode = 2; +} + +module.exports = { + promptInstallation, +}; diff --git a/packages/webpack-cli/lib/utils/run-command.js b/packages/webpack-cli/lib/utils/run-command.js new file mode 100644 index 00000000000..47c13d7b791 --- /dev/null +++ b/packages/webpack-cli/lib/utils/run-command.js @@ -0,0 +1,16 @@ +const execa = require('execa'); + +async function runCommand(command, args = []) { + try { + await execa(command, args, { + stdio: 'inherit', + shell: true, + }); + } catch (e) { + throw new Error(e); + } +} + +module.exports = { + runCommand, +}; diff --git a/packages/webpack-cli/package.json b/packages/webpack-cli/package.json index 5f411a7a68c..8283d8028fe 100644 --- a/packages/webpack-cli/package.json +++ b/packages/webpack-cli/package.json @@ -29,7 +29,6 @@ "dependencies": { "@webpack-cli/info": "^1.0.1-rc.1", "@webpack-cli/init": "^1.0.1-rc.1", - "@webpack-cli/package-utils": "^1.0.1-rc.1", "@webpack-cli/serve": "^1.0.1-rc.1", "ansi-escapes": "^4.3.1", "colorette": "^1.2.1", diff --git a/tsconfig.json b/tsconfig.json index 2955029a9f3..6090b21c41a 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -26,7 +26,6 @@ { "path": "packages/info" }, { "path": "packages/init" }, { "path": "packages/migrate" }, - { "path": "packages/package-utils" }, { "path": "packages/serve" }, { "path": "packages/utils" }, // { "path": "packages/webpack-cli" }, diff --git a/yarn.lock b/yarn.lock index 69fdb2e9b0d..122ddcbb23f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2235,7 +2235,7 @@ "@types/node" "*" "@types/responselike" "*" -"@types/cross-spawn@^6.0.1": +"@types/cross-spawn@^6.0.1", "@types/cross-spawn@^6.0.2": version "6.0.2" resolved "https://registry.yarnpkg.com/@types/cross-spawn/-/cross-spawn-6.0.2.tgz#168309de311cd30a2b8ae720de6475c2fbf33ac7" integrity sha512-KuwNhp3eza+Rhu8IFI5HUXRP0LIhqH5cAjubUvGXXthh4YYBuP2ntwEX+Cz8GJoZUHlKo247wPWOfA9LYEq4cw== @@ -4348,7 +4348,16 @@ cross-spawn@^6.0.0, cross-spawn@^6.0.5: shebang-command "^1.2.0" which "^1.2.9" -cross-spawn@^7.0.0, cross-spawn@^7.0.1: +cross-spawn@^7.0.0: + version "7.0.1" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.1.tgz#0ab56286e0f7c24e153d04cc2aa027e43a9a5d14" + integrity sha512-u7v4o84SwFpD32Z8IIcPZ6z1/ie24O6RU3RbtL5Y316l3KuHVPx9ItBgWQ6VlfAFnRnTtMUrsQ9MUUTuEZjogg== + dependencies: + path-key "^3.1.0" + shebang-command "^2.0.0" + which "^2.0.1" + +cross-spawn@^7.0.3: version "7.0.3" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==