@enspirit/emb 0.0.6 → 0.0.7
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.
- package/README.md +60 -13
- package/dist/src/cli/commands/clean.d.ts +3 -1
- package/dist/src/cli/commands/clean.js +13 -2
- package/dist/src/cli/commands/components/build.d.ts +6 -2
- package/dist/src/cli/commands/components/build.js +16 -7
- package/dist/src/cli/commands/config/print.js +2 -3
- package/dist/src/cli/commands/down.d.ts +2 -2
- package/dist/src/cli/commands/down.js +5 -25
- package/dist/src/cli/commands/tasks/index.js +11 -5
- package/dist/src/cli/commands/tasks/run.d.ts +1 -1
- package/dist/src/cli/commands/tasks/run.js +10 -43
- package/dist/src/cli/commands/up.d.ts +1 -1
- package/dist/src/cli/commands/up.js +12 -34
- package/dist/src/config/convert.js +2 -4
- package/dist/src/config/index.d.ts +1 -0
- package/dist/src/config/index.js +1 -0
- package/dist/src/config/schema.d.ts +32 -25
- package/dist/src/config/schema.json +48 -26
- package/dist/src/config/types.d.ts +10 -6
- package/dist/src/docker/compose/index.d.ts +1 -7
- package/dist/src/docker/compose/index.js +1 -13
- package/dist/src/docker/compose/operations/ComposeDownOperation.d.ts +12 -0
- package/dist/src/docker/compose/operations/ComposeDownOperation.js +21 -0
- package/dist/src/docker/compose/operations/ComposeUpOperation.d.ts +13 -0
- package/dist/src/docker/compose/operations/ComposeUpOperation.js +39 -0
- package/dist/src/docker/compose/operations/index.d.ts +2 -0
- package/dist/src/docker/compose/operations/index.js +2 -0
- package/dist/src/monorepo/component.d.ts +2 -2
- package/dist/src/monorepo/component.js +6 -6
- package/dist/src/monorepo/config.d.ts +4 -2
- package/dist/src/monorepo/config.js +14 -2
- package/dist/src/monorepo/index.d.ts +2 -0
- package/dist/src/monorepo/index.js +2 -0
- package/dist/src/monorepo/monorepo.d.ts +4 -2
- package/dist/src/monorepo/monorepo.js +30 -6
- package/dist/src/monorepo/operations/components/BuildComponentsOperation.d.ts +11 -2
- package/dist/src/monorepo/operations/components/BuildComponentsOperation.js +45 -59
- package/dist/src/monorepo/operations/components/GetComponentContainerOperation.d.ts +1 -1
- package/dist/src/monorepo/operations/components/GetComponentContainerOperation.js +1 -1
- package/dist/src/monorepo/operations/shell/ExecuteLocalCommandOperation.d.ts +4 -6
- package/dist/src/monorepo/operations/shell/ExecuteLocalCommandOperation.js +2 -7
- package/dist/src/monorepo/operations/tasks/RunTasksOperation.d.ts +21 -0
- package/dist/src/monorepo/operations/tasks/RunTasksOperation.js +84 -0
- package/dist/src/monorepo/operations/tasks/index.d.ts +1 -1
- package/dist/src/monorepo/operations/tasks/index.js +1 -1
- package/dist/src/monorepo/plugins/ComponentDiscoverPlugin.js +3 -1
- package/dist/src/monorepo/plugins/EmbfileLoaderPlugin.d.ts +3 -3
- package/dist/src/monorepo/plugins/EmbfileLoaderPlugin.js +9 -5
- package/dist/src/monorepo/store/index.js +5 -3
- package/dist/src/monorepo/taskManagerFactory.d.ts +3 -0
- package/dist/src/monorepo/taskManagerFactory.js +20 -0
- package/dist/src/monorepo/types.d.ts +2 -1
- package/dist/src/monorepo/utils/findRunOrder.d.ts +34 -0
- package/dist/src/monorepo/utils/findRunOrder.js +165 -0
- package/dist/src/monorepo/utils/index.d.ts +1 -1
- package/dist/src/monorepo/utils/index.js +1 -1
- package/oclif.manifest.json +63 -40
- package/package.json +4 -2
- package/dist/src/monorepo/operations/tasks/RunTaskOperation.d.ts +0 -18
- package/dist/src/monorepo/operations/tasks/RunTaskOperation.js +0 -50
- package/dist/src/monorepo/utils/findBuildOrder.d.ts +0 -2
- package/dist/src/monorepo/utils/findBuildOrder.js +0 -41
|
@@ -1,79 +1,58 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { createColors } from 'colorette';
|
|
3
|
-
import { delay, ListrDefaultRendererLogLevels, PRESET_TIMER, } from 'listr2';
|
|
1
|
+
import { getContext } from '../../../index.js';
|
|
4
2
|
import * as z from 'zod';
|
|
5
3
|
import { BuildImageOperation, getSentinelFile, } from '../../../docker/index.js';
|
|
6
|
-
import {
|
|
4
|
+
import { EMBCollection, findRunOrder, taskManagerFactory, } from '../../index.js';
|
|
7
5
|
import { AbstractOperation } from '../../../operations/index.js';
|
|
8
|
-
import { FilePrerequisitePlugin } from '../../../prerequisites/
|
|
9
|
-
import { PrerequisiteType } from '../../../prerequisites/types.js';
|
|
6
|
+
import { FilePrerequisitePlugin, PrerequisiteType } from '../../../prerequisites/index.js';
|
|
10
7
|
const schema = z.object({
|
|
11
8
|
components: z
|
|
12
9
|
.array(z.string())
|
|
13
10
|
.describe('The list of components to build')
|
|
14
11
|
.optional(),
|
|
12
|
+
dryRun: z
|
|
13
|
+
.boolean()
|
|
14
|
+
.optional()
|
|
15
|
+
.describe('Do not build but return the config that would be used to build the images'),
|
|
15
16
|
});
|
|
16
17
|
export class BuildComponentsOperation extends AbstractOperation {
|
|
17
18
|
constructor() {
|
|
18
19
|
super(schema);
|
|
19
20
|
}
|
|
20
21
|
async _run(input) {
|
|
21
|
-
const
|
|
22
|
+
const { monorepo } = getContext();
|
|
23
|
+
const manager = taskManagerFactory();
|
|
24
|
+
const selection = (input.components || []).map((t) => monorepo.component(t));
|
|
25
|
+
const collection = new EMBCollection(this.context.monorepo.components, {
|
|
26
|
+
idField: 'name',
|
|
27
|
+
depField: 'dependencies',
|
|
28
|
+
forbidIdNameCollision: true,
|
|
29
|
+
});
|
|
30
|
+
const ordered = findRunOrder(selection.map((s) => s.name), collection);
|
|
22
31
|
const tasks = await Promise.all(ordered.map((cmp) => {
|
|
23
32
|
return {
|
|
24
33
|
task: async (context, task) => {
|
|
25
|
-
return this.buildComponent(cmp, task);
|
|
34
|
+
return this.buildComponent(cmp, task, context, input.dryRun);
|
|
26
35
|
},
|
|
27
36
|
title: `Building ${cmp.name}`,
|
|
28
37
|
};
|
|
29
38
|
}));
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
exitOnError: true,
|
|
34
|
-
rendererOptions: {
|
|
35
|
-
collapseErrors: false,
|
|
36
|
-
collapseSubtasks: false,
|
|
37
|
-
color: {
|
|
38
|
-
// @ts-expect-error not sure why
|
|
39
|
-
[ListrDefaultRendererLogLevels.SKIPPED_WITH_COLLAPSE]: createColors().green,
|
|
40
|
-
},
|
|
41
|
-
icon: {
|
|
42
|
-
[ListrDefaultRendererLogLevels.SKIPPED_WITH_COLLAPSE]: '♺',
|
|
43
|
-
},
|
|
44
|
-
timer: {
|
|
45
|
-
...PRESET_TIMER,
|
|
46
|
-
},
|
|
47
|
-
},
|
|
39
|
+
const list = manager.newListr([...tasks], {
|
|
40
|
+
rendererOptions: { persistentOutput: true },
|
|
41
|
+
ctx: {},
|
|
48
42
|
});
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
async task(_context, task) {
|
|
52
|
-
return task.newListr([...tasks], {
|
|
53
|
-
rendererOptions: {
|
|
54
|
-
collapseSubtasks: false,
|
|
55
|
-
},
|
|
56
|
-
});
|
|
57
|
-
},
|
|
58
|
-
title: 'Building components',
|
|
59
|
-
},
|
|
60
|
-
]);
|
|
61
|
-
await manager.runAll();
|
|
62
|
-
return ordered;
|
|
43
|
+
const results = await list.run();
|
|
44
|
+
return results;
|
|
63
45
|
}
|
|
64
|
-
async buildComponent(cmp, parentTask) {
|
|
65
|
-
|
|
46
|
+
async buildComponent(cmp, parentTask, parentContext, dryRun = false) {
|
|
47
|
+
const prereqPlugin = new FilePrerequisitePlugin();
|
|
48
|
+
const list = parentTask.newListr([
|
|
66
49
|
// Collect all the prerequisites and other build infos
|
|
67
50
|
// (This is when variables are expanded etc)
|
|
68
51
|
{
|
|
69
52
|
async task(ctx) {
|
|
70
|
-
//
|
|
71
|
-
|
|
72
|
-
ctx.skip = false;
|
|
73
|
-
//
|
|
74
|
-
ctx.parentTask = parentTask;
|
|
53
|
+
// Install the context for this specific component build chain
|
|
54
|
+
ctx.cacheHit = false;
|
|
75
55
|
ctx.sentinelFile = getSentinelFile(cmp);
|
|
76
|
-
ctx.plugin = new FilePrerequisitePlugin();
|
|
77
56
|
ctx.build = await cmp.toDockerBuild();
|
|
78
57
|
},
|
|
79
58
|
title: 'Prepare build context',
|
|
@@ -81,7 +60,7 @@ export class BuildComponentsOperation extends AbstractOperation {
|
|
|
81
60
|
// Check for sentinal information to see if the build can be skipped
|
|
82
61
|
{
|
|
83
62
|
task: async (ctx) => {
|
|
84
|
-
ctx.preBuildMeta = await
|
|
63
|
+
ctx.preBuildMeta = await prereqPlugin.meta(cmp, ctx.build.prerequisites, 'pre');
|
|
85
64
|
let lastValue;
|
|
86
65
|
try {
|
|
87
66
|
lastValue = (await this.context.monorepo.store.readFile(ctx.sentinelFile)).toString();
|
|
@@ -90,10 +69,10 @@ export class BuildComponentsOperation extends AbstractOperation {
|
|
|
90
69
|
lastValue = undefined;
|
|
91
70
|
}
|
|
92
71
|
if (lastValue) {
|
|
93
|
-
const diff = await
|
|
72
|
+
const diff = await prereqPlugin.diff(cmp, ctx.build.prerequisites, lastValue, ctx.preBuildMeta);
|
|
94
73
|
if (!diff) {
|
|
95
|
-
ctx.
|
|
96
|
-
|
|
74
|
+
ctx.cacheHit = true;
|
|
75
|
+
// parentTask.skip(`${parentTask.title} (cache hit)`);
|
|
97
76
|
}
|
|
98
77
|
}
|
|
99
78
|
},
|
|
@@ -101,10 +80,9 @@ export class BuildComponentsOperation extends AbstractOperation {
|
|
|
101
80
|
},
|
|
102
81
|
{
|
|
103
82
|
task: async (ctx, task) => {
|
|
104
|
-
if (ctx.
|
|
83
|
+
if (ctx.cacheHit || ctx.dryRun) {
|
|
105
84
|
return task.skip();
|
|
106
85
|
}
|
|
107
|
-
await delay(500);
|
|
108
86
|
const title = `Building image ${ctx.build.name}:${ctx.build.tag}`;
|
|
109
87
|
task.title = title;
|
|
110
88
|
const op = new BuildImageOperation((progress) => {
|
|
@@ -127,18 +105,26 @@ export class BuildComponentsOperation extends AbstractOperation {
|
|
|
127
105
|
// Update sentinel file
|
|
128
106
|
{
|
|
129
107
|
task: async (ctx, task) => {
|
|
130
|
-
if (ctx.
|
|
108
|
+
if (ctx.cacheHit || ctx.dryRun) {
|
|
131
109
|
return task.skip();
|
|
132
110
|
}
|
|
133
|
-
const sentinelValue = await
|
|
111
|
+
const sentinelValue = await prereqPlugin.meta(cmp, ctx.build.prerequisites, 'post');
|
|
134
112
|
await this.context.monorepo.store.writeFile(ctx.sentinelFile, sentinelValue);
|
|
135
113
|
},
|
|
136
114
|
title: 'Dumping cache info',
|
|
137
115
|
},
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
116
|
+
{
|
|
117
|
+
// Return build meta data
|
|
118
|
+
async task(ctx) {
|
|
119
|
+
parentContext[cmp.name] = ctx;
|
|
120
|
+
if (ctx.dryRun) {
|
|
121
|
+
// parentTask.skip(`${parentTask.title} (dry run)`);
|
|
122
|
+
}
|
|
123
|
+
},
|
|
141
124
|
},
|
|
125
|
+
], {
|
|
126
|
+
ctx: { dryRun },
|
|
142
127
|
});
|
|
128
|
+
return list;
|
|
143
129
|
}
|
|
144
130
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ContainerInfo } from 'dockerode';
|
|
2
|
-
import { Component } from '../../
|
|
2
|
+
import { Component } from '../../index.js';
|
|
3
3
|
import { IOperation } from '../../../operations/index.js';
|
|
4
4
|
export declare class GetComponentContainerOperation implements IOperation<Component, ContainerInfo> {
|
|
5
5
|
run(component: Component | string): Promise<ContainerInfo>;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { getContext } from '../../../index.js';
|
|
2
2
|
import { ListContainersOperation } from '../../../docker/index.js';
|
|
3
|
-
import { Component } from '../../
|
|
3
|
+
import { Component } from '../../index.js';
|
|
4
4
|
export class GetComponentContainerOperation {
|
|
5
5
|
async run(component) {
|
|
6
6
|
const { monorepo } = getContext();
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { Writable } from 'node:stream';
|
|
1
|
+
import { Readable } from 'node:stream';
|
|
3
2
|
import * as z from 'zod';
|
|
4
3
|
import { AbstractOperation } from '../../../operations/index.js';
|
|
5
4
|
/**
|
|
@@ -10,9 +9,8 @@ declare const schema: z.ZodObject<{
|
|
|
10
9
|
script: z.ZodString;
|
|
11
10
|
workingDir: z.ZodOptional<z.ZodString>;
|
|
12
11
|
}, z.core.$strip>;
|
|
13
|
-
export declare class ExecuteLocalCommandOperation extends AbstractOperation<typeof schema,
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
protected _run(input: z.input<typeof schema>): Promise<ResultPromise>;
|
|
12
|
+
export declare class ExecuteLocalCommandOperation extends AbstractOperation<typeof schema, Readable> {
|
|
13
|
+
constructor();
|
|
14
|
+
protected _run(input: z.input<typeof schema>): Promise<Readable>;
|
|
17
15
|
}
|
|
18
16
|
export {};
|
|
@@ -16,10 +16,8 @@ const schema = z.object({
|
|
|
16
16
|
.describe('The working directory for the exec process inside the container'),
|
|
17
17
|
});
|
|
18
18
|
export class ExecuteLocalCommandOperation extends AbstractOperation {
|
|
19
|
-
|
|
20
|
-
constructor(out) {
|
|
19
|
+
constructor() {
|
|
21
20
|
super(schema);
|
|
22
|
-
this.out = out;
|
|
23
21
|
}
|
|
24
22
|
async _run(input) {
|
|
25
23
|
const process = execa(input.script, {
|
|
@@ -27,9 +25,6 @@ export class ExecuteLocalCommandOperation extends AbstractOperation {
|
|
|
27
25
|
cwd: input.workingDir,
|
|
28
26
|
shell: true,
|
|
29
27
|
});
|
|
30
|
-
|
|
31
|
-
process.all?.pipe(this.out);
|
|
32
|
-
}
|
|
33
|
-
return process;
|
|
28
|
+
return process.all;
|
|
34
29
|
}
|
|
35
30
|
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { Writable } from 'node:stream';
|
|
2
|
+
import { TaskInfo } from '../../index.js';
|
|
3
|
+
import { IOperation } from '../../../operations/index.js';
|
|
4
|
+
export declare enum ExecutorType {
|
|
5
|
+
container = "container",
|
|
6
|
+
local = "local"
|
|
7
|
+
}
|
|
8
|
+
export type RunTasksOperationParams = {
|
|
9
|
+
tasks: Array<string>;
|
|
10
|
+
executor?: ExecutorType | undefined;
|
|
11
|
+
};
|
|
12
|
+
export type TaskWithScript = TaskInfo & {
|
|
13
|
+
script: string;
|
|
14
|
+
};
|
|
15
|
+
export declare class RunTasksOperation implements IOperation<RunTasksOperationParams, Array<TaskInfo>> {
|
|
16
|
+
protected out?: Writable | undefined;
|
|
17
|
+
constructor(out?: Writable | undefined);
|
|
18
|
+
run(params: RunTasksOperationParams): Promise<Array<TaskInfo>>;
|
|
19
|
+
protected runDocker(task: TaskWithScript, out?: Writable): Promise<void>;
|
|
20
|
+
protected runLocal(task: TaskWithScript): Promise<import("stream").Readable>;
|
|
21
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { getContext } from '../../../index.js';
|
|
2
|
+
import { Manager } from '@listr2/manager';
|
|
3
|
+
import { ContainerExecOperation } from '../../../docker/index.js';
|
|
4
|
+
import { EMBCollection, findRunOrder } from '../../index.js';
|
|
5
|
+
import { ExecuteLocalCommandOperation, GetComponentContainerOperation, } from '../index.js';
|
|
6
|
+
export var ExecutorType;
|
|
7
|
+
(function (ExecutorType) {
|
|
8
|
+
ExecutorType["container"] = "container";
|
|
9
|
+
ExecutorType["local"] = "local";
|
|
10
|
+
})(ExecutorType || (ExecutorType = {}));
|
|
11
|
+
export class RunTasksOperation {
|
|
12
|
+
out;
|
|
13
|
+
constructor(out) {
|
|
14
|
+
this.out = out;
|
|
15
|
+
}
|
|
16
|
+
async run(params) {
|
|
17
|
+
const { monorepo } = getContext();
|
|
18
|
+
// First ensure the selection is valid (user can use task IDs or names)
|
|
19
|
+
const collection = new EMBCollection(monorepo.tasks, {
|
|
20
|
+
idField: 'id',
|
|
21
|
+
depField: 'pre',
|
|
22
|
+
});
|
|
23
|
+
const ordered = findRunOrder(params.tasks, collection);
|
|
24
|
+
const runner = new Manager({
|
|
25
|
+
concurrent: false,
|
|
26
|
+
exitOnError: false,
|
|
27
|
+
rendererOptions: {
|
|
28
|
+
collapseSubtasks: false,
|
|
29
|
+
collapseSkips: false,
|
|
30
|
+
},
|
|
31
|
+
});
|
|
32
|
+
const list = runner.newListr(ordered.map((task) => {
|
|
33
|
+
return {
|
|
34
|
+
rendererOptions: {
|
|
35
|
+
persistentOutput: true,
|
|
36
|
+
},
|
|
37
|
+
task: async (context, listrTask) => {
|
|
38
|
+
if (!task.script) {
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
const executor = params.executor ??
|
|
42
|
+
(task.component ? ExecutorType.container : ExecutorType.local);
|
|
43
|
+
if (executor === ExecutorType.container && !task.component) {
|
|
44
|
+
throw new Error('Cannot use the container executor with global tasks');
|
|
45
|
+
}
|
|
46
|
+
switch (executor) {
|
|
47
|
+
case ExecutorType.container: {
|
|
48
|
+
return this.runDocker(task, listrTask.stdout());
|
|
49
|
+
}
|
|
50
|
+
case ExecutorType.local: {
|
|
51
|
+
return this.runLocal(task);
|
|
52
|
+
}
|
|
53
|
+
default: {
|
|
54
|
+
throw new Error(`Unssuported executor type: ${executor}`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
title: `Running ${task.id}`,
|
|
59
|
+
};
|
|
60
|
+
}));
|
|
61
|
+
await list.run();
|
|
62
|
+
return ordered;
|
|
63
|
+
}
|
|
64
|
+
async runDocker(task, out) {
|
|
65
|
+
const { monorepo } = getContext();
|
|
66
|
+
const containerInfo = await monorepo.run(new GetComponentContainerOperation(), task.component);
|
|
67
|
+
return monorepo.run(new ContainerExecOperation(out), {
|
|
68
|
+
attachStderr: true,
|
|
69
|
+
attachStdout: true,
|
|
70
|
+
container: containerInfo.Id,
|
|
71
|
+
script: task.script,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
async runLocal(task) {
|
|
75
|
+
const { monorepo } = getContext();
|
|
76
|
+
const cwd = task.component
|
|
77
|
+
? monorepo.component(task.component).rootdir
|
|
78
|
+
: monorepo.rootDir;
|
|
79
|
+
return monorepo.run(new ExecuteLocalCommandOperation(), {
|
|
80
|
+
script: task.script,
|
|
81
|
+
workingDir: cwd,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export * from './
|
|
1
|
+
export * from './RunTasksOperation.js';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export * from './
|
|
1
|
+
export * from './RunTasksOperation.js';
|
|
@@ -25,8 +25,10 @@ export class ComponentDiscoverPlugin extends AbstractPlugin {
|
|
|
25
25
|
const name = dirname(path);
|
|
26
26
|
const component = config.components.find((cmp) => cmp.name === name);
|
|
27
27
|
const cfg = {
|
|
28
|
-
context: name,
|
|
29
28
|
name,
|
|
29
|
+
docker: {
|
|
30
|
+
context: name,
|
|
31
|
+
},
|
|
30
32
|
};
|
|
31
33
|
return component ? deepmerge()(component, cfg) : cfg;
|
|
32
34
|
});
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import { Monorepo, MonorepoConfig } from '../index.js';
|
|
2
2
|
import { AbstractPlugin } from './plugin.js';
|
|
3
3
|
export type EmbfileLoaderPluginOptions = {
|
|
4
|
-
glob?: string;
|
|
4
|
+
glob?: string | string[];
|
|
5
5
|
};
|
|
6
6
|
export declare const EmbfileLoaderPluginDefaultOptions: {
|
|
7
7
|
glob: string;
|
|
8
8
|
};
|
|
9
|
-
export declare class EmbfileLoaderPlugin extends AbstractPlugin<EmbfileLoaderPluginOptions
|
|
9
|
+
export declare class EmbfileLoaderPlugin extends AbstractPlugin<Required<EmbfileLoaderPluginOptions>> {
|
|
10
10
|
protected monorepo: Monorepo;
|
|
11
11
|
static name: string;
|
|
12
|
-
constructor(
|
|
12
|
+
constructor(cfg: Partial<EmbfileLoaderPluginOptions>, monorepo: Monorepo);
|
|
13
13
|
extendConfig(config: MonorepoConfig): Promise<MonorepoConfig>;
|
|
14
14
|
}
|
|
@@ -8,15 +8,19 @@ export const EmbfileLoaderPluginDefaultOptions = {
|
|
|
8
8
|
export class EmbfileLoaderPlugin extends AbstractPlugin {
|
|
9
9
|
monorepo;
|
|
10
10
|
static name = 'embfiles';
|
|
11
|
-
constructor(
|
|
12
|
-
|
|
11
|
+
constructor(cfg, monorepo) {
|
|
12
|
+
const config = {
|
|
13
13
|
...EmbfileLoaderPluginDefaultOptions,
|
|
14
|
-
...
|
|
15
|
-
}
|
|
14
|
+
...cfg,
|
|
15
|
+
};
|
|
16
|
+
if (!Array.isArray(config.glob)) {
|
|
17
|
+
config.glob = [config.glob];
|
|
18
|
+
}
|
|
19
|
+
super(config, monorepo);
|
|
16
20
|
this.monorepo = monorepo;
|
|
17
21
|
}
|
|
18
22
|
async extendConfig(config) {
|
|
19
|
-
const files = await glob(this.config.glob
|
|
23
|
+
const files = await glob(this.config.glob, {
|
|
20
24
|
...this.config,
|
|
21
25
|
cwd: config.project.rootDir,
|
|
22
26
|
});
|
|
@@ -10,9 +10,11 @@ import { dirname, join, normalize } from 'node:path';
|
|
|
10
10
|
export class EMBStore {
|
|
11
11
|
monorepo;
|
|
12
12
|
path;
|
|
13
|
-
constructor(monorepo, dirname
|
|
13
|
+
constructor(monorepo, dirname) {
|
|
14
14
|
this.monorepo = monorepo;
|
|
15
|
-
|
|
15
|
+
// By default, we use the flavor name to build that root of the store
|
|
16
|
+
// so that logs and sentinel files for different flavors are keps separate
|
|
17
|
+
this.path = this.monorepo.join(dirname || '.emb');
|
|
16
18
|
}
|
|
17
19
|
async createReadStream(path) {
|
|
18
20
|
await this.mkdirp(dirname(path));
|
|
@@ -45,7 +47,7 @@ export class EMBStore {
|
|
|
45
47
|
}
|
|
46
48
|
}
|
|
47
49
|
join(path) {
|
|
48
|
-
return join(this.path, path);
|
|
50
|
+
return join(this.path, this.monorepo.currentFlavor, path);
|
|
49
51
|
}
|
|
50
52
|
async mkdirp(path) {
|
|
51
53
|
// Avoid getting out of the store by ensuring nothing goes past ../
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { Manager } from '@listr2/manager';
|
|
2
|
+
import { ListrDefaultRendererLogLevels, PRESET_TIMER, } from 'listr2';
|
|
3
|
+
export function taskManagerFactory(override) {
|
|
4
|
+
return new Manager({
|
|
5
|
+
collectErrors: 'minimal',
|
|
6
|
+
concurrent: false,
|
|
7
|
+
exitOnError: true,
|
|
8
|
+
rendererOptions: {
|
|
9
|
+
collapseErrors: false,
|
|
10
|
+
collapseSubtasks: false,
|
|
11
|
+
icon: {
|
|
12
|
+
[ListrDefaultRendererLogLevels.SKIPPED_WITH_COLLAPSE]: '♺',
|
|
13
|
+
},
|
|
14
|
+
timer: {
|
|
15
|
+
...PRESET_TIMER,
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
...override,
|
|
19
|
+
});
|
|
20
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
type DepList = readonly string[] | string[] | undefined;
|
|
2
|
+
type CollectionConfig<IDK extends PropertyKey, DPK extends PropertyKey> = {
|
|
3
|
+
idField: IDK;
|
|
4
|
+
depField: DPK;
|
|
5
|
+
/** If true, throw when an item's id equals some other item's name (or vice versa). */
|
|
6
|
+
forbidIdNameCollision?: boolean;
|
|
7
|
+
};
|
|
8
|
+
type AmbiguityPolicy = 'error' | 'runAll';
|
|
9
|
+
export declare class EMBCollection<T extends Partial<Record<DPK, DepList>> & Record<IDK, string> & {
|
|
10
|
+
name: string;
|
|
11
|
+
}, IDK extends keyof T, DPK extends keyof T> {
|
|
12
|
+
private items;
|
|
13
|
+
readonly idField: IDK;
|
|
14
|
+
readonly depField: DPK;
|
|
15
|
+
private byId;
|
|
16
|
+
private byName;
|
|
17
|
+
constructor(items: Iterable<T>, cfg: CollectionConfig<IDK, DPK>);
|
|
18
|
+
/** All items (stable array iteration) */
|
|
19
|
+
get all(): Iterable<T>;
|
|
20
|
+
idOf(t: T): string;
|
|
21
|
+
depsOf(t: T): readonly string[];
|
|
22
|
+
matches(ref: string, opts?: {
|
|
23
|
+
multiple?: false;
|
|
24
|
+
}): T;
|
|
25
|
+
matches(ref: string, opts: {
|
|
26
|
+
multiple: true;
|
|
27
|
+
}): T[];
|
|
28
|
+
}
|
|
29
|
+
export declare function findRunOrder<T extends Partial<Record<DPK, DepList>> & Record<IDK, string> & {
|
|
30
|
+
name: string;
|
|
31
|
+
}, IDK extends keyof T, DPK extends keyof T>(selection: readonly string[], collection: EMBCollection<T, IDK, DPK>, { onAmbiguous }?: {
|
|
32
|
+
onAmbiguous?: AmbiguityPolicy | undefined;
|
|
33
|
+
}): T[];
|
|
34
|
+
export {};
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import graphlib from 'graphlib';
|
|
2
|
+
export class EMBCollection {
|
|
3
|
+
items;
|
|
4
|
+
idField;
|
|
5
|
+
depField;
|
|
6
|
+
byId;
|
|
7
|
+
byName;
|
|
8
|
+
constructor(items, cfg) {
|
|
9
|
+
this.items = [];
|
|
10
|
+
this.idField = cfg.idField;
|
|
11
|
+
this.depField = cfg.depField;
|
|
12
|
+
this.byId = new Map();
|
|
13
|
+
this.byName = new Map();
|
|
14
|
+
// single-pass validation state
|
|
15
|
+
const seenIds = new Set();
|
|
16
|
+
const seenNames = new Set();
|
|
17
|
+
const dupIdReports = [];
|
|
18
|
+
const collisions = [];
|
|
19
|
+
for (const t of items) {
|
|
20
|
+
const id = t[this.idField];
|
|
21
|
+
const { name } = t;
|
|
22
|
+
// duplicate id?
|
|
23
|
+
if (seenIds.has(id)) {
|
|
24
|
+
const firstOwner = this.byId.get(id); // first occurrence already stored
|
|
25
|
+
dupIdReports.push(`id "${id}" used by "${firstOwner.name}" and "${name}"`);
|
|
26
|
+
// keep the first owner in byId; do not overwrite
|
|
27
|
+
}
|
|
28
|
+
else {
|
|
29
|
+
this.byId.set(id, t);
|
|
30
|
+
seenIds.add(id);
|
|
31
|
+
}
|
|
32
|
+
// --- Optional validation: forbid id <-> name collisions ---
|
|
33
|
+
const checkCollisions = cfg.forbidIdNameCollision && this.idField !== 'name';
|
|
34
|
+
if (checkCollisions) {
|
|
35
|
+
if (seenNames.has(id)) {
|
|
36
|
+
const nameOwners = this.byName.get(id) ?? [];
|
|
37
|
+
const ownerIds = nameOwners.map((o) => o[this.idField]).join(', ');
|
|
38
|
+
collisions.push(`value "${id}" is an id of "${name}" and also a name of item(s) with id(s): [${ownerIds}]`);
|
|
39
|
+
}
|
|
40
|
+
if (seenIds.has(name)) {
|
|
41
|
+
const idOwner = this.byId.get(name);
|
|
42
|
+
collisions.push(`value "${name}" is a name of "${t.name}" and also an id of "${idOwner.name}"`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
// byName index
|
|
46
|
+
const list = this.byName.get(name);
|
|
47
|
+
if (list)
|
|
48
|
+
list.push(t);
|
|
49
|
+
else
|
|
50
|
+
this.byName.set(name, [t]);
|
|
51
|
+
// keep item list (stable order)
|
|
52
|
+
this.items.push(t);
|
|
53
|
+
// record name after checks so current name won’t collide with itself
|
|
54
|
+
seenNames.add(name);
|
|
55
|
+
}
|
|
56
|
+
if (dupIdReports.length > 0 || collisions.length > 0) {
|
|
57
|
+
const parts = [];
|
|
58
|
+
if (dupIdReports.length > 0) {
|
|
59
|
+
parts.push(`Duplicate ${String(this.idField)} values (${dupIdReports.length}):\n` +
|
|
60
|
+
dupIdReports.join('\n'));
|
|
61
|
+
}
|
|
62
|
+
if (collisions.length > 0) {
|
|
63
|
+
parts.push(`id↔name collisions (${collisions.length}):\n` +
|
|
64
|
+
collisions.join('\n'));
|
|
65
|
+
}
|
|
66
|
+
// eslint-disable-next-line unicorn/error-message
|
|
67
|
+
throw new Error(parts.join('\n\n'));
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
/** All items (stable array iteration) */
|
|
71
|
+
get all() {
|
|
72
|
+
return this.items;
|
|
73
|
+
}
|
|
74
|
+
idOf(t) {
|
|
75
|
+
return t[this.idField];
|
|
76
|
+
}
|
|
77
|
+
depsOf(t) {
|
|
78
|
+
return (t[this.depField] ?? []);
|
|
79
|
+
}
|
|
80
|
+
matches(ref, opts) {
|
|
81
|
+
const idHit = this.byId.get(ref);
|
|
82
|
+
if (idHit)
|
|
83
|
+
return opts?.multiple ? [idHit] : idHit;
|
|
84
|
+
const nameHits = this.byName.get(ref) ?? [];
|
|
85
|
+
if (nameHits.length === 0) {
|
|
86
|
+
throw new Error(`Unknown reference "${ref}"`);
|
|
87
|
+
}
|
|
88
|
+
if (opts?.multiple)
|
|
89
|
+
return nameHits;
|
|
90
|
+
if (nameHits.length > 1) {
|
|
91
|
+
const ids = nameHits.map((t) => this.idOf(t)).join(', ');
|
|
92
|
+
throw new Error(`Ambiguous reference "${ref}" matches multiple ids: [${ids}]`);
|
|
93
|
+
}
|
|
94
|
+
return nameHits[0];
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
/* ----------------- run-order helpers unchanged (for completeness) ---------------- */
|
|
98
|
+
function resolveRefSet(col, ref, policy) {
|
|
99
|
+
if (policy === 'runAll')
|
|
100
|
+
return col.matches(ref, { multiple: true }).map((t) => col.idOf(t));
|
|
101
|
+
return [col.idOf(col.matches(ref))];
|
|
102
|
+
}
|
|
103
|
+
function collectPredecessorClosure(g, seeds) {
|
|
104
|
+
const seen = new Set();
|
|
105
|
+
const q = [];
|
|
106
|
+
for (const s of seeds) {
|
|
107
|
+
if (!seen.has(s)) {
|
|
108
|
+
seen.add(s);
|
|
109
|
+
q.push(s);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
while (q.length > 0) {
|
|
113
|
+
const cur = q.shift();
|
|
114
|
+
for (const p of g.predecessors(cur) ?? [])
|
|
115
|
+
if (!seen.has(p)) {
|
|
116
|
+
seen.add(p);
|
|
117
|
+
q.push(p);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return seen;
|
|
121
|
+
}
|
|
122
|
+
function buildGraph(col, policy) {
|
|
123
|
+
const g = new graphlib.Graph({ directed: true });
|
|
124
|
+
for (const t of col.all)
|
|
125
|
+
g.setNode(col.idOf(t));
|
|
126
|
+
for (const t of col.all) {
|
|
127
|
+
const toId = col.idOf(t);
|
|
128
|
+
for (const ref of col.depsOf(t)) {
|
|
129
|
+
for (const fromId of resolveRefSet(col, ref, policy))
|
|
130
|
+
g.setEdge(fromId, toId);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return g;
|
|
134
|
+
}
|
|
135
|
+
export function findRunOrder(selection, collection, { onAmbiguous = 'error' } = {}) {
|
|
136
|
+
const g = buildGraph(collection, onAmbiguous);
|
|
137
|
+
const cycles = graphlib.alg.findCycles(g);
|
|
138
|
+
if (cycles.length > 0) {
|
|
139
|
+
throw new Error(`Circular dependencies detected: ${JSON.stringify(cycles)}`);
|
|
140
|
+
}
|
|
141
|
+
const selectedIds = new Set();
|
|
142
|
+
for (const ref of selection)
|
|
143
|
+
for (const id of resolveRefSet(collection, ref, onAmbiguous))
|
|
144
|
+
selectedIds.add(id);
|
|
145
|
+
if (selectedIds.size === 0)
|
|
146
|
+
throw new Error('Selection resolved to no items.');
|
|
147
|
+
const include = collectPredecessorClosure(g, selectedIds.values());
|
|
148
|
+
const sub = new graphlib.Graph({ directed: true });
|
|
149
|
+
for (const id of include)
|
|
150
|
+
sub.setNode(id);
|
|
151
|
+
for (const id of include)
|
|
152
|
+
for (const p of g.predecessors(id) ?? [])
|
|
153
|
+
if (include.has(p))
|
|
154
|
+
sub.setEdge(p, id);
|
|
155
|
+
const ids = graphlib.alg.topsort(sub);
|
|
156
|
+
const byId = new Map();
|
|
157
|
+
for (const t of collection.all)
|
|
158
|
+
byId.set(collection.idOf(t), t);
|
|
159
|
+
return ids.map((id) => {
|
|
160
|
+
const t = byId.get(id);
|
|
161
|
+
if (!t)
|
|
162
|
+
throw new Error(`Internal error: missing item for id "${id}"`);
|
|
163
|
+
return t;
|
|
164
|
+
});
|
|
165
|
+
}
|