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