Skip to content

Commit 43ef810

Browse files
committed
Updated to latest codify-schema version and fixed API changes. Added start-vm and init scripts to bin. Added ability to resolve local .ts and .js files for easier testing
1 parent 6a9ab39 commit 43ef810

File tree

8 files changed

+108
-38
lines changed

8 files changed

+108
-38
lines changed

bin/init.sh

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
tart clone ghcr.io/cirruslabs/macos-sonoma-base:latest codify-cli-vm

bin/start-vm.sh

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
#!/bin/bash
2+
3+
tart run codify-sonoma --dir ./dist/macos --no-graphics &
4+
5+
sleep 5
6+
7+
echo "Started tart vm"
8+
tart ip codify-sonoma
9+
10+
sleep 10
11+
12+
if [ -d .Trashes ]; then
13+
echo "Found .Trashes folder generated by Tart. Deleting... Not deleting this will lead to run time issues."
14+
sudo rm -rf .Trashes
15+
fi
16+
17+
if [ -d .fseventsd ]; then
18+
echo "Found .fseventsd folder generated by Tart. Deleting... Not deleting this will lead to run time issues."
19+
sudo rm -rf .fseventsd
20+
fi
21+
22+
# keep this script running indefinitely
23+
tail -f /dev/null
24+
25+
# Kill the vm (child process) when this script ends
26+
trap "tart stop codify-sonoma" EXIT

package.json

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,10 @@
88
"@oclif/plugin-help": "^5",
99
"@oclif/plugin-plugins": "^3.8.4",
1010
"semver": "^7.5.4",
11-
"codify-schemas": "1.0.28",
11+
"codify-schemas": "1.0.32",
1212
"ajv": "^8.12.0",
13-
"ajv-formats": "^3.0.1"
13+
"ajv-formats": "^3.0.1",
14+
"tsx": "^4.7.3"
1415
},
1516
"description": "Codify is a set up as code tool for developers",
1617
"devDependencies": {
@@ -76,12 +77,13 @@
7677
"build": "shx rm -rf dist && tsc -b",
7778
"lint": "eslint . --ext .ts",
7879
"postpack": "shx rm -f oclif.manifest.json",
79-
"pack": "oclif pack macos -r .",
80+
"build:release:macos": "oclif pack macos -r .",
8081
"posttest": "npm run lint",
8182
"prepack": "npm run build && oclif manifest && oclif readme",
8283
"test": "mocha --forbid-only \"test/**/*.test.ts\"",
8384
"version": "oclif readme && git add README.md",
84-
"start:dev": "tsc && node ./bin/run.js"
85+
"start:dev": "tsc && node ./bin/run.js",
86+
"start:vm": "npm run build && npm run pack:macos && npm run start:vm"
8587
},
8688
"version": "0.0.0",
8789
"bugs": "https://github.com/kevinwang5658/codify/issues",

src/entities/project.ts

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -38,15 +38,18 @@ export class Project {
3838
}
3939

