@kosdev-code/kos-codegen-core 3.0.19 → 3.0.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.d.ts +2 -2
- package/index.d.ts.map +1 -1
- package/index.js +1527 -569
- package/index.js.map +1 -1
- package/index.mjs +1523 -565
- 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-future-to-model/normalize-options.d.ts.map +1 -1
- package/lib/generators/add-future-to-model/service-transformer.d.ts +1 -3
- package/lib/generators/add-future-to-model/service-transformer.d.ts.map +1 -1
- package/lib/generators/add-parent-aware/generate-add-parent-aware.d.ts +15 -0
- package/lib/generators/add-parent-aware/generate-add-parent-aware.d.ts.map +1 -0
- package/lib/generators/add-parent-aware/index.d.ts +2 -0
- package/lib/generators/add-parent-aware/index.d.ts.map +1 -0
- package/lib/generators/augment/find-model-declaration.d.ts +11 -0
- package/lib/generators/augment/find-model-declaration.d.ts.map +1 -0
- package/lib/generators/augment/model-mocks.d.ts +47 -0
- package/lib/generators/augment/model-mocks.d.ts.map +1 -0
- package/lib/generators/augment/model-services.d.ts +11 -0
- package/lib/generators/augment/model-services.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 +61 -1
- package/lib/generators/augment/ts-toolkit.d.ts.map +1 -1
- package/lib/generators/generate-container-model.d.ts +7 -1
- package/lib/generators/generate-container-model.d.ts.map +1 -1
- package/lib/generators/generate-init.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-model.d.ts +0 -1
- package/lib/generators/generate-model.d.ts.map +1 -1
- package/lib/generators/generate-view-model.d.ts +35 -0
- package/lib/generators/generate-view-model.d.ts.map +1 -0
- package/lib/generators/index.d.ts +21 -16
- package/lib/generators/index.d.ts.map +1 -1
- package/lib/generators/member-mutators/add-child.d.ts +10 -2
- package/lib/generators/member-mutators/add-child.d.ts.map +1 -1
- package/lib/generators/member-mutators/add-service-request.d.ts +80 -2
- package/lib/generators/member-mutators/add-service-request.d.ts.map +1 -1
- package/lib/generators/member-mutators/index.d.ts +1 -1
- package/lib/generators/member-mutators/index.d.ts.map +1 -1
- package/lib/generators/registration/upsert-chain-entry.d.ts +13 -0
- package/lib/generators/registration/upsert-chain-entry.d.ts.map +1 -0
- package/lib/generators/service-catalog.d.ts +12 -0
- package/lib/generators/service-catalog.d.ts.map +1 -1
- package/package.json +2 -2
- package/templates/kos-container-model/model/index.ts.template +0 -2
- package/templates/kos-container-model/model/types/index.d.ts.template +2 -2
- package/templates/kos-model/model/index.ts__template__ +0 -3
- 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/templates/kos-container-model/services/__modelNameDashCase__-services.ts.template +0 -22
- package/templates/kos-container-model/services/index.ts.template +0 -1
- package/templates/kos-model/services/__nameDashCase__-services.ts.template +0 -58
- package/templates/kos-model/services/index.ts.template +0 -1
package/index.js
CHANGED
|
@@ -496,6 +496,84 @@ function generateSplashProject(codegenFs, templateDir, options) {
|
|
|
496
496
|
normalized
|
|
497
497
|
);
|
|
498
498
|
}
|
|
499
|
+
const MODEL_PROJECT_SUFFIX = "-models";
|
|
500
|
+
const DEFAULT_MODEL_LIBS_DIR = "libs";
|
|
501
|
+
const JSONC_ESLINT_PARSER_VERSION = "^2.1.0";
|
|
502
|
+
function resolveModelProjectLayout(codegenFs, options) {
|
|
503
|
+
const base = dashCase(options.name);
|
|
504
|
+
const projectName = base.endsWith(MODEL_PROJECT_SUFFIX) ? base : `${base}${MODEL_PROJECT_SUFFIX}`;
|
|
505
|
+
const libsDir = (options.libsDir || DEFAULT_MODEL_LIBS_DIR).replace(
|
|
506
|
+
/\\/g,
|
|
507
|
+
"/"
|
|
508
|
+
);
|
|
509
|
+
const scope = codegenFs ? readNpmScope(codegenFs) : void 0;
|
|
510
|
+
return {
|
|
511
|
+
projectName,
|
|
512
|
+
projectRoot: `${libsDir}/${projectName}`.replace(/^\/+/, ""),
|
|
513
|
+
importPath: scope ? `@${scope}/${projectName}` : projectName
|
|
514
|
+
};
|
|
515
|
+
}
|
|
516
|
+
function readNpmScope(codegenFs) {
|
|
517
|
+
if (!codegenFs.exists("package.json")) {
|
|
518
|
+
return void 0;
|
|
519
|
+
}
|
|
520
|
+
const name = readJson(codegenFs, "package.json").name;
|
|
521
|
+
if (!name?.startsWith("@")) {
|
|
522
|
+
return void 0;
|
|
523
|
+
}
|
|
524
|
+
return name.split("/")[0].slice(1);
|
|
525
|
+
}
|
|
526
|
+
function offsetFromRoot(projectRoot) {
|
|
527
|
+
return projectRoot.split("/").filter(Boolean).map(() => "../").join("");
|
|
528
|
+
}
|
|
529
|
+
function generateModelProject(codegenFs, templateDir, options) {
|
|
530
|
+
const layout = resolveModelProjectLayout(codegenFs, options);
|
|
531
|
+
generateFilesFromTemplates(
|
|
532
|
+
codegenFs,
|
|
533
|
+
path__namespace.join(templateDir, "project"),
|
|
534
|
+
layout.projectRoot,
|
|
535
|
+
{
|
|
536
|
+
...layout,
|
|
537
|
+
projectNameCamelCase: camelCase(layout.projectName),
|
|
538
|
+
offsetFromRoot: offsetFromRoot(layout.projectRoot),
|
|
539
|
+
template: ""
|
|
540
|
+
}
|
|
541
|
+
);
|
|
542
|
+
registerTsconfigPath(codegenFs, layout);
|
|
543
|
+
ensureJsoncEslintParser(codegenFs);
|
|
544
|
+
return layout;
|
|
545
|
+
}
|
|
546
|
+
function registerTsconfigPath(codegenFs, layout) {
|
|
547
|
+
if (!codegenFs.exists("tsconfig.base.json")) {
|
|
548
|
+
return;
|
|
549
|
+
}
|
|
550
|
+
updateJson(codegenFs, "tsconfig.base.json", (json) => {
|
|
551
|
+
json.compilerOptions = json.compilerOptions ?? {};
|
|
552
|
+
json.compilerOptions.paths = json.compilerOptions.paths ?? {};
|
|
553
|
+
json.compilerOptions.paths[layout.importPath] = [
|
|
554
|
+
`${layout.projectRoot}/src/index.ts`
|
|
555
|
+
];
|
|
556
|
+
return json;
|
|
557
|
+
});
|
|
558
|
+
}
|
|
559
|
+
function ensureJsoncEslintParser(codegenFs) {
|
|
560
|
+
if (!codegenFs.exists("package.json")) {
|
|
561
|
+
return;
|
|
562
|
+
}
|
|
563
|
+
const pkg = readJson(codegenFs, "package.json");
|
|
564
|
+
const devDependencies = pkg.devDependencies ?? {};
|
|
565
|
+
if ("jsonc-eslint-parser" in devDependencies) {
|
|
566
|
+
return;
|
|
567
|
+
}
|
|
568
|
+
devDependencies["jsonc-eslint-parser"] = JSONC_ESLINT_PARSER_VERSION;
|
|
569
|
+
pkg.devDependencies = Object.fromEntries(
|
|
570
|
+
// Codepoint order, matching what Nx's own dependency helper produces.
|
|
571
|
+
Object.entries(devDependencies).sort(
|
|
572
|
+
([a], [b]) => a < b ? -1 : a > b ? 1 : 0
|
|
573
|
+
)
|
|
574
|
+
);
|
|
575
|
+
writeJson(codegenFs, "package.json", pkg);
|
|
576
|
+
}
|
|
499
577
|
function generateInit(codegenFs, options) {
|
|
500
578
|
const logger = getCodegenLogger();
|
|
501
579
|
const { appProject, modelProject, registrationProject } = options;
|
|
@@ -514,7 +592,6 @@ function generateInit(codegenFs, options) {
|
|
|
514
592
|
modelDirectory: "lib",
|
|
515
593
|
appDirectory: "app",
|
|
516
594
|
components: true,
|
|
517
|
-
dataServices: true,
|
|
518
595
|
internal: false,
|
|
519
596
|
singleton: false,
|
|
520
597
|
unitTests: true
|
|
@@ -948,6 +1025,58 @@ function normalizeOptions(codegenFs, options, projects) {
|
|
|
948
1025
|
template: ""
|
|
949
1026
|
};
|
|
950
1027
|
}
|
|
1028
|
+
const UPDATE_RELEASE_VERSION_SCRIPT = `import devkit from "@nx/devkit";
|
|
1029
|
+
import { resolve } from "path";
|
|
1030
|
+
import { readFileSync, writeFileSync } from "fs";
|
|
1031
|
+
import prettier from "prettier";
|
|
1032
|
+
|
|
1033
|
+
// KOS artifact versioning: stamps the project's .kos.json "version" field
|
|
1034
|
+
// (which kabtool bakes into the KAB). Never touches package.json.
|
|
1035
|
+
// Driven by tag-based releases:
|
|
1036
|
+
// nx run-many --target=version --args=--ver=$KOSBUILD_VERSION
|
|
1037
|
+
|
|
1038
|
+
const { readCachedProjectGraph } = devkit;
|
|
1039
|
+
const [, , name, versionArg] = process.argv;
|
|
1040
|
+
|
|
1041
|
+
// "{args.ver}" arrives literally when the target runs without --args=--ver=<v>;
|
|
1042
|
+
// treat that (or a missing arg) as "report current version, change nothing".
|
|
1043
|
+
const version =
|
|
1044
|
+
versionArg && !versionArg.startsWith("{args") ? versionArg : undefined;
|
|
1045
|
+
|
|
1046
|
+
if (!name) {
|
|
1047
|
+
console.error("usage: update-release-version.mjs <project> <version>");
|
|
1048
|
+
process.exit(1);
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
const graph = readCachedProjectGraph();
|
|
1052
|
+
const project = graph.nodes[name];
|
|
1053
|
+
if (!project) {
|
|
1054
|
+
console.error("Unknown project: " + name);
|
|
1055
|
+
process.exit(1);
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
const kosJsonPath = resolve(process.cwd(), project.data.root, ".kos.json");
|
|
1059
|
+
let kosJson;
|
|
1060
|
+
try {
|
|
1061
|
+
kosJson = JSON.parse(readFileSync(kosJsonPath, "utf8"));
|
|
1062
|
+
} catch {
|
|
1063
|
+
console.error("Missing or invalid .kos.json: " + kosJsonPath);
|
|
1064
|
+
process.exit(1);
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
if (!version) {
|
|
1068
|
+
console.log(name + ": " + kosJson.version + " (no --ver given; unchanged)");
|
|
1069
|
+
process.exit(0);
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
const prettierOptions = await prettier.resolveConfig(kosJsonPath);
|
|
1073
|
+
const output = await prettier.format(
|
|
1074
|
+
JSON.stringify({ ...kosJson, version }, null, 2),
|
|
1075
|
+
{ ...prettierOptions, parser: "json" }
|
|
1076
|
+
);
|
|
1077
|
+
writeFileSync(kosJsonPath, output);
|
|
1078
|
+
console.log(name + ": version -> " + version);
|
|
1079
|
+
`;
|
|
951
1080
|
function transformSourceFile(codegenFs, filePath, mutate) {
|
|
952
1081
|
const content = codegenFs.read(filePath);
|
|
953
1082
|
if (content === null) {
|
|
@@ -1053,12 +1182,137 @@ function ensureModuleConst(sourceFile, spec) {
|
|
|
1053
1182
|
});
|
|
1054
1183
|
return true;
|
|
1055
1184
|
}
|
|
1185
|
+
function afterImports(sourceFile) {
|
|
1186
|
+
const imports = sourceFile.getImportDeclarations();
|
|
1187
|
+
return imports.length > 0 ? imports[imports.length - 1].getChildIndex() + 1 : 0;
|
|
1188
|
+
}
|
|
1189
|
+
function unquote$1(name) {
|
|
1190
|
+
return name.replace(/^["']|["']$/g, "");
|
|
1191
|
+
}
|
|
1192
|
+
function getConstCatalogLiteral(sourceFile, name) {
|
|
1193
|
+
const declaration = sourceFile.getVariableDeclaration(name);
|
|
1194
|
+
if (!declaration) return null;
|
|
1195
|
+
let initializer = declaration.getInitializer();
|
|
1196
|
+
if (tsMorph.Node.isAsExpression(initializer)) {
|
|
1197
|
+
initializer = initializer.getExpression();
|
|
1198
|
+
}
|
|
1199
|
+
if (!initializer || !tsMorph.Node.isObjectLiteralExpression(initializer)) {
|
|
1200
|
+
throw new Error(
|
|
1201
|
+
`"${name}" exists in ${sourceFile.getBaseName()} but is not an object literal.`
|
|
1202
|
+
);
|
|
1203
|
+
}
|
|
1204
|
+
return initializer;
|
|
1205
|
+
}
|
|
1206
|
+
function ensureConstCatalogEntry(sourceFile, spec) {
|
|
1207
|
+
const matches = spec.equals ?? ((existing, candidate) => existing === candidate);
|
|
1208
|
+
let literal = getConstCatalogLiteral(sourceFile, spec.catalogName);
|
|
1209
|
+
if (!literal) {
|
|
1210
|
+
sourceFile.insertVariableStatement(afterImports(sourceFile), {
|
|
1211
|
+
isExported: true,
|
|
1212
|
+
declarationKind: tsMorph.VariableDeclarationKind.Const,
|
|
1213
|
+
declarations: [{ name: spec.catalogName, initializer: "{} as const" }]
|
|
1214
|
+
});
|
|
1215
|
+
literal = getConstCatalogLiteral(sourceFile, spec.catalogName);
|
|
1216
|
+
if (!literal) {
|
|
1217
|
+
throw new Error(`Failed to create catalog "${spec.catalogName}".`);
|
|
1218
|
+
}
|
|
1219
|
+
literal.addPropertyAssignment({
|
|
1220
|
+
name: spec.key,
|
|
1221
|
+
initializer: spec.initializer,
|
|
1222
|
+
leadingTrivia: spec.leadingTrivia
|
|
1223
|
+
});
|
|
1224
|
+
return { key: spec.key, created: true };
|
|
1225
|
+
}
|
|
1226
|
+
for (const property of literal.getProperties().filter(tsMorph.Node.isPropertyAssignment)) {
|
|
1227
|
+
const existingKey = unquote$1(property.getName());
|
|
1228
|
+
const existingInitializer = property.getInitializerOrThrow().getText();
|
|
1229
|
+
if (matches(existingInitializer, spec.initializer)) {
|
|
1230
|
+
return { key: existingKey, created: false };
|
|
1231
|
+
}
|
|
1232
|
+
if (existingKey === spec.key) {
|
|
1233
|
+
throw new Error(
|
|
1234
|
+
`"${spec.catalogName}.${spec.key}" already exists with a different value (${existingInitializer}). Use a different methodName, or reconcile the catalog by hand.`
|
|
1235
|
+
);
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
literal.addPropertyAssignment({
|
|
1239
|
+
name: spec.key,
|
|
1240
|
+
initializer: spec.initializer,
|
|
1241
|
+
leadingTrivia: spec.leadingTrivia
|
|
1242
|
+
});
|
|
1243
|
+
return { key: spec.key, created: true };
|
|
1244
|
+
}
|
|
1245
|
+
function ensureExportedTypeAlias(sourceFile, spec) {
|
|
1246
|
+
if (sourceFile.getTypeAlias(spec.name)) return false;
|
|
1247
|
+
sourceFile.addTypeAlias({
|
|
1248
|
+
name: spec.name,
|
|
1249
|
+
type: spec.type,
|
|
1250
|
+
isExported: true,
|
|
1251
|
+
docs: spec.docs ? [spec.docs] : void 0
|
|
1252
|
+
});
|
|
1253
|
+
return true;
|
|
1254
|
+
}
|
|
1255
|
+
function ensureExportedMapper(sourceFile, spec) {
|
|
1256
|
+
if (sourceFile.getVariableDeclaration(spec.name)) return false;
|
|
1257
|
+
sourceFile.addVariableStatement({
|
|
1258
|
+
isExported: true,
|
|
1259
|
+
declarationKind: tsMorph.VariableDeclarationKind.Const,
|
|
1260
|
+
declarations: [
|
|
1261
|
+
{
|
|
1262
|
+
name: spec.name,
|
|
1263
|
+
initializer: `(raw: ${spec.rawType}): ${spec.dataType} => raw`
|
|
1264
|
+
}
|
|
1265
|
+
]
|
|
1266
|
+
});
|
|
1267
|
+
return true;
|
|
1268
|
+
}
|
|
1269
|
+
function ensureBarrelExport(sourceFile, moduleSpecifier) {
|
|
1270
|
+
const present = sourceFile.getExportDeclarations().some((d) => d.getModuleSpecifierValue() === moduleSpecifier);
|
|
1271
|
+
if (present) return false;
|
|
1272
|
+
sourceFile.addExportDeclaration({ moduleSpecifier });
|
|
1273
|
+
return true;
|
|
1274
|
+
}
|
|
1275
|
+
function ensureNamedExports(sourceFile, moduleSpecifier, names, isTypeOnly = false) {
|
|
1276
|
+
const existing = sourceFile.getExportDeclarations().find(
|
|
1277
|
+
(d) => d.getModuleSpecifierValue() === moduleSpecifier && d.isTypeOnly() === isTypeOnly
|
|
1278
|
+
);
|
|
1279
|
+
if (!existing) {
|
|
1280
|
+
sourceFile.addExportDeclaration({
|
|
1281
|
+
moduleSpecifier,
|
|
1282
|
+
isTypeOnly,
|
|
1283
|
+
namedExports: names.map((name) => {
|
|
1284
|
+
return { name };
|
|
1285
|
+
})
|
|
1286
|
+
});
|
|
1287
|
+
return true;
|
|
1288
|
+
}
|
|
1289
|
+
const present = new Set(existing.getNamedExports().map((e) => e.getName()));
|
|
1290
|
+
const missing = names.filter((name) => !present.has(name));
|
|
1291
|
+
if (missing.length === 0) return false;
|
|
1292
|
+
existing.addNamedExports(
|
|
1293
|
+
missing.map((name) => {
|
|
1294
|
+
return { name };
|
|
1295
|
+
})
|
|
1296
|
+
);
|
|
1297
|
+
return true;
|
|
1298
|
+
}
|
|
1299
|
+
function ensureExportedInterface(sourceFile, name, properties = [], extendsTypes = []) {
|
|
1300
|
+
if (sourceFile.getInterface(name)) return false;
|
|
1301
|
+
sourceFile.addInterface({
|
|
1302
|
+
name,
|
|
1303
|
+
isExported: true,
|
|
1304
|
+
extends: extendsTypes,
|
|
1305
|
+
properties
|
|
1306
|
+
});
|
|
1307
|
+
return true;
|
|
1308
|
+
}
|
|
1056
1309
|
function addDecoratedMethod(cls, spec) {
|
|
1057
1310
|
if (cls.getMethod(spec.name)) return false;
|
|
1058
1311
|
cls.addMethod({
|
|
1059
1312
|
name: spec.name,
|
|
1060
1313
|
isAsync: spec.isAsync,
|
|
1061
1314
|
returnType: spec.returnType,
|
|
1315
|
+
docs: spec.docs ? [spec.docs] : void 0,
|
|
1062
1316
|
parameters: spec.parameters?.map((p) => {
|
|
1063
1317
|
return { name: p.name, type: p.type, hasQuestionToken: p.optional };
|
|
1064
1318
|
}),
|
|
@@ -1066,6 +1320,7 @@ function addDecoratedMethod(cls, spec) {
|
|
|
1066
1320
|
decorators: [
|
|
1067
1321
|
{
|
|
1068
1322
|
name: spec.decoratorName,
|
|
1323
|
+
typeArguments: spec.decoratorTypeArgs,
|
|
1069
1324
|
arguments: spec.decoratorArgsText ? [spec.decoratorArgsText] : []
|
|
1070
1325
|
}
|
|
1071
1326
|
]
|
|
@@ -1148,17 +1403,28 @@ function listServiceCatalog(codegenFs, query, projects) {
|
|
|
1148
1403
|
app,
|
|
1149
1404
|
version,
|
|
1150
1405
|
serviceModulePath: posix,
|
|
1151
|
-
operations: readOperations(codegenFs, openapiPath)
|
|
1406
|
+
operations: readOperations(codegenFs, openapiPath) ?? []
|
|
1152
1407
|
});
|
|
1153
1408
|
}
|
|
1154
1409
|
return entries.sort(
|
|
1155
1410
|
(a, b) => a.app.localeCompare(b.app) || a.version.localeCompare(b.version)
|
|
1156
1411
|
);
|
|
1157
1412
|
}
|
|
1413
|
+
function readServiceModuleOperations(codegenFs, serviceModuleFile) {
|
|
1414
|
+
const moduleFile = serviceModuleFile.endsWith(".ts") ? serviceModuleFile : `${serviceModuleFile}.ts`;
|
|
1415
|
+
const posix = moduleFile.split(path__namespace.sep).join("/");
|
|
1416
|
+
const openapiPath = posix.replace(/service\.ts$/, "openapi.d.ts");
|
|
1417
|
+
for (const candidate of [openapiPath, posix]) {
|
|
1418
|
+
if (!codegenFs.exists(candidate)) continue;
|
|
1419
|
+
const operations = readOperations(codegenFs, candidate);
|
|
1420
|
+
if (operations) return operations;
|
|
1421
|
+
}
|
|
1422
|
+
return null;
|
|
1423
|
+
}
|
|
1158
1424
|
function readOperations(codegenFs, openapiPath) {
|
|
1159
1425
|
return readSourceFile(codegenFs, openapiPath, (sf) => {
|
|
1160
1426
|
const paths = sf.getInterface("paths");
|
|
1161
|
-
if (!paths) return
|
|
1427
|
+
if (!paths) return null;
|
|
1162
1428
|
const operations = [];
|
|
1163
1429
|
for (const pathProp of paths.getProperties()) {
|
|
1164
1430
|
const servicePath = unquote(pathProp.getName());
|
|
@@ -1192,58 +1458,6 @@ function readSummary(methodProp) {
|
|
|
1192
1458
|
function unquote(name) {
|
|
1193
1459
|
return name.replace(/^["']|["']$/g, "");
|
|
1194
1460
|
}
|
|
1195
|
-
const UPDATE_RELEASE_VERSION_SCRIPT = `import devkit from "@nx/devkit";
|
|
1196
|
-
import { resolve } from "path";
|
|
1197
|
-
import { readFileSync, writeFileSync } from "fs";
|
|
1198
|
-
import prettier from "prettier";
|
|
1199
|
-
|
|
1200
|
-
// KOS artifact versioning: stamps the project's .kos.json "version" field
|
|
1201
|
-
// (which kabtool bakes into the KAB). Never touches package.json.
|
|
1202
|
-
// Driven by tag-based releases:
|
|
1203
|
-
// nx run-many --target=version --args=--ver=$KOSBUILD_VERSION
|
|
1204
|
-
|
|
1205
|
-
const { readCachedProjectGraph } = devkit;
|
|
1206
|
-
const [, , name, versionArg] = process.argv;
|
|
1207
|
-
|
|
1208
|
-
// "{args.ver}" arrives literally when the target runs without --args=--ver=<v>;
|
|
1209
|
-
// treat that (or a missing arg) as "report current version, change nothing".
|
|
1210
|
-
const version =
|
|
1211
|
-
versionArg && !versionArg.startsWith("{args") ? versionArg : undefined;
|
|
1212
|
-
|
|
1213
|
-
if (!name) {
|
|
1214
|
-
console.error("usage: update-release-version.mjs <project> <version>");
|
|
1215
|
-
process.exit(1);
|
|
1216
|
-
}
|
|
1217
|
-
|
|
1218
|
-
const graph = readCachedProjectGraph();
|
|
1219
|
-
const project = graph.nodes[name];
|
|
1220
|
-
if (!project) {
|
|
1221
|
-
console.error("Unknown project: " + name);
|
|
1222
|
-
process.exit(1);
|
|
1223
|
-
}
|
|
1224
|
-
|
|
1225
|
-
const kosJsonPath = resolve(process.cwd(), project.data.root, ".kos.json");
|
|
1226
|
-
let kosJson;
|
|
1227
|
-
try {
|
|
1228
|
-
kosJson = JSON.parse(readFileSync(kosJsonPath, "utf8"));
|
|
1229
|
-
} catch {
|
|
1230
|
-
console.error("Missing or invalid .kos.json: " + kosJsonPath);
|
|
1231
|
-
process.exit(1);
|
|
1232
|
-
}
|
|
1233
|
-
|
|
1234
|
-
if (!version) {
|
|
1235
|
-
console.log(name + ": " + kosJson.version + " (no --ver given; unchanged)");
|
|
1236
|
-
process.exit(0);
|
|
1237
|
-
}
|
|
1238
|
-
|
|
1239
|
-
const prettierOptions = await prettier.resolveConfig(kosJsonPath);
|
|
1240
|
-
const output = await prettier.format(
|
|
1241
|
-
JSON.stringify({ ...kosJson, version }, null, 2),
|
|
1242
|
-
{ ...prettierOptions, parser: "json" }
|
|
1243
|
-
);
|
|
1244
|
-
writeFileSync(kosJsonPath, output);
|
|
1245
|
-
console.log(name + ": version -> " + version);
|
|
1246
|
-
`;
|
|
1247
1461
|
function appendBarrelExport(codegenFs, indexPath, exportPath) {
|
|
1248
1462
|
const exportLine = `export * from '${exportPath}'`;
|
|
1249
1463
|
const content = codegenFs.read(indexPath) ?? "";
|
|
@@ -1290,10 +1504,57 @@ function updateModelIndex(codegenFs, indexPath, modelPath) {
|
|
|
1290
1504
|
const newContents = printer.printFile(updatedSourceFile);
|
|
1291
1505
|
codegenFs.write(indexPath, newContents);
|
|
1292
1506
|
}
|
|
1293
|
-
function
|
|
1294
|
-
|
|
1295
|
-
|
|
1507
|
+
function generateCompanionModel(codegenFs, templateDir, options, projects) {
|
|
1508
|
+
const logger = getCodegenLogger();
|
|
1509
|
+
const normalized = normalizeAllValues({
|
|
1510
|
+
companionModelName: options.companionModelName,
|
|
1511
|
+
modelName: options.modelName
|
|
1512
|
+
});
|
|
1513
|
+
const companionChildKosConfig = getKosProjectConfiguration(
|
|
1514
|
+
codegenFs,
|
|
1515
|
+
options.companionModelProject,
|
|
1516
|
+
projects
|
|
1517
|
+
);
|
|
1518
|
+
const parentProject = findProjectByName(
|
|
1519
|
+
codegenFs.root,
|
|
1520
|
+
options.modelProject,
|
|
1521
|
+
projects
|
|
1522
|
+
);
|
|
1523
|
+
const childProject = findProjectByName(
|
|
1524
|
+
codegenFs.root,
|
|
1525
|
+
options.companionModelProject,
|
|
1526
|
+
projects
|
|
1527
|
+
);
|
|
1528
|
+
const projectRoot = childProject?.sourceRoot;
|
|
1529
|
+
if (!projectRoot) {
|
|
1530
|
+
logger.warn(`Companion child project source root not found`);
|
|
1531
|
+
return;
|
|
1532
|
+
}
|
|
1533
|
+
let importPath = "";
|
|
1534
|
+
if (parentProject) {
|
|
1535
|
+
const pkgJsonPath = path__namespace.join(parentProject.root, "package.json");
|
|
1536
|
+
try {
|
|
1537
|
+
const pkgJson = readJson(codegenFs, pkgJsonPath);
|
|
1538
|
+
importPath = pkgJson.name || "";
|
|
1539
|
+
} catch {
|
|
1540
|
+
importPath = "";
|
|
1541
|
+
}
|
|
1296
1542
|
}
|
|
1543
|
+
const modelLocation = companionChildKosConfig?.generator?.defaults?.model?.folder || "";
|
|
1544
|
+
const filePath = path__namespace.join(
|
|
1545
|
+
projectRoot,
|
|
1546
|
+
modelLocation,
|
|
1547
|
+
normalized.companionModelNameDashCase
|
|
1548
|
+
);
|
|
1549
|
+
logger.info(`Generating companion model in ${filePath}`);
|
|
1550
|
+
generateFilesFromTemplates(codegenFs, templateDir, filePath, {
|
|
1551
|
+
...options,
|
|
1552
|
+
...normalized,
|
|
1553
|
+
importPath
|
|
1554
|
+
});
|
|
1555
|
+
}
|
|
1556
|
+
function generateContainerModel(codegenFs, templateDir, options, cwd, projects) {
|
|
1557
|
+
const logger = getCodegenLogger();
|
|
1297
1558
|
const currentProject = getProject(codegenFs, cwd);
|
|
1298
1559
|
const modelProjectName = options.modelProject || currentProject?.name;
|
|
1299
1560
|
if (!modelProjectName) {
|
|
@@ -1301,57 +1562,110 @@ function generateHook(codegenFs, templateDir, options, cwd, projects) {
|
|
|
1301
1562
|
"No model project found. Please specify a model project with --modelProject."
|
|
1302
1563
|
);
|
|
1303
1564
|
}
|
|
1304
|
-
const modelName = options.
|
|
1565
|
+
const modelName = options.modelName || getCurrentDirectoryName(cwd);
|
|
1305
1566
|
if (!modelName) {
|
|
1306
1567
|
throw new Error(
|
|
1307
1568
|
"No model name found. Please specify a model name with --name."
|
|
1308
1569
|
);
|
|
1309
1570
|
}
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
modelProjectName,
|
|
1313
|
-
modelName,
|
|
1314
|
-
projects
|
|
1315
|
-
);
|
|
1316
|
-
options.singleton = kosModelConfig ? !!kosModelConfig.singleton : false;
|
|
1317
|
-
options.name = modelName;
|
|
1318
|
-
options.modelProject = modelProjectName;
|
|
1571
|
+
options.modelName = modelName;
|
|
1572
|
+
options.name = `${modelName}-container`;
|
|
1319
1573
|
const normalized = normalizeOptions(codegenFs, options, projects);
|
|
1320
|
-
const
|
|
1574
|
+
const projectConfig = findProjectByName(
|
|
1321
1575
|
codegenFs.root,
|
|
1322
|
-
normalized.
|
|
1576
|
+
normalized.modelProject,
|
|
1323
1577
|
projects
|
|
1324
1578
|
);
|
|
1325
|
-
if (!
|
|
1326
|
-
throw new Error(`
|
|
1579
|
+
if (!projectConfig) {
|
|
1580
|
+
throw new Error(`Model project '${normalized.modelProject}' not found`);
|
|
1327
1581
|
}
|
|
1582
|
+
addKosModelConfiguration({
|
|
1583
|
+
codegenFs,
|
|
1584
|
+
modelName: normalized.nameDashCase,
|
|
1585
|
+
projectName: projectConfig.name,
|
|
1586
|
+
projectRoot: projectConfig.root,
|
|
1587
|
+
singleton: !!options.singleton,
|
|
1588
|
+
container: true,
|
|
1589
|
+
// The container's exported registration bean (`export const <ProperCase>`).
|
|
1590
|
+
factory: normalized.nameProperCase
|
|
1591
|
+
});
|
|
1328
1592
|
const kosConfig = getKosProjectConfiguration(
|
|
1329
1593
|
codegenFs,
|
|
1330
|
-
|
|
1594
|
+
projectConfig.name,
|
|
1331
1595
|
projects
|
|
1332
1596
|
);
|
|
1333
|
-
const
|
|
1334
|
-
|
|
1335
|
-
|
|
1597
|
+
const modelLocation = kosConfig?.generator?.defaults?.model?.folder || "";
|
|
1598
|
+
const internal = !!kosConfig?.generator?.internal;
|
|
1599
|
+
options.modelDirectory = options.modelDirectory || modelLocation;
|
|
1600
|
+
const projectRoot = projectConfig.sourceRoot;
|
|
1336
1601
|
if (projectRoot) {
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
templateDir,
|
|
1340
|
-
path__namespace.join(
|
|
1341
|
-
projectRoot,
|
|
1342
|
-
options.appDirectory,
|
|
1343
|
-
"hooks",
|
|
1344
|
-
normalized.nameDashCase
|
|
1345
|
-
),
|
|
1346
|
-
normalized
|
|
1602
|
+
logger.info(
|
|
1603
|
+
`Generating container model ${normalized.nameDashCase} in ${projectRoot}`
|
|
1347
1604
|
);
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1605
|
+
const modelNameDashCase = normalized.modelNameDashCase || normalized.nameDashCase;
|
|
1606
|
+
const modelFolder = path__namespace.join(
|
|
1607
|
+
projectRoot,
|
|
1608
|
+
options.modelDirectory || "",
|
|
1609
|
+
modelNameDashCase
|
|
1352
1610
|
);
|
|
1611
|
+
if (options.existingModel) {
|
|
1612
|
+
addContainerToExistingModelFolder(codegenFs, templateDir, modelFolder, {
|
|
1613
|
+
...normalized,
|
|
1614
|
+
internal
|
|
1615
|
+
});
|
|
1616
|
+
} else {
|
|
1617
|
+
generateFilesFromTemplates(
|
|
1618
|
+
codegenFs,
|
|
1619
|
+
path__namespace.join(templateDir, "model"),
|
|
1620
|
+
modelFolder,
|
|
1621
|
+
{ ...normalized, internal }
|
|
1622
|
+
);
|
|
1623
|
+
}
|
|
1624
|
+
const modelIndex = path__namespace.join(projectRoot, "index.ts");
|
|
1625
|
+
const modelPath = normalized.modelDirectory ? `${normalized.modelDirectory}/${modelNameDashCase}` : modelNameDashCase;
|
|
1626
|
+
updateModelIndex(codegenFs, modelIndex, modelPath);
|
|
1353
1627
|
}
|
|
1354
1628
|
}
|
|
1629
|
+
function addContainerToExistingModelFolder(codegenFs, templateDir, modelFolder, substitutions) {
|
|
1630
|
+
const containerFileName = `${substitutions.nameDashCase}-model.ts`;
|
|
1631
|
+
const containerTemplate = path__namespace.join(
|
|
1632
|
+
templateDir,
|
|
1633
|
+
"model",
|
|
1634
|
+
"__nameDashCase__-model.ts.template"
|
|
1635
|
+
);
|
|
1636
|
+
const rendered = ejs__namespace.render(
|
|
1637
|
+
fs__namespace.readFileSync(containerTemplate, "utf-8"),
|
|
1638
|
+
substitutions,
|
|
1639
|
+
{ filename: containerTemplate }
|
|
1640
|
+
);
|
|
1641
|
+
codegenFs.write(path__namespace.join(modelFolder, containerFileName), rendered);
|
|
1642
|
+
const typesPath = path__namespace.join(modelFolder, "types", "index.d.ts");
|
|
1643
|
+
if (codegenFs.read(typesPath) === null) {
|
|
1644
|
+
codegenFs.write(typesPath, "");
|
|
1645
|
+
}
|
|
1646
|
+
transformSourceFile(codegenFs, typesPath, (sourceFile) => {
|
|
1647
|
+
ensureExportedInterface(
|
|
1648
|
+
sourceFile,
|
|
1649
|
+
`${substitutions.nameProperCase}Options`
|
|
1650
|
+
);
|
|
1651
|
+
});
|
|
1652
|
+
const barrelPath = path__namespace.join(modelFolder, "index.ts");
|
|
1653
|
+
if (codegenFs.read(barrelPath) === null) {
|
|
1654
|
+
codegenFs.write(barrelPath, "");
|
|
1655
|
+
}
|
|
1656
|
+
transformSourceFile(codegenFs, barrelPath, (sourceFile) => {
|
|
1657
|
+
const moduleSpecifier = `./${containerFileName.replace(/\.ts$/, "")}`;
|
|
1658
|
+
ensureNamedExports(sourceFile, moduleSpecifier, [
|
|
1659
|
+
substitutions.nameProperCase
|
|
1660
|
+
]);
|
|
1661
|
+
ensureNamedExports(
|
|
1662
|
+
sourceFile,
|
|
1663
|
+
moduleSpecifier,
|
|
1664
|
+
[`${substitutions.nameProperCase}Model`],
|
|
1665
|
+
true
|
|
1666
|
+
);
|
|
1667
|
+
});
|
|
1668
|
+
}
|
|
1355
1669
|
function generateContext(codegenFs, templateDir, options, cwd, projects) {
|
|
1356
1670
|
if (!options.appProject) {
|
|
1357
1671
|
throw new Error("No app project specified");
|
|
@@ -1404,8 +1718,10 @@ function generateContext(codegenFs, templateDir, options, cwd, projects) {
|
|
|
1404
1718
|
);
|
|
1405
1719
|
}
|
|
1406
1720
|
}
|
|
1407
|
-
function
|
|
1408
|
-
|
|
1721
|
+
function generateHook(codegenFs, templateDir, options, cwd, projects) {
|
|
1722
|
+
if (!options.appProject) {
|
|
1723
|
+
throw new Error("No app project specified");
|
|
1724
|
+
}
|
|
1409
1725
|
const currentProject = getProject(codegenFs, cwd);
|
|
1410
1726
|
const modelProjectName = options.modelProject || currentProject?.name;
|
|
1411
1727
|
if (!modelProjectName) {
|
|
@@ -1413,120 +1729,56 @@ function generateContainerModel(codegenFs, templateDir, options, cwd, projects)
|
|
|
1413
1729
|
"No model project found. Please specify a model project with --modelProject."
|
|
1414
1730
|
);
|
|
1415
1731
|
}
|
|
1416
|
-
const modelName = options.
|
|
1732
|
+
const modelName = options.name || getCurrentDirectoryName(cwd);
|
|
1417
1733
|
if (!modelName) {
|
|
1418
1734
|
throw new Error(
|
|
1419
1735
|
"No model name found. Please specify a model name with --name."
|
|
1420
1736
|
);
|
|
1421
1737
|
}
|
|
1422
|
-
|
|
1423
|
-
|
|
1738
|
+
const kosModelConfig = getKosModelConfiguration(
|
|
1739
|
+
codegenFs,
|
|
1740
|
+
modelProjectName,
|
|
1741
|
+
modelName,
|
|
1742
|
+
projects
|
|
1743
|
+
);
|
|
1744
|
+
options.singleton = kosModelConfig ? !!kosModelConfig.singleton : false;
|
|
1745
|
+
options.name = modelName;
|
|
1746
|
+
options.modelProject = modelProjectName;
|
|
1424
1747
|
const normalized = normalizeOptions(codegenFs, options, projects);
|
|
1425
|
-
const
|
|
1748
|
+
const appProject = findProjectByName(
|
|
1426
1749
|
codegenFs.root,
|
|
1427
|
-
normalized.
|
|
1750
|
+
normalized.appProject,
|
|
1428
1751
|
projects
|
|
1429
1752
|
);
|
|
1430
|
-
if (!
|
|
1431
|
-
throw new Error(`
|
|
1753
|
+
if (!appProject) {
|
|
1754
|
+
throw new Error(`App project '${normalized.appProject}' not found`);
|
|
1432
1755
|
}
|
|
1433
|
-
addKosModelConfiguration({
|
|
1434
|
-
codegenFs,
|
|
1435
|
-
modelName: normalized.nameDashCase,
|
|
1436
|
-
projectName: projectConfig.name,
|
|
1437
|
-
projectRoot: projectConfig.root,
|
|
1438
|
-
singleton: !!options.singleton,
|
|
1439
|
-
container: true,
|
|
1440
|
-
// The container's exported registration bean (`export const <ProperCase>`).
|
|
1441
|
-
factory: normalized.nameProperCase
|
|
1442
|
-
});
|
|
1443
1756
|
const kosConfig = getKosProjectConfiguration(
|
|
1444
1757
|
codegenFs,
|
|
1445
|
-
|
|
1758
|
+
appProject.name,
|
|
1446
1759
|
projects
|
|
1447
1760
|
);
|
|
1448
|
-
const
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
const projectRoot = projectConfig.sourceRoot;
|
|
1761
|
+
const componentLocation = kosConfig?.generator?.defaults?.component?.folder || "";
|
|
1762
|
+
options.appDirectory = options.appDirectory || componentLocation;
|
|
1763
|
+
const projectRoot = appProject.sourceRoot;
|
|
1452
1764
|
if (projectRoot) {
|
|
1453
|
-
logger.info(
|
|
1454
|
-
`Generating container model ${normalized.nameDashCase} in ${projectRoot}`
|
|
1455
|
-
);
|
|
1456
|
-
const modelNameDashCase = normalized.modelNameDashCase || normalized.nameDashCase;
|
|
1457
1765
|
generateFilesFromTemplates(
|
|
1458
1766
|
codegenFs,
|
|
1459
|
-
|
|
1460
|
-
path__namespace.join(
|
|
1461
|
-
|
|
1767
|
+
templateDir,
|
|
1768
|
+
path__namespace.join(
|
|
1769
|
+
projectRoot,
|
|
1770
|
+
options.appDirectory,
|
|
1771
|
+
"hooks",
|
|
1772
|
+
normalized.nameDashCase
|
|
1773
|
+
),
|
|
1774
|
+
normalized
|
|
1775
|
+
);
|
|
1776
|
+
appendBarrelExport(
|
|
1777
|
+
codegenFs,
|
|
1778
|
+
path__namespace.join(projectRoot, options.appDirectory, "hooks", "index.ts"),
|
|
1779
|
+
`./${normalized.nameDashCase}`
|
|
1462
1780
|
);
|
|
1463
|
-
if (options.dataServices) {
|
|
1464
|
-
logger.info(`Generating data services for ${modelNameDashCase}`);
|
|
1465
|
-
generateFilesFromTemplates(
|
|
1466
|
-
codegenFs,
|
|
1467
|
-
path__namespace.join(templateDir, "services"),
|
|
1468
|
-
path__namespace.join(
|
|
1469
|
-
projectRoot,
|
|
1470
|
-
options.modelDirectory,
|
|
1471
|
-
modelNameDashCase,
|
|
1472
|
-
"services"
|
|
1473
|
-
),
|
|
1474
|
-
{ ...normalized, internal }
|
|
1475
|
-
);
|
|
1476
|
-
}
|
|
1477
|
-
const modelIndex = path__namespace.join(projectRoot, "index.ts");
|
|
1478
|
-
const modelPath = normalized.modelDirectory ? `${normalized.modelDirectory}/${modelNameDashCase}` : modelNameDashCase;
|
|
1479
|
-
updateModelIndex(codegenFs, modelIndex, modelPath);
|
|
1480
|
-
}
|
|
1481
|
-
}
|
|
1482
|
-
function generateCompanionModel(codegenFs, templateDir, options, projects) {
|
|
1483
|
-
const logger = getCodegenLogger();
|
|
1484
|
-
const normalized = normalizeAllValues({
|
|
1485
|
-
companionModelName: options.companionModelName,
|
|
1486
|
-
modelName: options.modelName
|
|
1487
|
-
});
|
|
1488
|
-
const companionChildKosConfig = getKosProjectConfiguration(
|
|
1489
|
-
codegenFs,
|
|
1490
|
-
options.companionModelProject,
|
|
1491
|
-
projects
|
|
1492
|
-
);
|
|
1493
|
-
const parentProject = findProjectByName(
|
|
1494
|
-
codegenFs.root,
|
|
1495
|
-
options.modelProject,
|
|
1496
|
-
projects
|
|
1497
|
-
);
|
|
1498
|
-
const childProject = findProjectByName(
|
|
1499
|
-
codegenFs.root,
|
|
1500
|
-
options.companionModelProject,
|
|
1501
|
-
projects
|
|
1502
|
-
);
|
|
1503
|
-
const projectRoot = childProject?.sourceRoot;
|
|
1504
|
-
if (!projectRoot) {
|
|
1505
|
-
logger.warn(`Companion child project source root not found`);
|
|
1506
|
-
return;
|
|
1507
|
-
}
|
|
1508
|
-
let importPath = "";
|
|
1509
|
-
if (parentProject) {
|
|
1510
|
-
const pkgJsonPath = path__namespace.join(parentProject.root, "package.json");
|
|
1511
|
-
try {
|
|
1512
|
-
const pkgJson = readJson(codegenFs, pkgJsonPath);
|
|
1513
|
-
importPath = pkgJson.name || "";
|
|
1514
|
-
} catch {
|
|
1515
|
-
importPath = "";
|
|
1516
|
-
}
|
|
1517
1781
|
}
|
|
1518
|
-
const modelLocation = companionChildKosConfig?.generator?.defaults?.model?.folder || "";
|
|
1519
|
-
const filePath = path__namespace.join(
|
|
1520
|
-
projectRoot,
|
|
1521
|
-
modelLocation,
|
|
1522
|
-
normalized.companionModelNameDashCase
|
|
1523
|
-
);
|
|
1524
|
-
logger.info(`Generating companion model in ${filePath}`);
|
|
1525
|
-
generateFilesFromTemplates(codegenFs, templateDir, filePath, {
|
|
1526
|
-
...options,
|
|
1527
|
-
...normalized,
|
|
1528
|
-
importPath
|
|
1529
|
-
});
|
|
1530
1782
|
}
|
|
1531
1783
|
function generateModel(params) {
|
|
1532
1784
|
const {
|
|
@@ -1574,19 +1826,6 @@ function generateModel(params) {
|
|
|
1574
1826
|
path__namespace.join(projectRoot, options.modelDirectory, normalized.nameDashCase),
|
|
1575
1827
|
{ ...normalized, internal }
|
|
1576
1828
|
);
|
|
1577
|
-
if (normalized.dataServices) {
|
|
1578
|
-
generateFilesFromTemplates(
|
|
1579
|
-
codegenFs,
|
|
1580
|
-
path__namespace.join(modelTemplateDir, "services"),
|
|
1581
|
-
path__namespace.join(
|
|
1582
|
-
projectRoot,
|
|
1583
|
-
options.modelDirectory,
|
|
1584
|
-
normalized.nameDashCase,
|
|
1585
|
-
"services"
|
|
1586
|
-
),
|
|
1587
|
-
{ ...normalized, internal }
|
|
1588
|
-
);
|
|
1589
|
-
}
|
|
1590
1829
|
const modelIndex = path__namespace.join(projectRoot, "index.ts");
|
|
1591
1830
|
const modelPath = options.modelDirectory ? `${options.modelDirectory}/${normalized.nameDashCase}` : normalized.nameDashCase;
|
|
1592
1831
|
updateModelIndex(codegenFs, modelIndex, modelPath);
|
|
@@ -1617,8 +1856,7 @@ function generateModel(params) {
|
|
|
1617
1856
|
...options,
|
|
1618
1857
|
name: `${normalized.name}-container`,
|
|
1619
1858
|
modelName: normalized.name,
|
|
1620
|
-
singleton: normalized.isContainerSingleton
|
|
1621
|
-
dataServices: normalized.dataServices
|
|
1859
|
+
singleton: normalized.isContainerSingleton
|
|
1622
1860
|
},
|
|
1623
1861
|
cwd,
|
|
1624
1862
|
projects
|
|
@@ -1639,6 +1877,55 @@ function generateModel(params) {
|
|
|
1639
1877
|
);
|
|
1640
1878
|
}
|
|
1641
1879
|
}
|
|
1880
|
+
const DECLARATION_MARKER = "@kosModel";
|
|
1881
|
+
function findModelDeclarationFile(codegenFs, searchRoot, typeIds) {
|
|
1882
|
+
const wanted = new Set(typeIds.filter(Boolean));
|
|
1883
|
+
if (wanted.size === 0) return null;
|
|
1884
|
+
const matches = [];
|
|
1885
|
+
for (const filePath of codegenFs.listFiles(searchRoot)) {
|
|
1886
|
+
if (!filePath.endsWith(".ts") || filePath.endsWith(".d.ts")) continue;
|
|
1887
|
+
const content = codegenFs.read(filePath);
|
|
1888
|
+
if (!content || !content.includes(DECLARATION_MARKER)) continue;
|
|
1889
|
+
const declared = readDeclaredModelType(codegenFs, filePath);
|
|
1890
|
+
if (declared && wanted.has(declared)) matches.push(filePath);
|
|
1891
|
+
}
|
|
1892
|
+
if (matches.length === 0) return null;
|
|
1893
|
+
if (matches.length > 1) {
|
|
1894
|
+
throw new Error(
|
|
1895
|
+
`Model type '${[...wanted].join("' / '")}' is declared in more than one file:
|
|
1896
|
+
` + matches.sort().map((c) => ` - ${c}`).join("\n") + `
|
|
1897
|
+
Pass modelPath to pick one.`
|
|
1898
|
+
);
|
|
1899
|
+
}
|
|
1900
|
+
return matches[0];
|
|
1901
|
+
}
|
|
1902
|
+
function readDeclaredModelType(codegenFs, filePath) {
|
|
1903
|
+
try {
|
|
1904
|
+
return readSourceFile(codegenFs, filePath, (sf) => {
|
|
1905
|
+
const cls = sf.getClasses().find((c) => c.getDecorator("kosModel"));
|
|
1906
|
+
const arg = cls?.getDecorator("kosModel")?.getArguments()[0];
|
|
1907
|
+
if (!arg) return void 0;
|
|
1908
|
+
const inner = arg.getKindName() === "ObjectLiteralExpression" ? modelTypeIdProperty(arg.getText()) : arg.getText();
|
|
1909
|
+
if (!inner) return void 0;
|
|
1910
|
+
return resolveToStringLiteral(inner.trim(), sf.getFullText());
|
|
1911
|
+
});
|
|
1912
|
+
} catch {
|
|
1913
|
+
return void 0;
|
|
1914
|
+
}
|
|
1915
|
+
}
|
|
1916
|
+
function modelTypeIdProperty(objectText) {
|
|
1917
|
+
const m = objectText.match(/\bmodelTypeId\s*:\s*([^,}]+)/);
|
|
1918
|
+
return m ? m[1] : void 0;
|
|
1919
|
+
}
|
|
1920
|
+
function resolveToStringLiteral(expression, fileText) {
|
|
1921
|
+
const literal = expression.match(/^["'`](.*)["'`]$/);
|
|
1922
|
+
if (literal) return literal[1];
|
|
1923
|
+
if (!/^[A-Za-z_$][\w$]*$/.test(expression)) return void 0;
|
|
1924
|
+
const declared = fileText.match(
|
|
1925
|
+
new RegExp(`\\b(?:const|let|var)\\s+${expression}\\s*(?::[^=]+)?=\\s*["'\`]([^"'\`]+)["'\`]`)
|
|
1926
|
+
);
|
|
1927
|
+
return declared ? declared[1] : void 0;
|
|
1928
|
+
}
|
|
1642
1929
|
function resolveModelFilePath(codegenFs, query, projects) {
|
|
1643
1930
|
const kosConfig = getKosProjectConfiguration(
|
|
1644
1931
|
codegenFs,
|
|
@@ -1673,14 +1960,23 @@ function resolveModelFilePath(codegenFs, query, projects) {
|
|
|
1673
1960
|
if (codegenFs.exists(modelFilePath)) {
|
|
1674
1961
|
return { modelFilePath, internal, sourceRoot };
|
|
1675
1962
|
}
|
|
1963
|
+
const searchRoot = path__namespace.join(sourceRoot, modelLocation);
|
|
1676
1964
|
const discovered = findModelFileByName(
|
|
1677
1965
|
codegenFs,
|
|
1678
|
-
|
|
1966
|
+
searchRoot,
|
|
1679
1967
|
modelNameDashCase
|
|
1680
1968
|
);
|
|
1681
1969
|
if (discovered) {
|
|
1682
1970
|
return { modelFilePath: discovered, internal, sourceRoot };
|
|
1683
1971
|
}
|
|
1972
|
+
const declaredType = kosConfig?.models?.[query.modelName]?.type;
|
|
1973
|
+
const byDeclaration = findModelDeclarationFile(codegenFs, searchRoot, [
|
|
1974
|
+
query.modelName,
|
|
1975
|
+
...declaredType ? [declaredType] : []
|
|
1976
|
+
]);
|
|
1977
|
+
if (byDeclaration) {
|
|
1978
|
+
return { modelFilePath: byDeclaration, internal, sourceRoot };
|
|
1979
|
+
}
|
|
1684
1980
|
return { modelFilePath, internal, sourceRoot };
|
|
1685
1981
|
}
|
|
1686
1982
|
function findModelFileByName(codegenFs, searchRoot, modelNameDashCase) {
|
|
@@ -1696,6 +1992,184 @@ Pass modelPath to pick one.`
|
|
|
1696
1992
|
}
|
|
1697
1993
|
return candidates[0];
|
|
1698
1994
|
}
|
|
1995
|
+
function resolveChildModelType(codegenFs, query, projects) {
|
|
1996
|
+
const childProject = query.childModelProject || query.modelProject;
|
|
1997
|
+
const childType = `${properCase(query.childModel)}Model`;
|
|
1998
|
+
if (childProject !== query.modelProject) {
|
|
1999
|
+
const project = findProjectByName(codegenFs.root, childProject, projects);
|
|
2000
|
+
const pkgJson = project ? readJson(
|
|
2001
|
+
codegenFs,
|
|
2002
|
+
path__namespace.join(project.root, "package.json")
|
|
2003
|
+
) : void 0;
|
|
2004
|
+
return { childType, childTypeModule: pkgJson?.name };
|
|
2005
|
+
}
|
|
2006
|
+
const { modelFilePath: childFilePath } = resolveModelFilePath(
|
|
2007
|
+
codegenFs,
|
|
2008
|
+
{ modelName: query.childModel, modelProject: childProject },
|
|
2009
|
+
projects
|
|
2010
|
+
);
|
|
2011
|
+
const relative = path__namespace.relative(path__namespace.dirname(query.modelFilePath), childFilePath).replace(/\.ts$/, "");
|
|
2012
|
+
return {
|
|
2013
|
+
childType,
|
|
2014
|
+
childTypeModule: relative.startsWith(".") ? relative : `./${relative}`
|
|
2015
|
+
};
|
|
2016
|
+
}
|
|
2017
|
+
function generateViewModel(codegenFs, templateDir, options, cwd, projects) {
|
|
2018
|
+
const logger = getCodegenLogger();
|
|
2019
|
+
if (!options.name) {
|
|
2020
|
+
throw new Error(
|
|
2021
|
+
"No ViewModel name found. Please specify a name with --name."
|
|
2022
|
+
);
|
|
2023
|
+
}
|
|
2024
|
+
const currentProject = getProject(codegenFs, cwd);
|
|
2025
|
+
const modelProjectName = options.modelProject || currentProject?.name;
|
|
2026
|
+
if (!modelProjectName) {
|
|
2027
|
+
throw new Error(
|
|
2028
|
+
"No model project found. Please specify a model project with --project."
|
|
2029
|
+
);
|
|
2030
|
+
}
|
|
2031
|
+
const normalized = normalizeOptions(
|
|
2032
|
+
codegenFs,
|
|
2033
|
+
{ ...options, modelProject: modelProjectName },
|
|
2034
|
+
projects
|
|
2035
|
+
);
|
|
2036
|
+
const projectConfig = findProjectByName(
|
|
2037
|
+
codegenFs.root,
|
|
2038
|
+
modelProjectName,
|
|
2039
|
+
projects
|
|
2040
|
+
);
|
|
2041
|
+
if (!projectConfig) {
|
|
2042
|
+
throw new Error(`Model project '${modelProjectName}' not found`);
|
|
2043
|
+
}
|
|
2044
|
+
const kosConfig = getKosProjectConfiguration(
|
|
2045
|
+
codegenFs,
|
|
2046
|
+
projectConfig.name,
|
|
2047
|
+
projects
|
|
2048
|
+
);
|
|
2049
|
+
const modelDirectory = options.modelDirectory || kosConfig?.generator?.defaults?.model?.folder || "";
|
|
2050
|
+
const internal = !!kosConfig?.generator?.internal;
|
|
2051
|
+
const projectRoot = projectConfig.sourceRoot || path__namespace.join(projectConfig.root, "src");
|
|
2052
|
+
const viewModelFolder = path__namespace.join(
|
|
2053
|
+
projectRoot,
|
|
2054
|
+
modelDirectory,
|
|
2055
|
+
normalized.nameDashCase
|
|
2056
|
+
);
|
|
2057
|
+
const viewModelFilePath = path__namespace.join(
|
|
2058
|
+
viewModelFolder,
|
|
2059
|
+
`${normalized.nameDashCase}-view-model.ts`
|
|
2060
|
+
);
|
|
2061
|
+
if (codegenFs.exists(viewModelFilePath)) {
|
|
2062
|
+
logger.info(`ViewModel already exists: ${viewModelFilePath}`);
|
|
2063
|
+
return { viewModelFilePath, created: false };
|
|
2064
|
+
}
|
|
2065
|
+
const resolved = (options.models || []).map(
|
|
2066
|
+
(source) => resolveSourceModel(
|
|
2067
|
+
codegenFs,
|
|
2068
|
+
source,
|
|
2069
|
+
viewModelFilePath,
|
|
2070
|
+
modelProjectName,
|
|
2071
|
+
projects
|
|
2072
|
+
)
|
|
2073
|
+
);
|
|
2074
|
+
logger.info(
|
|
2075
|
+
`Generating ViewModel ${normalized.nameDashCase} in ${projectRoot}`
|
|
2076
|
+
);
|
|
2077
|
+
const constructorParams = resolved.map(({ name, type }) => ({ name, type }));
|
|
2078
|
+
generateFilesFromTemplates(codegenFs, templateDir, viewModelFolder, {
|
|
2079
|
+
...normalized,
|
|
2080
|
+
internal,
|
|
2081
|
+
typeId: options.typeId || normalized.nameDashCase,
|
|
2082
|
+
devToolsEnabled: !!options.devToolsEnabled,
|
|
2083
|
+
constructorParams,
|
|
2084
|
+
constructorArgs: constructorParams.map((param) => param.name).join(", "),
|
|
2085
|
+
modelImports: groupImports(resolved)
|
|
2086
|
+
});
|
|
2087
|
+
updateModelIndex(
|
|
2088
|
+
codegenFs,
|
|
2089
|
+
path__namespace.join(projectRoot, "index.ts"),
|
|
2090
|
+
modelDirectory ? `${modelDirectory}/${normalized.nameDashCase}` : normalized.nameDashCase
|
|
2091
|
+
);
|
|
2092
|
+
return { viewModelFilePath, created: true };
|
|
2093
|
+
}
|
|
2094
|
+
function resolveSourceModel(codegenFs, source, viewModelFilePath, modelProject, projects) {
|
|
2095
|
+
const sourceProject = source.project || modelProject;
|
|
2096
|
+
if (sourceProject === modelProject) {
|
|
2097
|
+
const { modelFilePath } = resolveModelFilePath(
|
|
2098
|
+
codegenFs,
|
|
2099
|
+
{ modelName: source.model, modelProject: sourceProject },
|
|
2100
|
+
projects
|
|
2101
|
+
);
|
|
2102
|
+
if (!codegenFs.exists(modelFilePath)) {
|
|
2103
|
+
throw new Error(
|
|
2104
|
+
`Model '${source.model}' not found in project '${sourceProject}' (looked for ${modelFilePath})`
|
|
2105
|
+
);
|
|
2106
|
+
}
|
|
2107
|
+
} else if (!findProjectByName(codegenFs.root, sourceProject, projects)) {
|
|
2108
|
+
throw new Error(`Model project '${sourceProject}' not found`);
|
|
2109
|
+
}
|
|
2110
|
+
const { childType, childTypeModule } = resolveChildModelType(
|
|
2111
|
+
codegenFs,
|
|
2112
|
+
{
|
|
2113
|
+
modelFilePath: viewModelFilePath,
|
|
2114
|
+
modelProject,
|
|
2115
|
+
childModel: source.model,
|
|
2116
|
+
childModelProject: sourceProject
|
|
2117
|
+
},
|
|
2118
|
+
projects
|
|
2119
|
+
);
|
|
2120
|
+
return {
|
|
2121
|
+
name: camelCase(source.model),
|
|
2122
|
+
type: childType,
|
|
2123
|
+
module: childTypeModule
|
|
2124
|
+
};
|
|
2125
|
+
}
|
|
2126
|
+
function groupImports(resolved) {
|
|
2127
|
+
const byModule = /* @__PURE__ */ new Map();
|
|
2128
|
+
for (const { type, module: module2 } of resolved) {
|
|
2129
|
+
if (!module2) continue;
|
|
2130
|
+
const types = byModule.get(module2);
|
|
2131
|
+
if (!types) {
|
|
2132
|
+
byModule.set(module2, [type]);
|
|
2133
|
+
} else if (!types.includes(type)) {
|
|
2134
|
+
types.push(type);
|
|
2135
|
+
}
|
|
2136
|
+
}
|
|
2137
|
+
return [...byModule].map(([module2, types]) => ({ module: module2, types }));
|
|
2138
|
+
}
|
|
2139
|
+
function modelBaseName(modelFilePath, modelName) {
|
|
2140
|
+
const base = path__namespace.basename(modelFilePath);
|
|
2141
|
+
const match = base.match(/^(.*)-model\.ts$/);
|
|
2142
|
+
return match ? match[1] : modelName;
|
|
2143
|
+
}
|
|
2144
|
+
function servicesFilePathFor(modelFilePath, modelBase) {
|
|
2145
|
+
return path__namespace.join(
|
|
2146
|
+
path__namespace.dirname(modelFilePath),
|
|
2147
|
+
"services",
|
|
2148
|
+
`${modelBase}-services.ts`
|
|
2149
|
+
);
|
|
2150
|
+
}
|
|
2151
|
+
function header$1(modelBase) {
|
|
2152
|
+
return `/**
|
|
2153
|
+
* Service layer for the ${modelBase} model: the endpoints it calls and the
|
|
2154
|
+
* types derived from them. Standalone service functions for callers outside a
|
|
2155
|
+
* model context belong here too.
|
|
2156
|
+
*/
|
|
2157
|
+
`;
|
|
2158
|
+
}
|
|
2159
|
+
function ensureServicesModule(codegenFs, modelFilePath, modelBase) {
|
|
2160
|
+
const servicesFilePath = servicesFilePathFor(modelFilePath, modelBase);
|
|
2161
|
+
if (!codegenFs.exists(servicesFilePath)) {
|
|
2162
|
+
codegenFs.write(servicesFilePath, header$1(modelBase));
|
|
2163
|
+
}
|
|
2164
|
+
const barrelPath = path__namespace.join(path__namespace.dirname(servicesFilePath), "index.ts");
|
|
2165
|
+
if (!codegenFs.exists(barrelPath)) {
|
|
2166
|
+
codegenFs.write(barrelPath, "");
|
|
2167
|
+
}
|
|
2168
|
+
transformSourceFile(codegenFs, barrelPath, (sf) => {
|
|
2169
|
+
ensureBarrelExport(sf, `./${modelBase}-services`);
|
|
2170
|
+
});
|
|
2171
|
+
return servicesFilePath;
|
|
2172
|
+
}
|
|
1699
2173
|
function normalizeAddFutureOptions(codegenFs, options, projects) {
|
|
1700
2174
|
const projectConfiguration = findProjectByName(
|
|
1701
2175
|
codegenFs.root,
|
|
@@ -1730,8 +2204,7 @@ function normalizeAddFutureOptions(codegenFs, options, projects) {
|
|
|
1730
2204
|
projects
|
|
1731
2205
|
);
|
|
1732
2206
|
const modelDirectory = path__namespace.dirname(modelFilePath);
|
|
1733
|
-
const
|
|
1734
|
-
const servicesFilePath = codegenFs.exists(servicesDirectory) ? path__namespace.join(servicesDirectory, `${nameDashCase}-services.ts`) : void 0;
|
|
2207
|
+
const servicesFilePath = servicesFilePathFor(modelFilePath, nameDashCase);
|
|
1735
2208
|
const registrationFilePath = path__namespace.join(
|
|
1736
2209
|
modelDirectory,
|
|
1737
2210
|
`${nameDashCase}-registration.ts`
|
|
@@ -1940,171 +2413,79 @@ class ModelFileTransformer {
|
|
|
1940
2413
|
]
|
|
1941
2414
|
});
|
|
1942
2415
|
}
|
|
1943
|
-
}
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
this.
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
type FutureResponse
|
|
1975
|
-
} from '@kosdev-code/kos-ui-sdk';
|
|
1976
|
-
|
|
1977
|
-
import API, { type KosApi, type ApiPath } from '../../../utils/service';
|
|
1978
|
-
|
|
1979
|
-
const log = KosLog.createLogger({name: "${nameDashCase}-service", group: "Services"});
|
|
1980
|
-
|
|
1981
|
-
const SERVICE_PATH: ApiPath = "ENTER_SERVICE_PATH"
|
|
1982
|
-
export type ${nameProperCase}ClientResponse = ClientResponse<
|
|
1983
|
-
KosApi,
|
|
1984
|
-
typeof SERVICE_PATH,
|
|
1985
|
-
'get'
|
|
1986
|
-
>;
|
|
1987
|
-
export type ${nameProperCase}Response = DeepRequired<${nameProperCase}ClientResponse>;
|
|
1988
|
-
|
|
1989
|
-
/**
|
|
1990
|
-
* @category Service
|
|
1991
|
-
* Retrieves the initial ${nameLowerCase} data.
|
|
1992
|
-
*/
|
|
1993
|
-
export const get${nameProperCase} = async (): Promise<ServiceResponse<${nameProperCase}Response>> => {
|
|
1994
|
-
log.debug('sending GET for ${nameLowerCase}');
|
|
1995
|
-
return await API.get(SERVICE_PATH);
|
|
1996
|
-
};
|
|
1997
|
-
|
|
1998
|
-
/**
|
|
1999
|
-
* @category Service - Future Operation
|
|
2000
|
-
* Placeholder for a long-running operation that returns a Future for progress tracking
|
|
2001
|
-
*
|
|
2002
|
-
* Replace this with your actual long-running service operation
|
|
2003
|
-
*/
|
|
2004
|
-
export const perform${nameProperCase}Operation = async (): Promise<FutureResponse> => {
|
|
2005
|
-
// TODO: Implement your long-running service operation here
|
|
2006
|
-
// This should return a Future that can be tracked for progress
|
|
2007
|
-
|
|
2008
|
-
log.debug('starting long-running ${nameLowerCase} operation');
|
|
2009
|
-
|
|
2010
|
-
// Example pattern:
|
|
2011
|
-
// return API.post(OPERATION_SERVICE_PATH, {
|
|
2012
|
-
// // operation parameters
|
|
2013
|
-
// });
|
|
2014
|
-
|
|
2015
|
-
// Placeholder - replace with actual implementation
|
|
2016
|
-
throw new Error('perform${nameProperCase}Operation not yet implemented');
|
|
2017
|
-
};
|
|
2018
|
-
|
|
2019
|
-
// Additional Future-aware service types (add as needed)
|
|
2020
|
-
export type ${nameProperCase}OperationProgress = {
|
|
2021
|
-
// Define your progress data structure here
|
|
2022
|
-
stage: string;
|
|
2023
|
-
percentComplete: number;
|
|
2024
|
-
currentItem?: string;
|
|
2025
|
-
totalItems?: number;
|
|
2026
|
-
};
|
|
2027
|
-
|
|
2028
|
-
export type ${nameProperCase}OperationResult = {
|
|
2029
|
-
// Define your operation result structure here
|
|
2030
|
-
success: boolean;
|
|
2031
|
-
message?: string;
|
|
2032
|
-
data?: any;
|
|
2033
|
-
};
|
|
2034
|
-
`;
|
|
2035
|
-
this.codegenFs.write(servicesFilePath, content);
|
|
2036
|
-
}
|
|
2037
|
-
addFutureImports(content) {
|
|
2038
|
-
if (content.includes("FutureResponse")) {
|
|
2039
|
-
return content;
|
|
2040
|
-
}
|
|
2041
|
-
const importRegex = /import {\s*([^}]*)\s*} from '@kosdev-code\/kos-ui-sdk';/;
|
|
2042
|
-
const importMatch = content.match(importRegex);
|
|
2043
|
-
if (importMatch) {
|
|
2044
|
-
const existingImports = importMatch[1];
|
|
2045
|
-
if (existingImports.includes("FutureResponse")) {
|
|
2046
|
-
return content;
|
|
2047
|
-
}
|
|
2048
|
-
const cleanedImports = existingImports.trim().replace(/,\s*$/, "");
|
|
2049
|
-
const newImports = cleanedImports ? `${cleanedImports},
|
|
2050
|
-
type FutureResponse` : `type FutureResponse`;
|
|
2051
|
-
const newImportStatement = `import {
|
|
2052
|
-
${newImports}
|
|
2053
|
-
} from '@kosdev-code/kos-ui-sdk';`;
|
|
2054
|
-
return content.replace(importMatch[0], newImportStatement);
|
|
2055
|
-
}
|
|
2056
|
-
return content;
|
|
2057
|
-
}
|
|
2058
|
-
addFutureService(content) {
|
|
2416
|
+
}
|
|
2417
|
+
const SDK_MODULE = "@kosdev-code/kos-ui-sdk";
|
|
2418
|
+
class ServiceFileTransformer {
|
|
2419
|
+
constructor(codegenFs, options) {
|
|
2420
|
+
this.codegenFs = codegenFs;
|
|
2421
|
+
this.options = options;
|
|
2422
|
+
}
|
|
2423
|
+
codegenFs;
|
|
2424
|
+
options;
|
|
2425
|
+
transform() {
|
|
2426
|
+
const { modelFilePath, nameDashCase } = this.options;
|
|
2427
|
+
if (!modelFilePath) return;
|
|
2428
|
+
const servicesFilePath = ensureServicesModule(
|
|
2429
|
+
this.codegenFs,
|
|
2430
|
+
modelFilePath,
|
|
2431
|
+
nameDashCase
|
|
2432
|
+
);
|
|
2433
|
+
transformSourceFile(this.codegenFs, servicesFilePath, (sf) => {
|
|
2434
|
+
ensureNamedImport(sf, SDK_MODULE, [
|
|
2435
|
+
{ name: "KosLog" },
|
|
2436
|
+
{ name: "FutureResponse", isTypeOnly: true }
|
|
2437
|
+
]);
|
|
2438
|
+
ensureModuleConst(sf, {
|
|
2439
|
+
name: "log",
|
|
2440
|
+
initializer: `KosLog.createLogger({ name: "${nameDashCase}-service", group: "Services" })`
|
|
2441
|
+
});
|
|
2442
|
+
this.addFutureOperation(sf);
|
|
2443
|
+
this.addProgressTypes(sf);
|
|
2444
|
+
});
|
|
2445
|
+
}
|
|
2446
|
+
addFutureOperation(sf) {
|
|
2059
2447
|
const { nameProperCase, nameLowerCase } = this.options;
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
// Placeholder - replace with actual implementation
|
|
2082
|
-
throw new Error('perform${nameProperCase}Operation not yet implemented');
|
|
2083
|
-
};`;
|
|
2084
|
-
return content + "\n" + futureService;
|
|
2448
|
+
const name = `perform${nameProperCase}Operation`;
|
|
2449
|
+
if (sf.getVariableDeclaration(name)) return;
|
|
2450
|
+
sf.addVariableStatement({
|
|
2451
|
+
isExported: true,
|
|
2452
|
+
declarationKind: tsMorph.VariableDeclarationKind.Const,
|
|
2453
|
+
declarations: [
|
|
2454
|
+
{
|
|
2455
|
+
name,
|
|
2456
|
+
initializer: `async (): Promise<FutureResponse> => {
|
|
2457
|
+
log.debug("starting long-running ${nameLowerCase} operation");
|
|
2458
|
+
throw new Error("${name} not yet implemented");
|
|
2459
|
+
}`
|
|
2460
|
+
}
|
|
2461
|
+
],
|
|
2462
|
+
docs: [
|
|
2463
|
+
{
|
|
2464
|
+
description: `@category Service - Future Operation
|
|
2465
|
+
A long-running operation returning a Future for progress tracking. Replace the body with the real service call.`
|
|
2466
|
+
}
|
|
2467
|
+
]
|
|
2468
|
+
});
|
|
2085
2469
|
}
|
|
2086
|
-
addProgressTypes(
|
|
2470
|
+
addProgressTypes(sf) {
|
|
2087
2471
|
const { nameProperCase } = this.options;
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
const progressTypes = `
|
|
2092
|
-
// Additional Future-aware service types (add as needed)
|
|
2093
|
-
export type ${nameProperCase}OperationProgress = {
|
|
2094
|
-
// Define your progress data structure here
|
|
2472
|
+
ensureExportedTypeAlias(sf, {
|
|
2473
|
+
name: `${nameProperCase}OperationProgress`,
|
|
2474
|
+
type: `{
|
|
2095
2475
|
stage: string;
|
|
2096
2476
|
percentComplete: number;
|
|
2097
2477
|
currentItem?: string;
|
|
2098
2478
|
totalItems?: number;
|
|
2099
|
-
}
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2479
|
+
}`
|
|
2480
|
+
});
|
|
2481
|
+
ensureExportedTypeAlias(sf, {
|
|
2482
|
+
name: `${nameProperCase}OperationResult`,
|
|
2483
|
+
type: `{
|
|
2103
2484
|
success: boolean;
|
|
2104
2485
|
message?: string;
|
|
2105
|
-
data?:
|
|
2106
|
-
}
|
|
2107
|
-
|
|
2486
|
+
data?: unknown;
|
|
2487
|
+
}`
|
|
2488
|
+
});
|
|
2108
2489
|
}
|
|
2109
2490
|
}
|
|
2110
2491
|
class RegistrationFileTransformer {
|
|
@@ -2263,11 +2644,25 @@ function buildDecoratorArgs(options) {
|
|
|
2263
2644
|
}
|
|
2264
2645
|
function addContainerSupportToModel(codegenFs, options, projects) {
|
|
2265
2646
|
const logger = getCodegenLogger();
|
|
2266
|
-
const childType = options.childType?.trim() || "IKosDataModel";
|
|
2267
2647
|
const { modelFilePath } = resolveModelFilePath(codegenFs, options, projects);
|
|
2268
2648
|
if (!codegenFs.exists(modelFilePath)) {
|
|
2269
2649
|
throw new Error(`Model file not found: ${modelFilePath}`);
|
|
2270
2650
|
}
|
|
2651
|
+
const resolvedChild = options.childModel ? resolveChildModelType(
|
|
2652
|
+
codegenFs,
|
|
2653
|
+
{
|
|
2654
|
+
modelFilePath,
|
|
2655
|
+
modelProject: options.modelProject,
|
|
2656
|
+
childModel: options.childModel,
|
|
2657
|
+
childModelProject: options.childModelProject
|
|
2658
|
+
},
|
|
2659
|
+
projects
|
|
2660
|
+
) : {
|
|
2661
|
+
childType: options.childType,
|
|
2662
|
+
childTypeModule: options.childTypeModule
|
|
2663
|
+
};
|
|
2664
|
+
const childType = resolvedChild.childType?.trim() || "IKosDataModel";
|
|
2665
|
+
const childTypeModule = resolvedChild.childTypeModule;
|
|
2271
2666
|
logger.info(
|
|
2272
2667
|
`Adding container support (<${childType}>) to model: ${options.modelName}`
|
|
2273
2668
|
);
|
|
@@ -2279,6 +2674,10 @@ function addContainerSupportToModel(codegenFs, options, projects) {
|
|
|
2279
2674
|
]);
|
|
2280
2675
|
if (childType === "IKosDataModel") {
|
|
2281
2676
|
ensureNamedImport(sf, sdk, [{ name: "IKosDataModel", isTypeOnly: true }]);
|
|
2677
|
+
} else if (childTypeModule) {
|
|
2678
|
+
ensureNamedImport(sf, childTypeModule, [
|
|
2679
|
+
{ name: childType, isTypeOnly: true }
|
|
2680
|
+
]);
|
|
2282
2681
|
}
|
|
2283
2682
|
const cls = getModelClass(sf);
|
|
2284
2683
|
const className = cls.getName();
|
|
@@ -2308,6 +2707,140 @@ function addContainerSupportToModel(codegenFs, options, projects) {
|
|
|
2308
2707
|
});
|
|
2309
2708
|
return { modelFilePath };
|
|
2310
2709
|
}
|
|
2710
|
+
function addParentAwareToModel(codegenFs, options, projects) {
|
|
2711
|
+
const logger = getCodegenLogger();
|
|
2712
|
+
const { modelFilePath } = resolveModelFilePath(codegenFs, options, projects);
|
|
2713
|
+
if (!codegenFs.exists(modelFilePath)) {
|
|
2714
|
+
throw new Error(`Model file not found: ${modelFilePath}`);
|
|
2715
|
+
}
|
|
2716
|
+
logger.info(`Adding parent awareness to model: ${options.modelName}`);
|
|
2717
|
+
let optionsTypeName;
|
|
2718
|
+
transformSourceFile(codegenFs, modelFilePath, (sf) => {
|
|
2719
|
+
const sdk = resolveSdkModuleSpecifier(sf);
|
|
2720
|
+
ensureNamedImport(sf, sdk, [{ name: "kosParentAware" }]);
|
|
2721
|
+
const cls = getModelClass(sf);
|
|
2722
|
+
addClassDecorator(cls, "kosParentAware", {
|
|
2723
|
+
argsText: options.parentId ? `{ parentId: ${JSON.stringify(options.parentId)} }` : ""
|
|
2724
|
+
});
|
|
2725
|
+
const ctor = cls.getConstructors()[0];
|
|
2726
|
+
optionsTypeName = ctor?.getParameters()[1]?.getTypeNode()?.getText()?.replace(/<.*$/, "");
|
|
2727
|
+
});
|
|
2728
|
+
const optionsFilePath = optionsTypeName ? extendOptionsInterface(codegenFs, modelFilePath, optionsTypeName, logger) : void 0;
|
|
2729
|
+
return { modelFilePath, optionsFilePath };
|
|
2730
|
+
}
|
|
2731
|
+
function extendOptionsInterface(codegenFs, modelFilePath, optionsTypeName, logger) {
|
|
2732
|
+
const typesPath = path__namespace.join(
|
|
2733
|
+
path__namespace.dirname(modelFilePath),
|
|
2734
|
+
"types",
|
|
2735
|
+
"index.d.ts"
|
|
2736
|
+
);
|
|
2737
|
+
const target = codegenFs.exists(typesPath) ? typesPath : modelFilePath;
|
|
2738
|
+
let extended = false;
|
|
2739
|
+
transformSourceFile(codegenFs, target, (sf) => {
|
|
2740
|
+
const iface = sf.getInterface(optionsTypeName);
|
|
2741
|
+
if (!iface) return;
|
|
2742
|
+
const already = iface.getExtends().some((clause) => clause.getText().includes("KosParentAware"));
|
|
2743
|
+
if (already) {
|
|
2744
|
+
extended = true;
|
|
2745
|
+
return;
|
|
2746
|
+
}
|
|
2747
|
+
ensureNamedImport(sf, "@kosdev-code/kos-ui-sdk", [
|
|
2748
|
+
{ name: "KosParentAware", isTypeOnly: true }
|
|
2749
|
+
]);
|
|
2750
|
+
iface.addExtends("KosParentAware");
|
|
2751
|
+
extended = true;
|
|
2752
|
+
});
|
|
2753
|
+
if (!extended) {
|
|
2754
|
+
logger.warn(
|
|
2755
|
+
`Could not find interface ${optionsTypeName}; add "extends KosParentAware" to it by hand.`
|
|
2756
|
+
);
|
|
2757
|
+
return void 0;
|
|
2758
|
+
}
|
|
2759
|
+
return target;
|
|
2760
|
+
}
|
|
2761
|
+
function firstDecoratorArg(decoratorText) {
|
|
2762
|
+
const open = decoratorText.indexOf("(");
|
|
2763
|
+
if (open === -1) return void 0;
|
|
2764
|
+
const inner = decoratorText.slice(open + 1, decoratorText.lastIndexOf(")")).replace(/\s+/g, " ").trim();
|
|
2765
|
+
return inner || void 0;
|
|
2766
|
+
}
|
|
2767
|
+
function collectDecorated(cls, decoratorName) {
|
|
2768
|
+
const members = [];
|
|
2769
|
+
const visit = (name, decoratorTextOf, typeText) => {
|
|
2770
|
+
const text = decoratorTextOf();
|
|
2771
|
+
if (text === void 0) return;
|
|
2772
|
+
members.push({
|
|
2773
|
+
name,
|
|
2774
|
+
arg: firstDecoratorArg(text),
|
|
2775
|
+
type: typeText || void 0
|
|
2776
|
+
});
|
|
2777
|
+
};
|
|
2778
|
+
for (const m of cls.getMethods()) {
|
|
2779
|
+
const dec = m.getDecorator(decoratorName);
|
|
2780
|
+
if (dec) {
|
|
2781
|
+
visit(m.getName(), () => dec.getText(), m.getReturnTypeNode()?.getText());
|
|
2782
|
+
}
|
|
2783
|
+
}
|
|
2784
|
+
for (const p of cls.getProperties()) {
|
|
2785
|
+
const dec = p.getDecorator(decoratorName);
|
|
2786
|
+
if (dec) {
|
|
2787
|
+
visit(p.getName(), () => dec.getText(), p.getTypeNode()?.getText());
|
|
2788
|
+
}
|
|
2789
|
+
}
|
|
2790
|
+
return members;
|
|
2791
|
+
}
|
|
2792
|
+
function describeModel(codegenFs, options, projects) {
|
|
2793
|
+
const { modelFilePath } = resolveModelFilePath(codegenFs, options, projects);
|
|
2794
|
+
const content = codegenFs.read(modelFilePath);
|
|
2795
|
+
if (content === null) {
|
|
2796
|
+
throw new Error(`Model file not found: ${modelFilePath}`);
|
|
2797
|
+
}
|
|
2798
|
+
const project = new tsMorph.Project({ useInMemoryFileSystem: true });
|
|
2799
|
+
const sf = project.createSourceFile(modelFilePath, content, {
|
|
2800
|
+
overwrite: true
|
|
2801
|
+
});
|
|
2802
|
+
let modelType;
|
|
2803
|
+
const modelTypeDecl = sf.getVariableDeclaration("MODEL_TYPE");
|
|
2804
|
+
if (modelTypeDecl) {
|
|
2805
|
+
const init = modelTypeDecl.getInitializer()?.getText();
|
|
2806
|
+
if (init) modelType = init.replace(/^["'`]|["'`]$/g, "");
|
|
2807
|
+
}
|
|
2808
|
+
const cls = sf.getClasses().find((c) => c.getDecorator("kosModel")) ?? sf.getClasses().find((c) => c.isExported()) ?? sf.getClasses()[0];
|
|
2809
|
+
if (!cls) {
|
|
2810
|
+
return {
|
|
2811
|
+
modelFilePath,
|
|
2812
|
+
modelType,
|
|
2813
|
+
classDecorators: [],
|
|
2814
|
+
singleton: false,
|
|
2815
|
+
isCompanion: false,
|
|
2816
|
+
children: [],
|
|
2817
|
+
dependencies: [],
|
|
2818
|
+
topicHandlers: [],
|
|
2819
|
+
configProperties: [],
|
|
2820
|
+
serviceRequests: [],
|
|
2821
|
+
effects: [],
|
|
2822
|
+
futures: []
|
|
2823
|
+
};
|
|
2824
|
+
}
|
|
2825
|
+
const classDecorators = cls.getDecorators().map((d) => d.getName());
|
|
2826
|
+
const kosModelArg = cls.getDecorator("kosModel") ? firstDecoratorArg(cls.getDecorator("kosModel").getText()) : void 0;
|
|
2827
|
+
const singleton = /\bsingleton\s*:\s*true\b/.test(kosModelArg ?? "");
|
|
2828
|
+
return {
|
|
2829
|
+
modelFilePath,
|
|
2830
|
+
modelType,
|
|
2831
|
+
className: cls.getName(),
|
|
2832
|
+
classDecorators,
|
|
2833
|
+
singleton,
|
|
2834
|
+
isCompanion: classDecorators.includes("kosCompanion"),
|
|
2835
|
+
children: collectDecorated(cls, "kosChild"),
|
|
2836
|
+
dependencies: collectDecorated(cls, "kosDependency"),
|
|
2837
|
+
topicHandlers: collectDecorated(cls, "kosTopicHandler"),
|
|
2838
|
+
configProperties: collectDecorated(cls, "kosConfigProperty"),
|
|
2839
|
+
serviceRequests: collectDecorated(cls, "kosServiceRequest"),
|
|
2840
|
+
effects: collectDecorated(cls, "kosModelEffect"),
|
|
2841
|
+
futures: collectDecorated(cls, "kosFuture")
|
|
2842
|
+
};
|
|
2843
|
+
}
|
|
2311
2844
|
function addModelEffectToModel(codegenFs, options, projects) {
|
|
2312
2845
|
const logger = getCodegenLogger();
|
|
2313
2846
|
const { modelFilePath } = resolveModelFilePath(codegenFs, options, projects);
|
|
@@ -2383,7 +2916,18 @@ function addChildToModel(codegenFs, options, projects) {
|
|
|
2383
2916
|
logger.info(
|
|
2384
2917
|
`Adding @kosChild "${options.propertyName}" (${shape}) to ${options.modelName}`
|
|
2385
2918
|
);
|
|
2386
|
-
const
|
|
2919
|
+
const resolvedChild = options.childModel ? resolveChildModelType(
|
|
2920
|
+
codegenFs,
|
|
2921
|
+
{
|
|
2922
|
+
modelFilePath,
|
|
2923
|
+
modelProject: options.modelProject,
|
|
2924
|
+
childModel: options.childModel,
|
|
2925
|
+
childModelProject: options.childModelProject
|
|
2926
|
+
},
|
|
2927
|
+
projects
|
|
2928
|
+
) : { childType: options.childType, childTypeModule: options.childPackage };
|
|
2929
|
+
const childType = resolvedChild.childType?.trim();
|
|
2930
|
+
const childTypeModule = resolvedChild.childTypeModule;
|
|
2387
2931
|
transformSourceFile(codegenFs, modelFilePath, (sf) => {
|
|
2388
2932
|
const sdk = resolveSdkModuleSpecifier(sf);
|
|
2389
2933
|
const sdkImports = [
|
|
@@ -2391,8 +2935,8 @@ function addChildToModel(codegenFs, options, projects) {
|
|
|
2391
2935
|
];
|
|
2392
2936
|
if (shape === "container") sdkImports.push({ name: "KosModelContainer" });
|
|
2393
2937
|
ensureNamedImport(sf, sdk, sdkImports);
|
|
2394
|
-
if (
|
|
2395
|
-
ensureNamedImport(sf,
|
|
2938
|
+
if (childTypeModule && childType && /^[A-Za-z_$][\w$]*$/.test(childType) && !FRAMEWORK_TYPES.has(childType)) {
|
|
2939
|
+
ensureNamedImport(sf, childTypeModule, [
|
|
2396
2940
|
{ name: childType, isTypeOnly: true }
|
|
2397
2941
|
]);
|
|
2398
2942
|
}
|
|
@@ -2538,6 +3082,154 @@ function addComputedToModel(codegenFs, options, projects) {
|
|
|
2538
3082
|
});
|
|
2539
3083
|
return { modelFilePath };
|
|
2540
3084
|
}
|
|
3085
|
+
function mocksFilePathFor(modelFilePath, modelBase) {
|
|
3086
|
+
return path__namespace.join(
|
|
3087
|
+
path__namespace.dirname(modelFilePath),
|
|
3088
|
+
"mocks",
|
|
3089
|
+
`${modelBase}-mocks.ts`
|
|
3090
|
+
);
|
|
3091
|
+
}
|
|
3092
|
+
function mockRegisterFunctionName(modelBase) {
|
|
3093
|
+
return `register${pascalCase(modelBase)}Mocks`;
|
|
3094
|
+
}
|
|
3095
|
+
function toMockRoutePattern(servicePath) {
|
|
3096
|
+
return servicePath.replace(/\{([^}]+)\}/g, ":$1");
|
|
3097
|
+
}
|
|
3098
|
+
function mockRouteCall(method, pattern, body) {
|
|
3099
|
+
const shorthand = {
|
|
3100
|
+
get: "get",
|
|
3101
|
+
post: "post",
|
|
3102
|
+
put: "put",
|
|
3103
|
+
delete: "del"
|
|
3104
|
+
};
|
|
3105
|
+
const fn = shorthand[method];
|
|
3106
|
+
return fn ? `KosMock.${fn}(${JSON.stringify(pattern)}, ${body});` : `KosMock.route(${JSON.stringify(method.toUpperCase())}, ${JSON.stringify(
|
|
3107
|
+
pattern
|
|
3108
|
+
)}, ${body});`;
|
|
3109
|
+
}
|
|
3110
|
+
function header(modelBase, registerFn) {
|
|
3111
|
+
return `/**
|
|
3112
|
+
* KosMock routes for the ${modelBase} model's PROVISIONAL endpoints — the ones
|
|
3113
|
+
* this project's generated OpenAPI types do not declare.
|
|
3114
|
+
*
|
|
3115
|
+
* NOTHING IMPORTS THIS FILE. Call \`${registerFn}()\` from the app's dev entry,
|
|
3116
|
+
* gated so it cannot run in a production build (\`import.meta.env.DEV\`, a
|
|
3117
|
+
* dev-only entry module, or behind your own switch). Registering a route enables
|
|
3118
|
+
* KosMock, and a mock that shipped would shadow the real endpoint once it exists.
|
|
3119
|
+
*
|
|
3120
|
+
* Unmatched requests still pass through to the real device (KosMock's hybrid
|
|
3121
|
+
* default), so these routes shadow only the paths named below. Every mocked
|
|
3122
|
+
* response carries the \`kos-mocked: true\` wire header, so mock-fed data is
|
|
3123
|
+
* identifiable in devtools.
|
|
3124
|
+
*
|
|
3125
|
+
* DELETE THIS FILE once every endpoint below is in the generated types.
|
|
3126
|
+
*/
|
|
3127
|
+
`;
|
|
3128
|
+
}
|
|
3129
|
+
function ensureMockRoute(codegenFs, options) {
|
|
3130
|
+
const mocksFilePath = mocksFilePathFor(
|
|
3131
|
+
options.modelFilePath,
|
|
3132
|
+
options.modelBase
|
|
3133
|
+
);
|
|
3134
|
+
const registerFunction = mockRegisterFunctionName(options.modelBase);
|
|
3135
|
+
const routePattern = toMockRoutePattern(options.servicePath);
|
|
3136
|
+
const sampleName = `SAMPLE_${constantCase(dashCase(options.endpointKey))}`;
|
|
3137
|
+
const samplePlaceholder = !options.sampleText;
|
|
3138
|
+
if (!codegenFs.exists(mocksFilePath)) {
|
|
3139
|
+
codegenFs.write(mocksFilePath, header(options.modelBase, registerFunction));
|
|
3140
|
+
}
|
|
3141
|
+
const sdkSpecifier = options.sdkModuleSpecifier.startsWith(".") ? `../${options.sdkModuleSpecifier}` : options.sdkModuleSpecifier;
|
|
3142
|
+
let routeCreated = false;
|
|
3143
|
+
transformSourceFile(codegenFs, mocksFilePath, (sf) => {
|
|
3144
|
+
ensureNamedImport(sf, sdkSpecifier, [{ name: "KosMock" }]);
|
|
3145
|
+
ensureNamedImport(sf, "../services", [
|
|
3146
|
+
{ name: options.rawAlias, isTypeOnly: true }
|
|
3147
|
+
]);
|
|
3148
|
+
ensureSample(sf, {
|
|
3149
|
+
name: sampleName,
|
|
3150
|
+
type: options.rawAlias,
|
|
3151
|
+
servicePath: options.servicePath,
|
|
3152
|
+
method: options.method,
|
|
3153
|
+
sampleText: options.sampleText
|
|
3154
|
+
});
|
|
3155
|
+
const statement = mockRouteCall(
|
|
3156
|
+
options.method,
|
|
3157
|
+
routePattern,
|
|
3158
|
+
`{ data: ${sampleName} }`
|
|
3159
|
+
);
|
|
3160
|
+
routeCreated = ensureRegisteredRoute(sf, {
|
|
3161
|
+
registerFunction,
|
|
3162
|
+
modelBase: options.modelBase,
|
|
3163
|
+
statement,
|
|
3164
|
+
// Matching on the pattern literal alone would collide across methods.
|
|
3165
|
+
marker: `${options.method.toUpperCase()} ${routePattern}`,
|
|
3166
|
+
matches: (existing) => existing.includes(JSON.stringify(routePattern)) && existing.includes(shorthandOrMethod(options.method))
|
|
3167
|
+
});
|
|
3168
|
+
});
|
|
3169
|
+
return {
|
|
3170
|
+
mocksFilePath,
|
|
3171
|
+
registerFunction,
|
|
3172
|
+
routePattern,
|
|
3173
|
+
sampleName,
|
|
3174
|
+
routeCreated,
|
|
3175
|
+
samplePlaceholder
|
|
3176
|
+
};
|
|
3177
|
+
}
|
|
3178
|
+
function shorthandOrMethod(method) {
|
|
3179
|
+
const shorthand = {
|
|
3180
|
+
get: "KosMock.get(",
|
|
3181
|
+
post: "KosMock.post(",
|
|
3182
|
+
put: "KosMock.put(",
|
|
3183
|
+
delete: "KosMock.del("
|
|
3184
|
+
};
|
|
3185
|
+
return shorthand[method] ?? `"${method.toUpperCase()}"`;
|
|
3186
|
+
}
|
|
3187
|
+
function ensureSample(sf, spec) {
|
|
3188
|
+
if (sf.getVariableDeclaration(spec.name)) return;
|
|
3189
|
+
const lines = [
|
|
3190
|
+
`Sample payload for \`${spec.method.toUpperCase()} ${spec.servicePath}\` — the`,
|
|
3191
|
+
"UNWRAPPED `data` payload, i.e. exactly what the transform receives."
|
|
3192
|
+
];
|
|
3193
|
+
if (!spec.sampleText) {
|
|
3194
|
+
lines.push(
|
|
3195
|
+
"",
|
|
3196
|
+
"TODO: replace the placeholder with something the backend would actually",
|
|
3197
|
+
"serve. Until then the route answers with nothing and the transform is",
|
|
3198
|
+
"never exercised."
|
|
3199
|
+
);
|
|
3200
|
+
}
|
|
3201
|
+
const docs = lines.join("\n");
|
|
3202
|
+
sf.addVariableStatement({
|
|
3203
|
+
isExported: true,
|
|
3204
|
+
declarationKind: tsMorph.VariableDeclarationKind.Const,
|
|
3205
|
+
docs: [docs],
|
|
3206
|
+
declarations: [
|
|
3207
|
+
{
|
|
3208
|
+
name: spec.name,
|
|
3209
|
+
type: spec.type,
|
|
3210
|
+
initializer: spec.sampleText ?? `null as unknown as ${spec.type}`
|
|
3211
|
+
}
|
|
3212
|
+
]
|
|
3213
|
+
});
|
|
3214
|
+
}
|
|
3215
|
+
function ensureRegisteredRoute(sf, spec) {
|
|
3216
|
+
let fn = sf.getFunction(spec.registerFunction);
|
|
3217
|
+
if (!fn) {
|
|
3218
|
+
fn = sf.addFunction({
|
|
3219
|
+
name: spec.registerFunction,
|
|
3220
|
+
isExported: true,
|
|
3221
|
+
returnType: "void",
|
|
3222
|
+
docs: [
|
|
3223
|
+
`Arm the ${spec.modelBase} model's provisional endpoints. Call this from a dev-only entry point — never from code that ships.`
|
|
3224
|
+
]
|
|
3225
|
+
});
|
|
3226
|
+
}
|
|
3227
|
+
const already = fn.getStatements().some((statement) => spec.matches(statement.getText()));
|
|
3228
|
+
if (already) return false;
|
|
3229
|
+
fn.addStatements(`// ${spec.marker}
|
|
3230
|
+
${spec.statement}`);
|
|
3231
|
+
return true;
|
|
3232
|
+
}
|
|
2541
3233
|
const SERVICE_MODULE_RE = /\/utils\/services\/.*\/service\.ts$/;
|
|
2542
3234
|
function findServiceModules(codegenFs, sourceRoot) {
|
|
2543
3235
|
return codegenFs.listFiles(sourceRoot).filter((f) => SERVICE_MODULE_RE.test(f.split(path__namespace.sep).join("/")));
|
|
@@ -2547,6 +3239,143 @@ function toImportSpecifier(fromFile, toFileNoExt) {
|
|
|
2547
3239
|
if (!rel.startsWith(".")) rel = `./${rel}`;
|
|
2548
3240
|
return rel;
|
|
2549
3241
|
}
|
|
3242
|
+
function fromImportSpecifier(fromFile, specifier) {
|
|
3243
|
+
return path__namespace.join(path__namespace.dirname(fromFile), specifier);
|
|
3244
|
+
}
|
|
3245
|
+
function assertServiceModuleAcceptsTransform(codegenFs, serviceModuleFile, modelProject) {
|
|
3246
|
+
const file = `${serviceModuleFile}.ts`;
|
|
3247
|
+
if (!codegenFs.exists(file)) return;
|
|
3248
|
+
const typeParams = readSourceFile(codegenFs, file, (sf) => {
|
|
3249
|
+
const alias = sf.getTypeAlias("EndpointCtx");
|
|
3250
|
+
return alias ? alias.getTypeParameters().length : null;
|
|
3251
|
+
});
|
|
3252
|
+
if (typeParams === null || typeParams >= 2) return;
|
|
3253
|
+
throw new Error(
|
|
3254
|
+
`${file} predates the transform-aware service layer (EndpointCtx takes ${typeParams} type parameter). Run \`kosui api:generate --project ${modelProject} --helpers-only\` to refresh the helper layer in place (no spec fetch, openapi.d.ts untouched), then add the service request.`
|
|
3255
|
+
);
|
|
3256
|
+
}
|
|
3257
|
+
function assertServiceModuleAcceptsProvisional(codegenFs, serviceModuleFile, modelProject) {
|
|
3258
|
+
const file = `${serviceModuleFile}.ts`;
|
|
3259
|
+
if (!codegenFs.exists(file)) return;
|
|
3260
|
+
const declared = readSourceFile(
|
|
3261
|
+
codegenFs,
|
|
3262
|
+
file,
|
|
3263
|
+
(sf) => Boolean(sf.getFunction("provisionalServiceRequest"))
|
|
3264
|
+
);
|
|
3265
|
+
if (declared) return;
|
|
3266
|
+
throw new Error(
|
|
3267
|
+
`${file} has no \`provisionalServiceRequest\` — the helper layer predates provisional endpoints. Run \`kosui api:generate --project ${modelProject} --helpers-only\` to refresh it in place (no spec fetch, openapi.d.ts untouched), then add the service request.`
|
|
3268
|
+
);
|
|
3269
|
+
}
|
|
3270
|
+
function sameEndpoint(existing, candidate) {
|
|
3271
|
+
const args = (text) => {
|
|
3272
|
+
const match = text.match(
|
|
3273
|
+
/^endpoint\s*\(\s*(['"])(.*?)\1(?:\s+as\s+ApiPath)?\s*,\s*(['"])(.*?)\3\s*,?\s*\)$/
|
|
3274
|
+
);
|
|
3275
|
+
return match ? [match[2], match[4].toLowerCase()] : null;
|
|
3276
|
+
};
|
|
3277
|
+
const a = args(existing.trim());
|
|
3278
|
+
const b = args(candidate.trim());
|
|
3279
|
+
return !!a && !!b && a[0] === b[0] && a[1] === b[1];
|
|
3280
|
+
}
|
|
3281
|
+
function editDistance(a, b) {
|
|
3282
|
+
let previous = Array.from({ length: b.length + 1 }, (_, i) => i);
|
|
3283
|
+
for (let i = 1; i <= a.length; i++) {
|
|
3284
|
+
const current = [i];
|
|
3285
|
+
for (let j = 1; j <= b.length; j++) {
|
|
3286
|
+
current[j] = Math.min(
|
|
3287
|
+
previous[j] + 1,
|
|
3288
|
+
current[j - 1] + 1,
|
|
3289
|
+
previous[j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1)
|
|
3290
|
+
);
|
|
3291
|
+
}
|
|
3292
|
+
previous = current;
|
|
3293
|
+
}
|
|
3294
|
+
return previous[b.length];
|
|
3295
|
+
}
|
|
3296
|
+
const NEAREST_SHOWN = 5;
|
|
3297
|
+
function provisionalEntryComment(servicePath, method, catalogName) {
|
|
3298
|
+
return `/**
|
|
3299
|
+
* PROVISIONAL — \`${method.toUpperCase()} ${servicePath}\` is NOT in this project's
|
|
3300
|
+
* generated OpenAPI types. \`as ApiPath\` is standing in for a \`paths\` entry that
|
|
3301
|
+
* does not exist, so nothing about this endpoint is checked against a spec and
|
|
3302
|
+
* its response cannot be derived from one. Do not read this as a real endpoint.
|
|
3303
|
+
*
|
|
3304
|
+
* WHEN THE ENDPOINT LANDS:
|
|
3305
|
+
* 1. regenerate this project's API types (\`kosui api:generate\`)
|
|
3306
|
+
* 2. delete \` as ApiPath\` from the entry below
|
|
3307
|
+
* 3. point the endpoint's \`…Raw\` alias at
|
|
3308
|
+
* \`EndpointResponse<typeof ${catalogName}.…>\`
|
|
3309
|
+
* 4. swap \`provisionalServiceRequest\` for \`serviceRequest\` in the model
|
|
3310
|
+
* 5. delete the model's \`mocks/\` module
|
|
3311
|
+
* 6. KEEP the transform — it is real code and survives all of the above
|
|
3312
|
+
*/
|
|
3313
|
+
`;
|
|
3314
|
+
}
|
|
3315
|
+
function provisionalMethodDocs(servicePath, method) {
|
|
3316
|
+
return [
|
|
3317
|
+
`PROVISIONAL — \`${method.toUpperCase()} ${servicePath}\` is not in the`,
|
|
3318
|
+
"generated OpenAPI types, so this request is served by a KosMock route, not",
|
|
3319
|
+
"by the device. The feature it backs is NOT complete: report the missing",
|
|
3320
|
+
"endpoint rather than treating this as finished.",
|
|
3321
|
+
"",
|
|
3322
|
+
"See the endpoint's entry in `./services` for what to undo when it lands."
|
|
3323
|
+
].join("\n");
|
|
3324
|
+
}
|
|
3325
|
+
function rawTypeDocs(servicePath, method, rawType) {
|
|
3326
|
+
const shape = rawType ? [
|
|
3327
|
+
"Stated by hand and unverified: there is no spec entry to check it",
|
|
3328
|
+
"against."
|
|
3329
|
+
] : [
|
|
3330
|
+
"TODO: state the shape the backend is expected to serve. `unknown`",
|
|
3331
|
+
"compiles, but leaves the response boundary undescribed and the",
|
|
3332
|
+
"transform with nothing to narrow."
|
|
3333
|
+
];
|
|
3334
|
+
return [
|
|
3335
|
+
`Raw wire shape of \`${method.toUpperCase()} ${servicePath}\` — the UNWRAPPED`,
|
|
3336
|
+
"`data` payload, since the client strips the `{status, data}` envelope",
|
|
3337
|
+
"before the transform runs.",
|
|
3338
|
+
"",
|
|
3339
|
+
...shape,
|
|
3340
|
+
"",
|
|
3341
|
+
"Replace with `EndpointResponse<…>` once the endpoint is in the generated",
|
|
3342
|
+
"types."
|
|
3343
|
+
].join("\n");
|
|
3344
|
+
}
|
|
3345
|
+
function removalSteps(spec) {
|
|
3346
|
+
return [
|
|
3347
|
+
`Regenerate the API types for ${spec.modelProject} (generate_api_types), and confirm ${spec.method.toUpperCase()} ${spec.servicePath} is now in them.`,
|
|
3348
|
+
`In ${spec.servicesFilePath}: delete \` as ApiPath\` from \`${spec.catalogName}.${spec.endpointKey}\`.`,
|
|
3349
|
+
`In ${spec.servicesFilePath}: point the endpoint's \`…Raw\` alias at \`EndpointResponse<typeof ${spec.catalogName}.${spec.endpointKey}>\` and drop the hand-written shape.`,
|
|
3350
|
+
`In the model: swap \`provisionalServiceRequest\` for \`serviceRequest\` and drop its explicit type arguments — the response type comes from the spec again.`,
|
|
3351
|
+
spec.mocksFilePath ? `Delete ${spec.mocksFilePath} (and the call to its register function in the app) once no provisional endpoint is left in it.` : `Delete the model's mocks module once no provisional endpoint is left in it.`,
|
|
3352
|
+
`KEEP \`${spec.mapperName}\` and the data type it produces. The transform is real code; it is what let the model read the final shape all along.`
|
|
3353
|
+
];
|
|
3354
|
+
}
|
|
3355
|
+
function untypedEndpointError(spec) {
|
|
3356
|
+
const methodsForPath = spec.operations.filter((op) => op.path === spec.servicePath).map((op) => op.method);
|
|
3357
|
+
const pathTypedForOtherMethods = methodsForPath.length > 0;
|
|
3358
|
+
const nearest = [...new Set(spec.operations.map((op) => op.path))].sort(
|
|
3359
|
+
(a, b) => editDistance(a, spec.servicePath) - editDistance(b, spec.servicePath)
|
|
3360
|
+
).slice(0, NEAREST_SHOWN);
|
|
3361
|
+
const waysForward = [
|
|
3362
|
+
`The endpoint EXISTS on the device but this project has not pulled types for it: re-run api:generate (generate_api_types) for ${spec.modelProject}, then add the request unchanged. Confirm on the device first — kos-device search_services / describe_endpoint — since the live OpenAPI is what says whether it exists.`,
|
|
3363
|
+
`The endpoint DOES NOT EXIST yet: pass mock:"auto" (the default) or mock:"always" to emit the provisional, mock-backed form — real decorator, lifecycle, error-first handler and transform, with a KosMock route standing in for the wire response.`,
|
|
3364
|
+
`What is NOT supported: falling back to resolveServiceUrl / ServiceFactory.build / getAll to make the UI render. That compiles and looks finished, which is worse than the hole. A missing endpoint is a blocker to report, not a licence to route around the generated service layer.`
|
|
3365
|
+
];
|
|
3366
|
+
const message = pathTypedForOtherMethods ? `${spec.method.toUpperCase()} ${spec.servicePath} is not in the generated OpenAPI types for ${spec.modelProject} — the path is typed, but only for: ${methodsForPath.map((m) => m.toUpperCase()).join(", ")}.` : `${spec.method.toUpperCase()} ${spec.servicePath} is not in the generated OpenAPI types for ${spec.modelProject} — the path is absent entirely. Nothing was written.`;
|
|
3367
|
+
const error = new Error(message);
|
|
3368
|
+
error.details = {
|
|
3369
|
+
servicePath: spec.servicePath,
|
|
3370
|
+
method: spec.method,
|
|
3371
|
+
pathTypedForOtherMethods,
|
|
3372
|
+
methodsForPath,
|
|
3373
|
+
otherEndpointsInThisApi: nearest,
|
|
3374
|
+
warning: "These are OTHER endpoints in this API, listed only so they can be ruled out — they are almost certainly NOT what you want. Do not substitute one for the requested path because the strings look similar.",
|
|
3375
|
+
waysForward
|
|
3376
|
+
};
|
|
3377
|
+
return error;
|
|
3378
|
+
}
|
|
2550
3379
|
function addServiceRequestToModel(codegenFs, options, projects) {
|
|
2551
3380
|
const logger = getCodegenLogger();
|
|
2552
3381
|
const { modelFilePath, sourceRoot } = resolveModelFilePath(
|
|
@@ -2557,14 +3386,15 @@ function addServiceRequestToModel(codegenFs, options, projects) {
|
|
|
2557
3386
|
if (!codegenFs.exists(modelFilePath)) {
|
|
2558
3387
|
throw new Error(`Model file not found: ${modelFilePath}`);
|
|
2559
3388
|
}
|
|
2560
|
-
let
|
|
2561
|
-
if (
|
|
2562
|
-
|
|
3389
|
+
let serviceModuleFile;
|
|
3390
|
+
if (options.serviceModulePath) {
|
|
3391
|
+
serviceModuleFile = options.serviceModulePath.replace(/\.ts$/, "");
|
|
3392
|
+
} else if (options.serviceModule) {
|
|
3393
|
+
serviceModuleFile = fromImportSpecifier(
|
|
2563
3394
|
modelFilePath,
|
|
2564
|
-
options.
|
|
3395
|
+
options.serviceModule
|
|
2565
3396
|
);
|
|
2566
|
-
}
|
|
2567
|
-
if (!serviceImport) {
|
|
3397
|
+
} else {
|
|
2568
3398
|
if (!sourceRoot) {
|
|
2569
3399
|
throw new Error(
|
|
2570
3400
|
"Cannot auto-resolve the service module without a project source root. Pass serviceModule."
|
|
@@ -2576,68 +3406,177 @@ function addServiceRequestToModel(codegenFs, options, projects) {
|
|
|
2576
3406
|
"No generated service module found. Run `kosui api:generate` for this project first."
|
|
2577
3407
|
);
|
|
2578
3408
|
}
|
|
2579
|
-
const specifiers = modules.map(
|
|
2580
|
-
(m) => toImportSpecifier(modelFilePath, m.replace(/\.ts$/, ""))
|
|
2581
|
-
);
|
|
2582
3409
|
if (modules.length > 1) {
|
|
3410
|
+
const specifiers = modules.map(
|
|
3411
|
+
(m) => toImportSpecifier(modelFilePath, m.replace(/\.ts$/, ""))
|
|
3412
|
+
);
|
|
2583
3413
|
throw new Error(
|
|
2584
3414
|
`Multiple service modules found — pass serviceModule (one of): ${specifiers.join(
|
|
2585
3415
|
", "
|
|
2586
3416
|
)}`
|
|
2587
3417
|
);
|
|
2588
3418
|
}
|
|
2589
|
-
|
|
3419
|
+
serviceModuleFile = modules[0].replace(/\.ts$/, "");
|
|
2590
3420
|
}
|
|
2591
|
-
|
|
3421
|
+
assertServiceModuleAcceptsTransform(
|
|
3422
|
+
codegenFs,
|
|
3423
|
+
serviceModuleFile,
|
|
3424
|
+
options.modelProject
|
|
3425
|
+
);
|
|
3426
|
+
const modelBase = modelBaseName(modelFilePath, options.modelName);
|
|
3427
|
+
const servicesFilePath = servicesFilePathFor(modelFilePath, modelBase);
|
|
3428
|
+
const serviceImportFromModel = toImportSpecifier(
|
|
3429
|
+
modelFilePath,
|
|
3430
|
+
serviceModuleFile
|
|
3431
|
+
);
|
|
3432
|
+
const serviceImportFromServices = toImportSpecifier(
|
|
3433
|
+
servicesFilePath,
|
|
3434
|
+
serviceModuleFile
|
|
3435
|
+
);
|
|
3436
|
+
const method = (options.method || "get").toLowerCase();
|
|
2592
3437
|
const mode = options.mode ?? (options.lifecycle ? "lifecycle" : "method");
|
|
2593
3438
|
const lifecycle = options.lifecycle || "LOAD";
|
|
3439
|
+
const catalogName = `${pascalCase(modelBase)}Endpoints`;
|
|
3440
|
+
const mockMode = options.mock ?? "auto";
|
|
3441
|
+
const operations = readServiceModuleOperations(codegenFs, serviceModuleFile);
|
|
3442
|
+
const pathValidated = operations !== null;
|
|
3443
|
+
const operationTyped = operations === null || operations.some(
|
|
3444
|
+
(op) => op.path === options.servicePath && op.method === method
|
|
3445
|
+
);
|
|
3446
|
+
if (!operationTyped && mockMode === "never") {
|
|
3447
|
+
throw untypedEndpointError({
|
|
3448
|
+
servicePath: options.servicePath,
|
|
3449
|
+
method,
|
|
3450
|
+
modelProject: options.modelProject,
|
|
3451
|
+
operations: operations ?? []
|
|
3452
|
+
});
|
|
3453
|
+
}
|
|
3454
|
+
const provisional = !operationTyped;
|
|
3455
|
+
const writeMock = provisional || mockMode === "always";
|
|
3456
|
+
if (provisional) {
|
|
3457
|
+
assertServiceModuleAcceptsProvisional(
|
|
3458
|
+
codegenFs,
|
|
3459
|
+
serviceModuleFile,
|
|
3460
|
+
options.modelProject
|
|
3461
|
+
);
|
|
3462
|
+
}
|
|
2594
3463
|
logger.info(
|
|
2595
|
-
`Adding ${mode}-driven @kosServiceRequest "${options.methodName}" (${method} ${options.servicePath}) to ${options.modelName}`
|
|
3464
|
+
`Adding ${mode}-driven ${provisional ? "PROVISIONAL " : ""}@kosServiceRequest "${options.methodName}" (${method} ${options.servicePath}) to ${options.modelName}`
|
|
2596
3465
|
);
|
|
3466
|
+
if (provisional) {
|
|
3467
|
+
logger.warn(
|
|
3468
|
+
`${method.toUpperCase()} ${options.servicePath} is not in ${options.modelProject}'s generated OpenAPI types — emitting the provisional, mock-backed form. The feature is NOT complete until the endpoint lands.`
|
|
3469
|
+
);
|
|
3470
|
+
}
|
|
3471
|
+
ensureServicesModule(codegenFs, modelFilePath, modelBase);
|
|
3472
|
+
let endpointKey = options.methodName;
|
|
3473
|
+
let endpointCreated = true;
|
|
3474
|
+
let dataAlias = "";
|
|
3475
|
+
let mapperName = "";
|
|
3476
|
+
let ctxAlias = "";
|
|
3477
|
+
let rawAlias = "";
|
|
3478
|
+
transformSourceFile(codegenFs, servicesFilePath, (sf) => {
|
|
3479
|
+
ensureNamedImport(sf, serviceImportFromServices, [{ name: "endpoint" }]);
|
|
3480
|
+
if (provisional) {
|
|
3481
|
+
ensureNamedImport(sf, serviceImportFromServices, [
|
|
3482
|
+
{ name: "ApiPath", isTypeOnly: true }
|
|
3483
|
+
]);
|
|
3484
|
+
}
|
|
3485
|
+
const pathText = provisional ? `${JSON.stringify(options.servicePath)} as ApiPath` : JSON.stringify(options.servicePath);
|
|
3486
|
+
const entry = ensureConstCatalogEntry(sf, {
|
|
3487
|
+
catalogName,
|
|
3488
|
+
key: options.methodName,
|
|
3489
|
+
initializer: `endpoint(${pathText}, ${JSON.stringify(method)})`,
|
|
3490
|
+
equals: sameEndpoint,
|
|
3491
|
+
leadingTrivia: provisional ? provisionalEntryComment(options.servicePath, method, catalogName) : void 0
|
|
3492
|
+
});
|
|
3493
|
+
endpointKey = entry.key;
|
|
3494
|
+
endpointCreated = entry.created;
|
|
3495
|
+
dataAlias = `${pascalCase(endpointKey)}Data`;
|
|
3496
|
+
mapperName = `to${pascalCase(endpointKey)}Data`;
|
|
3497
|
+
ctxAlias = `${pascalCase(endpointKey)}Ctx`;
|
|
3498
|
+
rawAlias = `${pascalCase(endpointKey)}Raw`;
|
|
3499
|
+
let rawType;
|
|
3500
|
+
if (provisional) {
|
|
3501
|
+
ensureExportedTypeAlias(sf, {
|
|
3502
|
+
name: rawAlias,
|
|
3503
|
+
type: options.rawType || "unknown",
|
|
3504
|
+
docs: rawTypeDocs(options.servicePath, method, options.rawType)
|
|
3505
|
+
});
|
|
3506
|
+
rawType = rawAlias;
|
|
3507
|
+
} else {
|
|
3508
|
+
rawType = `EndpointResponse<typeof ${catalogName}.${endpointKey}>`;
|
|
3509
|
+
ensureNamedImport(sf, serviceImportFromServices, [
|
|
3510
|
+
{ name: "EndpointResponse", isTypeOnly: true }
|
|
3511
|
+
]);
|
|
3512
|
+
if (writeMock) {
|
|
3513
|
+
ensureExportedTypeAlias(sf, {
|
|
3514
|
+
name: rawAlias,
|
|
3515
|
+
type: rawType,
|
|
3516
|
+
docs: `Raw wire shape of \`${method.toUpperCase()} ${options.servicePath}\`, as the spec declares it — what a mock for this endpoint must serve.`
|
|
3517
|
+
});
|
|
3518
|
+
}
|
|
3519
|
+
}
|
|
3520
|
+
ensureExportedTypeAlias(sf, { name: dataAlias, type: rawType });
|
|
3521
|
+
ensureExportedMapper(sf, {
|
|
3522
|
+
name: mapperName,
|
|
3523
|
+
rawType,
|
|
3524
|
+
dataType: dataAlias
|
|
3525
|
+
});
|
|
3526
|
+
if (mode === "method") {
|
|
3527
|
+
ensureNamedImport(sf, serviceImportFromServices, [
|
|
3528
|
+
{ name: "EndpointCtx", isTypeOnly: true }
|
|
3529
|
+
]);
|
|
3530
|
+
ensureExportedTypeAlias(sf, {
|
|
3531
|
+
name: ctxAlias,
|
|
3532
|
+
type: `EndpointCtx<typeof ${catalogName}.${endpointKey}, ${dataAlias}>`
|
|
3533
|
+
});
|
|
3534
|
+
}
|
|
3535
|
+
});
|
|
2597
3536
|
transformSourceFile(codegenFs, modelFilePath, (sf) => {
|
|
2598
|
-
const endpointConst = endpointConstName(options.methodName);
|
|
2599
3537
|
const className = getModelClass(sf).getName();
|
|
2600
3538
|
if (!className) throw new Error("Model class has no name.");
|
|
2601
3539
|
const typeParams = getModelClass(sf).getTypeParameters().map((tp) => tp.getText());
|
|
2602
|
-
|
|
2603
|
-
|
|
2604
|
-
|
|
2605
|
-
]);
|
|
2606
|
-
ensureModuleConst(sf, {
|
|
2607
|
-
name: endpointConst,
|
|
2608
|
-
initializer: `endpoint(${JSON.stringify(
|
|
2609
|
-
options.servicePath
|
|
2610
|
-
)}, ${JSON.stringify(method)})`
|
|
2611
|
-
});
|
|
3540
|
+
const decoratorName = provisional ? "provisionalServiceRequest" : "serviceRequest";
|
|
3541
|
+
const decoratorTypeArgs = provisional ? [rawAlias, dataAlias] : void 0;
|
|
3542
|
+
ensureNamedImport(sf, serviceImportFromModel, [{ name: decoratorName }]);
|
|
2612
3543
|
if (mode === "lifecycle") {
|
|
2613
|
-
ensureNamedImport(sf,
|
|
2614
|
-
{ name:
|
|
3544
|
+
ensureNamedImport(sf, "./services", [
|
|
3545
|
+
{ name: catalogName },
|
|
3546
|
+
{ name: mapperName },
|
|
3547
|
+
{ name: dataAlias, isTypeOnly: true },
|
|
3548
|
+
...provisional ? [{ name: rawAlias, isTypeOnly: true }] : []
|
|
2615
3549
|
]);
|
|
2616
3550
|
ensureNamedImport(sf, resolveSdkModuleSpecifier(sf), [
|
|
2617
3551
|
{ name: "DependencyLifecycle" }
|
|
2618
3552
|
]);
|
|
2619
3553
|
addDecoratedMethod(getModelClass(sf), {
|
|
2620
3554
|
name: options.methodName,
|
|
2621
|
-
decoratorName
|
|
2622
|
-
|
|
3555
|
+
decoratorName,
|
|
3556
|
+
decoratorTypeArgs,
|
|
3557
|
+
decoratorArgsText: `${catalogName}.${endpointKey}, { lifecycle: DependencyLifecycle.${lifecycle}, transform: ${mapperName} }`,
|
|
3558
|
+
docs: provisional ? provisionalMethodDocs(options.servicePath, method) : void 0,
|
|
2623
3559
|
// The manager invokes phase handlers error-first, passing (null, data)
|
|
2624
3560
|
// on success. The `error` half is only ever reached because
|
|
2625
3561
|
// `serviceRequest` supplies an errorHandler — the bare decorator
|
|
2626
3562
|
// defaults to `throw`, which fails the phase instead of calling back.
|
|
2627
3563
|
parameters: [
|
|
2628
3564
|
{ name: "error", type: "string | null" },
|
|
2629
|
-
{
|
|
2630
|
-
name: "data",
|
|
2631
|
-
type: `EndpointResponse<typeof ${endpointConst}>`
|
|
2632
|
-
}
|
|
3565
|
+
{ name: "data", type: dataAlias }
|
|
2633
3566
|
],
|
|
2634
3567
|
returnType: "void",
|
|
2635
3568
|
statements: "// TODO: apply `data` to the model"
|
|
2636
3569
|
});
|
|
2637
3570
|
return;
|
|
2638
3571
|
}
|
|
2639
|
-
ensureNamedImport(sf,
|
|
2640
|
-
{ name:
|
|
3572
|
+
ensureNamedImport(sf, "./services", [
|
|
3573
|
+
{ name: catalogName },
|
|
3574
|
+
{ name: mapperName },
|
|
3575
|
+
{ name: ctxAlias, isTypeOnly: true },
|
|
3576
|
+
...provisional ? [
|
|
3577
|
+
{ name: rawAlias, isTypeOnly: true },
|
|
3578
|
+
{ name: dataAlias, isTypeOnly: true }
|
|
3579
|
+
] : []
|
|
2641
3580
|
]);
|
|
2642
3581
|
ensureNamedImport(sf, resolveSdkModuleSpecifier(sf), [
|
|
2643
3582
|
{ name: "executeServiceRequest" },
|
|
@@ -2651,17 +3590,13 @@ function addServiceRequestToModel(codegenFs, options, projects) {
|
|
|
2651
3590
|
ensureDeclarationMerge(sf, className, "KosLoggerAware", typeParams);
|
|
2652
3591
|
addClassDecorator(getModelClass(sf), "kosLoggerAware", { argsText: "" });
|
|
2653
3592
|
addDecoratedMethod(getModelClass(sf), {
|
|
2654
|
-
name: options.methodName,
|
|
2655
|
-
decoratorName
|
|
2656
|
-
|
|
2657
|
-
|
|
2658
|
-
|
|
2659
|
-
|
|
2660
|
-
|
|
2661
|
-
type: `EndpointCtx<typeof ${endpointConst}>`,
|
|
2662
|
-
optional: true
|
|
2663
|
-
}
|
|
2664
|
-
],
|
|
3593
|
+
name: options.methodName,
|
|
3594
|
+
decoratorName,
|
|
3595
|
+
decoratorTypeArgs,
|
|
3596
|
+
decoratorArgsText: `${catalogName}.${endpointKey}, { transform: ${mapperName} }`,
|
|
3597
|
+
docs: provisional ? provisionalMethodDocs(options.servicePath, method) : void 0,
|
|
3598
|
+
// Optional: the framework appends the context, callers never pass it.
|
|
3599
|
+
parameters: [{ name: "$ctx", type: ctxAlias, optional: true }],
|
|
2665
3600
|
isAsync: true,
|
|
2666
3601
|
returnType: "Promise<void>",
|
|
2667
3602
|
statements: [
|
|
@@ -2671,169 +3606,43 @@ function addServiceRequestToModel(codegenFs, options, projects) {
|
|
|
2671
3606
|
].join("\n")
|
|
2672
3607
|
});
|
|
2673
3608
|
});
|
|
2674
|
-
|
|
2675
|
-
|
|
2676
|
-
|
|
2677
|
-
|
|
2678
|
-
|
|
2679
|
-
}
|
|
2680
|
-
function modelElementType(typeText) {
|
|
2681
|
-
let m;
|
|
2682
|
-
if (m = typeText.match(/^([A-Za-z_]\w*Model)\s*\[\]$/)) return m[1];
|
|
2683
|
-
if (m = typeText.match(/^Array<\s*([A-Za-z_]\w*Model)\s*>$/)) return m[1];
|
|
2684
|
-
if (m = typeText.match(/^(?:Map|Set)<[^>]*?\b([A-Za-z_]\w*Model)\b[^>]*>$/))
|
|
2685
|
-
return m[1];
|
|
2686
|
-
return null;
|
|
2687
|
-
}
|
|
2688
|
-
function validateModel(codegenFs, options, projects) {
|
|
2689
|
-
const { modelFilePath } = resolveModelFilePath(codegenFs, options, projects);
|
|
2690
|
-
const content = codegenFs.read(modelFilePath);
|
|
2691
|
-
if (content === null) {
|
|
2692
|
-
throw new Error(`Model file not found: ${modelFilePath}`);
|
|
2693
|
-
}
|
|
2694
|
-
const project = new tsMorph.Project({ useInMemoryFileSystem: true });
|
|
2695
|
-
const sf = project.createSourceFile(modelFilePath, content, {
|
|
2696
|
-
overwrite: true
|
|
2697
|
-
});
|
|
2698
|
-
const findings = [];
|
|
2699
|
-
const hasKosModel = sf.getClasses().some((c) => c.getDecorator("kosModel"));
|
|
2700
|
-
if (!hasKosModel) {
|
|
2701
|
-
findings.push({
|
|
2702
|
-
level: "error",
|
|
2703
|
-
rule: "missing-kosModel",
|
|
2704
|
-
message: "No @kosModel decorator found — this is not a KOS model."
|
|
2705
|
-
});
|
|
2706
|
-
}
|
|
2707
|
-
const imports = sf.getImportDeclarations();
|
|
2708
|
-
const mobxImport = imports.find(
|
|
2709
|
-
(d) => /^mobx(-react-lite)?$/.test(d.getModuleSpecifierValue())
|
|
2710
|
-
);
|
|
2711
|
-
if (mobxImport) {
|
|
2712
|
-
findings.push({
|
|
2713
|
-
level: "error",
|
|
2714
|
-
rule: "mobx-import",
|
|
2715
|
-
message: "Imports mobx directly. Reactivity is internal to KOS — remove the mobx import and use KOS reactivity / container models."
|
|
2716
|
-
});
|
|
2717
|
-
}
|
|
2718
|
-
const barrelServiceRequest = imports.find(
|
|
2719
|
-
(d) => d.getModuleSpecifierValue() === "@kosdev-code/kos-ui-sdk" && d.getNamedImports().some((n) => n.getName() === "kosServiceRequest")
|
|
2720
|
-
);
|
|
2721
|
-
if (barrelServiceRequest) {
|
|
2722
|
-
findings.push({
|
|
2723
|
-
level: "warning",
|
|
2724
|
-
rule: "untyped-service-request",
|
|
2725
|
-
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."
|
|
2726
|
-
});
|
|
2727
|
-
}
|
|
2728
|
-
for (const cls of sf.getClasses()) {
|
|
2729
|
-
for (const prop of cls.getProperties()) {
|
|
2730
|
-
const name = prop.getName();
|
|
2731
|
-
const typeText = (prop.getTypeNode()?.getText() ?? "").replace(/\s+/g, " ").trim();
|
|
2732
|
-
const initText = prop.getInitializer()?.getText() ?? "";
|
|
2733
|
-
const hasChild = !!prop.getDecorator("kosChild");
|
|
2734
|
-
const isModelContainer = /\bI?KosModelContainer\s*</.test(typeText) || /new\s+KosModelContainer\b/.test(initText);
|
|
2735
|
-
if (isModelContainer && !hasChild) {
|
|
2736
|
-
findings.push({
|
|
2737
|
-
level: "warning",
|
|
2738
|
-
rule: "container-missing-kosChild",
|
|
2739
|
-
message: `Property '${name}' is a model container but is not marked @kosChild — add @kosChild so its models join the model graph and lifecycle.`
|
|
2740
|
-
});
|
|
2741
|
-
continue;
|
|
2742
|
-
}
|
|
2743
|
-
const elem = modelElementType(typeText);
|
|
2744
|
-
if (elem && !isModelContainer) {
|
|
2745
|
-
findings.push({
|
|
2746
|
-
level: "warning",
|
|
2747
|
-
rule: "raw-model-collection",
|
|
2748
|
-
message: `Property '${name}' is a raw ${typeText} of models — prefer a KosModelContainer<${elem}> (indexing, sorting, lifecycle, delta handling) marked @kosChild.`
|
|
2749
|
-
});
|
|
2750
|
-
}
|
|
2751
|
-
}
|
|
2752
|
-
}
|
|
2753
|
-
const hasError = findings.some((f) => f.level === "error");
|
|
2754
|
-
return { modelFilePath, ok: !hasError, findings };
|
|
2755
|
-
}
|
|
2756
|
-
function firstDecoratorArg(decoratorText) {
|
|
2757
|
-
const open = decoratorText.indexOf("(");
|
|
2758
|
-
if (open === -1) return void 0;
|
|
2759
|
-
const inner = decoratorText.slice(open + 1, decoratorText.lastIndexOf(")")).replace(/\s+/g, " ").trim();
|
|
2760
|
-
return inner || void 0;
|
|
2761
|
-
}
|
|
2762
|
-
function collectDecorated(cls, decoratorName) {
|
|
2763
|
-
const members = [];
|
|
2764
|
-
const visit = (name, decoratorTextOf, typeText) => {
|
|
2765
|
-
const text = decoratorTextOf();
|
|
2766
|
-
if (text === void 0) return;
|
|
2767
|
-
members.push({
|
|
2768
|
-
name,
|
|
2769
|
-
arg: firstDecoratorArg(text),
|
|
2770
|
-
type: typeText || void 0
|
|
2771
|
-
});
|
|
2772
|
-
};
|
|
2773
|
-
for (const m of cls.getMethods()) {
|
|
2774
|
-
const dec = m.getDecorator(decoratorName);
|
|
2775
|
-
if (dec) {
|
|
2776
|
-
visit(m.getName(), () => dec.getText(), m.getReturnTypeNode()?.getText());
|
|
2777
|
-
}
|
|
2778
|
-
}
|
|
2779
|
-
for (const p of cls.getProperties()) {
|
|
2780
|
-
const dec = p.getDecorator(decoratorName);
|
|
2781
|
-
if (dec) {
|
|
2782
|
-
visit(p.getName(), () => dec.getText(), p.getTypeNode()?.getText());
|
|
2783
|
-
}
|
|
2784
|
-
}
|
|
2785
|
-
return members;
|
|
2786
|
-
}
|
|
2787
|
-
function describeModel(codegenFs, options, projects) {
|
|
2788
|
-
const { modelFilePath } = resolveModelFilePath(codegenFs, options, projects);
|
|
2789
|
-
const content = codegenFs.read(modelFilePath);
|
|
2790
|
-
if (content === null) {
|
|
2791
|
-
throw new Error(`Model file not found: ${modelFilePath}`);
|
|
2792
|
-
}
|
|
2793
|
-
const project = new tsMorph.Project({ useInMemoryFileSystem: true });
|
|
2794
|
-
const sf = project.createSourceFile(modelFilePath, content, {
|
|
2795
|
-
overwrite: true
|
|
2796
|
-
});
|
|
2797
|
-
let modelType;
|
|
2798
|
-
const modelTypeDecl = sf.getVariableDeclaration("MODEL_TYPE");
|
|
2799
|
-
if (modelTypeDecl) {
|
|
2800
|
-
const init = modelTypeDecl.getInitializer()?.getText();
|
|
2801
|
-
if (init) modelType = init.replace(/^["'`]|["'`]$/g, "");
|
|
2802
|
-
}
|
|
2803
|
-
const cls = sf.getClasses().find((c) => c.getDecorator("kosModel")) ?? sf.getClasses().find((c) => c.isExported()) ?? sf.getClasses()[0];
|
|
2804
|
-
if (!cls) {
|
|
2805
|
-
return {
|
|
3609
|
+
const mock = writeMock ? ensureMockRoute(codegenFs, {
|
|
3610
|
+
modelFilePath,
|
|
3611
|
+
modelBase,
|
|
3612
|
+
sdkModuleSpecifier: readSourceFile(
|
|
3613
|
+
codegenFs,
|
|
2806
3614
|
modelFilePath,
|
|
2807
|
-
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
|
|
2815
|
-
serviceRequests: [],
|
|
2816
|
-
effects: [],
|
|
2817
|
-
futures: []
|
|
2818
|
-
};
|
|
2819
|
-
}
|
|
2820
|
-
const classDecorators = cls.getDecorators().map((d) => d.getName());
|
|
2821
|
-
const kosModelArg = cls.getDecorator("kosModel") ? firstDecoratorArg(cls.getDecorator("kosModel").getText()) : void 0;
|
|
2822
|
-
const singleton = /\bsingleton\s*:\s*true\b/.test(kosModelArg ?? "");
|
|
3615
|
+
resolveSdkModuleSpecifier
|
|
3616
|
+
),
|
|
3617
|
+
rawAlias,
|
|
3618
|
+
servicePath: options.servicePath,
|
|
3619
|
+
method,
|
|
3620
|
+
endpointKey,
|
|
3621
|
+
sampleText: options.sample
|
|
3622
|
+
}) : void 0;
|
|
2823
3623
|
return {
|
|
2824
3624
|
modelFilePath,
|
|
2825
|
-
|
|
2826
|
-
|
|
2827
|
-
|
|
2828
|
-
|
|
2829
|
-
|
|
2830
|
-
|
|
2831
|
-
|
|
2832
|
-
|
|
2833
|
-
|
|
2834
|
-
|
|
2835
|
-
|
|
2836
|
-
|
|
3625
|
+
servicesFilePath,
|
|
3626
|
+
serviceModule: serviceImportFromModel,
|
|
3627
|
+
mode,
|
|
3628
|
+
endpointKey,
|
|
3629
|
+
endpointCreated,
|
|
3630
|
+
pathValidated,
|
|
3631
|
+
provisional,
|
|
3632
|
+
mocksFilePath: mock?.mocksFilePath,
|
|
3633
|
+
mockRegisterFunction: mock?.registerFunction,
|
|
3634
|
+
mockRoute: mock && `${method.toUpperCase()} ${mock.routePattern}`,
|
|
3635
|
+
mockSamplePlaceholder: mock?.samplePlaceholder,
|
|
3636
|
+
removalSteps: provisional ? removalSteps({
|
|
3637
|
+
modelProject: options.modelProject,
|
|
3638
|
+
servicePath: options.servicePath,
|
|
3639
|
+
method,
|
|
3640
|
+
catalogName,
|
|
3641
|
+
endpointKey,
|
|
3642
|
+
servicesFilePath,
|
|
3643
|
+
mocksFilePath: mock?.mocksFilePath,
|
|
3644
|
+
mapperName
|
|
3645
|
+
}) : void 0
|
|
2837
3646
|
};
|
|
2838
3647
|
}
|
|
2839
3648
|
const DEFAULT_SDK_PACKAGE = "@kosdev-code/kos-ui-sdk";
|
|
@@ -3019,6 +3828,82 @@ function lookupSdkType(codegenFs, options, projects) {
|
|
|
3019
3828
|
)}). Check the name, or it may be internal / not part of the public surface.`
|
|
3020
3829
|
};
|
|
3021
3830
|
}
|
|
3831
|
+
function modelElementType(typeText) {
|
|
3832
|
+
let m;
|
|
3833
|
+
if (m = typeText.match(/^([A-Za-z_]\w*Model)\s*\[\]$/)) return m[1];
|
|
3834
|
+
if (m = typeText.match(/^Array<\s*([A-Za-z_]\w*Model)\s*>$/)) return m[1];
|
|
3835
|
+
if (m = typeText.match(/^(?:Map|Set)<[^>]*?\b([A-Za-z_]\w*Model)\b[^>]*>$/))
|
|
3836
|
+
return m[1];
|
|
3837
|
+
return null;
|
|
3838
|
+
}
|
|
3839
|
+
function validateModel(codegenFs, options, projects) {
|
|
3840
|
+
const { modelFilePath } = resolveModelFilePath(codegenFs, options, projects);
|
|
3841
|
+
const content = codegenFs.read(modelFilePath);
|
|
3842
|
+
if (content === null) {
|
|
3843
|
+
throw new Error(`Model file not found: ${modelFilePath}`);
|
|
3844
|
+
}
|
|
3845
|
+
const project = new tsMorph.Project({ useInMemoryFileSystem: true });
|
|
3846
|
+
const sf = project.createSourceFile(modelFilePath, content, {
|
|
3847
|
+
overwrite: true
|
|
3848
|
+
});
|
|
3849
|
+
const findings = [];
|
|
3850
|
+
const hasKosModel = sf.getClasses().some((c) => c.getDecorator("kosModel"));
|
|
3851
|
+
if (!hasKosModel) {
|
|
3852
|
+
findings.push({
|
|
3853
|
+
level: "error",
|
|
3854
|
+
rule: "missing-kosModel",
|
|
3855
|
+
message: "No @kosModel decorator found — this is not a KOS model."
|
|
3856
|
+
});
|
|
3857
|
+
}
|
|
3858
|
+
const imports = sf.getImportDeclarations();
|
|
3859
|
+
const mobxImport = imports.find(
|
|
3860
|
+
(d) => /^mobx(-react-lite)?$/.test(d.getModuleSpecifierValue())
|
|
3861
|
+
);
|
|
3862
|
+
if (mobxImport) {
|
|
3863
|
+
findings.push({
|
|
3864
|
+
level: "error",
|
|
3865
|
+
rule: "mobx-import",
|
|
3866
|
+
message: "Imports mobx directly. Reactivity is internal to KOS — remove the mobx import and use KOS reactivity / container models."
|
|
3867
|
+
});
|
|
3868
|
+
}
|
|
3869
|
+
const barrelServiceRequest = imports.find(
|
|
3870
|
+
(d) => d.getModuleSpecifierValue() === "@kosdev-code/kos-ui-sdk" && d.getNamedImports().some((n) => n.getName() === "kosServiceRequest")
|
|
3871
|
+
);
|
|
3872
|
+
if (barrelServiceRequest) {
|
|
3873
|
+
findings.push({
|
|
3874
|
+
level: "warning",
|
|
3875
|
+
rule: "untyped-service-request",
|
|
3876
|
+
message: "kosServiceRequest imported from the SDK barrel. Use the typed decorator from the api:generate'd service module so paths are validated against the OpenAPI types."
|
|
3877
|
+
});
|
|
3878
|
+
}
|
|
3879
|
+
for (const cls of sf.getClasses()) {
|
|
3880
|
+
for (const prop of cls.getProperties()) {
|
|
3881
|
+
const name = prop.getName();
|
|
3882
|
+
const typeText = (prop.getTypeNode()?.getText() ?? "").replace(/\s+/g, " ").trim();
|
|
3883
|
+
const initText = prop.getInitializer()?.getText() ?? "";
|
|
3884
|
+
const hasChild = !!prop.getDecorator("kosChild");
|
|
3885
|
+
const isModelContainer = /\bI?KosModelContainer\s*</.test(typeText) || /new\s+KosModelContainer\b/.test(initText);
|
|
3886
|
+
if (isModelContainer && !hasChild) {
|
|
3887
|
+
findings.push({
|
|
3888
|
+
level: "warning",
|
|
3889
|
+
rule: "container-missing-kosChild",
|
|
3890
|
+
message: `Property '${name}' is a model container but is not marked @kosChild — add @kosChild so its models join the model graph and lifecycle.`
|
|
3891
|
+
});
|
|
3892
|
+
continue;
|
|
3893
|
+
}
|
|
3894
|
+
const elem = modelElementType(typeText);
|
|
3895
|
+
if (elem && !isModelContainer) {
|
|
3896
|
+
findings.push({
|
|
3897
|
+
level: "warning",
|
|
3898
|
+
rule: "raw-model-collection",
|
|
3899
|
+
message: `Property '${name}' is a raw ${typeText} of models — prefer a KosModelContainer<${elem}> (indexing, sorting, lifecycle, delta handling) marked @kosChild.`
|
|
3900
|
+
});
|
|
3901
|
+
}
|
|
3902
|
+
}
|
|
3903
|
+
}
|
|
3904
|
+
const hasError = findings.some((f) => f.level === "error");
|
|
3905
|
+
return { modelFilePath, ok: !hasError, findings };
|
|
3906
|
+
}
|
|
3022
3907
|
const PLUGIN_TYPES = {
|
|
3023
3908
|
CUI: "cui",
|
|
3024
3909
|
UTILITY: "utility",
|
|
@@ -3573,8 +4458,75 @@ function updatePluginConfiguration(codegenFs, projectConfig, options) {
|
|
|
3573
4458
|
);
|
|
3574
4459
|
}
|
|
3575
4460
|
}
|
|
4461
|
+
function parse(content) {
|
|
4462
|
+
const singleQuoted = /^\s*import\b[^"']*'/m.test(content);
|
|
4463
|
+
const project = new tsMorph.Project({
|
|
4464
|
+
useInMemoryFileSystem: true,
|
|
4465
|
+
manipulationSettings: {
|
|
4466
|
+
indentationText: tsMorph.IndentationText.TwoSpaces,
|
|
4467
|
+
quoteKind: singleQuoted ? tsMorph.QuoteKind.Single : tsMorph.QuoteKind.Double
|
|
4468
|
+
}
|
|
4469
|
+
});
|
|
4470
|
+
return project.createSourceFile("registration-chain.ts", content, {
|
|
4471
|
+
overwrite: true
|
|
4472
|
+
});
|
|
4473
|
+
}
|
|
4474
|
+
function chainCalls(sf, name) {
|
|
4475
|
+
return sf.getDescendantsOfKind(tsMorph.SyntaxKind.CallExpression).filter((call) => {
|
|
4476
|
+
const callee = call.getExpression();
|
|
4477
|
+
return callee.getKind() === tsMorph.SyntaxKind.PropertyAccessExpression && callee.asKindOrThrow(tsMorph.SyntaxKind.PropertyAccessExpression).getName() === name;
|
|
4478
|
+
});
|
|
4479
|
+
}
|
|
4480
|
+
function indentOfCall(sf, call) {
|
|
4481
|
+
const text = sf.getFullText();
|
|
4482
|
+
const nameNode = call.getExpression().asKindOrThrow(tsMorph.SyntaxKind.PropertyAccessExpression).getNameNode();
|
|
4483
|
+
const lineStart = text.lastIndexOf("\n", nameNode.getStart()) + 1;
|
|
4484
|
+
return text.slice(lineStart).match(/^[ \t]*/)?.[0] ?? "";
|
|
4485
|
+
}
|
|
4486
|
+
function countChainEntries(content) {
|
|
4487
|
+
return chainCalls(parse(content), "model").length;
|
|
4488
|
+
}
|
|
4489
|
+
function upsertChainEntry(content, bean, importSpec) {
|
|
4490
|
+
const sf = parse(content);
|
|
4491
|
+
const modelCalls = chainCalls(sf, "model");
|
|
4492
|
+
const registered = modelCalls.some((call) => {
|
|
4493
|
+
const [arg, ...rest] = call.getArguments();
|
|
4494
|
+
return rest.length === 0 && arg?.getText() === bean;
|
|
4495
|
+
});
|
|
4496
|
+
if (registered) return { changed: false, note: "already registered" };
|
|
4497
|
+
let anchor;
|
|
4498
|
+
let indent;
|
|
4499
|
+
if (modelCalls.length > 0) {
|
|
4500
|
+
anchor = modelCalls.reduce(
|
|
4501
|
+
(last, call) => call.getEnd() > last.getEnd() ? call : last
|
|
4502
|
+
);
|
|
4503
|
+
indent = indentOfCall(sf, anchor);
|
|
4504
|
+
} else {
|
|
4505
|
+
const starts = chainCalls(sf, "models").filter(
|
|
4506
|
+
(call) => call.getArguments().length === 0
|
|
4507
|
+
);
|
|
4508
|
+
anchor = starts[starts.length - 1];
|
|
4509
|
+
if (!anchor) {
|
|
4510
|
+
return {
|
|
4511
|
+
changed: false,
|
|
4512
|
+
error: "no .models() chain found in the registration file"
|
|
4513
|
+
};
|
|
4514
|
+
}
|
|
4515
|
+
const text = sf.getFullText();
|
|
4516
|
+
const lineStart = text.lastIndexOf("\n", anchor.getStart()) + 1;
|
|
4517
|
+
indent = (text.slice(lineStart).match(/^[ \t]*/)?.[0] ?? "") + " ";
|
|
4518
|
+
}
|
|
4519
|
+
anchor.replaceWithText(`${anchor.getText()}
|
|
4520
|
+
${indent}.model(${bean})`);
|
|
4521
|
+
const imported = sf.getImportDeclarations().some(
|
|
4522
|
+
(decl) => decl.getNamedImports().some((named) => named.getName() === bean)
|
|
4523
|
+
);
|
|
4524
|
+
if (!imported) ensureNamedImport(sf, importSpec, [{ name: bean }]);
|
|
4525
|
+
return { changed: true, content: sf.getFullText(), importAdded: !imported };
|
|
4526
|
+
}
|
|
3576
4527
|
exports.BasePluginHandler = BasePluginHandler;
|
|
3577
4528
|
exports.CONTRIBUTION_TYPE_MAP = CONTRIBUTION_TYPE_MAP;
|
|
4529
|
+
exports.DEFAULT_MODEL_LIBS_DIR = DEFAULT_MODEL_LIBS_DIR;
|
|
3578
4530
|
exports.DirectFileSystem = DirectFileSystem;
|
|
3579
4531
|
exports.KAB_OUTPUT_DIR = KAB_OUTPUT_DIR;
|
|
3580
4532
|
exports.KAB_OUTPUT_PATH = KAB_OUTPUT_PATH;
|
|
@@ -3596,6 +4548,7 @@ exports.addFutureToModel = addFutureToModel;
|
|
|
3596
4548
|
exports.addJavaArtifactToManifests = addJavaArtifactToManifests;
|
|
3597
4549
|
exports.addKosModelConfiguration = addKosModelConfiguration;
|
|
3598
4550
|
exports.addModelEffectToModel = addModelEffectToModel;
|
|
4551
|
+
exports.addParentAwareToModel = addParentAwareToModel;
|
|
3599
4552
|
exports.addPropertyToModel = addPropertyToModel;
|
|
3600
4553
|
exports.addServiceRequestToModel = addServiceRequestToModel;
|
|
3601
4554
|
exports.addTopicHandlerToModel = addTopicHandlerToModel;
|
|
@@ -3604,6 +4557,7 @@ exports.buildKabTargets = buildKabTargets;
|
|
|
3604
4557
|
exports.buildSbomTarget = buildSbomTarget;
|
|
3605
4558
|
exports.camelCase = camelCase;
|
|
3606
4559
|
exports.constantCase = constantCase;
|
|
4560
|
+
exports.countChainEntries = countChainEntries;
|
|
3607
4561
|
exports.dashCase = dashCase;
|
|
3608
4562
|
exports.describeModel = describeModel;
|
|
3609
4563
|
exports.discoverJavaArtifacts = discoverJavaArtifacts;
|
|
@@ -3622,8 +4576,10 @@ exports.generateFilesFromTemplates = generateFilesFromTemplates;
|
|
|
3622
4576
|
exports.generateHook = generateHook;
|
|
3623
4577
|
exports.generateInit = generateInit;
|
|
3624
4578
|
exports.generateModel = generateModel;
|
|
4579
|
+
exports.generateModelProject = generateModelProject;
|
|
3625
4580
|
exports.generatePolyglotWorkspace = generatePolyglotWorkspace;
|
|
3626
4581
|
exports.generateSplashProject = generateSplashProject;
|
|
4582
|
+
exports.generateViewModel = generateViewModel;
|
|
3627
4583
|
exports.getCodegenLogger = getCodegenLogger;
|
|
3628
4584
|
exports.getCurrentDirectoryName = getCurrentDirectoryName;
|
|
3629
4585
|
exports.getKosModelConfigProp = getKosModelConfigProp;
|
|
@@ -3641,10 +4597,12 @@ exports.readJson = readJson;
|
|
|
3641
4597
|
exports.readNxJson = readNxJson;
|
|
3642
4598
|
exports.resolveKabPath = resolveKabPath;
|
|
3643
4599
|
exports.resolveModelFilePath = resolveModelFilePath;
|
|
4600
|
+
exports.resolveModelProjectLayout = resolveModelProjectLayout;
|
|
3644
4601
|
exports.setCodegenLogger = setCodegenLogger;
|
|
3645
4602
|
exports.syncCiManifests = syncCiManifests;
|
|
3646
4603
|
exports.updateJson = updateJson;
|
|
3647
4604
|
exports.updateModelIndex = updateModelIndex;
|
|
4605
|
+
exports.upsertChainEntry = upsertChainEntry;
|
|
3648
4606
|
exports.validateModel = validateModel;
|
|
3649
4607
|
exports.writeJson = writeJson;
|
|
3650
4608
|
//# sourceMappingURL=index.js.map
|