Skip to content

Commit d8caaee

Browse files
[CODE-119] Complete refactor of parser + build improvements + validate improvements + error improvements (#32)
* Refactored a bunch of code related to the parser WIP * Refactored entire parser flow * Added source map cache and btree parsing for yaml source maps * Refactored yaml btree into a separate class * Added source map integration into the code * Fixed compile bugs and added descriptive comments on how the yaml b-tree works * Improved styling for code snippet, misc clean-up and added code snippet to type not found error * Improved error messages * Added support for plugin validation error messages with source maps and code snippets. Moved ajv error message to be a separate function so that it can be re-used. * Fixed codify.json file * Fixed the debugger finally!! Small tweaks and improvements * Removed tsc from start:dev to make development faster. Added additional folders to eslintignore * Fixed bug with re-create. Fixed yaml parser bug.
1 parent b249d43 commit d8caaee

38 files changed

+1379
-464
lines changed

.eslintignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,3 @@
11
/dist
2+
/node_modules
3+
/tmp

bin/dev.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
#!/usr/bin/env -S node --loader ts-node/esm --no-warnings=ExperimentalWarning
1+
#!/usr/bin/env tsx --no-warnings=ExperimentalWarning
22

33
import { execute } from '@oclif/core'
44

package.json

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,16 @@
1111
"ajv": "^8.12.0",
1212
"ajv-formats": "^3.0.1",
1313
"chalk": "^5.3.0",
14-
"codify-schemas": "1.0.39",
14+
"codify-schemas": "1.0.42",
1515
"debug": "^4.3.4",
1616
"ink": "^4.4.1",
1717
"parse-json": "^8.1.0",
1818
"react": "^18.3.1",
1919
"semver": "^7.5.4",
20-
"supports-color": "^9.4.0"
20+
"supports-color": "^9.4.0",
21+
"json-source-map": "^0.6.1",
22+
"js-yaml": "^4.1.0",
23+
"js-yaml-source-map": "^0.2.2"
2124
},
2225
"description": "Codify is a set up as code tool for developers",
2326
"devDependencies": {
@@ -32,6 +35,8 @@
3235
"@types/node": "^18",
3336
"@types/react": "^18.3.1",
3437
"@types/semver": "^7.5.4",
38+
"@types/js-yaml": "^4.0.9",
39+
"@types/strip-ansi": "^5.2.1",
3540
"chai": "^4",
3641
"chai-as-promised": "^7.1.1",
3742
"eslint": "^8.51.0",
@@ -42,6 +47,7 @@
4247
"mock-fs": "^5.2.0",
4348
"oclif": "^4.5.7",
4449
"shx": "^0.3.3",
50+
"strip-ansi": "^7.1.0",
4551
"tsx": "^4.7.3",
4652
"typescript": "^5",
4753
"vitest": "^1.4.0"
@@ -85,14 +91,14 @@
8591
"repository": "kevinwang5658/codify",
8692
"scripts": {
8793
"build": "shx rm -rf dist && tsc -b",
88-
"lint": "eslint . --ext .ts",
94+
"lint": "tsc && eslint . --ext .ts",
8995
"postpack": "shx rm -f oclif.manifest.json",
9096
"pkg": "oclif pack macos -r .",
9197
"posttest": "npm run lint",
9298
"prepack": "npm run build && oclif manifest && oclif readme",
9399
"test": "mocha --forbid-only \"test/**/*.test.ts\"",
94100
"version": "oclif readme && git add README.md",
95-
"start:dev": "tsc && node ./bin/run.js",
101+
"start:dev": "./bin/dev.js",
96102
"start:vm": "npm run build && npm run pack:macos && npm run start:vm"
97103
},
98104
"version": "0.0.0",

src/commands/apply/index.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
11
import { Args, Flags } from '@oclif/core'
2-
import { ResourceOperation } from 'codify-schemas';
32
import * as path from 'node:path';
43

4+
import { BaseCommand } from '../../common/base-command.js';
55
import { ApplyOrchestrator } from '../../orchestrators/apply.js';
66
import { PlanOrchestrator } from '../../orchestrators/plan.js';
7-
import { BaseCommand } from '../../common/base-command.js';
87

98
export default class Apply extends BaseCommand {
109
static args = {

src/common/base-command.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,9 @@ import chalk from 'chalk';
44
import { SudoRequestData } from 'codify-schemas';
55
import createDebug from 'debug';
66

7-
import { ctx, Event } from '../events/context.js';
7+
import { Event, ctx } from '../events/context.js';
88
import { Reporter, ReporterFactory, ReporterType } from '../ui/reporters/reporter.js';
9+
import { prettyPrintError } from './errors.js';
910

1011
export abstract class BaseCommand extends Command {
1112

@@ -53,7 +54,7 @@ export abstract class BaseCommand extends Command {
5354
}
5455

5556
protected async catch(err: Error): Promise<void> {
56-
console.log(chalk.red(err.message));
57+
prettyPrintError(err);
5758
process.exit(1);
5859
}
5960

src/common/errors.test.ts

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import { describe, expect, it } from 'vitest';
2+
import { ajv } from '../utils/ajv';
3+
import { AjvValidationError } from './errors';
4+
import { ResourceSchema } from 'codify-schemas';
5+
import { SourceMapCache } from '../parser/source-maps';
6+
import { JsonParser } from '../parser/json/json-parser';
7+
import { FileType } from '../parser/entities';
8+
import stripAnsi from 'strip-ansi';
9+
10+
describe('AjvValidationError tests', () => {
11+
it('Can properly format a AJV error message without source maps', () => {
12+
const validator = ajv.compile(ResourceSchema);
13+
const content = {
14+
// missing type
15+
"name": "something",
16+
"dependsOn": "supposed to be an array"
17+
};
18+
19+
const isValid = validator(content)
20+
expect(isValid).to.be.false;
21+
22+
const error = new AjvValidationError(
23+
'resource is not valid',
24+
validator.errors
25+
);
26+
27+
console.log(error.formattedMessage())
28+
})
29+
30+
it('Can properly format a AJV error message with source maps', () => {
31+
const contents = `
32+
[
33+
{
34+
"type": "resourceType",
35+
"name": "something",
36+
"dependsOn": []
37+
},
38+
{
39+
"type": "resourceType",
40+
"name": "something",
41+
"dependsOn": "supposed to be an array"
42+
}
43+
]`;
44+
45+
const sourceMaps = new SourceMapCache()
46+
const result = new JsonParser().parse({
47+
fileType: FileType.JSON,
48+
filePath: '/test/path/to/test.json',
49+
contents
50+
}, sourceMaps);
51+
52+
const validator = ajv.compile(ResourceSchema);
53+
const isValid = validator(result[1].contents)
54+
expect(isValid).to.be.false;
55+
56+
const error = new AjvValidationError(
57+
'resource is not valid',
58+
validator.errors,
59+
'/test/path/to/test.json#/1',
60+
sourceMaps
61+
);
62+
63+
console.log(error.formattedMessage())
64+
expect(stripAnsi(error.formattedMessage())).to.eq(
65+
`Validation error: resource is not valid
66+
67+
"/dependsOn" must be array
68+
69+
7| {
70+
8| "type": "resourceType",
71+
9| "name": "something",
72+
> 10| "dependsOn": "supposed to be an array"
73+
11| }
74+
12| ]`)
75+
})
76+
})

src/common/errors.ts

Lines changed: 124 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,117 @@
1+
import { ErrorObject } from 'ajv';
2+
import chalk from 'chalk';
3+
4+
import { ResourceConfig } from '../entities/resource-config.js';
5+
import { SourceMapCache } from '../parser/source-maps.js';
6+
import { formatAjvErrors } from '../utils/ajv.js';
17
import { RemoveErrorMethods } from './types.js';
28

3-
export class InternalError extends Error {
9+
export abstract class CodifyError extends Error {
10+
abstract formattedMessage(): string
11+
}
12+
13+
export class InternalError extends CodifyError {
414
name = 'InternalError'
15+
16+
formattedMessage(): string {
17+
return `Internal error: ${this.message}`;
18+
}
519
}
620

21+
export class AjvValidationError extends CodifyError {
22+
validationErrors: ErrorObject[];
23+
sourceMapKey?: string;
24+
sourceMaps?: SourceMapCache;
25+
26+
constructor(
27+
message: string,
28+
validationErrors: ErrorObject[],
29+
sourceMapKey?: string,
30+
sourceMaps?: SourceMapCache,
31+
) {
32+
super(message);
33+
this.validationErrors = validationErrors;
34+
this.sourceMapKey = sourceMapKey;
35+
this.sourceMaps = sourceMaps;
36+
}
737

8-
export class SyntaxError extends Error {
9-
name = 'ConfigFileSyntaxError'
38+
formattedMessage(): string {
39+
let errorMessage = `Validation error: ${this.message}.\n\n`;
40+
errorMessage += formatAjvErrors(this.validationErrors, this.sourceMapKey, this.sourceMaps)
41+
return errorMessage;
42+
}
43+
}
1044

11-
message!: string;
12-
fileName!: string;
13-
lineNumber!: string;
45+
export type PluginValidationErrorParams = Array<{
46+
customErrorMessage?: string,
47+
resource: ResourceConfig,
48+
schemaErrors: ErrorObject[],
49+
}>
1450

15-
constructor(props: RemoveErrorMethods<SyntaxError>) {
16-
super(props.message)
17-
Object.assign(this, props);
51+
export class PluginValidationError extends CodifyError {
52+
resourceErrors: PluginValidationErrorParams
53+
sourceMaps?: SourceMapCache
54+
55+
constructor(
56+
params: PluginValidationErrorParams,
57+
sourceMaps?: SourceMapCache,
58+
) {
59+
super('Validation error: the following parameters are not supported.\n\n');
60+
this.resourceErrors = params
61+
this.sourceMaps = sourceMaps;
62+
}
63+
64+
formattedMessage(): string {
65+
let errorMessage = `${this.message}`;
66+
67+
for (const resourceError of this.resourceErrors) {
68+
const { customErrorMessage, resource, schemaErrors } = resourceError;
69+
70+
errorMessage += `Resource "${resource.id}" has invalid parameters.\n`
71+
errorMessage += formatAjvErrors(schemaErrors, resource.sourceMapKey, this.sourceMaps)
72+
73+
if (customErrorMessage) {
74+
let childMessage = `${schemaErrors.length + 1}. ${customErrorMessage}\n`
75+
76+
if (resource.sourceMapKey && this.sourceMaps) {
77+
childMessage += `${this.sourceMaps.getCodeSnippet(resource.sourceMapKey)}\n`;
78+
}
79+
80+
errorMessage += childMessage.split(/\n/)
81+
.map((l) => ` ${l}`)
82+
.join('\n')
83+
}
84+
}
85+
86+
return errorMessage;
87+
}
88+
}
89+
90+
export class TypeNotFoundError extends CodifyError {
91+
invalidConfigs: ResourceConfig[];
92+
sourceMaps?: SourceMapCache;
93+
94+
constructor(invalidConfigs: ResourceConfig[], sourceMaps?: SourceMapCache) {
95+
super('Validation error: invalid type found. Resource type was not found in any plugins.')
96+
97+
this.invalidConfigs = invalidConfigs;
98+
this.sourceMaps = sourceMaps;
99+
}
100+
101+
formattedMessage(): string {
102+
let errorMessage = `${this.message}\n\n`
103+
104+
for (const invalidConfig of this.invalidConfigs) {
105+
if (!invalidConfig.sourceMapKey || !this.sourceMaps) {
106+
errorMessage += `type ${invalidConfig.type} is not valid.`
107+
continue;
108+
}
109+
110+
const codeSnippet = this.sourceMaps?.getCodeSnippet(SourceMapCache.combineKeys(invalidConfig.sourceMapKey!, 'type'))
111+
errorMessage += `Type "${invalidConfig.type}" is not valid\n${codeSnippet}`
112+
}
113+
114+
return errorMessage;
18115
}
19116
}
20117

@@ -31,12 +128,28 @@ export class InvalidResourceError extends Error {
31128
}
32129
}
33130

34-
export class JsonFileParseError extends Error {
131+
export class SyntaxError extends CodifyError {
35132
name = 'JsonFileParseError'
36133
fileName!: string;
37134

38-
constructor(props: RemoveErrorMethods<JsonFileParseError>) {
135+
constructor(props: RemoveErrorMethods<SyntaxError>) {
39136
super(props.message)
40137
Object.assign(this, props);
41138
}
139+
140+
formattedMessage(): string {
141+
return `Syntax error: found in codify.json: ${this.message}`
142+
}
143+
}
144+
145+
export function prettyPrintError(error: unknown): void {
146+
if (error instanceof CodifyError) {
147+
return console.error(chalk.red(error.formattedMessage()));
148+
}
149+
150+
if (error instanceof Error) {
151+
return console.error(chalk.red(error.message));
152+
}
153+
154+
console.error(chalk.red(String(error)));
42155
}

src/common/orchestrator.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Project } from '../entities/project.js';
2-
import { ctx, SubProcessName } from '../events/context.js';
2+
import { SubProcessName, ctx } from '../events/context.js';
33
import { DependencyMap, PluginManager } from '../plugins/plugin-manager.js';
44

55
export const CommonOrchestrator = {

src/entities/config.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
1-
import { ConfigClass } from '../parser/language-definition.js';
1+
export enum ConfigType {
2+
PROJECT = 'project',
3+
RESOURCE = 'resource',
4+
}
5+
26

37
export interface ConfigBlock {
4-
configClass: ConfigClass;
8+
configClass: ConfigType;
59
type: string;
6-
7-
validateConfig(config: unknown): never | void;
810
}

0 commit comments

Comments
 (0)