Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support workspaces #100

Draft
wants to merge 6 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion lib/config-generator.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
59 changes: 55 additions & 4 deletions lib/utils/npm-utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,20 +39,45 @@ function findPackageJson(startDir) {
return null;
}

/**
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can write this as a function(possible isRootWorkspace()), something like:

function isRootWorkspace(packageJsonPath){
  const pkg = JSON.parse(...)
  if(pkg.private === true && pkg.workspaces !== void 0) return true;
  return fs.existsSync(path.join(packageJsonPath, "../pnpm-workspace.yaml");
}

it can be evalutated in the constructor, so you can just use it when you need it:

this.isRootWorkspace = isRootWorkspace();

* 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
//------------------------------------------------------------------------------

/**
* 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") {
Expand Down Expand Up @@ -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
Expand All @@ -189,8 +238,10 @@ export {
installSyncSaveDev,
fetchPeerDependencies,
findPackageJson,
findPnpmWorkspaceYaml,
checkDeps,
checkDevDeps,
checkPackageJson,
isPackageTypeModule
isPackageTypeModule,
isYarnLegacyWorkspaceEnabled
};
13 changes: 13 additions & 0 deletions tests/fixtures/pnpm-workspace-project/package.json
Original file line number Diff line number Diff line change
@@ -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"
}
12 changes: 12 additions & 0 deletions tests/fixtures/pnpm-workspace-project/packages/sub/package.json
Original file line number Diff line number Diff line change
@@ -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"
}
11 changes: 11 additions & 0 deletions tests/fixtures/pnpm-workspace-project/pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Empty file.
16 changes: 16 additions & 0 deletions tests/fixtures/yarn-legacy-workspace-project/package.json
Original file line number Diff line number Diff line change
@@ -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"
}
Original file line number Diff line number Diff line change
@@ -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"
}
4 changes: 4 additions & 0 deletions tests/fixtures/yarn-legacy-workspace-project/yarn.lock
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1


15 changes: 12 additions & 3 deletions tests/utils/npm-utils.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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"]);
Expand All @@ -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"]);
Expand Down
55 changes: 55 additions & 0 deletions tests/workspace-support.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/**
* @fileoverview tests for pnpm workspace install packages at root
* @author Wataru Nishimura<wataru.chame.gon@gmail.com>
*/

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"]);

Check failure on line 52 in tests/workspace-support.spec.js

View workflow job for this annotation

GitHub Actions / Test (ubuntu-latest, 22.x)

tests/workspace-support.spec.js > pnpm workspace install packages at root > should invoke yarn legacy with workspace option to install a single desired packages

AssertionError: expected [ 'add', '-D', 'desired-package' ] to deeply equal [ Array(4) ] - Expected + Received Array [ "add", "-D", - "-W", "desired-package", ] ❯ tests/workspace-support.spec.js:52:16

Check failure on line 52 in tests/workspace-support.spec.js

View workflow job for this annotation

GitHub Actions / Test (ubuntu-latest, 21.x)

tests/workspace-support.spec.js > pnpm workspace install packages at root > should invoke yarn legacy with workspace option to install a single desired packages

AssertionError: expected [ 'add', '-D', 'desired-package' ] to deeply equal [ Array(4) ] - Expected + Received Array [ "add", "-D", - "-W", "desired-package", ] ❯ tests/workspace-support.spec.js:52:16

Check failure on line 52 in tests/workspace-support.spec.js

View workflow job for this annotation

GitHub Actions / Test (ubuntu-latest, 20.x)

tests/workspace-support.spec.js > pnpm workspace install packages at root > should invoke yarn legacy with workspace option to install a single desired packages

AssertionError: expected [ 'add', '-D', 'desired-package' ] to deeply equal [ Array(4) ] - Expected + Received Array [ "add", "-D", - "-W", "desired-package", ] ❯ tests/workspace-support.spec.js:52:16

Check failure on line 52 in tests/workspace-support.spec.js

View workflow job for this annotation

GitHub Actions / Test (ubuntu-latest, 18.x)

tests/workspace-support.spec.js > pnpm workspace install packages at root > should invoke yarn legacy with workspace option to install a single desired packages

AssertionError: expected [ 'add', '-D', 'desired-package' ] to deeply equal [ Array(4) ] - Expected + Received Array [ "add", "-D", - "-W", "desired-package", ] ❯ tests/workspace-support.spec.js:52:16

Check failure on line 52 in tests/workspace-support.spec.js

View workflow job for this annotation

GitHub Actions / Test (ubuntu-latest, 18.18.0)

tests/workspace-support.spec.js > pnpm workspace install packages at root > should invoke yarn legacy with workspace option to install a single desired packages

AssertionError: expected [ 'add', '-D', 'desired-package' ] to deeply equal [ Array(4) ] - Expected + Received Array [ "add", "-D", - "-W", "desired-package", ] ❯ tests/workspace-support.spec.js:52:16
});

});