semantic-release-npm-workspaces-monorepo
Version:
Help you use semantic-release with npm workspaces
85 lines (84 loc) • 3.15 kB
JavaScript
import fs from 'fs/promises';
import { SETTINGS } from '../settings.js';
import { glob } from 'glob';
export default class ScanPublishOrder {
get packageOrder() {
return [...this._packageOrder];
}
get packagesOrderPath() {
const entries = Object.entries(this._packageContentMap);
return this._packageOrder.map(packageName => {
const packageFound = entries.find(([, packageContent]) => packageContent.name === packageName);
return packageFound[0];
});
}
constructor(_scanLocations = SETTINGS.workspaces) {
this._scanLocations = _scanLocations;
this._packageContentMap = {};
this._packageOrder = [];
}
async _readAllPackages() {
const locations = this._scanLocations.map(location => {
if (location.at(-1) !== '/') {
location += '/';
}
location += 'package.json';
return location;
});
const packages = await glob(locations, {
ignore: '**/node_modules/**',
nodir: true,
stat: true,
withFileTypes: true,
});
for (const packageJsonState of packages) {
if (!packageJsonState.isFile())
continue;
const packagePath = packageJsonState.parent.fullpath();
try {
this._packageContentMap[packagePath] = await fs
.readFile(packageJsonState.fullpath(), 'utf-8')
.then(JSON.parse);
}
catch (error) {
console.warn(`Skipping package at ${packagePath}: ${error.message}`);
}
}
}
async _orderByDependencies() {
let somethingChanged = true;
while (somethingChanged) {
somethingChanged = false;
for (const content of Object.values(this._packageContentMap)) {
if (this._packageOrder.includes(content.name)) {
continue;
}
if (this._checkOkToBeNextInOrder(content.dependencies)) {
this._packageOrder.push(content.name);
somethingChanged = true;
}
}
}
}
_checkOkToBeNextInOrder(dependencies = {}) {
const monorepoIncludesDeps = Object.values(this._packageContentMap).map(x => x.name);
for (const [key] of Object.entries(dependencies)) {
if (monorepoIncludesDeps.includes(key) &&
!this._packageOrder.includes(key)) {
return false;
}
}
return true;
}
_assertMissingPackages() {
const missingPackages = Object.values(this._packageContentMap).filter(content => !this._packageOrder.includes(content.name));
if (missingPackages.length) {
throw new Error(`Missing packages: ${missingPackages.map(content => content.name).join(', ')}, probably circular dependencies`);
}
}
async scan() {
await this._readAllPackages();
await this._orderByDependencies();
this._assertMissingPackages();
}
}