@kosdev-code/kos-codegen-core 3.0.20 → 3.0.21

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.
Files changed (53) hide show
  1. package/index.d.ts +2 -2
  2. package/index.d.ts.map +1 -1
  3. package/index.js +1266 -379
  4. package/index.js.map +1 -1
  5. package/index.mjs +1267 -380
  6. package/index.mjs.map +1 -1
  7. package/lib/generators/add-container-support/generate-add-container-support.d.ts +14 -0
  8. package/lib/generators/add-container-support/generate-add-container-support.d.ts.map +1 -1
  9. package/lib/generators/add-parent-aware/generate-add-parent-aware.d.ts +15 -0
  10. package/lib/generators/add-parent-aware/generate-add-parent-aware.d.ts.map +1 -0
  11. package/lib/generators/add-parent-aware/index.d.ts +2 -0
  12. package/lib/generators/add-parent-aware/index.d.ts.map +1 -0
  13. package/lib/generators/augment/find-model-declaration.d.ts +11 -0
  14. package/lib/generators/augment/find-model-declaration.d.ts.map +1 -0
  15. package/lib/generators/augment/model-mocks.d.ts +47 -0
  16. package/lib/generators/augment/model-mocks.d.ts.map +1 -0
  17. package/lib/generators/augment/resolve-child-model.d.ts +16 -0
  18. package/lib/generators/augment/resolve-child-model.d.ts.map +1 -0
  19. package/lib/generators/augment/resolve-model-file.d.ts.map +1 -1
  20. package/lib/generators/augment/ts-toolkit.d.ts +26 -1
  21. package/lib/generators/augment/ts-toolkit.d.ts.map +1 -1
  22. package/lib/generators/generate-container-model.d.ts +7 -0
  23. package/lib/generators/generate-container-model.d.ts.map +1 -1
  24. package/lib/generators/generate-model-project.d.ts +36 -0
  25. package/lib/generators/generate-model-project.d.ts.map +1 -0
  26. package/lib/generators/generate-view-model.d.ts +35 -0
  27. package/lib/generators/generate-view-model.d.ts.map +1 -0
  28. package/lib/generators/index.d.ts +21 -16
  29. package/lib/generators/index.d.ts.map +1 -1
  30. package/lib/generators/member-mutators/add-child.d.ts +10 -2
  31. package/lib/generators/member-mutators/add-child.d.ts.map +1 -1
  32. package/lib/generators/member-mutators/add-service-request.d.ts +71 -0
  33. package/lib/generators/member-mutators/add-service-request.d.ts.map +1 -1
  34. package/lib/generators/member-mutators/index.d.ts +1 -1
  35. package/lib/generators/member-mutators/index.d.ts.map +1 -1
  36. package/lib/generators/registration/upsert-chain-entry.d.ts +13 -0
  37. package/lib/generators/registration/upsert-chain-entry.d.ts.map +1 -0
  38. package/lib/generators/service-catalog.d.ts +12 -0
  39. package/lib/generators/service-catalog.d.ts.map +1 -1
  40. package/package.json +2 -2
  41. package/templates/kos-container-model/model/types/index.d.ts.template +2 -2
  42. package/templates/kos-model-project/project/.eslintrc.json.template +33 -0
  43. package/templates/kos-model-project/project/.kos.json.template +14 -0
  44. package/templates/kos-model-project/project/README.md.template +7 -0
  45. package/templates/kos-model-project/project/package.json.template +9 -0
  46. package/templates/kos-model-project/project/project.json.template +35 -0
  47. package/templates/kos-model-project/project/src/index.ts.template +1 -0
  48. package/templates/kos-model-project/project/src/lib/__projectName__.ts.template +3 -0
  49. package/templates/kos-model-project/project/tsconfig.json.template +20 -0
  50. package/templates/kos-model-project/project/tsconfig.lib.json.template +10 -0
  51. package/templates/kos-model-project/project/vite.config.ts.template +47 -0
  52. package/templates/kos-view-model/__nameDashCase__-view-model.ts.template +30 -0
  53. package/templates/kos-view-model/index.ts.template +1 -0
package/index.mjs CHANGED
@@ -3,7 +3,7 @@ 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
+ import { Scope, Project, QuoteKind, IndentationText, VariableDeclarationKind, Node, SyntaxKind } from "ts-morph";
7
7
  import * as ts from "typescript";
8
8
  class TrackingFileSystem {
9
9
  inner;
@@ -474,6 +474,84 @@ function generateSplashProject(codegenFs, templateDir, options) {
474
474
  normalized
475
475
  );
476
476
  }
477
+ const MODEL_PROJECT_SUFFIX = "-models";
478
+ const DEFAULT_MODEL_LIBS_DIR = "libs";
479
+ const JSONC_ESLINT_PARSER_VERSION = "^2.1.0";
480
+ function resolveModelProjectLayout(codegenFs, options) {
481
+ const base = dashCase(options.name);
482
+ const projectName = base.endsWith(MODEL_PROJECT_SUFFIX) ? base : `${base}${MODEL_PROJECT_SUFFIX}`;
483
+ const libsDir = (options.libsDir || DEFAULT_MODEL_LIBS_DIR).replace(
484
+ /\\/g,
485
+ "/"
486
+ );
487
+ const scope = codegenFs ? readNpmScope(codegenFs) : void 0;
488
+ return {
489
+ projectName,
490
+ projectRoot: `${libsDir}/${projectName}`.replace(/^\/+/, ""),
491
+ importPath: scope ? `@${scope}/${projectName}` : projectName
492
+ };
493
+ }
494
+ function readNpmScope(codegenFs) {
495
+ if (!codegenFs.exists("package.json")) {
496
+ return void 0;
497
+ }
498
+ const name = readJson(codegenFs, "package.json").name;
499
+ if (!name?.startsWith("@")) {
500
+ return void 0;
501
+ }
502
+ return name.split("/")[0].slice(1);
503
+ }
504
+ function offsetFromRoot(projectRoot) {
505
+ return projectRoot.split("/").filter(Boolean).map(() => "../").join("");
506
+ }
507
+ function generateModelProject(codegenFs, templateDir, options) {
508
+ const layout = resolveModelProjectLayout(codegenFs, options);
509
+ generateFilesFromTemplates(
510
+ codegenFs,
511
+ path.join(templateDir, "project"),
512
+ layout.projectRoot,
513
+ {
514
+ ...layout,
515
+ projectNameCamelCase: camelCase(layout.projectName),
516
+ offsetFromRoot: offsetFromRoot(layout.projectRoot),
517
+ template: ""
518
+ }
519
+ );
520
+ registerTsconfigPath(codegenFs, layout);
521
+ ensureJsoncEslintParser(codegenFs);
522
+ return layout;
523
+ }
524
+ function registerTsconfigPath(codegenFs, layout) {
525
+ if (!codegenFs.exists("tsconfig.base.json")) {
526
+ return;
527
+ }
528
+ updateJson(codegenFs, "tsconfig.base.json", (json) => {
529
+ json.compilerOptions = json.compilerOptions ?? {};
530
+ json.compilerOptions.paths = json.compilerOptions.paths ?? {};
531
+ json.compilerOptions.paths[layout.importPath] = [
532
+ `${layout.projectRoot}/src/index.ts`
533
+ ];
534
+ return json;
535
+ });
536
+ }
537
+ function ensureJsoncEslintParser(codegenFs) {
538
+ if (!codegenFs.exists("package.json")) {
539
+ return;
540
+ }
541
+ const pkg = readJson(codegenFs, "package.json");
542
+ const devDependencies = pkg.devDependencies ?? {};
543
+ if ("jsonc-eslint-parser" in devDependencies) {
544
+ return;
545
+ }
546
+ devDependencies["jsonc-eslint-parser"] = JSONC_ESLINT_PARSER_VERSION;
547
+ pkg.devDependencies = Object.fromEntries(
548
+ // Codepoint order, matching what Nx's own dependency helper produces.
549
+ Object.entries(devDependencies).sort(
550
+ ([a], [b]) => a < b ? -1 : a > b ? 1 : 0
551
+ )
552
+ );
553
+ writeJson(codegenFs, "package.json", pkg);
554
+ }
477
555
  function generateInit(codegenFs, options) {
478
556
  const logger = getCodegenLogger();
479
557
  const { appProject, modelProject, registrationProject } = options;
@@ -925,6 +1003,58 @@ function normalizeOptions(codegenFs, options, projects) {
925
1003
  template: ""
926
1004
  };
927
1005
  }
1006
+ const UPDATE_RELEASE_VERSION_SCRIPT = `import devkit from "@nx/devkit";
1007
+ import { resolve } from "path";
1008
+ import { readFileSync, writeFileSync } from "fs";
1009
+ import prettier from "prettier";
1010
+
1011
+ // KOS artifact versioning: stamps the project's .kos.json "version" field
1012
+ // (which kabtool bakes into the KAB). Never touches package.json.
1013
+ // Driven by tag-based releases:
1014
+ // nx run-many --target=version --args=--ver=$KOSBUILD_VERSION
1015
+
1016
+ const { readCachedProjectGraph } = devkit;
1017
+ const [, , name, versionArg] = process.argv;
1018
+
1019
+ // "{args.ver}" arrives literally when the target runs without --args=--ver=<v>;
1020
+ // treat that (or a missing arg) as "report current version, change nothing".
1021
+ const version =
1022
+ versionArg && !versionArg.startsWith("{args") ? versionArg : undefined;
1023
+
1024
+ if (!name) {
1025
+ console.error("usage: update-release-version.mjs <project> <version>");
1026
+ process.exit(1);
1027
+ }
1028
+
1029
+ const graph = readCachedProjectGraph();
1030
+ const project = graph.nodes[name];
1031
+ if (!project) {
1032
+ console.error("Unknown project: " + name);
1033
+ process.exit(1);
1034
+ }
1035
+
1036
+ const kosJsonPath = resolve(process.cwd(), project.data.root, ".kos.json");
1037
+ let kosJson;
1038
+ try {
1039
+ kosJson = JSON.parse(readFileSync(kosJsonPath, "utf8"));
1040
+ } catch {
1041
+ console.error("Missing or invalid .kos.json: " + kosJsonPath);
1042
+ process.exit(1);
1043
+ }
1044
+
1045
+ if (!version) {
1046
+ console.log(name + ": " + kosJson.version + " (no --ver given; unchanged)");
1047
+ process.exit(0);
1048
+ }
1049
+
1050
+ const prettierOptions = await prettier.resolveConfig(kosJsonPath);
1051
+ const output = await prettier.format(
1052
+ JSON.stringify({ ...kosJson, version }, null, 2),
1053
+ { ...prettierOptions, parser: "json" }
1054
+ );
1055
+ writeFileSync(kosJsonPath, output);
1056
+ console.log(name + ": version -> " + version);
1057
+ `;
928
1058
  function transformSourceFile(codegenFs, filePath, mutate) {
929
1059
  const content = codegenFs.read(filePath);
930
1060
  if (content === null) {
@@ -1066,7 +1196,8 @@ function ensureConstCatalogEntry(sourceFile, spec) {
1066
1196
  }
1067
1197
  literal.addPropertyAssignment({
1068
1198
  name: spec.key,
1069
- initializer: spec.initializer
1199
+ initializer: spec.initializer,
1200
+ leadingTrivia: spec.leadingTrivia
1070
1201
  });
1071
1202
  return { key: spec.key, created: true };
1072
1203
  }
@@ -1084,7 +1215,8 @@ function ensureConstCatalogEntry(sourceFile, spec) {
1084
1215
  }
1085
1216
  literal.addPropertyAssignment({
1086
1217
  name: spec.key,
1087
- initializer: spec.initializer
1218
+ initializer: spec.initializer,
1219
+ leadingTrivia: spec.leadingTrivia
1088
1220
  });
1089
1221
  return { key: spec.key, created: true };
1090
1222
  }
@@ -1093,7 +1225,8 @@ function ensureExportedTypeAlias(sourceFile, spec) {
1093
1225
  sourceFile.addTypeAlias({
1094
1226
  name: spec.name,
1095
1227
  type: spec.type,
1096
- isExported: true
1228
+ isExported: true,
1229
+ docs: spec.docs ? [spec.docs] : void 0
1097
1230
  });
1098
1231
  return true;
1099
1232
  }
@@ -1117,12 +1250,47 @@ function ensureBarrelExport(sourceFile, moduleSpecifier) {
1117
1250
  sourceFile.addExportDeclaration({ moduleSpecifier });
1118
1251
  return true;
1119
1252
  }
