Skip to content

Commit

Permalink
Merge pull request #135 from ext/feature/integrate-lerna
Browse files Browse the repository at this point in the history
feat(deps): remove @lerna/project dependency
  • Loading branch information
ext authored Mar 20, 2024
2 parents d503ce0 + 5b5b71d commit 333313c
Show file tree
Hide file tree
Showing 8 changed files with 769 additions and 1,212 deletions.
1,439 changes: 238 additions & 1,201 deletions package-lock.json

Large diffs are not rendered by default.

18 changes: 12 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,24 +49,27 @@
"transformIgnorePatterns": []
},
"dependencies": {
"@lerna/package": "^6.0.0",
"@lerna/package-graph": "^6.0.0",
"@lerna/project": "^6.0.0",
"@lerna/validation-error": "6.4.1",
"@semantic-release/error": "^4.0.0",
"@semantic-release/release-notes-generator": "^12.0.0",
"aggregate-error": "5.0.0",
"conventional-changelog-writer": "^7.0.0",
"conventional-commits-filter": "^4.0.0",
"conventional-commits-parser": "^5.0.0",
"cosmiconfig": "^7.0.0",
"debug": "4.3.4",
"execa": "8.0.1",
"get-stream": "8.0.1",
"globby": "^11.0.2",
"into-stream": "8.0.1",
"js-yaml": "^4.1.0",
"libnpmversion": "^5.0.0",
"minimatch": "9.0.3",
"nerf-dart": "1.0.0",
"normalize-url": "8.0.1",
"npm-package-arg": "8.1.1",
"npmlog": "7.0.1",
"p-map": "^4.0.0",
"rc": "1.2.8",
"read-pkg": "9.0.1",
"read-pkg-up": "10.1.0",
Expand Down Expand Up @@ -98,21 +101,24 @@
"lerna": "^5 || ^6 || ^7 || ^8",
"semantic-release": "^22 || ^23"
},
"peerDependenciesMeta": {
"lerna": {
"optional": true
}
},
"engines": {
"node": ">= 18.17"
},
"publishConfig": {
"access": "public"
},
"externalDependencies": [
"@lerna/package",
"@lerna/package-graph",
"@lerna/project",
"@semantic-release/error",
"@semantic-release/release-notes-generator",
"conventional-changelog-writer",
"conventional-commits-filter",
"conventional-commits-parser",
"cosmiconfig",
"libnpmversion",
"registry-auth-token"
],
Expand Down
2 changes: 1 addition & 1 deletion src/generate-notes.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { readPackageUp } from "read-pkg-up";
import debugFactory from "debug";
import loadChangelogConfig from "@semantic-release/release-notes-generator/lib/load-changelog-config.js";
import HOSTS_CONFIG from "@semantic-release/release-notes-generator/lib/hosts-config.js";
import { Project } from "@lerna/project";
import { Project } from "./lerna/project";
import { makeDiffPredicate } from "./utils/index.js";

const debug = debugFactory("semantic-release:release-notes-generator");
Expand Down
4 changes: 2 additions & 2 deletions src/get-changed-packages.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { format } from "node:util";
import { PackageGraph } from "@lerna/package-graph";
import { Project } from "@lerna/project";
import { execaSync } from "execa";
import { PackageGraph } from "./lerna/package-graph";
import { Project } from "./lerna/project";
import { shouldLatch } from "./should-latch.js";
import { collectPackages, hasTags, makeDiffPredicate } from "./utils/index.js";

Expand Down
164 changes: 164 additions & 0 deletions src/lerna/package-graph.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
import { ValidationError } from "@lerna/validation-error";
import npa from "npm-package-arg";
import semver from "semver";

const PKG = Symbol("pkg");

/**
* A node in a PackageGraph.
*/
class PackageGraphNode {
/**
* @param {import("./package").Package} pkg
*/
constructor(pkg) {
this.name = pkg.name;
this[PKG] = pkg;

// omit raw pkg from default util.inspect() output
Object.defineProperty(this, PKG, { enumerable: false });

/** @type {Map<string, PackageGraphNode>} */
this.localDependents = new Map();
}

get location() {
return this[PKG].location;
}

get pkg() {
return this[PKG];
}

get version() {
return this[PKG].version;
}

/**
* Determine if the Node satisfies a resolved semver range.
* @see https://github.com/npm/npm-package-arg#result-object
*
* @param {!Result} resolved npm-package-arg Result object
* @returns {Boolean}
*/
satisfies({ gitCommittish, gitRange, fetchSpec }) {
return semver.satisfies(this.version, gitCommittish || gitRange || fetchSpec);
}

/**
* Returns a string representation of this node (its name)
*
* @returns {String}
*/
toString() {
return this.name;
}
}

