@enspirit/emb 0.4.0 → 0.4.1

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 (33) hide show
  1. package/README.md +1 -1
  2. package/dist/src/docker/operations/images/BuildImageOperation.d.ts +2 -2
  3. package/dist/src/docker/operations/images/BuildImageOperation.js +25 -28
  4. package/dist/src/docker/operations/images/PushImagesOperation.d.ts +14 -0
  5. package/dist/src/docker/operations/images/PushImagesOperation.js +22 -0
  6. package/dist/src/docker/resources/DockerImageResource.js +24 -25
  7. package/dist/src/monorepo/monorepo.d.ts +3 -1
  8. package/dist/src/monorepo/monorepo.js +5 -2
  9. package/dist/src/monorepo/operations/fs/CreateFileOperation.d.ts +3 -1
  10. package/dist/src/monorepo/operations/fs/CreateFileOperation.js +3 -1
  11. package/dist/src/monorepo/operations/resources/BuildResourcesOperation.d.ts +3 -6
  12. package/dist/src/monorepo/operations/resources/BuildResourcesOperation.js +19 -31
  13. package/dist/src/monorepo/operations/tasks/RunTasksOperation.js +1 -2
  14. package/dist/src/monorepo/resources/FileResourceBuilder.d.ts +15 -0
  15. package/dist/src/monorepo/resources/FileResourceBuilder.js +19 -0
  16. package/dist/src/monorepo/resources/ResourceFactory.d.ts +7 -29
  17. package/dist/src/monorepo/resources/ResourceFactory.js +5 -5
  18. package/dist/src/monorepo/resources/abstract/AbstractResourceBuilder.d.ts +23 -0
  19. package/dist/src/monorepo/resources/abstract/AbstractResourceBuilder.js +18 -0
  20. package/dist/src/monorepo/resources/abstract/SentinelFileBasedBuilder.d.ts +27 -0
  21. package/dist/src/monorepo/resources/abstract/SentinelFileBasedBuilder.js +46 -0
  22. package/dist/src/monorepo/resources/abstract/index.d.ts +2 -0
  23. package/dist/src/monorepo/resources/abstract/index.js +2 -0
  24. package/dist/src/monorepo/resources/index.d.ts +3 -1
  25. package/dist/src/monorepo/resources/index.js +3 -1
  26. package/dist/src/monorepo/resources/types.d.ts +36 -0
  27. package/dist/src/monorepo/types.d.ts +3 -1
  28. package/dist/src/operations/abstract/AbstractOperation.d.ts +5 -5
  29. package/dist/src/operations/types.d.ts +1 -1
  30. package/oclif.manifest.json +84 -84
  31. package/package.json +1 -1
  32. package/dist/src/monorepo/resources/FileResource.js +0 -17
  33. /package/dist/src/monorepo/resources/{FileResource.d.ts → types.js} +0 -0
package/README.md CHANGED
@@ -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.4.0 darwin-x64 node-v22.12.0
17
+ @enspirit/emb/0.4.1 darwin-x64 node-v22.18.0
18
18
  $ emb --help [COMMAND]
19
19
  USAGE
20
20
  $ emb COMMAND
@@ -13,8 +13,8 @@ export declare const BuildImageOperationInputSchema: z.ZodObject<{
13
13
  labels: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
14
14
  target: z.ZodOptional<z.ZodString>;
15
15
  }, z.core.$strip>;
