instant-cli 0.22.96-experimental.drewh-ts-target.20759870126.1 → 0.22.96-experimental.surgical.20765334274.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/__tests__/__snapshots__/updateSchemaFile.test.ts.snap +248 -0
  3. package/__tests__/updateSchemaFile.test.ts +438 -0
  4. package/dist/index.d.ts.map +1 -1
  5. package/dist/index.js +1152 -1044
  6. package/dist/index.js.map +1 -1
  7. package/dist/rename.js +69 -58
  8. package/dist/rename.js.map +1 -1
  9. package/dist/renderSchemaPlan.js +22 -10
  10. package/dist/renderSchemaPlan.js.map +1 -1
  11. package/dist/ui/index.js +102 -115
  12. package/dist/ui/index.js.map +1 -1
  13. package/dist/ui/lib.js +30 -29
  14. package/dist/ui/lib.js.map +1 -1
  15. package/dist/util/fs.js +30 -17
  16. package/dist/util/fs.js.map +1 -1
  17. package/dist/util/isHeadlessEnvironment.js +1 -1
  18. package/dist/util/isHeadlessEnvironment.js.map +1 -1
  19. package/dist/util/loadConfig.js +32 -32
  20. package/dist/util/loadConfig.js.map +1 -1
  21. package/dist/util/packageManager.js +37 -26
  22. package/dist/util/packageManager.js.map +1 -1
  23. package/dist/util/projectDir.js +27 -16
  24. package/dist/util/projectDir.js.map +1 -1
  25. package/dist/util/promptOk.js +21 -14
  26. package/dist/util/promptOk.js.map +1 -1
  27. package/dist/util/renamePrompt.js +2 -4
  28. package/dist/util/renamePrompt.js.map +1 -1
  29. package/dist/util/updateSchemaFile.d.ts +3 -0
  30. package/dist/util/updateSchemaFile.d.ts.map +1 -0
  31. package/dist/util/updateSchemaFile.js +610 -0
  32. package/dist/util/updateSchemaFile.js.map +1 -0
  33. package/package.json +4 -4
  34. package/src/index.js +19 -10
  35. package/src/util/updateSchemaFile.ts +760 -0
  36. package/__tests__/mergeSchema.test.ts +0 -197
  37. package/dist/util/mergeSchema.d.ts +0 -2
  38. package/dist/util/mergeSchema.d.ts.map +0 -1
  39. package/dist/util/mergeSchema.js +0 -334
  40. package/dist/util/mergeSchema.js.map +0 -1
  41. package/src/util/mergeSchema.js +0 -364
