@enspirit/emb 0.0.5 → 0.0.6

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 (53) hide show
  1. package/README.md +2 -19
  2. package/dist/src/cli/commands/config/print.js +1 -1
  3. package/dist/src/cli/commands/tasks/run.d.ts +0 -2
  4. package/dist/src/cli/commands/tasks/run.js +10 -59
  5. package/dist/src/config/convert.d.ts +2 -3
  6. package/dist/src/config/convert.js +2 -7
  7. package/dist/src/config/schema.d.ts +28 -28
  8. package/dist/src/config/schema.json +45 -50
  9. package/dist/src/config/types.d.ts +2 -2
  10. package/dist/src/config/validation.d.ts +2 -0
  11. package/dist/src/config/validation.js +26 -1
  12. package/dist/src/docker/operations/containers/ExecContainerOperation.d.ts +22 -0
  13. package/dist/src/docker/operations/containers/ExecContainerOperation.js +78 -0
  14. package/dist/src/docker/operations/containers/index.d.ts +1 -0
  15. package/dist/src/docker/operations/containers/index.js +1 -0
  16. package/dist/src/monorepo/config.d.ts +2 -1
  17. package/dist/src/monorepo/config.js +8 -2
  18. package/dist/src/monorepo/operations/components/GetComponentContainerOperation.d.ts +6 -0
  19. package/dist/src/monorepo/operations/components/GetComponentContainerOperation.js +21 -0
  20. package/dist/src/monorepo/operations/components/index.d.ts +1 -0
  21. package/dist/src/monorepo/operations/components/index.js +1 -0
  22. package/dist/src/monorepo/operations/index.d.ts +2 -0
  23. package/dist/src/monorepo/operations/index.js +2 -0
  24. package/dist/src/monorepo/operations/shell/ExecuteLocalCommandOperation.d.ts +18 -0
  25. package/dist/src/monorepo/operations/shell/ExecuteLocalCommandOperation.js +35 -0
  26. package/dist/src/monorepo/operations/shell/index.d.ts +1 -0
  27. package/dist/src/monorepo/operations/shell/index.js +1 -0
  28. package/dist/src/monorepo/operations/tasks/RunTaskOperation.d.ts +18 -0
  29. package/dist/src/monorepo/operations/tasks/RunTaskOperation.js +50 -0
  30. package/dist/src/monorepo/operations/tasks/index.d.ts +1 -0
  31. package/dist/src/monorepo/operations/tasks/index.js +1 -0
  32. package/dist/src/monorepo/plugins/ComponentDiscoverPlugin.d.ts +15 -0
  33. package/dist/src/monorepo/plugins/{ComponentsDiscover.js → ComponentDiscoverPlugin.js} +13 -1
  34. package/dist/src/monorepo/plugins/EmbfileLoaderPlugin.d.ts +14 -0
  35. package/dist/src/monorepo/plugins/EmbfileLoaderPlugin.js +33 -0
  36. package/dist/src/monorepo/plugins/index.d.ts +3 -2
  37. package/dist/src/monorepo/plugins/index.js +5 -2
  38. package/dist/src/operations/abstract/AbstractOperation.d.ts +1 -1
  39. package/dist/src/utils/deepMergeArray.d.ts +3 -0
  40. package/dist/src/utils/deepMergeArray.js +1 -0
  41. package/oclif.manifest.json +1 -35
  42. package/package.json +1 -1
  43. package/dist/src/cli/commands/run/index.d.ts +0 -10
  44. package/dist/src/cli/commands/run/index.js +0 -49
  45. package/dist/src/executors/docker.d.ts +0 -6
  46. package/dist/src/executors/docker.js +0 -14
  47. package/dist/src/executors/index.d.ts +0 -6
  48. package/dist/src/executors/index.js +0 -7
  49. package/dist/src/executors/shell.d.ts +0 -2
  50. package/dist/src/executors/shell.js +0 -14
  51. package/dist/src/executors/types.d.ts +0 -8
  52. package/dist/src/executors/types.js +0 -1
  53. package/dist/src/monorepo/plugins/ComponentsDiscover.d.ts +0 -6