16
- export declare class BuildImageOperation extends AbstractOperation<typeof BuildImageOperationInputSchema, Array<unknown>> {
16
+ export declare class BuildImageOperation extends AbstractOperation<typeof BuildImageOperationInputSchema, void> {
17
17
  private out?;
18
18
  constructor(out?: Writable | undefined);
19
- protected _run(input: z.input<typeof BuildImageOperationInputSchema>): Promise<Array<unknown>>;
19
+ protected _run(input: z.input<typeof BuildImageOperationInputSchema>): Promise<void>;
20
20
  }
@@ -1,4 +1,4 @@
1
- import { PassThrough } from 'node:stream';
1
+ import { Transform } from 'node:stream';
2
2
  import * as z from 'zod';
3
3
  import { decodeBuildkitStatusResponse } from '../../index.js';
4
4
  import { AbstractOperation } from '../../../operations/index.js';
@@ -37,13 +37,28 @@ export class BuildImageOperation extends AbstractOperation {
37
37
  this.out = out;
38
38
  }
39
39
  async _run(input) {
40
- const tee = new PassThrough();
41
40
  const logFile = await this.context.monorepo.store.createWriteStream(`logs/docker/build/${input.tag}.log`);
42
- tee.pipe(logFile);
43
- if (this.out) {
44
- tee.pipe(this.out);
45
- }
46
- tee.write('Sending build context to Docker\n');
41
+ const decodeBuildkit = new Transform({
42
+ transform: async (chunk, encoding, callback) => {
43
+ try {
44
+ try {
45
+ const { aux } = JSON.parse(chunk);
46
+ const { vertexes } = await decodeBuildkitStatusResponse(aux);
47
+ vertexes.forEach((v) => {
48
+ logFile.write(`${JSON.stringify(v)}\n`);
49
+ this.out?.write(`${v.name}\n`);
50
+ });
51
+ }
52
+ catch {
53
+ //
54
+ }
55
+ callback();
56
+ }
57
+ catch (error) {
58
+ console.error('__OOPS', error);
59
+ }
60
+ },
61
+ });
47
62
  const stream = await this.context.docker.buildImage({
48
63
  context: input.context,
49
64
  src: [...input.src],
@@ -55,28 +70,10 @@ export class BuildImageOperation extends AbstractOperation {
55
70
  target: input.target,
56
71
  version: '2',
57
72
  });
58
- tee.write('Starting build\n');
59
73
  return new Promise((resolve, reject) => {
60
- this.context.docker.modem.followProgress(stream, (err, traces) => {
61
- // logFile.close();
62
- return err ? reject(err) : resolve(traces);
63
- }, async (trace) => {
64
- if (trace.error) {
65
- // logFile.close();
66
- reject(new Error(trace.error));
67
- }
68
- else {
69
- try {
70
- const { vertexes } = await decodeBuildkitStatusResponse(trace.aux);
71
- vertexes.forEach((v) => {
72
- tee.write(v.name + '\n');
73
- });
74
- }
75
- catch (error) {
76
- console.error(error);
77
- }
78
- }
79
- });
74
+ stream.pipe(decodeBuildkit);
75
+ stream.on('close', () => resolve());
76
+ stream.on('error', (error) => reject(error));
80
77
  });
81
78
  }
82
79
  }
@@ -0,0 +1,14 @@
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/Image/operation/ImagePush
5
+ */
6
+ declare const schema: z.ZodObject<{
7
+ images: z.ZodOptional<z.ZodArray<z.ZodString>>;
8
+ tag: z.ZodDefault<z.ZodOptional<z.ZodString>>;
9
+ }, z.core.$strip>;
10
+ export declare class PushImagesOperation extends AbstractOperation<typeof schema, void> {
11
+ constructor();
12
+ protected _run(_input: z.input<typeof schema>): Promise<void>;
13
+ }
14
+ export {};
@@ -0,0 +1,22 @@
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/Image/operation/ImagePush
5
+ */
6
+ const schema = z.object({
7
+ images: z
8
+ .array(z.string())
9
+ .optional()
10
+ .describe('The names of images to push (The name should be provided without tag. Use the `tag` parameter to specify why tag to push)'),
11
+ tag: z
12
+ .string()
13
+ .optional()
14
+ .default('latest')
15
+ .describe('Tag of the images to push'),
16
+ });
17
+ export class PushImagesOperation extends AbstractOperation {
18
+ constructor() {
19
+ super(schema);
20
+ }
21
+ async _run(_input) { }
22
+ }
@@ -2,15 +2,17 @@ import { fdir as Fdir } from 'fdir';
2
2
  import { stat, statfs } from 'node:fs/promises';
3
3
  import { join } from 'node:path';
4
4
  import pMap from 'p-map';
5
+ import { SentinelFileBasedBuilder } from '../../monorepo/index.js';
5
6
  import { GitPrerequisitePlugin } from '../../prerequisites/index.js';
6
7
  import { ResourceFactory, } from '../../monorepo/resources/ResourceFactory.js';
7
8
  import { BuildImageOperation } from '../operations/index.js';
8
- class DockerImageResourceBuilder {
9
+ class DockerImageResourceBuilder extends SentinelFileBasedBuilder {
9
10
  buildContext;
10
- context;
11
+ dockerContext;
11
12
  constructor(buildContext) {
13
+ super(buildContext);
12
14
  this.buildContext = buildContext;
13
- this.context = this.config.context
15
+ this.dockerContext = this.config?.context
14
16
  ? this.config.context[0] === '/'
15
17
  ? buildContext.monorepo.join(this.config.context)
16
18
  : buildContext.component.join(this.config.context)
@@ -20,72 +22,69 @@ class DockerImageResourceBuilder {
20
22
  return this.buildContext.monorepo;
21
23
  }
22
24
  get config() {
23
- return (this.buildContext.config.params || {});
25
+ return this.buildContext.config.params;
24
26
  }
25
27
  get component() {
26
28
  return this.buildContext.component;
27
29
  }
28
- async build(out) {
30
+ async _build(_resource, out) {
29
31
  // Ensure the folder exists
30
- await statfs(this.context);
32
+ await statfs(this.dockerContext);
31
33
  const imageName = [
32
34
  this.monorepo.name,
33
- this.config.tag || this.component.name,
35
+ this.config?.tag || this.component.name,
34
36
  ].join('/');
35
- const tagName = this.config.tag || this.monorepo.defaults.docker?.tag || 'latest';
37
+ const tagName = this.config?.tag || this.monorepo.defaults.docker?.tag || 'latest';
36
38
  const crawler = new Fdir();
37
39
  const sources = await crawler
38
40
  .withRelativePaths()
39
- .crawl(this.context)
41
+ .crawl(this.dockerContext)
40
42
  .withPromise();
41
43
  const buildParams = {
42
- context: this.context,
43
- dockerfile: this.config.dockerfile || 'Dockerfile',
44
+ context: this.dockerContext,
45
+ dockerfile: this.config?.dockerfile || 'Dockerfile',
44
46
  src: sources,
45
47
  buildArgs: await this.monorepo.expand({
46
48
  ...this.monorepo.defaults.docker?.buildArgs,
47
- ...this.config.buildArgs,
49
+ ...this.config?.buildArgs,
48
50
  }),
49
51
  tag: await this.monorepo.expand(`${imageName}:${tagName}`),
50
52
  labels: await this.monorepo.expand({
51
- ...this.config.labels,
53
+ ...this.config?.labels,
52
54
  'emb/project': this.monorepo.name,
53
55
  'emb/component': this.component.name,
54
56
  'emb/flavor': this.monorepo.currentFlavor,
55
57
  }),
56
- target: this.config.target,
58
+ target: this.config?.target,
57
59
  };
58
60
  return {
59
61
  input: buildParams,
60
62
  operation: new BuildImageOperation(out),
61
63
  };
62
64
  }
63
- async mustBuild(sentinel) {
65
+ async _mustBuild() {
64
66
  const plugin = new GitPrerequisitePlugin();
65
- const sources = await plugin.collect(this.context);
67
+ const sources = await plugin.collect(this.dockerContext);
66
68
  const lastUpdated = await this.lastUpdatedInfo(sources);
67
- if (!sentinel) {
68
- return lastUpdated;
69
+ if (!lastUpdated) {
70
+ return;
69
71
  }
70
- return lastUpdated && lastUpdated.time.getTime() > sentinel.mtime
71
- ? lastUpdated
72
- : undefined;
72
+ return { mtime: lastUpdated.time.getTime() };
73
73
  }
74
74
  async lastUpdatedInfo(sources) {
75
75
  const stats = await pMap(sources, async (s) => {
76
- const stats = await stat(join(this.context, s.path));
76
+ const stats = await stat(join(this.dockerContext, s.path));
77
77
  return {
78
78
  time: stats.mtime,
79
79
  path: s.path,
80
80
  };
81
81
  }, { concurrency: 30 });
82
82
  if (stats.length === 0) {
83
- return 0;
83
+ return;
84
84
  }
85
85
  return stats.reduce((last, entry) => {
86
86
  return last.time > entry.time ? last : entry;
87
87
  }, stats[0]);
88
88
  }
89
89
  }
90
- // Bring better abstraction and register as part of the plugin initialization
91
- ResourceFactory.register('docker/image', async (context) => new DockerImageResourceBuilder(context));
90
+ ResourceFactory.register('docker/image', DockerImageResourceBuilder);
@@ -30,7 +30,9 @@ export declare class Monorepo {
30
30
  private installEnv;
31
31
  init(): Promise<Monorepo>;
32
32
  join(...paths: string[]): string;
33
- run<I, O>(operation: IOperation<I, O>, args: I): Promise<O>;
33
+ run<I extends void, O>(operation: IOperation<I, O>): Promise<O>;
34
+ run<I extends void, O>(operation: IOperation<I, O>): Promise<O>;
35
+ run<I, O>(operation: IOperation<I, O>, input: I): Promise<O>;
34
36
  private expandPatches;
35
37
  withFlavor(flavorName: string): Promise<Monorepo>;
36
38
  }
@@ -161,8 +161,11 @@ export class Monorepo {
161
161
  join(...paths) {
162
162
  return join(this.rootDir, ...paths);
163
163
  }
164
- async run(operation, args) {
165
- return operation.run(args);
164
+ run(operation, input = undefined) {
165
+ if (input === undefined) {
166
+ return operation.run();
167
+ }
168
+ return operation.run(input);
166
169
  }
167
170
  async expandPatches(patches) {
168
171
  const expanded = Promise.all(patches.map(async (patch) => {
@@ -1,3 +1,4 @@
1
+ import { Writable } from 'node:stream';
1
2
  import * as z from 'zod';
2
3
  import { AbstractOperation } from '../../../operations/index.js';
3
4
  declare const schema: z.ZodObject<{
@@ -5,7 +6,8 @@ declare const schema: z.ZodObject<{
5
6
  force: z.ZodOptional<z.ZodBoolean>;
6
7
  }, z.core.$strip>;
7
8
  export declare class CreateFileOperation extends AbstractOperation<typeof schema, unknown> {
8
- constructor();
9
+ protected out?: Writable | undefined;
10
+ constructor(out?: Writable | undefined);
9
11
  protected _run(input: z.input<typeof schema>): Promise<void>;
10
12
  }
11
13
  export {};
@@ -10,8 +10,10 @@ const schema = z.object({
10
10
  .describe("Update 'atime' and 'mtime' if the file already exists"),
11
11
  });
12
12
  export class CreateFileOperation extends AbstractOperation {
13
- constructor() {
13
+ out;
14
+ constructor(out) {
14
15
  super(schema);
16
+ this.out = out;
15
17
  }
16
18
  async _run(input) {
17
19
  try {
@@ -1,12 +1,11 @@
1
1
  import * as z from 'zod';
2
- import { ResourceInfo } from '../../index.js';
3
- import { ResourceBuilderInfo } from '../../resources/ResourceFactory.js';
2
+ import { IResourceBuilder, ResourceInfo } from '../../index.js';
4
3
  import { AbstractOperation } from '../../../operations/index.js';
5
4
  export type BuildResourceMeta = {
6
5
  dryRun?: boolean;
7
6
  force?: boolean;
8
7
  resource?: ResourceInfo;
9
- builder?: ResourceBuilderInfo<unknown, unknown>;
8
+ builder?: IResourceBuilder<unknown, unknown, unknown>;
10
9
  builderInput?: unknown;
11
10
  sentinelData?: unknown;
12
11
  cacheHit?: boolean;
@@ -18,11 +17,9 @@ declare const schema: z.ZodObject<{
18
17
  force: z.ZodOptional<z.ZodBoolean>;
19
18
  }, z.core.$strip>;
20
19
  export declare class BuildResourcesOperation extends AbstractOperation<typeof schema, Record<string, BuildResourceMeta>> {
20
+ private built;
21
21
  constructor();
22
22
  protected _run(input: z.input<typeof schema>): Promise<Record<string, BuildResourceMeta>>;
23
23
  private buildResource;
24
- private sentinelFilePath;
25
- private storeSentinelData;
26
- private readSentinelFile;
27
24
  }
28
25
  export {};
@@ -1,7 +1,7 @@
1
1
  import { PRESET_TIMER, } from 'listr2';
2
2
  import * as z from 'zod';
3
3
  import { EMBCollection, findRunOrder, taskManagerFactory, } from '../../index.js';
4
- import { ResourceFactory, } from '../../resources/ResourceFactory.js';
4
+ import { ResourceFactory } from '../../resources/ResourceFactory.js';
5
5
  import { AbstractOperation } from '../../../operations/index.js';
6
6
  const schema = z.object({
7
7
  resources: z
@@ -22,6 +22,9 @@ const schema = z.object({
22
22
  .describe('Bypass the cache and force the build'),
23
23
  });
24
24
  export class BuildResourcesOperation extends AbstractOperation {
25
+ // keep track of what has been built
26
+ // to ensure depedencies cannot ignore their turn
27
+ built = [];
25
28
  constructor() {
26
29
  super(schema);
27
30
  }
@@ -92,16 +95,18 @@ export class BuildResourcesOperation extends AbstractOperation {
92
95
  /** Skip the build if the builder knows it can be skipped */
93
96
  task: async (ctx) => {
94
97
  if (ctx.builder?.mustBuild) {
95
- const previousSentinelData = await this.readSentinelFile(resource);
96
- ctx.sentinelData =
97
- await ctx.builder.mustBuild(previousSentinelData);
98
+ ctx.sentinelData = await ctx.builder.mustBuild(ctx.resource);
98
99
  ctx.cacheHit = !ctx.sentinelData;
100
+ // If one of our dependency was built, we force the re-build
101
+ // despite the cache-hit
102
+ const found = ctx.resource.dependencies?.find((d) => Boolean(this.built.find((r) => r.id === d)));
103
+ ctx.force = ctx.force || Boolean(found);
99
104
  }
100
105
  },
101
106
  },
102
107
  {
103
- title: `Build image for ${resource.id}`,
104
- async task(ctx, task) {
108
+ title: `Build ${resource.id}`,
109
+ task: async (ctx, task) => {
105
110
  const skip = (prefix) => {
106
111
  parentTask.title = `${prefix} ${resource.id}`;
107
112
  task.skip();
@@ -110,26 +115,28 @@ export class BuildResourcesOperation extends AbstractOperation {
110
115
  if (ctx.cacheHit && !ctx.force && !ctx.dryRun) {
111
116
  return skip('[cache hit]');
112
117
  }
113
- const { input, operation } = await ctx.builder.build(task.stdout());
118
+ const { input, operation } = await ctx.builder.build(ctx.resource, task.stdout());
114
119
  ctx.builderInput = input;
120
+ this.built.push(ctx.resource);
115
121
  if (ctx.dryRun) {
116
122
  return skip('[dry run]');
117
123
  }
118
- return operation.run(ctx.builderInput);
124
+ const output = await operation.run(ctx.builderInput);
125
+ if (ctx.sentinelData) {
126
+ ctx.builder.commit?.(ctx.resource, output, ctx.sentinelData);
127
+ }
128
+ return output;
119
129
  },
120
130
  },
121
131
  {
122
132
  // Return build meta data and dump
123
133
  // cache data into sentinel file
124
- task: async (ctx) => {
134
+ async task(ctx) {
125
135
  if (ctx.builder) {
126
136
  delete ctx.builder;
127
137
  }
128
138
  //
129
139
  parentContext[resource.id] = ctx;
130
- if (ctx.sentinelData && !ctx.dryRun) {
131
- await this.storeSentinelData(resource, ctx.sentinelData);
132
- }
133
140
  },
134
141
  },
135
142
  ], {
@@ -142,23 +149,4 @@ export class BuildResourcesOperation extends AbstractOperation {
142
149
  });
143
150
  return list;
144
151
  }
145
- sentinelFilePath(resource) {
146
- const { monorepo } = this.context;
147
- return `sentinels/flavors/${monorepo.currentFlavor}/${resource.component}/${resource.name}.built`;
148
- }
149
- async storeSentinelData(resource, data) {
150
- await this.context.monorepo.store.writeFile(this.sentinelFilePath(resource), JSON.stringify(data));
151
- }
152
- async readSentinelFile(resource) {
153
- const path = this.sentinelFilePath(resource);
154
- const stats = await this.context.monorepo.store.stat(path, false);
155
- if (!stats) {
156
- return undefined;
157
- }
158
- const data = await this.context.monorepo.store.readFile(path, false);
159
- return {
160
- data,
161
- mtime: stats.mtime.getTime(),
162
- };
163
- }
164
152
  }
@@ -29,6 +29,7 @@ export class RunTasksOperation {
29
29
  if (!task.script) {
30
30
  return;
31
31
  }
32
+ console.log('YES WE DO', task.component);
32
33
  const executor = params.executor ??
33
34
  (task.component ? ExecutorType.container : ExecutorType.local);
34
35
  if (executor === ExecutorType.container && !task.component) {
@@ -59,8 +60,6 @@ export class RunTasksOperation {
59
60
  async runDocker(task, out) {
60
61
  const { monorepo } = getContext();
61
62
  return monorepo.run(new ComposeExecOperation(out), {
62
- attachStderr: true,
63
- attachStdout: true,
64
63
  service: task.component,
65
64
  command: task.script,
66
65
  });
@@ -0,0 +1,15 @@
1
+ import { Writable } from 'node:stream';
2
+ import { CreateFileOperation, IResourceBuilder, ResourceInfo } from '../index.js';
3
+ import { OpInput, OpOutput } from '../../operations/index.js';
4
+ import { ResourceBuildContext } from './ResourceFactory.js';
5
+ export declare class FileResourceBuilder implements IResourceBuilder<OpInput<CreateFileOperation>, OpOutput<CreateFileOperation>, void> {
6
+ protected context: ResourceBuildContext<OpInput<CreateFileOperation>>;
7
+ constructor(context: ResourceBuildContext<OpInput<CreateFileOperation>>);
8
+ build(resource: ResourceInfo<OpInput<CreateFileOperation>>, out?: Writable): Promise<{
9
+ input: {
10
+ path: string;
11
+ force?: boolean | undefined;
12
+ };
13
+ operation: CreateFileOperation;
14
+ }>;
15
+ }
@@ -0,0 +1,19 @@
1
+ import { CreateFileOperation, } from '../index.js';
2
+ import { ResourceFactory } from './ResourceFactory.js';
3
+ export class FileResourceBuilder {
4
+ context;
5
+ constructor(context) {
6
+ this.context = context;
7
+ }
8
+ async build(resource, out) {
9
+ const input = {
10
+ path: this.context.component.join(resource.params?.path || resource.name),
11
+ };
12
+ return {
13
+ input,
14
+ operation: new CreateFileOperation(out),
15
+ };
16
+ }
17
+ }
18
+ // Bring better abstraction and register as part of the plugin initialization
19
+ ResourceFactory.register('file', FileResourceBuilder);
@@ -1,34 +1,12 @@
1
- import { Component, Monorepo, ResourceInfo } from '../../index.js';
2
- import { Writable } from 'node:stream';
3
- import { IOperation } from '../../operations/types.js';
4
- export type ResourceBuildContext = {
5
- config: ResourceInfo;
1
+ import { Component, IResourceBuilder, Monorepo, ResourceInfo } from '../../index.js';
2
+ export type ResourceBuildContext<I> = {
3
+ config: ResourceInfo<I>;
6
4
  component: Component;
7
5
  monorepo: Monorepo;
8
6
  };
9
- export type SentinelData<T = void> = {
10
- mtime: number;
11
- data: T;
12
- };
13
- export type ResourceBuilderInfo<I, O, D = unknown> = {
14
- /**
15
- * Returns input and operation required to actually
16
- * build the resources.
17
- * This allows the dry-run mechanism to be implemented outside
18
- * resource builder implementations
19
- *
20
- * @param out The Writable to use to write logs
21
- */
22
- build(out?: Writable): Promise<{
23
- input: I;
24
- operation: IOperation<I, O>;
25
- }>;
26
- mustBuild?: (previousSentinelData: SentinelData<D> | undefined) => Promise<undefined | unknown>;
27
- };
28
- export type ResourceFactoryOutput<I, O> = Promise<ResourceBuilderInfo<I, O>>;
29
- export type ResourceOperationFactory<I, O> = (context: ResourceBuildContext) => ResourceFactoryOutput<I, O>;
7
+ export type ResourceBuilderConstructor<I, O, R> = new (context: ResourceBuildContext<I>) => IResourceBuilder<I, O, R>;
30
8
  export declare class ResourceFactory {
31
- protected static types: Record<string, ResourceOperationFactory<unknown, unknown>>;
32
- static register<I, O>(type: string, opFactory: ResourceOperationFactory<I, O>): void;
33
- static factor<I, O>(type: string, context: ResourceBuildContext): ResourceFactoryOutput<I, O>;
9
+ protected static types: Record<string, ResourceBuilderConstructor<any, any, any>>;
10
+ static register<I, O, R>(type: string, constructor: ResourceBuilderConstructor<I, O, R>): void;
11
+ static factor<I, O, R>(type: string, context: ResourceBuildContext<I>): IResourceBuilder<I, O, R>;
34
12
  }
@@ -1,16 +1,16 @@
1
1
  export class ResourceFactory {
2
2
  static types = {};
3
- static register(type, opFactory) {
3
+ static register(type, constructor) {
4
4
  if (this.types[type]) {
5
5
  throw new Error(`Resource type \`${type}\` already registered`);
6
6
  }
7
- this.types[type] = opFactory;
7
+ this.types[type] = constructor;
8
8
  }
9
9
  static factor(type, context) {
10
- const opFactory = this.types[type];
11
- if (!opFactory) {
10
+ const BuilderClass = this.types[type];
11
+ if (!BuilderClass) {
12
12
  throw new Error(`Unknown resource type \`${type}\``);
13
13
  }
14
- return opFactory(context);
14
+ return new BuilderClass(context);
15
15
  }
16
16
  }
@@ -0,0 +1,23 @@
1
+ import { ResourceInfo } from '../../../index.js';
2
+ import { Writable } from 'node:stream';
3
+ import { IOperation } from '../../../operations/types.js';
4
+ import { ResourceBuildContext } from '../ResourceFactory.js';
5
+ import { IResourceBuilder } from '../types.js';
6
+ export declare abstract class AbstractResourceBuilder<I, O, R> implements IResourceBuilder<I, O, R> {
7
+ protected context: ResourceBuildContext<I>;
8
+ constructor(context: ResourceBuildContext<I>);
9
+ abstract _build(resource: ResourceInfo<I>, out?: Writable): Promise<{
10
+ input: I;
11
+ operation: IOperation<I, O>;
12
+ }>;
13
+ build(resource: ResourceInfo<I>, out?: Writable): Promise<{
14
+ input: I;
15
+ operation: IOperation<I, O>;
16
+ }>;
17
+ abstract _mustBuild?(resource: ResourceInfo<I>): Promise<R | undefined> | undefined;
18
+ mustBuild(resource: ResourceInfo<I>): Promise<R | undefined>;
19
+ _publish?(resource: ResourceInfo<I>, out?: Writable): Promise<void>;
20
+ publish?(resource: ResourceInfo<I>, out?: Writable): Promise<void>;
21
+ abstract _commit(resource: ResourceInfo<I>, output: O, reason: R): Promise<void>;
22
+ commit(resource: ResourceInfo<I>, output: O, reason: R): Promise<void>;
23
+ }
@@ -0,0 +1,18 @@
1
+ export class AbstractResourceBuilder {
2
+ context;
3
+ constructor(context) {
4
+ this.context = context;
5
+ }
6
+ build(resource, out) {
7
+ return this._build(resource, out);
8
+ }
9
+ async mustBuild(resource) {
10
+ return this._mustBuild?.(resource);
11
+ }
12
+ async publish(resource, out) {
13
+ return this._publish?.(resource, out);
14
+ }
15
+ async commit(resource, output, reason) {
16
+ return this._commit?.(resource, output, reason);
17
+ }
18
+ }
@@ -0,0 +1,27 @@
1
+ import { ResourceInfo } from '../../../index.js';
2
+ import { Writable } from 'node:stream';
3
+ import { IOperation } from '../../../operations/types.js';
4
+ import { AbstractResourceBuilder } from './AbstractResourceBuilder.js';
5
+ export type SentinelFile<T> = {
6
+ mtime: number;
7
+ data?: T;
8
+ };
9
+ export declare abstract class SentinelFileBasedBuilder<I, O, SentinelData extends {
10
+ mtime: number;
11
+ }> extends AbstractResourceBuilder<I, O, SentinelData> {
12
+ private lastSentinelFile?;
13
+ private newSentinelData?;
14
+ /**
15
+ * Checks wether or not the sentinel file is more recent
16
+ * that the output of the builder's sentinel data
17
+ */
18
+ mustBuild(resource: ResourceInfo<I>): Promise<SentinelData | undefined>;
19
+ build(resource: ResourceInfo<I>, out?: Writable): Promise<{
20
+ input: I;
21
+ operation: IOperation<I, O>;
22
+ }>;
23
+ private get sentinelFileName();
24
+ private storeSentinelData;
25
+ private readSentinel;
26
+ _commit(_resource: ResourceInfo<I>, _output: O, reason: SentinelData): Promise<void>;
27
+ }
@@ -0,0 +1,46 @@
1
+ import { AbstractResourceBuilder } from './AbstractResourceBuilder.js';
2
+ export class SentinelFileBasedBuilder extends AbstractResourceBuilder {
3
+ lastSentinelFile;
4
+ newSentinelData;
5
+ /**
6
+ * Checks wether or not the sentinel file is more recent
7
+ * that the output of the builder's sentinel data
8
+ */
9
+ async mustBuild(resource) {
10
+ if (!this._mustBuild) {
11
+ return;
12
+ }
13
+ this.lastSentinelFile = await this.readSentinel();
14
+ this.newSentinelData = await this._mustBuild(resource);
15
+ if (!(this.lastSentinelFile && this.newSentinelData)) {
16
+ return this.newSentinelData;
17
+ }
18
+ if (this.lastSentinelFile.mtime < this.newSentinelData.mtime) {
19
+ return this.newSentinelData;
20
+ }
21
+ }
22
+ async build(resource, out) {
23
+ return this._build(resource, out);
24
+ }
25
+ get sentinelFileName() {
26
+ const { monorepo, config } = this.context;
27
+ return `sentinels/flavors/${monorepo.currentFlavor}/${config.component}/${config.name}.built`;
28
+ }
29
+ async storeSentinelData(data) {
30
+ await this.context.monorepo.store.writeFile(this.sentinelFileName, JSON.stringify(data));
31
+ }
32
+ async readSentinel() {
33
+ const stats = await this.context.monorepo.store.stat(this.sentinelFileName, false);
34
+ if (!stats) {
35
+ return undefined;
36
+ }
37
+ const data = await this.context.monorepo.store.readFile(this.sentinelFileName, false);
38
+ return {
39
+ data: data ? JSON.parse(data) : data,
40
+ mtime: stats.mtime.getTime(),
41
+ };
42
+ }
43
+ async _commit(_resource, _output, reason) {
44
+ this.storeSentinelData(reason);
45
+ }
46
+ }
@@ -0,0 +1,2 @@
1
+ export * from './AbstractResourceBuilder.js';
2
+ export * from './SentinelFileBasedBuilder.js';
@@ -0,0 +1,2 @@
1
+ export * from './AbstractResourceBuilder.js';
2
+ export * from './SentinelFileBasedBuilder.js';
@@ -1 +1,3 @@
1
- import './FileResource.js';
1
+ import './FileResourceBuilder.js';
2
+ export * from './abstract/index.js';
3
+ export * from './types.js';
@@ -1 +1,3 @@
1
- import './FileResource.js';
1
+ import './FileResourceBuilder.js';
2
+ export * from './abstract/index.js';
3
+ export * from './types.js';
@@ -0,0 +1,36 @@
1
+ import { ResourceInfo } from '../../index.js';
2
+ import { Writable } from 'node:stream';
3
+ import { IOperation } from '../../operations/types.js';
4
+ export type IResourceBuilder<Input, Output, Reason> = {
5
+ /**
6
+ * Returns input and operation required to actually
7
+ * build the resources.
8
+ * This allows the dry-run mechanism to be implemented outside
9
+ * resource implementations
10
+ *
11
+ * @param resource The resource config
12
+ * @param out The Writable to use to write logs
13
+ */
14
+ build(resource: ResourceInfo<Input>, out?: Writable): Promise<{
15
+ input: Input;
16
+ operation: IOperation<Input, Output>;
17
+ }>;
18
+ mustBuild?: (resource: ResourceInfo<Input>) => Promise<Reason | undefined>;
19
+ /**
20
+ * Resource builders will be informed when a successful build of resource
21
+ * has been produced through them
22
+ *
23
+ * This allows them to store metadata to improve their caching algorithm
24
+ */
25
+ commit?: (resource: ResourceInfo<Input>, output: Output, reason: Reason) => Promise<void>;
26
+ /**
27
+ * Similar to .build(), must return input and operation required to actually
28
+ * publish the resources.
29
+ * This allows the dry-run mechanism to be implemented outside
30
+ * resource implementations
31
+ *
32
+ * @param resource The resource config
33
+ * @param out The Writable to use to write logs
34
+ */
35
+ publish?(resource: ResourceInfo<Input>, out?: Writable): Promise<void>;
36
+ };
@@ -4,7 +4,9 @@ export type ComponentIdentifiable<T> = T & {
4
4
  name: string;
5
5
  component: string;
6
6
  };
7
- export type ResourceInfo = ComponentIdentifiable<IResourceConfig>;
7
+ export type ResourceInfo<T = unknown> = ComponentIdentifiable<IResourceConfig> & {
8
+ params?: T;
9
+ };
8
10
  export type Resources = {
9
11
  [k: string]: ResourceInfo;
10
12
  };
@@ -3,10 +3,10 @@ import * as z from 'zod';
3
3
  import { IOperation } from '../index.js';
4
4
  export type OpInput<A extends AbstractOperation<z.Schema, unknown>> = A extends AbstractOperation<infer I, unknown> ? z.infer<I> : never;
5
5
  export type OpOutput<A extends AbstractOperation<z.Schema, unknown>> = A extends AbstractOperation<z.Schema, infer O> ? O : never;
6
- export declare abstract class AbstractOperation<S extends z.Schema, O = unknown> implements IOperation<z.infer<S>, O> {
7
- protected inputSchema: S;
6
+ export declare abstract class AbstractOperation<I extends z.Schema, O> implements IOperation<z.infer<I>, O> {
7
+ protected inputSchema: I;
8
8
  protected context: EmbContext;
9
- constructor(inputSchema: S);
10
- protected abstract _run(input: z.infer<S>): Promise<O>;
11
- run(input: unknown | z.infer<S>): Promise<O>;
9
+ constructor(inputSchema: I);
10
+ protected abstract _run(input: z.infer<I>): Promise<O>;
11
+ run(input: z.infer<I>): Promise<O>;
12
12
  }
@@ -1,3 +1,3 @@
1
- export interface IOperation<I, O> {
1
+ export interface IOperation<I = void, O = unknown> {
2
2
  run(input: I): Promise<O>;
3
3
  }
@@ -520,12 +520,18 @@
520
520
  "prune.js"
521
521
  ]
522
522
  },
523
- "tasks": {
523
+ "resources:build": {
524
524
  "aliases": [],
525
- "args": {},
526
- "description": "List tasks.",
525
+ "args": {
526
+ "component": {
527
+ "description": "List of resources to build (defaults to all)",
528
+ "name": "component",
529
+ "required": false
530
+ }
531
+ },
532
+ "description": "Build the resources of the monorepo",
527
533
  "examples": [
528
- "<%= config.bin %> <%= command.id %>"
534
+ "<%= config.bin %> <%= command.id %> build --flavor development"
529
535
  ],
530
536
  "flags": {
531
537
  "json": {
@@ -534,15 +540,38 @@
534
540
  "name": "json",
535
541
  "allowNo": false,
536
542
  "type": "boolean"
543
+ },
544
+ "flavor": {
545
+ "description": "Specify the flavor to use.",
546
+ "name": "flavor",
547
+ "required": false,
548
+ "hasDynamicHelp": false,
549
+ "multiple": false,
550
+ "type": "option"
551
+ },
552
+ "dry-run": {
553
+ "description": "Do not build the resources but only produce build meta information",
554
+ "name": "dry-run",
555
+ "required": false,
556
+ "allowNo": false,
557
+ "type": "boolean"
558
+ },
559
+ "force": {
560
+ "char": "f",
561
+ "description": "Bypass the cache and force the build",
562
+ "name": "force",
563
+ "required": false,
564
+ "allowNo": false,
565
+ "type": "boolean"
537
566
  }
538
567
  },
539
568
  "hasDynamicHelp": false,
540
569
  "hiddenAliases": [],
541
- "id": "tasks",
570
+ "id": "resources:build",
542
571
  "pluginAlias": "@enspirit/emb",
543
572
  "pluginName": "@enspirit/emb",
544
573
  "pluginType": "core",
545
- "strict": true,
574
+ "strict": false,
546
575
  "enableJsonFlag": true,
547
576
  "isESM": true,
548
577
  "relativePath": [
@@ -550,20 +579,14 @@
550
579
  "src",
551
580
  "cli",
552
581
  "commands",
553
- "tasks",
554
- "index.js"
582
+ "resources",
583
+ "build.js"
555
584
  ]
556
585
  },
557
- "tasks:run": {
586
+ "resources": {
558
587
  "aliases": [],
559
- "args": {
560
- "task": {
561
- "description": "List of tasks to run. You can provide either ids or names (eg: component:task or task)",
562
- "name": "task",
563
- "required": true
564
- }
565
- },
566
- "description": "Run tasks.",
588
+ "args": {},
589
+ "description": "List resources.",
567
590
  "examples": [
568
591
  "<%= config.bin %> <%= command.id %>"
569
592
  ],
@@ -575,33 +598,21 @@
575
598
  "allowNo": false,
576
599
  "type": "boolean"
577
600
  },
578
- "executor": {
579
- "char": "x",
580
- "description": "Where to run the task. (experimental!)",
581
- "name": "executor",
601
+ "flavor": {
602
+ "description": "Specify the flavor to use.",
603
+ "name": "flavor",
604
+ "required": false,
582
605
  "hasDynamicHelp": false,
583
606
  "multiple": false,
584
- "options": [
585
- "container",
586
- "local"
587
- ],
588
607
  "type": "option"
589
- },
590
- "all-matching": {
591
- "char": "a",
592
- "description": "Run all tasks matching (when multiple matches)",
593
- "name": "all-matching",
594
- "allowNo": false,
595
- "type": "boolean"
596
608
  }
597
609
  },
598
610
  "hasDynamicHelp": false,
599
611
  "hiddenAliases": [],
600
- "id": "tasks:run",
612
+ "id": "resources",
601
613
  "pluginAlias": "@enspirit/emb",
602
614
  "pluginName": "@enspirit/emb",
603
615
  "pluginType": "core",
604
- "strict": false,
605
616
  "enableJsonFlag": true,
606
617
  "isESM": true,
607
618
  "relativePath": [
@@ -609,22 +620,16 @@
609
620
  "src",
610
621
  "cli",
611
622
  "commands",
612
- "tasks",
613
- "run.js"
623
+ "resources",
624
+ "index.js"
614
625
  ]
615
626
  },
616
- "resources:build": {
627
+ "tasks": {
617
628
  "aliases": [],
618
- "args": {
619
- "component": {
620
- "description": "List of resources to build (defaults to all)",
621
- "name": "component",
622
- "required": false
623
- }
624
- },
625
- "description": "Build the resources of the monorepo",
629
+ "args": {},
630
+ "description": "List tasks.",
626
631
  "examples": [
627
- "<%= config.bin %> <%= command.id %> build --flavor development"
632
+ "<%= config.bin %> <%= command.id %>"
628
633
  ],
629
634
  "flags": {
630
635
  "json": {
@@ -633,38 +638,15 @@
633
638
  "name": "json",
634
639
  "allowNo": false,
635
640
  "type": "boolean"
636
- },
637
- "flavor": {
638
- "description": "Specify the flavor to use.",
639
- "name": "flavor",
640
- "required": false,
641
- "hasDynamicHelp": false,
642
- "multiple": false,
643
- "type": "option"
644
- },
645
- "dry-run": {
646
- "description": "Do not build the resources but only produce build meta information",
647
- "name": "dry-run",
648
- "required": false,
649
- "allowNo": false,
650
- "type": "boolean"
651
- },
652
- "force": {
653
- "char": "f",
654
- "description": "Bypass the cache and force the build",
655
- "name": "force",
656
- "required": false,
657
- "allowNo": false,
658
- "type": "boolean"
659
641
  }
660
642
  },
661
643
  "hasDynamicHelp": false,
662
644
  "hiddenAliases": [],
663
- "id": "resources:build",
645
+ "id": "tasks",
664
646
  "pluginAlias": "@enspirit/emb",
665
647
  "pluginName": "@enspirit/emb",
666
648
  "pluginType": "core",
667
- "strict": false,
649
+ "strict": true,
668
650
  "enableJsonFlag": true,
669
651
  "isESM": true,
670
652
  "relativePath": [
@@ -672,14 +654,20 @@
672
654
  "src",
673
655
  "cli",
674
656
  "commands",
675
- "resources",
676
- "build.js"
657
+ "tasks",
658
+ "index.js"
677
659
  ]
678
660
  },
679
- "resources": {
661
+ "tasks:run": {
680
662
  "aliases": [],
681
- "args": {},
682
- "description": "List resources.",
663
+ "args": {
664
+ "task": {
665
+ "description": "List of tasks to run. You can provide either ids or names (eg: component:task or task)",
666
+ "name": "task",
667
+ "required": true
668
+ }
669
+ },
670
+ "description": "Run tasks.",
683
671
  "examples": [
684
672
  "<%= config.bin %> <%= command.id %>"
685
673
  ],
@@ -691,21 +679,33 @@
691
679
  "allowNo": false,
692
680
  "type": "boolean"
693
681
  },
694
- "flavor": {
695
- "description": "Specify the flavor to use.",
696
- "name": "flavor",
697
- "required": false,
682
+ "executor": {
683
+ "char": "x",
684
+ "description": "Where to run the task. (experimental!)",
685
+ "name": "executor",
698
686
  "hasDynamicHelp": false,
699
687
  "multiple": false,
688
+ "options": [
689
+ "container",
690
+ "local"
691
+ ],
700
692
  "type": "option"
693
+ },
694
+ "all-matching": {
695
+ "char": "a",
696
+ "description": "Run all tasks matching (when multiple matches)",
697
+ "name": "all-matching",
698
+ "allowNo": false,
699
+ "type": "boolean"
701
700
  }
702
701
  },
703
702
  "hasDynamicHelp": false,
704
703
  "hiddenAliases": [],
705
- "id": "resources",
704
+ "id": "tasks:run",
706
705
  "pluginAlias": "@enspirit/emb",
707
706
  "pluginName": "@enspirit/emb",
708
707
  "pluginType": "core",
708
+ "strict": false,
709
709
  "enableJsonFlag": true,
710
710
  "isESM": true,
711
711
  "relativePath": [
@@ -713,10 +713,10 @@
713
713
  "src",
714
714
  "cli",
715
715
  "commands",
716
- "resources",
717
- "index.js"
716
+ "tasks",
717
+ "run.js"
718
718
  ]
719
719
  }
720
720
  },
721
- "version": "0.4.0"
721
+ "version": "0.4.1"
722
722
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@enspirit/emb",
3
3
  "type": "module",
4
- "version": "0.4.0",
4
+ "version": "0.4.1",
5
5
  "keywords": [
6
6
  "monorepo",
7
7
  "docker",
@@ -1,17 +0,0 @@
1
- import { CreateFileOperation } from '../index.js';
2
- import { ResourceFactory } from './ResourceFactory.js';
3
- // Bring better abstraction and register as part of the plugin initialization
4
- ResourceFactory.register('file', async ({ config, component }) => {
5
- return {
6
- async build() {
7
- const fromConfig = (config.params || {});
8
- const input = {
9
- path: component.join(fromConfig?.path || config.name),
10
- };
11
- return {
12
- input,
13
- operation: new CreateFileOperation(),
14
- };
15
- },
16
- };
17
- });