@teambit/builder 1.0.107 → 1.0.108

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/build-pipe.ts +216 -0
  2. package/build-pipeline-order.ts +192 -0
  3. package/build-pipeline-result-list.ts +97 -0
  4. package/build-task.ts +151 -0
  5. package/build.cmd.ts +157 -0
  6. package/builder-env-type.ts +16 -0
  7. package/builder.aspect.ts +5 -0
  8. package/builder.graphql.ts +185 -0
  9. package/builder.main.runtime.ts +493 -0
  10. package/builder.route.ts +95 -0
  11. package/dist/artifact/artifact-definition.d.ts +2 -2
  12. package/dist/artifact/artifact-extractor.d.ts +3 -3
  13. package/dist/artifact/artifact-factory.d.ts +1 -1
  14. package/dist/artifact/artifact-factory.js +1 -2
  15. package/dist/artifact/artifact-factory.js.map +1 -1
  16. package/dist/artifact/artifact-list.d.ts +1 -1
  17. package/dist/artifact/artifact.d.ts +1 -1
  18. package/dist/artifact/artifacts.cmd.d.ts +1 -1
  19. package/dist/build-pipe.d.ts +3 -3
  20. package/dist/build-pipe.js +5 -12
  21. package/dist/build-pipe.js.map +1 -1
  22. package/dist/build-pipeline-result-list.d.ts +2 -2
  23. package/dist/build-pipeline-result-list.js +1 -2
  24. package/dist/build-pipeline-result-list.js.map +1 -1
  25. package/dist/build-task.d.ts +1 -1
  26. package/dist/build.cmd.d.ts +1 -1
  27. package/dist/builder.composition.d.ts +2 -2
  28. package/dist/builder.graphql.d.ts +17 -17
  29. package/dist/builder.graphql.js +4 -8
  30. package/dist/builder.graphql.js.map +1 -1
  31. package/dist/builder.main.runtime.d.ts +5 -5
  32. package/dist/builder.main.runtime.js +11 -14
  33. package/dist/builder.main.runtime.js.map +1 -1
  34. package/dist/builder.route.d.ts +1 -1
  35. package/dist/builder.route.js +3 -3
  36. package/dist/builder.route.js.map +1 -1
  37. package/dist/builder.service.d.ts +9 -9
  38. package/dist/builder.service.js +4 -4
  39. package/dist/builder.service.js.map +1 -1
  40. package/dist/pipeline.d.ts +1 -1
  41. package/dist/{preview-1703590665075.js → preview-1703647408454.js} +2 -2
  42. package/dist/storage/storage-resolver.d.ts +2 -2
  43. package/dist/task-results-list.js +2 -8
  44. package/dist/task-results-list.js.map +1 -1
  45. package/dist/tasks-queue.d.ts +1 -1
  46. package/dist/types.d.ts +2 -2
  47. package/index.ts +20 -0
  48. package/package.json +26 -33
  49. package/pipeline.ts +104 -0
  50. package/task-results-list.ts +68 -0
  51. package/task.ts +50 -0
  52. package/tasks-queue.ts +40 -0
  53. package/tsconfig.json +16 -21
  54. package/types/asset.d.ts +15 -3
  55. package/types.ts +37 -0
