@kosdev-code/kos-codegen-core 0.1.0-next.847 → 0.1.0-next.849

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/index.mjs CHANGED
@@ -3,8 +3,8 @@ import * as path from "path";
3
3
  import * as ejs from "ejs";
4
4
  import fg from "fast-glob";
5
5
  import prettier from "prettier";
6
+ import { Scope, Project, QuoteKind, IndentationText, VariableDeclarationKind, Node } from "ts-morph";
6
7
  import * as ts from "typescript";
7
- import { Scope, Project, QuoteKind, IndentationText, Node } from "ts-morph";
8
8
  class TrackingFileSystem {
9
9
  inner;
10
10
  _writtenPaths = [];
@@ -560,6 +560,72 @@ function generatePolyglotWorkspace(codegenFs, templateDir, options) {
560
560
  );
561
561
  return { executablePaths: [...BUILD_SCRIPTS] };
562
562
  }
563
+ const KAB_OUTPUT_DIR = "target";
564
+ const KAB_OUTPUT_PATH = `{projectRoot}/${KAB_OUTPUT_DIR}/`;
565
+ function resolveKabPath(value, projectRoot) {
566
+ const root = projectRoot.replace(/\\/g, "/").replace(/\/+$/, "");
567
+ return value.replace(/\{projectRoot\}/g, root).replace(/\{workspaceRoot\}\/?/g, "");
568
+ }
569
+ const UPDATE_RELEASE_VERSION_SCRIPT_PATH = "tools/scripts/update-release-version.mjs";
570
+ function buildKabTargets(options) {
571
+ const { name, descriptor = false, buildTarget = "build" } = options;
572
+ const outputPath = KAB_OUTPUT_PATH;
573
+ const zipPayloadDir = "dist/{projectRoot}";
574
+ const targets = {
575
+ kab: {
576
+ command: `node tools/scripts/kabtool.mjs build ${name} && node tools/scripts/kabtool.mjs list ${name} `,
577
+ options: {
578
+ outputPath,
579
+ zipName: "ui.zip",
580
+ kabName: `${name}.kab`
581
+ },
582
+ dependsOn: ["zip"]
583
+ },
584
+ zip: {
585
+ command: `node tools/scripts/archiver.js ${name}`,
586
+ options: {
587
+ outputPath,
588
+ zipName: "ui.zip"
589
+ },
590
+ // `sbom` is a zip dependency, not a kab one: kabtool packages an
591
+ // already-sealed ui.zip, so anything ordered after `zip` misses it.
592
+ dependsOn: descriptor ? [buildTarget, "descriptor", "sbom"] : [buildTarget, "sbom"]
593
+ }
594
+ };
595
+ if (descriptor) {
596
+ targets.descriptor = {
597
+ command: `node tools/scripts/descriptor.mjs ${name}`,
598
+ options: {
599
+ outputPath: zipPayloadDir,
600
+ fileName: "descriptor.json"
601
+ },
602
+ dependsOn: ["build"]
603
+ };
604
+ }
605
+ targets.version = {
606
+ command: `node ${UPDATE_RELEASE_VERSION_SCRIPT_PATH} ${name} {args.ver}`,
607
+ options: {},
608
+ dependsOn: []
609
+ };
610
+ targets.sbom = buildSbomTarget({ outputPath: zipPayloadDir });
611
+ return targets;
612
+ }
613
+ function buildSbomTarget({
614
+ outputPath
615
+ }) {
616
+ const dir = outputPath.replace(/\/+$/, "");
617
+ const outputDir = dir.startsWith("{") ? dir : `{workspaceRoot}/${dir}`;
618
+ return {
619
+ executor: "@kosdev-code/kos-nx-plugin:sbom",
620
+ outputs: [
621
+ `${outputDir}/sbom.spdx.json`,
622
+ `${outputDir}/sbom.cyclonedx.json`
623
+ ],
624
+ cache: true,
625
+ inputs: ["production", "^production", "{workspaceRoot}/package-lock.json"],
626
+ options: { outputPath: dir, softFail: true }
627
+ };
628
+ }
563
629
  const UI_PROJECT_DIRS = [
564
630
  "apps",
565
631
  "libs",
@@ -587,16 +653,25 @@ function discoverUiArtifacts(codegenFs) {
587
653
  }
588
654
  const name = project.name;
589
655
  if (!name) continue;
656
+ const projectRoot = file.slice("ui/".length, file.lastIndexOf("/"));
657
+ const resolve = (value) => resolveKabPath(value, projectRoot);
590
658
  const kabOptions = project.targets?.kab?.options;
591
659
  if (kabOptions?.outputPath && kabOptions?.kabName) {
592
660
  artifacts.push({
593
661
  id: name,
594
- filename: `ui/${joinArtifactPath(kabOptions.outputPath, kabOptions.kabName)}`
662
+ filename: `ui/${joinArtifactPath(
663
+ resolve(kabOptions.outputPath),
664
+ kabOptions.kabName
665
+ )}`
595
666
  });
596
667
  } else if (project.targets?.splash) {
668
+ const splashOutput = project.targets.splash.options?.outputPath;
597
669
  artifacts.push({
598
670
  id: name,
599
- filename: `ui/dist/archives/packages/${name}/${name}.kab`,
671
+ filename: `ui/${joinArtifactPath(
672
+ resolve(splashOutput ?? `{projectRoot}/${KAB_OUTPUT_DIR}`),
673
+ `${name}.kab`
674
+ )}`,
600
675
  layer: 1
601
676
  });
602
677
  }
@@ -851,67 +926,249 @@ function normalizeOptions(codegenFs, options, projects) {
851
926
  template: ""
852
927
  };
853
928
  }