1253
+ function ensureNamedExports(sourceFile, moduleSpecifier, names, isTypeOnly = false) {
1254
+ const existing = sourceFile.getExportDeclarations().find(
1255
+ (d) => d.getModuleSpecifierValue() === moduleSpecifier && d.isTypeOnly() === isTypeOnly
1256
+ );
1257
+ if (!existing) {
1258
+ sourceFile.addExportDeclaration({
1259
+ moduleSpecifier,
1260
+ isTypeOnly,
1261
+ namedExports: names.map((name) => {
1262
+ return { name };
1263
+ })
1264
+ });
1265
+ return true;
1266
+ }
1267
+ const present = new Set(existing.getNamedExports().map((e) => e.getName()));
1268
+ const missing = names.filter((name) => !present.has(name));
1269
+ if (missing.length === 0) return false;
1270
+ existing.addNamedExports(
1271
+ missing.map((name) => {
1272
+ return { name };
1273
+ })
1274
+ );
1275
+ return true;
1276
+ }
1277
+ function ensureExportedInterface(sourceFile, name, properties = [], extendsTypes = []) {
1278
+ if (sourceFile.getInterface(name)) return false;
1279
+ sourceFile.addInterface({
1280
+ name,
1281
+ isExported: true,
1282
+ extends: extendsTypes,
1283
+ properties
1284
+ });
1285
+ return true;
1286
+ }
1120
1287
  function addDecoratedMethod(cls, spec) {
1121
1288
  if (cls.getMethod(spec.name)) return false;
1122
1289
  cls.addMethod({
1123
1290
  name: spec.name,
1124
1291
  isAsync: spec.isAsync,
1125
1292
  returnType: spec.returnType,
1293
+ docs: spec.docs ? [spec.docs] : void 0,
1126
1294
  parameters: spec.parameters?.map((p) => {
1127
1295
  return { name: p.name, type: p.type, hasQuestionToken: p.optional };
1128
1296
  }),
@@ -1130,6 +1298,7 @@ function addDecoratedMethod(cls, spec) {
1130
1298
  decorators: [
1131
1299
  {
1132
1300
  name: spec.decoratorName,
1301
+ typeArguments: spec.decoratorTypeArgs,
1133
1302
  arguments: spec.decoratorArgsText ? [spec.decoratorArgsText] : []
1134
1303
  }
1135
1304
  ]
@@ -1212,17 +1381,28 @@ function listServiceCatalog(codegenFs, query, projects) {
1212
1381
  app,
1213
1382
  version,
1214
1383
  serviceModulePath: posix,
1215
- operations: readOperations(codegenFs, openapiPath)
1384
+ operations: readOperations(codegenFs, openapiPath) ?? []
1216
1385
  });
1217
1386
  }
1218
1387
  return entries.sort(
1219
1388
  (a, b) => a.app.localeCompare(b.app) || a.version.localeCompare(b.version)
1220
1389
  );
1221
1390
  }
1391
+ function readServiceModuleOperations(codegenFs, serviceModuleFile) {
1392
+ const moduleFile = serviceModuleFile.endsWith(".ts") ? serviceModuleFile : `${serviceModuleFile}.ts`;
1393
+ const posix = moduleFile.split(path.sep).join("/");
1394
+ const openapiPath = posix.replace(/service\.ts$/, "openapi.d.ts");
1395
+ for (const candidate of [openapiPath, posix]) {
1396
+ if (!codegenFs.exists(candidate)) continue;
1397
+ const operations = readOperations(codegenFs, candidate);
1398
+ if (operations) return operations;
1399
+ }
1400
+ return null;
1401
+ }
1222
1402
  function readOperations(codegenFs, openapiPath) {
1223
1403
  return readSourceFile(codegenFs, openapiPath, (sf) => {
1224
1404
  const paths = sf.getInterface("paths");
1225
- if (!paths) return [];
1405
+ if (!paths) return null;
1226
1406
  const operations = [];
1227
1407
  for (const pathProp of paths.getProperties()) {
1228
1408
  const servicePath = unquote(pathProp.getName());
@@ -1256,58 +1436,6 @@ function readSummary(methodProp) {
1256
1436
  function unquote(name) {
1257
1437
  return name.replace(/^["']|["']$/g, "");
1258
1438
  }
1259
- const UPDATE_RELEASE_VERSION_SCRIPT = `import devkit from "@nx/devkit";
1260
- import { resolve } from "path";
1261
- import { readFileSync, writeFileSync } from "fs";
1262
- import prettier from "prettier";
1263
-
1264
- // KOS artifact versioning: stamps the project's .kos.json "version" field
1265
- // (which kabtool bakes into the KAB). Never touches package.json.
1266
- // Driven by tag-based releases:
1267
- // nx run-many --target=version --args=--ver=$KOSBUILD_VERSION
1268
-
1269
- const { readCachedProjectGraph } = devkit;
1270
- const [, , name, versionArg] = process.argv;
1271
-
1272
- // "{args.ver}" arrives literally when the target runs without --args=--ver=<v>;
1273
- // treat that (or a missing arg) as "report current version, change nothing".
1274
- const version =
1275
- versionArg && !versionArg.startsWith("{args") ? versionArg : undefined;
1276
-
1277
- if (!name) {
1278
- console.error("usage: update-release-version.mjs <project> <version>");
1279
- process.exit(1);
1280
- }
1281
-
1282
- const graph = readCachedProjectGraph();
1283
- const project = graph.nodes[name];
1284
- if (!project) {
1285
- console.error("Unknown project: " + name);
1286
- process.exit(1);
1287
- }
1288
-
1289
- const kosJsonPath = resolve(process.cwd(), project.data.root, ".kos.json");
1290
- let kosJson;
1291
- try {
1292
- kosJson = JSON.parse(readFileSync(kosJsonPath, "utf8"));
1293
- } catch {
1294
- console.error("Missing or invalid .kos.json: " + kosJsonPath);
1295
- process.exit(1);
1296
- }
1297
-
1298
- if (!version) {
1299
- console.log(name + ": " + kosJson.version + " (no --ver given; unchanged)");
1300
- process.exit(0);
1301
- }
1302
-
1303
- const prettierOptions = await prettier.resolveConfig(kosJsonPath);
1304
- const output = await prettier.format(
1305
- JSON.stringify({ ...kosJson, version }, null, 2),
1306
- { ...prettierOptions, parser: "json" }
1307
- );
1308
- writeFileSync(kosJsonPath, output);
1309
- console.log(name + ": version -> " + version);
1310
- `;
1311
1439
  function appendBarrelExport(codegenFs, indexPath, exportPath) {
1312
1440
  const exportLine = `export * from '${exportPath}'`;
1313
1441
  const content = codegenFs.read(indexPath) ?? "";
@@ -1354,10 +1482,57 @@ function updateModelIndex(codegenFs, indexPath, modelPath) {
1354
1482
  const newContents = printer.printFile(updatedSourceFile);
1355
1483
  codegenFs.write(indexPath, newContents);
1356
1484
  }
1357
- function generateHook(codegenFs, templateDir, options, cwd, projects) {
1358
- if (!options.appProject) {
1359
- throw new Error("No app project specified");
1485
+ function generateCompanionModel(codegenFs, templateDir, options, projects) {
1486
+ const logger = getCodegenLogger();
1487
+ const normalized = normalizeAllValues({
1488
+ companionModelName: options.companionModelName,
1489
+ modelName: options.modelName
1490
+ });
1491
+ const companionChildKosConfig = getKosProjectConfiguration(
1492
+ codegenFs,
1493
+ options.companionModelProject,
1494
+ projects
1495
+ );
1496
+ const parentProject = findProjectByName(
1497
+ codegenFs.root,
1498
+ options.modelProject,
1499
+ projects
1500
+ );
1501
+ const childProject = findProjectByName(
1502
+ codegenFs.root,
1503
+ options.companionModelProject,
1504
+ projects
1505
+ );
1506
+ const projectRoot = childProject?.sourceRoot;
1507
+ if (!projectRoot) {
1508
+ logger.warn(`Companion child project source root not found`);
1509
+ return;
1510
+ }
1511
+ let importPath = "";
1512
+ if (parentProject) {
1513
+ const pkgJsonPath = path.join(parentProject.root, "package.json");
1514
+ try {
1515
+ const pkgJson = readJson(codegenFs, pkgJsonPath);
1516
+ importPath = pkgJson.name || "";
1517
+ } catch {
1518
+ importPath = "";
1519
+ }
1360
1520
  }
1521
+ const modelLocation = companionChildKosConfig?.generator?.defaults?.model?.folder || "";
1522
+ const filePath = path.join(
1523
+ projectRoot,
1524
+ modelLocation,
1525
+ normalized.companionModelNameDashCase
1526
+ );
1527
+ logger.info(`Generating companion model in ${filePath}`);
1528
+ generateFilesFromTemplates(codegenFs, templateDir, filePath, {
1529
+ ...options,
1530
+ ...normalized,
1531
+ importPath
1532
+ });
1533
+ }
1534
+ function generateContainerModel(codegenFs, templateDir, options, cwd, projects) {
1535
+ const logger = getCodegenLogger();
1361
1536
  const currentProject = getProject(codegenFs, cwd);
1362
1537
  const modelProjectName = options.modelProject || currentProject?.name;
1363
1538
  if (!modelProjectName) {
@@ -1365,56 +1540,109 @@ function generateHook(codegenFs, templateDir, options, cwd, projects) {
1365
1540
  "No model project found. Please specify a model project with --modelProject."
1366
1541
  );
1367
1542
  }
1368
- const modelName = options.name || getCurrentDirectoryName(cwd);
1543
+ const modelName = options.modelName || getCurrentDirectoryName(cwd);
1369
1544
  if (!modelName) {
1370
1545
  throw new Error(
1371
1546
  "No model name found. Please specify a model name with --name."
1372
1547
  );
1373
1548
  }
1374
- const kosModelConfig = getKosModelConfiguration(
1375
- codegenFs,
1376
- modelProjectName,
1377
- modelName,
1378
- projects
1379
- );
1380
- options.singleton = kosModelConfig ? !!kosModelConfig.singleton : false;
1381
- options.name = modelName;
1382
- options.modelProject = modelProjectName;
1549
+ options.modelName = modelName;
1550
+ options.name = `${modelName}-container`;
1383
1551
  const normalized = normalizeOptions(codegenFs, options, projects);
1384
- const appProject = findProjectByName(
1552
+ const projectConfig = findProjectByName(
1385
1553
  codegenFs.root,
1386
- normalized.appProject,
1554
+ normalized.modelProject,
1387
1555
  projects
1388
1556
  );
1389
- if (!appProject) {
1390
- throw new Error(`App project '${normalized.appProject}' not found`);
1557
+ if (!projectConfig) {
1558
+ throw new Error(`Model project '${normalized.modelProject}' not found`);
1391
1559
  }
1560
+ addKosModelConfiguration({
1561
+ codegenFs,
1562
+ modelName: normalized.nameDashCase,
1563
+ projectName: projectConfig.name,
1564
+ projectRoot: projectConfig.root,
1565
+ singleton: !!options.singleton,
1566
+ container: true,
1567
+ // The container's exported registration bean (`export const <ProperCase>`).
1568
+ factory: normalized.nameProperCase
1569
+ });
1392
1570
  const kosConfig = getKosProjectConfiguration(
1393
1571
  codegenFs,
1394
- appProject.name,
1572
+ projectConfig.name,
1395
1573
  projects
1396
1574
  );
1397
- const componentLocation = kosConfig?.generator?.defaults?.component?.folder || "";
1398
- options.appDirectory = options.appDirectory || componentLocation;
1399
- const projectRoot = appProject.sourceRoot;
1575
+ const modelLocation = kosConfig?.generator?.defaults?.model?.folder || "";
1576
+ const internal = !!kosConfig?.generator?.internal;
1577
+ options.modelDirectory = options.modelDirectory || modelLocation;
1578
+ const projectRoot = projectConfig.sourceRoot;
1400
1579
  if (projectRoot) {
1401
- generateFilesFromTemplates(
1402
- codegenFs,
1403
- templateDir,
1404
- path.join(
1405
- projectRoot,
1406
- options.appDirectory,
1407
- "hooks",
1408
- normalized.nameDashCase
1409
- ),
1410
- normalized
1580
+ logger.info(
1581
+ `Generating container model ${normalized.nameDashCase} in ${projectRoot}`
1411
1582
  );
1412
- appendBarrelExport(
1413
- codegenFs,
1414
- path.join(projectRoot, options.appDirectory, "hooks", "index.ts"),
1415
- `./${normalized.nameDashCase}`
1583
+ const modelNameDashCase = normalized.modelNameDashCase || normalized.nameDashCase;
1584
+ const modelFolder = path.join(
1585
+ projectRoot,
1586
+ options.modelDirectory || "",
1587
+ modelNameDashCase
1588
+ );
1589
+ if (options.existingModel) {
1590
+ addContainerToExistingModelFolder(codegenFs, templateDir, modelFolder, {
1591
+ ...normalized,
1592
+ internal
1593
+ });
1594
+ } else {
1595
+ generateFilesFromTemplates(
1596
+ codegenFs,
1597
+ path.join(templateDir, "model"),
1598
+ modelFolder,
1599
+ { ...normalized, internal }
1600
+ );
1601
+ }
1602
+ const modelIndex = path.join(projectRoot, "index.ts");
1603
+ const modelPath = normalized.modelDirectory ? `${normalized.modelDirectory}/${modelNameDashCase}` : modelNameDashCase;
1604
+ updateModelIndex(codegenFs, modelIndex, modelPath);
1605
+ }
1606
+ }
1607
+ function addContainerToExistingModelFolder(codegenFs, templateDir, modelFolder, substitutions) {
1608
+ const containerFileName = `${substitutions.nameDashCase}-model.ts`;
1609
+ const containerTemplate = path.join(
1610
+ templateDir,
1611
+ "model",
1612
+ "__nameDashCase__-model.ts.template"
1613
+ );
1614
+ const rendered = ejs.render(
1615
+ fs.readFileSync(containerTemplate, "utf-8"),
1616
+ substitutions,
1617
+ { filename: containerTemplate }
1618
+ );
1619
+ codegenFs.write(path.join(modelFolder, containerFileName), rendered);
1620
+ const typesPath = path.join(modelFolder, "types", "index.d.ts");
1621
+ if (codegenFs.read(typesPath) === null) {
1622
+ codegenFs.write(typesPath, "");
1623
+ }
1624
+ transformSourceFile(codegenFs, typesPath, (sourceFile) => {
1625
+ ensureExportedInterface(
1626
+ sourceFile,
1627
+ `${substitutions.nameProperCase}Options`
1416
1628
  );
1629
+ });
1630
+ const barrelPath = path.join(modelFolder, "index.ts");
1631
+ if (codegenFs.read(barrelPath) === null) {
1632
+ codegenFs.write(barrelPath, "");
1417
1633
  }
1634
+ transformSourceFile(codegenFs, barrelPath, (sourceFile) => {
1635
+ const moduleSpecifier = `./${containerFileName.replace(/\.ts$/, "")}`;
1636
+ ensureNamedExports(sourceFile, moduleSpecifier, [
1637
+ substitutions.nameProperCase
1638
+ ]);
1639
+ ensureNamedExports(
1640
+ sourceFile,
1641
+ moduleSpecifier,
1642
+ [`${substitutions.nameProperCase}Model`],
1643
+ true
1644
+ );
1645
+ });
1418
1646
  }
1419
1647
  function generateContext(codegenFs, templateDir, options, cwd, projects) {
1420
1648
  if (!options.appProject) {
@@ -1468,8 +1696,10 @@ function generateContext(codegenFs, templateDir, options, cwd, projects) {
1468
1696
  );
1469
1697
  }
1470
1698
  }
1471
- function generateContainerModel(codegenFs, templateDir, options, cwd, projects) {
1472
- const logger = getCodegenLogger();
1699
+ function generateHook(codegenFs, templateDir, options, cwd, projects) {
1700
+ if (!options.appProject) {
1701
+ throw new Error("No app project specified");
1702
+ }
1473
1703
  const currentProject = getProject(codegenFs, cwd);
1474
1704
  const modelProjectName = options.modelProject || currentProject?.name;
1475
1705
  if (!modelProjectName) {
@@ -1477,106 +1707,56 @@ function generateContainerModel(codegenFs, templateDir, options, cwd, projects)
1477
1707
  "No model project found. Please specify a model project with --modelProject."
1478
1708
  );
1479
1709
  }
1480
- const modelName = options.modelName || getCurrentDirectoryName(cwd);
1710
+ const modelName = options.name || getCurrentDirectoryName(cwd);
1481
1711
  if (!modelName) {
1482
1712
  throw new Error(
1483
1713
  "No model name found. Please specify a model name with --name."
1484
1714
  );
1485
1715
  }
1486
- options.modelName = modelName;
1487
- options.name = `${modelName}-container`;
1716
+ const kosModelConfig = getKosModelConfiguration(
1717
+ codegenFs,
1718
+ modelProjectName,
1719
+ modelName,
1720
+ projects
1721
+ );
1722
+ options.singleton = kosModelConfig ? !!kosModelConfig.singleton : false;
1723
+ options.name = modelName;
1724
+ options.modelProject = modelProjectName;
1488
1725
  const normalized = normalizeOptions(codegenFs, options, projects);
1489
- const projectConfig = findProjectByName(
1726
+ const appProject = findProjectByName(
1490
1727
  codegenFs.root,
1491
- normalized.modelProject,
1728
+ normalized.appProject,
1492
1729
  projects
1493
1730
  );
1494
- if (!projectConfig) {
1495
- throw new Error(`Model project '${normalized.modelProject}' not found`);
1731
+ if (!appProject) {
1732
+ throw new Error(`App project '${normalized.appProject}' not found`);
1496
1733
  }
1497
- addKosModelConfiguration({
1498
- codegenFs,
1499
- modelName: normalized.nameDashCase,
1500
- projectName: projectConfig.name,
1501
- projectRoot: projectConfig.root,
1502
- singleton: !!options.singleton,
1503
- container: true,
1504
- // The container's exported registration bean (`export const <ProperCase>`).
1505
- factory: normalized.nameProperCase
1506
- });
1507
1734
  const kosConfig = getKosProjectConfiguration(
1508
1735
  codegenFs,
1509
- projectConfig.name,
1736
+ appProject.name,
1510
1737
  projects
1511
1738
  );
1512
- const modelLocation = kosConfig?.generator?.defaults?.model?.folder || "";
1513
- const internal = !!kosConfig?.generator?.internal;
1514
- options.modelDirectory = options.modelDirectory || modelLocation;
1515
- const projectRoot = projectConfig.sourceRoot;
1739
+ const componentLocation = kosConfig?.generator?.defaults?.component?.folder || "";
1740
+ options.appDirectory = options.appDirectory || componentLocation;
1741
+ const projectRoot = appProject.sourceRoot;
1516
1742
  if (projectRoot) {
1517
- logger.info(
1518
- `Generating container model ${normalized.nameDashCase} in ${projectRoot}`
1519
- );
1520
- const modelNameDashCase = normalized.modelNameDashCase || normalized.nameDashCase;
1521
1743
  generateFilesFromTemplates(
1522
1744
  codegenFs,
1523
- path.join(templateDir, "model"),
1524
- path.join(projectRoot, options.modelDirectory || "", modelNameDashCase),
1525
- { ...normalized, internal }
1745
+ templateDir,
1746
+ path.join(
1747
+ projectRoot,
1748
+ options.appDirectory,
1749
+ "hooks",
1750
+ normalized.nameDashCase
1751
+ ),
1752
+ normalized
1753
+ );
1754
+ appendBarrelExport(
1755
+ codegenFs,
1756
+ path.join(projectRoot, options.appDirectory, "hooks", "index.ts"),
1757
+ `./${normalized.nameDashCase}`
1526
1758
  );
1527
- const modelIndex = path.join(projectRoot, "index.ts");
1528
- const modelPath = normalized.modelDirectory ? `${normalized.modelDirectory}/${modelNameDashCase}` : modelNameDashCase;
1529
- updateModelIndex(codegenFs, modelIndex, modelPath);
1530
- }
1531
- }
1532
- function generateCompanionModel(codegenFs, templateDir, options, projects) {
1533
- const logger = getCodegenLogger();
1534
- const normalized = normalizeAllValues({
1535
- companionModelName: options.companionModelName,
1536
- modelName: options.modelName
1537
- });
1538
- const companionChildKosConfig = getKosProjectConfiguration(
1539
- codegenFs,
1540
- options.companionModelProject,
1541
- projects
1542
- );
1543
- const parentProject = findProjectByName(
1544
- codegenFs.root,
1545
- options.modelProject,
1546
- projects
1547
- );
1548
- const childProject = findProjectByName(
1549
- codegenFs.root,
1550
- options.companionModelProject,
1551
- projects
1552
- );
1553
- const projectRoot = childProject?.sourceRoot;
1554
- if (!projectRoot) {
1555
- logger.warn(`Companion child project source root not found`);
1556
- return;
1557
- }
1558
- let importPath = "";
1559
- if (parentProject) {
1560
- const pkgJsonPath = path.join(parentProject.root, "package.json");
1561
- try {
1562
- const pkgJson = readJson(codegenFs, pkgJsonPath);
1563
- importPath = pkgJson.name || "";
1564
- } catch {
1565
- importPath = "";
1566
- }
1567
1759
  }
1568
- const modelLocation = companionChildKosConfig?.generator?.defaults?.model?.folder || "";
1569
- const filePath = path.join(
1570
- projectRoot,
1571
- modelLocation,
1572
- normalized.companionModelNameDashCase
1573
- );
1574
- logger.info(`Generating companion model in ${filePath}`);
1575
- generateFilesFromTemplates(codegenFs, templateDir, filePath, {
1576
- ...options,
1577
- ...normalized,
1578
- importPath
1579
- });
1580
1760
  }
1581
1761
  function generateModel(params) {
1582
1762
  const {
@@ -1675,6 +1855,55 @@ function generateModel(params) {
1675
1855
  );
1676
1856
  }
1677
1857
  }
1858
+ const DECLARATION_MARKER = "@kosModel";
1859
+ function findModelDeclarationFile(codegenFs, searchRoot, typeIds) {
1860
+ const wanted = new Set(typeIds.filter(Boolean));
1861
+ if (wanted.size === 0) return null;
1862
+ const matches = [];
1863
+ for (const filePath of codegenFs.listFiles(searchRoot)) {
1864
+ if (!filePath.endsWith(".ts") || filePath.endsWith(".d.ts")) continue;
1865
+ const content = codegenFs.read(filePath);
1866
+ if (!content || !content.includes(DECLARATION_MARKER)) continue;
1867
+ const declared = readDeclaredModelType(codegenFs, filePath);
1868
+ if (declared && wanted.has(declared)) matches.push(filePath);
1869
+ }
1870
+ if (matches.length === 0) return null;
1871
+ if (matches.length > 1) {
1872
+ throw new Error(
1873
+ `Model type '${[...wanted].join("' / '")}' is declared in more than one file:
1874
+ ` + matches.sort().map((c) => ` - ${c}`).join("\n") + `
1875
+ Pass modelPath to pick one.`
1876
+ );
1877
+ }
1878
+ return matches[0];
1879
+ }
1880
+ function readDeclaredModelType(codegenFs, filePath) {
1881
+ try {
1882
+ return readSourceFile(codegenFs, filePath, (sf) => {
1883
+ const cls = sf.getClasses().find((c) => c.getDecorator("kosModel"));
1884
+ const arg = cls?.getDecorator("kosModel")?.getArguments()[0];
1885
+ if (!arg) return void 0;
1886
+ const inner = arg.getKindName() === "ObjectLiteralExpression" ? modelTypeIdProperty(arg.getText()) : arg.getText();
1887
+ if (!inner) return void 0;
1888
+ return resolveToStringLiteral(inner.trim(), sf.getFullText());
1889
+ });
1890
+ } catch {
1891
+ return void 0;
1892
+ }
1893
+ }
1894
+ function modelTypeIdProperty(objectText) {
1895
+ const m = objectText.match(/\bmodelTypeId\s*:\s*([^,}]+)/);
1896
+ return m ? m[1] : void 0;
1897
+ }
1898
+ function resolveToStringLiteral(expression, fileText) {
1899
+ const literal = expression.match(/^["'`](.*)["'`]$/);
1900
+ if (literal) return literal[1];
1901
+ if (!/^[A-Za-z_$][\w$]*$/.test(expression)) return void 0;
1902
+ const declared = fileText.match(
1903
+ new RegExp(`\\b(?:const|let|var)\\s+${expression}\\s*(?::[^=]+)?=\\s*["'\`]([^"'\`]+)["'\`]`)
1904
+ );
1905
+ return declared ? declared[1] : void 0;
1906
+ }
1678
1907
  function resolveModelFilePath(codegenFs, query, projects) {
1679
1908
  const kosConfig = getKosProjectConfiguration(
1680
1909
  codegenFs,
@@ -1709,14 +1938,23 @@ function resolveModelFilePath(codegenFs, query, projects) {
1709
1938
  if (codegenFs.exists(modelFilePath)) {
1710
1939
  return { modelFilePath, internal, sourceRoot };
1711
1940
  }
1941
+ const searchRoot = path.join(sourceRoot, modelLocation);
1712
1942
  const discovered = findModelFileByName(
1713
1943
  codegenFs,
1714
- path.join(sourceRoot, modelLocation),
1944
+ searchRoot,
1715
1945
  modelNameDashCase
1716
1946
  );
1717
1947
  if (discovered) {
1718
1948
  return { modelFilePath: discovered, internal, sourceRoot };
1719
1949
  }
1950
+ const declaredType = kosConfig?.models?.[query.modelName]?.type;
1951
+ const byDeclaration = findModelDeclarationFile(codegenFs, searchRoot, [
1952
+ query.modelName,
1953
+ ...declaredType ? [declaredType] : []
1954
+ ]);
1955
+ if (byDeclaration) {
1956
+ return { modelFilePath: byDeclaration, internal, sourceRoot };
1957
+ }
1720
1958
  return { modelFilePath, internal, sourceRoot };
1721
1959
  }
1722
1960
  function findModelFileByName(codegenFs, searchRoot, modelNameDashCase) {
@@ -1732,6 +1970,150 @@ Pass modelPath to pick one.`
1732
1970
  }
1733
1971
  return candidates[0];
1734
1972
  }
1973
+ function resolveChildModelType(codegenFs, query, projects) {
1974
+ const childProject = query.childModelProject || query.modelProject;
1975
+ const childType = `${properCase(query.childModel)}Model`;
1976
+ if (childProject !== query.modelProject) {
1977
+ const project = findProjectByName(codegenFs.root, childProject, projects);
1978
+ const pkgJson = project ? readJson(
1979
+ codegenFs,
1980
+ path.join(project.root, "package.json")
1981
+ ) : void 0;
1982
+ return { childType, childTypeModule: pkgJson?.name };
1983
+ }
1984
+ const { modelFilePath: childFilePath } = resolveModelFilePath(
1985
+ codegenFs,
1986
+ { modelName: query.childModel, modelProject: childProject },
1987
+ projects
1988
+ );
1989
+ const relative = path.relative(path.dirname(query.modelFilePath), childFilePath).replace(/\.ts$/, "");
1990
+ return {
1991
+ childType,
1992
+ childTypeModule: relative.startsWith(".") ? relative : `./${relative}`
1993
+ };
1994
+ }
1995
+ function generateViewModel(codegenFs, templateDir, options, cwd, projects) {
1996
+ const logger = getCodegenLogger();
1997
+ if (!options.name) {
1998
+ throw new Error(
1999
+ "No ViewModel name found. Please specify a name with --name."
2000
+ );
2001
+ }
2002
+ const currentProject = getProject(codegenFs, cwd);
2003
+ const modelProjectName = options.modelProject || currentProject?.name;
2004
+ if (!modelProjectName) {
2005
+ throw new Error(
2006
+ "No model project found. Please specify a model project with --project."
2007
+ );
2008
+ }
2009
+ const normalized = normalizeOptions(
2010
+ codegenFs,
2011
+ { ...options, modelProject: modelProjectName },
2012
+ projects
2013
+ );
2014
+ const projectConfig = findProjectByName(
2015
+ codegenFs.root,
2016
+ modelProjectName,
2017
+ projects
2018
+ );
2019
+ if (!projectConfig) {
2020
+ throw new Error(`Model project '${modelProjectName}' not found`);
2021
+ }
2022
+ const kosConfig = getKosProjectConfiguration(
2023
+ codegenFs,
2024
+ projectConfig.name,
2025
+ projects
2026
+ );
2027
+ const modelDirectory = options.modelDirectory || kosConfig?.generator?.defaults?.model?.folder || "";
2028
+ const internal = !!kosConfig?.generator?.internal;
2029
+ const projectRoot = projectConfig.sourceRoot || path.join(projectConfig.root, "src");
2030
+ const viewModelFolder = path.join(
2031
+ projectRoot,
2032
+ modelDirectory,
2033
+ normalized.nameDashCase
2034
+ );
2035
+ const viewModelFilePath = path.join(
2036
+ viewModelFolder,
2037
+ `${normalized.nameDashCase}-view-model.ts`
2038
+ );
2039
+ if (codegenFs.exists(viewModelFilePath)) {
2040
+ logger.info(`ViewModel already exists: ${viewModelFilePath}`);
2041
+ return { viewModelFilePath, created: false };
2042
+ }
2043
+ const resolved = (options.models || []).map(
2044
+ (source) => resolveSourceModel(
2045
+ codegenFs,
2046
+ source,
2047
+ viewModelFilePath,
2048
+ modelProjectName,
2049
+ projects
2050
+ )
2051
+ );
2052
+ logger.info(
2053
+ `Generating ViewModel ${normalized.nameDashCase} in ${projectRoot}`
2054
+ );
2055
+ const constructorParams = resolved.map(({ name, type }) => ({ name, type }));
2056
+ generateFilesFromTemplates(codegenFs, templateDir, viewModelFolder, {
2057
+ ...normalized,
2058
+ internal,
2059
+ typeId: options.typeId || normalized.nameDashCase,
2060
+ devToolsEnabled: !!options.devToolsEnabled,
2061
+ constructorParams,
2062
+ constructorArgs: constructorParams.map((param) => param.name).join(", "),
2063
+ modelImports: groupImports(resolved)
2064
+ });
2065
+ updateModelIndex(
2066
+ codegenFs,
2067
+ path.join(projectRoot, "index.ts"),
2068
+ modelDirectory ? `${modelDirectory}/${normalized.nameDashCase}` : normalized.nameDashCase
2069
+ );
2070
+ return { viewModelFilePath, created: true };
2071
+ }
2072
+ function resolveSourceModel(codegenFs, source, viewModelFilePath, modelProject, projects) {
2073
+ const sourceProject = source.project || modelProject;
2074
+ if (sourceProject === modelProject) {
2075
+ const { modelFilePath } = resolveModelFilePath(
2076
+ codegenFs,
2077
+ { modelName: source.model, modelProject: sourceProject },
2078
+ projects
2079
+ );
2080
+ if (!codegenFs.exists(modelFilePath)) {
2081
+ throw new Error(
2082
+ `Model '${source.model}' not found in project '${sourceProject}' (looked for ${modelFilePath})`
2083
+ );
2084
+ }
2085
+ } else if (!findProjectByName(codegenFs.root, sourceProject, projects)) {
2086
+ throw new Error(`Model project '${sourceProject}' not found`);
2087
+ }
2088
+ const { childType, childTypeModule } = resolveChildModelType(
2089
+ codegenFs,
2090
+ {
2091
+ modelFilePath: viewModelFilePath,
2092
+ modelProject,
2093
+ childModel: source.model,
2094
+ childModelProject: sourceProject
2095
+ },
2096
+ projects
2097
+ );
2098
+ return {
2099
+ name: camelCase(source.model),
2100
+ type: childType,
2101
+ module: childTypeModule
2102
+ };
2103
+ }
2104
+ function groupImports(resolved) {
2105
+ const byModule = /* @__PURE__ */ new Map();
2106
+ for (const { type, module } of resolved) {
2107
+ if (!module) continue;
2108
+ const types = byModule.get(module);
2109
+ if (!types) {
2110
+ byModule.set(module, [type]);
2111
+ } else if (!types.includes(type)) {
2112
+ types.push(type);
2113
+ }
2114
+ }
2115
+ return [...byModule].map(([module, types]) => ({ module, types }));
2116
+ }
1735
2117
  function modelBaseName(modelFilePath, modelName) {
1736
2118
  const base = path.basename(modelFilePath);
1737
2119
  const match = base.match(/^(.*)-model\.ts$/);
@@ -1744,7 +2126,7 @@ function servicesFilePathFor(modelFilePath, modelBase) {
1744
2126
  `${modelBase}-services.ts`
1745
2127
  );
1746
2128
  }
1747
- function header(modelBase) {
2129
+ function header$1(modelBase) {
1748
2130
  return `/**
1749
2131
  * Service layer for the ${modelBase} model: the endpoints it calls and the
1750
2132
  * types derived from them. Standalone service functions for callers outside a
@@ -1755,7 +2137,7 @@ function header(modelBase) {
1755
2137
  function ensureServicesModule(codegenFs, modelFilePath, modelBase) {
1756
2138
  const servicesFilePath = servicesFilePathFor(modelFilePath, modelBase);
1757
2139
  if (!codegenFs.exists(servicesFilePath)) {
1758
- codegenFs.write(servicesFilePath, header(modelBase));
2140
+ codegenFs.write(servicesFilePath, header$1(modelBase));
1759
2141
  }
1760
2142
  const barrelPath = path.join(path.dirname(servicesFilePath), "index.ts");
1761
2143
  if (!codegenFs.exists(barrelPath)) {
@@ -2240,11 +2622,25 @@ function buildDecoratorArgs(options) {
2240
2622
  }
2241
2623
  function addContainerSupportToModel(codegenFs, options, projects) {
2242
2624
  const logger = getCodegenLogger();
2243
- const childType = options.childType?.trim() || "IKosDataModel";
2244
2625
  const { modelFilePath } = resolveModelFilePath(codegenFs, options, projects);
2245
2626
  if (!codegenFs.exists(modelFilePath)) {
2246
2627
  throw new Error(`Model file not found: ${modelFilePath}`);
2247
2628
  }
2629
+ const resolvedChild = options.childModel ? resolveChildModelType(
2630
+ codegenFs,
2631
+ {
2632
+ modelFilePath,
2633
+ modelProject: options.modelProject,
2634
+ childModel: options.childModel,
2635
+ childModelProject: options.childModelProject
2636
+ },
2637
+ projects
2638
+ ) : {
2639
+ childType: options.childType,
2640
+ childTypeModule: options.childTypeModule
2641
+ };
2642
+ const childType = resolvedChild.childType?.trim() || "IKosDataModel";
2643
+ const childTypeModule = resolvedChild.childTypeModule;
2248
2644
  logger.info(
2249
2645
  `Adding container support (<${childType}>) to model: ${options.modelName}`
2250
2646
  );
@@ -2256,6 +2652,10 @@ function addContainerSupportToModel(codegenFs, options, projects) {
2256
2652
  ]);
2257
2653
  if (childType === "IKosDataModel") {
2258
2654
  ensureNamedImport(sf, sdk, [{ name: "IKosDataModel", isTypeOnly: true }]);
2655
+ } else if (childTypeModule) {
2656
+ ensureNamedImport(sf, childTypeModule, [
2657
+ { name: childType, isTypeOnly: true }
2658
+ ]);
2259
2659
  }
2260
2660
  const cls = getModelClass(sf);
2261
2661
  const className = cls.getName();
@@ -2285,39 +2685,173 @@ function addContainerSupportToModel(codegenFs, options, projects) {
2285
2685
  });
2286
2686
  return { modelFilePath };
2287
2687
  }
2288
- function addModelEffectToModel(codegenFs, options, projects) {
2688
+ function addParentAwareToModel(codegenFs, options, projects) {
2289
2689
  const logger = getCodegenLogger();
2290
2690
  const { modelFilePath } = resolveModelFilePath(codegenFs, options, projects);
2291
2691
  if (!codegenFs.exists(modelFilePath)) {
2292
2692
  throw new Error(`Model file not found: ${modelFilePath}`);
2293
2693
  }
2294
- logger.info(
2295
- `Adding @kosModelEffect "${options.methodName}" to ${options.modelName}`
2296
- );
2694
+ logger.info(`Adding parent awareness to model: ${options.modelName}`);
2695
+ let optionsTypeName;
2297
2696
  transformSourceFile(codegenFs, modelFilePath, (sf) => {
2298
2697
  const sdk = resolveSdkModuleSpecifier(sf);
2299
- ensureNamedImport(sf, sdk, [{ name: "kosModelEffect" }]);
2698
+ ensureNamedImport(sf, sdk, [{ name: "kosParentAware" }]);
2300
2699
  const cls = getModelClass(sf);
2301
- const modelType = (cls.getName() ?? "").replace(/Impl$/, "");
2302
- addDecoratedMethod(cls, {
2303
- name: options.methodName,
2304
- decoratorName: "kosModelEffect",
2305
- decoratorArgsText: `{ dependencies: (model: ${modelType}) => [] }`,
2306
- isAsync: true,
2307
- returnType: "Promise<void>",
2308
- statements: "// TODO: react to the tracked dependencies"
2700
+ addClassDecorator(cls, "kosParentAware", {
2701
+ argsText: options.parentId ? `{ parentId: ${JSON.stringify(options.parentId)} }` : ""
2309
2702
  });
2703
+ const ctor = cls.getConstructors()[0];
2704
+ optionsTypeName = ctor?.getParameters()[1]?.getTypeNode()?.getText()?.replace(/<.*$/, "");
2310
2705
  });
2311
- return { modelFilePath };
2706
+ const optionsFilePath = optionsTypeName ? extendOptionsInterface(codegenFs, modelFilePath, optionsTypeName, logger) : void 0;
2707
+ return { modelFilePath, optionsFilePath };
2312
2708
  }
2313
- const FRAMEWORK_TYPES$1 = /* @__PURE__ */ new Set(["IKosDataModel", "IKosIdentifiable"]);
2314
- function addDependencyToModel(codegenFs, options, projects) {
2315
- const logger = getCodegenLogger();
2316
- const { modelFilePath } = resolveModelFilePath(codegenFs, options, projects);
2317
- if (!codegenFs.exists(modelFilePath)) {
2318
- throw new Error(`Model file not found: ${modelFilePath}`);
2319
- }
2320
- logger.info(
2709
+ function extendOptionsInterface(codegenFs, modelFilePath, optionsTypeName, logger) {
2710
+ const typesPath = path.join(
2711
+ path.dirname(modelFilePath),
2712
+ "types",
2713
+ "index.d.ts"
2714
+ );
2715
+ const target = codegenFs.exists(typesPath) ? typesPath : modelFilePath;
2716
+ let extended = false;
2717
+ transformSourceFile(codegenFs, target, (sf) => {
2718
+ const iface = sf.getInterface(optionsTypeName);
2719
+ if (!iface) return;
2720
+ const already = iface.getExtends().some((clause) => clause.getText().includes("KosParentAware"));
2721
+ if (already) {
2722
+ extended = true;
2723
+ return;
2724
+ }
2725
+ ensureNamedImport(sf, "@kosdev-code/kos-ui-sdk", [
2726
+ { name: "KosParentAware", isTypeOnly: true }
2727
+ ]);
2728
+ iface.addExtends("KosParentAware");
2729
+ extended = true;
2730
+ });
2731
+ if (!extended) {
2732
+ logger.warn(
2733
+ `Could not find interface ${optionsTypeName}; add "extends KosParentAware" to it by hand.`
2734
+ );
2735
+ return void 0;
2736
+ }
2737
+ return target;
2738
+ }
2739
+ function firstDecoratorArg(decoratorText) {
2740
+ const open = decoratorText.indexOf("(");
2741
+ if (open === -1) return void 0;
2742
+ const inner = decoratorText.slice(open + 1, decoratorText.lastIndexOf(")")).replace(/\s+/g, " ").trim();
2743
+ return inner || void 0;
2744
+ }
2745
+ function collectDecorated(cls, decoratorName) {
2746
+ const members = [];
2747
+ const visit = (name, decoratorTextOf, typeText) => {
2748
+ const text = decoratorTextOf();
2749
+ if (text === void 0) return;
2750
+ members.push({
2751
+ name,
2752
+ arg: firstDecoratorArg(text),
2753
+ type: typeText || void 0
2754
+ });
2755
+ };
2756
+ for (const m of cls.getMethods()) {
2757
+ const dec = m.getDecorator(decoratorName);
2758
+ if (dec) {
2759
+ visit(m.getName(), () => dec.getText(), m.getReturnTypeNode()?.getText());
2760
+ }
2761
+ }
2762
+ for (const p of cls.getProperties()) {
2763
+ const dec = p.getDecorator(decoratorName);
2764
+ if (dec) {
2765
+ visit(p.getName(), () => dec.getText(), p.getTypeNode()?.getText());
2766
+ }
2767
+ }
2768
+ return members;
2769
+ }
2770
+ function describeModel(codegenFs, options, projects) {
2771
+ const { modelFilePath } = resolveModelFilePath(codegenFs, options, projects);
2772
+ const content = codegenFs.read(modelFilePath);
2773
+ if (content === null) {
2774
+ throw new Error(`Model file not found: ${modelFilePath}`);
2775
+ }
2776
+ const project = new Project({ useInMemoryFileSystem: true });
2777
+ const sf = project.createSourceFile(modelFilePath, content, {
2778
+ overwrite: true
2779
+ });
2780
+ let modelType;
2781
+ const modelTypeDecl = sf.getVariableDeclaration("MODEL_TYPE");
2782
+ if (modelTypeDecl) {
2783
+ const init = modelTypeDecl.getInitializer()?.getText();
2784
+ if (init) modelType = init.replace(/^["'`]|["'`]$/g, "");
2785
+ }
2786
+ const cls = sf.getClasses().find((c) => c.getDecorator("kosModel")) ?? sf.getClasses().find((c) => c.isExported()) ?? sf.getClasses()[0];
2787
+ if (!cls) {
2788
+ return {
2789
+ modelFilePath,
2790
+ modelType,
2791
+ classDecorators: [],
2792
+ singleton: false,
2793
+ isCompanion: false,
2794
+ children: [],
2795
+ dependencies: [],
2796
+ topicHandlers: [],
2797
+ configProperties: [],
2798
+ serviceRequests: [],
2799
+ effects: [],
2800
+ futures: []
2801
+ };
2802
+ }
2803
+ const classDecorators = cls.getDecorators().map((d) => d.getName());
2804
+ const kosModelArg = cls.getDecorator("kosModel") ? firstDecoratorArg(cls.getDecorator("kosModel").getText()) : void 0;
2805
+ const singleton = /\bsingleton\s*:\s*true\b/.test(kosModelArg ?? "");
2806
+ return {
2807
+ modelFilePath,
2808
+ modelType,
2809
+ className: cls.getName(),
2810
+ classDecorators,
2811
+ singleton,
2812
+ isCompanion: classDecorators.includes("kosCompanion"),
2813
+ children: collectDecorated(cls, "kosChild"),
2814
+ dependencies: collectDecorated(cls, "kosDependency"),
2815
+ topicHandlers: collectDecorated(cls, "kosTopicHandler"),
2816
+ configProperties: collectDecorated(cls, "kosConfigProperty"),
2817
+ serviceRequests: collectDecorated(cls, "kosServiceRequest"),
2818
+ effects: collectDecorated(cls, "kosModelEffect"),
2819
+ futures: collectDecorated(cls, "kosFuture")
2820
+ };
2821
+ }
2822
+ function addModelEffectToModel(codegenFs, options, projects) {
2823
+ const logger = getCodegenLogger();
2824
+ const { modelFilePath } = resolveModelFilePath(codegenFs, options, projects);
2825
+ if (!codegenFs.exists(modelFilePath)) {
2826
+ throw new Error(`Model file not found: ${modelFilePath}`);
2827
+ }
2828
+ logger.info(
2829
+ `Adding @kosModelEffect "${options.methodName}" to ${options.modelName}`
2830
+ );
2831
+ transformSourceFile(codegenFs, modelFilePath, (sf) => {
2832
+ const sdk = resolveSdkModuleSpecifier(sf);
2833
+ ensureNamedImport(sf, sdk, [{ name: "kosModelEffect" }]);
2834
+ const cls = getModelClass(sf);
2835
+ const modelType = (cls.getName() ?? "").replace(/Impl$/, "");
2836
+ addDecoratedMethod(cls, {
2837
+ name: options.methodName,
2838
+ decoratorName: "kosModelEffect",
2839
+ decoratorArgsText: `{ dependencies: (model: ${modelType}) => [] }`,
2840
+ isAsync: true,
2841
+ returnType: "Promise<void>",
2842
+ statements: "// TODO: react to the tracked dependencies"
2843
+ });
2844
+ });
2845
+ return { modelFilePath };
2846
+ }
2847
+ const FRAMEWORK_TYPES$1 = /* @__PURE__ */ new Set(["IKosDataModel", "IKosIdentifiable"]);
2848
+ function addDependencyToModel(codegenFs, options, projects) {
2849
+ const logger = getCodegenLogger();
2850
+ const { modelFilePath } = resolveModelFilePath(codegenFs, options, projects);
2851
+ if (!codegenFs.exists(modelFilePath)) {
2852
+ throw new Error(`Model file not found: ${modelFilePath}`);
2853
+ }
2854
+ logger.info(
2321
2855
  `Adding @kosDependency "${options.propertyName}" to ${options.modelName}`
2322
2856
  );
2323
2857
  const argsParts = [`modelType: ${options.modelTypeRef}`];
@@ -2360,7 +2894,18 @@ function addChildToModel(codegenFs, options, projects) {
2360
2894
  logger.info(
2361
2895
  `Adding @kosChild "${options.propertyName}" (${shape}) to ${options.modelName}`
2362
2896
  );
2363
- const childType = options.childType?.trim();
2897
+ const resolvedChild = options.childModel ? resolveChildModelType(
2898
+ codegenFs,
2899
+ {
2900
+ modelFilePath,
2901
+ modelProject: options.modelProject,
2902
+ childModel: options.childModel,
2903
+ childModelProject: options.childModelProject
2904
+ },
2905
+ projects
2906
+ ) : { childType: options.childType, childTypeModule: options.childPackage };
2907
+ const childType = resolvedChild.childType?.trim();
2908
+ const childTypeModule = resolvedChild.childTypeModule;
2364
2909
  transformSourceFile(codegenFs, modelFilePath, (sf) => {
2365
2910
  const sdk = resolveSdkModuleSpecifier(sf);
2366
2911
  const sdkImports = [
@@ -2368,8 +2913,8 @@ function addChildToModel(codegenFs, options, projects) {
2368
2913
  ];
2369
2914
  if (shape === "container") sdkImports.push({ name: "KosModelContainer" });
2370
2915
  ensureNamedImport(sf, sdk, sdkImports);
2371
- if (options.childPackage && childType && /^[A-Za-z_$][\w$]*$/.test(childType) && !FRAMEWORK_TYPES.has(childType)) {
2372
- ensureNamedImport(sf, options.childPackage, [
2916
+ if (childTypeModule && childType && /^[A-Za-z_$][\w$]*$/.test(childType) && !FRAMEWORK_TYPES.has(childType)) {
2917
+ ensureNamedImport(sf, childTypeModule, [
2373
2918
  { name: childType, isTypeOnly: true }
2374
2919
  ]);
2375
2920
  }
@@ -2515,6 +3060,154 @@ function addComputedToModel(codegenFs, options, projects) {
2515
3060
  });
2516
3061
  return { modelFilePath };
2517
3062
  }
3063
+ function mocksFilePathFor(modelFilePath, modelBase) {
3064
+ return path.join(
3065
+ path.dirname(modelFilePath),
3066
+ "mocks",
3067
+ `${modelBase}-mocks.ts`
3068
+ );
3069
+ }
3070
+ function mockRegisterFunctionName(modelBase) {
3071
+ return `register${pascalCase(modelBase)}Mocks`;
3072
+ }
3073
+ function toMockRoutePattern(servicePath) {
3074
+ return servicePath.replace(/\{([^}]+)\}/g, ":$1");
3075
+ }
3076
+ function mockRouteCall(method, pattern, body) {
3077
+ const shorthand = {
3078
+ get: "get",
3079
+ post: "post",
3080
+ put: "put",
3081
+ delete: "del"
3082
+ };
3083
+ const fn = shorthand[method];
3084
+ return fn ? `KosMock.${fn}(${JSON.stringify(pattern)}, ${body});` : `KosMock.route(${JSON.stringify(method.toUpperCase())}, ${JSON.stringify(
3085
+ pattern
3086
+ )}, ${body});`;
3087
+ }
3088
+ function header(modelBase, registerFn) {
3089
+ return `/**
3090
+ * KosMock routes for the ${modelBase} model's PROVISIONAL endpoints — the ones
3091
+ * this project's generated OpenAPI types do not declare.
3092
+ *
3093
+ * NOTHING IMPORTS THIS FILE. Call \`${registerFn}()\` from the app's dev entry,
3094
+ * gated so it cannot run in a production build (\`import.meta.env.DEV\`, a
3095
+ * dev-only entry module, or behind your own switch). Registering a route enables
3096
+ * KosMock, and a mock that shipped would shadow the real endpoint once it exists.
3097
+ *
3098
+ * Unmatched requests still pass through to the real device (KosMock's hybrid
3099
+ * default), so these routes shadow only the paths named below. Every mocked
3100
+ * response carries the \`kos-mocked: true\` wire header, so mock-fed data is
3101
+ * identifiable in devtools.
3102
+ *
3103
+ * DELETE THIS FILE once every endpoint below is in the generated types.
3104
+ */
3105
+ `;
3106
+ }
3107
+ function ensureMockRoute(codegenFs, options) {
3108
+ const mocksFilePath = mocksFilePathFor(
3109
+ options.modelFilePath,
3110
+ options.modelBase
3111
+ );
3112
+ const registerFunction = mockRegisterFunctionName(options.modelBase);
3113
+ const routePattern = toMockRoutePattern(options.servicePath);
3114
+ const sampleName = `SAMPLE_${constantCase(dashCase(options.endpointKey))}`;
3115
+ const samplePlaceholder = !options.sampleText;
3116
+ if (!codegenFs.exists(mocksFilePath)) {
3117
+ codegenFs.write(mocksFilePath, header(options.modelBase, registerFunction));
3118
+ }
3119
+ const sdkSpecifier = options.sdkModuleSpecifier.startsWith(".") ? `../${options.sdkModuleSpecifier}` : options.sdkModuleSpecifier;
3120
+ let routeCreated = false;
3121
+ transformSourceFile(codegenFs, mocksFilePath, (sf) => {
3122
+ ensureNamedImport(sf, sdkSpecifier, [{ name: "KosMock" }]);
3123
+ ensureNamedImport(sf, "../services", [
3124
+ { name: options.rawAlias, isTypeOnly: true }
3125
+ ]);
3126
+ ensureSample(sf, {
3127
+ name: sampleName,
3128
+ type: options.rawAlias,
3129
+ servicePath: options.servicePath,
3130
+ method: options.method,
3131
+ sampleText: options.sampleText
3132
+ });
3133
+ const statement = mockRouteCall(
3134
+ options.method,
3135
+ routePattern,
3136
+ `{ data: ${sampleName} }`
3137
+ );
3138
+ routeCreated = ensureRegisteredRoute(sf, {
3139
+ registerFunction,
3140
+ modelBase: options.modelBase,
3141
+ statement,
3142
+ // Matching on the pattern literal alone would collide across methods.
3143
+ marker: `${options.method.toUpperCase()} ${routePattern}`,
3144
+ matches: (existing) => existing.includes(JSON.stringify(routePattern)) && existing.includes(shorthandOrMethod(options.method))
3145
+ });
3146
+ });
3147
+ return {
3148
+ mocksFilePath,
3149
+ registerFunction,
3150
+ routePattern,
3151
+ sampleName,
3152
+ routeCreated,
3153
+ samplePlaceholder
3154
+ };
3155
+ }
3156
+ function shorthandOrMethod(method) {
3157
+ const shorthand = {
3158
+ get: "KosMock.get(",
3159
+ post: "KosMock.post(",
3160
+ put: "KosMock.put(",
3161
+ delete: "KosMock.del("
3162
+ };
3163
+ return shorthand[method] ?? `"${method.toUpperCase()}"`;
3164
+ }
3165
+ function ensureSample(sf, spec) {
3166
+ if (sf.getVariableDeclaration(spec.name)) return;
3167
+ const lines = [
3168
+ `Sample payload for \`${spec.method.toUpperCase()} ${spec.servicePath}\` — the`,
3169
+ "UNWRAPPED `data` payload, i.e. exactly what the transform receives."
3170
+ ];
3171
+ if (!spec.sampleText) {
3172
+ lines.push(
3173
+ "",
3174
+ "TODO: replace the placeholder with something the backend would actually",
3175
+ "serve. Until then the route answers with nothing and the transform is",
3176
+ "never exercised."
3177
+ );
3178
+ }
3179
+ const docs = lines.join("\n");
3180
+ sf.addVariableStatement({
3181
+ isExported: true,
3182
+ declarationKind: VariableDeclarationKind.Const,
3183
+ docs: [docs],
3184
+ declarations: [
3185
+ {
3186
+ name: spec.name,
3187
+ type: spec.type,
3188
+ initializer: spec.sampleText ?? `null as unknown as ${spec.type}`
3189
+ }
3190
+ ]
3191
+ });
3192
+ }
3193
+ function ensureRegisteredRoute(sf, spec) {
3194
+ let fn = sf.getFunction(spec.registerFunction);
3195
+ if (!fn) {
3196
+ fn = sf.addFunction({
3197
+ name: spec.registerFunction,
3198
+ isExported: true,
3199
+ returnType: "void",
3200
+ docs: [
3201
+ `Arm the ${spec.modelBase} model's provisional endpoints. Call this from a dev-only entry point — never from code that ships.`
3202
+ ]
3203
+ });
3204
+ }
3205
+ const already = fn.getStatements().some((statement) => spec.matches(statement.getText()));
3206
+ if (already) return false;
3207
+ fn.addStatements(`// ${spec.marker}
3208
+ ${spec.statement}`);
3209
+ return true;
3210
+ }
2518
3211
  const SERVICE_MODULE_RE = /\/utils\/services\/.*\/service\.ts$/;
2519
3212
  function findServiceModules(codegenFs, sourceRoot) {
2520
3213
  return codegenFs.listFiles(sourceRoot).filter((f) => SERVICE_MODULE_RE.test(f.split(path.sep).join("/")));
@@ -2539,10 +3232,23 @@ function assertServiceModuleAcceptsTransform(codegenFs, serviceModuleFile, model
2539
3232
  `${file} predates the transform-aware service layer (EndpointCtx takes ${typeParams} type parameter). Run \`kosui api:generate --project ${modelProject} --helpers-only\` to refresh the helper layer in place (no spec fetch, openapi.d.ts untouched), then add the service request.`
2540
3233
  );
2541
3234
  }
3235
+ function assertServiceModuleAcceptsProvisional(codegenFs, serviceModuleFile, modelProject) {
3236
+ const file = `${serviceModuleFile}.ts`;
3237
+ if (!codegenFs.exists(file)) return;
3238
+ const declared = readSourceFile(
3239
+ codegenFs,
3240
+ file,
3241
+ (sf) => Boolean(sf.getFunction("provisionalServiceRequest"))
3242
+ );
3243
+ if (declared) return;
3244
+ throw new Error(
3245
+ `${file} has no \`provisionalServiceRequest\` — the helper layer predates provisional endpoints. Run \`kosui api:generate --project ${modelProject} --helpers-only\` to refresh it in place (no spec fetch, openapi.d.ts untouched), then add the service request.`
3246
+ );
3247
+ }
2542
3248
  function sameEndpoint(existing, candidate) {
2543
3249
  const args = (text) => {
2544
3250
  const match = text.match(
2545
- /^endpoint\s*\(\s*(['"])(.*?)\1\s*,\s*(['"])(.*?)\3\s*,?\s*\)$/
3251
+ /^endpoint\s*\(\s*(['"])(.*?)\1(?:\s+as\s+ApiPath)?\s*,\s*(['"])(.*?)\3\s*,?\s*\)$/
2546
3252
  );
2547
3253
  return match ? [match[2], match[4].toLowerCase()] : null;
2548
3254
  };
@@ -2550,6 +3256,104 @@ function sameEndpoint(existing, candidate) {
2550
3256
  const b = args(candidate.trim());
2551
3257
  return !!a && !!b && a[0] === b[0] && a[1] === b[1];
2552
3258
  }
3259
+ function editDistance(a, b) {
3260
+ let previous = Array.from({ length: b.length + 1 }, (_, i) => i);
3261
+ for (let i = 1; i <= a.length; i++) {
3262
+ const current = [i];
3263
+ for (let j = 1; j <= b.length; j++) {
3264
+ current[j] = Math.min(
3265
+ previous[j] + 1,
3266
+ current[j - 1] + 1,
3267
+ previous[j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1)
3268
+ );
3269
+ }
3270
+ previous = current;
3271
+ }
3272
+ return previous[b.length];
3273
+ }
3274
+ const NEAREST_SHOWN = 5;
3275
+ function provisionalEntryComment(servicePath, method, catalogName) {
3276
+ return `/**
3277
+ * PROVISIONAL — \`${method.toUpperCase()} ${servicePath}\` is NOT in this project's
3278
+ * generated OpenAPI types. \`as ApiPath\` is standing in for a \`paths\` entry that
3279
+ * does not exist, so nothing about this endpoint is checked against a spec and
3280
+ * its response cannot be derived from one. Do not read this as a real endpoint.
3281
+ *
3282
+ * WHEN THE ENDPOINT LANDS:
3283
+ * 1. regenerate this project's API types (\`kosui api:generate\`)
3284
+ * 2. delete \` as ApiPath\` from the entry below
3285
+ * 3. point the endpoint's \`…Raw\` alias at
3286
+ * \`EndpointResponse<typeof ${catalogName}.…>\`
3287
+ * 4. swap \`provisionalServiceRequest\` for \`serviceRequest\` in the model
3288
+ * 5. delete the model's \`mocks/\` module
3289
+ * 6. KEEP the transform — it is real code and survives all of the above
3290
+ */
3291
+ `;
3292
+ }
3293
+ function provisionalMethodDocs(servicePath, method) {
3294
+ return [
3295
+ `PROVISIONAL — \`${method.toUpperCase()} ${servicePath}\` is not in the`,
3296
+ "generated OpenAPI types, so this request is served by a KosMock route, not",
3297
+ "by the device. The feature it backs is NOT complete: report the missing",
3298
+ "endpoint rather than treating this as finished.",
3299
+ "",
3300
+ "See the endpoint's entry in `./services` for what to undo when it lands."
3301
+ ].join("\n");
3302
+ }
3303
+ function rawTypeDocs(servicePath, method, rawType) {
3304
+ const shape = rawType ? [
3305
+ "Stated by hand and unverified: there is no spec entry to check it",
3306
+ "against."
3307
+ ] : [
3308
+ "TODO: state the shape the backend is expected to serve. `unknown`",
3309
+ "compiles, but leaves the response boundary undescribed and the",
3310
+ "transform with nothing to narrow."
3311
+ ];
3312
+ return [
3313
+ `Raw wire shape of \`${method.toUpperCase()} ${servicePath}\` — the UNWRAPPED`,
3314
+ "`data` payload, since the client strips the `{status, data}` envelope",
3315
+ "before the transform runs.",
3316
+ "",
3317
+ ...shape,
3318
+ "",
3319
+ "Replace with `EndpointResponse<…>` once the endpoint is in the generated",
3320
+ "types."
3321
+ ].join("\n");
3322
+ }
3323
+ function removalSteps(spec) {
3324
+ return [
3325
+ `Regenerate the API types for ${spec.modelProject} (generate_api_types), and confirm ${spec.method.toUpperCase()} ${spec.servicePath} is now in them.`,
3326
+ `In ${spec.servicesFilePath}: delete \` as ApiPath\` from \`${spec.catalogName}.${spec.endpointKey}\`.`,
3327
+ `In ${spec.servicesFilePath}: point the endpoint's \`…Raw\` alias at \`EndpointResponse<typeof ${spec.catalogName}.${spec.endpointKey}>\` and drop the hand-written shape.`,
3328
+ `In the model: swap \`provisionalServiceRequest\` for \`serviceRequest\` and drop its explicit type arguments — the response type comes from the spec again.`,
3329
+ spec.mocksFilePath ? `Delete ${spec.mocksFilePath} (and the call to its register function in the app) once no provisional endpoint is left in it.` : `Delete the model's mocks module once no provisional endpoint is left in it.`,
3330
+ `KEEP \`${spec.mapperName}\` and the data type it produces. The transform is real code; it is what let the model read the final shape all along.`
3331
+ ];
3332
+ }
3333
+ function untypedEndpointError(spec) {
3334
+ const methodsForPath = spec.operations.filter((op) => op.path === spec.servicePath).map((op) => op.method);
3335
+ const pathTypedForOtherMethods = methodsForPath.length > 0;
3336
+ const nearest = [...new Set(spec.operations.map((op) => op.path))].sort(
3337
+ (a, b) => editDistance(a, spec.servicePath) - editDistance(b, spec.servicePath)
3338
+ ).slice(0, NEAREST_SHOWN);
3339
+ const waysForward = [
3340
+ `The endpoint EXISTS on the device but this project has not pulled types for it: re-run api:generate (generate_api_types) for ${spec.modelProject}, then add the request unchanged. Confirm on the device first — kos-device search_services / describe_endpoint — since the live OpenAPI is what says whether it exists.`,
3341
+ `The endpoint DOES NOT EXIST yet: pass mock:"auto" (the default) or mock:"always" to emit the provisional, mock-backed form — real decorator, lifecycle, error-first handler and transform, with a KosMock route standing in for the wire response.`,
3342
+ `What is NOT supported: falling back to resolveServiceUrl / ServiceFactory.build / getAll to make the UI render. That compiles and looks finished, which is worse than the hole. A missing endpoint is a blocker to report, not a licence to route around the generated service layer.`
3343
+ ];
3344
+ const message = pathTypedForOtherMethods ? `${spec.method.toUpperCase()} ${spec.servicePath} is not in the generated OpenAPI types for ${spec.modelProject} — the path is typed, but only for: ${methodsForPath.map((m) => m.toUpperCase()).join(", ")}.` : `${spec.method.toUpperCase()} ${spec.servicePath} is not in the generated OpenAPI types for ${spec.modelProject} — the path is absent entirely. Nothing was written.`;
3345
+ const error = new Error(message);
3346
+ error.details = {
3347
+ servicePath: spec.servicePath,
3348
+ method: spec.method,
3349
+ pathTypedForOtherMethods,
3350
+ methodsForPath,
3351
+ otherEndpointsInThisApi: nearest,
3352
+ warning: "These are OTHER endpoints in this API, listed only so they can be ruled out — they are almost certainly NOT what you want. Do not substitute one for the requested path because the strings look similar.",
3353
+ waysForward
3354
+ };
3355
+ return error;
3356
+ }
2553
3357
  function addServiceRequestToModel(codegenFs, options, projects) {
2554
3358
  const logger = getCodegenLogger();
2555
3359
  const { modelFilePath, sourceRoot } = resolveModelFilePath(
@@ -2611,34 +3415,86 @@ function addServiceRequestToModel(codegenFs, options, projects) {
2611
3415
  const mode = options.mode ?? (options.lifecycle ? "lifecycle" : "method");
2612
3416
  const lifecycle = options.lifecycle || "LOAD";
2613
3417
  const catalogName = `${pascalCase(modelBase)}Endpoints`;
3418
+ const mockMode = options.mock ?? "auto";
3419
+ const operations = readServiceModuleOperations(codegenFs, serviceModuleFile);
3420
+ const pathValidated = operations !== null;
3421
+ const operationTyped = operations === null || operations.some(
3422
+ (op) => op.path === options.servicePath && op.method === method
3423
+ );
3424
+ if (!operationTyped && mockMode === "never") {
3425
+ throw untypedEndpointError({
3426
+ servicePath: options.servicePath,
3427
+ method,
3428
+ modelProject: options.modelProject,
3429
+ operations: operations ?? []
3430
+ });
3431
+ }
3432
+ const provisional = !operationTyped;
3433
+ const writeMock = provisional || mockMode === "always";
3434
+ if (provisional) {
3435
+ assertServiceModuleAcceptsProvisional(
3436
+ codegenFs,
3437
+ serviceModuleFile,
3438
+ options.modelProject
3439
+ );
3440
+ }
2614
3441
  logger.info(
2615
- `Adding ${mode}-driven @kosServiceRequest "${options.methodName}" (${method} ${options.servicePath}) to ${options.modelName}`
3442
+ `Adding ${mode}-driven ${provisional ? "PROVISIONAL " : ""}@kosServiceRequest "${options.methodName}" (${method} ${options.servicePath}) to ${options.modelName}`
2616
3443
  );
3444
+ if (provisional) {
3445
+ logger.warn(
3446
+ `${method.toUpperCase()} ${options.servicePath} is not in ${options.modelProject}'s generated OpenAPI types — emitting the provisional, mock-backed form. The feature is NOT complete until the endpoint lands.`
3447
+ );
3448
+ }
2617
3449
  ensureServicesModule(codegenFs, modelFilePath, modelBase);
2618
3450
  let endpointKey = options.methodName;
2619
3451
  let endpointCreated = true;
2620
3452
  let dataAlias = "";
2621
3453
  let mapperName = "";
2622
3454
  let ctxAlias = "";
3455
+ let rawAlias = "";
2623
3456
  transformSourceFile(codegenFs, servicesFilePath, (sf) => {
2624
3457
  ensureNamedImport(sf, serviceImportFromServices, [{ name: "endpoint" }]);
3458
+ if (provisional) {
3459
+ ensureNamedImport(sf, serviceImportFromServices, [
3460
+ { name: "ApiPath", isTypeOnly: true }
3461
+ ]);
3462
+ }
3463
+ const pathText = provisional ? `${JSON.stringify(options.servicePath)} as ApiPath` : JSON.stringify(options.servicePath);
2625
3464
  const entry = ensureConstCatalogEntry(sf, {
2626
3465
  catalogName,
2627
3466
  key: options.methodName,
2628
- initializer: `endpoint(${JSON.stringify(
2629
- options.servicePath
2630
- )}, ${JSON.stringify(method)})`,
2631
- equals: sameEndpoint
3467
+ initializer: `endpoint(${pathText}, ${JSON.stringify(method)})`,
3468
+ equals: sameEndpoint,
3469
+ leadingTrivia: provisional ? provisionalEntryComment(options.servicePath, method, catalogName) : void 0
2632
3470
  });
2633
3471
  endpointKey = entry.key;
2634
3472
  endpointCreated = entry.created;
2635
3473
  dataAlias = `${pascalCase(endpointKey)}Data`;
2636
3474
  mapperName = `to${pascalCase(endpointKey)}Data`;
2637
3475
  ctxAlias = `${pascalCase(endpointKey)}Ctx`;
2638
- const rawType = `EndpointResponse<typeof ${catalogName}.${endpointKey}>`;
2639
- ensureNamedImport(sf, serviceImportFromServices, [
2640
- { name: "EndpointResponse", isTypeOnly: true }
2641
- ]);
3476
+ rawAlias = `${pascalCase(endpointKey)}Raw`;
3477
+ let rawType;
3478
+ if (provisional) {
3479
+ ensureExportedTypeAlias(sf, {
3480
+ name: rawAlias,
3481
+ type: options.rawType || "unknown",
3482
+ docs: rawTypeDocs(options.servicePath, method, options.rawType)
3483
+ });
3484
+ rawType = rawAlias;
3485
+ } else {
3486
+ rawType = `EndpointResponse<typeof ${catalogName}.${endpointKey}>`;
3487
+ ensureNamedImport(sf, serviceImportFromServices, [
3488
+ { name: "EndpointResponse", isTypeOnly: true }
3489
+ ]);
3490
+ if (writeMock) {
3491
+ ensureExportedTypeAlias(sf, {
3492
+ name: rawAlias,
3493
+ type: rawType,
3494
+ docs: `Raw wire shape of \`${method.toUpperCase()} ${options.servicePath}\`, as the spec declares it — what a mock for this endpoint must serve.`
3495
+ });
3496
+ }
3497
+ }
2642
3498
  ensureExportedTypeAlias(sf, { name: dataAlias, type: rawType });
2643
3499
  ensureExportedMapper(sf, {
2644
3500
  name: mapperName,
@@ -2659,20 +3515,25 @@ function addServiceRequestToModel(codegenFs, options, projects) {
2659
3515
  const className = getModelClass(sf).getName();
2660
3516
  if (!className) throw new Error("Model class has no name.");
2661
3517
  const typeParams = getModelClass(sf).getTypeParameters().map((tp) => tp.getText());
2662
- ensureNamedImport(sf, serviceImportFromModel, [{ name: "serviceRequest" }]);
3518
+ const decoratorName = provisional ? "provisionalServiceRequest" : "serviceRequest";
3519
+ const decoratorTypeArgs = provisional ? [rawAlias, dataAlias] : void 0;
3520
+ ensureNamedImport(sf, serviceImportFromModel, [{ name: decoratorName }]);
2663
3521
  if (mode === "lifecycle") {
2664
3522
  ensureNamedImport(sf, "./services", [
2665
3523
  { name: catalogName },
2666
3524
  { name: mapperName },
2667
- { name: dataAlias, isTypeOnly: true }
3525
+ { name: dataAlias, isTypeOnly: true },
3526
+ ...provisional ? [{ name: rawAlias, isTypeOnly: true }] : []
2668
3527
  ]);
2669
3528
  ensureNamedImport(sf, resolveSdkModuleSpecifier(sf), [
2670
3529
  { name: "DependencyLifecycle" }
2671
3530
  ]);
2672
3531
  addDecoratedMethod(getModelClass(sf), {
2673
3532
  name: options.methodName,
2674
- decoratorName: "serviceRequest",
3533
+ decoratorName,
3534
+ decoratorTypeArgs,
2675
3535
  decoratorArgsText: `${catalogName}.${endpointKey}, { lifecycle: DependencyLifecycle.${lifecycle}, transform: ${mapperName} }`,
3536
+ docs: provisional ? provisionalMethodDocs(options.servicePath, method) : void 0,
2676
3537
  // The manager invokes phase handlers error-first, passing (null, data)
2677
3538
  // on success. The `error` half is only ever reached because
2678
3539
  // `serviceRequest` supplies an errorHandler — the bare decorator
@@ -2689,7 +3550,11 @@ function addServiceRequestToModel(codegenFs, options, projects) {
2689
3550
  ensureNamedImport(sf, "./services", [
2690
3551
  { name: catalogName },
2691
3552
  { name: mapperName },
2692
- { name: ctxAlias, isTypeOnly: true }
3553
+ { name: ctxAlias, isTypeOnly: true },
3554
+ ...provisional ? [
3555
+ { name: rawAlias, isTypeOnly: true },
3556
+ { name: dataAlias, isTypeOnly: true }
3557
+ ] : []
2693
3558
  ]);
2694
3559
  ensureNamedImport(sf, resolveSdkModuleSpecifier(sf), [
2695
3560
  { name: "executeServiceRequest" },
@@ -2704,8 +3569,10 @@ function addServiceRequestToModel(codegenFs, options, projects) {
2704
3569
  addClassDecorator(getModelClass(sf), "kosLoggerAware", { argsText: "" });
2705
3570
  addDecoratedMethod(getModelClass(sf), {
2706
3571
  name: options.methodName,
2707
- decoratorName: "serviceRequest",
3572
+ decoratorName,
3573
+ decoratorTypeArgs,
2708
3574
  decoratorArgsText: `${catalogName}.${endpointKey}, { transform: ${mapperName} }`,
3575
+ docs: provisional ? provisionalMethodDocs(options.servicePath, method) : void 0,
2709
3576
  // Optional: the framework appends the context, callers never pass it.
2710
3577
  parameters: [{ name: "$ctx", type: ctxAlias, optional: true }],
2711
3578
  isAsync: true,
@@ -2717,172 +3584,43 @@ function addServiceRequestToModel(codegenFs, options, projects) {
2717
3584
  ].join("\n")
2718
3585
  });
2719
3586
  });
3587
+ const mock = writeMock ? ensureMockRoute(codegenFs, {
3588
+ modelFilePath,
3589
+ modelBase,
3590
+ sdkModuleSpecifier: readSourceFile(
3591
+ codegenFs,
3592
+ modelFilePath,
3593
+ resolveSdkModuleSpecifier
3594
+ ),
3595
+ rawAlias,
3596
+ servicePath: options.servicePath,
3597
+ method,
3598
+ endpointKey,
3599
+ sampleText: options.sample
3600
+ }) : void 0;
2720
3601
  return {
2721
3602
  modelFilePath,
2722
3603
  servicesFilePath,
2723
3604
  serviceModule: serviceImportFromModel,
2724
3605
  mode,
2725
3606
  endpointKey,
2726
- endpointCreated
2727
- };
2728
- }
2729
- function modelElementType(typeText) {
2730
- let m;
2731
- if (m = typeText.match(/^([A-Za-z_]\w*Model)\s*\[\]$/)) return m[1];
2732
- if (m = typeText.match(/^Array<\s*([A-Za-z_]\w*Model)\s*>$/)) return m[1];
2733
- if (m = typeText.match(/^(?:Map|Set)<[^>]*?\b([A-Za-z_]\w*Model)\b[^>]*>$/))
2734
- return m[1];
2735
- return null;
2736
- }
2737
- function validateModel(codegenFs, options, projects) {
2738
- const { modelFilePath } = resolveModelFilePath(codegenFs, options, projects);
2739
- const content = codegenFs.read(modelFilePath);
2740
- if (content === null) {
2741
- throw new Error(`Model file not found: ${modelFilePath}`);
2742
- }
2743
- const project = new Project({ useInMemoryFileSystem: true });
2744
- const sf = project.createSourceFile(modelFilePath, content, {
2745
- overwrite: true
2746
- });
2747
- const findings = [];
2748
- const hasKosModel = sf.getClasses().some((c) => c.getDecorator("kosModel"));
2749
- if (!hasKosModel) {
2750
- findings.push({
2751
- level: "error",
2752
- rule: "missing-kosModel",
2753
- message: "No @kosModel decorator found — this is not a KOS model."
2754
- });
2755
- }
2756
- const imports = sf.getImportDeclarations();
2757
- const mobxImport = imports.find(
2758
- (d) => /^mobx(-react-lite)?$/.test(d.getModuleSpecifierValue())
2759
- );
2760
- if (mobxImport) {
2761
- findings.push({
2762
- level: "error",
2763
- rule: "mobx-import",
2764
- message: "Imports mobx directly. Reactivity is internal to KOS — remove the mobx import and use KOS reactivity / container models."
2765
- });
2766
- }
2767
- const barrelServiceRequest = imports.find(
2768
- (d) => d.getModuleSpecifierValue() === "@kosdev-code/kos-ui-sdk" && d.getNamedImports().some((n) => n.getName() === "kosServiceRequest")
2769
- );
2770
- if (barrelServiceRequest) {
2771
- findings.push({
2772
- level: "warning",
2773
- rule: "untyped-service-request",
2774
- message: "kosServiceRequest imported from the SDK barrel. Use the typed decorator from the api:generate'd service module so paths are validated against the OpenAPI types."
2775
- });
2776
- }
2777
- for (const cls of sf.getClasses()) {
2778
- for (const prop of cls.getProperties()) {
2779
- const name = prop.getName();
2780
- const typeText = (prop.getTypeNode()?.getText() ?? "").replace(/\s+/g, " ").trim();
2781
- const initText = prop.getInitializer()?.getText() ?? "";
2782
- const hasChild = !!prop.getDecorator("kosChild");
2783
- const isModelContainer = /\bI?KosModelContainer\s*</.test(typeText) || /new\s+KosModelContainer\b/.test(initText);
2784
- if (isModelContainer && !hasChild) {
2785
- findings.push({
2786
- level: "warning",
2787
- rule: "container-missing-kosChild",
2788
- message: `Property '${name}' is a model container but is not marked @kosChild — add @kosChild so its models join the model graph and lifecycle.`
2789
- });
2790
- continue;
2791
- }
2792
- const elem = modelElementType(typeText);
2793
- if (elem && !isModelContainer) {
2794
- findings.push({
2795
- level: "warning",
2796
- rule: "raw-model-collection",
2797
- message: `Property '${name}' is a raw ${typeText} of models — prefer a KosModelContainer<${elem}> (indexing, sorting, lifecycle, delta handling) marked @kosChild.`
2798
- });
2799
- }
2800
- }
2801
- }
2802
- const hasError = findings.some((f) => f.level === "error");
2803
- return { modelFilePath, ok: !hasError, findings };
2804
- }
2805
- function firstDecoratorArg(decoratorText) {
2806
- const open = decoratorText.indexOf("(");
2807
- if (open === -1) return void 0;
2808
- const inner = decoratorText.slice(open + 1, decoratorText.lastIndexOf(")")).replace(/\s+/g, " ").trim();
2809
- return inner || void 0;
2810
- }
2811
- function collectDecorated(cls, decoratorName) {
2812
- const members = [];
2813
- const visit = (name, decoratorTextOf, typeText) => {
2814
- const text = decoratorTextOf();
2815
- if (text === void 0) return;
2816
- members.push({
2817
- name,
2818
- arg: firstDecoratorArg(text),
2819
- type: typeText || void 0
2820
- });
2821
- };
2822
- for (const m of cls.getMethods()) {
2823
- const dec = m.getDecorator(decoratorName);
2824
- if (dec) {
2825
- visit(m.getName(), () => dec.getText(), m.getReturnTypeNode()?.getText());
2826
- }
2827
- }
2828
- for (const p of cls.getProperties()) {
2829
- const dec = p.getDecorator(decoratorName);
2830
- if (dec) {
2831
- visit(p.getName(), () => dec.getText(), p.getTypeNode()?.getText());
2832
- }
2833
- }
2834
- return members;
2835
- }
2836
- function describeModel(codegenFs, options, projects) {
2837
- const { modelFilePath } = resolveModelFilePath(codegenFs, options, projects);
2838
- const content = codegenFs.read(modelFilePath);
2839
- if (content === null) {
2840
- throw new Error(`Model file not found: ${modelFilePath}`);
2841
- }
2842
- const project = new Project({ useInMemoryFileSystem: true });
2843
- const sf = project.createSourceFile(modelFilePath, content, {
2844
- overwrite: true
2845
- });
2846
- let modelType;
2847
- const modelTypeDecl = sf.getVariableDeclaration("MODEL_TYPE");
2848
- if (modelTypeDecl) {
2849
- const init = modelTypeDecl.getInitializer()?.getText();
2850
- if (init) modelType = init.replace(/^["'`]|["'`]$/g, "");
2851
- }
2852
- const cls = sf.getClasses().find((c) => c.getDecorator("kosModel")) ?? sf.getClasses().find((c) => c.isExported()) ?? sf.getClasses()[0];
2853
- if (!cls) {
2854
- return {
2855
- modelFilePath,
2856
- modelType,
2857
- classDecorators: [],
2858
- singleton: false,
2859
- isCompanion: false,
2860
- children: [],
2861
- dependencies: [],
2862
- topicHandlers: [],
2863
- configProperties: [],
2864
- serviceRequests: [],
2865
- effects: [],
2866
- futures: []
2867
- };
2868
- }
2869
- const classDecorators = cls.getDecorators().map((d) => d.getName());
2870
- const kosModelArg = cls.getDecorator("kosModel") ? firstDecoratorArg(cls.getDecorator("kosModel").getText()) : void 0;
2871
- const singleton = /\bsingleton\s*:\s*true\b/.test(kosModelArg ?? "");
2872
- return {
2873
- modelFilePath,
2874
- modelType,
2875
- className: cls.getName(),
2876
- classDecorators,
2877
- singleton,
2878
- isCompanion: classDecorators.includes("kosCompanion"),
2879
- children: collectDecorated(cls, "kosChild"),
2880
- dependencies: collectDecorated(cls, "kosDependency"),
2881
- topicHandlers: collectDecorated(cls, "kosTopicHandler"),
2882
- configProperties: collectDecorated(cls, "kosConfigProperty"),
2883
- serviceRequests: collectDecorated(cls, "kosServiceRequest"),
2884
- effects: collectDecorated(cls, "kosModelEffect"),
2885
- futures: collectDecorated(cls, "kosFuture")
3607
+ endpointCreated,
3608
+ pathValidated,
3609
+ provisional,
3610
+ mocksFilePath: mock?.mocksFilePath,
3611
+ mockRegisterFunction: mock?.registerFunction,
3612
+ mockRoute: mock && `${method.toUpperCase()} ${mock.routePattern}`,
3613
+ mockSamplePlaceholder: mock?.samplePlaceholder,
3614
+ removalSteps: provisional ? removalSteps({
3615
+ modelProject: options.modelProject,
3616
+ servicePath: options.servicePath,
3617
+ method,
3618
+ catalogName,
3619
+ endpointKey,
3620
+ servicesFilePath,
3621
+ mocksFilePath: mock?.mocksFilePath,
3622
+ mapperName
3623
+ }) : void 0
2886
3624
  };
2887
3625
  }
2888
3626
  const DEFAULT_SDK_PACKAGE = "@kosdev-code/kos-ui-sdk";
@@ -3068,6 +3806,82 @@ function lookupSdkType(codegenFs, options, projects) {
3068
3806
  )}). Check the name, or it may be internal / not part of the public surface.`
3069
3807
  };
3070
3808
  }
3809
+ function modelElementType(typeText) {
3810
+ let m;
3811
+ if (m = typeText.match(/^([A-Za-z_]\w*Model)\s*\[\]$/)) return m[1];
3812
+ if (m = typeText.match(/^Array<\s*([A-Za-z_]\w*Model)\s*>$/)) return m[1];
3813
+ if (m = typeText.match(/^(?:Map|Set)<[^>]*?\b([A-Za-z_]\w*Model)\b[^>]*>$/))
3814
+ return m[1];
3815
+ return null;
3816
+ }
3817
+ function validateModel(codegenFs, options, projects) {
3818
+ const { modelFilePath } = resolveModelFilePath(codegenFs, options, projects);
3819
+ const content = codegenFs.read(modelFilePath);
3820
+ if (content === null) {
3821
+ throw new Error(`Model file not found: ${modelFilePath}`);
3822
+ }
3823
+ const project = new Project({ useInMemoryFileSystem: true });
3824
+ const sf = project.createSourceFile(modelFilePath, content, {
3825
+ overwrite: true
3826
+ });
3827
+ const findings = [];
3828
+ const hasKosModel = sf.getClasses().some((c) => c.getDecorator("kosModel"));
3829
+ if (!hasKosModel) {
3830
+ findings.push({
3831
+ level: "error",
3832
+ rule: "missing-kosModel",
3833
+ message: "No @kosModel decorator found — this is not a KOS model."
3834
+ });
3835
+ }
3836
+ const imports = sf.getImportDeclarations();
3837
+ const mobxImport = imports.find(
3838
+ (d) => /^mobx(-react-lite)?$/.test(d.getModuleSpecifierValue())
3839
+ );
3840
+ if (mobxImport) {
3841
+ findings.push({
3842
+ level: "error",
3843
+ rule: "mobx-import",
3844
+ message: "Imports mobx directly. Reactivity is internal to KOS — remove the mobx import and use KOS reactivity / container models."
3845
+ });
3846
+ }
3847
+ const barrelServiceRequest = imports.find(
3848
+ (d) => d.getModuleSpecifierValue() === "@kosdev-code/kos-ui-sdk" && d.getNamedImports().some((n) => n.getName() === "kosServiceRequest")
3849
+ );
3850
+ if (barrelServiceRequest) {
3851
+ findings.push({
3852
+ level: "warning",
3853
+ rule: "untyped-service-request",
3854
+ message: "kosServiceRequest imported from the SDK barrel. Use the typed decorator from the api:generate'd service module so paths are validated against the OpenAPI types."
3855
+ });
3856
+ }
3857
+ for (const cls of sf.getClasses()) {
3858
+ for (const prop of cls.getProperties()) {
3859
+ const name = prop.getName();
3860
+ const typeText = (prop.getTypeNode()?.getText() ?? "").replace(/\s+/g, " ").trim();
3861
+ const initText = prop.getInitializer()?.getText() ?? "";
3862
+ const hasChild = !!prop.getDecorator("kosChild");
3863
+ const isModelContainer = /\bI?KosModelContainer\s*</.test(typeText) || /new\s+KosModelContainer\b/.test(initText);
3864
+ if (isModelContainer && !hasChild) {
3865
+ findings.push({
3866
+ level: "warning",
3867
+ rule: "container-missing-kosChild",
3868
+ message: `Property '${name}' is a model container but is not marked @kosChild — add @kosChild so its models join the model graph and lifecycle.`
3869
+ });
3870
+ continue;
3871
+ }
3872
+ const elem = modelElementType(typeText);
3873
+ if (elem && !isModelContainer) {
3874
+ findings.push({
3875
+ level: "warning",
3876
+ rule: "raw-model-collection",
3877
+ message: `Property '${name}' is a raw ${typeText} of models — prefer a KosModelContainer<${elem}> (indexing, sorting, lifecycle, delta handling) marked @kosChild.`
3878
+ });
3879
+ }
3880
+ }
3881
+ }
3882
+ const hasError = findings.some((f) => f.level === "error");
3883
+ return { modelFilePath, ok: !hasError, findings };
3884
+ }
3071
3885
  const PLUGIN_TYPES = {
3072
3886
  CUI: "cui",
3073
3887
  UTILITY: "utility",
@@ -3622,9 +4436,76 @@ function updatePluginConfiguration(codegenFs, projectConfig, options) {
3622
4436
  );
3623
4437
  }
3624
4438
  }
4439
+ function parse(content) {
4440
+ const singleQuoted = /^\s*import\b[^"']*'/m.test(content);
4441
+ const project = new Project({
4442
+ useInMemoryFileSystem: true,
4443
+ manipulationSettings: {
4444
+ indentationText: IndentationText.TwoSpaces,
4445
+ quoteKind: singleQuoted ? QuoteKind.Single : QuoteKind.Double
4446
+ }
4447
+ });
4448
+ return project.createSourceFile("registration-chain.ts", content, {
4449
+ overwrite: true
4450
+ });
4451
+ }
4452
+ function chainCalls(sf, name) {
4453
+ return sf.getDescendantsOfKind(SyntaxKind.CallExpression).filter((call) => {
4454
+ const callee = call.getExpression();
4455
+ return callee.getKind() === SyntaxKind.PropertyAccessExpression && callee.asKindOrThrow(SyntaxKind.PropertyAccessExpression).getName() === name;
4456
+ });
4457
+ }
4458
+ function indentOfCall(sf, call) {
4459
+ const text = sf.getFullText();
4460
+ const nameNode = call.getExpression().asKindOrThrow(SyntaxKind.PropertyAccessExpression).getNameNode();
4461
+ const lineStart = text.lastIndexOf("\n", nameNode.getStart()) + 1;
4462
+ return text.slice(lineStart).match(/^[ \t]*/)?.[0] ?? "";
4463
+ }
4464
+ function countChainEntries(content) {
4465
+ return chainCalls(parse(content), "model").length;
4466
+ }
4467
+ function upsertChainEntry(content, bean, importSpec) {
4468
+ const sf = parse(content);
4469
+ const modelCalls = chainCalls(sf, "model");
4470
+ const registered = modelCalls.some((call) => {
4471
+ const [arg, ...rest] = call.getArguments();
4472
+ return rest.length === 0 && arg?.getText() === bean;
4473
+ });
4474
+ if (registered) return { changed: false, note: "already registered" };
4475
+ let anchor;
4476
+ let indent;
4477
+ if (modelCalls.length > 0) {
4478
+ anchor = modelCalls.reduce(
4479
+ (last, call) => call.getEnd() > last.getEnd() ? call : last
4480
+ );
4481
+ indent = indentOfCall(sf, anchor);
4482
+ } else {
4483
+ const starts = chainCalls(sf, "models").filter(
4484
+ (call) => call.getArguments().length === 0
4485
+ );
4486
+ anchor = starts[starts.length - 1];
4487
+ if (!anchor) {
4488
+ return {
4489
+ changed: false,
4490
+ error: "no .models() chain found in the registration file"
4491
+ };
4492
+ }
4493
+ const text = sf.getFullText();
4494
+ const lineStart = text.lastIndexOf("\n", anchor.getStart()) + 1;
4495
+ indent = (text.slice(lineStart).match(/^[ \t]*/)?.[0] ?? "") + " ";
4496
+ }
4497
+ anchor.replaceWithText(`${anchor.getText()}
4498
+ ${indent}.model(${bean})`);
4499
+ const imported = sf.getImportDeclarations().some(
4500
+ (decl) => decl.getNamedImports().some((named) => named.getName() === bean)
4501
+ );
4502
+ if (!imported) ensureNamedImport(sf, importSpec, [{ name: bean }]);
4503
+ return { changed: true, content: sf.getFullText(), importAdded: !imported };
4504
+ }
3625
4505
  export {
3626
4506
  BasePluginHandler,
3627
4507
  CONTRIBUTION_TYPE_MAP,
4508
+ DEFAULT_MODEL_LIBS_DIR,
3628
4509
  DirectFileSystem,
3629
4510
  KAB_OUTPUT_DIR,
3630
4511
  KAB_OUTPUT_PATH,
@@ -3646,6 +4527,7 @@ export {
3646
4527
  addJavaArtifactToManifests,
3647
4528
  addKosModelConfiguration,
3648
4529
  addModelEffectToModel,
4530
+ addParentAwareToModel,
3649
4531
  addPropertyToModel,
3650
4532
  addServiceRequestToModel,
3651
4533
  addTopicHandlerToModel,
@@ -3654,6 +4536,7 @@ export {
3654
4536
  buildSbomTarget,
3655
4537
  camelCase,
3656
4538
  constantCase,
4539
+ countChainEntries,
3657
4540
  dashCase,
3658
4541
  describeModel,
3659
4542
  discoverJavaArtifacts,
@@ -3672,8 +4555,10 @@ export {
3672
4555
  generateHook,
3673
4556
  generateInit,
3674
4557
  generateModel,
4558
+ generateModelProject,
3675
4559
  generatePolyglotWorkspace,
3676
4560
  generateSplashProject,
4561
+ generateViewModel,
3677
4562
  getCodegenLogger,
3678
4563
  getCurrentDirectoryName,
3679
4564
  getKosModelConfigProp,
@@ -3691,10 +4576,12 @@ export {
3691
4576
  readNxJson,
3692
4577
  resolveKabPath,
3693
4578
  resolveModelFilePath,
4579
+ resolveModelProjectLayout,
3694
4580
  setCodegenLogger,
3695
4581
  syncCiManifests,
3696
4582
  updateJson,
3697
4583
  updateModelIndex,
4584
+ upsertChainEntry,
3698
4585
  validateModel,
3699
4586
  writeJson
3700
4587
  };