@baseplate-dev/sync
Version:
Library for syncing Baseplate descriptions
174 lines • 10.5 kB
JavaScript
import { enhanceErrorWithContext, mapGroupBy } from '@baseplate-dev/utils';
import { keyBy, mapValues } from 'es-toolkit';
import { sortTaskPhases } from '#src/phases/sort-task-phases.js';
import { findDuplicates } from '#src/utils/find-duplicates.js';
import { safeMergeMap } from '#src/utils/merge.js';
import { GeneratorTaskOutputBuilder } from '../output/index.js';
import { buildGeneratorIdToScopesMap, resolveTaskDependenciesForPhase, } from './dependency-map.js';
import { getSortedRunSteps } from './dependency-sort.js';
import { runInRunnerContext } from './runner-context.js';
import { flattenGeneratorTaskEntriesAndPhases } from './utils.js';
/**
* Execute a generator entry
*/
export async function executeGeneratorEntry(rootEntry, { templateMetadataOptions } = {}) {
const { taskEntries, phases } = flattenGeneratorTaskEntriesAndPhases(rootEntry);
const sortedPhases = sortTaskPhases(phases);
const taskEntriesById = keyBy(taskEntries, (item) => item.id);
// build a map of generators to their scoped exports
const generatorIdToScopesMap = buildGeneratorIdToScopesMap(rootEntry);
const taskInstanceById = {}; // map of entry ID to initialized generator
const providerMapById = {}; // map of entry ID to map of provider name to Provider
const generatorOutputs = [];
const generatorTaskMetadatas = [];
// map of phases to generator ID to task entries that are dynamically added
const dynamicTaskEntriesByPhase = new Map();
for (const phase of [undefined, ...sortedPhases]) {
const currentDynamicTaskEntries = dynamicTaskEntriesByPhase.get(phase?.name ?? '');
const dependencyMap = resolveTaskDependenciesForPhase(rootEntry, generatorIdToScopesMap, phase, currentDynamicTaskEntries);
const filteredTaskEntries = taskEntries.filter((taskEntry) => taskEntry.task.phase === phase);
const { steps: sortedRunSteps, metadata } = getSortedRunSteps([
...filteredTaskEntries,
...(currentDynamicTaskEntries
? [...currentDynamicTaskEntries.values()].flat()
: []),
], dependencyMap);
generatorTaskMetadatas.push(metadata);
for (const runStep of sortedRunSteps) {
const [action, taskId] = runStep.split('|');
try {
const { task, generatorId } = taskEntriesById[taskId];
const { dependencies = {}, exports = {}, outputs = {} } = task;
if (action === 'init') {
// run through init step
const resolvedDependencies = mapValues(dependencies, (dependency, key) => {
if (!dependency) {
return;
}
const dependencyId = dependencyMap[taskId][key]?.id;
const provider = dependencyId === undefined
? undefined
: providerMapById[dependencyId][dependency.name];
const { isReadOnly } = dependency;
const { optional } = dependency.type === 'dependency'
? dependency.options
: { optional: false };
// check dependency comes from a previous phase
if (phase !== undefined && dependencyId) {
const dependencyTask = taskEntriesById[dependencyId];
if (dependencyTask.task.phase !== phase) {
if (!isReadOnly) {
throw new Error(`Dependency ${key} in ${taskId} cannot come from a previous phase since it is not read-only`);
}
if (dependencyTask.task.phase &&
!phase.options.consumesOutputFrom?.includes(dependencyTask.task.phase)) {
throw new Error(`Dependency ${key} in ${taskId} cannot come from phase ${dependencyTask.task.phase.name} unless it is explicitly defined in consumesOutputFrom`);
}
}
}
if (!provider && !optional) {
throw new Error(`Could not resolve required dependency ${key} in ${taskId}`);
}
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
return provider; // cheat Type system to prevent null from appearing
});
const taskInstance = runInRunnerContext({ taskId }, () => task.run(resolvedDependencies, {
taskId,
})) ?? { providers: {} };
taskInstanceById[taskId] = taskInstance;
const { providers } = taskInstance;
if (!providers && Object.keys(exports).length > 0) {
throw new Error(`Task ${taskId} does not have providers despite having exports`);
}
if (providers) {
const missingProvider = Object.keys(exports).find((key) => !(key in providers));
if (missingProvider) {
throw new Error(`Task ${taskId} did not output provider ${missingProvider}`);
}
providerMapById[taskId] = Object.fromEntries(Object.entries(exports).map(([key, value]) => [
value.name,
providers[key],
]));
}
}
else if (action === 'build') {
// run through build step
const entry = taskEntriesById[taskId];
const generator = taskInstanceById[taskId];
const outputBuilder = new GeneratorTaskOutputBuilder({
generatorInfo: entry.generatorInfo,
generatorId: entry.generatorId,
templateMetadataOptions,
});
if (generator.build) {
const outputResult = (await Promise.resolve(runInRunnerContext({ taskId }, () => generator.build?.(outputBuilder)))) ?? {};
const outputKeys = Object.keys(outputs);
if (outputKeys.length > 0) {
const missingProvider = Object.keys(outputs).find((key) => !(key in outputResult));
if (missingProvider) {
throw new Error(`Task ${taskId} did not export provider ${missingProvider}`);
}
providerMapById[taskId] = {
...providerMapById[taskId],
...Object.fromEntries(Object.entries(outputs).map(([key, value]) => [
value.name,
outputResult[key],
])),
};
}
}
// validate dynamic tasks fulfill the requirements
const { dynamicTasks } = outputBuilder;
for (const dynamicTask of dynamicTasks) {
if (dynamicTask.id in taskEntriesById) {
throw new Error(`Cannot add dynamic task with the same name as a static task: ${dynamicTask.id}`);
}
if (!dynamicTask.task.phase ||
(phase &&
!phase.options.addsDynamicTasksTo?.includes(dynamicTask.task.phase))) {
throw new Error(`Dynamic task ${dynamicTask.id} must have an explicit phase and be added to the addsDynamicTasksTo option of the phase ${phase?.name}`);
}
if (!sortedPhases.includes(dynamicTask.task.phase)) {
throw new Error(`Could not find phase ${dynamicTask.task.phase.name} in the task phases. Make sure it is registered ahead of time.`);
}
// register it in the ID map
taskEntriesById[dynamicTask.id] = dynamicTask;
}
// group dynamic tasks by phase and add them to dynamicTaskEntriesByPhase
const dynamicTasksByPhase = mapGroupBy(dynamicTasks, (task) => task.task.phase?.name ?? '');
for (const [phaseName, dynamicTasksOfPhase,] of dynamicTasksByPhase.entries()) {
const dynamicTaskEntries = dynamicTaskEntriesByPhase.get(phaseName) ??
new Map();
dynamicTaskEntries.set(generatorId, dynamicTasksOfPhase);
dynamicTaskEntriesByPhase.set(phaseName, dynamicTaskEntries);
}
generatorOutputs.push(outputBuilder.output);
}
else {
throw new Error(`Unknown action ${action}`);
}
}
catch (error) {
const { generatorInfo } = taskEntriesById[taskId];
throw enhanceErrorWithContext(error, `Error in the ${action} step of the ${generatorInfo.name} generator task`);
}
}
}
const buildOutput = {
files: safeMergeMap(...generatorOutputs.map((output) => output.files)),
postWriteCommands: generatorOutputs.flatMap((output) => output.postWriteCommands),
globalFormatters: generatorOutputs.flatMap((output) => output.globalFormatters),
metadata: {
generatorProviderRelationships: generatorTaskMetadatas.flatMap((metadata) => metadata.generatorProviderRelationships),
generatorTaskEntries: generatorTaskMetadatas.flatMap((metadata) => metadata.generatorTaskEntries),
},
};
// verify no file IDs are duplicated
const fileIds = Array.from(buildOutput.files.values(), (file) => file.id);
const duplicateFileIds = findDuplicates(fileIds);
if (duplicateFileIds.length > 0) {
throw new Error(`Duplicate file IDs found: ${duplicateFileIds.join(', ')}`);
}
return buildOutput;
}
//# sourceMappingURL=generator-runner.js.map