@enspirit/emb
Version:
A replacement for our Makefile-for-monorepos
215 lines (214 loc) • 7.47 kB
JavaScript
import { getContext, TaskManagerFactory } from '../index.js';
import jsonpatch from 'fast-json-patch';
import { join } from 'node:path';
import { TemplateExpander } from '../utils/index.js';
import { Component } from './component.js';
import { MonorepoConfig } from './config.js';
import { getPlugin } from './plugins/index.js';
import { EMBStore } from './store/index.js';
export class Monorepo {
_rootDir;
defaultFlavor;
_config;
_store;
_managerFactory = new TaskManagerFactory();
initialized = false;
constructor(config, _rootDir, defaultFlavor = 'default') {
this._rootDir = _rootDir;
this.defaultFlavor = defaultFlavor;
this._config = new MonorepoConfig(config);
}
get config() {
return this._config.toJSON();
}
get defaults() {
return this._config.defaults;
}
get flavors() {
return this._config.flavors;
}
get name() {
return this._config.project.name;
}
get rootDir() {
return this._config.project.rootDir
? join(this._rootDir, this._config.project.rootDir)
: this._rootDir;
}
get currentFlavor() {
return this.defaultFlavor;
}
get store() {
return this._store;
}
get components() {
return Object.entries(this._config.components).map(([name, c]) => new Component(name, c, this));
}
component(name) {
return new Component(name, this._config.component(name), this);
}
get tasks() {
const globalTasks = Object.values(this._config.tasks);
return this.components.reduce((tasks, cmp) => {
const cmpTasks = Object.entries(cmp.tasks || {}).map(([name, task]) => {
return {
...task,
name,
component: cmp.name,
id: `${cmp.name}:${name}`,
};
});
return [...tasks, ...cmpTasks];
}, globalTasks);
}
task(nameOrId) {
const byId = this.tasks.find((t) => t.id === nameOrId);
if (byId) {
return byId;
}
const found = this.tasks.filter((t) => t.name === nameOrId);
if (found.length > 1) {
throw new Error(`Task name ambigous, found multiple matches: ${nameOrId}`);
}
if (found.length === 0) {
throw new Error(`Task not found: ${nameOrId}`);
}
return found[0];
}
get resources() {
return this.components.reduce((resources, cmp) => {
return [...resources, ...Object.values(cmp.resources)];
}, []);
}
resource(nameOrId) {
const byId = this.resources.find((t) => t.id === nameOrId);
if (byId) {
return byId;
}
const found = this.resources.filter((t) => t.name === nameOrId);
if (found.length > 1) {
throw new Error(`Resource name ambigous, found multiple matches: ${nameOrId}`);
}
if (found.length === 0) {
throw new Error(`Resource not found: ${nameOrId}`);
}
return found[0];
}
get vars() {
return this._config.vars;
}
// Helper to get a listr2 task manager
taskManager() {
return this._managerFactory.factor();
}
setTaskRenderer(renderer) {
this._managerFactory.setRenderer(renderer);
}
// Helper to expand a record of strings
async expand(toExpand, vars, expander = new TemplateExpander()) {
const secrets = getContext()?.secrets;
const sources = {
env: process.env,
vars: vars || this.vars,
};
// Add all registered secret providers as sources
if (secrets) {
for (const providerName of secrets.getProviderNames()) {
sources[providerName] = secrets.createSource(providerName);
}
}
const options = {
default: 'vars',
sources,
};
return expander.expandRecord(toExpand, options);
}
async installStore(store) {
this._store = store || new EMBStore(this);
await this._store.init();
}
async installEnv() {
// Expand env vars at the init and then we don't expand anymore
// The only available source for them is the existing env
const expander = new TemplateExpander();
const options = {
default: 'env',
sources: {
env: process.env,
},
};
const expanded = await expander.expandRecord(this._config.env, options);
Object.assign(process.env, expanded);
}
// Initialize
async init() {
if (this.initialized) {
throw new Error('Monorepo already initialized');
}
await this.installStore();
const plugins = this._config.plugins.map((p) => {
const PluginClass = getPlugin(p.name);
return new PluginClass(p.config, this);
});
this._config = await plugins.reduce(async (pConfig, plugin) => {
const newConfig = await plugin.extendConfig?.(await pConfig);
return newConfig ?? pConfig;
}, Promise.resolve(this._config));
await this.installEnv();
this.initialized = true;
await Promise.all(plugins.map(async (p) => {
await p.init?.();
}));
return this;
}
// Helper to build relative path to the root dir
join(...paths) {
return join(this.rootDir, ...paths);
}
run(operation, input = undefined) {
if (input === undefined) {
return operation.run();
}
return operation.run(input);
}
async expandPatches(patches) {
const expanded = Promise.all(patches.map(async (patch) => {
if (!('value' in patch)) {
return patch;
}
return {
...patch,
value: await this.expand(patch.value),
};
}));
return expanded;
}
async withFlavor(flavorName) {
const patches = await this.expandPatches(this._config.flavor(flavorName).patches || []);
const original = this._config.toJSON();
const errors = jsonpatch.validate(patches || [], original);
if (errors) {
throw errors;
}
const withComponentPatches = await this.components.reduce(async (pConfig, cmp) => {
const config = await pConfig;
const componentPatches = await this.expandPatches(cmp.flavor(flavorName, false)?.patches || []);
const errors = jsonpatch.validate(componentPatches || [], config.components[cmp.name]);
if (errors) {
throw errors;
}
config.components[cmp.name] = componentPatches.reduce((doc, patch, index) => {
return jsonpatch.applyReducer(doc, patch, index);
}, config.components[cmp.name]);
return config;
}, Promise.resolve(original));
const withGlobalPatches = patches.reduce((doc, patch, index) => {
return jsonpatch.applyReducer(doc, patch, index);
}, withComponentPatches);
const newConfig = new MonorepoConfig(withGlobalPatches);
const repo = new Monorepo(newConfig, this._rootDir, flavorName);
await repo.installStore();
await repo.installEnv();
return repo;
}
}