@kosdev-code/kos-codegen-core 0.1.0-next.862 → 0.1.0-next.865

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 CHANGED
@@ -13,5 +13,5 @@ export { getCurrentDirectoryName, getProject, getKosProjectConfiguration, getKos
13
13
  export type { KosModelConfiguration, KosProjectConfiguration, } from './lib/kos-config';
14
14
  export { getTemplateDir } from './lib/template-resolver';
15
15
  export { generateSplashProject, generateInit, generatePolyglotWorkspace, ensureJavaAggregatorPom, finalizeArchetypeModule, addJavaArtifactToManifests, syncCiManifests, discoverUiArtifacts, discoverJavaArtifacts, buildKabTargets, buildSbomTarget, listServiceCatalog, resolveKabPath, KAB_OUTPUT_DIR, KAB_OUTPUT_PATH, UPDATE_RELEASE_VERSION_SCRIPT, UPDATE_RELEASE_VERSION_SCRIPT_PATH, normalizeOptions, appendBarrelExport, updateModelIndex, generateHook, generateContext, generateContainerModel, generateCompanionModel, generateModel, addFutureToModel, addContainerSupportToModel, addModelEffectToModel, addDependencyToModel, addChildToModel, addTopicHandlerToModel, addConfigPropertyToModel, addServiceRequestToModel, addPropertyToModel, addComputedToModel, validateModel, describeModel, lookupSdkType, resolveModelFilePath, generateComponent, PLUGIN_TYPES, CONTRIBUTION_TYPE_MAP, LOCALIZED_PLUGIN_TYPES, KOS_JSON_PATHS, PluginHandlerFactory, BasePluginHandler, KosConfigBuilder, ValidationError, } from './lib/generators';
16
- export type { SplashProjectOptions, KosInitOptions, KabTargetsOptions, KabTargetDefinition, PolyglotWorkspaceOptions, PolyglotWorkspaceResult, EnsureJavaAggregatorOptions, FinalizeArchetypeModuleOptions, AddJavaArtifactOptions, CiSyncOptions, CiSyncResult, CiSyncManifestResult, DiscoveredArtifact, KosBaseGeneratorOptions, NormalizedOptions, HookOptions, ContextOptions, ContainerModelOptions, CompanionModelOptions, ModelOptions, GenerateModelParams, AddFutureToModelOptions, NormalizedAddFutureToModelOptions, AddContainerSupportOptions, AddModelEffectOptions, AddDependencyOptions, AddChildOptions, ChildShape, AddTopicHandlerOptions, AddConfigPropertyOptions, AddServiceRequestOptions, AddPropertyOptions, AddComputedOptions, ValidateModelOptions, ValidationFinding, ValidationResult, DescribeModelOptions, DescribedMember, ModelDescription, ModelFileQuery, LookupSdkTypeOptions, SdkSymbolDeclaration, SdkTypeLookupResult, ComponentOptions, NormalizedComponentOptions, PluginConfiguration, PluginContribution, ExperienceConfig, PluginType, PluginHandler, } from './lib/generators';
16
+ export type { SplashProjectOptions, KosInitOptions, KabTargetsOptions, KabTargetDefinition, PolyglotWorkspaceOptions, PolyglotWorkspaceResult, EnsureJavaAggregatorOptions, FinalizeArchetypeModuleOptions, AddJavaArtifactOptions, CiSyncOptions, CiSyncResult, CiSyncManifestResult, DiscoveredArtifact, KosBaseGeneratorOptions, NormalizedOptions, HookOptions, ContextOptions, ContainerModelOptions, CompanionModelOptions, ModelOptions, GenerateModelParams, AddFutureToModelOptions, NormalizedAddFutureToModelOptions, AddContainerSupportOptions, AddModelEffectOptions, AddDependencyOptions, AddChildOptions, ChildShape, AddTopicHandlerOptions, AddConfigPropertyOptions, AddServiceRequestOptions, AddServiceRequestResult, ServiceRequestMockMode, UntypedEndpointDetails, UntypedEndpointError, AddPropertyOptions, AddComputedOptions, ValidateModelOptions, ValidationFinding, ValidationResult, DescribeModelOptions, DescribedMember, ModelDescription, ModelFileQuery, LookupSdkTypeOptions, SdkSymbolDeclaration, SdkTypeLookupResult, ComponentOptions, NormalizedComponentOptions, PluginConfiguration, PluginContribution, ExperienceConfig, PluginType, PluginHandler, } from './lib/generators';
17
17
  //# sourceMappingURL=index.d.ts.map
package/index.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../packages/kos-codegen-core/src/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../packages/kos-codegen-core/src/index.ts"],"names":[],"mappings":"AACA,YAAY,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AAClE,OAAO,EAAE,gBAAgB,EAAE,kBAAkB,EAAE,MAAM,0BAA0B,CAAC;AAGhF,OAAO,EAAE,0BAA0B,EAAE,MAAM,sBAAsB,CAAC;AAGlE,YAAY,EAAE,oBAAoB,EAAE,MAAM,yBAAyB,CAAC;AACpE,OAAO,EACL,gBAAgB,EAChB,iBAAiB,EACjB,kBAAkB,EAClB,UAAU,GACX,MAAM,yBAAyB,CAAC;AAGjC,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAGnE,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAGjD,OAAO,EACL,QAAQ,EACR,SAAS,EACT,UAAU,EACV,UAAU,EACV,YAAY,GACb,MAAM,kBAAkB,CAAC;AAG1B,OAAO,EAAE,kBAAkB,EAAE,MAAM,wBAAwB,CAAC;AAG5D,YAAY,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAClD,OAAO,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAGlE,OAAO,EACL,uBAAuB,EACvB,UAAU,EACV,0BAA0B,EAC1B,wBAAwB,EACxB,qBAAqB,EACrB,wBAAwB,GACzB,MAAM,kBAAkB,CAAC;AAC1B,YAAY,EACV,qBAAqB,EACrB,uBAAuB,GACxB,MAAM,kBAAkB,CAAC;AAG1B,OAAO,EAAE,cAAc,EAAE,MAAM,yBAAyB,CAAC;AAGzD,OAAO,EACL,qBAAqB,EACrB,YAAY,EACZ,yBAAyB,EACzB,uBAAuB,EACvB,uBAAuB,EACvB,0BAA0B,EAC1B,eAAe,EACf,mBAAmB,EACnB,qBAAqB,EACrB,eAAe,EACf,eAAe,EACf,kBAAkB,EAClB,cAAc,EACd,cAAc,EACd,eAAe,EACf,6BAA6B,EAC7B,kCAAkC,EAClC,gBAAgB,EAChB,kBAAkB,EAClB,gBAAgB,EAChB,YAAY,EACZ,eAAe,EACf,sBAAsB,EACtB,sBAAsB,EACtB,aAAa,EACb,gBAAgB,EAChB,0BAA0B,EAC1B,qBAAqB,EACrB,oBAAoB,EACpB,eAAe,EACf,sBAAsB,EACtB,wBAAwB,EACxB,wBAAwB,EACxB,kBAAkB,EAClB,kBAAkB,EAClB,aAAa,EACb,aAAa,EACb,aAAa,EACb,oBAAoB,EACpB,iBAAiB,EACjB,YAAY,EACZ,qBAAqB,EACrB,sBAAsB,EACtB,cAAc,EACd,oBAAoB,EACpB,iBAAiB,EACjB,gBAAgB,EAChB,eAAe,GAChB,MAAM,kBAAkB,CAAC;AAC1B,YAAY,EACV,oBAAoB,EACpB,cAAc,EACd,iBAAiB,EACjB,mBAAmB,EACnB,wBAAwB,EACxB,uBAAuB,EACvB,2BAA2B,EAC3B,8BAA8B,EAC9B,sBAAsB,EACtB,aAAa,EACb,YAAY,EACZ,oBAAoB,EACpB,kBAAkB,EAClB,uBAAuB,EACvB,iBAAiB,EACjB,WAAW,EACX,cAAc,EACd,qBAAqB,EACrB,qBAAqB,EACrB,YAAY,EACZ,mBAAmB,EACnB,uBAAuB,EACvB,iCAAiC,EACjC,0BAA0B,EAC1B,qBAAqB,EACrB,oBAAoB,EACpB,eAAe,EACf,UAAU,EACV,sBAAsB,EACtB,wBAAwB,EACxB,wBAAwB,EACxB,uBAAuB,EACvB,sBAAsB,EACtB,sBAAsB,EACtB,oBAAoB,EACpB,kBAAkB,EAClB,kBAAkB,EAClB,oBAAoB,EACpB,iBAAiB,EACjB,gBAAgB,EAChB,oBAAoB,EACpB,eAAe,EACf,gBAAgB,EAChB,cAAc,EACd,oBAAoB,EACpB,oBAAoB,EACpB,mBAAmB,EACnB,gBAAgB,EAChB,0BAA0B,EAC1B,mBAAmB,EACnB,kBAAkB,EAClB,gBAAgB,EAChB,UAAU,EACV,aAAa,GACd,MAAM,kBAAkB,CAAC"}
package/index.js CHANGED
@@ -1088,7 +1088,8 @@ function ensureConstCatalogEntry(sourceFile, spec) {
1088
1088
  }
1089
1089
  literal.addPropertyAssignment({
1090
1090
  name: spec.key,
1091
- initializer: spec.initializer
1091
+ initializer: spec.initializer,
1092
+ leadingTrivia: spec.leadingTrivia
1092
1093
  });
1093
1094
  return { key: spec.key, created: true };
1094
1095
  }