@@ -0,0 +1,760 @@
1
+ import * as acorn from 'acorn';
2
+ import { tsPlugin } from 'acorn-typescript';
3
+ import {
4
+ diffSchemas,
5
+ renderAttrCall,
6
+ renderAttrProperty,
7
+ renderEntityProperty,
8
+ renderLinkProperty,
9
+ renderLinkValue,
10
+ type MigrationTx,
11
+ } from '@instantdb/platform';
12
+ import type { DataAttrDef, InstantSchemaDef } from '@instantdb/core';
13
+
14
+ const parser = (acorn.Parser as any).extend(tsPlugin({ dts: false }) as any);
15
+ const DEFAULT_INDENT = ' ';
16
+
17
+ export async function updateSchemaFile(
18
+ existingFileContent: string,
19
+ localSchema: InstantSchemaDef<any, any, any>,
20
+ serverSchema: InstantSchemaDef<any, any, any>,
21
+ ): Promise<string> {
22
+ const ast = parseFile(existingFileContent);
23
+ const schemaObj = findSchemaObject(ast);
24
+ if (!schemaObj) {
25
+ throw new Error('Could not find i.schema(...) in schema file.');
26
+ }
27
+
28
+ const { entitiesObj, linksObj } = getSchemaSections(schemaObj);
29
+ const diff = await diffSchemas(
30
+ localSchema,
31
+ serverSchema,
32
+ async (created) => created,
33
+ {},
34
+ );
35
+ if (diff.length === 0) {
36
+ return existingFileContent;
37
+ }
38
+
39
+ const { entitiesByName } = buildEntitiesIndex(entitiesObj);
40
+
41
+ const { localLinksByForward, serverLinksByForward } = buildLinkMaps(
42
+ localSchema.links || {},
43
+ serverSchema.links || {},
44
+ );
45
+ const changeBuckets = collectChanges(
46
+ diff,
47
+ localLinksByForward,
48
+ serverLinksByForward,
49
+ );
50
+
51
+ const edits: Edit[] = [];
52
+ edits.push(
53
+ ...collectEntityEdits(
54
+ existingFileContent,
55
+ entitiesObj,
56
+ entitiesByName,
57
+ changeBuckets,
58
+ serverSchema,
59
+ ),
60
+ );
61
+ edits.push(
62
+ ...collectLinkEdits(
63
+ existingFileContent,
64
+ linksObj,
65
+ changeBuckets,
66
+ localLinksByForward,
67
+ serverLinksByForward,
68
+ ),
69
+ );
70
+
71
+ const updated = applyEdits(existingFileContent, edits);
72
+ return normalizeEmptyLinksObject(updated);
73
+ }
74
+
75
+ type ObjectExpression = {
76
+ type: 'ObjectExpression';
77
+ start: number;
78
+ end: number;
79
+ properties: any[];
80
+ };
81
+
82
+ type PropertyNode = {
83
+ type: 'Property';
84
+ start: number;
85
+ end: number;
86
+ key: any;
87
+ value: any;
88
+ };
89
+
90
+ type EntityInfo = {
91
+ prop: PropertyNode;
92
+ attrsObj: ObjectExpression;
93
+ attrsByName: Map<string, PropertyNode>;
94
+ };
95
+
96
+ type Edit = { start: number; end: number; text: string };
97
+
98
+ type ChangeBuckets = {
99
+ createEntities: Set<string>;
100
+ deleteEntities: Set<string>;
101
+ addAttrs: Map<string, Set<string>>;
102
+ deleteAttrs: Map<string, Set<string>>;
103
+ updateAttrs: Map<string, Set<string>>;
104
+ addLinks: Set<string>;
105
+ deleteLinks: Set<string>;
106
+ updateLinks: Set<string>;
107
+ };
108
+
109
+ type LinkMap = Map<string, { name: string; link: any }>;
110
+
111
+ function parseFile(content: string) {
112
+ return parser.parse(content, {
113
+ sourceType: 'module',
114
+ ecmaVersion: 'latest',
115
+ });
116
+ }
117
+
118
+ function getSchemaSections(schemaObj: ObjectExpression) {
119
+ const entitiesProp = findObjectProperty(schemaObj, 'entities');
120
+ if (!entitiesProp || !isObjectExpression(entitiesProp.value)) {
121
+ throw new Error('Could not find entities object in schema file.');
122
+ }
123
+ const linksProp = findObjectProperty(schemaObj, 'links');
124
+ if (!linksProp || !isObjectExpression(linksProp.value)) {
125
+ throw new Error('Could not find links object in schema file.');
126
+ }
127
+
128
+ return {
129
+ entitiesObj: entitiesProp.value,
130
+ linksObj: linksProp.value,
131
+ };
132
+ }
133
+
134
+ function buildEntitiesIndex(entitiesObj: ObjectExpression) {
135
+ const entitiesByName = new Map<string, EntityInfo>();
136
+
137
+ for (const prop of entitiesObj.properties) {
138
+ if (!isProperty(prop)) continue;
139
+ const name = getPropName(prop);
140
+ if (!name) continue;
141
+ const attrsObj = getEntityAttrsObject(prop.value);
142
+ if (!attrsObj) continue;
143
+
144
+ const attrsByName = new Map<string, PropertyNode>();
145
+ for (const attrProp of attrsObj.properties) {
146
+ if (!isProperty(attrProp)) continue;
147
+ const attrName = getPropName(attrProp);
148
+ if (!attrName) continue;
149
+ attrsByName.set(attrName, attrProp);
150
+ }
151
+
152
+ entitiesByName.set(name, { prop, attrsObj, attrsByName });
153
+ }
154
+
155
+ return { entitiesByName };
156
+ }
157
+
158
+ function buildLinkMaps(
159
+ localLinks: Record<string, any>,
160
+ serverLinks: Record<string, any>,
161
+ ) {
162
+ return {
163
+ localLinksByForward: buildLinkForwardMap(localLinks),
164
+ serverLinksByForward: buildLinkForwardMap(serverLinks),
165
+ };
166
+ }
167
+
168
+ function collectEntityEdits(
169
+ content: string,
170
+ entitiesObj: ObjectExpression,
171
+ entitiesByName: Map<string, EntityInfo>,
172
+ changeBuckets: ChangeBuckets,
173
+ serverSchema: InstantSchemaDef<any, any, any>,
174
+ ): Edit[] {
175
+ const edits: Edit[] = [];
176
+
177
+ for (const entityName of changeBuckets.deleteEntities) {
178
+ const entity = entitiesByName.get(entityName);
179
+ if (!entity) continue;
180
+ edits.push(removeProperty(content, entitiesObj, entity.prop));
181
+ }
182
+
183
+ for (const entityName of changeBuckets.createEntities) {
184
+ if (entitiesByName.has(entityName)) continue;
185
+ const entityDef = serverSchema.entities?.[entityName];
186
+ if (!entityDef) continue;
187
+ const propText = renderEntityProperty(entityName, entityDef.attrs);
188
+ const indent = getObjectPropIndent(content, entitiesObj);
189
+ edits.push(insertProperty(content, entitiesObj, propText, indent));
190
+ }
191
+
192
+ for (const [entityName, attrs] of changeBuckets.deleteAttrs) {
193
+ const entity = entitiesByName.get(entityName);
194
+ if (!entity) continue;
195
+ for (const attrName of attrs) {
196
+ const attrProp = entity.attrsByName.get(attrName);
197
+ if (!attrProp) continue;
198
+ edits.push(removeProperty(content, entity.attrsObj, attrProp));
199
+ }
200
+ }
201
+
202
+ for (const [entityName, attrs] of changeBuckets.addAttrs) {
203
+ const entity = entitiesByName.get(entityName);
204
+ if (!entity) continue;
205
+ const entityDef = serverSchema.entities?.[entityName];
206
+ if (!entityDef) continue;
207
+ const indent = getObjectPropIndent(content, entity.attrsObj);
208
+ for (const attrName of attrs) {
209
+ const attrDef = entityDef.attrs?.[attrName];
210
+ if (!attrDef) continue;
211
+ const propText = renderAttrProperty(attrName, attrDef);
212
+ edits.push(
213
+ insertPropertyExpandingSingleLine(
214
+ content,
215
+ entity.attrsObj,
216
+ propText,
217
+ indent,
218
+ ),
219
+ );
220
+ }
221
+ }
222
+
223
+ for (const [entityName, attrs] of changeBuckets.updateAttrs) {
224
+ const entity = entitiesByName.get(entityName);
225
+ if (!entity) continue;
226
+ const entityDef = serverSchema.entities?.[entityName];
227
+ if (!entityDef) continue;
228
+ for (const attrName of attrs) {
229
+ const attrProp = entity.attrsByName.get(attrName);
230
+ const attrDef = entityDef.attrs?.[attrName];
231
+ if (!attrProp || !attrDef) continue;
232
+ edits.push(updateAttrEdit(content, attrProp, attrDef));
233
+ }
234
+ }
235
+
236
+ return edits;
237
+ }
238
+
239
+ function collectLinkEdits(
240
+ content: string,
241
+ linksObj: ObjectExpression,
242
+ changeBuckets: ChangeBuckets,
243
+ localLinksByForward: LinkMap,
244
+ serverLinksByForward: LinkMap,
245
+ ): Edit[] {
246
+ const edits: Edit[] = [];
247
+
248
+ for (const forwardKey of changeBuckets.deleteLinks) {
249
+ const localLink = localLinksByForward.get(forwardKey);
250
+ if (!localLink) continue;
251
+ const linkProp = findObjectProperty(linksObj, localLink.name);
252
+ if (!linkProp) continue;
253
+ edits.push(removeProperty(content, linksObj, linkProp));
254
+ }
255
+
256
+ for (const forwardKey of changeBuckets.addLinks) {
257
+ const serverLink = serverLinksByForward.get(forwardKey);
258
+ if (!serverLink) continue;
259
+ const linkName = serverLink.name;
260
+ if (findObjectProperty(linksObj, linkName)) continue;
261
+ const propText = renderLinkProperty(linkName, serverLink.link);
262
+ const indent = getObjectPropIndent(content, linksObj);
263
+ edits.push(insertProperty(content, linksObj, propText, indent));
264
+ }
265
+
266
+ for (const forwardKey of changeBuckets.updateLinks) {
267
+ const serverLink = serverLinksByForward.get(forwardKey);
268
+ const localLink = localLinksByForward.get(forwardKey);
269
+ if (!serverLink || !localLink) continue;
270
+ const linkProp = findObjectProperty(linksObj, localLink.name);
271
+ if (!linkProp) continue;
272
+ const nextValue = renderLinkValue(serverLink.link);
273
+ const propIndent = getLineIndent(content, linkProp.start);
274
+ edits.push({
275
+ start: linkProp.value.start,
276
+ end: linkProp.value.end,
277
+ text: indentValueAfterFirstLine(nextValue, propIndent),
278
+ });
279
+ }
280
+
281
+ return edits;
282
+ }
283
+
284
+ function updateAttrEdit(
285
+ content: string,
286
+ attrProp: PropertyNode,
287
+ attrDef: DataAttrDef<string, boolean, boolean>,
288
+ ): Edit {
289
+ const { typeParams } = analyzeChain(attrProp.value);
290
+ const typeParamsText = typeParams
291
+ ? content.slice(typeParams.start, typeParams.end)
292
+ : null;
293
+ const nextValue = renderAttrCall(attrDef, typeParamsText);
294
+ return {
295
+ start: attrProp.value.start,
296
+ end: attrProp.value.end,
297
+ text: nextValue,
298
+ };
299
+ }
300
+
301
+ function applyEdits(content: string, edits: Edit[]) {
302
+ if (edits.length === 0) {
303
+ return content;
304
+ }
305
+ const sorted = [...edits].sort((a, b) => b.start - a.start);
306
+ let output = content;
307
+ for (const edit of sorted) {
308
+ output = output.slice(0, edit.start) + edit.text + output.slice(edit.end);
309
+ }
310
+ return output;
311
+ }
312
+
313
+ function collectChanges(
314
+ steps: MigrationTx[],
315
+ localLinksByForward: LinkMap,
316
+ serverLinksByForward: LinkMap,
317
+ ): ChangeBuckets {
318
+ const buckets: ChangeBuckets = {
319
+ createEntities: new Set(),
320
+ deleteEntities: new Set(),
321
+ addAttrs: new Map(),
322
+ deleteAttrs: new Map(),
323
+ updateAttrs: new Map(),
324
+ addLinks: new Set(),
325
+ deleteLinks: new Set(),
326
+ updateLinks: new Set(),
327
+ };
328
+
329
+ for (const step of steps) {
330
+ const namespace = step.identifier.namespace;
331
+ const attrName = step.identifier.attrName;
332
+ const forwardKey = `${namespace}.${attrName}`;
333
+
334
+ switch (step.type) {
335
+ case 'add-attr': {
336
+ if (attrName === 'id') {
337
+ buckets.createEntities.add(namespace);
338
+ break;
339
+ }
340
+ if (step['value-type'] === 'ref') {
341
+ buckets.addLinks.add(forwardKey);
342
+ break;
343
+ }
344
+ ensureSet(buckets.addAttrs, namespace).add(attrName);
345
+ break;
346
+ }
347
+ case 'delete-attr': {
348
+ if (attrName === 'id') {
349
+ buckets.deleteEntities.add(namespace);
350
+ break;
351
+ }
352
+ if (localLinksByForward.has(forwardKey)) {
353
+ buckets.deleteLinks.add(forwardKey);
354
+ break;
355
+ }
356
+ ensureSet(buckets.deleteAttrs, namespace).add(attrName);
357
+ break;
358
+ }
359
+ case 'update-attr': {
360
+ if (step.partialAttr?.['value-type'] === 'ref') {
361
+ buckets.updateLinks.add(forwardKey);
362
+ break;
363
+ }
364
+ ensureSet(buckets.updateAttrs, namespace).add(attrName);
365
+ break;
366
+ }
367
+ case 'index':
368
+ case 'remove-index':
369
+ case 'unique':
370
+ case 'remove-unique':
371
+ case 'required':
372
+ case 'remove-required':
373
+ case 'check-data-type':
374
+ case 'remove-data-type': {
375
+ if (serverLinksByForward.has(forwardKey)) {
376
+ buckets.updateLinks.add(forwardKey);
377
+ break;
378
+ }
379
+ ensureSet(buckets.updateAttrs, namespace).add(attrName);
380
+ break;
381
+ }
382
+ default: {
383
+ assertNever(step);
384
+ }
385
+ }
386
+ }
387
+
388
+ pruneNamespaceBuckets(buckets);
389
+ return buckets;
390
+ }
391
+
392
+ function ensureSet(map: Map<string, Set<string>>, key: string) {
393
+ if (!map.has(key)) map.set(key, new Set());
394
+ return map.get(key)!;
395
+ }
396
+
397
+ function pruneNamespaceBuckets(buckets: ChangeBuckets) {
398
+ const namespaces = new Set<string>([
399
+ ...buckets.createEntities,
400
+ ...buckets.deleteEntities,
401
+ ]);
402
+
403
+ for (const namespace of namespaces) {
404
+ buckets.addAttrs.delete(namespace);
405
+ buckets.deleteAttrs.delete(namespace);
406
+ buckets.updateAttrs.delete(namespace);
407
+ removeNamespaceLinks(buckets.addLinks, namespace);
408
+ removeNamespaceLinks(buckets.deleteLinks, namespace);
409
+ removeNamespaceLinks(buckets.updateLinks, namespace);
410
+ }
411
+ }
412
+
413
+ function removeNamespaceLinks(set: Set<string>, namespace: string) {
414
+ const prefix = `${namespace}.`;
415
+ for (const key of Array.from(set)) {
416
+ if (key.startsWith(prefix)) {
417
+ set.delete(key);
418
+ }
419
+ }
420
+ }
421
+
422
+ function assertNever(value: never): never {
423
+ throw new Error(`Unhandled migration step: ${JSON.stringify(value)}`);
424
+ }
425
+
426
+ function analyzeChain(node: any): { typeParams: acorn.Node | null } {
427
+ let curr = node;
428
+ let typeParams: acorn.Node | null = null;
429
+
430
+ while (curr?.type === 'CallExpression') {
431
+ if (curr.typeParameters) {
432
+ typeParams = curr.typeParameters;
433
+ }
434
+ if (curr.callee?.type === 'MemberExpression') {
435
+ curr = curr.callee.object;
436
+ } else {
437
+ break;
438
+ }
439
+ }
440
+ return { typeParams };
441
+ }
442
+
443
+ function findSchemaObject(ast: any): ObjectExpression | null {
444
+ let schemaObj: ObjectExpression | null = null;
445
+ const walk = (node: any) => {
446
+ if (!node || schemaObj) return;
447
+ if (
448
+ node.type === 'CallExpression' &&
449
+ node.callee?.type === 'MemberExpression' &&
450
+ node.callee.object?.type === 'Identifier' &&
451
+ node.callee.object.name === 'i' &&
452
+ node.callee.property?.name === 'schema' &&
453
+ node.arguments?.length > 0
454
+ ) {
455
+ const arg = node.arguments[0];
456
+ if (isObjectExpression(arg)) {
457
+ schemaObj = arg;
458
+ }
459
+ return;
460
+ }
461
+ for (const key in node) {
462
+ if (key === 'loc' || key === 'start' || key === 'end') continue;
463
+ const value = node[key];
464
+ if (!value || typeof value !== 'object') continue;
465
+ if (Array.isArray(value)) {
466
+ value.forEach(walk);
467
+ } else {
468
+ walk(value);
469
+ }
470
+ }
471
+ };
472
+ walk(ast);
473
+ return schemaObj;
474
+ }
475
+
476
+ function findObjectProperty(
477
+ obj: ObjectExpression,
478
+ name: string,
479
+ ): PropertyNode | null {
480
+ for (const prop of obj.properties) {
481
+ if (!isProperty(prop)) continue;
482
+ const propName = getPropName(prop);
483
+ if (propName === name) return prop;
484
+ }
485
+ return null;
486
+ }
487
+
488
+ function getEntityAttrsObject(value: any): ObjectExpression | null {
489
+ if (
490
+ value?.type !== 'CallExpression' ||
491
+ value.callee?.type !== 'MemberExpression' ||
492
+ value.callee.object?.type !== 'Identifier' ||
493
+ value.callee.object.name !== 'i' ||
494
+ value.callee.property?.name !== 'entity'
495
+ ) {
496
+ return null;
497
+ }
498
+ const attrsObj = value.arguments?.[0];
499
+ return isObjectExpression(attrsObj) ? attrsObj : null;
500
+ }
501
+
502
+ function buildLinkForwardMap(links: Record<string, any>) {
503
+ const map = new Map<string, { name: string; link: any }>();
504
+ for (const [name, link] of Object.entries(links || {})) {
505
+ map.set(linkForwardKey(link), { name, link });
506
+ }
507
+ return map;
508
+ }
509
+
510
+ function linkForwardKey(link: any) {
511
+ return `${link.forward.on}.${link.forward.label}`;
512
+ }
513
+
514
+ function isObjectExpression(node: any): node is ObjectExpression {
515
+ return node?.type === 'ObjectExpression';
516
+ }
517
+
518
+ function isProperty(node: any): node is PropertyNode {
519
+ return node?.type === 'Property';
520
+ }
521
+
522
+ function getPropName(prop: PropertyNode) {
523
+ if (prop.key.type === 'Identifier') return prop.key.name;
524
+ if (prop.key.type === 'Literal') return String(prop.key.value);
525
+ return null;
526
+ }
527
+
528
+ function indentLines(text: string, indent: string) {
529
+ return text
530
+ .split('\n')
531
+ .map((line) => (line.length ? indent + line : line))
532
+ .join('\n');
533
+ }
534
+
535
+ function getObjectPropIndent(source: string, obj: ObjectExpression) {
536
+ const props = obj.properties.filter(isProperty);
537
+ if (props.length > 0) {
538
+ return getLineIndent(source, props[0].start);
539
+ }
540
+ const closingIndent = getLineIndent(source, obj.end - 1);
541
+ return closingIndent + DEFAULT_INDENT;
542
+ }
543
+
544
+ function getLineIndent(source: string, pos: number) {
545
+ const lineStart = source.lastIndexOf('\n', pos - 1) + 1;
546
+ const match = source.slice(lineStart, pos).match(/^[\t ]*/);
547
+ return match ? match[0] : '';
548
+ }
549
+
550
+ function insertProperty(
551
+ source: string,
552
+ obj: ObjectExpression,
553
+ propText: string,
554
+ indent: string,
555
+ ) {
556
+ const props = obj.properties.filter(isProperty);
557
+ const closingBrace = obj.end - 1;
558
+ const propTextWithIndent = indentLines(propText, indent);
559
+ const propTextSingleLine = propText.trim();
560
+ const innerStart = obj.start + 1;
561
+ const innerEnd = closingBrace;
562
+ const innerContent = source.slice(innerStart, innerEnd);
563
+ const innerWhitespaceOnly = /^[\s]*$/.test(innerContent);
564
+
565
+ if (props.length === 0) {
566
+ const objSource = source.slice(obj.start, obj.end);
567
+ const multiline = objSource.includes('\n') || propText.includes('\n');
568
+ if (!multiline) {
569
+ return {
570
+ start: closingBrace,
571
+ end: closingBrace,
572
+ text: ` ${propTextSingleLine} `,
573
+ };
574
+ }
575
+ const closingIndent = getLineIndent(source, closingBrace);
576
+ if (innerWhitespaceOnly) {
577
+ return {
578
+ start: innerStart,
579
+ end: innerEnd,
580
+ text: `\n${propTextWithIndent},\n${closingIndent}`,
581
+ };
582
+ }
583
+ return {
584
+ start: closingBrace,
585
+ end: closingBrace,
586
+ text: `\n${propTextWithIndent},\n${closingIndent}`,
587
+ };
588
+ }
589
+
590
+ const lastProp = props[props.length - 1];
591
+ const multiline =
592
+ source.slice(lastProp.end, closingBrace).includes('\n') ||
593
+ propText.includes('\n');
594
+ const needsComma = !hasTrailingComma(source, lastProp.end, obj.end);
595
+
596
+ if (!multiline) {
597
+ let insertPos = closingBrace;
598
+ while (insertPos > lastProp.end && /\s/.test(source[insertPos - 1])) {
599
+ insertPos -= 1;
600
+ }
601
+ return {
602
+ start: insertPos,
603
+ end: insertPos,
604
+ text: `${needsComma ? ',' : ''} ${propTextSingleLine}`,
605
+ };
606
+ }
607
+
608
+ const lineStart = source.lastIndexOf('\n', closingBrace);
609
+ return {
610
+ start: lineStart,
611
+ end: lineStart,
612
+ text: `${needsComma ? ',' : ''}\n${propTextWithIndent},`,
613
+ };
614
+ }
615
+
616
+ function removeProperty(
617
+ source: string,
618
+ obj: ObjectExpression,
619
+ prop: PropertyNode,
620
+ ) {
621
+ let start = prop.start;
622
+ let end = prop.end;
623
+ const lineStart = source.lastIndexOf('\n', start - 1) + 1;
624
+ let shouldTrimLineEnd = false;
625
+ if (/^[\t ]*$/.test(source.slice(lineStart, start))) {
626
+ start = lineStart;
627
+ shouldTrimLineEnd = true;
628
+ }
629
+ const after = skipWhitespaceAndComments(source, end, obj.end);
630
+ if (source[after] === ',') {
631
+ end = after + 1;
632
+ } else {
633
+ const before = skipWhitespaceAndCommentsBackward(source, start, obj.start);
634
+ if (source[before] === ',') {
635
+ start = before;
636
+ }
637
+ }
638
+ if (shouldTrimLineEnd) {
639
+ const lineEnd = source.indexOf('\n', end);
640
+ if (lineEnd !== -1 && /^[\t ]*$/.test(source.slice(end, lineEnd))) {
641
+ end = lineEnd + 1;
642
+ }
643
+ }
644
+ return { start, end, text: '' };
645
+ }
646
+
647
+ function indentValueAfterFirstLine(value: string, indent: string) {
648
+ const lines = value.split('\n');
649
+ if (lines.length <= 1) return value;
650
+ return [
651
+ lines[0],
652
+ ...lines.slice(1).map((line) => (line ? indent + line : line)),
653
+ ].join('\n');
654
+ }
655
+
656
+ function insertPropertyExpandingSingleLine(
657
+ source: string,
658
+ obj: ObjectExpression,
659
+ propText: string,
660
+ indent: string,
661
+ ) {
662
+ const props = obj.properties.filter(isProperty);
663
+ if (!props.length) {
664
+ return insertProperty(source, obj, propText, indent);
665
+ }
666
+ const objSource = source.slice(obj.start, obj.end);
667
+ if (objSource.includes('\n') || propText.includes('\n')) {
668
+ return insertProperty(source, obj, propText, indent);
669
+ }
670
+ const closingBrace = obj.end - 1;
671
+ const closingIndent = getLineIndent(source, closingBrace);
672
+ const innerIndent = closingIndent + DEFAULT_INDENT;
673
+ const propTexts = props.map((prop) =>
674
+ source.slice(prop.start, prop.end).trim(),
675
+ );
676
+ const lines = [...propTexts, propText.trim()].map(
677
+ (prop) => `${innerIndent}${prop},`,
678
+ );
679
+ const nextObject = `{\n${lines.join('\n')}\n${closingIndent}}`;
680
+ return { start: obj.start, end: obj.end, text: nextObject };
681
+ }
682
+
683
+ function normalizeEmptyLinksObject(content: string) {
684
+ let ast: any;
685
+ try {
686
+ ast = parseFile(content);
687
+ } catch {
688
+ return content;
689
+ }
690
+ const schemaObj = findSchemaObject(ast);
691
+ if (!schemaObj) return content;
692
+ const linksProp = findObjectProperty(schemaObj, 'links');
693
+ if (!linksProp || !isObjectExpression(linksProp.value)) return content;
694
+ if (linksProp.value.properties.some(isProperty)) return content;
695
+ const inner = content.slice(
696
+ linksProp.value.start + 1,
697
+ linksProp.value.end - 1,
698
+ );
699
+ if (!/^[\s]*$/.test(inner)) return content;
700
+ return (
701
+ content.slice(0, linksProp.value.start) +
702
+ '{}' +
703
+ content.slice(linksProp.value.end)
704
+ );
705
+ }
706
+
707
+ function hasTrailingComma(source: string, afterPos: number, endPos: number) {
708
+ const next = skipWhitespaceAndComments(source, afterPos, endPos);
709
+ return source[next] === ',';
710
+ }
711
+
712
+ function skipWhitespaceAndComments(source: string, start: number, end: number) {
713
+ let i = start;
714
+ while (i < end) {
715
+ const ch = source[i];
716
+ if (/\s/.test(ch)) {
717
+ i += 1;
718
+ continue;
719
+ }
720
+ if (ch === '/' && source[i + 1] === '/') {
721
+ const nextLine = source.indexOf('\n', i + 2);
722
+ i = nextLine === -1 ? end : nextLine + 1;
723
+ continue;
724
+ }
725
+ if (ch === '/' && source[i + 1] === '*') {
726
+ const close = source.indexOf('*/', i + 2);
727
+ i = close === -1 ? end : close + 2;
728
+ continue;
729
+ }
730
+ break;
731
+ }
732
+ return i;
733
+ }
734
+
735
+ function skipWhitespaceAndCommentsBackward(
736
+ source: string,
737
+ start: number,
738
+ end: number,
739
+ ) {
740
+ let i = start - 1;
741
+ while (i >= end) {
742
+ const ch = source[i];
743
+ if (/\s/.test(ch)) {
744
+ i -= 1;
745
+ continue;
746
+ }
747
+ if (ch === '/' && source[i - 1] === '/') {
748
+ const prevLine = source.lastIndexOf('\n', i - 2);
749
+ i = prevLine === -1 ? end - 1 : prevLine - 1;
750
+ continue;
751
+ }
752
+ if (ch === '/' && source[i - 1] === '*') {
753
+ const open = source.lastIndexOf('/*', i - 2);
754
+ i = open === -1 ? end - 1 : open - 1;
755
+ continue;
756
+ }
757
+ break;
758
+ }
759
+ return i;
760
+ }