Skip to content

Commit a533e53

Browse files
committed
Add plugin entity + plugin initialization
1 parent 52bbf7b commit a533e53

21 files changed

Lines changed: 4142 additions & 66 deletions

File tree

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
11
import { ConfigCompiler } from '../../config-compiler';
2-
import { PluginsManager } from '../../plugins/manager';
2+
import { PluginCollection } from '../../plugins/plugin-collection';
33

44
export const PlanOrchestrator = {
55
async run(rootDirectory: string): Promise<void> {
66
const project = await ConfigCompiler.parseProject(rootDirectory);
77

8-
const pluginsManager = new PluginsManager();
9-
await pluginsManager.initializePlugins(project);
8+
const pluginCollection = new PluginCollection();
9+
await pluginCollection.initializePlugins(project);
1010

11+
const resourceDefinitions = await pluginCollection.getAllResourceDefinitions();
12+
console.log(resourceDefinitions);
1113

14+
await pluginCollection.killPlugins();
1215
},
1316
};

codify-core/src/config-compiler/index.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import { ParsedModule, ParsedProject } from './parser/entities';
66
import { ProjectConfig } from './parser/entities/project';
77
import { JsonFileParser } from './parser/json/file-parser';
88

9-
109
export class ConfigCompiler {
1110

1211
static readonly supportedParsers: Record<string, FileParser> = {
@@ -34,7 +33,7 @@ export class ConfigCompiler {
3433
const projectConfig = parsedProjectConfigs[0] as ProjectConfig;
3534
return new ParsedProject({
3635
coreModule: new ParsedModule({
37-
configBlocks: configBlocks.flat(1),
36+
configBlocks: configBlocks.filter((u) => u.configType !== ConfigBlockType.PROJECT),
3837
}),
3938
projectConfig,
4039
})

codify-core/src/config-compiler/parser/entities/project.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,11 @@ export class ProjectConfig implements ConfigBlock {
3939
if (this.validate(config)) {
4040
this.name = config.name;
4141
this.plugins = config.plugins;
42+
43+
return;
4244
}
45+
46+
throw new Error('Unable to parse project');
4347
}
4448

4549
validate(config: unknown): config is RemoveMethods<ProjectConfig> {

codify-core/src/config-compiler/parser/entities/resource.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ export class ResourceConfig implements ConfigBlock {
3232
this.type = type;
3333
this.name = name;
3434
this.parameters = params ?? {};
35+
36+
return;
3537
}
3638

3739
throw new Error('Unable to parse resource config');
@@ -42,8 +44,8 @@ export class ResourceConfig implements ConfigBlock {
4244
throw new Error('Config is not an object');
4345
}
4446

45-
if (!validateStringEq(config.type, 'project')) {
46-
throw new Error('Config is not of type project');
47+
if (!validateStringEq(config.type, 'resource')) {
48+
throw new Error('Config is not of type resource');
4749
}
4850

4951
if (config.name && !validateNameString(config.name)) {
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export interface PluginMessage {
2+
cmd: string;
3+
data?: unknown;
4+
}

codify-core/src/plugins/entities/plugin.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,15 @@ import { validateTypeArray, validateTypeRecordStringUnknown } from '../../utils/
66
type ResourceName = string;
77

88
export class Plugin {
9+
10+
// Plugin names should be globally unique
911
name: string;
1012
directory: string;
1113

1214
process: ChildProcess;
1315
resourceDefinitions?: Map<ResourceName, ResourceDefinition>;
1416

15-
constructor(name: string, directory: string, process: ChildProcess) {
17+
constructor(directory: string, name: string, process: ChildProcess) {
1618
this.name = name;
1719
this.directory = directory;
1820
this.process = process;
@@ -22,22 +24,25 @@ export class Plugin {
2224
if (this.validate(definitions)) {
2325
const entries = definitions.map((u) => {
2426
const resourceDefinition = ResourceDefinition.fromJson(u);
25-
return [resourceDefinition.name, resourceDefinition] as const;
27+
28+
// Append the plugin name to all resources to prevent conflicts across plugins
29+
return [`${this.name}_${resourceDefinition.name}`, resourceDefinition] as const;
2630
})
2731

2832
this.resourceDefinitions = new Map(entries);
33+
return;
2934
}
3035

3136
throw new Error('Unable to parse resource definition');
3237
}
3338

3439
private validate(definitions: unknown): definitions is Array<Record<string, unknown>> {
3540
if (!validateTypeArray(definitions)) {
36-
return false;
41+
throw new Error('Definitions is not of type array')
3742
}
3843

3944
if (!definitions.every((u) => validateTypeRecordStringUnknown(u))) {
40-
return false;
45+
throw new Error('Type definitions is not of Record<string, unknown>')
4146
}
4247

4348
return true;

codify-core/src/plugins/ipc-bridge.ts

Lines changed: 53 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,79 @@
11
import * as ChildProcess from 'node:child_process';
22

33
import { config } from '../project-configs';
4+
import { validateTypeRecordStringUnknown } from '../utils/validator';
5+
import { PluginMessage } from './entities/message';
46
import { Plugin } from './entities/plugin';
57

68
export class PluginIpcBridge {
79

810
static async initializePlugin(directory: string, name: string): Promise<Plugin> {
9-
const childProcess = ChildProcess.fork(location + config.defaultPluginEntryPoint);
11+
const childProcess = ChildProcess.fork(
12+
directory + '/' + name + config.defaultPluginEntryPoint,
13+
[],
14+
{ execArgv: ['-r', 'ts-node/register'], silent: true },
15+
);
1016
const plugin = new Plugin(directory, name, childProcess);
17+
18+
childProcess.stdout!.on('data', (data) => {
19+
console.log(data.toString());
20+
});
21+
22+
childProcess.stderr!.on('data', (data) => {
23+
console.log(data.toString());
24+
})
25+
1126
await this.initializeResourceDefinitions(plugin);
1227

1328
return plugin;
1429
}
1530

1631
static async initializeResourceDefinitions(plugin: Plugin): Promise<void> {
17-
const resourceDefinitions = await this.sendMessageForResult(plugin, 'getResourceDefinitions');
32+
const resourceDefinitions = await this.sendMessageForResult(plugin, { cmd: 'getResourceDefinitions' });
1833
plugin.setResourceDefinitions(resourceDefinitions);
1934
}
2035

21-
private static async sendMessageForResult(plugin: Plugin, rpcFunctionName: string): Promise<void> {
36+
static killPlugin(plugin: Plugin): void {
37+
plugin.process.kill();
38+
}
39+
40+
private static async sendMessageForResult(plugin: Plugin, message: PluginMessage): Promise<unknown> {
2241
return new Promise((resolve, reject) => {
23-
setTimeout(() => reject(new Error(`Plugin did respond in 10s to call: ${rpcFunctionName}`)), 10_000);
24-
plugin.process.on(this.getResultFunctionName(rpcFunctionName), (message) => {
25-
resolve(message);
26-
});
42+
const timer = setTimeout(() => {
43+
plugin.process.kill();
44+
reject(new Error(`Plugin did not respond in 10s to call: ${message.cmd}`))
45+
}, 10_000);
2746

28-
plugin.process.send(rpcFunctionName);
47+
const errorListener = (error: Buffer) => {
48+
plugin.process.kill();
49+
reject(error.toString());
50+
}
51+
52+
const messageListener = (incomingMessage: unknown) => {
53+
console.log(incomingMessage);
54+
55+
if (!validateTypeRecordStringUnknown(incomingMessage)) {
56+
return reject(new Error(`Bad message from plugin ${plugin.name}. ${JSON.stringify(incomingMessage, null, 2)}`))
57+
}
58+
59+
if (incomingMessage.cmd === this.getResultFunctionName(message.cmd)) {
60+
clearTimeout(timer);
61+
plugin.process.removeListener('message', messageListener);
62+
plugin.process.removeListener('error', errorListener);
63+
resolve(incomingMessage.data);
64+
}
65+
};
66+
67+
plugin.process.on('message', messageListener);
68+
plugin.process.stderr!.on('data', errorListener);
69+
plugin.process.send(message);
2970
});
3071
}
3172

73+
private sendMessage(plugin: Plugin, message: PluginMessage): void {
74+
plugin.process.send(message);
75+
}
76+
3277
private static getResultFunctionName(rpcFunctionName: string): string {
3378
return rpcFunctionName + 'Result';
3479
}

codify-core/src/plugins/manager.ts

Lines changed: 0 additions & 41 deletions
This file was deleted.
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import { ParsedProject } from '../config-compiler/parser/entities';
2+
import { ResourceDefinition } from '../entities/resource-definition';
3+
import { Plugin } from './entities/plugin';
4+
import { PluginIpcBridge } from './ipc-bridge';
5+
import { PluginResolver } from './resolver';
6+
7+
type PluginName = string;
8+
9+
const DEFAULT_PLUGINS = {
10+
'default:homebrew': 'latest',
11+
// 'default:node': 'latest',
12+
}
13+
14+
export class PluginCollection {
15+
16+
private plugins: Map<PluginName, Plugin> = new Map();
17+
18+
async initializePlugins(project: ParsedProject): Promise<void> {
19+
const pluginDefinitions = {
20+
...DEFAULT_PLUGINS,
21+
...project.projectConfig.plugins,
22+
};
23+
24+
const pluginResolver = new PluginResolver();
25+
const plugins = await Promise.all(Object.entries(pluginDefinitions).map(([name, version]) =>
26+
pluginResolver.resolve(name, version)
27+
));
28+
29+
for (const u of plugins) {
30+
this.plugins.set(u.name, u);
31+
}
32+
}
33+
34+
async getAllResourceDefinitions(): Promise<Map<string, ResourceDefinition>> {
35+
const result = new Map<string, ResourceDefinition>();
36+
for (const plugin of this.plugins.values()) {
37+
const { resourceDefinitions } = plugin;
38+
if (!resourceDefinitions) {
39+
continue;
40+
}
41+
42+
for (const [name, resourceDef] of resourceDefinitions) {
43+
if (result.has(name)) {
44+
throw new Error(`Resource definition conflict error. Two resource definitions have the same name
45+
${JSON.stringify(resourceDef, null, 2)}
46+
${JSON.stringify(result.get(name))}
47+
`,)
48+
}
49+
50+
result.set(name, resourceDef);
51+
}
52+
}
53+
54+
return result;
55+
}
56+
57+
async killPlugins(): Promise<void> {
58+
for (const plugin of this.plugins.values()) {
59+
PluginIpcBridge.killPlugin(plugin);
60+
}
61+
}
62+
63+
}

codify-core/src/plugins/resolver.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import * as fs from 'node:fs/promises';
33
import { Plugin } from './entities/plugin';
44
import { PluginIpcBridge } from './ipc-bridge';
55

6-
const DEFAULT_PLUGIN_REGEX = /^default:(.*)/gi
6+
const DEFAULT_PLUGIN_REGEX = /(?<=default:).*$/g
77

88
export class PluginResolver {
99

@@ -20,13 +20,13 @@ export class PluginResolver {
2020
private async resolveDefaultPlugin(name: string, _version: string): Promise<Plugin> {
2121
const pluginName = name.match(DEFAULT_PLUGIN_REGEX)![0]
2222

23-
const defaultPluginDir = '../../../';
24-
const pluginDirFiles = await fs.readdir('../../../');
23+
const defaultPluginDir = '../plugins';
24+
const pluginDirFiles = await fs.readdir(defaultPluginDir);
2525
if (!pluginDirFiles.includes(pluginName)) {
2626
throw new Error(`Unable to find default plugin: ${name}`)
2727
}
2828

29-
return PluginIpcBridge.initializePlugin(defaultPluginDir, name);
29+
return PluginIpcBridge.initializePlugin(defaultPluginDir, pluginName);
3030
}
3131

3232
}

0 commit comments

Comments
 (0)