/**
* A graph of packages in the current project.
*
* @extends {Map<string, PackageGraphNode>}
*/
export class PackageGraph extends Map {
/**
* @param {import("./package").Package[]} packages An array of Packages to build the graph out of.
* @param {'allDependencies'|'dependencies'} [graphType]
* Pass "dependencies" to create a graph of only dependencies,
* excluding the devDependencies that would normally be included.
*/
constructor(packages, graphType = "allDependencies") {
super(packages.map((pkg) => [pkg.name, new PackageGraphNode(pkg)]));

if (packages.length !== this.size) {
// weed out the duplicates
const seen = new Map();

for (const { name, location } of packages) {
if (seen.has(name)) {
seen.get(name).push(location);
} else {
seen.set(name, [location]);
}
}

for (const [name, locations] of seen) {
if (locations.length > 1) {
throw new ValidationError(
"ENAME",
[`Package name "${name}" used in multiple packages:`, ...locations].join("\n\t"),
);
}
}
}

this.forEach((currentNode, currentName) => {
const graphDependencies =
graphType === "dependencies"
? Object.assign({}, currentNode.pkg.optionalDependencies, currentNode.pkg.dependencies)
: Object.assign(
{},
currentNode.pkg.devDependencies,
currentNode.pkg.optionalDependencies,
currentNode.pkg.dependencies,
);

/* eslint-disable-next-line complexity, sonarjs/cognitive-complexity -- inherited technical debt */
Object.keys(graphDependencies).forEach((depName) => {
const depNode = this.get(depName);
// Yarn decided to ignore https://github.com/npm/npm/pull/15900 and implemented "link:"
// As they apparently have no intention of being compatible, we have to do it for them.
// @see https://github.com/yarnpkg/yarn/issues/4212
let spec = graphDependencies[depName].replace(/^link:/, "file:");

// Support workspace: protocol for pnpm and yarn 2+ (https://pnpm.io/workspaces#workspace-protocol-workspace)
const isWorkspaceSpec = /^workspace:/.test(spec);

let fullWorkspaceSpec;
let workspaceAlias;
if (isWorkspaceSpec) {
fullWorkspaceSpec = spec;
spec = spec.replace(/^workspace:/, "");

// replace aliases (https://pnpm.io/workspaces#referencing-workspace-packages-through-aliases)
if (spec === "*" || spec === "^" || spec === "~") {
workspaceAlias = spec;
if (depNode?.version) {
const prefix = spec === "*" ? "" : spec;
const version = depNode.version;
spec = `${prefix}${version}`;
} else {
spec = "*";
}
}
}

const resolved = npa.resolve(depName, spec, currentNode.location);
resolved.workspaceSpec = fullWorkspaceSpec;
resolved.workspaceAlias = workspaceAlias;

if (!depNode) {
// it's an external dependency, store the resolution and bail
return;
}

if (resolved.fetchSpec === depNode.location || depNode.satisfies(resolved)) {
// a local file: specifier OR a matching semver
depNode.localDependents.set(currentName, currentNode);
} else {
if (isWorkspaceSpec) {
// pnpm refuses to resolve remote dependencies when using the workspace: protocol, so lerna does too. See: https://pnpm.io/workspaces#workspace-protocol-workspace.
throw new ValidationError(
"EWORKSPACE",
`Package specification "${depName}@${spec}" could not be resolved within the workspace. To reference a non-matching, remote version of a local dependency, remove the 'workspace:' prefix.`,
);
}
}
});
});
}

get rawPackageList() {
return Array.from(this.values(), (node) => node.pkg);
}
}
130 changes: 130 additions & 0 deletions src/lerna/package.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import path from "node:path";
import npa from "npm-package-arg";

// symbol used to "hide" internal state
const PKG = Symbol("pkg");

// private fields
const _location = Symbol("location");
const _resolved = Symbol("resolved");
const _rootPath = Symbol("rootPath");
const _scripts = Symbol("scripts");

/**
* @typedef {object} RawManifest The subset of package.json properties that Lerna uses
* @property {string} name
* @property {string} version
* @property {boolean} [private]
* @property {Record<string, string>|string} [bin]
* @property {Record<string, string>} [scripts]
* @property {Record<string, string>} [dependencies]
* @property {Record<string, string>} [devDependencies]
* @property {Record<string, string>} [optionalDependencies]
* @property {Record<string, string>} [peerDependencies]
* @property {Record<'directory' | 'registry' | 'tag', string>} [publishConfig]
*/

/**
* Lerna's internal representation of a local package, with
* many values resolved directly from the original JSON.
*/
export class Package {
/**
* @param {RawManifest} pkg
* @param {string} location
* @param {string} [rootPath]
*/
constructor(pkg, location, rootPath = location) {
// npa will throw an error if the name is invalid
const resolved = npa.resolve(pkg.name, `file:${path.relative(rootPath, location)}`, rootPath);

this.name = pkg.name;
this[PKG] = pkg;

// omit raw pkg from default util.inspect() output, but preserve internal mutability
Object.defineProperty(this, PKG, { enumerable: false, writable: true });

this[_location] = location;
this[_resolved] = resolved;
this[_rootPath] = rootPath;
this[_scripts] = { ...pkg.scripts };
}

// readonly getters
get location() {
return this[_location];
}

get private() {
return Boolean(this[PKG].private);
}

get resolved() {
return this[_resolved];
}

get rootPath() {
return this[_rootPath];
}

get scripts() {
return this[_scripts];
}

get manifestLocation() {
return path.join(this.location, "package.json");
}

get nodeModulesLocation() {
return path.join(this.location, "node_modules");
}

// accessors
get version() {
return this[PKG].version;
}

set version(version) {
this[PKG].version = version;
}

// "live" collections
get dependencies() {
return this[PKG].dependencies;
}

get devDependencies() {
return this[PKG].devDependencies;
}

get optionalDependencies() {
return this[PKG].optionalDependencies;
}

get peerDependencies() {
return this[PKG].peerDependencies;
}

/**
* Map-like retrieval of arbitrary values
* @template {keyof RawManifest} K
* @param {K} key field name to retrieve value
* @returns {RawManifest[K]} value stored under key, if present
*/
get(key) {
return this[PKG][key];
}

/**
* Map-like storage of arbitrary values
* @template {keyof RawManifest} K
* @param {T} key field name to store value
* @param {RawManifest[K]} val value to store
* @returns {Package} instance for chaining
*/
set(key, val) {
this[PKG][key] = val;

return this;
}
}
Loading

0 comments on commit 333313c

Please sign in to comment.