@kosdev-code/kos-codegen-core 0.1.0-next.871 → 0.1.0-next.888
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.d.ts +2 -2
- package/index.d.ts.map +1 -1
- package/index.js +854 -335
- package/index.js.map +1 -1
- package/index.mjs +855 -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 +11 -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 +19 -1
- package/lib/generators/augment/ts-toolkit.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 +35 -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/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-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;
|
|
@@ -925,6 +1003,58 @@ function normalizeOptions(codegenFs, options, projects) {
|
|
|
925
1003
|
template: ""
|
|
926
1004
|
};
|
|
927
1005
|
}
|
|
1006
|
+
const UPDATE_RELEASE_VERSION_SCRIPT = `import devkit from "@nx/devkit";
|
|
1007
|
+
import { resolve } from "path";
|
|
1008
|
+
import { readFileSync, writeFileSync } from "fs";
|
|
1009
|
+
import prettier from "prettier";
|
|
1010
|
+
|
|
1011
|
+
// KOS artifact versioning: stamps the project's .kos.json "version" field
|
|
1012
|
+
// (which kabtool bakes into the KAB). Never touches package.json.
|
|
1013
|
+
// Driven by tag-based releases:
|
|
1014
|
+
// nx run-many --target=version --args=--ver=$KOSBUILD_VERSION
|
|
1015
|
+
|
|
1016
|
+
const { readCachedProjectGraph } = devkit;
|
|
1017
|
+
const [, , name, versionArg] = process.argv;
|
|
1018
|
+
|
|
1019
|
+
// "{args.ver}" arrives literally when the target runs without --args=--ver=<v>;
|
|
1020
|
+
// treat that (or a missing arg) as "report current version, change nothing".
|
|
1021
|
+
const version =
|
|
1022
|
+
versionArg && !versionArg.startsWith("{args") ? versionArg : undefined;
|
|
1023
|
+
|
|
1024
|
+
if (!name) {
|
|
1025
|
+
console.error("usage: update-release-version.mjs <project> <version>");
|
|
1026
|
+
process.exit(1);
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
const graph = readCachedProjectGraph();
|
|
1030
|
+
const project = graph.nodes[name];
|
|
1031
|
+
if (!project) {
|
|
1032
|
+
console.error("Unknown project: " + name);
|
|
1033
|
+
process.exit(1);
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
const kosJsonPath = resolve(process.cwd(), project.data.root, ".kos.json");
|
|
1037
|
+
let kosJson;
|
|
1038
|
+
try {
|
|
1039
|
+
kosJson = JSON.parse(readFileSync(kosJsonPath, "utf8"));
|
|
1040
|
+
} catch {
|
|
1041
|
+
console.error("Missing or invalid .kos.json: " + kosJsonPath);
|
|
1042
|
+
process.exit(1);
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
if (!version) {
|
|
1046
|
+
console.log(name + ": " + kosJson.version + " (no --ver given; unchanged)");
|
|
1047
|
+
process.exit(0);
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
const prettierOptions = await prettier.resolveConfig(kosJsonPath);
|
|
1051
|
+
const output = await prettier.format(
|
|
1052
|
+
JSON.stringify({ ...kosJson, version }, null, 2),
|
|
1053
|
+
{ ...prettierOptions, parser: "json" }
|
|
1054
|
+
);
|
|
1055
|
+
writeFileSync(kosJsonPath, output);
|
|
1056
|
+
console.log(name + ": version -> " + version);
|
|
1057
|
+
`;
|
|
928
1058
|
function transformSourceFile(codegenFs, filePath, mutate) {
|
|
929
1059
|
const content = codegenFs.read(filePath);
|
|
930
1060
|
if (content === null) {
|
|
@@ -1120,6 +1250,40 @@ function ensureBarrelExport(sourceFile, moduleSpecifier) {
|
|
|
1120
1250
|
sourceFile.addExportDeclaration({ moduleSpecifier });
|
|
1121
1251
|
return true;
|
|
1122
1252
|
}
|
|
1253
|
+
function ensureNamedExports(sourceFile, moduleSpecifier, names, isTypeOnly = false) {
|
|
1254
|
+
const existing = sourceFile.getExportDeclarations().find(
|
|
1255
|
+
(d) => d.getModuleSpecifierValue() === moduleSpecifier && d.isTypeOnly() === isTypeOnly
|
|
1256
|
+
);
|
|
1257
|
+
if (!existing) {
|
|
1258
|
+
sourceFile.addExportDeclaration({
|
|
1259
|
+
moduleSpecifier,
|
|
1260
|
+
isTypeOnly,
|
|
1261
|
+
namedExports: names.map((name) => {
|
|
1262
|
+
return { name };
|
|
1263
|
+
})
|
|
1264
|
+
});
|
|
1265
|
+
return true;
|
|
1266
|
+
}
|
|
1267
|
+
const present = new Set(existing.getNamedExports().map((e) => e.getName()));
|
|
1268
|
+
const missing = names.filter((name) => !present.has(name));
|
|
1269
|
+
if (missing.length === 0) return false;
|
|
1270
|
+
existing.addNamedExports(
|
|
1271
|
+
missing.map((name) => {
|
|
1272
|
+
return { name };
|
|
1273
|
+
})
|
|
1274
|
+
);
|
|
1275
|
+
return true;
|
|
1276
|
+
}
|
|
1277
|
+
function ensureExportedInterface(sourceFile, name, properties = [], extendsTypes = []) {
|
|
1278
|
+
if (sourceFile.getInterface(name)) return false;
|
|
1279
|
+
sourceFile.addInterface({
|
|
1280
|
+
name,
|
|
1281
|
+
isExported: true,
|
|
1282
|
+
extends: extendsTypes,
|
|
1283
|
+
properties
|
|
1284
|
+
});
|
|
1285
|
+
return true;
|
|
1286
|
+
}
|
|
1123
1287
|
function addDecoratedMethod(cls, spec) {
|
|
1124
1288
|
if (cls.getMethod(spec.name)) return false;
|
|
1125
1289
|
cls.addMethod({
|
|
@@ -1272,58 +1436,6 @@ function readSummary(methodProp) {
|
|
|
1272
1436
|
function unquote(name) {
|
|
1273
1437
|
return name.replace(/^["']|["']$/g, "");
|
|
1274
1438
|
}
|
|
1275
|
-
const UPDATE_RELEASE_VERSION_SCRIPT = `import devkit from "@nx/devkit";
|
|
1276
|
-
import { resolve } from "path";
|
|
1277
|
-
import { readFileSync, writeFileSync } from "fs";
|
|
1278
|
-
import prettier from "prettier";
|
|
1279
|
-
|
|
1280
|
-
// KOS artifact versioning: stamps the project's .kos.json "version" field
|
|
1281
|
-
// (which kabtool bakes into the KAB). Never touches package.json.
|
|
1282
|
-
// Driven by tag-based releases:
|
|
1283
|
-
// nx run-many --target=version --args=--ver=$KOSBUILD_VERSION
|
|
1284
|
-
|
|
1285
|
-
const { readCachedProjectGraph } = devkit;
|
|
1286
|
-
const [, , name, versionArg] = process.argv;
|
|
1287
|
-
|
|
1288
|
-
// "{args.ver}" arrives literally when the target runs without --args=--ver=<v>;
|
|
1289
|
-
// treat that (or a missing arg) as "report current version, change nothing".
|
|
1290
|
-
const version =
|
|
1291
|
-
versionArg && !versionArg.startsWith("{args") ? versionArg : undefined;
|
|
1292
|
-
|
|
1293
|
-
if (!name) {
|
|
1294
|
-
console.error("usage: update-release-version.mjs <project> <version>");
|
|
1295
|
-
process.exit(1);
|
|
1296
|
-
}
|
|
1297
|
-
|
|
1298
|
-
const graph = readCachedProjectGraph();
|
|
1299
|
-
const project = graph.nodes[name];
|
|
1300
|
-
if (!project) {
|
|
1301
|
-
console.error("Unknown project: " + name);
|
|
1302
|
-
process.exit(1);
|
|
1303
|
-
}
|
|
1304
|
-
|
|
1305
|
-
const kosJsonPath = resolve(process.cwd(), project.data.root, ".kos.json");
|
|
1306
|
-
let kosJson;
|
|
1307
|
-
try {
|
|
1308
|
-
kosJson = JSON.parse(readFileSync(kosJsonPath, "utf8"));
|
|
1309
|
-
} catch {
|
|
1310
|
-
console.error("Missing or invalid .kos.json: " + kosJsonPath);
|
|
1311
|
-
process.exit(1);
|
|
1312
|
-
}
|
|
1313
|
-
|
|
1314
|
-
if (!version) {
|
|
1315
|
-
console.log(name + ": " + kosJson.version + " (no --ver given; unchanged)");
|
|
1316
|
-
process.exit(0);
|
|
1317
|
-
}
|
|
1318
|
-
|
|
1319
|
-
const prettierOptions = await prettier.resolveConfig(kosJsonPath);
|
|
1320
|
-
const output = await prettier.format(
|
|
1321
|
-
JSON.stringify({ ...kosJson, version }, null, 2),
|
|
1322
|
-
{ ...prettierOptions, parser: "json" }
|
|
1323
|
-
);
|
|
1324
|
-
writeFileSync(kosJsonPath, output);
|
|
1325
|
-
console.log(name + ": version -> " + version);
|
|
1326
|
-
`;
|
|
1327
1439
|
function appendBarrelExport(codegenFs, indexPath, exportPath) {
|
|
1328
1440
|
const exportLine = `export * from '${exportPath}'`;
|
|
1329
1441
|
const content = codegenFs.read(indexPath) ?? "";
|
|
@@ -1370,10 +1482,57 @@ function updateModelIndex(codegenFs, indexPath, modelPath) {
|
|
|
1370
1482
|
const newContents = printer.printFile(updatedSourceFile);
|
|
1371
1483
|
codegenFs.write(indexPath, newContents);
|
|
1372
1484
|
}
|
|
1373
|
-
function
|
|
1374
|
-
|
|
1375
|
-
|
|
1485
|
+
function generateCompanionModel(codegenFs, templateDir, options, projects) {
|
|
1486
|
+
const logger = getCodegenLogger();
|
|
1487
|
+
const normalized = normalizeAllValues({
|
|
1488
|
+
companionModelName: options.companionModelName,
|
|
1489
|
+
modelName: options.modelName
|
|
1490
|
+
});
|
|
1491
|
+
const companionChildKosConfig = getKosProjectConfiguration(
|
|
1492
|
+
codegenFs,
|
|
1493
|
+
options.companionModelProject,
|
|
1494
|
+
projects
|
|
1495
|
+
);
|
|
1496
|
+
const parentProject = findProjectByName(
|
|
1497
|
+
codegenFs.root,
|
|
1498
|
+
options.modelProject,
|
|
1499
|
+
projects
|
|
1500
|
+
);
|
|
1501
|
+
const childProject = findProjectByName(
|
|
1502
|
+
codegenFs.root,
|
|
1503
|
+
options.companionModelProject,
|
|
1504
|
+
projects
|
|
1505
|
+
);
|
|
1506
|
+
const projectRoot = childProject?.sourceRoot;
|
|
1507
|
+
if (!projectRoot) {
|
|
1508
|
+
logger.warn(`Companion child project source root not found`);
|
|
1509
|
+
return;
|
|
1510
|
+
}
|
|
1511
|
+
let importPath = "";
|
|
1512
|
+
if (parentProject) {
|
|
1513
|
+
const pkgJsonPath = path.join(parentProject.root, "package.json");
|
|
1514
|
+
try {
|
|
1515
|
+
const pkgJson = readJson(codegenFs, pkgJsonPath);
|
|
1516
|
+
importPath = pkgJson.name || "";
|
|
1517
|
+
} catch {
|
|
1518
|
+
importPath = "";
|
|
1519
|
+
}
|
|
1376
1520
|
}
|
|
1521
|
+
const modelLocation = companionChildKosConfig?.generator?.defaults?.model?.folder || "";
|
|
1522
|
+
const filePath = path.join(
|
|
1523
|
+
projectRoot,
|
|
1524
|
+
modelLocation,
|
|
1525
|
+
normalized.companionModelNameDashCase
|
|
1526
|
+
);
|
|
1527
|
+
logger.info(`Generating companion model in ${filePath}`);
|
|
1528
|
+
generateFilesFromTemplates(codegenFs, templateDir, filePath, {
|
|
1529
|
+
...options,
|
|
1530
|
+
...normalized,
|
|
1531
|
+
importPath
|
|
1532
|
+
});
|
|
1533
|
+
}
|
|
1534
|
+
function generateContainerModel(codegenFs, templateDir, options, cwd, projects) {
|
|
1535
|
+
const logger = getCodegenLogger();
|
|
1377
1536
|
const currentProject = getProject(codegenFs, cwd);
|
|
1378
1537
|
const modelProjectName = options.modelProject || currentProject?.name;
|
|
1379
1538
|
if (!modelProjectName) {
|
|
@@ -1381,56 +1540,109 @@ function generateHook(codegenFs, templateDir, options, cwd, projects) {
|
|
|
1381
1540
|
"No model project found. Please specify a model project with --modelProject."
|
|
1382
1541
|
);
|
|
1383
1542
|
}
|
|
1384
|
-
const modelName = options.
|
|
1543
|
+
const modelName = options.modelName || getCurrentDirectoryName(cwd);
|
|
1385
1544
|
if (!modelName) {
|
|
1386
1545
|
throw new Error(
|
|
1387
1546
|
"No model name found. Please specify a model name with --name."
|
|
1388
1547
|
);
|
|
1389
1548
|
}
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
modelProjectName,
|
|
1393
|
-
modelName,
|
|
1394
|
-
projects
|
|
1395
|
-
);
|
|
1396
|
-
options.singleton = kosModelConfig ? !!kosModelConfig.singleton : false;
|
|
1397
|
-
options.name = modelName;
|
|
1398
|
-
options.modelProject = modelProjectName;
|
|
1549
|
+
options.modelName = modelName;
|
|
1550
|
+
options.name = `${modelName}-container`;
|
|
1399
1551
|
const normalized = normalizeOptions(codegenFs, options, projects);
|
|
1400
|
-
const
|
|
1552
|
+
const projectConfig = findProjectByName(
|
|
1401
1553
|
codegenFs.root,
|
|
1402
|
-
normalized.
|
|
1554
|
+
normalized.modelProject,
|
|
1403
1555
|
projects
|
|
1404
1556
|
);
|
|
1405
|
-
if (!
|
|
1406
|
-
throw new Error(`
|
|
1557
|
+
if (!projectConfig) {
|
|
1558
|
+
throw new Error(`Model project '${normalized.modelProject}' not found`);
|
|
1407
1559
|
}
|
|
1560
|
+
addKosModelConfiguration({
|
|
1561
|
+
codegenFs,
|
|
1562
|
+
modelName: normalized.nameDashCase,
|
|
1563
|
+
projectName: projectConfig.name,
|
|
1564
|
+
projectRoot: projectConfig.root,
|
|
1565
|
+
singleton: !!options.singleton,
|
|
1566
|
+
container: true,
|
|
1567
|
+
// The container's exported registration bean (`export const <ProperCase>`).
|
|
1568
|
+
factory: normalized.nameProperCase
|
|
1569
|
+
});
|
|
1408
1570
|
const kosConfig = getKosProjectConfiguration(
|
|
1409
1571
|
codegenFs,
|
|
1410
|
-
|
|
1572
|
+
projectConfig.name,
|
|
1411
1573
|
projects
|
|
1412
1574
|
);
|
|
1413
|
-
const
|
|
1414
|
-
|
|
1415
|
-
|
|
1575
|
+
const modelLocation = kosConfig?.generator?.defaults?.model?.folder || "";
|
|
1576
|
+
const internal = !!kosConfig?.generator?.internal;
|
|
1577
|
+
options.modelDirectory = options.modelDirectory || modelLocation;
|
|
1578
|
+
const projectRoot = projectConfig.sourceRoot;
|
|
1416
1579
|
if (projectRoot) {
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
templateDir,
|
|
1420
|
-
path.join(
|
|
1421
|
-
projectRoot,
|
|
1422
|
-
options.appDirectory,
|
|
1423
|
-
"hooks",
|
|
1424
|
-
normalized.nameDashCase
|
|
1425
|
-
),
|
|
1426
|
-
normalized
|
|
1580
|
+
logger.info(
|
|
1581
|
+
`Generating container model ${normalized.nameDashCase} in ${projectRoot}`
|
|
1427
1582
|
);
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1583
|
+
const modelNameDashCase = normalized.modelNameDashCase || normalized.nameDashCase;
|
|
1584
|
+
const modelFolder = path.join(
|
|
1585
|
+
projectRoot,
|
|
1586
|
+
options.modelDirectory || "",
|
|
1587
|
+
modelNameDashCase
|
|
1432
1588
|
);
|
|
1433
|
-
|
|
1589
|
+
if (options.existingModel) {
|
|
1590
|
+
addContainerToExistingModelFolder(codegenFs, templateDir, modelFolder, {
|
|
1591
|
+
...normalized,
|
|
1592
|
+
internal
|
|
1593
|
+
});
|
|
1594
|
+
} else {
|
|
1595
|
+
generateFilesFromTemplates(
|
|
1596
|
+
codegenFs,
|
|
1597
|
+
path.join(templateDir, "model"),
|
|
1598
|
+
modelFolder,
|
|
1599
|
+
{ ...normalized, internal }
|
|
1600
|
+
);
|
|
1601
|
+
}
|
|
1602
|
+
const modelIndex = path.join(projectRoot, "index.ts");
|
|
1603
|
+
const modelPath = normalized.modelDirectory ? `${normalized.modelDirectory}/${modelNameDashCase}` : modelNameDashCase;
|
|
1604
|
+
updateModelIndex(codegenFs, modelIndex, modelPath);
|
|
1605
|
+
}
|
|
1606
|
+
}
|
|
1607
|
+
function addContainerToExistingModelFolder(codegenFs, templateDir, modelFolder, substitutions) {
|
|
1608
|
+
const containerFileName = `${substitutions.nameDashCase}-model.ts`;
|
|
1609
|
+
const containerTemplate = path.join(
|
|
1610
|
+
templateDir,
|
|
1611
|
+
"model",
|
|
1612
|
+
"__nameDashCase__-model.ts.template"
|
|
1613
|
+
);
|
|
1614
|
+
const rendered = ejs.render(
|
|
1615
|
+
fs.readFileSync(containerTemplate, "utf-8"),
|
|
1616
|
+
substitutions,
|
|
1617
|
+
{ filename: containerTemplate }
|
|
1618
|
+
);
|
|
1619
|
+
codegenFs.write(path.join(modelFolder, containerFileName), rendered);
|
|
1620
|
+
const typesPath = path.join(modelFolder, "types", "index.d.ts");
|
|
1621
|
+
if (codegenFs.read(typesPath) === null) {
|
|
1622
|
+
codegenFs.write(typesPath, "");
|
|
1623
|
+
}
|
|
1624
|
+
transformSourceFile(codegenFs, typesPath, (sourceFile) => {
|
|
1625
|
+
ensureExportedInterface(
|
|
1626
|
+
sourceFile,
|
|
1627
|
+
`${substitutions.nameProperCase}Options`
|
|
1628
|
+
);
|
|
1629
|
+
});
|
|
1630
|
+
const barrelPath = path.join(modelFolder, "index.ts");
|
|
1631
|
+
if (codegenFs.read(barrelPath) === null) {
|
|
1632
|
+
codegenFs.write(barrelPath, "");
|
|
1633
|
+
}
|
|
1634
|
+
transformSourceFile(codegenFs, barrelPath, (sourceFile) => {
|
|
1635
|
+
const moduleSpecifier = `./${containerFileName.replace(/\.ts$/, "")}`;
|
|
1636
|
+
ensureNamedExports(sourceFile, moduleSpecifier, [
|
|
1637
|
+
substitutions.nameProperCase
|
|
1638
|
+
]);
|
|
1639
|
+
ensureNamedExports(
|
|
1640
|
+
sourceFile,
|
|
1641
|
+
moduleSpecifier,
|
|
1642
|
+
[`${substitutions.nameProperCase}Model`],
|
|
1643
|
+
true
|
|
1644
|
+
);
|
|
1645
|
+
});
|
|
1434
1646
|
}
|
|
1435
1647
|
function generateContext(codegenFs, templateDir, options, cwd, projects) {
|
|
1436
1648
|
if (!options.appProject) {
|
|
@@ -1484,8 +1696,10 @@ function generateContext(codegenFs, templateDir, options, cwd, projects) {
|
|
|
1484
1696
|
);
|
|
1485
1697
|
}
|
|
1486
1698
|
}
|
|
1487
|
-
function
|
|
1488
|
-
|
|
1699
|
+
function generateHook(codegenFs, templateDir, options, cwd, projects) {
|
|
1700
|
+
if (!options.appProject) {
|
|
1701
|
+
throw new Error("No app project specified");
|
|
1702
|
+
}
|
|
1489
1703
|
const currentProject = getProject(codegenFs, cwd);
|
|
1490
1704
|
const modelProjectName = options.modelProject || currentProject?.name;
|
|
1491
1705
|
if (!modelProjectName) {
|
|
@@ -1493,106 +1707,56 @@ function generateContainerModel(codegenFs, templateDir, options, cwd, projects)
|
|
|
1493
1707
|
"No model project found. Please specify a model project with --modelProject."
|
|
1494
1708
|
);
|
|
1495
1709
|
}
|
|
1496
|
-
const modelName = options.
|
|
1710
|
+
const modelName = options.name || getCurrentDirectoryName(cwd);
|
|
1497
1711
|
if (!modelName) {
|
|
1498
1712
|
throw new Error(
|
|
1499
1713
|
"No model name found. Please specify a model name with --name."
|
|
1500
1714
|
);
|
|
1501
1715
|
}
|
|
1502
|
-
|
|
1503
|
-
|
|
1716
|
+
const kosModelConfig = getKosModelConfiguration(
|
|
1717
|
+
codegenFs,
|
|
1718
|
+
modelProjectName,
|
|
1719
|
+
modelName,
|
|
1720
|
+
projects
|
|
1721
|
+
);
|
|
1722
|
+
options.singleton = kosModelConfig ? !!kosModelConfig.singleton : false;
|
|
1723
|
+
options.name = modelName;
|
|
1724
|
+
options.modelProject = modelProjectName;
|
|
1504
1725
|
const normalized = normalizeOptions(codegenFs, options, projects);
|
|
1505
|
-
const
|
|
1726
|
+
const appProject = findProjectByName(
|
|
1506
1727
|
codegenFs.root,
|
|
1507
|
-
normalized.
|
|
1728
|
+
normalized.appProject,
|
|
1508
1729
|
projects
|
|
1509
1730
|
);
|
|
1510
|
-
if (!
|
|
1511
|
-
throw new Error(`
|
|
1731
|
+
if (!appProject) {
|
|
1732
|
+
throw new Error(`App project '${normalized.appProject}' not found`);
|
|
1512
1733
|
}
|
|
1513
|
-
addKosModelConfiguration({
|
|
1514
|
-
codegenFs,
|
|
1515
|
-
modelName: normalized.nameDashCase,
|
|
1516
|
-
projectName: projectConfig.name,
|
|
1517
|
-
projectRoot: projectConfig.root,
|
|
1518
|
-
singleton: !!options.singleton,
|
|
1519
|
-
container: true,
|
|
1520
|
-
// The container's exported registration bean (`export const <ProperCase>`).
|
|
1521
|
-
factory: normalized.nameProperCase
|
|
1522
|
-
});
|
|
1523
1734
|
const kosConfig = getKosProjectConfiguration(
|
|
1524
1735
|
codegenFs,
|
|
1525
|
-
|
|
1736
|
+
appProject.name,
|
|
1526
1737
|
projects
|
|
1527
1738
|
);
|
|
1528
|
-
const
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
const projectRoot = projectConfig.sourceRoot;
|
|
1739
|
+
const componentLocation = kosConfig?.generator?.defaults?.component?.folder || "";
|
|
1740
|
+
options.appDirectory = options.appDirectory || componentLocation;
|
|
1741
|
+
const projectRoot = appProject.sourceRoot;
|
|
1532
1742
|
if (projectRoot) {
|
|
1533
|
-
logger.info(
|
|
1534
|
-
`Generating container model ${normalized.nameDashCase} in ${projectRoot}`
|
|
1535
|
-
);
|
|
1536
|
-
const modelNameDashCase = normalized.modelNameDashCase || normalized.nameDashCase;
|
|
1537
1743
|
generateFilesFromTemplates(
|
|
1538
1744
|
codegenFs,
|
|
1539
|
-
|
|
1540
|
-
path.join(
|
|
1541
|
-
|
|
1745
|
+
templateDir,
|
|
1746
|
+
path.join(
|
|
1747
|
+
projectRoot,
|
|
1748
|
+
options.appDirectory,
|
|
1749
|
+
"hooks",
|
|
1750
|
+
normalized.nameDashCase
|
|
1751
|
+
),
|
|
1752
|
+
normalized
|
|
1753
|
+
);
|
|
1754
|
+
appendBarrelExport(
|
|
1755
|
+
codegenFs,
|
|
1756
|
+
path.join(projectRoot, options.appDirectory, "hooks", "index.ts"),
|
|
1757
|
+
`./${normalized.nameDashCase}`
|
|
1542
1758
|
);
|
|
1543
|
-
const modelIndex = path.join(projectRoot, "index.ts");
|
|
1544
|
-
const modelPath = normalized.modelDirectory ? `${normalized.modelDirectory}/${modelNameDashCase}` : modelNameDashCase;
|
|
1545
|
-
updateModelIndex(codegenFs, modelIndex, modelPath);
|
|
1546
|
-
}
|
|
1547
|
-
}
|
|
1548
|
-
function generateCompanionModel(codegenFs, templateDir, options, projects) {
|
|
1549
|
-
const logger = getCodegenLogger();
|
|
1550
|
-
const normalized = normalizeAllValues({
|
|
1551
|
-
companionModelName: options.companionModelName,
|
|
1552
|
-
modelName: options.modelName
|
|
1553
|
-
});
|
|
1554
|
-
const companionChildKosConfig = getKosProjectConfiguration(
|
|
1555
|
-
codegenFs,
|
|
1556
|
-
options.companionModelProject,
|
|
1557
|
-
projects
|
|
1558
|
-
);
|
|
1559
|
-
const parentProject = findProjectByName(
|
|
1560
|
-
codegenFs.root,
|
|
1561
|
-
options.modelProject,
|
|
1562
|
-
projects
|
|
1563
|
-
);
|
|
1564
|
-
const childProject = findProjectByName(
|
|
1565
|
-
codegenFs.root,
|
|
1566
|
-
options.companionModelProject,
|
|
1567
|
-
projects
|
|
1568
|
-
);
|
|
1569
|
-
const projectRoot = childProject?.sourceRoot;
|
|
1570
|
-
if (!projectRoot) {
|
|
1571
|
-
logger.warn(`Companion child project source root not found`);
|
|
1572
|
-
return;
|
|
1573
|
-
}
|
|
1574
|
-
let importPath = "";
|
|
1575
|
-
if (parentProject) {
|
|
1576
|
-
const pkgJsonPath = path.join(parentProject.root, "package.json");
|
|
1577
|
-
try {
|
|
1578
|
-
const pkgJson = readJson(codegenFs, pkgJsonPath);
|
|
1579
|
-
importPath = pkgJson.name || "";
|
|
1580
|
-
} catch {
|
|
1581
|
-
importPath = "";
|
|
1582
|
-
}
|
|
1583
1759
|
}
|
|
1584
|
-
const modelLocation = companionChildKosConfig?.generator?.defaults?.model?.folder || "";
|
|
1585
|
-
const filePath = path.join(
|
|
1586
|
-
projectRoot,
|
|
1587
|
-
modelLocation,
|
|
1588
|
-
normalized.companionModelNameDashCase
|
|
1589
|
-
);
|
|
1590
|
-
logger.info(`Generating companion model in ${filePath}`);
|
|
1591
|
-
generateFilesFromTemplates(codegenFs, templateDir, filePath, {
|
|
1592
|
-
...options,
|
|
1593
|
-
...normalized,
|
|
1594
|
-
importPath
|
|
1595
|
-
});
|
|
1596
1760
|
}
|
|
1597
1761
|
function generateModel(params) {
|
|
1598
1762
|
const {
|
|
@@ -1691,6 +1855,55 @@ function generateModel(params) {
|
|
|
1691
1855
|
);
|
|
1692
1856
|
}
|
|
1693
1857
|
}
|
|
1858
|
+
const DECLARATION_MARKER = "@kosModel";
|
|
1859
|
+
function findModelDeclarationFile(codegenFs, searchRoot, typeIds) {
|
|
1860
|
+
const wanted = new Set(typeIds.filter(Boolean));
|
|
1861
|
+
if (wanted.size === 0) return null;
|
|
1862
|
+
const matches = [];
|
|
1863
|
+
for (const filePath of codegenFs.listFiles(searchRoot)) {
|
|
1864
|
+
if (!filePath.endsWith(".ts") || filePath.endsWith(".d.ts")) continue;
|
|
1865
|
+
const content = codegenFs.read(filePath);
|
|
1866
|
+
if (!content || !content.includes(DECLARATION_MARKER)) continue;
|
|
1867
|
+
const declared = readDeclaredModelType(codegenFs, filePath);
|
|
1868
|
+
if (declared && wanted.has(declared)) matches.push(filePath);
|
|
1869
|
+
}
|
|
1870
|
+
if (matches.length === 0) return null;
|
|
1871
|
+
if (matches.length > 1) {
|
|
1872
|
+
throw new Error(
|
|
1873
|
+
`Model type '${[...wanted].join("' / '")}' is declared in more than one file:
|
|
1874
|
+
` + matches.sort().map((c) => ` - ${c}`).join("\n") + `
|
|
1875
|
+
Pass modelPath to pick one.`
|
|
1876
|
+
);
|
|
1877
|
+
}
|
|
1878
|
+
return matches[0];
|
|
1879
|
+
}
|
|
1880
|
+
function readDeclaredModelType(codegenFs, filePath) {
|
|
1881
|
+
try {
|
|
1882
|
+
return readSourceFile(codegenFs, filePath, (sf) => {
|
|
1883
|
+
const cls = sf.getClasses().find((c) => c.getDecorator("kosModel"));
|
|
1884
|
+
const arg = cls?.getDecorator("kosModel")?.getArguments()[0];
|
|
1885
|
+
if (!arg) return void 0;
|
|
1886
|
+
const inner = arg.getKindName() === "ObjectLiteralExpression" ? modelTypeIdProperty(arg.getText()) : arg.getText();
|
|
1887
|
+
if (!inner) return void 0;
|
|
1888
|
+
return resolveToStringLiteral(inner.trim(), sf.getFullText());
|
|
1889
|
+
});
|
|
1890
|
+
} catch {
|
|
1891
|
+
return void 0;
|
|
1892
|
+
}
|
|
1893
|
+
}
|
|
1894
|
+
function modelTypeIdProperty(objectText) {
|
|
1895
|
+
const m = objectText.match(/\bmodelTypeId\s*:\s*([^,}]+)/);
|
|
1896
|
+
return m ? m[1] : void 0;
|
|
1897
|
+
}
|
|
1898
|
+
function resolveToStringLiteral(expression, fileText) {
|
|
1899
|
+
const literal = expression.match(/^["'`](.*)["'`]$/);
|
|
1900
|
+
if (literal) return literal[1];
|
|
1901
|
+
if (!/^[A-Za-z_$][\w$]*$/.test(expression)) return void 0;
|
|
1902
|
+
const declared = fileText.match(
|
|
1903
|
+
new RegExp(`\\b(?:const|let|var)\\s+${expression}\\s*(?::[^=]+)?=\\s*["'\`]([^"'\`]+)["'\`]`)
|
|
1904
|
+
);
|
|
1905
|
+
return declared ? declared[1] : void 0;
|
|
1906
|
+
}
|
|
1694
1907
|
function resolveModelFilePath(codegenFs, query, projects) {
|
|
1695
1908
|
const kosConfig = getKosProjectConfiguration(
|
|
1696
1909
|
codegenFs,
|
|
@@ -1725,14 +1938,23 @@ function resolveModelFilePath(codegenFs, query, projects) {
|
|
|
1725
1938
|
if (codegenFs.exists(modelFilePath)) {
|
|
1726
1939
|
return { modelFilePath, internal, sourceRoot };
|
|
1727
1940
|
}
|
|
1941
|
+
const searchRoot = path.join(sourceRoot, modelLocation);
|
|
1728
1942
|
const discovered = findModelFileByName(
|
|
1729
1943
|
codegenFs,
|
|
1730
|
-
|
|
1944
|
+
searchRoot,
|
|
1731
1945
|
modelNameDashCase
|
|
1732
1946
|
);
|
|
1733
1947
|
if (discovered) {
|
|
1734
1948
|
return { modelFilePath: discovered, internal, sourceRoot };
|
|
1735
1949
|
}
|
|
1950
|
+
const declaredType = kosConfig?.models?.[query.modelName]?.type;
|
|
1951
|
+
const byDeclaration = findModelDeclarationFile(codegenFs, searchRoot, [
|
|
1952
|
+
query.modelName,
|
|
1953
|
+
...declaredType ? [declaredType] : []
|
|
1954
|
+
]);
|
|
1955
|
+
if (byDeclaration) {
|
|
1956
|
+
return { modelFilePath: byDeclaration, internal, sourceRoot };
|
|
1957
|
+
}
|
|
1736
1958
|
return { modelFilePath, internal, sourceRoot };
|
|
1737
1959
|
}
|
|
1738
1960
|
function findModelFileByName(codegenFs, searchRoot, modelNameDashCase) {
|
|
@@ -1748,6 +1970,150 @@ Pass modelPath to pick one.`
|
|
|
1748
1970
|
}
|
|
1749
1971
|
return candidates[0];
|
|
1750
1972
|
}
|
|
1973
|
+
function resolveChildModelType(codegenFs, query, projects) {
|
|
1974
|
+
const childProject = query.childModelProject || query.modelProject;
|
|
1975
|
+
const childType = `${properCase(query.childModel)}Model`;
|
|
1976
|
+
if (childProject !== query.modelProject) {
|
|
1977
|
+
const project = findProjectByName(codegenFs.root, childProject, projects);
|
|
1978
|
+
const pkgJson = project ? readJson(
|
|
1979
|
+
codegenFs,
|
|
1980
|
+
path.join(project.root, "package.json")
|
|
1981
|
+
) : void 0;
|
|
1982
|
+
return { childType, childTypeModule: pkgJson?.name };
|
|
1983
|
+
}
|
|
1984
|
+
const { modelFilePath: childFilePath } = resolveModelFilePath(
|
|
1985
|
+
codegenFs,
|
|
1986
|
+
{ modelName: query.childModel, modelProject: childProject },
|
|
1987
|
+
projects
|
|
1988
|
+
);
|
|
1989
|
+
const relative = path.relative(path.dirname(query.modelFilePath), childFilePath).replace(/\.ts$/, "");
|
|
1990
|
+
return {
|
|
1991
|
+
childType,
|
|
1992
|
+
childTypeModule: relative.startsWith(".") ? relative : `./${relative}`
|
|
1993
|
+
};
|
|
1994
|
+
}
|
|
1995
|
+
function generateViewModel(codegenFs, templateDir, options, cwd, projects) {
|
|
1996
|
+
const logger = getCodegenLogger();
|
|
1997
|
+
if (!options.name) {
|
|
1998
|
+
throw new Error(
|
|
1999
|
+
"No ViewModel name found. Please specify a name with --name."
|
|
2000
|
+
);
|
|
2001
|
+
}
|
|
2002
|
+
const currentProject = getProject(codegenFs, cwd);
|
|
2003
|
+
const modelProjectName = options.modelProject || currentProject?.name;
|
|
2004
|
+
if (!modelProjectName) {
|
|
2005
|
+
throw new Error(
|
|
2006
|
+
"No model project found. Please specify a model project with --project."
|
|
2007
|
+
);
|
|
2008
|
+
}
|
|
2009
|
+
const normalized = normalizeOptions(
|
|
2010
|
+
codegenFs,
|
|
2011
|
+
{ ...options, modelProject: modelProjectName },
|
|
2012
|
+
projects
|
|
2013
|
+
);
|
|
2014
|
+
const projectConfig = findProjectByName(
|
|
2015
|
+
codegenFs.root,
|
|
2016
|
+
modelProjectName,
|
|
2017
|
+
projects
|
|
2018
|
+
);
|
|
2019
|
+
if (!projectConfig) {
|
|
2020
|
+
throw new Error(`Model project '${modelProjectName}' not found`);
|
|
2021
|
+
}
|
|
2022
|
+
const kosConfig = getKosProjectConfiguration(
|
|
2023
|
+
codegenFs,
|
|
2024
|
+
projectConfig.name,
|
|
2025
|
+
projects
|
|
2026
|
+
);
|
|
2027
|
+
const modelDirectory = options.modelDirectory || kosConfig?.generator?.defaults?.model?.folder || "";
|
|
2028
|
+
const internal = !!kosConfig?.generator?.internal;
|
|
2029
|
+
const projectRoot = projectConfig.sourceRoot || path.join(projectConfig.root, "src");
|
|
2030
|
+
const viewModelFolder = path.join(
|
|
2031
|
+
projectRoot,
|
|
2032
|
+
modelDirectory,
|
|
2033
|
+
normalized.nameDashCase
|
|
2034
|
+
);
|
|
2035
|
+
const viewModelFilePath = path.join(
|
|
2036
|
+
viewModelFolder,
|
|
2037
|
+
`${normalized.nameDashCase}-view-model.ts`
|
|
2038
|
+
);
|
|
2039
|
+
if (codegenFs.exists(viewModelFilePath)) {
|
|
2040
|
+
logger.info(`ViewModel already exists: ${viewModelFilePath}`);
|
|
2041
|
+
return { viewModelFilePath, created: false };
|
|
2042
|
+
}
|
|
2043
|
+
const resolved = (options.models || []).map(
|
|
2044
|
+
(source) => resolveSourceModel(
|
|
2045
|
+
codegenFs,
|
|
2046
|
+
source,
|
|
2047
|
+
viewModelFilePath,
|
|
2048
|
+
modelProjectName,
|
|
2049
|
+
projects
|
|
2050
|
+
)
|
|
2051
|
+
);
|
|
2052
|
+
logger.info(
|
|
2053
|
+
`Generating ViewModel ${normalized.nameDashCase} in ${projectRoot}`
|
|
2054
|
+
);
|
|
2055
|
+
const constructorParams = resolved.map(({ name, type }) => ({ name, type }));
|
|
2056
|
+
generateFilesFromTemplates(codegenFs, templateDir, viewModelFolder, {
|
|
2057
|
+
...normalized,
|
|
2058
|
+
internal,
|
|
2059
|
+
typeId: options.typeId || normalized.nameDashCase,
|
|
2060
|
+
devToolsEnabled: !!options.devToolsEnabled,
|
|
2061
|
+
constructorParams,
|
|
2062
|
+
constructorArgs: constructorParams.map((param) => param.name).join(", "),
|
|
2063
|
+
modelImports: groupImports(resolved)
|
|
2064
|
+
});
|
|
2065
|
+
updateModelIndex(
|
|
2066
|
+
codegenFs,
|
|
2067
|
+
path.join(projectRoot, "index.ts"),
|
|
2068
|
+
modelDirectory ? `${modelDirectory}/${normalized.nameDashCase}` : normalized.nameDashCase
|
|
2069
|
+
);
|
|
2070
|
+
return { viewModelFilePath, created: true };
|
|
2071
|
+
}
|
|
2072
|
+
function resolveSourceModel(codegenFs, source, viewModelFilePath, modelProject, projects) {
|
|
2073
|
+
const sourceProject = source.project || modelProject;
|
|
2074
|
+
if (sourceProject === modelProject) {
|
|
2075
|
+
const { modelFilePath } = resolveModelFilePath(
|
|
2076
|
+
codegenFs,
|
|
2077
|
+
{ modelName: source.model, modelProject: sourceProject },
|
|
2078
|
+
projects
|
|
2079
|
+
);
|
|
2080
|
+
if (!codegenFs.exists(modelFilePath)) {
|
|
2081
|
+
throw new Error(
|
|
2082
|
+
`Model '${source.model}' not found in project '${sourceProject}' (looked for ${modelFilePath})`
|
|
2083
|
+
);
|
|
2084
|
+
}
|
|
2085
|
+
} else if (!findProjectByName(codegenFs.root, sourceProject, projects)) {
|
|
2086
|
+
throw new Error(`Model project '${sourceProject}' not found`);
|
|
2087
|
+
}
|
|
2088
|
+
const { childType, childTypeModule } = resolveChildModelType(
|
|
2089
|
+
codegenFs,
|
|
2090
|
+
{
|
|
2091
|
+
modelFilePath: viewModelFilePath,
|
|
2092
|
+
modelProject,
|
|
2093
|
+
childModel: source.model,
|
|
2094
|
+
childModelProject: sourceProject
|
|
2095
|
+
},
|
|
2096
|
+
projects
|
|
2097
|
+
);
|
|
2098
|
+
return {
|
|
2099
|
+
name: camelCase(source.model),
|
|
2100
|
+
type: childType,
|
|
2101
|
+
module: childTypeModule
|
|
2102
|
+
};
|
|
2103
|
+
}
|
|
2104
|
+
function groupImports(resolved) {
|
|
2105
|
+
const byModule = /* @__PURE__ */ new Map();
|
|
2106
|
+
for (const { type, module } of resolved) {
|
|
2107
|
+
if (!module) continue;
|
|
2108
|
+
const types = byModule.get(module);
|
|
2109
|
+
if (!types) {
|
|
2110
|
+
byModule.set(module, [type]);
|
|
2111
|
+
} else if (!types.includes(type)) {
|
|
2112
|
+
types.push(type);
|
|
2113
|
+
}
|
|
2114
|
+
}
|
|
2115
|
+
return [...byModule].map(([module, types]) => ({ module, types }));
|
|
2116
|
+
}
|
|
1751
2117
|
function modelBaseName(modelFilePath, modelName) {
|
|
1752
2118
|
const base = path.basename(modelFilePath);
|
|
1753
2119
|
const match = base.match(/^(.*)-model\.ts$/);
|
|
@@ -2256,11 +2622,25 @@ function buildDecoratorArgs(options) {
|
|
|
2256
2622
|
}
|
|
2257
2623
|
function addContainerSupportToModel(codegenFs, options, projects) {
|
|
2258
2624
|
const logger = getCodegenLogger();
|
|
2259
|
-
const childType = options.childType?.trim() || "IKosDataModel";
|
|
2260
2625
|
const { modelFilePath } = resolveModelFilePath(codegenFs, options, projects);
|
|
2261
2626
|
if (!codegenFs.exists(modelFilePath)) {
|
|
2262
2627
|
throw new Error(`Model file not found: ${modelFilePath}`);
|
|
2263
2628
|
}
|
|
2629
|
+
const resolvedChild = options.childModel ? resolveChildModelType(
|
|
2630
|
+
codegenFs,
|
|
2631
|
+
{
|
|
2632
|
+
modelFilePath,
|
|
2633
|
+
modelProject: options.modelProject,
|
|
2634
|
+
childModel: options.childModel,
|
|
2635
|
+
childModelProject: options.childModelProject
|
|
2636
|
+
},
|
|
2637
|
+
projects
|
|
2638
|
+
) : {
|
|
2639
|
+
childType: options.childType,
|
|
2640
|
+
childTypeModule: options.childTypeModule
|
|
2641
|
+
};
|
|
2642
|
+
const childType = resolvedChild.childType?.trim() || "IKosDataModel";
|
|
2643
|
+
const childTypeModule = resolvedChild.childTypeModule;
|
|
2264
2644
|
logger.info(
|
|
2265
2645
|
`Adding container support (<${childType}>) to model: ${options.modelName}`
|
|
2266
2646
|
);
|
|
@@ -2272,6 +2652,10 @@ function addContainerSupportToModel(codegenFs, options, projects) {
|
|
|
2272
2652
|
]);
|
|
2273
2653
|
if (childType === "IKosDataModel") {
|
|
2274
2654
|
ensureNamedImport(sf, sdk, [{ name: "IKosDataModel", isTypeOnly: true }]);
|
|
2655
|
+
} else if (childTypeModule) {
|
|
2656
|
+
ensureNamedImport(sf, childTypeModule, [
|
|
2657
|
+
{ name: childType, isTypeOnly: true }
|
|
2658
|
+
]);
|
|
2275
2659
|
}
|
|
2276
2660
|
const cls = getModelClass(sf);
|
|
2277
2661
|
const className = cls.getName();
|
|
@@ -2301,6 +2685,140 @@ function addContainerSupportToModel(codegenFs, options, projects) {
|
|
|
2301
2685
|
});
|
|
2302
2686
|
return { modelFilePath };
|
|
2303
2687
|
}
|
|
2688
|
+
function addParentAwareToModel(codegenFs, options, projects) {
|
|
2689
|
+
const logger = getCodegenLogger();
|
|
2690
|
+
const { modelFilePath } = resolveModelFilePath(codegenFs, options, projects);
|
|
2691
|
+
if (!codegenFs.exists(modelFilePath)) {
|
|
2692
|
+
throw new Error(`Model file not found: ${modelFilePath}`);
|
|
2693
|
+
}
|
|
2694
|
+
logger.info(`Adding parent awareness to model: ${options.modelName}`);
|
|
2695
|
+
let optionsTypeName;
|
|
2696
|
+
transformSourceFile(codegenFs, modelFilePath, (sf) => {
|
|
2697
|
+
const sdk = resolveSdkModuleSpecifier(sf);
|
|
2698
|
+
ensureNamedImport(sf, sdk, [{ name: "kosParentAware" }]);
|
|
2699
|
+
const cls = getModelClass(sf);
|
|
2700
|
+
addClassDecorator(cls, "kosParentAware", {
|
|
2701
|
+
argsText: options.parentId ? `{ parentId: ${JSON.stringify(options.parentId)} }` : ""
|
|
2702
|
+
});
|
|
2703
|
+
const ctor = cls.getConstructors()[0];
|
|
2704
|
+
optionsTypeName = ctor?.getParameters()[1]?.getTypeNode()?.getText()?.replace(/<.*$/, "");
|
|
2705
|
+
});
|
|
2706
|
+
const optionsFilePath = optionsTypeName ? extendOptionsInterface(codegenFs, modelFilePath, optionsTypeName, logger) : void 0;
|
|
2707
|
+
return { modelFilePath, optionsFilePath };
|
|
2708
|
+
}
|
|
2709
|
+
function extendOptionsInterface(codegenFs, modelFilePath, optionsTypeName, logger) {
|
|
2710
|
+
const typesPath = path.join(
|
|
2711
|
+
path.dirname(modelFilePath),
|
|
2712
|
+
"types",
|
|
2713
|
+
"index.d.ts"
|
|
2714
|
+
);
|
|
2715
|
+
const target = codegenFs.exists(typesPath) ? typesPath : modelFilePath;
|
|
2716
|
+
let extended = false;
|
|
2717
|
+
transformSourceFile(codegenFs, target, (sf) => {
|
|
2718
|
+
const iface = sf.getInterface(optionsTypeName);
|
|
2719
|
+
if (!iface) return;
|
|
2720
|
+
const already = iface.getExtends().some((clause) => clause.getText().includes("KosParentAware"));
|
|
2721
|
+
if (already) {
|
|
2722
|
+
extended = true;
|
|
2723
|
+
return;
|
|
2724
|
+
}
|
|
2725
|
+
ensureNamedImport(sf, "@kosdev-code/kos-ui-sdk", [
|
|
2726
|
+
{ name: "KosParentAware", isTypeOnly: true }
|
|
2727
|
+
]);
|
|
2728
|
+
iface.addExtends("KosParentAware");
|
|
2729
|
+
extended = true;
|
|
2730
|
+
});
|
|
2731
|
+
if (!extended) {
|
|
2732
|
+
logger.warn(
|
|
2733
|
+
`Could not find interface ${optionsTypeName}; add "extends KosParentAware" to it by hand.`
|
|
2734
|
+
);
|
|
2735
|
+
return void 0;
|
|
2736
|
+
}
|
|
2737
|
+
return target;
|
|
2738
|
+
}
|
|
2739
|
+
function firstDecoratorArg(decoratorText) {
|
|
2740
|
+
const open = decoratorText.indexOf("(");
|
|
2741
|
+
if (open === -1) return void 0;
|
|
2742
|
+
const inner = decoratorText.slice(open + 1, decoratorText.lastIndexOf(")")).replace(/\s+/g, " ").trim();
|
|
2743
|
+
return inner || void 0;
|
|
2744
|
+
}
|
|
2745
|
+
function collectDecorated(cls, decoratorName) {
|
|
2746
|
+
const members = [];
|
|
2747
|
+
const visit = (name, decoratorTextOf, typeText) => {
|
|
2748
|
+
const text = decoratorTextOf();
|
|
2749
|
+
if (text === void 0) return;
|
|
2750
|
+
members.push({
|
|
2751
|
+
name,
|
|
2752
|
+
arg: firstDecoratorArg(text),
|
|
2753
|
+
type: typeText || void 0
|
|
2754
|
+
});
|
|
2755
|
+
};
|
|
2756
|
+
for (const m of cls.getMethods()) {
|
|
2757
|
+
const dec = m.getDecorator(decoratorName);
|
|
2758
|
+
if (dec) {
|
|
2759
|
+
visit(m.getName(), () => dec.getText(), m.getReturnTypeNode()?.getText());
|
|
2760
|
+
}
|
|
2761
|
+
}
|
|
2762
|
+
for (const p of cls.getProperties()) {
|
|
2763
|
+
const dec = p.getDecorator(decoratorName);
|
|
2764
|
+
if (dec) {
|
|
2765
|
+
visit(p.getName(), () => dec.getText(), p.getTypeNode()?.getText());
|
|
2766
|
+
}
|
|
2767
|
+
}
|
|
2768
|
+
return members;
|
|
2769
|
+
}
|
|
2770
|
+
function describeModel(codegenFs, options, projects) {
|
|
2771
|
+
const { modelFilePath } = resolveModelFilePath(codegenFs, options, projects);
|
|
2772
|
+
const content = codegenFs.read(modelFilePath);
|
|
2773
|
+
if (content === null) {
|
|
2774
|
+
throw new Error(`Model file not found: ${modelFilePath}`);
|
|
2775
|
+
}
|
|
2776
|
+
const project = new Project({ useInMemoryFileSystem: true });
|
|
2777
|
+
const sf = project.createSourceFile(modelFilePath, content, {
|
|
2778
|
+
overwrite: true
|
|
2779
|
+
});
|
|
2780
|
+
let modelType;
|
|
2781
|
+
const modelTypeDecl = sf.getVariableDeclaration("MODEL_TYPE");
|
|
2782
|
+
if (modelTypeDecl) {
|
|
2783
|
+
const init = modelTypeDecl.getInitializer()?.getText();
|
|
2784
|
+
if (init) modelType = init.replace(/^["'`]|["'`]$/g, "");
|
|
2785
|
+
}
|
|
2786
|
+
const cls = sf.getClasses().find((c) => c.getDecorator("kosModel")) ?? sf.getClasses().find((c) => c.isExported()) ?? sf.getClasses()[0];
|
|
2787
|
+
if (!cls) {
|
|
2788
|
+
return {
|
|
2789
|
+
modelFilePath,
|
|
2790
|
+
modelType,
|
|
2791
|
+
classDecorators: [],
|
|
2792
|
+
singleton: false,
|
|
2793
|
+
isCompanion: false,
|
|
2794
|
+
children: [],
|
|
2795
|
+
dependencies: [],
|
|
2796
|
+
topicHandlers: [],
|
|
2797
|
+
configProperties: [],
|
|
2798
|
+
serviceRequests: [],
|
|
2799
|
+
effects: [],
|
|
2800
|
+
futures: []
|
|
2801
|
+
};
|
|
2802
|
+
}
|
|
2803
|
+
const classDecorators = cls.getDecorators().map((d) => d.getName());
|
|
2804
|
+
const kosModelArg = cls.getDecorator("kosModel") ? firstDecoratorArg(cls.getDecorator("kosModel").getText()) : void 0;
|
|
2805
|
+
const singleton = /\bsingleton\s*:\s*true\b/.test(kosModelArg ?? "");
|
|
2806
|
+
return {
|
|
2807
|
+
modelFilePath,
|
|
2808
|
+
modelType,
|
|
2809
|
+
className: cls.getName(),
|
|
2810
|
+
classDecorators,
|
|
2811
|
+
singleton,
|
|
2812
|
+
isCompanion: classDecorators.includes("kosCompanion"),
|
|
2813
|
+
children: collectDecorated(cls, "kosChild"),
|
|
2814
|
+
dependencies: collectDecorated(cls, "kosDependency"),
|
|
2815
|
+
topicHandlers: collectDecorated(cls, "kosTopicHandler"),
|
|
2816
|
+
configProperties: collectDecorated(cls, "kosConfigProperty"),
|
|
2817
|
+
serviceRequests: collectDecorated(cls, "kosServiceRequest"),
|
|
2818
|
+
effects: collectDecorated(cls, "kosModelEffect"),
|
|
2819
|
+
futures: collectDecorated(cls, "kosFuture")
|
|
2820
|
+
};
|
|
2821
|
+
}
|
|
2304
2822
|
function addModelEffectToModel(codegenFs, options, projects) {
|
|
2305
2823
|
const logger = getCodegenLogger();
|
|
2306
2824
|
const { modelFilePath } = resolveModelFilePath(codegenFs, options, projects);
|
|
@@ -2376,7 +2894,18 @@ function addChildToModel(codegenFs, options, projects) {
|
|
|
2376
2894
|
logger.info(
|
|
2377
2895
|
`Adding @kosChild "${options.propertyName}" (${shape}) to ${options.modelName}`
|
|
2378
2896
|
);
|
|
2379
|
-
const
|
|
2897
|
+
const resolvedChild = options.childModel ? resolveChildModelType(
|
|
2898
|
+
codegenFs,
|
|
2899
|
+
{
|
|
2900
|
+
modelFilePath,
|
|
2901
|
+
modelProject: options.modelProject,
|
|
2902
|
+
childModel: options.childModel,
|
|
2903
|
+
childModelProject: options.childModelProject
|
|
2904
|
+
},
|
|
2905
|
+
projects
|
|
2906
|
+
) : { childType: options.childType, childTypeModule: options.childPackage };
|
|
2907
|
+
const childType = resolvedChild.childType?.trim();
|
|
2908
|
+
const childTypeModule = resolvedChild.childTypeModule;
|
|
2380
2909
|
transformSourceFile(codegenFs, modelFilePath, (sf) => {
|
|
2381
2910
|
const sdk = resolveSdkModuleSpecifier(sf);
|
|
2382
2911
|
const sdkImports = [
|
|
@@ -2384,8 +2913,8 @@ function addChildToModel(codegenFs, options, projects) {
|
|
|
2384
2913
|
];
|
|
2385
2914
|
if (shape === "container") sdkImports.push({ name: "KosModelContainer" });
|
|
2386
2915
|
ensureNamedImport(sf, sdk, sdkImports);
|
|
2387
|
-
if (
|
|
2388
|
-
ensureNamedImport(sf,
|
|
2916
|
+
if (childTypeModule && childType && /^[A-Za-z_$][\w$]*$/.test(childType) && !FRAMEWORK_TYPES.has(childType)) {
|
|
2917
|
+
ensureNamedImport(sf, childTypeModule, [
|
|
2389
2918
|
{ name: childType, isTypeOnly: true }
|
|
2390
2919
|
]);
|
|
2391
2920
|
}
|
|
@@ -3094,165 +3623,6 @@ function addServiceRequestToModel(codegenFs, options, projects) {
|
|
|
3094
3623
|
}) : void 0
|
|
3095
3624
|
};
|
|
3096
3625
|
}
|
|
3097
|
-
function modelElementType(typeText) {
|
|
3098
|
-
let m;
|
|
3099
|
-
if (m = typeText.match(/^([A-Za-z_]\w*Model)\s*\[\]$/)) return m[1];
|
|
3100
|
-
if (m = typeText.match(/^Array<\s*([A-Za-z_]\w*Model)\s*>$/)) return m[1];
|
|
3101
|
-
if (m = typeText.match(/^(?:Map|Set)<[^>]*?\b([A-Za-z_]\w*Model)\b[^>]*>$/))
|
|
3102
|
-
return m[1];
|
|
3103
|
-
return null;
|
|
3104
|
-
}
|
|
3105
|
-
function validateModel(codegenFs, options, projects) {
|
|
3106
|
-
const { modelFilePath } = resolveModelFilePath(codegenFs, options, projects);
|
|
3107
|
-
const content = codegenFs.read(modelFilePath);
|
|
3108
|
-
if (content === null) {
|
|
3109
|
-
throw new Error(`Model file not found: ${modelFilePath}`);
|
|
3110
|
-
}
|
|
3111
|
-
const project = new Project({ useInMemoryFileSystem: true });
|
|
3112
|
-
const sf = project.createSourceFile(modelFilePath, content, {
|
|
3113
|
-
overwrite: true
|
|
3114
|
-
});
|
|
3115
|
-
const findings = [];
|
|
3116
|
-
const hasKosModel = sf.getClasses().some((c) => c.getDecorator("kosModel"));
|
|
3117
|
-
if (!hasKosModel) {
|
|
3118
|
-
findings.push({
|
|
3119
|
-
level: "error",
|
|
3120
|
-
rule: "missing-kosModel",
|
|
3121
|
-
message: "No @kosModel decorator found — this is not a KOS model."
|
|
3122
|
-
});
|
|
3123
|
-
}
|
|
3124
|
-
const imports = sf.getImportDeclarations();
|
|
3125
|
-
const mobxImport = imports.find(
|
|
3126
|
-
(d) => /^mobx(-react-lite)?$/.test(d.getModuleSpecifierValue())
|
|
3127
|
-
);
|
|
3128
|
-
if (mobxImport) {
|
|
3129
|
-
findings.push({
|
|
3130
|
-
level: "error",
|
|
3131
|
-
rule: "mobx-import",
|
|
3132
|
-
message: "Imports mobx directly. Reactivity is internal to KOS — remove the mobx import and use KOS reactivity / container models."
|
|
3133
|
-
});
|
|
3134
|
-
}
|
|
3135
|
-
const barrelServiceRequest = imports.find(
|
|
3136
|
-
(d) => d.getModuleSpecifierValue() === "@kosdev-code/kos-ui-sdk" && d.getNamedImports().some((n) => n.getName() === "kosServiceRequest")
|
|
3137
|
-
);
|
|
3138
|
-
if (barrelServiceRequest) {
|
|
3139
|
-
findings.push({
|
|
3140
|
-
level: "warning",
|
|
3141
|
-
rule: "untyped-service-request",
|
|
3142
|
-
message: "kosServiceRequest imported from the SDK barrel. Use the typed decorator from the api:generate'd service module so paths are validated against the OpenAPI types."
|
|
3143
|
-
});
|
|
3144
|
-
}
|
|
3145
|
-
for (const cls of sf.getClasses()) {
|
|
3146
|
-
for (const prop of cls.getProperties()) {
|
|
3147
|
-
const name = prop.getName();
|
|
3148
|
-
const typeText = (prop.getTypeNode()?.getText() ?? "").replace(/\s+/g, " ").trim();
|
|
3149
|
-
const initText = prop.getInitializer()?.getText() ?? "";
|
|
3150
|
-
const hasChild = !!prop.getDecorator("kosChild");
|
|
3151
|
-
const isModelContainer = /\bI?KosModelContainer\s*</.test(typeText) || /new\s+KosModelContainer\b/.test(initText);
|
|
3152
|
-
if (isModelContainer && !hasChild) {
|
|
3153
|
-
findings.push({
|
|
3154
|
-
level: "warning",
|
|
3155
|
-
rule: "container-missing-kosChild",
|
|
3156
|
-
message: `Property '${name}' is a model container but is not marked @kosChild — add @kosChild so its models join the model graph and lifecycle.`
|
|
3157
|
-
});
|
|
3158
|
-
continue;
|
|
3159
|
-
}
|
|
3160
|
-
const elem = modelElementType(typeText);
|
|
3161
|
-
if (elem && !isModelContainer) {
|
|
3162
|
-
findings.push({
|
|
3163
|
-
level: "warning",
|
|
3164
|
-
rule: "raw-model-collection",
|
|
3165
|
-
message: `Property '${name}' is a raw ${typeText} of models — prefer a KosModelContainer<${elem}> (indexing, sorting, lifecycle, delta handling) marked @kosChild.`
|
|
3166
|
-
});
|
|
3167
|
-
}
|
|
3168
|
-
}
|
|
3169
|
-
}
|
|
3170
|
-
const hasError = findings.some((f) => f.level === "error");
|
|
3171
|
-
return { modelFilePath, ok: !hasError, findings };
|
|
3172
|
-
}
|
|
3173
|
-
function firstDecoratorArg(decoratorText) {
|
|
3174
|
-
const open = decoratorText.indexOf("(");
|
|
3175
|
-
if (open === -1) return void 0;
|
|
3176
|
-
const inner = decoratorText.slice(open + 1, decoratorText.lastIndexOf(")")).replace(/\s+/g, " ").trim();
|
|
3177
|
-
return inner || void 0;
|
|
3178
|
-
}
|
|
3179
|
-
function collectDecorated(cls, decoratorName) {
|
|
3180
|
-
const members = [];
|
|
3181
|
-
const visit = (name, decoratorTextOf, typeText) => {
|
|
3182
|
-
const text = decoratorTextOf();
|
|
3183
|
-
if (text === void 0) return;
|
|
3184
|
-
members.push({
|
|
3185
|
-
name,
|
|
3186
|
-
arg: firstDecoratorArg(text),
|
|
3187
|
-
type: typeText || void 0
|
|
3188
|
-
});
|
|
3189
|
-
};
|
|
3190
|
-
for (const m of cls.getMethods()) {
|
|
3191
|
-
const dec = m.getDecorator(decoratorName);
|
|
3192
|
-
if (dec) {
|
|
3193
|
-
visit(m.getName(), () => dec.getText(), m.getReturnTypeNode()?.getText());
|
|
3194
|
-
}
|
|
3195
|
-
}
|
|
3196
|
-
for (const p of cls.getProperties()) {
|
|
3197
|
-
const dec = p.getDecorator(decoratorName);
|
|
3198
|
-
if (dec) {
|
|
3199
|
-
visit(p.getName(), () => dec.getText(), p.getTypeNode()?.getText());
|
|
3200
|
-
}
|
|
3201
|
-
}
|
|
3202
|
-
return members;
|
|
3203
|
-
}
|
|
3204
|
-
function describeModel(codegenFs, options, projects) {
|
|
3205
|
-
const { modelFilePath } = resolveModelFilePath(codegenFs, options, projects);
|
|
3206
|
-
const content = codegenFs.read(modelFilePath);
|
|
3207
|
-
if (content === null) {
|
|
3208
|
-
throw new Error(`Model file not found: ${modelFilePath}`);
|
|
3209
|
-
}
|
|
3210
|
-
const project = new Project({ useInMemoryFileSystem: true });
|
|
3211
|
-
const sf = project.createSourceFile(modelFilePath, content, {
|
|
3212
|
-
overwrite: true
|
|
3213
|
-
});
|
|
3214
|
-
let modelType;
|
|
3215
|
-
const modelTypeDecl = sf.getVariableDeclaration("MODEL_TYPE");
|
|
3216
|
-
if (modelTypeDecl) {
|
|
3217
|
-
const init = modelTypeDecl.getInitializer()?.getText();
|
|
3218
|
-
if (init) modelType = init.replace(/^["'`]|["'`]$/g, "");
|
|
3219
|
-
}
|
|
3220
|
-
const cls = sf.getClasses().find((c) => c.getDecorator("kosModel")) ?? sf.getClasses().find((c) => c.isExported()) ?? sf.getClasses()[0];
|
|
3221
|
-
if (!cls) {
|
|
3222
|
-
return {
|
|
3223
|
-
modelFilePath,
|
|
3224
|
-
modelType,
|
|
3225
|
-
classDecorators: [],
|
|
3226
|
-
singleton: false,
|
|
3227
|
-
isCompanion: false,
|
|
3228
|
-
children: [],
|
|
3229
|
-
dependencies: [],
|
|
3230
|
-
topicHandlers: [],
|
|
3231
|
-
configProperties: [],
|
|
3232
|
-
serviceRequests: [],
|
|
3233
|
-
effects: [],
|
|
3234
|
-
futures: []
|
|
3235
|
-
};
|
|
3236
|
-
}
|
|
3237
|
-
const classDecorators = cls.getDecorators().map((d) => d.getName());
|
|
3238
|
-
const kosModelArg = cls.getDecorator("kosModel") ? firstDecoratorArg(cls.getDecorator("kosModel").getText()) : void 0;
|
|
3239
|
-
const singleton = /\bsingleton\s*:\s*true\b/.test(kosModelArg ?? "");
|
|
3240
|
-
return {
|
|
3241
|
-
modelFilePath,
|
|
3242
|
-
modelType,
|
|
3243
|
-
className: cls.getName(),
|
|
3244
|
-
classDecorators,
|
|
3245
|
-
singleton,
|
|
3246
|
-
isCompanion: classDecorators.includes("kosCompanion"),
|
|
3247
|
-
children: collectDecorated(cls, "kosChild"),
|
|
3248
|
-
dependencies: collectDecorated(cls, "kosDependency"),
|
|
3249
|
-
topicHandlers: collectDecorated(cls, "kosTopicHandler"),
|
|
3250
|
-
configProperties: collectDecorated(cls, "kosConfigProperty"),
|
|
3251
|
-
serviceRequests: collectDecorated(cls, "kosServiceRequest"),
|
|
3252
|
-
effects: collectDecorated(cls, "kosModelEffect"),
|
|
3253
|
-
futures: collectDecorated(cls, "kosFuture")
|
|
3254
|
-
};
|
|
3255
|
-
}
|
|
3256
3626
|
const DEFAULT_SDK_PACKAGE = "@kosdev-code/kos-ui-sdk";
|
|
3257
3627
|
const MAX_SIGNATURE_CHARS = 2e3;
|
|
3258
3628
|
function declarationEntryFromPackageJson(pkgDir) {
|
|
@@ -3436,6 +3806,82 @@ function lookupSdkType(codegenFs, options, projects) {
|
|
|
3436
3806
|
)}). Check the name, or it may be internal / not part of the public surface.`
|
|
3437
3807
|
};
|
|
3438
3808
|
}
|
|
3809
|
+
function modelElementType(typeText) {
|
|
3810
|
+
let m;
|
|
3811
|
+
if (m = typeText.match(/^([A-Za-z_]\w*Model)\s*\[\]$/)) return m[1];
|
|
3812
|
+
if (m = typeText.match(/^Array<\s*([A-Za-z_]\w*Model)\s*>$/)) return m[1];
|
|
3813
|
+
if (m = typeText.match(/^(?:Map|Set)<[^>]*?\b([A-Za-z_]\w*Model)\b[^>]*>$/))
|
|
3814
|
+
return m[1];
|
|
3815
|
+
return null;
|
|
3816
|
+
}
|
|
3817
|
+
function validateModel(codegenFs, options, projects) {
|
|
3818
|
+
const { modelFilePath } = resolveModelFilePath(codegenFs, options, projects);
|
|
3819
|
+
const content = codegenFs.read(modelFilePath);
|
|
3820
|
+
if (content === null) {
|
|
3821
|
+
throw new Error(`Model file not found: ${modelFilePath}`);
|
|
3822
|
+
}
|
|
3823
|
+
const project = new Project({ useInMemoryFileSystem: true });
|
|
3824
|
+
const sf = project.createSourceFile(modelFilePath, content, {
|
|
3825
|
+
overwrite: true
|
|
3826
|
+
});
|
|
3827
|
+
const findings = [];
|
|
3828
|
+
const hasKosModel = sf.getClasses().some((c) => c.getDecorator("kosModel"));
|
|
3829
|
+
if (!hasKosModel) {
|
|
3830
|
+
findings.push({
|
|
3831
|
+
level: "error",
|
|
3832
|
+
rule: "missing-kosModel",
|
|
3833
|
+
message: "No @kosModel decorator found — this is not a KOS model."
|
|
3834
|
+
});
|
|
3835
|
+
}
|
|
3836
|
+
const imports = sf.getImportDeclarations();
|
|
3837
|
+
const mobxImport = imports.find(
|
|
3838
|
+
(d) => /^mobx(-react-lite)?$/.test(d.getModuleSpecifierValue())
|
|
3839
|
+
);
|
|
3840
|
+
if (mobxImport) {
|
|
3841
|
+
findings.push({
|
|
3842
|
+
level: "error",
|
|
3843
|
+
rule: "mobx-import",
|
|
3844
|
+
message: "Imports mobx directly. Reactivity is internal to KOS — remove the mobx import and use KOS reactivity / container models."
|
|
3845
|
+
});
|
|
3846
|
+
}
|
|
3847
|
+
const barrelServiceRequest = imports.find(
|
|
3848
|
+
(d) => d.getModuleSpecifierValue() === "@kosdev-code/kos-ui-sdk" && d.getNamedImports().some((n) => n.getName() === "kosServiceRequest")
|
|
3849
|
+
);
|
|
3850
|
+
if (barrelServiceRequest) {
|
|
3851
|
+
findings.push({
|
|
3852
|
+
level: "warning",
|
|
3853
|
+
rule: "untyped-service-request",
|
|
3854
|
+
message: "kosServiceRequest imported from the SDK barrel. Use the typed decorator from the api:generate'd service module so paths are validated against the OpenAPI types."
|
|
3855
|
+
});
|
|
3856
|
+
}
|
|
3857
|
+
for (const cls of sf.getClasses()) {
|
|
3858
|
+
for (const prop of cls.getProperties()) {
|
|
3859
|
+
const name = prop.getName();
|
|
3860
|
+
const typeText = (prop.getTypeNode()?.getText() ?? "").replace(/\s+/g, " ").trim();
|
|
3861
|
+
const initText = prop.getInitializer()?.getText() ?? "";
|
|
3862
|
+
const hasChild = !!prop.getDecorator("kosChild");
|
|
3863
|
+
const isModelContainer = /\bI?KosModelContainer\s*</.test(typeText) || /new\s+KosModelContainer\b/.test(initText);
|
|
3864
|
+
if (isModelContainer && !hasChild) {
|
|
3865
|
+
findings.push({
|
|
3866
|
+
level: "warning",
|
|
3867
|
+
rule: "container-missing-kosChild",
|
|
3868
|
+
message: `Property '${name}' is a model container but is not marked @kosChild — add @kosChild so its models join the model graph and lifecycle.`
|
|
3869
|
+
});
|
|
3870
|
+
continue;
|
|
3871
|
+
}
|
|
3872
|
+
const elem = modelElementType(typeText);
|
|
3873
|
+
if (elem && !isModelContainer) {
|
|
3874
|
+
findings.push({
|
|
3875
|
+
level: "warning",
|
|
3876
|
+
rule: "raw-model-collection",
|
|
3877
|
+
message: `Property '${name}' is a raw ${typeText} of models — prefer a KosModelContainer<${elem}> (indexing, sorting, lifecycle, delta handling) marked @kosChild.`
|
|
3878
|
+
});
|
|
3879
|
+
}
|
|
3880
|
+
}
|
|
3881
|
+
}
|
|
3882
|
+
const hasError = findings.some((f) => f.level === "error");
|
|
3883
|
+
return { modelFilePath, ok: !hasError, findings };
|
|
3884
|
+
}
|
|
3439
3885
|
const PLUGIN_TYPES = {
|
|
3440
3886
|
CUI: "cui",
|
|
3441
3887
|
UTILITY: "utility",
|
|
@@ -3990,9 +4436,76 @@ function updatePluginConfiguration(codegenFs, projectConfig, options) {
|
|
|
3990
4436
|
);
|
|
3991
4437
|
}
|
|
3992
4438
|
}
|
|
4439
|
+
function parse(content) {
|
|
4440
|
+
const singleQuoted = /^\s*import\b[^"']*'/m.test(content);
|
|
4441
|
+
const project = new Project({
|
|
4442
|
+
useInMemoryFileSystem: true,
|
|
4443
|
+
manipulationSettings: {
|
|
4444
|
+
indentationText: IndentationText.TwoSpaces,
|
|
4445
|
+
quoteKind: singleQuoted ? QuoteKind.Single : QuoteKind.Double
|
|
4446
|
+
}
|
|
4447
|
+
});
|
|
4448
|
+
return project.createSourceFile("registration-chain.ts", content, {
|
|
4449
|
+
overwrite: true
|
|
4450
|
+
});
|
|
4451
|
+
}
|
|
4452
|
+
function chainCalls(sf, name) {
|
|
4453
|
+
return sf.getDescendantsOfKind(SyntaxKind.CallExpression).filter((call) => {
|
|
4454
|
+
const callee = call.getExpression();
|
|
4455
|
+
return callee.getKind() === SyntaxKind.PropertyAccessExpression && callee.asKindOrThrow(SyntaxKind.PropertyAccessExpression).getName() === name;
|
|
4456
|
+
});
|
|
4457
|
+
}
|
|
4458
|
+
function indentOfCall(sf, call) {
|
|
4459
|
+
const text = sf.getFullText();
|
|
4460
|
+
const nameNode = call.getExpression().asKindOrThrow(SyntaxKind.PropertyAccessExpression).getNameNode();
|
|
4461
|
+
const lineStart = text.lastIndexOf("\n", nameNode.getStart()) + 1;
|
|
4462
|
+
return text.slice(lineStart).match(/^[ \t]*/)?.[0] ?? "";
|
|
4463
|
+
}
|
|
4464
|
+
function countChainEntries(content) {
|
|
4465
|
+
return chainCalls(parse(content), "model").length;
|
|
4466
|
+
}
|
|
4467
|
+
function upsertChainEntry(content, bean, importSpec) {
|
|
4468
|
+
const sf = parse(content);
|
|
4469
|
+
const modelCalls = chainCalls(sf, "model");
|
|
4470
|
+
const registered = modelCalls.some((call) => {
|
|
4471
|
+
const [arg, ...rest] = call.getArguments();
|
|
4472
|
+
return rest.length === 0 && arg?.getText() === bean;
|
|
4473
|
+
});
|
|
4474
|
+
if (registered) return { changed: false, note: "already registered" };
|
|
4475
|
+
let anchor;
|
|
4476
|
+
let indent;
|
|
4477
|
+
if (modelCalls.length > 0) {
|
|
4478
|
+
anchor = modelCalls.reduce(
|
|
4479
|
+
(last, call) => call.getEnd() > last.getEnd() ? call : last
|
|
4480
|
+
);
|
|
4481
|
+
indent = indentOfCall(sf, anchor);
|
|
4482
|
+
} else {
|
|
4483
|
+
const starts = chainCalls(sf, "models").filter(
|
|
4484
|
+
(call) => call.getArguments().length === 0
|
|
4485
|
+
);
|
|
4486
|
+
anchor = starts[starts.length - 1];
|
|
4487
|
+
if (!anchor) {
|
|
4488
|
+
return {
|
|
4489
|
+
changed: false,
|
|
4490
|
+
error: "no .models() chain found in the registration file"
|
|
4491
|
+
};
|
|
4492
|
+
}
|
|
4493
|
+
const text = sf.getFullText();
|
|
4494
|
+
const lineStart = text.lastIndexOf("\n", anchor.getStart()) + 1;
|
|
4495
|
+
indent = (text.slice(lineStart).match(/^[ \t]*/)?.[0] ?? "") + " ";
|
|
4496
|
+
}
|
|
4497
|
+
anchor.replaceWithText(`${anchor.getText()}
|
|
4498
|
+
${indent}.model(${bean})`);
|
|
4499
|
+
const imported = sf.getImportDeclarations().some(
|
|
4500
|
+
(decl) => decl.getNamedImports().some((named) => named.getName() === bean)
|
|
4501
|
+
);
|
|
4502
|
+
if (!imported) ensureNamedImport(sf, importSpec, [{ name: bean }]);
|
|
4503
|
+
return { changed: true, content: sf.getFullText(), importAdded: !imported };
|
|
4504
|
+
}
|
|
3993
4505
|
export {
|
|
3994
4506
|
BasePluginHandler,
|
|
3995
4507
|
CONTRIBUTION_TYPE_MAP,
|
|
4508
|
+
DEFAULT_MODEL_LIBS_DIR,
|
|
3996
4509
|
DirectFileSystem,
|
|
3997
4510
|
KAB_OUTPUT_DIR,
|
|
3998
4511
|
KAB_OUTPUT_PATH,
|
|
@@ -4014,6 +4527,7 @@ export {
|
|
|
4014
4527
|
addJavaArtifactToManifests,
|
|
4015
4528
|
addKosModelConfiguration,
|
|
4016
4529
|
addModelEffectToModel,
|
|
4530
|
+
addParentAwareToModel,
|
|
4017
4531
|
addPropertyToModel,
|
|
4018
4532
|
addServiceRequestToModel,
|
|
4019
4533
|
addTopicHandlerToModel,
|
|
@@ -4022,6 +4536,7 @@ export {
|
|
|
4022
4536
|
buildSbomTarget,
|
|
4023
4537
|
camelCase,
|
|
4024
4538
|
constantCase,
|
|
4539
|
+
countChainEntries,
|
|
4025
4540
|
dashCase,
|
|
4026
4541
|
describeModel,
|
|
4027
4542
|
discoverJavaArtifacts,
|
|
@@ -4040,8 +4555,10 @@ export {
|
|
|
4040
4555
|
generateHook,
|
|
4041
4556
|
generateInit,
|
|
4042
4557
|
generateModel,
|
|
4558
|
+
generateModelProject,
|
|
4043
4559
|
generatePolyglotWorkspace,
|
|
4044
4560
|
generateSplashProject,
|
|
4561
|
+
generateViewModel,
|
|
4045
4562
|
getCodegenLogger,
|
|
4046
4563
|
getCurrentDirectoryName,
|
|
4047
4564
|
getKosModelConfigProp,
|
|
@@ -4059,10 +4576,12 @@ export {
|
|
|
4059
4576
|
readNxJson,
|
|
4060
4577
|
resolveKabPath,
|
|
4061
4578
|
resolveModelFilePath,
|
|
4579
|
+
resolveModelProjectLayout,
|
|
4062
4580
|
setCodegenLogger,
|
|
4063
4581
|
syncCiManifests,
|
|
4064
4582
|
updateJson,
|
|
4065
4583
|
updateModelIndex,
|
|
4584
|
+
upsertChainEntry,
|
|
4066
4585
|
validateModel,
|
|
4067
4586
|
writeJson
|
|
4068
4587
|
};
|