Skip to content

Commit a0c319a

Browse files
committed
feat: WIP added updates (supports both single line and multi-line configs)
1 parent c7a4712 commit a0c319a

File tree

2 files changed

+213
-31
lines changed

2 files changed

+213
-31
lines changed

src/utils/file-modification-calculator.test.ts

Lines changed: 142 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ describe('File modification calculator tests', () => {
4747
})
4848
modifiedResource.attachResourceInfo(generateResourceInfo('resource1'))
4949

50-
const calculator = new FileModificationCalculator(project.resourceConfigs, project.sourceMaps.getSourceMap(defaultPath).file, project.sourceMaps);
50+
const calculator = new FileModificationCalculator(project);
5151
const result = await calculator.calculate([{
5252
modification: ModificationType.INSERT_OR_UPDATE,
5353
resource: modifiedResource,
@@ -177,11 +177,151 @@ describe('File modification calculator tests', () => {
177177
}])
178178

179179
expect(result.newFile).to.eq('[\n' +
180-
' { "type": "resource2", "param2": ["a", "b", "c"] }, {\n' +
180+
' { "type": "resource2", "param2": ["a", "b", "c"] },\n' +
181+
' {\n' +
182+
' "type": "project",\n' +
183+
' "plugins": {\n' +
184+
' "default": "latest"\n' +
185+
' }\n' +
186+
' }\n' +
187+
']')
188+
console.log(result)
189+
console.log(result.diff)
190+
})
191+
192+
it('Can update a resource in an existing config', async () => {
193+
const existingFile =
194+
`[
195+
{
196+
"type": "project",
197+
"plugins": {
198+
"default": "latest"
199+
}
200+
},
201+
{
202+
"type": "resource1",
203+
"param2": ["a", "b", "c"]
204+
}
205+
]`
206+
generateTestFile(existingFile);
207+
208+
const project = await CodifyParser.parse(defaultPath)
209+
project.resourceConfigs.forEach((r) => {
210+
r.attachResourceInfo(generateResourceInfo(r.type, ['param2']))
211+
});
212+
213+
const modifiedResource = new ResourceConfig({
214+
type: 'resource1',
215+
param2: ['a', 'b', 'c', 'd']
216+
})
217+
modifiedResource.attachResourceInfo(generateResourceInfo('resource1'))
218+
219+
const calculator = new FileModificationCalculator(project);
220+
const result = await calculator.calculate([{
221+
modification: ModificationType.INSERT_OR_UPDATE,
222+
resource: modifiedResource,
223+
}])
224+
225+
expect(result.newFile).to.eq('[\n' +
226+
' {\n' +
227+
' "type": "project",\n' +
228+
' "plugins": {\n' +
229+
' "default": "latest"\n' +
230+
' }\n' +
231+
' },\n' +
232+
' {\n' +
233+
' "type": "resource1",\n' +
234+
' "param2": ["a","b","c","d"]\n' +
235+
' }\n' +
236+
']',)
237+
console.log(result)
238+
console.log(result.diff)
239+
})
240+
241+
it('Can update a resource in an existing config 2 (works between two configs)', async () => {
242+
const existingFile =
243+
`[
244+
{
245+
"type": "project",
246+
"plugins": {
247+
"default": "latest"
248+
}
249+
},
250+
{
251+
"type": "resource1",
252+
"param2": ["a", "b", "c"]
253+
},
254+
{
255+
"type": "resource2",
256+
"param1": false,
257+
"param2": { "a": "aValue" },
258+
"param3": "this is a string"
259+
},
260+
{
261+
"type": "resource3",
262+
"param1": "param3",
263+
"param2": [
264+
"a",
265+
"b"
266+
]
267+
}
268+
]`
269+
generateTestFile(existingFile);
270+
271+
const project = await CodifyParser.parse(defaultPath)
272+
project.resourceConfigs.forEach((r) => {
273+
switch (r.type) {
274+
case 'resource1': {
275+
r.attachResourceInfo(generateResourceInfo(r.type, ['param2']))
276+
break;
277+
}
278+
case 'resource2': {
279+
r.attachResourceInfo(generateResourceInfo(r.type, ['param1']))
280+
break;
281+
}
282+
case 'resource3': {
283+
r.attachResourceInfo(generateResourceInfo(r.type, ['param2']))
284+
break;
285+
}
286+
}
287+
});
288+
289+
const modifiedResource = new ResourceConfig({
290+
type: 'resource2',
291+
param1: false,
292+
param3: "this is another string",
293+
})
294+
modifiedResource.attachResourceInfo(generateResourceInfo('resource2', ['param1']))
295+
296+
const calculator = new FileModificationCalculator(project);
297+
const result = await calculator.calculate([{
298+
modification: ModificationType.INSERT_OR_UPDATE,
299+
resource: modifiedResource,
300+
}])
301+
302+
expect(result.newFile).to.eq('[\n' +
303+
' {\n' +
181304
' "type": "project",\n' +
182305
' "plugins": {\n' +
183306
' "default": "latest"\n' +
184307
' }\n' +
308+
' },\n' +
309+
' { \n' +
310+
' "type": "resource1",\n' +
311+
' "param2": ["a", "b", "c"]\n' +
312+
' },\n' +
313+
' {\n' +
314+
' "type": "resource2",\n' +
315+
' "param1": false,\n' +
316+
' "param3": "this is another string"\n' +
317+
' },\n' +
318+
' { \n' +
319+
' "type": "resource3",\n' +
320+
' "param1": "param3",\n' +
321+
' "param2": [\n' +
322+
' "a",\n' +
323+
' "b"\n' +
324+
' ]\n' +
185325
' }\n' +
186326
']')
187327
console.log(result)

src/utils/file-modification-calculator.ts

Lines changed: 71 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import chalk from 'chalk';
22
import { ResourceConfig } from '../entities/resource-config.js';
33
import * as Diff from 'diff'
4+
import * as jsonSourceMap from 'json-source-map';
5+
46
import { FileType, InMemoryFile } from '../parser/entities.js';
5-
import { SourceLocation, SourceMap, SourceMapCache } from '../parser/source-maps.js';
7+
import { SourceMap, SourceMapCache } from '../parser/source-maps.js';
68
import detectIndent from 'detect-indent';
79
import { Project } from '../entities/project.js';
810
import { ProjectConfig } from '../entities/project-config.js';
@@ -27,13 +29,17 @@ export class FileModificationCalculator {
2729
private existingConfigs: ResourceConfig[];
2830
private sourceMap: SourceMap;
2931
private totalConfigLength: number;
32+
private indentString: string;
3033

3134
constructor(existing: Project) {
3235
const { file, sourceMap } = existing.sourceMaps?.getSourceMap(existing.codifyFiles[0])!;
3336
this.existingFile = file;
3437
this.sourceMap = sourceMap;
3538
this.existingConfigs = [...existing.resourceConfigs];
3639
this.totalConfigLength = existing.resourceConfigs.length + (existing.projectConfig ? 1 : 0);
40+
41+
const fileIndents = detectIndent(this.existingFile.contents);
42+
this.indentString = fileIndents.indent;
3743
}
3844

3945
async calculate(modifications: ModifiedResource[]): Promise<FileModificationResult> {
@@ -54,13 +60,8 @@ export class FileModificationCalculator {
5460

5561
this.validate(modifications);
5662

57-
const fileIndents = detectIndent(this.existingFile.contents);
58-
const indentString = fileIndents.indent;
59-
6063
let newFile = this.existingFile.contents.trimEnd();
6164

62-
console.log(JSON.stringify(this.sourceMap, null, 2))
63-
6465
// Reverse the traversal order so we edit from the back. This way the line numbers won't be messed up with new edits.
6566
for (const existing of this.existingConfigs.reverse()) {
6667
const duplicateIndex = modifications.findIndex((modified) => existing.isSameOnSystem(modified.resource))
@@ -75,23 +76,14 @@ export class FileModificationCalculator {
7576
const sourceIndex = Number.parseInt(duplicateSourceKey.split('/').at(1)!)
7677

7778
if (modified.modification === ModificationType.DELETE) {
78-
const isLast = sourceIndex === this.totalConfigLength - 1;
79-
const isFirst = sourceIndex === 0;
80-
81-
// We try to start deleting from the previous element to the next element if possible. This covers any spaces as well.
82-
const value = !isFirst ? this.sourceMap.lookup(`/${sourceIndex - 1}`)?.valueEnd : this.sourceMap.lookup(duplicateSourceKey)?.value;
83-
const valueEnd = !isLast ? this.sourceMap.lookup(`/${sourceIndex + 1}`)?.value : this.sourceMap.lookup(duplicateSourceKey)?.valueEnd;
79+
newFile = this.remove(newFile, this.sourceMap, sourceIndex);
80+
this.totalConfigLength -= 1;
8481

85-
newFile = this.remove(newFile, value!, valueEnd!, isFirst, isLast);
8682
continue;
8783
}
8884

89-
if (modified.modification === ModificationType.INSERT_OR_UPDATE) {
90-
const config = JSON.stringify(modified.resource.raw, null, indentString)
91-
newFile = this.insertConfig(newFile, config, indentString);
92-
}
93-
94-
resultResources.splice(duplicateIndex, 1, modified.resource);
85+
newFile = this.remove(newFile, this.sourceMap, sourceIndex);
86+
newFile = this.update(newFile, modified.resource, this.sourceMap, sourceIndex);
9587
}
9688

9789
return {
@@ -111,9 +103,9 @@ export class FileModificationCalculator {
111103
}
112104

113105
if (this.existingConfigs.some((r) => !r.resourceInfo)) {
114-
const badResources = this.existingConfigs
115-
.filter((r) => this.isResourceConfig(r))
116-
.map((r) => r.id)
106+
const badResources = this.existingConfigs
107+
.filter((r) => this.isResourceConfig(r))
108+
.map((r) => r.id)
117109

118110
throw new Error(`All resources must have resource info attached to generate diff. Found bad resources: ${badResources}`);
119111
}
@@ -160,24 +152,74 @@ export class FileModificationCalculator {
160152

161153
private remove(
162154
file: string,
163-
value: SourceLocation,
164-
valueEnd: SourceLocation,
165-
isFirst: boolean,
166-
isLast: boolean,
155+
sourceMap: SourceMap,
156+
sourceIndex: number,
167157
): string {
158+
const isLast = sourceIndex === this.totalConfigLength - 1;
159+
const isFirst = sourceIndex === 0;
160+
161+
// We try to start deleting from the previous element to the next element if possible. This covers any spaces as well.
162+
const value = !isFirst ? this.sourceMap.lookup(`/${sourceIndex - 1}`)?.valueEnd : this.sourceMap.lookup(`/${sourceIndex}`)?.value;
163+
const valueEnd = !isLast ? this.sourceMap.lookup(`/${sourceIndex + 1}`)?.value : this.sourceMap.lookup(`/${sourceIndex}`)?.valueEnd;
164+
168165
// Start one later so we leave the previous trailing comma alone
169-
const start = isFirst || isLast ? value.position : value.position + 1;
166+
const start = isFirst || isLast ? value!.position : value!.position + 1;
170167

171-
let result = this.r(file, start, valueEnd.position)
168+
let result = this.r(file, start, valueEnd!.position)
172169

173170
// If there's no gap between the remaining elements, we add a space.
174171
if (!isFirst && !/\s/.test(result[start])) {
175-
result = this.splice(result, start, 0, ' ');
172+
result = this.splice(result, start, 0, `\n${this.indentString}`);
176173
}
177174

178175
return result;
179176
}
180177

178+
/** Updates an existing resource config JSON with new values, this method replaces the old object but tries be either 1 line or multi-line like the original */
179+
private update(
180+
file: string,
181+
resource: ResourceConfig,
182+
sourceMap: SourceMap,
183+
sourceIndex: number,
184+
): string {
185+
// Updates: for now let's remove and re-add the entire object. Only two formatting availalbe either same line or multi-line
186+
const { value, valueEnd } = this.sourceMap.lookup(`/${sourceIndex}`)!;
187+
const isSameLine = value.line === valueEnd.line;
188+
189+
const isLast = sourceIndex === this.totalConfigLength - 1;
190+
const isFirst = sourceIndex === 0;
191+
192+
// We try to start deleting from the previous element to the next element if possible. This covers any spaces as well.
193+
const start = !isFirst ? this.sourceMap.lookup(`/${sourceIndex - 1}`)?.valueEnd : this.sourceMap.lookup(`/${sourceIndex}`)?.value;
194+
195+
let content = isSameLine ? JSON.stringify(resource.raw) : JSON.stringify(resource.raw, null, this.indentString);
196+
content = this.updateParamsToOnelineIfNeeded(content, sourceMap, sourceIndex);
197+
198+
content = content.split(/\n/).map((l) => `${this.indentString}${l}`).join('\n');
199+
content = isFirst ? `\n${content},` : `,\n${content}`
200+
201+
return this.splice(file, start?.position!, 0, content);
202+
}
203+
204+
/** Attempt to make arrays and objects oneliners if they were before. It does this by creating a new source map */
205+
private updateParamsToOnelineIfNeeded(content: string, sourceMap: SourceMap, sourceIndex: number): string {
206+
// Attempt to make arrays and objects oneliners if they were before. It does this by creating a new source map
207+
const parsedContent = JSON.parse(content);
208+
const parsedPointers = jsonSourceMap.parse(content);
209+
const parsedSourceMap = new SourceMapCache()
210+
parsedSourceMap.addSourceMap({ filePath: '', fileType: FileType.JSON, contents: parsedContent }, parsedPointers);
211+
212+
for (const [key, value] of Object.entries(parsedContent)) {
213+
const source = sourceMap.lookup(`/${sourceIndex}/${key}`);
214+
if ((Array.isArray(value) || typeof value === 'object') && source && source.value.line === source.valueEnd.line) {
215+
const { value, valueEnd } = parsedSourceMap.lookup(`#/${key}`)!
216+
content = this.splice(content, value.position, valueEnd.position - value.position, JSON.stringify(parsedContent[key]))
217+
}
218+
}
219+
220+
return content;
221+
}
222+
181223
private splice(s: string, start: number, deleteCount = 0, insert = '') {
182224
return s.substring(0, start) + insert + s.substring(start + deleteCount);
183225
}

0 commit comments

Comments
 (0)