package/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  emb (Enspirit's Monorepo Builder)
2
2
  =================
3
3
 
4
- A CLI to help on Enspirit monorepos. This aims at replacing our aging [Makefile for monorepos](https://github.com/enspirit/makefile-for-monorepos/pulls)
4
+ A CLI to help on Enspirit monorepos. This aims at replacing our aging [Makefile for monorepos](https://github.com/enspirit/makefile-for-monorepos)
5
5
 
6
6
  <!-- toc -->
7
7
  * [Usage](#usage)
@@ -14,7 +14,7 @@ $ npm install -g @enspirit/emb
14
14
  $ emb COMMAND
15
15
  running command...
16
16
  $ emb (--version)
17
- @enspirit/emb/0.0.5 darwin-x64 node-v22.12.0
17
+ @enspirit/emb/0.0.6 darwin-x64 node-v22.12.0
18
18
  $ emb --help [COMMAND]
19
19
  USAGE
20
20
  $ emb COMMAND
@@ -36,7 +36,6 @@ USAGE
36
36
  * [`emb images delete`](#emb-images-delete)
37
37
  * [`emb images prune`](#emb-images-prune)
38
38
  * [`emb ps`](#emb-ps)
39
- * [`emb run COMPONENT SCRIPT`](#emb-run-component-script)
40
39
  * [`emb tasks`](#emb-tasks)
41
40
  * [`emb tasks run [TASK]`](#emb-tasks-run-task)
42
41
  * [`emb up`](#emb-up)
@@ -323,22 +322,6 @@ EXAMPLES
323
322
  $ emb ps
324
323
  ```
325
324
 
326
- ## `emb run COMPONENT SCRIPT`
327
-
328
- Run an npm script from a component's package.json
329
-
330
- ```
331
- USAGE
332
- $ emb run COMPONENT SCRIPT
333
-
334
- ARGUMENTS
335
- COMPONENT Component name
336
- SCRIPT NPM script to run
337
-
338
- DESCRIPTION
339
- Run an npm script from a component's package.json
340
- ```
341
-
342
325
  ## `emb tasks`
343
326
 
344
327
  List tasks.
@@ -9,7 +9,7 @@ export default class ConfigPrint extends FlavoredCommand {
9
9
  const context = await getContext();
10
10
  const { monorepo } = context;
11
11
  if (!flags.json) {
12
- console.log(YAML.stringify(monorepo.config));
12
+ this.log(YAML.stringify(monorepo.config));
13
13
  }
14
14
  return monorepo.config;
15
15
  }
@@ -11,6 +11,4 @@ export default class RunTask extends Command {
11
11
  };
12
12
  static strict: boolean;
13
13
  run(): Promise<void>;
14
- private dockerExec;
15
- private shellExec;
16
14
  }
@@ -1,11 +1,7 @@
1
1
  import { getContext } from '../../../index.js';
2
2
  import { Args, Command, Flags } from '@oclif/core';
3
3
  import { Listr } from 'listr2';
4
- import { PassThrough } from 'node:stream';
5
- import { getContainer, ListContainersOperation } from '../../../docker/index.js';
6
- import { dockerExecutor } from '../../../executors/docker.js';
7
- import { ExecutorType } from '../../../executors/index.js';
8
- import { shellExecutor } from '../../../executors/shell.js';
4
+ import { ExecutorType, RunTaskOperation, } from '../../../monorepo/operations/tasks/RunTaskOperation.js';
9
5
  export default class RunTask extends Command {
10
6
  static args = {
11
7
  task: Args.string({
@@ -20,7 +16,7 @@ export default class RunTask extends Command {
20
16
  executor: Flags.string({
21
17
  char: 'x',
22
18
  name: 'executor',
23
- options: Object.keys(ExecutorType),
19
+ options: Object.values(ExecutorType),
24
20
  }),
25
21
  };
26
22
  static strict = false;
@@ -38,30 +34,17 @@ export default class RunTask extends Command {
38
34
  }, [])
39
35
  : monorepo.tasks;
40
36
  const runTasks = toRun.map((task) => {
37
+ const executor = flags.executor ||
38
+ (task.component ? ExecutorType.container : ExecutorType.local);
41
39
  return {
42
40
  rendererOptions: { persistentOutput: true },
43
- task: async (_ctx, listrTask) => {
44
- const type = flags.executor
45
- ? flags.executor
46
- : ExecutorType.container;
47
- const logStream = await monorepo.store.createWriteStream(`logs/tasks/run/${task.id}.log`);
48
- // Gonna log on both the logStream and stdout
49
- const tee = new PassThrough();
50
- tee.pipe(listrTask.stdout());
51
- tee.pipe(logStream);
52
- switch (type) {
53
- case ExecutorType.container: {
54
- return this.dockerExec(task, tee);
55
- }
56
- case ExecutorType.local: {
57
- return this.shellExec(task, tee);
58
- }
59
- default: {
60
- throw new Error(`Unsupported executor: ${type}`);
61
- }
62
- }
41
+ async task(_ctx, listrTask) {
42
+ await monorepo.run(new RunTaskOperation(listrTask.stdout()), {
43
+ executor,
44
+ task,
45
+ });
63
46
  },
64
- title: `Running ${task.id}`,
47
+ title: `Running ${task.id} (${executor})`,
65
48
  };
66
49
  });
67
50
  const runner = new Listr([
@@ -78,36 +61,4 @@ export default class RunTask extends Command {
78
61
  ]);
79
62
  await runner.run();
80
63
  }
81
- async dockerExec(task, out) {
82
- const { monorepo } = getContext();
83
- const matching = await monorepo.run(new ListContainersOperation(), {
84
- filters: {
85
- label: [
86
- `emb/project=${monorepo.name}`,
87
- `emb/component=${task.component}`,
88
- ],
89
- },
90
- });
91
- if (matching.length === 0) {
92
- throw new Error(`Could not find a running container for '${task.component}'`);
93
- }
94
- if (matching.length > 1) {
95
- throw new Error(`More than one running container found for '${task.component}'`);
96
- }
97
- const container = await getContainer(matching[0].Id);
98
- return dockerExecutor.run(task.script, {
99
- container,
100
- out,
101
- });
102
- }
103
- async shellExec(task, out) {
104
- const { monorepo } = getContext();
105
- const cwd = task.component
106
- ? monorepo.component(task.component).rootdir
107
- : monorepo.rootDir;
108
- return shellExecutor.run(task.script, {
109
- cwd,
110
- out,
111
- });
112
- }
113
64
  }
@@ -1,5 +1,4 @@
1
- import { Component, Flavor } from './schema.js';
2
- import { ComponentConfig, FlavorConfig, IMonorepoConfig, UserConfig } from './types.js';
1
+ import { Flavor } from './schema.js';
2
+ import { FlavorConfig, IMonorepoConfig, UserConfig } from './types.js';
3
3
  export declare const toFlavor: (flavor: Flavor) => FlavorConfig;
4
- export declare const toComponent: (cmp: Component) => ComponentConfig;
5
4
  export declare const toProjectConfig: (config: UserConfig, rootDir?: string) => IMonorepoConfig;
@@ -3,14 +3,9 @@ import { cwd } from 'node:process';
3
3
  export const toFlavor = (flavor) => {
4
4
  return {
5
5
  ...flavor,
6
- components: flavor.components?.map(toComponent),
6
+ components: flavor.components,
7
7
  };
8
8
  };
9
- export const toComponent = (cmp) => {
10
- return typeof cmp === 'string'
11
- ? { context: cmp, name: cmp }
12
- : cmp;
13
- };
14
9
  export const toProjectConfig = (config, rootDir) => {
15
10
  const project = typeof config.project === 'string'
16
11
  ? { name: config.project }
@@ -23,7 +18,7 @@ export const toProjectConfig = (config, rootDir) => {
23
18
  else {
24
19
  project.rootDir = rootDir || cwd();
25
20
  }
26
- const components = (config.components || []).map((cmp) => toComponent(cmp));
21
+ const components = config.components || [];
27
22
  const { defaults, env, flavors, plugins, vars } = config;
28
23
  return {
29
24
  components,
@@ -3,33 +3,6 @@
3
3
  * DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file,
4
4
  * and run json-schema-to-typescript to regenerate this file.
5
5
  */
6
- export type Component = string | {
7
- /**
8
- * The name of the component.
9
- */
10
- name: string;
11
- /**
12
- * The directory of the component.
13
- */
14
- context?: string;
15
- /**
16
- * A description of the component.
17
- */
18
- description?: string;
19
- buildArgs?: {
20
- [k: string]: unknown;
21
- };
22
- dependencies?: string[];
23
- /**
24
- * The stage to target for the build
25
- */
26
- target?: string;
27
- /**
28
- * The Dockerfile to use
29
- */
30
- dockerfile?: string;
31
- tasks?: Task[];
32
- };
33
6
  export interface EMBConfigSchema {
34
7
  project: string | {
35
8
  /**
@@ -72,7 +45,7 @@ export interface Defaults {
72
45
  tag?: string;
73
46
  target?: string;
74
47
  buildArgs?: {
75
- [k: string]: unknown;
48
+ [k: string]: string;
76
49
  };
77
50
  labels?: {
78
51
  [k: string]: string;
@@ -80,6 +53,33 @@ export interface Defaults {
80
53
  [k: string]: unknown;
81
54
  };
82
55
  }
56
+ export interface Component {
57
+ /**
58
+ * The name of the component.
59
+ */
60
+ name: string;
61
+ /**
62
+ * The directory of the component.
63
+ */
64
+ context?: string;
65
+ /**
66
+ * A description of the component.
67
+ */
68
+ description?: string;
69
+ buildArgs?: {
70
+ [k: string]: string;
71
+ };
72
+ dependencies?: string[];
73
+ /**
74
+ * The stage to target for the build
75
+ */
76
+ target?: string;
77
+ /**
78
+ * The Dockerfile to use
79
+ */
80
+ dockerfile?: string;
81
+ tasks?: Task[];
82
+ }
83
83
  export interface Task {
84
84
  name: string;
85
85
  description?: string;
@@ -88,7 +88,9 @@
88
88
  "target": { "type": "string" },
89
89
  "buildArgs": {
90
90
  "type": "object",
91
- "additionalProperties": true
91
+ "additionalProperties": {
92
+ "type": "string"
93
+ }
92
94
  },
93
95
  "labels": {
94
96
  "type": "object",
@@ -124,60 +126,53 @@
124
126
  "additionalProperties": false
125
127
  },
126
128
  "component": {
127
- "oneOf": [
128
- {
129
+ "type": "object",
130
+ "required": ["name"],
131
+ "properties": {
132
+ "name": {
129
133
  "type": "string",
130
134
  "minLength": 3,
131
- "description": "Shortcut for specifying the component name."
135
+ "description": "The name of the component."
132
136
  },
133
- {
137
+ "context": {
138
+ "type": "string",
139
+ "minLength": 1,
140
+ "description": "The directory of the component."
141
+ },
142
+ "description": {
143
+ "type": "string",
144
+ "minLength": 1,
145
+ "description": "A description of the component."
146
+ },
147
+ "buildArgs": {
134
148
  "type": "object",
135
- "required": ["name"],
136
- "properties": {
137
- "name": {
138
- "type": "string",
139
- "minLength": 3,
140
- "description": "The name of the component."
141
- },
142
- "context": {
143
- "type": "string",
144
- "minLength": 1,
145
- "description": "The directory of the component."
146
- },
147
- "description": {
148
- "type": "string",
149
- "minLength": 1,
150
- "description": "A description of the component."
151
- },
152
- "buildArgs": {
153
- "type": "object",
154
- "properties": {},
155
- "additionalProperties": true
156
- },
157
- "dependencies": {
158
- "type": "array",
159
- "items": {
160
- "type": "string"
161
- }
162
- },
163
- "target": {
164
- "type": "string",
165
- "description": "The stage to target for the build"
166
- },
167
- "dockerfile": {
168
- "type": "string",
169
- "description": "The Dockerfile to use"
170
- },
171
- "tasks": {
172
- "type": "array",
173
- "items": {
174
- "$ref": "#/$defs/task"
175
- }
176
- }
177
- },
178
- "additionalProperties": false
149
+ "properties": {},
150
+ "additionalProperties": {
151
+ "type": "string"
152
+ }
153
+ },
154
+ "dependencies": {
155
+ "type": "array",
156
+ "items": {
157
+ "type": "string"
158
+ }
159
+ },
160
+ "target": {
161
+ "type": "string",
162
+ "description": "The stage to target for the build"
163
+ },
164
+ "dockerfile": {
165
+ "type": "string",
166
+ "description": "The Dockerfile to use"
167
+ },
168
+ "tasks": {
169
+ "type": "array",
170
+ "items": {
171
+ "$ref": "#/$defs/task"
172
+ }
179
173
  }
180
- ]
174
+ },
175
+ "additionalProperties": false
181
176
  },
182
177
  "flavor": {
183
178
  "type": "object",
@@ -5,7 +5,7 @@ export type IProjectConfig = {
5
5
  rootDir: string;
6
6
  };
7
7
  export type ComponentConfig = {
8
- buildArgs?: Record<PropertyKey, string>;
8
+ buildArgs?: Record<string, string>;
9
9
  context?: string;
10
10
  dependencies?: Array<string>;
11
11
  dockerfile?: string;
@@ -16,7 +16,7 @@ export type ComponentConfig = {
16
16
  };
17
17
  export type DefaultSettings = {
18
18
  docker?: {
19
- buildArgs?: Record<string, unknown>;
19
+ buildArgs?: Record<string, string>;
20
20
  labels?: Record<string, string>;
21
21
  tag?: string;
22
22
  target?: string;
@@ -1 +1,3 @@
1
+ import { Component } from './schema.js';
1
2
  export declare const validateUserConfig: (pathOrObject: string | unknown) => Promise<import("./types.js").IMonorepoConfig>;
3
+ export declare const validateEmbfile: (pathOrObject: string | unknown) => Promise<Component>;
@@ -3,8 +3,9 @@ import { readFile, stat } from 'node:fs/promises';
3
3
  import yaml from 'yaml';
4
4
  import { toProjectConfig } from './convert.js';
5
5
  import configSchema from './schema.json' with { type: 'json' };
6
+ const ajv = new Ajv();
7
+ ajv.addSchema(configSchema);
6
8
  export const validateUserConfig = async (pathOrObject) => {
7
- const ajv = new Ajv();
8
9
  let userConfig;
9
10
  if (typeof pathOrObject === 'string') {
10
11
  if (await stat(pathOrObject)) {
@@ -24,3 +25,27 @@ export const validateUserConfig = async (pathOrObject) => {
24
25
  }
25
26
  return toProjectConfig(userConfig);
26
27
  };
28
+ export const validateEmbfile = async (pathOrObject) => {
29
+ let component;
30
+ if (typeof pathOrObject === 'string') {
31
+ if (await stat(pathOrObject)) {
32
+ const cfgYaml = (await readFile(pathOrObject)).toString();
33
+ component = yaml.parse(cfgYaml.toString());
34
+ }
35
+ else {
36
+ throw new Error(`Could not find file: ${pathOrObject}`);
37
+ }
38
+ }
39
+ else {
40
+ component = pathOrObject;
41
+ }
42
+ const validate = ajv.getSchema('/schemas/config#/$defs/component');
43
+ if (!validate) {
44
+ throw new Error('Could not find the JSON schema validator for Embfile');
45
+ }
46
+ if (!validate(component)) {
47
+ ajv.errors.forEach((err) => console.error(err));
48
+ throw new Error(`Your .emb.yml is incorrect`);
49
+ }
50
+ return component;
51
+ };
@@ -0,0 +1,22 @@
1
+ import { Writable } from 'node:stream';
2
+ import * as z from 'zod';
3
+ import { AbstractOperation } from '../../../operations/index.js';
4
+ /**
5
+ * https://docs.docker.com/reference/api/engine/version/v1.37/#tag/Exec/operation/ContainerExec
6
+ */
7
+ declare const schema: z.ZodObject<{
8
+ attachStderr: z.ZodOptional<z.ZodDefault<z.ZodBoolean>>;
9
+ attachStdin: z.ZodOptional<z.ZodDefault<z.ZodBoolean>>;
10
+ attachStdout: z.ZodOptional<z.ZodDefault<z.ZodBoolean>>;
11
+ container: z.ZodString;
12
+ env: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
13
+ script: z.ZodString;
14
+ tty: z.ZodOptional<z.ZodDefault<z.ZodBoolean>>;
15
+ workingDir: z.ZodOptional<z.ZodString>;
16
+ }, z.core.$strip>;
17
+ export declare class ContainerExecOperation extends AbstractOperation<typeof schema, void> {
18
+ protected out?: Writable | undefined;
19
+ constructor(out?: Writable | undefined);
20
+ protected _run(input: z.input<typeof schema>): Promise<void>;
21
+ }
22
+ export {};
@@ -0,0 +1,78 @@
1
+ import * as z from 'zod';
2
+ import { AbstractOperation } from '../../../operations/index.js';
3
+ /**
4
+ * https://docs.docker.com/reference/api/engine/version/v1.37/#tag/Exec/operation/ContainerExec
5
+ */
6
+ const schema = z.object({
7
+ attachStderr: z
8
+ .boolean()
9
+ .default(false)
10
+ .optional()
11
+ .describe('Attach to `stderr` of the exec command.'),
12
+ attachStdin: z
13
+ .boolean()
14
+ .default(false)
15
+ .optional()
16
+ .describe('Attach to `stdin` of the exec command.'),
17
+ attachStdout: z
18
+ .boolean()
19
+ .default(false)
20
+ .optional()
21
+ .describe('Attach to `stdout` of the exec command.'),
22
+ container: z.string().describe('ID or name of the container'),
23
+ env: z
24
+ .record(z.string(), z.string())
25
+ .optional()
26
+ .describe('A list of environment variables in the form'),
27
+ script: z.string().describe('Command to run, as a string'),
28
+ tty: z.boolean().default(false).optional().describe('Allocate a pseudo-TTY'),
29
+ workingDir: z
30
+ .string()
31
+ .optional()
32
+ .describe('The working directory for the exec process inside the container'),
33
+ });
34
+ export class ContainerExecOperation extends AbstractOperation {
35
+ out;
36
+ constructor(out) {
37
+ super(schema);
38
+ this.out = out;
39
+ }
40
+ async _run(input) {
41
+ const container = await this.context.docker.getContainer(input.container);
42
+ const envVars = Object.entries(input.env || {}).reduce((arr, [key, value]) => {
43
+ return [...arr, `${key}=${value}`];
44
+ }, []);
45
+ const options = {
46
+ AttachStderr: input.attachStderr,
47
+ AttachStdin: input.attachStdin,
48
+ AttachStdout: input.attachStdout,
49
+ Cmd: ['bash', '-eu', '-o', 'pipefail', '-c', input.script],
50
+ Env: envVars,
51
+ Tty: input.tty,
52
+ WorkingDir: input.workingDir,
53
+ };
54
+ const exec = await container.exec(options);
55
+ const stream = await exec.start({});
56
+ container.modem.demuxStream(stream, this.out || process.stdout, this.out || process.stderr);
57
+ await new Promise((resolve, reject) => {
58
+ const onError = (err) => reject(err);
59
+ const onEnd = async () => {
60
+ exec.inspect((error, res) => {
61
+ if (error)
62
+ return reject(error);
63
+ const code = res?.ExitCode ?? 0;
64
+ if (code !== 0) {
65
+ const msg = res?.ProcessConfig?.entrypoint
66
+ ? `container exec failed (exit ${code})`
67
+ : `command failed (exit ${code})`;
68
+ return reject(new Error(msg));
69
+ }
70
+ resolve();
71
+ });
72
+ };
73
+ stream.on('error', onError);
74
+ stream.on('end', onEnd);
75
+ stream.on('close', onEnd); // some engines emit 'close' not 'end'
76
+ });
77
+ }
78
+ }
@@ -1,2 +1,3 @@
1
+ export * from './ExecContainerOperation.js';
1
2
  export * from './ListContainersOperation.js';
2
3
  export * from './PruneContainersOperation.js';
@@ -1,2 +1,3 @@
1
+ export * from './ExecContainerOperation.js';
1
2
  export * from './ListContainersOperation.js';
2
3
  export * from './PruneContainersOperation.js';
@@ -1,13 +1,14 @@
1
1
  import { ComponentConfig, DefaultSettings, FlavorConfig, IMonorepoConfig, IProjectConfig, PluginConfig } from '../config/index.js';
2
2
  export declare class MonorepoConfig implements IMonorepoConfig {
3
- components: ComponentConfig[];
4
3
  defaults: DefaultSettings;
5
4
  env: Record<string, string>;
6
5
  flavors: Array<FlavorConfig>;
7
6
  plugins: Array<PluginConfig>;
8
7
  project: IProjectConfig;
9
8
  vars: Record<string, unknown>;
9
+ private _components;
10
10
  constructor(config: IMonorepoConfig);
11
+ get components(): ComponentConfig[];
11
12
  component(name: string): ComponentConfig;
12
13
  flavor(name: string): FlavorConfig;
13
14
  toJSON(): Required<IMonorepoConfig>;
@@ -1,15 +1,18 @@
1
1
  import deepMerge from '@fastify/deepmerge';
2
2
  import { deepMergeArray } from '../utils/index.js';
3
3
  export class MonorepoConfig {
4
- components;
5
4
  defaults;
6
5
  env;
7
6
  flavors;
8
7
  plugins;
9
8
  project;
10
9
  vars;
10
+ _components;
11
11
  constructor(config) {
12
- this.components = config.components;
12
+ this._components = config.components.reduce((map, cmp) => {
13
+ map.set(cmp.name, cmp);
14
+ return map;
15
+ }, new Map());
13
16
  this.defaults = config.defaults || {};
14
17
  this.project = config.project;
15
18
  this.vars = config.vars || {};
@@ -17,6 +20,9 @@ export class MonorepoConfig {
17
20
  this.env = config.env || {};
18
21
  this.plugins = config.plugins || [];
19
22
  }
23
+ get components() {
24
+ return [...this._components.values()];
25
+ }
20
26
  component(name) {
21
27
  const config = this.components.find((c) => c.name === name);
22
28
  if (!config) {
@@ -0,0 +1,6 @@
1
+ import { ContainerInfo } from 'dockerode';
2
+ import { Component } from '../../component.js';
3
+ import { IOperation } from '../../../operations/index.js';
4
+ export declare class GetComponentContainerOperation implements IOperation<Component, ContainerInfo> {
5
+ run(component: Component | string): Promise<ContainerInfo>;
6
+ }
@@ -0,0 +1,21 @@
1
+ import { getContext } from '../../../index.js';
2
+ import { ListContainersOperation } from '../../../docker/index.js';
3
+ import { Component } from '../../component.js';
4
+ export class GetComponentContainerOperation {
5
+ async run(component) {
6
+ const { monorepo } = getContext();
7
+ const cmpName = component instanceof Component ? component.name : component;
8
+ const matching = await monorepo.run(new ListContainersOperation(), {
9
+ filters: {
10
+ label: [`emb/project=${monorepo.name}`, `emb/component=${cmpName}`],
11
+ },
12
+ });
13
+ if (matching.length === 0) {
14
+ throw new Error(`Could not find a running container for '${cmpName}'`);
15
+ }
16
+ if (matching.length > 1) {
17
+ throw new Error(`More than one running container found for '${cmpName}'`);
18
+ }
19
+ return matching[0];
20
+ }
21
+ }
@@ -1 +1,2 @@
1
1
  export * from './BuildComponentsOperation.js';
2
+ export * from './GetComponentContainerOperation.js';
@@ -1 +1,2 @@
1
1
  export * from './BuildComponentsOperation.js';
2
+ export * from './GetComponentContainerOperation.js';
@@ -1 +1,3 @@
1
1
  export * from './components/index.js';
2
+ export * from './shell/index.js';
3
+ export * from './tasks/index.js';
@@ -1 +1,3 @@
1
1
  export * from './components/index.js';
2
+ export * from './shell/index.js';
3
+ export * from './tasks/index.js';
@@ -0,0 +1,18 @@
1
+ import { ResultPromise } from 'execa';
2
+ import { Writable } from 'node:stream';
3
+ import * as z from 'zod';
4
+ import { AbstractOperation } from '../../../operations/index.js';
5
+ /**
6
+ * https://docs.docker.com/reference/api/engine/version/v1.37/#tag/Exec/operation/ContainerExec
7
+ */
8
+ declare const schema: z.ZodObject<{
9
+ env: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
10
+ script: z.ZodString;
11
+ workingDir: z.ZodOptional<z.ZodString>;
12
+ }, z.core.$strip>;
13
+ export declare class ExecuteLocalCommandOperation extends AbstractOperation<typeof schema, ResultPromise> {
14
+ protected out?: Writable | undefined;
15
+ constructor(out?: Writable | undefined);
16
+ protected _run(input: z.input<typeof schema>): Promise<ResultPromise>;
17
+ }
18
+ export {};
@@ -0,0 +1,35 @@
1
+ import { execa } from 'execa';
2
+ import * as z from 'zod';
3
+ import { AbstractOperation } from '../../../operations/index.js';
4
+ /**
5
+ * https://docs.docker.com/reference/api/engine/version/v1.37/#tag/Exec/operation/ContainerExec
6
+ */
7
+ const schema = z.object({
8
+ env: z
9
+ .record(z.string(), z.string())
10
+ .optional()
11
+ .describe('A list of environment variables in the form'),
12
+ script: z.string().describe('Command to run, as a string'),
13
+ workingDir: z
14
+ .string()
15
+ .optional()
16
+ .describe('The working directory for the exec process inside the container'),
17
+ });
18
+ export class ExecuteLocalCommandOperation extends AbstractOperation {
19
+ out;
20
+ constructor(out) {
21
+ super(schema);
22
+ this.out = out;
23
+ }
24
+ async _run(input) {
25
+ const process = execa(input.script, {
26
+ all: true,
27
+ cwd: input.workingDir,
28
+ shell: true,
29
+ });
30
+ if (this.out) {
31
+ process.all?.pipe(this.out);
32
+ }
33
+ return process;
34
+ }
35
+ }
@@ -0,0 +1 @@
1
+ export * from './ExecuteLocalCommandOperation.js';
@@ -0,0 +1 @@
1
+ export * from './ExecuteLocalCommandOperation.js';
@@ -0,0 +1,18 @@
1
+ import { Writable } from 'node:stream';
2
+ import { TaskInfo } from '../../types.js';
3
+ import { IOperation } from '../../../operations/types.js';
4
+ export declare enum ExecutorType {
5
+ container = "container",
6
+ local = "local"
7
+ }
8
+ export type RunTaskOperationParams = {
9
+ executor: ExecutorType;
10
+ task: TaskInfo;
11
+ };
12
+ export declare class RunTaskOperation implements IOperation<RunTaskOperationParams, void> {
13
+ protected out?: Writable | undefined;
14
+ constructor(out?: Writable | undefined);
15
+ run(params: RunTaskOperationParams): Promise<void>;
16
+ private runDocker;
17
+ private runLocal;
18
+ }
@@ -0,0 +1,50 @@
1
+ import { getContext } from '../../../index.js';
2
+ import { ContainerExecOperation } from '../../../docker/index.js';
3
+ import { ExecuteLocalCommandOperation, GetComponentContainerOperation, } from '../index.js';
4
+ export var ExecutorType;
5
+ (function (ExecutorType) {
6
+ ExecutorType["container"] = "container";
7
+ ExecutorType["local"] = "local";
8
+ })(ExecutorType || (ExecutorType = {}));
9
+ export class RunTaskOperation {
10
+ out;
11
+ constructor(out) {
12
+ this.out = out;
13
+ }
14
+ async run(params) {
15
+ switch (params.executor) {
16
+ case ExecutorType.container: {
17
+ return this.runDocker(params);
18
+ }
19
+ case ExecutorType.local: {
20
+ return this.runLocal(params);
21
+ }
22
+ default: {
23
+ throw new Error(`Unsupported executor type: ${params.executor}`);
24
+ }
25
+ }
26
+ }
27
+ async runDocker(params) {
28
+ const { monorepo } = getContext();
29
+ if (!params.task.component) {
30
+ throw new Error(`Support for non-component tasks not implemented`);
31
+ }
32
+ const containerInfo = await monorepo.run(new GetComponentContainerOperation(), params.task.component);
33
+ await monorepo.run(new ContainerExecOperation(this.out), {
34
+ attachStderr: true,
35
+ attachStdout: true,
36
+ container: containerInfo.Id,
37
+ script: params.task.script,
38
+ });
39
+ }
40
+ async runLocal(params) {
41
+ const { monorepo } = getContext();
42
+ const cwd = params.task.component
43
+ ? monorepo.component(params.task.component).rootdir
44
+ : monorepo.rootDir;
45
+ await monorepo.run(new ExecuteLocalCommandOperation(this.out), {
46
+ script: params.task.script,
47
+ workingDir: cwd,
48
+ });
49
+ }
50
+ }
@@ -0,0 +1 @@
1
+ export * from './RunTaskOperation.js';
@@ -0,0 +1 @@
1
+ export * from './RunTaskOperation.js';
@@ -0,0 +1,15 @@
1
+ import { Monorepo, MonorepoConfig } from '../index.js';
2
+ import { AbstractPlugin } from './plugin.js';
3
+ export type ComponentDiscoverPluginOptions = {
4
+ glob?: string;
5
+ ignore?: string | string[];
6
+ };
7
+ export declare const ComponentDiscoverPluginDefaultOptions: {
8
+ glob: string;
9
+ };
10
+ export declare class ComponentDiscoverPlugin extends AbstractPlugin<ComponentDiscoverPluginOptions> {
11
+ protected monorepo: Monorepo;
12
+ static name: string;
13
+ constructor(config: Partial<ComponentDiscoverPluginOptions>, monorepo: Monorepo);
14
+ extendConfig(config: MonorepoConfig): Promise<MonorepoConfig>;
15
+ }
@@ -3,10 +3,22 @@ import { glob } from 'glob';
3
3
  import { dirname } from 'node:path';
4
4
  import { MonorepoConfig } from '../index.js';
5
5
  import { AbstractPlugin } from './plugin.js';
6
+ export const ComponentDiscoverPluginDefaultOptions = {
7
+ glob: '*/Dockerfile',
8
+ };
6
9
  export class ComponentDiscoverPlugin extends AbstractPlugin {
10
+ monorepo;
7
11
  static name = 'autodiscover';
12
+ constructor(config, monorepo) {
13
+ super({
14
+ ...ComponentDiscoverPluginDefaultOptions,
15
+ ...config,
16
+ }, monorepo);
17
+ this.monorepo = monorepo;
18
+ }
8
19
  async extendConfig(config) {
9
- const files = await glob('*/Dockerfile', {
20
+ const files = await glob(this.config.glob || ComponentDiscoverPluginDefaultOptions.glob, {
21
+ ...this.config,
10
22
  cwd: config.project.rootDir,
11
23
  });
12
24
  const overrides = files.map((path) => {
@@ -0,0 +1,14 @@
1
+ import { Monorepo, MonorepoConfig } from '../index.js';
2
+ import { AbstractPlugin } from './plugin.js';
3
+ export type EmbfileLoaderPluginOptions = {
4
+ glob?: string;
5
+ };
6
+ export declare const EmbfileLoaderPluginDefaultOptions: {
7
+ glob: string;
8
+ };
9
+ export declare class EmbfileLoaderPlugin extends AbstractPlugin<EmbfileLoaderPluginOptions> {
10
+ protected monorepo: Monorepo;
11
+ static name: string;
12
+ constructor(config: Partial<EmbfileLoaderPluginOptions>, monorepo: Monorepo);
13
+ extendConfig(config: MonorepoConfig): Promise<MonorepoConfig>;
14
+ }
@@ -0,0 +1,33 @@
1
+ import { glob } from 'glob';
2
+ import { join } from 'node:path';
3
+ import { validateEmbfile } from '../../config/index.js';
4
+ import { AbstractPlugin } from './plugin.js';
5
+ export const EmbfileLoaderPluginDefaultOptions = {
6
+ glob: '*/Embfile.{yaml,yml}',
7
+ };
8
+ export class EmbfileLoaderPlugin extends AbstractPlugin {
9
+ monorepo;
10
+ static name = 'embfiles';
11
+ constructor(config, monorepo) {
12
+ super({
13
+ ...EmbfileLoaderPluginDefaultOptions,
14
+ ...config,
15
+ }, monorepo);
16
+ this.monorepo = monorepo;
17
+ }
18
+ async extendConfig(config) {
19
+ const files = await glob(this.config.glob || EmbfileLoaderPluginDefaultOptions.glob, {
20
+ ...this.config,
21
+ cwd: config.project.rootDir,
22
+ });
23
+ const newConfig = await files.reduce(async (pConfig, path) => {
24
+ const config = await pConfig;
25
+ const embfile = await join(config.project.rootDir, path);
26
+ const component = await validateEmbfile(embfile);
27
+ return config.with({
28
+ components: [component],
29
+ });
30
+ }, Promise.resolve(config));
31
+ return newConfig;
32
+ }
33
+ }
@@ -1,7 +1,8 @@
1
1
  import { AbstractPlugin } from './plugin.js';
2
- export * from './ComponentsDiscover.js';
2
+ export * from './ComponentDiscoverPlugin.js';
3
3
  export * from './DotEnvPlugin.js';
4
- import { Monorepo } from '../monorepo.js';
4
+ export * from './EmbfileLoaderPlugin.js';
5
+ import { Monorepo } from '../index.js';
5
6
  export type AbstractPluginConstructor = new <C, P extends AbstractPlugin<C>>(config: C, monorepo: Monorepo) => P;
6
7
  export declare const registerPlugin: (plugin: AbstractPluginConstructor) => void;
7
8
  export declare const getPlugin: (name: string) => AbstractPluginConstructor;
@@ -1,7 +1,9 @@
1
- export * from './ComponentsDiscover.js';
1
+ export * from './ComponentDiscoverPlugin.js';
2
2
  export * from './DotEnvPlugin.js';
3
- import { ComponentDiscoverPlugin } from './ComponentsDiscover.js';
3
+ export * from './EmbfileLoaderPlugin.js';
4
+ import { ComponentDiscoverPlugin } from './ComponentDiscoverPlugin.js';
4
5
  import { DotEnvPlugin } from './DotEnvPlugin.js';
6
+ import { EmbfileLoaderPlugin } from './EmbfileLoaderPlugin.js';
5
7
  const PluginRegistry = new Map();
6
8
  export const registerPlugin = (plugin) => {
7
9
  if (PluginRegistry.has(plugin.name)) {
@@ -18,3 +20,4 @@ export const getPlugin = (name) => {
18
20
  /** Not sure why we need casting */
19
21
  registerPlugin(ComponentDiscoverPlugin);
20
22
  registerPlugin(DotEnvPlugin);
23
+ registerPlugin(EmbfileLoaderPlugin);
@@ -1,6 +1,6 @@
1
1
  import { EmbContext } from '../../index.js';
2
2
  import * as z from 'zod';
3
- import { IOperation } from '../types.js';
3
+ import { IOperation } from '../index.js';
4
4
  export declare abstract class AbstractOperation<S extends z.Schema, O = unknown> implements IOperation<z.infer<S>, O> {
5
5
  protected inputSchema: S;
6
6
  protected context: EmbContext;
@@ -1 +1,4 @@
1
+ export declare const byName: <T extends {
2
+ name: string;
3
+ }>(item: T) => string;
1
4
  export declare const deepMergeArray: <T>(target: Array<T>, source: Array<T>, identifierFn?: (item: T) => unknown) => Array<T>;
@@ -1,4 +1,5 @@
1
1
  import deepmerge from '@fastify/deepmerge';
2
+ export const byName = (item) => item.name;
2
3
  export const deepMergeArray = (target, source, identifierFn) => {
3
4
  if (!identifierFn) {
4
5
  return deepmerge()(target, source);
@@ -450,40 +450,6 @@
450
450
  "prune.js"
451
451
  ]
452
452
  },
453
- "run": {
454
- "aliases": [],
455
- "args": {
456
- "component": {
457
- "description": "Component name",
458
- "name": "component",
459
- "required": true
460
- },
461
- "script": {
462
- "description": "NPM script to run",
463
- "name": "script",
464
- "required": true
465
- }
466
- },
467
- "description": "Run an npm script from a component's package.json",
468
- "flags": {},
469
- "hasDynamicHelp": false,
470
- "hiddenAliases": [],
471
- "id": "run",
472
- "pluginAlias": "@enspirit/emb",
473
- "pluginName": "@enspirit/emb",
474
- "pluginType": "core",
475
- "strict": true,
476
- "enableJsonFlag": false,
477
- "isESM": true,
478
- "relativePath": [
479
- "dist",
480
- "src",
481
- "cli",
482
- "commands",
483
- "run",
484
- "index.js"
485
- ]
486
- },
487
453
  "tasks": {
488
454
  "aliases": [],
489
455
  "args": {},
@@ -570,5 +536,5 @@
570
536
  ]
571
537
  }
572
538
  },
573
- "version": "0.0.5"
539
+ "version": "0.0.6"
574
540
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@enspirit/emb",
3
3
  "type": "module",
4
- "version": "0.0.5",
4
+ "version": "0.0.6",
5
5
  "keywords": ["monorepo", "docker", "taskrunner", "ci", "docker compose", "sentinel", "makefile"],
6
6
  "author": "Louis Lambeau <louis.lambeau@enspirit.be>",
7
7
  "license": "ISC",
@@ -1,10 +0,0 @@
1
- import { Command } from '@oclif/core';
2
- export default class RunComponentScript extends Command {
3
- static args: {
4
- component: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
5
- script: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
6
- };
7
- static description: string;
8
- static strict: boolean;
9
- run(): Promise<void>;
10
- }
@@ -1,49 +0,0 @@
1
- import { getContext } from '../../../index.js';
2
- import { Args, Command } from '@oclif/core';
3
- import { Listr } from 'listr2';
4
- import { spawn } from 'node:child_process';
5
- import fs from 'node:fs/promises';
6
- export default class RunComponentScript extends Command {
7
- static args = {
8
- component: Args.string({
9
- description: 'Component name',
10
- required: true,
11
- }),
12
- script: Args.string({ description: 'NPM script to run', required: true }),
13
- };
14
- static description = "Run an npm script from a component's package.json";
15
- static strict = true;
16
- async run() {
17
- const { args } = await this.parse(RunComponentScript);
18
- const { monorepo } = getContext();
19
- const component = monorepo.component(args.component);
20
- const pkgPath = component.join('package.json');
21
- const tasks = new Listr([
22
- {
23
- rendererOptions: {
24
- persistentOutput: true,
25
- },
26
- async task(_ctx, _task) {
27
- try {
28
- const pkgRaw = await fs.readFile(pkgPath, 'utf8');
29
- const pkg = JSON.parse(pkgRaw);
30
- if (!pkg.scripts?.[args.script]) {
31
- throw new Error(`Script "${args.script}" not found in ${component.name}/package.json`);
32
- }
33
- return spawn('npm', ['run', args.script], {
34
- cwd: component.rootdir,
35
- }).stdout;
36
- }
37
- catch (error) {
38
- const error_ = error instanceof Error
39
- ? new TypeError(`Failed to run ${component.name}:${args.script}\n${error.message}`)
40
- : new Error(`Failed to run ${component.name}:${args.script}\n${error}`);
41
- throw error_;
42
- }
43
- },
44
- title: `Running npm script '${args.script}' on ${args.component}`,
45
- },
46
- ], { concurrent: false });
47
- await tasks.run();
48
- }
49
- }
@@ -1,6 +0,0 @@
1
- import { Container } from 'dockerode';
2
- import { Executor, ExecutorRunOptions } from './types.js';
3
- export type DockerExecutorRunOptions = ExecutorRunOptions & {
4
- container: Container;
5
- };
6
- export declare const dockerExecutor: Executor<DockerExecutorRunOptions>;
@@ -1,14 +0,0 @@
1
- export const dockerExecutor = {
2
- async run(script, options) {
3
- const exec = await options.container.exec({
4
- AttachStderr: true,
5
- AttachStdout: true,
6
- Cmd: ['bash', '-c', script],
7
- });
8
- const stream = await exec.start({});
9
- if (options.out) {
10
- options.container.modem.demuxStream(stream, options.out, options.out);
11
- }
12
- return stream;
13
- },
14
- };
@@ -1,6 +0,0 @@
1
- export * from './docker.js';
2
- export * from './shell.js';
3
- export declare enum ExecutorType {
4
- container = "container",
5
- local = "local"
6
- }
@@ -1,7 +0,0 @@
1
- export * from './docker.js';
2
- export * from './shell.js';
3
- export var ExecutorType;
4
- (function (ExecutorType) {
5
- ExecutorType["container"] = "container";
6
- ExecutorType["local"] = "local";
7
- })(ExecutorType || (ExecutorType = {}));
@@ -1,2 +0,0 @@
1
- import { Executor } from './types.js';
2
- export declare const shellExecutor: Executor;
@@ -1,14 +0,0 @@
1
- import { execa } from 'execa';
2
- export const shellExecutor = {
3
- async run(script, options) {
4
- const process = execa(script, {
5
- all: true,
6
- cwd: options.cwd,
7
- shell: true,
8
- });
9
- if (options.out) {
10
- process.all?.pipe(options.out);
11
- }
12
- return process;
13
- },
14
- };
@@ -1,8 +0,0 @@
1
- import { Writable } from 'node:stream';
2
- export type ExecutorRunOptions = {
3
- cwd?: string;
4
- out?: Writable;
5
- };
6
- export type Executor<RO extends ExecutorRunOptions = ExecutorRunOptions, T = unknown> = {
7
- run(script: string, options?: RO): Promise<T>;
8
- };
@@ -1 +0,0 @@
1
- export {};
@@ -1,6 +0,0 @@
1
- import { MonorepoConfig } from '../index.js';
2
- import { AbstractPlugin } from './plugin.js';
3
- export declare class ComponentDiscoverPlugin extends AbstractPlugin {
4
- static name: string;
5
- extendConfig(config: MonorepoConfig): Promise<MonorepoConfig>;
6
- }