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