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