@enspirit/emb
Version:
A replacement for our Makefile-for-monorepos
152 lines (151 loc) • 5.66 kB
JavaScript
import { PRESET_TIMER, } from 'listr2';
import * as z from 'zod';
import { EMBCollection, findRunOrder, ResourceFactory, } from '../../index.js';
import { AbstractOperation } from '../../../operations/index.js';
const schema = z.object({
resources: z
.array(z.string())
.describe('The list of resources to build')
.optional(),
dryRun: z
.boolean()
.optional()
.describe('Do not build but return the config that would be used to build the resources'),
silent: z
.boolean()
.optional()
.describe('Do not produce any output on the terminal'),
force: z
.boolean()
.optional()
.describe('Bypass the cache and force the build'),
});
export class BuildResourcesOperation extends AbstractOperation {
// keep track of what has been built
// to ensure depedencies cannot ignore their turn
built = [];
constructor() {
super(schema);
}
async _run(input) {
const { monorepo } = this.context;
const manager = monorepo.taskManager();
const collection = new EMBCollection(monorepo.resources, {
idField: 'id',
depField: 'dependencies',
});
const ordered = findRunOrder(input.resources || [], collection);
const tasks = ordered.map((resource) => {
return {
task: async (context, task) => {
return this.buildResource(resource, task, context, {
dryRun: input.dryRun,
force: input.force,
});
},
title: `Building ${resource.id}`,
};
});
return manager.run([
{
title: 'Build resources',
async task(ctx, task) {
return task.newListr(tasks, {
rendererOptions: {
collapseSubtasks: false,
collapseSkips: true,
},
});
},
},
], {
silentRendererCondition() {
return Boolean(input.silent);
},
rendererOptions: {
collapseSkips: true,
collapseSubtasks: true,
timer: {
...PRESET_TIMER,
},
},
ctx: {},
});
}
async buildResource(resource, parentTask, parentContext, options) {
const list = parentTask.newListr([
{
title: 'Prepare build context',
task: async (ctx) => {
// Extend the context for this specific resource build chain
Object.assign(ctx, options, { resource });
const { monorepo } = this.context;
ctx.builder = await ResourceFactory.factor(resource.type, {
monorepo,
config: resource,
component: monorepo.component(resource.component),
});
},
},
// Actual build
{
title: `Checking cache for ${resource.id}`,
/** Skip the build if the builder knows it can be skipped */
task: async (ctx) => {
if (ctx.builder?.mustBuild) {
ctx.sentinelData = await ctx.builder.mustBuild(ctx.resource);
ctx.cacheHit = !ctx.sentinelData;
// If one of our dependency was built, we force the re-build
// despite the cache-hit
const found = ctx.resource.dependencies?.find((d) => Boolean(this.built.find((r) => r.id === d)));
ctx.force = ctx.force || Boolean(found);
}
},
},
{
rendererOptions: { persistentOutput: true },
title: `Build ${resource.id}`,
task: async (ctx, task) => {
const skip = (prefix) => {
parentTask.title = `${prefix} ${resource.id}`;
task.skip();
return parentTask.skip();
};
if (ctx.cacheHit && !ctx.force && !ctx.dryRun) {
return skip('[cache hit]');
}
const { input, operation } = await ctx.builder.build(ctx.resource, task.stdout());
ctx.builderInput = input;
this.built.push(ctx.resource);
if (ctx.dryRun) {
return skip('[dry run]');
}
const output = await operation.run(ctx.builderInput);
if (ctx.sentinelData) {
ctx.builder.commit?.(ctx.resource, output, ctx.sentinelData);
}
return output;
},
},
{
// Return build meta data and dump
// cache data into sentinel file
async task(ctx) {
if (ctx.builder) {
delete ctx.builder;
}
//
parentContext[resource.id] = ctx;
},
},
], {
ctx: {
...options,
},
rendererOptions: {
collapseSubtasks: true,
},
});
return list;
}
}