@kosdev-code/kos-codegen-core 3.0.20 → 3.0.21
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 +1266 -379
- package/index.js.map +1 -1
- package/index.mjs +1267 -380
- 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/model-mocks.d.ts +47 -0
- package/lib/generators/augment/model-mocks.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 +26 -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/member-mutators/add-service-request.d.ts +71 -0
- package/lib/generators/member-mutators/add-service-request.d.ts.map +1 -1
- package/lib/generators/member-mutators/index.d.ts +1 -1
- package/lib/generators/member-mutators/index.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/lib/generators/service-catalog.d.ts +12 -0
- package/lib/generators/service-catalog.d.ts.map +1 -1
- 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.js
CHANGED
|
@@ -496,6 +496,84 @@ function generateSplashProject(codegenFs, templateDir, options) {
|
|
|
496
496
|
normalized
|
|
497
497
|
);
|
|
498
498
|
}
|
|
499
|
+
const MODEL_PROJECT_SUFFIX = "-models";
|
|
500
|
+
const DEFAULT_MODEL_LIBS_DIR = "libs";
|
|
501
|
+
const JSONC_ESLINT_PARSER_VERSION = "^2.1.0";
|
|
502
|
+
function resolveModelProjectLayout(codegenFs, options) {
|
|
503
|
+
const base = dashCase(options.name);
|
|
504
|
+
const projectName = base.endsWith(MODEL_PROJECT_SUFFIX) ? base : `${base}${MODEL_PROJECT_SUFFIX}`;
|
|
505
|
+
const libsDir = (options.libsDir || DEFAULT_MODEL_LIBS_DIR).replace(
|
|
506
|
+
/\\/g,
|
|
507
|
+
"/"
|
|
508
|
+
);
|
|
509
|
+
const scope = codegenFs ? readNpmScope(codegenFs) : void 0;
|
|
510
|
+
return {
|
|
511
|
+
projectName,
|
|
512
|
+
projectRoot: `${libsDir}/${projectName}`.replace(/^\/+/, ""),
|
|
513
|
+
importPath: scope ? `@${scope}/${projectName}` : projectName
|
|
514
|
+
};
|
|
515
|
+
}
|
|
516
|
+
function readNpmScope(codegenFs) {
|
|
517
|
+
if (!codegenFs.exists("package.json")) {
|
|
518
|
+
return void 0;
|
|
519
|
+
}
|
|
520
|
+
const name = readJson(codegenFs, "package.json").name;
|
|
521
|
+
if (!name?.startsWith("@")) {
|
|
522
|
+
return void 0;
|
|
523
|
+
}
|
|
524
|
+
return name.split("/")[0].slice(1);
|
|
525
|
+
}
|
|
526
|
+
function offsetFromRoot(projectRoot) {
|
|
527
|
+
return projectRoot.split("/").filter(Boolean).map(() => "../").join("");
|
|
528
|
+
}
|
|
529
|
+
function generateModelProject(codegenFs, templateDir, options) {
|
|
530
|
+
const layout = resolveModelProjectLayout(codegenFs, options);
|
|
531
|
+
generateFilesFromTemplates(
|
|
532
|
+
codegenFs,
|
|
533
|
+
path__namespace.join(templateDir, "project"),
|
|
534
|
+
layout.projectRoot,
|
|
535
|
+
{
|
|
536
|
+
...layout,
|
|
537
|
+
projectNameCamelCase: camelCase(layout.projectName),
|
|
538
|
+
offsetFromRoot: offsetFromRoot(layout.projectRoot),
|
|
539
|
+
template: ""
|
|
540
|
+
}
|
|
541
|
+
);
|
|
542
|
+
registerTsconfigPath(codegenFs, layout);
|
|
543
|
+
ensureJsoncEslintParser(codegenFs);
|
|
544
|
+
return layout;
|
|
545
|
+
}
|
|
546
|
+
function registerTsconfigPath(codegenFs, layout) {
|
|
547
|
+
if (!codegenFs.exists("tsconfig.base.json")) {
|
|
548
|
+
return;
|
|
549
|
+
}
|
|
550
|
+
updateJson(codegenFs, "tsconfig.base.json", (json) => {
|
|
551
|
+
json.compilerOptions = json.compilerOptions ?? {};
|
|
552
|
+
json.compilerOptions.paths = json.compilerOptions.paths ?? {};
|
|
553
|
+
json.compilerOptions.paths[layout.importPath] = [
|
|
554
|
+
`${layout.projectRoot}/src/index.ts`
|
|
555
|
+
];
|
|
556
|
+
return json;
|
|
557
|
+
});
|
|
558
|
+
}
|
|
559
|
+
function ensureJsoncEslintParser(codegenFs) {
|
|
560
|
+
if (!codegenFs.exists("package.json")) {
|
|
561
|
+
return;
|
|
562
|
+
}
|
|
563
|
+
const pkg = readJson(codegenFs, "package.json");
|
|
564
|
+
const devDependencies = pkg.devDependencies ?? {};
|
|
565
|
+
if ("jsonc-eslint-parser" in devDependencies) {
|
|
566
|
+
return;
|
|
567
|
+
}
|
|
568
|
+
devDependencies["jsonc-eslint-parser"] = JSONC_ESLINT_PARSER_VERSION;
|
|
569
|
+
pkg.devDependencies = Object.fromEntries(
|
|
570
|
+
// Codepoint order, matching what Nx's own dependency helper produces.
|
|
571
|
+
Object.entries(devDependencies).sort(
|
|
572
|
+
([a], [b]) => a < b ? -1 : a > b ? 1 : 0
|
|
573
|
+
)
|
|
574
|
+
);
|
|
575
|
+
writeJson(codegenFs, "package.json", pkg);
|
|
576
|
+
}
|
|
499
577
|
function generateInit(codegenFs, options) {
|
|
500
578
|
const logger = getCodegenLogger();
|
|
501
579
|
const { appProject, modelProject, registrationProject } = options;
|
|
@@ -947,6 +1025,58 @@ function normalizeOptions(codegenFs, options, projects) {
|
|
|
947
1025
|
template: ""
|
|
948
1026
|
};
|
|
949
1027
|
}
|
|
1028
|
+
const UPDATE_RELEASE_VERSION_SCRIPT = `import devkit from "@nx/devkit";
|
|
1029
|
+
import { resolve } from "path";
|
|
1030
|
+
import { readFileSync, writeFileSync } from "fs";
|
|
1031
|
+
import prettier from "prettier";
|
|
1032
|
+
|
|
1033
|
+
// KOS artifact versioning: stamps the project's .kos.json "version" field
|
|
1034
|
+
// (which kabtool bakes into the KAB). Never touches package.json.
|
|
1035
|
+
// Driven by tag-based releases:
|
|
1036
|
+
// nx run-many --target=version --args=--ver=$KOSBUILD_VERSION
|
|
1037
|
+
|
|
1038
|
+
const { readCachedProjectGraph } = devkit;
|
|
1039
|
+
const [, , name, versionArg] = process.argv;
|
|
1040
|
+
|
|
1041
|
+
// "{args.ver}" arrives literally when the target runs without --args=--ver=<v>;
|
|
1042
|
+
// treat that (or a missing arg) as "report current version, change nothing".
|
|
1043
|
+
const version =
|
|
1044
|
+
versionArg && !versionArg.startsWith("{args") ? versionArg : undefined;
|
|
1045
|
+
|
|
1046
|
+
if (!name) {
|
|
1047
|
+
console.error("usage: update-release-version.mjs <project> <version>");
|
|
1048
|
+
process.exit(1);
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
const graph = readCachedProjectGraph();
|
|
1052
|
+
const project = graph.nodes[name];
|
|
1053
|
+
if (!project) {
|
|
1054
|
+
console.error("Unknown project: " + name);
|
|
1055
|
+
process.exit(1);
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
const kosJsonPath = resolve(process.cwd(), project.data.root, ".kos.json");
|
|
1059
|
+
let kosJson;
|
|
1060
|
+
try {
|
|
1061
|
+
kosJson = JSON.parse(readFileSync(kosJsonPath, "utf8"));
|
|
1062
|
+
} catch {
|
|
1063
|
+
console.error("Missing or invalid .kos.json: " + kosJsonPath);
|
|
1064
|
+
process.exit(1);
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
if (!version) {
|
|
1068
|
+
console.log(name + ": " + kosJson.version + " (no --ver given; unchanged)");
|
|
1069
|
+
process.exit(0);
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
const prettierOptions = await prettier.resolveConfig(kosJsonPath);
|
|
1073
|
+
const output = await prettier.format(
|
|
1074
|
+
JSON.stringify({ ...kosJson, version }, null, 2),
|
|
1075
|
+
{ ...prettierOptions, parser: "json" }
|
|
1076
|
+
);
|
|
1077
|
+
writeFileSync(kosJsonPath, output);
|
|
1078
|
+
console.log(name + ": version -> " + version);
|
|
1079
|
+
`;
|
|
950
1080
|
function transformSourceFile(codegenFs, filePath, mutate) {
|
|
951
1081
|
const content = codegenFs.read(filePath);
|
|
952
1082
|
if (content === null) {
|
|
@@ -1088,7 +1218,8 @@ function ensureConstCatalogEntry(sourceFile, spec) {
|
|
|
1088
1218
|
}
|
|
1089
1219
|
literal.addPropertyAssignment({
|
|
1090
1220
|
name: spec.key,
|
|
1091
|
-
initializer: spec.initializer
|
|
1221
|
+
initializer: spec.initializer,
|
|
1222
|
+
leadingTrivia: spec.leadingTrivia
|
|
1092
1223
|
});
|
|
1093
1224
|
return { key: spec.key, created: true };
|
|
1094
1225
|
}
|
|
@@ -1106,7 +1237,8 @@ function ensureConstCatalogEntry(sourceFile, spec) {
|
|
|
1106
1237
|
}
|
|
1107
1238
|
literal.addPropertyAssignment({
|
|
1108
1239
|
name: spec.key,
|
|
1109
|
-
initializer: spec.initializer
|
|
1240
|
+
initializer: spec.initializer,
|
|
1241
|
+
leadingTrivia: spec.leadingTrivia
|
|
1110
1242
|
});
|
|
1111
1243
|
return { key: spec.key, created: true };
|
|
1112
1244
|
}
|
|
@@ -1115,7 +1247,8 @@ function ensureExportedTypeAlias(sourceFile, spec) {
|
|
|
1115
1247
|
sourceFile.addTypeAlias({
|
|
1116
1248
|
name: spec.name,
|
|
1117
1249
|
type: spec.type,
|
|
1118
|
-
isExported: true
|
|
1250
|
+
isExported: true,
|
|
1251
|
+
docs: spec.docs ? [spec.docs] : void 0
|
|
1119
1252
|
});
|
|
1120
1253
|
return true;
|
|
1121
1254
|
}
|
|
@@ -1139,12 +1272,47 @@ function ensureBarrelExport(sourceFile, moduleSpecifier) {
|
|
|
1139
1272
|
sourceFile.addExportDeclaration({ moduleSpecifier });
|
|
1140
1273
|
return true;
|
|
1141
1274
|
}
|
|
1275
|
+
function ensureNamedExports(sourceFile, moduleSpecifier, names, isTypeOnly = false) {
|
|
1276
|
+
const existing = sourceFile.getExportDeclarations().find(
|
|
1277
|
+
(d) => d.getModuleSpecifierValue() === moduleSpecifier && d.isTypeOnly() === isTypeOnly
|
|
1278
|
+
);
|
|
1279
|
+
if (!existing) {
|
|
1280
|
+
sourceFile.addExportDeclaration({
|
|
1281
|
+
moduleSpecifier,
|
|
1282
|
+
isTypeOnly,
|
|
1283
|
+
namedExports: names.map((name) => {
|
|
1284
|
+
return { name };
|
|
1285
|
+
})
|
|
1286
|
+
});
|
|
1287
|
+
return true;
|
|
1288
|
+
}
|
|
1289
|
+
const present = new Set(existing.getNamedExports().map((e) => e.getName()));
|
|
1290
|
+
const missing = names.filter((name) => !present.has(name));
|
|
1291
|
+
if (missing.length === 0) return false;
|
|
1292
|
+
existing.addNamedExports(
|
|
1293
|
+
missing.map((name) => {
|
|
1294
|
+
return { name };
|
|
1295
|
+
})
|
|
1296
|
+
);
|
|
1297
|
+
return true;
|
|
1298
|
+
}
|
|
1299
|
+
function ensureExportedInterface(sourceFile, name, properties = [], extendsTypes = []) {
|
|
1300
|
+
if (sourceFile.getInterface(name)) return false;
|
|
1301
|
+
sourceFile.addInterface({
|
|
1302
|
+
name,
|
|
1303
|
+
isExported: true,
|
|
1304
|
+
extends: extendsTypes,
|
|
1305
|
+
properties
|
|
1306
|
+
});
|
|
1307
|
+
return true;
|
|
1308
|
+
}
|
|
1142
1309
|
function addDecoratedMethod(cls, spec) {
|
|
1143
1310
|
if (cls.getMethod(spec.name)) return false;
|
|
1144
1311
|
cls.addMethod({
|
|
1145
1312
|
name: spec.name,
|
|
1146
1313
|
isAsync: spec.isAsync,
|
|
1147
1314
|
returnType: spec.returnType,
|
|
1315
|
+
docs: spec.docs ? [spec.docs] : void 0,
|
|
1148
1316
|
parameters: spec.parameters?.map((p) => {
|
|
1149
1317
|
return { name: p.name, type: p.type, hasQuestionToken: p.optional };
|
|
1150
1318
|
}),
|
|
@@ -1152,6 +1320,7 @@ function addDecoratedMethod(cls, spec) {
|
|
|
1152
1320
|
decorators: [
|
|
1153
1321
|
{
|
|
1154
1322
|
name: spec.decoratorName,
|
|
1323
|
+
typeArguments: spec.decoratorTypeArgs,
|
|
1155
1324
|
arguments: spec.decoratorArgsText ? [spec.decoratorArgsText] : []
|
|
1156
1325
|
}
|
|
1157
1326
|
]
|
|
@@ -1234,17 +1403,28 @@ function listServiceCatalog(codegenFs, query, projects) {
|
|
|
1234
1403
|
app,
|
|
1235
1404
|
version,
|
|
1236
1405
|
serviceModulePath: posix,
|
|
1237
|
-
operations: readOperations(codegenFs, openapiPath)
|
|
1406
|
+
operations: readOperations(codegenFs, openapiPath) ?? []
|
|
1238
1407
|
});
|
|
1239
1408
|
}
|
|
1240
1409
|
return entries.sort(
|
|
1241
1410
|
(a, b) => a.app.localeCompare(b.app) || a.version.localeCompare(b.version)
|
|
1242
1411
|
);
|
|
1243
1412
|
}
|
|
1413
|
+
function readServiceModuleOperations(codegenFs, serviceModuleFile) {
|
|
1414
|
+
const moduleFile = serviceModuleFile.endsWith(".ts") ? serviceModuleFile : `${serviceModuleFile}.ts`;
|
|
1415
|
+
const posix = moduleFile.split(path__namespace.sep).join("/");
|
|
1416
|
+
const openapiPath = posix.replace(/service\.ts$/, "openapi.d.ts");
|
|
1417
|
+
for (const candidate of [openapiPath, posix]) {
|
|
1418
|
+
if (!codegenFs.exists(candidate)) continue;
|
|
1419
|
+
const operations = readOperations(codegenFs, candidate);
|
|
1420
|
+
if (operations) return operations;
|
|
1421
|
+
}
|
|
1422
|
+
return null;
|
|
1423
|
+
}
|
|
1244
1424
|
function readOperations(codegenFs, openapiPath) {
|
|
1245
1425
|
return readSourceFile(codegenFs, openapiPath, (sf) => {
|
|
1246
1426
|
const paths = sf.getInterface("paths");
|
|
1247
|
-
if (!paths) return
|
|
1427
|
+
if (!paths) return null;
|
|
1248
1428
|
const operations = [];
|
|
1249
1429
|
for (const pathProp of paths.getProperties()) {
|
|
1250
1430
|
const servicePath = unquote(pathProp.getName());
|
|
@@ -1278,58 +1458,6 @@ function readSummary(methodProp) {
|
|
|
1278
1458
|
function unquote(name) {
|
|
1279
1459
|
return name.replace(/^["']|["']$/g, "");
|
|
1280
1460
|
}
|
|
1281
|
-
const UPDATE_RELEASE_VERSION_SCRIPT = `import devkit from "@nx/devkit";
|
|
1282
|
-
import { resolve } from "path";
|
|
1283
|
-
import { readFileSync, writeFileSync } from "fs";
|
|
1284
|
-
import prettier from "prettier";
|
|
1285
|
-
|
|
1286
|
-
// KOS artifact versioning: stamps the project's .kos.json "version" field
|
|
1287
|
-
// (which kabtool bakes into the KAB). Never touches package.json.
|
|
1288
|
-
// Driven by tag-based releases:
|
|
1289
|
-
// nx run-many --target=version --args=--ver=$KOSBUILD_VERSION
|
|
1290
|
-
|
|
1291
|
-
const { readCachedProjectGraph } = devkit;
|
|
1292
|
-
const [, , name, versionArg] = process.argv;
|
|
1293
|
-
|
|
1294
|
-
// "{args.ver}" arrives literally when the target runs without --args=--ver=<v>;
|
|
1295
|
-
// treat that (or a missing arg) as "report current version, change nothing".
|
|
1296
|
-
const version =
|
|
1297
|
-
versionArg && !versionArg.startsWith("{args") ? versionArg : undefined;
|
|
1298
|
-
|
|
1299
|
-
if (!name) {
|
|
1300
|
-
console.error("usage: update-release-version.mjs <project> <version>");
|
|
1301
|
-
process.exit(1);
|
|
1302
|
-
}
|
|
1303
|
-
|
|
1304
|
-
const graph = readCachedProjectGraph();
|
|
1305
|
-
const project = graph.nodes[name];
|
|
1306
|
-
if (!project) {
|
|
1307
|
-
console.error("Unknown project: " + name);
|
|
1308
|
-
process.exit(1);
|
|
1309
|
-
}
|
|
1310
|
-
|
|
1311
|
-
const kosJsonPath = resolve(process.cwd(), project.data.root, ".kos.json");
|
|
1312
|
-
let kosJson;
|
|
1313
|
-
try {
|
|
1314
|
-
kosJson = JSON.parse(readFileSync(kosJsonPath, "utf8"));
|
|
1315
|
-
} catch {
|
|
1316
|
-
console.error("Missing or invalid .kos.json: " + kosJsonPath);
|
|
1317
|
-
process.exit(1);
|
|
1318
|
-
}
|
|
1319
|
-
|
|
1320
|
-
if (!version) {
|
|
1321
|
-
console.log(name + ": " + kosJson.version + " (no --ver given; unchanged)");
|
|
1322
|
-
process.exit(0);
|
|
1323
|
-
}
|
|
1324
|
-
|
|
1325
|
-
const prettierOptions = await prettier.resolveConfig(kosJsonPath);
|
|
1326
|
-
const output = await prettier.format(
|
|
1327
|
-
JSON.stringify({ ...kosJson, version }, null, 2),
|
|
1328
|
-
{ ...prettierOptions, parser: "json" }
|
|
1329
|
-
);
|
|
1330
|
-
writeFileSync(kosJsonPath, output);
|
|
1331
|
-
console.log(name + ": version -> " + version);
|
|
1332
|
-
`;
|
|
1333
1461
|
function appendBarrelExport(codegenFs, indexPath, exportPath) {
|
|
1334
1462
|
const exportLine = `export * from '${exportPath}'`;
|
|
1335
1463
|
const content = codegenFs.read(indexPath) ?? "";
|
|
@@ -1376,10 +1504,57 @@ function updateModelIndex(codegenFs, indexPath, modelPath) {
|
|
|
1376
1504
|
const newContents = printer.printFile(updatedSourceFile);
|
|
1377
1505
|
codegenFs.write(indexPath, newContents);
|
|
1378
1506
|
}
|
|
1379
|
-
function
|
|
1380
|
-
|
|
1381
|
-
|
|
1507
|
+
function generateCompanionModel(codegenFs, templateDir, options, projects) {
|
|
1508
|
+
const logger = getCodegenLogger();
|
|
1509
|
+
const normalized = normalizeAllValues({
|
|
1510
|
+
companionModelName: options.companionModelName,
|
|
1511
|
+
modelName: options.modelName
|
|
1512
|
+
});
|
|
1513
|
+
const companionChildKosConfig = getKosProjectConfiguration(
|
|
1514
|
+
codegenFs,
|
|
1515
|
+
options.companionModelProject,
|
|
1516
|
+
projects
|
|
1517
|
+
);
|
|
1518
|
+
const parentProject = findProjectByName(
|
|
1519
|
+
codegenFs.root,
|
|
1520
|
+
options.modelProject,
|
|
1521
|
+
projects
|
|
1522
|
+
);
|
|
1523
|
+
const childProject = findProjectByName(
|
|
1524
|
+
codegenFs.root,
|
|
1525
|
+
options.companionModelProject,
|
|
1526
|
+
projects
|
|
1527
|
+
);
|
|
1528
|
+
const projectRoot = childProject?.sourceRoot;
|
|
1529
|
+
if (!projectRoot) {
|
|
1530
|
+
logger.warn(`Companion child project source root not found`);
|
|
1531
|
+
return;
|
|
1532
|
+
}
|
|
1533
|
+
let importPath = "";
|
|
1534
|
+
if (parentProject) {
|
|
1535
|
+
const pkgJsonPath = path__namespace.join(parentProject.root, "package.json");
|
|
1536
|
+
try {
|
|
1537
|
+
const pkgJson = readJson(codegenFs, pkgJsonPath);
|
|
1538
|
+
importPath = pkgJson.name || "";
|
|
1539
|
+
} catch {
|
|
1540
|
+
importPath = "";
|
|
1541
|
+
}
|
|
1382
1542
|
}
|
|
1543
|
+
const modelLocation = companionChildKosConfig?.generator?.defaults?.model?.folder || "";
|
|
1544
|
+
const filePath = path__namespace.join(
|
|
1545
|
+
projectRoot,
|
|
1546
|
+
modelLocation,
|
|
1547
|
+
normalized.companionModelNameDashCase
|
|
1548
|
+
);
|
|
1549
|
+
logger.info(`Generating companion model in ${filePath}`);
|
|
1550
|
+
generateFilesFromTemplates(codegenFs, templateDir, filePath, {
|
|
1551
|
+
...options,
|
|
1552
|
+
...normalized,
|
|
1553
|
+
importPath
|
|
1554
|
+
});
|
|
1555
|
+
}
|
|
1556
|
+
function generateContainerModel(codegenFs, templateDir, options, cwd, projects) {
|
|
1557
|
+
const logger = getCodegenLogger();
|
|
1383
1558
|
const currentProject = getProject(codegenFs, cwd);
|
|
1384
1559
|
const modelProjectName = options.modelProject || currentProject?.name;
|
|
1385
1560
|
if (!modelProjectName) {
|
|
@@ -1387,56 +1562,109 @@ function generateHook(codegenFs, templateDir, options, cwd, projects) {
|
|
|
1387
1562
|
"No model project found. Please specify a model project with --modelProject."
|
|
1388
1563
|
);
|
|
1389
1564
|
}
|
|
1390
|
-
const modelName = options.
|
|
1565
|
+
const modelName = options.modelName || getCurrentDirectoryName(cwd);
|
|
1391
1566
|
if (!modelName) {
|
|
1392
1567
|
throw new Error(
|
|
1393
1568
|
"No model name found. Please specify a model name with --name."
|
|
1394
1569
|
);
|
|
1395
1570
|
}
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
modelProjectName,
|
|
1399
|
-
modelName,
|
|
1400
|
-
projects
|
|
1401
|
-
);
|
|
1402
|
-
options.singleton = kosModelConfig ? !!kosModelConfig.singleton : false;
|
|
1403
|
-
options.name = modelName;
|
|
1404
|
-
options.modelProject = modelProjectName;
|
|
1571
|
+
options.modelName = modelName;
|
|
1572
|
+
options.name = `${modelName}-container`;
|
|
1405
1573
|
const normalized = normalizeOptions(codegenFs, options, projects);
|
|
1406
|
-
const
|
|
1574
|
+
const projectConfig = findProjectByName(
|
|
1407
1575
|
codegenFs.root,
|
|
1408
|
-
normalized.
|
|
1576
|
+
normalized.modelProject,
|
|
1409
1577
|
projects
|
|
1410
1578
|
);
|
|
1411
|
-
if (!
|
|
1412
|
-
throw new Error(`
|
|
1579
|
+
if (!projectConfig) {
|
|
1580
|
+
throw new Error(`Model project '${normalized.modelProject}' not found`);
|
|
1413
1581
|
}
|
|
1582
|
+
addKosModelConfiguration({
|
|
1583
|
+
codegenFs,
|
|
1584
|
+
modelName: normalized.nameDashCase,
|
|
1585
|
+
projectName: projectConfig.name,
|
|
1586
|
+
projectRoot: projectConfig.root,
|
|
1587
|
+
singleton: !!options.singleton,
|
|
1588
|
+
container: true,
|
|
1589
|
+
// The container's exported registration bean (`export const <ProperCase>`).
|
|
1590
|
+
factory: normalized.nameProperCase
|
|
1591
|
+
});
|
|
1414
1592
|
const kosConfig = getKosProjectConfiguration(
|
|
1415
1593
|
codegenFs,
|
|
1416
|
-
|
|
1594
|
+
projectConfig.name,
|
|
1417
1595
|
projects
|
|
1418
1596
|
);
|
|
1419
|
-
const
|
|
1420
|
-
|
|
1421
|
-
|
|
1597
|
+
const modelLocation = kosConfig?.generator?.defaults?.model?.folder || "";
|
|
1598
|
+
const internal = !!kosConfig?.generator?.internal;
|
|
1599
|
+
options.modelDirectory = options.modelDirectory || modelLocation;
|
|
1600
|
+
const projectRoot = projectConfig.sourceRoot;
|
|
1422
1601
|
if (projectRoot) {
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
templateDir,
|
|
1426
|
-
path__namespace.join(
|
|
1427
|
-
projectRoot,
|
|
1428
|
-
options.appDirectory,
|
|
1429
|
-
"hooks",
|
|
1430
|
-
normalized.nameDashCase
|
|
1431
|
-
),
|
|
1432
|
-
normalized
|
|
1602
|
+
logger.info(
|
|
1603
|
+
`Generating container model ${normalized.nameDashCase} in ${projectRoot}`
|
|
1433
1604
|
);
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1605
|
+
const modelNameDashCase = normalized.modelNameDashCase || normalized.nameDashCase;
|
|
1606
|
+
const modelFolder = path__namespace.join(
|
|
1607
|
+
projectRoot,
|
|
1608
|
+
options.modelDirectory || "",
|
|
1609
|
+
modelNameDashCase
|
|
1610
|
+
);
|
|
1611
|
+
if (options.existingModel) {
|
|
1612
|
+
addContainerToExistingModelFolder(codegenFs, templateDir, modelFolder, {
|
|
1613
|
+
...normalized,
|
|
1614
|
+
internal
|
|
1615
|
+
});
|
|
1616
|
+
} else {
|
|
1617
|
+
generateFilesFromTemplates(
|
|
1618
|
+
codegenFs,
|
|
1619
|
+
path__namespace.join(templateDir, "model"),
|
|
1620
|
+
modelFolder,
|
|
1621
|
+
{ ...normalized, internal }
|
|
1622
|
+
);
|
|
1623
|
+
}
|
|
1624
|
+
const modelIndex = path__namespace.join(projectRoot, "index.ts");
|
|
1625
|
+
const modelPath = normalized.modelDirectory ? `${normalized.modelDirectory}/${modelNameDashCase}` : modelNameDashCase;
|
|
1626
|
+
updateModelIndex(codegenFs, modelIndex, modelPath);
|
|
1627
|
+
}
|
|
1628
|
+
}
|
|
1629
|
+
function addContainerToExistingModelFolder(codegenFs, templateDir, modelFolder, substitutions) {
|
|
1630
|
+
const containerFileName = `${substitutions.nameDashCase}-model.ts`;
|
|
1631
|
+
const containerTemplate = path__namespace.join(
|
|
1632
|
+
templateDir,
|
|
1633
|
+
"model",
|
|
1634
|
+
"__nameDashCase__-model.ts.template"
|
|
1635
|
+
);
|
|
1636
|
+
const rendered = ejs__namespace.render(
|
|
1637
|
+
fs__namespace.readFileSync(containerTemplate, "utf-8"),
|
|
1638
|
+
substitutions,
|
|
1639
|
+
{ filename: containerTemplate }
|
|
1640
|
+
);
|
|
1641
|
+
codegenFs.write(path__namespace.join(modelFolder, containerFileName), rendered);
|
|
1642
|
+
const typesPath = path__namespace.join(modelFolder, "types", "index.d.ts");
|
|
1643
|
+
if (codegenFs.read(typesPath) === null) {
|
|
1644
|
+
codegenFs.write(typesPath, "");
|
|
1645
|
+
}
|
|
1646
|
+
transformSourceFile(codegenFs, typesPath, (sourceFile) => {
|
|
1647
|
+
ensureExportedInterface(
|
|
1648
|
+
sourceFile,
|
|
1649
|
+
`${substitutions.nameProperCase}Options`
|
|
1438
1650
|
);
|
|
1651
|
+
});
|
|
1652
|
+
const barrelPath = path__namespace.join(modelFolder, "index.ts");
|
|
1653
|
+
if (codegenFs.read(barrelPath) === null) {
|
|
1654
|
+
codegenFs.write(barrelPath, "");
|
|
1439
1655
|
}
|
|
1656
|
+
transformSourceFile(codegenFs, barrelPath, (sourceFile) => {
|
|
1657
|
+
const moduleSpecifier = `./${containerFileName.replace(/\.ts$/, "")}`;
|
|
1658
|
+
ensureNamedExports(sourceFile, moduleSpecifier, [
|
|
1659
|
+
substitutions.nameProperCase
|
|
1660
|
+
]);
|
|
1661
|
+
ensureNamedExports(
|
|
1662
|
+
sourceFile,
|
|
1663
|
+
moduleSpecifier,
|
|
1664
|
+
[`${substitutions.nameProperCase}Model`],
|
|
1665
|
+
true
|
|
1666
|
+
);
|
|
1667
|
+
});
|
|
1440
1668
|
}
|
|
1441
1669
|
function generateContext(codegenFs, templateDir, options, cwd, projects) {
|
|
1442
1670
|
if (!options.appProject) {
|
|
@@ -1490,8 +1718,10 @@ function generateContext(codegenFs, templateDir, options, cwd, projects) {
|
|
|
1490
1718
|
);
|
|
1491
1719
|
}
|
|
1492
1720
|
}
|
|
1493
|
-
function
|
|
1494
|
-
|
|
1721
|
+
function generateHook(codegenFs, templateDir, options, cwd, projects) {
|
|
1722
|
+
if (!options.appProject) {
|
|
1723
|
+
throw new Error("No app project specified");
|
|
1724
|
+
}
|
|
1495
1725
|
const currentProject = getProject(codegenFs, cwd);
|
|
1496
1726
|
const modelProjectName = options.modelProject || currentProject?.name;
|
|
1497
1727
|
if (!modelProjectName) {
|
|
@@ -1499,106 +1729,56 @@ function generateContainerModel(codegenFs, templateDir, options, cwd, projects)
|
|
|
1499
1729
|
"No model project found. Please specify a model project with --modelProject."
|
|
1500
1730
|
);
|
|
1501
1731
|
}
|
|
1502
|
-
const modelName = options.
|
|
1732
|
+
const modelName = options.name || getCurrentDirectoryName(cwd);
|
|
1503
1733
|
if (!modelName) {
|
|
1504
1734
|
throw new Error(
|
|
1505
1735
|
"No model name found. Please specify a model name with --name."
|
|
1506
1736
|
);
|
|
1507
1737
|
}
|
|
1508
|
-
|
|
1509
|
-
|
|
1738
|
+
const kosModelConfig = getKosModelConfiguration(
|
|
1739
|
+
codegenFs,
|
|
1740
|
+
modelProjectName,
|
|
1741
|
+
modelName,
|
|
1742
|
+
projects
|
|
1743
|
+
);
|
|
1744
|
+
options.singleton = kosModelConfig ? !!kosModelConfig.singleton : false;
|
|
1745
|
+
options.name = modelName;
|
|
1746
|
+
options.modelProject = modelProjectName;
|
|
1510
1747
|
const normalized = normalizeOptions(codegenFs, options, projects);
|
|
1511
|
-
const
|
|
1748
|
+
const appProject = findProjectByName(
|
|
1512
1749
|
codegenFs.root,
|
|
1513
|
-
normalized.
|
|
1750
|
+
normalized.appProject,
|
|
1514
1751
|
projects
|
|
1515
1752
|
);
|
|
1516
|
-
if (!
|
|
1517
|
-
throw new Error(`
|
|
1753
|
+
if (!appProject) {
|
|
1754
|
+
throw new Error(`App project '${normalized.appProject}' not found`);
|
|
1518
1755
|
}
|
|
1519
|
-
addKosModelConfiguration({
|
|
1520
|
-
codegenFs,
|
|
1521
|
-
modelName: normalized.nameDashCase,
|
|
1522
|
-
projectName: projectConfig.name,
|
|
1523
|
-
projectRoot: projectConfig.root,
|
|
1524
|
-
singleton: !!options.singleton,
|
|
1525
|
-
container: true,
|
|
1526
|
-
// The container's exported registration bean (`export const <ProperCase>`).
|
|
1527
|
-
factory: normalized.nameProperCase
|
|
1528
|
-
});
|
|
1529
1756
|
const kosConfig = getKosProjectConfiguration(
|
|
1530
1757
|
codegenFs,
|
|
1531
|
-
|
|
1758
|
+
appProject.name,
|
|
1532
1759
|
projects
|
|
1533
1760
|
);
|
|
1534
|
-
const
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
const projectRoot = projectConfig.sourceRoot;
|
|
1761
|
+
const componentLocation = kosConfig?.generator?.defaults?.component?.folder || "";
|
|
1762
|
+
options.appDirectory = options.appDirectory || componentLocation;
|
|
1763
|
+
const projectRoot = appProject.sourceRoot;
|
|
1538
1764
|
if (projectRoot) {
|
|
1539
|
-
logger.info(
|
|
1540
|
-
`Generating container model ${normalized.nameDashCase} in ${projectRoot}`
|
|
1541
|
-
);
|
|
1542
|
-
const modelNameDashCase = normalized.modelNameDashCase || normalized.nameDashCase;
|
|
1543
1765
|
generateFilesFromTemplates(
|
|
1544
1766
|
codegenFs,
|
|
1545
|
-
|
|
1546
|
-
path__namespace.join(
|
|
1547
|
-
|
|
1767
|
+
templateDir,
|
|
1768
|
+
path__namespace.join(
|
|
1769
|
+
projectRoot,
|
|
1770
|
+
options.appDirectory,
|
|
1771
|
+
"hooks",
|
|
1772
|
+
normalized.nameDashCase
|
|
1773
|
+
),
|
|
1774
|
+
normalized
|
|
1775
|
+
);
|
|
1776
|
+
appendBarrelExport(
|
|
1777
|
+
codegenFs,
|
|
1778
|
+
path__namespace.join(projectRoot, options.appDirectory, "hooks", "index.ts"),
|
|
1779
|
+
`./${normalized.nameDashCase}`
|
|
1548
1780
|
);
|
|
1549
|
-
const modelIndex = path__namespace.join(projectRoot, "index.ts");
|
|
1550
|
-
const modelPath = normalized.modelDirectory ? `${normalized.modelDirectory}/${modelNameDashCase}` : modelNameDashCase;
|
|
1551
|
-
updateModelIndex(codegenFs, modelIndex, modelPath);
|
|
1552
|
-
}
|
|
1553
|
-
}
|
|
1554
|
-
function generateCompanionModel(codegenFs, templateDir, options, projects) {
|
|
1555
|
-
const logger = getCodegenLogger();
|
|
1556
|
-
const normalized = normalizeAllValues({
|
|
1557
|
-
companionModelName: options.companionModelName,
|
|
1558
|
-
modelName: options.modelName
|
|
1559
|
-
});
|
|
1560
|
-
const companionChildKosConfig = getKosProjectConfiguration(
|
|
1561
|
-
codegenFs,
|
|
1562
|
-
options.companionModelProject,
|
|
1563
|
-
projects
|
|
1564
|
-
);
|
|
1565
|
-
const parentProject = findProjectByName(
|
|
1566
|
-
codegenFs.root,
|
|
1567
|
-
options.modelProject,
|
|
1568
|
-
projects
|
|
1569
|
-
);
|
|
1570
|
-
const childProject = findProjectByName(
|
|
1571
|
-
codegenFs.root,
|
|
1572
|
-
options.companionModelProject,
|
|
1573
|
-
projects
|
|
1574
|
-
);
|
|
1575
|
-
const projectRoot = childProject?.sourceRoot;
|
|
1576
|
-
if (!projectRoot) {
|
|
1577
|
-
logger.warn(`Companion child project source root not found`);
|
|
1578
|
-
return;
|
|
1579
|
-
}
|
|
1580
|
-
let importPath = "";
|
|
1581
|
-
if (parentProject) {
|
|
1582
|
-
const pkgJsonPath = path__namespace.join(parentProject.root, "package.json");
|
|
1583
|
-
try {
|
|
1584
|
-
const pkgJson = readJson(codegenFs, pkgJsonPath);
|
|
1585
|
-
importPath = pkgJson.name || "";
|
|
1586
|
-
} catch {
|
|
1587
|
-
importPath = "";
|
|
1588
|
-
}
|
|
1589
1781
|
}
|
|
1590
|
-
const modelLocation = companionChildKosConfig?.generator?.defaults?.model?.folder || "";
|
|
1591
|
-
const filePath = path__namespace.join(
|
|
1592
|
-
projectRoot,
|
|
1593
|
-
modelLocation,
|
|
1594
|
-
normalized.companionModelNameDashCase
|
|
1595
|
-
);
|
|
1596
|
-
logger.info(`Generating companion model in ${filePath}`);
|
|
1597
|
-
generateFilesFromTemplates(codegenFs, templateDir, filePath, {
|
|
1598
|
-
...options,
|
|
1599
|
-
...normalized,
|
|
1600
|
-
importPath
|
|
1601
|
-
});
|
|
1602
1782
|
}
|
|
1603
1783
|
function generateModel(params) {
|
|
1604
1784
|
const {
|
|
@@ -1697,6 +1877,55 @@ function generateModel(params) {
|
|
|
1697
1877
|
);
|
|
1698
1878
|
}
|
|
1699
1879
|
}
|
|
1880
|
+
const DECLARATION_MARKER = "@kosModel";
|
|
1881
|
+
function findModelDeclarationFile(codegenFs, searchRoot, typeIds) {
|
|
1882
|
+
const wanted = new Set(typeIds.filter(Boolean));
|
|
1883
|
+
if (wanted.size === 0) return null;
|
|
1884
|
+
const matches = [];
|
|
1885
|
+
for (const filePath of codegenFs.listFiles(searchRoot)) {
|
|
1886
|
+
if (!filePath.endsWith(".ts") || filePath.endsWith(".d.ts")) continue;
|
|
1887
|
+
const content = codegenFs.read(filePath);
|
|
1888
|
+
if (!content || !content.includes(DECLARATION_MARKER)) continue;
|
|
1889
|
+
const declared = readDeclaredModelType(codegenFs, filePath);
|
|
1890
|
+
if (declared && wanted.has(declared)) matches.push(filePath);
|
|
1891
|
+
}
|
|
1892
|
+
if (matches.length === 0) return null;
|
|
1893
|
+
if (matches.length > 1) {
|
|
1894
|
+
throw new Error(
|
|
1895
|
+
`Model type '${[...wanted].join("' / '")}' is declared in more than one file:
|
|
1896
|
+
` + matches.sort().map((c) => ` - ${c}`).join("\n") + `
|
|
1897
|
+
Pass modelPath to pick one.`
|
|
1898
|
+
);
|
|
1899
|
+
}
|
|
1900
|
+
return matches[0];
|
|
1901
|
+
}
|
|
1902
|
+
function readDeclaredModelType(codegenFs, filePath) {
|
|
1903
|
+
try {
|
|
1904
|
+
return readSourceFile(codegenFs, filePath, (sf) => {
|
|
1905
|
+
const cls = sf.getClasses().find((c) => c.getDecorator("kosModel"));
|
|
1906
|
+
const arg = cls?.getDecorator("kosModel")?.getArguments()[0];
|
|
1907
|
+
if (!arg) return void 0;
|
|
1908
|
+
const inner = arg.getKindName() === "ObjectLiteralExpression" ? modelTypeIdProperty(arg.getText()) : arg.getText();
|
|
1909
|
+
if (!inner) return void 0;
|
|
1910
|
+
return resolveToStringLiteral(inner.trim(), sf.getFullText());
|
|
1911
|
+
});
|
|
1912
|
+
} catch {
|
|
1913
|
+
return void 0;
|
|
1914
|
+
}
|
|
1915
|
+
}
|
|
1916
|
+
function modelTypeIdProperty(objectText) {
|
|
1917
|
+
const m = objectText.match(/\bmodelTypeId\s*:\s*([^,}]+)/);
|
|
1918
|
+
return m ? m[1] : void 0;
|
|
1919
|
+
}
|
|
1920
|
+
function resolveToStringLiteral(expression, fileText) {
|
|
1921
|
+
const literal = expression.match(/^["'`](.*)["'`]$/);
|
|
1922
|
+
if (literal) return literal[1];
|
|
1923
|
+
if (!/^[A-Za-z_$][\w$]*$/.test(expression)) return void 0;
|
|
1924
|
+
const declared = fileText.match(
|
|
1925
|
+
new RegExp(`\\b(?:const|let|var)\\s+${expression}\\s*(?::[^=]+)?=\\s*["'\`]([^"'\`]+)["'\`]`)
|
|
1926
|
+
);
|
|
1927
|
+
return declared ? declared[1] : void 0;
|
|
1928
|
+
}
|
|
1700
1929
|
function resolveModelFilePath(codegenFs, query, projects) {
|
|
1701
1930
|
const kosConfig = getKosProjectConfiguration(
|
|
1702
1931
|
codegenFs,
|
|
@@ -1731,14 +1960,23 @@ function resolveModelFilePath(codegenFs, query, projects) {
|
|
|
1731
1960
|
if (codegenFs.exists(modelFilePath)) {
|
|
1732
1961
|
return { modelFilePath, internal, sourceRoot };
|
|
1733
1962
|
}
|
|
1963
|
+
const searchRoot = path__namespace.join(sourceRoot, modelLocation);
|
|
1734
1964
|
const discovered = findModelFileByName(
|
|
1735
1965
|
codegenFs,
|
|
1736
|
-
|
|
1966
|
+
searchRoot,
|
|
1737
1967
|
modelNameDashCase
|
|
1738
1968
|
);
|
|
1739
1969
|
if (discovered) {
|
|
1740
1970
|
return { modelFilePath: discovered, internal, sourceRoot };
|
|
1741
1971
|
}
|
|
1972
|
+
const declaredType = kosConfig?.models?.[query.modelName]?.type;
|
|
1973
|
+
const byDeclaration = findModelDeclarationFile(codegenFs, searchRoot, [
|
|
1974
|
+
query.modelName,
|
|
1975
|
+
...declaredType ? [declaredType] : []
|
|
1976
|
+
]);
|
|
1977
|
+
if (byDeclaration) {
|
|
1978
|
+
return { modelFilePath: byDeclaration, internal, sourceRoot };
|
|
1979
|
+
}
|
|
1742
1980
|
return { modelFilePath, internal, sourceRoot };
|
|
1743
1981
|
}
|
|
1744
1982
|
function findModelFileByName(codegenFs, searchRoot, modelNameDashCase) {
|
|
@@ -1754,6 +1992,150 @@ Pass modelPath to pick one.`
|
|
|
1754
1992
|
}
|
|
1755
1993
|
return candidates[0];
|
|
1756
1994
|
}
|
|
1995
|
+
function resolveChildModelType(codegenFs, query, projects) {
|
|
1996
|
+
const childProject = query.childModelProject || query.modelProject;
|
|
1997
|
+
const childType = `${properCase(query.childModel)}Model`;
|
|
1998
|
+
if (childProject !== query.modelProject) {
|
|
1999
|
+
const project = findProjectByName(codegenFs.root, childProject, projects);
|
|
2000
|
+
const pkgJson = project ? readJson(
|
|
2001
|
+
codegenFs,
|
|
2002
|
+
path__namespace.join(project.root, "package.json")
|
|
2003
|
+
) : void 0;
|
|
2004
|
+
return { childType, childTypeModule: pkgJson?.name };
|
|
2005
|
+
}
|
|
2006
|
+
const { modelFilePath: childFilePath } = resolveModelFilePath(
|
|
2007
|
+
codegenFs,
|
|
2008
|
+
{ modelName: query.childModel, modelProject: childProject },
|
|
2009
|
+
projects
|
|
2010
|
+
);
|
|
2011
|
+
const relative = path__namespace.relative(path__namespace.dirname(query.modelFilePath), childFilePath).replace(/\.ts$/, "");
|
|
2012
|
+
return {
|
|
2013
|
+
childType,
|
|
2014
|
+
childTypeModule: relative.startsWith(".") ? relative : `./${relative}`
|
|
2015
|
+
};
|
|
2016
|
+
}
|
|
2017
|
+
function generateViewModel(codegenFs, templateDir, options, cwd, projects) {
|
|
2018
|
+
const logger = getCodegenLogger();
|
|
2019
|
+
if (!options.name) {
|
|
2020
|
+
throw new Error(
|
|
2021
|
+
"No ViewModel name found. Please specify a name with --name."
|
|
2022
|
+
);
|
|
2023
|
+
}
|
|
2024
|
+
const currentProject = getProject(codegenFs, cwd);
|
|
2025
|
+
const modelProjectName = options.modelProject || currentProject?.name;
|
|
2026
|
+
if (!modelProjectName) {
|
|
2027
|
+
throw new Error(
|
|
2028
|
+
"No model project found. Please specify a model project with --project."
|
|
2029
|
+
);
|
|
2030
|
+
}
|
|
2031
|
+
const normalized = normalizeOptions(
|
|
2032
|
+
codegenFs,
|
|
2033
|
+
{ ...options, modelProject: modelProjectName },
|
|
2034
|
+
projects
|
|
2035
|
+
);
|
|
2036
|
+
const projectConfig = findProjectByName(
|
|
2037
|
+
codegenFs.root,
|
|
2038
|
+
modelProjectName,
|
|
2039
|
+
projects
|
|
2040
|
+
);
|
|
2041
|
+
if (!projectConfig) {
|
|
2042
|
+
throw new Error(`Model project '${modelProjectName}' not found`);
|
|
2043
|
+
}
|
|
2044
|
+
const kosConfig = getKosProjectConfiguration(
|
|
2045
|
+
codegenFs,
|
|
2046
|
+
projectConfig.name,
|
|
2047
|
+
projects
|
|
2048
|
+
);
|
|
2049
|
+
const modelDirectory = options.modelDirectory || kosConfig?.generator?.defaults?.model?.folder || "";
|
|
2050
|
+
const internal = !!kosConfig?.generator?.internal;
|
|
2051
|
+
const projectRoot = projectConfig.sourceRoot || path__namespace.join(projectConfig.root, "src");
|
|
2052
|
+
const viewModelFolder = path__namespace.join(
|
|
2053
|
+
projectRoot,
|
|
2054
|
+
modelDirectory,
|
|
2055
|
+
normalized.nameDashCase
|
|
2056
|
+
);
|
|
2057
|
+
const viewModelFilePath = path__namespace.join(
|
|
2058
|
+
viewModelFolder,
|
|
2059
|
+
`${normalized.nameDashCase}-view-model.ts`
|
|
2060
|
+
);
|
|
2061
|
+
if (codegenFs.exists(viewModelFilePath)) {
|
|
2062
|
+
logger.info(`ViewModel already exists: ${viewModelFilePath}`);
|
|
2063
|
+
return { viewModelFilePath, created: false };
|
|
2064
|
+
}
|
|
2065
|
+
const resolved = (options.models || []).map(
|
|
2066
|
+
(source) => resolveSourceModel(
|
|
2067
|
+
codegenFs,
|
|
2068
|
+
source,
|
|
2069
|
+
viewModelFilePath,
|
|
2070
|
+
modelProjectName,
|
|
2071
|
+
projects
|
|
2072
|
+
)
|
|
2073
|
+
);
|
|
2074
|
+
logger.info(
|
|
2075
|
+
`Generating ViewModel ${normalized.nameDashCase} in ${projectRoot}`
|
|
2076
|
+
);
|
|
2077
|
+
const constructorParams = resolved.map(({ name, type }) => ({ name, type }));
|
|
2078
|
+
generateFilesFromTemplates(codegenFs, templateDir, viewModelFolder, {
|
|
2079
|
+
...normalized,
|
|
2080
|
+
internal,
|
|
2081
|
+
typeId: options.typeId || normalized.nameDashCase,
|
|
2082
|
+
devToolsEnabled: !!options.devToolsEnabled,
|
|
2083
|
+
constructorParams,
|
|
2084
|
+
constructorArgs: constructorParams.map((param) => param.name).join(", "),
|
|
2085
|
+
modelImports: groupImports(resolved)
|
|
2086
|
+
});
|
|
2087
|
+
updateModelIndex(
|
|
2088
|
+
codegenFs,
|
|
2089
|
+
path__namespace.join(projectRoot, "index.ts"),
|
|
2090
|
+
modelDirectory ? `${modelDirectory}/${normalized.nameDashCase}` : normalized.nameDashCase
|
|
2091
|
+
);
|
|
2092
|
+
return { viewModelFilePath, created: true };
|
|
2093
|
+
}
|
|
2094
|
+
function resolveSourceModel(codegenFs, source, viewModelFilePath, modelProject, projects) {
|
|
2095
|
+
const sourceProject = source.project || modelProject;
|
|
2096
|
+
if (sourceProject === modelProject) {
|
|
2097
|
+
const { modelFilePath } = resolveModelFilePath(
|
|
2098
|
+
codegenFs,
|
|
2099
|
+
{ modelName: source.model, modelProject: sourceProject },
|
|
2100
|
+
projects
|
|
2101
|
+
);
|
|
2102
|
+
if (!codegenFs.exists(modelFilePath)) {
|
|
2103
|
+
throw new Error(
|
|
2104
|
+
`Model '${source.model}' not found in project '${sourceProject}' (looked for ${modelFilePath})`
|
|
2105
|
+
);
|
|
2106
|
+
}
|
|
2107
|
+
} else if (!findProjectByName(codegenFs.root, sourceProject, projects)) {
|
|
2108
|
+
throw new Error(`Model project '${sourceProject}' not found`);
|
|
2109
|
+
}
|
|
2110
|
+
const { childType, childTypeModule } = resolveChildModelType(
|
|
2111
|
+
codegenFs,
|
|
2112
|
+
{
|
|
2113
|
+
modelFilePath: viewModelFilePath,
|
|
2114
|
+
modelProject,
|
|
2115
|
+
childModel: source.model,
|
|
2116
|
+
childModelProject: sourceProject
|
|
2117
|
+
},
|
|
2118
|
+
projects
|
|
2119
|
+
);
|
|
2120
|
+
return {
|
|
2121
|
+
name: camelCase(source.model),
|
|
2122
|
+
type: childType,
|
|
2123
|
+
module: childTypeModule
|
|
2124
|
+
};
|
|
2125
|
+
}
|
|
2126
|
+
function groupImports(resolved) {
|
|
2127
|
+
const byModule = /* @__PURE__ */ new Map();
|
|
2128
|
+
for (const { type, module: module2 } of resolved) {
|
|
2129
|
+
if (!module2) continue;
|
|
2130
|
+
const types = byModule.get(module2);
|
|
2131
|
+
if (!types) {
|
|
2132
|
+
byModule.set(module2, [type]);
|
|
2133
|
+
} else if (!types.includes(type)) {
|
|
2134
|
+
types.push(type);
|
|
2135
|
+
}
|
|
2136
|
+
}
|
|
2137
|
+
return [...byModule].map(([module2, types]) => ({ module: module2, types }));
|
|
2138
|
+
}
|
|
1757
2139
|
function modelBaseName(modelFilePath, modelName) {
|
|
1758
2140
|
const base = path__namespace.basename(modelFilePath);
|
|
1759
2141
|
const match = base.match(/^(.*)-model\.ts$/);
|
|
@@ -1766,7 +2148,7 @@ function servicesFilePathFor(modelFilePath, modelBase) {
|
|
|
1766
2148
|
`${modelBase}-services.ts`
|
|
1767
2149
|
);
|
|
1768
2150
|
}
|
|
1769
|
-
function header(modelBase) {
|
|
2151
|
+
function header$1(modelBase) {
|
|
1770
2152
|
return `/**
|
|
1771
2153
|
* Service layer for the ${modelBase} model: the endpoints it calls and the
|
|
1772
2154
|
* types derived from them. Standalone service functions for callers outside a
|
|
@@ -1777,7 +2159,7 @@ function header(modelBase) {
|
|
|
1777
2159
|
function ensureServicesModule(codegenFs, modelFilePath, modelBase) {
|
|
1778
2160
|
const servicesFilePath = servicesFilePathFor(modelFilePath, modelBase);
|
|
1779
2161
|
if (!codegenFs.exists(servicesFilePath)) {
|
|
1780
|
-
codegenFs.write(servicesFilePath, header(modelBase));
|
|
2162
|
+
codegenFs.write(servicesFilePath, header$1(modelBase));
|
|
1781
2163
|
}
|
|
1782
2164
|
const barrelPath = path__namespace.join(path__namespace.dirname(servicesFilePath), "index.ts");
|
|
1783
2165
|
if (!codegenFs.exists(barrelPath)) {
|
|
@@ -2262,11 +2644,25 @@ function buildDecoratorArgs(options) {
|
|
|
2262
2644
|
}
|
|
2263
2645
|
function addContainerSupportToModel(codegenFs, options, projects) {
|
|
2264
2646
|
const logger = getCodegenLogger();
|
|
2265
|
-
const childType = options.childType?.trim() || "IKosDataModel";
|
|
2266
2647
|
const { modelFilePath } = resolveModelFilePath(codegenFs, options, projects);
|
|
2267
2648
|
if (!codegenFs.exists(modelFilePath)) {
|
|
2268
2649
|
throw new Error(`Model file not found: ${modelFilePath}`);
|
|
2269
2650
|
}
|
|
2651
|
+
const resolvedChild = options.childModel ? resolveChildModelType(
|
|
2652
|
+
codegenFs,
|
|
2653
|
+
{
|
|
2654
|
+
modelFilePath,
|
|
2655
|
+
modelProject: options.modelProject,
|
|
2656
|
+
childModel: options.childModel,
|
|
2657
|
+
childModelProject: options.childModelProject
|
|
2658
|
+
},
|
|
2659
|
+
projects
|
|
2660
|
+
) : {
|
|
2661
|
+
childType: options.childType,
|
|
2662
|
+
childTypeModule: options.childTypeModule
|
|
2663
|
+
};
|
|
2664
|
+
const childType = resolvedChild.childType?.trim() || "IKosDataModel";
|
|
2665
|
+
const childTypeModule = resolvedChild.childTypeModule;
|
|
2270
2666
|
logger.info(
|
|
2271
2667
|
`Adding container support (<${childType}>) to model: ${options.modelName}`
|
|
2272
2668
|
);
|
|
@@ -2278,6 +2674,10 @@ function addContainerSupportToModel(codegenFs, options, projects) {
|
|
|
2278
2674
|
]);
|
|
2279
2675
|
if (childType === "IKosDataModel") {
|
|
2280
2676
|
ensureNamedImport(sf, sdk, [{ name: "IKosDataModel", isTypeOnly: true }]);
|
|
2677
|
+
} else if (childTypeModule) {
|
|
2678
|
+
ensureNamedImport(sf, childTypeModule, [
|
|
2679
|
+
{ name: childType, isTypeOnly: true }
|
|
2680
|
+
]);
|
|
2281
2681
|
}
|
|
2282
2682
|
const cls = getModelClass(sf);
|
|
2283
2683
|
const className = cls.getName();
|
|
@@ -2307,39 +2707,173 @@ function addContainerSupportToModel(codegenFs, options, projects) {
|
|
|
2307
2707
|
});
|
|
2308
2708
|
return { modelFilePath };
|
|
2309
2709
|
}
|
|
2310
|
-
function
|
|
2710
|
+
function addParentAwareToModel(codegenFs, options, projects) {
|
|
2311
2711
|
const logger = getCodegenLogger();
|
|
2312
2712
|
const { modelFilePath } = resolveModelFilePath(codegenFs, options, projects);
|
|
2313
2713
|
if (!codegenFs.exists(modelFilePath)) {
|
|
2314
2714
|
throw new Error(`Model file not found: ${modelFilePath}`);
|
|
2315
2715
|
}
|
|
2316
|
-
logger.info(
|
|
2317
|
-
|
|
2318
|
-
);
|
|
2716
|
+
logger.info(`Adding parent awareness to model: ${options.modelName}`);
|
|
2717
|
+
let optionsTypeName;
|
|
2319
2718
|
transformSourceFile(codegenFs, modelFilePath, (sf) => {
|
|
2320
2719
|
const sdk = resolveSdkModuleSpecifier(sf);
|
|
2321
|
-
ensureNamedImport(sf, sdk, [{ name: "
|
|
2720
|
+
ensureNamedImport(sf, sdk, [{ name: "kosParentAware" }]);
|
|
2322
2721
|
const cls = getModelClass(sf);
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
name: options.methodName,
|
|
2326
|
-
decoratorName: "kosModelEffect",
|
|
2327
|
-
decoratorArgsText: `{ dependencies: (model: ${modelType}) => [] }`,
|
|
2328
|
-
isAsync: true,
|
|
2329
|
-
returnType: "Promise<void>",
|
|
2330
|
-
statements: "// TODO: react to the tracked dependencies"
|
|
2722
|
+
addClassDecorator(cls, "kosParentAware", {
|
|
2723
|
+
argsText: options.parentId ? `{ parentId: ${JSON.stringify(options.parentId)} }` : ""
|
|
2331
2724
|
});
|
|
2725
|
+
const ctor = cls.getConstructors()[0];
|
|
2726
|
+
optionsTypeName = ctor?.getParameters()[1]?.getTypeNode()?.getText()?.replace(/<.*$/, "");
|
|
2332
2727
|
});
|
|
2333
|
-
|
|
2728
|
+
const optionsFilePath = optionsTypeName ? extendOptionsInterface(codegenFs, modelFilePath, optionsTypeName, logger) : void 0;
|
|
2729
|
+
return { modelFilePath, optionsFilePath };
|
|
2334
2730
|
}
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2731
|
+
function extendOptionsInterface(codegenFs, modelFilePath, optionsTypeName, logger) {
|
|
2732
|
+
const typesPath = path__namespace.join(
|
|
2733
|
+
path__namespace.dirname(modelFilePath),
|
|
2734
|
+
"types",
|
|
2735
|
+
"index.d.ts"
|
|
2736
|
+
);
|
|
2737
|
+
const target = codegenFs.exists(typesPath) ? typesPath : modelFilePath;
|
|
2738
|
+
let extended = false;
|
|
2739
|
+
transformSourceFile(codegenFs, target, (sf) => {
|
|
2740
|
+
const iface = sf.getInterface(optionsTypeName);
|
|
2741
|
+
if (!iface) return;
|
|
2742
|
+
const already = iface.getExtends().some((clause) => clause.getText().includes("KosParentAware"));
|
|
2743
|
+
if (already) {
|
|
2744
|
+
extended = true;
|
|
2745
|
+
return;
|
|
2746
|
+
}
|
|
2747
|
+
ensureNamedImport(sf, "@kosdev-code/kos-ui-sdk", [
|
|
2748
|
+
{ name: "KosParentAware", isTypeOnly: true }
|
|
2749
|
+
]);
|
|
2750
|
+
iface.addExtends("KosParentAware");
|
|
2751
|
+
extended = true;
|
|
2752
|
+
});
|
|
2753
|
+
if (!extended) {
|
|
2754
|
+
logger.warn(
|
|
2755
|
+
`Could not find interface ${optionsTypeName}; add "extends KosParentAware" to it by hand.`
|
|
2756
|
+
);
|
|
2757
|
+
return void 0;
|
|
2758
|
+
}
|
|
2759
|
+
return target;
|
|
2760
|
+
}
|
|
2761
|
+
function firstDecoratorArg(decoratorText) {
|
|
2762
|
+
const open = decoratorText.indexOf("(");
|
|
2763
|
+
if (open === -1) return void 0;
|
|
2764
|
+
const inner = decoratorText.slice(open + 1, decoratorText.lastIndexOf(")")).replace(/\s+/g, " ").trim();
|
|
2765
|
+
return inner || void 0;
|
|
2766
|
+
}
|
|
2767
|
+
function collectDecorated(cls, decoratorName) {
|
|
2768
|
+
const members = [];
|
|
2769
|
+
const visit = (name, decoratorTextOf, typeText) => {
|
|
2770
|
+
const text = decoratorTextOf();
|
|
2771
|
+
if (text === void 0) return;
|
|
2772
|
+
members.push({
|
|
2773
|
+
name,
|
|
2774
|
+
arg: firstDecoratorArg(text),
|
|
2775
|
+
type: typeText || void 0
|
|
2776
|
+
});
|
|
2777
|
+
};
|
|
2778
|
+
for (const m of cls.getMethods()) {
|
|
2779
|
+
const dec = m.getDecorator(decoratorName);
|
|
2780
|
+
if (dec) {
|
|
2781
|
+
visit(m.getName(), () => dec.getText(), m.getReturnTypeNode()?.getText());
|
|
2782
|
+
}
|
|
2783
|
+
}
|
|
2784
|
+
for (const p of cls.getProperties()) {
|
|
2785
|
+
const dec = p.getDecorator(decoratorName);
|
|
2786
|
+
if (dec) {
|
|
2787
|
+
visit(p.getName(), () => dec.getText(), p.getTypeNode()?.getText());
|
|
2788
|
+
}
|
|
2789
|
+
}
|
|
2790
|
+
return members;
|
|
2791
|
+
}
|
|
2792
|
+
function describeModel(codegenFs, options, projects) {
|
|
2793
|
+
const { modelFilePath } = resolveModelFilePath(codegenFs, options, projects);
|
|
2794
|
+
const content = codegenFs.read(modelFilePath);
|
|
2795
|
+
if (content === null) {
|
|
2796
|
+
throw new Error(`Model file not found: ${modelFilePath}`);
|
|
2797
|
+
}
|
|
2798
|
+
const project = new tsMorph.Project({ useInMemoryFileSystem: true });
|
|
2799
|
+
const sf = project.createSourceFile(modelFilePath, content, {
|
|
2800
|
+
overwrite: true
|
|
2801
|
+
});
|
|
2802
|
+
let modelType;
|
|
2803
|
+
const modelTypeDecl = sf.getVariableDeclaration("MODEL_TYPE");
|
|
2804
|
+
if (modelTypeDecl) {
|
|
2805
|
+
const init = modelTypeDecl.getInitializer()?.getText();
|
|
2806
|
+
if (init) modelType = init.replace(/^["'`]|["'`]$/g, "");
|
|
2807
|
+
}
|
|
2808
|
+
const cls = sf.getClasses().find((c) => c.getDecorator("kosModel")) ?? sf.getClasses().find((c) => c.isExported()) ?? sf.getClasses()[0];
|
|
2809
|
+
if (!cls) {
|
|
2810
|
+
return {
|
|
2811
|
+
modelFilePath,
|
|
2812
|
+
modelType,
|
|
2813
|
+
classDecorators: [],
|
|
2814
|
+
singleton: false,
|
|
2815
|
+
isCompanion: false,
|
|
2816
|
+
children: [],
|
|
2817
|
+
dependencies: [],
|
|
2818
|
+
topicHandlers: [],
|
|
2819
|
+
configProperties: [],
|
|
2820
|
+
serviceRequests: [],
|
|
2821
|
+
effects: [],
|
|
2822
|
+
futures: []
|
|
2823
|
+
};
|
|
2824
|
+
}
|
|
2825
|
+
const classDecorators = cls.getDecorators().map((d) => d.getName());
|
|
2826
|
+
const kosModelArg = cls.getDecorator("kosModel") ? firstDecoratorArg(cls.getDecorator("kosModel").getText()) : void 0;
|
|
2827
|
+
const singleton = /\bsingleton\s*:\s*true\b/.test(kosModelArg ?? "");
|
|
2828
|
+
return {
|
|
2829
|
+
modelFilePath,
|
|
2830
|
+
modelType,
|
|
2831
|
+
className: cls.getName(),
|
|
2832
|
+
classDecorators,
|
|
2833
|
+
singleton,
|
|
2834
|
+
isCompanion: classDecorators.includes("kosCompanion"),
|
|
2835
|
+
children: collectDecorated(cls, "kosChild"),
|
|
2836
|
+
dependencies: collectDecorated(cls, "kosDependency"),
|
|
2837
|
+
topicHandlers: collectDecorated(cls, "kosTopicHandler"),
|
|
2838
|
+
configProperties: collectDecorated(cls, "kosConfigProperty"),
|
|
2839
|
+
serviceRequests: collectDecorated(cls, "kosServiceRequest"),
|
|
2840
|
+
effects: collectDecorated(cls, "kosModelEffect"),
|
|
2841
|
+
futures: collectDecorated(cls, "kosFuture")
|
|
2842
|
+
};
|
|
2843
|
+
}
|
|
2844
|
+
function addModelEffectToModel(codegenFs, options, projects) {
|
|
2845
|
+
const logger = getCodegenLogger();
|
|
2846
|
+
const { modelFilePath } = resolveModelFilePath(codegenFs, options, projects);
|
|
2847
|
+
if (!codegenFs.exists(modelFilePath)) {
|
|
2848
|
+
throw new Error(`Model file not found: ${modelFilePath}`);
|
|
2849
|
+
}
|
|
2850
|
+
logger.info(
|
|
2851
|
+
`Adding @kosModelEffect "${options.methodName}" to ${options.modelName}`
|
|
2852
|
+
);
|
|
2853
|
+
transformSourceFile(codegenFs, modelFilePath, (sf) => {
|
|
2854
|
+
const sdk = resolveSdkModuleSpecifier(sf);
|
|
2855
|
+
ensureNamedImport(sf, sdk, [{ name: "kosModelEffect" }]);
|
|
2856
|
+
const cls = getModelClass(sf);
|
|
2857
|
+
const modelType = (cls.getName() ?? "").replace(/Impl$/, "");
|
|
2858
|
+
addDecoratedMethod(cls, {
|
|
2859
|
+
name: options.methodName,
|
|
2860
|
+
decoratorName: "kosModelEffect",
|
|
2861
|
+
decoratorArgsText: `{ dependencies: (model: ${modelType}) => [] }`,
|
|
2862
|
+
isAsync: true,
|
|
2863
|
+
returnType: "Promise<void>",
|
|
2864
|
+
statements: "// TODO: react to the tracked dependencies"
|
|
2865
|
+
});
|
|
2866
|
+
});
|
|
2867
|
+
return { modelFilePath };
|
|
2868
|
+
}
|
|
2869
|
+
const FRAMEWORK_TYPES$1 = /* @__PURE__ */ new Set(["IKosDataModel", "IKosIdentifiable"]);
|
|
2870
|
+
function addDependencyToModel(codegenFs, options, projects) {
|
|
2871
|
+
const logger = getCodegenLogger();
|
|
2872
|
+
const { modelFilePath } = resolveModelFilePath(codegenFs, options, projects);
|
|
2873
|
+
if (!codegenFs.exists(modelFilePath)) {
|
|
2874
|
+
throw new Error(`Model file not found: ${modelFilePath}`);
|
|
2875
|
+
}
|
|
2876
|
+
logger.info(
|
|
2343
2877
|
`Adding @kosDependency "${options.propertyName}" to ${options.modelName}`
|
|
2344
2878
|
);
|
|
2345
2879
|
const argsParts = [`modelType: ${options.modelTypeRef}`];
|
|
@@ -2382,7 +2916,18 @@ function addChildToModel(codegenFs, options, projects) {
|
|
|
2382
2916
|
logger.info(
|
|
2383
2917
|
`Adding @kosChild "${options.propertyName}" (${shape}) to ${options.modelName}`
|
|
2384
2918
|
);
|
|
2385
|
-
const
|
|
2919
|
+
const resolvedChild = options.childModel ? resolveChildModelType(
|
|
2920
|
+
codegenFs,
|
|
2921
|
+
{
|
|
2922
|
+
modelFilePath,
|
|
2923
|
+
modelProject: options.modelProject,
|
|
2924
|
+
childModel: options.childModel,
|
|
2925
|
+
childModelProject: options.childModelProject
|
|
2926
|
+
},
|
|
2927
|
+
projects
|
|
2928
|
+
) : { childType: options.childType, childTypeModule: options.childPackage };
|
|
2929
|
+
const childType = resolvedChild.childType?.trim();
|
|
2930
|
+
const childTypeModule = resolvedChild.childTypeModule;
|
|
2386
2931
|
transformSourceFile(codegenFs, modelFilePath, (sf) => {
|
|
2387
2932
|
const sdk = resolveSdkModuleSpecifier(sf);
|
|
2388
2933
|
const sdkImports = [
|
|
@@ -2390,8 +2935,8 @@ function addChildToModel(codegenFs, options, projects) {
|
|
|
2390
2935
|
];
|
|
2391
2936
|
if (shape === "container") sdkImports.push({ name: "KosModelContainer" });
|
|
2392
2937
|
ensureNamedImport(sf, sdk, sdkImports);
|
|
2393
|
-
if (
|
|
2394
|
-
ensureNamedImport(sf,
|
|
2938
|
+
if (childTypeModule && childType && /^[A-Za-z_$][\w$]*$/.test(childType) && !FRAMEWORK_TYPES.has(childType)) {
|
|
2939
|
+
ensureNamedImport(sf, childTypeModule, [
|
|
2395
2940
|
{ name: childType, isTypeOnly: true }
|
|
2396
2941
|
]);
|
|
2397
2942
|
}
|
|
@@ -2537,6 +3082,154 @@ function addComputedToModel(codegenFs, options, projects) {
|
|
|
2537
3082
|
});
|
|
2538
3083
|
return { modelFilePath };
|
|
2539
3084
|
}
|
|
3085
|
+
function mocksFilePathFor(modelFilePath, modelBase) {
|
|
3086
|
+
return path__namespace.join(
|
|
3087
|
+
path__namespace.dirname(modelFilePath),
|
|
3088
|
+
"mocks",
|
|
3089
|
+
`${modelBase}-mocks.ts`
|
|
3090
|
+
);
|
|
3091
|
+
}
|
|
3092
|
+
function mockRegisterFunctionName(modelBase) {
|
|
3093
|
+
return `register${pascalCase(modelBase)}Mocks`;
|
|
3094
|
+
}
|
|
3095
|
+
function toMockRoutePattern(servicePath) {
|
|
3096
|
+
return servicePath.replace(/\{([^}]+)\}/g, ":$1");
|
|
3097
|
+
}
|
|
3098
|
+
function mockRouteCall(method, pattern, body) {
|
|
3099
|
+
const shorthand = {
|
|
3100
|
+
get: "get",
|
|
3101
|
+
post: "post",
|
|
3102
|
+
put: "put",
|
|
3103
|
+
delete: "del"
|
|
3104
|
+
};
|
|
3105
|
+
const fn = shorthand[method];
|
|
3106
|
+
return fn ? `KosMock.${fn}(${JSON.stringify(pattern)}, ${body});` : `KosMock.route(${JSON.stringify(method.toUpperCase())}, ${JSON.stringify(
|
|
3107
|
+
pattern
|
|
3108
|
+
)}, ${body});`;
|
|
3109
|
+
}
|
|
3110
|
+
function header(modelBase, registerFn) {
|
|
3111
|
+
return `/**
|
|
3112
|
+
* KosMock routes for the ${modelBase} model's PROVISIONAL endpoints — the ones
|
|
3113
|
+
* this project's generated OpenAPI types do not declare.
|
|
3114
|
+
*
|
|
3115
|
+
* NOTHING IMPORTS THIS FILE. Call \`${registerFn}()\` from the app's dev entry,
|
|
3116
|
+
* gated so it cannot run in a production build (\`import.meta.env.DEV\`, a
|
|
3117
|
+
* dev-only entry module, or behind your own switch). Registering a route enables
|
|
3118
|
+
* KosMock, and a mock that shipped would shadow the real endpoint once it exists.
|
|
3119
|
+
*
|
|
3120
|
+
* Unmatched requests still pass through to the real device (KosMock's hybrid
|
|
3121
|
+
* default), so these routes shadow only the paths named below. Every mocked
|
|
3122
|
+
* response carries the \`kos-mocked: true\` wire header, so mock-fed data is
|
|
3123
|
+
* identifiable in devtools.
|
|
3124
|
+
*
|
|
3125
|
+
* DELETE THIS FILE once every endpoint below is in the generated types.
|
|
3126
|
+
*/
|
|
3127
|
+
`;
|
|
3128
|
+
}
|
|
3129
|
+
function ensureMockRoute(codegenFs, options) {
|
|
3130
|
+
const mocksFilePath = mocksFilePathFor(
|
|
3131
|
+
options.modelFilePath,
|
|
3132
|
+
options.modelBase
|
|
3133
|
+
);
|
|
3134
|
+
const registerFunction = mockRegisterFunctionName(options.modelBase);
|
|
3135
|
+
const routePattern = toMockRoutePattern(options.servicePath);
|
|
3136
|
+
const sampleName = `SAMPLE_${constantCase(dashCase(options.endpointKey))}`;
|
|
3137
|
+
const samplePlaceholder = !options.sampleText;
|
|
3138
|
+
if (!codegenFs.exists(mocksFilePath)) {
|
|
3139
|
+
codegenFs.write(mocksFilePath, header(options.modelBase, registerFunction));
|
|
3140
|
+
}
|
|
3141
|
+
const sdkSpecifier = options.sdkModuleSpecifier.startsWith(".") ? `../${options.sdkModuleSpecifier}` : options.sdkModuleSpecifier;
|
|
3142
|
+
let routeCreated = false;
|
|
3143
|
+
transformSourceFile(codegenFs, mocksFilePath, (sf) => {
|
|
3144
|
+
ensureNamedImport(sf, sdkSpecifier, [{ name: "KosMock" }]);
|
|
3145
|
+
ensureNamedImport(sf, "../services", [
|
|
3146
|
+
{ name: options.rawAlias, isTypeOnly: true }
|
|
3147
|
+
]);
|
|
3148
|
+
ensureSample(sf, {
|
|
3149
|
+
name: sampleName,
|
|
3150
|
+
type: options.rawAlias,
|
|
3151
|
+
servicePath: options.servicePath,
|
|
3152
|
+
method: options.method,
|
|
3153
|
+
sampleText: options.sampleText
|
|
3154
|
+
});
|
|
3155
|
+
const statement = mockRouteCall(
|
|
3156
|
+
options.method,
|
|
3157
|
+
routePattern,
|
|
3158
|
+
`{ data: ${sampleName} }`
|
|
3159
|
+
);
|
|
3160
|
+
routeCreated = ensureRegisteredRoute(sf, {
|
|
3161
|
+
registerFunction,
|
|
3162
|
+
modelBase: options.modelBase,
|
|
3163
|
+
statement,
|
|
3164
|
+
// Matching on the pattern literal alone would collide across methods.
|
|
3165
|
+
marker: `${options.method.toUpperCase()} ${routePattern}`,
|
|
3166
|
+
matches: (existing) => existing.includes(JSON.stringify(routePattern)) && existing.includes(shorthandOrMethod(options.method))
|
|
3167
|
+
});
|
|
3168
|
+
});
|
|
3169
|
+
return {
|
|
3170
|
+
mocksFilePath,
|
|
3171
|
+
registerFunction,
|
|
3172
|
+
routePattern,
|
|
3173
|
+
sampleName,
|
|
3174
|
+
routeCreated,
|
|
3175
|
+
samplePlaceholder
|
|
3176
|
+
};
|
|
3177
|
+
}
|
|
3178
|
+
function shorthandOrMethod(method) {
|
|
3179
|
+
const shorthand = {
|
|
3180
|
+
get: "KosMock.get(",
|
|
3181
|
+
post: "KosMock.post(",
|
|
3182
|
+
put: "KosMock.put(",
|
|
3183
|
+
delete: "KosMock.del("
|
|
3184
|
+
};
|
|
3185
|
+
return shorthand[method] ?? `"${method.toUpperCase()}"`;
|
|
3186
|
+
}
|
|
3187
|
+
function ensureSample(sf, spec) {
|
|
3188
|
+
if (sf.getVariableDeclaration(spec.name)) return;
|
|
3189
|
+
const lines = [
|
|
3190
|
+
`Sample payload for \`${spec.method.toUpperCase()} ${spec.servicePath}\` — the`,
|
|
3191
|
+
"UNWRAPPED `data` payload, i.e. exactly what the transform receives."
|
|
3192
|
+
];
|
|
3193
|
+
if (!spec.sampleText) {
|
|
3194
|
+
lines.push(
|
|
3195
|
+
"",
|
|
3196
|
+
"TODO: replace the placeholder with something the backend would actually",
|
|
3197
|
+
"serve. Until then the route answers with nothing and the transform is",
|
|
3198
|
+
"never exercised."
|
|
3199
|
+
);
|
|
3200
|
+
}
|
|
3201
|
+
const docs = lines.join("\n");
|
|
3202
|
+
sf.addVariableStatement({
|
|
3203
|
+
isExported: true,
|
|
3204
|
+
declarationKind: tsMorph.VariableDeclarationKind.Const,
|
|
3205
|
+
docs: [docs],
|
|
3206
|
+
declarations: [
|
|
3207
|
+
{
|
|
3208
|
+
name: spec.name,
|
|
3209
|
+
type: spec.type,
|
|
3210
|
+
initializer: spec.sampleText ?? `null as unknown as ${spec.type}`
|
|
3211
|
+
}
|
|
3212
|
+
]
|
|
3213
|
+
});
|
|
3214
|
+
}
|
|
3215
|
+
function ensureRegisteredRoute(sf, spec) {
|
|
3216
|
+
let fn = sf.getFunction(spec.registerFunction);
|
|
3217
|
+
if (!fn) {
|
|
3218
|
+
fn = sf.addFunction({
|
|
3219
|
+
name: spec.registerFunction,
|
|
3220
|
+
isExported: true,
|
|
3221
|
+
returnType: "void",
|
|
3222
|
+
docs: [
|
|
3223
|
+
`Arm the ${spec.modelBase} model's provisional endpoints. Call this from a dev-only entry point — never from code that ships.`
|
|
3224
|
+
]
|
|
3225
|
+
});
|
|
3226
|
+
}
|
|
3227
|
+
const already = fn.getStatements().some((statement) => spec.matches(statement.getText()));
|
|
3228
|
+
if (already) return false;
|
|
3229
|
+
fn.addStatements(`// ${spec.marker}
|
|
3230
|
+
${spec.statement}`);
|
|
3231
|
+
return true;
|
|
3232
|
+
}
|
|
2540
3233
|
const SERVICE_MODULE_RE = /\/utils\/services\/.*\/service\.ts$/;
|
|
2541
3234
|
function findServiceModules(codegenFs, sourceRoot) {
|
|
2542
3235
|
return codegenFs.listFiles(sourceRoot).filter((f) => SERVICE_MODULE_RE.test(f.split(path__namespace.sep).join("/")));
|
|
@@ -2561,10 +3254,23 @@ function assertServiceModuleAcceptsTransform(codegenFs, serviceModuleFile, model
|
|
|
2561
3254
|
`${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.`
|
|
2562
3255
|
);
|
|
2563
3256
|
}
|
|
3257
|
+
function assertServiceModuleAcceptsProvisional(codegenFs, serviceModuleFile, modelProject) {
|
|
3258
|
+
const file = `${serviceModuleFile}.ts`;
|
|
3259
|
+
if (!codegenFs.exists(file)) return;
|
|
3260
|
+
const declared = readSourceFile(
|
|
3261
|
+
codegenFs,
|
|
3262
|
+
file,
|
|
3263
|
+
(sf) => Boolean(sf.getFunction("provisionalServiceRequest"))
|
|
3264
|
+
);
|
|
3265
|
+
if (declared) return;
|
|
3266
|
+
throw new Error(
|
|
3267
|
+
`${file} has no \`provisionalServiceRequest\` — the helper layer predates provisional endpoints. Run \`kosui api:generate --project ${modelProject} --helpers-only\` to refresh it in place (no spec fetch, openapi.d.ts untouched), then add the service request.`
|
|
3268
|
+
);
|
|
3269
|
+
}
|
|
2564
3270
|
function sameEndpoint(existing, candidate) {
|
|
2565
3271
|
const args = (text) => {
|
|
2566
3272
|
const match = text.match(
|
|
2567
|
-
/^endpoint\s*\(\s*(['"])(.*?)\1\s*,\s*(['"])(.*?)\3\s*,?\s*\)$/
|
|
3273
|
+
/^endpoint\s*\(\s*(['"])(.*?)\1(?:\s+as\s+ApiPath)?\s*,\s*(['"])(.*?)\3\s*,?\s*\)$/
|
|
2568
3274
|
);
|
|
2569
3275
|
return match ? [match[2], match[4].toLowerCase()] : null;
|
|
2570
3276
|
};
|
|
@@ -2572,6 +3278,104 @@ function sameEndpoint(existing, candidate) {
|
|
|
2572
3278
|
const b = args(candidate.trim());
|
|
2573
3279
|
return !!a && !!b && a[0] === b[0] && a[1] === b[1];
|
|
2574
3280
|
}
|
|
3281
|
+
function editDistance(a, b) {
|
|
3282
|
+
let previous = Array.from({ length: b.length + 1 }, (_, i) => i);
|
|
3283
|
+
for (let i = 1; i <= a.length; i++) {
|
|
3284
|
+
const current = [i];
|
|
3285
|
+
for (let j = 1; j <= b.length; j++) {
|
|
3286
|
+
current[j] = Math.min(
|
|
3287
|
+
previous[j] + 1,
|
|
3288
|
+
current[j - 1] + 1,
|
|
3289
|
+
previous[j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1)
|
|
3290
|
+
);
|
|
3291
|
+
}
|
|
3292
|
+
previous = current;
|
|
3293
|
+
}
|
|
3294
|
+
return previous[b.length];
|
|
3295
|
+
}
|
|
3296
|
+
const NEAREST_SHOWN = 5;
|
|
3297
|
+
function provisionalEntryComment(servicePath, method, catalogName) {
|
|
3298
|
+
return `/**
|
|
3299
|
+
* PROVISIONAL — \`${method.toUpperCase()} ${servicePath}\` is NOT in this project's
|
|
3300
|
+
* generated OpenAPI types. \`as ApiPath\` is standing in for a \`paths\` entry that
|
|
3301
|
+
* does not exist, so nothing about this endpoint is checked against a spec and
|
|
3302
|
+
* its response cannot be derived from one. Do not read this as a real endpoint.
|
|
3303
|
+
*
|
|
3304
|
+
* WHEN THE ENDPOINT LANDS:
|
|
3305
|
+
* 1. regenerate this project's API types (\`kosui api:generate\`)
|
|
3306
|
+
* 2. delete \` as ApiPath\` from the entry below
|
|
3307
|
+
* 3. point the endpoint's \`…Raw\` alias at
|
|
3308
|
+
* \`EndpointResponse<typeof ${catalogName}.…>\`
|
|
3309
|
+
* 4. swap \`provisionalServiceRequest\` for \`serviceRequest\` in the model
|
|
3310
|
+
* 5. delete the model's \`mocks/\` module
|
|
3311
|
+
* 6. KEEP the transform — it is real code and survives all of the above
|
|
3312
|
+
*/
|
|
3313
|
+
`;
|
|
3314
|
+
}
|
|
3315
|
+
function provisionalMethodDocs(servicePath, method) {
|
|
3316
|
+
return [
|
|
3317
|
+
`PROVISIONAL — \`${method.toUpperCase()} ${servicePath}\` is not in the`,
|
|
3318
|
+
"generated OpenAPI types, so this request is served by a KosMock route, not",
|
|
3319
|
+
"by the device. The feature it backs is NOT complete: report the missing",
|
|
3320
|
+
"endpoint rather than treating this as finished.",
|
|
3321
|
+
"",
|
|
3322
|
+
"See the endpoint's entry in `./services` for what to undo when it lands."
|
|
3323
|
+
].join("\n");
|
|
3324
|
+
}
|
|
3325
|
+
function rawTypeDocs(servicePath, method, rawType) {
|
|
3326
|
+
const shape = rawType ? [
|
|
3327
|
+
"Stated by hand and unverified: there is no spec entry to check it",
|
|
3328
|
+
"against."
|
|
3329
|
+
] : [
|
|
3330
|
+
"TODO: state the shape the backend is expected to serve. `unknown`",
|
|
3331
|
+
"compiles, but leaves the response boundary undescribed and the",
|
|
3332
|
+
"transform with nothing to narrow."
|
|
3333
|
+
];
|
|
3334
|
+
return [
|
|
3335
|
+
`Raw wire shape of \`${method.toUpperCase()} ${servicePath}\` — the UNWRAPPED`,
|
|
3336
|
+
"`data` payload, since the client strips the `{status, data}` envelope",
|
|
3337
|
+
"before the transform runs.",
|
|
3338
|
+
"",
|
|
3339
|
+
...shape,
|
|
3340
|
+
"",
|
|
3341
|
+
"Replace with `EndpointResponse<…>` once the endpoint is in the generated",
|
|
3342
|
+
"types."
|
|
3343
|
+
].join("\n");
|
|
3344
|
+
}
|
|
3345
|
+
function removalSteps(spec) {
|
|
3346
|
+
return [
|
|
3347
|
+
`Regenerate the API types for ${spec.modelProject} (generate_api_types), and confirm ${spec.method.toUpperCase()} ${spec.servicePath} is now in them.`,
|
|
3348
|
+
`In ${spec.servicesFilePath}: delete \` as ApiPath\` from \`${spec.catalogName}.${spec.endpointKey}\`.`,
|
|
3349
|
+
`In ${spec.servicesFilePath}: point the endpoint's \`…Raw\` alias at \`EndpointResponse<typeof ${spec.catalogName}.${spec.endpointKey}>\` and drop the hand-written shape.`,
|
|
3350
|
+
`In the model: swap \`provisionalServiceRequest\` for \`serviceRequest\` and drop its explicit type arguments — the response type comes from the spec again.`,
|
|
3351
|
+
spec.mocksFilePath ? `Delete ${spec.mocksFilePath} (and the call to its register function in the app) once no provisional endpoint is left in it.` : `Delete the model's mocks module once no provisional endpoint is left in it.`,
|
|
3352
|
+
`KEEP \`${spec.mapperName}\` and the data type it produces. The transform is real code; it is what let the model read the final shape all along.`
|
|
3353
|
+
];
|
|
3354
|
+
}
|
|
3355
|
+
function untypedEndpointError(spec) {
|
|
3356
|
+
const methodsForPath = spec.operations.filter((op) => op.path === spec.servicePath).map((op) => op.method);
|
|
3357
|
+
const pathTypedForOtherMethods = methodsForPath.length > 0;
|
|
3358
|
+
const nearest = [...new Set(spec.operations.map((op) => op.path))].sort(
|
|
3359
|
+
(a, b) => editDistance(a, spec.servicePath) - editDistance(b, spec.servicePath)
|
|
3360
|
+
).slice(0, NEAREST_SHOWN);
|
|
3361
|
+
const waysForward = [
|
|
3362
|
+
`The endpoint EXISTS on the device but this project has not pulled types for it: re-run api:generate (generate_api_types) for ${spec.modelProject}, then add the request unchanged. Confirm on the device first — kos-device search_services / describe_endpoint — since the live OpenAPI is what says whether it exists.`,
|
|
3363
|
+
`The endpoint DOES NOT EXIST yet: pass mock:"auto" (the default) or mock:"always" to emit the provisional, mock-backed form — real decorator, lifecycle, error-first handler and transform, with a KosMock route standing in for the wire response.`,
|
|
3364
|
+
`What is NOT supported: falling back to resolveServiceUrl / ServiceFactory.build / getAll to make the UI render. That compiles and looks finished, which is worse than the hole. A missing endpoint is a blocker to report, not a licence to route around the generated service layer.`
|
|
3365
|
+
];
|
|
3366
|
+
const message = pathTypedForOtherMethods ? `${spec.method.toUpperCase()} ${spec.servicePath} is not in the generated OpenAPI types for ${spec.modelProject} — the path is typed, but only for: ${methodsForPath.map((m) => m.toUpperCase()).join(", ")}.` : `${spec.method.toUpperCase()} ${spec.servicePath} is not in the generated OpenAPI types for ${spec.modelProject} — the path is absent entirely. Nothing was written.`;
|
|
3367
|
+
const error = new Error(message);
|
|
3368
|
+
error.details = {
|
|
3369
|
+
servicePath: spec.servicePath,
|
|
3370
|
+
method: spec.method,
|
|
3371
|
+
pathTypedForOtherMethods,
|
|
3372
|
+
methodsForPath,
|
|
3373
|
+
otherEndpointsInThisApi: nearest,
|
|
3374
|
+
warning: "These are OTHER endpoints in this API, listed only so they can be ruled out — they are almost certainly NOT what you want. Do not substitute one for the requested path because the strings look similar.",
|
|
3375
|
+
waysForward
|
|
3376
|
+
};
|
|
3377
|
+
return error;
|
|
3378
|
+
}
|
|
2575
3379
|
function addServiceRequestToModel(codegenFs, options, projects) {
|
|
2576
3380
|
const logger = getCodegenLogger();
|
|
2577
3381
|
const { modelFilePath, sourceRoot } = resolveModelFilePath(
|
|
@@ -2633,34 +3437,86 @@ function addServiceRequestToModel(codegenFs, options, projects) {
|
|
|
2633
3437
|
const mode = options.mode ?? (options.lifecycle ? "lifecycle" : "method");
|
|
2634
3438
|
const lifecycle = options.lifecycle || "LOAD";
|
|
2635
3439
|
const catalogName = `${pascalCase(modelBase)}Endpoints`;
|
|
3440
|
+
const mockMode = options.mock ?? "auto";
|
|
3441
|
+
const operations = readServiceModuleOperations(codegenFs, serviceModuleFile);
|
|
3442
|
+
const pathValidated = operations !== null;
|
|
3443
|
+
const operationTyped = operations === null || operations.some(
|
|
3444
|
+
(op) => op.path === options.servicePath && op.method === method
|
|
3445
|
+
);
|
|
3446
|
+
if (!operationTyped && mockMode === "never") {
|
|
3447
|
+
throw untypedEndpointError({
|
|
3448
|
+
servicePath: options.servicePath,
|
|
3449
|
+
method,
|
|
3450
|
+
modelProject: options.modelProject,
|
|
3451
|
+
operations: operations ?? []
|
|
3452
|
+
});
|
|
3453
|
+
}
|
|
3454
|
+
const provisional = !operationTyped;
|
|
3455
|
+
const writeMock = provisional || mockMode === "always";
|
|
3456
|
+
if (provisional) {
|
|
3457
|
+
assertServiceModuleAcceptsProvisional(
|
|
3458
|
+
codegenFs,
|
|
3459
|
+
serviceModuleFile,
|
|
3460
|
+
options.modelProject
|
|
3461
|
+
);
|
|
3462
|
+
}
|
|
2636
3463
|
logger.info(
|
|
2637
|
-
`Adding ${mode}-driven @kosServiceRequest "${options.methodName}" (${method} ${options.servicePath}) to ${options.modelName}`
|
|
3464
|
+
`Adding ${mode}-driven ${provisional ? "PROVISIONAL " : ""}@kosServiceRequest "${options.methodName}" (${method} ${options.servicePath}) to ${options.modelName}`
|
|
2638
3465
|
);
|
|
3466
|
+
if (provisional) {
|
|
3467
|
+
logger.warn(
|
|
3468
|
+
`${method.toUpperCase()} ${options.servicePath} is not in ${options.modelProject}'s generated OpenAPI types — emitting the provisional, mock-backed form. The feature is NOT complete until the endpoint lands.`
|
|
3469
|
+
);
|
|
3470
|
+
}
|
|
2639
3471
|
ensureServicesModule(codegenFs, modelFilePath, modelBase);
|
|
2640
3472
|
let endpointKey = options.methodName;
|
|
2641
3473
|
let endpointCreated = true;
|
|
2642
3474
|
let dataAlias = "";
|
|
2643
3475
|
let mapperName = "";
|
|
2644
3476
|
let ctxAlias = "";
|
|
3477
|
+
let rawAlias = "";
|
|
2645
3478
|
transformSourceFile(codegenFs, servicesFilePath, (sf) => {
|
|
2646
3479
|
ensureNamedImport(sf, serviceImportFromServices, [{ name: "endpoint" }]);
|
|
3480
|
+
if (provisional) {
|
|
3481
|
+
ensureNamedImport(sf, serviceImportFromServices, [
|
|
3482
|
+
{ name: "ApiPath", isTypeOnly: true }
|
|
3483
|
+
]);
|
|
3484
|
+
}
|
|
3485
|
+
const pathText = provisional ? `${JSON.stringify(options.servicePath)} as ApiPath` : JSON.stringify(options.servicePath);
|
|
2647
3486
|
const entry = ensureConstCatalogEntry(sf, {
|
|
2648
3487
|
catalogName,
|
|
2649
3488
|
key: options.methodName,
|
|
2650
|
-
initializer: `endpoint(${JSON.stringify(
|
|
2651
|
-
|
|
2652
|
-
|
|
2653
|
-
equals: sameEndpoint
|
|
3489
|
+
initializer: `endpoint(${pathText}, ${JSON.stringify(method)})`,
|
|
3490
|
+
equals: sameEndpoint,
|
|
3491
|
+
leadingTrivia: provisional ? provisionalEntryComment(options.servicePath, method, catalogName) : void 0
|
|
2654
3492
|
});
|
|
2655
3493
|
endpointKey = entry.key;
|
|
2656
3494
|
endpointCreated = entry.created;
|
|
2657
3495
|
dataAlias = `${pascalCase(endpointKey)}Data`;
|
|
2658
3496
|
mapperName = `to${pascalCase(endpointKey)}Data`;
|
|
2659
3497
|
ctxAlias = `${pascalCase(endpointKey)}Ctx`;
|
|
2660
|
-
|
|
2661
|
-
|
|
2662
|
-
|
|
2663
|
-
|
|
3498
|
+
rawAlias = `${pascalCase(endpointKey)}Raw`;
|
|
3499
|
+
let rawType;
|
|
3500
|
+
if (provisional) {
|
|
3501
|
+
ensureExportedTypeAlias(sf, {
|
|
3502
|
+
name: rawAlias,
|
|
3503
|
+
type: options.rawType || "unknown",
|
|
3504
|
+
docs: rawTypeDocs(options.servicePath, method, options.rawType)
|
|
3505
|
+
});
|
|
3506
|
+
rawType = rawAlias;
|
|
3507
|
+
} else {
|
|
3508
|
+
rawType = `EndpointResponse<typeof ${catalogName}.${endpointKey}>`;
|
|
3509
|
+
ensureNamedImport(sf, serviceImportFromServices, [
|
|
3510
|
+
{ name: "EndpointResponse", isTypeOnly: true }
|
|
3511
|
+
]);
|
|
3512
|
+
if (writeMock) {
|
|
3513
|
+
ensureExportedTypeAlias(sf, {
|
|
3514
|
+
name: rawAlias,
|
|
3515
|
+
type: rawType,
|
|
3516
|
+
docs: `Raw wire shape of \`${method.toUpperCase()} ${options.servicePath}\`, as the spec declares it — what a mock for this endpoint must serve.`
|
|
3517
|
+
});
|
|
3518
|
+
}
|
|
3519
|
+
}
|
|
2664
3520
|
ensureExportedTypeAlias(sf, { name: dataAlias, type: rawType });
|
|
2665
3521
|
ensureExportedMapper(sf, {
|
|
2666
3522
|
name: mapperName,
|
|
@@ -2681,20 +3537,25 @@ function addServiceRequestToModel(codegenFs, options, projects) {
|
|
|
2681
3537
|
const className = getModelClass(sf).getName();
|
|
2682
3538
|
if (!className) throw new Error("Model class has no name.");
|
|
2683
3539
|
const typeParams = getModelClass(sf).getTypeParameters().map((tp) => tp.getText());
|
|
2684
|
-
|
|
3540
|
+
const decoratorName = provisional ? "provisionalServiceRequest" : "serviceRequest";
|
|
3541
|
+
const decoratorTypeArgs = provisional ? [rawAlias, dataAlias] : void 0;
|
|
3542
|
+
ensureNamedImport(sf, serviceImportFromModel, [{ name: decoratorName }]);
|
|
2685
3543
|
if (mode === "lifecycle") {
|
|
2686
3544
|
ensureNamedImport(sf, "./services", [
|
|
2687
3545
|
{ name: catalogName },
|
|
2688
3546
|
{ name: mapperName },
|
|
2689
|
-
{ name: dataAlias, isTypeOnly: true }
|
|
3547
|
+
{ name: dataAlias, isTypeOnly: true },
|
|
3548
|
+
...provisional ? [{ name: rawAlias, isTypeOnly: true }] : []
|
|
2690
3549
|
]);
|
|
2691
3550
|
ensureNamedImport(sf, resolveSdkModuleSpecifier(sf), [
|
|
2692
3551
|
{ name: "DependencyLifecycle" }
|
|
2693
3552
|
]);
|
|
2694
3553
|
addDecoratedMethod(getModelClass(sf), {
|
|
2695
3554
|
name: options.methodName,
|
|
2696
|
-
decoratorName
|
|
3555
|
+
decoratorName,
|
|
3556
|
+
decoratorTypeArgs,
|
|
2697
3557
|
decoratorArgsText: `${catalogName}.${endpointKey}, { lifecycle: DependencyLifecycle.${lifecycle}, transform: ${mapperName} }`,
|
|
3558
|
+
docs: provisional ? provisionalMethodDocs(options.servicePath, method) : void 0,
|
|
2698
3559
|
// The manager invokes phase handlers error-first, passing (null, data)
|
|
2699
3560
|
// on success. The `error` half is only ever reached because
|
|
2700
3561
|
// `serviceRequest` supplies an errorHandler — the bare decorator
|
|
@@ -2711,7 +3572,11 @@ function addServiceRequestToModel(codegenFs, options, projects) {
|
|
|
2711
3572
|
ensureNamedImport(sf, "./services", [
|
|
2712
3573
|
{ name: catalogName },
|
|
2713
3574
|
{ name: mapperName },
|
|
2714
|
-
{ name: ctxAlias, isTypeOnly: true }
|
|
3575
|
+
{ name: ctxAlias, isTypeOnly: true },
|
|
3576
|
+
...provisional ? [
|
|
3577
|
+
{ name: rawAlias, isTypeOnly: true },
|
|
3578
|
+
{ name: dataAlias, isTypeOnly: true }
|
|
3579
|
+
] : []
|
|
2715
3580
|
]);
|
|
2716
3581
|
ensureNamedImport(sf, resolveSdkModuleSpecifier(sf), [
|
|
2717
3582
|
{ name: "executeServiceRequest" },
|
|
@@ -2726,8 +3591,10 @@ function addServiceRequestToModel(codegenFs, options, projects) {
|
|
|
2726
3591
|
addClassDecorator(getModelClass(sf), "kosLoggerAware", { argsText: "" });
|
|
2727
3592
|
addDecoratedMethod(getModelClass(sf), {
|
|
2728
3593
|
name: options.methodName,
|
|
2729
|
-
decoratorName
|
|
3594
|
+
decoratorName,
|
|
3595
|
+
decoratorTypeArgs,
|
|
2730
3596
|
decoratorArgsText: `${catalogName}.${endpointKey}, { transform: ${mapperName} }`,
|
|
3597
|
+
docs: provisional ? provisionalMethodDocs(options.servicePath, method) : void 0,
|
|
2731
3598
|
// Optional: the framework appends the context, callers never pass it.
|
|
2732
3599
|
parameters: [{ name: "$ctx", type: ctxAlias, optional: true }],
|
|
2733
3600
|
isAsync: true,
|
|
@@ -2739,172 +3606,43 @@ function addServiceRequestToModel(codegenFs, options, projects) {
|
|
|
2739
3606
|
].join("\n")
|
|
2740
3607
|
});
|
|
2741
3608
|
});
|
|
3609
|
+
const mock = writeMock ? ensureMockRoute(codegenFs, {
|
|
3610
|
+
modelFilePath,
|
|
3611
|
+
modelBase,
|
|
3612
|
+
sdkModuleSpecifier: readSourceFile(
|
|
3613
|
+
codegenFs,
|
|
3614
|
+
modelFilePath,
|
|
3615
|
+
resolveSdkModuleSpecifier
|
|
3616
|
+
),
|
|
3617
|
+
rawAlias,
|
|
3618
|
+
servicePath: options.servicePath,
|
|
3619
|
+
method,
|
|
3620
|
+
endpointKey,
|
|
3621
|
+
sampleText: options.sample
|
|
3622
|
+
}) : void 0;
|
|
2742
3623
|
return {
|
|
2743
3624
|
modelFilePath,
|
|
2744
3625
|
servicesFilePath,
|
|
2745
3626
|
serviceModule: serviceImportFromModel,
|
|
2746
3627
|
mode,
|
|
2747
3628
|
endpointKey,
|
|
2748
|
-
endpointCreated
|
|
2749
|
-
|
|
2750
|
-
|
|
2751
|
-
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
|
|
2758
|
-
|
|
2759
|
-
|
|
2760
|
-
|
|
2761
|
-
|
|
2762
|
-
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
const project = new tsMorph.Project({ useInMemoryFileSystem: true });
|
|
2766
|
-
const sf = project.createSourceFile(modelFilePath, content, {
|
|
2767
|
-
overwrite: true
|
|
2768
|
-
});
|
|
2769
|
-
const findings = [];
|
|
2770
|
-
const hasKosModel = sf.getClasses().some((c) => c.getDecorator("kosModel"));
|
|
2771
|
-
if (!hasKosModel) {
|
|
2772
|
-
findings.push({
|
|
2773
|
-
level: "error",
|
|
2774
|
-
rule: "missing-kosModel",
|
|
2775
|
-
message: "No @kosModel decorator found — this is not a KOS model."
|
|
2776
|
-
});
|
|
2777
|
-
}
|
|
2778
|
-
const imports = sf.getImportDeclarations();
|
|
2779
|
-
const mobxImport = imports.find(
|
|
2780
|
-
(d) => /^mobx(-react-lite)?$/.test(d.getModuleSpecifierValue())
|
|
2781
|
-
);
|
|
2782
|
-
if (mobxImport) {
|
|
2783
|
-
findings.push({
|
|
2784
|
-
level: "error",
|
|
2785
|
-
rule: "mobx-import",
|
|
2786
|
-
message: "Imports mobx directly. Reactivity is internal to KOS — remove the mobx import and use KOS reactivity / container models."
|
|
2787
|
-
});
|
|
2788
|
-
}
|
|
2789
|
-
const barrelServiceRequest = imports.find(
|
|
2790
|
-
(d) => d.getModuleSpecifierValue() === "@kosdev-code/kos-ui-sdk" && d.getNamedImports().some((n) => n.getName() === "kosServiceRequest")
|
|
2791
|
-
);
|
|
2792
|
-
if (barrelServiceRequest) {
|
|
2793
|
-
findings.push({
|
|
2794
|
-
level: "warning",
|
|
2795
|
-
rule: "untyped-service-request",
|
|
2796
|
-
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."
|
|
2797
|
-
});
|
|
2798
|
-
}
|
|
2799
|
-
for (const cls of sf.getClasses()) {
|
|
2800
|
-
for (const prop of cls.getProperties()) {
|
|
2801
|
-
const name = prop.getName();
|
|
2802
|
-
const typeText = (prop.getTypeNode()?.getText() ?? "").replace(/\s+/g, " ").trim();
|
|
2803
|
-
const initText = prop.getInitializer()?.getText() ?? "";
|
|
2804
|
-
const hasChild = !!prop.getDecorator("kosChild");
|
|
2805
|
-
const isModelContainer = /\bI?KosModelContainer\s*</.test(typeText) || /new\s+KosModelContainer\b/.test(initText);
|
|
2806
|
-
if (isModelContainer && !hasChild) {
|
|
2807
|
-
findings.push({
|
|
2808
|
-
level: "warning",
|
|
2809
|
-
rule: "container-missing-kosChild",
|
|
2810
|
-
message: `Property '${name}' is a model container but is not marked @kosChild — add @kosChild so its models join the model graph and lifecycle.`
|
|
2811
|
-
});
|
|
2812
|
-
continue;
|
|
2813
|
-
}
|
|
2814
|
-
const elem = modelElementType(typeText);
|
|
2815
|
-
if (elem && !isModelContainer) {
|
|
2816
|
-
findings.push({
|
|
2817
|
-
level: "warning",
|
|
2818
|
-
rule: "raw-model-collection",
|
|
2819
|
-
message: `Property '${name}' is a raw ${typeText} of models — prefer a KosModelContainer<${elem}> (indexing, sorting, lifecycle, delta handling) marked @kosChild.`
|
|
2820
|
-
});
|
|
2821
|
-
}
|
|
2822
|
-
}
|
|
2823
|
-
}
|
|
2824
|
-
const hasError = findings.some((f) => f.level === "error");
|
|
2825
|
-
return { modelFilePath, ok: !hasError, findings };
|
|
2826
|
-
}
|
|
2827
|
-
function firstDecoratorArg(decoratorText) {
|
|
2828
|
-
const open = decoratorText.indexOf("(");
|
|
2829
|
-
if (open === -1) return void 0;
|
|
2830
|
-
const inner = decoratorText.slice(open + 1, decoratorText.lastIndexOf(")")).replace(/\s+/g, " ").trim();
|
|
2831
|
-
return inner || void 0;
|
|
2832
|
-
}
|
|
2833
|
-
function collectDecorated(cls, decoratorName) {
|
|
2834
|
-
const members = [];
|
|
2835
|
-
const visit = (name, decoratorTextOf, typeText) => {
|
|
2836
|
-
const text = decoratorTextOf();
|
|
2837
|
-
if (text === void 0) return;
|
|
2838
|
-
members.push({
|
|
2839
|
-
name,
|
|
2840
|
-
arg: firstDecoratorArg(text),
|
|
2841
|
-
type: typeText || void 0
|
|
2842
|
-
});
|
|
2843
|
-
};
|
|
2844
|
-
for (const m of cls.getMethods()) {
|
|
2845
|
-
const dec = m.getDecorator(decoratorName);
|
|
2846
|
-
if (dec) {
|
|
2847
|
-
visit(m.getName(), () => dec.getText(), m.getReturnTypeNode()?.getText());
|
|
2848
|
-
}
|
|
2849
|
-
}
|
|
2850
|
-
for (const p of cls.getProperties()) {
|
|
2851
|
-
const dec = p.getDecorator(decoratorName);
|
|
2852
|
-
if (dec) {
|
|
2853
|
-
visit(p.getName(), () => dec.getText(), p.getTypeNode()?.getText());
|
|
2854
|
-
}
|
|
2855
|
-
}
|
|
2856
|
-
return members;
|
|
2857
|
-
}
|
|
2858
|
-
function describeModel(codegenFs, options, projects) {
|
|
2859
|
-
const { modelFilePath } = resolveModelFilePath(codegenFs, options, projects);
|
|
2860
|
-
const content = codegenFs.read(modelFilePath);
|
|
2861
|
-
if (content === null) {
|
|
2862
|
-
throw new Error(`Model file not found: ${modelFilePath}`);
|
|
2863
|
-
}
|
|
2864
|
-
const project = new tsMorph.Project({ useInMemoryFileSystem: true });
|
|
2865
|
-
const sf = project.createSourceFile(modelFilePath, content, {
|
|
2866
|
-
overwrite: true
|
|
2867
|
-
});
|
|
2868
|
-
let modelType;
|
|
2869
|
-
const modelTypeDecl = sf.getVariableDeclaration("MODEL_TYPE");
|
|
2870
|
-
if (modelTypeDecl) {
|
|
2871
|
-
const init = modelTypeDecl.getInitializer()?.getText();
|
|
2872
|
-
if (init) modelType = init.replace(/^["'`]|["'`]$/g, "");
|
|
2873
|
-
}
|
|
2874
|
-
const cls = sf.getClasses().find((c) => c.getDecorator("kosModel")) ?? sf.getClasses().find((c) => c.isExported()) ?? sf.getClasses()[0];
|
|
2875
|
-
if (!cls) {
|
|
2876
|
-
return {
|
|
2877
|
-
modelFilePath,
|
|
2878
|
-
modelType,
|
|
2879
|
-
classDecorators: [],
|
|
2880
|
-
singleton: false,
|
|
2881
|
-
isCompanion: false,
|
|
2882
|
-
children: [],
|
|
2883
|
-
dependencies: [],
|
|
2884
|
-
topicHandlers: [],
|
|
2885
|
-
configProperties: [],
|
|
2886
|
-
serviceRequests: [],
|
|
2887
|
-
effects: [],
|
|
2888
|
-
futures: []
|
|
2889
|
-
};
|
|
2890
|
-
}
|
|
2891
|
-
const classDecorators = cls.getDecorators().map((d) => d.getName());
|
|
2892
|
-
const kosModelArg = cls.getDecorator("kosModel") ? firstDecoratorArg(cls.getDecorator("kosModel").getText()) : void 0;
|
|
2893
|
-
const singleton = /\bsingleton\s*:\s*true\b/.test(kosModelArg ?? "");
|
|
2894
|
-
return {
|
|
2895
|
-
modelFilePath,
|
|
2896
|
-
modelType,
|
|
2897
|
-
className: cls.getName(),
|
|
2898
|
-
classDecorators,
|
|
2899
|
-
singleton,
|
|
2900
|
-
isCompanion: classDecorators.includes("kosCompanion"),
|
|
2901
|
-
children: collectDecorated(cls, "kosChild"),
|
|
2902
|
-
dependencies: collectDecorated(cls, "kosDependency"),
|
|
2903
|
-
topicHandlers: collectDecorated(cls, "kosTopicHandler"),
|
|
2904
|
-
configProperties: collectDecorated(cls, "kosConfigProperty"),
|
|
2905
|
-
serviceRequests: collectDecorated(cls, "kosServiceRequest"),
|
|
2906
|
-
effects: collectDecorated(cls, "kosModelEffect"),
|
|
2907
|
-
futures: collectDecorated(cls, "kosFuture")
|
|
3629
|
+
endpointCreated,
|
|
3630
|
+
pathValidated,
|
|
3631
|
+
provisional,
|
|
3632
|
+
mocksFilePath: mock?.mocksFilePath,
|
|
3633
|
+
mockRegisterFunction: mock?.registerFunction,
|
|
3634
|
+
mockRoute: mock && `${method.toUpperCase()} ${mock.routePattern}`,
|
|
3635
|
+
mockSamplePlaceholder: mock?.samplePlaceholder,
|
|
3636
|
+
removalSteps: provisional ? removalSteps({
|
|
3637
|
+
modelProject: options.modelProject,
|
|
3638
|
+
servicePath: options.servicePath,
|
|
3639
|
+
method,
|
|
3640
|
+
catalogName,
|
|
3641
|
+
endpointKey,
|
|
3642
|
+
servicesFilePath,
|
|
3643
|
+
mocksFilePath: mock?.mocksFilePath,
|
|
3644
|
+
mapperName
|
|
3645
|
+
}) : void 0
|
|
2908
3646
|
};
|
|
2909
3647
|
}
|
|
2910
3648
|
const DEFAULT_SDK_PACKAGE = "@kosdev-code/kos-ui-sdk";
|
|
@@ -3090,6 +3828,82 @@ function lookupSdkType(codegenFs, options, projects) {
|
|
|
3090
3828
|
)}). Check the name, or it may be internal / not part of the public surface.`
|
|
3091
3829
|
};
|
|
3092
3830
|
}
|
|
3831
|
+
function modelElementType(typeText) {
|
|
3832
|
+
let m;
|
|
3833
|
+
if (m = typeText.match(/^([A-Za-z_]\w*Model)\s*\[\]$/)) return m[1];
|
|
3834
|
+
if (m = typeText.match(/^Array<\s*([A-Za-z_]\w*Model)\s*>$/)) return m[1];
|
|
3835
|
+
if (m = typeText.match(/^(?:Map|Set)<[^>]*?\b([A-Za-z_]\w*Model)\b[^>]*>$/))
|
|
3836
|
+
return m[1];
|
|
3837
|
+
return null;
|
|
3838
|
+
}
|
|
3839
|
+
function validateModel(codegenFs, options, projects) {
|
|
3840
|
+
const { modelFilePath } = resolveModelFilePath(codegenFs, options, projects);
|
|
3841
|
+
const content = codegenFs.read(modelFilePath);
|
|
3842
|
+
if (content === null) {
|
|
3843
|
+
throw new Error(`Model file not found: ${modelFilePath}`);
|
|
3844
|
+
}
|
|
3845
|
+
const project = new tsMorph.Project({ useInMemoryFileSystem: true });
|
|
3846
|
+
const sf = project.createSourceFile(modelFilePath, content, {
|
|
3847
|
+
overwrite: true
|
|
3848
|
+
});
|
|
3849
|
+
const findings = [];
|
|
3850
|
+
const hasKosModel = sf.getClasses().some((c) => c.getDecorator("kosModel"));
|
|
3851
|
+
if (!hasKosModel) {
|
|
3852
|
+
findings.push({
|
|
3853
|
+
level: "error",
|
|
3854
|
+
rule: "missing-kosModel",
|
|
3855
|
+
message: "No @kosModel decorator found — this is not a KOS model."
|
|
3856
|
+
});
|
|
3857
|
+
}
|
|
3858
|
+
const imports = sf.getImportDeclarations();
|
|
3859
|
+
const mobxImport = imports.find(
|
|
3860
|
+
(d) => /^mobx(-react-lite)?$/.test(d.getModuleSpecifierValue())
|
|
3861
|
+
);
|
|
3862
|
+
if (mobxImport) {
|
|
3863
|
+
findings.push({
|
|
3864
|
+
level: "error",
|
|
3865
|
+
rule: "mobx-import",
|
|
3866
|
+
message: "Imports mobx directly. Reactivity is internal to KOS — remove the mobx import and use KOS reactivity / container models."
|
|
3867
|
+
});
|
|
3868
|
+
}
|
|
3869
|
+
const barrelServiceRequest = imports.find(
|
|
3870
|
+
(d) => d.getModuleSpecifierValue() === "@kosdev-code/kos-ui-sdk" && d.getNamedImports().some((n) => n.getName() === "kosServiceRequest")
|
|
3871
|
+
);
|
|
3872
|
+
if (barrelServiceRequest) {
|
|
3873
|
+
findings.push({
|
|
3874
|
+
level: "warning",
|
|
3875
|
+
rule: "untyped-service-request",
|
|
3876
|
+
message: "kosServiceRequest imported from the SDK barrel. Use the typed decorator from the api:generate'd service module so paths are validated against the OpenAPI types."
|
|
3877
|
+
});
|
|
3878
|
+
}
|
|
3879
|
+
for (const cls of sf.getClasses()) {
|
|
3880
|
+
for (const prop of cls.getProperties()) {
|
|
3881
|
+
const name = prop.getName();
|
|
3882
|
+
const typeText = (prop.getTypeNode()?.getText() ?? "").replace(/\s+/g, " ").trim();
|
|
3883
|
+
const initText = prop.getInitializer()?.getText() ?? "";
|
|
3884
|
+
const hasChild = !!prop.getDecorator("kosChild");
|
|
3885
|
+
const isModelContainer = /\bI?KosModelContainer\s*</.test(typeText) || /new\s+KosModelContainer\b/.test(initText);
|
|
3886
|
+
if (isModelContainer && !hasChild) {
|
|
3887
|
+
findings.push({
|
|
3888
|
+
level: "warning",
|
|
3889
|
+
rule: "container-missing-kosChild",
|
|
3890
|
+
message: `Property '${name}' is a model container but is not marked @kosChild — add @kosChild so its models join the model graph and lifecycle.`
|
|
3891
|
+
});
|
|
3892
|
+
continue;
|
|
3893
|
+
}
|
|
3894
|
+
const elem = modelElementType(typeText);
|
|
3895
|
+
if (elem && !isModelContainer) {
|
|
3896
|
+
findings.push({
|
|
3897
|
+
level: "warning",
|
|
3898
|
+
rule: "raw-model-collection",
|
|
3899
|
+
message: `Property '${name}' is a raw ${typeText} of models — prefer a KosModelContainer<${elem}> (indexing, sorting, lifecycle, delta handling) marked @kosChild.`
|
|
3900
|
+
});
|
|
3901
|
+
}
|
|
3902
|
+
}
|
|
3903
|
+
}
|
|
3904
|
+
const hasError = findings.some((f) => f.level === "error");
|
|
3905
|
+
return { modelFilePath, ok: !hasError, findings };
|
|
3906
|
+
}
|
|
3093
3907
|
const PLUGIN_TYPES = {
|
|
3094
3908
|
CUI: "cui",
|
|
3095
3909
|
UTILITY: "utility",
|
|
@@ -3644,8 +4458,75 @@ function updatePluginConfiguration(codegenFs, projectConfig, options) {
|
|
|
3644
4458
|
);
|
|
3645
4459
|
}
|
|
3646
4460
|
}
|
|
4461
|
+
function parse(content) {
|
|
4462
|
+
const singleQuoted = /^\s*import\b[^"']*'/m.test(content);
|
|
4463
|
+
const project = new tsMorph.Project({
|
|
4464
|
+
useInMemoryFileSystem: true,
|
|
4465
|
+
manipulationSettings: {
|
|
4466
|
+
indentationText: tsMorph.IndentationText.TwoSpaces,
|
|
4467
|
+
quoteKind: singleQuoted ? tsMorph.QuoteKind.Single : tsMorph.QuoteKind.Double
|
|
4468
|
+
}
|
|
4469
|
+
});
|
|
4470
|
+
return project.createSourceFile("registration-chain.ts", content, {
|
|
4471
|
+
overwrite: true
|
|
4472
|
+
});
|
|
4473
|
+
}
|
|
4474
|
+
function chainCalls(sf, name) {
|
|
4475
|
+
return sf.getDescendantsOfKind(tsMorph.SyntaxKind.CallExpression).filter((call) => {
|
|
4476
|
+
const callee = call.getExpression();
|
|
4477
|
+
return callee.getKind() === tsMorph.SyntaxKind.PropertyAccessExpression && callee.asKindOrThrow(tsMorph.SyntaxKind.PropertyAccessExpression).getName() === name;
|
|
4478
|
+
});
|
|
4479
|
+
}
|
|
4480
|
+
function indentOfCall(sf, call) {
|
|
4481
|
+
const text = sf.getFullText();
|
|
4482
|
+
const nameNode = call.getExpression().asKindOrThrow(tsMorph.SyntaxKind.PropertyAccessExpression).getNameNode();
|
|
4483
|
+
const lineStart = text.lastIndexOf("\n", nameNode.getStart()) + 1;
|
|
4484
|
+
return text.slice(lineStart).match(/^[ \t]*/)?.[0] ?? "";
|
|
4485
|
+
}
|
|
4486
|
+
function countChainEntries(content) {
|
|
4487
|
+
return chainCalls(parse(content), "model").length;
|
|
4488
|
+
}
|
|
4489
|
+
function upsertChainEntry(content, bean, importSpec) {
|
|
4490
|
+
const sf = parse(content);
|
|
4491
|
+
const modelCalls = chainCalls(sf, "model");
|
|
4492
|
+
const registered = modelCalls.some((call) => {
|
|
4493
|
+
const [arg, ...rest] = call.getArguments();
|
|
4494
|
+
return rest.length === 0 && arg?.getText() === bean;
|
|
4495
|
+
});
|
|
4496
|
+
if (registered) return { changed: false, note: "already registered" };
|
|
4497
|
+
let anchor;
|
|
4498
|
+
let indent;
|
|
4499
|
+
if (modelCalls.length > 0) {
|
|
4500
|
+
anchor = modelCalls.reduce(
|
|
4501
|
+
(last, call) => call.getEnd() > last.getEnd() ? call : last
|
|
4502
|
+
);
|
|
4503
|
+
indent = indentOfCall(sf, anchor);
|
|
4504
|
+
} else {
|
|
4505
|
+
const starts = chainCalls(sf, "models").filter(
|
|
4506
|
+
(call) => call.getArguments().length === 0
|
|
4507
|
+
);
|
|
4508
|
+
anchor = starts[starts.length - 1];
|
|
4509
|
+
if (!anchor) {
|
|
4510
|
+
return {
|
|
4511
|
+
changed: false,
|
|
4512
|
+
error: "no .models() chain found in the registration file"
|
|
4513
|
+
};
|
|
4514
|
+
}
|
|
4515
|
+
const text = sf.getFullText();
|
|
4516
|
+
const lineStart = text.lastIndexOf("\n", anchor.getStart()) + 1;
|
|
4517
|
+
indent = (text.slice(lineStart).match(/^[ \t]*/)?.[0] ?? "") + " ";
|
|
4518
|
+
}
|
|
4519
|
+
anchor.replaceWithText(`${anchor.getText()}
|
|
4520
|
+
${indent}.model(${bean})`);
|
|
4521
|
+
const imported = sf.getImportDeclarations().some(
|
|
4522
|
+
(decl) => decl.getNamedImports().some((named) => named.getName() === bean)
|
|
4523
|
+
);
|
|
4524
|
+
if (!imported) ensureNamedImport(sf, importSpec, [{ name: bean }]);
|
|
4525
|
+
return { changed: true, content: sf.getFullText(), importAdded: !imported };
|
|
4526
|
+
}
|
|
3647
4527
|
exports.BasePluginHandler = BasePluginHandler;
|
|
3648
4528
|
exports.CONTRIBUTION_TYPE_MAP = CONTRIBUTION_TYPE_MAP;
|
|
4529
|
+
exports.DEFAULT_MODEL_LIBS_DIR = DEFAULT_MODEL_LIBS_DIR;
|
|
3649
4530
|
exports.DirectFileSystem = DirectFileSystem;
|
|
3650
4531
|
exports.KAB_OUTPUT_DIR = KAB_OUTPUT_DIR;
|
|
3651
4532
|
exports.KAB_OUTPUT_PATH = KAB_OUTPUT_PATH;
|
|
@@ -3667,6 +4548,7 @@ exports.addFutureToModel = addFutureToModel;
|
|
|
3667
4548
|
exports.addJavaArtifactToManifests = addJavaArtifactToManifests;
|
|
3668
4549
|
exports.addKosModelConfiguration = addKosModelConfiguration;
|
|
3669
4550
|
exports.addModelEffectToModel = addModelEffectToModel;
|
|
4551
|
+
exports.addParentAwareToModel = addParentAwareToModel;
|
|
3670
4552
|
exports.addPropertyToModel = addPropertyToModel;
|
|
3671
4553
|
exports.addServiceRequestToModel = addServiceRequestToModel;
|
|
3672
4554
|
exports.addTopicHandlerToModel = addTopicHandlerToModel;
|
|
@@ -3675,6 +4557,7 @@ exports.buildKabTargets = buildKabTargets;
|
|
|
3675
4557
|
exports.buildSbomTarget = buildSbomTarget;
|
|
3676
4558
|
exports.camelCase = camelCase;
|
|
3677
4559
|
exports.constantCase = constantCase;
|
|
4560
|
+
exports.countChainEntries = countChainEntries;
|
|
3678
4561
|
exports.dashCase = dashCase;
|
|
3679
4562
|
exports.describeModel = describeModel;
|
|
3680
4563
|
exports.discoverJavaArtifacts = discoverJavaArtifacts;
|
|
@@ -3693,8 +4576,10 @@ exports.generateFilesFromTemplates = generateFilesFromTemplates;
|
|
|
3693
4576
|
exports.generateHook = generateHook;
|
|
3694
4577
|
exports.generateInit = generateInit;
|
|
3695
4578
|
exports.generateModel = generateModel;
|
|
4579
|
+
exports.generateModelProject = generateModelProject;
|
|
3696
4580
|
exports.generatePolyglotWorkspace = generatePolyglotWorkspace;
|
|
3697
4581
|
exports.generateSplashProject = generateSplashProject;
|
|
4582
|
+
exports.generateViewModel = generateViewModel;
|
|
3698
4583
|
exports.getCodegenLogger = getCodegenLogger;
|
|
3699
4584
|
exports.getCurrentDirectoryName = getCurrentDirectoryName;
|
|
3700
4585
|
exports.getKosModelConfigProp = getKosModelConfigProp;
|
|
@@ -3712,10 +4597,12 @@ exports.readJson = readJson;
|
|
|
3712
4597
|
exports.readNxJson = readNxJson;
|
|
3713
4598
|
exports.resolveKabPath = resolveKabPath;
|
|
3714
4599
|
exports.resolveModelFilePath = resolveModelFilePath;
|
|
4600
|
+
exports.resolveModelProjectLayout = resolveModelProjectLayout;
|
|
3715
4601
|
exports.setCodegenLogger = setCodegenLogger;
|
|
3716
4602
|
exports.syncCiManifests = syncCiManifests;
|
|
3717
4603
|
exports.updateJson = updateJson;
|
|
3718
4604
|
exports.updateModelIndex = updateModelIndex;
|
|
4605
|
+
exports.upsertChainEntry = upsertChainEntry;
|
|
3719
4606
|
exports.validateModel = validateModel;
|
|
3720
4607
|
exports.writeJson = writeJson;
|
|
3721
4608
|
//# sourceMappingURL=index.js.map
|