Adds npm 7 package-lock.json parser (#82)

* Adds npm 7 package-lock.json parcer

* Adds a check for unsupported package-lock version

* Updates error message

* Adds tests for parseLockFile with npm 7 workspaces

* Updates README with npm workspaces

* Style cleanup

* Changes the error message language

* Rewrites types in a different style

* Change files

* Adds changelog file.

Co-authored-by: Ria Carmin <riacarmin@microsoft.com>
This commit is contained in:
Ria Nicole Carmin 2021-12-02 09:10:14 -08:00 коммит произвёл GitHub
Родитель 1c825bab65
Коммит 0d9b3bd5c0
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
19 изменённых файлов: 205 добавлений и 23 удалений

Просмотреть файл

@ -2,10 +2,11 @@
A collection of tools that are useful in a git-controlled monorepo that is managed by one of these software:
- lerna
- npm workspaces
- pnpm workspaces
- rush
- yarn workspaces
- pnpm workspaces
- lerna
# Environment Variables
@ -17,7 +18,7 @@ default node.js maxBuffer of 1MB)
## PREFERRED_WORKSPACE_MANAGER
Sometimes multiple package manager files are checked in. It is necessary to hint to `workspace-tools` which manager
is used: `yarn`, `pnpm`, `rush`, or `lerna`
is used: `npm`, `yarn`, `pnpm`, `rush`, or `lerna`
# Contributing

Просмотреть файл

@ -0,0 +1,7 @@
{
"type": "minor",
"comment": "Implements NPM workspaces support to parseLockFile utility.",
"packageName": "workspace-tools",
"email": "riacarmin@microsoft.com",
"dependentChangeType": "patch"
}

5
src/__fixtures__/monorepo-npm-unsupported/package-lock.json сгенерированный Normal file
Просмотреть файл

@ -0,0 +1,5 @@
{
"name": "monorepo-npm",
"version": "0.1.0",
"lockfileVersion": 1
}

Просмотреть файл

@ -0,0 +1,10 @@
{
"name": "monorepo-npm",
"license": "MIT",
"version": "0.1.0",
"workspaces": {
"packages": [
"packages/*"
]
}
}

17
src/__fixtures__/monorepo-npm-unsupported/packages/package-a/node_modules/.bin/copy сгенерированный поставляемый Normal file
Просмотреть файл

@ -0,0 +1,17 @@
#!/usr/bin/env node
const path = require("path");
const fs = require("fs");
const source = process.argv[2];
const destination = process.argv[3];
const sourceAbsolute = path.join(process.cwd(), source);
const destinationAbsolute = path.join(process.cwd(), destination);
const destinationAbsoluteDir = path.dirname(destinationAbsolute);
if (!fs.existsSync(destinationAbsoluteDir)) {
fs.mkdirSync(destinationAbsoluteDir);
}
fs.copyFileSync(sourceAbsolute, destinationAbsolute);

Просмотреть файл

@ -0,0 +1,9 @@
{
"name": "package-a",
"license": "MIT",
"version": "0.1.0",
"scripts": {
"compile": "node node_modules/.bin/copy src/index.ts lib/index.js",
"side-effect": "node node_modules/.bin/copy src/index.ts ../DONE"
}
}

Просмотреть файл

@ -0,0 +1 @@
console.log("foo");

17
src/__fixtures__/monorepo-npm-unsupported/packages/package-b/node_modules/.bin/copy сгенерированный поставляемый Normal file
Просмотреть файл

@ -0,0 +1,17 @@
#!/usr/bin/env node
const path = require("path");
const fs = require("fs");
const source = process.argv[2];
const destination = process.argv[3];
const sourceAbsolute = path.join(process.cwd(), source);
const destinationAbsolute = path.join(process.cwd(), destination);
const destinationAbsoluteDir = path.dirname(destinationAbsolute);
if (!fs.existsSync(destinationAbsoluteDir)) {
fs.mkdirSync(destinationAbsoluteDir);
}
fs.copyFileSync(sourceAbsolute, destinationAbsolute);

Просмотреть файл

@ -0,0 +1,8 @@
{
"name": "package-b",
"license": "MIT",
"version": "0.1.0",
"scripts": {
"compile": "node node_modules/.bin/copy src/index.ts lib/index.js"
}
}

