@kosdev-code/kos-codegen-core 0.1.0-next.871 → 0.1.0-next.888

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 (45) hide show
  1. package/index.d.ts +2 -2
  2. package/index.d.ts.map +1 -1
  3. package/index.js +854 -335
  4. package/index.js.map +1 -1
  5. package/index.mjs +855 -336
  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/resolve-child-model.d.ts +16 -0
  16. package/lib/generators/augment/resolve-child-model.d.ts.map +1 -0
  17. package/lib/generators/augment/resolve-model-file.d.ts.map +1 -1
  18. package/lib/generators/augment/ts-toolkit.d.ts +19 -1
  19. package/lib/generators/augment/ts-toolkit.d.ts.map +1 -1
  20. package/lib/generators/generate-container-model.d.ts +7 -0
  21. package/lib/generators/generate-container-model.d.ts.map +1 -1
  22. package/lib/generators/generate-model-project.d.ts +36 -0
  23. package/lib/generators/generate-model-project.d.ts.map +1 -0
  24. package/lib/generators/generate-view-model.d.ts +35 -0
  25. package/lib/generators/generate-view-model.d.ts.map +1 -0
  26. package/lib/generators/index.d.ts +21 -16
  27. package/lib/generators/index.d.ts.map +1 -1
  28. package/lib/generators/member-mutators/add-child.d.ts +10 -2
  29. package/lib/generators/member-mutators/add-child.d.ts.map +1 -1
  30. package/lib/generators/registration/upsert-chain-entry.d.ts +13 -0
  31. package/lib/generators/registration/upsert-chain-entry.d.ts.map +1 -0
  32. package/package.json +2 -2
  33. package/templates/kos-container-model/model/types/index.d.ts.template +2 -2
  34. package/templates/kos-model-project/project/.eslintrc.json.template +33 -0
  35. package/templates/kos-model-project/project/.kos.json.template +14 -0
  36. package/templates/kos-model-project/project/README.md.template +7 -0
  37. package/templates/kos-model-project/project/package.json.template +9 -0
  38. package/templates/kos-model-project/project/project.json.template +35 -0
  39. package/templates/kos-model-project/project/src/index.ts.template +1 -0
  40. package/templates/kos-model-project/project/src/lib/__projectName__.ts.template +3 -0
  41. package/templates/kos-model-project/project/tsconfig.json.template +20 -0
  42. package/templates/kos-model-project/project/tsconfig.lib.json.template +10 -0
  43. package/templates/kos-model-project/project/vite.config.ts.template +47 -0
  44. package/templates/kos-view-model/__nameDashCase__-view-model.ts.template +30 -0
  45. package/templates/kos-view-model/index.ts.template +1 -0
package/index.js CHANGED
@@ -496,6 +496,84 @@ function generateSplashProject(codegenFs, templateDir, options) {
496
496
  normalized
497
497
  );
498
498
  }
499
+ const MODEL_PROJECT_SUFFIX = "-models";
500
+ const DEFAULT_MODEL_LIBS_DIR = "libs";
501
+ const JSONC_ESLINT_PARSER_VERSION = "^2.1.0";
502
+ function resolveModelProjectLayout(codegenFs, options) {
503
+ const base = dashCase(options.name);
504
+ const projectName = base.endsWith(MODEL_PROJECT_SUFFIX) ? base : `${base}${MODEL_PROJECT_SUFFIX}`;
505
+ const libsDir = (options.libsDir || DEFAULT_MODEL_LIBS_DIR).replace(
506
+ /\\/g,
507
+ "/"
508
+ );
509
+ const scope = codegenFs ? readNpmScope(codegenFs) : void 0;
510
+ return {
511
+ projectName,
512
+ projectRoot: `${libsDir}/${projectName}`.replace(/^\/+/, ""),
513
+ importPath: scope ? `@${scope}/${projectName}` : projectName
514
+ };
515
+ }
516
+ function readNpmScope(codegenFs) {
517
+ if (!codegenFs.exists("package.json")) {
518
+ return void 0;
519
+ }
520
+ const name = readJson(codegenFs, "package.json").name;
521
+ if (!name?.startsWith("@")) {
522
+ return void 0;
523
+ }
524
+ return name.split("/")[0].slice(1);
525
+ }
526
+ function offsetFromRoot(projectRoot) {
527
+ return projectRoot.split("/").filter(Boolean).map(() => "../").join("");
528
+ }
529
+ function generateModelProject(codegenFs, templateDir, options) {
530
+ const layout = resolveModelProjectLayout(codegenFs, options);
531
+ generateFilesFromTemplates(
532
+ codegenFs,
533
+ path__namespace.join(templateDir, "project"),
534
+ layout.projectRoot,
535
+ {
536
+ ...layout,
537
+ projectNameCamelCase: camelCase(layout.projectName),
538
+ offsetFromRoot: offsetFromRoot(layout.projectRoot),
539
+ template: ""
540
+ }
541
+ );
542
+ registerTsconfigPath(codegenFs, layout);
543
+ ensureJsoncEslintParser(codegenFs);
544
+ return layout;
545
+ }
546
+ function registerTsconfigPath(codegenFs, layout) {
547
+ if (!codegenFs.exists("tsconfig.base.json")) {
548
+ return;
549
+ }
550
+ updateJson(codegenFs, "tsconfig.base.json", (json) => {
551
+ json.compilerOptions = json.compilerOptions ?? {};
552
+ json.compilerOptions.paths = json.compilerOptions.paths ?? {};
553
+ json.compilerOptions.paths[layout.importPath] = [
554
+ `${layout.projectRoot}/src/index.ts`
555
+ ];
556
+ return json;
557
+ });
558
+ }
559
+ function ensureJsoncEslintParser(codegenFs) {
560
+ if (!codegenFs.exists("package.json")) {
561
+ return;
562
+ }
563
+ const pkg = readJson(codegenFs, "package.json");
564
+ const devDependencies = pkg.devDependencies ?? {};
565
+ if ("jsonc-eslint-parser" in devDependencies) {
566
+ return;
567
+ }
568
+ devDependencies["jsonc-eslint-parser"] = JSONC_ESLINT_PARSER_VERSION;
569
+ pkg.devDependencies = Object.fromEntries(
570
+ // Codepoint order, matching what Nx's own dependency helper produces.
571
+ Object.entries(devDependencies).sort(
572
+ ([a], [b]) => a < b ? -1 : a > b ? 1 : 0
573
+ )
574
+ );
575
+ writeJson(codegenFs, "package.json", pkg);
576
+ }
499
577
  function generateInit(codegenFs, options) {
500
578
  const logger = getCodegenLogger();
501
579
  const { appProject, modelProject, registrationProject } = options;
@@ -947,6 +1025,58 @@ function normalizeOptions(codegenFs, options, projects) {
947
1025
  template: ""
948
1026
  };
949
1027
  }
