semantic-release-npm-workspaces-monorepo
Version:
Help you use semantic-release with npm workspaces
80 lines (79 loc) • 3.11 kB
JavaScript
import fs from 'fs/promises';
import * as path from 'node:path';
import { SETTINGS } from '../settings.js';
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(_scanLocation = SETTINGS.workspace) {
this._scanLocation = _scanLocation;
this._packageContentMap = {};
this._packageOrder = [];
}
async _readAllPackages() {
const packages = await fs.readdir(this._scanLocation, {
withFileTypes: true,
});
for (const packageState of packages) {
if (packageState.isFile())
continue;
const packagePath = path.join(this._scanLocation, packageState.name);
const packageJsonPath = path.join(packagePath, 'package.json');
try {
if (await fs.stat(packageJsonPath).then(stat => stat.isFile())) {
const packageContent = await fs
.readFile(packageJsonPath, 'utf-8')
.then(JSON.parse);
this._packageContentMap[packagePath] = packageContent;
}
}
catch (error) {
console.warn(`Skipping package at ${packagePath}: ${error.message}`);
continue;
}
}
}
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();
}
}