@kosdev-code/kos-codegen-core 0.1.0-next.871 → 0.1.0-next.894
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.d.ts +2 -2
- package/index.d.ts.map +1 -1
- package/index.js +872 -335
- package/index.js.map +1 -1
- package/index.mjs +873 -336
- package/index.mjs.map +1 -1
- package/lib/generators/add-container-support/generate-add-container-support.d.ts +14 -0
- package/lib/generators/add-container-support/generate-add-container-support.d.ts.map +1 -1
- package/lib/generators/add-parent-aware/generate-add-parent-aware.d.ts +15 -0
- package/lib/generators/add-parent-aware/generate-add-parent-aware.d.ts.map +1 -0
- package/lib/generators/add-parent-aware/index.d.ts +2 -0
- package/lib/generators/add-parent-aware/index.d.ts.map +1 -0
- package/lib/generators/augment/find-model-declaration.d.ts +4 -0
- package/lib/generators/augment/find-model-declaration.d.ts.map +1 -0
- package/lib/generators/augment/resolve-child-model.d.ts +16 -0
- package/lib/generators/augment/resolve-child-model.d.ts.map +1 -0
- package/lib/generators/augment/resolve-model-file.d.ts.map +1 -1
- package/lib/generators/augment/ts-toolkit.d.ts +25 -1
- package/lib/generators/augment/ts-toolkit.d.ts.map +1 -1
- package/lib/generators/describe/describe-model.d.ts.map +1 -1
- package/lib/generators/generate-container-model.d.ts +7 -0
- package/lib/generators/generate-container-model.d.ts.map +1 -1
- package/lib/generators/generate-model-project.d.ts +36 -0
- package/lib/generators/generate-model-project.d.ts.map +1 -0
- package/lib/generators/generate-view-model.d.ts +27 -0
- package/lib/generators/generate-view-model.d.ts.map +1 -0
- package/lib/generators/index.d.ts +21 -16
- package/lib/generators/index.d.ts.map +1 -1
- package/lib/generators/member-mutators/add-child.d.ts +10 -2
- package/lib/generators/member-mutators/add-child.d.ts.map +1 -1
- package/lib/generators/member-mutators/add-service-request.d.ts.map +1 -1
- package/lib/generators/normalize-options.d.ts +2 -0
- package/lib/generators/normalize-options.d.ts.map +1 -1
- package/lib/generators/registration/upsert-chain-entry.d.ts +13 -0
- package/lib/generators/registration/upsert-chain-entry.d.ts.map +1 -0
- package/package.json +2 -2
- package/templates/kos-container-model/model/types/index.d.ts.template +2 -2
- package/templates/kos-model/model/__nameDashCase__-model.ts.template +7 -4
- package/templates/kos-model-project/project/.eslintrc.json.template +33 -0
- package/templates/kos-model-project/project/.kos.json.template +14 -0
- package/templates/kos-model-project/project/README.md.template +7 -0
- package/templates/kos-model-project/project/package.json.template +9 -0
- package/templates/kos-model-project/project/project.json.template +35 -0
- package/templates/kos-model-project/project/src/index.ts.template +1 -0
- package/templates/kos-model-project/project/src/lib/__projectName__.ts.template +3 -0
- package/templates/kos-model-project/project/tsconfig.json.template +20 -0
- package/templates/kos-model-project/project/tsconfig.lib.json.template +10 -0
- package/templates/kos-model-project/project/vite.config.ts.template +47 -0
- package/templates/kos-view-model/__nameDashCase__-view-model.ts.template +30 -0
- 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;
|
|
@@ -913,7 +991,8 @@ function normalizeOptions(codegenFs, options, projects) {
|
|
|
913
991
|
const booleanDefaults = {
|
|
914
992
|
companion: false,
|
|
915
993
|
skipRegistration: false,
|
|
916
|
-
futureAware: "none"
|
|
994
|
+
futureAware: "none",
|
|
995
|
+
singleton: false
|
|
917
996
|
};
|
|
918
997
|
return {
|
|
919
998
|
...booleanDefaults,
|
|
@@ -925,6 +1004,58 @@ function normalizeOptions(codegenFs, options, projects) {
|
|
|
925
1004
|
template: ""
|
|
926
1005
|
};
|
|
927
1006
|
}
|
|
1007
|
+
const UPDATE_RELEASE_VERSION_SCRIPT = `import devkit from "@nx/devkit";
|
|
1008
|
+
import { resolve } from "path";
|
|
1009
|
+
import { readFileSync, writeFileSync } from "fs";
|
|
1010
|
+
import prettier from "prettier";
|
|
1011
|
+
|
|
1012
|
+
// KOS artifact versioning: stamps the project's .kos.json "version" field
|
|
1013
|
+
// (which kabtool bakes into the KAB). Never touches package.json.
|
|
1014
|
+
// Driven by tag-based releases:
|
|
1015
|
+
// nx run-many --target=version --args=--ver=$KOSBUILD_VERSION
|
|
1016
|
+
|
|
1017
|
+
const { readCachedProjectGraph } = devkit;
|
|
1018
|
+
const [, , name, versionArg] = process.argv;
|
|
1019
|
+
|
|
1020
|
+
// "{args.ver}" arrives literally when the target runs without --args=--ver=<v>;
|
|
1021
|
+
// treat that (or a missing arg) as "report current version, change nothing".
|
|
1022
|
+
const version =
|
|
1023
|
+
versionArg && !versionArg.startsWith("{args") ? versionArg : undefined;
|
|
1024
|
+
|
|
1025
|
+
if (!name) {
|
|
1026
|
+
console.error("usage: update-release-version.mjs <project> <version>");
|
|
1027
|
+
process.exit(1);
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
const graph = readCachedProjectGraph();
|
|
1031
|
+
const project = graph.nodes[name];
|
|
1032
|
+
if (!project) {
|
|
1033
|
+
console.error("Unknown project: " + name);
|
|
1034
|
+
process.exit(1);
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
const kosJsonPath = resolve(process.cwd(), project.data.root, ".kos.json");
|
|
1038
|
+
let kosJson;
|
|
1039
|
+
try {
|
|
1040
|
+
kosJson = JSON.parse(readFileSync(kosJsonPath, "utf8"));
|
|
1041
|
+
} catch {
|
|
1042
|
+
console.error("Missing or invalid .kos.json: " + kosJsonPath);
|
|
1043
|
+
process.exit(1);
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
if (!version) {
|
|
1047
|
+
console.log(name + ": " + kosJson.version + " (no --ver given; unchanged)");
|
|
1048
|
+
process.exit(0);
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
const prettierOptions = await prettier.resolveConfig(kosJsonPath);
|
|
1052
|
+
const output = await prettier.format(
|
|
1053
|
+
JSON.stringify({ ...kosJson, version }, null, 2),
|
|
1054
|
+
{ ...prettierOptions, parser: "json" }
|
|
1055
|
+
);
|
|
1056
|
+
writeFileSync(kosJsonPath, output);
|
|
1057
|
+
console.log(name + ": version -> " + version);
|
|
1058
|
+
`;
|
|
928
1059
|
function transformSourceFile(codegenFs, filePath, mutate) {
|
|
929
1060
|
const content = codegenFs.read(filePath);
|
|
930
1061
|
if (content === null) {
|
|
@@ -953,6 +1084,15 @@ function readSourceFile(codegenFs, filePath, inspect) {
|
|
|
953
1084
|
project.createSourceFile(filePath, content, { overwrite: true })
|
|
954
1085
|
);
|
|
955
1086
|
}
|
|
1087
|
+
function decoratorConfigText(sourceFile, cls, decoratorName) {
|
|
1088
|
+
const arg = cls.getDecorator(decoratorName)?.getArguments()[0];
|
|
1089
|
+
if (!arg) return void 0;
|
|
1090
|
+
if (arg.getKindName() === "ObjectLiteralExpression") return arg.getText();
|
|
1091
|
+
const text = arg.getText().trim();
|
|
1092
|
+
if (!/^[A-Za-z_$][\w$]*$/.test(text)) return text;
|
|
1093
|
+
const initializer = sourceFile.getVariableDeclaration(text)?.getInitializer()?.getText();
|
|
1094
|
+
return initializer ? initializer.replace(/\s+as\s+const\s*$/, "") : text;
|
|
1095
|
+
}
|
|
956
1096
|
function ensureNamedImport(sourceFile, moduleSpecifier, names) {
|
|
957
1097
|
const decls = sourceFile.getImportDeclarations().filter((d) => d.getModuleSpecifierValue() === moduleSpecifier);
|
|
958
1098
|
const existing = /* @__PURE__ */ new Set();
|
|
@@ -1120,6 +1260,40 @@ function ensureBarrelExport(sourceFile, moduleSpecifier) {
|
|
|
1120
1260
|
sourceFile.addExportDeclaration({ moduleSpecifier });
|
|
1121
1261
|
return true;
|
|
1122
1262
|
}
|
|
1263
|
+
function ensureNamedExports(sourceFile, moduleSpecifier, names, isTypeOnly = false) {
|
|
1264
|
+
const existing = sourceFile.getExportDeclarations().find(
|
|
1265
|
+
(d) => d.getModuleSpecifierValue() === moduleSpecifier && d.isTypeOnly() === isTypeOnly
|
|
1266
|
+
);
|
|
1267
|
+
if (!existing) {
|
|
1268
|
+
sourceFile.addExportDeclaration({
|
|
1269
|
+
moduleSpecifier,
|
|
1270
|
+
isTypeOnly,
|
|
1271
|
+
namedExports: names.map((name) => {
|
|
1272
|
+
return { name };
|
|
1273
|
+
})
|
|
1274
|
+
});
|
|
1275
|
+
return true;
|
|
1276
|
+
}
|
|
1277
|
+
const present = new Set(existing.getNamedExports().map((e) => e.getName()));
|
|
1278
|
+
const missing = names.filter((name) => !present.has(name));
|
|
1279
|
+
if (missing.length === 0) return false;
|
|
1280
|
+
existing.addNamedExports(
|
|
1281
|
+
missing.map((name) => {
|
|
1282
|
+
return { name };
|
|
1283
|
+
})
|
|
1284
|
+
);
|
|
1285
|
+
return true;
|
|
1286
|
+
}
|
|
1287
|
+
function ensureExportedInterface(sourceFile, name, properties = [], extendsTypes = []) {
|
|
1288
|
+
if (sourceFile.getInterface(name)) return false;
|
|
1289
|
+
sourceFile.addInterface({
|
|
1290
|
+
name,
|
|
1291
|
+
isExported: true,
|
|
1292
|
+
extends: extendsTypes,
|
|
1293
|
+
properties
|
|
1294
|
+
});
|
|
1295
|
+
return true;
|
|
1296
|
+
}
|
|
1123
1297
|
function addDecoratedMethod(cls, spec) {
|
|
1124
1298
|
if (cls.getMethod(spec.name)) return false;
|
|
1125
1299
|
cls.addMethod({
|
|
@@ -1272,58 +1446,6 @@ function readSummary(methodProp) {
|
|
|
1272
1446
|
function unquote(name) {
|
|
1273
1447
|
return name.replace(/^["']|["']$/g, "");
|
|
1274
1448
|
}
|
|
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
1449
|
function appendBarrelExport(codegenFs, indexPath, exportPath) {
|
|
1328
1450
|
const exportLine = `export * from '${exportPath}'`;
|
|
1329
1451
|
const content = codegenFs.read(indexPath) ?? "";
|
|
@@ -1370,10 +1492,57 @@ function updateModelIndex(codegenFs, indexPath, modelPath) {
|
|
|
1370
1492
|
const newContents = printer.printFile(updatedSourceFile);
|
|
1371
1493
|
codegenFs.write(indexPath, newContents);
|
|
1372
1494
|
}
|
|
1373
|
-
function
|
|
1374
|
-
|
|
1375
|
-
|
|
1495
|
+
function generateCompanionModel(codegenFs, templateDir, options, projects) {
|
|
1496
|
+
const logger = getCodegenLogger();
|
|
1497
|
+
const normalized = normalizeAllValues({
|
|
1498
|
+
companionModelName: options.companionModelName,
|
|
1499
|
+
modelName: options.modelName
|
|
1500
|
+
});
|
|
1501
|
+
const companionChildKosConfig = getKosProjectConfiguration(
|
|
1502
|
+
codegenFs,
|
|
1503
|
+
options.companionModelProject,
|
|
1504
|
+
projects
|
|
1505
|
+
);
|
|
1506
|
+
const parentProject = findProjectByName(
|
|
1507
|
+
codegenFs.root,
|
|
1508
|
+
options.modelProject,
|
|
1509
|
+
projects
|
|
1510
|
+
);
|
|
1511
|
+
const childProject = findProjectByName(
|
|
1512
|
+
codegenFs.root,
|
|
1513
|
+
options.companionModelProject,
|
|
1514
|
+
projects
|
|
1515
|
+
);
|
|
1516
|
+
const projectRoot = childProject?.sourceRoot;
|
|
1517
|
+
if (!projectRoot) {
|
|
1518
|
+
logger.warn(`Companion child project source root not found`);
|
|
1519
|
+
return;
|
|
1520
|
+
}
|
|
1521
|
+
let importPath = "";
|
|
1522
|
+
if (parentProject) {
|
|
1523
|
+
const pkgJsonPath = path.join(parentProject.root, "package.json");
|
|
1524
|
+
try {
|
|
1525
|
+
const pkgJson = readJson(codegenFs, pkgJsonPath);
|
|
1526
|
+
importPath = pkgJson.name || "";
|
|
1527
|
+
} catch {
|
|
1528
|
+
importPath = "";
|
|
1529
|
+
}
|
|
1376
1530
|
}
|
|
1531
|
+
const modelLocation = companionChildKosConfig?.generator?.defaults?.model?.folder || "";
|
|
1532
|
+
const filePath = path.join(
|
|
1533
|
+
projectRoot,
|
|
1534
|
+
modelLocation,
|
|
1535
|
+
normalized.companionModelNameDashCase
|
|
1536
|
+
);
|
|
1537
|
+
logger.info(`Generating companion model in ${filePath}`);
|
|
1538
|
+
generateFilesFromTemplates(codegenFs, templateDir, filePath, {
|
|
1539
|
+
...options,
|
|
1540
|
+
...normalized,
|
|
1541
|
+
importPath
|
|
1542
|
+
});
|
|
1543
|
+
}
|
|
1544
|
+
function generateContainerModel(codegenFs, templateDir, options, cwd, projects) {
|
|
1545
|
+
const logger = getCodegenLogger();
|
|
1377
1546
|
const currentProject = getProject(codegenFs, cwd);
|
|
1378
1547
|
const modelProjectName = options.modelProject || currentProject?.name;
|
|
1379
1548
|
if (!modelProjectName) {
|
|
@@ -1381,56 +1550,109 @@ function generateHook(codegenFs, templateDir, options, cwd, projects) {
|
|
|
1381
1550
|
"No model project found. Please specify a model project with --modelProject."
|
|
1382
1551
|
);
|
|
1383
1552
|
}
|
|
1384
|
-
const modelName = options.
|
|
1553
|
+
const modelName = options.modelName || getCurrentDirectoryName(cwd);
|
|
1385
1554
|
if (!modelName) {
|
|
1386
1555
|
throw new Error(
|
|
1387
1556
|
"No model name found. Please specify a model name with --name."
|
|
1388
1557
|
);
|
|
1389
1558
|
}
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
modelProjectName,
|
|
1393
|
-
modelName,
|
|
1394
|
-
projects
|
|
1395
|
-
);
|
|
1396
|
-
options.singleton = kosModelConfig ? !!kosModelConfig.singleton : false;
|
|
1397
|
-
options.name = modelName;
|
|
1398
|
-
options.modelProject = modelProjectName;
|
|
1559
|
+
options.modelName = modelName;
|
|
1560
|
+
options.name = `${modelName}-container`;
|
|
1399
1561
|
const normalized = normalizeOptions(codegenFs, options, projects);
|
|
1400
|
-
const
|
|
1562
|
+
const projectConfig = findProjectByName(
|
|
1401
1563
|
codegenFs.root,
|
|
1402
|
-
normalized.
|
|
1564
|
+
normalized.modelProject,
|
|
1403
1565
|
projects
|
|
1404
1566
|
);
|
|
1405
|
-
if (!
|
|
1406
|
-
throw new Error(`
|
|
1567
|
+
if (!projectConfig) {
|
|
1568
|
+
throw new Error(`Model project '${normalized.modelProject}' not found`);
|
|
1407
1569
|
}
|
|
1570
|
+
addKosModelConfiguration({
|
|
1571
|
+
codegenFs,
|
|
1572
|
+
modelName: normalized.nameDashCase,
|
|
1573
|
+
projectName: projectConfig.name,
|
|
1574
|
+
projectRoot: projectConfig.root,
|
|
1575
|
+
singleton: !!options.singleton,
|
|
1576
|
+
container: true,
|
|
1577
|
+
// The container's exported registration bean (`export const <ProperCase>`).
|
|
1578
|
+
factory: normalized.nameProperCase
|
|
1579
|
+
});
|
|
1408
1580
|
const kosConfig = getKosProjectConfiguration(
|
|
1409
1581
|
codegenFs,
|
|
1410
|
-
|
|
1582
|
+
projectConfig.name,
|
|
1411
1583
|
projects
|
|
1412
1584
|
);
|
|
1413
|
-
const
|
|
1414
|
-
|
|
1415
|
-
|
|
1585
|
+
const modelLocation = kosConfig?.generator?.defaults?.model?.folder || "";
|
|
1586
|
+
const internal = !!kosConfig?.generator?.internal;
|
|
1587
|
+
options.modelDirectory = options.modelDirectory || modelLocation;
|
|
1588
|
+
const projectRoot = projectConfig.sourceRoot;
|
|
1416
1589
|
if (projectRoot) {
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
templateDir,
|
|
1420
|
-
path.join(
|
|
1421
|
-
projectRoot,
|
|
1422
|
-
options.appDirectory,
|
|
1423
|
-
"hooks",
|
|
1424
|
-
normalized.nameDashCase
|
|
1425
|
-
),
|
|
1426
|
-
normalized
|
|
1590
|
+
logger.info(
|
|
1591
|
+
`Generating container model ${normalized.nameDashCase} in ${projectRoot}`
|
|
1427
1592
|
);
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1593
|
+
const modelNameDashCase = normalized.modelNameDashCase || normalized.nameDashCase;
|
|
1594
|
+
const modelFolder = path.join(
|
|
1595
|
+
projectRoot,
|
|
1596
|
+
options.modelDirectory || "",
|
|
1597
|
+
modelNameDashCase
|
|
1432
1598
|
);
|
|
1599
|
+
if (options.existingModel) {
|
|
1600
|
+
addContainerToExistingModelFolder(codegenFs, templateDir, modelFolder, {
|
|
1601
|
+
...normalized,
|
|
1602
|
+
internal
|
|
1603
|
+
});
|
|
1604
|
+
} else {
|
|
1605
|
+
generateFilesFromTemplates(
|
|
1606
|
+
codegenFs,
|
|
1607
|
+
path.join(templateDir, "model"),
|
|
1608
|
+
modelFolder,
|
|
1609
|
+
{ ...normalized, internal }
|
|
1610
|
+
);
|
|
1611
|
+
}
|
|
1612
|
+
const modelIndex = path.join(projectRoot, "index.ts");
|
|
1613
|
+
const modelPath = normalized.modelDirectory ? `${normalized.modelDirectory}/${modelNameDashCase}` : modelNameDashCase;
|
|
1614
|
+
updateModelIndex(codegenFs, modelIndex, modelPath);
|
|
1615
|
+
}
|
|
1616
|
+
}
|
|
1617
|
+
function addContainerToExistingModelFolder(codegenFs, templateDir, modelFolder, substitutions) {
|
|
1618
|
+
const containerFileName = `${substitutions.nameDashCase}-model.ts`;
|
|
1619
|
+
const containerTemplate = path.join(
|
|
1620
|
+
templateDir,
|
|
1621
|
+
"model",
|
|
1622
|
+
"__nameDashCase__-model.ts.template"
|
|
1623
|
+
);
|
|
1624
|
+
const rendered = ejs.render(
|
|
1625
|
+
fs.readFileSync(containerTemplate, "utf-8"),
|
|
1626
|
+
substitutions,
|
|
1627
|
+
{ filename: containerTemplate }
|
|
1628
|
+
);
|
|
1629
|
+
codegenFs.write(path.join(modelFolder, containerFileName), rendered);
|
|
1630
|
+
const typesPath = path.join(modelFolder, "types", "index.d.ts");
|
|
1631
|
+
if (codegenFs.read(typesPath) === null) {
|
|
1632
|
+
codegenFs.write(typesPath, "");
|
|
1633
|
+
}
|
|
1634
|
+
transformSourceFile(codegenFs, typesPath, (sourceFile) => {
|
|
1635
|
+
ensureExportedInterface(
|
|
1636
|
+
sourceFile,
|
|
1637
|
+
`${substitutions.nameProperCase}Options`
|
|
1638
|
+
);
|
|
1639
|
+
});
|
|
1640
|
+
const barrelPath = path.join(modelFolder, "index.ts");
|
|
1641
|
+
if (codegenFs.read(barrelPath) === null) {
|
|
1642
|
+
codegenFs.write(barrelPath, "");
|
|
1433
1643
|
}
|
|
1644
|
+
transformSourceFile(codegenFs, barrelPath, (sourceFile) => {
|
|
1645
|
+
const moduleSpecifier = `./${containerFileName.replace(/\.ts$/, "")}`;
|
|
1646
|
+
ensureNamedExports(sourceFile, moduleSpecifier, [
|
|
1647
|
+
substitutions.nameProperCase
|
|
1648
|
+
]);
|
|
1649
|
+
ensureNamedExports(
|
|
1650
|
+
sourceFile,
|
|
1651
|
+
moduleSpecifier,
|
|
1652
|
+
[`${substitutions.nameProperCase}Model`],
|
|
1653
|
+
true
|
|
1654
|
+
);
|
|
1655
|
+
});
|
|
1434
1656
|
}
|
|
1435
1657
|
function generateContext(codegenFs, templateDir, options, cwd, projects) {
|
|
1436
1658
|
if (!options.appProject) {
|
|
@@ -1484,8 +1706,10 @@ function generateContext(codegenFs, templateDir, options, cwd, projects) {
|
|
|
1484
1706
|
);
|
|
1485
1707
|
}
|
|
1486
1708
|
}
|
|
1487
|
-
function
|
|
1488
|
-
|
|
1709
|
+
function generateHook(codegenFs, templateDir, options, cwd, projects) {
|
|
1710
|
+
if (!options.appProject) {
|
|
1711
|
+
throw new Error("No app project specified");
|
|
1712
|
+
}
|
|
1489
1713
|
const currentProject = getProject(codegenFs, cwd);
|
|
1490
1714
|
const modelProjectName = options.modelProject || currentProject?.name;
|
|
1491
1715
|
if (!modelProjectName) {
|
|
@@ -1493,106 +1717,56 @@ function generateContainerModel(codegenFs, templateDir, options, cwd, projects)
|
|
|
1493
1717
|
"No model project found. Please specify a model project with --modelProject."
|
|
1494
1718
|
);
|
|
1495
1719
|
}
|
|
1496
|
-
const modelName = options.
|
|
1720
|
+
const modelName = options.name || getCurrentDirectoryName(cwd);
|
|
1497
1721
|
if (!modelName) {
|
|
1498
1722
|
throw new Error(
|
|
1499
1723
|
"No model name found. Please specify a model name with --name."
|
|
1500
1724
|
);
|
|
1501
1725
|
}
|
|
1502
|
-
|
|
1503
|
-
|
|
1726
|
+
const kosModelConfig = getKosModelConfiguration(
|
|
1727
|
+
codegenFs,
|
|
1728
|
+
modelProjectName,
|
|
1729
|
+
modelName,
|
|
1730
|
+
projects
|
|
1731
|
+
);
|
|
1732
|
+
options.singleton = kosModelConfig ? !!kosModelConfig.singleton : false;
|
|
1733
|
+
options.name = modelName;
|
|
1734
|
+
options.modelProject = modelProjectName;
|
|
1504
1735
|
const normalized = normalizeOptions(codegenFs, options, projects);
|
|
1505
|
-
const
|
|
1736
|
+
const appProject = findProjectByName(
|
|
1506
1737
|
codegenFs.root,
|
|
1507
|
-
normalized.
|
|
1738
|
+
normalized.appProject,
|
|
1508
1739
|
projects
|
|
1509
1740
|
);
|
|
1510
|
-
if (!
|
|
1511
|
-
throw new Error(`
|
|
1741
|
+
if (!appProject) {
|
|
1742
|
+
throw new Error(`App project '${normalized.appProject}' not found`);
|
|
1512
1743
|
}
|
|
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
1744
|
const kosConfig = getKosProjectConfiguration(
|
|
1524
1745
|
codegenFs,
|
|
1525
|
-
|
|
1746
|
+
appProject.name,
|
|
1526
1747
|
projects
|
|
1527
1748
|
);
|
|
1528
|
-
const
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
const projectRoot = projectConfig.sourceRoot;
|
|
1749
|
+
const componentLocation = kosConfig?.generator?.defaults?.component?.folder || "";
|
|
1750
|
+
options.appDirectory = options.appDirectory || componentLocation;
|
|
1751
|
+
const projectRoot = appProject.sourceRoot;
|
|
1532
1752
|
if (projectRoot) {
|
|
1533
|
-
logger.info(
|
|
1534
|
-
`Generating container model ${normalized.nameDashCase} in ${projectRoot}`
|
|
1535
|
-
);
|
|
1536
|
-
const modelNameDashCase = normalized.modelNameDashCase || normalized.nameDashCase;
|
|
1537
1753
|
generateFilesFromTemplates(
|
|
1538
1754
|
codegenFs,
|
|
1539
|
-
|
|
1540
|
-
path.join(
|
|
1541
|
-
|
|
1755
|
+
templateDir,
|
|
1756
|
+
path.join(
|
|
1757
|
+
projectRoot,
|
|
1758
|
+
options.appDirectory,
|
|
1759
|
+
"hooks",
|
|
1760
|
+
normalized.nameDashCase
|
|
1761
|
+
),
|
|
1762
|
+
normalized
|
|
1763
|
+
);
|
|
1764
|
+
appendBarrelExport(
|
|
1765
|
+
codegenFs,
|
|
1766
|
+
path.join(projectRoot, options.appDirectory, "hooks", "index.ts"),
|
|
1767
|
+
`./${normalized.nameDashCase}`
|
|
1542
1768
|
);
|
|
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
1769
|
}
|
|
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
1770
|
}
|
|
1597
1771
|
function generateModel(params) {
|
|
1598
1772
|
const {
|
|
@@ -1691,6 +1865,55 @@ function generateModel(params) {
|
|
|
1691
1865
|
);
|
|
1692
1866
|
}
|
|
1693
1867
|
}
|
|
1868
|
+
const DECLARATION_MARKER = "@kosModel";
|
|
1869
|
+
function findModelDeclarationFile(codegenFs, searchRoot, typeIds) {
|
|
1870
|
+
const wanted = new Set(typeIds.filter(Boolean));
|
|
1871
|
+
if (wanted.size === 0) return null;
|
|
1872
|
+
const matches = [];
|
|
1873
|
+
for (const filePath of codegenFs.listFiles(searchRoot)) {
|
|
1874
|
+
if (!filePath.endsWith(".ts") || filePath.endsWith(".d.ts")) continue;
|
|
1875
|
+
const content = codegenFs.read(filePath);
|
|
1876
|
+
if (!content || !content.includes(DECLARATION_MARKER)) continue;
|
|
1877
|
+
const declared = readDeclaredModelType(codegenFs, filePath);
|
|
1878
|
+
if (declared && wanted.has(declared)) matches.push(filePath);
|
|
1879
|
+
}
|
|
1880
|
+
if (matches.length === 0) return null;
|
|
1881
|
+
if (matches.length > 1) {
|
|
1882
|
+
throw new Error(
|
|
1883
|
+
`Model type '${[...wanted].join("' / '")}' is declared in more than one file:
|
|
1884
|
+
` + matches.sort().map((c) => ` - ${c}`).join("\n") + `
|
|
1885
|
+
Pass modelPath to pick one.`
|
|
1886
|
+
);
|
|
1887
|
+
}
|
|
1888
|
+
return matches[0];
|
|
1889
|
+
}
|
|
1890
|
+
function readDeclaredModelType(codegenFs, filePath) {
|
|
1891
|
+
try {
|
|
1892
|
+
return readSourceFile(codegenFs, filePath, (sf) => {
|
|
1893
|
+
const cls = sf.getClasses().find((c) => c.getDecorator("kosModel"));
|
|
1894
|
+
if (!cls) return void 0;
|
|
1895
|
+
const config = decoratorConfigText(sf, cls, "kosModel");
|
|
1896
|
+
const inner = config?.startsWith("{") ? modelTypeIdProperty(config) : config;
|
|
1897
|
+
if (!inner) return void 0;
|
|
1898
|
+
return resolveToStringLiteral(inner.trim(), sf.getFullText());
|
|
1899
|
+
});
|
|
1900
|
+
} catch {
|
|
1901
|
+
return void 0;
|
|
1902
|
+
}
|
|
1903
|
+
}
|
|
1904
|
+
function modelTypeIdProperty(objectText) {
|
|
1905
|
+
const m = objectText.match(/\bmodelTypeId\s*:\s*([^,}]+)/);
|
|
1906
|
+
return m ? m[1] : void 0;
|
|
1907
|
+
}
|
|
1908
|
+
function resolveToStringLiteral(expression, fileText) {
|
|
1909
|
+
const literal = expression.match(/^["'`](.*)["'`]$/);
|
|
1910
|
+
if (literal) return literal[1];
|
|
1911
|
+
if (!/^[A-Za-z_$][\w$]*$/.test(expression)) return void 0;
|
|
1912
|
+
const declared = fileText.match(
|
|
1913
|
+
new RegExp(`\\b(?:const|let|var)\\s+${expression}\\s*(?::[^=]+)?=\\s*["'\`]([^"'\`]+)["'\`]`)
|
|
1914
|
+
);
|
|
1915
|
+
return declared ? declared[1] : void 0;
|
|
1916
|
+
}
|
|
1694
1917
|
function resolveModelFilePath(codegenFs, query, projects) {
|
|
1695
1918
|
const kosConfig = getKosProjectConfiguration(
|
|
1696
1919
|
codegenFs,
|
|
@@ -1725,14 +1948,23 @@ function resolveModelFilePath(codegenFs, query, projects) {
|
|
|
1725
1948
|
if (codegenFs.exists(modelFilePath)) {
|
|
1726
1949
|
return { modelFilePath, internal, sourceRoot };
|
|
1727
1950
|
}
|
|
1951
|
+
const searchRoot = path.join(sourceRoot, modelLocation);
|
|
1728
1952
|
const discovered = findModelFileByName(
|
|
1729
1953
|
codegenFs,
|
|
1730
|
-
|
|
1954
|
+
searchRoot,
|
|
1731
1955
|
modelNameDashCase
|
|
1732
1956
|
);
|
|
1733
1957
|
if (discovered) {
|
|
1734
1958
|
return { modelFilePath: discovered, internal, sourceRoot };
|
|
1735
1959
|
}
|
|
1960
|
+
const declaredType = kosConfig?.models?.[query.modelName]?.type;
|
|
1961
|
+
const byDeclaration = findModelDeclarationFile(codegenFs, searchRoot, [
|
|
1962
|
+
query.modelName,
|
|
1963
|
+
...declaredType ? [declaredType] : []
|
|
1964
|
+
]);
|
|
1965
|
+
if (byDeclaration) {
|
|
1966
|
+
return { modelFilePath: byDeclaration, internal, sourceRoot };
|
|
1967
|
+
}
|
|
1736
1968
|
return { modelFilePath, internal, sourceRoot };
|
|
1737
1969
|
}
|
|
1738
1970
|
function findModelFileByName(codegenFs, searchRoot, modelNameDashCase) {
|
|
@@ -1748,6 +1980,150 @@ Pass modelPath to pick one.`
|
|
|
1748
1980
|
}
|
|
1749
1981
|
return candidates[0];
|
|
1750
1982
|
}
|
|
1983
|
+
function resolveChildModelType(codegenFs, query, projects) {
|
|
1984
|
+
const childProject = query.childModelProject || query.modelProject;
|
|
1985
|
+
const childType = `${properCase(query.childModel)}Model`;
|
|
1986
|
+
if (childProject !== query.modelProject) {
|
|
1987
|
+
const project = findProjectByName(codegenFs.root, childProject, projects);
|
|
1988
|
+
const pkgJson = project ? readJson(
|
|
1989
|
+
codegenFs,
|
|
1990
|
+
path.join(project.root, "package.json")
|
|
1991
|
+
) : void 0;
|
|
1992
|
+
return { childType, childTypeModule: pkgJson?.name };
|
|
1993
|
+
}
|
|
1994
|
+
const { modelFilePath: childFilePath } = resolveModelFilePath(
|
|
1995
|
+
codegenFs,
|
|
1996
|
+
{ modelName: query.childModel, modelProject: childProject },
|
|
1997
|
+
projects
|
|
1998
|
+
);
|
|
1999
|
+
const relative = path.relative(path.dirname(query.modelFilePath), childFilePath).replace(/\.ts$/, "");
|
|
2000
|
+
return {
|
|
2001
|
+
childType,
|
|
2002
|
+
childTypeModule: relative.startsWith(".") ? relative : `./${relative}`
|
|
2003
|
+
};
|
|
2004
|
+
}
|
|
2005
|
+
function generateViewModel(codegenFs, templateDir, options, cwd, projects) {
|
|
2006
|
+
const logger = getCodegenLogger();
|
|
2007
|
+
if (!options.name) {
|
|
2008
|
+
throw new Error(
|
|
2009
|
+
"No ViewModel name found. Please specify a name with --name."
|
|
2010
|
+
);
|
|
2011
|
+
}
|
|
2012
|
+
const currentProject = getProject(codegenFs, cwd);
|
|
2013
|
+
const modelProjectName = options.modelProject || currentProject?.name;
|
|
2014
|
+
if (!modelProjectName) {
|
|
2015
|
+
throw new Error(
|
|
2016
|
+
"No model project found. Please specify a model project with --project."
|
|
2017
|
+
);
|
|
2018
|
+
}
|
|
2019
|
+
const normalized = normalizeOptions(
|
|
2020
|
+
codegenFs,
|
|
2021
|
+
{ ...options, modelProject: modelProjectName },
|
|
2022
|
+
projects
|
|
2023
|
+
);
|
|
2024
|
+
const projectConfig = findProjectByName(
|
|
2025
|
+
codegenFs.root,
|
|
2026
|
+
modelProjectName,
|
|
2027
|
+
projects
|
|
2028
|
+
);
|
|
2029
|
+
if (!projectConfig) {
|
|
2030
|
+
throw new Error(`Model project '${modelProjectName}' not found`);
|
|
2031
|
+
}
|
|
2032
|
+
const kosConfig = getKosProjectConfiguration(
|
|
2033
|
+
codegenFs,
|
|
2034
|
+
projectConfig.name,
|
|
2035
|
+
projects
|
|
2036
|
+
);
|
|
2037
|
+
const modelDirectory = options.modelDirectory || kosConfig?.generator?.defaults?.model?.folder || "";
|
|
2038
|
+
const internal = !!kosConfig?.generator?.internal;
|
|
2039
|
+
const projectRoot = projectConfig.sourceRoot || path.join(projectConfig.root, "src");
|
|
2040
|
+
const viewModelFolder = path.join(
|
|
2041
|
+
projectRoot,
|
|
2042
|
+
modelDirectory,
|
|
2043
|
+
normalized.nameDashCase
|
|
2044
|
+
);
|
|
2045
|
+
const viewModelFilePath = path.join(
|
|
2046
|
+
viewModelFolder,
|
|
2047
|
+
`${normalized.nameDashCase}-view-model.ts`
|
|
2048
|
+
);
|
|
2049
|
+
if (codegenFs.exists(viewModelFilePath)) {
|
|
2050
|
+
logger.info(`ViewModel already exists: ${viewModelFilePath}`);
|
|
2051
|
+
return { viewModelFilePath, created: false };
|
|
2052
|
+
}
|
|
2053
|
+
const resolved = (options.models || []).map(
|
|
2054
|
+
(source) => resolveSourceModel(
|
|
2055
|
+
codegenFs,
|
|
2056
|
+
source,
|
|
2057
|
+
viewModelFilePath,
|
|
2058
|
+
modelProjectName,
|
|
2059
|
+
projects
|
|
2060
|
+
)
|
|
2061
|
+
);
|
|
2062
|
+
logger.info(
|
|
2063
|
+
`Generating ViewModel ${normalized.nameDashCase} in ${projectRoot}`
|
|
2064
|
+
);
|
|
2065
|
+
const constructorParams = resolved.map(({ name, type }) => ({ name, type }));
|
|
2066
|
+
generateFilesFromTemplates(codegenFs, templateDir, viewModelFolder, {
|
|
2067
|
+
...normalized,
|
|
2068
|
+
internal,
|
|
2069
|
+
typeId: options.typeId || normalized.nameDashCase,
|
|
2070
|
+
devToolsEnabled: !!options.devToolsEnabled,
|
|
2071
|
+
constructorParams,
|
|
2072
|
+
constructorArgs: constructorParams.map((param) => param.name).join(", "),
|
|
2073
|
+
modelImports: groupImports(resolved)
|
|
2074
|
+
});
|
|
2075
|
+
updateModelIndex(
|
|
2076
|
+
codegenFs,
|
|
2077
|
+
path.join(projectRoot, "index.ts"),
|
|
2078
|
+
modelDirectory ? `${modelDirectory}/${normalized.nameDashCase}` : normalized.nameDashCase
|
|
2079
|
+
);
|
|
2080
|
+
return { viewModelFilePath, created: true };
|
|
2081
|
+
}
|
|
2082
|
+
function resolveSourceModel(codegenFs, source, viewModelFilePath, modelProject, projects) {
|
|
2083
|
+
const sourceProject = source.project || modelProject;
|
|
2084
|
+
if (sourceProject === modelProject) {
|
|
2085
|
+
const { modelFilePath } = resolveModelFilePath(
|
|
2086
|
+
codegenFs,
|
|
2087
|
+
{ modelName: source.model, modelProject: sourceProject },
|
|
2088
|
+
projects
|
|
2089
|
+
);
|
|
2090
|
+
if (!codegenFs.exists(modelFilePath)) {
|
|
2091
|
+
throw new Error(
|
|
2092
|
+
`Model '${source.model}' not found in project '${sourceProject}' (looked for ${modelFilePath})`
|
|
2093
|
+
);
|
|
2094
|
+
}
|
|
2095
|
+
} else if (!findProjectByName(codegenFs.root, sourceProject, projects)) {
|
|
2096
|
+
throw new Error(`Model project '${sourceProject}' not found`);
|
|
2097
|
+
}
|
|
2098
|
+
const { childType, childTypeModule } = resolveChildModelType(
|
|
2099
|
+
codegenFs,
|
|
2100
|
+
{
|
|
2101
|
+
modelFilePath: viewModelFilePath,
|
|
2102
|
+
modelProject,
|
|
2103
|
+
childModel: source.model,
|
|
2104
|
+
childModelProject: sourceProject
|
|
2105
|
+
},
|
|
2106
|
+
projects
|
|
2107
|
+
);
|
|
2108
|
+
return {
|
|
2109
|
+
name: camelCase(source.model),
|
|
2110
|
+
type: childType,
|
|
2111
|
+
module: childTypeModule
|
|
2112
|
+
};
|
|
2113
|
+
}
|
|
2114
|
+
function groupImports(resolved) {
|
|
2115
|
+
const byModule = /* @__PURE__ */ new Map();
|
|
2116
|
+
for (const { type, module } of resolved) {
|
|
2117
|
+
if (!module) continue;
|
|
2118
|
+
const types = byModule.get(module);
|
|
2119
|
+
if (!types) {
|
|
2120
|
+
byModule.set(module, [type]);
|
|
2121
|
+
} else if (!types.includes(type)) {
|
|
2122
|
+
types.push(type);
|
|
2123
|
+
}
|
|
2124
|
+
}
|
|
2125
|
+
return [...byModule].map(([module, types]) => ({ module, types }));
|
|
2126
|
+
}
|
|
1751
2127
|
function modelBaseName(modelFilePath, modelName) {
|
|
1752
2128
|
const base = path.basename(modelFilePath);
|
|
1753
2129
|
const match = base.match(/^(.*)-model\.ts$/);
|
|
@@ -2256,11 +2632,25 @@ function buildDecoratorArgs(options) {
|
|
|
2256
2632
|
}
|
|
2257
2633
|
function addContainerSupportToModel(codegenFs, options, projects) {
|
|
2258
2634
|
const logger = getCodegenLogger();
|
|
2259
|
-
const childType = options.childType?.trim() || "IKosDataModel";
|
|
2260
2635
|
const { modelFilePath } = resolveModelFilePath(codegenFs, options, projects);
|
|
2261
2636
|
if (!codegenFs.exists(modelFilePath)) {
|
|
2262
2637
|
throw new Error(`Model file not found: ${modelFilePath}`);
|
|
2263
2638
|
}
|
|
2639
|
+
const resolvedChild = options.childModel ? resolveChildModelType(
|
|
2640
|
+
codegenFs,
|
|
2641
|
+
{
|
|
2642
|
+
modelFilePath,
|
|
2643
|
+
modelProject: options.modelProject,
|
|
2644
|
+
childModel: options.childModel,
|
|
2645
|
+
childModelProject: options.childModelProject
|
|
2646
|
+
},
|
|
2647
|
+
projects
|
|
2648
|
+
) : {
|
|
2649
|
+
childType: options.childType,
|
|
2650
|
+
childTypeModule: options.childTypeModule
|
|
2651
|
+
};
|
|
2652
|
+
const childType = resolvedChild.childType?.trim() || "IKosDataModel";
|
|
2653
|
+
const childTypeModule = resolvedChild.childTypeModule;
|
|
2264
2654
|
logger.info(
|
|
2265
2655
|
`Adding container support (<${childType}>) to model: ${options.modelName}`
|
|
2266
2656
|
);
|
|
@@ -2272,6 +2662,10 @@ function addContainerSupportToModel(codegenFs, options, projects) {
|
|
|
2272
2662
|
]);
|
|
2273
2663
|
if (childType === "IKosDataModel") {
|
|
2274
2664
|
ensureNamedImport(sf, sdk, [{ name: "IKosDataModel", isTypeOnly: true }]);
|
|
2665
|
+
} else if (childTypeModule) {
|
|
2666
|
+
ensureNamedImport(sf, childTypeModule, [
|
|
2667
|
+
{ name: childType, isTypeOnly: true }
|
|
2668
|
+
]);
|
|
2275
2669
|
}
|
|
2276
2670
|
const cls = getModelClass(sf);
|
|
2277
2671
|
const className = cls.getName();
|
|
@@ -2301,6 +2695,140 @@ function addContainerSupportToModel(codegenFs, options, projects) {
|
|
|
2301
2695
|
});
|
|
2302
2696
|
return { modelFilePath };
|
|
2303
2697
|
}
|
|
2698
|
+
function addParentAwareToModel(codegenFs, options, projects) {
|
|
2699
|
+
const logger = getCodegenLogger();
|
|
2700
|
+
const { modelFilePath } = resolveModelFilePath(codegenFs, options, projects);
|
|
2701
|
+
if (!codegenFs.exists(modelFilePath)) {
|
|
2702
|
+
throw new Error(`Model file not found: ${modelFilePath}`);
|
|
2703
|
+
}
|
|
2704
|
+
logger.info(`Adding parent awareness to model: ${options.modelName}`);
|
|
2705
|
+
let optionsTypeName;
|
|
2706
|
+
transformSourceFile(codegenFs, modelFilePath, (sf) => {
|
|
2707
|
+
const sdk = resolveSdkModuleSpecifier(sf);
|
|
2708
|
+
ensureNamedImport(sf, sdk, [{ name: "kosParentAware" }]);
|
|
2709
|
+
const cls = getModelClass(sf);
|
|
2710
|
+
addClassDecorator(cls, "kosParentAware", {
|
|
2711
|
+
argsText: options.parentId ? `{ parentId: ${JSON.stringify(options.parentId)} }` : ""
|
|
2712
|
+
});
|
|
2713
|
+
const ctor = cls.getConstructors()[0];
|
|
2714
|
+
optionsTypeName = ctor?.getParameters()[1]?.getTypeNode()?.getText()?.replace(/<.*$/, "");
|
|
2715
|
+
});
|
|
2716
|
+
const optionsFilePath = optionsTypeName ? extendOptionsInterface(codegenFs, modelFilePath, optionsTypeName, logger) : void 0;
|
|
2717
|
+
return { modelFilePath, optionsFilePath };
|
|
2718
|
+
}
|
|
2719
|
+
function extendOptionsInterface(codegenFs, modelFilePath, optionsTypeName, logger) {
|
|
2720
|
+
const typesPath = path.join(
|
|
2721
|
+
path.dirname(modelFilePath),
|
|
2722
|
+
"types",
|
|
2723
|
+
"index.d.ts"
|
|
2724
|
+
);
|
|
2725
|
+
const target = codegenFs.exists(typesPath) ? typesPath : modelFilePath;
|
|
2726
|
+
let extended = false;
|
|
2727
|
+
transformSourceFile(codegenFs, target, (sf) => {
|
|
2728
|
+
const iface = sf.getInterface(optionsTypeName);
|
|
2729
|
+
if (!iface) return;
|
|
2730
|
+
const already = iface.getExtends().some((clause) => clause.getText().includes("KosParentAware"));
|
|
2731
|
+
if (already) {
|
|
2732
|
+
extended = true;
|
|
2733
|
+
return;
|
|
2734
|
+
}
|
|
2735
|
+
ensureNamedImport(sf, "@kosdev-code/kos-ui-sdk", [
|
|
2736
|
+
{ name: "KosParentAware", isTypeOnly: true }
|
|
2737
|
+
]);
|
|
2738
|
+
iface.addExtends("KosParentAware");
|
|
2739
|
+
extended = true;
|
|
2740
|
+
});
|
|
2741
|
+
if (!extended) {
|
|
2742
|
+
logger.warn(
|
|
2743
|
+
`Could not find interface ${optionsTypeName}; add "extends KosParentAware" to it by hand.`
|
|
2744
|
+
);
|
|
2745
|
+
return void 0;
|
|
2746
|
+
}
|
|
2747
|
+
return target;
|
|
2748
|
+
}
|
|
2749
|
+
function firstDecoratorArg(decoratorText) {
|
|
2750
|
+
const open = decoratorText.indexOf("(");
|
|
2751
|
+
if (open === -1) return void 0;
|
|
2752
|
+
const inner = decoratorText.slice(open + 1, decoratorText.lastIndexOf(")")).replace(/\s+/g, " ").trim();
|
|
2753
|
+
return inner || void 0;
|
|
2754
|
+
}
|
|
2755
|
+
function collectDecorated(cls, decoratorName) {
|
|
2756
|
+
const members = [];
|
|
2757
|
+
const visit = (name, decoratorTextOf, typeText) => {
|
|
2758
|
+
const text = decoratorTextOf();
|
|
2759
|
+
if (text === void 0) return;
|
|
2760
|
+
members.push({
|
|
2761
|
+
name,
|
|
2762
|
+
arg: firstDecoratorArg(text),
|
|
2763
|
+
type: typeText || void 0
|
|
2764
|
+
});
|
|
2765
|
+
};
|
|
2766
|
+
for (const m of cls.getMethods()) {
|
|
2767
|
+
const dec = m.getDecorator(decoratorName);
|
|
2768
|
+
if (dec) {
|
|
2769
|
+
visit(m.getName(), () => dec.getText(), m.getReturnTypeNode()?.getText());
|
|
2770
|
+
}
|
|
2771
|
+
}
|
|
2772
|
+
for (const p of cls.getProperties()) {
|
|
2773
|
+
const dec = p.getDecorator(decoratorName);
|
|
2774
|
+
if (dec) {
|
|
2775
|
+
visit(p.getName(), () => dec.getText(), p.getTypeNode()?.getText());
|
|
2776
|
+
}
|
|
2777
|
+
}
|
|
2778
|
+
return members;
|
|
2779
|
+
}
|
|
2780
|
+
function describeModel(codegenFs, options, projects) {
|
|
2781
|
+
const { modelFilePath } = resolveModelFilePath(codegenFs, options, projects);
|
|
2782
|
+
const content = codegenFs.read(modelFilePath);
|
|
2783
|
+
if (content === null) {
|
|
2784
|
+
throw new Error(`Model file not found: ${modelFilePath}`);
|
|
2785
|
+
}
|
|
2786
|
+
const project = new Project({ useInMemoryFileSystem: true });
|
|
2787
|
+
const sf = project.createSourceFile(modelFilePath, content, {
|
|
2788
|
+
overwrite: true
|
|
2789
|
+
});
|
|
2790
|
+
let modelType;
|
|
2791
|
+
const modelTypeDecl = sf.getVariableDeclaration("MODEL_TYPE");
|
|
2792
|
+
if (modelTypeDecl) {
|
|
2793
|
+
const init = modelTypeDecl.getInitializer()?.getText();
|
|
2794
|
+
if (init) modelType = init.replace(/^["'`]|["'`]$/g, "");
|
|
2795
|
+
}
|
|
2796
|
+
const cls = sf.getClasses().find((c) => c.getDecorator("kosModel")) ?? sf.getClasses().find((c) => c.isExported()) ?? sf.getClasses()[0];
|
|
2797
|
+
if (!cls) {
|
|
2798
|
+
return {
|
|
2799
|
+
modelFilePath,
|
|
2800
|
+
modelType,
|
|
2801
|
+
classDecorators: [],
|
|
2802
|
+
singleton: false,
|
|
2803
|
+
isCompanion: false,
|
|
2804
|
+
children: [],
|
|
2805
|
+
dependencies: [],
|
|
2806
|
+
topicHandlers: [],
|
|
2807
|
+
configProperties: [],
|
|
2808
|
+
serviceRequests: [],
|
|
2809
|
+
effects: [],
|
|
2810
|
+
futures: []
|
|
2811
|
+
};
|
|
2812
|
+
}
|
|
2813
|
+
const classDecorators = cls.getDecorators().map((d) => d.getName());
|
|
2814
|
+
const kosModelArg = decoratorConfigText(sf, cls, "kosModel");
|
|
2815
|
+
const singleton = /\bsingleton\s*:\s*true\b/.test(kosModelArg ?? "");
|
|
2816
|
+
return {
|
|
2817
|
+
modelFilePath,
|
|
2818
|
+
modelType,
|
|
2819
|
+
className: cls.getName(),
|
|
2820
|
+
classDecorators,
|
|
2821
|
+
singleton,
|
|
2822
|
+
isCompanion: classDecorators.includes("kosCompanion"),
|
|
2823
|
+
children: collectDecorated(cls, "kosChild"),
|
|
2824
|
+
dependencies: collectDecorated(cls, "kosDependency"),
|
|
2825
|
+
topicHandlers: collectDecorated(cls, "kosTopicHandler"),
|
|
2826
|
+
configProperties: collectDecorated(cls, "kosConfigProperty"),
|
|
2827
|
+
serviceRequests: collectDecorated(cls, "kosServiceRequest"),
|
|
2828
|
+
effects: collectDecorated(cls, "kosModelEffect"),
|
|
2829
|
+
futures: collectDecorated(cls, "kosFuture")
|
|
2830
|
+
};
|
|
2831
|
+
}
|
|
2304
2832
|
function addModelEffectToModel(codegenFs, options, projects) {
|
|
2305
2833
|
const logger = getCodegenLogger();
|
|
2306
2834
|
const { modelFilePath } = resolveModelFilePath(codegenFs, options, projects);
|
|
@@ -2376,7 +2904,18 @@ function addChildToModel(codegenFs, options, projects) {
|
|
|
2376
2904
|
logger.info(
|
|
2377
2905
|
`Adding @kosChild "${options.propertyName}" (${shape}) to ${options.modelName}`
|
|
2378
2906
|
);
|
|
2379
|
-
const
|
|
2907
|
+
const resolvedChild = options.childModel ? resolveChildModelType(
|
|
2908
|
+
codegenFs,
|
|
2909
|
+
{
|
|
2910
|
+
modelFilePath,
|
|
2911
|
+
modelProject: options.modelProject,
|
|
2912
|
+
childModel: options.childModel,
|
|
2913
|
+
childModelProject: options.childModelProject
|
|
2914
|
+
},
|
|
2915
|
+
projects
|
|
2916
|
+
) : { childType: options.childType, childTypeModule: options.childPackage };
|
|
2917
|
+
const childType = resolvedChild.childType?.trim();
|
|
2918
|
+
const childTypeModule = resolvedChild.childTypeModule;
|
|
2380
2919
|
transformSourceFile(codegenFs, modelFilePath, (sf) => {
|
|
2381
2920
|
const sdk = resolveSdkModuleSpecifier(sf);
|
|
2382
2921
|
const sdkImports = [
|
|
@@ -2384,8 +2923,8 @@ function addChildToModel(codegenFs, options, projects) {
|
|
|
2384
2923
|
];
|
|
2385
2924
|
if (shape === "container") sdkImports.push({ name: "KosModelContainer" });
|
|
2386
2925
|
ensureNamedImport(sf, sdk, sdkImports);
|
|
2387
|
-
if (
|
|
2388
|
-
ensureNamedImport(sf,
|
|
2926
|
+
if (childTypeModule && childType && /^[A-Za-z_$][\w$]*$/.test(childType) && !FRAMEWORK_TYPES.has(childType)) {
|
|
2927
|
+
ensureNamedImport(sf, childTypeModule, [
|
|
2389
2928
|
{ name: childType, isTypeOnly: true }
|
|
2390
2929
|
]);
|
|
2391
2930
|
}
|
|
@@ -2703,6 +3242,13 @@ function assertServiceModuleAcceptsTransform(codegenFs, serviceModuleFile, model
|
|
|
2703
3242
|
`${file} predates the transform-aware service layer (EndpointCtx takes ${typeParams} type parameter). Run \`kosui api:generate --project ${modelProject} --helpers-only\` to refresh the helper layer in place (no spec fetch, openapi.d.ts untouched), then add the service request.`
|
|
2704
3243
|
);
|
|
2705
3244
|
}
|
|
3245
|
+
const MUTATING_METHODS = /* @__PURE__ */ new Set(["post", "put", "delete", "patch"]);
|
|
3246
|
+
function assertLifecycleSuitsMethod(method, mode, methodName) {
|
|
3247
|
+
if (mode !== "lifecycle" || !MUTATING_METHODS.has(method)) return;
|
|
3248
|
+
throw new Error(
|
|
3249
|
+
`${methodName} is a ${method.toUpperCase()} with a lifecycle, so it would run every time the model reaches that phase rather than when something calls it — a mutation on every load. Drop \`lifecycle\` to get the method-driven form (the default for ${method.toUpperCase()}), or pass \`mode: "method"\`. If the phase really is the trigger, the model calls the method from its own lifecycle hook, where the call is visible.`
|
|
3250
|
+
);
|
|
3251
|
+
}
|
|
2706
3252
|
function assertServiceModuleAcceptsProvisional(codegenFs, serviceModuleFile, modelProject) {
|
|
2707
3253
|
const file = `${serviceModuleFile}.ts`;
|
|
2708
3254
|
if (!codegenFs.exists(file)) return;
|
|
@@ -2884,6 +3430,7 @@ function addServiceRequestToModel(codegenFs, options, projects) {
|
|
|
2884
3430
|
);
|
|
2885
3431
|
const method = (options.method || "get").toLowerCase();
|
|
2886
3432
|
const mode = options.mode ?? (options.lifecycle ? "lifecycle" : "method");
|
|
3433
|
+
assertLifecycleSuitsMethod(method, mode, options.methodName);
|
|
2887
3434
|
const lifecycle = options.lifecycle || "LOAD";
|
|
2888
3435
|
const catalogName = `${pascalCase(modelBase)}Endpoints`;
|
|
2889
3436
|
const mockMode = options.mock ?? "auto";
|
|
@@ -3094,165 +3641,6 @@ function addServiceRequestToModel(codegenFs, options, projects) {
|
|
|
3094
3641
|
}) : void 0
|
|
3095
3642
|
};
|
|
3096
3643
|
}
|
|
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
3644
|
const DEFAULT_SDK_PACKAGE = "@kosdev-code/kos-ui-sdk";
|
|
3257
3645
|
const MAX_SIGNATURE_CHARS = 2e3;
|
|
3258
3646
|
function declarationEntryFromPackageJson(pkgDir) {
|
|
@@ -3436,6 +3824,82 @@ function lookupSdkType(codegenFs, options, projects) {
|
|
|
3436
3824
|
)}). Check the name, or it may be internal / not part of the public surface.`
|
|
3437
3825
|
};
|
|
3438
3826
|
}
|
|
3827
|
+
function modelElementType(typeText) {
|
|
3828
|
+
let m;
|
|
3829
|
+
if (m = typeText.match(/^([A-Za-z_]\w*Model)\s*\[\]$/)) return m[1];
|
|
3830
|
+
if (m = typeText.match(/^Array<\s*([A-Za-z_]\w*Model)\s*>$/)) return m[1];
|
|
3831
|
+
if (m = typeText.match(/^(?:Map|Set)<[^>]*?\b([A-Za-z_]\w*Model)\b[^>]*>$/))
|
|
3832
|
+
return m[1];
|
|
3833
|
+
return null;
|
|
3834
|
+
}
|
|
3835
|
+
function validateModel(codegenFs, options, projects) {
|
|
3836
|
+
const { modelFilePath } = resolveModelFilePath(codegenFs, options, projects);
|
|
3837
|
+
const content = codegenFs.read(modelFilePath);
|
|
3838
|
+
if (content === null) {
|
|
3839
|
+
throw new Error(`Model file not found: ${modelFilePath}`);
|
|
3840
|
+
}
|
|
3841
|
+
const project = new Project({ useInMemoryFileSystem: true });
|
|
3842
|
+
const sf = project.createSourceFile(modelFilePath, content, {
|
|
3843
|
+
overwrite: true
|
|
3844
|
+
});
|
|
3845
|
+
const findings = [];
|
|
3846
|
+
const hasKosModel = sf.getClasses().some((c) => c.getDecorator("kosModel"));
|
|
3847
|
+
if (!hasKosModel) {
|
|
3848
|
+
findings.push({
|
|
3849
|
+
level: "error",
|
|
3850
|
+
rule: "missing-kosModel",
|
|
3851
|
+
message: "No @kosModel decorator found — this is not a KOS model."
|
|
3852
|
+
});
|
|
3853
|
+
}
|
|
3854
|
+
const imports = sf.getImportDeclarations();
|
|
3855
|
+
const mobxImport = imports.find(
|
|
3856
|
+
(d) => /^mobx(-react-lite)?$/.test(d.getModuleSpecifierValue())
|
|
3857
|
+
);
|
|
3858
|
+
if (mobxImport) {
|
|
3859
|
+
findings.push({
|
|
3860
|
+
level: "error",
|
|
3861
|
+
rule: "mobx-import",
|
|
3862
|
+
message: "Imports mobx directly. Reactivity is internal to KOS — remove the mobx import and use KOS reactivity / container models."
|
|
3863
|
+
});
|
|
3864
|
+
}
|
|
3865
|
+
const barrelServiceRequest = imports.find(
|
|
3866
|
+
(d) => d.getModuleSpecifierValue() === "@kosdev-code/kos-ui-sdk" && d.getNamedImports().some((n) => n.getName() === "kosServiceRequest")
|
|
3867
|
+
);
|
|
3868
|
+
if (barrelServiceRequest) {
|
|
3869
|
+
findings.push({
|
|
3870
|
+
level: "warning",
|
|
3871
|
+
rule: "untyped-service-request",
|
|
3872
|
+
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."
|
|
3873
|
+
});
|
|
3874
|
+
}
|
|
3875
|
+
for (const cls of sf.getClasses()) {
|
|
3876
|
+
for (const prop of cls.getProperties()) {
|
|
3877
|
+
const name = prop.getName();
|
|
3878
|
+
const typeText = (prop.getTypeNode()?.getText() ?? "").replace(/\s+/g, " ").trim();
|
|
3879
|
+
const initText = prop.getInitializer()?.getText() ?? "";
|
|
3880
|
+
const hasChild = !!prop.getDecorator("kosChild");
|
|
3881
|
+
const isModelContainer = /\bI?KosModelContainer\s*</.test(typeText) || /new\s+KosModelContainer\b/.test(initText);
|
|
3882
|
+
if (isModelContainer && !hasChild) {
|
|
3883
|
+
findings.push({
|
|
3884
|
+
level: "warning",
|
|
3885
|
+
rule: "container-missing-kosChild",
|
|
3886
|
+
message: `Property '${name}' is a model container but is not marked @kosChild — add @kosChild so its models join the model graph and lifecycle.`
|
|
3887
|
+
});
|
|
3888
|
+
continue;
|
|
3889
|
+
}
|
|
3890
|
+
const elem = modelElementType(typeText);
|
|
3891
|
+
if (elem && !isModelContainer) {
|
|
3892
|
+
findings.push({
|
|
3893
|
+
level: "warning",
|
|
3894
|
+
rule: "raw-model-collection",
|
|
3895
|
+
message: `Property '${name}' is a raw ${typeText} of models — prefer a KosModelContainer<${elem}> (indexing, sorting, lifecycle, delta handling) marked @kosChild.`
|
|
3896
|
+
});
|
|
3897
|
+
}
|
|
3898
|
+
}
|
|
3899
|
+
}
|
|
3900
|
+
const hasError = findings.some((f) => f.level === "error");
|
|
3901
|
+
return { modelFilePath, ok: !hasError, findings };
|
|
3902
|
+
}
|
|
3439
3903
|
const PLUGIN_TYPES = {
|
|
3440
3904
|
CUI: "cui",
|
|
3441
3905
|
UTILITY: "utility",
|
|
@@ -3990,9 +4454,76 @@ function updatePluginConfiguration(codegenFs, projectConfig, options) {
|
|
|
3990
4454
|
);
|
|
3991
4455
|
}
|
|
3992
4456
|
}
|
|
4457
|
+
function parse(content) {
|
|
4458
|
+
const singleQuoted = /^\s*import\b[^"']*'/m.test(content);
|
|
4459
|
+
const project = new Project({
|
|
4460
|
+
useInMemoryFileSystem: true,
|
|
4461
|
+
manipulationSettings: {
|
|
4462
|
+
indentationText: IndentationText.TwoSpaces,
|
|
4463
|
+
quoteKind: singleQuoted ? QuoteKind.Single : QuoteKind.Double
|
|
4464
|
+
}
|
|
4465
|
+
});
|
|
4466
|
+
return project.createSourceFile("registration-chain.ts", content, {
|
|
4467
|
+
overwrite: true
|
|
4468
|
+
});
|
|
4469
|
+
}
|
|
4470
|
+
function chainCalls(sf, name) {
|
|
4471
|
+
return sf.getDescendantsOfKind(SyntaxKind.CallExpression).filter((call) => {
|
|
4472
|
+
const callee = call.getExpression();
|
|
4473
|
+
return callee.getKind() === SyntaxKind.PropertyAccessExpression && callee.asKindOrThrow(SyntaxKind.PropertyAccessExpression).getName() === name;
|
|
4474
|
+
});
|
|
4475
|
+
}
|
|
4476
|
+
function indentOfCall(sf, call) {
|
|
4477
|
+
const text = sf.getFullText();
|
|
4478
|
+
const nameNode = call.getExpression().asKindOrThrow(SyntaxKind.PropertyAccessExpression).getNameNode();
|
|
4479
|
+
const lineStart = text.lastIndexOf("\n", nameNode.getStart()) + 1;
|
|
4480
|
+
return text.slice(lineStart).match(/^[ \t]*/)?.[0] ?? "";
|
|
4481
|
+
}
|
|
4482
|
+
function countChainEntries(content) {
|
|
4483
|
+
return chainCalls(parse(content), "model").length;
|
|
4484
|
+
}
|
|
4485
|
+
function upsertChainEntry(content, bean, importSpec) {
|
|
4486
|
+
const sf = parse(content);
|
|
4487
|
+
const modelCalls = chainCalls(sf, "model");
|
|
4488
|
+
const registered = modelCalls.some((call) => {
|
|
4489
|
+
const [arg, ...rest] = call.getArguments();
|
|
4490
|
+
return rest.length === 0 && arg?.getText() === bean;
|
|
4491
|
+
});
|
|
4492
|
+
if (registered) return { changed: false, note: "already registered" };
|
|
4493
|
+
let anchor;
|
|
4494
|
+
let indent;
|
|
4495
|
+
if (modelCalls.length > 0) {
|
|
4496
|
+
anchor = modelCalls.reduce(
|
|
4497
|
+
(last, call) => call.getEnd() > last.getEnd() ? call : last
|
|
4498
|
+
);
|
|
4499
|
+
indent = indentOfCall(sf, anchor);
|
|
4500
|
+
} else {
|
|
4501
|
+
const starts = chainCalls(sf, "models").filter(
|
|
4502
|
+
(call) => call.getArguments().length === 0
|
|
4503
|
+
);
|
|
4504
|
+
anchor = starts[starts.length - 1];
|
|
4505
|
+
if (!anchor) {
|
|
4506
|
+
return {
|
|
4507
|
+
changed: false,
|
|
4508
|
+
error: "no .models() chain found in the registration file"
|
|
4509
|
+
};
|
|
4510
|
+
}
|
|
4511
|
+
const text = sf.getFullText();
|
|
4512
|
+
const lineStart = text.lastIndexOf("\n", anchor.getStart()) + 1;
|
|
4513
|
+
indent = (text.slice(lineStart).match(/^[ \t]*/)?.[0] ?? "") + " ";
|
|
4514
|
+
}
|
|
4515
|
+
anchor.replaceWithText(`${anchor.getText()}
|
|
4516
|
+
${indent}.model(${bean})`);
|
|
4517
|
+
const imported = sf.getImportDeclarations().some(
|
|
4518
|
+
(decl) => decl.getNamedImports().some((named) => named.getName() === bean)
|
|
4519
|
+
);
|
|
4520
|
+
if (!imported) ensureNamedImport(sf, importSpec, [{ name: bean }]);
|
|
4521
|
+
return { changed: true, content: sf.getFullText(), importAdded: !imported };
|
|
4522
|
+
}
|
|
3993
4523
|
export {
|
|
3994
4524
|
BasePluginHandler,
|
|
3995
4525
|
CONTRIBUTION_TYPE_MAP,
|
|
4526
|
+
DEFAULT_MODEL_LIBS_DIR,
|
|
3996
4527
|
DirectFileSystem,
|
|
3997
4528
|
KAB_OUTPUT_DIR,
|
|
3998
4529
|
KAB_OUTPUT_PATH,
|
|
@@ -4014,6 +4545,7 @@ export {
|
|
|
4014
4545
|
addJavaArtifactToManifests,
|
|
4015
4546
|
addKosModelConfiguration,
|
|
4016
4547
|
addModelEffectToModel,
|
|
4548
|
+
addParentAwareToModel,
|
|
4017
4549
|
addPropertyToModel,
|
|
4018
4550
|
addServiceRequestToModel,
|
|
4019
4551
|
addTopicHandlerToModel,
|
|
@@ -4022,6 +4554,7 @@ export {
|
|
|
4022
4554
|
buildSbomTarget,
|
|
4023
4555
|
camelCase,
|
|
4024
4556
|
constantCase,
|
|
4557
|
+
countChainEntries,
|
|
4025
4558
|
dashCase,
|
|
4026
4559
|
describeModel,
|
|
4027
4560
|
discoverJavaArtifacts,
|
|
@@ -4040,8 +4573,10 @@ export {
|
|
|
4040
4573
|
generateHook,
|
|
4041
4574
|
generateInit,
|
|
4042
4575
|
generateModel,
|
|
4576
|
+
generateModelProject,
|
|
4043
4577
|
generatePolyglotWorkspace,
|
|
4044
4578
|
generateSplashProject,
|
|
4579
|
+
generateViewModel,
|
|
4045
4580
|
getCodegenLogger,
|
|
4046
4581
|
getCurrentDirectoryName,
|
|
4047
4582
|
getKosModelConfigProp,
|
|
@@ -4059,10 +4594,12 @@ export {
|
|
|
4059
4594
|
readNxJson,
|
|
4060
4595
|
resolveKabPath,
|
|
4061
4596
|
resolveModelFilePath,
|
|
4597
|
+
resolveModelProjectLayout,
|
|
4062
4598
|
setCodegenLogger,
|
|
4063
4599
|
syncCiManifests,
|
|
4064
4600
|
updateJson,
|
|
4065
4601
|
updateModelIndex,
|
|
4602
|
+
upsertChainEntry,
|
|
4066
4603
|
validateModel,
|
|
4067
4604
|
writeJson
|
|
4068
4605
|
};
|