Skip to content

Commit

Permalink
feat: support other package managers -- add tests for pnpm
Browse files Browse the repository at this point in the history
  • Loading branch information
David CLEMENT committed Feb 20, 2024
1 parent 4b2e55d commit 15a0ea4
Show file tree
Hide file tree
Showing 6 changed files with 288 additions and 45 deletions.
14 changes: 13 additions & 1 deletion .github/workflows/node.js.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ jobs:
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
- name: Use pnpm for version
uses: pnpm/action-setup@v3
with:
version: 8
- run: npm ci
- name: Build
run: |
Expand All @@ -36,6 +40,10 @@ jobs:
- uses: actions/checkout@v4
- name: Use Node.js 16
uses: actions/setup-node@v4
- name: Use pnpm for version
uses: pnpm/action-setup@v3
with:
version: 8
- name: Build
run: |
npm ci
Expand All @@ -45,7 +53,7 @@ jobs:
npm install $(npx -y npm-min-peer lerna --major ${{ matrix.lerna-version }} --with-name)
npm ls lerna
- name: Integration test
run: npm exec jest --no-coverage integration
run: LERNA_VERSION=${{ matrix.lerna-version }} npm exec jest --no-coverage integration

semantic-release:
runs-on: ubuntu-latest
Expand All @@ -58,6 +66,10 @@ jobs:
- uses: actions/checkout@v4
- name: Use Node.js 16
uses: actions/setup-node@v4
- name: Use pnpm for version
uses: pnpm/action-setup@v3
with:
version: 8
- name: Build
run: |
npm ci
Expand Down
34 changes: 9 additions & 25 deletions lib/prepare.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@ import { Package } from "@lerna/package";
import { writeJsonFile } from "write-json-file";
import semverParse from "semver/functions/parse.js";
import getChangedPackages from "./get-changed-packages.js";
import {
getLockFileFromPackageManager,
getPackageManagerFromLockFile,
getUpdateLockFileCommand,
} from "./utils/package-manager-commands.js";

/**
* @param {string} path
Expand Down Expand Up @@ -83,35 +88,14 @@ async function updatePackage(npmrc, pkg, context, currentVersions) {
async function updateLockfile(npmrc, pkg, context) {
const { env, stdout, stderr, logger } = context;

const packageManagerConfigs = [
[
"package-lock.json",
[
"npm",
"install",
"--package-lock-only",
"--ignore-scripts",
"--no-audit",
"--userconfig",
npmrc,
],
],
[
"pnpm-lock.yaml",
["pnpm", "install", "--lockfile-only", "--ignore-scripts", " --config", npmrc],
],
["yarn.lock", ["yarn", "install"]],
];

const packageManagerConfig = packageManagerConfigs.find(([file]) =>
existsSync(path.join(pkg.location, file)),
);
const packageManager = getPackageManagerFromLockFile(pkg.location);
const packageFile = getLockFileFromPackageManager(packageManager);

if (!packageManagerConfig) {
if (!existsSync(path.join(pkg.location, packageFile))) {
return;
}

const [packageFile, [command, ...options]] = packageManagerConfig;
const [command, ...options] = getUpdateLockFileCommand(packageManager, npmrc);

logger.log("Update %s file in %s", packageFile, pkg.location);

Expand Down
90 changes: 90 additions & 0 deletions lib/utils/package-manager-commands.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import path from "node:path";
import { existsSync } from "node:fs";

/**
* @typedef {'npm'|'pnpm'|'yarn'} PackageManager
*/

/**
* The list of configuration for package managers
* @type {[{lockFile: string, packageManager: PackageManager, updateLockFileCommand: string[]}]}
*/
const packageManagerConfigurations = [
{
packageManager: "npm",
lockFile: "package-lock.json",
updateLockFileCommand: [
"npm",
"install",
"--package-lock-only",
"--ignore-scripts",
"--no-audit",
],
},
{
packageManager: "pnpm",
lockFile: "pnpm-lock.yaml",
updateLockFileCommand: ["pnpm", "install", "--lockfile-only", "--ignore-scripts"],
},
{
packageManager: "yarn",
lockFile: "yarn.lock",
updateLockFileCommand: ["yarn", "install"],
},
];