854
- const UPDATE_RELEASE_VERSION_SCRIPT_PATH = "tools/scripts/update-release-version.mjs";
855
- function buildKabTargets(options) {
856
- const {
857
- name,
858
- archiveDir = "packages",
859
- descriptorDir,
860
- buildTarget = "build"
861
- } = options;
862
- const outputPath = `dist/archives/${archiveDir}/${name}/`;
863
- const targets = {
864
- kab: {
865
- command: `node tools/scripts/kabtool.mjs build ${name} && node tools/scripts/kabtool.mjs list ${name} `,
866
- options: {
867
- outputPath,
868
- zipName: "ui.zip",
869
- kabName: `${name}.kab`
870
- },
871
- dependsOn: ["zip"]
872
- },
873
- zip: {
874
- command: `node tools/scripts/archiver.js ${name}`,
875
- options: {
876
- outputPath,
877
- zipName: "ui.zip"
878
- },
879
- dependsOn: descriptorDir ? [buildTarget, "descriptor"] : [buildTarget]
929
+ function transformSourceFile(codegenFs, filePath, mutate) {
930
+ const content = codegenFs.read(filePath);
931
+ if (content === null) {
932
+ throw new Error(`File not found: ${filePath}`);
933
+ }
934
+ const project = new Project({
935
+ useInMemoryFileSystem: true,
936
+ manipulationSettings: {
937
+ indentationText: IndentationText.TwoSpaces,
938
+ quoteKind: QuoteKind.Double
880
939
  }
881
- };
882
- if (descriptorDir) {
883
- targets.descriptor = {
884
- command: `node tools/scripts/descriptor.mjs ${name}`,
885
- options: {
886
- outputPath: `dist/${descriptorDir}`,
887
- fileName: "descriptor.json"
888
- },
889
- dependsOn: ["build"]
890
- };
940
+ });
941
+ const sourceFile = project.createSourceFile(filePath, content, {
942
+ overwrite: true
943
+ });
944
+ mutate(sourceFile);
945
+ codegenFs.write(filePath, sourceFile.getFullText());
946
+ }
947
+ function readSourceFile(codegenFs, filePath, inspect) {
948
+ const content = codegenFs.read(filePath);
949
+ if (content === null) {
950
+ throw new Error(`File not found: ${filePath}`);
891
951
  }
892
- targets.version = {
893
- command: `node ${UPDATE_RELEASE_VERSION_SCRIPT_PATH} ${name} {args.ver}`,
894
- options: {},
895
- dependsOn: []
896
- };
897
- targets.sbom = buildSbomTarget({ outputPath });
898
- targets.kab.dependsOn = [...targets.kab.dependsOn ?? [], "sbom"];
899
- return targets;
952
+ const project = new Project({ useInMemoryFileSystem: true });
953
+ return inspect(
954
+ project.createSourceFile(filePath, content, { overwrite: true })
955
+ );
900
956
  }
901
- function buildSbomTarget({
902
- outputPath
903
- }) {
904
- const dir = outputPath.replace(/\/+$/, "");
905
- return {
906
- executor: "@kosdev-code/kos-nx-plugin:sbom",
907
- outputs: [
908
- `{workspaceRoot}/${dir}/sbom.spdx.json`,
909
- `{workspaceRoot}/${dir}/sbom.cyclonedx.json`
910
- ],
911
- cache: true,
912
- inputs: ["production", "^production", "{workspaceRoot}/package-lock.json"],
913
- options: { outputPath: dir, softFail: true }
914
- };
957
+ function ensureNamedImport(sourceFile, moduleSpecifier, names) {
958
+ const decls = sourceFile.getImportDeclarations().filter((d) => d.getModuleSpecifierValue() === moduleSpecifier);
959
+ const existing = /* @__PURE__ */ new Set();
960
+ for (const d of decls) {
961
+ for (const n of d.getNamedImports()) existing.add(n.getName());
962
+ }
963
+ let target = decls.find((d) => !d.isTypeOnly());
964
+ if (!target) {
965
+ target = sourceFile.addImportDeclaration({ moduleSpecifier });
966
+ }
967
+ for (const { name, isTypeOnly } of names) {
968
+ if (existing.has(name)) continue;
969
+ target.addNamedImport({ name, isTypeOnly: !!isTypeOnly });
970
+ existing.add(name);
971
+ }
972
+ }
973
+ function resolveSdkModuleSpecifier(sourceFile) {
974
+ const decl = sourceFile.getImportDeclarations().find((d) => d.getNamedImports().some((n) => n.getName() === "kosModel"));
975
+ return decl?.getModuleSpecifierValue() ?? "@kosdev-code/kos-ui-sdk";
976
+ }
977
+ function getModelClass(sourceFile, preferName) {
978
+ const classes = sourceFile.getClasses();
979
+ const byName = preferName ? classes.find((c) => c.getName() === preferName) : void 0;
980
+ if (byName) return byName;
981
+ const impl = classes.find((c) => /ModelImpl$/.test(c.getName() ?? ""));
982
+ if (impl) return impl;
983
+ const exported = classes.find((c) => c.isExported());
984
+ if (exported) return exported;
985
+ if (classes.length > 0) return classes[0];
986
+ throw new Error("No class declaration found in model file.");
987
+ }
988
+ function addClassDecorator(cls, name, opts) {
989
+ if (cls.getDecorator(name)) return;
990
+ const typeArgs = opts?.typeArgs?.length ? `<${opts.typeArgs.join(", ")}>` : "";
991
+ const args = opts?.argsText ?? "";
992
+ const decorators = cls.getDecorators();
993
+ const kosModelIdx = decorators.findIndex((d) => d.getName() === "kosModel");
994
+ const insertIdx = kosModelIdx >= 0 ? kosModelIdx + 1 : decorators.length;
995
+ cls.insertDecorator(insertIdx, {
996
+ name: `${name}${typeArgs}`,
997
+ arguments: args ? [args] : []
998
+ });
999
+ }
1000
+ function ensureDeclarationMerge(sourceFile, interfaceName, extendsExpr, typeParameters = []) {
1001
+ const baseType = extendsExpr.split("<")[0].trim();
1002
+ let iface = sourceFile.getInterface(interfaceName);
1003
+ if (iface) {
1004
+ const already = iface.getExtends().some((e) => e.getText().split("<")[0].trim() === baseType);
1005
+ if (!already) iface.addExtends(extendsExpr);
1006
+ return;
1007
+ }
1008
+ iface = sourceFile.addInterface({
1009
+ name: interfaceName,
1010
+ isExported: true,
1011
+ typeParameters,
1012
+ extends: [extendsExpr]
1013
+ });
1014
+ sourceFile.insertText(
1015
+ iface.getStart(),
1016
+ "// eslint-disable-next-line @typescript-eslint/no-empty-interface\n"
1017
+ );
1018
+ }
1019
+ function ensureFileEslintDisable(sourceFile, rule) {
1020
+ if (sourceFile.getFullText().includes(rule)) return;
1021
+ sourceFile.insertText(0, `/* eslint-disable ${rule} */
1022
+ `);
1023
+ }
1024
+ function ensureModuleConst(sourceFile, spec) {
1025
+ if (sourceFile.getVariableDeclaration(spec.name)) return false;
1026
+ const imports = sourceFile.getImportDeclarations();
1027
+ const index = imports.length > 0 ? imports[imports.length - 1].getChildIndex() + 1 : 0;
1028
+ sourceFile.insertVariableStatement(index, {
1029
+ declarationKind: VariableDeclarationKind.Const,
1030
+ declarations: [{ name: spec.name, initializer: spec.initializer }]
1031
+ });
1032
+ return true;
1033
+ }
1034
+ function addDecoratedMethod(cls, spec) {
1035
+ if (cls.getMethod(spec.name)) return false;
1036
+ cls.addMethod({
1037
+ name: spec.name,
1038
+ isAsync: spec.isAsync,
1039
+ returnType: spec.returnType,
1040
+ parameters: spec.parameters?.map((p) => {
1041
+ return { name: p.name, type: p.type, hasQuestionToken: p.optional };
1042
+ }),
1043
+ statements: spec.statements,
1044
+ decorators: [
1045
+ {
1046
+ name: spec.decoratorName,
1047
+ arguments: spec.decoratorArgsText ? [spec.decoratorArgsText] : []
1048
+ }
1049
+ ]
1050
+ });
1051
+ return true;
1052
+ }
1053
+ function propertyInsertIndex(cls) {
1054
+ const props = cls.getProperties();
1055
+ if (props.length === 0) return 0;
1056
+ return props[props.length - 1].getChildIndex() + 1;
1057
+ }
1058
+ function addPlainProperty(cls, spec) {
1059
+ if (cls.getProperty(spec.name)) return false;
1060
+ cls.insertProperty(propertyInsertIndex(cls), {
1061
+ name: spec.name,
1062
+ type: spec.type,
1063
+ initializer: spec.initializer,
1064
+ isReadonly: !!spec.readonly,
1065
+ scope: spec.scope ? SCOPE_MAP[spec.scope] : void 0,
1066
+ hasExclamationToken: spec.initializer ? false : !!spec.hasExclamation
1067
+ });
1068
+ return true;
1069
+ }
1070
+ const SCOPE_MAP = {
1071
+ private: Scope.Private,
1072
+ protected: Scope.Protected,
1073
+ public: Scope.Public
1074
+ };
1075
+ function addDecoratedProperty(cls, spec) {
1076
+ if (cls.getProperty(spec.name)) return false;
1077
+ cls.insertProperty(propertyInsertIndex(cls), {
1078
+ name: spec.name,
1079
+ type: spec.type,
1080
+ initializer: spec.initializer,
1081
+ scope: spec.scope ? SCOPE_MAP[spec.scope] : void 0,
1082
+ hasExclamationToken: spec.initializer ? false : !!spec.hasExclamation,
1083
+ decorators: [
1084
+ spec.bare && !spec.decoratorArgsText ? { name: spec.decoratorName } : {
1085
+ name: spec.decoratorName,
1086
+ arguments: spec.decoratorArgsText ? [spec.decoratorArgsText] : []
1087
+ }
1088
+ ]
1089
+ });
1090
+ return true;
1091
+ }
1092
+ function addGetter(cls, spec) {
1093
+ if (cls.getGetAccessor(spec.name)) return false;
1094
+ const ctor = cls.getConstructors()[0];
1095
+ const index = ctor ? ctor.getChildIndex() + 1 : propertyInsertIndex(cls);
1096
+ cls.insertGetAccessor(index, {
1097
+ name: spec.name,
1098
+ returnType: spec.returnType,
1099
+ statements: spec.statements ?? "// TODO: derive and return the computed value"
1100
+ });
1101
+ return true;
1102
+ }
1103
+ const CATALOG_METHODS = ["get", "put", "post", "delete", "patch"];
1104
+ const SERVICE_MODULE_RE$1 = /\/utils\/services\/([^/]+)\/([^/]+)\/service\.ts$/;
1105
+ function listServiceCatalog(codegenFs, query, projects) {
1106
+ const project = findProjectByName(
1107
+ codegenFs.root,
1108
+ query.modelProject,
1109
+ projects
1110
+ );
1111
+ if (!project) {
1112
+ throw new Error(`Project not found: ${query.modelProject}`);
1113
+ }
1114
+ const sourceRoot = project.sourceRoot || path.join(project.root, "src");
1115
+ const entries = [];
1116
+ for (const file of codegenFs.listFiles(sourceRoot)) {
1117
+ const posix = file.split(path.sep).join("/");
1118
+ const match = SERVICE_MODULE_RE$1.exec(posix);
1119
+ if (!match) continue;
1120
+ const [, app, version] = match;
1121
+ if (query.app && app !== query.app) continue;
1122
+ if (query.version && version !== query.version) continue;
1123
+ const openapiPath = posix.replace(/service\.ts$/, "openapi.d.ts");
1124
+ if (!codegenFs.exists(openapiPath)) continue;
1125
+ entries.push({
1126
+ app,
1127
+ version,
1128
+ serviceModulePath: posix,
1129
+ operations: readOperations(codegenFs, openapiPath)
1130
+ });
1131
+ }
1132
+ return entries.sort(
1133
+ (a, b) => a.app.localeCompare(b.app) || a.version.localeCompare(b.version)
1134
+ );
1135
+ }
1136
+ function readOperations(codegenFs, openapiPath) {
1137
+ return readSourceFile(codegenFs, openapiPath, (sf) => {
1138
+ const paths = sf.getInterface("paths");
1139
+ if (!paths) return [];
1140
+ const operations = [];
1141
+ for (const pathProp of paths.getProperties()) {
1142
+ const servicePath = unquote(pathProp.getName());
1143
+ const literal = pathProp.getTypeNode();
1144
+ if (!literal || !("getProperties" in literal)) continue;
1145
+ for (const methodProp of literal.getProperties()) {
1146
+ const method = methodProp.getName();
1147
+ if (!isCatalogMethod(method)) continue;
1148
+ const typeText = methodProp.getTypeNode()?.getText() ?? "";
1149
+ if (methodProp.hasQuestionToken() && typeText === "never") continue;
1150
+ operations.push({
1151
+ path: servicePath,
1152
+ method,
1153
+ summary: readSummary(methodProp),
1154
+ pathParams: [...servicePath.matchAll(/\{([^}]+)\}/g)].map(
1155
+ (m) => m[1]
1156
+ )
1157
+ });
1158
+ }
1159
+ }
1160
+ return operations;
1161
+ });
1162
+ }
1163
+ function isCatalogMethod(value) {
1164
+ return CATALOG_METHODS.includes(value);
1165
+ }
1166
+ function readSummary(methodProp) {
1167
+ const text = methodProp.getLeadingCommentRanges().map((c) => c.getText()).join(" ").replace(/^\/\*+/, "").replace(/\*+\/$/, "").replace(/^\s*\*\s?/gm, "").replace(/\s+/g, " ").trim();
1168
+ return text || void 0;
1169
+ }
1170
+ function unquote(name) {
1171
+ return name.replace(/^["']|["']$/g, "");
915
1172
  }
916
1173
  const UPDATE_RELEASE_VERSION_SCRIPT = `import devkit from "@nx/devkit";
917
1174
  import { resolve } from "path";
@@ -1473,160 +1730,6 @@ function normalizeAddFutureOptions(codegenFs, options, projects) {
1473
1730
  internal
1474
1731
  };
1475
1732
  }
1476
- function transformSourceFile(codegenFs, filePath, mutate) {
1477
- const content = codegenFs.read(filePath);
1478
- if (content === null) {
1479
- throw new Error(`File not found: ${filePath}`);
1480
- }
1481
- const project = new Project({
1482
- useInMemoryFileSystem: true,
1483
- manipulationSettings: {
1484
- indentationText: IndentationText.TwoSpaces,
1485
- quoteKind: QuoteKind.Double
1486
- }
1487
- });
1488
- const sourceFile = project.createSourceFile(filePath, content, {
1489
- overwrite: true
1490
- });
1491
- mutate(sourceFile);
1492
- codegenFs.write(filePath, sourceFile.getFullText());
1493
- }
1494
- function ensureNamedImport(sourceFile, moduleSpecifier, names) {
1495
- const decls = sourceFile.getImportDeclarations().filter((d) => d.getModuleSpecifierValue() === moduleSpecifier);
1496
- const existing = /* @__PURE__ */ new Set();
1497
- for (const d of decls) {
1498
- for (const n of d.getNamedImports()) existing.add(n.getName());
1499
- }
1500
- let target = decls.find((d) => !d.isTypeOnly());
1501
- if (!target) {
1502
- target = sourceFile.addImportDeclaration({ moduleSpecifier });
1503
- }
1504
- for (const { name, isTypeOnly } of names) {
1505
- if (existing.has(name)) continue;
1506
- target.addNamedImport({ name, isTypeOnly: !!isTypeOnly });
1507
- existing.add(name);
1508
- }
1509
- }
1510
- function resolveSdkModuleSpecifier(sourceFile) {
1511
- const decl = sourceFile.getImportDeclarations().find((d) => d.getNamedImports().some((n) => n.getName() === "kosModel"));
1512
- return decl?.getModuleSpecifierValue() ?? "@kosdev-code/kos-ui-sdk";
1513
- }
1514
- function getModelClass(sourceFile, preferName) {
1515
- const classes = sourceFile.getClasses();
1516
- const byName = preferName ? classes.find((c) => c.getName() === preferName) : void 0;
1517
- if (byName) return byName;
1518
- const impl = classes.find((c) => /ModelImpl$/.test(c.getName() ?? ""));
1519
- if (impl) return impl;
1520
- const exported = classes.find((c) => c.isExported());
1521
- if (exported) return exported;
1522
- if (classes.length > 0) return classes[0];
1523
- throw new Error("No class declaration found in model file.");
1524
- }
1525
- function addClassDecorator(cls, name, opts) {
1526
- if (cls.getDecorator(name)) return;
1527
- const typeArgs = opts?.typeArgs?.length ? `<${opts.typeArgs.join(", ")}>` : "";
1528
- const args = opts?.argsText ?? "";
1529
- const decorators = cls.getDecorators();
1530
- const kosModelIdx = decorators.findIndex((d) => d.getName() === "kosModel");
1531
- const insertIdx = kosModelIdx >= 0 ? kosModelIdx + 1 : decorators.length;
1532
- cls.insertDecorator(insertIdx, {
1533
- name: `${name}${typeArgs}`,
1534
- arguments: args ? [args] : []
1535
- });
1536
- }
1537
- function ensureDeclarationMerge(sourceFile, interfaceName, extendsExpr, typeParameters = []) {
1538
- const baseType = extendsExpr.split("<")[0].trim();
1539
- let iface = sourceFile.getInterface(interfaceName);
1540
- if (iface) {
1541
- const already = iface.getExtends().some((e) => e.getText().split("<")[0].trim() === baseType);
1542
- if (!already) iface.addExtends(extendsExpr);
1543
- return;
1544
- }
1545
- iface = sourceFile.addInterface({
1546
- name: interfaceName,
1547
- isExported: true,
1548
- typeParameters,
1549
- extends: [extendsExpr]
1550
- });
1551
- sourceFile.insertText(
1552
- iface.getStart(),
1553
- "// eslint-disable-next-line @typescript-eslint/no-empty-interface\n"
1554
- );
1555
- }
1556
- function ensureFileEslintDisable(sourceFile, rule) {
1557
- if (sourceFile.getFullText().includes(rule)) return;
1558
- sourceFile.insertText(0, `/* eslint-disable ${rule} */
1559
- `);
1560
- }
1561
- function addDecoratedMethod(cls, spec) {
1562
- if (cls.getMethod(spec.name)) return false;
1563
- cls.addMethod({
1564
- name: spec.name,
1565
- isAsync: spec.isAsync,
1566
- returnType: spec.returnType,
1567
- parameters: spec.parameters?.map((p) => {
1568
- return { name: p.name, type: p.type };
1569
- }),
1570
- statements: spec.statements,
1571
- decorators: [
1572
- {
1573
- name: spec.decoratorName,
1574
- arguments: spec.decoratorArgsText ? [spec.decoratorArgsText] : []
1575
- }
1576
- ]
1577
- });
1578
- return true;
1579
- }
1580
- function propertyInsertIndex(cls) {
1581
- const props = cls.getProperties();
1582
- if (props.length === 0) return 0;
1583
- return props[props.length - 1].getChildIndex() + 1;
1584
- }
1585
- function addPlainProperty(cls, spec) {
1586
- if (cls.getProperty(spec.name)) return false;
1587
- cls.insertProperty(propertyInsertIndex(cls), {
1588
- name: spec.name,
1589
- type: spec.type,
1590
- initializer: spec.initializer,
1591
- isReadonly: !!spec.readonly,
1592
- scope: spec.scope ? SCOPE_MAP[spec.scope] : void 0,
1593
- hasExclamationToken: spec.initializer ? false : !!spec.hasExclamation
1594
- });
1595
- return true;
1596
- }
1597
- const SCOPE_MAP = {
1598
- private: Scope.Private,
1599
- protected: Scope.Protected,
1600
- public: Scope.Public
1601
- };
1602
- function addDecoratedProperty(cls, spec) {
1603
- if (cls.getProperty(spec.name)) return false;
1604
- cls.insertProperty(propertyInsertIndex(cls), {
1605
- name: spec.name,
1606
- type: spec.type,
1607
- initializer: spec.initializer,
1608
- scope: spec.scope ? SCOPE_MAP[spec.scope] : void 0,
1609
- hasExclamationToken: spec.initializer ? false : !!spec.hasExclamation,
1610
- decorators: [
1611
- spec.bare && !spec.decoratorArgsText ? { name: spec.decoratorName } : {
1612
- name: spec.decoratorName,
1613
- arguments: spec.decoratorArgsText ? [spec.decoratorArgsText] : []
1614
- }
1615
- ]
1616
- });
1617
- return true;
1618
- }
1619
- function addGetter(cls, spec) {
1620
- if (cls.getGetAccessor(spec.name)) return false;
1621
- const ctor = cls.getConstructors()[0];
1622
- const index = ctor ? ctor.getChildIndex() + 1 : propertyInsertIndex(cls);
1623
- cls.insertGetAccessor(index, {
1624
- name: spec.name,
1625
- returnType: spec.returnType,
1626
- statements: spec.statements ?? "// TODO: derive and return the computed value"
1627
- });
1628
- return true;
1629
- }
1630
1733
  const LEGACY_IMPORTS = /* @__PURE__ */ new Set([
1631
1734
  "setupCompleteFutureSupport",
1632
1735
  "setupMinimalFutureSupport",
@@ -2433,6 +2536,12 @@ function addServiceRequestToModel(codegenFs, options, projects) {
2433
2536
  throw new Error(`Model file not found: ${modelFilePath}`);
2434
2537
  }
2435
2538
  let serviceImport = options.serviceModule;
2539
+ if (!serviceImport && options.serviceModulePath) {
2540
+ serviceImport = toImportSpecifier(
2541
+ modelFilePath,
2542
+ options.serviceModulePath.replace(/\.ts$/, "")
2543
+ );
2544
+ }
2436
2545
  if (!serviceImport) {
2437
2546
  if (!sourceRoot) {
2438
2547
  throw new Error(
@@ -2458,31 +2567,93 @@ function addServiceRequestToModel(codegenFs, options, projects) {
2458
2567
  serviceImport = specifiers[0];
2459
2568
  }
2460
2569
  const method = options.method || "get";
2570
+ const mode = options.mode ?? (options.lifecycle ? "lifecycle" : "method");
2461
2571
  const lifecycle = options.lifecycle || "LOAD";
2462
2572
  logger.info(
2463
- `Adding @kosServiceRequest "${options.methodName}" (${method} ${options.servicePath}) to ${options.modelName}`
2573
+ `Adding ${mode}-driven @kosServiceRequest "${options.methodName}" (${method} ${options.servicePath}) to ${options.modelName}`
2464
2574
  );
2465
2575
  transformSourceFile(codegenFs, modelFilePath, (sf) => {
2576
+ const endpointConst = endpointConstName(options.methodName);
2577
+ const className = getModelClass(sf).getName();
2578
+ if (!className) throw new Error("Model class has no name.");
2579
+ const typeParams = getModelClass(sf).getTypeParameters().map((tp) => tp.getText());
2580
+ ensureNamedImport(sf, serviceImport, [
2581
+ { name: "endpoint" },
2582
+ { name: "serviceRequest" }
2583
+ ]);
2584
+ ensureModuleConst(sf, {
2585
+ name: endpointConst,
2586
+ initializer: `endpoint(${JSON.stringify(
2587
+ options.servicePath
2588
+ )}, ${JSON.stringify(method)})`
2589
+ });
2590
+ if (mode === "lifecycle") {
2591
+ ensureNamedImport(sf, serviceImport, [
2592
+ { name: "EndpointResponse", isTypeOnly: true }
2593
+ ]);
2594
+ ensureNamedImport(sf, resolveSdkModuleSpecifier(sf), [
2595
+ { name: "DependencyLifecycle" }
2596
+ ]);
2597
+ addDecoratedMethod(getModelClass(sf), {
2598
+ name: options.methodName,
2599
+ decoratorName: "serviceRequest",
2600
+ decoratorArgsText: `${endpointConst}, { lifecycle: DependencyLifecycle.${lifecycle} }`,
2601
+ // The manager invokes phase handlers error-first, passing (null, data)
2602
+ // on success. The `error` half is only ever reached because
2603
+ // `serviceRequest` supplies an errorHandler — the bare decorator
2604
+ // defaults to `throw`, which fails the phase instead of calling back.
2605
+ parameters: [
2606
+ { name: "error", type: "string | null" },
2607
+ {
2608
+ name: "data",
2609
+ type: `EndpointResponse<typeof ${endpointConst}>`
2610
+ }
2611
+ ],
2612
+ returnType: "void",
2613
+ statements: "// TODO: apply `data` to the model"
2614
+ });
2615
+ return;
2616
+ }
2466
2617
  ensureNamedImport(sf, serviceImport, [
2467
- { name: "kosServiceRequest" }
2618
+ { name: "EndpointCtx", isTypeOnly: true }
2468
2619
  ]);
2469
2620
  ensureNamedImport(sf, resolveSdkModuleSpecifier(sf), [
2470
- { name: "DependencyLifecycle" }
2621
+ { name: "executeServiceRequest" },
2622
+ { name: "kosLoggerAware" },
2623
+ { name: "KosLoggerAware", isTypeOnly: true }
2471
2624
  ]);
2472
- const cls = getModelClass(sf);
2473
- addDecoratedMethod(cls, {
2625
+ ensureFileEslintDisable(
2626
+ sf,
2627
+ "@typescript-eslint/no-unsafe-declaration-merging"
2628
+ );
2629
+ ensureDeclarationMerge(sf, className, "KosLoggerAware", typeParams);
2630
+ addClassDecorator(getModelClass(sf), "kosLoggerAware", { argsText: "" });
2631
+ addDecoratedMethod(getModelClass(sf), {
2474
2632
  name: options.methodName,
2475
- decoratorName: "kosServiceRequest",
2476
- decoratorArgsText: `{ path: ${JSON.stringify(
2477
- options.servicePath
2478
- )}, method: ${JSON.stringify(
2479
- method
2480
- )}, lifecycle: DependencyLifecycle.${lifecycle} }`,
2481
- returnType: "void",
2482
- statements: "// TODO: handle the typed response"
2633
+ decoratorName: "serviceRequest",
2634
+ decoratorArgsText: endpointConst,
2635
+ // Optional: the framework appends the context, callers never pass it.
2636
+ parameters: [
2637
+ {
2638
+ name: "$ctx",
2639
+ type: `EndpointCtx<typeof ${endpointConst}>`,
2640
+ optional: true
2641
+ }
2642
+ ],
2643
+ isAsync: true,
2644
+ returnType: "Promise<void>",
2645
+ statements: [
2646
+ "const data = await executeServiceRequest(this, $ctx);",
2647
+ "if (!data) return;",
2648
+ "// TODO: apply `data` to the model"
2649
+ ].join("\n")
2483
2650
  });
2484
2651
  });
2485
- return { modelFilePath, serviceModule: serviceImport };
2652
+ return { modelFilePath, serviceModule: serviceImport, mode };
2653
+ }
2654
+ function endpointConstName(methodName) {
2655
+ const snake = methodName.replace(/([a-z0-9])([A-Z])/g, "$1_$2").replace(/[^A-Za-z0-9]+/g, "_").toUpperCase();
2656
+ return `ENDPOINT_${snake}`;
2486
2657
  }
2487
2658
  function modelElementType(typeText) {
2488
2659
  let m;
@@ -3384,6 +3555,8 @@ export {
3384
3555
  BasePluginHandler,
3385
3556
  CONTRIBUTION_TYPE_MAP,
3386
3557
  DirectFileSystem,
3558
+ KAB_OUTPUT_DIR,
3559
+ KAB_OUTPUT_PATH,
3387
3560
  KOS_JSON_PATHS,
3388
3561
  KosConfigBuilder,
3389
3562
  LOCALIZED_PLUGIN_TYPES,
@@ -3437,6 +3610,7 @@ export {
3437
3610
  getKosProjectConfiguration,
3438
3611
  getProject,
3439
3612
  getTemplateDir,
3613
+ listServiceCatalog,
3440
3614
  lookupSdkType,
3441
3615
  normalizeAllValues,
3442
3616
  normalizeOptions,
@@ -3444,6 +3618,7 @@ export {
3444
3618
  properCase,
3445
3619
  readJson,
3446
3620
  readNxJson,
3621
+ resolveKabPath,
3447
3622
  resolveModelFilePath,
3448
3623
  setCodegenLogger,
3449
3624
  syncCiManifests,