@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.mjs
CHANGED
|
@@ -3,7 +3,7 @@ import * as path from "path";
|
|
|
3
3
|
import * as ejs from "ejs";
|
|
4
4
|
import fg from "fast-glob";
|
|
5
5
|
import prettier from "prettier";
|
|
6
|
-
import { Scope, Project, QuoteKind, IndentationText, VariableDeclarationKind, Node } from "ts-morph";
|
|
6
|
+
import { Scope, Project, QuoteKind, IndentationText, VariableDeclarationKind, Node, SyntaxKind } from "ts-morph";
|
|
7
7
|
import * as ts from "typescript";
|
|
8
8
|
class TrackingFileSystem {
|
|
9
9
|
inner;
|
|
@@ -474,6 +474,84 @@ function generateSplashProject(codegenFs, templateDir, options) {
|
|
|
474
474
|
normalized
|
|
475
475
|
);
|
|
476
476
|
}
|
|
477
|
+
const MODEL_PROJECT_SUFFIX = "-models";
|
|
478
|
+
const DEFAULT_MODEL_LIBS_DIR = "libs";
|
|
479
|
+
const JSONC_ESLINT_PARSER_VERSION = "^2.1.0";
|
|
480
|
+
function resolveModelProjectLayout(codegenFs, options) {
|
|
481
|
+
const base = dashCase(options.name);
|
|
482
|
+
const projectName = base.endsWith(MODEL_PROJECT_SUFFIX) ? base : `${base}${MODEL_PROJECT_SUFFIX}`;
|
|
483
|
+
const libsDir = (options.libsDir || DEFAULT_MODEL_LIBS_DIR).replace(
|
|
484
|
+
/\\/g,
|
|
485
|
+
"/"
|
|
486
|
+
);
|
|
487
|
+
const scope = codegenFs ? readNpmScope(codegenFs) : void 0;
|
|
488
|
+
return {
|
|
489
|
+
projectName,
|
|
490
|
+
projectRoot: `${libsDir}/${projectName}`.replace(/^\/+/, ""),
|
|
491
|
+
importPath: scope ? `@${scope}/${projectName}` : projectName
|
|
492
|
+
};
|
|
493
|
+
}
|
|
494
|
+
function readNpmScope(codegenFs) {
|
|
495
|
+
if (!codegenFs.exists("package.json")) {
|
|
496
|
+
return void 0;
|
|
497
|
+
}
|
|
498
|
+
const name = readJson(codegenFs, "package.json").name;
|
|
499
|
+
if (!name?.startsWith("@")) {
|
|
500
|
+
return void 0;
|
|
501
|
+
}
|
|
502
|
+
return name.split("/")[0].slice(1);
|
|
503
|
+
}
|
|
504
|
+
function offsetFromRoot(projectRoot) {
|
|
505
|
+
return projectRoot.split("/").filter(Boolean).map(() => "../").join("");
|
|
506
|
+
}
|
|
507
|
+
function generateModelProject(codegenFs, templateDir, options) {
|
|
508
|
+
const layout = resolveModelProjectLayout(codegenFs, options);
|
|
509
|
+
generateFilesFromTemplates(
|
|
510
|
+
codegenFs,
|
|
511
|
+
path.join(templateDir, "project"),
|
|
512
|
+
layout.projectRoot,
|
|
513
|
+
{
|
|
514
|
+
...layout,
|
|
515
|
+
projectNameCamelCase: camelCase(layout.projectName),
|
|
516
|
+
offsetFromRoot: offsetFromRoot(layout.projectRoot),
|
|
517
|
+
template: ""
|
|
518
|
+
}
|
|
519
|
+
);
|
|
520
|
+
registerTsconfigPath(codegenFs, layout);
|
|
521
|
+
ensureJsoncEslintParser(codegenFs);
|
|
522
|
+
return layout;
|
|
523
|
+
}
|
|
524
|
+
function registerTsconfigPath(codegenFs, layout) {
|
|
525
|
+
if (!codegenFs.exists("tsconfig.base.json")) {
|
|
526
|
+
return;
|
|
527
|
+
}
|
|
528
|
+
updateJson(codegenFs, "tsconfig.base.json", (json) => {
|
|
529
|
+
json.compilerOptions = json.compilerOptions ?? {};
|
|
530
|
+
json.compilerOptions.paths = json.compilerOptions.paths ?? {};
|
|
531
|
+
json.compilerOptions.paths[layout.importPath] = [
|
|
532
|
+
`${layout.projectRoot}/src/index.ts`
|
|
533
|
+
];
|
|
534
|
+
return json;
|
|
535
|
+
});
|
|
536
|
+
}
|
|
537
|
+
function ensureJsoncEslintParser(codegenFs) {
|
|
538
|
+
if (!codegenFs.exists("package.json")) {
|
|
539
|
+
return;
|
|
540
|
+
}
|
|
541
|
+
const pkg = readJson(codegenFs, "package.json");
|
|
542
|
+
const devDependencies = pkg.devDependencies ?? {};
|
|
543
|
+
if ("jsonc-eslint-parser" in devDependencies) {
|
|
544
|
+
return;
|
|
545
|
+
}
|
|
546
|
+
devDependencies["jsonc-eslint-parser"] = JSONC_ESLINT_PARSER_VERSION;
|
|
547
|
+
pkg.devDependencies = Object.fromEntries(
|
|
548
|
+
// Codepoint order, matching what Nx's own dependency helper produces.
|
|
549
|
+
Object.entries(devDependencies).sort(
|
|
550
|
+
([a], [b]) => a < b ? -1 : a > b ? 1 : 0
|
|
551
|
+
)
|
|
552
|
+
);
|
|
553
|
+
writeJson(codegenFs, "package.json", pkg);
|
|
554
|
+
}
|
|
477
555
|
function generateInit(codegenFs, options) {
|
|
478
556
|
const logger = getCodegenLogger();
|
|
479
557
|
const { appProject, modelProject, registrationProject } = options;
|
|
@@ -492,7 +570,6 @@ function generateInit(codegenFs, options) {
|
|
|
492
570
|
modelDirectory: "lib",
|
|
493
571
|
appDirectory: "app",
|
|
494
572
|
components: true,
|
|
495
|
-
dataServices: true,
|
|
496
573
|
internal: false,
|
|
497
574
|
singleton: false,
|
|
498
575
|
unitTests: true
|
|
@@ -926,6 +1003,58 @@ function normalizeOptions(codegenFs, options, projects) {
|
|
|
926
1003
|
template: ""
|
|
927
1004
|
};
|
|
928
1005
|
}
|
|
1006
|
+
const UPDATE_RELEASE_VERSION_SCRIPT = `import devkit from "@nx/devkit";
|
|
1007
|
+
import { resolve } from "path";
|
|
1008
|
+
import { readFileSync, writeFileSync } from "fs";
|
|
1009
|
+
import prettier from "prettier";
|
|
1010
|
+
|
|
1011
|
+
// KOS artifact versioning: stamps the project's .kos.json "version" field
|
|
1012
|
+
// (which kabtool bakes into the KAB). Never touches package.json.
|
|
1013
|
+
// Driven by tag-based releases:
|
|
1014
|
+
// nx run-many --target=version --args=--ver=$KOSBUILD_VERSION
|
|
1015
|
+
|
|
1016
|
+
const { readCachedProjectGraph } = devkit;
|
|
1017
|
+
const [, , name, versionArg] = process.argv;
|
|
1018
|
+
|
|
1019
|
+
// "{args.ver}" arrives literally when the target runs without --args=--ver=<v>;
|
|
1020
|
+
// treat that (or a missing arg) as "report current version, change nothing".
|
|
1021
|
+
const version =
|
|
1022
|
+
versionArg && !versionArg.startsWith("{args") ? versionArg : undefined;
|
|
1023
|
+
|
|
1024
|
+
if (!name) {
|
|
1025
|
+
console.error("usage: update-release-version.mjs <project> <version>");
|
|
1026
|
+
process.exit(1);
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
const graph = readCachedProjectGraph();
|
|
1030
|
+
const project = graph.nodes[name];
|
|
1031
|
+
if (!project) {
|
|
1032
|
+
console.error("Unknown project: " + name);
|
|
1033
|
+
process.exit(1);
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
const kosJsonPath = resolve(process.cwd(), project.data.root, ".kos.json");
|
|
1037
|
+
let kosJson;
|
|
1038
|
+
try {
|
|
1039
|
+
kosJson = JSON.parse(readFileSync(kosJsonPath, "utf8"));
|
|
1040
|
+
} catch {
|
|
1041
|
+
console.error("Missing or invalid .kos.json: " + kosJsonPath);
|
|
1042
|
+
process.exit(1);
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
if (!version) {
|
|
1046
|
+
console.log(name + ": " + kosJson.version + " (no --ver given; unchanged)");
|
|
1047
|
+
process.exit(0);
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
const prettierOptions = await prettier.resolveConfig(kosJsonPath);
|
|
1051
|
+
const output = await prettier.format(
|
|
1052
|
+
JSON.stringify({ ...kosJson, version }, null, 2),
|
|
1053
|
+
{ ...prettierOptions, parser: "json" }
|
|
1054
|
+
);
|
|
1055
|
+
writeFileSync(kosJsonPath, output);
|
|
1056
|
+
console.log(name + ": version -> " + version);
|
|
1057
|
+
`;
|
|
929
1058
|
function transformSourceFile(codegenFs, filePath, mutate) {
|
|
930
1059
|
const content = codegenFs.read(filePath);
|
|
931
1060
|
if (content === null) {
|
|
@@ -1031,12 +1160,137 @@ function ensureModuleConst(sourceFile, spec) {
|
|
|
1031
1160
|
});
|
|
1032
1161
|
return true;
|
|
1033
1162
|
}
|
|
1163
|
+
function afterImports(sourceFile) {
|
|
1164
|
+
const imports = sourceFile.getImportDeclarations();
|
|
1165
|
+
return imports.length > 0 ? imports[imports.length - 1].getChildIndex() + 1 : 0;
|
|
1166
|
+
}
|
|
1167
|
+
function unquote$1(name) {
|
|
1168
|
+
return name.replace(/^["']|["']$/g, "");
|
|
1169
|
+
}
|
|
1170
|
+
function getConstCatalogLiteral(sourceFile, name) {
|
|
1171
|
+
const declaration = sourceFile.getVariableDeclaration(name);
|
|
1172
|
+
if (!declaration) return null;
|
|
1173
|
+
let initializer = declaration.getInitializer();
|
|
1174
|
+
if (Node.isAsExpression(initializer)) {
|
|
1175
|
+
initializer = initializer.getExpression();
|
|
1176
|
+
}
|
|
1177
|
+
if (!initializer || !Node.isObjectLiteralExpression(initializer)) {
|
|
1178
|
+
throw new Error(
|
|
1179
|
+
`"${name}" exists in ${sourceFile.getBaseName()} but is not an object literal.`
|
|
1180
|
+
);
|
|
1181
|
+
}
|
|
1182
|
+
return initializer;
|
|
1183
|
+
}
|
|
1184
|
+
function ensureConstCatalogEntry(sourceFile, spec) {
|
|
1185
|
+
const matches = spec.equals ?? ((existing, candidate) => existing === candidate);
|
|
1186
|
+
let literal = getConstCatalogLiteral(sourceFile, spec.catalogName);
|
|
1187
|
+
if (!literal) {
|
|
1188
|
+
sourceFile.insertVariableStatement(afterImports(sourceFile), {
|
|
1189
|
+
isExported: true,
|
|
1190
|
+
declarationKind: VariableDeclarationKind.Const,
|
|
1191
|
+
declarations: [{ name: spec.catalogName, initializer: "{} as const" }]
|
|
1192
|
+
});
|
|
1193
|
+
literal = getConstCatalogLiteral(sourceFile, spec.catalogName);
|
|
1194
|
+
if (!literal) {
|
|
1195
|
+
throw new Error(`Failed to create catalog "${spec.catalogName}".`);
|
|
1196
|
+
}
|
|
1197
|
+
literal.addPropertyAssignment({
|
|
1198
|
+
name: spec.key,
|
|
1199
|
+
initializer: spec.initializer,
|
|
1200
|
+
leadingTrivia: spec.leadingTrivia
|
|
1201
|
+
});
|
|
1202
|
+
return { key: spec.key, created: true };
|
|
1203
|
+
}
|
|
1204
|
+
for (const property of literal.getProperties().filter(Node.isPropertyAssignment)) {
|
|
1205
|
+
const existingKey = unquote$1(property.getName());
|
|
1206
|
+
const existingInitializer = property.getInitializerOrThrow().getText();
|
|
1207
|
+
if (matches(existingInitializer, spec.initializer)) {
|
|
1208
|
+
return { key: existingKey, created: false };
|
|
1209
|
+
}
|
|
1210
|
+
if (existingKey === spec.key) {
|
|
1211
|
+
throw new Error(
|
|
1212
|
+
`"${spec.catalogName}.${spec.key}" already exists with a different value (${existingInitializer}). Use a different methodName, or reconcile the catalog by hand.`
|
|
1213
|
+
);
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1216
|
+
literal.addPropertyAssignment({
|
|
1217
|
+
name: spec.key,
|
|
1218
|
+
initializer: spec.initializer,
|
|
1219
|
+
leadingTrivia: spec.leadingTrivia
|
|
1220
|
+
});
|
|
1221
|
+
return { key: spec.key, created: true };
|
|
1222
|
+
}
|
|
1223
|
+
function ensureExportedTypeAlias(sourceFile, spec) {
|
|
1224
|
+
if (sourceFile.getTypeAlias(spec.name)) return false;
|
|
1225
|
+
sourceFile.addTypeAlias({
|
|
1226
|
+
name: spec.name,
|
|
1227
|
+
type: spec.type,
|
|
1228
|
+
isExported: true,
|
|
1229
|
+
docs: spec.docs ? [spec.docs] : void 0
|
|
1230
|
+
});
|
|
1231
|
+
return true;
|
|
1232
|
+
}
|
|
1233
|
+
function ensureExportedMapper(sourceFile, spec) {
|
|
1234
|
+
if (sourceFile.getVariableDeclaration(spec.name)) return false;
|
|
1235
|
+
sourceFile.addVariableStatement({
|
|
1236
|
+
isExported: true,
|
|
1237
|
+
declarationKind: VariableDeclarationKind.Const,
|
|
1238
|
+
declarations: [
|
|
1239
|
+
{
|
|
1240
|
+
name: spec.name,
|
|
1241
|
+
initializer: `(raw: ${spec.rawType}): ${spec.dataType} => raw`
|
|
1242
|
+
}
|
|
1243
|
+
]
|
|
1244
|
+
});
|
|
1245
|
+
return true;
|
|
1246
|
+
}
|
|
1247
|
+
function ensureBarrelExport(sourceFile, moduleSpecifier) {
|
|
1248
|
+
const present = sourceFile.getExportDeclarations().some((d) => d.getModuleSpecifierValue() === moduleSpecifier);
|
|
1249
|
+
if (present) return false;
|
|
1250
|
+
sourceFile.addExportDeclaration({ moduleSpecifier });
|
|
1251
|
+
return true;
|
|
1252
|
+
}
|
|
1253
|
+
function ensureNamedExports(sourceFile, moduleSpecifier, names, isTypeOnly = false) {
|
|
1254
|
+
const existing = sourceFile.getExportDeclarations().find(
|
|
1255
|
+
(d) => d.getModuleSpecifierValue() === moduleSpecifier && d.isTypeOnly() === isTypeOnly
|
|
1256
|
+
);
|
|
1257
|
+
if (!existing) {
|
|
1258
|
+
sourceFile.addExportDeclaration({
|
|
1259
|
+
moduleSpecifier,
|
|
1260
|
+
isTypeOnly,
|
|
1261
|
+
namedExports: names.map((name) => {
|
|
1262
|
+
return { name };
|
|
1263
|
+
})
|
|
1264
|
+
});
|
|
1265
|
+
return true;
|
|
1266
|
+
}
|
|
1267
|
+
const present = new Set(existing.getNamedExports().map((e) => e.getName()));
|
|
1268
|
+
const missing = names.filter((name) => !present.has(name));
|
|
1269
|
+
if (missing.length === 0) return false;
|
|
1270
|
+
existing.addNamedExports(
|
|
1271
|
+
missing.map((name) => {
|
|
1272
|
+
return { name };
|
|
1273
|
+
})
|
|
1274
|
+
);
|
|
1275
|
+
return true;
|
|
1276
|
+
}
|
|
1277
|
+
function ensureExportedInterface(sourceFile, name, properties = [], extendsTypes = []) {
|
|
1278
|
+
if (sourceFile.getInterface(name)) return false;
|
|
1279
|
+
sourceFile.addInterface({
|
|
1280
|
+
name,
|
|
1281
|
+
isExported: true,
|
|
1282
|
+
extends: extendsTypes,
|
|
1283
|
+
properties
|
|
1284
|
+
});
|
|
1285
|
+
return true;
|
|
1286
|
+
}
|
|
1034
1287
|
function addDecoratedMethod(cls, spec) {
|
|
1035
1288
|
if (cls.getMethod(spec.name)) return false;
|
|
1036
1289
|
cls.addMethod({
|
|
1037
1290
|
name: spec.name,
|
|
1038
1291
|
isAsync: spec.isAsync,
|
|
1039
1292
|
returnType: spec.returnType,
|
|
1293
|
+
docs: spec.docs ? [spec.docs] : void 0,
|
|
1040
1294
|
parameters: spec.parameters?.map((p) => {
|
|
1041
1295
|
return { name: p.name, type: p.type, hasQuestionToken: p.optional };
|
|
1042
1296
|
}),
|
|
@@ -1044,6 +1298,7 @@ function addDecoratedMethod(cls, spec) {
|
|
|
1044
1298
|
decorators: [
|
|
1045
1299
|
{
|
|
1046
1300
|
name: spec.decoratorName,
|
|
1301
|
+
typeArguments: spec.decoratorTypeArgs,
|
|
1047
1302
|
arguments: spec.decoratorArgsText ? [spec.decoratorArgsText] : []
|
|
1048
1303
|
}
|
|
1049
1304
|
]
|
|
@@ -1126,17 +1381,28 @@ function listServiceCatalog(codegenFs, query, projects) {
|
|
|
1126
1381
|
app,
|
|
1127
1382
|
version,
|
|
1128
1383
|
serviceModulePath: posix,
|
|
1129
|
-
operations: readOperations(codegenFs, openapiPath)
|
|
1384
|
+
operations: readOperations(codegenFs, openapiPath) ?? []
|
|
1130
1385
|
});
|
|
1131
1386
|
}
|
|
1132
1387
|
return entries.sort(
|
|
1133
1388
|
(a, b) => a.app.localeCompare(b.app) || a.version.localeCompare(b.version)
|
|
1134
1389
|
);
|
|
1135
1390
|
}
|
|
1391
|
+
function readServiceModuleOperations(codegenFs, serviceModuleFile) {
|
|
1392
|
+
const moduleFile = serviceModuleFile.endsWith(".ts") ? serviceModuleFile : `${serviceModuleFile}.ts`;
|
|
1393
|
+
const posix = moduleFile.split(path.sep).join("/");
|
|
1394
|
+
const openapiPath = posix.replace(/service\.ts$/, "openapi.d.ts");
|
|
1395
|
+
for (const candidate of [openapiPath, posix]) {
|
|
1396
|
+
if (!codegenFs.exists(candidate)) continue;
|
|
1397
|
+
const operations = readOperations(codegenFs, candidate);
|
|
1398
|
+
if (operations) return operations;
|
|
1399
|
+
}
|
|
1400
|
+
return null;
|
|
1401
|
+
}
|
|
1136
1402
|
function readOperations(codegenFs, openapiPath) {
|
|
1137
1403
|
return readSourceFile(codegenFs, openapiPath, (sf) => {
|
|
1138
1404
|
const paths = sf.getInterface("paths");
|
|
1139
|
-
if (!paths) return
|
|
1405
|
+
if (!paths) return null;
|
|
1140
1406
|
const operations = [];
|
|
1141
1407
|
for (const pathProp of paths.getProperties()) {
|
|
1142
1408
|
const servicePath = unquote(pathProp.getName());
|
|
@@ -1170,58 +1436,6 @@ function readSummary(methodProp) {
|
|
|
1170
1436
|
function unquote(name) {
|
|
1171
1437
|
return name.replace(/^["']|["']$/g, "");
|
|
1172
1438
|
}
|
|
1173
|
-
const UPDATE_RELEASE_VERSION_SCRIPT = `import devkit from "@nx/devkit";
|
|
1174
|
-
import { resolve } from "path";
|
|
1175
|
-
import { readFileSync, writeFileSync } from "fs";
|
|
1176
|
-
import prettier from "prettier";
|
|
1177
|
-
|
|
1178
|
-
// KOS artifact versioning: stamps the project's .kos.json "version" field
|
|
1179
|
-
// (which kabtool bakes into the KAB). Never touches package.json.
|
|
1180
|
-
// Driven by tag-based releases:
|
|
1181
|
-
// nx run-many --target=version --args=--ver=$KOSBUILD_VERSION
|
|
1182
|
-
|
|
1183
|
-
const { readCachedProjectGraph } = devkit;
|
|
1184
|
-
const [, , name, versionArg] = process.argv;
|
|
1185
|
-
|
|
1186
|
-
// "{args.ver}" arrives literally when the target runs without --args=--ver=<v>;
|
|
1187
|
-
// treat that (or a missing arg) as "report current version, change nothing".
|
|
1188
|
-
const version =
|
|
1189
|
-
versionArg && !versionArg.startsWith("{args") ? versionArg : undefined;
|
|
1190
|
-
|
|
1191
|
-
if (!name) {
|
|
1192
|
-
console.error("usage: update-release-version.mjs <project> <version>");
|
|
1193
|
-
process.exit(1);
|
|
1194
|
-
}
|
|
1195
|
-
|
|
1196
|
-
const graph = readCachedProjectGraph();
|
|
1197
|
-
const project = graph.nodes[name];
|
|
1198
|
-
if (!project) {
|
|
1199
|
-
console.error("Unknown project: " + name);
|
|
1200
|
-
process.exit(1);
|
|
1201
|
-
}
|
|
1202
|
-
|
|
1203
|
-
const kosJsonPath = resolve(process.cwd(), project.data.root, ".kos.json");
|
|
1204
|
-
let kosJson;
|
|
1205
|
-
try {
|
|
1206
|
-
kosJson = JSON.parse(readFileSync(kosJsonPath, "utf8"));
|
|
1207
|
-
} catch {
|
|
1208
|
-
console.error("Missing or invalid .kos.json: " + kosJsonPath);
|
|
1209
|
-
process.exit(1);
|
|
1210
|
-
}
|
|
1211
|
-
|
|
1212
|
-
if (!version) {
|
|
1213
|
-
console.log(name + ": " + kosJson.version + " (no --ver given; unchanged)");
|
|
1214
|
-
process.exit(0);
|
|
1215
|
-
}
|
|
1216
|
-
|
|
1217
|
-
const prettierOptions = await prettier.resolveConfig(kosJsonPath);
|
|
1218
|
-
const output = await prettier.format(
|
|
1219
|
-
JSON.stringify({ ...kosJson, version }, null, 2),
|
|
1220
|
-
{ ...prettierOptions, parser: "json" }
|
|
1221
|
-
);
|
|
1222
|
-
writeFileSync(kosJsonPath, output);
|
|
1223
|
-
console.log(name + ": version -> " + version);
|
|
1224
|
-
`;
|
|
1225
1439
|
function appendBarrelExport(codegenFs, indexPath, exportPath) {
|
|
1226
1440
|
const exportLine = `export * from '${exportPath}'`;
|
|
1227
1441
|
const content = codegenFs.read(indexPath) ?? "";
|
|
@@ -1268,10 +1482,57 @@ function updateModelIndex(codegenFs, indexPath, modelPath) {
|
|
|
1268
1482
|
const newContents = printer.printFile(updatedSourceFile);
|
|
1269
1483
|
codegenFs.write(indexPath, newContents);
|
|
1270
1484
|
}
|
|
1271
|
-
function
|
|
1272
|
-
|
|
1273
|
-
|
|
1485
|
+
function generateCompanionModel(codegenFs, templateDir, options, projects) {
|
|
1486
|
+
const logger = getCodegenLogger();
|
|
1487
|
+
const normalized = normalizeAllValues({
|
|
1488
|
+
companionModelName: options.companionModelName,
|
|
1489
|
+
modelName: options.modelName
|
|
1490
|
+
});
|
|
1491
|
+
const companionChildKosConfig = getKosProjectConfiguration(
|
|
1492
|
+
codegenFs,
|
|
1493
|
+
options.companionModelProject,
|
|
1494
|
+
projects
|
|
1495
|
+
);
|
|
1496
|
+
const parentProject = findProjectByName(
|
|
1497
|
+
codegenFs.root,
|
|
1498
|
+
options.modelProject,
|
|
1499
|
+
projects
|
|
1500
|
+
);
|
|
1501
|
+
const childProject = findProjectByName(
|
|
1502
|
+
codegenFs.root,
|
|
1503
|
+
options.companionModelProject,
|
|
1504
|
+
projects
|
|
1505
|
+
);
|
|
1506
|
+
const projectRoot = childProject?.sourceRoot;
|
|
1507
|
+
if (!projectRoot) {
|
|
1508
|
+
logger.warn(`Companion child project source root not found`);
|
|
1509
|
+
return;
|
|
1510
|
+
}
|
|
1511
|
+
let importPath = "";
|
|
1512
|
+
if (parentProject) {
|
|
1513
|
+
const pkgJsonPath = path.join(parentProject.root, "package.json");
|
|
1514
|
+
try {
|
|
1515
|
+
const pkgJson = readJson(codegenFs, pkgJsonPath);
|
|
1516
|
+
importPath = pkgJson.name || "";
|
|
1517
|
+
} catch {
|
|
1518
|
+
importPath = "";
|
|
1519
|
+
}
|
|
1274
1520
|
}
|
|
1521
|
+
const modelLocation = companionChildKosConfig?.generator?.defaults?.model?.folder || "";
|
|
1522
|
+
const filePath = path.join(
|
|
1523
|
+
projectRoot,
|
|
1524
|
+
modelLocation,
|
|
1525
|
+
normalized.companionModelNameDashCase
|
|
1526
|
+
);
|
|
1527
|
+
logger.info(`Generating companion model in ${filePath}`);
|
|
1528
|
+
generateFilesFromTemplates(codegenFs, templateDir, filePath, {
|
|
1529
|
+
...options,
|
|
1530
|
+
...normalized,
|
|
1531
|
+
importPath
|
|
1532
|
+
});
|
|
1533
|
+
}
|
|
1534
|
+
function generateContainerModel(codegenFs, templateDir, options, cwd, projects) {
|
|
1535
|
+
const logger = getCodegenLogger();
|
|
1275
1536
|
const currentProject = getProject(codegenFs, cwd);
|
|
1276
1537
|
const modelProjectName = options.modelProject || currentProject?.name;
|
|
1277
1538
|
if (!modelProjectName) {
|
|
@@ -1279,57 +1540,110 @@ function generateHook(codegenFs, templateDir, options, cwd, projects) {
|
|
|
1279
1540
|
"No model project found. Please specify a model project with --modelProject."
|
|
1280
1541
|
);
|
|
1281
1542
|
}
|
|
1282
|
-
const modelName = options.
|
|
1543
|
+
const modelName = options.modelName || getCurrentDirectoryName(cwd);
|
|
1283
1544
|
if (!modelName) {
|
|
1284
1545
|
throw new Error(
|
|
1285
1546
|
"No model name found. Please specify a model name with --name."
|
|
1286
1547
|
);
|
|
1287
1548
|
}
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
modelProjectName,
|
|
1291
|
-
modelName,
|
|
1292
|
-
projects
|
|
1293
|
-
);
|
|
1294
|
-
options.singleton = kosModelConfig ? !!kosModelConfig.singleton : false;
|
|
1295
|
-
options.name = modelName;
|
|
1296
|
-
options.modelProject = modelProjectName;
|
|
1549
|
+
options.modelName = modelName;
|
|
1550
|
+
options.name = `${modelName}-container`;
|
|
1297
1551
|
const normalized = normalizeOptions(codegenFs, options, projects);
|
|
1298
|
-
const
|
|
1552
|
+
const projectConfig = findProjectByName(
|
|
1299
1553
|
codegenFs.root,
|
|
1300
|
-
normalized.
|
|
1554
|
+
normalized.modelProject,
|
|
1301
1555
|
projects
|
|
1302
1556
|
);
|
|
1303
|
-
if (!
|
|
1304
|
-
throw new Error(`
|
|
1557
|
+
if (!projectConfig) {
|
|
1558
|
+
throw new Error(`Model project '${normalized.modelProject}' not found`);
|
|
1305
1559
|
}
|
|
1560
|
+
addKosModelConfiguration({
|
|
1561
|
+
codegenFs,
|
|
1562
|
+
modelName: normalized.nameDashCase,
|
|
1563
|
+
projectName: projectConfig.name,
|
|
1564
|
+
projectRoot: projectConfig.root,
|
|
1565
|
+
singleton: !!options.singleton,
|
|
1566
|
+
container: true,
|
|
1567
|
+
// The container's exported registration bean (`export const <ProperCase>`).
|
|
1568
|
+
factory: normalized.nameProperCase
|
|
1569
|
+
});
|
|
1306
1570
|
const kosConfig = getKosProjectConfiguration(
|
|
1307
1571
|
codegenFs,
|
|
1308
|
-
|
|
1572
|
+
projectConfig.name,
|
|
1309
1573
|
projects
|
|
1310
1574
|
);
|
|
1311
|
-
const
|
|
1312
|
-
|
|
1313
|
-
|
|
1575
|
+
const modelLocation = kosConfig?.generator?.defaults?.model?.folder || "";
|
|
1576
|
+
const internal = !!kosConfig?.generator?.internal;
|
|
1577
|
+
options.modelDirectory = options.modelDirectory || modelLocation;
|
|
1578
|
+
const projectRoot = projectConfig.sourceRoot;
|
|
1314
1579
|
if (projectRoot) {
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
templateDir,
|
|
1318
|
-
path.join(
|
|
1319
|
-
projectRoot,
|
|
1320
|
-
options.appDirectory,
|
|
1321
|
-
"hooks",
|
|
1322
|
-
normalized.nameDashCase
|
|
1323
|
-
),
|
|
1324
|
-
normalized
|
|
1580
|
+
logger.info(
|
|
1581
|
+
`Generating container model ${normalized.nameDashCase} in ${projectRoot}`
|
|
1325
1582
|
);
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1583
|
+
const modelNameDashCase = normalized.modelNameDashCase || normalized.nameDashCase;
|
|
1584
|
+
const modelFolder = path.join(
|
|
1585
|
+
projectRoot,
|
|
1586
|
+
options.modelDirectory || "",
|
|
1587
|
+
modelNameDashCase
|
|
1330
1588
|
);
|
|
1589
|
+
if (options.existingModel) {
|
|
1590
|
+
addContainerToExistingModelFolder(codegenFs, templateDir, modelFolder, {
|
|
1591
|
+
...normalized,
|
|
1592
|
+
internal
|
|
1593
|
+
});
|
|
1594
|
+
} else {
|
|
1595
|
+
generateFilesFromTemplates(
|
|
1596
|
+
codegenFs,
|
|
1597
|
+
path.join(templateDir, "model"),
|
|
1598
|
+
modelFolder,
|
|
1599
|
+
{ ...normalized, internal }
|
|
1600
|
+
);
|
|
1601
|
+
}
|
|
1602
|
+
const modelIndex = path.join(projectRoot, "index.ts");
|
|
1603
|
+
const modelPath = normalized.modelDirectory ? `${normalized.modelDirectory}/${modelNameDashCase}` : modelNameDashCase;
|
|
1604
|
+
updateModelIndex(codegenFs, modelIndex, modelPath);
|
|
1331
1605
|
}
|
|
1332
1606
|
}
|
|
1607
|
+
function addContainerToExistingModelFolder(codegenFs, templateDir, modelFolder, substitutions) {
|
|
1608
|
+
const containerFileName = `${substitutions.nameDashCase}-model.ts`;
|
|
1609
|
+
const containerTemplate = path.join(
|
|
1610
|
+
templateDir,
|
|
1611
|
+
"model",
|
|
1612
|
+
"__nameDashCase__-model.ts.template"
|
|
1613
|
+
);
|
|
1614
|
+
const rendered = ejs.render(
|
|
1615
|
+
fs.readFileSync(containerTemplate, "utf-8"),
|
|
1616
|
+
substitutions,
|
|
1617
|
+
{ filename: containerTemplate }
|
|
1618
|
+
);
|
|
1619
|
+
codegenFs.write(path.join(modelFolder, containerFileName), rendered);
|
|
1620
|
+
const typesPath = path.join(modelFolder, "types", "index.d.ts");
|
|
1621
|
+
if (codegenFs.read(typesPath) === null) {
|
|
1622
|
+
codegenFs.write(typesPath, "");
|
|
1623
|
+
}
|
|
1624
|
+
transformSourceFile(codegenFs, typesPath, (sourceFile) => {
|
|
1625
|
+
ensureExportedInterface(
|
|
1626
|
+
sourceFile,
|
|
1627
|
+
`${substitutions.nameProperCase}Options`
|
|
1628
|
+
);
|
|
1629
|
+
});
|
|
1630
|
+
const barrelPath = path.join(modelFolder, "index.ts");
|
|
1631
|
+
if (codegenFs.read(barrelPath) === null) {
|
|
1632
|
+
codegenFs.write(barrelPath, "");
|
|
1633
|
+
}
|
|
1634
|
+
transformSourceFile(codegenFs, barrelPath, (sourceFile) => {
|
|
1635
|
+
const moduleSpecifier = `./${containerFileName.replace(/\.ts$/, "")}`;
|
|
1636
|
+
ensureNamedExports(sourceFile, moduleSpecifier, [
|
|
1637
|
+
substitutions.nameProperCase
|
|
1638
|
+
]);
|
|
1639
|
+
ensureNamedExports(
|
|
1640
|
+
sourceFile,
|
|
1641
|
+
moduleSpecifier,
|
|
1642
|
+
[`${substitutions.nameProperCase}Model`],
|
|
1643
|
+
true
|
|
1644
|
+
);
|
|
1645
|
+
});
|
|
1646
|
+
}
|
|
1333
1647
|
function generateContext(codegenFs, templateDir, options, cwd, projects) {
|
|
1334
1648
|
if (!options.appProject) {
|
|
1335
1649
|
throw new Error("No app project specified");
|
|
@@ -1382,8 +1696,10 @@ function generateContext(codegenFs, templateDir, options, cwd, projects) {
|
|
|
1382
1696
|
);
|
|
1383
1697
|
}
|
|
1384
1698
|
}
|
|
1385
|
-
function
|
|
1386
|
-
|
|
1699
|
+
function generateHook(codegenFs, templateDir, options, cwd, projects) {
|
|
1700
|
+
if (!options.appProject) {
|
|
1701
|
+
throw new Error("No app project specified");
|
|
1702
|
+
}
|
|
1387
1703
|
const currentProject = getProject(codegenFs, cwd);
|
|
1388
1704
|
const modelProjectName = options.modelProject || currentProject?.name;
|
|
1389
1705
|
if (!modelProjectName) {
|
|
@@ -1391,120 +1707,56 @@ function generateContainerModel(codegenFs, templateDir, options, cwd, projects)
|
|
|
1391
1707
|
"No model project found. Please specify a model project with --modelProject."
|
|
1392
1708
|
);
|
|
1393
1709
|
}
|
|
1394
|
-
const modelName = options.
|
|
1710
|
+
const modelName = options.name || getCurrentDirectoryName(cwd);
|
|
1395
1711
|
if (!modelName) {
|
|
1396
1712
|
throw new Error(
|
|
1397
1713
|
"No model name found. Please specify a model name with --name."
|
|
1398
1714
|
);
|
|
1399
1715
|
}
|
|
1400
|
-
|
|
1401
|
-
|
|
1716
|
+
const kosModelConfig = getKosModelConfiguration(
|
|
1717
|
+
codegenFs,
|
|
1718
|
+
modelProjectName,
|
|
1719
|
+
modelName,
|
|
1720
|
+
projects
|
|
1721
|
+
);
|
|
1722
|
+
options.singleton = kosModelConfig ? !!kosModelConfig.singleton : false;
|
|
1723
|
+
options.name = modelName;
|
|
1724
|
+
options.modelProject = modelProjectName;
|
|
1402
1725
|
const normalized = normalizeOptions(codegenFs, options, projects);
|
|
1403
|
-
const
|
|
1726
|
+
const appProject = findProjectByName(
|
|
1404
1727
|
codegenFs.root,
|
|
1405
|
-
normalized.
|
|
1728
|
+
normalized.appProject,
|
|
1406
1729
|
projects
|
|
1407
1730
|
);
|
|
1408
|
-
if (!
|
|
1409
|
-
throw new Error(`
|
|
1731
|
+
if (!appProject) {
|
|
1732
|
+
throw new Error(`App project '${normalized.appProject}' not found`);
|
|
1410
1733
|
}
|
|
1411
|
-
addKosModelConfiguration({
|
|
1412
|
-
codegenFs,
|
|
1413
|
-
modelName: normalized.nameDashCase,
|
|
1414
|
-
projectName: projectConfig.name,
|
|
1415
|
-
projectRoot: projectConfig.root,
|
|
1416
|
-
singleton: !!options.singleton,
|
|
1417
|
-
container: true,
|
|
1418
|
-
// The container's exported registration bean (`export const <ProperCase>`).
|
|
1419
|
-
factory: normalized.nameProperCase
|
|
1420
|
-
});
|
|
1421
1734
|
const kosConfig = getKosProjectConfiguration(
|
|
1422
1735
|
codegenFs,
|
|
1423
|
-
|
|
1736
|
+
appProject.name,
|
|
1424
1737
|
projects
|
|
1425
1738
|
);
|
|
1426
|
-
const
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
const projectRoot = projectConfig.sourceRoot;
|
|
1739
|
+
const componentLocation = kosConfig?.generator?.defaults?.component?.folder || "";
|
|
1740
|
+
options.appDirectory = options.appDirectory || componentLocation;
|
|
1741
|
+
const projectRoot = appProject.sourceRoot;
|
|
1430
1742
|
if (projectRoot) {
|
|
1431
|
-
logger.info(
|
|
1432
|
-
`Generating container model ${normalized.nameDashCase} in ${projectRoot}`
|
|
1433
|
-
);
|
|
1434
|
-
const modelNameDashCase = normalized.modelNameDashCase || normalized.nameDashCase;
|
|
1435
1743
|
generateFilesFromTemplates(
|
|
1436
1744
|
codegenFs,
|
|
1437
|
-
|
|
1438
|
-
path.join(
|
|
1439
|
-
|
|
1745
|
+
templateDir,
|
|
1746
|
+
path.join(
|
|
1747
|
+
projectRoot,
|
|
1748
|
+
options.appDirectory,
|
|
1749
|
+
"hooks",
|
|
1750
|
+
normalized.nameDashCase
|
|
1751
|
+
),
|
|
1752
|
+
normalized
|
|
1753
|
+
);
|
|
1754
|
+
appendBarrelExport(
|
|
1755
|
+
codegenFs,
|
|
1756
|
+
path.join(projectRoot, options.appDirectory, "hooks", "index.ts"),
|
|
1757
|
+
`./${normalized.nameDashCase}`
|
|
1440
1758
|
);
|
|
1441
|
-
if (options.dataServices) {
|
|
1442
|
-
logger.info(`Generating data services for ${modelNameDashCase}`);
|
|
1443
|
-
generateFilesFromTemplates(
|
|
1444
|
-
codegenFs,
|
|
1445
|
-
path.join(templateDir, "services"),
|
|
1446
|
-
path.join(
|
|
1447
|
-
projectRoot,
|
|
1448
|
-
options.modelDirectory,
|
|
1449
|
-
modelNameDashCase,
|
|
1450
|
-
"services"
|
|
1451
|
-
),
|
|
1452
|
-
{ ...normalized, internal }
|
|
1453
|
-
);
|
|
1454
|
-
}
|
|
1455
|
-
const modelIndex = path.join(projectRoot, "index.ts");
|
|
1456
|
-
const modelPath = normalized.modelDirectory ? `${normalized.modelDirectory}/${modelNameDashCase}` : modelNameDashCase;
|
|
1457
|
-
updateModelIndex(codegenFs, modelIndex, modelPath);
|
|
1458
|
-
}
|
|
1459
|
-
}
|
|
1460
|
-
function generateCompanionModel(codegenFs, templateDir, options, projects) {
|
|
1461
|
-
const logger = getCodegenLogger();
|
|
1462
|
-
const normalized = normalizeAllValues({
|
|
1463
|
-
companionModelName: options.companionModelName,
|
|
1464
|
-
modelName: options.modelName
|
|
1465
|
-
});
|
|
1466
|
-
const companionChildKosConfig = getKosProjectConfiguration(
|
|
1467
|
-
codegenFs,
|
|
1468
|
-
options.companionModelProject,
|
|
1469
|
-
projects
|
|
1470
|
-
);
|
|
1471
|
-
const parentProject = findProjectByName(
|
|
1472
|
-
codegenFs.root,
|
|
1473
|
-
options.modelProject,
|
|
1474
|
-
projects
|
|
1475
|
-
);
|
|
1476
|
-
const childProject = findProjectByName(
|
|
1477
|
-
codegenFs.root,
|
|
1478
|
-
options.companionModelProject,
|
|
1479
|
-
projects
|
|
1480
|
-
);
|
|
1481
|
-
const projectRoot = childProject?.sourceRoot;
|
|
1482
|
-
if (!projectRoot) {
|
|
1483
|
-
logger.warn(`Companion child project source root not found`);
|
|
1484
|
-
return;
|
|
1485
|
-
}
|
|
1486
|
-
let importPath = "";
|
|
1487
|
-
if (parentProject) {
|
|
1488
|
-
const pkgJsonPath = path.join(parentProject.root, "package.json");
|
|
1489
|
-
try {
|
|
1490
|
-
const pkgJson = readJson(codegenFs, pkgJsonPath);
|
|
1491
|
-
importPath = pkgJson.name || "";
|
|
1492
|
-
} catch {
|
|
1493
|
-
importPath = "";
|
|
1494
|
-
}
|
|
1495
1759
|
}
|
|
1496
|
-
const modelLocation = companionChildKosConfig?.generator?.defaults?.model?.folder || "";
|
|
1497
|
-
const filePath = path.join(
|
|
1498
|
-
projectRoot,
|
|
1499
|
-
modelLocation,
|
|
1500
|
-
normalized.companionModelNameDashCase
|
|
1501
|
-
);
|
|
1502
|
-
logger.info(`Generating companion model in ${filePath}`);
|
|
1503
|
-
generateFilesFromTemplates(codegenFs, templateDir, filePath, {
|
|
1504
|
-
...options,
|
|
1505
|
-
...normalized,
|
|
1506
|
-
importPath
|
|
1507
|
-
});
|
|
1508
1760
|
}
|
|
1509
1761
|
function generateModel(params) {
|
|
1510
1762
|
const {
|
|
@@ -1552,19 +1804,6 @@ function generateModel(params) {
|
|
|
1552
1804
|
path.join(projectRoot, options.modelDirectory, normalized.nameDashCase),
|
|
1553
1805
|
{ ...normalized, internal }
|
|
1554
1806
|
);
|
|
1555
|
-
if (normalized.dataServices) {
|
|
1556
|
-
generateFilesFromTemplates(
|
|
1557
|
-
codegenFs,
|
|
1558
|
-
path.join(modelTemplateDir, "services"),
|
|
1559
|
-
path.join(
|
|
1560
|
-
projectRoot,
|
|
1561
|
-
options.modelDirectory,
|
|
1562
|
-
normalized.nameDashCase,
|
|
1563
|
-
"services"
|
|
1564
|
-
),
|
|
1565
|
-
{ ...normalized, internal }
|
|
1566
|
-
);
|
|
1567
|
-
}
|
|
1568
1807
|
const modelIndex = path.join(projectRoot, "index.ts");
|
|
1569
1808
|
const modelPath = options.modelDirectory ? `${options.modelDirectory}/${normalized.nameDashCase}` : normalized.nameDashCase;
|
|
1570
1809
|
updateModelIndex(codegenFs, modelIndex, modelPath);
|
|
@@ -1595,8 +1834,7 @@ function generateModel(params) {
|
|
|
1595
1834
|
...options,
|
|
1596
1835
|
name: `${normalized.name}-container`,
|
|
1597
1836
|
modelName: normalized.name,
|
|
1598
|
-
singleton: normalized.isContainerSingleton
|
|
1599
|
-
dataServices: normalized.dataServices
|
|
1837
|
+
singleton: normalized.isContainerSingleton
|
|
1600
1838
|
},
|
|
1601
1839
|
cwd,
|
|
1602
1840
|
projects
|
|
@@ -1617,6 +1855,55 @@ function generateModel(params) {
|
|
|
1617
1855
|
);
|
|
1618
1856
|
}
|
|
1619
1857
|
}
|
|
1858
|
+
const DECLARATION_MARKER = "@kosModel";
|
|
1859
|
+
function findModelDeclarationFile(codegenFs, searchRoot, typeIds) {
|
|
1860
|
+
const wanted = new Set(typeIds.filter(Boolean));
|
|
1861
|
+
if (wanted.size === 0) return null;
|
|
1862
|
+
const matches = [];
|
|
1863
|
+
for (const filePath of codegenFs.listFiles(searchRoot)) {
|
|
1864
|
+
if (!filePath.endsWith(".ts") || filePath.endsWith(".d.ts")) continue;
|
|
1865
|
+
const content = codegenFs.read(filePath);
|
|
1866
|
+
if (!content || !content.includes(DECLARATION_MARKER)) continue;
|
|
1867
|
+
const declared = readDeclaredModelType(codegenFs, filePath);
|
|
1868
|
+
if (declared && wanted.has(declared)) matches.push(filePath);
|
|
1869
|
+
}
|
|
1870
|
+
if (matches.length === 0) return null;
|
|
1871
|
+
if (matches.length > 1) {
|
|
1872
|
+
throw new Error(
|
|
1873
|
+
`Model type '${[...wanted].join("' / '")}' is declared in more than one file:
|
|
1874
|
+
` + matches.sort().map((c) => ` - ${c}`).join("\n") + `
|
|
1875
|
+
Pass modelPath to pick one.`
|
|
1876
|
+
);
|
|
1877
|
+
}
|
|
1878
|
+
return matches[0];
|
|
1879
|
+
}
|
|
1880
|
+
function readDeclaredModelType(codegenFs, filePath) {
|
|
1881
|
+
try {
|
|
1882
|
+
return readSourceFile(codegenFs, filePath, (sf) => {
|
|
1883
|
+
const cls = sf.getClasses().find((c) => c.getDecorator("kosModel"));
|
|
1884
|
+
const arg = cls?.getDecorator("kosModel")?.getArguments()[0];
|
|
1885
|
+
if (!arg) return void 0;
|
|
1886
|
+
const inner = arg.getKindName() === "ObjectLiteralExpression" ? modelTypeIdProperty(arg.getText()) : arg.getText();
|
|
1887
|
+
if (!inner) return void 0;
|
|
1888
|
+
return resolveToStringLiteral(inner.trim(), sf.getFullText());
|
|
1889
|
+
});
|
|
1890
|
+
} catch {
|
|
1891
|
+
return void 0;
|
|
1892
|
+
}
|
|
1893
|
+
}
|
|
1894
|
+
function modelTypeIdProperty(objectText) {
|
|
1895
|
+
const m = objectText.match(/\bmodelTypeId\s*:\s*([^,}]+)/);
|
|
1896
|
+
return m ? m[1] : void 0;
|
|
1897
|
+
}
|
|
1898
|
+
function resolveToStringLiteral(expression, fileText) {
|
|
1899
|
+
const literal = expression.match(/^["'`](.*)["'`]$/);
|
|
1900
|
+
if (literal) return literal[1];
|
|
1901
|
+
if (!/^[A-Za-z_$][\w$]*$/.test(expression)) return void 0;
|
|
1902
|
+
const declared = fileText.match(
|
|
1903
|
+
new RegExp(`\\b(?:const|let|var)\\s+${expression}\\s*(?::[^=]+)?=\\s*["'\`]([^"'\`]+)["'\`]`)
|
|
1904
|
+
);
|
|
1905
|
+
return declared ? declared[1] : void 0;
|
|
1906
|
+
}
|
|
1620
1907
|
function resolveModelFilePath(codegenFs, query, projects) {
|
|
1621
1908
|
const kosConfig = getKosProjectConfiguration(
|
|
1622
1909
|
codegenFs,
|
|
@@ -1651,14 +1938,23 @@ function resolveModelFilePath(codegenFs, query, projects) {
|
|
|
1651
1938
|
if (codegenFs.exists(modelFilePath)) {
|
|
1652
1939
|
return { modelFilePath, internal, sourceRoot };
|
|
1653
1940
|
}
|
|
1941
|
+
const searchRoot = path.join(sourceRoot, modelLocation);
|
|
1654
1942
|
const discovered = findModelFileByName(
|
|
1655
1943
|
codegenFs,
|
|
1656
|
-
|
|
1944
|
+
searchRoot,
|
|
1657
1945
|
modelNameDashCase
|
|
1658
1946
|
);
|
|
1659
1947
|
if (discovered) {
|
|
1660
1948
|
return { modelFilePath: discovered, internal, sourceRoot };
|
|
1661
1949
|
}
|
|
1950
|
+
const declaredType = kosConfig?.models?.[query.modelName]?.type;
|
|
1951
|
+
const byDeclaration = findModelDeclarationFile(codegenFs, searchRoot, [
|
|
1952
|
+
query.modelName,
|
|
1953
|
+
...declaredType ? [declaredType] : []
|
|
1954
|
+
]);
|
|
1955
|
+
if (byDeclaration) {
|
|
1956
|
+
return { modelFilePath: byDeclaration, internal, sourceRoot };
|
|
1957
|
+
}
|
|
1662
1958
|
return { modelFilePath, internal, sourceRoot };
|
|
1663
1959
|
}
|
|
1664
1960
|
function findModelFileByName(codegenFs, searchRoot, modelNameDashCase) {
|
|
@@ -1674,6 +1970,184 @@ Pass modelPath to pick one.`
|
|
|
1674
1970
|
}
|
|
1675
1971
|
return candidates[0];
|
|
1676
1972
|
}
|
|
1973
|
+
function resolveChildModelType(codegenFs, query, projects) {
|
|
1974
|
+
const childProject = query.childModelProject || query.modelProject;
|
|
1975
|
+
const childType = `${properCase(query.childModel)}Model`;
|
|
1976
|
+
if (childProject !== query.modelProject) {
|
|
1977
|
+
const project = findProjectByName(codegenFs.root, childProject, projects);
|
|
1978
|
+
const pkgJson = project ? readJson(
|
|
1979
|
+
codegenFs,
|
|
1980
|
+
path.join(project.root, "package.json")
|
|
1981
|
+
) : void 0;
|
|
1982
|
+
return { childType, childTypeModule: pkgJson?.name };
|
|
1983
|
+
}
|
|
1984
|
+
const { modelFilePath: childFilePath } = resolveModelFilePath(
|
|
1985
|
+
codegenFs,
|
|
1986
|
+
{ modelName: query.childModel, modelProject: childProject },
|
|
1987
|
+
projects
|
|
1988
|
+
);
|
|
1989
|
+
const relative = path.relative(path.dirname(query.modelFilePath), childFilePath).replace(/\.ts$/, "");
|
|
1990
|
+
return {
|
|
1991
|
+
childType,
|
|
1992
|
+
childTypeModule: relative.startsWith(".") ? relative : `./${relative}`
|
|
1993
|
+
};
|
|
1994
|
+
}
|
|
1995
|
+
function generateViewModel(codegenFs, templateDir, options, cwd, projects) {
|
|
1996
|
+
const logger = getCodegenLogger();
|
|
1997
|
+
if (!options.name) {
|
|
1998
|
+
throw new Error(
|
|
1999
|
+
"No ViewModel name found. Please specify a name with --name."
|
|
2000
|
+
);
|
|
2001
|
+
}
|
|
2002
|
+
const currentProject = getProject(codegenFs, cwd);
|
|
2003
|
+
const modelProjectName = options.modelProject || currentProject?.name;
|
|
2004
|
+
if (!modelProjectName) {
|
|
2005
|
+
throw new Error(
|
|
2006
|
+
"No model project found. Please specify a model project with --project."
|
|
2007
|
+
);
|
|
2008
|
+
}
|
|
2009
|
+
const normalized = normalizeOptions(
|
|
2010
|
+
codegenFs,
|
|
2011
|
+
{ ...options, modelProject: modelProjectName },
|
|
2012
|
+
projects
|
|
2013
|
+
);
|
|
2014
|
+
const projectConfig = findProjectByName(
|
|
2015
|
+
codegenFs.root,
|
|
2016
|
+
modelProjectName,
|
|
2017
|
+
projects
|
|
2018
|
+
);
|
|
2019
|
+
if (!projectConfig) {
|
|
2020
|
+
throw new Error(`Model project '${modelProjectName}' not found`);
|
|
2021
|
+
}
|
|
2022
|
+
const kosConfig = getKosProjectConfiguration(
|
|
2023
|
+
codegenFs,
|
|
2024
|
+
projectConfig.name,
|
|
2025
|
+
projects
|
|
2026
|
+
);
|
|
2027
|
+
const modelDirectory = options.modelDirectory || kosConfig?.generator?.defaults?.model?.folder || "";
|
|
2028
|
+
const internal = !!kosConfig?.generator?.internal;
|
|
2029
|
+
const projectRoot = projectConfig.sourceRoot || path.join(projectConfig.root, "src");
|
|
2030
|
+
const viewModelFolder = path.join(
|
|
2031
|
+
projectRoot,
|
|
2032
|
+
modelDirectory,
|
|
2033
|
+
normalized.nameDashCase
|
|
2034
|
+
);
|
|
2035
|
+
const viewModelFilePath = path.join(
|
|
2036
|
+
viewModelFolder,
|
|
2037
|
+
`${normalized.nameDashCase}-view-model.ts`
|
|
2038
|
+
);
|
|
2039
|
+
if (codegenFs.exists(viewModelFilePath)) {
|
|
2040
|
+
logger.info(`ViewModel already exists: ${viewModelFilePath}`);
|
|
2041
|
+
return { viewModelFilePath, created: false };
|
|
2042
|
+
}
|
|
2043
|
+
const resolved = (options.models || []).map(
|
|
2044
|
+
(source) => resolveSourceModel(
|
|
2045
|
+
codegenFs,
|
|
2046
|
+
source,
|
|
2047
|
+
viewModelFilePath,
|
|
2048
|
+
modelProjectName,
|
|
2049
|
+
projects
|
|
2050
|
+
)
|
|
2051
|
+
);
|
|
2052
|
+
logger.info(
|
|
2053
|
+
`Generating ViewModel ${normalized.nameDashCase} in ${projectRoot}`
|
|
2054
|
+
);
|
|
2055
|
+
const constructorParams = resolved.map(({ name, type }) => ({ name, type }));
|
|
2056
|
+
generateFilesFromTemplates(codegenFs, templateDir, viewModelFolder, {
|
|
2057
|
+
...normalized,
|
|
2058
|
+
internal,
|
|
2059
|
+
typeId: options.typeId || normalized.nameDashCase,
|
|
2060
|
+
devToolsEnabled: !!options.devToolsEnabled,
|
|
2061
|
+
constructorParams,
|
|
2062
|
+
constructorArgs: constructorParams.map((param) => param.name).join(", "),
|
|
2063
|
+
modelImports: groupImports(resolved)
|
|
2064
|
+
});
|
|
2065
|
+
updateModelIndex(
|
|
2066
|
+
codegenFs,
|
|
2067
|
+
path.join(projectRoot, "index.ts"),
|
|
2068
|
+
modelDirectory ? `${modelDirectory}/${normalized.nameDashCase}` : normalized.nameDashCase
|
|
2069
|
+
);
|
|
2070
|
+
return { viewModelFilePath, created: true };
|
|
2071
|
+
}
|
|
2072
|
+
function resolveSourceModel(codegenFs, source, viewModelFilePath, modelProject, projects) {
|
|
2073
|
+
const sourceProject = source.project || modelProject;
|
|
2074
|
+
if (sourceProject === modelProject) {
|
|
2075
|
+
const { modelFilePath } = resolveModelFilePath(
|
|
2076
|
+
codegenFs,
|
|
2077
|
+
{ modelName: source.model, modelProject: sourceProject },
|
|
2078
|
+
projects
|
|
2079
|
+
);
|
|
2080
|
+
if (!codegenFs.exists(modelFilePath)) {
|
|
2081
|
+
throw new Error(
|
|
2082
|
+
`Model '${source.model}' not found in project '${sourceProject}' (looked for ${modelFilePath})`
|
|
2083
|
+
);
|
|
2084
|
+
}
|
|
2085
|
+
} else if (!findProjectByName(codegenFs.root, sourceProject, projects)) {
|
|
2086
|
+
throw new Error(`Model project '${sourceProject}' not found`);
|
|
2087
|
+
}
|
|
2088
|
+
const { childType, childTypeModule } = resolveChildModelType(
|
|
2089
|
+
codegenFs,
|
|
2090
|
+
{
|
|
2091
|
+
modelFilePath: viewModelFilePath,
|
|
2092
|
+
modelProject,
|
|
2093
|
+
childModel: source.model,
|
|
2094
|
+
childModelProject: sourceProject
|
|
2095
|
+
},
|
|
2096
|
+
projects
|
|
2097
|
+
);
|
|
2098
|
+
return {
|
|
2099
|
+
name: camelCase(source.model),
|
|
2100
|
+
type: childType,
|
|
2101
|
+
module: childTypeModule
|
|
2102
|
+
};
|
|
2103
|
+
}
|
|
2104
|
+
function groupImports(resolved) {
|
|
2105
|
+
const byModule = /* @__PURE__ */ new Map();
|
|
2106
|
+
for (const { type, module } of resolved) {
|
|
2107
|
+
if (!module) continue;
|
|
2108
|
+
const types = byModule.get(module);
|
|
2109
|
+
if (!types) {
|
|
2110
|
+
byModule.set(module, [type]);
|
|
2111
|
+
} else if (!types.includes(type)) {
|
|
2112
|
+
types.push(type);
|
|
2113
|
+
}
|
|
2114
|
+
}
|
|
2115
|
+
return [...byModule].map(([module, types]) => ({ module, types }));
|
|
2116
|
+
}
|
|
2117
|
+
function modelBaseName(modelFilePath, modelName) {
|
|
2118
|
+
const base = path.basename(modelFilePath);
|
|
2119
|
+
const match = base.match(/^(.*)-model\.ts$/);
|
|
2120
|
+
return match ? match[1] : modelName;
|
|
2121
|
+
}
|
|
2122
|
+
function servicesFilePathFor(modelFilePath, modelBase) {
|
|
2123
|
+
return path.join(
|
|
2124
|
+
path.dirname(modelFilePath),
|
|
2125
|
+
"services",
|
|
2126
|
+
`${modelBase}-services.ts`
|
|
2127
|
+
);
|
|
2128
|
+
}
|
|
2129
|
+
function header$1(modelBase) {
|
|
2130
|
+
return `/**
|
|
2131
|
+
* Service layer for the ${modelBase} model: the endpoints it calls and the
|
|
2132
|
+
* types derived from them. Standalone service functions for callers outside a
|
|
2133
|
+
* model context belong here too.
|
|
2134
|
+
*/
|
|
2135
|
+
`;
|
|
2136
|
+
}
|
|
2137
|
+
function ensureServicesModule(codegenFs, modelFilePath, modelBase) {
|
|
2138
|
+
const servicesFilePath = servicesFilePathFor(modelFilePath, modelBase);
|
|
2139
|
+
if (!codegenFs.exists(servicesFilePath)) {
|
|
2140
|
+
codegenFs.write(servicesFilePath, header$1(modelBase));
|
|
2141
|
+
}
|
|
2142
|
+
const barrelPath = path.join(path.dirname(servicesFilePath), "index.ts");
|
|
2143
|
+
if (!codegenFs.exists(barrelPath)) {
|
|
2144
|
+
codegenFs.write(barrelPath, "");
|
|
2145
|
+
}
|
|
2146
|
+
transformSourceFile(codegenFs, barrelPath, (sf) => {
|
|
2147
|
+
ensureBarrelExport(sf, `./${modelBase}-services`);
|
|
2148
|
+
});
|
|
2149
|
+
return servicesFilePath;
|
|
2150
|
+
}
|
|
1677
2151
|
function normalizeAddFutureOptions(codegenFs, options, projects) {
|
|
1678
2152
|
const projectConfiguration = findProjectByName(
|
|
1679
2153
|
codegenFs.root,
|
|
@@ -1708,8 +2182,7 @@ function normalizeAddFutureOptions(codegenFs, options, projects) {
|
|
|
1708
2182
|
projects
|
|
1709
2183
|
);
|
|
1710
2184
|
const modelDirectory = path.dirname(modelFilePath);
|
|
1711
|
-
const
|
|
1712
|
-
const servicesFilePath = codegenFs.exists(servicesDirectory) ? path.join(servicesDirectory, `${nameDashCase}-services.ts`) : void 0;
|
|
2185
|
+
const servicesFilePath = servicesFilePathFor(modelFilePath, nameDashCase);
|
|
1713
2186
|
const registrationFilePath = path.join(
|
|
1714
2187
|
modelDirectory,
|
|
1715
2188
|
`${nameDashCase}-registration.ts`
|
|
@@ -1919,170 +2392,78 @@ class ModelFileTransformer {
|
|
|
1919
2392
|
});
|
|
1920
2393
|
}
|
|
1921
2394
|
}
|
|
2395
|
+
const SDK_MODULE = "@kosdev-code/kos-ui-sdk";
|
|
1922
2396
|
class ServiceFileTransformer {
|
|
1923
|
-
constructor(codegenFs, options) {
|
|
1924
|
-
this.codegenFs = codegenFs;
|
|
1925
|
-
this.options = options;
|
|
1926
|
-
}
|
|
1927
|
-
codegenFs;
|
|
1928
|
-
options;
|
|
1929
|
-
transform() {
|
|
1930
|
-
const {
|
|
1931
|
-
if (!
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
type DeepRequired,
|
|
1950
|
-
type ElementType,
|
|
1951
|
-
type ServiceResponse,
|
|
1952
|
-
type FutureResponse
|
|
1953
|
-
} from '@kosdev-code/kos-ui-sdk';
|
|
1954
|
-
|
|
1955
|
-
import API, { type KosApi, type ApiPath } from '../../../utils/service';
|
|
1956
|
-
|
|
1957
|
-
const log = KosLog.createLogger({name: "${nameDashCase}-service", group: "Services"});
|
|
1958
|
-
|
|
1959
|
-
const SERVICE_PATH: ApiPath = "ENTER_SERVICE_PATH"
|
|
1960
|
-
export type ${nameProperCase}ClientResponse = ClientResponse<
|
|
1961
|
-
KosApi,
|
|
1962
|
-
typeof SERVICE_PATH,
|
|
1963
|
-
'get'
|
|
1964
|
-
>;
|
|
1965
|
-
export type ${nameProperCase}Response = DeepRequired<${nameProperCase}ClientResponse>;
|
|
1966
|
-
|
|
1967
|
-
/**
|
|
1968
|
-
* @category Service
|
|
1969
|
-
* Retrieves the initial ${nameLowerCase} data.
|
|
1970
|
-
*/
|
|
1971
|
-
export const get${nameProperCase} = async (): Promise<ServiceResponse<${nameProperCase}Response>> => {
|
|
1972
|
-
log.debug('sending GET for ${nameLowerCase}');
|
|
1973
|
-
return await API.get(SERVICE_PATH);
|
|
1974
|
-
};
|
|
1975
|
-
|
|
1976
|
-
/**
|
|
1977
|
-
* @category Service - Future Operation
|
|
1978
|
-
* Placeholder for a long-running operation that returns a Future for progress tracking
|
|
1979
|
-
*
|
|
1980
|
-
* Replace this with your actual long-running service operation
|
|
1981
|
-
*/
|
|
1982
|
-
export const perform${nameProperCase}Operation = async (): Promise<FutureResponse> => {
|
|
1983
|
-
// TODO: Implement your long-running service operation here
|
|
1984
|
-
// This should return a Future that can be tracked for progress
|
|
1985
|
-
|
|
1986
|
-
log.debug('starting long-running ${nameLowerCase} operation');
|
|
1987
|
-
|
|
1988
|
-
// Example pattern:
|
|
1989
|
-
// return API.post(OPERATION_SERVICE_PATH, {
|
|
1990
|
-
// // operation parameters
|
|
1991
|
-
// });
|
|
1992
|
-
|
|
1993
|
-
// Placeholder - replace with actual implementation
|
|
1994
|
-
throw new Error('perform${nameProperCase}Operation not yet implemented');
|
|
1995
|
-
};
|
|
1996
|
-
|
|
1997
|
-
// Additional Future-aware service types (add as needed)
|
|
1998
|
-
export type ${nameProperCase}OperationProgress = {
|
|
1999
|
-
// Define your progress data structure here
|
|
2000
|
-
stage: string;
|
|
2001
|
-
percentComplete: number;
|
|
2002
|
-
currentItem?: string;
|
|
2003
|
-
totalItems?: number;
|
|
2004
|
-
};
|
|
2005
|
-
|
|
2006
|
-
export type ${nameProperCase}OperationResult = {
|
|
2007
|
-
// Define your operation result structure here
|
|
2008
|
-
success: boolean;
|
|
2009
|
-
message?: string;
|
|
2010
|
-
data?: any;
|
|
2011
|
-
};
|
|
2012
|
-
`;
|
|
2013
|
-
this.codegenFs.write(servicesFilePath, content);
|
|
2014
|
-
}
|
|
2015
|
-
addFutureImports(content) {
|
|
2016
|
-
if (content.includes("FutureResponse")) {
|
|
2017
|
-
return content;
|
|
2018
|
-
}
|
|
2019
|
-
const importRegex = /import {\s*([^}]*)\s*} from '@kosdev-code\/kos-ui-sdk';/;
|
|
2020
|
-
const importMatch = content.match(importRegex);
|
|
2021
|
-
if (importMatch) {
|
|
2022
|
-
const existingImports = importMatch[1];
|
|
2023
|
-
if (existingImports.includes("FutureResponse")) {
|
|
2024
|
-
return content;
|
|
2025
|
-
}
|
|
2026
|
-
const cleanedImports = existingImports.trim().replace(/,\s*$/, "");
|
|
2027
|
-
const newImports = cleanedImports ? `${cleanedImports},
|
|
2028
|
-
type FutureResponse` : `type FutureResponse`;
|
|
2029
|
-
const newImportStatement = `import {
|
|
2030
|
-
${newImports}
|
|
2031
|
-
} from '@kosdev-code/kos-ui-sdk';`;
|
|
2032
|
-
return content.replace(importMatch[0], newImportStatement);
|
|
2033
|
-
}
|
|
2034
|
-
return content;
|
|
2397
|
+
constructor(codegenFs, options) {
|
|
2398
|
+
this.codegenFs = codegenFs;
|
|
2399
|
+
this.options = options;
|
|
2400
|
+
}
|
|
2401
|
+
codegenFs;
|
|
2402
|
+
options;
|
|
2403
|
+
transform() {
|
|
2404
|
+
const { modelFilePath, nameDashCase } = this.options;
|
|
2405
|
+
if (!modelFilePath) return;
|
|
2406
|
+
const servicesFilePath = ensureServicesModule(
|
|
2407
|
+
this.codegenFs,
|
|
2408
|
+
modelFilePath,
|
|
2409
|
+
nameDashCase
|
|
2410
|
+
);
|
|
2411
|
+
transformSourceFile(this.codegenFs, servicesFilePath, (sf) => {
|
|
2412
|
+
ensureNamedImport(sf, SDK_MODULE, [
|
|
2413
|
+
{ name: "KosLog" },
|
|
2414
|
+
{ name: "FutureResponse", isTypeOnly: true }
|
|
2415
|
+
]);
|
|
2416
|
+
ensureModuleConst(sf, {
|
|
2417
|
+
name: "log",
|
|
2418
|
+
initializer: `KosLog.createLogger({ name: "${nameDashCase}-service", group: "Services" })`
|
|
2419
|
+
});
|
|
2420
|
+
this.addFutureOperation(sf);
|
|
2421
|
+
this.addProgressTypes(sf);
|
|
2422
|
+
});
|
|
2035
2423
|
}
|
|
2036
|
-
|
|
2424
|
+
addFutureOperation(sf) {
|
|
2037
2425
|
const { nameProperCase, nameLowerCase } = this.options;
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
// Placeholder - replace with actual implementation
|
|
2060
|
-
throw new Error('perform${nameProperCase}Operation not yet implemented');
|
|
2061
|
-
};`;
|
|
2062
|
-
return content + "\n" + futureService;
|
|
2426
|
+
const name = `perform${nameProperCase}Operation`;
|
|
2427
|
+
if (sf.getVariableDeclaration(name)) return;
|
|
2428
|
+
sf.addVariableStatement({
|
|
2429
|
+
isExported: true,
|
|
2430
|
+
declarationKind: VariableDeclarationKind.Const,
|
|
2431
|
+
declarations: [
|
|
2432
|
+
{
|
|
2433
|
+
name,
|
|
2434
|
+
initializer: `async (): Promise<FutureResponse> => {
|
|
2435
|
+
log.debug("starting long-running ${nameLowerCase} operation");
|
|
2436
|
+
throw new Error("${name} not yet implemented");
|
|
2437
|
+
}`
|
|
2438
|
+
}
|
|
2439
|
+
],
|
|
2440
|
+
docs: [
|
|
2441
|
+
{
|
|
2442
|
+
description: `@category Service - Future Operation
|
|
2443
|
+
A long-running operation returning a Future for progress tracking. Replace the body with the real service call.`
|
|
2444
|
+
}
|
|
2445
|
+
]
|
|
2446
|
+
});
|
|
2063
2447
|
}
|
|
2064
|
-
addProgressTypes(
|
|
2448
|
+
addProgressTypes(sf) {
|
|
2065
2449
|
const { nameProperCase } = this.options;
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
const progressTypes = `
|
|
2070
|
-
// Additional Future-aware service types (add as needed)
|
|
2071
|
-
export type ${nameProperCase}OperationProgress = {
|
|
2072
|
-
// Define your progress data structure here
|
|
2450
|
+
ensureExportedTypeAlias(sf, {
|
|
2451
|
+
name: `${nameProperCase}OperationProgress`,
|
|
2452
|
+
type: `{
|
|
2073
2453
|
stage: string;
|
|
2074
2454
|
percentComplete: number;
|
|
2075
2455
|
currentItem?: string;
|
|
2076
2456
|
totalItems?: number;
|
|
2077
|
-
}
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2457
|
+
}`
|
|
2458
|
+
});
|
|
2459
|
+
ensureExportedTypeAlias(sf, {
|
|
2460
|
+
name: `${nameProperCase}OperationResult`,
|
|
2461
|
+
type: `{
|
|
2081
2462
|
success: boolean;
|
|
2082
2463
|
message?: string;
|
|
2083
|
-
data?:
|
|
2084
|
-
}
|
|
2085
|
-
|
|
2464
|
+
data?: unknown;
|
|
2465
|
+
}`
|
|
2466
|
+
});
|
|
2086
2467
|
}
|
|
2087
2468
|
}
|
|
2088
2469
|
class RegistrationFileTransformer {
|
|
@@ -2241,11 +2622,25 @@ function buildDecoratorArgs(options) {
|
|
|
2241
2622
|
}
|
|
2242
2623
|
function addContainerSupportToModel(codegenFs, options, projects) {
|
|
2243
2624
|
const logger = getCodegenLogger();
|
|
2244
|
-
const childType = options.childType?.trim() || "IKosDataModel";
|
|
2245
2625
|
const { modelFilePath } = resolveModelFilePath(codegenFs, options, projects);
|
|
2246
2626
|
if (!codegenFs.exists(modelFilePath)) {
|
|
2247
2627
|
throw new Error(`Model file not found: ${modelFilePath}`);
|
|
2248
2628
|
}
|
|
2629
|
+
const resolvedChild = options.childModel ? resolveChildModelType(
|
|
2630
|
+
codegenFs,
|
|
2631
|
+
{
|
|
2632
|
+
modelFilePath,
|
|
2633
|
+
modelProject: options.modelProject,
|
|
2634
|
+
childModel: options.childModel,
|
|
2635
|
+
childModelProject: options.childModelProject
|
|
2636
|
+
},
|
|
2637
|
+
projects
|
|
2638
|
+
) : {
|
|
2639
|
+
childType: options.childType,
|
|
2640
|
+
childTypeModule: options.childTypeModule
|
|
2641
|
+
};
|
|
2642
|
+
const childType = resolvedChild.childType?.trim() || "IKosDataModel";
|
|
2643
|
+
const childTypeModule = resolvedChild.childTypeModule;
|
|
2249
2644
|
logger.info(
|
|
2250
2645
|
`Adding container support (<${childType}>) to model: ${options.modelName}`
|
|
2251
2646
|
);
|
|
@@ -2257,6 +2652,10 @@ function addContainerSupportToModel(codegenFs, options, projects) {
|
|
|
2257
2652
|
]);
|
|
2258
2653
|
if (childType === "IKosDataModel") {
|
|
2259
2654
|
ensureNamedImport(sf, sdk, [{ name: "IKosDataModel", isTypeOnly: true }]);
|
|
2655
|
+
} else if (childTypeModule) {
|
|
2656
|
+
ensureNamedImport(sf, childTypeModule, [
|
|
2657
|
+
{ name: childType, isTypeOnly: true }
|
|
2658
|
+
]);
|
|
2260
2659
|
}
|
|
2261
2660
|
const cls = getModelClass(sf);
|
|
2262
2661
|
const className = cls.getName();
|
|
@@ -2286,6 +2685,140 @@ function addContainerSupportToModel(codegenFs, options, projects) {
|
|
|
2286
2685
|
});
|
|
2287
2686
|
return { modelFilePath };
|
|
2288
2687
|
}
|
|
2688
|
+
function addParentAwareToModel(codegenFs, options, projects) {
|
|
2689
|
+
const logger = getCodegenLogger();
|
|
2690
|
+
const { modelFilePath } = resolveModelFilePath(codegenFs, options, projects);
|
|
2691
|
+
if (!codegenFs.exists(modelFilePath)) {
|
|
2692
|
+
throw new Error(`Model file not found: ${modelFilePath}`);
|
|
2693
|
+
}
|
|
2694
|
+
logger.info(`Adding parent awareness to model: ${options.modelName}`);
|
|
2695
|
+
let optionsTypeName;
|
|
2696
|
+
transformSourceFile(codegenFs, modelFilePath, (sf) => {
|
|
2697
|
+
const sdk = resolveSdkModuleSpecifier(sf);
|
|
2698
|
+
ensureNamedImport(sf, sdk, [{ name: "kosParentAware" }]);
|
|
2699
|
+
const cls = getModelClass(sf);
|
|
2700
|
+
addClassDecorator(cls, "kosParentAware", {
|
|
2701
|
+
argsText: options.parentId ? `{ parentId: ${JSON.stringify(options.parentId)} }` : ""
|
|
2702
|
+
});
|
|
2703
|
+
const ctor = cls.getConstructors()[0];
|
|
2704
|
+
optionsTypeName = ctor?.getParameters()[1]?.getTypeNode()?.getText()?.replace(/<.*$/, "");
|
|
2705
|
+
});
|
|
2706
|
+
const optionsFilePath = optionsTypeName ? extendOptionsInterface(codegenFs, modelFilePath, optionsTypeName, logger) : void 0;
|
|
2707
|
+
return { modelFilePath, optionsFilePath };
|
|
2708
|
+
}
|
|
2709
|
+
function extendOptionsInterface(codegenFs, modelFilePath, optionsTypeName, logger) {
|
|
2710
|
+
const typesPath = path.join(
|
|
2711
|
+
path.dirname(modelFilePath),
|
|
2712
|
+
"types",
|
|
2713
|
+
"index.d.ts"
|
|
2714
|
+
);
|
|
2715
|
+
const target = codegenFs.exists(typesPath) ? typesPath : modelFilePath;
|
|
2716
|
+
let extended = false;
|
|
2717
|
+
transformSourceFile(codegenFs, target, (sf) => {
|
|
2718
|
+
const iface = sf.getInterface(optionsTypeName);
|
|
2719
|
+
if (!iface) return;
|
|
2720
|
+
const already = iface.getExtends().some((clause) => clause.getText().includes("KosParentAware"));
|
|
2721
|
+
if (already) {
|
|
2722
|
+
extended = true;
|
|
2723
|
+
return;
|
|
2724
|
+
}
|
|
2725
|
+
ensureNamedImport(sf, "@kosdev-code/kos-ui-sdk", [
|
|
2726
|
+
{ name: "KosParentAware", isTypeOnly: true }
|
|
2727
|
+
]);
|
|
2728
|
+
iface.addExtends("KosParentAware");
|
|
2729
|
+
extended = true;
|
|
2730
|
+
});
|
|
2731
|
+
if (!extended) {
|
|
2732
|
+
logger.warn(
|
|
2733
|
+
`Could not find interface ${optionsTypeName}; add "extends KosParentAware" to it by hand.`
|
|
2734
|
+
);
|
|
2735
|
+
return void 0;
|
|
2736
|
+
}
|
|
2737
|
+
return target;
|
|
2738
|
+
}
|
|
2739
|
+
function firstDecoratorArg(decoratorText) {
|
|
2740
|
+
const open = decoratorText.indexOf("(");
|
|
2741
|
+
if (open === -1) return void 0;
|
|
2742
|
+
const inner = decoratorText.slice(open + 1, decoratorText.lastIndexOf(")")).replace(/\s+/g, " ").trim();
|
|
2743
|
+
return inner || void 0;
|
|
2744
|
+
}
|
|
2745
|
+
function collectDecorated(cls, decoratorName) {
|
|
2746
|
+
const members = [];
|
|
2747
|
+
const visit = (name, decoratorTextOf, typeText) => {
|
|
2748
|
+
const text = decoratorTextOf();
|
|
2749
|
+
if (text === void 0) return;
|
|
2750
|
+
members.push({
|
|
2751
|
+
name,
|
|
2752
|
+
arg: firstDecoratorArg(text),
|
|
2753
|
+
type: typeText || void 0
|
|
2754
|
+
});
|
|
2755
|
+
};
|
|
2756
|
+
for (const m of cls.getMethods()) {
|
|
2757
|
+
const dec = m.getDecorator(decoratorName);
|
|
2758
|
+
if (dec) {
|
|
2759
|
+
visit(m.getName(), () => dec.getText(), m.getReturnTypeNode()?.getText());
|
|
2760
|
+
}
|
|
2761
|
+
}
|
|
2762
|
+
for (const p of cls.getProperties()) {
|
|
2763
|
+
const dec = p.getDecorator(decoratorName);
|
|
2764
|
+
if (dec) {
|
|
2765
|
+
visit(p.getName(), () => dec.getText(), p.getTypeNode()?.getText());
|
|
2766
|
+
}
|
|
2767
|
+
}
|
|
2768
|
+
return members;
|
|
2769
|
+
}
|
|
2770
|
+
function describeModel(codegenFs, options, projects) {
|
|
2771
|
+
const { modelFilePath } = resolveModelFilePath(codegenFs, options, projects);
|
|
2772
|
+
const content = codegenFs.read(modelFilePath);
|
|
2773
|
+
if (content === null) {
|
|
2774
|
+
throw new Error(`Model file not found: ${modelFilePath}`);
|
|
2775
|
+
}
|
|
2776
|
+
const project = new Project({ useInMemoryFileSystem: true });
|
|
2777
|
+
const sf = project.createSourceFile(modelFilePath, content, {
|
|
2778
|
+
overwrite: true
|
|
2779
|
+
});
|
|
2780
|
+
let modelType;
|
|
2781
|
+
const modelTypeDecl = sf.getVariableDeclaration("MODEL_TYPE");
|
|
2782
|
+
if (modelTypeDecl) {
|
|
2783
|
+
const init = modelTypeDecl.getInitializer()?.getText();
|
|
2784
|
+
if (init) modelType = init.replace(/^["'`]|["'`]$/g, "");
|
|
2785
|
+
}
|
|
2786
|
+
const cls = sf.getClasses().find((c) => c.getDecorator("kosModel")) ?? sf.getClasses().find((c) => c.isExported()) ?? sf.getClasses()[0];
|
|
2787
|
+
if (!cls) {
|
|
2788
|
+
return {
|
|
2789
|
+
modelFilePath,
|
|
2790
|
+
modelType,
|
|
2791
|
+
classDecorators: [],
|
|
2792
|
+
singleton: false,
|
|
2793
|
+
isCompanion: false,
|
|
2794
|
+
children: [],
|
|
2795
|
+
dependencies: [],
|
|
2796
|
+
topicHandlers: [],
|
|
2797
|
+
configProperties: [],
|
|
2798
|
+
serviceRequests: [],
|
|
2799
|
+
effects: [],
|
|
2800
|
+
futures: []
|
|
2801
|
+
};
|
|
2802
|
+
}
|
|
2803
|
+
const classDecorators = cls.getDecorators().map((d) => d.getName());
|
|
2804
|
+
const kosModelArg = cls.getDecorator("kosModel") ? firstDecoratorArg(cls.getDecorator("kosModel").getText()) : void 0;
|
|
2805
|
+
const singleton = /\bsingleton\s*:\s*true\b/.test(kosModelArg ?? "");
|
|
2806
|
+
return {
|
|
2807
|
+
modelFilePath,
|
|
2808
|
+
modelType,
|
|
2809
|
+
className: cls.getName(),
|
|
2810
|
+
classDecorators,
|
|
2811
|
+
singleton,
|
|
2812
|
+
isCompanion: classDecorators.includes("kosCompanion"),
|
|
2813
|
+
children: collectDecorated(cls, "kosChild"),
|
|
2814
|
+
dependencies: collectDecorated(cls, "kosDependency"),
|
|
2815
|
+
topicHandlers: collectDecorated(cls, "kosTopicHandler"),
|
|
2816
|
+
configProperties: collectDecorated(cls, "kosConfigProperty"),
|
|
2817
|
+
serviceRequests: collectDecorated(cls, "kosServiceRequest"),
|
|
2818
|
+
effects: collectDecorated(cls, "kosModelEffect"),
|
|
2819
|
+
futures: collectDecorated(cls, "kosFuture")
|
|
2820
|
+
};
|
|
2821
|
+
}
|
|
2289
2822
|
function addModelEffectToModel(codegenFs, options, projects) {
|
|
2290
2823
|
const logger = getCodegenLogger();
|
|
2291
2824
|
const { modelFilePath } = resolveModelFilePath(codegenFs, options, projects);
|
|
@@ -2361,7 +2894,18 @@ function addChildToModel(codegenFs, options, projects) {
|
|
|
2361
2894
|
logger.info(
|
|
2362
2895
|
`Adding @kosChild "${options.propertyName}" (${shape}) to ${options.modelName}`
|
|
2363
2896
|
);
|
|
2364
|
-
const
|
|
2897
|
+
const resolvedChild = options.childModel ? resolveChildModelType(
|
|
2898
|
+
codegenFs,
|
|
2899
|
+
{
|
|
2900
|
+
modelFilePath,
|
|
2901
|
+
modelProject: options.modelProject,
|
|
2902
|
+
childModel: options.childModel,
|
|
2903
|
+
childModelProject: options.childModelProject
|
|
2904
|
+
},
|
|
2905
|
+
projects
|
|
2906
|
+
) : { childType: options.childType, childTypeModule: options.childPackage };
|
|
2907
|
+
const childType = resolvedChild.childType?.trim();
|
|
2908
|
+
const childTypeModule = resolvedChild.childTypeModule;
|
|
2365
2909
|
transformSourceFile(codegenFs, modelFilePath, (sf) => {
|
|
2366
2910
|
const sdk = resolveSdkModuleSpecifier(sf);
|
|
2367
2911
|
const sdkImports = [
|
|
@@ -2369,8 +2913,8 @@ function addChildToModel(codegenFs, options, projects) {
|
|
|
2369
2913
|
];
|
|
2370
2914
|
if (shape === "container") sdkImports.push({ name: "KosModelContainer" });
|
|
2371
2915
|
ensureNamedImport(sf, sdk, sdkImports);
|
|
2372
|
-
if (
|
|
2373
|
-
ensureNamedImport(sf,
|
|
2916
|
+
if (childTypeModule && childType && /^[A-Za-z_$][\w$]*$/.test(childType) && !FRAMEWORK_TYPES.has(childType)) {
|
|
2917
|
+
ensureNamedImport(sf, childTypeModule, [
|
|
2374
2918
|
{ name: childType, isTypeOnly: true }
|
|
2375
2919
|
]);
|
|
2376
2920
|
}
|
|
@@ -2516,6 +3060,154 @@ function addComputedToModel(codegenFs, options, projects) {
|
|
|
2516
3060
|
});
|
|
2517
3061
|
return { modelFilePath };
|
|
2518
3062
|
}
|
|
3063
|
+
function mocksFilePathFor(modelFilePath, modelBase) {
|
|
3064
|
+
return path.join(
|
|
3065
|
+
path.dirname(modelFilePath),
|
|
3066
|
+
"mocks",
|
|
3067
|
+
`${modelBase}-mocks.ts`
|
|
3068
|
+
);
|
|
3069
|
+
}
|
|
3070
|
+
function mockRegisterFunctionName(modelBase) {
|
|
3071
|
+
return `register${pascalCase(modelBase)}Mocks`;
|
|
3072
|
+
}
|
|
3073
|
+
function toMockRoutePattern(servicePath) {
|
|
3074
|
+
return servicePath.replace(/\{([^}]+)\}/g, ":$1");
|
|
3075
|
+
}
|
|
3076
|
+
function mockRouteCall(method, pattern, body) {
|
|
3077
|
+
const shorthand = {
|
|
3078
|
+
get: "get",
|
|
3079
|
+
post: "post",
|
|
3080
|
+
put: "put",
|
|
3081
|
+
delete: "del"
|
|
3082
|
+
};
|
|
3083
|
+
const fn = shorthand[method];
|
|
3084
|
+
return fn ? `KosMock.${fn}(${JSON.stringify(pattern)}, ${body});` : `KosMock.route(${JSON.stringify(method.toUpperCase())}, ${JSON.stringify(
|
|
3085
|
+
pattern
|
|
3086
|
+
)}, ${body});`;
|
|
3087
|
+
}
|
|
3088
|
+
function header(modelBase, registerFn) {
|
|
3089
|
+
return `/**
|
|
3090
|
+
* KosMock routes for the ${modelBase} model's PROVISIONAL endpoints — the ones
|
|
3091
|
+
* this project's generated OpenAPI types do not declare.
|
|
3092
|
+
*
|
|
3093
|
+
* NOTHING IMPORTS THIS FILE. Call \`${registerFn}()\` from the app's dev entry,
|
|
3094
|
+
* gated so it cannot run in a production build (\`import.meta.env.DEV\`, a
|
|
3095
|
+
* dev-only entry module, or behind your own switch). Registering a route enables
|
|
3096
|
+
* KosMock, and a mock that shipped would shadow the real endpoint once it exists.
|
|
3097
|
+
*
|
|
3098
|
+
* Unmatched requests still pass through to the real device (KosMock's hybrid
|
|
3099
|
+
* default), so these routes shadow only the paths named below. Every mocked
|
|
3100
|
+
* response carries the \`kos-mocked: true\` wire header, so mock-fed data is
|
|
3101
|
+
* identifiable in devtools.
|
|
3102
|
+
*
|
|
3103
|
+
* DELETE THIS FILE once every endpoint below is in the generated types.
|
|
3104
|
+
*/
|
|
3105
|
+
`;
|
|
3106
|
+
}
|
|
3107
|
+
function ensureMockRoute(codegenFs, options) {
|
|
3108
|
+
const mocksFilePath = mocksFilePathFor(
|
|
3109
|
+
options.modelFilePath,
|
|
3110
|
+
options.modelBase
|
|
3111
|
+
);
|
|
3112
|
+
const registerFunction = mockRegisterFunctionName(options.modelBase);
|
|
3113
|
+
const routePattern = toMockRoutePattern(options.servicePath);
|
|
3114
|
+
const sampleName = `SAMPLE_${constantCase(dashCase(options.endpointKey))}`;
|
|
3115
|
+
const samplePlaceholder = !options.sampleText;
|
|
3116
|
+
if (!codegenFs.exists(mocksFilePath)) {
|
|
3117
|
+
codegenFs.write(mocksFilePath, header(options.modelBase, registerFunction));
|
|
3118
|
+
}
|
|
3119
|
+
const sdkSpecifier = options.sdkModuleSpecifier.startsWith(".") ? `../${options.sdkModuleSpecifier}` : options.sdkModuleSpecifier;
|
|
3120
|
+
let routeCreated = false;
|
|
3121
|
+
transformSourceFile(codegenFs, mocksFilePath, (sf) => {
|
|
3122
|
+
ensureNamedImport(sf, sdkSpecifier, [{ name: "KosMock" }]);
|
|
3123
|
+
ensureNamedImport(sf, "../services", [
|
|
3124
|
+
{ name: options.rawAlias, isTypeOnly: true }
|
|
3125
|
+
]);
|
|
3126
|
+
ensureSample(sf, {
|
|
3127
|
+
name: sampleName,
|
|
3128
|
+
type: options.rawAlias,
|
|
3129
|
+
servicePath: options.servicePath,
|
|
3130
|
+
method: options.method,
|
|
3131
|
+
sampleText: options.sampleText
|
|
3132
|
+
});
|
|
3133
|
+
const statement = mockRouteCall(
|
|
3134
|
+
options.method,
|
|
3135
|
+
routePattern,
|
|
3136
|
+
`{ data: ${sampleName} }`
|
|
3137
|
+
);
|
|
3138
|
+
routeCreated = ensureRegisteredRoute(sf, {
|
|
3139
|
+
registerFunction,
|
|
3140
|
+
modelBase: options.modelBase,
|
|
3141
|
+
statement,
|
|
3142
|
+
// Matching on the pattern literal alone would collide across methods.
|
|
3143
|
+
marker: `${options.method.toUpperCase()} ${routePattern}`,
|
|
3144
|
+
matches: (existing) => existing.includes(JSON.stringify(routePattern)) && existing.includes(shorthandOrMethod(options.method))
|
|
3145
|
+
});
|
|
3146
|
+
});
|
|
3147
|
+
return {
|
|
3148
|
+
mocksFilePath,
|
|
3149
|
+
registerFunction,
|
|
3150
|
+
routePattern,
|
|
3151
|
+
sampleName,
|
|
3152
|
+
routeCreated,
|
|
3153
|
+
samplePlaceholder
|
|
3154
|
+
};
|
|
3155
|
+
}
|
|
3156
|
+
function shorthandOrMethod(method) {
|
|
3157
|
+
const shorthand = {
|
|
3158
|
+
get: "KosMock.get(",
|
|
3159
|
+
post: "KosMock.post(",
|
|
3160
|
+
put: "KosMock.put(",
|
|
3161
|
+
delete: "KosMock.del("
|
|
3162
|
+
};
|
|
3163
|
+
return shorthand[method] ?? `"${method.toUpperCase()}"`;
|
|
3164
|
+
}
|
|
3165
|
+
function ensureSample(sf, spec) {
|
|
3166
|
+
if (sf.getVariableDeclaration(spec.name)) return;
|
|
3167
|
+
const lines = [
|
|
3168
|
+
`Sample payload for \`${spec.method.toUpperCase()} ${spec.servicePath}\` — the`,
|
|
3169
|
+
"UNWRAPPED `data` payload, i.e. exactly what the transform receives."
|
|
3170
|
+
];
|
|
3171
|
+
if (!spec.sampleText) {
|
|
3172
|
+
lines.push(
|
|
3173
|
+
"",
|
|
3174
|
+
"TODO: replace the placeholder with something the backend would actually",
|
|
3175
|
+
"serve. Until then the route answers with nothing and the transform is",
|
|
3176
|
+
"never exercised."
|
|
3177
|
+
);
|
|
3178
|
+
}
|
|
3179
|
+
const docs = lines.join("\n");
|
|
3180
|
+
sf.addVariableStatement({
|
|
3181
|
+
isExported: true,
|
|
3182
|
+
declarationKind: VariableDeclarationKind.Const,
|
|
3183
|
+
docs: [docs],
|
|
3184
|
+
declarations: [
|
|
3185
|
+
{
|
|
3186
|
+
name: spec.name,
|
|
3187
|
+
type: spec.type,
|
|
3188
|
+
initializer: spec.sampleText ?? `null as unknown as ${spec.type}`
|
|
3189
|
+
}
|
|
3190
|
+
]
|
|
3191
|
+
});
|
|
3192
|
+
}
|
|
3193
|
+
function ensureRegisteredRoute(sf, spec) {
|
|
3194
|
+
let fn = sf.getFunction(spec.registerFunction);
|
|
3195
|
+
if (!fn) {
|
|
3196
|
+
fn = sf.addFunction({
|
|
3197
|
+
name: spec.registerFunction,
|
|
3198
|
+
isExported: true,
|
|
3199
|
+
returnType: "void",
|
|
3200
|
+
docs: [
|
|
3201
|
+
`Arm the ${spec.modelBase} model's provisional endpoints. Call this from a dev-only entry point — never from code that ships.`
|
|
3202
|
+
]
|
|
3203
|
+
});
|
|
3204
|
+
}
|
|
3205
|
+
const already = fn.getStatements().some((statement) => spec.matches(statement.getText()));
|
|
3206
|
+
if (already) return false;
|
|
3207
|
+
fn.addStatements(`// ${spec.marker}
|
|
3208
|
+
${spec.statement}`);
|
|
3209
|
+
return true;
|
|
3210
|
+
}
|
|
2519
3211
|
const SERVICE_MODULE_RE = /\/utils\/services\/.*\/service\.ts$/;
|
|
2520
3212
|
function findServiceModules(codegenFs, sourceRoot) {
|
|
2521
3213
|
return codegenFs.listFiles(sourceRoot).filter((f) => SERVICE_MODULE_RE.test(f.split(path.sep).join("/")));
|
|
@@ -2525,6 +3217,143 @@ function toImportSpecifier(fromFile, toFileNoExt) {
|
|
|
2525
3217
|
if (!rel.startsWith(".")) rel = `./${rel}`;
|
|
2526
3218
|
return rel;
|
|
2527
3219
|
}
|
|
3220
|
+
function fromImportSpecifier(fromFile, specifier) {
|
|
3221
|
+
return path.join(path.dirname(fromFile), specifier);
|
|
3222
|
+
}
|
|
3223
|
+
function assertServiceModuleAcceptsTransform(codegenFs, serviceModuleFile, modelProject) {
|
|
3224
|
+
const file = `${serviceModuleFile}.ts`;
|
|
3225
|
+
if (!codegenFs.exists(file)) return;
|
|
3226
|
+
const typeParams = readSourceFile(codegenFs, file, (sf) => {
|
|
3227
|
+
const alias = sf.getTypeAlias("EndpointCtx");
|
|
3228
|
+
return alias ? alias.getTypeParameters().length : null;
|
|
3229
|
+
});
|
|
3230
|
+
if (typeParams === null || typeParams >= 2) return;
|
|
3231
|
+
throw new Error(
|
|
3232
|
+
`${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.`
|
|
3233
|
+
);
|
|
3234
|
+
}
|
|
3235
|
+
function assertServiceModuleAcceptsProvisional(codegenFs, serviceModuleFile, modelProject) {
|
|
3236
|
+
const file = `${serviceModuleFile}.ts`;
|
|
3237
|
+
if (!codegenFs.exists(file)) return;
|
|
3238
|
+
const declared = readSourceFile(
|
|
3239
|
+
codegenFs,
|
|
3240
|
+
file,
|
|
3241
|
+
(sf) => Boolean(sf.getFunction("provisionalServiceRequest"))
|
|
3242
|
+
);
|
|
3243
|
+
if (declared) return;
|
|
3244
|
+
throw new Error(
|
|
3245
|
+
`${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.`
|
|
3246
|
+
);
|
|
3247
|
+
}
|
|
3248
|
+
function sameEndpoint(existing, candidate) {
|
|
3249
|
+
const args = (text) => {
|
|
3250
|
+
const match = text.match(
|
|
3251
|
+
/^endpoint\s*\(\s*(['"])(.*?)\1(?:\s+as\s+ApiPath)?\s*,\s*(['"])(.*?)\3\s*,?\s*\)$/
|
|
3252
|
+
);
|
|
3253
|
+
return match ? [match[2], match[4].toLowerCase()] : null;
|
|
3254
|
+
};
|
|
3255
|
+
const a = args(existing.trim());
|
|
3256
|
+
const b = args(candidate.trim());
|
|
3257
|
+
return !!a && !!b && a[0] === b[0] && a[1] === b[1];
|
|
3258
|
+
}
|
|
3259
|
+
function editDistance(a, b) {
|
|
3260
|
+
let previous = Array.from({ length: b.length + 1 }, (_, i) => i);
|
|
3261
|
+
for (let i = 1; i <= a.length; i++) {
|
|
3262
|
+
const current = [i];
|
|
3263
|
+
for (let j = 1; j <= b.length; j++) {
|
|
3264
|
+
current[j] = Math.min(
|
|
3265
|
+
previous[j] + 1,
|
|
3266
|
+
current[j - 1] + 1,
|
|
3267
|
+
previous[j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1)
|
|
3268
|
+
);
|
|
3269
|
+
}
|
|
3270
|
+
previous = current;
|
|
3271
|
+
}
|
|
3272
|
+
return previous[b.length];
|
|
3273
|
+
}
|
|
3274
|
+
const NEAREST_SHOWN = 5;
|
|
3275
|
+
function provisionalEntryComment(servicePath, method, catalogName) {
|
|
3276
|
+
return `/**
|
|
3277
|
+
* PROVISIONAL — \`${method.toUpperCase()} ${servicePath}\` is NOT in this project's
|
|
3278
|
+
* generated OpenAPI types. \`as ApiPath\` is standing in for a \`paths\` entry that
|
|
3279
|
+
* does not exist, so nothing about this endpoint is checked against a spec and
|
|
3280
|
+
* its response cannot be derived from one. Do not read this as a real endpoint.
|
|
3281
|
+
*
|
|
3282
|
+
* WHEN THE ENDPOINT LANDS:
|
|
3283
|
+
* 1. regenerate this project's API types (\`kosui api:generate\`)
|
|
3284
|
+
* 2. delete \` as ApiPath\` from the entry below
|
|
3285
|
+
* 3. point the endpoint's \`…Raw\` alias at
|
|
3286
|
+
* \`EndpointResponse<typeof ${catalogName}.…>\`
|
|
3287
|
+
* 4. swap \`provisionalServiceRequest\` for \`serviceRequest\` in the model
|
|
3288
|
+
* 5. delete the model's \`mocks/\` module
|
|
3289
|
+
* 6. KEEP the transform — it is real code and survives all of the above
|
|
3290
|
+
*/
|
|
3291
|
+
`;
|
|
3292
|
+
}
|
|
3293
|
+
function provisionalMethodDocs(servicePath, method) {
|
|
3294
|
+
return [
|
|
3295
|
+
`PROVISIONAL — \`${method.toUpperCase()} ${servicePath}\` is not in the`,
|
|
3296
|
+
"generated OpenAPI types, so this request is served by a KosMock route, not",
|
|
3297
|
+
"by the device. The feature it backs is NOT complete: report the missing",
|
|
3298
|
+
"endpoint rather than treating this as finished.",
|
|
3299
|
+
"",
|
|
3300
|
+
"See the endpoint's entry in `./services` for what to undo when it lands."
|
|
3301
|
+
].join("\n");
|
|
3302
|
+
}
|
|
3303
|
+
function rawTypeDocs(servicePath, method, rawType) {
|
|
3304
|
+
const shape = rawType ? [
|
|
3305
|
+
"Stated by hand and unverified: there is no spec entry to check it",
|
|
3306
|
+
"against."
|
|
3307
|
+
] : [
|
|
3308
|
+
"TODO: state the shape the backend is expected to serve. `unknown`",
|
|
3309
|
+
"compiles, but leaves the response boundary undescribed and the",
|
|
3310
|
+
"transform with nothing to narrow."
|
|
3311
|
+
];
|
|
3312
|
+
return [
|
|
3313
|
+
`Raw wire shape of \`${method.toUpperCase()} ${servicePath}\` — the UNWRAPPED`,
|
|
3314
|
+
"`data` payload, since the client strips the `{status, data}` envelope",
|
|
3315
|
+
"before the transform runs.",
|
|
3316
|
+
"",
|
|
3317
|
+
...shape,
|
|
3318
|
+
"",
|
|
3319
|
+
"Replace with `EndpointResponse<…>` once the endpoint is in the generated",
|
|
3320
|
+
"types."
|
|
3321
|
+
].join("\n");
|
|
3322
|
+
}
|
|
3323
|
+
function removalSteps(spec) {
|
|
3324
|
+
return [
|
|
3325
|
+
`Regenerate the API types for ${spec.modelProject} (generate_api_types), and confirm ${spec.method.toUpperCase()} ${spec.servicePath} is now in them.`,
|
|
3326
|
+
`In ${spec.servicesFilePath}: delete \` as ApiPath\` from \`${spec.catalogName}.${spec.endpointKey}\`.`,
|
|
3327
|
+
`In ${spec.servicesFilePath}: point the endpoint's \`…Raw\` alias at \`EndpointResponse<typeof ${spec.catalogName}.${spec.endpointKey}>\` and drop the hand-written shape.`,
|
|
3328
|
+
`In the model: swap \`provisionalServiceRequest\` for \`serviceRequest\` and drop its explicit type arguments — the response type comes from the spec again.`,
|
|
3329
|
+
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.`,
|
|
3330
|
+
`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.`
|
|
3331
|
+
];
|
|
3332
|
+
}
|
|
3333
|
+
function untypedEndpointError(spec) {
|
|
3334
|
+
const methodsForPath = spec.operations.filter((op) => op.path === spec.servicePath).map((op) => op.method);
|
|
3335
|
+
const pathTypedForOtherMethods = methodsForPath.length > 0;
|
|
3336
|
+
const nearest = [...new Set(spec.operations.map((op) => op.path))].sort(
|
|
3337
|
+
(a, b) => editDistance(a, spec.servicePath) - editDistance(b, spec.servicePath)
|
|
3338
|
+
).slice(0, NEAREST_SHOWN);
|
|
3339
|
+
const waysForward = [
|
|
3340
|
+
`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.`,
|
|
3341
|
+
`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.`,
|
|
3342
|
+
`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.`
|
|
3343
|
+
];
|
|
3344
|
+
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.`;
|
|
3345
|
+
const error = new Error(message);
|
|
3346
|
+
error.details = {
|
|
3347
|
+
servicePath: spec.servicePath,
|
|
3348
|
+
method: spec.method,
|
|
3349
|
+
pathTypedForOtherMethods,
|
|
3350
|
+
methodsForPath,
|
|
3351
|
+
otherEndpointsInThisApi: nearest,
|
|
3352
|
+
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.",
|
|
3353
|
+
waysForward
|
|
3354
|
+
};
|
|
3355
|
+
return error;
|
|
3356
|
+
}
|
|
2528
3357
|
function addServiceRequestToModel(codegenFs, options, projects) {
|
|
2529
3358
|
const logger = getCodegenLogger();
|
|
2530
3359
|
const { modelFilePath, sourceRoot } = resolveModelFilePath(
|
|
@@ -2535,14 +3364,15 @@ function addServiceRequestToModel(codegenFs, options, projects) {
|
|
|
2535
3364
|
if (!codegenFs.exists(modelFilePath)) {
|
|
2536
3365
|
throw new Error(`Model file not found: ${modelFilePath}`);
|
|
2537
3366
|
}
|
|
2538
|
-
let
|
|
2539
|
-
if (
|
|
2540
|
-
|
|
3367
|
+
let serviceModuleFile;
|
|
3368
|
+
if (options.serviceModulePath) {
|
|
3369
|
+
serviceModuleFile = options.serviceModulePath.replace(/\.ts$/, "");
|
|
3370
|
+
} else if (options.serviceModule) {
|
|
3371
|
+
serviceModuleFile = fromImportSpecifier(
|
|
2541
3372
|
modelFilePath,
|
|
2542
|
-
options.
|
|
3373
|
+
options.serviceModule
|
|
2543
3374
|
);
|
|
2544
|
-
}
|
|
2545
|
-
if (!serviceImport) {
|
|
3375
|
+
} else {
|
|
2546
3376
|
if (!sourceRoot) {
|
|
2547
3377
|
throw new Error(
|
|
2548
3378
|
"Cannot auto-resolve the service module without a project source root. Pass serviceModule."
|
|
@@ -2554,68 +3384,177 @@ function addServiceRequestToModel(codegenFs, options, projects) {
|
|
|
2554
3384
|
"No generated service module found. Run `kosui api:generate` for this project first."
|
|
2555
3385
|
);
|
|
2556
3386
|
}
|
|
2557
|
-
const specifiers = modules.map(
|
|
2558
|
-
(m) => toImportSpecifier(modelFilePath, m.replace(/\.ts$/, ""))
|
|
2559
|
-
);
|
|
2560
3387
|
if (modules.length > 1) {
|
|
3388
|
+
const specifiers = modules.map(
|
|
3389
|
+
(m) => toImportSpecifier(modelFilePath, m.replace(/\.ts$/, ""))
|
|
3390
|
+
);
|
|
2561
3391
|
throw new Error(
|
|
2562
3392
|
`Multiple service modules found — pass serviceModule (one of): ${specifiers.join(
|
|
2563
3393
|
", "
|
|
2564
3394
|
)}`
|
|
2565
3395
|
);
|
|
2566
3396
|
}
|
|
2567
|
-
|
|
3397
|
+
serviceModuleFile = modules[0].replace(/\.ts$/, "");
|
|
2568
3398
|
}
|
|
2569
|
-
|
|
3399
|
+
assertServiceModuleAcceptsTransform(
|
|
3400
|
+
codegenFs,
|
|
3401
|
+
serviceModuleFile,
|
|
3402
|
+
options.modelProject
|
|
3403
|
+
);
|
|
3404
|
+
const modelBase = modelBaseName(modelFilePath, options.modelName);
|
|
3405
|
+
const servicesFilePath = servicesFilePathFor(modelFilePath, modelBase);
|
|
3406
|
+
const serviceImportFromModel = toImportSpecifier(
|
|
3407
|
+
modelFilePath,
|
|
3408
|
+
serviceModuleFile
|
|
3409
|
+
);
|
|
3410
|
+
const serviceImportFromServices = toImportSpecifier(
|
|
3411
|
+
servicesFilePath,
|
|
3412
|
+
serviceModuleFile
|
|
3413
|
+
);
|
|
3414
|
+
const method = (options.method || "get").toLowerCase();
|
|
2570
3415
|
const mode = options.mode ?? (options.lifecycle ? "lifecycle" : "method");
|
|
2571
3416
|
const lifecycle = options.lifecycle || "LOAD";
|
|
3417
|
+
const catalogName = `${pascalCase(modelBase)}Endpoints`;
|
|
3418
|
+
const mockMode = options.mock ?? "auto";
|
|
3419
|
+
const operations = readServiceModuleOperations(codegenFs, serviceModuleFile);
|
|
3420
|
+
const pathValidated = operations !== null;
|
|
3421
|
+
const operationTyped = operations === null || operations.some(
|
|
3422
|
+
(op) => op.path === options.servicePath && op.method === method
|
|
3423
|
+
);
|
|
3424
|
+
if (!operationTyped && mockMode === "never") {
|
|
3425
|
+
throw untypedEndpointError({
|
|
3426
|
+
servicePath: options.servicePath,
|
|
3427
|
+
method,
|
|
3428
|
+
modelProject: options.modelProject,
|
|
3429
|
+
operations: operations ?? []
|
|
3430
|
+
});
|
|
3431
|
+
}
|
|
3432
|
+
const provisional = !operationTyped;
|
|
3433
|
+
const writeMock = provisional || mockMode === "always";
|
|
3434
|
+
if (provisional) {
|
|
3435
|
+
assertServiceModuleAcceptsProvisional(
|
|
3436
|
+
codegenFs,
|
|
3437
|
+
serviceModuleFile,
|
|
3438
|
+
options.modelProject
|
|
3439
|
+
);
|
|
3440
|
+
}
|
|
2572
3441
|
logger.info(
|
|
2573
|
-
`Adding ${mode}-driven @kosServiceRequest "${options.methodName}" (${method} ${options.servicePath}) to ${options.modelName}`
|
|
3442
|
+
`Adding ${mode}-driven ${provisional ? "PROVISIONAL " : ""}@kosServiceRequest "${options.methodName}" (${method} ${options.servicePath}) to ${options.modelName}`
|
|
2574
3443
|
);
|
|
3444
|
+
if (provisional) {
|
|
3445
|
+
logger.warn(
|
|
3446
|
+
`${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.`
|
|
3447
|
+
);
|
|
3448
|
+
}
|
|
3449
|
+
ensureServicesModule(codegenFs, modelFilePath, modelBase);
|
|
3450
|
+
let endpointKey = options.methodName;
|
|
3451
|
+
let endpointCreated = true;
|
|
3452
|
+
let dataAlias = "";
|
|
3453
|
+
let mapperName = "";
|
|
3454
|
+
let ctxAlias = "";
|
|
3455
|
+
let rawAlias = "";
|
|
3456
|
+
transformSourceFile(codegenFs, servicesFilePath, (sf) => {
|
|
3457
|
+
ensureNamedImport(sf, serviceImportFromServices, [{ name: "endpoint" }]);
|
|
3458
|
+
if (provisional) {
|
|
3459
|
+
ensureNamedImport(sf, serviceImportFromServices, [
|
|
3460
|
+
{ name: "ApiPath", isTypeOnly: true }
|
|
3461
|
+
]);
|
|
3462
|
+
}
|
|
3463
|
+
const pathText = provisional ? `${JSON.stringify(options.servicePath)} as ApiPath` : JSON.stringify(options.servicePath);
|
|
3464
|
+
const entry = ensureConstCatalogEntry(sf, {
|
|
3465
|
+
catalogName,
|
|
3466
|
+
key: options.methodName,
|
|
3467
|
+
initializer: `endpoint(${pathText}, ${JSON.stringify(method)})`,
|
|
3468
|
+
equals: sameEndpoint,
|
|
3469
|
+
leadingTrivia: provisional ? provisionalEntryComment(options.servicePath, method, catalogName) : void 0
|
|
3470
|
+
});
|
|
3471
|
+
endpointKey = entry.key;
|
|
3472
|
+
endpointCreated = entry.created;
|
|
3473
|
+
dataAlias = `${pascalCase(endpointKey)}Data`;
|
|
3474
|
+
mapperName = `to${pascalCase(endpointKey)}Data`;
|
|
3475
|
+
ctxAlias = `${pascalCase(endpointKey)}Ctx`;
|
|
3476
|
+
rawAlias = `${pascalCase(endpointKey)}Raw`;
|
|
3477
|
+
let rawType;
|
|
3478
|
+
if (provisional) {
|
|
3479
|
+
ensureExportedTypeAlias(sf, {
|
|
3480
|
+
name: rawAlias,
|
|
3481
|
+
type: options.rawType || "unknown",
|
|
3482
|
+
docs: rawTypeDocs(options.servicePath, method, options.rawType)
|
|
3483
|
+
});
|
|
3484
|
+
rawType = rawAlias;
|
|
3485
|
+
} else {
|
|
3486
|
+
rawType = `EndpointResponse<typeof ${catalogName}.${endpointKey}>`;
|
|
3487
|
+
ensureNamedImport(sf, serviceImportFromServices, [
|
|
3488
|
+
{ name: "EndpointResponse", isTypeOnly: true }
|
|
3489
|
+
]);
|
|
3490
|
+
if (writeMock) {
|
|
3491
|
+
ensureExportedTypeAlias(sf, {
|
|
3492
|
+
name: rawAlias,
|
|
3493
|
+
type: rawType,
|
|
3494
|
+
docs: `Raw wire shape of \`${method.toUpperCase()} ${options.servicePath}\`, as the spec declares it — what a mock for this endpoint must serve.`
|
|
3495
|
+
});
|
|
3496
|
+
}
|
|
3497
|
+
}
|
|
3498
|
+
ensureExportedTypeAlias(sf, { name: dataAlias, type: rawType });
|
|
3499
|
+
ensureExportedMapper(sf, {
|
|
3500
|
+
name: mapperName,
|
|
3501
|
+
rawType,
|
|
3502
|
+
dataType: dataAlias
|
|
3503
|
+
});
|
|
3504
|
+
if (mode === "method") {
|
|
3505
|
+
ensureNamedImport(sf, serviceImportFromServices, [
|
|
3506
|
+
{ name: "EndpointCtx", isTypeOnly: true }
|
|
3507
|
+
]);
|
|
3508
|
+
ensureExportedTypeAlias(sf, {
|
|
3509
|
+
name: ctxAlias,
|
|
3510
|
+
type: `EndpointCtx<typeof ${catalogName}.${endpointKey}, ${dataAlias}>`
|
|
3511
|
+
});
|
|
3512
|
+
}
|
|
3513
|
+
});
|
|
2575
3514
|
transformSourceFile(codegenFs, modelFilePath, (sf) => {
|
|
2576
|
-
const endpointConst = endpointConstName(options.methodName);
|
|
2577
3515
|
const className = getModelClass(sf).getName();
|
|
2578
3516
|
if (!className) throw new Error("Model class has no name.");
|
|
2579
3517
|
const typeParams = getModelClass(sf).getTypeParameters().map((tp) => tp.getText());
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
]);
|
|
2584
|
-
ensureModuleConst(sf, {
|
|
2585
|
-
name: endpointConst,
|
|
2586
|
-
initializer: `endpoint(${JSON.stringify(
|
|
2587
|
-
options.servicePath
|
|
2588
|
-
)}, ${JSON.stringify(method)})`
|
|
2589
|
-
});
|
|
3518
|
+
const decoratorName = provisional ? "provisionalServiceRequest" : "serviceRequest";
|
|
3519
|
+
const decoratorTypeArgs = provisional ? [rawAlias, dataAlias] : void 0;
|
|
3520
|
+
ensureNamedImport(sf, serviceImportFromModel, [{ name: decoratorName }]);
|
|
2590
3521
|
if (mode === "lifecycle") {
|
|
2591
|
-
ensureNamedImport(sf,
|
|
2592
|
-
{ name:
|
|
3522
|
+
ensureNamedImport(sf, "./services", [
|
|
3523
|
+
{ name: catalogName },
|
|
3524
|
+
{ name: mapperName },
|
|
3525
|
+
{ name: dataAlias, isTypeOnly: true },
|
|
3526
|
+
...provisional ? [{ name: rawAlias, isTypeOnly: true }] : []
|
|
2593
3527
|
]);
|
|
2594
3528
|
ensureNamedImport(sf, resolveSdkModuleSpecifier(sf), [
|
|
2595
3529
|
{ name: "DependencyLifecycle" }
|
|
2596
3530
|
]);
|
|
2597
3531
|
addDecoratedMethod(getModelClass(sf), {
|
|
2598
3532
|
name: options.methodName,
|
|
2599
|
-
decoratorName
|
|
2600
|
-
|
|
3533
|
+
decoratorName,
|
|
3534
|
+
decoratorTypeArgs,
|
|
3535
|
+
decoratorArgsText: `${catalogName}.${endpointKey}, { lifecycle: DependencyLifecycle.${lifecycle}, transform: ${mapperName} }`,
|
|
3536
|
+
docs: provisional ? provisionalMethodDocs(options.servicePath, method) : void 0,
|
|
2601
3537
|
// The manager invokes phase handlers error-first, passing (null, data)
|
|
2602
3538
|
// on success. The `error` half is only ever reached because
|
|
2603
3539
|
// `serviceRequest` supplies an errorHandler — the bare decorator
|
|
2604
3540
|
// defaults to `throw`, which fails the phase instead of calling back.
|
|
2605
3541
|
parameters: [
|
|
2606
3542
|
{ name: "error", type: "string | null" },
|
|
2607
|
-
{
|
|
2608
|
-
name: "data",
|
|
2609
|
-
type: `EndpointResponse<typeof ${endpointConst}>`
|
|
2610
|
-
}
|
|
3543
|
+
{ name: "data", type: dataAlias }
|
|
2611
3544
|
],
|
|
2612
3545
|
returnType: "void",
|
|
2613
3546
|
statements: "// TODO: apply `data` to the model"
|
|
2614
3547
|
});
|
|
2615
3548
|
return;
|
|
2616
3549
|
}
|
|
2617
|
-
ensureNamedImport(sf,
|
|
2618
|
-
{ name:
|
|
3550
|
+
ensureNamedImport(sf, "./services", [
|
|
3551
|
+
{ name: catalogName },
|
|
3552
|
+
{ name: mapperName },
|
|
3553
|
+
{ name: ctxAlias, isTypeOnly: true },
|
|
3554
|
+
...provisional ? [
|
|
3555
|
+
{ name: rawAlias, isTypeOnly: true },
|
|
3556
|
+
{ name: dataAlias, isTypeOnly: true }
|
|
3557
|
+
] : []
|
|
2619
3558
|
]);
|
|
2620
3559
|
ensureNamedImport(sf, resolveSdkModuleSpecifier(sf), [
|
|
2621
3560
|
{ name: "executeServiceRequest" },
|
|
@@ -2630,16 +3569,12 @@ function addServiceRequestToModel(codegenFs, options, projects) {
|
|
|
2630
3569
|
addClassDecorator(getModelClass(sf), "kosLoggerAware", { argsText: "" });
|
|
2631
3570
|
addDecoratedMethod(getModelClass(sf), {
|
|
2632
3571
|
name: options.methodName,
|
|
2633
|
-
decoratorName
|
|
2634
|
-
|
|
3572
|
+
decoratorName,
|
|
3573
|
+
decoratorTypeArgs,
|
|
3574
|
+
decoratorArgsText: `${catalogName}.${endpointKey}, { transform: ${mapperName} }`,
|
|
3575
|
+
docs: provisional ? provisionalMethodDocs(options.servicePath, method) : void 0,
|
|
2635
3576
|
// Optional: the framework appends the context, callers never pass it.
|
|
2636
|
-
parameters: [
|
|
2637
|
-
{
|
|
2638
|
-
name: "$ctx",
|
|
2639
|
-
type: `EndpointCtx<typeof ${endpointConst}>`,
|
|
2640
|
-
optional: true
|
|
2641
|
-
}
|
|
2642
|
-
],
|
|
3577
|
+
parameters: [{ name: "$ctx", type: ctxAlias, optional: true }],
|
|
2643
3578
|
isAsync: true,
|
|
2644
3579
|
returnType: "Promise<void>",
|
|
2645
3580
|
statements: [
|
|
@@ -2649,169 +3584,43 @@ function addServiceRequestToModel(codegenFs, options, projects) {
|
|
|
2649
3584
|
].join("\n")
|
|
2650
3585
|
});
|
|
2651
3586
|
});
|
|
2652
|
-
|
|
2653
|
-
|
|
2654
|
-
|
|
2655
|
-
|
|
2656
|
-
|
|
2657
|
-
}
|
|
2658
|
-
function modelElementType(typeText) {
|
|
2659
|
-
let m;
|
|
2660
|
-
if (m = typeText.match(/^([A-Za-z_]\w*Model)\s*\[\]$/)) return m[1];
|
|
2661
|
-
if (m = typeText.match(/^Array<\s*([A-Za-z_]\w*Model)\s*>$/)) return m[1];
|
|
2662
|
-
if (m = typeText.match(/^(?:Map|Set)<[^>]*?\b([A-Za-z_]\w*Model)\b[^>]*>$/))
|
|
2663
|
-
return m[1];
|
|
2664
|
-
return null;
|
|
2665
|
-
}
|
|
2666
|
-
function validateModel(codegenFs, options, projects) {
|
|
2667
|
-
const { modelFilePath } = resolveModelFilePath(codegenFs, options, projects);
|
|
2668
|
-
const content = codegenFs.read(modelFilePath);
|
|
2669
|
-
if (content === null) {
|
|
2670
|
-
throw new Error(`Model file not found: ${modelFilePath}`);
|
|
2671
|
-
}
|
|
2672
|
-
const project = new Project({ useInMemoryFileSystem: true });
|
|
2673
|
-
const sf = project.createSourceFile(modelFilePath, content, {
|
|
2674
|
-
overwrite: true
|
|
2675
|
-
});
|
|
2676
|
-
const findings = [];
|
|
2677
|
-
const hasKosModel = sf.getClasses().some((c) => c.getDecorator("kosModel"));
|
|
2678
|
-
if (!hasKosModel) {
|
|
2679
|
-
findings.push({
|
|
2680
|
-
level: "error",
|
|
2681
|
-
rule: "missing-kosModel",
|
|
2682
|
-
message: "No @kosModel decorator found — this is not a KOS model."
|
|
2683
|
-
});
|
|
2684
|
-
}
|
|
2685
|
-
const imports = sf.getImportDeclarations();
|
|
2686
|
-
const mobxImport = imports.find(
|
|
2687
|
-
(d) => /^mobx(-react-lite)?$/.test(d.getModuleSpecifierValue())
|
|
2688
|
-
);
|
|
2689
|
-
if (mobxImport) {
|
|
2690
|
-
findings.push({
|
|
2691
|
-
level: "error",
|
|
2692
|
-
rule: "mobx-import",
|
|
2693
|
-
message: "Imports mobx directly. Reactivity is internal to KOS — remove the mobx import and use KOS reactivity / container models."
|
|
2694
|
-
});
|
|
2695
|
-
}
|
|
2696
|
-
const barrelServiceRequest = imports.find(
|
|
2697
|
-
(d) => d.getModuleSpecifierValue() === "@kosdev-code/kos-ui-sdk" && d.getNamedImports().some((n) => n.getName() === "kosServiceRequest")
|
|
2698
|
-
);
|
|
2699
|
-
if (barrelServiceRequest) {
|
|
2700
|
-
findings.push({
|
|
2701
|
-
level: "warning",
|
|
2702
|
-
rule: "untyped-service-request",
|
|
2703
|
-
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."
|
|
2704
|
-
});
|
|
2705
|
-
}
|
|
2706
|
-
for (const cls of sf.getClasses()) {
|
|
2707
|
-
for (const prop of cls.getProperties()) {
|
|
2708
|
-
const name = prop.getName();
|
|
2709
|
-
const typeText = (prop.getTypeNode()?.getText() ?? "").replace(/\s+/g, " ").trim();
|
|
2710
|
-
const initText = prop.getInitializer()?.getText() ?? "";
|
|
2711
|
-
const hasChild = !!prop.getDecorator("kosChild");
|
|
2712
|
-
const isModelContainer = /\bI?KosModelContainer\s*</.test(typeText) || /new\s+KosModelContainer\b/.test(initText);
|
|
2713
|
-
if (isModelContainer && !hasChild) {
|
|
2714
|
-
findings.push({
|
|
2715
|
-
level: "warning",
|
|
2716
|
-
rule: "container-missing-kosChild",
|
|
2717
|
-
message: `Property '${name}' is a model container but is not marked @kosChild — add @kosChild so its models join the model graph and lifecycle.`
|
|
2718
|
-
});
|
|
2719
|
-
continue;
|
|
2720
|
-
}
|
|
2721
|
-
const elem = modelElementType(typeText);
|
|
2722
|
-
if (elem && !isModelContainer) {
|
|
2723
|
-
findings.push({
|
|
2724
|
-
level: "warning",
|
|
2725
|
-
rule: "raw-model-collection",
|
|
2726
|
-
message: `Property '${name}' is a raw ${typeText} of models — prefer a KosModelContainer<${elem}> (indexing, sorting, lifecycle, delta handling) marked @kosChild.`
|
|
2727
|
-
});
|
|
2728
|
-
}
|
|
2729
|
-
}
|
|
2730
|
-
}
|
|
2731
|
-
const hasError = findings.some((f) => f.level === "error");
|
|
2732
|
-
return { modelFilePath, ok: !hasError, findings };
|
|
2733
|
-
}
|
|
2734
|
-
function firstDecoratorArg(decoratorText) {
|
|
2735
|
-
const open = decoratorText.indexOf("(");
|
|
2736
|
-
if (open === -1) return void 0;
|
|
2737
|
-
const inner = decoratorText.slice(open + 1, decoratorText.lastIndexOf(")")).replace(/\s+/g, " ").trim();
|
|
2738
|
-
return inner || void 0;
|
|
2739
|
-
}
|
|
2740
|
-
function collectDecorated(cls, decoratorName) {
|
|
2741
|
-
const members = [];
|
|
2742
|
-
const visit = (name, decoratorTextOf, typeText) => {
|
|
2743
|
-
const text = decoratorTextOf();
|
|
2744
|
-
if (text === void 0) return;
|
|
2745
|
-
members.push({
|
|
2746
|
-
name,
|
|
2747
|
-
arg: firstDecoratorArg(text),
|
|
2748
|
-
type: typeText || void 0
|
|
2749
|
-
});
|
|
2750
|
-
};
|
|
2751
|
-
for (const m of cls.getMethods()) {
|
|
2752
|
-
const dec = m.getDecorator(decoratorName);
|
|
2753
|
-
if (dec) {
|
|
2754
|
-
visit(m.getName(), () => dec.getText(), m.getReturnTypeNode()?.getText());
|
|
2755
|
-
}
|
|
2756
|
-
}
|
|
2757
|
-
for (const p of cls.getProperties()) {
|
|
2758
|
-
const dec = p.getDecorator(decoratorName);
|
|
2759
|
-
if (dec) {
|
|
2760
|
-
visit(p.getName(), () => dec.getText(), p.getTypeNode()?.getText());
|
|
2761
|
-
}
|
|
2762
|
-
}
|
|
2763
|
-
return members;
|
|
2764
|
-
}
|
|
2765
|
-
function describeModel(codegenFs, options, projects) {
|
|
2766
|
-
const { modelFilePath } = resolveModelFilePath(codegenFs, options, projects);
|
|
2767
|
-
const content = codegenFs.read(modelFilePath);
|
|
2768
|
-
if (content === null) {
|
|
2769
|
-
throw new Error(`Model file not found: ${modelFilePath}`);
|
|
2770
|
-
}
|
|
2771
|
-
const project = new Project({ useInMemoryFileSystem: true });
|
|
2772
|
-
const sf = project.createSourceFile(modelFilePath, content, {
|
|
2773
|
-
overwrite: true
|
|
2774
|
-
});
|
|
2775
|
-
let modelType;
|
|
2776
|
-
const modelTypeDecl = sf.getVariableDeclaration("MODEL_TYPE");
|
|
2777
|
-
if (modelTypeDecl) {
|
|
2778
|
-
const init = modelTypeDecl.getInitializer()?.getText();
|
|
2779
|
-
if (init) modelType = init.replace(/^["'`]|["'`]$/g, "");
|
|
2780
|
-
}
|
|
2781
|
-
const cls = sf.getClasses().find((c) => c.getDecorator("kosModel")) ?? sf.getClasses().find((c) => c.isExported()) ?? sf.getClasses()[0];
|
|
2782
|
-
if (!cls) {
|
|
2783
|
-
return {
|
|
3587
|
+
const mock = writeMock ? ensureMockRoute(codegenFs, {
|
|
3588
|
+
modelFilePath,
|
|
3589
|
+
modelBase,
|
|
3590
|
+
sdkModuleSpecifier: readSourceFile(
|
|
3591
|
+
codegenFs,
|
|
2784
3592
|
modelFilePath,
|
|
2785
|
-
|
|
2786
|
-
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
|
|
2790
|
-
|
|
2791
|
-
|
|
2792
|
-
|
|
2793
|
-
serviceRequests: [],
|
|
2794
|
-
effects: [],
|
|
2795
|
-
futures: []
|
|
2796
|
-
};
|
|
2797
|
-
}
|
|
2798
|
-
const classDecorators = cls.getDecorators().map((d) => d.getName());
|
|
2799
|
-
const kosModelArg = cls.getDecorator("kosModel") ? firstDecoratorArg(cls.getDecorator("kosModel").getText()) : void 0;
|
|
2800
|
-
const singleton = /\bsingleton\s*:\s*true\b/.test(kosModelArg ?? "");
|
|
3593
|
+
resolveSdkModuleSpecifier
|
|
3594
|
+
),
|
|
3595
|
+
rawAlias,
|
|
3596
|
+
servicePath: options.servicePath,
|
|
3597
|
+
method,
|
|
3598
|
+
endpointKey,
|
|
3599
|
+
sampleText: options.sample
|
|
3600
|
+
}) : void 0;
|
|
2801
3601
|
return {
|
|
2802
3602
|
modelFilePath,
|
|
2803
|
-
|
|
2804
|
-
|
|
2805
|
-
|
|
2806
|
-
|
|
2807
|
-
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
|
|
3603
|
+
servicesFilePath,
|
|
3604
|
+
serviceModule: serviceImportFromModel,
|
|
3605
|
+
mode,
|
|
3606
|
+
endpointKey,
|
|
3607
|
+
endpointCreated,
|
|
3608
|
+
pathValidated,
|
|
3609
|
+
provisional,
|
|
3610
|
+
mocksFilePath: mock?.mocksFilePath,
|
|
3611
|
+
mockRegisterFunction: mock?.registerFunction,
|
|
3612
|
+
mockRoute: mock && `${method.toUpperCase()} ${mock.routePattern}`,
|
|
3613
|
+
mockSamplePlaceholder: mock?.samplePlaceholder,
|
|
3614
|
+
removalSteps: provisional ? removalSteps({
|
|
3615
|
+
modelProject: options.modelProject,
|
|
3616
|
+
servicePath: options.servicePath,
|
|
3617
|
+
method,
|
|
3618
|
+
catalogName,
|
|
3619
|
+
endpointKey,
|
|
3620
|
+
servicesFilePath,
|
|
3621
|
+
mocksFilePath: mock?.mocksFilePath,
|
|
3622
|
+
mapperName
|
|
3623
|
+
}) : void 0
|
|
2815
3624
|
};
|
|
2816
3625
|
}
|
|
2817
3626
|
const DEFAULT_SDK_PACKAGE = "@kosdev-code/kos-ui-sdk";
|
|
@@ -2997,6 +3806,82 @@ function lookupSdkType(codegenFs, options, projects) {
|
|
|
2997
3806
|
)}). Check the name, or it may be internal / not part of the public surface.`
|
|
2998
3807
|
};
|
|
2999
3808
|
}
|
|
3809
|
+
function modelElementType(typeText) {
|
|
3810
|
+
let m;
|
|
3811
|
+
if (m = typeText.match(/^([A-Za-z_]\w*Model)\s*\[\]$/)) return m[1];
|
|
3812
|
+
if (m = typeText.match(/^Array<\s*([A-Za-z_]\w*Model)\s*>$/)) return m[1];
|
|
3813
|
+
if (m = typeText.match(/^(?:Map|Set)<[^>]*?\b([A-Za-z_]\w*Model)\b[^>]*>$/))
|
|
3814
|
+
return m[1];
|
|
3815
|
+
return null;
|
|
3816
|
+
}
|
|
3817
|
+
function validateModel(codegenFs, options, projects) {
|
|
3818
|
+
const { modelFilePath } = resolveModelFilePath(codegenFs, options, projects);
|
|
3819
|
+
const content = codegenFs.read(modelFilePath);
|
|
3820
|
+
if (content === null) {
|
|
3821
|
+
throw new Error(`Model file not found: ${modelFilePath}`);
|
|
3822
|
+
}
|
|
3823
|
+
const project = new Project({ useInMemoryFileSystem: true });
|
|
3824
|
+
const sf = project.createSourceFile(modelFilePath, content, {
|
|
3825
|
+
overwrite: true
|
|
3826
|
+
});
|
|
3827
|
+
const findings = [];
|
|
3828
|
+
const hasKosModel = sf.getClasses().some((c) => c.getDecorator("kosModel"));
|
|
3829
|
+
if (!hasKosModel) {
|
|
3830
|
+
findings.push({
|
|
3831
|
+
level: "error",
|
|
3832
|
+
rule: "missing-kosModel",
|
|
3833
|
+
message: "No @kosModel decorator found — this is not a KOS model."
|
|
3834
|
+
});
|
|
3835
|
+
}
|
|
3836
|
+
const imports = sf.getImportDeclarations();
|
|
3837
|
+
const mobxImport = imports.find(
|
|
3838
|
+
(d) => /^mobx(-react-lite)?$/.test(d.getModuleSpecifierValue())
|
|
3839
|
+
);
|
|
3840
|
+
if (mobxImport) {
|
|
3841
|
+
findings.push({
|
|
3842
|
+
level: "error",
|
|
3843
|
+
rule: "mobx-import",
|
|
3844
|
+
message: "Imports mobx directly. Reactivity is internal to KOS — remove the mobx import and use KOS reactivity / container models."
|
|
3845
|
+
});
|
|
3846
|
+
}
|
|
3847
|
+
const barrelServiceRequest = imports.find(
|
|
3848
|
+
(d) => d.getModuleSpecifierValue() === "@kosdev-code/kos-ui-sdk" && d.getNamedImports().some((n) => n.getName() === "kosServiceRequest")
|
|
3849
|
+
);
|
|
3850
|
+
if (barrelServiceRequest) {
|
|
3851
|
+
findings.push({
|
|
3852
|
+
level: "warning",
|
|
3853
|
+
rule: "untyped-service-request",
|
|
3854
|
+
message: "kosServiceRequest imported from the SDK barrel. Use the typed decorator from the api:generate'd service module so paths are validated against the OpenAPI types."
|
|
3855
|
+
});
|
|
3856
|
+
}
|
|
3857
|
+
for (const cls of sf.getClasses()) {
|
|
3858
|
+
for (const prop of cls.getProperties()) {
|
|
3859
|
+
const name = prop.getName();
|
|
3860
|
+
const typeText = (prop.getTypeNode()?.getText() ?? "").replace(/\s+/g, " ").trim();
|
|
3861
|
+
const initText = prop.getInitializer()?.getText() ?? "";
|
|
3862
|
+
const hasChild = !!prop.getDecorator("kosChild");
|
|
3863
|
+
const isModelContainer = /\bI?KosModelContainer\s*</.test(typeText) || /new\s+KosModelContainer\b/.test(initText);
|
|
3864
|
+
if (isModelContainer && !hasChild) {
|
|
3865
|
+
findings.push({
|
|
3866
|
+
level: "warning",
|
|
3867
|
+
rule: "container-missing-kosChild",
|
|
3868
|
+
message: `Property '${name}' is a model container but is not marked @kosChild — add @kosChild so its models join the model graph and lifecycle.`
|
|
3869
|
+
});
|
|
3870
|
+
continue;
|
|
3871
|
+
}
|
|
3872
|
+
const elem = modelElementType(typeText);
|
|
3873
|
+
if (elem && !isModelContainer) {
|
|
3874
|
+
findings.push({
|
|
3875
|
+
level: "warning",
|
|
3876
|
+
rule: "raw-model-collection",
|
|
3877
|
+
message: `Property '${name}' is a raw ${typeText} of models — prefer a KosModelContainer<${elem}> (indexing, sorting, lifecycle, delta handling) marked @kosChild.`
|
|
3878
|
+
});
|
|
3879
|
+
}
|
|
3880
|
+
}
|
|
3881
|
+
}
|
|
3882
|
+
const hasError = findings.some((f) => f.level === "error");
|
|
3883
|
+
return { modelFilePath, ok: !hasError, findings };
|
|
3884
|
+
}
|
|
3000
3885
|
const PLUGIN_TYPES = {
|
|
3001
3886
|
CUI: "cui",
|
|
3002
3887
|
UTILITY: "utility",
|
|
@@ -3551,9 +4436,76 @@ function updatePluginConfiguration(codegenFs, projectConfig, options) {
|
|
|
3551
4436
|
);
|
|
3552
4437
|
}
|
|
3553
4438
|
}
|
|
4439
|
+
function parse(content) {
|
|
4440
|
+
const singleQuoted = /^\s*import\b[^"']*'/m.test(content);
|
|
4441
|
+
const project = new Project({
|
|
4442
|
+
useInMemoryFileSystem: true,
|
|
4443
|
+
manipulationSettings: {
|
|
4444
|
+
indentationText: IndentationText.TwoSpaces,
|
|
4445
|
+
quoteKind: singleQuoted ? QuoteKind.Single : QuoteKind.Double
|
|
4446
|
+
}
|
|
4447
|
+
});
|
|
4448
|
+
return project.createSourceFile("registration-chain.ts", content, {
|
|
4449
|
+
overwrite: true
|
|
4450
|
+
});
|
|
4451
|
+
}
|
|
4452
|
+
function chainCalls(sf, name) {
|
|
4453
|
+
return sf.getDescendantsOfKind(SyntaxKind.CallExpression).filter((call) => {
|
|
4454
|
+
const callee = call.getExpression();
|
|
4455
|
+
return callee.getKind() === SyntaxKind.PropertyAccessExpression && callee.asKindOrThrow(SyntaxKind.PropertyAccessExpression).getName() === name;
|
|
4456
|
+
});
|
|
4457
|
+
}
|
|
4458
|
+
function indentOfCall(sf, call) {
|
|
4459
|
+
const text = sf.getFullText();
|
|
4460
|
+
const nameNode = call.getExpression().asKindOrThrow(SyntaxKind.PropertyAccessExpression).getNameNode();
|
|
4461
|
+
const lineStart = text.lastIndexOf("\n", nameNode.getStart()) + 1;
|
|
4462
|
+
return text.slice(lineStart).match(/^[ \t]*/)?.[0] ?? "";
|
|
4463
|
+
}
|
|
4464
|
+
function countChainEntries(content) {
|
|
4465
|
+
return chainCalls(parse(content), "model").length;
|
|
4466
|
+
}
|
|
4467
|
+
function upsertChainEntry(content, bean, importSpec) {
|
|
4468
|
+
const sf = parse(content);
|
|
4469
|
+
const modelCalls = chainCalls(sf, "model");
|
|
4470
|
+
const registered = modelCalls.some((call) => {
|
|
4471
|
+
const [arg, ...rest] = call.getArguments();
|
|
4472
|
+
return rest.length === 0 && arg?.getText() === bean;
|
|
4473
|
+
});
|
|
4474
|
+
if (registered) return { changed: false, note: "already registered" };
|
|
4475
|
+
let anchor;
|
|
4476
|
+
let indent;
|
|
4477
|
+
if (modelCalls.length > 0) {
|
|
4478
|
+
anchor = modelCalls.reduce(
|
|
4479
|
+
(last, call) => call.getEnd() > last.getEnd() ? call : last
|
|
4480
|
+
);
|
|
4481
|
+
indent = indentOfCall(sf, anchor);
|
|
4482
|
+
} else {
|
|
4483
|
+
const starts = chainCalls(sf, "models").filter(
|
|
4484
|
+
(call) => call.getArguments().length === 0
|
|
4485
|
+
);
|
|
4486
|
+
anchor = starts[starts.length - 1];
|
|
4487
|
+
if (!anchor) {
|
|
4488
|
+
return {
|
|
4489
|
+
changed: false,
|
|
4490
|
+
error: "no .models() chain found in the registration file"
|
|
4491
|
+
};
|
|
4492
|
+
}
|
|
4493
|
+
const text = sf.getFullText();
|
|
4494
|
+
const lineStart = text.lastIndexOf("\n", anchor.getStart()) + 1;
|
|
4495
|
+
indent = (text.slice(lineStart).match(/^[ \t]*/)?.[0] ?? "") + " ";
|
|
4496
|
+
}
|
|
4497
|
+
anchor.replaceWithText(`${anchor.getText()}
|
|
4498
|
+
${indent}.model(${bean})`);
|
|
4499
|
+
const imported = sf.getImportDeclarations().some(
|
|
4500
|
+
(decl) => decl.getNamedImports().some((named) => named.getName() === bean)
|
|
4501
|
+
);
|
|
4502
|
+
if (!imported) ensureNamedImport(sf, importSpec, [{ name: bean }]);
|
|
4503
|
+
return { changed: true, content: sf.getFullText(), importAdded: !imported };
|
|
4504
|
+
}
|
|
3554
4505
|
export {
|
|
3555
4506
|
BasePluginHandler,
|
|
3556
4507
|
CONTRIBUTION_TYPE_MAP,
|
|
4508
|
+
DEFAULT_MODEL_LIBS_DIR,
|
|
3557
4509
|
DirectFileSystem,
|
|
3558
4510
|
KAB_OUTPUT_DIR,
|
|
3559
4511
|
KAB_OUTPUT_PATH,
|
|
@@ -3575,6 +4527,7 @@ export {
|
|
|
3575
4527
|
addJavaArtifactToManifests,
|
|
3576
4528
|
addKosModelConfiguration,
|
|
3577
4529
|
addModelEffectToModel,
|
|
4530
|
+
addParentAwareToModel,
|
|
3578
4531
|
addPropertyToModel,
|
|
3579
4532
|
addServiceRequestToModel,
|
|
3580
4533
|
addTopicHandlerToModel,
|
|
@@ -3583,6 +4536,7 @@ export {
|
|
|
3583
4536
|
buildSbomTarget,
|
|
3584
4537
|
camelCase,
|
|
3585
4538
|
constantCase,
|
|
4539
|
+
countChainEntries,
|
|
3586
4540
|
dashCase,
|
|
3587
4541
|
describeModel,
|
|
3588
4542
|
discoverJavaArtifacts,
|
|
@@ -3601,8 +4555,10 @@ export {
|
|
|
3601
4555
|
generateHook,
|
|
3602
4556
|
generateInit,
|
|
3603
4557
|
generateModel,
|
|
4558
|
+
generateModelProject,
|
|
3604
4559
|
generatePolyglotWorkspace,
|
|
3605
4560
|
generateSplashProject,
|
|
4561
|
+
generateViewModel,
|
|
3606
4562
|
getCodegenLogger,
|
|
3607
4563
|
getCurrentDirectoryName,
|
|
3608
4564
|
getKosModelConfigProp,
|
|
@@ -3620,10 +4576,12 @@ export {
|
|
|
3620
4576
|
readNxJson,
|
|
3621
4577
|
resolveKabPath,
|
|
3622
4578
|
resolveModelFilePath,
|
|
4579
|
+
resolveModelProjectLayout,
|
|
3623
4580
|
setCodegenLogger,
|
|
3624
4581
|
syncCiManifests,
|
|
3625
4582
|
updateJson,
|
|
3626
4583
|
updateModelIndex,
|
|
4584
|
+
upsertChainEntry,
|
|
3627
4585
|
validateModel,
|
|
3628
4586
|
writeJson
|
|
3629
4587
|
};
|