diff --git a/lib/config-generator.js b/lib/config-generator.js index 170b053b..43918b77 100644 --- a/lib/config-generator.js +++ b/lib/config-generator.js @@ -263,7 +263,7 @@ export default [\n${exportContent}];`; })).packageManager; log.info("☕️Installing..."); - installSyncSaveDev(this.result.devDependencies, packageManager); + installSyncSaveDev(this.result.devDependencies, this.cwd, packageManager); await writeFile(configPath, this.result.configContent); // import("eslint") won't work in some cases. diff --git a/lib/utils/npm-utils.js b/lib/utils/npm-utils.js index f8a4dd2c..82104c54 100644 --- a/lib/utils/npm-utils.js +++ b/lib/utils/npm-utils.js @@ -39,6 +39,23 @@ function findPackageJson(startDir) { return null; } +/** + * Find the pnpm-workspace.yaml at package root. + * @param {string} [startDir=process.cwd()] Starting directory, default is process.cwd() + * @returns {boolean} Whether a pnpm-workspace.yaml is found in current path. + */ +function findPnpmWorkspaceYaml(startDir) { + const dir = path.resolve(startDir || process.cwd()); + + const yamlFile = path.join(dir, "pnpm-workspace.yaml"); + + if (!fs.existsSync(yamlFile) || !fs.statSync(yamlFile).isFile()) { + return false; + } + + return true; +} + //------------------------------------------------------------------------------ // Private //------------------------------------------------------------------------------ @@ -46,13 +63,21 @@ function findPackageJson(startDir) { /** * Install node modules synchronously and save to devDependencies in package.json * @param {string|string[]} packages Node module or modules to install + * @param {string} cwd working directory * @param {string} packageManager Package manager to use for installation. * @returns {void} */ -function installSyncSaveDev(packages, packageManager = "npm") { +function installSyncSaveDev(packages, cwd = process.cwd(), packageManager = "npm") { const packageList = Array.isArray(packages) ? packages : [packages]; - const installCmd = packageManager === "yarn" ? "add" : "install"; - const installProcess = spawn.sync(packageManager, [installCmd, "-D"].concat(packageList), { stdio: "inherit" }); + const installCmd = packageManager === "npm" ? "install" : "add"; + + // When cmd executed at pnpm workspace, apply "-w" option. + const pnpmWorkspaceRootOption = packageManager === "pnpm" && findPnpmWorkspaceYaml(cwd) ? "-w" : ""; + + // filter nullish values and create options. + const installOptions = [installCmd, "-D"].concat(pnpmWorkspaceRootOption).concat(packageList).filter(value => !!value); + + const installProcess = spawn.sync(packageManager, installOptions, { stdio: "inherit", cwd }); const error = installProcess.error; if (error && error.code === "ENOENT") { @@ -180,6 +205,30 @@ function isPackageTypeModule(pkgJSONPath) { return false; } +/** + * check if yarn legacy workspace enabled + * @param {string} pkgJSONPath path to package.json + * @returns {boolean} return true if the package.json includes worksapces and private option is "true" + */ +function isYarnLegacyWorkspaceEnabled(pkgJSONPath) { + if (pkgJSONPath) { + const pkgJSONContents = JSON.parse(fs.readFileSync(pkgJSONPath, "utf8")); + + if (pkgJSONContents.private === "false") { + return false; + } + + const workspaceOption = pkgJSONContents.workspace; + + if (!workspaceOption || !Array.isArray(workspaceOption)) { + return false; + } + + return true; + } + + return false; +} //------------------------------------------------------------------------------ // Public Interface @@ -189,8 +238,10 @@ export { installSyncSaveDev, fetchPeerDependencies, findPackageJson, + findPnpmWorkspaceYaml, checkDeps, checkDevDeps, checkPackageJson, - isPackageTypeModule + isPackageTypeModule, + isYarnLegacyWorkspaceEnabled }; diff --git a/tests/fixtures/pnpm-workspace-project/package.json b/tests/fixtures/pnpm-workspace-project/package.json new file mode 100644 index 00000000..313d760f --- /dev/null +++ b/tests/fixtures/pnpm-workspace-project/package.json @@ -0,0 +1,13 @@ +{ + "name": "pnpm-workspace-project", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "type": "module", + "keywords": [], + "author": "", + "license": "ISC" +} diff --git a/tests/fixtures/pnpm-workspace-project/packages/sub/package.json b/tests/fixtures/pnpm-workspace-project/packages/sub/package.json new file mode 100644 index 00000000..12084a0f --- /dev/null +++ b/tests/fixtures/pnpm-workspace-project/packages/sub/package.json @@ -0,0 +1,12 @@ +{ + "name": "sub", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC" +} diff --git a/tests/fixtures/pnpm-workspace-project/pnpm-lock.yaml b/tests/fixtures/pnpm-workspace-project/pnpm-lock.yaml new file mode 100644 index 00000000..db4e02c7 --- /dev/null +++ b/tests/fixtures/pnpm-workspace-project/pnpm-lock.yaml @@ -0,0 +1,11 @@ +lockfileVersion: '6.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: {} + + packages/sub: {} diff --git a/tests/fixtures/pnpm-workspace-project/pnpm-workspace.yaml b/tests/fixtures/pnpm-workspace-project/pnpm-workspace.yaml new file mode 100644 index 00000000..e69de29b diff --git a/tests/fixtures/yarn-legacy-workspace-project/package.json b/tests/fixtures/yarn-legacy-workspace-project/package.json new file mode 100644 index 00000000..f6547553 --- /dev/null +++ b/tests/fixtures/yarn-legacy-workspace-project/package.json @@ -0,0 +1,16 @@ +{ + "name": "pnpm-workspace-project", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "type": "module", + "keywords": [], + "author": "", + "license": "ISC", + "devDependencies": {}, + "private": true, + "workspaces": "sub" +} diff --git a/tests/fixtures/yarn-legacy-workspace-project/packages/sub/package.json b/tests/fixtures/yarn-legacy-workspace-project/packages/sub/package.json new file mode 100644 index 00000000..12084a0f --- /dev/null +++ b/tests/fixtures/yarn-legacy-workspace-project/packages/sub/package.json @@ -0,0 +1,12 @@ +{ + "name": "sub", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC" +} diff --git a/tests/fixtures/yarn-legacy-workspace-project/yarn.lock b/tests/fixtures/yarn-legacy-workspace-project/yarn.lock new file mode 100644 index 00000000..fb57ccd1 --- /dev/null +++ b/tests/fixtures/yarn-legacy-workspace-project/yarn.lock @@ -0,0 +1,4 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + diff --git a/tests/utils/npm-utils.spec.js b/tests/utils/npm-utils.spec.js index 1cb89837..f4ac61aa 100644 --- a/tests/utils/npm-utils.spec.js +++ b/tests/utils/npm-utils.spec.js @@ -157,7 +157,7 @@ describe("npmUtils", () => { it("should invoke npm to install a single desired package", () => { const stub = sinon.stub(spawn, "sync").returns({ stdout: "" }); - installSyncSaveDev("desired-package", "npm"); + installSyncSaveDev("desired-package", process.cwd(), "npm"); assert(stub.calledOnce); assert.strictEqual(stub.firstCall.args[0], "npm"); assert.deepStrictEqual(stub.firstCall.args[1], ["install", "-D", "desired-package"]); @@ -167,18 +167,27 @@ describe("npmUtils", () => { it("should invoke yarn to install a single desired package", () => { const stub = sinon.stub(spawn, "sync").returns({ stdout: "" }); - installSyncSaveDev("desired-package", "yarn"); + installSyncSaveDev("desired-package", process.cwd(), "yarn"); assert(stub.calledOnce); assert.strictEqual(stub.firstCall.args[0], "yarn"); assert.deepStrictEqual(stub.firstCall.args[1], ["add", "-D", "desired-package"]); stub.restore(); }); + it("should invoke pnpm to install a single desired package", () => { + const stub = sinon.stub(spawn, "sync").returns({ stdout: "" }); + + installSyncSaveDev("desired-package", process.cwd(), "pnpm"); + assert(stub.calledOnce); + assert.strictEqual(stub.firstCall.args[0], "pnpm"); + assert.deepStrictEqual(stub.firstCall.args[1], ["add", "-D", "desired-package"]); + }); + it("should accept an array of packages to install", () => { const stub = sinon.stub(spawn, "sync").returns({ stdout: "" }); - installSyncSaveDev(["first-package", "second-package"], "npm"); + installSyncSaveDev(["first-package", "second-package"], process.cwd(), "npm"); assert(stub.calledOnce); assert.strictEqual(stub.firstCall.args[0], "npm"); assert.deepStrictEqual(stub.firstCall.args[1], ["install", "-D", "first-package", "second-package"]); diff --git a/tests/workspace-support.spec.js b/tests/workspace-support.spec.js new file mode 100644 index 00000000..f45ad6a2 --- /dev/null +++ b/tests/workspace-support.spec.js @@ -0,0 +1,55 @@ +/** + * @fileoverview tests for pnpm workspace install packages at root + * @author Wataru Nishimura + */ + +import { describe, it, expect, assert, afterEach } from "vitest"; +import { fileURLToPath } from "node:url"; +import { join } from "path"; +import { findPnpmWorkspaceYaml, installSyncSaveDev } from "../lib/utils/npm-utils.js"; +import sinon from "sinon"; +import spawn from "cross-spawn"; + +const __filename = fileURLToPath(import.meta.url); // eslint-disable-line no-underscore-dangle -- commonjs convention + +describe("pnpm workspace install packages at root", () => { + const pnpmWithWorkspaceDir = join(__filename, "../fixtures/pnpm-workspace-project"); + const yarnLegacyWithWorkspaceDir = join(__filename, "../fixtures/yarn-legacy-workspace-project"); + + afterEach(() => { + sinon.verifyAndRestore(); + }); + + /** + * pnpm recognizes whether workspace is enabled by `pnpm-workspace.yaml`. + * This test case tests function to find `pnpm-workspace.yaml`. + */ + it("find pnpm-workspace.yaml", () => { + const pnpmWorkspaceYaml = findPnpmWorkspaceYaml(pnpmWithWorkspaceDir); + + expect(pnpmWorkspaceYaml).toBeTruthy(); + }); + + /** + * at project root, `pnpm add` needs to be applied "-w" option. + */ + it("should invoke pnpm with workspace option to install a single desired packages", async () => { + const stub = sinon.stub(spawn, "sync").returns({ stdout: 0 }); + + installSyncSaveDev("desired-package", pnpmWithWorkspaceDir, "pnpm"); + assert(stub.calledOnce); + assert.strictEqual(stub.firstCall.args[0], "pnpm"); + assert.deepStrictEqual(stub.firstCall.args[1], ["add", "-D", "-w", "desired-package"]); + stub.restore(); + }); + + it("should invoke yarn legacy with workspace option to install a single desired packages", async () => { + const stub = sinon.stub(spawn, "sync").returns({ stdout: 0 }); + + installSyncSaveDev("desired-package", yarnLegacyWithWorkspaceDir, "yarn"); + assert(stub.calledOnce); + assert.strictEqual(stub.firstCall.args[0], "yarn"); + assert.deepStrictEqual(stub.firstCall.args[1], ["add", "-D", "-W", "desired-package"]); + }); + +});