@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.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) {
@@ -1120,6 +1250,40 @@ function ensureBarrelExport(sourceFile, moduleSpecifier) {
1120
1250
  sourceFile.addExportDeclaration({ moduleSpecifier });
1121
1251
  return true;
1122
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
+ }
1123
1287
  function addDecoratedMethod(cls, spec) {
1124
1288
  if (cls.getMethod(spec.name)) return false;
1125
1289
  cls.addMethod({
@@ -1272,58 +1436,6 @@ function readSummary(methodProp) {
1272
1436
  function unquote(name) {
1273
1437
  return name.replace(/^["']|["']$/g, "");
1274
1438
  }
1275
- const UPDATE_RELEASE_VERSION_SCRIPT = `import devkit from "@nx/devkit";
1276
- import { resolve } from "path";
1277
- import { readFileSync, writeFileSync } from "fs";
1278
- import prettier from "prettier";
1279
-
1280
- // KOS artifact versioning: stamps the project's .kos.json "version" field
1281
- // (which kabtool bakes into the KAB). Never touches package.json.
1282
- // Driven by tag-based releases:
1283
- // nx run-many --target=version --args=--ver=$KOSBUILD_VERSION
1284
-
1285
- const { readCachedProjectGraph } = devkit;
1286
- const [, , name, versionArg] = process.argv;
1287
-
1288
- // "{args.ver}" arrives literally when the target runs without --args=--ver=<v>;
1289
- // treat that (or a missing arg) as "report current version, change nothing".
1290
- const version =
1291
- versionArg && !versionArg.startsWith("{args") ? versionArg : undefined;
1292
-
1293
- if (!name) {
1294
- console.error("usage: update-release-version.mjs <project> <version>");
1295
- process.exit(1);
1296
- }
1297
-
1298
- const graph = readCachedProjectGraph();
1299
- const project = graph.nodes[name];
1300
- if (!project) {
1301
- console.error("Unknown project: " + name);
1302
- process.exit(1);
1303
- }
1304
-
1305
- const kosJsonPath = resolve(process.cwd(), project.data.root, ".kos.json");
1306
- let kosJson;
1307
- try {
1308
- kosJson = JSON.parse(readFileSync(kosJsonPath, "utf8"));
1309
- } catch {
1310
- console.error("Missing or invalid .kos.json: " + kosJsonPath);
1311
- process.exit(1);
1312
- }
1313
-
1314
- if (!version) {
1315
- console.log(name + ": " + kosJson.version + " (no --ver given; unchanged)");
1316
- process.exit(0);
1317
- }
1318
-
1319
- const prettierOptions = await prettier.resolveConfig(kosJsonPath);
1320
- const output = await prettier.format(
1321
- JSON.stringify({ ...kosJson, version }, null, 2),
1322
- { ...prettierOptions, parser: "json" }
1323
- );
1324
- writeFileSync(kosJsonPath, output);
1325
- console.log(name + ": version -> " + version);
1326
- `;
1327
1439
  function appendBarrelExport(codegenFs, indexPath, exportPath) {
1328
1440
  const exportLine = `export * from '${exportPath}'`;
1329
1441
  const content = codegenFs.read(indexPath) ?? "";
@@ -1370,10 +1482,57 @@ function updateModelIndex(codegenFs, indexPath, modelPath) {
1370
1482
  const newContents = printer.printFile(updatedSourceFile);
1371
1483
  codegenFs.write(indexPath, newContents);
1372
1484
  }
1373
- function generateHook(codegenFs, templateDir, options, cwd, projects) {
1374
- if (!options.appProject) {
1375
- 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
+ }
1376
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();
1377
1536
  const currentProject = getProject(codegenFs, cwd);
1378
1537
  const modelProjectName = options.modelProject || currentProject?.name;
1379
1538
  if (!modelProjectName) {
@@ -1381,56 +1540,109 @@ function generateHook(codegenFs, templateDir, options, cwd, projects) {
1381
1540
  "No model project found. Please specify a model project with --modelProject."
1382
1541
  );
1383
1542
  }
1384
- const modelName = options.name || getCurrentDirectoryName(cwd);
1543
+ const modelName = options.modelName || getCurrentDirectoryName(cwd);
1385
1544
  if (!modelName) {
1386
1545
  throw new Error(
1387
1546
  "No model name found. Please specify a model name with --name."
1388
1547
  );
1389
1548
  }
1390
- const kosModelConfig = getKosModelConfiguration(
1391
- codegenFs,
1392
- modelProjectName,
1393
- modelName,
1394
- projects
1395
- );
1396
- options.singleton = kosModelConfig ? !!kosModelConfig.singleton : false;
1397
- options.name = modelName;
1398
- options.modelProject = modelProjectName;
1549
+ options.modelName = modelName;
1550
+ options.name = `${modelName}-container`;
1399
1551
  const normalized = normalizeOptions(codegenFs, options, projects);
1400
- const appProject = findProjectByName(
1552
+ const projectConfig = findProjectByName(
1401
1553
  codegenFs.root,
1402
- normalized.appProject,
1554
+ normalized.modelProject,
1403
1555
  projects
1404
1556
  );
1405
- if (!appProject) {
1406
- throw new Error(`App project '${normalized.appProject}' not found`);
1557
+ if (!projectConfig) {
1558
+ throw new Error(`Model project '${normalized.modelProject}' not found`);
1407
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
+ });
1408
1570
  const kosConfig = getKosProjectConfiguration(
1409
1571
  codegenFs,
1410
- appProject.name,
1572
+ projectConfig.name,
1411
1573
  projects
1412
1574
  );
1413
- const componentLocation = kosConfig?.generator?.defaults?.component?.folder || "";
1414
- options.appDirectory = options.appDirectory || componentLocation;
1415
- 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;
1416
1579
  if (projectRoot) {
1417
- generateFilesFromTemplates(
1418
- codegenFs,
1419
- templateDir,
1420
- path.join(
1421
- projectRoot,
1422
- options.appDirectory,
1423
- "hooks",
1424
- normalized.nameDashCase
1425
- ),
1426
- normalized
1580
+ logger.info(
1581
+ `Generating container model ${normalized.nameDashCase} in ${projectRoot}`
1427
1582
  );
1428
- appendBarrelExport(
1429
- codegenFs,
1430
- path.join(projectRoot, options.appDirectory, "hooks", "index.ts"),
1431
- `./${normalized.nameDashCase}`
1583
+ const modelNameDashCase = normalized.modelNameDashCase || normalized.nameDashCase;
1584
+ const modelFolder = path.join(
1585
+ projectRoot,
1586
+ options.modelDirectory || "",
1587
+ modelNameDashCase
1432
1588
  );
1433
- }
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`
1628
+ );
1629
+ });
1630
+ const barrelPath = path.join(modelFolder, "index.ts");
1631
+ if (codegenFs.read(barrelPath) === null) {
1632
+ codegenFs.write(barrelPath, "");
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
+ });
1434
1646
  }
1435
1647
  function generateContext(codegenFs, templateDir, options, cwd, projects) {
1436
1648
  if (!options.appProject) {
@@ -1484,8 +1696,10 @@ function generateContext(codegenFs, templateDir, options, cwd, projects) {
1484
1696
  );
1485
1697
  }
1486
1698
  }
1487
- function generateContainerModel(codegenFs, templateDir, options, cwd, projects) {
1488
- const logger = getCodegenLogger();
1699
+ function generateHook(codegenFs, templateDir, options, cwd, projects) {
1700
+ if (!options.appProject) {
1701
+ throw new Error("No app project specified");
1702
+ }
1489
1703
  const currentProject = getProject(codegenFs, cwd);
1490
1704
  const modelProjectName = options.modelProject || currentProject?.name;
1491
1705
  if (!modelProjectName) {
@@ -1493,106 +1707,56 @@ function generateContainerModel(codegenFs, templateDir, options, cwd, projects)
1493
1707
  "No model project found. Please specify a model project with --modelProject."
1494
1708
  );
1495
1709
  }
1496
- const modelName = options.modelName || getCurrentDirectoryName(cwd);
1710
+ const modelName = options.name || getCurrentDirectoryName(cwd);
1497
1711
  if (!modelName) {
1498
1712
  throw new Error(
1499
1713
  "No model name found. Please specify a model name with --name."
1500
1714
  );
1501
1715
  }
1502
- options.modelName = modelName;
1503
- 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;
1504
1725
  const normalized = normalizeOptions(codegenFs, options, projects);
1505
- const projectConfig = findProjectByName(
1726
+ const appProject = findProjectByName(
1506
1727
  codegenFs.root,
1507
- normalized.modelProject,
1728
+ normalized.appProject,
1508
1729
  projects
1509
1730
  );
1510
- if (!projectConfig) {
1511
- throw new Error(`Model project '${normalized.modelProject}' not found`);
1731
+ if (!appProject) {
1732
+ throw new Error(`App project '${normalized.appProject}' not found`);
1512
1733
  }
1513
- addKosModelConfiguration({
1514
- codegenFs,
1515
- modelName: normalized.nameDashCase,
1516
- projectName: projectConfig.name,
1517
- projectRoot: projectConfig.root,
1518
- singleton: !!options.singleton,
1519
- container: true,
1520
- // The container's exported registration bean (`export const <ProperCase>`).
1521
- factory: normalized.nameProperCase
1522
- });
1523
1734
  const kosConfig = getKosProjectConfiguration(
1524
1735
  codegenFs,
1525
- projectConfig.name,
1736
+ appProject.name,
1526
1737
  projects
1527
1738
  );
1528
- const modelLocation = kosConfig?.generator?.defaults?.model?.folder || "";
1529
- const internal = !!kosConfig?.generator?.internal;
1530
- options.modelDirectory = options.modelDirectory || modelLocation;
1531
- const projectRoot = projectConfig.sourceRoot;
1739
+ const componentLocation = kosConfig?.generator?.defaults?.component?.folder || "";
1740
+ options.appDirectory = options.appDirectory || componentLocation;
1741
+ const projectRoot = appProject.sourceRoot;
1532
1742
  if (projectRoot) {
1533
- logger.info(
1534
- `Generating container model ${normalized.nameDashCase} in ${projectRoot}`
1535
- );
1536
- const modelNameDashCase = normalized.modelNameDashCase || normalized.nameDashCase;
1537
1743
  generateFilesFromTemplates(
1538
1744
  codegenFs,
1539
- path.join(templateDir, "model"),
1540
- path.join(projectRoot, options.modelDirectory || "", modelNameDashCase),
1541
- { ...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}`
1542
1758
  );
1543
- const modelIndex = path.join(projectRoot, "index.ts");
1544
- const modelPath = normalized.modelDirectory ? `${normalized.modelDirectory}/${modelNameDashCase}` : modelNameDashCase;
1545
- updateModelIndex(codegenFs, modelIndex, modelPath);
1546
- }
1547
- }
1548
- function generateCompanionModel(codegenFs, templateDir, options, projects) {
1549
- const logger = getCodegenLogger();
1550
- const normalized = normalizeAllValues({
1551
- companionModelName: options.companionModelName,
1552
- modelName: options.modelName
1553
- });
1554
- const companionChildKosConfig = getKosProjectConfiguration(
1555
- codegenFs,
1556
- options.companionModelProject,
1557
- projects
1558
- );
1559
- const parentProject = findProjectByName(
1560
- codegenFs.root,
1561
- options.modelProject,
1562
- projects
1563
- );
1564
- const childProject = findProjectByName(
1565
- codegenFs.root,
1566
- options.companionModelProject,
1567
- projects
1568
- );
1569
- const projectRoot = childProject?.sourceRoot;
1570
- if (!projectRoot) {
1571
- logger.warn(`Companion child project source root not found`);
1572
- return;
1573
- }
1574
- let importPath = "";
1575
- if (parentProject) {
1576
- const pkgJsonPath = path.join(parentProject.root, "package.json");
1577
- try {
1578
- const pkgJson = readJson(codegenFs, pkgJsonPath);
1579
- importPath = pkgJson.name || "";
1580
- } catch {
1581
- importPath = "";
1582
- }
1583
1759
  }
1584
- const modelLocation = companionChildKosConfig?.generator?.defaults?.model?.folder || "";
1585
- const filePath = path.join(
1586
- projectRoot,
1587
- modelLocation,
1588
- normalized.companionModelNameDashCase
1589
- );
1590
- logger.info(`Generating companion model in ${filePath}`);
1591
- generateFilesFromTemplates(codegenFs, templateDir, filePath, {
1592
- ...options,
1593
- ...normalized,
1594
- importPath
1595
- });
1596
1760
  }
1597
1761
  function generateModel(params) {
1598
1762
  const {
@@ -1691,6 +1855,55 @@ function generateModel(params) {
1691
1855
  );
1692
1856
  }
1693
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
+ }
1694
1907
  function resolveModelFilePath(codegenFs, query, projects) {
1695
1908
  const kosConfig = getKosProjectConfiguration(
1696
1909
  codegenFs,
@@ -1725,14 +1938,23 @@ function resolveModelFilePath(codegenFs, query, projects) {
1725
1938
  if (codegenFs.exists(modelFilePath)) {
1726
1939
  return { modelFilePath, internal, sourceRoot };
1727
1940
  }
1941
+ const searchRoot = path.join(sourceRoot, modelLocation);
1728
1942
  const discovered = findModelFileByName(
1729
1943
  codegenFs,
1730
- path.join(sourceRoot, modelLocation),
1944
+ searchRoot,
1731
1945
  modelNameDashCase
1732
1946
  );
1733
1947
  if (discovered) {
1734
1948
  return { modelFilePath: discovered, internal, sourceRoot };
1735
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
+ }
1736
1958
  return { modelFilePath, internal, sourceRoot };
1737
1959
  }
1738
1960
  function findModelFileByName(codegenFs, searchRoot, modelNameDashCase) {
@@ -1748,6 +1970,150 @@ Pass modelPath to pick one.`
1748
1970
  }
1749
1971
  return candidates[0];
1750
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
+ }
1751
2117
  function modelBaseName(modelFilePath, modelName) {
1752
2118
  const base = path.basename(modelFilePath);
1753
2119
  const match = base.match(/^(.*)-model\.ts$/);
@@ -2256,11 +2622,25 @@ function buildDecoratorArgs(options) {
2256
2622
  }
2257
2623
  function addContainerSupportToModel(codegenFs, options, projects) {
2258
2624
  const logger = getCodegenLogger();
2259
- const childType = options.childType?.trim() || "IKosDataModel";
2260
2625
  const { modelFilePath } = resolveModelFilePath(codegenFs, options, projects);
2261
2626
  if (!codegenFs.exists(modelFilePath)) {
2262
2627
  throw new Error(`Model file not found: ${modelFilePath}`);
2263
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;
2264
2644
  logger.info(
2265
2645
  `Adding container support (<${childType}>) to model: ${options.modelName}`
2266
2646
  );
@@ -2272,6 +2652,10 @@ function addContainerSupportToModel(codegenFs, options, projects) {
2272
2652
  ]);
2273
2653
  if (childType === "IKosDataModel") {
2274
2654
  ensureNamedImport(sf, sdk, [{ name: "IKosDataModel", isTypeOnly: true }]);
2655
+ } else if (childTypeModule) {
2656
+ ensureNamedImport(sf, childTypeModule, [
2657
+ { name: childType, isTypeOnly: true }
2658
+ ]);
2275
2659
  }
2276
2660
  const cls = getModelClass(sf);
2277
2661
  const className = cls.getName();
@@ -2301,6 +2685,140 @@ function addContainerSupportToModel(codegenFs, options, projects) {
2301
2685
  });
2302
2686
  return { modelFilePath };
2303
2687
  }
2688
+ function addParentAwareToModel(codegenFs, options, projects) {
2689
+ const logger = getCodegenLogger();
2690
+ const { modelFilePath } = resolveModelFilePath(codegenFs, options, projects);
2691
+ if (!codegenFs.exists(modelFilePath)) {
2692
+ throw new Error(`Model file not found: ${modelFilePath}`);
2693
+ }
2694
+ logger.info(`Adding parent awareness to model: ${options.modelName}`);
2695
+ let optionsTypeName;
2696
+ transformSourceFile(codegenFs, modelFilePath, (sf) => {
2697
+ const sdk = resolveSdkModuleSpecifier(sf);
2698
+ ensureNamedImport(sf, sdk, [{ name: "kosParentAware" }]);
2699
+ const cls = getModelClass(sf);
2700
+ addClassDecorator(cls, "kosParentAware", {
2701
+ argsText: options.parentId ? `{ parentId: ${JSON.stringify(options.parentId)} }` : ""
2702
+ });
2703
+ const ctor = cls.getConstructors()[0];
2704
+ optionsTypeName = ctor?.getParameters()[1]?.getTypeNode()?.getText()?.replace(/<.*$/, "");
2705
+ });
2706
+ const optionsFilePath = optionsTypeName ? extendOptionsInterface(codegenFs, modelFilePath, optionsTypeName, logger) : void 0;
2707
+ return { modelFilePath, optionsFilePath };
2708
+ }
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
+ }
2304
2822
  function addModelEffectToModel(codegenFs, options, projects) {
2305
2823
  const logger = getCodegenLogger();
2306
2824
  const { modelFilePath } = resolveModelFilePath(codegenFs, options, projects);
@@ -2376,7 +2894,18 @@ function addChildToModel(codegenFs, options, projects) {
2376
2894
  logger.info(
2377
2895
  `Adding @kosChild "${options.propertyName}" (${shape}) to ${options.modelName}`
2378
2896
  );
2379
- 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;
2380
2909
  transformSourceFile(codegenFs, modelFilePath, (sf) => {
2381
2910
  const sdk = resolveSdkModuleSpecifier(sf);
2382
2911
  const sdkImports = [
@@ -2384,8 +2913,8 @@ function addChildToModel(codegenFs, options, projects) {
2384
2913
  ];
2385
2914
  if (shape === "container") sdkImports.push({ name: "KosModelContainer" });
2386
2915
  ensureNamedImport(sf, sdk, sdkImports);
2387
- if (options.childPackage && childType && /^[A-Za-z_$][\w$]*$/.test(childType) && !FRAMEWORK_TYPES.has(childType)) {
2388
- ensureNamedImport(sf, options.childPackage, [
2916
+ if (childTypeModule && childType && /^[A-Za-z_$][\w$]*$/.test(childType) && !FRAMEWORK_TYPES.has(childType)) {
2917
+ ensureNamedImport(sf, childTypeModule, [
2389
2918
  { name: childType, isTypeOnly: true }
2390
2919
  ]);
2391
2920
  }
@@ -3094,165 +3623,6 @@ function addServiceRequestToModel(codegenFs, options, projects) {
3094
3623
  }) : void 0
3095
3624
  };
3096
3625
  }
3097
- function modelElementType(typeText) {
3098
- let m;
3099
- if (m = typeText.match(/^([A-Za-z_]\w*Model)\s*\[\]$/)) return m[1];
3100
- if (m = typeText.match(/^Array<\s*([A-Za-z_]\w*Model)\s*>$/)) return m[1];
3101
- if (m = typeText.match(/^(?:Map|Set)<[^>]*?\b([A-Za-z_]\w*Model)\b[^>]*>$/))
3102
- return m[1];
3103
- return null;
3104
- }
3105
- function validateModel(codegenFs, options, projects) {
3106
- const { modelFilePath } = resolveModelFilePath(codegenFs, options, projects);
3107
- const content = codegenFs.read(modelFilePath);
3108
- if (content === null) {
3109
- throw new Error(`Model file not found: ${modelFilePath}`);
3110
- }
3111
- const project = new Project({ useInMemoryFileSystem: true });
3112
- const sf = project.createSourceFile(modelFilePath, content, {
3113
- overwrite: true
3114
- });
3115
- const findings = [];
3116
- const hasKosModel = sf.getClasses().some((c) => c.getDecorator("kosModel"));
3117
- if (!hasKosModel) {
3118
- findings.push({
3119
- level: "error",
3120
- rule: "missing-kosModel",
3121
- message: "No @kosModel decorator found — this is not a KOS model."
3122
- });
3123
- }
3124
- const imports = sf.getImportDeclarations();
3125
- const mobxImport = imports.find(
3126
- (d) => /^mobx(-react-lite)?$/.test(d.getModuleSpecifierValue())
3127
- );
3128
- if (mobxImport) {
3129
- findings.push({
3130
- level: "error",
3131
- rule: "mobx-import",
3132
- message: "Imports mobx directly. Reactivity is internal to KOS — remove the mobx import and use KOS reactivity / container models."
3133
- });
3134
- }
3135
- const barrelServiceRequest = imports.find(
3136
- (d) => d.getModuleSpecifierValue() === "@kosdev-code/kos-ui-sdk" && d.getNamedImports().some((n) => n.getName() === "kosServiceRequest")
3137
- );
3138
- if (barrelServiceRequest) {
3139
- findings.push({
3140
- level: "warning",
3141
- rule: "untyped-service-request",
3142
- 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."
3143
- });
3144
- }
3145
- for (const cls of sf.getClasses()) {
3146
- for (const prop of cls.getProperties()) {
3147
- const name = prop.getName();
3148
- const typeText = (prop.getTypeNode()?.getText() ?? "").replace(/\s+/g, " ").trim();
3149
- const initText = prop.getInitializer()?.getText() ?? "";
3150
- const hasChild = !!prop.getDecorator("kosChild");
3151
- const isModelContainer = /\bI?KosModelContainer\s*</.test(typeText) || /new\s+KosModelContainer\b/.test(initText);
3152
- if (isModelContainer && !hasChild) {
3153
- findings.push({
3154
- level: "warning",
3155
- rule: "container-missing-kosChild",
3156
- message: `Property '${name}' is a model container but is not marked @kosChild — add @kosChild so its models join the model graph and lifecycle.`
3157
- });
3158
- continue;
3159
- }
3160
- const elem = modelElementType(typeText);
3161
- if (elem && !isModelContainer) {
3162
- findings.push({
3163
- level: "warning",
3164
- rule: "raw-model-collection",
3165
- message: `Property '${name}' is a raw ${typeText} of models — prefer a KosModelContainer<${elem}> (indexing, sorting, lifecycle, delta handling) marked @kosChild.`
3166
- });
3167
- }
3168
- }
3169
- }
3170
- const hasError = findings.some((f) => f.level === "error");
3171
- return { modelFilePath, ok: !hasError, findings };
3172
- }
3173
- function firstDecoratorArg(decoratorText) {
3174
- const open = decoratorText.indexOf("(");
3175
- if (open === -1) return void 0;
3176
- const inner = decoratorText.slice(open + 1, decoratorText.lastIndexOf(")")).replace(/\s+/g, " ").trim();
3177
- return inner || void 0;
3178
- }
3179
- function collectDecorated(cls, decoratorName) {
3180
- const members = [];
3181
- const visit = (name, decoratorTextOf, typeText) => {
3182
- const text = decoratorTextOf();
3183
- if (text === void 0) return;
3184
- members.push({
3185
- name,
3186
- arg: firstDecoratorArg(text),
3187
- type: typeText || void 0
3188
- });
3189
- };
3190
- for (const m of cls.getMethods()) {
3191
- const dec = m.getDecorator(decoratorName);
3192
- if (dec) {
3193
- visit(m.getName(), () => dec.getText(), m.getReturnTypeNode()?.getText());
3194
- }
3195
- }
3196
- for (const p of cls.getProperties()) {
3197
- const dec = p.getDecorator(decoratorName);
3198
- if (dec) {
3199
- visit(p.getName(), () => dec.getText(), p.getTypeNode()?.getText());
3200
- }
3201
- }
3202
- return members;
3203
- }
3204
- function describeModel(codegenFs, options, projects) {
3205
- const { modelFilePath } = resolveModelFilePath(codegenFs, options, projects);
3206
- const content = codegenFs.read(modelFilePath);
3207
- if (content === null) {
3208
- throw new Error(`Model file not found: ${modelFilePath}`);
3209
- }
3210
- const project = new Project({ useInMemoryFileSystem: true });
3211
- const sf = project.createSourceFile(modelFilePath, content, {
3212
- overwrite: true
3213
- });
3214
- let modelType;
3215
- const modelTypeDecl = sf.getVariableDeclaration("MODEL_TYPE");
3216
- if (modelTypeDecl) {
3217
- const init = modelTypeDecl.getInitializer()?.getText();
3218
- if (init) modelType = init.replace(/^["'`]|["'`]$/g, "");
3219
- }
3220
- const cls = sf.getClasses().find((c) => c.getDecorator("kosModel")) ?? sf.getClasses().find((c) => c.isExported()) ?? sf.getClasses()[0];
3221
- if (!cls) {
3222
- return {
3223
- modelFilePath,
3224
- modelType,
3225
- classDecorators: [],
3226
- singleton: false,
3227
- isCompanion: false,
3228
- children: [],
3229
- dependencies: [],
3230
- topicHandlers: [],
3231
- configProperties: [],
3232
- serviceRequests: [],
3233
- effects: [],
3234
- futures: []
3235
- };
3236
- }
3237
- const classDecorators = cls.getDecorators().map((d) => d.getName());
3238
- const kosModelArg = cls.getDecorator("kosModel") ? firstDecoratorArg(cls.getDecorator("kosModel").getText()) : void 0;
3239
- const singleton = /\bsingleton\s*:\s*true\b/.test(kosModelArg ?? "");
3240
- return {
3241
- modelFilePath,
3242
- modelType,
3243
- className: cls.getName(),
3244
- classDecorators,
3245
- singleton,
3246
- isCompanion: classDecorators.includes("kosCompanion"),
3247
- children: collectDecorated(cls, "kosChild"),
3248
- dependencies: collectDecorated(cls, "kosDependency"),
3249
- topicHandlers: collectDecorated(cls, "kosTopicHandler"),
3250
- configProperties: collectDecorated(cls, "kosConfigProperty"),
3251
- serviceRequests: collectDecorated(cls, "kosServiceRequest"),
3252
- effects: collectDecorated(cls, "kosModelEffect"),
3253
- futures: collectDecorated(cls, "kosFuture")
3254
- };
3255
- }
3256
3626
  const DEFAULT_SDK_PACKAGE = "@kosdev-code/kos-ui-sdk";
3257
3627
  const MAX_SIGNATURE_CHARS = 2e3;
3258
3628
  function declarationEntryFromPackageJson(pkgDir) {
@@ -3436,6 +3806,82 @@ function lookupSdkType(codegenFs, options, projects) {
3436
3806
  )}). Check the name, or it may be internal / not part of the public surface.`
3437
3807
  };
3438
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
+ }
3439
3885
  const PLUGIN_TYPES = {
3440
3886
  CUI: "cui",
3441
3887
  UTILITY: "utility",
@@ -3990,9 +4436,76 @@ function updatePluginConfiguration(codegenFs, projectConfig, options) {
3990
4436
  );
3991
4437
  }
3992
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
+ }
3993
4505
  export {
3994
4506
  BasePluginHandler,
3995
4507
  CONTRIBUTION_TYPE_MAP,
4508
+ DEFAULT_MODEL_LIBS_DIR,
3996
4509
  DirectFileSystem,
3997
4510
  KAB_OUTPUT_DIR,
3998
4511
  KAB_OUTPUT_PATH,
@@ -4014,6 +4527,7 @@ export {
4014
4527
  addJavaArtifactToManifests,
4015
4528
  addKosModelConfiguration,
4016
4529
  addModelEffectToModel,
4530
+ addParentAwareToModel,
4017
4531
  addPropertyToModel,
4018
4532
  addServiceRequestToModel,
4019
4533
  addTopicHandlerToModel,
@@ -4022,6 +4536,7 @@ export {
4022
4536
  buildSbomTarget,
4023
4537
  camelCase,
4024
4538
  constantCase,
4539
+ countChainEntries,
4025
4540
  dashCase,
4026
4541
  describeModel,
4027
4542
  discoverJavaArtifacts,
@@ -4040,8 +4555,10 @@ export {
4040
4555
  generateHook,
4041
4556
  generateInit,
4042
4557
  generateModel,
4558
+ generateModelProject,
4043
4559
  generatePolyglotWorkspace,
4044
4560
  generateSplashProject,
4561
+ generateViewModel,
4045
4562
  getCodegenLogger,
4046
4563
  getCurrentDirectoryName,
4047
4564
  getKosModelConfigProp,
@@ -4059,10 +4576,12 @@ export {
4059
4576
  readNxJson,
4060
4577
  resolveKabPath,
4061
4578
  resolveModelFilePath,
4579
+ resolveModelProjectLayout,
4062
4580
  setCodegenLogger,
4063
4581
  syncCiManifests,
4064
4582
  updateJson,
4065
4583
  updateModelIndex,
4584
+ upsertChainEntry,
4066
4585
  validateModel,
4067
4586
  writeJson
4068
4587
  };