Skip to content

Commit 834e39f

Browse files
committed
feat: WIP added ability to delete a resource from an existing file
1 parent f1cbda2 commit 834e39f

File tree

4 files changed

+205
-32
lines changed

4 files changed

+205
-32
lines changed

package-lock.json

Lines changed: 4 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,12 @@
1010
"@oclif/core": "^4.0.8",
1111
"@oclif/plugin-help": "^6.2.4",
1212
"@oclif/plugin-update": "^4.6.13",
13-
"@types/diff": "^7.0.1",
1413
"ajv": "^8.12.0",
1514
"ajv-formats": "^3.0.1",
1615
"chalk": "^5.3.0",
1716
"codify-schemas": "^1.0.63",
1817
"debug": "^4.3.4",
18+
"detect-indent": "^7.0.1",
1919
"diff": "^7.0.0",
2020
"ink": "^5",
2121
"jotai": "^2.11.1",
@@ -34,6 +34,7 @@
3434
"@oclif/prettier-config": "^0.2.1",
3535
"@types/chalk": "^2.2.0",
3636
"@types/debug": "^4.1.12",
37+
"@types/diff": "^7.0.1",
3738
"@types/js-yaml": "^4.0.9",
3839
"@types/mocha": "^10.0.10",
3940
"@types/node": "^20",
Lines changed: 119 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
1-
import { describe, it, vi, afterEach } from 'vitest';
2-
import { FileModificationCalculator, ModificationType } from './file-modification-calculator';
3-
import { ResourceConfig } from '../entities/resource-config';
4-
import { ResourceInfo } from '../entities/resource-info';
5-
import { FileType, InMemoryFile } from '../parser/entities';
1+
import * as fs from 'node:fs';
2+
import * as os from 'node:os';
3+
import * as path from 'node:path';
4+
5+
import { describe, it, vi, afterEach, expect } from 'vitest';
6+
import { FileModificationCalculator, ModificationType } from './file-modification-calculator.js';
7+
import { ResourceConfig } from '../entities/resource-config.js';
8+
import { ResourceInfo } from '../entities/resource-info.js';
9+
import { CodifyParser } from '../parser/index.js';
610