@@ -1106,7 +1107,8 @@ function ensureConstCatalogEntry(sourceFile, spec) {
1106
1107
  }
1107
1108
  literal.addPropertyAssignment({
1108
1109
  name: spec.key,
1109
- initializer: spec.initializer
1110
+ initializer: spec.initializer,
1111
+ leadingTrivia: spec.leadingTrivia
1110
1112
  });
1111
1113
  return { key: spec.key, created: true };
1112
1114
  }
@@ -1115,7 +1117,8 @@ function ensureExportedTypeAlias(sourceFile, spec) {
1115
1117
  sourceFile.addTypeAlias({
1116
1118
  name: spec.name,
1117
1119
  type: spec.type,
1118
- isExported: true
1120
+ isExported: true,
1121
+ docs: spec.docs ? [spec.docs] : void 0
1119
1122
  });
1120
1123
  return true;
1121
1124
  }
@@ -1145,6 +1148,7 @@ function addDecoratedMethod(cls, spec) {
1145
1148
  name: spec.name,
1146
1149
  isAsync: spec.isAsync,
1147
1150
  returnType: spec.returnType,
1151
+ docs: spec.docs ? [spec.docs] : void 0,
1148
1152
  parameters: spec.parameters?.map((p) => {
1149
1153
  return { name: p.name, type: p.type, hasQuestionToken: p.optional };
1150
1154
  }),
@@ -1152,6 +1156,7 @@ function addDecoratedMethod(cls, spec) {
1152
1156
  decorators: [
1153
1157
  {
1154
1158
  name: spec.decoratorName,
1159
+ typeArguments: spec.decoratorTypeArgs,
1155
1160
  arguments: spec.decoratorArgsText ? [spec.decoratorArgsText] : []
1156
1161
  }
1157
1162
  ]
@@ -1234,17 +1239,28 @@ function listServiceCatalog(codegenFs, query, projects) {
1234
1239
  app,
1235
1240
  version,
1236
1241
  serviceModulePath: posix,
1237
- operations: readOperations(codegenFs, openapiPath)
1242
+ operations: readOperations(codegenFs, openapiPath) ?? []
1238
1243
  });
1239
1244
  }