4040
handlePluginResourceValidationResults(results: ValidateResponseData[]) {
41-
const isValid = results.reduce((prev, curr) => prev && curr.isValid, true);
42-
if (!isValid) {
43-
const errors = results
44-
.filter((r) => (r.errors?.length ?? 0) > 0)
45-
.flat(1)
46-
.map((e) => JSON.stringify(e, null, 2))
47-
.join('\n\n');
41+
const resultsFlattened = results.flatMap((r) => r.validationResults);
4842

49-
throw new Error(`Config definition errors: \n ${errors}`);
43+
const isValid = resultsFlattened.every((r) => r.isValid);
44+
if (!isValid) {
45+
throw new Error(`Config definition errors:
46+
${JSON.stringify(
47+
resultsFlattened
48+
.filter((r) => !r.isValid),
49+
null,
50+
2
51+
)}
52+
`);
5053
}
5154
}
5255

src/plugins/entities/plugin.ts

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,29 +9,31 @@ import {
99

1010
import { ResourceConfig } from '../../entities/resource-config.js';
1111
import { ajv } from '../../utils/ajv.js';
12-
import { PluginIpcBridge } from '../ipc-bridge.js';
12+
import { PluginProcess } from '../plugin-process.js';
1313

1414
const initializeResponseValidator = ajv.compile(InitializeResponseDataSchema);
1515
const validateResponseValidator = ajv.compile(ValidateResponseDataSchema);
1616
const planResponseValidator = ajv.compile(PlanResponseDataSchema);
1717

1818
export class Plugin {
1919

20-
ipcBridge?: PluginIpcBridge;
20+
process?: PluginProcess;
2121

2222
name: string;
23+
version: string;
2324
path: string;
2425
resourceDependenciesMap = new Map<string, string[]>()
2526

26-
constructor(name: string, path: string) {
27+
constructor(name: string, version: string, path: string) {
2728
this.name = name;
29+
this.version = version;
2830
this.path = path;
2931
}
3032

3133
async initialize(): Promise<InitializeResponseData> {
32-
this.ipcBridge = await PluginIpcBridge.create(this.path);
34+
this.process = await PluginProcess.start(this.path);
3335

34-
const initializeResponse = await this.ipcBridge.sendMessageForResult({ cmd: 'initialize', data: {} });
36+
const initializeResponse = await this.process.sendMessageForResult({ cmd: 'initialize', data: {} });
3537

3638
if (!this.validateInitializeResponse(initializeResponse)) {
3739
throw new Error(`Invalid initialize response from plugin: ${this.name}`);
@@ -46,7 +48,7 @@ export class Plugin {
4648

4749
async validate(configs: ResourceConfig[]): Promise<ValidateResponseData> {
4850
const rawConfigs = configs.map((c) => c.raw);
49-
const response = await this.ipcBridge!.sendMessageForResult({ cmd: 'validate', data: { configs: rawConfigs } });
51+
const response = await this.process!.sendMessageForResult({ cmd: 'validate', data: { configs: rawConfigs } });
5052

5153
if (!this.validateValidateResponse(response)) {
5254
throw new Error(`Invalid validate response from plugin: ${this.name}`);
@@ -56,7 +58,7 @@ export class Plugin {
5658
}
5759

5860
async plan(resource: ResourceConfig): Promise<PlanResponseData> {
59-
const response = await this.ipcBridge!.sendMessageForResult({ cmd: 'plan', data: resource.raw });
61+
const response = await this.process!.sendMessageForResult({ cmd: 'plan', data: resource.raw });
6062

6163
if (!this.validatePlanResponse(response)) {
6264
throw new Error(`Plugin error: plugin ${this.name} returned invalid plan response`)
@@ -66,11 +68,11 @@ export class Plugin {
6668
}
6769

6870
async apply(planId: string): Promise<void> {
69-
await this.ipcBridge!.sendMessageForResult({ cmd: 'apply', data: { planId } });
71+
await this.process!.sendMessageForResult({ cmd: 'apply', data: { planId } });
7072
}
7173

7274
destroy() {
73-
this.ipcBridge!.killPlugin();
75+
this.process!.killPlugin();
7476
}
7577

7678
private validateInitializeResponse(response: unknown): response is InitializeResponseData {
Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { EventEmitter } from 'node:events';
33
import { ChildProcess } from 'node:child_process';
44

55
import { Readable } from 'stream';
6-
import { PluginIpcBridge } from './ipc-bridge.js';
6+
import { PluginProcess } from './plugin-process.js';
77
import { mock } from 'node:test';
88
import { expect } from '@oclif/test';
99
import * as chai from 'chai';
@@ -30,7 +30,7 @@ describe('Plugin IPC Bridge tests', async () => {
3030
const process = mockChildProcess();
3131
const sendMock = mock.method(process, 'send');
3232

33-
const ipcBridge = new PluginIpcBridge(process);
33+
const ipcBridge = new PluginProcess(process);
3434
ipcBridge.sendMessage({ cmd: 'message', data: 'data' })
3535

3636
expect(sendMock.mock.calls.length).to.eq(1);
@@ -39,7 +39,7 @@ describe('Plugin IPC Bridge tests', async () => {
3939

4040
it('send a message and receives the response', async () => {
4141
const process = mockChildProcess();
42-
const ipcBridge = new PluginIpcBridge(process);
42+
const ipcBridge = new PluginProcess(process);
4343

4444
try {
4545
await Promise.all([
@@ -53,7 +53,7 @@ describe('Plugin IPC Bridge tests', async () => {
5353

5454
it('validates bad responses', async () => {
5555
const process = mockChildProcess();
56-
const ipcBridge = new PluginIpcBridge(process);
56+
const ipcBridge = new PluginProcess(process);
5757

5858
try {
5959
await Promise.all([
@@ -67,7 +67,7 @@ describe('Plugin IPC Bridge tests', async () => {
6767

6868
it('does not leave additional listeners', async () => {
6969
const process = mockChildProcess();
70-
const ipcBridge = new PluginIpcBridge(process);
70+
const ipcBridge = new PluginProcess(process);
7171

7272
// NodeJS promise.all is executed in order
7373
await Promise.all([
@@ -82,7 +82,7 @@ describe('Plugin IPC Bridge tests', async () => {
8282

8383
it('does not interfere with existing listeners', async () => {
8484
const process = mockChildProcess();
85-
const ipcBridge = new PluginIpcBridge(process);
85+
const ipcBridge = new PluginProcess(process);
8686
process.on('message', () => {
8787
})
8888

@@ -96,7 +96,7 @@ describe('Plugin IPC Bridge tests', async () => {
9696

9797
it('allows new listeners to be added while waiting for the result', async () => {
9898
const process = mockChildProcess();
99-
const ipcBridge = new PluginIpcBridge(process);
99+
const ipcBridge = new PluginProcess(process);
100100

101101
await Promise.all([
102102
ipcBridge.sendMessageForResult({ cmd: 'message', data: 'data' }),
Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,25 +11,25 @@ type Reject = (reason?: Error) => void;
1111

1212
const resultFunctionName = (cmd: string) => `${cmd}_Response`;
1313

14-
export class PluginIpcBridge {
14+
export class PluginProcess {
1515
process: ChildProcess;
1616

1717
constructor(process: ChildProcess) {
1818
this.process = process;
1919
}
2020

21-
static async create(jsFileDir: string): Promise<PluginIpcBridge> {
21+
static async start(jsFileDir: string): Promise<PluginProcess> {
2222
const process = fork(
2323
jsFileDir,
2424
[],
25-
{ execArgv: ['-r', 'ts-node/register'], silent: true },
25+
{ execArgv: ['--import', 'tsx'], silent: true },
2626
);
2727

2828
process.stdout!.on('data', (message) => console.log(message.toString()));
2929
process.stderr!.on('data', (message) => console.log(message.toString()));
3030

3131

32-
return new PluginIpcBridge(process);
32+
return new PluginProcess(process);
3333
}
3434

3535
killPlugin(): void {

src/plugins/resolver.ts

Lines changed: 42 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,37 @@ const PLUGIN_CACHE_DIR = '/Library/Caches/codify/plugins'
1111

1212
export class PluginResolver {
1313

14-
static async resolve(name: string, version?: string): Promise<Plugin> {
14+
static async resolve(name: string, version: string): Promise<Plugin> {
1515
await PluginResolver.checkAndCreateCacheDirIfNotExists()
1616

17-
// TODO: Add plugin versioning in the future
18-
return this.resolvePlugin(name)
17+
let directoryStat;
18+
try {
19+
directoryStat = await fs.stat(version);
20+
} catch {
21+
}
22+
23+
// For easier development. A direct js file can be specified for the plugin.
24+
if (directoryStat && directoryStat.isFile()) {
25+
return PluginResolver.resolvePluginFs(name, version)
26+
}
27+
28+
return PluginResolver.resolvePluginWeb(name, version)
1929
}
2030

21-
private static async resolvePlugin(name: string): Promise<Plugin> {
31+
private static async resolvePluginFs(name: string, filePath: string): Promise<Plugin> {
32+
const fileExtension = filePath.slice(filePath.lastIndexOf('.'))
33+
if (fileExtension !== '.js' && fileExtension !== '.ts') {
34+
throw new Error(`Only .js and .ts plugins are support currently. Can't resolve ${filePath}`);
35+
}
36+
37+
return new Plugin(
38+
name,
39+
'0.0.0',
40+
filePath,
41+
)
42+
}
43+
44+
private static async resolvePluginWeb(name: string, version: string): Promise<Plugin> {
2245
const { body } = await fetch(DEFAULT_PLUGIN_URL)
2346
if (!body) {
2447
throw new Error(`Un-able to fetch plugin ${name}. Body was null`);
@@ -32,13 +55,26 @@ export class PluginResolver {
3255

3356
return new Plugin(
3457
name,
58+
version,
3559
fileUrl,
3660
)
3761
}
3862

3963
private static async checkAndCreateCacheDirIfNotExists() {
40-
if (!(await fs.stat(PLUGIN_CACHE_DIR))) {
41-
await fs.mkdir(PLUGIN_CACHE_DIR);
64+
let pluginDirStat = null;
65+
try {
66+
pluginDirStat = await fs.stat(PLUGIN_CACHE_DIR)
67+
} catch {
68+
}
69+
70+
if (pluginDirStat && pluginDirStat.isDirectory()) {
71+
return;
4272
}
73+
74+
if (pluginDirStat && !pluginDirStat.isDirectory()) {
75+
throw new Error(`An object already exists at ${PLUGIN_CACHE_DIR} and is not a directory. Please delete and try again`);
76+
}
77+
78+
await fs.mkdir(PLUGIN_CACHE_DIR, { recursive: true });
4379
}
4480
}

0 commit comments

Comments
 (0)