/**
* Detect package manager from lockFile
* @param {string} location root of the project used to search for lockfile
* @returns {PackageManager}
*/
export function getPackageManagerFromLockFile(location) {
return (
packageManagerConfigurations.find(({ lockFile }) => existsSync(path.join(location, lockFile)))
?.packageManager ?? "npm"
);
}

/**
* define lockfile from package manager
* @param {PackageManager} packageManager
* @returns {string}
*/
export function getLockFileFromPackageManager(packageManager) {
return packageManagerConfiguration(packageManager).lockFile;
}

/**
* @param {PackageManager} pm the package manager
* @returns {{lockFile: string, packageManager: ("npm"|"pnpm"|"yarn"), updateLockFileCommand: string[]}} the associated configuration
*/
function packageManagerConfiguration(pm) {
return packageManagerConfigurations.find(({ packageManager }) => packageManager === pm);
}

/**
* Add dynamic configuration to a command
* @param {(any) => string[]} commandSelector
* @param {PackageManager} packageManager
* @param {string} npmrc
* @returns {string[]}
*/
function findAndPrepareCommand(commandSelector, packageManager, npmrc) {
const command = commandSelector(packageManagerConfiguration(packageManager));
const extension = packageManager === "npm" ? ["--userconfig", npmrc] : [];
return [...command, ...extension];
}

/**
* Get the update lockFile command
* @param {PackageManager} packageManager
* @param {string} npmrc
* @returns {string[]}
*/
export function getUpdateLockFileCommand(packageManager, npmrc) {
return findAndPrepareCommand(
(configuration) => configuration.updateLockFileCommand,
packageManager,
npmrc,
);
}
38 changes: 33 additions & 5 deletions test/helpers/package.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import path from "node:path";
import { execa } from "execa";
import { outputJson } from "fs-extra";
import {
getLockFileFromPackageManager,
getUpdateLockFileCommand,
} from "../../lib/utils/package-manager-commands.js";