package/build-pipe.ts ADDED
@@ -0,0 +1,216 @@
1
+ import { EnvDefinition } from '@teambit/envs';
2
+ import { ComponentMap, ComponentID } from '@teambit/component';
3
+ import { Logger, LongProcessLogger } from '@teambit/logger';
4
+ import mapSeries from 'p-map-series';
5
+ import prettyTime from 'pretty-time';
6
+ import { capitalize } from '@teambit/toolbox.string.capitalize';
7
+ import chalk from 'chalk';
8
+ import { ArtifactFactory, ArtifactList, FsArtifact } from './artifact';
9
+ import { BuildContext, BuildTask, BuildTaskHelper, BuiltTaskResult } from './build-task';
10
+ import { ComponentResult } from './types';
11
+ import { TasksQueue } from './tasks-queue';
12
+ import { EnvsBuildContext } from './builder.service';
13
+ import { TaskResultsList } from './task-results-list';
14
+
15
+ export type TaskResults = {
16
+ /**
17
+ * task itself. useful for getting its id/description later on.
18
+ */
19
+ task: BuildTask;
20
+
21
+ /**
22
+ * environment were the task was running
23
+ */
24
+ env: EnvDefinition;
25
+
26
+ /**
27
+ * component build results.
28
+ */
29
+ componentsResults: ComponentResult[];
30
+
31
+ /**
32
+ * artifacts generated by the build pipeline.
33
+ * in case the task finished with errors, this prop is undefined.
34
+ */
35
+ artifacts: ComponentMap<ArtifactList<FsArtifact>> | undefined;
36
+
37
+ /**
38
+ * timestamp of start initiation.
39
+ */
40
+ startTime: number;
41
+
42
+ /**
43
+ * timestamp of task completion.
44
+ */
45
+ endTime: number;
46
+ };
47
+
48
+ type PipeOptions = {
49
+ exitOnFirstFailedTask?: boolean; // by default it skips only when a dependent failed.
50
+ showEnvNameInOutput?: boolean;
51
+ showEnvVersionInOutput?: boolean; // in case it shows the env-name, whether should show also the version
52
+ };
53
+
54
+ export class BuildPipe {
55
+ private failedTasks: BuildTask[] = [];
56
+ private failedDependencyTask: BuildTask | undefined;
57
+ private longProcessLogger: LongProcessLogger;
58
+ private taskResults: TaskResults[] = [];
59
+ constructor(
60
+ /**
61
+ * array of services to apply on the components.
62
+ */
63
+ readonly tasksQueue: TasksQueue,
64
+ readonly envsBuildContext: EnvsBuildContext,
65
+ readonly logger: Logger,
66
+ readonly artifactFactory: ArtifactFactory,
67
+ private previousTaskResults?: TaskResults[],
68
+ private options?: PipeOptions
69
+ ) {}
70
+
71
+ get allTasksResults(): TaskResults[] {
72
+ return [...(this.previousTaskResults || []), ...(this.taskResults || [])];
73
+ }
74
+
75
+ /**
76
+ * execute a pipeline of build tasks.
77
+ */
78
+ async execute(): Promise<TaskResultsList> {
79
+ this.addSignalListener();
80
+ await this.executePreBuild();
81
+ this.longProcessLogger = this.logger.createLongProcessLogger('running tasks', this.tasksQueue.length);
82
+ await mapSeries(this.tasksQueue, async ({ task, env }) => this.executeTask(task, env));
83
+ this.longProcessLogger.end();
84
+ const capsuleRootDir = Object.values(this.envsBuildContext)[0]?.capsuleNetwork.capsulesRootDir;
85
+ const tasksResultsList = new TaskResultsList(this.tasksQueue, this.taskResults, capsuleRootDir);
86
+ await this.executePostBuild(tasksResultsList);
87
+
88
+ return tasksResultsList;
89
+ }
90
+
91
+ /**
92
+ * for some reason, some tasks (such as typescript compilation) ignore ctrl+C. this fixes it.
93
+ */
94
+ private addSignalListener() {
95
+ process.on('SIGTERM', () => {
96
+ process.exit();
97
+ });
98
+
99
+ process.on('SIGINT', () => {
100
+ process.exit();
101
+ });
102
+ }
103
+
104
+ private async executePreBuild() {
105
+ this.logger.setStatusLine('executing pre-build for all tasks');
106
+ const longProcessLogger = this.logger.createLongProcessLogger('running pre-build for all tasks');
107
+ await mapSeries(this.tasksQueue, async ({ task, env }) => {
108
+ if (!task.preBuild) return;
109
+ await task.preBuild(this.getBuildContext(env.id));
110
+ });
111
+ longProcessLogger.end();
112
+ }
113
+
114
+ private async executeTask(task: BuildTask, env: EnvDefinition): Promise<void> {
115
+ const taskId = BuildTaskHelper.serializeId(task);
116
+ const envName = this.options?.showEnvNameInOutput ? `(${this.getPrettyEnvName(env.id)}) ` : '';
117
+ const taskLogPrefix = `${envName}[${this.getPrettyAspectName(task.aspectId)}: ${task.name}]`;
118
+ this.longProcessLogger.logProgress(`${taskLogPrefix}${task.description ? ` ${task.description}` : ''}`, false);
119
+ this.updateFailedDependencyTask(task);
120
+ if (this.shouldSkipTask(taskId, env.id)) {
121
+ return;
122
+ }
123
+ const startTask = process.hrtime();
124
+ const taskStartTime = Date.now();
125
+ const buildContext = this.getBuildContext(env.id);
126
+ let buildTaskResult: BuiltTaskResult;
127
+ try {
128
+ buildTaskResult = await task.execute(buildContext);
129
+ } catch (err) {
130
+ this.logger.consoleFailure(`env: ${env.id}, task "${taskId}" threw an error`);
131
+ throw err;
132
+ }
133
+
134
+ const endTime = Date.now();
135
+ const compsWithErrors = buildTaskResult.componentsResults.filter((c) => c.errors?.length);
136
+ let artifacts: ComponentMap<ArtifactList<FsArtifact>> | undefined;
137
+ const duration = prettyTime(process.hrtime(startTask));
138
+ if (compsWithErrors.length) {
139
+ this.logger.consoleFailure(`env: ${env.id}, task "${taskId}" has failed`);
140
+ this.logger.consoleFailure(
141
+ chalk.red(`${this.longProcessLogger.getProgress()} env: ${env.id}, task "${taskId}" has failed in ${duration}`)
142
+ );
143
+ this.failedTasks.push(task);
144
+ } else {
145
+ this.logger.consoleSuccess(
146
+ chalk.green(`${this.longProcessLogger.getProgress()} ${taskLogPrefix} Completed successfully in ${duration}`)
147
+ );
148
+ const defs = buildTaskResult.artifacts || [];
149
+ artifacts = this.artifactFactory.generate(buildContext, defs, task);
150
+ }
151
+
152
+ const taskResults: TaskResults = {
153
+ task,
154
+ env,
155
+ componentsResults: buildTaskResult.componentsResults,
156
+ artifacts,
157
+ startTime: taskStartTime,
158
+ endTime,
159
+ };
160
+
161
+ this.taskResults.push(taskResults);
162
+ }
163
+
164
+ private getPrettyAspectName(aspectId: string): string {
165
+ const resolvedId = ComponentID.fromString(aspectId);
166
+ const tokens = resolvedId.name.split('-').map((token) => capitalize(token));
167
+ return tokens.join(' ');
168
+ }
169
+
170
+ private getPrettyEnvName(envId: string) {
171
+ const resolvedId = ComponentID.fromString(envId);
172
+ const ver = this.options?.showEnvVersionInOutput ? `@${resolvedId.version}` : '';
173
+ return `${resolvedId.fullName}${ver}`;
174
+ }
175
+
176
+ private async executePostBuild(tasksResults: TaskResultsList) {
177
+ const longProcessLogger = this.logger.createLongProcessLogger('running post-build for all tasks');
178
+ this.logger.setStatusLine('executing post-build for all tasks');
179
+ await mapSeries(this.tasksQueue, async ({ task, env }) => {
180
+ if (!task.postBuild) return;
181
+ await task.postBuild(this.getBuildContext(env.id), tasksResults);
182
+ });
183
+ longProcessLogger.end();
184
+ }
185
+
186
+ private updateFailedDependencyTask(task: BuildTask) {
187
+ if (!this.failedDependencyTask && this.failedTasks.length && task.dependencies) {
188
+ task.dependencies.forEach((dependency) => {
189
+ const { aspectId, name } = BuildTaskHelper.deserializeIdAllowEmptyName(dependency);
190
+ this.failedDependencyTask = this.failedTasks.find((failedTask) => {
191
+ if (name && name !== failedTask.name) return false;
192
+ return aspectId === failedTask.aspectId;
193
+ });
194
+ });
195
+ }
196
+ }
197
+
198
+ private shouldSkipTask(taskId: string, envId: string): boolean {
199
+ if (this.options?.exitOnFirstFailedTask && this.failedTasks.length) {
200
+ const failedTaskId = BuildTaskHelper.serializeId(this.failedTasks[0]);
201
+ this.logger.consoleWarning(`env: ${envId}, task "${taskId}" has skipped due to "${failedTaskId}" failure`);
202
+ return true;
203
+ }
204
+ if (!this.failedDependencyTask) return false;
205
+ const failedTaskId = BuildTaskHelper.serializeId(this.failedDependencyTask);
206
+ this.logger.consoleWarning(`env: ${envId}, task "${taskId}" has skipped due to "${failedTaskId}" failure`);
207
+ return true;
208
+ }
209
+
210
+ private getBuildContext(envId: string): BuildContext {
211
+ const buildContext = this.envsBuildContext[envId];
212
+ if (!buildContext) throw new Error(`unable to find buildContext for ${envId}`);
213
+ buildContext.previousTasksResults = this.allTasksResults;
214
+ return buildContext;
215
+ }
216
+ }
@@ -0,0 +1,192 @@
1
+ import { Graph, Node, Edge } from '@teambit/graph.cleargraph';
2
+ import TesterAspect from '@teambit/tester';
3
+ import { EnvDefinition, Environment } from '@teambit/envs';
4
+ import { BuildTask, BuildTaskHelper } from './build-task';
5
+ import type { TaskSlot } from './builder.main.runtime';
6
+ import { TasksQueue } from './tasks-queue';
7
+ import { PipeFunctionNames } from './builder.service';
8
+
9
+ type TaskDependenciesGraph = Graph<string, string>;
10
+ type Location = 'start' | 'middle' | 'end';
11
+ type TasksLocationGraph = { location: Location; graph: TaskDependenciesGraph };
12
+ type PipelineEnv = { env: EnvDefinition; pipeline: BuildTask[] };
13
+ type DataPerLocation = { location: Location; graph: TaskDependenciesGraph; pipelineEnvs: PipelineEnv[] };
14
+
15
+ /**
16
+ * there are two ways how to add tasks to build pipeline.
17
+ * 1. `getBuildPipe()` method of the env.
18
+ * 2. registering to the `builder.registerBuildTask()`.
19
+ *
20
+ * in the option #1, it's possible to determine the order. e.g. `getBuildPipe() { return [taskA, taskB, taskC]; }`
21
+ * in the option #2, the register happens once the extension is loaded, so there is no way to put
22
+ * one task before/after another task.
23
+ *
24
+ * To be able to determine the order, you can do the following
25
+ * 1. "task.location", it has two options "start" and "end". the rest are "middle".
26
+ * 2. "task.dependencies", the dependencies must be completed for all envs before this task starts.
27
+ * the dependencies are applicable inside a location and not across locations. see getLocation()
28
+ * or/and continue reading for more info about this.
29
+ *
30
+ * to determine the final order of the tasks, the following is done:
31
+ * 1. split all tasks to three groups: start, middle and end.
32
+ * 2. for each group define a dependencies graph for the tasks with "dependencies" prop and the pipeline.
33
+ * 3. start with the first group "start", toposort the dependencies graph and push the found tasks
34
+ * to a queue. once completed, iterate the pipeline and add all tasks to the queue.
35
+ * 4. do the same for the "middle" and "end" groups.
36
+ *
37
+ * the reason for splitting the tasks to the three groups and not using the "dependencies" field
38
+ * alone to determine the order is that the "start" and "end" groups are mostly core and "middle"
39
+ * is mostly the user entering tasks to the pipeline and we as the core don't know about the users
40
+ * tasks. For example, a core task "PublishComponent" must happen after the compiler, however, a
41
+ * user might have an env without a compiler. if we determine the order only by the dependencies
42
+ * field, the "PublishComponent" would have a dependency "compiler" and because in this case there
43
+ * is no compiler task, it would throw an error about missing dependencies.
44
+ */
45
+ export function calculatePipelineOrder(
46
+ taskSlot: TaskSlot,
47
+ envs: EnvDefinition[],
48
+ pipeNameOnEnv: PipeFunctionNames,
49
+ tasks: string[] = [],
50
+ skipTests = false
51
+ ): TasksQueue {
52
+ const graphs: TasksLocationGraph[] = [];
53
+ const locations: Location[] = ['start', 'middle', 'end']; // the order is important here!
54
+ locations.forEach((location) => {
55
+ graphs.push({ location, graph: new Graph<string, string>() });
56
+ });
57
+ const pipelineEnvs: PipelineEnv[] = [];
58
+ envs.forEach((envDefinition) => {
59
+ const pipeline = getPipelineForEnv(taskSlot, envDefinition.env, pipeNameOnEnv);
60
+ pipelineEnvs.push({ env: envDefinition, pipeline });
61
+ });
62
+
63
+ const flattenedPipeline: BuildTask[] = pipelineEnvs.map((pipelineEnv) => pipelineEnv.pipeline).flat();
64
+ flattenedPipeline.forEach((task) => addDependenciesToGraph(graphs, flattenedPipeline, task));
65
+
66
+ const dataPerLocation: DataPerLocation[] = graphs.map(({ location, graph }) => {
67
+ const pipelineEnvsPerLocation: PipelineEnv[] = pipelineEnvs.map(({ env, pipeline }) => {
68
+ return { env, pipeline: pipeline.filter((task) => (task.location || 'middle') === location) };
69
+ });
70
+ return { location, graph, pipelineEnvs: pipelineEnvsPerLocation };
71
+ });
72
+
73
+ const tasksQueue = new TasksQueue();
74
+ locations.forEach((location) => addTasksToGraph(tasksQueue, dataPerLocation, location));
75
+ if (tasks.length) {
76
+ return new TasksQueue(
77
+ ...tasksQueue.filter(({ task }) => tasks.includes(task.name) || tasks.includes(task.aspectId))
78
+ );
79
+ }
80
+ if (skipTests) {
81
+ return new TasksQueue(...tasksQueue.filter(({ task }) => task.aspectId !== TesterAspect.id));
82
+ }
83
+ return tasksQueue;
84
+ }
85
+
86
+ function addTasksToGraph(tasksQueue: TasksQueue, dataPerLocation: DataPerLocation[], location: Location) {
87
+ const data = dataPerLocation.find((d) => d.location === location);
88
+ if (!data) return;
89
+ const sorted = data.graph.toposort();
90
+ sorted.forEach((taskNode) => {
91
+ const { aspectId, name } = BuildTaskHelper.deserializeId(taskNode.attr);
92
+ data.pipelineEnvs.forEach(({ env, pipeline }) => {
93
+ const taskIndex = pipeline.findIndex(
94
+ (pipelineTask) => pipelineTask.aspectId === aspectId && pipelineTask.name === name
95
+ );
96
+ if (taskIndex < 0) return;
97
+ const task = pipeline[taskIndex];
98
+ tasksQueue.push({ env, task });
99
+ pipeline.splice(taskIndex, 1); // delete the task from the pipeline
100
+ });
101
+ });
102
+ data.pipelineEnvs.forEach(({ env, pipeline }) => {
103
+ pipeline.forEach((task) => tasksQueue.push({ env, task }));
104
+ });
105
+ }
106
+
107
+ function addDependenciesToGraph(graphs: TasksLocationGraph[], pipeline: BuildTask[], task: BuildTask) {
108
+ if (!task.dependencies || !task.dependencies.length) return;
109
+ const taskId = BuildTaskHelper.serializeId(task);
110
+ task.dependencies.forEach((dependency) => {
111
+ const { aspectId, name } = BuildTaskHelper.deserializeIdAllowEmptyName(dependency);
112
+ const dependencyTasks = pipeline.filter((pipelineTask) => {
113
+ if (pipelineTask.aspectId !== aspectId) return false;
114
+ return name ? name === pipelineTask.name : true;
115
+ });
116
+ if (dependencyTasks.length === 0) {
117
+ throw new Error(
118
+ `Pipeline error - missing task dependency "${dependency}" of the "${BuildTaskHelper.serializeId(task)}"`
119
+ );
120
+ }
121
+ dependencyTasks.forEach((dependencyTask) => {
122
+ const location = getLocation(task, dependencyTask);
123
+ if (!location) {
124
+ // the dependency is behind and will be in the correct order regardless the graph.
125
+ return;
126
+ }
127
+ const graphLocation = graphs.find((g) => g.location === location);
128
+ if (!graphLocation) throw new Error(`unable to find graph for location ${location}`);
129
+ const dependencyId = BuildTaskHelper.serializeId(dependencyTask);
130
+ const graph = graphLocation.graph;
131
+ graph.setNode(new Node(taskId, taskId));
132
+ graph.setNode(new Node(dependencyId, dependencyId));
133
+ graph.setEdge(new Edge(dependencyId, taskId, 'dependency'));
134
+ });
135
+ });
136
+ }
137
+
138
+ /**
139
+ * since the task execution is happening per group: "start", "middle" and "end", the dependencies
140
+ * need to be inside the same group.
141
+ * e.g. if a dependency located at "end" group and the task located at "start", it's impossible to
142
+ * complete the dependency before the task, there it throws an error.
143
+ * it's ok to have the dependency located earlier, e.g. "start" and the task at "end", and in this
144
+ * case, it will not be part of the graph because there is no need to do any special calculation.
145
+ */
146
+ function getLocation(task: BuildTask, dependencyTask: BuildTask): Location | null {
147
+ const taskLocation = task.location || 'middle';
148
+ const dependencyLocation = dependencyTask.location || 'middle';
149
+
150
+ const isDependencyAhead =
151
+ (taskLocation === 'start' && dependencyLocation !== 'start') ||
152
+ (taskLocation === 'middle' && dependencyLocation === 'end');
153
+
154
+ const isDependencyEqual = taskLocation === dependencyLocation;
155
+
156
+ if (isDependencyAhead) {
157
+ throw new Error(`a task "${BuildTaskHelper.serializeId(task)}" located at ${taskLocation}
158
+ has a dependency "${BuildTaskHelper.serializeId(dependencyTask)} located at ${dependencyLocation},
159
+ which is invalid. the dependency must be located earlier or in the same location as the task"`);
160
+ }
161
+
162
+ if (isDependencyEqual) {
163
+ return taskLocation;
164
+ }
165
+
166
+ // dependency is behind. e.g. task is "end" and dependency is "start". no need to enter to the
167
+ // graph as it's going to be executed in the right order regardless the graph.
168
+ return null;
169
+ }
170
+
171
+ function getPipelineForEnv(taskSlot: TaskSlot, env: Environment, pipeNameOnEnv: string): BuildTask[] {
172
+ const buildTasks: BuildTask[] = env[pipeNameOnEnv] ? env[pipeNameOnEnv]() : [];
173
+ const slotsTasks = taskSlot.values().flat();
174
+ const tasksAtStart: BuildTask[] = [];
175
+ const tasksAtEnd: BuildTask[] = [];
176
+ slotsTasks.forEach((task) => {
177
+ if (task.location === 'start') {
178
+ tasksAtStart.push(task);
179
+ return;
180
+ }
181
+ if (task.location === 'end') {
182
+ tasksAtEnd.push(task);
183
+ return;
184
+ }
185
+ tasksAtStart.push(task);
186
+ });
187
+
188
+ // merge with extension registered tasks.
189
+ const mergedTasks = [...tasksAtStart, ...buildTasks, ...tasksAtEnd];
190
+
191
+ return mergedTasks;
192
+ }
@@ -0,0 +1,97 @@
1
+ import { ComponentID, ComponentMap, Component } from '@teambit/component';
2
+ import { isEmpty, compact } from 'lodash';
3
+ import type { ArtifactObject } from '@teambit/legacy/dist/consumer/component/sources/artifact-files';
4
+ import { Artifact, ArtifactList } from './artifact';
5
+ import { TaskResults } from './build-pipe';
6
+ import { TaskMetadata } from './types';
7
+
8
+ export type PipelineReport = {
9
+ taskId: string; // task aspect-id
10
+ taskName: string;
11
+ taskDescription?: string;
12
+ startTime?: number;
13
+ endTime?: number;
14
+ errors?: Array<Error | string>;
15
+ warnings?: string[];
16
+ };
17
+
18
+ export type AspectData = {
19
+ aspectId: string;
20
+ data: TaskMetadata;
21
+ };
22
+
23
+ /**
24
+ * Helper to get the data and artifacts from the TasksResultsList before saving during the tag
25
+ */
26
+ export class BuildPipelineResultList {
27
+ private artifactListsMap: ComponentMap<ArtifactList<Artifact>>;
28
+ constructor(private tasksResults: TaskResults[], private components: Component[]) {
29
+ this.artifactListsMap = this.getFlattenedArtifactListsMapFromAllTasks();
30
+ }
31
+
32
+ private getFlattenedArtifactListsMapFromAllTasks(): ComponentMap<ArtifactList<Artifact>> {
33
+ const artifactListsMaps = this.tasksResults.flatMap((t) => (t.artifacts ? [t.artifacts] : []));
34
+ return ComponentMap.as<ArtifactList<Artifact>>(this.components, (component) => {
35
+ const artifacts: Artifact[] = [];
36
+ artifactListsMaps.forEach((artifactListMap) => {
37
+ const artifactList = artifactListMap.getValueByComponentId(component.id);
38
+ if (artifactList) artifacts.push(...artifactList);
39
+ });
40
+ return ArtifactList.fromArray(artifacts);
41
+ });
42
+ }
43
+
44
+ public getMetadataFromTaskResults(componentId: ComponentID): { [taskId: string]: TaskMetadata } {
45
+ const compResults = this.tasksResults.reduce((acc, current: TaskResults) => {
46
+ const foundComponent = current.componentsResults.find((c) => c.component.id.isEqual(componentId));
47
+ const taskId = current.task.aspectId;
48
+ if (foundComponent && foundComponent.metadata) {
49
+ acc[taskId] = this.mergeDataIfPossible(foundComponent.metadata, acc[taskId], taskId);
50
+ }
51
+ return acc;
52
+ }, {});
53
+ return compResults;
54
+ }
55
+
56
+ public getPipelineReportOfComponent(componentId: ComponentID): PipelineReport[] {
57
+ const compResults = this.tasksResults.map((taskResults: TaskResults) => {
58
+ const foundComponent = taskResults.componentsResults.find((c) => c.component.id.isEqual(componentId));
59
+ if (!foundComponent) return null;
60
+ const pipelineReport: PipelineReport = {
61
+ taskId: taskResults.task.aspectId,
62
+ taskName: taskResults.task.name,
63
+ taskDescription: taskResults.task.description,
64
+ errors: foundComponent.errors,
65
+ warnings: foundComponent.warnings,
66
+ startTime: foundComponent.startTime,
67
+ endTime: foundComponent.endTime,
68
+ };
69
+ return pipelineReport;
70
+ });
71
+ return compact(compResults);
72
+ }
73
+
74
+ public getDataOfComponent(componentId: ComponentID): AspectData[] {
75
+ const tasksData = this.getMetadataFromTaskResults(componentId);
76
+ return Object.keys(tasksData).map((taskId) => ({
77
+ aspectId: taskId,
78
+ data: tasksData[taskId],
79
+ }));
80
+ }
81
+
82
+ public getArtifactsDataOfComponent(componentId: ComponentID): ArtifactObject[] | undefined {
83
+ return this.artifactListsMap.getValueByComponentId(componentId)?.toObject();
84
+ }
85
+
86
+ private mergeDataIfPossible(currentData: TaskMetadata, existingData: TaskMetadata | undefined, taskId: string) {
87
+ if (!existingData || isEmpty(existingData)) return currentData;
88
+ // both exist
89
+ if (typeof currentData !== 'object') {
90
+ throw new Error(`task data must be "object", get ${typeof currentData} for ${taskId}`);
91
+ }
92
+ if (Array.isArray(currentData)) {
93
+ throw new Error(`task data must be "object", get Array for ${taskId}`);
94
+ }
95
+ return { ...currentData, ...existingData };
96
+ }
97
+ }
package/build-task.ts ADDED
@@ -0,0 +1,151 @@
1
+ import type { Component } from '@teambit/component';
2
+ import { LaneId } from '@teambit/lane-id';
3
+ import { ExecutionContext } from '@teambit/envs';
4
+ import type { Network } from '@teambit/isolator';
5
+ import type { ComponentResult } from './types';
6
+ import type { ArtifactDefinition } from './artifact';
7
+ import { TaskResultsList } from './task-results-list';
8
+ import { TaskResults } from './build-pipe';
9
+ import { PipeName } from './builder.service';
10
+
11
+ export type TaskLocation = 'start' | 'end';
12
+
13
+ /**
14
+ * delimiter between task.aspectId and task.name
15
+ */
16
+ export const TaskIdDelimiter = ':';
17
+
18
+ /**
19
+ * A folder to write artifacts generated during a build task
20
+ * This folder is used in the core envs and excluded by default from the package tar file (the core envs is writing this into the npmignore file)
21
+ */
22
+ export const CAPSULE_ARTIFACTS_DIR = 'artifacts';
23
+
24
+ export interface BuildContext extends ExecutionContext {
25
+ /**
26
+ * all components about to be built/tagged.
27
+ */
28
+ components: Component[];
29
+
30
+ /**
31
+ * network of capsules ready to be built.
32
+ */
33
+ capsuleNetwork: Network;
34
+
35
+ /**
36
+ * data generated by tasks that were running before this task
37
+ */
38
+ previousTasksResults: TaskResults[];
39
+
40
+ /**
41
+ * Run the build pipeline in dev mode
42
+ */
43
+ dev?: boolean;
44
+
45
+ /**
46
+ * pipe name such as "build", "tas", "snap".
47
+ * an example usage is "deploy" task which is running in snap and tag pipeline and has different needs in each one.
48
+ */
49
+ pipeName: PipeName;
50
+
51
+ /**
52
+ * current lane-id if exists. empty when on main.
53
+ */
54
+ laneId?: LaneId;
55
+ }
56
+
57
+ export interface TaskDescriptor {
58
+ aspectId: string;
59
+ name?: string;
60
+ description?: string;
61
+ }
62
+
63
+ export interface BuildTask {
64
+ /**
65
+ * aspect id serialized of the creator of the task.
66
+ * todo: automate this so then it won't be needed to pass manually.
67
+ */
68
+ aspectId: string;
69
+
70
+ /**
71
+ * name of the task. function as an identifier among other tasks of the same aspectId.
72
+ * spaces and special characters are not allowed. as a convention, use UpperCamelCase style.
73
+ * (e.g. TypescriptCompiler).
74
+ */
75
+ name: string;
76
+
77
+ /**
78
+ * description of what the task does.
79
+ * if available, the logger will log it show it in the status-line.
80
+ */
81
+ description?: string;
82
+
83
+ /**
84
+ * where to put the task, before the env pipeline or after
85
+ */
86
+ location?: TaskLocation;
87
+
88
+ /**
89
+ * execute a task in a build context
90
+ */
91
+ execute(context: BuildContext): Promise<BuiltTaskResult>;
92
+
93
+ /**
94
+ * run before the build pipeline has started. this is useful when some preparation are needed to
95
+ * be done on all envs before the build starts.
96
+ * e.g. typescript compiler needs to write the tsconfig file. doing it during the task, will
97
+ * cause dependencies from other envs to get this tsconfig written.
98
+ */
99
+ preBuild?(context: BuildContext): Promise<void>;
100
+
101
+ /**
102
+ * run after the build pipeline completed for all envs. useful for doing some cleanup on the
103
+ * capsules before the deployment starts.
104
+ */
105
+ postBuild?(context: BuildContext, tasksResults: TaskResultsList): Promise<void>;
106
+
107
+ /**
108
+ * needed if you want the task to be running only after the dependencies were completed
109
+ * for *all* envs.
110
+ * normally this is not needed because the build-pipeline runs the tasks in the same order
111
+ * they're located in the `getBuildPipe()` array and according to the task.location.
112
+ * the case where this is useful is when a task not only needs to be after another task, but also
113
+ * after all environments were running that task.
114
+ * a dependency is task.aspectId. if an aspect has multiple tasks, to be more specific, use
115
+ * "aspectId:name", e.g. "teambit.compilation/compiler:TypescriptCompiler".
116
+ */
117
+ dependencies?: string[];
118
+ }
119
+
120
+ // TODO: rename to BuildTaskResults
121
+ export interface BuiltTaskResult {
122
+ /**
123
+ * build results for each of the components in the build context.
124
+ */
125
+ componentsResults: ComponentResult[];
126
+
127
+ /**
128
+ * array of artifact definitions to generate after a successful build.
129
+ */
130
+ artifacts?: ArtifactDefinition[];
131
+ }
132
+
133
+ export class BuildTaskHelper {
134
+ static serializeId({ aspectId, name }: { aspectId: string; name: string }): string {
135
+ return aspectId + TaskIdDelimiter + name;
136
+ }
137
+ static deserializeId(id: string): { aspectId: string; name: string } {
138
+ const split = id.split(TaskIdDelimiter);
139
+ if (split.length === 0) throw new Error(`deserializeId, ${id} is empty`);
140
+ if (split.length === 1) throw new Error(`deserializeId, ${id} has only aspect-id without name`);
141
+ if (split.length === 2) return { aspectId: split[0], name: split[1] };
142
+ throw new Error(`deserializeId, id ${id} has more than one ${TaskIdDelimiter}`);
143
+ }
144
+ /**
145
+ * don't throw an error when the id includes only the aspect-id without the task name.
146
+ * useful for task dependencies, when it's allowed to specify the aspect-id only.
147
+ */
148
+ static deserializeIdAllowEmptyName(id: string): { aspectId: string; name?: string } {
149
+ return id.includes(TaskIdDelimiter) ? BuildTaskHelper.deserializeId(id) : { aspectId: id, name: undefined };
150
+ }
151
+ }