From 7d8dd51aa194612f698b2b61b572de4152704a05 Mon Sep 17 00:00:00 2001 From: kevinwang Date: Sat, 5 Apr 2025 20:42:37 -0400 Subject: [PATCH 1/7] feat: added ability to parse json5 --- README.md | 12 +- codify.json | 13 - codify.json5 | 19 ++ package-lock.json | 76 ++++- package.json | 5 +- src/common/errors.ts | 2 +- src/parser/entities.ts | 3 +- src/parser/index.ts | 4 +- src/parser/json/json-source-map.d.ts | 1 + src/parser/json/source-map.ts | 85 +++++ src/parser/json5/json-parser.test.ts | 41 +++ src/parser/json5/json-parser.ts | 41 +++ src/parser/reader.ts | 4 + src/parser/source-maps.ts | 375 +--------------------- src/parser/yaml/source-map.ts | 287 +++++++++++++++++ src/utils/file-modification-calculator.ts | 4 +- 16 files changed, 568 insertions(+), 404 deletions(-) delete mode 100644 codify.json create mode 100644 codify.json5 create mode 100644 src/parser/json/source-map.ts create mode 100644 src/parser/json5/json-parser.test.ts create mode 100644 src/parser/json5/json-parser.ts create mode 100644 src/parser/yaml/source-map.ts diff --git a/README.md b/README.md index 0a0d8163..045af466 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ $ npm install -g codify $ codify COMMAND running command... $ codify (--version) -codify/0.7.1 darwin-arm64 node-v20.15.1 +codify/0.7.2 darwin-arm64 node-v20.15.1 $ codify --help [COMMAND] USAGE $ codify COMMAND @@ -74,7 +74,7 @@ EXAMPLES $ codify apply -S ``` -_See code: [src/commands/apply.ts](https://github.com/kevinwang5658/codify/blob/v0.7.1/src/commands/apply.ts)_ +_See code: [src/commands/apply.ts](https://github.com/kevinwang5658/codify/blob/v0.7.2/src/commands/apply.ts)_ ## `codify destroy` @@ -113,7 +113,7 @@ EXAMPLES $ codify destroy ``` -_See code: [src/commands/destroy.ts](https://github.com/kevinwang5658/codify/blob/v0.7.1/src/commands/destroy.ts)_ +_See code: [src/commands/destroy.ts](https://github.com/kevinwang5658/codify/blob/v0.7.2/src/commands/destroy.ts)_ ## `codify help [COMMAND]` @@ -189,7 +189,7 @@ EXAMPLES $ codify import \* ``` -_See code: [src/commands/import.ts](https://github.com/kevinwang5658/codify/blob/v0.7.1/src/commands/import.ts)_ +_See code: [src/commands/import.ts](https://github.com/kevinwang5658/codify/blob/v0.7.2/src/commands/import.ts)_ ## `codify init` @@ -217,7 +217,7 @@ EXAMPLES $ codify init ``` -_See code: [src/commands/init.ts](https://github.com/kevinwang5658/codify/blob/v0.7.1/src/commands/init.ts)_ +_See code: [src/commands/init.ts](https://github.com/kevinwang5658/codify/blob/v0.7.2/src/commands/init.ts)_ ## `codify plan` @@ -254,7 +254,7 @@ EXAMPLES $ codify plan -p ../ ``` -_See code: [src/commands/plan.ts](https://github.com/kevinwang5658/codify/blob/v0.7.1/src/commands/plan.ts)_ +_See code: [src/commands/plan.ts](https://github.com/kevinwang5658/codify/blob/v0.7.2/src/commands/plan.ts)_ ## `codify update [CHANNEL]` diff --git a/codify.json b/codify.json deleted file mode 100644 index 3da40469..00000000 --- a/codify.json +++ /dev/null @@ -1,13 +0,0 @@ -[ - { - "type": "project", - "plugins": { - "default": "../codify-homebrew-plugin/src/index.ts" - } - }, - { - "type": "alias", - "alias": "la", - "value": "ls" - } -] diff --git a/codify.json5 b/codify.json5 new file mode 100644 index 00000000..7dedbb60 --- /dev/null +++ b/codify.json5 @@ -0,0 +1,19 @@ +[ + { + "type": "project", + "plugins": { + "default": "../codify-homebrew-plugin/src/index.ts" + } + }, + { + "type": "macports" + }, + // This is a test comment + { + "type": "pnpm", + "globalEnvNodeVersion": null + }, + { + "type": "homebrew" + } +] diff --git a/package-lock.json b/package-lock.json index 5ffcd69b..faa658d2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,17 +1,18 @@ { "name": "codify", - "version": "0.6.0", + "version": "0.7.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "codify", - "version": "0.6.0", + "version": "0.7.2", "license": "MIT", "dependencies": { "@codifycli/ink-form": "0.0.11", "@homebridge/node-pty-prebuilt-multiarch": "^0.12.0-beta.5", "@inkjs/ui": "^2", + "@mischnic/json-sourcemap": "^0.1.1", "@oclif/core": "^4.0.8", "@oclif/plugin-autocomplete": "^3.2.24", "@oclif/plugin-help": "^6.2.4", @@ -31,6 +32,7 @@ "js-yaml": "^4.1.0", "js-yaml-source-map": "^0.2.2", "json-source-map": "^0.6.1", + "json5": "^2.2.3", "latest-semver": "^4.0.0", "nanoid": "^5.0.9", "parse-json": "^8.1.0", @@ -48,6 +50,7 @@ "@types/debug": "^4.1.12", "@types/diff": "^7.0.1", "@types/js-yaml": "^4.0.9", + "@types/json5": "^2.2.0", "@types/mocha": "^10.0.10", "@types/node": "^20", "@types/react": "^18.3.1", @@ -2809,6 +2812,19 @@ "tslib": "2" } }, + "node_modules/@lezer/common": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.2.3.tgz", + "integrity": "sha512-w7ojc8ejBqr2REPsWxJjrMFsA/ysDCFICn8zEOR9mrqzOu2amhITYuLD8ag6XZf0CFXDrhKqw7+tW8cX66NaDA==" + }, + "node_modules/@lezer/lr": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.2.tgz", + "integrity": "sha512-pu0K1jCIdnQ12aWNaAVU5bzi7Bd1w54J3ECgANPmYLtQKP0HBj2cE/5coBD66MT10xbtIuUr7tg0Shbsvk0mDA==", + "dependencies": { + "@lezer/common": "^1.0.0" + } + }, "node_modules/@memlab/core": { "version": "1.1.39", "resolved": "https://registry.npmjs.org/@memlab/core/-/core-1.1.39.tgz", @@ -2884,6 +2900,19 @@ "node": ">=8" } }, + "node_modules/@mischnic/json-sourcemap": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@mischnic/json-sourcemap/-/json-sourcemap-0.1.1.tgz", + "integrity": "sha512-iA7+tyVqfrATAIsIRWQG+a7ZLLD0VaOCKV2Wd/v4mqIU3J9c4jx9p7S0nw1XH3gJCKNBOOwACOPYYSUu9pgT+w==", + "dependencies": { + "@lezer/common": "^1.0.0", + "@lezer/lr": "^1.0.0", + "json5": "^2.2.1" + }, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -4302,10 +4331,14 @@ "license": "MIT" }, "node_modules/@types/json5": { - "version": "0.0.29", - "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", - "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", - "dev": true + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@types/json5/-/json5-2.2.0.tgz", + "integrity": "sha512-NrVug5woqbvNZ0WX+Gv4R+L4TGddtmFek2u8RtccAgFZWtS9QXF2xCXY22/M4nzkaKF0q9Fc6M/5rxLDhfwc/A==", + "deprecated": "This is a stub types definition. json5 provides its own type definitions, so you do not need this installed.", + "dev": true, + "dependencies": { + "json5": "*" + } }, "node_modules/@types/mocha": { "version": "10.0.10", @@ -9968,15 +10001,14 @@ "dev": true }, "node_modules/json5": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", - "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", - "dev": true, - "dependencies": { - "minimist": "^1.2.0" - }, + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", "bin": { "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" } }, "node_modules/jsonfile": { @@ -12921,6 +12953,24 @@ "strip-bom": "^3.0.0" } }, + "node_modules/tsconfig-paths/node_modules/@types/json5": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", + "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", + "dev": true + }, + "node_modules/tsconfig-paths/node_modules/json5": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", + "dev": true, + "dependencies": { + "minimist": "^1.2.0" + }, + "bin": { + "json5": "lib/cli.js" + } + }, "node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", diff --git a/package.json b/package.json index 3d2028cf..90dd4175 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,9 @@ "parse-json": "^8.1.0", "react": "^18.3.1", "semver": "^7.5.4", - "supports-color": "^9.4.0" + "supports-color": "^9.4.0", + "json5": "^2.2.3", + "@mischnic/json-sourcemap": "^0.1.1" }, "description": "Codify allows users to configure settings, install new packages, and automate their systems using code instead of the GUI. Get set up on a new laptop in one click, maintain a Codify file within your project so anyone can get started and never lose your cool apps or favourite settings again.", "devDependencies": { @@ -47,6 +49,7 @@ "@types/semver": "^7.5.4", "@types/strip-ansi": "^5.2.1", "@typescript-eslint/eslint-plugin": "^8.16.0", + "@types/json5": "^2.2.0", "codify-plugin-lib": "^1.0.151", "esbuild": "^0.24.0", "esbuild-plugin-copy": "^2.1.1", diff --git a/src/common/errors.ts b/src/common/errors.ts index bbe23a16..2af0ffa1 100644 --- a/src/common/errors.ts +++ b/src/common/errors.ts @@ -138,7 +138,7 @@ export class SyntaxError extends CodifyError { } formattedMessage(): string { - return `Syntax error: found in codify.json: ${this.message}` + return `Syntax error: found in ${this.fileName}: ${this.message}` } } diff --git a/src/parser/entities.ts b/src/parser/entities.ts index e9b5e935..327f0c62 100644 --- a/src/parser/entities.ts +++ b/src/parser/entities.ts @@ -18,5 +18,6 @@ export interface LanguageSpecificParser { export enum FileType { JSON = 'json', - YAML = 'yaml' + YAML = 'yaml', + JSON5 = 'json5' } diff --git a/src/parser/index.ts b/src/parser/index.ts index 2a5b3020..bd158c9d 100644 --- a/src/parser/index.ts +++ b/src/parser/index.ts @@ -7,16 +7,18 @@ import { Project } from '../entities/project.js'; import { ConfigFactory } from './config-factory.js'; import { FileType, InMemoryFile, ParsedConfig } from './entities.js'; import { JsonParser } from './json/json-parser.js'; +import { Json5Parser } from './json5/json-parser.js'; import { FileReader } from './reader.js'; import { SourceMapCache } from './source-maps.js'; import { YamlParser } from './yaml/yaml-parser.js'; -export const CODIFY_FILE_REGEX = /^(.*)?codify(.json|.yaml)$/; +export const CODIFY_FILE_REGEX = /^(.*)?codify(.json|.yaml|.json5)$/; class Parser { private readonly languageSpecificParsers= { [FileType.JSON]: new JsonParser(), [FileType.YAML]: new YamlParser(), + [FileType.JSON5]: new Json5Parser() } async parse(dirOrFile: string): Promise { diff --git a/src/parser/json/json-source-map.d.ts b/src/parser/json/json-source-map.d.ts index 5e2005ad..4149e9cf 100644 --- a/src/parser/json/json-source-map.d.ts +++ b/src/parser/json/json-source-map.d.ts @@ -1,5 +1,6 @@ declare module 'json-source-map' { export interface JsonSourceMap { + data: any; pointers: JsonPointerObject; } diff --git a/src/parser/json/source-map.ts b/src/parser/json/source-map.ts new file mode 100644 index 00000000..4216f7fe --- /dev/null +++ b/src/parser/json/source-map.ts @@ -0,0 +1,85 @@ +import { JsonSourceMap } from 'json-source-map'; + +import { SourceMap, SourceMapPointer } from '../source-maps.js'; + +export class JsonSourceMapAdapter implements SourceMap { + + /** + * { + * "data": [ + * { + * "type": "type1" + * }, + * { + * "type": "type2", + * "propA": "a", + * "propB": "b" + * } + * ], + * "pointers": { + * "": { + * "value": { "line": 1, "column": 0, "pos": 1 }, + * "valueEnd": { "line": 10, "column": 1, "pos": 97 } + * }, + * "/0": { + * "value": { "line": 2, "column": 2, "pos": 5 }, + * "valueEnd": { "line": 4, "column": 3, "pos": 30 } + * }, + * "/0/type": { + * "key": { "line": 3, "column": 4, "pos": 11 }, + * "keyEnd": { "line": 3, "column": 10, "pos": 17 }, + * "value": { "line": 3, "column": 12, "pos": 19 }, + * "valueEnd": { "line": 3, "column": 19, "pos": 26 } + * }, + * "/1": { + * "value": { "line": 5, "column": 2, "pos": 34 }, + * "valueEnd": { "line": 9, "column": 3, "pos": 95 } + * }, + * "/1/type": { + * "key": { "line": 6, "column": 4, "pos": 40 }, + * "keyEnd": { "line": 6, "column": 10, "pos": 46 }, + * "value": { "line": 6, "column": 12, "pos": 48 }, + * "valueEnd": { "line": 6, "column": 19, "pos": 55 } + * }, + * "/1/propA": { + * "key": { "line": 7, "column": 4, "pos": 61 }, + * "keyEnd": { "line": 7, "column": 11, "pos": 68 }, + * "value": { "line": 7, "column": 13, "pos": 70 }, + * "valueEnd": { "line": 7, "column": 16, "pos": 73 } + * }, + * "/1/propB": { + * "key": { "line": 8, "column": 4, "pos": 79 }, + * "keyEnd": { "line": 8, "column": 11, "pos": 86 }, + * "value": { "line": 8, "column": 13, "pos": 88 }, + * "valueEnd": { "line": 8, "column": 16, "pos": 91 } + * } + * } + * } + */ + private jsonSourceMap: JsonSourceMap; + + constructor(jsonSourceMap: JsonSourceMap) { + this.jsonSourceMap = jsonSourceMap; + } + + lookup(jsonKey: string): SourceMapPointer | null { + const pointer = this.jsonSourceMap.pointers[jsonKey]; + if (!pointer) { + return null; + } + + return { + value: { + line: pointer.value.line, + column: pointer.value.column, + position: pointer.value.pos, + }, + valueEnd: { + line: pointer.valueEnd.line, + column: pointer.valueEnd.column, + position: pointer.valueEnd.pos, + } + } + } +} + diff --git a/src/parser/json5/json-parser.test.ts b/src/parser/json5/json-parser.test.ts new file mode 100644 index 00000000..8a8da3c7 --- /dev/null +++ b/src/parser/json5/json-parser.test.ts @@ -0,0 +1,41 @@ +import { describe, expect, it } from 'vitest'; +import { Json5Parser } from './json-parser.js'; +import { FileType, InMemoryFile } from '../entities.js'; + +describe('JSONParser tests', () => { + it('Can parse a codify json file', () => { + const json = `[ + { "type": "resourceA", "propA": "propA" }, + { "type": "project", "description": "description" }, + // This is a comment + { + "type": 'resourceB' + } +]` + + const inMemoryFile: InMemoryFile = { + filePath: '/path/to/test.json5', + fileType: FileType.JSON, + contents: json + } + + const jsonParser = new Json5Parser() + const result = jsonParser.parse(inMemoryFile) + expect(result.length).to.eq(3); + + expect(result[0]).toMatchObject({ + contents: { type: 'resourceA', propA: 'propA' }, + sourceMapKey: "/path/to/test.json5#/0" + }) + + expect(result[1]).toMatchObject({ + contents: { type: 'project', description: 'description' }, + sourceMapKey: "/path/to/test.json5#/1", + }) + + expect(result[2]).toMatchObject({ + contents: { type: 'resourceB' }, + sourceMapKey: "/path/to/test.json5#/2" + }) + }) +}) diff --git a/src/parser/json5/json-parser.ts b/src/parser/json5/json-parser.ts new file mode 100644 index 00000000..6cffd65c --- /dev/null +++ b/src/parser/json5/json-parser.ts @@ -0,0 +1,41 @@ +import JsonSourceMap from '@mischnic/json-sourcemap'; +import { Config, ConfigFileSchema } from 'codify-schemas'; +import JSON5 from 'json5' + +import { AjvValidationError, SyntaxError } from '../../common/errors.js'; +import { ajv } from '../../utils/ajv.js'; +import { InMemoryFile, LanguageSpecificParser, ParsedConfig } from '../entities.js'; +import { SourceMapCache } from '../source-maps.js'; + +const validator = ajv.compile(ConfigFileSchema); + +export class Json5Parser implements LanguageSpecificParser { + parse(file: InMemoryFile, sourceMaps?: SourceMapCache): ParsedConfig[] { + let content; + try { + content = JSON5.parse(file.contents); + + if (sourceMaps) { + sourceMaps.addSourceMap(file, JsonSourceMap.parse(file.contents, undefined, { dialect: 'JSON5' })); + } + } catch (error) { + throw new SyntaxError({ + fileName: file.filePath, + message: (error as Error).message, + }); + } + + if (!this.validate(content)) { + throw new AjvValidationError('invalid config file', validator.errors!, file.filePath, sourceMaps); + } + + return content.map((contents, idx) => ({ + contents, + sourceMapKey: SourceMapCache.constructKey(file.filePath, `/${idx}`) + })) + } + + private validate(content: unknown): content is Config[] { + return validator(content); + } +} diff --git a/src/parser/reader.ts b/src/parser/reader.ts index f3d5cce6..101ead99 100644 --- a/src/parser/reader.ts +++ b/src/parser/reader.ts @@ -22,6 +22,10 @@ export class FileReader { if (filePath.endsWith('.yaml')) { return FileType.YAML } + + if (filePath.endsWith('.json5')) { + return FileType.JSON5 + } throw new InternalError(`Unsupported file type passed to FileReader. File path: ${filePath}`); } diff --git a/src/parser/source-maps.ts b/src/parser/source-maps.ts index 5df8521f..f5ab7296 100644 --- a/src/parser/source-maps.ts +++ b/src/parser/source-maps.ts @@ -3,6 +3,8 @@ import { default as YamlSourceMap, SourceLocation as YamlSourceLocation } from ' import { FileType, InMemoryFile } from './entities.js'; import { InternalError } from '../common/errors.js'; import chalk from 'chalk'; +import { JsonSourceMapAdapter } from './json/source-map.js'; +import { YamlSourceMapAdapter } from './yaml/source-map.js'; type FilePath = string; @@ -42,17 +44,21 @@ export class SourceMapCache { } addSourceMap(file: InMemoryFile, sourceMap: JsonSourceMap | YamlSourceMap) { - const isJson = file.fileType === FileType.JSON; - if (isJson) { + if (file.fileType === FileType.JSON) { this.sourceMaps.set(file.filePath, { file, sourceMap: new JsonSourceMapAdapter(sourceMap as JsonSourceMap), }) - } else { + } else if (file.fileType === FileType.YAML) { this.sourceMaps.set(file.filePath, { file, sourceMap: new YamlSourceMapAdapter(sourceMap as YamlSourceMap, file), }) + } else if (file.fileType === FileType.JSON5) { + this.sourceMaps.set(file.filePath, { + file, + sourceMap: new JsonSourceMapAdapter(sourceMap as JsonSourceMap), + }) } } @@ -148,366 +154,3 @@ export interface SourceLocation { export interface SourceMap { lookup(jsonKey: string): SourceMapPointer | null; } - -export class JsonSourceMapAdapter implements SourceMap { - - /** - * { - * "data": [ - * { - * "type": "type1" - * }, - * { - * "type": "type2", - * "propA": "a", - * "propB": "b" - * } - * ], - * "pointers": { - * "": { - * "value": { "line": 1, "column": 0, "pos": 1 }, - * "valueEnd": { "line": 10, "column": 1, "pos": 97 } - * }, - * "/0": { - * "value": { "line": 2, "column": 2, "pos": 5 }, - * "valueEnd": { "line": 4, "column": 3, "pos": 30 } - * }, - * "/0/type": { - * "key": { "line": 3, "column": 4, "pos": 11 }, - * "keyEnd": { "line": 3, "column": 10, "pos": 17 }, - * "value": { "line": 3, "column": 12, "pos": 19 }, - * "valueEnd": { "line": 3, "column": 19, "pos": 26 } - * }, - * "/1": { - * "value": { "line": 5, "column": 2, "pos": 34 }, - * "valueEnd": { "line": 9, "column": 3, "pos": 95 } - * }, - * "/1/type": { - * "key": { "line": 6, "column": 4, "pos": 40 }, - * "keyEnd": { "line": 6, "column": 10, "pos": 46 }, - * "value": { "line": 6, "column": 12, "pos": 48 }, - * "valueEnd": { "line": 6, "column": 19, "pos": 55 } - * }, - * "/1/propA": { - * "key": { "line": 7, "column": 4, "pos": 61 }, - * "keyEnd": { "line": 7, "column": 11, "pos": 68 }, - * "value": { "line": 7, "column": 13, "pos": 70 }, - * "valueEnd": { "line": 7, "column": 16, "pos": 73 } - * }, - * "/1/propB": { - * "key": { "line": 8, "column": 4, "pos": 79 }, - * "keyEnd": { "line": 8, "column": 11, "pos": 86 }, - * "value": { "line": 8, "column": 13, "pos": 88 }, - * "valueEnd": { "line": 8, "column": 16, "pos": 91 } - * } - * } - * } - */ - private jsonSourceMap: JsonSourceMap; - - constructor(jsonSourceMap: JsonSourceMap) { - this.jsonSourceMap = jsonSourceMap; - } - - lookup(jsonKey: string): SourceMapPointer | null { - const pointer = this.jsonSourceMap.pointers[jsonKey]; - if (!pointer) { - return null; - } - - return { - value: { - line: pointer.value.line, - column: pointer.value.column, - position: pointer.value.pos, - }, - valueEnd: { - line: pointer.valueEnd.line, - column: pointer.valueEnd.column, - position: pointer.valueEnd.pos, - } - } - } -} - -export class YamlSourceMapAdapter implements SourceMap { - - /** - * (empty line) - * - type: project - * plugins: - * default: "../homebrew-plugin/src/index.ts" - * - type: nvm - * global: '18.20' - * nodeVersions: - * - '18.20' - * - type: homebrew - * formulae: - * - cirruslabs/cli/cirrus - * - cirruslabs/cli/tart - * - type: vscode - * - * SourceMap { - * _map: { - * '.3.type': { line: 12, position: 218, lineStart: 212 }, - * '.3': { line: 12, position: 218, lineStart: 212 }, - * '.2.formulae.1': { line: 11, position: 211, lineStart: 188 }, - * '.2.formulae.0': { line: 10, position: 187, lineStart: 162 }, - * '.2.formulae': { line: 10, position: 187, lineStart: 162 }, - * '.2.type': { line: 8, position: 139, lineStart: 133 }, - * '.2': { line: 8, position: 139, lineStart: 133 }, - * '.1.nodeVersions.0': { line: 7, position: 132, lineStart: 121 }, - * '.1.nodeVersions': { line: 7, position: 132, lineStart: 121 }, - * '.1.global': { line: 5, position: 95, lineStart: 87 }, - * '.1.type': { line: 4, position: 81, lineStart: 75 }, - * '.1': { line: 4, position: 81, lineStart: 75 }, - * '.plugins.default': { line: 3, position: 39, lineStart: 28 }, - * '.plugins': { line: 3, position: 39, lineStart: 28 }, - * '.type': { line: 1, position: 7, lineStart: 1 } - * '.': { line: 1, position: 1, lineStart: 1 }, - * }, - * _path: [], - * _lastScalar: 'vscode', - * _fragments: [], - * _count: 29 - * } - */ - private yamlSourceMap: YamlSourceMap; - private sourceMapTree: YamlSourceMapBTree; - - constructor(yamlSourceMap: YamlSourceMap, file: InMemoryFile) { - this.yamlSourceMap = yamlSourceMap; - - const original = file.contents; - const originalLines = file.contents.split(/\n/gm); - - this.sourceMapTree = new YamlSourceMapBTree( - yamlSourceMap, - { line: originalLines.length - 1, position: original.length - 1 } - ) - } - - lookup(jsonKey: string): SourceMapPointer | null { - const yamlKey = this.convertJsonKeyToYaml(jsonKey) - const pointer = this.yamlSourceMap.lookup(yamlKey) - if (!pointer) { - return null; - } - - const endPointer = this.calculateEndPointer(yamlKey); - - return { - value: { - line: pointer.line - 1, - column: pointer.column - 1, - position: pointer.position, - }, - valueEnd: { - line: endPointer.line - 1, - column: endPointer.column -1, - position: endPointer.position, - } - } - } - - private convertJsonKeyToYaml(key: string): string { - return key.replace(/^\/0/, '').replace(/^\//, '').replace(/\//g, '.'); - } - - private calculateEndPointer(key: string): YamlSourceLocation { - const nextElement = this.sourceMapTree.findNextElement(key); - return nextElement.value; - } -} - -interface YamlBTreeNode { - key: string; - value: YamlSourceLocation; - children: YamlBTreeNode[]; -} - -/** - * A helper b-tree to find the corresponding end line number for each item in the source map. This is because js-yaml-source-map - * does not provide valueEnd unlike js-source-map. - * - * How it works is it constructs a b-tree that presents the yaml and then retrieves the next element on the same level. - * If it happens to be the last element at that level, it tries to retrieve the next element on the parent level recursively. - * If it's the very last element, it'll return a (fake) end node representing the end of the yaml - * - * Example yaml: - * - type: project - * plugins: - * default: "../homebrew-plugin/src/index.ts" - * - type: nvm - * global: '18.20' - * nodeVersions: - * - '18.20' - * - * Becomes the below but in tree form: - * - * '.' - * '.0' - * 'type' - * 'plugins' - * ... - * '.1' - * 'type' - * 'global' - * 'nodeVersions' - * ... - * 'end' - * - * If we're looking for the next element of '.0', it'll return '.1' - * If we're looking for the next element of '.1.nodeVersions', it'll return 'end' - */ -export class YamlSourceMapBTree { - sourceMapTree: YamlBTreeNode[]; - - private endLine: number; - private endPosition: number; - private integerRegex = /^[0-9]+$/g - - constructor(sourceMap: YamlSourceMap, end: { line: number; position: number }) { - this.endLine = end.line; - this.endPosition = end.position; - - this.sourceMapTree = this.constructSourceMapBTree(sourceMap); - } - - private constructSourceMapBTree(sourceMap: YamlSourceMap): YamlBTreeNode[] { - const sortedKeys = [...Object.entries(sourceMap.map)] - // Hack: There is a bug in js-yaml-source-maps where the first element of a top level array is not treated as an array - .map(([k, v]) => [this.mapToFixedKey(k), v] as const) - // Sort the entries to make easier to construct the B-tree. Use the position to sort. - .sort(([k1, v1], [k2, v2]) => v1.position - v2.position) - .map(([k]) => k) - - // Hack: add this to match the json source map version. Json source map adds an empty '' top level key which translates - // to '.' in yaml - sortedKeys.unshift('.'); - - const tree: YamlBTreeNode[] = []; - for (const key of sortedKeys) { - const parts = key.split('.').filter(Boolean) - const originalKey = this.mapToOriginalKey(key); - - // Root node - if (parts.length === 0) { - tree.push({ - key, - value: sourceMap.lookup(originalKey)!, - children: [], - }) - continue; - } - - recursiveBTreeInsert(tree[0], parts, sourceMap.lookup(originalKey)!, 0) - } - - // For some reason, the js-yaml-source-map likes to 1 index numbers. We have to account for that with our custom end node - // This end node is useful for the findNextElement. It makes sure there is a end element opposite the top level '.' - tree.push({ - key: 'end', - value: { - line: this.endLine + 1, - column: 1, - position: this.endPosition, - }, - children: [] - }) - - return tree; - - /** - * Recursively attempt to put in each node. This only works because the nodes are inserted in sorted order. In the sort, - * parents always come before their children. This always takes advantage of the pre-sorted order to order the b-tree children's array - */ - function recursiveBTreeInsert(node: YamlBTreeNode, keyParts: string[], value: YamlSourceLocation, idx: number): void { - if (idx === keyParts.length - 1) { - node.children.push({ - key: keyParts[idx], - value, - children: [] - }); - return; - } - - const keyPart = keyParts[idx]; - const childNode = node.children.find((c) => c.key === keyPart) - if (!childNode) { - throw new InternalError(`Unable to insert into btree when constructing yaml source map. \n\n${JSON.stringify(node, null, 2)} \n\n${keyParts} \n\n${idx}`); - } - - return recursiveBTreeInsert(childNode, keyParts, value, idx + 1); - } - } - - // Key here is a yaml key - findNextElement(key: string): YamlBTreeNode { - const fixedKey = this.mapToFixedKey(key); - const keyParts = fixedKey.split('.').filter(Boolean); - if (keyParts.length === 0) { - return this.sourceMapTree[1]; - } - - const nextElement = recursiveFindNextElement(this.sourceMapTree[0], keyParts) - if (!nextElement) { - return this.sourceMapTree[1]; - } - - return nextElement; - - function recursiveFindNextElement(node: YamlBTreeNode, keyParts: string[], idx = 0): YamlBTreeNode | null { - if (idx === keyParts.length - 1) { - const part = keyParts[idx]; - const currentNodeIdx = node.children.findIndex((c) => c.key === part); - return node.children[currentNodeIdx + 1] ?? null; - } - - const nextPart = keyParts[idx]; - const nextNode = node.children.find((c) => c.key === nextPart) - if (!nextNode) { - throw Error(`Internal error: invalid path ${keyParts} provided to b-tree next element`) - } - - const result = recursiveFindNextElement(nextNode, keyParts, idx + 1) - if (!result) { - const part = keyParts[idx]; - const currentNodeIdx = node.children.findIndex((c) => c.key === part); - return node.children[currentNodeIdx + 1] ?? null; - } - - return result; - } - } - - // This is a fix to account for the fact that js-yaml-source-maps can't handle - // top level arrays. We have to manually add the .0 key for the tree doesn't think that - // the first element of a top level array is actually part of the root object - private mapToFixedKey(key: string): string { - if (key === '') { - return '.' - } - - if (!key.startsWith('.')) { - key = `.${key}` - } - - const firstKey = key.split('.').filter(Boolean)[0] - if (!firstKey) { - return '.0'; - } - - return !/^[0-9]+$/.test(firstKey) - ? `.0${key}` - : key; - } - - private mapToOriginalKey(fixedKey: string): string { - return fixedKey === '.0' - ? '.' - : fixedKey.includes('.0') - ? fixedKey.slice(2) - : fixedKey - } -} diff --git a/src/parser/yaml/source-map.ts b/src/parser/yaml/source-map.ts new file mode 100644 index 00000000..d98601e8 --- /dev/null +++ b/src/parser/yaml/source-map.ts @@ -0,0 +1,287 @@ +import { SourceLocation as YamlSourceLocation, default as YamlSourceMap } from 'js-yaml-source-map'; + +import { InternalError } from '../../common/errors.js'; +import { InMemoryFile } from '../entities.js'; +import { SourceMap, SourceMapPointer } from '../source-maps.js'; + +export class YamlSourceMapAdapter implements SourceMap { + + /** + * (empty line) + * - type: project + * plugins: + * default: "../homebrew-plugin/src/index.ts" + * - type: nvm + * global: '18.20' + * nodeVersions: + * - '18.20' + * - type: homebrew + * formulae: + * - cirruslabs/cli/cirrus + * - cirruslabs/cli/tart + * - type: vscode + * + * SourceMap { + * _map: { + * '.3.type': { line: 12, position: 218, lineStart: 212 }, + * '.3': { line: 12, position: 218, lineStart: 212 }, + * '.2.formulae.1': { line: 11, position: 211, lineStart: 188 }, + * '.2.formulae.0': { line: 10, position: 187, lineStart: 162 }, + * '.2.formulae': { line: 10, position: 187, lineStart: 162 }, + * '.2.type': { line: 8, position: 139, lineStart: 133 }, + * '.2': { line: 8, position: 139, lineStart: 133 }, + * '.1.nodeVersions.0': { line: 7, position: 132, lineStart: 121 }, + * '.1.nodeVersions': { line: 7, position: 132, lineStart: 121 }, + * '.1.global': { line: 5, position: 95, lineStart: 87 }, + * '.1.type': { line: 4, position: 81, lineStart: 75 }, + * '.1': { line: 4, position: 81, lineStart: 75 }, + * '.plugins.default': { line: 3, position: 39, lineStart: 28 }, + * '.plugins': { line: 3, position: 39, lineStart: 28 }, + * '.type': { line: 1, position: 7, lineStart: 1 } + * '.': { line: 1, position: 1, lineStart: 1 }, + * }, + * _path: [], + * _lastScalar: 'vscode', + * _fragments: [], + * _count: 29 + * } + */ + private yamlSourceMap: YamlSourceMap; + private sourceMapTree: YamlSourceMapBTree; + + constructor(yamlSourceMap: YamlSourceMap, file: InMemoryFile) { + this.yamlSourceMap = yamlSourceMap; + + const original = file.contents; + const originalLines = file.contents.split(/\n/gm); + + this.sourceMapTree = new YamlSourceMapBTree( + yamlSourceMap, + { line: originalLines.length - 1, position: original.length - 1 } + ) + } + + lookup(jsonKey: string): SourceMapPointer | null { + const yamlKey = this.convertJsonKeyToYaml(jsonKey) + const pointer = this.yamlSourceMap.lookup(yamlKey) + if (!pointer) { + return null; + } + + const endPointer = this.calculateEndPointer(yamlKey); + + return { + value: { + line: pointer.line - 1, + column: pointer.column - 1, + position: pointer.position, + }, + valueEnd: { + line: endPointer.line - 1, + column: endPointer.column -1, + position: endPointer.position, + } + } + } + + private convertJsonKeyToYaml(key: string): string { + return key.replace(/^\/0/, '').replace(/^\//, '').replaceAll('/', '.'); + } + + private calculateEndPointer(key: string): YamlSourceLocation { + const nextElement = this.sourceMapTree.findNextElement(key); + return nextElement.value; + } +} + +interface YamlBTreeNode { + key: string; + value: YamlSourceLocation; + children: YamlBTreeNode[]; +} + +/** + * A helper b-tree to find the corresponding end line number for each item in the source map. This is because js-yaml-source-map + * does not provide valueEnd unlike js-source-map. + * + * How it works is it constructs a b-tree that presents the yaml and then retrieves the next element on the same level. + * If it happens to be the last element at that level, it tries to retrieve the next element on the parent level recursively. + * If it's the very last element, it'll return a (fake) end node representing the end of the yaml + * + * Example yaml: + * - type: project + * plugins: + * default: "../homebrew-plugin/src/index.ts" + * - type: nvm + * global: '18.20' + * nodeVersions: + * - '18.20' + * + * Becomes the below but in tree form: + * + * '.' + * '.0' + * 'type' + * 'plugins' + * ... + * '.1' + * 'type' + * 'global' + * 'nodeVersions' + * ... + * 'end' + * + * If we're looking for the next element of '.0', it'll return '.1' + * If we're looking for the next element of '.1.nodeVersions', it'll return 'end' + */ +export class YamlSourceMapBTree { + sourceMapTree: YamlBTreeNode[]; + + private endLine: number; + private endPosition: number; + private integerRegex = /^\d+$/g + + constructor(sourceMap: YamlSourceMap, end: { line: number; position: number }) { + this.endLine = end.line; + this.endPosition = end.position; + + this.sourceMapTree = this.constructSourceMapBTree(sourceMap); + } + + private constructSourceMapBTree(sourceMap: YamlSourceMap): YamlBTreeNode[] { + const sortedKeys = [...Object.entries(sourceMap.map)] + // Hack: There is a bug in js-yaml-source-maps where the first element of a top level array is not treated as an array + .map(([k, v]) => [this.mapToFixedKey(k), v] as const) + // Sort the entries to make easier to construct the B-tree. Use the position to sort. + .sort(([k1, v1], [k2, v2]) => v1.position - v2.position) + .map(([k]) => k) + + // Hack: add this to match the json source map version. Json source map adds an empty '' top level key which translates + // to '.' in yaml + sortedKeys.unshift('.'); + + const tree: YamlBTreeNode[] = []; + for (const key of sortedKeys) { + const parts = key.split('.').filter(Boolean) + const originalKey = this.mapToOriginalKey(key); + + // Root node + if (parts.length === 0) { + tree.push({ + key, + value: sourceMap.lookup(originalKey)!, + children: [], + }) + continue; + } + + recursiveBTreeInsert(tree[0], parts, sourceMap.lookup(originalKey)!, 0) + } + + // For some reason, the js-yaml-source-map likes to 1 index numbers. We have to account for that with our custom end node + // This end node is useful for the findNextElement. It makes sure there is a end element opposite the top level '.' + tree.push({ + key: 'end', + value: { + line: this.endLine + 1, + column: 1, + position: this.endPosition, + }, + children: [] + }) + + return tree; + + /** + * Recursively attempt to put in each node. This only works because the nodes are inserted in sorted order. In the sort, + * parents always come before their children. This always takes advantage of the pre-sorted order to order the b-tree children's array + */ + function recursiveBTreeInsert(node: YamlBTreeNode, keyParts: string[], value: YamlSourceLocation, idx: number): void { + if (idx === keyParts.length - 1) { + node.children.push({ + key: keyParts[idx], + value, + children: [] + }); + return; + } + + const keyPart = keyParts[idx]; + const childNode = node.children.find((c) => c.key === keyPart) + if (!childNode) { + throw new InternalError(`Unable to insert into btree when constructing yaml source map. \n\n${JSON.stringify(node, null, 2)} \n\n${keyParts} \n\n${idx}`); + } + + return recursiveBTreeInsert(childNode, keyParts, value, idx + 1); + } + } + + // Key here is a yaml key + findNextElement(key: string): YamlBTreeNode { + const fixedKey = this.mapToFixedKey(key); + const keyParts = fixedKey.split('.').filter(Boolean); + if (keyParts.length === 0) { + return this.sourceMapTree[1]; + } + + const nextElement = recursiveFindNextElement(this.sourceMapTree[0], keyParts) + if (!nextElement) { + return this.sourceMapTree[1]; + } + + return nextElement; + + function recursiveFindNextElement(node: YamlBTreeNode, keyParts: string[], idx = 0): YamlBTreeNode | null { + if (idx === keyParts.length - 1) { + const part = keyParts[idx]; + const currentNodeIdx = node.children.findIndex((c) => c.key === part); + return node.children[currentNodeIdx + 1] ?? null; + } + + const nextPart = keyParts[idx]; + const nextNode = node.children.find((c) => c.key === nextPart) + if (!nextNode) { + throw new Error(`Internal error: invalid path ${keyParts} provided to b-tree next element`) + } + + const result = recursiveFindNextElement(nextNode, keyParts, idx + 1) + if (!result) { + const part = keyParts[idx]; + const currentNodeIdx = node.children.findIndex((c) => c.key === part); + return node.children[currentNodeIdx + 1] ?? null; + } + + return result; + } + } + + // This is a fix to account for the fact that js-yaml-source-maps can't handle + // top level arrays. We have to manually add the .0 key for the tree doesn't think that + // the first element of a top level array is actually part of the root object + private mapToFixedKey(key: string): string { + if (key === '') { + return '.' + } + + if (!key.startsWith('.')) { + key = `.${key}` + } + + const firstKey = key.split('.').find(Boolean) + if (!firstKey) { + return '.0'; + } + + return /^\d+$/.test(firstKey) + ? key + : `.0${key}`; + } + + private mapToOriginalKey(fixedKey: string): string { + return fixedKey === '.0' + ? '.' + : fixedKey.includes('.0') + ? fixedKey.slice(2) + : fixedKey + } +} diff --git a/src/utils/file-modification-calculator.ts b/src/utils/file-modification-calculator.ts index b2d5cbb4..4db2f48f 100644 --- a/src/utils/file-modification-calculator.ts +++ b/src/utils/file-modification-calculator.ts @@ -110,8 +110,8 @@ export class FileModificationCalculator { return; } - if (this.existingFile?.fileType !== FileType.JSON) { - throw new Error(`Only updating .json files are currently supported. Found ${this.existingFile?.filePath}`); + if (this.existingFile?.fileType !== FileType.JSON && this.existingFile?.fileType !== FileType.JSON5) { + throw new Error(`Only updating .json and .json5 files are currently supported. Found ${this.existingFile?.filePath}`); } if (this.existingConfigs.some((r) => !r.resourceInfo)) { From 45187b1c9c87f539ea3eb044e16c4e09ab60ff47 Mon Sep 17 00:00:00 2001 From: kevinwang Date: Sat, 5 Apr 2025 22:58:45 -0400 Subject: [PATCH 2/7] feat: switch to jju for automatic file generation --- codify.json5 | 11 ++- package-lock.json | 13 +++ package.json | 4 +- .../file-modification-calculator.test.ts | 83 ++++++++++++++----- src/utils/file-modification-calculator.ts | 34 ++------ 5 files changed, 91 insertions(+), 54 deletions(-) diff --git a/codify.json5 b/codify.json5 index 7dedbb60..b8442f87 100644 --- a/codify.json5 +++ b/codify.json5 @@ -6,14 +6,17 @@ } }, { - "type": "macports" + type: "macports" }, // This is a test comment { - "type": "pnpm", - "globalEnvNodeVersion": null + type: "pnpm", + globalEnvNodeVersion: null, }, + /* + This is my multi-line comment + */ { - "type": "homebrew" + type: "homebrew" } ] diff --git a/package-lock.json b/package-lock.json index faa658d2..77a31fca 100644 --- a/package-lock.json +++ b/package-lock.json @@ -28,6 +28,7 @@ "ink-big-text": "^2.0.0", "ink-gradient": "^3.0.0", "ink-select-input": "^6.0.0", + "jju": "^1.4.0", "jotai": "^2.11.1", "js-yaml": "^4.1.0", "js-yaml-source-map": "^0.2.2", @@ -49,6 +50,7 @@ "@types/chalk": "^2.2.0", "@types/debug": "^4.1.12", "@types/diff": "^7.0.1", + "@types/jju": "^1.4.5", "@types/js-yaml": "^4.0.9", "@types/json5": "^2.2.0", "@types/mocha": "^10.0.10", @@ -4317,6 +4319,12 @@ "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.4.tgz", "integrity": "sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==" }, + "node_modules/@types/jju": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/@types/jju/-/jju-1.4.5.tgz", + "integrity": "sha512-5Yx4wOq3X+xArOlyZcuAK1Pli4vW0E6nQ6UC8jWdbcY5OlBsoElFTOf4kggYKls/NCHdShAK6UR8NRSQARUNfQ==", + "dev": true + }, "node_modules/@types/js-yaml": { "version": "4.0.9", "resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.9.tgz", @@ -9906,6 +9914,11 @@ "node": ">=8" } }, + "node_modules/jju": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/jju/-/jju-1.4.0.tgz", + "integrity": "sha512-8wb9Yw966OSxApiCt0K3yNJL8pnNeIv+OEq2YMidz4FKP6nonSRoOXc80iXY4JaN2FC11B9qsNmDsm+ZOfMROA==" + }, "node_modules/jotai": { "version": "2.11.1", "resolved": "https://registry.npmjs.org/jotai/-/jotai-2.11.1.tgz", diff --git a/package.json b/package.json index 90dd4175..1ce52f24 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,8 @@ "semver": "^7.5.4", "supports-color": "^9.4.0", "json5": "^2.2.3", - "@mischnic/json-sourcemap": "^0.1.1" + "@mischnic/json-sourcemap": "^0.1.1", + "jju": "^1.4.0" }, "description": "Codify allows users to configure settings, install new packages, and automate their systems using code instead of the GUI. Get set up on a new laptop in one click, maintain a Codify file within your project so anyone can get started and never lose your cool apps or favourite settings again.", "devDependencies": { @@ -48,6 +49,7 @@ "@types/react": "^18.3.1", "@types/semver": "^7.5.4", "@types/strip-ansi": "^5.2.1", + "@types/jju": "^1.4.5", "@typescript-eslint/eslint-plugin": "^8.16.0", "@types/json5": "^2.2.0", "codify-plugin-lib": "^1.0.151", diff --git a/src/utils/file-modification-calculator.test.ts b/src/utils/file-modification-calculator.test.ts index 73aef9f0..0b21e770 100644 --- a/src/utils/file-modification-calculator.test.ts +++ b/src/utils/file-modification-calculator.test.ts @@ -54,7 +54,7 @@ describe('File modification calculator tests', () => { resource: modifiedResource, }], match) - console.log(result) + console.log(result.newFile) console.log(result.diff) }) @@ -230,9 +230,9 @@ describe('File modification calculator tests', () => { ' "default": "latest"\n' + ' }\n' + ' },\n' + - ' {\n' + + ' { \n' + ' "type": "resource1",\n' + - ' "param2": ["a","b","c","d"]\n' + + ' "param2": ["a", "b", "c", "d"]\n' + ' }\n' + ']',) console.log(result) @@ -311,7 +311,7 @@ describe('File modification calculator tests', () => { ' "type": "resource1",\n' + ' "param2": ["a", "b", "c"]\n' + ' },\n' + - ' {\n' + + ' { \n' + ' "type": "resource2",\n' + ' "param1": false,\n' + ' "param3": "this is another string"\n' + @@ -367,12 +367,7 @@ describe('File modification calculator tests', () => { ' },\n' + ' {\n' + ' "type": "resource1",\n' + - ' "param2": [\n' + - ' "a",\n' + - ' "b",\n' + - ' "c",\n' + - ' "d"\n' + - ' ]\n' + + ' "param2": ["a", "b", "c", "d"],\n' + ' }\n' + ']') console.log(result) @@ -426,21 +421,11 @@ describe('File modification calculator tests', () => { ' },\n' + ' {\n' + ' "type": "resource1",\n' + - ' "param2": [\n' + - ' "a",\n' + - ' "b",\n' + - ' "c",\n' + - ' "d"\n' + - ' ]\n' + + ' "param2": ["a", "b", "c", "d"],\n' + ' },\n' + ' {\n' + ' "type": "resource2",\n' + - ' "param2": [\n' + - ' "a",\n' + - ' "b",\n' + - ' "c",\n' + - ' "d"\n' + - ' ]\n' + + ' "param2": ["a", "b", "c", "d"],\n' + ' }\n' + ']',) console.log(result) @@ -517,6 +502,60 @@ describe('File modification calculator tests', () => { ) }) + it('Can handle comments inside a .json5 file', async () => { + const existingFile = + `[ + // I am a comment + /* + I am a multi-line comment + */ + { + "type": "jenv", + "add": [ + "system", + "11", + "17", // Line comment + "17.0.12", // Line comment + "openjdk64-11.0.24", + "openjdk64-17.0.12" + ], + "global": "17" + } +] +` + generateTestFile(existingFile, '/codify.json5'); + + const project = await CodifyParser.parse('/codify.json5') + project.resourceConfigs.forEach((r) => { + r.attachResourceInfo(generateResourceInfo(r.type, [])) + }); + + const modifiedResource = new ResourceConfig({ + "type": "jenv", + "add": [ + "system", + "11", + "11.0", + "11.0.24", + "17", + "17.0.12", + "openjdk64-11.0.24", + "openjdk64-17.0.12" + ], + "global": "17" + }) + modifiedResource.attachResourceInfo(generateResourceInfo('jenv')) + + const calculator = new FileModificationCalculator(project); + const result = await calculator.calculate([{ + modification: ModificationType.INSERT_OR_UPDATE, + resource: modifiedResource, + }], match) + + console.log(result.newFile); + console.log(result.diff) + }) + afterEach(() => { vi.resetAllMocks(); }) diff --git a/src/utils/file-modification-calculator.ts b/src/utils/file-modification-calculator.ts index 4db2f48f..c233dc53 100644 --- a/src/utils/file-modification-calculator.ts +++ b/src/utils/file-modification-calculator.ts @@ -1,6 +1,7 @@ import { ResourceConfig } from '../entities/resource-config.js'; import * as jsonSourceMap from 'json-source-map'; +import jju from 'jju' import { FileType, InMemoryFile } from '../parser/entities.js'; import { SourceMap, SourceMapCache } from '../parser/source-maps.js'; @@ -80,8 +81,7 @@ export class FileModificationCalculator { } // Update an existing resource - newFile = this.remove(newFile, this.sourceMap, sourceIndex, isOnly); - newFile = this.update(newFile, modified.resource, existing, this.sourceMap, sourceIndex, isOnly); + newFile = this.update(newFile, modified.resource, existing, sourceIndex); } // Insert new resources @@ -143,9 +143,11 @@ export class FileModificationCalculator { ): string { let result = file; + const fileStyle = jju.analyze(file); + for (const newResource of resources.reverse()) { const sortedResource = { ...newResource.core(true), ...this.sortKeys(newResource.parameters) } - let content = JSON.stringify(sortedResource, null, 2); + let content = jju.stringify(sortedResource, fileStyle as any); content = content.split(/\n/).map((l) => `${this.indentString}${l}`).join('\n') content = `,\n${content}`; @@ -162,11 +164,6 @@ export class FileModificationCalculator { sourceIndex: number, isOnly: boolean, ): string { - // The element being removed is the only element left, - if (isOnly) { - return '[]'; - } - const isLast = sourceIndex === this.totalConfigLength - 1; const isFirst = sourceIndex === 0; @@ -192,32 +189,15 @@ export class FileModificationCalculator { file: string, resource: ResourceConfig, existing: ResourceConfig, - sourceMap: SourceMap, sourceIndex: number, - isOnly: boolean, ): string { // Updates: for now let's remove and re-add the entire object. Only two formatting availalbe either same line or multi-line const { value, valueEnd } = this.sourceMap.lookup(`/${sourceIndex}`)!; - const isSameLine = value.line === valueEnd.line; const isFirst = sourceIndex === 0; - - // We try to start deleting from the previous element to the next element if possible. This covers any spaces as well. - const start = !isFirst ? this.sourceMap.lookup(`/${sourceIndex - 1}`)?.valueEnd : this.sourceMap.lookup(`/${sourceIndex}`)?.value; - const sortedResource = this.sortKeys(resource.raw, existing.raw); - let content = isSameLine - ? JSON.stringify(sortedResource, null, 1).replaceAll('\n', '').replaceAll(/}$/g, ' }') - : JSON.stringify(sortedResource, null, this.indentString); - content = this.updateParamsToOnelineIfNeeded(content, sourceMap, sourceIndex); - - content = content.split(/\n/).map((l) => `${this.indentString}${l}`).join('\n'); - if (isOnly) { - return `[\n${content}\n]`; - } - content = isFirst ? `\n${content},` : `,\n${content}` - - return this.splice(file, start?.position!, 0, content); + let content = jju.update(file.slice(value.position, valueEnd.position), sortedResource) + return this.splice(file, value?.position!, valueEnd.position - value.position, content); } /** Attempt to make arrays and objects oneliners if they were before. It does this by creating a new source map */ From 23b353496b59763d7bb9e4dbee16007031ec96ea Mon Sep 17 00:00:00 2001 From: kevinwang Date: Sat, 5 Apr 2025 23:38:20 -0400 Subject: [PATCH 3/7] feat: bumped import ui version --- package-lock.json | 8 ++++---- package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 77a31fca..e9c62510 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "0.7.2", "license": "MIT", "dependencies": { - "@codifycli/ink-form": "0.0.11", + "@codifycli/ink-form": "0.0.12", "@homebridge/node-pty-prebuilt-multiarch": "^0.12.0-beta.5", "@inkjs/ui": "^2", "@mischnic/json-sourcemap": "^0.1.1", @@ -1083,9 +1083,9 @@ } }, "node_modules/@codifycli/ink-form": { - "version": "0.0.11", - "resolved": "https://npm.pkg.github.com/download/@codifycli/ink-form/0.0.11/a12900d9ada20b1c14e182f4ca738d06f0d5e3e8", - "integrity": "sha512-fPg5MPMJ+JaVqnynvCf3FTFnwNMnYPRERMXl4Y7GxlTb5gcyqT4kWQU27Y4Le4VILQIooveczJEL9t3CfjekSQ==", + "version": "0.0.12", + "resolved": "https://npm.pkg.github.com/download/@codifycli/ink-form/0.0.12/14ab51d2c8de7fb740d167e4dbd384c03d4e3386", + "integrity": "sha512-YT3ww8NmScRLstqROJfL4aBvHSdp2ySGzI78Mi6V8wZUO9akH6JsnYJQQaP5a3XnK2iGkzgzjtGl4jeejAKbOg==", "dependencies": { "ink-select-input": "^6.0.0", "ink-text-input": "^6.0.0" diff --git a/package.json b/package.json index 1ce52f24..ae9b3a31 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "codify": "./bin/run.js" }, "dependencies": { - "@codifycli/ink-form": "0.0.11", + "@codifycli/ink-form": "0.0.12", "@homebridge/node-pty-prebuilt-multiarch": "^0.12.0-beta.5", "@inkjs/ui": "^2", "@oclif/core": "^4.0.8", From 2264008b48b5c00439d067d1cb70ffc68f4f7623 Mon Sep 17 00:00:00 2001 From: kevinwang Date: Sun, 6 Apr 2025 12:43:34 -0400 Subject: [PATCH 4/7] feat: add jsonc support --- codify.json5 | 22 ------------ src/orchestrators/import.ts | 4 +-- src/parser/entities.ts | 3 +- src/parser/index.ts | 6 ++-- src/parser/jsonc/json-parser.ts | 41 +++++++++++++++++++++++ src/parser/reader.ts | 4 +++ src/parser/source-maps.ts | 9 ++--- src/utils/file-modification-calculator.ts | 31 ++++++++++++++--- 8 files changed, 82 insertions(+), 38 deletions(-) delete mode 100644 codify.json5 create mode 100644 src/parser/jsonc/json-parser.ts diff --git a/codify.json5 b/codify.json5 deleted file mode 100644 index b8442f87..00000000 --- a/codify.json5 +++ /dev/null @@ -1,22 +0,0 @@ -[ - { - "type": "project", - "plugins": { - "default": "../codify-homebrew-plugin/src/index.ts" - } - }, - { - type: "macports" - }, - // This is a test comment - { - type: "pnpm", - globalEnvNodeVersion: null, - }, - /* - This is my multi-line comment - */ - { - type: "homebrew" - } -] diff --git a/src/orchestrators/import.ts b/src/orchestrators/import.ts index 21eff605..981d285c 100644 --- a/src/orchestrators/import.ts +++ b/src/orchestrators/import.ts @@ -347,7 +347,7 @@ ${JSON.stringify(unsupportedTypeIds)}`); const folderPath = path.join(cwd, 'codify-imports') await FileUtils.createFolder(folderPath) - let fileName = path.join(folderPath, 'import.codify.json') + let fileName = path.join(folderPath, 'import.codify.jsonc') let counter = 1; while(true) { @@ -355,7 +355,7 @@ ${JSON.stringify(unsupportedTypeIds)}`); return fileName; } - fileName = path.join(folderPath, `import-${counter}.codify.json`); + fileName = path.join(folderPath, `import-${counter}.codify.jsonc`); counter++; } } diff --git a/src/parser/entities.ts b/src/parser/entities.ts index 327f0c62..b52ad02d 100644 --- a/src/parser/entities.ts +++ b/src/parser/entities.ts @@ -19,5 +19,6 @@ export interface LanguageSpecificParser { export enum FileType { JSON = 'json', YAML = 'yaml', - JSON5 = 'json5' + JSON5 = 'json5', + JSONC = 'jsonc', } diff --git a/src/parser/index.ts b/src/parser/index.ts index bd158c9d..6b430998 100644 --- a/src/parser/index.ts +++ b/src/parser/index.ts @@ -8,17 +8,19 @@ import { ConfigFactory } from './config-factory.js'; import { FileType, InMemoryFile, ParsedConfig } from './entities.js'; import { JsonParser } from './json/json-parser.js'; import { Json5Parser } from './json5/json-parser.js'; +import { JsoncParser } from './jsonc/json-parser.js'; import { FileReader } from './reader.js'; import { SourceMapCache } from './source-maps.js'; import { YamlParser } from './yaml/yaml-parser.js'; -export const CODIFY_FILE_REGEX = /^(.*)?codify(.json|.yaml|.json5)$/; +export const CODIFY_FILE_REGEX = /^(.*)?codify(.json|.yaml|.json5|.jsonc)$/; class Parser { private readonly languageSpecificParsers= { [FileType.JSON]: new JsonParser(), [FileType.YAML]: new YamlParser(), - [FileType.JSON5]: new Json5Parser() + [FileType.JSON5]: new Json5Parser(), + [FileType.JSONC]: new JsoncParser() } async parse(dirOrFile: string): Promise { diff --git a/src/parser/jsonc/json-parser.ts b/src/parser/jsonc/json-parser.ts new file mode 100644 index 00000000..ce9135f6 --- /dev/null +++ b/src/parser/jsonc/json-parser.ts @@ -0,0 +1,41 @@ +import JsonSourceMap from '@mischnic/json-sourcemap'; +import { Config, ConfigFileSchema } from 'codify-schemas'; +import jju from 'jju' + +import { AjvValidationError, SyntaxError } from '../../common/errors.js'; +import { ajv } from '../../utils/ajv.js'; +import { InMemoryFile, LanguageSpecificParser, ParsedConfig } from '../entities.js'; +import { SourceMapCache } from '../source-maps.js'; + +const validator = ajv.compile(ConfigFileSchema); + +export class JsoncParser implements LanguageSpecificParser { + parse(file: InMemoryFile, sourceMaps?: SourceMapCache): ParsedConfig[] { + let content; + try { + content = jju.parse(file.contents); + + if (sourceMaps) { + sourceMaps.addSourceMap(file, JsonSourceMap.parse(file.contents, undefined, { dialect: 'JSON5' })); + } + } catch (error) { + throw new SyntaxError({ + fileName: file.filePath, + message: (error as Error).message, + }); + } + + if (!this.validate(content)) { + throw new AjvValidationError('invalid config file', validator.errors!, file.filePath, sourceMaps); + } + + return content.map((contents, idx) => ({ + contents, + sourceMapKey: SourceMapCache.constructKey(file.filePath, `/${idx}`) + })) + } + + private validate(content: unknown): content is Config[] { + return validator(content); + } +} diff --git a/src/parser/reader.ts b/src/parser/reader.ts index 101ead99..ddfe048c 100644 --- a/src/parser/reader.ts +++ b/src/parser/reader.ts @@ -26,6 +26,10 @@ export class FileReader { if (filePath.endsWith('.json5')) { return FileType.JSON5 } + + if (filePath.endsWith('.jsonc')) { + return FileType.JSONC + } throw new InternalError(`Unsupported file type passed to FileReader. File path: ${filePath}`); } diff --git a/src/parser/source-maps.ts b/src/parser/source-maps.ts index f5ab7296..81d3728d 100644 --- a/src/parser/source-maps.ts +++ b/src/parser/source-maps.ts @@ -44,17 +44,12 @@ export class SourceMapCache { } addSourceMap(file: InMemoryFile, sourceMap: JsonSourceMap | YamlSourceMap) { - if (file.fileType === FileType.JSON) { - this.sourceMaps.set(file.filePath, { - file, - sourceMap: new JsonSourceMapAdapter(sourceMap as JsonSourceMap), - }) - } else if (file.fileType === FileType.YAML) { + if (file.fileType === FileType.YAML) { this.sourceMaps.set(file.filePath, { file, sourceMap: new YamlSourceMapAdapter(sourceMap as YamlSourceMap, file), }) - } else if (file.fileType === FileType.JSON5) { + } else { this.sourceMaps.set(file.filePath, { file, sourceMap: new JsonSourceMapAdapter(sourceMap as JsonSourceMap), diff --git a/src/utils/file-modification-calculator.ts b/src/utils/file-modification-calculator.ts index c233dc53..6972e660 100644 --- a/src/utils/file-modification-calculator.ts +++ b/src/utils/file-modification-calculator.ts @@ -90,7 +90,7 @@ export class FileModificationCalculator { .map((r) => r.resource) const insertionIndex = newFile.length - 2; // Last element is guarenteed to be the closing bracket. We insert 1 before that - newFile = this.insert(newFile, newResourcesToInsert, insertionIndex); + newFile = this.insert(newFile, this.existingFile.fileType, newResourcesToInsert, insertionIndex); const lastCharacterIndex = this.existingFile.contents.lastIndexOf(']') if (lastCharacterIndex < this.existingFile.contents.length - 1) { @@ -110,8 +110,8 @@ export class FileModificationCalculator { return; } - if (this.existingFile?.fileType !== FileType.JSON && this.existingFile?.fileType !== FileType.JSON5) { - throw new Error(`Only updating .json and .json5 files are currently supported. Found ${this.existingFile?.filePath}`); + if (this.existingFile?.fileType !== FileType.JSON && this.existingFile?.fileType !== FileType.JSON5 && this.existingFile?.fileType !== FileType.JSONC) { + throw new Error(`Only updating .json, .json5, and .jsonc files are currently supported. Found ${this.existingFile?.filePath}`); } if (this.existingConfigs.some((r) => !r.resourceInfo)) { @@ -138,6 +138,7 @@ export class FileModificationCalculator { // Insert always works at the end private insert( file: string, + fileType: FileType, resources: ResourceConfig[], position: number, ): string { @@ -147,7 +148,13 @@ export class FileModificationCalculator { for (const newResource of resources.reverse()) { const sortedResource = { ...newResource.core(true), ...this.sortKeys(newResource.parameters) } - let content = jju.stringify(sortedResource, fileStyle as any); + let content = jju.stringify(sortedResource, { + indent: fileStyle.indent, + no_trailing_comma: true, + quote: '"', + quote_keys: fileStyle.quote_keys, + mode: this.fileTypeString(fileType), + }); content = content.split(/\n/).map((l) => `${this.indentString}${l}`).join('\n') content = `,\n${content}`; @@ -254,4 +261,20 @@ export class FileModificationCalculator { }) ) } + + private fileTypeString(fileType: FileType): 'json' | 'json5' | 'cjson' { + if (fileType === FileType.JSON) { + return 'json' + } + + if (fileType === FileType.JSON5) { + return 'json5' + } + + if (fileType === FileType.JSONC) { + return 'cjson' + } + + throw new Error(`Unsupported file type ${fileType} when trying to generate new configs`); + } } From cac456bc770faafe60b51468f50224ddf2b056f9 Mon Sep 17 00:00:00 2001 From: kevinwang Date: Sun, 6 Apr 2025 20:03:12 -0400 Subject: [PATCH 5/7] fix: fixed tests --- src/parser/{source-maps.test.ts => yaml/source-map.test.ts} | 4 ++-- src/utils/file-modification-calculator.test.ts | 6 +++--- test/orchestrator/import/import.test.ts | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) rename src/parser/{source-maps.test.ts => yaml/source-map.test.ts} (98%) diff --git a/src/parser/source-maps.test.ts b/src/parser/yaml/source-map.test.ts similarity index 98% rename from src/parser/source-maps.test.ts rename to src/parser/yaml/source-map.test.ts index f60649ef..e58f44a9 100644 --- a/src/parser/source-maps.test.ts +++ b/src/parser/yaml/source-map.test.ts @@ -1,9 +1,9 @@ import { describe, it, expect } from 'vitest'; -import { FileType, InMemoryFile } from './entities.js'; -import { YamlSourceMapAdapter, YamlSourceMapBTree } from './source-maps.js'; +import { FileType, InMemoryFile } from '../entities.js'; import SourceMap from 'js-yaml-source-map'; import * as yaml from 'js-yaml'; import { exec } from 'child_process'; +import { YamlSourceMapAdapter, YamlSourceMapBTree } from './source-map.js'; describe('Yaml source map tests', () => { it('Can generate the correct yaml endPointers', () => { diff --git a/src/utils/file-modification-calculator.test.ts b/src/utils/file-modification-calculator.test.ts index 0b21e770..c06938b3 100644 --- a/src/utils/file-modification-calculator.test.ts +++ b/src/utils/file-modification-calculator.test.ts @@ -367,7 +367,7 @@ describe('File modification calculator tests', () => { ' },\n' + ' {\n' + ' "type": "resource1",\n' + - ' "param2": ["a", "b", "c", "d"],\n' + + ' "param2": ["a", "b", "c", "d"]\n' + ' }\n' + ']') console.log(result) @@ -421,11 +421,11 @@ describe('File modification calculator tests', () => { ' },\n' + ' {\n' + ' "type": "resource1",\n' + - ' "param2": ["a", "b", "c", "d"],\n' + + ' "param2": ["a", "b", "c", "d"]\n' + ' },\n' + ' {\n' + ' "type": "resource2",\n' + - ' "param2": ["a", "b", "c", "d"],\n' + + ' "param2": ["a", "b", "c", "d"]\n' + ' }\n' + ']',) console.log(result) diff --git a/test/orchestrator/import/import.test.ts b/test/orchestrator/import/import.test.ts index 197a7055..e7b063de 100644 --- a/test/orchestrator/import/import.test.ts +++ b/test/orchestrator/import/import.test.ts @@ -184,7 +184,7 @@ describe('Import orchestrator tests', () => { expect(displayFileModifications).toHaveBeenCalledOnce(); expect(promptConfirmationSpy).toHaveBeenCalledOnce(); - const fileWritten = fs.readFileSync('/codify-imports/import.codify.json', 'utf8') as string; + const fileWritten = fs.readFileSync('/codify-imports/import.codify.jsonc', 'utf8') as string; console.log(fileWritten); expect(JSON.parse(fileWritten)).toMatchObject([ From 5433a0e93c8f40084ca201bd10c352a497732db5 Mon Sep 17 00:00:00 2001 From: kevinwang Date: Sun, 6 Apr 2025 20:44:15 -0400 Subject: [PATCH 6/7] chore: renamed references to codify.json to codify.jsonc --- README.md | 6 +++--- src/commands/apply.ts | 2 +- src/commands/destroy.ts | 6 +++--- src/commands/import.ts | 6 +++--- src/commands/init.ts | 2 +- src/commands/plan.ts | 4 ++-- src/common/initialize-plugins.ts | 6 +++--- src/orchestrators/init.ts | 4 ++-- src/parser/index.ts | 2 +- src/ui/components/default-component.test.tsx | 2 +- src/ui/components/default-component.tsx | 2 +- src/utils/file-modification-calculator.test.ts | 2 +- 12 files changed, 22 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index 045af466..92a12234 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,7 @@ USAGE ## `codify apply` -Install or update resources on the system based on a codify.json file. +Install or update resources on the system based on a codify.jsonc file. ``` USAGE @@ -51,7 +51,7 @@ FLAGS --debug Print additional debug logs. DESCRIPTION - Install or update resources on the system based on a codify.json file. + Install or update resources on the system based on a codify.jsonc file. Codify first generates a plan to determine the necessary execution steps. See codify plan --help for more details. @@ -95,7 +95,7 @@ DESCRIPTION Use Codify to uninstall a supported package or setting on the system. This command will only work for resources with Codify support. This command - can work with or without a codify.json file. + can work with or without a codify.jsonc file. Modes: • If a codify.json file exists, destroy the resource specified in the Codify.json file diff --git a/src/commands/apply.ts b/src/commands/apply.ts index 9e9b73b0..6b9d9348 100644 --- a/src/commands/apply.ts +++ b/src/commands/apply.ts @@ -6,7 +6,7 @@ import { ApplyOrchestrator } from '../orchestrators/apply.js'; export default class Apply extends BaseCommand { static description = -`Install or update resources on the system based on a codify.json file. +`Install or update resources on the system based on a codify.jsonc file. Codify first generates a plan to determine the necessary execution steps. See ${chalk.bold.bgMagenta(' codify plan --help ')} for more details. diff --git a/src/commands/destroy.ts b/src/commands/destroy.ts index 29c02c92..15fa52c8 100644 --- a/src/commands/destroy.ts +++ b/src/commands/destroy.ts @@ -10,12 +10,12 @@ export default class Destroy extends BaseCommand { `Use Codify to uninstall a supported package or setting on the system. This command will only work for resources with Codify support. This command -can work with or without a codify.json file. +can work with or without a codify.jsonc file. ${chalk.bold('Modes:')} - • If a codify.json file exists, destroy the resource specified in the Codify.json file + • If a codify.jsonc file exists, destroy the resource specified in the Codify.jsonc file with a matching type. - • If a codify.json file doesn't exist, additional information may be asked to identify + • If a codify.jsonc file doesn't exist, additional information may be asked to identify the specific resource to destroy. For more information, visit: https://docs.codifycli.com/commands/destory` diff --git a/src/commands/import.ts b/src/commands/import.ts index 33578c85..833ae648 100644 --- a/src/commands/import.ts +++ b/src/commands/import.ts @@ -12,10 +12,10 @@ export default class Import extends BaseCommand { `Generate Codify configurations from already installed packages. Use a space-separated list of arguments to specify the resource types to import. -If a codify.json file already exists, omit arguments to update the file to match the system. +If a codify.jsonc file already exists, omit arguments to update the file to match the system. ${chalk.bold('Modes:')} -1. ${chalk.bold('No args:')} If no args are specified and an *.codify.json already exists, Codify +1. ${chalk.bold('No args:')} If no args are specified and an *.codify.jsonc already exists, Codify will update the existing file with new changes on the system. ${chalk.underline('Command:')} @@ -30,7 +30,7 @@ codify import nvm asdf* codify import \\* (for importing all supported resources) The results can be saved in one of three ways: - a. To an existing *.codify.json file + a. To an existing *.codify.jsonc file b. To a new file c. Printed to the console only diff --git a/src/commands/init.ts b/src/commands/init.ts index 2d95fd7a..03fcb560 100644 --- a/src/commands/init.ts +++ b/src/commands/init.ts @@ -11,7 +11,7 @@ export default class Init extends BaseCommand { Use this command to automatically generate Codify configs based on the currently installed system resources. By default, the new file -will be written to ${chalk.bold.bgMagenta(' ~/codify.json ')}. +will be written to ${chalk.bold.bgMagenta(' ~/codify.jsonc ')}. For more information, visit: https://docs.codifycli.com/commands/init` diff --git a/src/commands/plan.ts b/src/commands/plan.ts index f985d270..3b470c7e 100644 --- a/src/commands/plan.ts +++ b/src/commands/plan.ts @@ -4,9 +4,9 @@ import { PlanOrchestrator } from '../orchestrators/plan.js'; export default class Plan extends BaseCommand { static description = -`Generate an execution plan to apply changes from a codify.json file. +`Generate an execution plan to apply changes from a codify.jsonc file. -This plan lists all the changes Codify needs to make to apply the codify.json file. +This plan lists all the changes Codify needs to make to apply the codify.jsonc file. The plan will not be executed. Behind the scenes, Codify performs a refresh scan to determine the current configuration and installed resources, then compares them with the desired configuration to compute the execution plan. diff --git a/src/common/initialize-plugins.ts b/src/common/initialize-plugins.ts index 4c2d6a1f..5696310d 100644 --- a/src/common/initialize-plugins.ts +++ b/src/common/initialize-plugins.ts @@ -58,11 +58,11 @@ export class PluginInitOrchestrator { if (!pathToParse && !allowEmptyProject) { ctx.subprocessFinished(SubProcessName.PARSE); ctx.subprocessStarted(SubProcessName.CREATE_ROOT_FILE) - const createRootCodifyFile = await reporter.promptConfirmation('\nNo codify file found. Do you want to create a root file at ~/codify.json?'); + const createRootCodifyFile = await reporter.promptConfirmation('\nNo codify file found. Do you want to create a root file at ~/codify.jsonc?'); if (createRootCodifyFile) { await fs.writeFile( - path.resolve(os.homedir(), 'codify.json'), + path.resolve(os.homedir(), 'codify.jsonc'), '[]', { encoding: 'utf8', flag: 'wx' } ); // flag: 'wx' prevents overwrites if the file exists @@ -70,7 +70,7 @@ export class PluginInitOrchestrator { ctx.subprocessFinished(SubProcessName.CREATE_ROOT_FILE) - console.log('Created ~/codify.json file') + console.log('Created ~/codify.jsonc file') process.exit(0); } diff --git a/src/orchestrators/init.ts b/src/orchestrators/init.ts index 2df9bb73..0e9eb8c6 100644 --- a/src/orchestrators/init.ts +++ b/src/orchestrators/init.ts @@ -75,10 +75,10 @@ Enjoy! while (!isValidSaveLocation) { input = (await reporter.promptInput( - `Where to save the new Codify configs? ${chalk.grey.dim('(leave blank for ~/codify.json)')}`, + `Where to save the new Codify configs? ${chalk.grey.dim('(leave blank for ~/codify.jsonc)')}`, error ? `Invalid location: ${input} already exists` : undefined) ) - input = input ? input : '~/codify.json'; + input = input ? input : '~/codify.jsonc'; locationToSave = path.resolve(untildify(resolvePathWithVariables(input))); diff --git a/src/parser/index.ts b/src/parser/index.ts index 6b430998..8f9bbdd3 100644 --- a/src/parser/index.ts +++ b/src/parser/index.ts @@ -42,7 +42,7 @@ class Parser { if (!isDirectory) { const fileName = path.basename(dirOrFile); if (!CODIFY_FILE_REGEX.test(fileName)) { - throw new Error(`Invalid file path provided ${dirOrFile} ${fileName}. Expected the file to be *codify.json or *codify.yaml `) + throw new Error(`Invalid file path provided ${dirOrFile} ${fileName}. Expected the file to be *codify.jsonc, *codify.json5, *codify.json, or *codify.yaml `) } return [dirOrFile]; diff --git a/src/ui/components/default-component.test.tsx b/src/ui/components/default-component.test.tsx index fd12dbd8..37092214 100644 --- a/src/ui/components/default-component.test.tsx +++ b/src/ui/components/default-component.test.tsx @@ -37,7 +37,7 @@ describe('DefaultComponent', () => { it('Renders the init completed message', () => { const reporter = new DefaultReporter(); - const locationToSave = '~/codify.json' + const locationToSave = '~/codify.jsonc' reporter.displayMessage(` 🎉🎉 Codify successfully initialized. 🎉🎉 diff --git a/src/ui/components/default-component.tsx b/src/ui/components/default-component.tsx index 18d09104..0b6354bb 100644 --- a/src/ui/components/default-component.tsx +++ b/src/ui/components/default-component.tsx @@ -158,7 +158,7 @@ export function DefaultComponent(props: { {(renderData as any).prompt} { (renderData as any).error && ({(renderData as any).error}) } - emitter.emit(RenderEvent.PROMPT_RESULT, result)} placeholder='~/codify.json' /> + emitter.emit(RenderEvent.PROMPT_RESULT, result)} placeholder='~/codify.jsonc' /> ) } diff --git a/src/utils/file-modification-calculator.test.ts b/src/utils/file-modification-calculator.test.ts index c06938b3..ad8aab53 100644 --- a/src/utils/file-modification-calculator.test.ts +++ b/src/utils/file-modification-calculator.test.ts @@ -19,7 +19,7 @@ vi.mock('node:fs/promises', async () => { return fs.promises; }) -const defaultPath = '/codify.json' +const defaultPath = '/codify.jsonc' describe('File modification calculator tests', () => { From 863a4e5125503643a190d144847f0112d0b1e9cf Mon Sep 17 00:00:00 2001 From: kevinwang Date: Sun, 6 Apr 2025 21:14:50 -0400 Subject: [PATCH 7/7] fix: error message to have the correct file names --- src/parser/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/parser/index.ts b/src/parser/index.ts index 8f9bbdd3..4789fc32 100644 --- a/src/parser/index.ts +++ b/src/parser/index.ts @@ -42,7 +42,7 @@ class Parser { if (!isDirectory) { const fileName = path.basename(dirOrFile); if (!CODIFY_FILE_REGEX.test(fileName)) { - throw new Error(`Invalid file path provided ${dirOrFile} ${fileName}. Expected the file to be *codify.jsonc, *codify.json5, *codify.json, or *codify.yaml `) + throw new Error(`Invalid file path provided ${dirOrFile} ${fileName}. Expected the file to be *.codify.jsonc, *.codify.json5, *.codify.json, or *.codify.yaml `) } return [dirOrFile];