@akanjs/devkit 2.3.9-rc.6 → 2.3.9-rc.8

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.
@@ -1,3 +1,4 @@
1
+ import ts from "typescript";
1
2
  import type { Sys } from "../commandDecorators";
2
3
  import { generatedFilePathsForTarget } from "./artifacts";
3
4
  import { createPrimitiveWriteReport } from "./primitive";
@@ -19,7 +20,10 @@ export const sourceFile = (sys: Sys, path: string, action: PrimitiveChangedFile[
19
20
  });
20
21
 
21
22
  export const moduleComponentName = (moduleName: string) =>
22
- `${moduleName.slice(0, 1).toUpperCase()}${moduleName.slice(1)}`;
23
+ moduleName
24
+ .replace(/[-_]+/g, " ")
25
+ .replace(/(?:^|\s+)([a-zA-Z0-9])/g, (_, char: string) => char.toUpperCase())
26
+ .replace(/\s+/g, "");
23
27
 
24
28
  export const moduleSourcePaths = (moduleName: string) => {
25
29
  const componentName = moduleComponentName(moduleName);
@@ -158,14 +162,17 @@ export const normalizeFieldType = (typeName: string) => {
158
162
 
159
163
  export const ensureBaseTypeImport = (content: string, typeName: string) => {
160
164
  if (typeName !== "Int" && typeName !== "Float") return content;
161
- if (new RegExp(`import \\{[^}]*\\b${typeName}\\b[^}]*\\} from "akanjs/base";`).test(content)) return content;
162
- const baseImport = /import \{ ([^}]+) \} from "akanjs\/base";/.exec(content);
165
+ const source = sourceFileFor("constant.ts", content);
166
+ const baseImport = findNamedImport(source, "akanjs/base");
163
167
  if (baseImport) {
164
- const names = baseImport[1]
165
- .split(",")
166
- .map((name) => name.trim())
167
- .filter(Boolean);
168
- return content.replace(baseImport[0], `import { ${[...names, typeName].sort().join(", ")} } from "akanjs/base";`);
168
+ if (baseImport.names.includes(typeName)) return content;
169
+ const nextNames = [...baseImport.names, typeName].sort();
170
+ return spliceText(
171
+ content,
172
+ baseImport.namedBindingsStart,
173
+ baseImport.namedBindingsEnd,
174
+ `{ ${nextNames.join(", ")} }`,
175
+ );
169
176
  }
170
177
  return `import { ${typeName} } from "akanjs/base";\n${content}`;
171
178
  };
@@ -302,39 +309,446 @@ export const fieldExpression = (
302
309
  return `${options.builderName ?? "field"}(${typeExpression}${defaultOption})`;
303
310
  };
304
311
 
312
+ export const fieldOrderingPriority = [
313
+ "id",
314
+ "name",
315
+ "title",
316
+ "status",
317
+ "category",
318
+ "description",
319
+ "content",
320
+ "startAt",
321
+ "dueAt",
322
+ "endAt",
323
+ "createdAt",
324
+ "updatedAt",
325
+ ] as const;
326
+
327
+ type FieldOrderingName = string;
328
+
329
+ interface LocatedField {
330
+ name: FieldOrderingName;
331
+ start: number;
332
+ fullStart: number;
333
+ end: number;
334
+ }
335
+
336
+ interface ObjectInsertionLocator {
337
+ objectStart: number;
338
+ objectEnd: number;
339
+ fields: LocatedField[];
340
+ }
341
+
342
+ interface ArrayInsertionLocator {
343
+ projectionStart: number;
344
+ projectionEnd: number;
345
+ fields: string[];
346
+ }
347
+
348
+ interface NamedImportLocator {
349
+ names: string[];
350
+ namedBindingsStart: number;
351
+ namedBindingsEnd: number;
352
+ }
353
+
354
+ export interface AkanConstantFieldStructure {
355
+ name: string;
356
+ expressionBuilder: string | null;
357
+ }
358
+
359
+ export interface AkanConstantStructure {
360
+ parseValid: boolean;
361
+ inputObjectFound: boolean;
362
+ builderName: string | null;
363
+ fields: AkanConstantFieldStructure[];
364
+ lightProjectionFields: string[];
365
+ baseImports: string[];
366
+ }
367
+
368
+ export interface AkanDictionaryStructure {
369
+ parseValid: boolean;
370
+ modelObjectFound: boolean;
371
+ chainOrderValid: boolean;
372
+ chainMethods: string[];
373
+ fields: string[];
374
+ }
375
+
376
+ const sourceFileFor = (fileName: string, content: string, scriptKind = ts.ScriptKind.TS) =>
377
+ ts.createSourceFile(fileName, content, ts.ScriptTarget.Latest, true, scriptKind);
378
+
379
+ const hasParseDiagnostics = (source: ts.SourceFile) =>
380
+ ((source as ts.SourceFile & { parseDiagnostics?: readonly ts.Diagnostic[] }).parseDiagnostics ?? []).length > 0;
381
+
382
+ const spliceText = (content: string, start: number, end: number, replacement: string) =>
383
+ `${content.slice(0, start)}${replacement}${content.slice(end)}`;
384
+
385
+ const lineStartAt = (content: string, position: number) => content.lastIndexOf("\n", Math.max(0, position - 1)) + 1;
386
+
387
+ const lineEndAt = (content: string, position: number) => {
388
+ const end = content.indexOf("\n", position);
389
+ return end < 0 ? content.length : end + 1;
390
+ };
391
+
392
+ const lineIndentAt = (content: string, position: number) =>
393
+ /^[ \t]*/.exec(content.slice(lineStartAt(content, position)))?.[0] ?? "";
394
+
395
+ const nodeName = (node: ts.PropertyName | ts.BindingName | undefined) => {
396
+ if (!node) return null;
397
+ if (ts.isIdentifier(node) || ts.isStringLiteral(node) || ts.isNumericLiteral(node)) return node.text;
398
+ return null;
399
+ };
400
+
401
+ const propertyName = (node: ts.ObjectLiteralElementLike) =>
402
+ ts.isPropertyAssignment(node) || ts.isShorthandPropertyAssignment(node) || ts.isMethodDeclaration(node)
403
+ ? nodeName(node.name)
404
+ : null;
405
+
406
+ const expressionName = (expression: ts.Expression): string | null => {
407
+ if (ts.isIdentifier(expression)) return expression.text;
408
+ if (ts.isPropertyAccessExpression(expression)) return expression.name.text;
409
+ if (ts.isCallExpression(expression)) return expressionName(expression.expression);
410
+ if (ts.isAsExpression(expression)) return expressionName(expression.expression);
411
+ return null;
412
+ };
413
+
414
+ const firstObjectReturnedByArrow = (node: ts.Node): ts.ObjectLiteralExpression | null => {
415
+ if (!ts.isArrowFunction(node) && !ts.isFunctionExpression(node)) return null;
416
+ if (ts.isObjectLiteralExpression(node.body)) return node.body;
417
+ if (ts.isParenthesizedExpression(node.body) && ts.isObjectLiteralExpression(node.body.expression)) {
418
+ return node.body.expression;
419
+ }
420
+ if (!ts.isBlock(node.body)) return null;
421
+ for (const statement of node.body.statements) {
422
+ if (ts.isReturnStatement(statement) && statement.expression && ts.isObjectLiteralExpression(statement.expression)) {
423
+ return statement.expression;
424
+ }
425
+ }
426
+ return null;
427
+ };
428
+
429
+ const isViaCall = (expression: ts.Expression) =>
430
+ ts.isCallExpression(expression) && expressionName(expression.expression) === "via";
431
+
432
+ const heritageCall = (node: ts.ClassDeclaration) => {
433
+ const heritage = node.heritageClauses?.flatMap((clause) => [...clause.types]) ?? [];
434
+ const expression = heritage.find((clause) => isViaCall(clause.expression))?.expression;
435
+ return expression && ts.isCallExpression(expression) ? expression : null;
436
+ };
437
+
438
+ const callExpressionName = (node: ts.CallExpression) =>
439
+ ts.isPropertyAccessExpression(node.expression) ? node.expression.name.text : expressionName(node.expression);
440
+
441
+ const locatedObject = (source: ts.SourceFile, objectLiteral: ts.ObjectLiteralExpression): ObjectInsertionLocator => ({
442
+ objectStart: objectLiteral.getStart(source),
443
+ objectEnd: objectLiteral.getEnd(),
444
+ fields: objectLiteral.properties
445
+ .map((property): LocatedField | null => {
446
+ const name = propertyName(property);
447
+ if (!name) return null;
448
+ return {
449
+ name,
450
+ start: property.getStart(source),
451
+ fullStart: property.getFullStart(),
452
+ end: property.getEnd(),
453
+ };
454
+ })
455
+ .filter((field): field is LocatedField => field !== null),
456
+ });
457
+
458
+ const findConstantInputObject = (source: ts.SourceFile, className: string): ObjectInsertionLocator | null => {
459
+ let locator: ObjectInsertionLocator | null = null;
460
+ const visit = (node: ts.Node) => {
461
+ if (locator) return;
462
+ if (!ts.isClassDeclaration(node) || node.name?.text !== className) {
463
+ ts.forEachChild(node, visit);
464
+ return;
465
+ }
466
+ const viaCall = heritageCall(node);
467
+ const callback = viaCall?.arguments.find((arg) => ts.isArrowFunction(arg) || ts.isFunctionExpression(arg));
468
+ const objectLiteral = callback ? firstObjectReturnedByArrow(callback) : null;
469
+ if (objectLiteral) locator = locatedObject(source, objectLiteral);
470
+ };
471
+ ts.forEachChild(source, visit);
472
+ return locator;
473
+ };
474
+
475
+ interface DictionaryModelLocator extends ObjectInsertionLocator {
476
+ chainMethods: string[];
477
+ }
478
+
479
+ const chainMethodsForCall = (node: ts.Expression): string[] => {
480
+ if (ts.isAsExpression(node) || ts.isParenthesizedExpression(node)) return chainMethodsForCall(node.expression);
481
+ if (!ts.isCallExpression(node)) return [];
482
+ if (ts.isPropertyAccessExpression(node.expression)) {
483
+ return [...chainMethodsForCall(node.expression.expression), node.expression.name.text];
484
+ }
485
+ const name = expressionName(node.expression);
486
+ return name ? [name] : [];
487
+ };
488
+
489
+ const outermostFluentCall = (node: ts.CallExpression) => {
490
+ let current: ts.CallExpression = node;
491
+ while (
492
+ ts.isPropertyAccessExpression(current.parent) &&
493
+ current.parent.expression === current &&
494
+ ts.isCallExpression(current.parent.parent) &&
495
+ current.parent.parent.expression === current.parent
496
+ ) {
497
+ current = current.parent.parent;
498
+ }
499
+ return current;
500
+ };
501
+
502
+ const protectedDictionaryChainOrder = ["model", "slice", "enum", "error", "translate"] as const;
503
+
504
+ const dictionaryChainOrderValid = (chainMethods: readonly string[]) => {
505
+ const protectedOrder = new Map(protectedDictionaryChainOrder.map((method, index) => [method, index]));
506
+ let lastOrder = -1;
507
+ for (const method of chainMethods) {
508
+ const order = protectedOrder.get(method as (typeof protectedDictionaryChainOrder)[number]);
509
+ if (order === undefined) continue;
510
+ if (order < lastOrder) return false;
511
+ lastOrder = order;
512
+ }
513
+ return true;
514
+ };
515
+
516
+ const findDictionaryModelObject = (source: ts.SourceFile, moduleClassName: string): DictionaryModelLocator | null => {
517
+ let locator: DictionaryModelLocator | null = null;
518
+ const visit = (node: ts.Node) => {
519
+ if (locator) return;
520
+ if (!ts.isCallExpression(node) || callExpressionName(node) !== "model") {
521
+ ts.forEachChild(node, visit);
522
+ return;
523
+ }
524
+ const typeArgument = node.typeArguments?.[0];
525
+ if (!typeArgument || typeArgument.getText(source) !== moduleClassName) {
526
+ ts.forEachChild(node, visit);
527
+ return;
528
+ }
529
+ const callback = node.arguments.find((arg) => ts.isArrowFunction(arg) || ts.isFunctionExpression(arg));
530
+ const objectLiteral = callback ? firstObjectReturnedByArrow(callback) : null;
531
+ if (objectLiteral) {
532
+ locator = {
533
+ ...locatedObject(source, objectLiteral),
534
+ chainMethods: chainMethodsForCall(outermostFluentCall(node)),
535
+ };
536
+ }
537
+ };
538
+ ts.forEachChild(source, visit);
539
+ return locator;
540
+ };
541
+
542
+ const findLightProjectionArray = (source: ts.SourceFile, moduleClassName: string): ArrayInsertionLocator | null => {
543
+ let locator: ArrayInsertionLocator | null = null;
544
+ const visit = (node: ts.Node) => {
545
+ if (locator) return;
546
+ if (!ts.isClassDeclaration(node) || node.name?.text !== `Light${moduleClassName}`) {
547
+ ts.forEachChild(node, visit);
548
+ return;
549
+ }
550
+ const viaCall = heritageCall(node);
551
+ const projectionArg = viaCall?.arguments.find((arg) => {
552
+ const expression = ts.isAsExpression(arg) ? arg.expression : arg;
553
+ return ts.isArrayLiteralExpression(expression);
554
+ });
555
+ const arrayLiteral = projectionArg
556
+ ? ts.isAsExpression(projectionArg)
557
+ ? projectionArg.expression
558
+ : projectionArg
559
+ : null;
560
+ if (!projectionArg || !arrayLiteral || !ts.isArrayLiteralExpression(arrayLiteral)) return;
561
+ locator = {
562
+ projectionStart: projectionArg.getStart(source),
563
+ projectionEnd: projectionArg.getEnd(),
564
+ fields: arrayLiteral.elements
565
+ .map((element) => (ts.isStringLiteralLike(element) ? element.text : null))
566
+ .filter((field): field is string => field !== null),
567
+ };
568
+ };
569
+ ts.forEachChild(source, visit);
570
+ return locator;
571
+ };
572
+
573
+ const findNamedImport = (source: ts.SourceFile, moduleSpecifier: string): NamedImportLocator | null => {
574
+ for (const statement of source.statements) {
575
+ if (!ts.isImportDeclaration(statement) || !ts.isStringLiteral(statement.moduleSpecifier)) continue;
576
+ if (statement.moduleSpecifier.text !== moduleSpecifier) continue;
577
+ const namedBindings = statement.importClause?.namedBindings;
578
+ if (!namedBindings || !ts.isNamedImports(namedBindings)) continue;
579
+ return {
580
+ names: namedBindings.elements.map((element) => element.name.text),
581
+ namedBindingsStart: namedBindings.getStart(source),
582
+ namedBindingsEnd: namedBindings.getEnd(),
583
+ };
584
+ }
585
+ return null;
586
+ };
587
+
588
+ const fieldExpressionBuilder = (property: ts.ObjectLiteralElementLike) => {
589
+ if (!ts.isPropertyAssignment(property)) return null;
590
+ const initializer = ts.isAsExpression(property.initializer) ? property.initializer.expression : property.initializer;
591
+ if (!ts.isCallExpression(initializer)) return null;
592
+ return expressionName(initializer.expression);
593
+ };
594
+
595
+ const priorityOf = (fieldName: string) => {
596
+ const priority = (fieldOrderingPriority as readonly string[]).indexOf(fieldName);
597
+ return priority < 0 ? null : priority;
598
+ };
599
+
600
+ export const insertionIndexForFieldOrder = (fieldNames: readonly string[], newFieldName: string) => {
601
+ const newPriority = priorityOf(newFieldName);
602
+ if (newPriority !== null) {
603
+ const greaterPriorityIndex = fieldNames.findIndex((name) => {
604
+ const existingPriority = priorityOf(name);
605
+ return existingPriority !== null && existingPriority > newPriority;
606
+ });
607
+ if (greaterPriorityIndex >= 0) return greaterPriorityIndex;
608
+
609
+ const lastPriorityIndex = fieldNames.reduce((lastIndex, name, index) => {
610
+ const existingPriority = priorityOf(name);
611
+ return existingPriority !== null ? index : lastIndex;
612
+ }, -1);
613
+ return lastPriorityIndex + 1;
614
+ }
615
+
616
+ const lastNonPriorityIndex = fieldNames.reduce(
617
+ (lastIndex, name, index) => (priorityOf(name) === null ? index : lastIndex),
618
+ -1,
619
+ );
620
+ return lastNonPriorityIndex >= 0 ? lastNonPriorityIndex + 1 : fieldNames.length;
621
+ };
622
+
623
+ const insertOrderedFieldLine = (
624
+ content: string,
625
+ locator: ObjectInsertionLocator,
626
+ fieldName: string,
627
+ line: string,
628
+ options: { fieldIndent: string; closingIndent: string },
629
+ ) => {
630
+ if (locator.fields.some((field) => field.name === fieldName)) return content;
631
+ const insertIndex = insertionIndexForFieldOrder(
632
+ locator.fields.map((field) => field.name),
633
+ fieldName,
634
+ );
635
+ const formattedLine = line.trim();
636
+ if (locator.fields.length === 0) {
637
+ return spliceText(
638
+ content,
639
+ locator.objectStart,
640
+ locator.objectEnd,
641
+ `{\n${options.fieldIndent}${formattedLine}\n${options.closingIndent}}`,
642
+ );
643
+ }
644
+
645
+ const beforeField = locator.fields[insertIndex];
646
+ if (beforeField) {
647
+ const leadingComments = ts.getLeadingCommentRanges(content, beforeField.fullStart) ?? [];
648
+ const insertAt = lineStartAt(content, leadingComments[0]?.pos ?? beforeField.start);
649
+ const indent = lineIndentAt(content, beforeField.start) || options.fieldIndent;
650
+ return spliceText(content, insertAt, insertAt, `${indent}${formattedLine}\n`);
651
+ }
652
+
653
+ const afterField = locator.fields[locator.fields.length - 1];
654
+ const insertAt = lineEndAt(content, afterField.end);
655
+ const indent = lineIndentAt(content, afterField.start) || options.fieldIndent;
656
+ return spliceText(content, insertAt, insertAt, `${indent}${formattedLine}\n`);
657
+ };
658
+
305
659
  export const viaBuilderParameterName = (content: string, className: string) => {
306
- const classIndex = content.indexOf(`export class ${className} extends via`);
307
- if (classIndex < 0) return null;
308
- const signature = content.slice(classIndex, content.indexOf("=>", classIndex));
309
- return /via\(\(\s*([A-Za-z_$][\w$]*)\s*\)/.exec(signature)?.[1] ?? null;
660
+ const source = sourceFileFor("constant.ts", content);
661
+ let builderName: string | null = null;
662
+ const visit = (node: ts.Node) => {
663
+ if (builderName !== null) return;
664
+ if (!ts.isClassDeclaration(node) || node.name?.text !== className) {
665
+ ts.forEachChild(node, visit);
666
+ return;
667
+ }
668
+ const viaCall = heritageCall(node);
669
+ const callback = viaCall?.arguments.find((arg) => ts.isArrowFunction(arg) || ts.isFunctionExpression(arg));
670
+ if (callback && (ts.isArrowFunction(callback) || ts.isFunctionExpression(callback))) {
671
+ builderName = nodeName(callback.parameters[0]?.name);
672
+ }
673
+ };
674
+ ts.forEachChild(source, visit);
675
+ return builderName;
676
+ };
677
+
678
+ export const inspectConstantStructure = (
679
+ content: string,
680
+ className: string,
681
+ moduleClassName: string,
682
+ ): AkanConstantStructure => {
683
+ const source = sourceFileFor("constant.ts", content);
684
+ const inputObject = findConstantInputObject(source, className);
685
+ const lightProjection = findLightProjectionArray(source, moduleClassName);
686
+ const baseImport = findNamedImport(source, "akanjs/base");
687
+ const fields: AkanConstantFieldStructure[] = [];
688
+ if (inputObject) {
689
+ const visit = (node: ts.Node) => {
690
+ if (!ts.isClassDeclaration(node) || node.name?.text !== className) {
691
+ ts.forEachChild(node, visit);
692
+ return;
693
+ }
694
+ const viaCall = heritageCall(node);
695
+ const callback = viaCall?.arguments.find((arg) => ts.isArrowFunction(arg) || ts.isFunctionExpression(arg));
696
+ const objectLiteral = callback ? firstObjectReturnedByArrow(callback) : null;
697
+ if (!objectLiteral) return;
698
+ fields.push(
699
+ ...objectLiteral.properties
700
+ .map((property): AkanConstantFieldStructure | null => {
701
+ const name = propertyName(property);
702
+ if (!name) return null;
703
+ return { name, expressionBuilder: fieldExpressionBuilder(property) };
704
+ })
705
+ .filter((field): field is AkanConstantFieldStructure => field !== null),
706
+ );
707
+ };
708
+ ts.forEachChild(source, visit);
709
+ }
710
+ return {
711
+ parseValid: !hasParseDiagnostics(source),
712
+ inputObjectFound: inputObject !== null,
713
+ builderName: viaBuilderParameterName(content, className),
714
+ fields,
715
+ lightProjectionFields: lightProjection?.fields ?? [],
716
+ baseImports: baseImport?.names ?? [],
717
+ };
718
+ };
719
+
720
+ export const inspectDictionaryStructure = (content: string, moduleClassName: string): AkanDictionaryStructure => {
721
+ const source = sourceFileFor("dictionary.ts", content);
722
+ const modelObject = findDictionaryModelObject(source, moduleClassName);
723
+ return {
724
+ parseValid: !hasParseDiagnostics(source),
725
+ modelObjectFound: modelObject !== null,
726
+ chainOrderValid: modelObject ? dictionaryChainOrderValid(modelObject.chainMethods) : false,
727
+ chainMethods: modelObject?.chainMethods ?? [],
728
+ fields: modelObject?.fields.map((field) => field.name) ?? [],
729
+ };
310
730
  };
311
731
 
312
732
  export const insertIntoObject = (content: string, className: string, line: string) => {
313
- const classIndex = content.indexOf(`export class ${className} extends via`);
314
- if (classIndex < 0) return null;
315
- const objectEndIndex = content.indexOf("}))", classIndex);
316
- if (objectEndIndex < 0) return null;
317
- const prefix = content.slice(0, objectEndIndex);
318
- const suffix = content.slice(objectEndIndex);
319
- const insertion = prefix.endsWith("\n") ? ` ${line}\n` : `\n ${line}\n`;
320
- return `${prefix}${insertion}${suffix}`;
733
+ const fieldName = /^([A-Za-z_$][\w$]*)\s*:/.exec(line.trim())?.[1];
734
+ if (!fieldName) return null;
735
+ const source = sourceFileFor("constant.ts", content);
736
+ const locator = findConstantInputObject(source, className);
737
+ if (!locator) return null;
738
+ return insertOrderedFieldLine(content, locator, fieldName, line, { fieldIndent: " ", closingIndent: "" });
321
739
  };
322
740
 
323
741
  export const insertLightProjectionField = (content: string, moduleClassName: string, fieldName: string) => {
324
- const classIndex = content.indexOf(`export class Light${moduleClassName} extends via`);
325
- if (classIndex < 0) return null;
326
- const arrayMatch = /\[([\s\S]*?)\]\s+as const/.exec(content.slice(classIndex));
327
- if (!arrayMatch || arrayMatch.index === undefined) return null;
328
- const arrayStart = classIndex + arrayMatch.index;
329
- const arrayEnd = arrayStart + arrayMatch[0].length;
330
- const fields = [...arrayMatch[1].matchAll(/"([^"]+)"/g)].map((match) => match[1]).filter(Boolean);
331
- if (fields.includes(fieldName)) return content;
332
- const nextFields = [...fields, fieldName];
742
+ const source = sourceFileFor("constant.ts", content);
743
+ const locator = findLightProjectionArray(source, moduleClassName);
744
+ if (!locator) return null;
745
+ if (locator.fields.includes(fieldName)) return content;
746
+ const nextFields = [...locator.fields, fieldName];
333
747
  const nextArray =
334
748
  nextFields.length === 0
335
749
  ? "[] as const"
336
750
  : `[\n${nextFields.map((field) => ` ${JSON.stringify(field)},`).join("\n")}\n] as const`;
337
- return `${content.slice(0, arrayStart)}${nextArray}${content.slice(arrayEnd)}`;
751
+ return spliceText(content, locator.projectionStart, locator.projectionEnd, nextArray);
338
752
  };
339
753
 
340
754
  export const insertTemplateField = ({
@@ -386,37 +800,6 @@ export const insertEnumClass = (content: string, enumClassName: string, enumName
386
800
  return `${content.slice(0, firstClassIndex)}${enumClass}${content.slice(firstClassIndex)}`;
387
801
  };
388
802
 
389
- const findMatchingBrace = (content: string, openIndex: number) => {
390
- let depth = 0;
391
- let quote: '"' | "'" | "`" | null = null;
392
- let escaped = false;
393
- for (let index = openIndex; index < content.length; index++) {
394
- const char = content[index];
395
- if (quote) {
396
- if (escaped) {
397
- escaped = false;
398
- continue;
399
- }
400
- if (char === "\\") {
401
- escaped = true;
402
- continue;
403
- }
404
- if (char === quote) quote = null;
405
- continue;
406
- }
407
- if (char === '"' || char === "'" || char === "`") {
408
- quote = char;
409
- continue;
410
- }
411
- if (char === "{") depth += 1;
412
- if (char === "}") {
413
- depth -= 1;
414
- if (depth === 0) return index;
415
- }
416
- }
417
- return -1;
418
- };
419
-
420
803
  const dictionaryModelFieldLine = (fieldName: string) => {
421
804
  const label = bilingualLabelForField(fieldName);
422
805
  const desc = bilingualDescriptionForField(fieldName);
@@ -426,20 +809,25 @@ const dictionaryModelFieldLine = (fieldName: string) => {
426
809
  };
427
810
 
428
811
  export const insertDictionaryModelField = (content: string, moduleClassName: string, fieldName: string) => {
429
- if (new RegExp(`\\b${fieldName}\\s*:`).test(content)) return content;
430
- const modelIndex = content.indexOf(`.model<${moduleClassName}>((t) => (`);
431
- if (modelIndex < 0) return null;
432
- const objectStartIndex = content.indexOf("{", modelIndex);
433
- if (objectStartIndex < 0) return null;
434
- const objectEndIndex = findMatchingBrace(content, objectStartIndex);
435
- if (objectEndIndex < 0) return null;
436
- const fieldLine = dictionaryModelFieldLine(fieldName);
437
- const body = content.slice(objectStartIndex + 1, objectEndIndex);
438
- if (body.trim().length === 0) {
439
- return `${content.slice(0, objectStartIndex + 1)}\n ${fieldLine}\n ${content.slice(objectEndIndex)}`;
440
- }
441
- const insertion = body.endsWith("\n") ? ` ${fieldLine}\n` : `\n ${fieldLine}\n`;
442
- return `${content.slice(0, objectEndIndex)}${insertion}${content.slice(objectEndIndex)}`;
812
+ const source = sourceFileFor("dictionary.ts", content);
813
+ const locator = findDictionaryModelObject(source, moduleClassName);
814
+ if (!locator) return null;
815
+ return insertOrderedFieldLine(content, locator, fieldName, dictionaryModelFieldLine(fieldName), {
816
+ fieldIndent: " ",
817
+ closingIndent: " ",
818
+ });
819
+ };
820
+
821
+ export const hasConstantInputField = (content: string, className: string, fieldName: string) => {
822
+ const source = sourceFileFor("constant.ts", content);
823
+ if (hasParseDiagnostics(source)) return false;
824
+ return findConstantInputObject(source, className)?.fields.some((field) => field.name === fieldName) ?? false;
825
+ };
826
+
827
+ export const hasDictionaryModelField = (content: string, moduleClassName: string, fieldName: string) => {
828
+ const source = sourceFileFor("dictionary.ts", content);
829
+ if (hasParseDiagnostics(source)) return false;
830
+ return findDictionaryModelObject(source, moduleClassName)?.fields.some((field) => field.name === fieldName) ?? false;
443
831
  };
444
832
 
445
833
  export const ensureConstantTypeImport = (content: string, constantPath: string, typeName: string) => {