711
vi.mock('node:fs', async () => {
812
const { fs } = await import('memfs');
@@ -14,39 +18,133 @@ vi.mock('node:fs/promises', async () => {
1418
return fs.promises;
1519
})
1620

21+
const defaultPath = '/codify.json'
22+
1723

1824
describe('File modification calculator tests', () => {
1925

2026
it('Can generate a diff and a new file', async () => {
21-
const existingResource = new ResourceConfig({
22-
type: 'resource1'
27+
const existingFile =
28+
`[
29+
{
30+
"type": "project",
31+
"plugins": {
32+
"default": "latest"
33+
}
34+
},
35+
{ "type": "resource1", "param2": ["a", "b", "c"]}
36+
]`
37+
generateTestFile(existingFile);
38+
39+
const project = await CodifyParser.parse(defaultPath)
40+
project.resourceConfigs.forEach((r) => {
41+
r.attachResourceInfo(generateResourceInfo(r.type))
2342
});
24-
existingResource.attachResourceInfo(generateResourceInfo('resource1'))
2543

26-
const existingFileContents =
44+
const modifiedResource = new ResourceConfig({
45+
type: 'resource1',
46+
parameter1: 'abc'
47+
})
48+
modifiedResource.attachResourceInfo(generateResourceInfo('resource1'))
49+
50+
const calculator = new FileModificationCalculator(project.resourceConfigs, project.sourceMaps.getSourceMap(defaultPath).file, project.sourceMaps);
51+
const result = await calculator.calculate([{
52+
modification: ModificationType.INSERT_OR_UPDATE,
53+
resource: modifiedResource,
54+
}])
55+
56+
console.log(result)
57+
console.log(result.diff)
58+
})
59+
60+
it('Can delete a resource from an existing config (with proper commas)', async () => {
61+
const existingFile =
2762
`[
2863
{
2964
"type": "project",
3065
"plugins": {
31-
"default": "latest",
66+
"default": "latest"
3267
}
3368
},
34-
{ "type": "resource1" }
69+
{
70+
"type": "resource1",
71+
"param2": ["a", "b", "c"]
72+
}
3573
]`
36-
const existingFile = <InMemoryFile> { filePath: '/path/to/file.json', fileType: FileType.JSON, contents: existingFileContents };
74+
generateTestFile(existingFile);
75+
76+
const project = await CodifyParser.parse(defaultPath)
77+
project.resourceConfigs.forEach((r) => {
78+
r.attachResourceInfo(generateResourceInfo(r.type))
79+
});
3780

3881
const modifiedResource = new ResourceConfig({
3982
type: 'resource1',
4083
parameter1: 'abc'
4184
})
4285
modifiedResource.attachResourceInfo(generateResourceInfo('resource1'))
4386

44-
const calculator = new FileModificationCalculator([existingResource], existingFile)
87+
const calculator = new FileModificationCalculator(project.resourceConfigs, project.sourceMaps.getSourceMap(defaultPath).file, project.sourceMaps);
4588
const result = await calculator.calculate([{
46-
modification: ModificationType.INSERT_OR_UPDATE,
89+
modification: ModificationType.DELETE,
4790
resource: modifiedResource,
4891
}])
4992

93+
expect(result.newFile).to.eq('[\n' +
94+
' {\n' +
95+
' "type": "project",\n' +
96+
' "plugins": {\n' +
97+
' "default": "latest"\n' +
98+
' }\n' +
99+
' }\n' +
100+
' \n' +
101+
']')
102+
console.log(result)
103+
console.log(result.diff)
104+
})
105+
106+
it('Can delete a resource from an existing config 2 (with proper commas)', async () => {
107+
const existingFile =
108+
`[
109+
{
110+
"type": "resource1",
111+
"param2": ["a", "b", "c"]
112+
},
113+
{
114+
"type": "project",
115+
"plugins": {
116+
"default": "latest"
117+
}
118+
}
119+
]`
120+
generateTestFile(existingFile);
121+
122+
const project = await CodifyParser.parse(defaultPath)
123+
project.resourceConfigs.forEach((r) => {
124+
r.attachResourceInfo(generateResourceInfo(r.type))
125+
});
126+
127+
const modifiedResource = new ResourceConfig({
128+
type: 'resource1',
129+
parameter1: 'abc'
130+
})
131+
modifiedResource.attachResourceInfo(generateResourceInfo('resource1'))
132+
133+
const calculator = new FileModificationCalculator(project.resourceConfigs, project.sourceMaps.getSourceMap(defaultPath).file, project.sourceMaps);
134+
const result = await calculator.calculate([{
135+
modification: ModificationType.DELETE,
136+
resource: modifiedResource,
137+
}])
138+
139+
// expect(result.newFile).to.eq('[\n' +
140+
// ' {\n' +
141+
// ' "type": "project",\n' +
142+
// ' "plugins": {\n' +
143+
// ' "default": "latest"\n' +
144+
// ' }\n' +
145+
// ' }\n' +
146+
// ' \n' +
147+
// ']')
50148
console.log(result)
51149
console.log(result.diff)
52150
})
@@ -63,3 +161,10 @@ function generateResourceInfo(type: string, requiredParameters?: string[]): Reso
63161
import: { requiredParameters }
64162
})
65163
}
164+
165+
/**
166+
* To generate the source maps and parsed resources it's easier to write it to the file-system and parse it for real
167+
*/
168+
function generateTestFile(contents: string, filePath = defaultPath): void {
169+
fs.writeFileSync(filePath, contents, { encoding: 'utf8' });
170+
}

src/utils/file-modification-calculator.ts

Lines changed: 80 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import chalk from 'chalk';
22
import { ResourceConfig } from '../entities/resource-config.js';
33
import * as Diff from 'diff'
44
import { FileType, InMemoryFile } from '../parser/entities.js';
5+
import { SourceLocation, SourceMapCache } from '../parser/source-maps.js';
6+
import detectIndent from 'detect-indent';
57

68
export enum ModificationType {
79
INSERT_OR_UPDATE,
@@ -21,10 +23,12 @@ export interface FileModificationResult {
2123
export class FileModificationCalculator {
2224
private existingFile?: InMemoryFile;
2325
private existingResources: ResourceConfig[];
26+
private sourceMaps: SourceMapCache;
2427

25-
constructor(existingResources: ResourceConfig[], existingFile: InMemoryFile) {
28+
constructor(existingResources: ResourceConfig[], existingFile: InMemoryFile, sourceMaps: SourceMapCache) {
2629
this.existingFile = existingFile;
2730
this.existingResources = existingResources;
31+
this.sourceMaps = sourceMaps;
2832
}
2933

3034
async calculate(modifications: ModifiedResource[]): Promise<FileModificationResult> {
@@ -44,41 +48,49 @@ export class FileModificationCalculator {
4448
}
4549

4650
this.validate(modifications);
51+
const { sourceMap, file } = this.sourceMaps.getSourceMap('/codify.json')!;
52+
const fileIndents = detectIndent(file.contents);
53+
const indentString = fileIndents.indent;
4754

48-
for (const modified of modifications) {
55+
let newFile = file.contents.trimEnd();
56+
57+
console.log(JSON.stringify(sourceMap, null, 2))
58+
59+
// Reverse the traversal order so we edit from the back. This way the line numbers won't be messed up with new edits.
60+
for (const modified of modifications.reverse()) {
4961
const duplicateIndex = this.existingResources.findIndex((existing) => existing.isSameOnSystem(modified.resource))
5062

5163
if (duplicateIndex === -1) {
5264
if (modified.modification === ModificationType.INSERT_OR_UPDATE) {
53-
resultResources.push(modified.resource);
65+
const config = JSON.stringify(modified.resource.raw, null, indentString)
66+
newFile = this.insertConfig(newFile, config, indentString);
5467
}
5568

5669
continue;
5770
}
5871

72+
const duplicate = this.existingResources[duplicateIndex];
73+
const duplicateSourceKey = duplicate.sourceMapKey?.split('#').at(1)!;
74+
5975
if (modified.modification === ModificationType.DELETE) {
60-
resultResources.splice(duplicateIndex, 1);
76+
const { value, valueEnd } = sourceMap.lookup(duplicateSourceKey)!
77+
78+
newFile = this.remove(newFile, value, valueEnd);
6179
continue;
62-
}
6380

64-
const duplicate = resultResources[duplicateIndex];
65-
for (const [key, newValue] of Object.entries(modified.resource.parameters)) {
66-
duplicate.setParameter(key, newValue);
6781
}
68-
}
6982

70-
const newFile = JSON.stringify(
71-
resultResources.map((r) => r.raw),
72-
null, 2
73-
);
83+
resultResources.splice(duplicateIndex, 1, modified.resource);
84+
}
7485

7586
return {
76-
newFile,
87+
newFile: newFile,
7788
diff: this.diff(this.existingFile.contents, newFile),
7889
}
7990
}
8091

8192
validate(modifiedResources: ModifiedResource[]): void {
93+
// The result of the validation rules only apply if we want to insert into a file. If it's a new file nothing is really needed
8294
if (!this.existingFile) {
8395
return;
8496
}
@@ -102,6 +114,10 @@ export class FileModificationCalculator {
102114

103115
throw new Error(`All resources must have resource info attached to generate diff. Found bad resources: ${badResources}`);
104116
}
117+
118+
if (!this.sourceMaps) {
119+
throw new Error('Source maps must be provided to generate new code');
120+
}
105121
}
106122

107123
diff(a: string, b: string): string {
@@ -116,4 +132,54 @@ export class FileModificationCalculator {
116132

117133
return result;
118134
}
135+
136+
// Insert always works at the end
137+
private insertConfig(
138+
file: string,
139+
config: string,
140+
indentString: string,
141+
) {
142+
const configWithIndents = config.split(/\n/).map((l) => `${indentString}l`).join('\n');
143+
const result = file.substring(0, configWithIndents.length - 1) + ',' + configWithIndents + file.at(-1);
144+
145+
// Need to fix the position of the comma
146+
147+
return result;
148+
}
149+
150+
private remove(
151+
file: string,
152+
value: SourceLocation,
153+
valueEnd: SourceLocation,
154+
): string {
155+
let result = file.substring(0, value.position) + file.substring(valueEnd.position)
156+
157+
let commaIndex = - 1;
158+
for (let counter = value.position; counter > 0; counter--) {
159+
if (result[counter] === ',') {
160+
commaIndex = counter;
161+
break;
162+
}
163+
}
164+
165+
// Not able to find comma behind (this was the first element). We want to delete the comma behind then.
166+
if (commaIndex === -1) {
167+
for (let counter = value.position; counter < file.length - 1; counter++) {
168+
if (result[counter] === ',') {
169+
commaIndex = counter;
170+
break;
171+
}
172+
}
173+
}
174+
175+
if (commaIndex !== -1) {
176+
result = this.splice(result, commaIndex, 1)
177+
}
178+
179+
return result;
180+
}
181+
182+
private splice(s: string, start: number, deleteCount = 0, insert = '') {
183+
return s.substring(0, start) + insert + s.substring(start + deleteCount);
184+
}
119185
}

0 commit comments

Comments
 (0)