1240
1245
  return entries.sort(
1241
1246
  (a, b) => a.app.localeCompare(b.app) || a.version.localeCompare(b.version)
1242
1247
  );
1243
1248
  }
1249
+ function readServiceModuleOperations(codegenFs, serviceModuleFile) {
1250
+ const moduleFile = serviceModuleFile.endsWith(".ts") ? serviceModuleFile : `${serviceModuleFile}.ts`;
1251
+ const posix = moduleFile.split(path__namespace.sep).join("/");
1252
+ const openapiPath = posix.replace(/service\.ts$/, "openapi.d.ts");
1253
+ for (const candidate of [openapiPath, posix]) {
1254
+ if (!codegenFs.exists(candidate)) continue;
1255
+ const operations = readOperations(codegenFs, candidate);
1256
+ if (operations) return operations;
1257
+ }
1258
+ return null;
1259
+ }
1244
1260
  function readOperations(codegenFs, openapiPath) {
1245
1261
  return readSourceFile(codegenFs, openapiPath, (sf) => {
1246
1262
  const paths = sf.getInterface("paths");
1247
- if (!paths) return [];
1263
+ if (!paths) return null;
1248
1264
  const operations = [];
1249
1265
  for (const pathProp of paths.getProperties()) {
1250
1266
  const servicePath = unquote(pathProp.getName());
@@ -1766,7 +1782,7 @@ function servicesFilePathFor(modelFilePath, modelBase) {
1766
1782
  `${modelBase}-services.ts`
1767
1783
  );
1768
1784
  }
1769
- function header(modelBase) {
1785
+ function header$1(modelBase) {
1770
1786
  return `/**
1771
1787
  * Service layer for the ${modelBase} model: the endpoints it calls and the
1772
1788
  * types derived from them. Standalone service functions for callers outside a
@@ -1777,7 +1793,7 @@ function header(modelBase) {
1777
1793
  function ensureServicesModule(codegenFs, modelFilePath, modelBase) {
1778
1794
  const servicesFilePath = servicesFilePathFor(modelFilePath, modelBase);
1779
1795
  if (!codegenFs.exists(servicesFilePath)) {
1780
- codegenFs.write(servicesFilePath, header(modelBase));
1796
+ codegenFs.write(servicesFilePath, header$1(modelBase));
1781
1797
  }
1782
1798
  const barrelPath = path__namespace.join(path__namespace.dirname(servicesFilePath), "index.ts");
1783
1799
  if (!codegenFs.exists(barrelPath)) {
@@ -2537,6 +2553,154 @@ function addComputedToModel(codegenFs, options, projects) {
2537
2553
  });
2538
2554
  return { modelFilePath };
2539
2555
  }
2556
+ function mocksFilePathFor(modelFilePath, modelBase) {
2557
+ return path__namespace.join(
2558
+ path__namespace.dirname(modelFilePath),
2559
+ "mocks",
2560
+ `${modelBase}-mocks.ts`
2561
+ );
2562
+ }
2563
+ function mockRegisterFunctionName(modelBase) {
2564
+ return `register${pascalCase(modelBase)}Mocks`;
2565
+ }
2566
+ function toMockRoutePattern(servicePath) {
2567
+ return servicePath.replace(/\{([^}]+)\}/g, ":$1");
2568
+ }
2569
+ function mockRouteCall(method, pattern, body) {
2570
+ const shorthand = {
2571
+ get: "get",
2572
+ post: "post",
2573
+ put: "put",
2574
+ delete: "del"
2575
+ };
2576
+ const fn = shorthand[method];
2577
+ return fn ? `KosMock.${fn}(${JSON.stringify(pattern)}, ${body});` : `KosMock.route(${JSON.stringify(method.toUpperCase())}, ${JSON.stringify(
2578
+ pattern
2579
+ )}, ${body});`;
2580
+ }
2581
+ function header(modelBase, registerFn) {
2582
+ return `/**
2583
+ * KosMock routes for the ${modelBase} model's PROVISIONAL endpoints — the ones
2584
+ * this project's generated OpenAPI types do not declare.
2585
+ *
2586
+ * NOTHING IMPORTS THIS FILE. Call \`${registerFn}()\` from the app's dev entry,
2587
+ * gated so it cannot run in a production build (\`import.meta.env.DEV\`, a
2588
+ * dev-only entry module, or behind your own switch). Registering a route enables
2589
+ * KosMock, and a mock that shipped would shadow the real endpoint once it exists.
2590
+ *
2591
+ * Unmatched requests still pass through to the real device (KosMock's hybrid
2592
+ * default), so these routes shadow only the paths named below. Every mocked
2593
+ * response carries the \`kos-mocked: true\` wire header, so mock-fed data is
2594
+ * identifiable in devtools.
2595
+ *
2596
+ * DELETE THIS FILE once every endpoint below is in the generated types.
2597
+ */
2598
+ `;
2599
+ }
2600
+ function ensureMockRoute(codegenFs, options) {
2601
+ const mocksFilePath = mocksFilePathFor(
2602
+ options.modelFilePath,
2603
+ options.modelBase
2604
+ );
2605
+ const registerFunction = mockRegisterFunctionName(options.modelBase);
2606
+ const routePattern = toMockRoutePattern(options.servicePath);
2607
+ const sampleName = `SAMPLE_${constantCase(dashCase(options.endpointKey))}`;
2608
+ const samplePlaceholder = !options.sampleText;
2609
+ if (!codegenFs.exists(mocksFilePath)) {
2610
+ codegenFs.write(mocksFilePath, header(options.modelBase, registerFunction));
2611
+ }
2612
+ const sdkSpecifier = options.sdkModuleSpecifier.startsWith(".") ? `../${options.sdkModuleSpecifier}` : options.sdkModuleSpecifier;
2613
+ let routeCreated = false;
2614
+ transformSourceFile(codegenFs, mocksFilePath, (sf) => {
2615
+ ensureNamedImport(sf, sdkSpecifier, [{ name: "KosMock" }]);
2616
+ ensureNamedImport(sf, "../services", [
2617
+ { name: options.rawAlias, isTypeOnly: true }
2618
+ ]);
2619
+ ensureSample(sf, {
2620
+ name: sampleName,
2621
+ type: options.rawAlias,
2622
+ servicePath: options.servicePath,
2623
+ method: options.method,
2624
+ sampleText: options.sampleText
2625
+ });
2626
+ const statement = mockRouteCall(
2627
+ options.method,
2628
+ routePattern,
2629
+ `{ data: ${sampleName} }`
2630
+ );
2631
+ routeCreated = ensureRegisteredRoute(sf, {
2632
+ registerFunction,
2633
+ modelBase: options.modelBase,
2634
+ statement,
2635
+ // Matching on the pattern literal alone would collide across methods.
2636
+ marker: `${options.method.toUpperCase()} ${routePattern}`,
2637
+ matches: (existing) => existing.includes(JSON.stringify(routePattern)) && existing.includes(shorthandOrMethod(options.method))
2638
+ });
2639
+ });
2640
+ return {
2641
+ mocksFilePath,
2642
+ registerFunction,
2643
+ routePattern,
2644
+ sampleName,
2645
+ routeCreated,
2646
+ samplePlaceholder
2647
+ };
2648
+ }
2649
+ function shorthandOrMethod(method) {
2650
+ const shorthand = {
2651
+ get: "KosMock.get(",
2652
+ post: "KosMock.post(",
2653
+ put: "KosMock.put(",
2654
+ delete: "KosMock.del("
2655
+ };
2656
+ return shorthand[method] ?? `"${method.toUpperCase()}"`;
2657
+ }
2658
+ function ensureSample(sf, spec) {
2659
+ if (sf.getVariableDeclaration(spec.name)) return;
2660
+ const lines = [
2661
+ `Sample payload for \`${spec.method.toUpperCase()} ${spec.servicePath}\` — the`,
2662
+ "UNWRAPPED `data` payload, i.e. exactly what the transform receives."
2663
+ ];
2664
+ if (!spec.sampleText) {
2665
+ lines.push(
2666
+ "",
2667
+ "TODO: replace the placeholder with something the backend would actually",
2668
+ "serve. Until then the route answers with nothing and the transform is",
2669
+ "never exercised."
2670
+ );
2671
+ }
2672
+ const docs = lines.join("\n");
2673
+ sf.addVariableStatement({
2674
+ isExported: true,
2675
+ declarationKind: tsMorph.VariableDeclarationKind.Const,
2676
+ docs: [docs],
2677
+ declarations: [
2678
+ {
2679
+ name: spec.name,
2680
+ type: spec.type,
2681
+ initializer: spec.sampleText ?? `null as unknown as ${spec.type}`
2682
+ }
2683
+ ]
2684
+ });
2685
+ }
2686
+ function ensureRegisteredRoute(sf, spec) {
2687
+ let fn = sf.getFunction(spec.registerFunction);
2688
+ if (!fn) {
2689
+ fn = sf.addFunction({
2690
+ name: spec.registerFunction,
2691
+ isExported: true,
2692
+ returnType: "void",
2693
+ docs: [
2694
+ `Arm the ${spec.modelBase} model's provisional endpoints. Call this from a dev-only entry point — never from code that ships.`
2695
+ ]
2696
+ });
2697
+ }
2698
+ const already = fn.getStatements().some((statement) => spec.matches(statement.getText()));
2699
+ if (already) return false;
2700
+ fn.addStatements(`// ${spec.marker}
2701
+ ${spec.statement}`);
2702
+ return true;
2703
+ }
2540
2704
  const SERVICE_MODULE_RE = /\/utils\/services\/.*\/service\.ts$/;
2541
2705
  function findServiceModules(codegenFs, sourceRoot) {
2542
2706
  return codegenFs.listFiles(sourceRoot).filter((f) => SERVICE_MODULE_RE.test(f.split(path__namespace.sep).join("/")));
@@ -2561,10 +2725,23 @@ function assertServiceModuleAcceptsTransform(codegenFs, serviceModuleFile, model
2561
2725
  `${file} predates the transform-aware service layer (EndpointCtx takes ${typeParams} type parameter). Run \`kosui api:generate --project ${modelProject} --helpers-only\` to refresh the helper layer in place (no spec fetch, openapi.d.ts untouched), then add the service request.`
2562
2726
  );
2563
2727
  }
2728
+ function assertServiceModuleAcceptsProvisional(codegenFs, serviceModuleFile, modelProject) {
2729
+ const file = `${serviceModuleFile}.ts`;
2730
+ if (!codegenFs.exists(file)) return;
2731
+ const declared = readSourceFile(
2732
+ codegenFs,
2733
+ file,
2734
+ (sf) => Boolean(sf.getFunction("provisionalServiceRequest"))
2735
+ );
2736
+ if (declared) return;
2737
+ throw new Error(
2738
+ `${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.`
2739
+ );
2740
+ }
2564
2741
  function sameEndpoint(existing, candidate) {
2565
2742
  const args = (text) => {
2566
2743
  const match = text.match(
2567
- /^endpoint\s*\(\s*(['"])(.*?)\1\s*,\s*(['"])(.*?)\3\s*,?\s*\)$/
2744
+ /^endpoint\s*\(\s*(['"])(.*?)\1(?:\s+as\s+ApiPath)?\s*,\s*(['"])(.*?)\3\s*,?\s*\)$/
2568
2745
  );
2569
2746
  return match ? [match[2], match[4].toLowerCase()] : null;
2570
2747
  };
@@ -2572,6 +2749,104 @@ function sameEndpoint(existing, candidate) {
2572
2749
  const b = args(candidate.trim());
2573
2750
  return !!a && !!b && a[0] === b[0] && a[1] === b[1];
2574
2751
  }
2752
+ function editDistance(a, b) {
2753
+ let previous = Array.from({ length: b.length + 1 }, (_, i) => i);
2754
+ for (let i = 1; i <= a.length; i++) {
2755
+ const current = [i];
2756
+ for (let j = 1; j <= b.length; j++) {
2757
+ current[j] = Math.min(
2758
+ previous[j] + 1,
2759
+ current[j - 1] + 1,
2760
+ previous[j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1)
2761
+ );
2762
+ }
2763
+ previous = current;
2764
+ }
2765
+ return previous[b.length];
2766
+ }
2767
+ const NEAREST_SHOWN = 5;
2768
+ function provisionalEntryComment(servicePath, method, catalogName) {
2769
+ return `/**
2770
+ * PROVISIONAL — \`${method.toUpperCase()} ${servicePath}\` is NOT in this project's
2771
+ * generated OpenAPI types. \`as ApiPath\` is standing in for a \`paths\` entry that
2772
+ * does not exist, so nothing about this endpoint is checked against a spec and
2773
+ * its response cannot be derived from one. Do not read this as a real endpoint.
2774
+ *
2775
+ * WHEN THE ENDPOINT LANDS:
2776
+ * 1. regenerate this project's API types (\`kosui api:generate\`)
2777
+ * 2. delete \` as ApiPath\` from the entry below
2778
+ * 3. point the endpoint's \`…Raw\` alias at
2779
+ * \`EndpointResponse<typeof ${catalogName}.…>\`
2780
+ * 4. swap \`provisionalServiceRequest\` for \`serviceRequest\` in the model
2781
+ * 5. delete the model's \`mocks/\` module
2782
+ * 6. KEEP the transform — it is real code and survives all of the above
2783
+ */
2784
+ `;
2785
+ }
2786
+ function provisionalMethodDocs(servicePath, method) {
2787
+ return [
2788
+ `PROVISIONAL — \`${method.toUpperCase()} ${servicePath}\` is not in the`,
2789
+ "generated OpenAPI types, so this request is served by a KosMock route, not",
2790
+ "by the device. The feature it backs is NOT complete: report the missing",
2791
+ "endpoint rather than treating this as finished.",
2792
+ "",
2793
+ "See the endpoint's entry in `./services` for what to undo when it lands."
2794
+ ].join("\n");
2795
+ }
2796
+ function rawTypeDocs(servicePath, method, rawType) {
2797
+ const shape = rawType ? [
2798
+ "Stated by hand and unverified: there is no spec entry to check it",
2799
+ "against."
2800
+ ] : [
2801
+ "TODO: state the shape the backend is expected to serve. `unknown`",
2802
+ "compiles, but leaves the response boundary undescribed and the",
2803
+ "transform with nothing to narrow."
2804
+ ];
2805
+ return [
2806
+ `Raw wire shape of \`${method.toUpperCase()} ${servicePath}\` — the UNWRAPPED`,
2807
+ "`data` payload, since the client strips the `{status, data}` envelope",
2808
+ "before the transform runs.",
2809
+ "",
2810
+ ...shape,
2811
+ "",
2812
+ "Replace with `EndpointResponse<…>` once the endpoint is in the generated",
2813
+ "types."
2814
+ ].join("\n");
2815
+ }
2816
+ function removalSteps(spec) {
2817
+ return [
2818
+ `Regenerate the API types for ${spec.modelProject} (generate_api_types), and confirm ${spec.method.toUpperCase()} ${spec.servicePath} is now in them.`,
2819
+ `In ${spec.servicesFilePath}: delete \` as ApiPath\` from \`${spec.catalogName}.${spec.endpointKey}\`.`,
2820
+ `In ${spec.servicesFilePath}: point the endpoint's \`…Raw\` alias at \`EndpointResponse<typeof ${spec.catalogName}.${spec.endpointKey}>\` and drop the hand-written shape.`,
2821
+ `In the model: swap \`provisionalServiceRequest\` for \`serviceRequest\` and drop its explicit type arguments — the response type comes from the spec again.`,
2822
+ 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.`,
2823
+ `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.`
2824
+ ];
2825
+ }
2826
+ function untypedEndpointError(spec) {
2827
+ const methodsForPath = spec.operations.filter((op) => op.path === spec.servicePath).map((op) => op.method);
2828
+ const pathTypedForOtherMethods = methodsForPath.length > 0;
2829
+ const nearest = [...new Set(spec.operations.map((op) => op.path))].sort(
2830
+ (a, b) => editDistance(a, spec.servicePath) - editDistance(b, spec.servicePath)
2831
+ ).slice(0, NEAREST_SHOWN);
2832
+ const waysForward = [
2833
+ `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.`,
2834
+ `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.`,
2835
+ `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.`
2836
+ ];
2837
+ 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.`;
2838
+ const error = new Error(message);
2839
+ error.details = {
2840
+ servicePath: spec.servicePath,
2841
+ method: spec.method,
2842
+ pathTypedForOtherMethods,
2843
+ methodsForPath,
2844
+ otherEndpointsInThisApi: nearest,
2845
+ 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.",
2846
+ waysForward
2847
+ };
2848
+ return error;
2849
+ }
2575
2850
  function addServiceRequestToModel(codegenFs, options, projects) {
2576
2851
  const logger = getCodegenLogger();
2577
2852
  const { modelFilePath, sourceRoot } = resolveModelFilePath(
@@ -2633,34 +2908,86 @@ function addServiceRequestToModel(codegenFs, options, projects) {
2633
2908
  const mode = options.mode ?? (options.lifecycle ? "lifecycle" : "method");
2634
2909
  const lifecycle = options.lifecycle || "LOAD";
2635
2910
  const catalogName = `${pascalCase(modelBase)}Endpoints`;
2911
+ const mockMode = options.mock ?? "auto";
2912
+ const operations = readServiceModuleOperations(codegenFs, serviceModuleFile);
2913
+ const pathValidated = operations !== null;
2914
+ const operationTyped = operations === null || operations.some(
2915
+ (op) => op.path === options.servicePath && op.method === method
2916
+ );
2917
+ if (!operationTyped && mockMode === "never") {
2918
+ throw untypedEndpointError({
2919
+ servicePath: options.servicePath,
2920
+ method,
2921
+ modelProject: options.modelProject,
2922
+ operations: operations ?? []
2923
+ });
2924
+ }
2925
+ const provisional = !operationTyped;
2926
+ const writeMock = provisional || mockMode === "always";
2927
+ if (provisional) {
2928
+ assertServiceModuleAcceptsProvisional(
2929
+ codegenFs,
2930
+ serviceModuleFile,
2931
+ options.modelProject
2932
+ );
2933
+ }
2636
2934
  logger.info(
2637
- `Adding ${mode}-driven @kosServiceRequest "${options.methodName}" (${method} ${options.servicePath}) to ${options.modelName}`
2935
+ `Adding ${mode}-driven ${provisional ? "PROVISIONAL " : ""}@kosServiceRequest "${options.methodName}" (${method} ${options.servicePath}) to ${options.modelName}`
2638
2936
  );
2937
+ if (provisional) {
2938
+ logger.warn(
2939
+ `${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.`
2940
+ );
2941
+ }
2639
2942
  ensureServicesModule(codegenFs, modelFilePath, modelBase);
2640
2943
  let endpointKey = options.methodName;
2641
2944
  let endpointCreated = true;
2642
2945
  let dataAlias = "";
2643
2946
  let mapperName = "";
2644
2947
  let ctxAlias = "";
2948
+ let rawAlias = "";
2645
2949
  transformSourceFile(codegenFs, servicesFilePath, (sf) => {
2646
2950
  ensureNamedImport(sf, serviceImportFromServices, [{ name: "endpoint" }]);
2951
+ if (provisional) {
2952
+ ensureNamedImport(sf, serviceImportFromServices, [
2953
+ { name: "ApiPath", isTypeOnly: true }
2954
+ ]);
2955
+ }
2956
+ const pathText = provisional ? `${JSON.stringify(options.servicePath)} as ApiPath` : JSON.stringify(options.servicePath);
2647
2957
  const entry = ensureConstCatalogEntry(sf, {
2648
2958
  catalogName,
2649
2959
  key: options.methodName,
2650
- initializer: `endpoint(${JSON.stringify(
2651
- options.servicePath
2652
- )}, ${JSON.stringify(method)})`,
2653
- equals: sameEndpoint
2960
+ initializer: `endpoint(${pathText}, ${JSON.stringify(method)})`,
2961
+ equals: sameEndpoint,
2962
+ leadingTrivia: provisional ? provisionalEntryComment(options.servicePath, method, catalogName) : void 0
2654
2963
  });
2655
2964
  endpointKey = entry.key;
2656
2965
  endpointCreated = entry.created;
2657
2966
  dataAlias = `${pascalCase(endpointKey)}Data`;
2658
2967
  mapperName = `to${pascalCase(endpointKey)}Data`;
2659
2968
  ctxAlias = `${pascalCase(endpointKey)}Ctx`;
2660
- const rawType = `EndpointResponse<typeof ${catalogName}.${endpointKey}>`;
2661
- ensureNamedImport(sf, serviceImportFromServices, [
2662
- { name: "EndpointResponse", isTypeOnly: true }
2663
- ]);
2969
+ rawAlias = `${pascalCase(endpointKey)}Raw`;
2970
+ let rawType;
2971
+ if (provisional) {
2972
+ ensureExportedTypeAlias(sf, {
2973
+ name: rawAlias,
2974
+ type: options.rawType || "unknown",
2975
+ docs: rawTypeDocs(options.servicePath, method, options.rawType)
2976
+ });
2977
+ rawType = rawAlias;
2978
+ } else {
2979
+ rawType = `EndpointResponse<typeof ${catalogName}.${endpointKey}>`;
2980
+ ensureNamedImport(sf, serviceImportFromServices, [
2981
+ { name: "EndpointResponse", isTypeOnly: true }
2982
+ ]);
2983
+ if (writeMock) {
2984
+ ensureExportedTypeAlias(sf, {
2985
+ name: rawAlias,
2986
+ type: rawType,
2987
+ docs: `Raw wire shape of \`${method.toUpperCase()} ${options.servicePath}\`, as the spec declares it — what a mock for this endpoint must serve.`
2988
+ });
2989
+ }
2990
+ }
2664
2991
  ensureExportedTypeAlias(sf, { name: dataAlias, type: rawType });
2665
2992
  ensureExportedMapper(sf, {
2666
2993
  name: mapperName,
@@ -2681,20 +3008,25 @@ function addServiceRequestToModel(codegenFs, options, projects) {
2681
3008
  const className = getModelClass(sf).getName();
2682
3009
  if (!className) throw new Error("Model class has no name.");
2683
3010
  const typeParams = getModelClass(sf).getTypeParameters().map((tp) => tp.getText());
2684
- ensureNamedImport(sf, serviceImportFromModel, [{ name: "serviceRequest" }]);
3011
+ const decoratorName = provisional ? "provisionalServiceRequest" : "serviceRequest";
3012
+ const decoratorTypeArgs = provisional ? [rawAlias, dataAlias] : void 0;
3013
+ ensureNamedImport(sf, serviceImportFromModel, [{ name: decoratorName }]);
2685
3014
  if (mode === "lifecycle") {
2686
3015
  ensureNamedImport(sf, "./services", [
2687
3016
  { name: catalogName },
2688
3017
  { name: mapperName },
2689
- { name: dataAlias, isTypeOnly: true }
3018
+ { name: dataAlias, isTypeOnly: true },
3019
+ ...provisional ? [{ name: rawAlias, isTypeOnly: true }] : []
2690
3020
  ]);
2691
3021
  ensureNamedImport(sf, resolveSdkModuleSpecifier(sf), [
2692
3022
  { name: "DependencyLifecycle" }
2693
3023
  ]);
2694
3024
  addDecoratedMethod(getModelClass(sf), {
2695
3025
  name: options.methodName,
2696
- decoratorName: "serviceRequest",
3026
+ decoratorName,
3027
+ decoratorTypeArgs,
2697
3028
  decoratorArgsText: `${catalogName}.${endpointKey}, { lifecycle: DependencyLifecycle.${lifecycle}, transform: ${mapperName} }`,
3029
+ docs: provisional ? provisionalMethodDocs(options.servicePath, method) : void 0,
2698
3030
  // The manager invokes phase handlers error-first, passing (null, data)
2699
3031
  // on success. The `error` half is only ever reached because
2700
3032
  // `serviceRequest` supplies an errorHandler — the bare decorator
@@ -2711,7 +3043,11 @@ function addServiceRequestToModel(codegenFs, options, projects) {
2711
3043
  ensureNamedImport(sf, "./services", [
2712
3044
  { name: catalogName },
2713
3045
  { name: mapperName },
2714
- { name: ctxAlias, isTypeOnly: true }
3046
+ { name: ctxAlias, isTypeOnly: true },
3047
+ ...provisional ? [
3048
+ { name: rawAlias, isTypeOnly: true },
3049
+ { name: dataAlias, isTypeOnly: true }
3050
+ ] : []
2715
3051
  ]);
2716
3052
  ensureNamedImport(sf, resolveSdkModuleSpecifier(sf), [
2717
3053
  { name: "executeServiceRequest" },
@@ -2726,8 +3062,10 @@ function addServiceRequestToModel(codegenFs, options, projects) {
2726
3062
  addClassDecorator(getModelClass(sf), "kosLoggerAware", { argsText: "" });
2727
3063
  addDecoratedMethod(getModelClass(sf), {
2728
3064
  name: options.methodName,
2729
- decoratorName: "serviceRequest",
3065
+ decoratorName,
3066
+ decoratorTypeArgs,
2730
3067
  decoratorArgsText: `${catalogName}.${endpointKey}, { transform: ${mapperName} }`,
3068
+ docs: provisional ? provisionalMethodDocs(options.servicePath, method) : void 0,
2731
3069
  // Optional: the framework appends the context, callers never pass it.
2732
3070
  parameters: [{ name: "$ctx", type: ctxAlias, optional: true }],
2733
3071
  isAsync: true,
@@ -2739,13 +3077,43 @@ function addServiceRequestToModel(codegenFs, options, projects) {
2739
3077
  ].join("\n")
2740
3078
  });
2741
3079
  });
3080
+ const mock = writeMock ? ensureMockRoute(codegenFs, {
3081
+ modelFilePath,
3082
+ modelBase,
3083
+ sdkModuleSpecifier: readSourceFile(
3084
+ codegenFs,
3085
+ modelFilePath,
3086
+ resolveSdkModuleSpecifier
3087
+ ),
3088
+ rawAlias,
3089
+ servicePath: options.servicePath,
3090
+ method,
3091
+ endpointKey,
3092
+ sampleText: options.sample
3093
+ }) : void 0;
2742
3094
  return {
2743
3095
  modelFilePath,
2744
3096
  servicesFilePath,
2745
3097
  serviceModule: serviceImportFromModel,
2746
3098
  mode,
2747
3099
  endpointKey,
2748
- endpointCreated
3100
+ endpointCreated,
3101
+ pathValidated,
3102
+ provisional,
3103
+ mocksFilePath: mock?.mocksFilePath,
3104
+ mockRegisterFunction: mock?.registerFunction,
3105
+ mockRoute: mock && `${method.toUpperCase()} ${mock.routePattern}`,
3106
+ mockSamplePlaceholder: mock?.samplePlaceholder,
3107
+ removalSteps: provisional ? removalSteps({
3108
+ modelProject: options.modelProject,
3109
+ servicePath: options.servicePath,
3110
+ method,
3111
+ catalogName,
3112
+ endpointKey,
3113
+ servicesFilePath,
3114
+ mocksFilePath: mock?.mocksFilePath,
3115
+ mapperName
3116
+ }) : void 0
2749
3117
  };
2750
3118
  }
2751
3119
  function modelElementType(typeText) {