-
Notifications
You must be signed in to change notification settings - Fork 8
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #135 from ext/feature/integrate-lerna
feat(deps): remove @lerna/project dependency
- Loading branch information
Showing
8 changed files
with
769 additions
and
1,212 deletions.
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} |
Oops, something went wrong.