@kosdev-code/kos-codegen-core 0.1.0-next.790 → 0.1.0-next.792

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
@@ -955,6 +955,63 @@ function generateModel(params) {
955
955
  );
956
956
  }
957
957
  }
958
+ function resolveModelFilePath(codegenFs, query, projects) {
959
+ const kosConfig = getKosProjectConfiguration(
960
+ codegenFs,
961
+ query.modelProject,
962
+ projects
963
+ );
964
+ const internal = !!kosConfig?.generator?.internal;
965
+ const project = findProjectByName(
966
+ codegenFs.root,
967
+ query.modelProject,
968
+ projects
969
+ );
970
+ const sourceRoot = project ? project.sourceRoot || path.join(project.root, "src") : void 0;
971
+ if (query.modelPath) {
972
+ return { modelFilePath: query.modelPath, internal, sourceRoot };
973
+ }
974
+ if (!project) {
975
+ throw new Error(
976
+ `Project not found: ${query.modelProject}. Ensure a project.json exists.`
977
+ );
978
+ }
979
+ const { modelNameDashCase } = normalizeAllValues({
980
+ modelName: query.modelName
981
+ });
982
+ const modelLocation = kosConfig?.generator?.defaults?.model?.folder || "";
983
+ const modelFilePath = path.join(
984
+ sourceRoot,
985
+ modelLocation,
986
+ modelNameDashCase,
987
+ `${modelNameDashCase}-model.ts`
988
+ );
989
+ if (codegenFs.exists(modelFilePath)) {
990
+ return { modelFilePath, internal, sourceRoot };
991
+ }
992
+ const discovered = findModelFileByName(
993
+ codegenFs,
994
+ path.join(sourceRoot, modelLocation),
995
+ modelNameDashCase
996
+ );
997
+ if (discovered) {
998
+ return { modelFilePath: discovered, internal, sourceRoot };
999
+ }
1000
+ return { modelFilePath, internal, sourceRoot };
1001
+ }
1002
+ function findModelFileByName(codegenFs, searchRoot, modelNameDashCase) {
1003
+ const fileName = `${modelNameDashCase}-model.ts`;
1004
+ const candidates = codegenFs.listFiles(searchRoot).filter((f) => path.basename(f) === fileName).sort();
1005
+ if (candidates.length === 0) return null;
1006
+ if (candidates.length > 1) {
1007
+ throw new Error(
1008
+ `Model name '${modelNameDashCase}' is ambiguous — multiple files match ${fileName}:
1009
+ ` + candidates.map((c) => ` - ${c}`).join("\n") + `
1010
+ Pass modelPath to pick one.`
1011
+ );
1012
+ }
1013
+ return candidates[0];
1014
+ }
958
1015
  function normalizeAddFutureOptions(codegenFs, options, projects) {
959
1016
  const projectConfiguration = findProjectByName(
960
1017
  codegenFs.root,
@@ -983,9 +1040,12 @@ function normalizeAddFutureOptions(codegenFs, options, projects) {
983
1040
  const nameLowerCase = normalizedValues.modelNameLowerCase;
984
1041
  const projectRoot = projectConfiguration.root;
985
1042
  const sourceRoot = projectConfiguration.sourceRoot || path.join(projectRoot, "src");
986
- const modelLocation = kosConfig?.generator?.defaults?.model?.folder || "";
987
- const modelDirectory = path.join(sourceRoot, modelLocation, nameDashCase);
988
- const modelFilePath = path.join(modelDirectory, `${nameDashCase}-model.ts`);
1043
+ const { modelFilePath } = resolveModelFilePath(
1044
+ codegenFs,
1045
+ { modelName: options.modelName, modelProject: options.modelProject },
1046
+ projects
1047
+ );
1048
+ const modelDirectory = path.dirname(modelFilePath);
989
1049
  const servicesDirectory = path.join(modelDirectory, "services");
990
1050
  const servicesFilePath = codegenFs.exists(servicesDirectory) ? path.join(servicesDirectory, `${nameDashCase}-services.ts`) : void 0;
991
1051
  const registrationFilePath = path.join(
@@ -1008,6 +1068,173 @@ function normalizeAddFutureOptions(codegenFs, options, projects) {
1008
1068
  internal
1009
1069
  };
1010
1070
  }
1071
+ function transformSourceFile(codegenFs, filePath, mutate) {
1072
+ const content = codegenFs.read(filePath);
1073
+ if (content === null) {
1074
+ throw new Error(`File not found: ${filePath}`);
1075
+ }
1076
+ const project = new Project({
1077
+ useInMemoryFileSystem: true,
1078
+ manipulationSettings: {
1079
+ indentationText: IndentationText.TwoSpaces,
1080
+ quoteKind: QuoteKind.Double
1081
+ }
1082
+ });
1083
+ const sourceFile = project.createSourceFile(filePath, content, {
1084
+ overwrite: true
1085
+ });
1086
+ mutate(sourceFile);
1087
+ codegenFs.write(filePath, sourceFile.getFullText());
1088
+ }
1089
+ function ensureNamedImport(sourceFile, moduleSpecifier, names) {
1090
+ const decls = sourceFile.getImportDeclarations().filter((d) => d.getModuleSpecifierValue() === moduleSpecifier);
1091
+ const existing = /* @__PURE__ */ new Set();
1092
+ for (const d of decls) {
1093
+ for (const n of d.getNamedImports()) existing.add(n.getName());
1094
+ }
1095
+ let target = decls.find((d) => !d.isTypeOnly());
1096
+ if (!target) {
1097
+ target = sourceFile.addImportDeclaration({ moduleSpecifier });
1098
+ }
1099
+ for (const { name, isTypeOnly } of names) {
1100
+ if (existing.has(name)) continue;
1101
+ target.addNamedImport({ name, isTypeOnly: !!isTypeOnly });
1102
+ existing.add(name);
1103
+ }
1104
+ }
1105
+ function resolveSdkModuleSpecifier(sourceFile) {
1106
+ const decl = sourceFile.getImportDeclarations().find((d) => d.getNamedImports().some((n) => n.getName() === "kosModel"));
1107
+ return decl?.getModuleSpecifierValue() ?? "@kosdev-code/kos-ui-sdk";
1108
+ }
1109
+ function getModelClass(sourceFile, preferName) {
1110
+ const classes = sourceFile.getClasses();
1111
+ const byName = preferName ? classes.find((c) => c.getName() === preferName) : void 0;
1112
+ if (byName) return byName;
1113
+ const impl = classes.find((c) => /ModelImpl$/.test(c.getName() ?? ""));
1114
+ if (impl) return impl;
1115
+ const exported = classes.find((c) => c.isExported());
1116
+ if (exported) return exported;
1117
+ if (classes.length > 0) return classes[0];
1118
+ throw new Error("No class declaration found in model file.");
1119
+ }
1120
+ function addClassDecorator(cls, name, opts) {
1121
+ if (cls.getDecorator(name)) return;
1122
+ const typeArgs = opts?.typeArgs?.length ? `<${opts.typeArgs.join(", ")}>` : "";
1123
+ const args = opts?.argsText ?? "";
1124
+ const decorators = cls.getDecorators();
1125
+ const kosModelIdx = decorators.findIndex((d) => d.getName() === "kosModel");
1126
+ const insertIdx = kosModelIdx >= 0 ? kosModelIdx + 1 : decorators.length;
1127
+ cls.insertDecorator(insertIdx, {
1128
+ name: `${name}${typeArgs}`,
1129
+ arguments: args ? [args] : []
1130
+ });
1131
+ }
1132
+ function ensureDeclarationMerge(sourceFile, interfaceName, extendsExpr, typeParameters = []) {
1133
+ const baseType = extendsExpr.split("<")[0].trim();
1134
+ let iface = sourceFile.getInterface(interfaceName);
1135
+ if (iface) {
1136
+ const already = iface.getExtends().some((e) => e.getText().split("<")[0].trim() === baseType);
1137
+ if (!already) iface.addExtends(extendsExpr);
1138
+ return;
1139
+ }
1140
+ iface = sourceFile.addInterface({
1141
+ name: interfaceName,
1142
+ isExported: true,
1143
+ typeParameters,
1144
+ extends: [extendsExpr]
1145
+ });
1146
+ sourceFile.insertText(
1147
+ iface.getStart(),
1148
+ "// eslint-disable-next-line @typescript-eslint/no-empty-interface\n"
1149
+ );
1150
+ }
1151
+ function ensureFileEslintDisable(sourceFile, rule) {
1152
+ if (sourceFile.getFullText().includes(rule)) return;
1153
+ sourceFile.insertText(0, `/* eslint-disable ${rule} */
1154
+ `);
1155
+ }
1156
+ function addDecoratedMethod(cls, spec) {
1157
+ if (cls.getMethod(spec.name)) return false;
1158
+ cls.addMethod({
1159
+ name: spec.name,
1160
+ isAsync: spec.isAsync,
1161
+ returnType: spec.returnType,
1162
+ parameters: spec.parameters?.map((p) => {
1163
+ return { name: p.name, type: p.type };
1164
+ }),
1165
+ statements: spec.statements,
1166
+ decorators: [
1167
+ {
1168
+ name: spec.decoratorName,
1169
+ arguments: spec.decoratorArgsText ? [spec.decoratorArgsText] : []
1170
+ }
1171
+ ]
1172
+ });
1173
+ return true;
1174
+ }
1175
+ function propertyInsertIndex(cls) {
1176
+ const props = cls.getProperties();
1177
+ if (props.length === 0) return 0;
1178
+ return props[props.length - 1].getChildIndex() + 1;
1179
+ }
1180
+ function addPlainProperty(cls, spec) {
1181
+ if (cls.getProperty(spec.name)) return false;
1182
+ cls.insertProperty(propertyInsertIndex(cls), {
1183
+ name: spec.name,
1184
+ type: spec.type,
1185
+ initializer: spec.initializer,
1186
+ isReadonly: !!spec.readonly,
1187
+ scope: spec.scope ? SCOPE_MAP[spec.scope] : void 0,
1188
+ hasExclamationToken: spec.initializer ? false : !!spec.hasExclamation
1189
+ });
1190
+ return true;
1191
+ }
1192
+ const SCOPE_MAP = {
1193
+ private: Scope.Private,
1194
+ protected: Scope.Protected,
1195
+ public: Scope.Public
1196
+ };
1197
+ function addDecoratedProperty(cls, spec) {
1198
+ if (cls.getProperty(spec.name)) return false;
1199
+ cls.insertProperty(propertyInsertIndex(cls), {
1200
+ name: spec.name,
1201
+ type: spec.type,
1202
+ initializer: spec.initializer,
1203
+ scope: spec.scope ? SCOPE_MAP[spec.scope] : void 0,
1204
+ hasExclamationToken: spec.initializer ? false : !!spec.hasExclamation,
1205
+ decorators: [
1206
+ spec.bare && !spec.decoratorArgsText ? { name: spec.decoratorName } : {
1207
+ name: spec.decoratorName,
1208
+ arguments: spec.decoratorArgsText ? [spec.decoratorArgsText] : []
1209
+ }
1210
+ ]
1211
+ });
1212
+ return true;
1213
+ }
1214
+ function addGetter(cls, spec) {
1215
+ if (cls.getGetAccessor(spec.name)) return false;
1216
+ const ctor = cls.getConstructors()[0];
1217
+ const index = ctor ? ctor.getChildIndex() + 1 : propertyInsertIndex(cls);
1218
+ cls.insertGetAccessor(index, {
1219
+ name: spec.name,
1220
+ returnType: spec.returnType,
1221
+ statements: spec.statements ?? "// TODO: derive and return the computed value"
1222
+ });
1223
+ return true;
1224
+ }
1225
+ const LEGACY_IMPORTS = /* @__PURE__ */ new Set([
1226
+ "setupCompleteFutureSupport",
1227
+ "setupMinimalFutureSupport",
1228
+ "FutureAwareContainer",
1229
+ "FutureHandlerContainer",
1230
+ "FutureStateAccessor",
1231
+ "FutureUpdateHandler"
1232
+ ]);
1233
+ const LEGACY_IMPLEMENTS = /* @__PURE__ */ new Set([
1234
+ "FutureUpdateHandler",
1235
+ "FutureHandlerContainer",
1236
+ "FutureStateAccessor"
1237
+ ]);
1011
1238
  class ModelFileTransformer {
1012
1239
  constructor(codegenFs, options) {
1013
1240
  this.codegenFs = codegenFs;
@@ -1015,292 +1242,173 @@ class ModelFileTransformer {
1015
1242
  }
1016
1243
  codegenFs;
1017
1244
  options;
1245
+ get progressType() {
1246
+ const { nameProperCase, updateServices } = this.options;
1247
+ return updateServices ? `${nameProperCase}OperationProgress` : "Record<string, unknown>";
1248
+ }
1018
1249
  transform() {
1019
1250
  const { modelFilePath } = this.options;
1020
1251
  if (!this.codegenFs.exists(modelFilePath)) {
1021
1252
  throw new Error(`Model file not found: ${modelFilePath}`);
1022
1253
  }
1023
- let content = this.codegenFs.read(modelFilePath);
1024
- content = this.addESLintDisable(content);
1025
- content = this.addImports(content);
1026
- content = this.addServiceImport(content);
1027
- content = this.addInterfaceMerging(content);
1028
- content = this.addDecorator(content);
1029
- content = this.updatePublicType(content);
1030
- content = this.removeLegacySetup(content);
1031
- content = this.addFutureMethod(content);
1032
- if (this.options.futureType === "complete") {
1033
- content = this.addOnFutureUpdateMethod(content);
1034
- }
1035
- this.codegenFs.write(modelFilePath, content);
1036
- }
1037
- addESLintDisable(content) {
1038
- if (content.includes("@typescript-eslint/no-unsafe-declaration-merging")) {
1039
- return content;
1040
- }
1041
- const eslintDisable = "/* eslint-disable @typescript-eslint/no-unsafe-declaration-merging */\n";
1042
- return eslintDisable + content;
1043
- }
1044
- addImports(content) {
1045
- const { internal, futureType } = this.options;
1046
- const isComplete = futureType === "complete";
1047
- const kosModelImportRegex = internal ? /import\s*{\s*([^}]*kosModel[^}]*)\s*}\s*from\s*"\.\.\/\.\.\/\.\.\/core\/core\/decorators"/ : /import\s*{\s*([^}]*kosModel[^}]*)\s*}\s*from\s*"@kosdev-code\/kos-ui-sdk"/;
1048
- const kosModelMatch = content.match(kosModelImportRegex);
1049
- if (kosModelMatch) {
1050
- const existingImportsStr = kosModelMatch[1] || "";
1051
- const newImports = [
1052
- "kosFuture",
1053
- "kosFutureAware",
1054
- isComplete ? "KosFutureAwareFull" : "KosFutureAwareMinimal"
1055
- ];
1056
- const existingImports = existingImportsStr.split(",").map((s) => s.trim()).filter(Boolean);
1057
- const importsToRemove = [
1058
- "setupCompleteFutureSupport",
1059
- "setupMinimalFutureSupport"
1060
- ];
1061
- const cleanedImports = existingImports.filter(
1062
- (imp) => !importsToRemove.includes(imp)
1063
- );
1064
- const importsToAdd = newImports.filter(
1065
- (imp) => !cleanedImports.includes(imp)
1066
- );
1067
- const allImports = [...cleanedImports, ...importsToAdd];
1068
- const newImportLine = internal ? `import { ${allImports.join(
1069
- ", "
1070
- )} } from "../../../core/core/decorators"` : `import { ${allImports.join(", ")} } from "@kosdev-code/kos-ui-sdk"`;
1071
- content = content.replace(kosModelImportRegex, newImportLine);
1072
- }
1073
- const typeImportsBase = internal ? "../../../models/types/future-interfaces" : "@kosdev-code/kos-ui-sdk";
1074
- const futureTypeImports = ["ExternalFutureInterface", "IFutureModel"];
1075
- const typeImportRegex = internal ? /import type {([^}]*)} from "\.\.\/\.\.\/\.\.\/models\/types\/future-interfaces"/ : /import type {([^}]*)} from "@kosdev-code\/kos-ui-sdk"/;
1076
- const typeImportMatch = content.match(typeImportRegex);
1077
- if (typeImportMatch) {
1078
- const existingTypes = typeImportMatch[1] || "";
1079
- const existingTypesList = existingTypes.split(",").map((s) => s.trim()).filter(Boolean);
1080
- const typesToRemove = [
1081
- "FutureAwareContainer",
1082
- "FutureHandlerContainer",
1083
- "FutureStateAccessor",
1084
- "FutureUpdateHandler"
1085
- ];
1086
- const cleanedTypes = existingTypesList.filter(
1087
- (type) => !typesToRemove.includes(type)
1088
- );
1089
- const typesToAdd = futureTypeImports.filter(
1090
- (type) => !cleanedTypes.includes(type)
1254
+ transformSourceFile(this.codegenFs, modelFilePath, (sf) => {
1255
+ this.removeLegacyImports(sf);
1256
+ this.addImports(sf);
1257
+ const cls = getModelClass(sf, `${this.options.nameProperCase}ModelImpl`);
1258
+ this.removeLegacyClassMembers(cls);
1259
+ this.addDecorator(cls);
1260
+ this.addFutureMethod(cls);
1261
+ if (this.options.futureType === "complete") {
1262
+ this.addOnFutureUpdateMethod(cls);
1263
+ }
1264
+ this.updatePublicType(sf);
1265
+ ensureDeclarationMerge(
1266
+ sf,
1267
+ `${this.options.nameProperCase}ModelImpl`,
1268
+ `${this.options.futureType === "complete" ? "KosFutureAwareFull" : "KosFutureAwareMinimal"}<${this.progressType}>`
1091
1269
  );
1092
- const allTypes = [...cleanedTypes, ...typesToAdd].join(", ");
1093
- const newTypeImport = `import type { ${allTypes} } from "${typeImportsBase}"`;
1094
- content = content.replace(typeImportRegex, newTypeImport);
1095
- } else {
1096
- const importLines = content.split("\n");
1097
- const lastImportIndex = importLines.findLastIndex(
1098
- (line) => line.trim().startsWith("import")
1270
+ ensureFileEslintDisable(
1271
+ sf,
1272
+ "@typescript-eslint/no-unsafe-declaration-merging"
1099
1273
  );
1100
- if (lastImportIndex >= 0) {
1101
- const newTypeImport = `import type { ${futureTypeImports.join(
1102
- ", "
1103
- )} } from "${typeImportsBase}";`;
1104
- importLines.splice(lastImportIndex + 1, 0, newTypeImport);
1105
- content = importLines.join("\n");
1106
- }
1107
- }
1108
- return content;
1274
+ });
1109
1275
  }
1110
- addServiceImport(content) {
1111
- const { nameProperCase, updateServices } = this.options;
1112
- if (!updateServices) {
1113
- return content;
1114
- }
1115
- if (content.includes(`${nameProperCase}OperationProgress`)) {
1116
- return content;
1117
- }
1118
- const servicesImportRegex = /import\s*{([^}]*)}\s*from\s*["']\.\/services["'];?/s;
1119
- const servicesMatch = content.match(servicesImportRegex);
1120
- if (servicesMatch) {
1121
- const existingImports = servicesMatch[1];
1122
- const cleanedImports = existingImports.split(",").map((s) => s.trim()).filter(Boolean);
1123
- cleanedImports.push(`${nameProperCase}OperationProgress`);
1124
- const newImport = `import { ${cleanedImports.join(
1125
- ", "
1126
- )} } from "./services";`;
1127
- content = content.replace(servicesImportRegex, newImport);
1128
- } else {
1129
- const typesImportRegex = /import\s+type\s+{[^}]*}\s+from\s+["']\.\/types["'];?/;
1130
- const typesMatch = content.match(typesImportRegex);
1131
- if (typesMatch) {
1132
- const newImport = `
1133
- import type { ${nameProperCase}OperationProgress } from "./services";`;
1134
- content = content.replace(typesMatch[0], typesMatch[0] + newImport);
1276
+ removeLegacyImports(sf) {
1277
+ for (const decl of sf.getImportDeclarations()) {
1278
+ for (const named of decl.getNamedImports()) {
1279
+ if (LEGACY_IMPORTS.has(named.getName())) named.remove();
1135
1280
  }
1136
- }
1137
- return content;
1138
- }
1139
- addInterfaceMerging(content) {
1140
- const { nameProperCase, futureType } = this.options;
1141
- const isComplete = futureType === "complete";
1142
- const interfaceType = isComplete ? "KosFutureAwareFull" : "KosFutureAwareMinimal";
1143
- const progressType = this.options.updateServices ? `${nameProperCase}OperationProgress` : "Record<string, unknown>";
1144
- const classRegex = new RegExp(
1145
- `(@kosModel[^\\n]*\\n)([^\\n]*export\\s+class\\s+${nameProperCase}ModelImpl)`,
1146
- "m"
1147
- );
1148
- const classMatch = content.match(classRegex);
1149
- if (classMatch) {
1150
- const interfaceRegex = new RegExp(
1151
- `interface\\s+${nameProperCase}ModelImpl\\s+extends`
1152
- );
1153
- if (!content.match(interfaceRegex)) {
1154
- const interfaceMerging = `
1155
- // Interface merging for Future Container type safety
1156
- // eslint-disable-next-line @typescript-eslint/no-empty-interface
1157
- export interface ${nameProperCase}ModelImpl extends ${interfaceType}<${progressType}> {}
1158
-
1159
- `;
1160
- content = content.replace(
1161
- classMatch[0],
1162
- interfaceMerging + classMatch[0]
1163
- );
1281
+ if (decl.getNamedImports().length === 0 && !decl.getDefaultImport() && !decl.getNamespaceImport()) {
1282
+ decl.remove();
1164
1283
  }
1165
1284
  }
1166
- return content;
1167
1285
  }
1168
- addDecorator(content) {
1169
- const { nameProperCase, futureType } = this.options;
1286
+ addImports(sf) {
1287
+ const { internal, futureType, updateServices, nameProperCase } = this.options;
1170
1288
  const isComplete = futureType === "complete";
1171
- const classRegex = new RegExp(
1172
- `(@kosModel[^\\n]*\\n)((?:@[^\\n]*\\n)*)([^\\n]*export\\s+class\\s+${nameProperCase}ModelImpl)`,
1173
- "m"
1174
- );
1175
- const classMatch = content.match(classRegex);
1176
- if (classMatch) {
1177
- if (!classMatch[2].includes("@kosFutureAware")) {
1178
- const decoratorOptions = isComplete ? "" : "{ mode: 'minimal' }";
1179
- const futureDecorator = `@kosFutureAware(${decoratorOptions})
1180
- `;
1181
- content = content.replace(
1182
- classMatch[0],
1183
- classMatch[1] + classMatch[2] + futureDecorator + classMatch[3]
1184
- );
1289
+ const sdkSpec = resolveSdkModuleSpecifier(sf);
1290
+ ensureNamedImport(sf, sdkSpec, [
1291
+ { name: "kosFuture" },
1292
+ { name: "kosFutureAware" },
1293
+ {
1294
+ name: isComplete ? "KosFutureAwareFull" : "KosFutureAwareMinimal",
1295
+ isTypeOnly: true
1185
1296
  }
1297
+ ]);
1298
+ const typeSpec = internal ? "../../../models/types/future-interfaces" : sdkSpec;
1299
+ ensureNamedImport(sf, typeSpec, [
1300
+ { name: "ExternalFutureInterface", isTypeOnly: true },
1301
+ ...isComplete ? [{ name: "IFutureModel", isTypeOnly: true }] : []
1302
+ ]);
1303
+ if (updateServices) {
1304
+ ensureNamedImport(sf, "./services", [
1305
+ { name: `${nameProperCase}OperationProgress`, isTypeOnly: true }
1306
+ ]);
1186
1307
  }
1187
- return content;
1188
1308
  }
1189
- updatePublicType(content) {
1190
- const { nameProperCase, updateServices } = this.options;
1191
- const progressType = updateServices ? `${nameProperCase}OperationProgress` : "Record<string, unknown>";
1192
- const typeRegex = new RegExp(
1193
- `export\\s+type\\s+${nameProperCase}Model\\s*=\\s*PublicModelInterface<${nameProperCase}ModelImpl>([^;]*);`,
1194
- "s"
1195
- );
1196
- const typeMatch = content.match(typeRegex);
1197
- if (typeMatch) {
1198
- if (!typeMatch[1].includes("ExternalFutureInterface")) {
1199
- const newType = `export type ${nameProperCase}Model = PublicModelInterface<${nameProperCase}ModelImpl> & ExternalFutureInterface<${progressType}>;`;
1200
- content = content.replace(typeMatch[0], newType);
1309
+ removeLegacyClassMembers(cls) {
1310
+ for (const ctor of cls.getConstructors()) {
1311
+ for (const stmt of ctor.getStatements()) {
1312
+ if (/setup(Complete|Minimal)FutureSupport\s*\(\s*this\s*\)/.test(
1313
+ stmt.getText()
1314
+ )) {
1315
+ stmt.remove();
1316
+ }
1201
1317
  }
1202
1318
  }
1203
- return content;
1319
+ const futureHandler = cls.getProperty("futureHandler");
1320
+ if (futureHandler?.getTypeNode()?.getText().includes("FutureAwareContainer")) {
1321
+ futureHandler.remove();
1322
+ }
1323
+ const future = cls.getProperty("future");
1324
+ if (future?.getTypeNode()?.getText().includes("IFutureModel")) {
1325
+ future.remove();
1326
+ }
1327
+ const impls = cls.getImplements();
1328
+ for (let i = impls.length - 1; i >= 0; i--) {
1329
+ const base = impls[i].getText().split("<")[0].trim();
1330
+ if (LEGACY_IMPLEMENTS.has(base)) cls.removeImplements(i);
1331
+ }
1204
1332
  }
1205
- removeLegacySetup(content) {
1206
- const setupRegex = /\s*setup(Complete|Minimal)FutureSupport\(this\);?\s*/g;
1207
- content = content.replace(setupRegex, "");
1208
- const propertyRegex = /\s*(public|private|protected)?\s*(declare\s+)?futureHandler[!?]?:\s*FutureAwareContainer[^;]*;\s*/g;
1209
- content = content.replace(propertyRegex, "");
1210
- const futurePropertyRegex = /\s*(public|private|protected)?\s*(declare\s+)?future\??:\s*IFutureModel[^;]*;\s*/g;
1211
- content = content.replace(futurePropertyRegex, "");
1212
- const implementsRegex = new RegExp(
1213
- `(implements\\s+[^{]*?)\\s*,?\\s*(FutureUpdateHandler|FutureHandlerContainer|FutureStateAccessor)`,
1214
- "g"
1215
- );
1216
- content = content.replace(implementsRegex, "$1");
1217
- content = content.replace(/,\s*,/g, ",");
1218
- content = content.replace(/implements\s*,/g, "implements");
1219
- return content;
1333
+ addDecorator(cls) {
1334
+ const argsText = this.options.futureType === "complete" ? "" : `{ mode: "minimal" }`;
1335
+ addClassDecorator(cls, "kosFutureAware", { argsText });
1220
1336
  }
1221
- addFutureMethod(content) {
1337
+ updatePublicType(sf) {
1222
1338
  const { nameProperCase } = this.options;
1223
- if (content.includes("@kosFuture()")) {
1224
- return content;
1339
+ const alias = sf.getTypeAlias(`${nameProperCase}Model`);
1340
+ if (!alias) return;
1341
+ const text = alias.getTypeNode()?.getText() ?? "";
1342
+ if (!text.includes(`PublicModelInterface<${nameProperCase}ModelImpl>`) || text.includes("ExternalFutureInterface")) {
1343
+ return;
1225
1344
  }
1226
- const classRegex = new RegExp(
1227
- `class\\s+${nameProperCase}ModelImpl[^{]*{([\\s\\S]*)}\\s*$`,
1228
- "m"
1345
+ alias.setType(
1346
+ `PublicModelInterface<${nameProperCase}ModelImpl> & ExternalFutureInterface<${this.progressType}>`
1229
1347
  );
1230
- const classMatch = content.match(classRegex);
1231
- if (classMatch) {
1232
- const methodCode = `
1233
- /**
1234
- * Placeholder method for Future operations
1235
- * Replace this with your actual long-running operation
1236
- */
1237
- @kosFuture()
1238
- async performLongRunningOperation(): Promise<void> {
1239
- // TODO: Implement your long-running operation here
1240
- // This method should use a service that returns a Future for progress tracking
1241
-
1242
- this.logger.debug(\`Starting long-running operation for \${this.id}\`);
1243
-
1244
- // Example implementation pattern using services:
1245
- // import { perform${nameProperCase}Operation } from './services';
1246
- //
1247
- // const future = await perform${nameProperCase}Operation();
1248
- // return this.futureHandler.setFuture(future);
1249
-
1250
- // Placeholder that doesn't actually do anything
1251
- await new Promise(resolve => setTimeout(resolve, 1000));
1252
-
1253
- this.logger.debug(\`Completed long-running operation for \${this.id}\`);
1254
1348
  }
1255
- `;
1256
- const classContent = classMatch[1];
1257
- const lastBraceIndex = classContent.lastIndexOf("}");
1258
- if (lastBraceIndex >= 0) {
1259
- const updatedContent = classContent.slice(0, lastBraceIndex) + methodCode + classContent.slice(lastBraceIndex);
1260
- content = content.replace(
1261
- classMatch[0],
1262
- `class ${nameProperCase}ModelImpl${classMatch[0].match(/[^{]*/)?.[0]}{${updatedContent}}`
1263
- );
1264
- }
1265
- }
1266
- return content;
1349
+ addFutureMethod(cls) {
1350
+ const { nameProperCase } = this.options;
1351
+ const hasFutureMethod = cls.getMethods().some((m) => m.getDecorator("kosFuture"));
1352
+ if (hasFutureMethod || cls.getMethod("performLongRunningOperation")) return;
1353
+ cls.addMethod({
1354
+ name: "performLongRunningOperation",
1355
+ isAsync: true,
1356
+ returnType: "Promise<void>",
1357
+ decorators: [{ name: "kosFuture", arguments: [] }],
1358
+ docs: [
1359
+ {
1360
+ description: "Placeholder method for Future operations\nReplace this with your actual long-running operation"
1361
+ }
1362
+ ],
1363
+ statements: [
1364
+ "// TODO: Implement your long-running operation here",
1365
+ "// This method should use a service that returns a Future for progress tracking",
1366
+ "",
1367
+ "this.logger.debug(`Starting long-running operation for ${this.id}`);",
1368
+ "",
1369
+ "// Example implementation pattern using services:",
1370
+ `// import { perform${nameProperCase}Operation } from './services';`,
1371
+ "//",
1372
+ `// const future = await perform${nameProperCase}Operation();`,
1373
+ "// return this.futureHandler.setFuture(future);",
1374
+ "",
1375
+ "// Placeholder that doesn't actually do anything",
1376
+ "await new Promise((resolve) => setTimeout(resolve, 1000));",
1377
+ "",
1378
+ "this.logger.debug(`Completed long-running operation for ${this.id}`);"
1379
+ ]
1380
+ });
1267
1381
  }
1268
- addOnFutureUpdateMethod(content) {
1269
- const { nameProperCase, updateServices } = this.options;
1270
- const progressType = updateServices ? `${nameProperCase}OperationProgress` : "Record<string, unknown>";
1271
- if (content.includes("onFutureUpdate")) {
1272
- return content;
1273
- }
1274
- const futureMethodRegex = /@kosFuture\(\)[^}]*}/s;
1275
- const futureMethodMatch = content.match(futureMethodRegex);
1276
- if (futureMethodMatch) {
1277
- const methodCode = `
1278
-
1279
- /**
1280
- * Optional: Custom Future update handling
1281
- * Called whenever the Future state changes (progress, status, completion, etc.)
1282
- */
1283
- onFutureUpdate?(update: IFutureModel<${progressType}>): void {
1284
- // Add custom Future update logic here
1285
- // Examples:
1286
- // - Log progress milestones
1287
- // - Update derived state based on progress
1288
- // - Handle specific error conditions
1289
- // - Trigger notifications at certain thresholds
1290
-
1291
- this.logger.debug(\`Future update for \${this.id}:\`, {
1292
- progress: update.progress,
1293
- status: update.status,
1294
- endState: update.endState,
1295
- clientData: update.clientData
1382
+ addOnFutureUpdateMethod(cls) {
1383
+ if (cls.getMethod("onFutureUpdate")) return;
1384
+ cls.addMethod({
1385
+ name: "onFutureUpdate",
1386
+ hasQuestionToken: true,
1387
+ returnType: "void",
1388
+ parameters: [
1389
+ { name: "update", type: `IFutureModel<${this.progressType}>` }
1390
+ ],
1391
+ docs: [
1392
+ {
1393
+ description: "Optional: Custom Future update handling\nCalled whenever the Future state changes (progress, status, completion, etc.)"
1394
+ }
1395
+ ],
1396
+ statements: [
1397
+ "// Add custom Future update logic here",
1398
+ "// Examples:",
1399
+ "// - Log progress milestones",
1400
+ "// - Update derived state based on progress",
1401
+ "// - Handle specific error conditions",
1402
+ "// - Trigger notifications at certain thresholds",
1403
+ "",
1404
+ "this.logger.debug(`Future update for ${this.id}:`, {",
1405
+ " progress: update.progress,",
1406
+ " status: update.status,",
1407
+ " endState: update.endState,",
1408
+ " clientData: update.clientData,",
1409
+ "});"
1410
+ ]
1296
1411
  });
1297
- }`;
1298
- content = content.replace(
1299
- futureMethodMatch[0],
1300
- futureMethodMatch[0] + methodCode
1301
- );
1302
- }
1303
- return content;
1304
1412
  }
1305
1413
  }
1306
1414
  class ServiceFileTransformer {
@@ -1483,9 +1591,16 @@ class RegistrationFileTransformer {
1483
1591
  logger.warn("Registration file not found, skipping registration updates");
1484
1592
  return;
1485
1593
  }
1486
- let content = this.codegenFs.read(registrationFilePath);
1594
+ const original = this.codegenFs.read(registrationFilePath);
1595
+ let content = original;
1487
1596
  content = this.addTypeCast(content);
1488
1597
  content = this.updateDocumentation(content);
1598
+ if (content === original) {
1599
+ logger.info(
1600
+ "Registration file has no legacy future patterns — leaving it untouched"
1601
+ );
1602
+ return;
1603
+ }
1489
1604
  this.codegenFs.write(registrationFilePath, content);
1490
1605
  }
1491
1606
  addTypeCast(content) {
@@ -1553,7 +1668,7 @@ function addFutureToModel(codegenFs, options, projects) {
1553
1668
  }
1554
1669
  if (normalized.registrationFilePath) {
1555
1670
  logger.info(
1556
- `Would modify registration file: ${normalized.registrationFilePath}`
1671
+ `Would update registration file (only if legacy patterns are present): ${normalized.registrationFilePath}`
1557
1672
  );
1558
1673
  }
1559
1674
  return;
@@ -1604,191 +1719,6 @@ function addFutureToModel(codegenFs, options, projects) {
1604
1719
  throw error;
1605
1720
  }
1606
1721
  }
1607
- function resolveModelFilePath(codegenFs, query, projects) {
1608
- const kosConfig = getKosProjectConfiguration(
1609
- codegenFs,
1610
- query.modelProject,
1611
- projects
1612
- );
1613
- const internal = !!kosConfig?.generator?.internal;
1614
- const project = findProjectByName(
1615
- codegenFs.root,
1616
- query.modelProject,
1617
- projects
1618
- );
1619
- const sourceRoot = project ? project.sourceRoot || path.join(project.root, "src") : void 0;
1620
- if (query.modelPath) {
1621
- return { modelFilePath: query.modelPath, internal, sourceRoot };
1622
- }
1623
- if (!project) {
1624
- throw new Error(
1625
- `Project not found: ${query.modelProject}. Ensure a project.json exists.`
1626
- );
1627
- }
1628
- const { modelNameDashCase } = normalizeAllValues({
1629
- modelName: query.modelName
1630
- });
1631
- const modelLocation = kosConfig?.generator?.defaults?.model?.folder || "";
1632
- const modelFilePath = path.join(
1633
- sourceRoot,
1634
- modelLocation,
1635
- modelNameDashCase,
1636
- `${modelNameDashCase}-model.ts`
1637
- );
1638
- return { modelFilePath, internal, sourceRoot };
1639
- }
1640
- function transformSourceFile(codegenFs, filePath, mutate) {
1641
- const content = codegenFs.read(filePath);
1642
- if (content === null) {
1643
- throw new Error(`File not found: ${filePath}`);
1644
- }
1645
- const project = new Project({
1646
- useInMemoryFileSystem: true,
1647
- manipulationSettings: {
1648
- indentationText: IndentationText.TwoSpaces,
1649
- quoteKind: QuoteKind.Double
1650
- }
1651
- });
1652
- const sourceFile = project.createSourceFile(filePath, content, {
1653
- overwrite: true
1654
- });
1655
- mutate(sourceFile);
1656
- codegenFs.write(filePath, sourceFile.getFullText());
1657
- }
1658
- function ensureNamedImport(sourceFile, moduleSpecifier, names) {
1659
- const decls = sourceFile.getImportDeclarations().filter((d) => d.getModuleSpecifierValue() === moduleSpecifier);
1660
- const existing = /* @__PURE__ */ new Set();
1661
- for (const d of decls) {
1662
- for (const n of d.getNamedImports()) existing.add(n.getName());
1663
- }
1664
- let target = decls.find((d) => !d.isTypeOnly());
1665
- if (!target) {
1666
- target = sourceFile.addImportDeclaration({ moduleSpecifier });
1667
- }
1668
- for (const { name, isTypeOnly } of names) {
1669
- if (existing.has(name)) continue;
1670
- target.addNamedImport({ name, isTypeOnly: !!isTypeOnly });
1671
- existing.add(name);
1672
- }
1673
- }
1674
- function resolveSdkModuleSpecifier(sourceFile) {
1675
- const decl = sourceFile.getImportDeclarations().find((d) => d.getNamedImports().some((n) => n.getName() === "kosModel"));
1676
- return decl?.getModuleSpecifierValue() ?? "@kosdev-code/kos-ui-sdk";
1677
- }
1678
- function getModelClass(sourceFile, preferName) {
1679
- const classes = sourceFile.getClasses();
1680
- const impl = classes.find((c) => /ModelImpl$/.test(c.getName() ?? ""));
1681
- if (impl) return impl;
1682
- const exported = classes.find((c) => c.isExported());
1683
- if (exported) return exported;
1684
- if (classes.length > 0) return classes[0];
1685
- throw new Error("No class declaration found in model file.");
1686
- }
1687
- function addClassDecorator(cls, name, opts) {
1688
- if (cls.getDecorator(name)) return;
1689
- const typeArgs = opts?.typeArgs?.length ? `<${opts.typeArgs.join(", ")}>` : "";
1690
- const args = opts?.argsText ?? "";
1691
- const decorators = cls.getDecorators();
1692
- const kosModelIdx = decorators.findIndex((d) => d.getName() === "kosModel");
1693
- const insertIdx = kosModelIdx >= 0 ? kosModelIdx + 1 : decorators.length;
1694
- cls.insertDecorator(insertIdx, {
1695
- name: `${name}${typeArgs}`,
1696
- arguments: args ? [args] : []
1697
- });
1698
- }
1699
- function ensureDeclarationMerge(sourceFile, interfaceName, extendsExpr, typeParameters = []) {
1700
- const baseType = extendsExpr.split("<")[0].trim();
1701
- let iface = sourceFile.getInterface(interfaceName);
1702
- if (iface) {
1703
- const already = iface.getExtends().some((e) => e.getText().split("<")[0].trim() === baseType);
1704
- if (!already) iface.addExtends(extendsExpr);
1705
- return;
1706
- }
1707
- iface = sourceFile.addInterface({
1708
- name: interfaceName,
1709
- isExported: true,
1710
- typeParameters,
1711
- extends: [extendsExpr]
1712
- });
1713
- sourceFile.insertText(
1714
- iface.getStart(),
1715
- "// eslint-disable-next-line @typescript-eslint/no-empty-interface\n"
1716
- );
1717
- }
1718
- function ensureFileEslintDisable(sourceFile, rule) {
1719
- if (sourceFile.getFullText().includes(rule)) return;
1720
- sourceFile.insertText(0, `/* eslint-disable ${rule} */
1721
- `);
1722
- }
1723
- function addDecoratedMethod(cls, spec) {
1724
- if (cls.getMethod(spec.name)) return false;
1725
- cls.addMethod({
1726
- name: spec.name,
1727
- isAsync: spec.isAsync,
1728
- returnType: spec.returnType,
1729
- parameters: spec.parameters?.map((p) => {
1730
- return { name: p.name, type: p.type };
1731
- }),
1732
- statements: spec.statements,
1733
- decorators: [
1734
- {
1735
- name: spec.decoratorName,
1736
- arguments: spec.decoratorArgsText ? [spec.decoratorArgsText] : []
1737
- }
1738
- ]
1739
- });
1740
- return true;
1741
- }
1742
- function propertyInsertIndex(cls) {
1743
- const props = cls.getProperties();
1744
- if (props.length === 0) return 0;
1745
- return props[props.length - 1].getChildIndex() + 1;
1746
- }
1747
- function addPlainProperty(cls, spec) {
1748
- if (cls.getProperty(spec.name)) return false;
1749
- cls.insertProperty(propertyInsertIndex(cls), {
1750
- name: spec.name,
1751
- type: spec.type,
1752
- initializer: spec.initializer,
1753
- isReadonly: !!spec.readonly,
1754
- scope: spec.scope ? SCOPE_MAP[spec.scope] : void 0,
1755
- hasExclamationToken: spec.initializer ? false : !!spec.hasExclamation
1756
- });
1757
- return true;
1758
- }
1759
- const SCOPE_MAP = {
1760
- private: Scope.Private,
1761
- protected: Scope.Protected,
1762
- public: Scope.Public
1763
- };
1764
- function addDecoratedProperty(cls, spec) {
1765
- if (cls.getProperty(spec.name)) return false;
1766
- cls.insertProperty(propertyInsertIndex(cls), {
1767
- name: spec.name,
1768
- type: spec.type,
1769
- initializer: spec.initializer,
1770
- scope: spec.scope ? SCOPE_MAP[spec.scope] : void 0,
1771
- hasExclamationToken: spec.initializer ? false : !!spec.hasExclamation,
1772
- decorators: [
1773
- spec.bare && !spec.decoratorArgsText ? { name: spec.decoratorName } : {
1774
- name: spec.decoratorName,
1775
- arguments: spec.decoratorArgsText ? [spec.decoratorArgsText] : []
1776
- }
1777
- ]
1778
- });
1779
- return true;
1780
- }
1781
- function addGetter(cls, spec) {
1782
- if (cls.getGetAccessor(spec.name)) return false;
1783
- const ctor = cls.getConstructors()[0];
1784
- const index = ctor ? ctor.getChildIndex() + 1 : propertyInsertIndex(cls);
1785
- cls.insertGetAccessor(index, {
1786
- name: spec.name,
1787
- returnType: spec.returnType,
1788
- statements: spec.statements ?? "// TODO: derive and return the computed value"
1789
- });
1790
- return true;
1791
- }
1792
1722
  function buildDecoratorArgs(options) {
1793
1723
  const top = [];
1794
1724
  if (options.containerProperty) {
@@ -3099,6 +3029,7 @@ export {
3099
3029
  properCase,
3100
3030
  readJson,
3101
3031
  readNxJson,
3032
+ resolveModelFilePath,
3102
3033
  setCodegenLogger,
3103
3034
  updateJson,
3104
3035
  updateModelIndex,