@backstage/cli
Version:
CLI for developing Backstage plugins and apps
291 lines (285 loc) • 10.1 kB
JavaScript
var fs = require('fs-extra');
var semver = require('semver');
var parsers = require('@yarnpkg/parsers');
var lockfile = require('@yarnpkg/lockfile');
function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }
var fs__default = /*#__PURE__*/_interopDefaultLegacy(fs);
var semver__default = /*#__PURE__*/_interopDefaultLegacy(semver);
const ENTRY_PATTERN = /^((?:@[^/]+\/)?[^@/]+)@(.+)$/;
const NEW_HEADER = `${[
`# This file is generated by running "yarn install" inside your project.
`,
`# Manual changes might be lost - proceed with caution!
`
].join(``)}
`;
const LEGACY_REGEX = /^(#.*(\r?\n))*?#\s+yarn\s+lockfile\s+v1\r?\n/i;
const SPECIAL_OBJECT_KEYS = [
`__metadata`,
`version`,
`resolution`,
`dependencies`,
`peerDependencies`,
`dependenciesMeta`,
`peerDependenciesMeta`,
`binaries`
];
class Lockfile {
constructor(packages, data, legacy = false) {
this.packages = packages;
this.data = data;
this.legacy = legacy;
}
static async load(path) {
const lockfileContents = await fs__default["default"].readFile(path, "utf8");
return Lockfile.parse(lockfileContents);
}
static parse(content) {
var _a;
const legacy = LEGACY_REGEX.test(content);
let data;
try {
data = parsers.parseSyml(content);
} catch (err) {
throw new Error(`Failed yarn.lock parse, ${err}`);
}
const packages = /* @__PURE__ */ new Map();
for (const [key, value] of Object.entries(data)) {
if (SPECIAL_OBJECT_KEYS.includes(key))
continue;
const [, name, ranges] = (_a = ENTRY_PATTERN.exec(key)) != null ? _a : [];
if (!name) {
throw new Error(`Failed to parse yarn.lock entry '${key}'`);
}
let queries = packages.get(name);
if (!queries) {
queries = [];
packages.set(name, queries);
}
for (let range of ranges.split(/\s*,\s*/)) {
if (range.startsWith(`${name}@`)) {
range = range.slice(`${name}@`.length);
}
if (range.startsWith("npm:")) {
range = range.slice("npm:".length);
}
queries.push({ range, version: value.version, dataKey: key });
}
}
return new Lockfile(packages, data, legacy);
}
/** Get the entries for a single package in the lockfile */
get(name) {
return this.packages.get(name);
}
/** Returns the name of all packages available in the lockfile */
keys() {
return this.packages.keys();
}
/** Analyzes the lockfile to identify possible actions and warnings for the entries */
analyze(options) {
var _a;
const { filter, localPackages } = options;
const result = {
invalidRanges: [],
newVersions: [],
newRanges: []
};
for (const [name, allEntries] of this.packages) {
if (filter && !filter(name)) {
continue;
}
const invalid = allEntries.filter(
(e) => !semver__default["default"].validRange(e.range) && !e.range.startsWith("workspace:")
);
result.invalidRanges.push(
...invalid.map(({ range }) => ({ name, range }))
);
const entries = allEntries.filter((e) => semver__default["default"].validRange(e.range));
if (entries.length < 2) {
continue;
}
const versions = Array.from(new Set(entries.map((e) => e.version))).map((v) => {
if (v === "0.0.0-use.local") {
const local = localPackages.get(name);
if (!local) {
throw new Error(`No local package found for ${name}`);
}
if (!local.packageJson.version) {
throw new Error(`No version found for local package ${name}`);
}
return {
entryVersion: v,
actualVersion: local.packageJson.version
};
}
return { entryVersion: v, actualVersion: v };
}).sort((v1, v2) => semver__default["default"].rcompare(v1.actualVersion, v2.actualVersion));
if (versions.length < 2) {
continue;
}
const acceptedVersions = /* @__PURE__ */ new Set();
for (const { version, range } of entries) {
const acceptedVersion = versions.find(
(v) => semver__default["default"].satisfies(v.actualVersion, range)
);
if (!acceptedVersion) {
throw new Error(
`No existing version was accepted for range ${range}, searching through ${versions}, for package ${name}`
);
}
if (acceptedVersion.entryVersion !== version) {
result.newVersions.push({
name,
range,
newVersion: acceptedVersion.entryVersion,
oldVersion: version
});
}
acceptedVersions.add(acceptedVersion.actualVersion);
}
if (acceptedVersions.size === 1) {
continue;
}
const maxVersion = Array.from(acceptedVersions).sort(semver__default["default"].rcompare)[0];
const maxEntry = (_a = entries.filter((e) => semver__default["default"].satisfies(maxVersion, e.range)).map((e) => ({ e, min: semver__default["default"].minVersion(e.range) })).filter((p) => p.min).sort((a, b) => semver__default["default"].rcompare(a.min, b.min))[0]) == null ? void 0 : _a.e;
if (!maxEntry) {
throw new Error(
`No entry found that satisfies max version '${maxVersion}'`
);
}
for (const { version, range } of entries) {
if (semver__default["default"].satisfies(maxVersion, range)) {
continue;
}
result.newRanges.push({
name,
oldRange: range,
newRange: maxEntry.range,
oldVersion: version,
newVersion: maxVersion
});
}
}
return result;
}
remove(name, range) {
var _a;
const query = `${name}@${range}`;
const existed = Boolean(this.data[query]);
delete this.data[query];
const newEntries = (_a = this.packages.get(name)) == null ? void 0 : _a.filter((e) => e.range !== range);
if (newEntries) {
this.packages.set(name, newEntries);
}
return existed;
}
/** Modifies the lockfile by bumping packages to the suggested versions */
replaceVersions(results) {
var _a;
for (const { name, range, oldVersion, newVersion } of results) {
const query = `${name}@${range}`;
const entryData = this.data[query];
if (!entryData) {
throw new Error(`No entry data for ${query}`);
}
if (entryData.version !== oldVersion) {
throw new Error(
`Expected existing version data for ${query} to be ${oldVersion}, was ${entryData.version}`
);
}
const matchingEntry = Object.entries(this.data).find(
([q, e]) => q.startsWith(`${name}@`) && e.version === newVersion
);
if (!matchingEntry) {
throw new Error(
`No matching entry found for ${name} at version ${newVersion}`
);
}
this.data[query] = matchingEntry[1];
const entry = (_a = this.packages.get(name)) == null ? void 0 : _a.find((e) => e.range === range);
if (!entry) {
throw new Error(`No entry data for ${query}`);
}
if (entry.version !== oldVersion) {
throw new Error(
`Expected existing version data for ${query} to be ${oldVersion}, was ${entryData.version}`
);
}
entry.version = newVersion;
}
}
createSimplifiedDependencyGraph() {
const graph = /* @__PURE__ */ new Map();
for (const [name, entries] of this.packages) {
const dependencies = new Set(
entries.flatMap((e) => {
var _a, _b;
const data = this.data[e.dataKey];
return [
...Object.keys((_a = data == null ? void 0 : data.dependencies) != null ? _a : {}),
...Object.keys((_b = data == null ? void 0 : data.peerDependencies) != null ? _b : {})
];
})
);
graph.set(name, dependencies);
}
return graph;
}
/**
* Diff with another lockfile, returning entries that have been
* added, changed, and removed compared to the other lockfile.
*/
diff(otherLockfile) {
var _a;
const diff = {
added: new Array(),
changed: new Array(),
removed: new Array()
};
const remainingOldNames = new Set(this.packages.keys());
for (const [name, otherQueries] of otherLockfile.packages) {
remainingOldNames.delete(name);
const thisQueries = this.packages.get(name);
if (!thisQueries) {
diff.removed.push(...otherQueries.map((q) => ({ name, range: q.range })));
continue;
}
const remainingOldRanges = new Set(thisQueries.map((q) => q.range));
for (const otherQuery of otherQueries) {
remainingOldRanges.delete(otherQuery.range);
const thisQuery = thisQueries.find((q) => q.range === otherQuery.range);
if (!thisQuery) {
diff.removed.push({ name, range: otherQuery.range });
continue;
}
const otherPkg = otherLockfile.data[otherQuery.dataKey];
const thisPkg = this.data[thisQuery.dataKey];
if (otherPkg && thisPkg) {
const thisCheck = thisPkg.integrity || thisPkg.checksum;
const otherCheck = otherPkg.integrity || otherPkg.checksum;
if (thisCheck !== otherCheck) {
diff.changed.push({ name, range: otherQuery.range });
}
}
}
for (const thisRange of remainingOldRanges) {
diff.added.push({ name, range: thisRange });
}
}
for (const name of remainingOldNames) {
const queries = (_a = this.packages.get(name)) != null ? _a : [];
diff.added.push(...queries.map((q) => ({ name, range: q.range })));
}
return diff;
}
async save(path) {
await fs__default["default"].writeFile(path, this.toString(), "utf8");
}
toString() {
return this.legacy ? lockfile.stringify(this.data) : NEW_HEADER + parsers.stringifySyml(this.data);
}
}
exports.Lockfile = Lockfile;
//# sourceMappingURL=Lockfile-e5943b84.cjs.js.map
;