Просмотреть файл

@ -0,0 +1 @@
console.log("foo");

24
src/__fixtures__/monorepo-npm/node_modules/.package-lock.json сгенерированный поставляемый Normal file
Просмотреть файл

@ -0,0 +1,24 @@
{
"name": "monorepo-npm",
"version": "0.1.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"node_modules/package-a": {
"resolved": "packages/package-a",
"link": true
},
"node_modules/package-b": {
"resolved": "packages/package-b",
"link": true
},
"packages/package-a": {
"version": "0.1.0",
"license": "MIT"
},
"packages/package-b": {
"version": "0.1.0",
"license": "MIT"
}
}
}

1
src/__fixtures__/monorepo-npm/node_modules/package-a сгенерированный поставляемый Symbolic link
Просмотреть файл

@ -0,0 +1 @@
../packages/package-a

1
src/__fixtures__/monorepo-npm/node_modules/package-b сгенерированный поставляемый Symbolic link
Просмотреть файл

@ -0,0 +1 @@
../packages/package-b

Просмотреть файл

@ -1,20 +1,40 @@
import { setupFixture } from "../helpers/setupFixture";
import { parseLockFile } from "../lockfile";
const ERROR_MESSAGES = {
NO_LOCK: "You do not have yarn.lock, pnpm-lock.yaml or package-lock.json. Please use one of these package managers.",
UNSUPPORTED:
"Your package-lock.json version is not supported: lockfileVersion is 1. You need npm version 7 or above and package-lock version 2 or above. Please, upgrade npm or choose a different package manager.",
};
describe("parseLockFile()", () => {
it("parses yarn.lock file when it is found", async () => {
const packageRoot = await setupFixture("basic");
// General
it("throws if it cannot find lock file", async () => {
const packageRoot = await setupFixture("basic-without-lock-file");
await expect(parseLockFile(packageRoot)).rejects.toThrow(ERROR_MESSAGES.NO_LOCK);
});
// NPM
it("parses package-lock.json file when it is found", async () => {
const packageRoot = await setupFixture("monorepo-npm");
const parsedLockeFile = await parseLockFile(packageRoot);
expect(parsedLockeFile).toHaveProperty("type", "success");
});
it("throws if it cannot find a yarn.lock file", async () => {
const packageRoot = await setupFixture("basic-without-lock-file");
it("throws if npm version is unsupported", async () => {
const packageRoot = await setupFixture("monorepo-npm-unsupported");
await expect(parseLockFile(packageRoot)).rejects.toThrow(
"You do not have either yarn.lock nor pnpm-lock.yaml. Please use one of these package managers"
);
await expect(parseLockFile(packageRoot)).rejects.toThrow(ERROR_MESSAGES.UNSUPPORTED);
});
// Yarn
it("parses yarn.lock file when it is found", async () => {
const packageRoot = await setupFixture("basic");
const parsedLockeFile = await parseLockFile(packageRoot);
expect(parsedLockeFile).toHaveProperty("type", "success");
});
it("parses combined ranges in yarn.lock", async () => {
@ -26,12 +46,11 @@ describe("parseLockFile()", () => {
);
});
it("parses pnpm-lock.yaml properly", async () => {
// PNPM
it("parses pnpm-lock.yaml file when it is found", async () => {
const packageRoot = await setupFixture("basic-pnpm");
const parsedLockeFile = await parseLockFile(packageRoot);
expect(Object.keys(parsedLockeFile.object["yargs@16.2.0"].dependencies!)).toContain(
"cliui"
);
expect(Object.keys(parsedLockeFile.object["yargs@16.2.0"].dependencies!)).toContain("cliui");
});
});

Просмотреть файл

@ -1,11 +1,11 @@
// NOTE: never place the import of lockfile implementation here, as it slows down the library as a whole
import path from "path";
import findUp from "find-up";
import fs from "fs-extra";
import { ParsedLock, PnpmLockFile } from "./types";
import { ParsedLock, PnpmLockFile, NpmLockFile } from "./types";
import readYamlFile from "read-yaml-file";
import { nameAtVersion } from "./nameAtVersion";
import { parsePnpmLock } from "./parsePnpmLock";
import { parseNpmLock } from "./parseNpmLock";
const memoization: { [path: string]: ParsedLock } = {};
@ -42,9 +42,38 @@ export async function parseLockFile(packageRoot: string): Promise<ParsedLock> {
return memoization[pnpmLockPath];
}
throw new Error("You do not have either yarn.lock nor pnpm-lock.yaml. Please use one of these package managers");
// Third, try for npm workspaces
let npmLockPath = await findUp(["package-lock.json"], { cwd: packageRoot });
if (npmLockPath) {
if (memoization[npmLockPath]) {
return memoization[npmLockPath];
}
let npmLockJson;
try {
npmLockJson = fs.readFileSync(npmLockPath);
} catch {
throw new Error("Couldnt parse package-lock.json.");
}
const npmLock: NpmLockFile = JSON.parse(npmLockJson.toString());
if (!npmLock?.lockfileVersion || npmLock.lockfileVersion < 2) {
throw new Error(
`Your package-lock.json version is not supported: lockfileVersion is ${npmLock.lockfileVersion}. You need npm version 7 or above and package-lock version 2 or above. Please, upgrade npm or choose a different package manager.`
);
}
memoization[npmLockPath] = parseNpmLock(npmLock);
return memoization[npmLockPath];
}
throw new Error(
"You do not have yarn.lock, pnpm-lock.yaml or package-lock.json. Please use one of these package managers."
);
}
export { nameAtVersion };
export { queryLockFile } from "./queryLockFile";
export * from './types';
export * from "./types";

Просмотреть файл

@ -0,0 +1,6 @@
import { ParsedLock, NpmLockFile } from "./types";
export const parseNpmLock = (lock: NpmLockFile): ParsedLock => ({
object: lock.dependencies ?? {},
type: "success",
});

Просмотреть файл

@ -15,3 +15,29 @@ export type ParsedLock = {
export interface PnpmLockFile {
packages: { [name: string]: any };
}
export interface NpmWorkspacesInfo {
version: string;
workspaces: { packages: string[] };
}
export interface NpmSymlinkInfo {
resolved: string; // Where the package is resolved from.
link: boolean; // A flag to indicate that this is a symbolic link.
integrity?: "sha512" | "sha1";
dev?: boolean;
optional?: boolean;
devOptional?: boolean;
dependencies?: { [key: string]: LockDependency };
}
export interface NpmLockFile {
name: string;
version: string;
lockfileVersion?: 1 | 2 | 3; // 1: v5, v6; 2: backwards compatible v7; 3: non-backwards compatible v7
requires?: boolean;
packages?: {
""?: NpmWorkspacesInfo; // Monorepo root
} & { [key: string]: NpmSymlinkInfo | LockDependency };
dependencies?: { [key: string]: LockDependency };
}

Просмотреть файл

@ -3,14 +3,14 @@ import {
WorkspaceImplementations,
} from "./implementations";
import { getLernaWorkspaces } from "./implementations/lerna";
import { getNpmWorkspaces } from "./implementations/npm";
import { getPnpmWorkspaces } from "./implementations/pnpm";
import { getYarnWorkspaces } from "./implementations/yarn";
import { getRushWorkspaces } from "./implementations/rush";
import { getYarnWorkspaces } from "./implementations/yarn";
import { WorkspaceInfo } from "../types/WorkspaceInfo";
import { WorkspaceManager } from "./WorkspaceManager";
import { getNpmWorkspaces } from "./implementations/npm";
import { getLernaWorkspaces } from "./implementations/lerna";
const workspaceGetter: {
[key in WorkspaceImplementations]: (cwd: string) => WorkspaceInfo;

Просмотреть файл

@ -12,6 +12,6 @@ export function getNpmWorkspaceRoot(cwd: string): string {
}
export function getNpmWorkspaces(cwd: string): WorkspaceInfo {
const yarnWorkspacesRoot = getNpmWorkspaceRoot(cwd);
return getWorkspaceInfoFromWorkspaceRoot(yarnWorkspacesRoot);
const npmWorkspacesRoot = getNpmWorkspaceRoot(cwd);
return getWorkspaceInfoFromWorkspaceRoot(npmWorkspacesRoot);
}