1028
+ const UPDATE_RELEASE_VERSION_SCRIPT = `import devkit from "@nx/devkit";
1029
+ import { resolve } from "path";
1030
+ import { readFileSync, writeFileSync } from "fs";
1031
+ import prettier from "prettier";
1032
+
1033
+ // KOS artifact versioning: stamps the project's .kos.json "version" field
1034
+ // (which kabtool bakes into the KAB). Never touches package.json.
1035
+ // Driven by tag-based releases:
1036
+ // nx run-many --target=version --args=--ver=$KOSBUILD_VERSION
1037
+
1038
+ const { readCachedProjectGraph } = devkit;
1039
+ const [, , name, versionArg] = process.argv;
1040
+
1041
+ // "{args.ver}" arrives literally when the target runs without --args=--ver=<v>;
1042
+ // treat that (or a missing arg) as "report current version, change nothing".
1043
+ const version =
1044
+ versionArg && !versionArg.startsWith("{args") ? versionArg : undefined;
1045
+
1046
+ if (!name) {
1047
+ console.error("usage: update-release-version.mjs <project> <version>");
1048
+ process.exit(1);
1049
+ }
1050
+
1051
+ const graph = readCachedProjectGraph();
1052
+ const project = graph.nodes[name];
1053
+ if (!project) {
1054
+ console.error("Unknown project: " + name);
1055
+ process.exit(1);
1056
+ }
1057
+
1058
+ const kosJsonPath = resolve(process.cwd(), project.data.root, ".kos.json");
1059
+ let kosJson;
1060
+ try {
1061
+ kosJson = JSON.parse(readFileSync(kosJsonPath, "utf8"));
1062
+ } catch {
1063
+ console.error("Missing or invalid .kos.json: " + kosJsonPath);
1064
+ process.exit(1);
1065
+ }
1066
+
1067
+ if (!version) {
1068
+ console.log(name + ": " + kosJson.version + " (no --ver given; unchanged)");
1069
+ process.exit(0);
1070
+ }
1071
+
1072
+ const prettierOptions = await prettier.resolveConfig(kosJsonPath);
1073
+ const output = await prettier.format(
1074
+ JSON.stringify({ ...kosJson, version }, null, 2),
1075
+ { ...prettierOptions, parser: "json" }
1076
+ );
1077
+ writeFileSync(kosJsonPath, output);
1078
+ console.log(name + ": version -> " + version);
1079
+ `;
950
1080
  function transformSourceFile(codegenFs, filePath, mutate) {
951
1081
  const content = codegenFs.read(filePath);
952
1082
  if (content === null) {
@@ -1142,6 +1272,40 @@ function ensureBarrelExport(sourceFile, moduleSpecifier) {
1142
1272
  sourceFile.addExportDeclaration({ moduleSpecifier });
1143
1273
  return true;
1144
1274
  }
1275
+ function ensureNamedExports(sourceFile, moduleSpecifier, names, isTypeOnly = false) {
1276
+ const existing = sourceFile.getExportDeclarations().find(
1277
+ (d) => d.getModuleSpecifierValue() === moduleSpecifier && d.isTypeOnly() === isTypeOnly
1278
+ );
1279
+ if (!existing) {
1280
+ sourceFile.addExportDeclaration({
1281
+ moduleSpecifier,
1282
+ isTypeOnly,
1283
+ namedExports: names.map((name) => {
1284
+ return { name };
1285
+ })
1286
+ });
1287
+ return true;
1288
+ }
1289
+ const present = new Set(existing.getNamedExports().map((e) => e.getName()));
1290
+ const missing = names.filter((name) => !present.has(name));
1291
+ if (missing.length === 0) return false;
1292
+ existing.addNamedExports(
1293
+ missing.map((name) => {
1294
+ return { name };
1295
+ })
1296
+ );
1297
+ return true;
1298
+ }
1299
+ function ensureExportedInterface(sourceFile, name, properties = [], extendsTypes = []) {
1300
+ if (sourceFile.getInterface(name)) return false;
1301
+ sourceFile.addInterface({
1302
+ name,
1303
+ isExported: true,
1304
+ extends: extendsTypes,
1305
+ properties
1306
+ });
1307
+ return true;
1308
+ }
1145
1309
  function addDecoratedMethod(cls, spec) {
1146
1310
  if (cls.getMethod(spec.name)) return false;
1147
1311
  cls.addMethod({
@@ -1294,58 +1458,6 @@ function readSummary(methodProp) {
1294
1458
  function unquote(name) {
1295
1459
  return name.replace(/^["']|["']$/g, "");
1296
1460
  }
1297
- const UPDATE_RELEASE_VERSION_SCRIPT = `import devkit from "@nx/devkit";
1298
- import { resolve } from "path";
1299
- import { readFileSync, writeFileSync } from "fs";
1300
- import prettier from "prettier";
1301
-
1302
- // KOS artifact versioning: stamps the project's .kos.json "version" field
1303
- // (which kabtool bakes into the KAB). Never touches package.json.
1304
- // Driven by tag-based releases:
1305
- // nx run-many --target=version --args=--ver=$KOSBUILD_VERSION
1306
-
1307
- const { readCachedProjectGraph } = devkit;
1308
- const [, , name, versionArg] = process.argv;
1309
-
1310
- // "{args.ver}" arrives literally when the target runs without --args=--ver=<v>;
1311
- // treat that (or a missing arg) as "report current version, change nothing".
1312
- const version =
1313
- versionArg && !versionArg.startsWith("{args") ? versionArg : undefined;
1314
-
1315
- if (!name) {
1316
- console.error("usage: update-release-version.mjs <project> <version>");
1317
- process.exit(1);
1318
- }
1319
-
1320
- const graph = readCachedProjectGraph();
1321
- const project = graph.nodes[name];
1322
- if (!project) {
1323
- console.error("Unknown project: " + name);
1324
- process.exit(1);
1325
- }
1326
-
1327
- const kosJsonPath = resolve(process.cwd(), project.data.root, ".kos.json");
1328
- let kosJson;
1329
- try {
1330
- kosJson = JSON.parse(readFileSync(kosJsonPath, "utf8"));
1331
- } catch {
1332
- console.error("Missing or invalid .kos.json: " + kosJsonPath);
1333
- process.exit(1);
1334
- }
1335
-
1336
- if (!version) {
1337
- console.log(name + ": " + kosJson.version + " (no --ver given; unchanged)");
1338
- process.exit(0);
1339
- }
1340
-
1341
- const prettierOptions = await prettier.resolveConfig(kosJsonPath);
1342
- const output = await prettier.format(
1343
- JSON.stringify({ ...kosJson, version }, null, 2),
1344
- { ...prettierOptions, parser: "json" }
1345
- );
1346
- writeFileSync(kosJsonPath, output);
1347
- console.log(name + ": version -> " + version);
1348
- `;
1349
1461
  function appendBarrelExport(codegenFs, indexPath, exportPath) {
1350
1462
  const exportLine = `export * from '${exportPath}'`;
1351
1463
  const content = codegenFs.read(indexPath) ?? "";
@@ -1392,10 +1504,57 @@ function updateModelIndex(codegenFs, indexPath, modelPath) {
1392
1504
  const newContents = printer.printFile(updatedSourceFile);
1393
1505
  codegenFs.write(indexPath, newContents);
1394
1506
  }
1395
- function generateHook(codegenFs, templateDir, options, cwd, projects) {
1396
- if (!options.appProject) {
1397
- throw new Error("No app project specified");
1507
+ function generateCompanionModel(codegenFs, templateDir, options, projects) {
1508
+ const logger = getCodegenLogger();
1509
+ const normalized = normalizeAllValues({
1510
+ companionModelName: options.companionModelName,
1511
+ modelName: options.modelName
1512
+ });
1513
+ const companionChildKosConfig = getKosProjectConfiguration(
1514
+ codegenFs,
1515
+ options.companionModelProject,
1516
+ projects
1517
+ );
1518
+ const parentProject = findProjectByName(
1519
+ codegenFs.root,
1520
+ options.modelProject,
1521
+ projects
1522
+ );
1523
+ const childProject = findProjectByName(
1524
+ codegenFs.root,
1525
+ options.companionModelProject,
1526
+ projects
1527
+ );
1528
+ const projectRoot = childProject?.sourceRoot;
1529
+ if (!projectRoot) {
1530
+ logger.warn(`Companion child project source root not found`);
1531
+ return;
1532
+ }
1533
+ let importPath = "";
1534
+ if (parentProject) {
1535
+ const pkgJsonPath = path__namespace.join(parentProject.root, "package.json");
1536
+ try {
1537
+ const pkgJson = readJson(codegenFs, pkgJsonPath);
1538
+ importPath = pkgJson.name || "";
1539
+ } catch {
1540
+ importPath = "";
1541
+ }
1398
1542
  }
1543
+ const modelLocation = companionChildKosConfig?.generator?.defaults?.model?.folder || "";
1544
+ const filePath = path__namespace.join(
1545
+ projectRoot,
1546
+ modelLocation,
1547
+ normalized.companionModelNameDashCase
1548
+ );
1549
+ logger.info(`Generating companion model in ${filePath}`);
1550
+ generateFilesFromTemplates(codegenFs, templateDir, filePath, {
1551
+ ...options,
1552
+ ...normalized,
1553
+ importPath
1554
+ });
1555
+ }
1556
+ function generateContainerModel(codegenFs, templateDir, options, cwd, projects) {
1557
+ const logger = getCodegenLogger();
1399
1558
  const currentProject = getProject(codegenFs, cwd);
1400
1559
  const modelProjectName = options.modelProject || currentProject?.name;
1401
1560
  if (!modelProjectName) {
@@ -1403,56 +1562,109 @@ function generateHook(codegenFs, templateDir, options, cwd, projects) {
1403
1562
  "No model project found. Please specify a model project with --modelProject."
1404
1563
  );
1405
1564
  }
1406
- const modelName = options.name || getCurrentDirectoryName(cwd);
1565
+ const modelName = options.modelName || getCurrentDirectoryName(cwd);
1407
1566
  if (!modelName) {
1408
1567
  throw new Error(
1409
1568
  "No model name found. Please specify a model name with --name."
1410
1569
  );
1411
1570
  }
1412
- const kosModelConfig = getKosModelConfiguration(
1413
- codegenFs,
1414
- modelProjectName,
1415
- modelName,
1416
- projects
1417
- );
1418
- options.singleton = kosModelConfig ? !!kosModelConfig.singleton : false;
1419
- options.name = modelName;
1420
- options.modelProject = modelProjectName;
1571
+ options.modelName = modelName;
1572
+ options.name = `${modelName}-container`;
1421
1573
  const normalized = normalizeOptions(codegenFs, options, projects);
1422
- const appProject = findProjectByName(
1574
+ const projectConfig = findProjectByName(
1423
1575
  codegenFs.root,
1424
- normalized.appProject,
1576
+ normalized.modelProject,
1425
1577
  projects
1426
1578
  );
1427
- if (!appProject) {
1428
- throw new Error(`App project '${normalized.appProject}' not found`);
1579
+ if (!projectConfig) {
1580
+ throw new Error(`Model project '${normalized.modelProject}' not found`);
1429
1581
  }
1582
+ addKosModelConfiguration({
1583
+ codegenFs,
1584
+ modelName: normalized.nameDashCase,
1585
+ projectName: projectConfig.name,
1586
+ projectRoot: projectConfig.root,
1587
+ singleton: !!options.singleton,
1588
+ container: true,
1589
+ // The container's exported registration bean (`export const <ProperCase>`).
1590
+ factory: normalized.nameProperCase
1591
+ });
1430
1592
  const kosConfig = getKosProjectConfiguration(
1431
1593
  codegenFs,
1432
- appProject.name,
1594
+ projectConfig.name,
1433
1595
  projects
1434
1596
  );
1435
- const componentLocation = kosConfig?.generator?.defaults?.component?.folder || "";
1436
- options.appDirectory = options.appDirectory || componentLocation;
1437
- const projectRoot = appProject.sourceRoot;
1597
+ const modelLocation = kosConfig?.generator?.defaults?.model?.folder || "";
1598
+ const internal = !!kosConfig?.generator?.internal;
1599
+ options.modelDirectory = options.modelDirectory || modelLocation;
1600
+ const projectRoot = projectConfig.sourceRoot;
1438
1601
  if (projectRoot) {
1439
- generateFilesFromTemplates(
1440
- codegenFs,
1441
- templateDir,
1442
- path__namespace.join(
1443
- projectRoot,
1444
- options.appDirectory,
1445
- "hooks",
1446
- normalized.nameDashCase
1447
- ),
1448
- normalized
1602
+ logger.info(
1603
+ `Generating container model ${normalized.nameDashCase} in ${projectRoot}`
1449
1604
  );
1450
- appendBarrelExport(
1451
- codegenFs,
1452
- path__namespace.join(projectRoot, options.appDirectory, "hooks", "index.ts"),
1453
- `./${normalized.nameDashCase}`
1605
+ const modelNameDashCase = normalized.modelNameDashCase || normalized.nameDashCase;
1606
+ const modelFolder = path__namespace.join(
1607
+ projectRoot,
1608
+ options.modelDirectory || "",
1609
+ modelNameDashCase
1454
1610
  );
1455
- }
1611
+ if (options.existingModel) {
1612
+ addContainerToExistingModelFolder(codegenFs, templateDir, modelFolder, {
1613
+ ...normalized,
1614
+ internal
1615
+ });
1616
+ } else {
1617
+ generateFilesFromTemplates(
1618
+ codegenFs,
1619
+ path__namespace.join(templateDir, "model"),
1620
+ modelFolder,
1621
+ { ...normalized, internal }
1622
+ );
1623
+ }
1624
+ const modelIndex = path__namespace.join(projectRoot, "index.ts");
1625
+ const modelPath = normalized.modelDirectory ? `${normalized.modelDirectory}/${modelNameDashCase}` : modelNameDashCase;
1626
+ updateModelIndex(codegenFs, modelIndex, modelPath);
1627
+ }
1628
+ }
1629
+ function addContainerToExistingModelFolder(codegenFs, templateDir, modelFolder, substitutions) {
1630
+ const containerFileName = `${substitutions.nameDashCase}-model.ts`;
1631
+ const containerTemplate = path__namespace.join(
1632
+ templateDir,
1633
+ "model",
1634
+ "__nameDashCase__-model.ts.template"
1635
+ );
1636
+ const rendered = ejs__namespace.render(
1637
+ fs__namespace.readFileSync(containerTemplate, "utf-8"),
1638
+ substitutions,
1639
+ { filename: containerTemplate }
1640
+ );
1641
+ codegenFs.write(path__namespace.join(modelFolder, containerFileName), rendered);
1642
+ const typesPath = path__namespace.join(modelFolder, "types", "index.d.ts");
1643
+ if (codegenFs.read(typesPath) === null) {
1644
+ codegenFs.write(typesPath, "");
1645
+ }
1646
+ transformSourceFile(codegenFs, typesPath, (sourceFile) => {
1647
+ ensureExportedInterface(
1648
+ sourceFile,
1649
+ `${substitutions.nameProperCase}Options`
1650
+ );
1651
+ });
1652
+ const barrelPath = path__namespace.join(modelFolder, "index.ts");
1653
+ if (codegenFs.read(barrelPath) === null) {
1654
+ codegenFs.write(barrelPath, "");
1655
+ }
1656
+ transformSourceFile(codegenFs, barrelPath, (sourceFile) => {
1657
+ const moduleSpecifier = `./${containerFileName.replace(/\.ts$/, "")}`;
1658
+ ensureNamedExports(sourceFile, moduleSpecifier, [
1659
+ substitutions.nameProperCase
1660
+ ]);
1661
+ ensureNamedExports(
1662
+ sourceFile,
1663
+ moduleSpecifier,
1664
+ [`${substitutions.nameProperCase}Model`],
1665
+ true
1666
+ );
1667
+ });
1456
1668
  }
1457
1669
  function generateContext(codegenFs, templateDir, options, cwd, projects) {
1458
1670
  if (!options.appProject) {
@@ -1506,8 +1718,10 @@ function generateContext(codegenFs, templateDir, options, cwd, projects) {
1506
1718
  );
1507
1719
  }
1508
1720
  }
1509
- function generateContainerModel(codegenFs, templateDir, options, cwd, projects) {
1510
- const logger = getCodegenLogger();
1721
+ function generateHook(codegenFs, templateDir, options, cwd, projects) {
1722
+ if (!options.appProject) {
1723
+ throw new Error("No app project specified");
1724
+ }
1511
1725
  const currentProject = getProject(codegenFs, cwd);
1512
1726
  const modelProjectName = options.modelProject || currentProject?.name;
1513
1727
  if (!modelProjectName) {
@@ -1515,106 +1729,56 @@ function generateContainerModel(codegenFs, templateDir, options, cwd, projects)
1515
1729
  "No model project found. Please specify a model project with --modelProject."
1516
1730
  );
1517
1731
  }
1518
- const modelName = options.modelName || getCurrentDirectoryName(cwd);
1732
+ const modelName = options.name || getCurrentDirectoryName(cwd);
1519
1733
  if (!modelName) {
1520
1734
  throw new Error(
1521
1735
  "No model name found. Please specify a model name with --name."
1522
1736
  );
1523
1737
  }
1524
- options.modelName = modelName;
1525
- options.name = `${modelName}-container`;
1738
+ const kosModelConfig = getKosModelConfiguration(
1739
+ codegenFs,
1740
+ modelProjectName,
1741
+ modelName,
1742
+ projects
1743
+ );
1744
+ options.singleton = kosModelConfig ? !!kosModelConfig.singleton : false;
1745
+ options.name = modelName;
1746
+ options.modelProject = modelProjectName;
1526
1747
  const normalized = normalizeOptions(codegenFs, options, projects);
1527
- const projectConfig = findProjectByName(
1748
+ const appProject = findProjectByName(
1528
1749
  codegenFs.root,
1529
- normalized.modelProject,
1750
+ normalized.appProject,
1530
1751
  projects
1531
1752
  );
1532
- if (!projectConfig) {
1533
- throw new Error(`Model project '${normalized.modelProject}' not found`);
1753
+ if (!appProject) {
1754
+ throw new Error(`App project '${normalized.appProject}' not found`);
1534
1755
  }
1535
- addKosModelConfiguration({
1536
- codegenFs,
1537
- modelName: normalized.nameDashCase,
1538
- projectName: projectConfig.name,
1539
- projectRoot: projectConfig.root,
1540
- singleton: !!options.singleton,
1541
- container: true,
1542
- // The container's exported registration bean (`export const <ProperCase>`).
1543
- factory: normalized.nameProperCase
1544
- });
1545
1756
  const kosConfig = getKosProjectConfiguration(
1546
1757
  codegenFs,
1547
- projectConfig.name,
1758
+ appProject.name,
1548
1759
  projects
1549
1760
  );
1550
- const modelLocation = kosConfig?.generator?.defaults?.model?.folder || "";
1551
- const internal = !!kosConfig?.generator?.internal;
1552
- options.modelDirectory = options.modelDirectory || modelLocation;
1553
- const projectRoot = projectConfig.sourceRoot;
1761
+ const componentLocation = kosConfig?.generator?.defaults?.component?.folder || "";
1762
+ options.appDirectory = options.appDirectory || componentLocation;
1763
+ const projectRoot = appProject.sourceRoot;
1554
1764
  if (projectRoot) {
1555
- logger.info(
1556
- `Generating container model ${normalized.nameDashCase} in ${projectRoot}`
1557
- );
1558
- const modelNameDashCase = normalized.modelNameDashCase || normalized.nameDashCase;
1559
1765
  generateFilesFromTemplates(
1560
1766
  codegenFs,
1561
- path__namespace.join(templateDir, "model"),
1562
- path__namespace.join(projectRoot, options.modelDirectory || "", modelNameDashCase),
1563
- { ...normalized, internal }
1767
+ templateDir,
1768
+ path__namespace.join(
1769
+ projectRoot,
1770
+ options.appDirectory,
1771
+ "hooks",
1772
+ normalized.nameDashCase
1773
+ ),
1774
+ normalized
1775
+ );
1776
+ appendBarrelExport(
1777
+ codegenFs,
1778
+ path__namespace.join(projectRoot, options.appDirectory, "hooks", "index.ts"),
1779
+ `./${normalized.nameDashCase}`
1564
1780
  );
1565
- const modelIndex = path__namespace.join(projectRoot, "index.ts");
1566
- const modelPath = normalized.modelDirectory ? `${normalized.modelDirectory}/${modelNameDashCase}` : modelNameDashCase;
1567
- updateModelIndex(codegenFs, modelIndex, modelPath);
1568
- }
1569
- }
1570
- function generateCompanionModel(codegenFs, templateDir, options, projects) {
1571
- const logger = getCodegenLogger();
1572
- const normalized = normalizeAllValues({
1573
- companionModelName: options.companionModelName,
1574
- modelName: options.modelName
1575
- });
1576
- const companionChildKosConfig = getKosProjectConfiguration(
1577
- codegenFs,
1578
- options.companionModelProject,
1579
- projects
1580
- );
1581
- const parentProject = findProjectByName(
1582
- codegenFs.root,
1583
- options.modelProject,
1584
- projects
1585
- );
1586
- const childProject = findProjectByName(
1587
- codegenFs.root,
1588
- options.companionModelProject,
1589
- projects
1590
- );
1591
- const projectRoot = childProject?.sourceRoot;
1592
- if (!projectRoot) {
1593
- logger.warn(`Companion child project source root not found`);
1594
- return;
1595
- }
1596
- let importPath = "";
1597
- if (parentProject) {
1598
- const pkgJsonPath = path__namespace.join(parentProject.root, "package.json");
1599
- try {
1600
- const pkgJson = readJson(codegenFs, pkgJsonPath);
1601
- importPath = pkgJson.name || "";
1602
- } catch {
1603
- importPath = "";
1604
- }
1605
1781
  }
1606
- const modelLocation = companionChildKosConfig?.generator?.defaults?.model?.folder || "";
1607
- const filePath = path__namespace.join(
1608
- projectRoot,
1609
- modelLocation,
1610
- normalized.companionModelNameDashCase
1611
- );
1612
- logger.info(`Generating companion model in ${filePath}`);
1613
- generateFilesFromTemplates(codegenFs, templateDir, filePath, {
1614
- ...options,
1615
- ...normalized,
1616
- importPath
1617
- });
1618
1782
  }
1619
1783
  function generateModel(params) {
1620
1784
  const {
@@ -1713,6 +1877,55 @@ function generateModel(params) {
1713
1877
  );
1714
1878
  }
1715
1879
  }
1880
+ const DECLARATION_MARKER = "@kosModel";
1881
+ function findModelDeclarationFile(codegenFs, searchRoot, typeIds) {
1882
+ const wanted = new Set(typeIds.filter(Boolean));
1883
+ if (wanted.size === 0) return null;
1884
+ const matches = [];
1885
+ for (const filePath of codegenFs.listFiles(searchRoot)) {
1886
+ if (!filePath.endsWith(".ts") || filePath.endsWith(".d.ts")) continue;
1887
+ const content = codegenFs.read(filePath);
1888
+ if (!content || !content.includes(DECLARATION_MARKER)) continue;
1889
+ const declared = readDeclaredModelType(codegenFs, filePath);
1890
+ if (declared && wanted.has(declared)) matches.push(filePath);
1891
+ }
1892
+ if (matches.length === 0) return null;
1893
+ if (matches.length > 1) {
1894
+ throw new Error(
1895
+ `Model type '${[...wanted].join("' / '")}' is declared in more than one file:
1896
+ ` + matches.sort().map((c) => ` - ${c}`).join("\n") + `
1897
+ Pass modelPath to pick one.`
1898
+ );
1899
+ }
1900
+ return matches[0];
1901
+ }
1902
+ function readDeclaredModelType(codegenFs, filePath) {
1903
+ try {
1904
+ return readSourceFile(codegenFs, filePath, (sf) => {
1905
+ const cls = sf.getClasses().find((c) => c.getDecorator("kosModel"));
1906
+ const arg = cls?.getDecorator("kosModel")?.getArguments()[0];
1907
+ if (!arg) return void 0;
1908
+ const inner = arg.getKindName() === "ObjectLiteralExpression" ? modelTypeIdProperty(arg.getText()) : arg.getText();
1909
+ if (!inner) return void 0;
1910
+ return resolveToStringLiteral(inner.trim(), sf.getFullText());
1911
+ });
1912
+ } catch {
1913
+ return void 0;
1914
+ }
1915
+ }
1916
+ function modelTypeIdProperty(objectText) {
1917
+ const m = objectText.match(/\bmodelTypeId\s*:\s*([^,}]+)/);
1918
+ return m ? m[1] : void 0;
1919
+ }
1920
+ function resolveToStringLiteral(expression, fileText) {
1921
+ const literal = expression.match(/^["'`](.*)["'`]$/);
1922
+ if (literal) return literal[1];
1923
+ if (!/^[A-Za-z_$][\w$]*$/.test(expression)) return void 0;
1924
+ const declared = fileText.match(
1925
+ new RegExp(`\\b(?:const|let|var)\\s+${expression}\\s*(?::[^=]+)?=\\s*["'\`]([^"'\`]+)["'\`]`)
1926
+ );
1927
+ return declared ? declared[1] : void 0;
1928
+ }
1716
1929
  function resolveModelFilePath(codegenFs, query, projects) {
1717
1930
  const kosConfig = getKosProjectConfiguration(
1718
1931
  codegenFs,
@@ -1747,14 +1960,23 @@ function resolveModelFilePath(codegenFs, query, projects) {
1747
1960
  if (codegenFs.exists(modelFilePath)) {
1748
1961
  return { modelFilePath, internal, sourceRoot };
1749
1962
  }
1963
+ const searchRoot = path__namespace.join(sourceRoot, modelLocation);
1750
1964
  const discovered = findModelFileByName(
1751
1965
  codegenFs,
1752
- path__namespace.join(sourceRoot, modelLocation),
1966
+ searchRoot,
1753
1967
  modelNameDashCase
1754
1968
  );
1755
1969
  if (discovered) {
1756
1970
  return { modelFilePath: discovered, internal, sourceRoot };
1757
1971
  }
1972
+ const declaredType = kosConfig?.models?.[query.modelName]?.type;
1973
+ const byDeclaration = findModelDeclarationFile(codegenFs, searchRoot, [
1974
+ query.modelName,
1975
+ ...declaredType ? [declaredType] : []
1976
+ ]);
1977
+ if (byDeclaration) {
1978
+ return { modelFilePath: byDeclaration, internal, sourceRoot };
1979
+ }
1758
1980
  return { modelFilePath, internal, sourceRoot };
1759
1981
  }
1760
1982
  function findModelFileByName(codegenFs, searchRoot, modelNameDashCase) {
@@ -1770,6 +1992,150 @@ Pass modelPath to pick one.`
1770
1992
  }
1771
1993
  return candidates[0];
1772
1994
  }
1995
+ function resolveChildModelType(codegenFs, query, projects) {
1996
+ const childProject = query.childModelProject || query.modelProject;
1997
+ const childType = `${properCase(query.childModel)}Model`;
1998
+ if (childProject !== query.modelProject) {
1999
+ const project = findProjectByName(codegenFs.root, childProject, projects);
2000
+ const pkgJson = project ? readJson(
2001
+ codegenFs,
2002
+ path__namespace.join(project.root, "package.json")
2003
+ ) : void 0;
2004
+ return { childType, childTypeModule: pkgJson?.name };
2005
+ }
2006
+ const { modelFilePath: childFilePath } = resolveModelFilePath(
2007
+ codegenFs,
2008
+ { modelName: query.childModel, modelProject: childProject },
2009
+ projects
2010
+ );
2011
+ const relative = path__namespace.relative(path__namespace.dirname(query.modelFilePath), childFilePath).replace(/\.ts$/, "");
2012
+ return {
2013
+ childType,
2014
+ childTypeModule: relative.startsWith(".") ? relative : `./${relative}`
2015
+ };
2016
+ }
2017
+ function generateViewModel(codegenFs, templateDir, options, cwd, projects) {
2018
+ const logger = getCodegenLogger();
2019
+ if (!options.name) {
2020
+ throw new Error(
2021
+ "No ViewModel name found. Please specify a name with --name."
2022
+ );
2023
+ }
2024
+ const currentProject = getProject(codegenFs, cwd);
2025
+ const modelProjectName = options.modelProject || currentProject?.name;
2026
+ if (!modelProjectName) {
2027
+ throw new Error(
2028
+ "No model project found. Please specify a model project with --project."
2029
+ );
2030
+ }
2031
+ const normalized = normalizeOptions(
2032
+ codegenFs,
2033
+ { ...options, modelProject: modelProjectName },
2034
+ projects
2035
+ );
2036
+ const projectConfig = findProjectByName(
2037
+ codegenFs.root,
2038
+ modelProjectName,
2039
+ projects
2040
+ );
2041
+ if (!projectConfig) {
2042
+ throw new Error(`Model project '${modelProjectName}' not found`);
2043
+ }
2044
+ const kosConfig = getKosProjectConfiguration(
2045
+ codegenFs,
2046
+ projectConfig.name,
2047
+ projects
2048
+ );
2049
+ const modelDirectory = options.modelDirectory || kosConfig?.generator?.defaults?.model?.folder || "";
2050
+ const internal = !!kosConfig?.generator?.internal;
2051
+ const projectRoot = projectConfig.sourceRoot || path__namespace.join(projectConfig.root, "src");
2052
+ const viewModelFolder = path__namespace.join(
2053
+ projectRoot,
2054
+ modelDirectory,
2055
+ normalized.nameDashCase
2056
+ );
2057
+ const viewModelFilePath = path__namespace.join(
2058
+ viewModelFolder,
2059
+ `${normalized.nameDashCase}-view-model.ts`
2060
+ );
2061
+ if (codegenFs.exists(viewModelFilePath)) {
2062
+ logger.info(`ViewModel already exists: ${viewModelFilePath}`);
2063
+ return { viewModelFilePath, created: false };
2064
+ }
2065
+ const resolved = (options.models || []).map(
2066
+ (source) => resolveSourceModel(
2067
+ codegenFs,
2068
+ source,
2069
+ viewModelFilePath,
2070
+ modelProjectName,
2071
+ projects
2072
+ )
2073
+ );
2074
+ logger.info(
2075
+ `Generating ViewModel ${normalized.nameDashCase} in ${projectRoot}`
2076
+ );
2077
+ const constructorParams = resolved.map(({ name, type }) => ({ name, type }));
2078
+ generateFilesFromTemplates(codegenFs, templateDir, viewModelFolder, {
2079
+ ...normalized,
2080
+ internal,
2081
+ typeId: options.typeId || normalized.nameDashCase,
2082
+ devToolsEnabled: !!options.devToolsEnabled,
2083
+ constructorParams,
2084
+ constructorArgs: constructorParams.map((param) => param.name).join(", "),
2085
+ modelImports: groupImports(resolved)
2086
+ });
2087
+ updateModelIndex(
2088
+ codegenFs,
2089
+ path__namespace.join(projectRoot, "index.ts"),
2090
+ modelDirectory ? `${modelDirectory}/${normalized.nameDashCase}` : normalized.nameDashCase
2091
+ );
2092
+ return { viewModelFilePath, created: true };
2093
+ }
2094
+ function resolveSourceModel(codegenFs, source, viewModelFilePath, modelProject, projects) {
2095
+ const sourceProject = source.project || modelProject;
2096
+ if (sourceProject === modelProject) {
2097
+ const { modelFilePath } = resolveModelFilePath(
2098
+ codegenFs,
2099
+ { modelName: source.model, modelProject: sourceProject },
2100
+ projects
2101
+ );
2102
+ if (!codegenFs.exists(modelFilePath)) {
2103
+ throw new Error(
2104
+ `Model '${source.model}' not found in project '${sourceProject}' (looked for ${modelFilePath})`
2105
+ );
2106
+ }
2107
+ } else if (!findProjectByName(codegenFs.root, sourceProject, projects)) {
2108
+ throw new Error(`Model project '${sourceProject}' not found`);
2109
+ }
2110
+ const { childType, childTypeModule } = resolveChildModelType(
2111
+ codegenFs,
2112
+ {
2113
+ modelFilePath: viewModelFilePath,
2114
+ modelProject,
2115
+ childModel: source.model,
2116
+ childModelProject: sourceProject
2117
+ },
2118
+ projects
2119
+ );
2120
+ return {
2121
+ name: camelCase(source.model),
2122
+ type: childType,
2123
+ module: childTypeModule
2124
+ };
2125
+ }
2126
+ function groupImports(resolved) {
2127
+ const byModule = /* @__PURE__ */ new Map();
2128
+ for (const { type, module: module2 } of resolved) {
2129
+ if (!module2) continue;
2130
+ const types = byModule.get(module2);
2131
+ if (!types) {
2132
+ byModule.set(module2, [type]);
2133
+ } else if (!types.includes(type)) {
2134
+ types.push(type);
2135
+ }
2136
+ }
2137
+ return [...byModule].map(([module2, types]) => ({ module: module2, types }));
2138
+ }
1773
2139
  function modelBaseName(modelFilePath, modelName) {
1774
2140
  const base = path__namespace.basename(modelFilePath);
1775
2141
  const match = base.match(/^(.*)-model\.ts$/);
@@ -2278,11 +2644,25 @@ function buildDecoratorArgs(options) {
2278
2644
  }
2279
2645
  function addContainerSupportToModel(codegenFs, options, projects) {
2280
2646
  const logger = getCodegenLogger();
2281
- const childType = options.childType?.trim() || "IKosDataModel";
2282
2647
  const { modelFilePath } = resolveModelFilePath(codegenFs, options, projects);
2283
2648
  if (!codegenFs.exists(modelFilePath)) {
2284
2649
  throw new Error(`Model file not found: ${modelFilePath}`);
2285
2650
  }
2651
+ const resolvedChild = options.childModel ? resolveChildModelType(
2652
+ codegenFs,
2653
+ {
2654
+ modelFilePath,
2655
+ modelProject: options.modelProject,
2656
+ childModel: options.childModel,
2657
+ childModelProject: options.childModelProject
2658
+ },
2659
+ projects
2660
+ ) : {
2661
+ childType: options.childType,
2662
+ childTypeModule: options.childTypeModule
2663
+ };
2664
+ const childType = resolvedChild.childType?.trim() || "IKosDataModel";
2665
+ const childTypeModule = resolvedChild.childTypeModule;
2286
2666
  logger.info(
2287
2667
  `Adding container support (<${childType}>) to model: ${options.modelName}`
2288
2668
  );
@@ -2294,6 +2674,10 @@ function addContainerSupportToModel(codegenFs, options, projects) {
2294
2674
  ]);
2295
2675
  if (childType === "IKosDataModel") {
2296
2676
  ensureNamedImport(sf, sdk, [{ name: "IKosDataModel", isTypeOnly: true }]);
2677
+ } else if (childTypeModule) {
2678
+ ensureNamedImport(sf, childTypeModule, [
2679
+ { name: childType, isTypeOnly: true }
2680
+ ]);
2297
2681
  }
2298
2682
  const cls = getModelClass(sf);
2299
2683
  const className = cls.getName();
@@ -2323,6 +2707,140 @@ function addContainerSupportToModel(codegenFs, options, projects) {
2323
2707
  });
2324
2708
  return { modelFilePath };
2325
2709
  }
2710
+ function addParentAwareToModel(codegenFs, options, projects) {
2711
+ const logger = getCodegenLogger();
2712
+ const { modelFilePath } = resolveModelFilePath(codegenFs, options, projects);
2713
+ if (!codegenFs.exists(modelFilePath)) {
2714
+ throw new Error(`Model file not found: ${modelFilePath}`);
2715
+ }
2716
+ logger.info(`Adding parent awareness to model: ${options.modelName}`);
2717
+ let optionsTypeName;
2718
+ transformSourceFile(codegenFs, modelFilePath, (sf) => {
2719
+ const sdk = resolveSdkModuleSpecifier(sf);
2720
+ ensureNamedImport(sf, sdk, [{ name: "kosParentAware" }]);
2721
+ const cls = getModelClass(sf);
2722
+ addClassDecorator(cls, "kosParentAware", {
2723
+ argsText: options.parentId ? `{ parentId: ${JSON.stringify(options.parentId)} }` : ""
2724
+ });
2725
+ const ctor = cls.getConstructors()[0];
2726
+ optionsTypeName = ctor?.getParameters()[1]?.getTypeNode()?.getText()?.replace(/<.*$/, "");
2727
+ });
2728
+ const optionsFilePath = optionsTypeName ? extendOptionsInterface(codegenFs, modelFilePath, optionsTypeName, logger) : void 0;
2729
+ return { modelFilePath, optionsFilePath };
2730
+ }
2731
+ function extendOptionsInterface(codegenFs, modelFilePath, optionsTypeName, logger) {
2732
+ const typesPath = path__namespace.join(
2733
+ path__namespace.dirname(modelFilePath),
2734
+ "types",
2735
+ "index.d.ts"
2736
+ );
2737
+ const target = codegenFs.exists(typesPath) ? typesPath : modelFilePath;
2738
+ let extended = false;
2739
+ transformSourceFile(codegenFs, target, (sf) => {
2740
+ const iface = sf.getInterface(optionsTypeName);
2741
+ if (!iface) return;
2742
+ const already = iface.getExtends().some((clause) => clause.getText().includes("KosParentAware"));
2743
+ if (already) {
2744
+ extended = true;
2745
+ return;
2746
+ }
2747
+ ensureNamedImport(sf, "@kosdev-code/kos-ui-sdk", [
2748
+ { name: "KosParentAware", isTypeOnly: true }
2749
+ ]);
2750
+ iface.addExtends("KosParentAware");
2751
+ extended = true;
2752
+ });
2753
+ if (!extended) {
2754
+ logger.warn(
2755
+ `Could not find interface ${optionsTypeName}; add "extends KosParentAware" to it by hand.`
2756
+ );
2757
+ return void 0;
2758
+ }
2759
+ return target;
2760
+ }
2761
+ function firstDecoratorArg(decoratorText) {
2762
+ const open = decoratorText.indexOf("(");
2763
+ if (open === -1) return void 0;
2764
+ const inner = decoratorText.slice(open + 1, decoratorText.lastIndexOf(")")).replace(/\s+/g, " ").trim();
2765
+ return inner || void 0;
2766
+ }
2767
+ function collectDecorated(cls, decoratorName) {
2768
+ const members = [];
2769
+ const visit = (name, decoratorTextOf, typeText) => {
2770
+ const text = decoratorTextOf();
2771
+ if (text === void 0) return;
2772
+ members.push({
2773
+ name,
2774
+ arg: firstDecoratorArg(text),
2775
+ type: typeText || void 0
2776
+ });
2777
+ };
2778
+ for (const m of cls.getMethods()) {
2779
+ const dec = m.getDecorator(decoratorName);
2780
+ if (dec) {
2781
+ visit(m.getName(), () => dec.getText(), m.getReturnTypeNode()?.getText());
2782
+ }
2783
+ }
2784
+ for (const p of cls.getProperties()) {
2785
+ const dec = p.getDecorator(decoratorName);
2786
+ if (dec) {
2787
+ visit(p.getName(), () => dec.getText(), p.getTypeNode()?.getText());
2788
+ }
2789
+ }
2790
+ return members;
2791
+ }
2792
+ function describeModel(codegenFs, options, projects) {
2793
+ const { modelFilePath } = resolveModelFilePath(codegenFs, options, projects);
2794
+ const content = codegenFs.read(modelFilePath);
2795
+ if (content === null) {
2796
+ throw new Error(`Model file not found: ${modelFilePath}`);
2797
+ }
2798
+ const project = new tsMorph.Project({ useInMemoryFileSystem: true });
2799
+ const sf = project.createSourceFile(modelFilePath, content, {
2800
+ overwrite: true
2801
+ });
2802
+ let modelType;
2803
+ const modelTypeDecl = sf.getVariableDeclaration("MODEL_TYPE");
2804
+ if (modelTypeDecl) {
2805
+ const init = modelTypeDecl.getInitializer()?.getText();
2806
+ if (init) modelType = init.replace(/^["'`]|["'`]$/g, "");
2807
+ }
2808
+ const cls = sf.getClasses().find((c) => c.getDecorator("kosModel")) ?? sf.getClasses().find((c) => c.isExported()) ?? sf.getClasses()[0];
2809
+ if (!cls) {
2810
+ return {
2811
+ modelFilePath,
2812
+ modelType,
2813
+ classDecorators: [],
2814
+ singleton: false,
2815
+ isCompanion: false,
2816
+ children: [],
2817
+ dependencies: [],
2818
+ topicHandlers: [],
2819
+ configProperties: [],
2820
+ serviceRequests: [],
2821
+ effects: [],
2822
+ futures: []
2823
+ };
2824
+ }
2825
+ const classDecorators = cls.getDecorators().map((d) => d.getName());
2826
+ const kosModelArg = cls.getDecorator("kosModel") ? firstDecoratorArg(cls.getDecorator("kosModel").getText()) : void 0;
2827
+ const singleton = /\bsingleton\s*:\s*true\b/.test(kosModelArg ?? "");
2828
+ return {
2829
+ modelFilePath,
2830
+ modelType,
2831
+ className: cls.getName(),
2832
+ classDecorators,
2833
+ singleton,
2834
+ isCompanion: classDecorators.includes("kosCompanion"),
2835
+ children: collectDecorated(cls, "kosChild"),
2836
+ dependencies: collectDecorated(cls, "kosDependency"),
2837
+ topicHandlers: collectDecorated(cls, "kosTopicHandler"),
2838
+ configProperties: collectDecorated(cls, "kosConfigProperty"),
2839
+ serviceRequests: collectDecorated(cls, "kosServiceRequest"),
2840
+ effects: collectDecorated(cls, "kosModelEffect"),
2841
+ futures: collectDecorated(cls, "kosFuture")
2842
+ };
2843
+ }
2326
2844
  function addModelEffectToModel(codegenFs, options, projects) {
2327
2845
  const logger = getCodegenLogger();
2328
2846
  const { modelFilePath } = resolveModelFilePath(codegenFs, options, projects);
@@ -2398,7 +2916,18 @@ function addChildToModel(codegenFs, options, projects) {
2398
2916
  logger.info(
2399
2917
  `Adding @kosChild "${options.propertyName}" (${shape}) to ${options.modelName}`
2400
2918
  );
2401
- const childType = options.childType?.trim();
2919
+ const resolvedChild = options.childModel ? resolveChildModelType(
2920
+ codegenFs,
2921
+ {
2922
+ modelFilePath,
2923
+ modelProject: options.modelProject,
2924
+ childModel: options.childModel,
2925
+ childModelProject: options.childModelProject
2926
+ },
2927
+ projects
2928
+ ) : { childType: options.childType, childTypeModule: options.childPackage };
2929
+ const childType = resolvedChild.childType?.trim();
2930
+ const childTypeModule = resolvedChild.childTypeModule;
2402
2931
  transformSourceFile(codegenFs, modelFilePath, (sf) => {
2403
2932
  const sdk = resolveSdkModuleSpecifier(sf);
2404
2933
  const sdkImports = [
@@ -2406,8 +2935,8 @@ function addChildToModel(codegenFs, options, projects) {
2406
2935
  ];
2407
2936
  if (shape === "container") sdkImports.push({ name: "KosModelContainer" });
2408
2937
  ensureNamedImport(sf, sdk, sdkImports);
2409
- if (options.childPackage && childType && /^[A-Za-z_$][\w$]*$/.test(childType) && !FRAMEWORK_TYPES.has(childType)) {
2410
- ensureNamedImport(sf, options.childPackage, [
2938
+ if (childTypeModule && childType && /^[A-Za-z_$][\w$]*$/.test(childType) && !FRAMEWORK_TYPES.has(childType)) {
2939
+ ensureNamedImport(sf, childTypeModule, [
2411
2940
  { name: childType, isTypeOnly: true }
2412
2941
  ]);
2413
2942
  }
@@ -3116,165 +3645,6 @@ function addServiceRequestToModel(codegenFs, options, projects) {
3116
3645
  }) : void 0
3117
3646
  };
3118
3647
  }
3119
- function modelElementType(typeText) {
3120
- let m;
3121
- if (m = typeText.match(/^([A-Za-z_]\w*Model)\s*\[\]$/)) return m[1];
3122
- if (m = typeText.match(/^Array<\s*([A-Za-z_]\w*Model)\s*>$/)) return m[1];
3123
- if (m = typeText.match(/^(?:Map|Set)<[^>]*?\b([A-Za-z_]\w*Model)\b[^>]*>$/))
3124
- return m[1];
3125
- return null;
3126
- }
3127
- function validateModel(codegenFs, options, projects) {
3128
- const { modelFilePath } = resolveModelFilePath(codegenFs, options, projects);
3129
- const content = codegenFs.read(modelFilePath);
3130
- if (content === null) {
3131
- throw new Error(`Model file not found: ${modelFilePath}`);
3132
- }
3133
- const project = new tsMorph.Project({ useInMemoryFileSystem: true });
3134
- const sf = project.createSourceFile(modelFilePath, content, {
3135
- overwrite: true
3136
- });
3137
- const findings = [];
3138
- const hasKosModel = sf.getClasses().some((c) => c.getDecorator("kosModel"));
3139
- if (!hasKosModel) {
3140
- findings.push({
3141
- level: "error",
3142
- rule: "missing-kosModel",
3143
- message: "No @kosModel decorator found — this is not a KOS model."
3144
- });
3145
- }
3146
- const imports = sf.getImportDeclarations();
3147
- const mobxImport = imports.find(
3148
- (d) => /^mobx(-react-lite)?$/.test(d.getModuleSpecifierValue())
3149
- );
3150
- if (mobxImport) {
3151
- findings.push({
3152
- level: "error",
3153
- rule: "mobx-import",
3154
- message: "Imports mobx directly. Reactivity is internal to KOS — remove the mobx import and use KOS reactivity / container models."
3155
- });
3156
- }
3157
- const barrelServiceRequest = imports.find(
3158
- (d) => d.getModuleSpecifierValue() === "@kosdev-code/kos-ui-sdk" && d.getNamedImports().some((n) => n.getName() === "kosServiceRequest")
3159
- );
3160
- if (barrelServiceRequest) {
3161
- findings.push({
3162
- level: "warning",
3163
- rule: "untyped-service-request",
3164
- 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."
3165
- });
3166
- }
3167
- for (const cls of sf.getClasses()) {
3168
- for (const prop of cls.getProperties()) {
3169
- const name = prop.getName();
3170
- const typeText = (prop.getTypeNode()?.getText() ?? "").replace(/\s+/g, " ").trim();
3171
- const initText = prop.getInitializer()?.getText() ?? "";
3172
- const hasChild = !!prop.getDecorator("kosChild");
3173
- const isModelContainer = /\bI?KosModelContainer\s*</.test(typeText) || /new\s+KosModelContainer\b/.test(initText);
3174
- if (isModelContainer && !hasChild) {
3175
- findings.push({
3176
- level: "warning",
3177
- rule: "container-missing-kosChild",
3178
- message: `Property '${name}' is a model container but is not marked @kosChild — add @kosChild so its models join the model graph and lifecycle.`
3179
- });
3180
- continue;
3181
- }
3182
- const elem = modelElementType(typeText);
3183
- if (elem && !isModelContainer) {
3184
- findings.push({
3185
- level: "warning",
3186
- rule: "raw-model-collection",
3187
- message: `Property '${name}' is a raw ${typeText} of models — prefer a KosModelContainer<${elem}> (indexing, sorting, lifecycle, delta handling) marked @kosChild.`
3188
- });
3189
- }
3190
- }
3191
- }
3192
- const hasError = findings.some((f) => f.level === "error");
3193
- return { modelFilePath, ok: !hasError, findings };
3194
- }
3195
- function firstDecoratorArg(decoratorText) {
3196
- const open = decoratorText.indexOf("(");
3197
- if (open === -1) return void 0;
3198
- const inner = decoratorText.slice(open + 1, decoratorText.lastIndexOf(")")).replace(/\s+/g, " ").trim();
3199
- return inner || void 0;
3200
- }
3201
- function collectDecorated(cls, decoratorName) {
3202
- const members = [];
3203
- const visit = (name, decoratorTextOf, typeText) => {
3204
- const text = decoratorTextOf();
3205
- if (text === void 0) return;
3206
- members.push({
3207
- name,
3208
- arg: firstDecoratorArg(text),
3209
- type: typeText || void 0
3210
- });
3211
- };
3212
- for (const m of cls.getMethods()) {
3213
- const dec = m.getDecorator(decoratorName);
3214
- if (dec) {
3215
- visit(m.getName(), () => dec.getText(), m.getReturnTypeNode()?.getText());
3216
- }
3217
- }
3218
- for (const p of cls.getProperties()) {
3219
- const dec = p.getDecorator(decoratorName);
3220
- if (dec) {
3221
- visit(p.getName(), () => dec.getText(), p.getTypeNode()?.getText());
3222
- }
3223
- }
3224
- return members;
3225
- }
3226
- function describeModel(codegenFs, options, projects) {
3227
- const { modelFilePath } = resolveModelFilePath(codegenFs, options, projects);
3228
- const content = codegenFs.read(modelFilePath);
3229
- if (content === null) {
3230
- throw new Error(`Model file not found: ${modelFilePath}`);
3231
- }
3232
- const project = new tsMorph.Project({ useInMemoryFileSystem: true });
3233
- const sf = project.createSourceFile(modelFilePath, content, {
3234
- overwrite: true
3235
- });
3236
- let modelType;
3237
- const modelTypeDecl = sf.getVariableDeclaration("MODEL_TYPE");
3238
- if (modelTypeDecl) {
3239
- const init = modelTypeDecl.getInitializer()?.getText();
3240
- if (init) modelType = init.replace(/^["'`]|["'`]$/g, "");
3241
- }
3242
- const cls = sf.getClasses().find((c) => c.getDecorator("kosModel")) ?? sf.getClasses().find((c) => c.isExported()) ?? sf.getClasses()[0];
3243
- if (!cls) {
3244
- return {
3245
- modelFilePath,
3246
- modelType,
3247
- classDecorators: [],
3248
- singleton: false,
3249
- isCompanion: false,
3250
- children: [],
3251
- dependencies: [],
3252
- topicHandlers: [],
3253
- configProperties: [],
3254
- serviceRequests: [],
3255
- effects: [],
3256
- futures: []
3257
- };
3258
- }
3259
- const classDecorators = cls.getDecorators().map((d) => d.getName());
3260
- const kosModelArg = cls.getDecorator("kosModel") ? firstDecoratorArg(cls.getDecorator("kosModel").getText()) : void 0;
3261
- const singleton = /\bsingleton\s*:\s*true\b/.test(kosModelArg ?? "");
3262
- return {
3263
- modelFilePath,
3264
- modelType,
3265
- className: cls.getName(),
3266
- classDecorators,
3267
- singleton,
3268
- isCompanion: classDecorators.includes("kosCompanion"),
3269
- children: collectDecorated(cls, "kosChild"),
3270
- dependencies: collectDecorated(cls, "kosDependency"),
3271
- topicHandlers: collectDecorated(cls, "kosTopicHandler"),
3272
- configProperties: collectDecorated(cls, "kosConfigProperty"),
3273
- serviceRequests: collectDecorated(cls, "kosServiceRequest"),
3274
- effects: collectDecorated(cls, "kosModelEffect"),
3275
- futures: collectDecorated(cls, "kosFuture")
3276
- };
3277
- }
3278
3648
  const DEFAULT_SDK_PACKAGE = "@kosdev-code/kos-ui-sdk";
3279
3649
  const MAX_SIGNATURE_CHARS = 2e3;
3280
3650
  function declarationEntryFromPackageJson(pkgDir) {
@@ -3458,6 +3828,82 @@ function lookupSdkType(codegenFs, options, projects) {
3458
3828
  )}). Check the name, or it may be internal / not part of the public surface.`
3459
3829
  };
3460
3830
  }
3831
+ function modelElementType(typeText) {
3832
+ let m;
3833
+ if (m = typeText.match(/^([A-Za-z_]\w*Model)\s*\[\]$/)) return m[1];
3834
+ if (m = typeText.match(/^Array<\s*([A-Za-z_]\w*Model)\s*>$/)) return m[1];
3835
+ if (m = typeText.match(/^(?:Map|Set)<[^>]*?\b([A-Za-z_]\w*Model)\b[^>]*>$/))
3836
+ return m[1];
3837
+ return null;
3838
+ }
3839
+ function validateModel(codegenFs, options, projects) {
3840
+ const { modelFilePath } = resolveModelFilePath(codegenFs, options, projects);
3841
+ const content = codegenFs.read(modelFilePath);
3842
+ if (content === null) {
3843
+ throw new Error(`Model file not found: ${modelFilePath}`);
3844
+ }
3845
+ const project = new tsMorph.Project({ useInMemoryFileSystem: true });
3846
+ const sf = project.createSourceFile(modelFilePath, content, {
3847
+ overwrite: true
3848
+ });
3849
+ const findings = [];
3850
+ const hasKosModel = sf.getClasses().some((c) => c.getDecorator("kosModel"));
3851
+ if (!hasKosModel) {
3852
+ findings.push({
3853
+ level: "error",
3854
+ rule: "missing-kosModel",
3855
+ message: "No @kosModel decorator found — this is not a KOS model."
3856
+ });
3857
+ }
3858
+ const imports = sf.getImportDeclarations();
3859
+ const mobxImport = imports.find(
3860
+ (d) => /^mobx(-react-lite)?$/.test(d.getModuleSpecifierValue())
3861
+ );
3862
+ if (mobxImport) {
3863
+ findings.push({
3864
+ level: "error",
3865
+ rule: "mobx-import",
3866
+ message: "Imports mobx directly. Reactivity is internal to KOS — remove the mobx import and use KOS reactivity / container models."
3867
+ });
3868
+ }
3869
+ const barrelServiceRequest = imports.find(
3870
+ (d) => d.getModuleSpecifierValue() === "@kosdev-code/kos-ui-sdk" && d.getNamedImports().some((n) => n.getName() === "kosServiceRequest")
3871
+ );
3872
+ if (barrelServiceRequest) {
3873
+ findings.push({
3874
+ level: "warning",
3875
+ rule: "untyped-service-request",
3876
+ 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."
3877
+ });
3878
+ }
3879
+ for (const cls of sf.getClasses()) {
3880
+ for (const prop of cls.getProperties()) {
3881
+ const name = prop.getName();
3882
+ const typeText = (prop.getTypeNode()?.getText() ?? "").replace(/\s+/g, " ").trim();
3883
+ const initText = prop.getInitializer()?.getText() ?? "";
3884
+ const hasChild = !!prop.getDecorator("kosChild");
3885
+ const isModelContainer = /\bI?KosModelContainer\s*</.test(typeText) || /new\s+KosModelContainer\b/.test(initText);
3886
+ if (isModelContainer && !hasChild) {
3887
+ findings.push({
3888
+ level: "warning",
3889
+ rule: "container-missing-kosChild",
3890
+ message: `Property '${name}' is a model container but is not marked @kosChild — add @kosChild so its models join the model graph and lifecycle.`
3891
+ });
3892
+ continue;
3893
+ }
3894
+ const elem = modelElementType(typeText);
3895
+ if (elem && !isModelContainer) {
3896
+ findings.push({
3897
+ level: "warning",
3898
+ rule: "raw-model-collection",
3899
+ message: `Property '${name}' is a raw ${typeText} of models — prefer a KosModelContainer<${elem}> (indexing, sorting, lifecycle, delta handling) marked @kosChild.`
3900
+ });
3901
+ }
3902
+ }
3903
+ }
3904
+ const hasError = findings.some((f) => f.level === "error");
3905
+ return { modelFilePath, ok: !hasError, findings };
3906
+ }
3461
3907
  const PLUGIN_TYPES = {
3462
3908
  CUI: "cui",
3463
3909
  UTILITY: "utility",
@@ -4012,8 +4458,75 @@ function updatePluginConfiguration(codegenFs, projectConfig, options) {
4012
4458
  );
4013
4459
  }
4014
4460
  }
4461
+ function parse(content) {
4462
+ const singleQuoted = /^\s*import\b[^"']*'/m.test(content);
4463
+ const project = new tsMorph.Project({
4464
+ useInMemoryFileSystem: true,
4465
+ manipulationSettings: {
4466
+ indentationText: tsMorph.IndentationText.TwoSpaces,
4467
+ quoteKind: singleQuoted ? tsMorph.QuoteKind.Single : tsMorph.QuoteKind.Double
4468
+ }
4469
+ });
4470
+ return project.createSourceFile("registration-chain.ts", content, {
4471
+ overwrite: true
4472
+ });
4473
+ }
4474
+ function chainCalls(sf, name) {
4475
+ return sf.getDescendantsOfKind(tsMorph.SyntaxKind.CallExpression).filter((call) => {
4476
+ const callee = call.getExpression();
4477
+ return callee.getKind() === tsMorph.SyntaxKind.PropertyAccessExpression && callee.asKindOrThrow(tsMorph.SyntaxKind.PropertyAccessExpression).getName() === name;
4478
+ });
4479
+ }
4480
+ function indentOfCall(sf, call) {
4481
+ const text = sf.getFullText();
4482
+ const nameNode = call.getExpression().asKindOrThrow(tsMorph.SyntaxKind.PropertyAccessExpression).getNameNode();
4483
+ const lineStart = text.lastIndexOf("\n", nameNode.getStart()) + 1;
4484
+ return text.slice(lineStart).match(/^[ \t]*/)?.[0] ?? "";
4485
+ }
4486
+ function countChainEntries(content) {
4487
+ return chainCalls(parse(content), "model").length;
4488
+ }
4489
+ function upsertChainEntry(content, bean, importSpec) {
4490
+ const sf = parse(content);
4491
+ const modelCalls = chainCalls(sf, "model");
4492
+ const registered = modelCalls.some((call) => {
4493
+ const [arg, ...rest] = call.getArguments();
4494
+ return rest.length === 0 && arg?.getText() === bean;
4495
+ });
4496
+ if (registered) return { changed: false, note: "already registered" };
4497
+ let anchor;
4498
+ let indent;
4499
+ if (modelCalls.length > 0) {
4500
+ anchor = modelCalls.reduce(
4501
+ (last, call) => call.getEnd() > last.getEnd() ? call : last
4502
+ );
4503
+ indent = indentOfCall(sf, anchor);
4504
+ } else {
4505
+ const starts = chainCalls(sf, "models").filter(
4506
+ (call) => call.getArguments().length === 0
4507
+ );
4508
+ anchor = starts[starts.length - 1];
4509
+ if (!anchor) {
4510
+ return {
4511
+ changed: false,
4512
+ error: "no .models() chain found in the registration file"
4513
+ };
4514
+ }
4515
+ const text = sf.getFullText();
4516
+ const lineStart = text.lastIndexOf("\n", anchor.getStart()) + 1;
4517
+ indent = (text.slice(lineStart).match(/^[ \t]*/)?.[0] ?? "") + " ";
4518
+ }
4519
+ anchor.replaceWithText(`${anchor.getText()}
4520
+ ${indent}.model(${bean})`);
4521
+ const imported = sf.getImportDeclarations().some(
4522
+ (decl) => decl.getNamedImports().some((named) => named.getName() === bean)
4523
+ );
4524
+ if (!imported) ensureNamedImport(sf, importSpec, [{ name: bean }]);
4525
+ return { changed: true, content: sf.getFullText(), importAdded: !imported };
4526
+ }
4015
4527
  exports.BasePluginHandler = BasePluginHandler;
4016
4528
  exports.CONTRIBUTION_TYPE_MAP = CONTRIBUTION_TYPE_MAP;
4529
+ exports.DEFAULT_MODEL_LIBS_DIR = DEFAULT_MODEL_LIBS_DIR;
4017
4530
  exports.DirectFileSystem = DirectFileSystem;
4018
4531
  exports.KAB_OUTPUT_DIR = KAB_OUTPUT_DIR;
4019
4532
  exports.KAB_OUTPUT_PATH = KAB_OUTPUT_PATH;
@@ -4035,6 +4548,7 @@ exports.addFutureToModel = addFutureToModel;
4035
4548
  exports.addJavaArtifactToManifests = addJavaArtifactToManifests;
4036
4549
  exports.addKosModelConfiguration = addKosModelConfiguration;
4037
4550
  exports.addModelEffectToModel = addModelEffectToModel;
4551
+ exports.addParentAwareToModel = addParentAwareToModel;
4038
4552
  exports.addPropertyToModel = addPropertyToModel;
4039
4553
  exports.addServiceRequestToModel = addServiceRequestToModel;
4040
4554
  exports.addTopicHandlerToModel = addTopicHandlerToModel;
@@ -4043,6 +4557,7 @@ exports.buildKabTargets = buildKabTargets;
4043
4557
  exports.buildSbomTarget = buildSbomTarget;
4044
4558
  exports.camelCase = camelCase;
4045
4559
  exports.constantCase = constantCase;
4560
+ exports.countChainEntries = countChainEntries;
4046
4561
  exports.dashCase = dashCase;
4047
4562
  exports.describeModel = describeModel;
4048
4563
  exports.discoverJavaArtifacts = discoverJavaArtifacts;
@@ -4061,8 +4576,10 @@ exports.generateFilesFromTemplates = generateFilesFromTemplates;
4061
4576
  exports.generateHook = generateHook;
4062
4577
  exports.generateInit = generateInit;
4063
4578
  exports.generateModel = generateModel;
4579
+ exports.generateModelProject = generateModelProject;
4064
4580
  exports.generatePolyglotWorkspace = generatePolyglotWorkspace;
4065
4581
  exports.generateSplashProject = generateSplashProject;
4582
+ exports.generateViewModel = generateViewModel;
4066
4583
  exports.getCodegenLogger = getCodegenLogger;
4067
4584
  exports.getCurrentDirectoryName = getCurrentDirectoryName;
4068
4585
  exports.getKosModelConfigProp = getKosModelConfigProp;
@@ -4080,10 +4597,12 @@ exports.readJson = readJson;
4080
4597
  exports.readNxJson = readNxJson;
4081
4598
  exports.resolveKabPath = resolveKabPath;
4082
4599
  exports.resolveModelFilePath = resolveModelFilePath;
4600
+ exports.resolveModelProjectLayout = resolveModelProjectLayout;
4083
4601
  exports.setCodegenLogger = setCodegenLogger;
4084
4602
  exports.syncCiManifests = syncCiManifests;
4085
4603
  exports.updateJson = updateJson;
4086
4604
  exports.updateModelIndex = updateModelIndex;
4605
+ exports.upsertChainEntry = upsertChainEntry;
4087
4606
  exports.validateModel = validateModel;
4088
4607
  exports.writeJson = writeJson;
4089
4608
  //# sourceMappingURL=index.js.map