@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.
- package/akanMcpContract.test.ts +37 -0
- package/akanMcpContract.ts +670 -0
- package/index.ts +1 -0
- package/package.json +2 -2
- package/workflow/artifacts.ts +58 -6
- package/workflow/executor.ts +208 -4
- package/workflow/index.ts +2 -0
- package/workflow/moduleIndex.test.ts +296 -0
- package/workflow/moduleIndex.ts +504 -0
- package/workflow/render.ts +4 -3
- package/workflow/rolloutGate.test.ts +109 -0
- package/workflow/rolloutGate.ts +138 -0
- package/workflow/source.test.ts +62 -0
- package/workflow/source.ts +463 -75
- package/workflow/types.ts +130 -0
package/workflow/source.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
162
|
-
const baseImport =
|
|
165
|
+
const source = sourceFileFor("constant.ts", content);
|
|
166
|
+
const baseImport = findNamedImport(source, "akanjs/base");
|
|
163
167
|
if (baseImport) {
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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
|
|
307
|
-
|
|
308
|
-
const
|
|
309
|
-
|
|
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
|
|
314
|
-
if (
|
|
315
|
-
const
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
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
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
if (
|
|
328
|
-
const
|
|
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
|
|
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
|
-
|
|
430
|
-
const
|
|
431
|
-
if (
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
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) => {
|