@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.mjs
CHANGED
|
@@ -3,7 +3,7 @@ import * as path from "path";
|
|
|
3
3
|
import * as ejs from "ejs";
|
|
4
4
|
import fg from "fast-glob";
|
|
5
5
|
import prettier from "prettier";
|
|
6
|
-
import { Scope, Project, QuoteKind, IndentationText, VariableDeclarationKind, Node } from "ts-morph";
|
|
6
|
+
import { Scope, Project, QuoteKind, IndentationText, VariableDeclarationKind, Node, SyntaxKind } from "ts-morph";
|
|
7
7
|
import * as ts from "typescript";
|
|
8
8
|
class TrackingFileSystem {
|
|
9
9
|
inner;
|
|
@@ -474,6 +474,84 @@ function generateSplashProject(codegenFs, templateDir, options) {
|
|
|
474
474
|
normalized
|
|
475
475
|
);
|
|
476
476
|
}
|
|
477
|
+
const MODEL_PROJECT_SUFFIX = "-models";
|
|
478
|
+
const DEFAULT_MODEL_LIBS_DIR = "libs";
|
|
479
|
+
const JSONC_ESLINT_PARSER_VERSION = "^2.1.0";
|
|
480
|
+
function resolveModelProjectLayout(codegenFs, options) {
|
|
481
|
+
const base = dashCase(options.name);
|
|
482
|
+
const projectName = base.endsWith(MODEL_PROJECT_SUFFIX) ? base : `${base}${MODEL_PROJECT_SUFFIX}`;
|
|
483
|
+
const libsDir = (options.libsDir || DEFAULT_MODEL_LIBS_DIR).replace(
|
|
484
|
+
/\\/g,
|
|
485
|
+
"/"
|
|
486
|
+
);
|
|
487
|
+
const scope = codegenFs ? readNpmScope(codegenFs) : void 0;
|
|
488
|
+
return {
|
|
489
|
+
projectName,
|
|
490
|
+
projectRoot: `${libsDir}/${projectName}`.replace(/^\/+/, ""),
|
|
491
|
+
importPath: scope ? `@${scope}/${projectName}` : projectName
|
|
492
|
+
};
|
|
493
|
+
}
|
|
494
|
+
function readNpmScope(codegenFs) {
|
|
495
|
+
if (!codegenFs.exists("package.json")) {
|
|
496
|
+
return void 0;
|
|
497
|
+
}
|
|
498
|
+
const name = readJson(codegenFs, "package.json").name;
|
|
499
|
+
if (!name?.startsWith("@")) {
|
|
500
|
+
return void 0;
|
|
501
|
+
}
|
|
502
|
+
return name.split("/")[0].slice(1);
|
|
503
|
+
}
|
|
504
|
+
function offsetFromRoot(projectRoot) {
|
|
505
|
+
return projectRoot.split("/").filter(Boolean).map(() => "../").join("");
|
|
506
|
+
}
|
|
507
|
+
function generateModelProject(codegenFs, templateDir, options) {
|
|
508
|
+
const layout = resolveModelProjectLayout(codegenFs, options);
|
|
509
|
+
generateFilesFromTemplates(
|
|
510
|
+
codegenFs,
|
|
511
|
+
path.join(templateDir, "project"),
|
|
512
|
+
layout.projectRoot,
|
|
513
|
+
{
|
|
514
|
+
...layout,
|
|
515
|
+
projectNameCamelCase: camelCase(layout.projectName),
|
|
516
|
+
offsetFromRoot: offsetFromRoot(layout.projectRoot),
|
|
517
|
+
template: ""
|
|
518
|
+
}
|
|
519
|
+
);
|
|
520
|
+
registerTsconfigPath(codegenFs, layout);
|
|
521
|
+
ensureJsoncEslintParser(codegenFs);
|
|
522
|
+
return layout;
|
|
523
|
+
}
|
|
524
|
+
function registerTsconfigPath(codegenFs, layout) {
|
|
525
|
+
if (!codegenFs.exists("tsconfig.base.json")) {
|
|
526
|
+
return;
|
|
527
|
+
}
|
|
528
|
+
updateJson(codegenFs, "tsconfig.base.json", (json) => {
|
|
529
|
+
json.compilerOptions = json.compilerOptions ?? {};
|
|
530
|
+
json.compilerOptions.paths = json.compilerOptions.paths ?? {};
|
|
531
|
+
json.compilerOptions.paths[layout.importPath] = [
|
|
532
|
+
`${layout.projectRoot}/src/index.ts`
|
|
533
|
+
];
|
|
534
|
+
return json;
|
|
535
|
+
});
|
|
536
|
+
}
|
|
537
|
+
function ensureJsoncEslintParser(codegenFs) {
|
|
538
|
+
if (!codegenFs.exists("package.json")) {
|
|
539
|
+
return;
|
|
540
|
+
}
|
|
541
|
+
const pkg = readJson(codegenFs, "package.json");
|
|
542
|
+
const devDependencies = pkg.devDependencies ?? {};
|
|
543
|
+
if ("jsonc-eslint-parser" in devDependencies) {
|
|
544
|
+
return;
|
|
545
|
+
}
|
|
546
|
+
devDependencies["jsonc-eslint-parser"] = JSONC_ESLINT_PARSER_VERSION;
|
|
547
|
+
pkg.devDependencies = Object.fromEntries(
|
|
548
|
+
// Codepoint order, matching what Nx's own dependency helper produces.
|
|
549
|
+
Object.entries(devDependencies).sort(
|
|
550
|
+
([a], [b]) => a < b ? -1 : a > b ? 1 : 0
|
|
551
|
+
)
|
|
552
|
+
);
|
|
553
|
+
writeJson(codegenFs, "package.json", pkg);
|
|
554
|
+
}
|
|
477
555
|
function generateInit(codegenFs, options) {
|
|
478
556
|
const logger = getCodegenLogger();
|
|
479
557
|
const { appProject, modelProject, registrationProject } = options;
|
|
@@ -913,7 +991,8 @@ function normalizeOptions(codegenFs, options, projects) {
|
|
|
913
991
|
const booleanDefaults = {
|
|
914
992
|
companion: false,
|
|
915
993
|
skipRegistration: false,
|
|
916
|
-
futureAware: "none"
|
|
994
|
+
futureAware: "none",
|
|
995
|
+
singleton: false
|
|
917
996
|
};
|
|
918
997
|
return {
|
|
919
998
|
...booleanDefaults,
|
|
@@ -925,6 +1004,58 @@ function normalizeOptions(codegenFs, options, projects) {
|
|
|
925
1004
|
template: ""
|
|
926
1005
|
};
|
|
927
1006
|
}
|
|
1007
|
+
const UPDATE_RELEASE_VERSION_SCRIPT = `import devkit from "@nx/devkit";
|
|
1008
|
+
import { resolve } from "path";
|
|
1009
|
+
import { readFileSync, writeFileSync } from "fs";
|
|
1010
|
+
import prettier from "prettier";
|
|
1011
|
+
|
|
1012
|
+
// KOS artifact versioning: stamps the project's .kos.json "version" field
|
|
1013
|
+
// (which kabtool bakes into the KAB). Never touches package.json.
|
|
1014
|
+
// Driven by tag-based releases:
|
|
1015
|
+
// nx run-many --target=version --args=--ver=$KOSBUILD_VERSION
|
|
1016
|
+
|
|
1017
|
+
const { readCachedProjectGraph } = devkit;
|
|
1018
|
+
const [, , name, versionArg] = process.argv;
|
|
1019
|
+
|
|
1020
|
+
// "{args.ver}" arrives literally when the target runs without --args=--ver=<v>;
|
|
1021
|
+
// treat that (or a missing arg) as "report current version, change nothing".
|
|
1022
|
+
const version =
|
|
1023
|
+
versionArg && !versionArg.startsWith("{args") ? versionArg : undefined;
|
|
1024
|
+
|
|
1025
|
+
if (!name) {
|
|
1026
|
+
console.error("usage: update-release-version.mjs <project> <version>");
|
|
1027
|
+
process.exit(1);
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
const graph = readCachedProjectGraph();
|
|
1031
|
+
const project = graph.nodes[name];
|
|
1032
|
+
if (!project) {
|
|
1033
|
+
console.error("Unknown project: " + name);
|
|
1034
|
+
process.exit(1);
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
const kosJsonPath = resolve(process.cwd(), project.data.root, ".kos.json");
|
|
1038
|
+
let kosJson;
|
|
1039
|
+
try {
|
|
1040
|
+
kosJson = JSON.parse(readFileSync(kosJsonPath, "utf8"));
|
|
1041
|
+
} catch {
|
|
1042
|
+
console.error("Missing or invalid .kos.json: " + kosJsonPath);
|
|
1043
|
+
process.exit(1);
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
if (!version) {
|
|
1047
|
+
console.log(name + ": " + kosJson.version + " (no --ver given; unchanged)");
|
|
1048
|
+
process.exit(0);
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
const prettierOptions = await prettier.resolveConfig(kosJsonPath);
|
|
1052
|
+
const output = await prettier.format(
|
|
1053
|
+
JSON.stringify({ ...kosJson, version }, null, 2),
|
|
1054
|
+
{ ...prettierOptions, parser: "json" }
|
|
1055
|
+
);
|
|
1056
|
+
writeFileSync(kosJsonPath, output);
|
|
1057
|
+
console.log(name + ": version -> " + version);
|
|
1058
|
+
`;
|
|
928
1059
|
function transformSourceFile(codegenFs, filePath, mutate) {
|
|
929
1060
|
const content = codegenFs.read(filePath);
|
|
930
1061
|
if (content === null) {
|
|
@@ -953,6 +1084,15 @@ function readSourceFile(codegenFs, filePath, inspect) {
|
|
|
953
1084
|
project.createSourceFile(filePath, content, { overwrite: true })
|
|
954
1085
|
);
|
|
955
1086
|
}
|
|
1087
|
+
function decoratorConfigText(sourceFile, cls, decoratorName) {
|
|
1088
|
+
const arg = cls.getDecorator(decoratorName)?.getArguments()[0];
|
|
1089
|
+
if (!arg) return void 0;
|
|
1090
|
+
if (arg.getKindName() === "ObjectLiteralExpression") return arg.getText();
|
|
1091
|
+
const text = arg.getText().trim();
|
|
1092
|
+
if (!/^[A-Za-z_$][\w$]*$/.test(text)) return text;
|
|
1093
|
+
const initializer = sourceFile.getVariableDeclaration(text)?.getInitializer()?.getText();
|
|
1094
|
+
return initializer ? initializer.replace(/\s+as\s+const\s*$/, "") : text;
|
|
1095
|
+
}
|
|
956
1096
|
function ensureNamedImport(sourceFile, moduleSpecifier, names) {
|
|
957
1097
|
const decls = sourceFile.getImportDeclarations().filter((d) => d.getModuleSpecifierValue() === moduleSpecifier);
|
|
958
1098
|
const existing = /* @__PURE__ */ new Set();
|
|
@@ -1066,7 +1206,8 @@ function ensureConstCatalogEntry(sourceFile, spec) {
|
|
|
1066
1206
|
}
|
|
1067
1207
|
literal.addPropertyAssignment({
|
|
1068
1208
|
name: spec.key,
|
|
1069
|
-
initializer: spec.initializer
|
|
1209
|
+
initializer: spec.initializer,
|
|
1210
|
+
leadingTrivia: spec.leadingTrivia
|
|
1070
1211
|
});
|
|
1071
1212
|
return { key: spec.key, created: true };
|
|
1072
1213
|
}
|
|
@@ -1084,7 +1225,8 @@ function ensureConstCatalogEntry(sourceFile, spec) {
|
|
|
1084
1225
|
}
|
|
1085
1226
|
literal.addPropertyAssignment({
|
|
1086
1227
|
name: spec.key,
|
|
1087
|
-
initializer: spec.initializer
|
|
1228
|
+
initializer: spec.initializer,
|
|
1229
|
+
leadingTrivia: spec.leadingTrivia
|
|
1088
1230
|
});
|
|
1089
1231
|
return { key: spec.key, created: true };
|
|
1090
1232
|
}
|
|
@@ -1093,7 +1235,8 @@ function ensureExportedTypeAlias(sourceFile, spec) {
|
|
|
1093
1235
|
sourceFile.addTypeAlias({
|
|
1094
1236
|
name: spec.name,
|
|
1095
1237
|
type: spec.type,
|
|
1096
|
-
isExported: true
|
|
1238
|
+
isExported: true,
|
|
1239
|
+
docs: spec.docs ? [spec.docs] : void 0
|
|
1097
1240
|
});
|
|
1098
1241
|
return true;
|
|
1099
1242
|
}
|
|
@@ -1117,12 +1260,47 @@ function ensureBarrelExport(sourceFile, moduleSpecifier) {
|
|
|
1117
1260
|
sourceFile.addExportDeclaration({ moduleSpecifier });
|
|
1118
1261
|
return true;
|
|
1119
1262
|
}
|
|
1263
|
+
function ensureNamedExports(sourceFile, moduleSpecifier, names, isTypeOnly = false) {
|
|
1264
|
+
const existing = sourceFile.getExportDeclarations().find(
|
|
1265
|
+
(d) => d.getModuleSpecifierValue() === moduleSpecifier && d.isTypeOnly() === isTypeOnly
|
|
1266
|
+
);
|
|
1267
|
+
if (!existing) {
|
|
1268
|
+
sourceFile.addExportDeclaration({
|
|
1269
|
+
moduleSpecifier,
|
|
1270
|
+
isTypeOnly,
|
|
1271
|
+
namedExports: names.map((name) => {
|
|
1272
|
+
return { name };
|
|
1273
|
+
})
|
|
1274
|
+
});
|
|
1275
|
+
return true;
|
|
1276
|
+
}
|
|
1277
|
+
const present = new Set(existing.getNamedExports().map((e) => e.getName()));
|
|
1278
|
+
const missing = names.filter((name) => !present.has(name));
|
|
1279
|
+
if (missing.length === 0) return false;
|
|
1280
|
+
existing.addNamedExports(
|
|
1281
|
+
missing.map((name) => {
|
|
1282
|
+
return { name };
|
|
1283
|
+
})
|
|
1284
|
+
);
|
|
1285
|
+
return true;
|
|
1286
|
+
}
|
|
1287
|
+
function ensureExportedInterface(sourceFile, name, properties = [], extendsTypes = []) {
|
|
1288
|
+
if (sourceFile.getInterface(name)) return false;
|
|
1289
|
+
sourceFile.addInterface({
|
|
1290
|
+
name,
|
|
1291
|
+
isExported: true,
|
|
1292
|
+
extends: extendsTypes,
|
|
1293
|
+
properties
|
|
1294
|
+
});
|
|
1295
|
+
return true;
|
|
1296
|
+
}
|
|
1120
1297
|
function addDecoratedMethod(cls, spec) {
|
|
1121
1298
|
if (cls.getMethod(spec.name)) return false;
|
|
1122
1299
|
cls.addMethod({
|
|
1123
1300
|
name: spec.name,
|
|
1124
1301
|
isAsync: spec.isAsync,
|
|
1125
1302
|
returnType: spec.returnType,
|
|
1303
|
+
docs: spec.docs ? [spec.docs] : void 0,
|
|
1126
1304
|
parameters: spec.parameters?.map((p) => {
|
|
1127
1305
|
return { name: p.name, type: p.type, hasQuestionToken: p.optional };
|
|
1128
1306
|
}),
|
|
@@ -1130,6 +1308,7 @@ function addDecoratedMethod(cls, spec) {
|
|
|
1130
1308
|
decorators: [
|
|
1131
1309
|
{
|
|
1132
1310
|
name: spec.decoratorName,
|
|
1311
|
+
typeArguments: spec.decoratorTypeArgs,
|
|
1133
1312
|
arguments: spec.decoratorArgsText ? [spec.decoratorArgsText] : []
|
|
1134
1313
|
}
|
|
1135
1314
|
]
|
|
@@ -1212,17 +1391,28 @@ function listServiceCatalog(codegenFs, query, projects) {
|
|
|
1212
1391
|
app,
|
|
1213
1392
|
version,
|
|
1214
1393
|
serviceModulePath: posix,
|
|
1215
|
-
operations: readOperations(codegenFs, openapiPath)
|
|
1394
|
+
operations: readOperations(codegenFs, openapiPath) ?? []
|
|
1216
1395
|
});
|
|
1217
1396
|
}
|
|
1218
1397
|
return entries.sort(
|
|
1219
1398
|
(a, b) => a.app.localeCompare(b.app) || a.version.localeCompare(b.version)
|
|
1220
1399
|
);
|
|
1221
1400
|
}
|
|
1401
|
+
function readServiceModuleOperations(codegenFs, serviceModuleFile) {
|
|
1402
|
+
const moduleFile = serviceModuleFile.endsWith(".ts") ? serviceModuleFile : `${serviceModuleFile}.ts`;
|
|
1403
|
+
const posix = moduleFile.split(path.sep).join("/");
|
|
1404
|
+
const openapiPath = posix.replace(/service\.ts$/, "openapi.d.ts");
|
|
1405
|
+
for (const candidate of [openapiPath, posix]) {
|
|
1406
|
+
if (!codegenFs.exists(candidate)) continue;
|
|
1407
|
+
const operations = readOperations(codegenFs, candidate);
|
|
1408
|
+
if (operations) return operations;
|
|
1409
|
+
}
|
|
1410
|
+
return null;
|
|
1411
|
+
}
|
|
1222
1412
|
function readOperations(codegenFs, openapiPath) {
|
|
1223
1413
|
return readSourceFile(codegenFs, openapiPath, (sf) => {
|
|
1224
1414
|
const paths = sf.getInterface("paths");
|
|
1225
|
-
if (!paths) return
|
|
1415
|
+
if (!paths) return null;
|
|
1226
1416
|
const operations = [];
|
|
1227
1417
|
for (const pathProp of paths.getProperties()) {
|
|
1228
1418
|
const servicePath = unquote(pathProp.getName());
|
|
@@ -1256,58 +1446,6 @@ function readSummary(methodProp) {
|
|
|
1256
1446
|
function unquote(name) {
|
|
1257
1447
|
return name.replace(/^["']|["']$/g, "");
|
|
1258
1448
|
}
|
|
1259
|
-
const UPDATE_RELEASE_VERSION_SCRIPT = `import devkit from "@nx/devkit";
|
|
1260
|
-
import { resolve } from "path";
|
|
1261
|
-
import { readFileSync, writeFileSync } from "fs";
|
|
1262
|
-
import prettier from "prettier";
|
|
1263
|
-
|
|
1264
|
-
// KOS artifact versioning: stamps the project's .kos.json "version" field
|
|
1265
|
-
// (which kabtool bakes into the KAB). Never touches package.json.
|
|
1266
|
-
// Driven by tag-based releases:
|
|
1267
|
-
// nx run-many --target=version --args=--ver=$KOSBUILD_VERSION
|
|
1268
|
-
|
|
1269
|
-
const { readCachedProjectGraph } = devkit;
|
|
1270
|
-
const [, , name, versionArg] = process.argv;
|
|
1271
|
-
|
|
1272
|
-
// "{args.ver}" arrives literally when the target runs without --args=--ver=<v>;
|
|
1273
|
-
// treat that (or a missing arg) as "report current version, change nothing".
|
|
1274
|
-
const version =
|
|
1275
|
-
versionArg && !versionArg.startsWith("{args") ? versionArg : undefined;
|
|
1276
|
-
|
|
1277
|
-
if (!name) {
|
|
1278
|
-
console.error("usage: update-release-version.mjs <project> <version>");
|
|
1279
|
-
process.exit(1);
|
|
1280
|
-
}
|
|
1281
|
-
|
|
1282
|
-
const graph = readCachedProjectGraph();
|
|
1283
|
-
const project = graph.nodes[name];
|
|
1284
|
-
if (!project) {
|
|
1285
|
-
console.error("Unknown project: " + name);
|
|
1286
|
-
process.exit(1);
|
|
1287
|
-
}
|
|
1288
|
-
|
|
1289
|
-
const kosJsonPath = resolve(process.cwd(), project.data.root, ".kos.json");
|
|
1290
|
-
let kosJson;
|
|
1291
|
-
try {
|
|
1292
|
-
kosJson = JSON.parse(readFileSync(kosJsonPath, "utf8"));
|
|
1293
|
-
} catch {
|
|
1294
|
-
console.error("Missing or invalid .kos.json: " + kosJsonPath);
|
|
1295
|
-
process.exit(1);
|
|
1296
|
-
}
|
|
1297
|
-
|
|
1298
|
-
if (!version) {
|
|
1299
|
-
console.log(name + ": " + kosJson.version + " (no --ver given; unchanged)");
|
|
1300
|
-
process.exit(0);
|
|
1301
|
-
}
|
|
1302
|
-
|
|
1303
|
-
const prettierOptions = await prettier.resolveConfig(kosJsonPath);
|
|
1304
|
-
const output = await prettier.format(
|
|
1305
|
-
JSON.stringify({ ...kosJson, version }, null, 2),
|
|
1306
|
-
{ ...prettierOptions, parser: "json" }
|
|
1307
|
-
);
|
|
1308
|
-
writeFileSync(kosJsonPath, output);
|
|
1309
|
-
console.log(name + ": version -> " + version);
|
|
1310
|
-
`;
|
|
1311
1449
|
function appendBarrelExport(codegenFs, indexPath, exportPath) {
|
|
1312
1450
|
const exportLine = `export * from '${exportPath}'`;
|
|
1313
1451
|
const content = codegenFs.read(indexPath) ?? "";
|
|
@@ -1354,10 +1492,57 @@ function updateModelIndex(codegenFs, indexPath, modelPath) {
|
|
|
1354
1492
|
const newContents = printer.printFile(updatedSourceFile);
|
|
1355
1493
|
codegenFs.write(indexPath, newContents);
|
|
1356
1494
|
}
|
|
1357
|
-
function
|
|
1358
|
-
|
|
1359
|
-
|
|
1495
|
+
function generateCompanionModel(codegenFs, templateDir, options, projects) {
|
|
1496
|
+
const logger = getCodegenLogger();
|
|
1497
|
+
const normalized = normalizeAllValues({
|
|
1498
|
+
companionModelName: options.companionModelName,
|
|
1499
|
+
modelName: options.modelName
|
|
1500
|
+
});
|
|
1501
|
+
const companionChildKosConfig = getKosProjectConfiguration(
|
|
1502
|
+
codegenFs,
|
|
1503
|
+
options.companionModelProject,
|
|
1504
|
+
projects
|
|
1505
|
+
);
|
|
1506
|
+
const parentProject = findProjectByName(
|
|
1507
|
+
codegenFs.root,
|
|
1508
|
+
options.modelProject,
|
|
1509
|
+
projects
|
|
1510
|
+
);
|
|
1511
|
+
const childProject = findProjectByName(
|
|
1512
|
+
codegenFs.root,
|
|
1513
|
+
options.companionModelProject,
|
|
1514
|
+
projects
|
|
1515
|
+
);
|
|
1516
|
+
const projectRoot = childProject?.sourceRoot;
|
|
1517
|
+
if (!projectRoot) {
|
|
1518
|
+
logger.warn(`Companion child project source root not found`);
|
|
1519
|
+
return;
|
|
1520
|
+
}
|
|
1521
|
+
let importPath = "";
|
|
1522
|
+
if (parentProject) {
|
|
1523
|
+
const pkgJsonPath = path.join(parentProject.root, "package.json");
|
|
1524
|
+
try {
|
|
1525
|
+
const pkgJson = readJson(codegenFs, pkgJsonPath);
|
|
1526
|
+
importPath = pkgJson.name || "";
|
|
1527
|
+
} catch {
|
|
1528
|
+
importPath = "";
|
|
1529
|
+
}
|
|
1360
1530
|
}
|
|
1531
|
+
const modelLocation = companionChildKosConfig?.generator?.defaults?.model?.folder || "";
|
|
1532
|
+
const filePath = path.join(
|
|
1533
|
+
projectRoot,
|
|
1534
|
+
modelLocation,
|
|
1535
|
+
normalized.companionModelNameDashCase
|
|
1536
|
+
);
|
|
1537
|
+
logger.info(`Generating companion model in ${filePath}`);
|
|
1538
|
+
generateFilesFromTemplates(codegenFs, templateDir, filePath, {
|
|
1539
|
+
...options,
|
|
1540
|
+
...normalized,
|
|
1541
|
+
importPath
|
|
1542
|
+
});
|
|
1543
|
+
}
|
|
1544
|
+
function generateContainerModel(codegenFs, templateDir, options, cwd, projects) {
|
|
1545
|
+
const logger = getCodegenLogger();
|
|
1361
1546
|
const currentProject = getProject(codegenFs, cwd);
|
|
1362
1547
|
const modelProjectName = options.modelProject || currentProject?.name;
|
|
1363
1548
|
if (!modelProjectName) {
|
|
@@ -1365,56 +1550,109 @@ function generateHook(codegenFs, templateDir, options, cwd, projects) {
|
|
|
1365
1550
|
"No model project found. Please specify a model project with --modelProject."
|
|
1366
1551
|
);
|
|
1367
1552
|
}
|
|
1368
|
-
const modelName = options.
|
|
1553
|
+
const modelName = options.modelName || getCurrentDirectoryName(cwd);
|
|
1369
1554
|
if (!modelName) {
|
|
1370
1555
|
throw new Error(
|
|
1371
1556
|
"No model name found. Please specify a model name with --name."
|
|
1372
1557
|
);
|
|
1373
1558
|
}
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
modelProjectName,
|
|
1377
|
-
modelName,
|
|
1378
|
-
projects
|
|
1379
|
-
);
|
|
1380
|
-
options.singleton = kosModelConfig ? !!kosModelConfig.singleton : false;
|
|
1381
|
-
options.name = modelName;
|
|
1382
|
-
options.modelProject = modelProjectName;
|
|
1559
|
+
options.modelName = modelName;
|
|
1560
|
+
options.name = `${modelName}-container`;
|
|
1383
1561
|
const normalized = normalizeOptions(codegenFs, options, projects);
|
|
1384
|
-
const
|
|
1562
|
+
const projectConfig = findProjectByName(
|
|
1385
1563
|
codegenFs.root,
|
|
1386
|
-
normalized.
|
|
1564
|
+
normalized.modelProject,
|
|
1387
1565
|
projects
|
|
1388
1566
|
);
|
|
1389
|
-
if (!
|
|
1390
|
-
throw new Error(`
|
|
1567
|
+
if (!projectConfig) {
|
|
1568
|
+
throw new Error(`Model project '${normalized.modelProject}' not found`);
|
|
1391
1569
|
}
|
|
1570
|
+
addKosModelConfiguration({
|
|
1571
|
+
codegenFs,
|
|
1572
|
+
modelName: normalized.nameDashCase,
|
|
1573
|
+
projectName: projectConfig.name,
|
|
1574
|
+
projectRoot: projectConfig.root,
|
|
1575
|
+
singleton: !!options.singleton,
|
|
1576
|
+
container: true,
|
|
1577
|
+
// The container's exported registration bean (`export const <ProperCase>`).
|
|
1578
|
+
factory: normalized.nameProperCase
|
|
1579
|
+
});
|
|
1392
1580
|
const kosConfig = getKosProjectConfiguration(
|
|
1393
1581
|
codegenFs,
|
|
1394
|
-
|
|
1582
|
+
projectConfig.name,
|
|
1395
1583
|
projects
|
|
1396
1584
|
);
|
|
1397
|
-
const
|
|
1398
|
-
|
|
1399
|
-
|
|
1585
|
+
const modelLocation = kosConfig?.generator?.defaults?.model?.folder || "";
|
|
1586
|
+
const internal = !!kosConfig?.generator?.internal;
|
|
1587
|
+
options.modelDirectory = options.modelDirectory || modelLocation;
|
|
1588
|
+
const projectRoot = projectConfig.sourceRoot;
|
|
1400
1589
|
if (projectRoot) {
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
templateDir,
|
|
1404
|
-
path.join(
|
|
1405
|
-
projectRoot,
|
|
1406
|
-
options.appDirectory,
|
|
1407
|
-
"hooks",
|
|
1408
|
-
normalized.nameDashCase
|
|
1409
|
-
),
|
|
1410
|
-
normalized
|
|
1590
|
+
logger.info(
|
|
1591
|
+
`Generating container model ${normalized.nameDashCase} in ${projectRoot}`
|
|
1411
1592
|
);
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1593
|
+
const modelNameDashCase = normalized.modelNameDashCase || normalized.nameDashCase;
|
|
1594
|
+
const modelFolder = path.join(
|
|
1595
|
+
projectRoot,
|
|
1596
|
+
options.modelDirectory || "",
|
|
1597
|
+
modelNameDashCase
|
|
1598
|
+
);
|
|
1599
|
+
if (options.existingModel) {
|
|
1600
|
+
addContainerToExistingModelFolder(codegenFs, templateDir, modelFolder, {
|
|
1601
|
+
...normalized,
|
|
1602
|
+
internal
|
|
1603
|
+
});
|
|
1604
|
+
} else {
|
|
1605
|
+
generateFilesFromTemplates(
|
|
1606
|
+
codegenFs,
|
|
1607
|
+
path.join(templateDir, "model"),
|
|
1608
|
+
modelFolder,
|
|
1609
|
+
{ ...normalized, internal }
|
|
1610
|
+
);
|
|
1611
|
+
}
|
|
1612
|
+
const modelIndex = path.join(projectRoot, "index.ts");
|
|
1613
|
+
const modelPath = normalized.modelDirectory ? `${normalized.modelDirectory}/${modelNameDashCase}` : modelNameDashCase;
|
|
1614
|
+
updateModelIndex(codegenFs, modelIndex, modelPath);
|
|
1615
|
+
}
|
|
1616
|
+
}
|
|
1617
|
+
function addContainerToExistingModelFolder(codegenFs, templateDir, modelFolder, substitutions) {
|
|
1618
|
+
const containerFileName = `${substitutions.nameDashCase}-model.ts`;
|
|
1619
|
+
const containerTemplate = path.join(
|
|
1620
|
+
templateDir,
|
|
1621
|
+
"model",
|
|
1622
|
+
"__nameDashCase__-model.ts.template"
|
|
1623
|
+
);
|
|
1624
|
+
const rendered = ejs.render(
|
|
1625
|
+
fs.readFileSync(containerTemplate, "utf-8"),
|
|
1626
|
+
substitutions,
|
|
1627
|
+
{ filename: containerTemplate }
|
|
1628
|
+
);
|
|
1629
|
+
codegenFs.write(path.join(modelFolder, containerFileName), rendered);
|
|
1630
|
+
const typesPath = path.join(modelFolder, "types", "index.d.ts");
|
|
1631
|
+
if (codegenFs.read(typesPath) === null) {
|
|
1632
|
+
codegenFs.write(typesPath, "");
|
|
1633
|
+
}
|
|
1634
|
+
transformSourceFile(codegenFs, typesPath, (sourceFile) => {
|
|
1635
|
+
ensureExportedInterface(
|
|
1636
|
+
sourceFile,
|
|
1637
|
+
`${substitutions.nameProperCase}Options`
|
|
1416
1638
|
);
|
|
1639
|
+
});
|
|
1640
|
+
const barrelPath = path.join(modelFolder, "index.ts");
|
|
1641
|
+
if (codegenFs.read(barrelPath) === null) {
|
|
1642
|
+
codegenFs.write(barrelPath, "");
|
|
1417
1643
|
}
|
|
1644
|
+
transformSourceFile(codegenFs, barrelPath, (sourceFile) => {
|
|
1645
|
+
const moduleSpecifier = `./${containerFileName.replace(/\.ts$/, "")}`;
|
|
1646
|
+
ensureNamedExports(sourceFile, moduleSpecifier, [
|
|
1647
|
+
substitutions.nameProperCase
|
|
1648
|
+
]);
|
|
1649
|
+
ensureNamedExports(
|
|
1650
|
+
sourceFile,
|
|
1651
|
+
moduleSpecifier,
|
|
1652
|
+
[`${substitutions.nameProperCase}Model`],
|
|
1653
|
+
true
|
|
1654
|
+
);
|
|
1655
|
+
});
|
|
1418
1656
|
}
|
|
1419
1657
|
function generateContext(codegenFs, templateDir, options, cwd, projects) {
|
|
1420
1658
|
if (!options.appProject) {
|
|
@@ -1468,8 +1706,10 @@ function generateContext(codegenFs, templateDir, options, cwd, projects) {
|
|
|
1468
1706
|
);
|
|
1469
1707
|
}
|
|
1470
1708
|
}
|
|
1471
|
-
function
|
|
1472
|
-
|
|
1709
|
+
function generateHook(codegenFs, templateDir, options, cwd, projects) {
|
|
1710
|
+
if (!options.appProject) {
|
|
1711
|
+
throw new Error("No app project specified");
|
|
1712
|
+
}
|
|
1473
1713
|
const currentProject = getProject(codegenFs, cwd);
|
|
1474
1714
|
const modelProjectName = options.modelProject || currentProject?.name;
|
|
1475
1715
|
if (!modelProjectName) {
|
|
@@ -1477,106 +1717,56 @@ function generateContainerModel(codegenFs, templateDir, options, cwd, projects)
|
|
|
1477
1717
|
"No model project found. Please specify a model project with --modelProject."
|
|
1478
1718
|
);
|
|
1479
1719
|
}
|
|
1480
|
-
const modelName = options.
|
|
1720
|
+
const modelName = options.name || getCurrentDirectoryName(cwd);
|
|
1481
1721
|
if (!modelName) {
|
|
1482
1722
|
throw new Error(
|
|
1483
1723
|
"No model name found. Please specify a model name with --name."
|
|
1484
1724
|
);
|
|
1485
1725
|
}
|
|
1486
|
-
|
|
1487
|
-
|
|
1726
|
+
const kosModelConfig = getKosModelConfiguration(
|
|
1727
|
+
codegenFs,
|
|
1728
|
+
modelProjectName,
|
|
1729
|
+
modelName,
|
|
1730
|
+
projects
|
|
1731
|
+
);
|
|
1732
|
+
options.singleton = kosModelConfig ? !!kosModelConfig.singleton : false;
|
|
1733
|
+
options.name = modelName;
|
|
1734
|
+
options.modelProject = modelProjectName;
|
|
1488
1735
|
const normalized = normalizeOptions(codegenFs, options, projects);
|
|
1489
|
-
const
|
|
1736
|
+
const appProject = findProjectByName(
|
|
1490
1737
|
codegenFs.root,
|
|
1491
|
-
normalized.
|
|
1738
|
+
normalized.appProject,
|
|
1492
1739
|
projects
|
|
1493
1740
|
);
|
|
1494
|
-
if (!
|
|
1495
|
-
throw new Error(`
|
|
1741
|
+
if (!appProject) {
|
|
1742
|
+
throw new Error(`App project '${normalized.appProject}' not found`);
|
|
1496
1743
|
}
|
|
1497
|
-
addKosModelConfiguration({
|
|
1498
|
-
codegenFs,
|
|
1499
|
-
modelName: normalized.nameDashCase,
|
|
1500
|
-
projectName: projectConfig.name,
|
|
1501
|
-
projectRoot: projectConfig.root,
|
|
1502
|
-
singleton: !!options.singleton,
|
|
1503
|
-
container: true,
|
|
1504
|
-
// The container's exported registration bean (`export const <ProperCase>`).
|
|
1505
|
-
factory: normalized.nameProperCase
|
|
1506
|
-
});
|
|
1507
1744
|
const kosConfig = getKosProjectConfiguration(
|
|
1508
1745
|
codegenFs,
|
|
1509
|
-
|
|
1746
|
+
appProject.name,
|
|
1510
1747
|
projects
|
|
1511
1748
|
);
|
|
1512
|
-
const
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
const projectRoot = projectConfig.sourceRoot;
|
|
1749
|
+
const componentLocation = kosConfig?.generator?.defaults?.component?.folder || "";
|
|
1750
|
+
options.appDirectory = options.appDirectory || componentLocation;
|
|
1751
|
+
const projectRoot = appProject.sourceRoot;
|
|
1516
1752
|
if (projectRoot) {
|
|
1517
|
-
logger.info(
|
|
1518
|
-
`Generating container model ${normalized.nameDashCase} in ${projectRoot}`
|
|
1519
|
-
);
|
|
1520
|
-
const modelNameDashCase = normalized.modelNameDashCase || normalized.nameDashCase;
|
|
1521
1753
|
generateFilesFromTemplates(
|
|
1522
1754
|
codegenFs,
|
|
1523
|
-
|
|
1524
|
-
path.join(
|
|
1525
|
-
|
|
1755
|
+
templateDir,
|
|
1756
|
+
path.join(
|
|
1757
|
+
projectRoot,
|
|
1758
|
+
options.appDirectory,
|
|
1759
|
+
"hooks",
|
|
1760
|
+
normalized.nameDashCase
|
|
1761
|
+
),
|
|
1762
|
+
normalized
|
|
1763
|
+
);
|
|
1764
|
+
appendBarrelExport(
|
|
1765
|
+
codegenFs,
|
|
1766
|
+
path.join(projectRoot, options.appDirectory, "hooks", "index.ts"),
|
|
1767
|
+
`./${normalized.nameDashCase}`
|
|
1526
1768
|
);
|
|
1527
|
-
const modelIndex = path.join(projectRoot, "index.ts");
|
|
1528
|
-
const modelPath = normalized.modelDirectory ? `${normalized.modelDirectory}/${modelNameDashCase}` : modelNameDashCase;
|
|
1529
|
-
updateModelIndex(codegenFs, modelIndex, modelPath);
|
|
1530
|
-
}
|
|
1531
|
-
}
|
|
1532
|
-
function generateCompanionModel(codegenFs, templateDir, options, projects) {
|
|
1533
|
-
const logger = getCodegenLogger();
|
|
1534
|
-
const normalized = normalizeAllValues({
|
|
1535
|
-
companionModelName: options.companionModelName,
|
|
1536
|
-
modelName: options.modelName
|
|
1537
|
-
});
|
|
1538
|
-
const companionChildKosConfig = getKosProjectConfiguration(
|
|
1539
|
-
codegenFs,
|
|
1540
|
-
options.companionModelProject,
|
|
1541
|
-
projects
|
|
1542
|
-
);
|
|
1543
|
-
const parentProject = findProjectByName(
|
|
1544
|
-
codegenFs.root,
|
|
1545
|
-
options.modelProject,
|
|
1546
|
-
projects
|
|
1547
|
-
);
|
|
1548
|
-
const childProject = findProjectByName(
|
|
1549
|
-
codegenFs.root,
|
|
1550
|
-
options.companionModelProject,
|
|
1551
|
-
projects
|
|
1552
|
-
);
|
|
1553
|
-
const projectRoot = childProject?.sourceRoot;
|
|
1554
|
-
if (!projectRoot) {
|
|
1555
|
-
logger.warn(`Companion child project source root not found`);
|
|
1556
|
-
return;
|
|
1557
|
-
}
|
|
1558
|
-
let importPath = "";
|
|
1559
|
-
if (parentProject) {
|
|
1560
|
-
const pkgJsonPath = path.join(parentProject.root, "package.json");
|
|
1561
|
-
try {
|
|
1562
|
-
const pkgJson = readJson(codegenFs, pkgJsonPath);
|
|
1563
|
-
importPath = pkgJson.name || "";
|
|
1564
|
-
} catch {
|
|
1565
|
-
importPath = "";
|
|
1566
|
-
}
|
|
1567
1769
|
}
|
|
1568
|
-
const modelLocation = companionChildKosConfig?.generator?.defaults?.model?.folder || "";
|
|
1569
|
-
const filePath = path.join(
|
|
1570
|
-
projectRoot,
|
|
1571
|
-
modelLocation,
|
|
1572
|
-
normalized.companionModelNameDashCase
|
|
1573
|
-
);
|
|
1574
|
-
logger.info(`Generating companion model in ${filePath}`);
|
|
1575
|
-
generateFilesFromTemplates(codegenFs, templateDir, filePath, {
|
|
1576
|
-
...options,
|
|
1577
|
-
...normalized,
|
|
1578
|
-
importPath
|
|
1579
|
-
});
|
|
1580
1770
|
}
|
|
1581
1771
|
function generateModel(params) {
|
|
1582
1772
|
const {
|
|
@@ -1675,6 +1865,55 @@ function generateModel(params) {
|
|
|
1675
1865
|
);
|
|
1676
1866
|
}
|
|
1677
1867
|
}
|
|
1868
|
+
const DECLARATION_MARKER = "@kosModel";
|
|
1869
|
+
function findModelDeclarationFile(codegenFs, searchRoot, typeIds) {
|
|
1870
|
+
const wanted = new Set(typeIds.filter(Boolean));
|
|
1871
|
+
if (wanted.size === 0) return null;
|
|
1872
|
+
const matches = [];
|
|
1873
|
+
for (const filePath of codegenFs.listFiles(searchRoot)) {
|
|
1874
|
+
if (!filePath.endsWith(".ts") || filePath.endsWith(".d.ts")) continue;
|
|
1875
|
+
const content = codegenFs.read(filePath);
|
|
1876
|
+
if (!content || !content.includes(DECLARATION_MARKER)) continue;
|
|
1877
|
+
const declared = readDeclaredModelType(codegenFs, filePath);
|
|
1878
|
+
if (declared && wanted.has(declared)) matches.push(filePath);
|
|
1879
|
+
}
|
|
1880
|
+
if (matches.length === 0) return null;
|
|
1881
|
+
if (matches.length > 1) {
|
|
1882
|
+
throw new Error(
|
|
1883
|
+
`Model type '${[...wanted].join("' / '")}' is declared in more than one file:
|
|
1884
|
+
` + matches.sort().map((c) => ` - ${c}`).join("\n") + `
|
|
1885
|
+
Pass modelPath to pick one.`
|
|
1886
|
+
);
|
|
1887
|
+
}
|
|
1888
|
+
return matches[0];
|
|
1889
|
+
}
|
|
1890
|
+
function readDeclaredModelType(codegenFs, filePath) {
|
|
1891
|
+
try {
|
|
1892
|
+
return readSourceFile(codegenFs, filePath, (sf) => {
|
|
1893
|
+
const cls = sf.getClasses().find((c) => c.getDecorator("kosModel"));
|
|
1894
|
+
if (!cls) return void 0;
|
|
1895
|
+
const config = decoratorConfigText(sf, cls, "kosModel");
|
|
1896
|
+
const inner = config?.startsWith("{") ? modelTypeIdProperty(config) : config;
|
|
1897
|
+
if (!inner) return void 0;
|
|
1898
|
+
return resolveToStringLiteral(inner.trim(), sf.getFullText());
|
|
1899
|
+
});
|
|
1900
|
+
} catch {
|
|
1901
|
+
return void 0;
|
|
1902
|
+
}
|
|
1903
|
+
}
|
|
1904
|
+
function modelTypeIdProperty(objectText) {
|
|
1905
|
+
const m = objectText.match(/\bmodelTypeId\s*:\s*([^,}]+)/);
|
|
1906
|
+
return m ? m[1] : void 0;
|
|
1907
|
+
}
|
|
1908
|
+
function resolveToStringLiteral(expression, fileText) {
|
|
1909
|
+
const literal = expression.match(/^["'`](.*)["'`]$/);
|
|
1910
|
+
if (literal) return literal[1];
|
|
1911
|
+
if (!/^[A-Za-z_$][\w$]*$/.test(expression)) return void 0;
|
|
1912
|
+
const declared = fileText.match(
|
|
1913
|
+
new RegExp(`\\b(?:const|let|var)\\s+${expression}\\s*(?::[^=]+)?=\\s*["'\`]([^"'\`]+)["'\`]`)
|
|
1914
|
+
);
|
|
1915
|
+
return declared ? declared[1] : void 0;
|
|
1916
|
+
}
|
|
1678
1917
|
function resolveModelFilePath(codegenFs, query, projects) {
|
|
1679
1918
|
const kosConfig = getKosProjectConfiguration(
|
|
1680
1919
|
codegenFs,
|
|
@@ -1709,14 +1948,23 @@ function resolveModelFilePath(codegenFs, query, projects) {
|
|
|
1709
1948
|
if (codegenFs.exists(modelFilePath)) {
|
|
1710
1949
|
return { modelFilePath, internal, sourceRoot };
|
|
1711
1950
|
}
|
|
1951
|
+
const searchRoot = path.join(sourceRoot, modelLocation);
|
|
1712
1952
|
const discovered = findModelFileByName(
|
|
1713
1953
|
codegenFs,
|
|
1714
|
-
|
|
1954
|
+
searchRoot,
|
|
1715
1955
|
modelNameDashCase
|
|
1716
1956
|
);
|
|
1717
1957
|
if (discovered) {
|
|
1718
1958
|
return { modelFilePath: discovered, internal, sourceRoot };
|
|
1719
1959
|
}
|
|
1960
|
+
const declaredType = kosConfig?.models?.[query.modelName]?.type;
|
|
1961
|
+
const byDeclaration = findModelDeclarationFile(codegenFs, searchRoot, [
|
|
1962
|
+
query.modelName,
|
|
1963
|
+
...declaredType ? [declaredType] : []
|
|
1964
|
+
]);
|
|
1965
|
+
if (byDeclaration) {
|
|
1966
|
+
return { modelFilePath: byDeclaration, internal, sourceRoot };
|
|
1967
|
+
}
|
|
1720
1968
|
return { modelFilePath, internal, sourceRoot };
|
|
1721
1969
|
}
|
|
1722
1970
|
function findModelFileByName(codegenFs, searchRoot, modelNameDashCase) {
|
|
@@ -1732,6 +1980,150 @@ Pass modelPath to pick one.`
|
|
|
1732
1980
|
}
|
|
1733
1981
|
return candidates[0];
|
|
1734
1982
|
}
|
|
1983
|
+
function resolveChildModelType(codegenFs, query, projects) {
|
|
1984
|
+
const childProject = query.childModelProject || query.modelProject;
|
|
1985
|
+
const childType = `${properCase(query.childModel)}Model`;
|
|
1986
|
+
if (childProject !== query.modelProject) {
|
|
1987
|
+
const project = findProjectByName(codegenFs.root, childProject, projects);
|
|
1988
|
+
const pkgJson = project ? readJson(
|
|
1989
|
+
codegenFs,
|
|
1990
|
+
path.join(project.root, "package.json")
|
|
1991
|
+
) : void 0;
|
|
1992
|
+
return { childType, childTypeModule: pkgJson?.name };
|
|
1993
|
+
}
|
|
1994
|
+
const { modelFilePath: childFilePath } = resolveModelFilePath(
|
|
1995
|
+
codegenFs,
|
|
1996
|
+
{ modelName: query.childModel, modelProject: childProject },
|
|
1997
|
+
projects
|
|
1998
|
+
);
|
|
1999
|
+
const relative = path.relative(path.dirname(query.modelFilePath), childFilePath).replace(/\.ts$/, "");
|
|
2000
|
+
return {
|
|
2001
|
+
childType,
|
|
2002
|
+
childTypeModule: relative.startsWith(".") ? relative : `./${relative}`
|
|
2003
|
+
};
|
|
2004
|
+
}
|
|
2005
|
+
function generateViewModel(codegenFs, templateDir, options, cwd, projects) {
|
|
2006
|
+
const logger = getCodegenLogger();
|
|
2007
|
+
if (!options.name) {
|
|
2008
|
+
throw new Error(
|
|
2009
|
+
"No ViewModel name found. Please specify a name with --name."
|
|
2010
|
+
);
|
|
2011
|
+
}
|
|
2012
|
+
const currentProject = getProject(codegenFs, cwd);
|
|
2013
|
+
const modelProjectName = options.modelProject || currentProject?.name;
|
|
2014
|
+
if (!modelProjectName) {
|
|
2015
|
+
throw new Error(
|
|
2016
|
+
"No model project found. Please specify a model project with --project."
|
|
2017
|
+
);
|
|
2018
|
+
}
|
|
2019
|
+
const normalized = normalizeOptions(
|
|
2020
|
+
codegenFs,
|
|
2021
|
+
{ ...options, modelProject: modelProjectName },
|
|
2022
|
+
projects
|
|
2023
|
+
);
|
|
2024
|
+
const projectConfig = findProjectByName(
|
|
2025
|
+
codegenFs.root,
|
|
2026
|
+
modelProjectName,
|
|
2027
|
+
projects
|
|
2028
|
+
);
|
|
2029
|
+
if (!projectConfig) {
|
|
2030
|
+
throw new Error(`Model project '${modelProjectName}' not found`);
|
|
2031
|
+
}
|
|
2032
|
+
const kosConfig = getKosProjectConfiguration(
|
|
2033
|
+
codegenFs,
|
|
2034
|
+
projectConfig.name,
|
|
2035
|
+
projects
|
|
2036
|
+
);
|
|
2037
|
+
const modelDirectory = options.modelDirectory || kosConfig?.generator?.defaults?.model?.folder || "";
|
|
2038
|
+
const internal = !!kosConfig?.generator?.internal;
|
|
2039
|
+
const projectRoot = projectConfig.sourceRoot || path.join(projectConfig.root, "src");
|
|
2040
|
+
const viewModelFolder = path.join(
|
|
2041
|
+
projectRoot,
|
|
2042
|
+
modelDirectory,
|
|
2043
|
+
normalized.nameDashCase
|
|
2044
|
+
);
|
|
2045
|
+
const viewModelFilePath = path.join(
|
|
2046
|
+
viewModelFolder,
|
|
2047
|
+
`${normalized.nameDashCase}-view-model.ts`
|
|
2048
|
+
);
|
|
2049
|
+
if (codegenFs.exists(viewModelFilePath)) {
|
|
2050
|
+
logger.info(`ViewModel already exists: ${viewModelFilePath}`);
|
|
2051
|
+
return { viewModelFilePath, created: false };
|
|
2052
|
+
}
|
|
2053
|
+
const resolved = (options.models || []).map(
|
|
2054
|
+
(source) => resolveSourceModel(
|
|
2055
|
+
codegenFs,
|
|
2056
|
+
source,
|
|
2057
|
+
viewModelFilePath,
|
|
2058
|
+
modelProjectName,
|
|
2059
|
+
projects
|
|
2060
|
+
)
|
|
2061
|
+
);
|
|
2062
|
+
logger.info(
|
|
2063
|
+
`Generating ViewModel ${normalized.nameDashCase} in ${projectRoot}`
|
|
2064
|
+
);
|
|
2065
|
+
const constructorParams = resolved.map(({ name, type }) => ({ name, type }));
|
|
2066
|
+
generateFilesFromTemplates(codegenFs, templateDir, viewModelFolder, {
|
|
2067
|
+
...normalized,
|
|
2068
|
+
internal,
|
|
2069
|
+
typeId: options.typeId || normalized.nameDashCase,
|
|
2070
|
+
devToolsEnabled: !!options.devToolsEnabled,
|
|
2071
|
+
constructorParams,
|
|
2072
|
+
constructorArgs: constructorParams.map((param) => param.name).join(", "),
|
|
2073
|
+
modelImports: groupImports(resolved)
|
|
2074
|
+
});
|
|
2075
|
+
updateModelIndex(
|
|
2076
|
+
codegenFs,
|
|
2077
|
+
path.join(projectRoot, "index.ts"),
|
|
2078
|
+
modelDirectory ? `${modelDirectory}/${normalized.nameDashCase}` : normalized.nameDashCase
|
|
2079
|
+
);
|
|
2080
|
+
return { viewModelFilePath, created: true };
|
|
2081
|
+
}
|
|
2082
|
+
function resolveSourceModel(codegenFs, source, viewModelFilePath, modelProject, projects) {
|
|
2083
|
+
const sourceProject = source.project || modelProject;
|
|
2084
|
+
if (sourceProject === modelProject) {
|
|
2085
|
+
const { modelFilePath } = resolveModelFilePath(
|
|
2086
|
+
codegenFs,
|
|
2087
|
+
{ modelName: source.model, modelProject: sourceProject },
|
|
2088
|
+
projects
|
|
2089
|
+
);
|
|
2090
|
+
if (!codegenFs.exists(modelFilePath)) {
|
|
2091
|
+
throw new Error(
|
|
2092
|
+
`Model '${source.model}' not found in project '${sourceProject}' (looked for ${modelFilePath})`
|
|
2093
|
+
);
|
|
2094
|
+
}
|
|
2095
|
+
} else if (!findProjectByName(codegenFs.root, sourceProject, projects)) {
|
|
2096
|
+
throw new Error(`Model project '${sourceProject}' not found`);
|
|
2097
|
+
}
|
|
2098
|
+
const { childType, childTypeModule } = resolveChildModelType(
|
|
2099
|
+
codegenFs,
|
|
2100
|
+
{
|
|
2101
|
+
modelFilePath: viewModelFilePath,
|
|
2102
|
+
modelProject,
|
|
2103
|
+
childModel: source.model,
|
|
2104
|
+
childModelProject: sourceProject
|
|
2105
|
+
},
|
|
2106
|
+
projects
|
|
2107
|
+
);
|
|
2108
|
+
return {
|
|
2109
|
+
name: camelCase(source.model),
|
|
2110
|
+
type: childType,
|
|
2111
|
+
module: childTypeModule
|
|
2112
|
+
};
|
|
2113
|
+
}
|
|
2114
|
+
function groupImports(resolved) {
|
|
2115
|
+
const byModule = /* @__PURE__ */ new Map();
|
|
2116
|
+
for (const { type, module } of resolved) {
|
|
2117
|
+
if (!module) continue;
|
|
2118
|
+
const types = byModule.get(module);
|
|
2119
|
+
if (!types) {
|
|
2120
|
+
byModule.set(module, [type]);
|
|
2121
|
+
} else if (!types.includes(type)) {
|
|
2122
|
+
types.push(type);
|
|
2123
|
+
}
|
|
2124
|
+
}
|
|
2125
|
+
return [...byModule].map(([module, types]) => ({ module, types }));
|
|
2126
|
+
}
|
|
1735
2127
|
function modelBaseName(modelFilePath, modelName) {
|
|
1736
2128
|
const base = path.basename(modelFilePath);
|
|
1737
2129
|
const match = base.match(/^(.*)-model\.ts$/);
|
|
@@ -1744,7 +2136,7 @@ function servicesFilePathFor(modelFilePath, modelBase) {
|
|
|
1744
2136
|
`${modelBase}-services.ts`
|
|
1745
2137
|
);
|
|
1746
2138
|
}
|
|
1747
|
-
function header(modelBase) {
|
|
2139
|
+
function header$1(modelBase) {
|
|
1748
2140
|
return `/**
|
|
1749
2141
|
* Service layer for the ${modelBase} model: the endpoints it calls and the
|
|
1750
2142
|
* types derived from them. Standalone service functions for callers outside a
|
|
@@ -1755,7 +2147,7 @@ function header(modelBase) {
|
|
|
1755
2147
|
function ensureServicesModule(codegenFs, modelFilePath, modelBase) {
|
|
1756
2148
|
const servicesFilePath = servicesFilePathFor(modelFilePath, modelBase);
|
|
1757
2149
|
if (!codegenFs.exists(servicesFilePath)) {
|
|
1758
|
-
codegenFs.write(servicesFilePath, header(modelBase));
|
|
2150
|
+
codegenFs.write(servicesFilePath, header$1(modelBase));
|
|
1759
2151
|
}
|
|
1760
2152
|
const barrelPath = path.join(path.dirname(servicesFilePath), "index.ts");
|
|
1761
2153
|
if (!codegenFs.exists(barrelPath)) {
|
|
@@ -2240,11 +2632,25 @@ function buildDecoratorArgs(options) {
|
|
|
2240
2632
|
}
|
|
2241
2633
|
function addContainerSupportToModel(codegenFs, options, projects) {
|
|
2242
2634
|
const logger = getCodegenLogger();
|
|
2243
|
-
const childType = options.childType?.trim() || "IKosDataModel";
|
|
2244
2635
|
const { modelFilePath } = resolveModelFilePath(codegenFs, options, projects);
|
|
2245
2636
|
if (!codegenFs.exists(modelFilePath)) {
|
|
2246
2637
|
throw new Error(`Model file not found: ${modelFilePath}`);
|
|
2247
2638
|
}
|
|
2639
|
+
const resolvedChild = options.childModel ? resolveChildModelType(
|
|
2640
|
+
codegenFs,
|
|
2641
|
+
{
|
|
2642
|
+
modelFilePath,
|
|
2643
|
+
modelProject: options.modelProject,
|
|
2644
|
+
childModel: options.childModel,
|
|
2645
|
+
childModelProject: options.childModelProject
|
|
2646
|
+
},
|
|
2647
|
+
projects
|
|
2648
|
+
) : {
|
|
2649
|
+
childType: options.childType,
|
|
2650
|
+
childTypeModule: options.childTypeModule
|
|
2651
|
+
};
|
|
2652
|
+
const childType = resolvedChild.childType?.trim() || "IKosDataModel";
|
|
2653
|
+
const childTypeModule = resolvedChild.childTypeModule;
|
|
2248
2654
|
logger.info(
|
|
2249
2655
|
`Adding container support (<${childType}>) to model: ${options.modelName}`
|
|
2250
2656
|
);
|
|
@@ -2256,6 +2662,10 @@ function addContainerSupportToModel(codegenFs, options, projects) {
|
|
|
2256
2662
|
]);
|
|
2257
2663
|
if (childType === "IKosDataModel") {
|
|
2258
2664
|
ensureNamedImport(sf, sdk, [{ name: "IKosDataModel", isTypeOnly: true }]);
|
|
2665
|
+
} else if (childTypeModule) {
|
|
2666
|
+
ensureNamedImport(sf, childTypeModule, [
|
|
2667
|
+
{ name: childType, isTypeOnly: true }
|
|
2668
|
+
]);
|
|
2259
2669
|
}
|
|
2260
2670
|
const cls = getModelClass(sf);
|
|
2261
2671
|
const className = cls.getName();
|
|
@@ -2285,38 +2695,172 @@ function addContainerSupportToModel(codegenFs, options, projects) {
|
|
|
2285
2695
|
});
|
|
2286
2696
|
return { modelFilePath };
|
|
2287
2697
|
}
|
|
2288
|
-
function
|
|
2698
|
+
function addParentAwareToModel(codegenFs, options, projects) {
|
|
2289
2699
|
const logger = getCodegenLogger();
|
|
2290
2700
|
const { modelFilePath } = resolveModelFilePath(codegenFs, options, projects);
|
|
2291
2701
|
if (!codegenFs.exists(modelFilePath)) {
|
|
2292
2702
|
throw new Error(`Model file not found: ${modelFilePath}`);
|
|
2293
2703
|
}
|
|
2294
|
-
logger.info(
|
|
2295
|
-
|
|
2296
|
-
);
|
|
2704
|
+
logger.info(`Adding parent awareness to model: ${options.modelName}`);
|
|
2705
|
+
let optionsTypeName;
|
|
2297
2706
|
transformSourceFile(codegenFs, modelFilePath, (sf) => {
|
|
2298
2707
|
const sdk = resolveSdkModuleSpecifier(sf);
|
|
2299
|
-
ensureNamedImport(sf, sdk, [{ name: "
|
|
2708
|
+
ensureNamedImport(sf, sdk, [{ name: "kosParentAware" }]);
|
|
2300
2709
|
const cls = getModelClass(sf);
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
name: options.methodName,
|
|
2304
|
-
decoratorName: "kosModelEffect",
|
|
2305
|
-
decoratorArgsText: `{ dependencies: (model: ${modelType}) => [] }`,
|
|
2306
|
-
isAsync: true,
|
|
2307
|
-
returnType: "Promise<void>",
|
|
2308
|
-
statements: "// TODO: react to the tracked dependencies"
|
|
2710
|
+
addClassDecorator(cls, "kosParentAware", {
|
|
2711
|
+
argsText: options.parentId ? `{ parentId: ${JSON.stringify(options.parentId)} }` : ""
|
|
2309
2712
|
});
|
|
2713
|
+
const ctor = cls.getConstructors()[0];
|
|
2714
|
+
optionsTypeName = ctor?.getParameters()[1]?.getTypeNode()?.getText()?.replace(/<.*$/, "");
|
|
2310
2715
|
});
|
|
2311
|
-
|
|
2716
|
+
const optionsFilePath = optionsTypeName ? extendOptionsInterface(codegenFs, modelFilePath, optionsTypeName, logger) : void 0;
|
|
2717
|
+
return { modelFilePath, optionsFilePath };
|
|
2312
2718
|
}
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2719
|
+
function extendOptionsInterface(codegenFs, modelFilePath, optionsTypeName, logger) {
|
|
2720
|
+
const typesPath = path.join(
|
|
2721
|
+
path.dirname(modelFilePath),
|
|
2722
|
+
"types",
|
|
2723
|
+
"index.d.ts"
|
|
2724
|
+
);
|
|
2725
|
+
const target = codegenFs.exists(typesPath) ? typesPath : modelFilePath;
|
|
2726
|
+
let extended = false;
|
|
2727
|
+
transformSourceFile(codegenFs, target, (sf) => {
|
|
2728
|
+
const iface = sf.getInterface(optionsTypeName);
|
|
2729
|
+
if (!iface) return;
|
|
2730
|
+
const already = iface.getExtends().some((clause) => clause.getText().includes("KosParentAware"));
|
|
2731
|
+
if (already) {
|
|
2732
|
+
extended = true;
|
|
2733
|
+
return;
|
|
2734
|
+
}
|
|
2735
|
+
ensureNamedImport(sf, "@kosdev-code/kos-ui-sdk", [
|
|
2736
|
+
{ name: "KosParentAware", isTypeOnly: true }
|
|
2737
|
+
]);
|
|
2738
|
+
iface.addExtends("KosParentAware");
|
|
2739
|
+
extended = true;
|
|
2740
|
+
});
|
|
2741
|
+
if (!extended) {
|
|
2742
|
+
logger.warn(
|
|
2743
|
+
`Could not find interface ${optionsTypeName}; add "extends KosParentAware" to it by hand.`
|
|
2744
|
+
);
|
|
2745
|
+
return void 0;
|
|
2746
|
+
}
|
|
2747
|
+
return target;
|
|
2748
|
+
}
|
|
2749
|
+
function firstDecoratorArg(decoratorText) {
|
|
2750
|
+
const open = decoratorText.indexOf("(");
|
|
2751
|
+
if (open === -1) return void 0;
|
|
2752
|
+
const inner = decoratorText.slice(open + 1, decoratorText.lastIndexOf(")")).replace(/\s+/g, " ").trim();
|
|
2753
|
+
return inner || void 0;
|
|
2754
|
+
}
|
|
2755
|
+
function collectDecorated(cls, decoratorName) {
|
|
2756
|
+
const members = [];
|
|
2757
|
+
const visit = (name, decoratorTextOf, typeText) => {
|
|
2758
|
+
const text = decoratorTextOf();
|
|
2759
|
+
if (text === void 0) return;
|
|
2760
|
+
members.push({
|
|
2761
|
+
name,
|
|
2762
|
+
arg: firstDecoratorArg(text),
|
|
2763
|
+
type: typeText || void 0
|
|
2764
|
+
});
|
|
2765
|
+
};
|
|
2766
|
+
for (const m of cls.getMethods()) {
|
|
2767
|
+
const dec = m.getDecorator(decoratorName);
|
|
2768
|
+
if (dec) {
|
|
2769
|
+
visit(m.getName(), () => dec.getText(), m.getReturnTypeNode()?.getText());
|
|
2770
|
+
}
|
|
2771
|
+
}
|
|
2772
|
+
for (const p of cls.getProperties()) {
|
|
2773
|
+
const dec = p.getDecorator(decoratorName);
|
|
2774
|
+
if (dec) {
|
|
2775
|
+
visit(p.getName(), () => dec.getText(), p.getTypeNode()?.getText());
|
|
2776
|
+
}
|
|
2777
|
+
}
|
|
2778
|
+
return members;
|
|
2779
|
+
}
|
|
2780
|
+
function describeModel(codegenFs, options, projects) {
|
|
2781
|
+
const { modelFilePath } = resolveModelFilePath(codegenFs, options, projects);
|
|
2782
|
+
const content = codegenFs.read(modelFilePath);
|
|
2783
|
+
if (content === null) {
|
|
2784
|
+
throw new Error(`Model file not found: ${modelFilePath}`);
|
|
2785
|
+
}
|
|
2786
|
+
const project = new Project({ useInMemoryFileSystem: true });
|
|
2787
|
+
const sf = project.createSourceFile(modelFilePath, content, {
|
|
2788
|
+
overwrite: true
|
|
2789
|
+
});
|
|
2790
|
+
let modelType;
|
|
2791
|
+
const modelTypeDecl = sf.getVariableDeclaration("MODEL_TYPE");
|
|
2792
|
+
if (modelTypeDecl) {
|
|
2793
|
+
const init = modelTypeDecl.getInitializer()?.getText();
|
|
2794
|
+
if (init) modelType = init.replace(/^["'`]|["'`]$/g, "");
|
|
2795
|
+
}
|
|
2796
|
+
const cls = sf.getClasses().find((c) => c.getDecorator("kosModel")) ?? sf.getClasses().find((c) => c.isExported()) ?? sf.getClasses()[0];
|
|
2797
|
+
if (!cls) {
|
|
2798
|
+
return {
|
|
2799
|
+
modelFilePath,
|
|
2800
|
+
modelType,
|
|
2801
|
+
classDecorators: [],
|
|
2802
|
+
singleton: false,
|
|
2803
|
+
isCompanion: false,
|
|
2804
|
+
children: [],
|
|
2805
|
+
dependencies: [],
|
|
2806
|
+
topicHandlers: [],
|
|
2807
|
+
configProperties: [],
|
|
2808
|
+
serviceRequests: [],
|
|
2809
|
+
effects: [],
|
|
2810
|
+
futures: []
|
|
2811
|
+
};
|
|
2812
|
+
}
|
|
2813
|
+
const classDecorators = cls.getDecorators().map((d) => d.getName());
|
|
2814
|
+
const kosModelArg = decoratorConfigText(sf, cls, "kosModel");
|
|
2815
|
+
const singleton = /\bsingleton\s*:\s*true\b/.test(kosModelArg ?? "");
|
|
2816
|
+
return {
|
|
2817
|
+
modelFilePath,
|
|
2818
|
+
modelType,
|
|
2819
|
+
className: cls.getName(),
|
|
2820
|
+
classDecorators,
|
|
2821
|
+
singleton,
|
|
2822
|
+
isCompanion: classDecorators.includes("kosCompanion"),
|
|
2823
|
+
children: collectDecorated(cls, "kosChild"),
|
|
2824
|
+
dependencies: collectDecorated(cls, "kosDependency"),
|
|
2825
|
+
topicHandlers: collectDecorated(cls, "kosTopicHandler"),
|
|
2826
|
+
configProperties: collectDecorated(cls, "kosConfigProperty"),
|
|
2827
|
+
serviceRequests: collectDecorated(cls, "kosServiceRequest"),
|
|
2828
|
+
effects: collectDecorated(cls, "kosModelEffect"),
|
|
2829
|
+
futures: collectDecorated(cls, "kosFuture")
|
|
2830
|
+
};
|
|
2831
|
+
}
|
|
2832
|
+
function addModelEffectToModel(codegenFs, options, projects) {
|
|
2833
|
+
const logger = getCodegenLogger();
|
|
2834
|
+
const { modelFilePath } = resolveModelFilePath(codegenFs, options, projects);
|
|
2835
|
+
if (!codegenFs.exists(modelFilePath)) {
|
|
2836
|
+
throw new Error(`Model file not found: ${modelFilePath}`);
|
|
2837
|
+
}
|
|
2838
|
+
logger.info(
|
|
2839
|
+
`Adding @kosModelEffect "${options.methodName}" to ${options.modelName}`
|
|
2840
|
+
);
|
|
2841
|
+
transformSourceFile(codegenFs, modelFilePath, (sf) => {
|
|
2842
|
+
const sdk = resolveSdkModuleSpecifier(sf);
|
|
2843
|
+
ensureNamedImport(sf, sdk, [{ name: "kosModelEffect" }]);
|
|
2844
|
+
const cls = getModelClass(sf);
|
|
2845
|
+
const modelType = (cls.getName() ?? "").replace(/Impl$/, "");
|
|
2846
|
+
addDecoratedMethod(cls, {
|
|
2847
|
+
name: options.methodName,
|
|
2848
|
+
decoratorName: "kosModelEffect",
|
|
2849
|
+
decoratorArgsText: `{ dependencies: (model: ${modelType}) => [] }`,
|
|
2850
|
+
isAsync: true,
|
|
2851
|
+
returnType: "Promise<void>",
|
|
2852
|
+
statements: "// TODO: react to the tracked dependencies"
|
|
2853
|
+
});
|
|
2854
|
+
});
|
|
2855
|
+
return { modelFilePath };
|
|
2856
|
+
}
|
|
2857
|
+
const FRAMEWORK_TYPES$1 = /* @__PURE__ */ new Set(["IKosDataModel", "IKosIdentifiable"]);
|
|
2858
|
+
function addDependencyToModel(codegenFs, options, projects) {
|
|
2859
|
+
const logger = getCodegenLogger();
|
|
2860
|
+
const { modelFilePath } = resolveModelFilePath(codegenFs, options, projects);
|
|
2861
|
+
if (!codegenFs.exists(modelFilePath)) {
|
|
2862
|
+
throw new Error(`Model file not found: ${modelFilePath}`);
|
|
2863
|
+
}
|
|
2320
2864
|
logger.info(
|
|
2321
2865
|
`Adding @kosDependency "${options.propertyName}" to ${options.modelName}`
|
|
2322
2866
|
);
|
|
@@ -2360,7 +2904,18 @@ function addChildToModel(codegenFs, options, projects) {
|
|
|
2360
2904
|
logger.info(
|
|
2361
2905
|
`Adding @kosChild "${options.propertyName}" (${shape}) to ${options.modelName}`
|
|
2362
2906
|
);
|
|
2363
|
-
const
|
|
2907
|
+
const resolvedChild = options.childModel ? resolveChildModelType(
|
|
2908
|
+
codegenFs,
|
|
2909
|
+
{
|
|
2910
|
+
modelFilePath,
|
|
2911
|
+
modelProject: options.modelProject,
|
|
2912
|
+
childModel: options.childModel,
|
|
2913
|
+
childModelProject: options.childModelProject
|
|
2914
|
+
},
|
|
2915
|
+
projects
|
|
2916
|
+
) : { childType: options.childType, childTypeModule: options.childPackage };
|
|
2917
|
+
const childType = resolvedChild.childType?.trim();
|
|
2918
|
+
const childTypeModule = resolvedChild.childTypeModule;
|
|
2364
2919
|
transformSourceFile(codegenFs, modelFilePath, (sf) => {
|
|
2365
2920
|
const sdk = resolveSdkModuleSpecifier(sf);
|
|
2366
2921
|
const sdkImports = [
|
|
@@ -2368,8 +2923,8 @@ function addChildToModel(codegenFs, options, projects) {
|
|
|
2368
2923
|
];
|
|
2369
2924
|
if (shape === "container") sdkImports.push({ name: "KosModelContainer" });
|
|
2370
2925
|
ensureNamedImport(sf, sdk, sdkImports);
|
|
2371
|
-
if (
|
|
2372
|
-
ensureNamedImport(sf,
|
|
2926
|
+
if (childTypeModule && childType && /^[A-Za-z_$][\w$]*$/.test(childType) && !FRAMEWORK_TYPES.has(childType)) {
|
|
2927
|
+
ensureNamedImport(sf, childTypeModule, [
|
|
2373
2928
|
{ name: childType, isTypeOnly: true }
|
|
2374
2929
|
]);
|
|
2375
2930
|
}
|
|
@@ -2515,6 +3070,154 @@ function addComputedToModel(codegenFs, options, projects) {
|
|
|
2515
3070
|
});
|
|
2516
3071
|
return { modelFilePath };
|
|
2517
3072
|
}
|
|
3073
|
+
function mocksFilePathFor(modelFilePath, modelBase) {
|
|
3074
|
+
return path.join(
|
|
3075
|
+
path.dirname(modelFilePath),
|
|
3076
|
+
"mocks",
|
|
3077
|
+
`${modelBase}-mocks.ts`
|
|
3078
|
+
);
|
|
3079
|
+
}
|
|
3080
|
+
function mockRegisterFunctionName(modelBase) {
|
|
3081
|
+
return `register${pascalCase(modelBase)}Mocks`;
|
|
3082
|
+
}
|
|
3083
|
+
function toMockRoutePattern(servicePath) {
|
|
3084
|
+
return servicePath.replace(/\{([^}]+)\}/g, ":$1");
|
|
3085
|
+
}
|
|
3086
|
+
function mockRouteCall(method, pattern, body) {
|
|
3087
|
+
const shorthand = {
|
|
3088
|
+
get: "get",
|
|
3089
|
+
post: "post",
|
|
3090
|
+
put: "put",
|
|
3091
|
+
delete: "del"
|
|
3092
|
+
};
|
|
3093
|
+
const fn = shorthand[method];
|
|
3094
|
+
return fn ? `KosMock.${fn}(${JSON.stringify(pattern)}, ${body});` : `KosMock.route(${JSON.stringify(method.toUpperCase())}, ${JSON.stringify(
|
|
3095
|
+
pattern
|
|
3096
|
+
)}, ${body});`;
|
|
3097
|
+
}
|
|
3098
|
+
function header(modelBase, registerFn) {
|
|
3099
|
+
return `/**
|
|
3100
|
+
* KosMock routes for the ${modelBase} model's PROVISIONAL endpoints — the ones
|
|
3101
|
+
* this project's generated OpenAPI types do not declare.
|
|
3102
|
+
*
|
|
3103
|
+
* NOTHING IMPORTS THIS FILE. Call \`${registerFn}()\` from the app's dev entry,
|
|
3104
|
+
* gated so it cannot run in a production build (\`import.meta.env.DEV\`, a
|
|
3105
|
+
* dev-only entry module, or behind your own switch). Registering a route enables
|
|
3106
|
+
* KosMock, and a mock that shipped would shadow the real endpoint once it exists.
|
|
3107
|
+
*
|
|
3108
|
+
* Unmatched requests still pass through to the real device (KosMock's hybrid
|
|
3109
|
+
* default), so these routes shadow only the paths named below. Every mocked
|
|
3110
|
+
* response carries the \`kos-mocked: true\` wire header, so mock-fed data is
|
|
3111
|
+
* identifiable in devtools.
|
|
3112
|
+
*
|
|
3113
|
+
* DELETE THIS FILE once every endpoint below is in the generated types.
|
|
3114
|
+
*/
|
|
3115
|
+
`;
|
|
3116
|
+
}
|
|
3117
|
+
function ensureMockRoute(codegenFs, options) {
|
|
3118
|
+
const mocksFilePath = mocksFilePathFor(
|
|
3119
|
+
options.modelFilePath,
|
|
3120
|
+
options.modelBase
|
|
3121
|
+
);
|
|
3122
|
+
const registerFunction = mockRegisterFunctionName(options.modelBase);
|
|
3123
|
+
const routePattern = toMockRoutePattern(options.servicePath);
|
|
3124
|
+
const sampleName = `SAMPLE_${constantCase(dashCase(options.endpointKey))}`;
|
|
3125
|
+
const samplePlaceholder = !options.sampleText;
|
|
3126
|
+
if (!codegenFs.exists(mocksFilePath)) {
|
|
3127
|
+
codegenFs.write(mocksFilePath, header(options.modelBase, registerFunction));
|
|
3128
|
+
}
|
|
3129
|
+
const sdkSpecifier = options.sdkModuleSpecifier.startsWith(".") ? `../${options.sdkModuleSpecifier}` : options.sdkModuleSpecifier;
|
|
3130
|
+
let routeCreated = false;
|
|
3131
|
+
transformSourceFile(codegenFs, mocksFilePath, (sf) => {
|
|
3132
|
+
ensureNamedImport(sf, sdkSpecifier, [{ name: "KosMock" }]);
|
|
3133
|
+
ensureNamedImport(sf, "../services", [
|
|
3134
|
+
{ name: options.rawAlias, isTypeOnly: true }
|
|
3135
|
+
]);
|
|
3136
|
+
ensureSample(sf, {
|
|
3137
|
+
name: sampleName,
|
|
3138
|
+
type: options.rawAlias,
|
|
3139
|
+
servicePath: options.servicePath,
|
|
3140
|
+
method: options.method,
|
|
3141
|
+
sampleText: options.sampleText
|
|
3142
|
+
});
|
|
3143
|
+
const statement = mockRouteCall(
|
|
3144
|
+
options.method,
|
|
3145
|
+
routePattern,
|
|
3146
|
+
`{ data: ${sampleName} }`
|
|
3147
|
+
);
|
|
3148
|
+
routeCreated = ensureRegisteredRoute(sf, {
|
|
3149
|
+
registerFunction,
|
|
3150
|
+
modelBase: options.modelBase,
|
|
3151
|
+
statement,
|
|
3152
|
+
// Matching on the pattern literal alone would collide across methods.
|
|
3153
|
+
marker: `${options.method.toUpperCase()} ${routePattern}`,
|
|
3154
|
+
matches: (existing) => existing.includes(JSON.stringify(routePattern)) && existing.includes(shorthandOrMethod(options.method))
|
|
3155
|
+
});
|
|
3156
|
+
});
|
|
3157
|
+
return {
|
|
3158
|
+
mocksFilePath,
|
|
3159
|
+
registerFunction,
|
|
3160
|
+
routePattern,
|
|
3161
|
+
sampleName,
|
|
3162
|
+
routeCreated,
|
|
3163
|
+
samplePlaceholder
|
|
3164
|
+
};
|
|
3165
|
+
}
|
|
3166
|
+
function shorthandOrMethod(method) {
|
|
3167
|
+
const shorthand = {
|
|
3168
|
+
get: "KosMock.get(",
|
|
3169
|
+
post: "KosMock.post(",
|
|
3170
|
+
put: "KosMock.put(",
|
|
3171
|
+
delete: "KosMock.del("
|
|
3172
|
+
};
|
|
3173
|
+
return shorthand[method] ?? `"${method.toUpperCase()}"`;
|
|
3174
|
+
}
|
|
3175
|
+
function ensureSample(sf, spec) {
|
|
3176
|
+
if (sf.getVariableDeclaration(spec.name)) return;
|
|
3177
|
+
const lines = [
|
|
3178
|
+
`Sample payload for \`${spec.method.toUpperCase()} ${spec.servicePath}\` — the`,
|
|
3179
|
+
"UNWRAPPED `data` payload, i.e. exactly what the transform receives."
|
|
3180
|
+
];
|
|
3181
|
+
if (!spec.sampleText) {
|
|
3182
|
+
lines.push(
|
|
3183
|
+
"",
|
|
3184
|
+
"TODO: replace the placeholder with something the backend would actually",
|
|
3185
|
+
"serve. Until then the route answers with nothing and the transform is",
|
|
3186
|
+
"never exercised."
|
|
3187
|
+
);
|
|
3188
|
+
}
|
|
3189
|
+
const docs = lines.join("\n");
|
|
3190
|
+
sf.addVariableStatement({
|
|
3191
|
+
isExported: true,
|
|
3192
|
+
declarationKind: VariableDeclarationKind.Const,
|
|
3193
|
+
docs: [docs],
|
|
3194
|
+
declarations: [
|
|
3195
|
+
{
|
|
3196
|
+
name: spec.name,
|
|
3197
|
+
type: spec.type,
|
|
3198
|
+
initializer: spec.sampleText ?? `null as unknown as ${spec.type}`
|
|
3199
|
+
}
|
|
3200
|
+
]
|
|
3201
|
+
});
|
|
3202
|
+
}
|
|
3203
|
+
function ensureRegisteredRoute(sf, spec) {
|
|
3204
|
+
let fn = sf.getFunction(spec.registerFunction);
|
|
3205
|
+
if (!fn) {
|
|
3206
|
+
fn = sf.addFunction({
|
|
3207
|
+
name: spec.registerFunction,
|
|
3208
|
+
isExported: true,
|
|
3209
|
+
returnType: "void",
|
|
3210
|
+
docs: [
|
|
3211
|
+
`Arm the ${spec.modelBase} model's provisional endpoints. Call this from a dev-only entry point — never from code that ships.`
|
|
3212
|
+
]
|
|
3213
|
+
});
|
|
3214
|
+
}
|
|
3215
|
+
const already = fn.getStatements().some((statement) => spec.matches(statement.getText()));
|
|
3216
|
+
if (already) return false;
|
|
3217
|
+
fn.addStatements(`// ${spec.marker}
|
|
3218
|
+
${spec.statement}`);
|
|
3219
|
+
return true;
|
|
3220
|
+
}
|
|
2518
3221
|
const SERVICE_MODULE_RE = /\/utils\/services\/.*\/service\.ts$/;
|
|
2519
3222
|
function findServiceModules(codegenFs, sourceRoot) {
|
|
2520
3223
|
return codegenFs.listFiles(sourceRoot).filter((f) => SERVICE_MODULE_RE.test(f.split(path.sep).join("/")));
|
|
@@ -2539,10 +3242,30 @@ function assertServiceModuleAcceptsTransform(codegenFs, serviceModuleFile, model
|
|
|
2539
3242
|
`${file} predates the transform-aware service layer (EndpointCtx takes ${typeParams} type parameter). Run \`kosui api:generate --project ${modelProject} --helpers-only\` to refresh the helper layer in place (no spec fetch, openapi.d.ts untouched), then add the service request.`
|
|
2540
3243
|
);
|
|
2541
3244
|
}
|
|
3245
|
+
const MUTATING_METHODS = /* @__PURE__ */ new Set(["post", "put", "delete", "patch"]);
|
|
3246
|
+
function assertLifecycleSuitsMethod(method, mode, methodName) {
|
|
3247
|
+
if (mode !== "lifecycle" || !MUTATING_METHODS.has(method)) return;
|
|
3248
|
+
throw new Error(
|
|
3249
|
+
`${methodName} is a ${method.toUpperCase()} with a lifecycle, so it would run every time the model reaches that phase rather than when something calls it — a mutation on every load. Drop \`lifecycle\` to get the method-driven form (the default for ${method.toUpperCase()}), or pass \`mode: "method"\`. If the phase really is the trigger, the model calls the method from its own lifecycle hook, where the call is visible.`
|
|
3250
|
+
);
|
|
3251
|
+
}
|
|
3252
|
+
function assertServiceModuleAcceptsProvisional(codegenFs, serviceModuleFile, modelProject) {
|
|
3253
|
+
const file = `${serviceModuleFile}.ts`;
|
|
3254
|
+
if (!codegenFs.exists(file)) return;
|
|
3255
|
+
const declared = readSourceFile(
|
|
3256
|
+
codegenFs,
|
|
3257
|
+
file,
|
|
3258
|
+
(sf) => Boolean(sf.getFunction("provisionalServiceRequest"))
|
|
3259
|
+
);
|
|
3260
|
+
if (declared) return;
|
|
3261
|
+
throw new Error(
|
|
3262
|
+
`${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.`
|
|
3263
|
+
);
|
|
3264
|
+
}
|
|
2542
3265
|
function sameEndpoint(existing, candidate) {
|
|
2543
3266
|
const args = (text) => {
|
|
2544
3267
|
const match = text.match(
|
|
2545
|
-
/^endpoint\s*\(\s*(['"])(.*?)\1\s*,\s*(['"])(.*?)\3\s*,?\s*\)$/
|
|
3268
|
+
/^endpoint\s*\(\s*(['"])(.*?)\1(?:\s+as\s+ApiPath)?\s*,\s*(['"])(.*?)\3\s*,?\s*\)$/
|
|
2546
3269
|
);
|
|
2547
3270
|
return match ? [match[2], match[4].toLowerCase()] : null;
|
|
2548
3271
|
};
|
|
@@ -2550,6 +3273,104 @@ function sameEndpoint(existing, candidate) {
|
|
|
2550
3273
|
const b = args(candidate.trim());
|
|
2551
3274
|
return !!a && !!b && a[0] === b[0] && a[1] === b[1];
|
|
2552
3275
|
}
|
|
3276
|
+
function editDistance(a, b) {
|
|
3277
|
+
let previous = Array.from({ length: b.length + 1 }, (_, i) => i);
|
|
3278
|
+
for (let i = 1; i <= a.length; i++) {
|
|
3279
|
+
const current = [i];
|
|
3280
|
+
for (let j = 1; j <= b.length; j++) {
|
|
3281
|
+
current[j] = Math.min(
|
|
3282
|
+
previous[j] + 1,
|
|
3283
|
+
current[j - 1] + 1,
|
|
3284
|
+
previous[j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1)
|
|
3285
|
+
);
|
|
3286
|
+
}
|
|
3287
|
+
previous = current;
|
|
3288
|
+
}
|
|
3289
|
+
return previous[b.length];
|
|
3290
|
+
}
|
|
3291
|
+
const NEAREST_SHOWN = 5;
|
|
3292
|
+
function provisionalEntryComment(servicePath, method, catalogName) {
|
|
3293
|
+
return `/**
|
|
3294
|
+
* PROVISIONAL — \`${method.toUpperCase()} ${servicePath}\` is NOT in this project's
|
|
3295
|
+
* generated OpenAPI types. \`as ApiPath\` is standing in for a \`paths\` entry that
|
|
3296
|
+
* does not exist, so nothing about this endpoint is checked against a spec and
|
|
3297
|
+
* its response cannot be derived from one. Do not read this as a real endpoint.
|
|
3298
|
+
*
|
|
3299
|
+
* WHEN THE ENDPOINT LANDS:
|
|
3300
|
+
* 1. regenerate this project's API types (\`kosui api:generate\`)
|
|
3301
|
+
* 2. delete \` as ApiPath\` from the entry below
|
|
3302
|
+
* 3. point the endpoint's \`…Raw\` alias at
|
|
3303
|
+
* \`EndpointResponse<typeof ${catalogName}.…>\`
|
|
3304
|
+
* 4. swap \`provisionalServiceRequest\` for \`serviceRequest\` in the model
|
|
3305
|
+
* 5. delete the model's \`mocks/\` module
|
|
3306
|
+
* 6. KEEP the transform — it is real code and survives all of the above
|
|
3307
|
+
*/
|
|
3308
|
+
`;
|
|
3309
|
+
}
|
|
3310
|
+
function provisionalMethodDocs(servicePath, method) {
|
|
3311
|
+
return [
|
|
3312
|
+
`PROVISIONAL — \`${method.toUpperCase()} ${servicePath}\` is not in the`,
|
|
3313
|
+
"generated OpenAPI types, so this request is served by a KosMock route, not",
|
|
3314
|
+
"by the device. The feature it backs is NOT complete: report the missing",
|
|
3315
|
+
"endpoint rather than treating this as finished.",
|
|
3316
|
+
"",
|
|
3317
|
+
"See the endpoint's entry in `./services` for what to undo when it lands."
|
|
3318
|
+
].join("\n");
|
|
3319
|
+
}
|
|
3320
|
+
function rawTypeDocs(servicePath, method, rawType) {
|
|
3321
|
+
const shape = rawType ? [
|
|
3322
|
+
"Stated by hand and unverified: there is no spec entry to check it",
|
|
3323
|
+
"against."
|
|
3324
|
+
] : [
|
|
3325
|
+
"TODO: state the shape the backend is expected to serve. `unknown`",
|
|
3326
|
+
"compiles, but leaves the response boundary undescribed and the",
|
|
3327
|
+
"transform with nothing to narrow."
|
|
3328
|
+
];
|
|
3329
|
+
return [
|
|
3330
|
+
`Raw wire shape of \`${method.toUpperCase()} ${servicePath}\` — the UNWRAPPED`,
|
|
3331
|
+
"`data` payload, since the client strips the `{status, data}` envelope",
|
|
3332
|
+
"before the transform runs.",
|
|
3333
|
+
"",
|
|
3334
|
+
...shape,
|
|
3335
|
+
"",
|
|
3336
|
+
"Replace with `EndpointResponse<…>` once the endpoint is in the generated",
|
|
3337
|
+
"types."
|
|
3338
|
+
].join("\n");
|
|
3339
|
+
}
|
|
3340
|
+
function removalSteps(spec) {
|
|
3341
|
+
return [
|
|
3342
|
+
`Regenerate the API types for ${spec.modelProject} (generate_api_types), and confirm ${spec.method.toUpperCase()} ${spec.servicePath} is now in them.`,
|
|
3343
|
+
`In ${spec.servicesFilePath}: delete \` as ApiPath\` from \`${spec.catalogName}.${spec.endpointKey}\`.`,
|
|
3344
|
+
`In ${spec.servicesFilePath}: point the endpoint's \`…Raw\` alias at \`EndpointResponse<typeof ${spec.catalogName}.${spec.endpointKey}>\` and drop the hand-written shape.`,
|
|
3345
|
+
`In the model: swap \`provisionalServiceRequest\` for \`serviceRequest\` and drop its explicit type arguments — the response type comes from the spec again.`,
|
|
3346
|
+
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.`,
|
|
3347
|
+
`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.`
|
|
3348
|
+
];
|
|
3349
|
+
}
|
|
3350
|
+
function untypedEndpointError(spec) {
|
|
3351
|
+
const methodsForPath = spec.operations.filter((op) => op.path === spec.servicePath).map((op) => op.method);
|
|
3352
|
+
const pathTypedForOtherMethods = methodsForPath.length > 0;
|
|
3353
|
+
const nearest = [...new Set(spec.operations.map((op) => op.path))].sort(
|
|
3354
|
+
(a, b) => editDistance(a, spec.servicePath) - editDistance(b, spec.servicePath)
|
|
3355
|
+
).slice(0, NEAREST_SHOWN);
|
|
3356
|
+
const waysForward = [
|
|
3357
|
+
`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.`,
|
|
3358
|
+
`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.`,
|
|
3359
|
+
`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.`
|
|
3360
|
+
];
|
|
3361
|
+
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.`;
|
|
3362
|
+
const error = new Error(message);
|
|
3363
|
+
error.details = {
|
|
3364
|
+
servicePath: spec.servicePath,
|
|
3365
|
+
method: spec.method,
|
|
3366
|
+
pathTypedForOtherMethods,
|
|
3367
|
+
methodsForPath,
|
|
3368
|
+
otherEndpointsInThisApi: nearest,
|
|
3369
|
+
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.",
|
|
3370
|
+
waysForward
|
|
3371
|
+
};
|
|
3372
|
+
return error;
|
|
3373
|
+
}
|
|
2553
3374
|
function addServiceRequestToModel(codegenFs, options, projects) {
|
|
2554
3375
|
const logger = getCodegenLogger();
|
|
2555
3376
|
const { modelFilePath, sourceRoot } = resolveModelFilePath(
|
|
@@ -2609,36 +3430,89 @@ function addServiceRequestToModel(codegenFs, options, projects) {
|
|
|
2609
3430
|
);
|
|
2610
3431
|
const method = (options.method || "get").toLowerCase();
|
|
2611
3432
|
const mode = options.mode ?? (options.lifecycle ? "lifecycle" : "method");
|
|
3433
|
+
assertLifecycleSuitsMethod(method, mode, options.methodName);
|
|
2612
3434
|
const lifecycle = options.lifecycle || "LOAD";
|
|
2613
3435
|
const catalogName = `${pascalCase(modelBase)}Endpoints`;
|
|
3436
|
+
const mockMode = options.mock ?? "auto";
|
|
3437
|
+
const operations = readServiceModuleOperations(codegenFs, serviceModuleFile);
|
|
3438
|
+
const pathValidated = operations !== null;
|
|
3439
|
+
const operationTyped = operations === null || operations.some(
|
|
3440
|
+
(op) => op.path === options.servicePath && op.method === method
|
|
3441
|
+
);
|
|
3442
|
+
if (!operationTyped && mockMode === "never") {
|
|
3443
|
+
throw untypedEndpointError({
|
|
3444
|
+
servicePath: options.servicePath,
|
|
3445
|
+
method,
|
|
3446
|
+
modelProject: options.modelProject,
|
|
3447
|
+
operations: operations ?? []
|
|
3448
|
+
});
|
|
3449
|
+
}
|
|
3450
|
+
const provisional = !operationTyped;
|
|
3451
|
+
const writeMock = provisional || mockMode === "always";
|
|
3452
|
+
if (provisional) {
|
|
3453
|
+
assertServiceModuleAcceptsProvisional(
|
|
3454
|
+
codegenFs,
|
|
3455
|
+
serviceModuleFile,
|
|
3456
|
+
options.modelProject
|
|
3457
|
+
);
|
|
3458
|
+
}
|
|
2614
3459
|
logger.info(
|
|
2615
|
-
`Adding ${mode}-driven @kosServiceRequest "${options.methodName}" (${method} ${options.servicePath}) to ${options.modelName}`
|
|
3460
|
+
`Adding ${mode}-driven ${provisional ? "PROVISIONAL " : ""}@kosServiceRequest "${options.methodName}" (${method} ${options.servicePath}) to ${options.modelName}`
|
|
2616
3461
|
);
|
|
3462
|
+
if (provisional) {
|
|
3463
|
+
logger.warn(
|
|
3464
|
+
`${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.`
|
|
3465
|
+
);
|
|
3466
|
+
}
|
|
2617
3467
|
ensureServicesModule(codegenFs, modelFilePath, modelBase);
|
|
2618
3468
|
let endpointKey = options.methodName;
|
|
2619
3469
|
let endpointCreated = true;
|
|
2620
3470
|
let dataAlias = "";
|
|
2621
3471
|
let mapperName = "";
|
|
2622
3472
|
let ctxAlias = "";
|
|
3473
|
+
let rawAlias = "";
|
|
2623
3474
|
transformSourceFile(codegenFs, servicesFilePath, (sf) => {
|
|
2624
3475
|
ensureNamedImport(sf, serviceImportFromServices, [{ name: "endpoint" }]);
|
|
3476
|
+
if (provisional) {
|
|
3477
|
+
ensureNamedImport(sf, serviceImportFromServices, [
|
|
3478
|
+
{ name: "ApiPath", isTypeOnly: true }
|
|
3479
|
+
]);
|
|
3480
|
+
}
|
|
3481
|
+
const pathText = provisional ? `${JSON.stringify(options.servicePath)} as ApiPath` : JSON.stringify(options.servicePath);
|
|
2625
3482
|
const entry = ensureConstCatalogEntry(sf, {
|
|
2626
3483
|
catalogName,
|
|
2627
3484
|
key: options.methodName,
|
|
2628
|
-
initializer: `endpoint(${JSON.stringify(
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
equals: sameEndpoint
|
|
3485
|
+
initializer: `endpoint(${pathText}, ${JSON.stringify(method)})`,
|
|
3486
|
+
equals: sameEndpoint,
|
|
3487
|
+
leadingTrivia: provisional ? provisionalEntryComment(options.servicePath, method, catalogName) : void 0
|
|
2632
3488
|
});
|
|
2633
3489
|
endpointKey = entry.key;
|
|
2634
3490
|
endpointCreated = entry.created;
|
|
2635
3491
|
dataAlias = `${pascalCase(endpointKey)}Data`;
|
|
2636
3492
|
mapperName = `to${pascalCase(endpointKey)}Data`;
|
|
2637
3493
|
ctxAlias = `${pascalCase(endpointKey)}Ctx`;
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
|
|
3494
|
+
rawAlias = `${pascalCase(endpointKey)}Raw`;
|
|
3495
|
+
let rawType;
|
|
3496
|
+
if (provisional) {
|
|
3497
|
+
ensureExportedTypeAlias(sf, {
|
|
3498
|
+
name: rawAlias,
|
|
3499
|
+
type: options.rawType || "unknown",
|
|
3500
|
+
docs: rawTypeDocs(options.servicePath, method, options.rawType)
|
|
3501
|
+
});
|
|
3502
|
+
rawType = rawAlias;
|
|
3503
|
+
} else {
|
|
3504
|
+
rawType = `EndpointResponse<typeof ${catalogName}.${endpointKey}>`;
|
|
3505
|
+
ensureNamedImport(sf, serviceImportFromServices, [
|
|
3506
|
+
{ name: "EndpointResponse", isTypeOnly: true }
|
|
3507
|
+
]);
|
|
3508
|
+
if (writeMock) {
|
|
3509
|
+
ensureExportedTypeAlias(sf, {
|
|
3510
|
+
name: rawAlias,
|
|
3511
|
+
type: rawType,
|
|
3512
|
+
docs: `Raw wire shape of \`${method.toUpperCase()} ${options.servicePath}\`, as the spec declares it — what a mock for this endpoint must serve.`
|
|
3513
|
+
});
|
|
3514
|
+
}
|
|
3515
|
+
}
|
|
2642
3516
|
ensureExportedTypeAlias(sf, { name: dataAlias, type: rawType });
|
|
2643
3517
|
ensureExportedMapper(sf, {
|
|
2644
3518
|
name: mapperName,
|
|
@@ -2659,20 +3533,25 @@ function addServiceRequestToModel(codegenFs, options, projects) {
|
|
|
2659
3533
|
const className = getModelClass(sf).getName();
|
|
2660
3534
|
if (!className) throw new Error("Model class has no name.");
|
|
2661
3535
|
const typeParams = getModelClass(sf).getTypeParameters().map((tp) => tp.getText());
|
|
2662
|
-
|
|
3536
|
+
const decoratorName = provisional ? "provisionalServiceRequest" : "serviceRequest";
|
|
3537
|
+
const decoratorTypeArgs = provisional ? [rawAlias, dataAlias] : void 0;
|
|
3538
|
+
ensureNamedImport(sf, serviceImportFromModel, [{ name: decoratorName }]);
|
|
2663
3539
|
if (mode === "lifecycle") {
|
|
2664
3540
|
ensureNamedImport(sf, "./services", [
|
|
2665
3541
|
{ name: catalogName },
|
|
2666
3542
|
{ name: mapperName },
|
|
2667
|
-
{ name: dataAlias, isTypeOnly: true }
|
|
3543
|
+
{ name: dataAlias, isTypeOnly: true },
|
|
3544
|
+
...provisional ? [{ name: rawAlias, isTypeOnly: true }] : []
|
|
2668
3545
|
]);
|
|
2669
3546
|
ensureNamedImport(sf, resolveSdkModuleSpecifier(sf), [
|
|
2670
3547
|
{ name: "DependencyLifecycle" }
|
|
2671
3548
|
]);
|
|
2672
3549
|
addDecoratedMethod(getModelClass(sf), {
|
|
2673
3550
|
name: options.methodName,
|
|
2674
|
-
decoratorName
|
|
3551
|
+
decoratorName,
|
|
3552
|
+
decoratorTypeArgs,
|
|
2675
3553
|
decoratorArgsText: `${catalogName}.${endpointKey}, { lifecycle: DependencyLifecycle.${lifecycle}, transform: ${mapperName} }`,
|
|
3554
|
+
docs: provisional ? provisionalMethodDocs(options.servicePath, method) : void 0,
|
|
2676
3555
|
// The manager invokes phase handlers error-first, passing (null, data)
|
|
2677
3556
|
// on success. The `error` half is only ever reached because
|
|
2678
3557
|
// `serviceRequest` supplies an errorHandler — the bare decorator
|
|
@@ -2689,7 +3568,11 @@ function addServiceRequestToModel(codegenFs, options, projects) {
|
|
|
2689
3568
|
ensureNamedImport(sf, "./services", [
|
|
2690
3569
|
{ name: catalogName },
|
|
2691
3570
|
{ name: mapperName },
|
|
2692
|
-
{ name: ctxAlias, isTypeOnly: true }
|
|
3571
|
+
{ name: ctxAlias, isTypeOnly: true },
|
|
3572
|
+
...provisional ? [
|
|
3573
|
+
{ name: rawAlias, isTypeOnly: true },
|
|
3574
|
+
{ name: dataAlias, isTypeOnly: true }
|
|
3575
|
+
] : []
|
|
2693
3576
|
]);
|
|
2694
3577
|
ensureNamedImport(sf, resolveSdkModuleSpecifier(sf), [
|
|
2695
3578
|
{ name: "executeServiceRequest" },
|
|
@@ -2704,8 +3587,10 @@ function addServiceRequestToModel(codegenFs, options, projects) {
|
|
|
2704
3587
|
addClassDecorator(getModelClass(sf), "kosLoggerAware", { argsText: "" });
|
|
2705
3588
|
addDecoratedMethod(getModelClass(sf), {
|
|
2706
3589
|
name: options.methodName,
|
|
2707
|
-
decoratorName
|
|
3590
|
+
decoratorName,
|
|
3591
|
+
decoratorTypeArgs,
|
|
2708
3592
|
decoratorArgsText: `${catalogName}.${endpointKey}, { transform: ${mapperName} }`,
|
|
3593
|
+
docs: provisional ? provisionalMethodDocs(options.servicePath, method) : void 0,
|
|
2709
3594
|
// Optional: the framework appends the context, callers never pass it.
|
|
2710
3595
|
parameters: [{ name: "$ctx", type: ctxAlias, optional: true }],
|
|
2711
3596
|
isAsync: true,
|
|
@@ -2717,172 +3602,43 @@ function addServiceRequestToModel(codegenFs, options, projects) {
|
|
|
2717
3602
|
].join("\n")
|
|
2718
3603
|
});
|
|
2719
3604
|
});
|
|
3605
|
+
const mock = writeMock ? ensureMockRoute(codegenFs, {
|
|
3606
|
+
modelFilePath,
|
|
3607
|
+
modelBase,
|
|
3608
|
+
sdkModuleSpecifier: readSourceFile(
|
|
3609
|
+
codegenFs,
|
|
3610
|
+
modelFilePath,
|
|
3611
|
+
resolveSdkModuleSpecifier
|
|
3612
|
+
),
|
|
3613
|
+
rawAlias,
|
|
3614
|
+
servicePath: options.servicePath,
|
|
3615
|
+
method,
|
|
3616
|
+
endpointKey,
|
|
3617
|
+
sampleText: options.sample
|
|
3618
|
+
}) : void 0;
|
|
2720
3619
|
return {
|
|
2721
3620
|
modelFilePath,
|
|
2722
3621
|
servicesFilePath,
|
|
2723
3622
|
serviceModule: serviceImportFromModel,
|
|
2724
3623
|
mode,
|
|
2725
3624
|
endpointKey,
|
|
2726
|
-
endpointCreated
|
|
2727
|
-
|
|
2728
|
-
|
|
2729
|
-
|
|
2730
|
-
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
|
|
2736
|
-
|
|
2737
|
-
|
|
2738
|
-
|
|
2739
|
-
|
|
2740
|
-
|
|
2741
|
-
|
|
2742
|
-
|
|
2743
|
-
const project = new Project({ useInMemoryFileSystem: true });
|
|
2744
|
-
const sf = project.createSourceFile(modelFilePath, content, {
|
|
2745
|
-
overwrite: true
|
|
2746
|
-
});
|
|
2747
|
-
const findings = [];
|
|
2748
|
-
const hasKosModel = sf.getClasses().some((c) => c.getDecorator("kosModel"));
|
|
2749
|
-
if (!hasKosModel) {
|
|
2750
|
-
findings.push({
|
|
2751
|
-
level: "error",
|
|
2752
|
-
rule: "missing-kosModel",
|
|
2753
|
-
message: "No @kosModel decorator found — this is not a KOS model."
|
|
2754
|
-
});
|
|
2755
|
-
}
|
|
2756
|
-
const imports = sf.getImportDeclarations();
|
|
2757
|
-
const mobxImport = imports.find(
|
|
2758
|
-
(d) => /^mobx(-react-lite)?$/.test(d.getModuleSpecifierValue())
|
|
2759
|
-
);
|
|
2760
|
-
if (mobxImport) {
|
|
2761
|
-
findings.push({
|
|
2762
|
-
level: "error",
|
|
2763
|
-
rule: "mobx-import",
|
|
2764
|
-
message: "Imports mobx directly. Reactivity is internal to KOS — remove the mobx import and use KOS reactivity / container models."
|
|
2765
|
-
});
|
|
2766
|
-
}
|
|
2767
|
-
const barrelServiceRequest = imports.find(
|
|
2768
|
-
(d) => d.getModuleSpecifierValue() === "@kosdev-code/kos-ui-sdk" && d.getNamedImports().some((n) => n.getName() === "kosServiceRequest")
|
|
2769
|
-
);
|
|
2770
|
-
if (barrelServiceRequest) {
|
|
2771
|
-
findings.push({
|
|
2772
|
-
level: "warning",
|
|
2773
|
-
rule: "untyped-service-request",
|
|
2774
|
-
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."
|
|
2775
|
-
});
|
|
2776
|
-
}
|
|
2777
|
-
for (const cls of sf.getClasses()) {
|
|
2778
|
-
for (const prop of cls.getProperties()) {
|
|
2779
|
-
const name = prop.getName();
|
|
2780
|
-
const typeText = (prop.getTypeNode()?.getText() ?? "").replace(/\s+/g, " ").trim();
|
|
2781
|
-
const initText = prop.getInitializer()?.getText() ?? "";
|
|
2782
|
-
const hasChild = !!prop.getDecorator("kosChild");
|
|
2783
|
-
const isModelContainer = /\bI?KosModelContainer\s*</.test(typeText) || /new\s+KosModelContainer\b/.test(initText);
|
|
2784
|
-
if (isModelContainer && !hasChild) {
|
|
2785
|
-
findings.push({
|
|
2786
|
-
level: "warning",
|
|
2787
|
-
rule: "container-missing-kosChild",
|
|
2788
|
-
message: `Property '${name}' is a model container but is not marked @kosChild — add @kosChild so its models join the model graph and lifecycle.`
|
|
2789
|
-
});
|
|
2790
|
-
continue;
|
|
2791
|
-
}
|
|
2792
|
-
const elem = modelElementType(typeText);
|
|
2793
|
-
if (elem && !isModelContainer) {
|
|
2794
|
-
findings.push({
|
|
2795
|
-
level: "warning",
|
|
2796
|
-
rule: "raw-model-collection",
|
|
2797
|
-
message: `Property '${name}' is a raw ${typeText} of models — prefer a KosModelContainer<${elem}> (indexing, sorting, lifecycle, delta handling) marked @kosChild.`
|
|
2798
|
-
});
|
|
2799
|
-
}
|
|
2800
|
-
}
|
|
2801
|
-
}
|
|
2802
|
-
const hasError = findings.some((f) => f.level === "error");
|
|
2803
|
-
return { modelFilePath, ok: !hasError, findings };
|
|
2804
|
-
}
|
|
2805
|
-
function firstDecoratorArg(decoratorText) {
|
|
2806
|
-
const open = decoratorText.indexOf("(");
|
|
2807
|
-
if (open === -1) return void 0;
|
|
2808
|
-
const inner = decoratorText.slice(open + 1, decoratorText.lastIndexOf(")")).replace(/\s+/g, " ").trim();
|
|
2809
|
-
return inner || void 0;
|
|
2810
|
-
}
|
|
2811
|
-
function collectDecorated(cls, decoratorName) {
|
|
2812
|
-
const members = [];
|
|
2813
|
-
const visit = (name, decoratorTextOf, typeText) => {
|
|
2814
|
-
const text = decoratorTextOf();
|
|
2815
|
-
if (text === void 0) return;
|
|
2816
|
-
members.push({
|
|
2817
|
-
name,
|
|
2818
|
-
arg: firstDecoratorArg(text),
|
|
2819
|
-
type: typeText || void 0
|
|
2820
|
-
});
|
|
2821
|
-
};
|
|
2822
|
-
for (const m of cls.getMethods()) {
|
|
2823
|
-
const dec = m.getDecorator(decoratorName);
|
|
2824
|
-
if (dec) {
|
|
2825
|
-
visit(m.getName(), () => dec.getText(), m.getReturnTypeNode()?.getText());
|
|
2826
|
-
}
|
|
2827
|
-
}
|
|
2828
|
-
for (const p of cls.getProperties()) {
|
|
2829
|
-
const dec = p.getDecorator(decoratorName);
|
|
2830
|
-
if (dec) {
|
|
2831
|
-
visit(p.getName(), () => dec.getText(), p.getTypeNode()?.getText());
|
|
2832
|
-
}
|
|
2833
|
-
}
|
|
2834
|
-
return members;
|
|
2835
|
-
}
|
|
2836
|
-
function describeModel(codegenFs, options, projects) {
|
|
2837
|
-
const { modelFilePath } = resolveModelFilePath(codegenFs, options, projects);
|
|
2838
|
-
const content = codegenFs.read(modelFilePath);
|
|
2839
|
-
if (content === null) {
|
|
2840
|
-
throw new Error(`Model file not found: ${modelFilePath}`);
|
|
2841
|
-
}
|
|
2842
|
-
const project = new Project({ useInMemoryFileSystem: true });
|
|
2843
|
-
const sf = project.createSourceFile(modelFilePath, content, {
|
|
2844
|
-
overwrite: true
|
|
2845
|
-
});
|
|
2846
|
-
let modelType;
|
|
2847
|
-
const modelTypeDecl = sf.getVariableDeclaration("MODEL_TYPE");
|
|
2848
|
-
if (modelTypeDecl) {
|
|
2849
|
-
const init = modelTypeDecl.getInitializer()?.getText();
|
|
2850
|
-
if (init) modelType = init.replace(/^["'`]|["'`]$/g, "");
|
|
2851
|
-
}
|
|
2852
|
-
const cls = sf.getClasses().find((c) => c.getDecorator("kosModel")) ?? sf.getClasses().find((c) => c.isExported()) ?? sf.getClasses()[0];
|
|
2853
|
-
if (!cls) {
|
|
2854
|
-
return {
|
|
2855
|
-
modelFilePath,
|
|
2856
|
-
modelType,
|
|
2857
|
-
classDecorators: [],
|
|
2858
|
-
singleton: false,
|
|
2859
|
-
isCompanion: false,
|
|
2860
|
-
children: [],
|
|
2861
|
-
dependencies: [],
|
|
2862
|
-
topicHandlers: [],
|
|
2863
|
-
configProperties: [],
|
|
2864
|
-
serviceRequests: [],
|
|
2865
|
-
effects: [],
|
|
2866
|
-
futures: []
|
|
2867
|
-
};
|
|
2868
|
-
}
|
|
2869
|
-
const classDecorators = cls.getDecorators().map((d) => d.getName());
|
|
2870
|
-
const kosModelArg = cls.getDecorator("kosModel") ? firstDecoratorArg(cls.getDecorator("kosModel").getText()) : void 0;
|
|
2871
|
-
const singleton = /\bsingleton\s*:\s*true\b/.test(kosModelArg ?? "");
|
|
2872
|
-
return {
|
|
2873
|
-
modelFilePath,
|
|
2874
|
-
modelType,
|
|
2875
|
-
className: cls.getName(),
|
|
2876
|
-
classDecorators,
|
|
2877
|
-
singleton,
|
|
2878
|
-
isCompanion: classDecorators.includes("kosCompanion"),
|
|
2879
|
-
children: collectDecorated(cls, "kosChild"),
|
|
2880
|
-
dependencies: collectDecorated(cls, "kosDependency"),
|
|
2881
|
-
topicHandlers: collectDecorated(cls, "kosTopicHandler"),
|
|
2882
|
-
configProperties: collectDecorated(cls, "kosConfigProperty"),
|
|
2883
|
-
serviceRequests: collectDecorated(cls, "kosServiceRequest"),
|
|
2884
|
-
effects: collectDecorated(cls, "kosModelEffect"),
|
|
2885
|
-
futures: collectDecorated(cls, "kosFuture")
|
|
3625
|
+
endpointCreated,
|
|
3626
|
+
pathValidated,
|
|
3627
|
+
provisional,
|
|
3628
|
+
mocksFilePath: mock?.mocksFilePath,
|
|
3629
|
+
mockRegisterFunction: mock?.registerFunction,
|
|
3630
|
+
mockRoute: mock && `${method.toUpperCase()} ${mock.routePattern}`,
|
|
3631
|
+
mockSamplePlaceholder: mock?.samplePlaceholder,
|
|
3632
|
+
removalSteps: provisional ? removalSteps({
|
|
3633
|
+
modelProject: options.modelProject,
|
|
3634
|
+
servicePath: options.servicePath,
|
|
3635
|
+
method,
|
|
3636
|
+
catalogName,
|
|
3637
|
+
endpointKey,
|
|
3638
|
+
servicesFilePath,
|
|
3639
|
+
mocksFilePath: mock?.mocksFilePath,
|
|
3640
|
+
mapperName
|
|
3641
|
+
}) : void 0
|
|
2886
3642
|
};
|
|
2887
3643
|
}
|
|
2888
3644
|
const DEFAULT_SDK_PACKAGE = "@kosdev-code/kos-ui-sdk";
|
|
@@ -3068,6 +3824,82 @@ function lookupSdkType(codegenFs, options, projects) {
|
|
|
3068
3824
|
)}). Check the name, or it may be internal / not part of the public surface.`
|
|
3069
3825
|
};
|
|
3070
3826
|
}
|
|
3827
|
+
function modelElementType(typeText) {
|
|
3828
|
+
let m;
|
|
3829
|
+
if (m = typeText.match(/^([A-Za-z_]\w*Model)\s*\[\]$/)) return m[1];
|
|
3830
|
+
if (m = typeText.match(/^Array<\s*([A-Za-z_]\w*Model)\s*>$/)) return m[1];
|
|
3831
|
+
if (m = typeText.match(/^(?:Map|Set)<[^>]*?\b([A-Za-z_]\w*Model)\b[^>]*>$/))
|
|
3832
|
+
return m[1];
|
|
3833
|
+
return null;
|
|
3834
|
+
}
|
|
3835
|
+
function validateModel(codegenFs, options, projects) {
|
|
3836
|
+
const { modelFilePath } = resolveModelFilePath(codegenFs, options, projects);
|
|
3837
|
+
const content = codegenFs.read(modelFilePath);
|
|
3838
|
+
if (content === null) {
|
|
3839
|
+
throw new Error(`Model file not found: ${modelFilePath}`);
|
|
3840
|
+
}
|
|
3841
|
+
const project = new Project({ useInMemoryFileSystem: true });
|
|
3842
|
+
const sf = project.createSourceFile(modelFilePath, content, {
|
|
3843
|
+
overwrite: true
|
|
3844
|
+
});
|
|
3845
|
+
const findings = [];
|
|
3846
|
+
const hasKosModel = sf.getClasses().some((c) => c.getDecorator("kosModel"));
|
|
3847
|
+
if (!hasKosModel) {
|
|
3848
|
+
findings.push({
|
|
3849
|
+
level: "error",
|
|
3850
|
+
rule: "missing-kosModel",
|
|
3851
|
+
message: "No @kosModel decorator found — this is not a KOS model."
|
|
3852
|
+
});
|
|
3853
|
+
}
|
|
3854
|
+
const imports = sf.getImportDeclarations();
|
|
3855
|
+
const mobxImport = imports.find(
|
|
3856
|
+
(d) => /^mobx(-react-lite)?$/.test(d.getModuleSpecifierValue())
|
|
3857
|
+
);
|
|
3858
|
+
if (mobxImport) {
|
|
3859
|
+
findings.push({
|
|
3860
|
+
level: "error",
|
|
3861
|
+
rule: "mobx-import",
|
|
3862
|
+
message: "Imports mobx directly. Reactivity is internal to KOS — remove the mobx import and use KOS reactivity / container models."
|
|
3863
|
+
});
|
|
3864
|
+
}
|
|
3865
|
+
const barrelServiceRequest = imports.find(
|
|
3866
|
+
(d) => d.getModuleSpecifierValue() === "@kosdev-code/kos-ui-sdk" && d.getNamedImports().some((n) => n.getName() === "kosServiceRequest")
|
|
3867
|
+
);
|
|
3868
|
+
if (barrelServiceRequest) {
|
|
3869
|
+
findings.push({
|
|
3870
|
+
level: "warning",
|
|
3871
|
+
rule: "untyped-service-request",
|
|
3872
|
+
message: "kosServiceRequest imported from the SDK barrel. Use the typed decorator from the api:generate'd service module so paths are validated against the OpenAPI types."
|
|
3873
|
+
});
|
|
3874
|
+
}
|
|
3875
|
+
for (const cls of sf.getClasses()) {
|
|
3876
|
+
for (const prop of cls.getProperties()) {
|
|
3877
|
+
const name = prop.getName();
|
|
3878
|
+
const typeText = (prop.getTypeNode()?.getText() ?? "").replace(/\s+/g, " ").trim();
|
|
3879
|
+
const initText = prop.getInitializer()?.getText() ?? "";
|
|
3880
|
+
const hasChild = !!prop.getDecorator("kosChild");
|
|
3881
|
+
const isModelContainer = /\bI?KosModelContainer\s*</.test(typeText) || /new\s+KosModelContainer\b/.test(initText);
|
|
3882
|
+
if (isModelContainer && !hasChild) {
|
|
3883
|
+
findings.push({
|
|
3884
|
+
level: "warning",
|
|
3885
|
+
rule: "container-missing-kosChild",
|
|
3886
|
+
message: `Property '${name}' is a model container but is not marked @kosChild — add @kosChild so its models join the model graph and lifecycle.`
|
|
3887
|
+
});
|
|
3888
|
+
continue;
|
|
3889
|
+
}
|
|
3890
|
+
const elem = modelElementType(typeText);
|
|
3891
|
+
if (elem && !isModelContainer) {
|
|
3892
|
+
findings.push({
|
|
3893
|
+
level: "warning",
|
|
3894
|
+
rule: "raw-model-collection",
|
|
3895
|
+
message: `Property '${name}' is a raw ${typeText} of models — prefer a KosModelContainer<${elem}> (indexing, sorting, lifecycle, delta handling) marked @kosChild.`
|
|
3896
|
+
});
|
|
3897
|
+
}
|
|
3898
|
+
}
|
|
3899
|
+
}
|
|
3900
|
+
const hasError = findings.some((f) => f.level === "error");
|
|
3901
|
+
return { modelFilePath, ok: !hasError, findings };
|
|
3902
|
+
}
|
|
3071
3903
|
const PLUGIN_TYPES = {
|
|
3072
3904
|
CUI: "cui",
|
|
3073
3905
|
UTILITY: "utility",
|
|
@@ -3622,9 +4454,76 @@ function updatePluginConfiguration(codegenFs, projectConfig, options) {
|
|
|
3622
4454
|
);
|
|
3623
4455
|
}
|
|
3624
4456
|
}
|
|
4457
|
+
function parse(content) {
|
|
4458
|
+
const singleQuoted = /^\s*import\b[^"']*'/m.test(content);
|
|
4459
|
+
const project = new Project({
|
|
4460
|
+
useInMemoryFileSystem: true,
|
|
4461
|
+
manipulationSettings: {
|
|
4462
|
+
indentationText: IndentationText.TwoSpaces,
|
|
4463
|
+
quoteKind: singleQuoted ? QuoteKind.Single : QuoteKind.Double
|
|
4464
|
+
}
|
|
4465
|
+
});
|
|
4466
|
+
return project.createSourceFile("registration-chain.ts", content, {
|
|
4467
|
+
overwrite: true
|
|
4468
|
+
});
|
|
4469
|
+
}
|
|
4470
|
+
function chainCalls(sf, name) {
|
|
4471
|
+
return sf.getDescendantsOfKind(SyntaxKind.CallExpression).filter((call) => {
|
|
4472
|
+
const callee = call.getExpression();
|
|
4473
|
+
return callee.getKind() === SyntaxKind.PropertyAccessExpression && callee.asKindOrThrow(SyntaxKind.PropertyAccessExpression).getName() === name;
|
|
4474
|
+
});
|
|
4475
|
+
}
|
|
4476
|
+
function indentOfCall(sf, call) {
|
|
4477
|
+
const text = sf.getFullText();
|
|
4478
|
+
const nameNode = call.getExpression().asKindOrThrow(SyntaxKind.PropertyAccessExpression).getNameNode();
|
|
4479
|
+
const lineStart = text.lastIndexOf("\n", nameNode.getStart()) + 1;
|
|
4480
|
+
return text.slice(lineStart).match(/^[ \t]*/)?.[0] ?? "";
|
|
4481
|
+
}
|
|
4482
|
+
function countChainEntries(content) {
|
|
4483
|
+
return chainCalls(parse(content), "model").length;
|
|
4484
|
+
}
|
|
4485
|
+
function upsertChainEntry(content, bean, importSpec) {
|
|
4486
|
+
const sf = parse(content);
|
|
4487
|
+
const modelCalls = chainCalls(sf, "model");
|
|
4488
|
+
const registered = modelCalls.some((call) => {
|
|
4489
|
+
const [arg, ...rest] = call.getArguments();
|
|
4490
|
+
return rest.length === 0 && arg?.getText() === bean;
|
|
4491
|
+
});
|
|
4492
|
+
if (registered) return { changed: false, note: "already registered" };
|
|
4493
|
+
let anchor;
|
|
4494
|
+
let indent;
|
|
4495
|
+
if (modelCalls.length > 0) {
|
|
4496
|
+
anchor = modelCalls.reduce(
|
|
4497
|
+
(last, call) => call.getEnd() > last.getEnd() ? call : last
|
|
4498
|
+
);
|
|
4499
|
+
indent = indentOfCall(sf, anchor);
|
|
4500
|
+
} else {
|
|
4501
|
+
const starts = chainCalls(sf, "models").filter(
|
|
4502
|
+
(call) => call.getArguments().length === 0
|
|
4503
|
+
);
|
|
4504
|
+
anchor = starts[starts.length - 1];
|
|
4505
|
+
if (!anchor) {
|
|
4506
|
+
return {
|
|
4507
|
+
changed: false,
|
|
4508
|
+
error: "no .models() chain found in the registration file"
|
|
4509
|
+
};
|
|
4510
|
+
}
|
|
4511
|
+
const text = sf.getFullText();
|
|
4512
|
+
const lineStart = text.lastIndexOf("\n", anchor.getStart()) + 1;
|
|
4513
|
+
indent = (text.slice(lineStart).match(/^[ \t]*/)?.[0] ?? "") + " ";
|
|
4514
|
+
}
|
|
4515
|
+
anchor.replaceWithText(`${anchor.getText()}
|
|
4516
|
+
${indent}.model(${bean})`);
|
|
4517
|
+
const imported = sf.getImportDeclarations().some(
|
|
4518
|
+
(decl) => decl.getNamedImports().some((named) => named.getName() === bean)
|
|
4519
|
+
);
|
|
4520
|
+
if (!imported) ensureNamedImport(sf, importSpec, [{ name: bean }]);
|
|
4521
|
+
return { changed: true, content: sf.getFullText(), importAdded: !imported };
|
|
4522
|
+
}
|
|
3625
4523
|
export {
|
|
3626
4524
|
BasePluginHandler,
|
|
3627
4525
|
CONTRIBUTION_TYPE_MAP,
|
|
4526
|
+
DEFAULT_MODEL_LIBS_DIR,
|
|
3628
4527
|
DirectFileSystem,
|
|
3629
4528
|
KAB_OUTPUT_DIR,
|
|
3630
4529
|
KAB_OUTPUT_PATH,
|
|
@@ -3646,6 +4545,7 @@ export {
|
|
|
3646
4545
|
addJavaArtifactToManifests,
|
|
3647
4546
|
addKosModelConfiguration,
|
|
3648
4547
|
addModelEffectToModel,
|
|
4548
|
+
addParentAwareToModel,
|
|
3649
4549
|
addPropertyToModel,
|
|
3650
4550
|
addServiceRequestToModel,
|
|
3651
4551
|
addTopicHandlerToModel,
|
|
@@ -3654,6 +4554,7 @@ export {
|
|
|
3654
4554
|
buildSbomTarget,
|
|
3655
4555
|
camelCase,
|
|
3656
4556
|
constantCase,
|
|
4557
|
+
countChainEntries,
|
|
3657
4558
|
dashCase,
|
|
3658
4559
|
describeModel,
|
|
3659
4560
|
discoverJavaArtifacts,
|
|
@@ -3672,8 +4573,10 @@ export {
|
|
|
3672
4573
|
generateHook,
|
|
3673
4574
|
generateInit,
|
|
3674
4575
|
generateModel,
|
|
4576
|
+
generateModelProject,
|
|
3675
4577
|
generatePolyglotWorkspace,
|
|
3676
4578
|
generateSplashProject,
|
|
4579
|
+
generateViewModel,
|
|
3677
4580
|
getCodegenLogger,
|
|
3678
4581
|
getCurrentDirectoryName,
|
|
3679
4582
|
getKosModelConfigProp,
|
|
@@ -3691,10 +4594,12 @@ export {
|
|
|
3691
4594
|
readNxJson,
|
|
3692
4595
|
resolveKabPath,
|
|
3693
4596
|
resolveModelFilePath,
|
|
4597
|
+
resolveModelProjectLayout,
|
|
3694
4598
|
setCodegenLogger,
|
|
3695
4599
|
syncCiManifests,
|
|
3696
4600
|
updateJson,
|
|
3697
4601
|
updateModelIndex,
|
|
4602
|
+
upsertChainEntry,
|
|
3698
4603
|
validateModel,
|
|
3699
4604
|
writeJson
|
|
3700
4605
|
};
|