const MOCK_NAME = "Mock user";
const MOCK_EMAIL = "[email protected]";
Expand All @@ -18,13 +22,15 @@ const MOCK_EMAIL = "[email protected]";
* @param {string} cwd - Project directory
* @param {string} name - Package name
* @param {string} version - Package initial version
* @param {{private: boolean, lockfile: boolean}} [options] - Package options
* @param {{private?: boolean, lockfile?: boolean, packageManager?: 'npm' | 'pnpm' | 'yarn', workspaces?: boolean}} [options] - Package options
* @returns {Promise<Package>}
*/
export async function createPackage(cwd, name, version, options = {}) {
const pkgRoot = `packages/${name}`;
const manifestLocation = path.resolve(cwd, pkgRoot, "package.json");
const lockfileLocation = path.resolve(cwd, pkgRoot, "package-lock.json");
const { lockfile, packageManager = "npm" } = options;
const lockFileName = getLockFileFromPackageManager(packageManager);
const lockfileLocation = lockfile ? path.resolve(cwd, pkgRoot, lockFileName) : null;
const npmEnv = {
...process.env,
NPM_EMAIL: MOCK_EMAIL,
Expand All @@ -38,8 +44,18 @@ export async function createPackage(cwd, name, version, options = {}) {
};

await outputJson(manifestLocation, { name, version, private: options.private });

if (options.lockfile) {
await execa("npm", ["install", "--package-lock-only", "--ignore-scripts", "--no-audit"], {
const [command, ...args] = getUpdateLockFileCommand(
packageManager,
path.resolve(cwd, ".npmrc"),
);

if (packageManager === "pnpm" && options.workspaces) {
args.push("-w");
}

await execa(command, args, {
cwd: path.resolve(cwd, pkgRoot),
env: npmEnv,
});
Expand All @@ -51,9 +67,21 @@ export async function createPackage(cwd, name, version, options = {}) {
return {
name,
manifestLocation,
lockfileLocation: options.lockfile ? lockfileLocation : null,
lockfileLocation,
async require(dep) {
await execa("npm", ["install", "--workspace", this.name, dep.name], { cwd });
let args;
switch (packageManager) {
case "npm":
args = ["install", "--workspace", this.name, dep.name];
break;
case "pnpm":
args = ["add", "--workspace", "--filter", this.name, dep.name];
break;
case "yarn":
args = ["add", this.name, dep.name];
break;
}
await execa(packageManager, args, { cwd });
},
resolve(...parts) {
return path.resolve(cwd, pkgRoot, ...parts);
Expand Down
51 changes: 37 additions & 14 deletions test/helpers/project.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import path from "node:path";
import { execa } from "execa";
import { outputJson, outputFile } from "fs-extra";
import {
getLockFileFromPackageManager,
getUpdateLockFileCommand,
} from "../../lib/utils/package-manager-commands.js";
import * as npmRegistry from "./npm-registry";

const MOCK_NAME = "Mock user";
Expand All @@ -27,16 +31,20 @@ function generateNpmrc() {
].join("\n");
}

const WORKSPACES = ["packages/*"];

/**
* @param {string} cwd - Project directory
* @param {string} version - Root project initial version
* @param {{lockfile: boolean, workspaces: boolean}} [options] - Package options
* @param {{private?: boolean, lockfile?: boolean, packageManager?: 'npm' | 'pnpm' | 'yarn', workspaces?: boolean}} [options] - Package options
* @returns {Promise<Project>}
*/
export async function createProject(cwd, version, options = {}) {
const name = "root-pkg";
const manifestLocation = path.resolve(cwd, "package.json");
const lockfileLocation = path.resolve(cwd, "package-lock.json");
const { lockfile, packageManager = "npm" } = options;
const lockFileName = getLockFileFromPackageManager(packageManager);
const lockfileLocation = lockfile ? path.resolve(cwd, lockFileName) : null;
const lernaPath = path.resolve(cwd, "lerna.json");
const npmEnv = {
...process.env,
Expand All @@ -56,11 +64,22 @@ export async function createProject(cwd, version, options = {}) {
name,
version: "0.0.0",
publishConfig: {},
workspaces: options.workspaces ? ["packages/*"] : undefined,
workspaces: options.workspaces ? WORKSPACES : undefined,
},
{ spaces: 2 },
);
await outputJson(lernaPath, { version, packages: ["packages/*"] });

if (packageManager === "pnpm" && options.workspaces) {
await outputJson(
path.resolve(cwd, "pnpm-workspace.yaml"),
{
packages: WORKSPACES,
},
{ spaces: 2 },
);
}

await outputJson(lernaPath, { version, packages: WORKSPACES });
await outputFile(path.resolve(cwd, ".npmrc"), generateNpmrc(), "utf-8");
await outputFile(path.resolve(cwd, ".gitignore"), ["node_modules"].join("\n"), "utf-8");

Expand All @@ -71,18 +90,22 @@ export async function createProject(cwd, version, options = {}) {
});

if (options.lockfile) {
const args = [
"install",
"--package-lock-only",
"--lockfile-version=2",
"--ignore-scripts",
"--no-audit",
];
await execa("npm", args, {
const [command, ...args] = getUpdateLockFileCommand(
packageManager,
path.resolve(cwd, ".npmrc"),
);

if (packageManager === "npm") {
args.push("--lockfile-version=2");
} else if (packageManager === "pnpm" && options.workspaces) {
args.push("-w");
}

await execa(command, args, {
cwd,
env: npmEnv,
});
await execa("git", ["add", "package-lock.json"], {
await execa("git", ["add", lockFileName], {
cwd,
env: gitEnv,
});
Expand All @@ -93,7 +116,7 @@ export async function createProject(cwd, version, options = {}) {
return {
name,
manifestLocation,
lockfileLocation: options.lockfile ? lockfileLocation : null,
lockfileLocation,
lernaPath,
async commit(message) {
await execa("git", ["add", "."], { cwd, env: gitEnv });
Expand Down
Loading

0 comments on commit 15a0ea4

Please sign in to comment.