@techspokes/typescript-wsdl-client 0.7.15 → 0.8.14

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.
Files changed (60) hide show
  1. package/README.md +1504 -263
  2. package/dist/cli.js +423 -270
  3. package/dist/{emit/clientEmitter.d.ts → client/generateClient.d.ts} +2 -2
  4. package/dist/client/generateClient.d.ts.map +1 -0
  5. package/dist/{emit/clientEmitter.js → client/generateClient.js} +5 -5
  6. package/dist/{emit/typesEmitter.d.ts → client/generateTypes.d.ts} +4 -4
  7. package/dist/client/generateTypes.d.ts.map +1 -0
  8. package/dist/{emit/typesEmitter.js → client/generateTypes.js} +6 -6
  9. package/dist/{emit/utilsEmitter.d.ts → client/generateUtils.d.ts} +4 -4
  10. package/dist/client/generateUtils.d.ts.map +1 -0
  11. package/dist/{emit/utilsEmitter.js → client/generateUtils.js} +7 -7
  12. package/dist/{emit/catalogEmitter.d.ts → compiler/generateCatalog.d.ts} +4 -4
  13. package/dist/compiler/generateCatalog.d.ts.map +1 -0
  14. package/dist/{emit/catalogEmitter.js → compiler/generateCatalog.js} +5 -5
  15. package/dist/compiler/schemaCompiler.d.ts +1 -1
  16. package/dist/compiler/schemaCompiler.js +1 -1
  17. package/dist/config.d.ts +13 -0
  18. package/dist/config.d.ts.map +1 -1
  19. package/dist/config.js +17 -0
  20. package/dist/gateway/generateGateway.d.ts +73 -0
  21. package/dist/gateway/generateGateway.d.ts.map +1 -0
  22. package/dist/gateway/generateGateway.js +135 -0
  23. package/dist/gateway/generators.d.ts +90 -0
  24. package/dist/gateway/generators.d.ts.map +1 -0
  25. package/dist/gateway/generators.js +270 -0
  26. package/dist/gateway/helpers.d.ts +115 -0
  27. package/dist/gateway/helpers.d.ts.map +1 -0
  28. package/dist/gateway/helpers.js +224 -0
  29. package/dist/index.d.ts +1 -0
  30. package/dist/index.d.ts.map +1 -1
  31. package/dist/index.js +18 -18
  32. package/dist/loader/wsdlLoader.d.ts.map +1 -1
  33. package/dist/loader/wsdlLoader.js +1 -3
  34. package/dist/openapi/generateOpenAPI.d.ts +25 -1
  35. package/dist/openapi/generateOpenAPI.d.ts.map +1 -1
  36. package/dist/openapi/generateOpenAPI.js +28 -27
  37. package/dist/openapi/{buildPaths.d.ts → generatePaths.d.ts} +6 -6
  38. package/dist/openapi/generatePaths.d.ts.map +1 -0
  39. package/dist/openapi/{buildPaths.js → generatePaths.js} +1 -1
  40. package/dist/openapi/{buildSchemas.d.ts → generateSchemas.d.ts} +10 -10
  41. package/dist/openapi/generateSchemas.d.ts.map +1 -0
  42. package/dist/openapi/{buildSchemas.js → generateSchemas.js} +5 -5
  43. package/dist/openapi/security.d.ts.map +1 -1
  44. package/dist/openapi/security.js +2 -1
  45. package/dist/pipeline.d.ts +21 -7
  46. package/dist/pipeline.d.ts.map +1 -1
  47. package/dist/pipeline.js +66 -32
  48. package/dist/util/builder.d.ts +25 -0
  49. package/dist/util/builder.d.ts.map +1 -0
  50. package/dist/util/builder.js +52 -0
  51. package/dist/util/cli.d.ts +106 -0
  52. package/dist/util/cli.d.ts.map +1 -0
  53. package/dist/util/cli.js +164 -0
  54. package/package.json +11 -8
  55. package/dist/emit/catalogEmitter.d.ts.map +0 -1
  56. package/dist/emit/clientEmitter.d.ts.map +0 -1
  57. package/dist/emit/typesEmitter.d.ts.map +0 -1
  58. package/dist/emit/utilsEmitter.d.ts.map +0 -1
  59. package/dist/openapi/buildPaths.d.ts.map +0 -1
  60. package/dist/openapi/buildSchemas.d.ts.map +0 -1
package/dist/cli.js CHANGED
@@ -1,16 +1,13 @@
1
1
  #!/usr/bin/env node
2
+ // noinspection RequiredAttributes,XmlDeprecatedElement,HtmlDeprecatedTag
2
3
  /**
3
4
  * CLI Entry Point for TypeScript WSDL Client Generator
4
5
  *
5
6
  * This file implements the command-line interface for the wsdl-tsc tool, which generates
6
- * fully-typed TypeScript SOAP clients from WSDL/XSD schemas. The CLI supports two main commands:
7
- *
8
- * 1. Default command (wsdl-tsc): Generates TypeScript client code from WSDL
9
- * 2. OpenAPI subcommand (wsdl-tsc openapi): Generates OpenAPI 3.1 specifications from WSDL
10
- *
11
- * The CLI is built using yargs for argument parsing and provides extensive options for
12
- * customizing the code generation process, including TypeScript output configurations and
13
- * OpenAPI specification details.
7
+ * fully-typed TypeScript SOAP clients from WSDL/XSD schemas. The CLI supports subcommands for
8
+ * client generation, OpenAPI generation, full pipeline, and gateway generation. The bare
9
+ * invocation (no subcommand) is kept for backward compatibility and behaves like the client
10
+ * command with legacy flags.
14
11
  */
15
12
  import yargs from "yargs/yargs";
16
13
  import { hideBin } from "yargs/helpers";
@@ -18,14 +15,174 @@ import fs from "node:fs";
18
15
  import path from "node:path";
19
16
  import { loadWsdl } from "./loader/wsdlLoader.js";
20
17
  import { compileCatalog } from "./compiler/schemaCompiler.js";
21
- import { emitTypes } from "./emit/typesEmitter.js";
22
- import { emitUtils } from "./emit/utilsEmitter.js";
23
- import { emitCatalog } from "./emit/catalogEmitter.js";
24
- import { emitClient } from "./emit/clientEmitter.js";
18
+ import { generateTypes } from "./client/generateTypes.js";
19
+ import { generateUtils } from "./client/generateUtils.js";
20
+ import { generateCatalog } from "./compiler/generateCatalog.js";
21
+ import { generateClient } from "./client/generateClient.js";
25
22
  import { generateOpenAPI } from "./openapi/generateOpenAPI.js";
26
23
  import { runGenerationPipeline } from "./pipeline.js";
24
+ import { resolveCompilerOptions } from "./config.js";
25
+ import { emitClientArtifacts, handleCLIError, parseServers, parseStatusCodes, reportCompilationStats, reportOpenApiSuccess, success, validateGatewayRequirements, } from "./util/cli.js";
26
+ import { buildCompilerOptionsFromArgv, buildOpenApiOptionsFromArgv } from "./util/builder.js";
27
27
  // Process command line arguments, removing the first two elements (node executable and script path)
28
28
  const rawArgs = hideBin(process.argv);
29
+ // ---------------------------------------------------------------------------
30
+ // Show help if no subcommand provided
31
+ // ---------------------------------------------------------------------------
32
+ if (!rawArgs[0] || !["compile", "client", "openapi", "gateway", "pipeline"].includes(rawArgs[0])) {
33
+ await yargs(rawArgs)
34
+ .version(false)
35
+ .scriptName("wsdl-tsc")
36
+ .usage("$0 <command> [options]")
37
+ .command("compile", "Compile WSDL to catalog.json")
38
+ .command("client", "Generate TypeScript SOAP client from WSDL or catalog")
39
+ .command("openapi", "Generate OpenAPI specification from WSDL or catalog")
40
+ .command("gateway", "Generate Fastify gateway from OpenAPI specification")
41
+ .command("pipeline", "Run full generation pipeline (client + OpenAPI + gateway)")
42
+ .demandCommand(1, "Please specify a command: compile, client, openapi, gateway, or pipeline")
43
+ .strict()
44
+ .help()
45
+ .parse();
46
+ process.exit(1);
47
+ }
48
+ // ---------------------------------------------------------------------------
49
+ // Compile subcommand - Parse WSDL and generate catalog
50
+ // ---------------------------------------------------------------------------
51
+ if (rawArgs[0] === "compile") {
52
+ const compileArgv = await yargs(rawArgs.slice(1))
53
+ .version(false)
54
+ .scriptName("wsdl-tsc compile")
55
+ .usage("$0 --wsdl-source <file|url> --catalog-file <path> [options]")
56
+ .option("wsdl-source", { type: "string", demandOption: true, desc: "Path or URL to the WSDL" })
57
+ .option("catalog-file", { type: "string", demandOption: true, desc: "Output path for catalog.json" })
58
+ // Compiler flags
59
+ .option("import-extensions", {
60
+ type: "string",
61
+ choices: ["js", "ts", "bare"],
62
+ default: "js",
63
+ desc: "Intra-generated import specifiers: 'js', 'ts', or 'bare'.",
64
+ })
65
+ .option("client-attributes-key", { type: "string", default: "$attributes" })
66
+ .option("client-class-name", { type: "string" })
67
+ .option("client-int64-as", { type: "string", choices: ["string", "number", "bigint"], default: "string" })
68
+ .option("client-bigint-as", { type: "string", choices: ["string", "number"], default: "string" })
69
+ .option("client-decimal-as", { type: "string", choices: ["string", "number"], default: "string" })
70
+ .option("client-date-as", { type: "string", choices: ["string", "Date"], default: "string" })
71
+ .option("client-choice-mode", { type: "string", choices: ["all-optional", "union"], default: "all-optional" })
72
+ .option("client-fail-on-unresolved", { type: "boolean", default: false })
73
+ .option("client-nillable-as-optional", { type: "boolean", default: false })
74
+ .strict()
75
+ .help()
76
+ .parse();
77
+ const catalogOut = path.resolve(String(compileArgv["catalog-file"]));
78
+ // Load WSDL
79
+ const wsdlCatalog = await loadWsdl(String(compileArgv["wsdl-source"]));
80
+ // Build compiler options using shared resolver and builder
81
+ const compilerOptions = resolveCompilerOptions({
82
+ ...buildCompilerOptionsFromArgv(compileArgv),
83
+ catalog: true, // Always emit catalog for compile subcommand
84
+ }, {
85
+ wsdl: String(compileArgv["wsdl-source"]),
86
+ out: path.dirname(catalogOut),
87
+ });
88
+ const compiled = compileCatalog(wsdlCatalog, compilerOptions);
89
+ // Report compilation statistics
90
+ reportCompilationStats(wsdlCatalog, compiled);
91
+ // Ensure output directory exists
92
+ fs.mkdirSync(path.dirname(catalogOut), { recursive: true });
93
+ // Emit catalog
94
+ generateCatalog(catalogOut, compiled);
95
+ success(`Compiled catalog written to ${catalogOut}`);
96
+ process.exit(0);
97
+ }
98
+ // ---------------------------------------------------------------------------
99
+ // Client generation subcommand
100
+ // ---------------------------------------------------------------------------
101
+ if (rawArgs[0] === "client") {
102
+ const clientArgv = await yargs(rawArgs.slice(1))
103
+ .version(false)
104
+ .scriptName("wsdl-tsc client")
105
+ .usage("$0 [--wsdl-source <file|url> | --catalog-file <file>] --client-dir <dir> [--catalog-file <path>] [options]")
106
+ .option("wsdl-source", {
107
+ type: "string",
108
+ desc: "Path or URL to the WSDL (exclusive with --catalog-file when used as input)"
109
+ })
110
+ .option("catalog-file", {
111
+ type: "string",
112
+ desc: "Existing compiled catalog.json (for input), or output path when compiling from WSDL (default: tmp/catalog.json)"
113
+ })
114
+ .option("client-dir", {
115
+ type: "string",
116
+ demandOption: true,
117
+ desc: "Directory for generated client (client.ts, types.ts, utils.ts)"
118
+ })
119
+ // Compiler flags
120
+ .option("import-extensions", {
121
+ type: "string",
122
+ choices: ["js", "ts", "bare"],
123
+ default: "js",
124
+ desc: "Intra-generated import specifiers: 'js', 'ts', or 'bare'.",
125
+ })
126
+ .option("client-attributes-key", { type: "string", default: "$attributes" })
127
+ .option("client-class-name", { type: "string" })
128
+ .option("client-int64-as", { type: "string", choices: ["string", "number", "bigint"], default: "string" })
129
+ .option("client-bigint-as", { type: "string", choices: ["string", "number"], default: "string" })
130
+ .option("client-decimal-as", { type: "string", choices: ["string", "number"], default: "string" })
131
+ .option("client-date-as", { type: "string", choices: ["string", "Date"], default: "string" })
132
+ .option("client-choice-mode", { type: "string", choices: ["all-optional", "union"], default: "all-optional" })
133
+ .option("client-fail-on-unresolved", { type: "boolean", default: false })
134
+ .option("client-nillable-as-optional", { type: "boolean", default: false })
135
+ .strict()
136
+ .help()
137
+ .parse();
138
+ // Validate mutually exclusive options
139
+ const hasWsdl = !!clientArgv["wsdl-source"];
140
+ const hasCatalog = !!clientArgv["catalog-file"];
141
+ if (!hasWsdl && !hasCatalog) {
142
+ handleCLIError("either --wsdl-source or --catalog-file must be provided for client generation");
143
+ }
144
+ const clientOutDir = path.resolve(String(clientArgv["client-dir"]));
145
+ let compiled;
146
+ let catalogOutPath;
147
+ // Determine if catalog-file is input or output based on wsdl-source presence
148
+ if (hasWsdl && !hasCatalog) {
149
+ // WSDL provided, no catalog-file: default catalog output to client-dir/catalog.json
150
+ catalogOutPath = path.join(clientOutDir, "catalog.json");
151
+ }
152
+ else if (hasWsdl && hasCatalog) {
153
+ // Both provided: catalog-file is output path
154
+ catalogOutPath = path.resolve(String(clientArgv["catalog-file"]));
155
+ }
156
+ else {
157
+ // Only catalog-file provided: it's an input, load it
158
+ const catalogPath = path.resolve(String(clientArgv["catalog-file"]));
159
+ const catalogContent = fs.readFileSync(catalogPath, "utf-8");
160
+ compiled = JSON.parse(catalogContent);
161
+ success(`Loaded catalog from ${catalogPath}`);
162
+ }
163
+ // If we need to compile from WSDL
164
+ if (hasWsdl) {
165
+ const wsdlCatalog = await loadWsdl(String(clientArgv["wsdl-source"]));
166
+ // Build compiler options using shared resolver and builder
167
+ const compilerOptions = resolveCompilerOptions({
168
+ ...buildCompilerOptionsFromArgv(clientArgv),
169
+ catalog: true, // Always generate catalog when compiling from WSDL
170
+ }, {
171
+ wsdl: String(clientArgv["wsdl-source"]),
172
+ out: clientOutDir,
173
+ });
174
+ compiled = compileCatalog(wsdlCatalog, compilerOptions);
175
+ // Report compilation statistics
176
+ reportCompilationStats(wsdlCatalog, compiled);
177
+ // Emit catalog
178
+ fs.mkdirSync(path.dirname(catalogOutPath), { recursive: true });
179
+ generateCatalog(catalogOutPath, compiled);
180
+ success(`Compiled catalog written to ${catalogOutPath}`);
181
+ }
182
+ // Emit client artifacts (excluding catalog since we already emitted it above if needed)
183
+ emitClientArtifacts(clientOutDir, compiled, generateClient, generateTypes, generateUtils);
184
+ process.exit(0);
185
+ }
29
186
  /**
30
187
  * Command handler for "openapi" subcommand
31
188
  *
@@ -36,325 +193,321 @@ if (rawArgs[0] === "openapi") {
36
193
  const openapiArgv = await yargs(rawArgs.slice(1))
37
194
  .version(false)
38
195
  .scriptName("wsdl-tsc openapi")
39
- .usage("$0 --wsdl <file|url> --out <openapi.(json|yaml)> [options]")
40
- .option("wsdl", { type: "string", desc: "Path or URL to the WSDL (exclusive with --catalog)" })
41
- .option("catalog", { type: "string", desc: "Existing compiled catalog.json (exclusive with --wsdl)" })
42
- .option("out", { type: "string", demandOption: true, desc: "Output path (base or with extension)" })
43
- .option("format", {
196
+ .usage("$0 [--wsdl-source <file|url> | --catalog-file <file>] --openapi-file <path> [options]")
197
+ .option("wsdl-source", { type: "string", desc: "Path or URL to the WSDL (exclusive with --catalog-file)" })
198
+ .option("catalog-file", { type: "string", desc: "Existing compiled catalog.json (default: tmp/catalog.json if --wsdl-source not provided)" })
199
+ .option("openapi-file", { type: "string", demandOption: true, desc: "Output file or base path for OpenAPI" })
200
+ .option("openapi-format", {
44
201
  type: "string",
45
202
  choices: ["json", "yaml", "both"],
46
203
  desc: "Output format: json|yaml|both (default json)"
47
204
  })
48
- .option("yaml", { type: "boolean", default: false, desc: "[DEPRECATED] Use --format yaml or both" })
49
- .option("title", { type: "string", desc: "API title (defaults to derived service name)" })
50
- .option("version", { type: "string", desc: "API version for info.version (default 0.0.0)" })
51
- .option("servers", { type: "string", desc: "Comma-separated server URLs" })
52
- .option("basePath", { type: "string", desc: "Base path prefix added before operation segments (e.g. /v1/soap)" })
53
- .option("pathStyle", {
205
+ .option("openapi-title", { type: "string", desc: "API title (defaults to derived service name)" })
206
+ .option("openapi-version-tag", {
207
+ type: "string",
208
+ desc: "API version tag for info.version (e.g. 1.0.2; default 0.0.0)"
209
+ })
210
+ .option("openapi-servers", { type: "string", desc: "Comma-separated server URLs" })
211
+ .option("openapi-base-path", {
212
+ type: "string",
213
+ desc: "Base path prefix added before operation segments (e.g. /v1/soap)"
214
+ })
215
+ .option("openapi-path-style", {
54
216
  type: "string",
55
217
  choices: ["kebab", "asis", "lower"],
56
218
  default: "kebab",
57
219
  desc: "Path segment style applied to operation names"
58
220
  })
59
- .option("method", {
221
+ .option("openapi-default-method", {
60
222
  type: "string",
61
223
  choices: ["post", "get", "put", "patch", "delete"],
62
224
  default: "post",
63
- desc: "Default HTTP method for all operations (can be overridden via --ops)"
225
+ desc: "Default HTTP method for all operations (can be overridden via --openapi-ops-file)"
64
226
  })
65
- .option("security", { type: "string", desc: "Path to security.json configuration" })
66
- .option("tags", { type: "string", desc: "Path to tags.json mapping operation name -> tag" })
67
- .option("ops", {
227
+ .option("openapi-security-file", { type: "string", desc: "Path to security.json configuration" })
228
+ .option("openapi-tags-file", { type: "string", desc: "Path to tags.json mapping operation name -> tag" })
229
+ .option("openapi-ops-file", {
68
230
  type: "string",
69
231
  desc: "Path to ops.json per-operation overrides (method, deprecated, summary, description)"
70
232
  })
71
- .option("closedSchemas", {
233
+ .option("openapi-closed-schemas", {
72
234
  type: "boolean",
73
235
  default: false,
74
236
  desc: "Emit additionalProperties:false for object schemas"
75
237
  })
76
- .option("pruneUnusedSchemas", {
238
+ .option("openapi-prune-unused-schemas", {
77
239
  type: "boolean",
78
240
  default: false,
79
241
  desc: "Emit only schemas reachable from operations"
80
242
  })
81
- .option("validate", { type: "boolean", default: true, desc: "Validate generated document (always on unless false)" })
82
- .option("no-validate", { type: "boolean", default: false, desc: "Alias for --validate=false" })
83
- .option("tag-style", {
243
+ .option("openapi-tag-style", {
84
244
  type: "string",
85
245
  choices: ["default", "first", "service"],
86
246
  default: "default",
87
- desc: "Heuristic for inferring tags when no --tags map provided"
247
+ desc: "Heuristic for inferring tags when no map is provided"
88
248
  })
89
- // Standard envelope feature flags
90
- .option("envelope-namespace", {
249
+ .option("openapi-envelope-namespace", {
91
250
  type: "string",
92
- desc: "Override the standard response envelope base name segment (default <ServiceName>ResponseEnvelope or provided segment alone)"
251
+ desc: "Override the standard response envelope base name segment"
93
252
  })
94
- .option("error-namespace", {
253
+ .option("openapi-error-namespace", {
95
254
  type: "string",
96
- desc: "Override the standard error object schema name segment (default <ServiceName>ErrorObject or provided segment alone)"
255
+ desc: "Override the standard error object schema name segment"
97
256
  })
98
257
  .strict()
99
258
  .help()
100
259
  .parse();
101
- // Show deprecation warning for the legacy --yaml option
102
- if (openapiArgv.yaml && !openapiArgv.format) {
103
- console.warn("[deprecation] --yaml is deprecated; use --format yaml or --format both");
104
- }
105
- // Handle the no-validate flag which overrides the validate option
106
- if (openapiArgv["no-validate"]) {
107
- openapiArgv.validate = false;
260
+ // Resolve format
261
+ const format = openapiArgv["openapi-format"] ?? "json";
262
+ // Default to {openapi-file-dir}/catalog.json if neither wsdl-source nor catalog-file provided
263
+ if (!openapiArgv["wsdl-source"] && !openapiArgv["catalog-file"]) {
264
+ const openapiFileArg = String(openapiArgv["openapi-file"]);
265
+ const openapiDir = path.dirname(path.resolve(openapiFileArg));
266
+ openapiArgv["catalog-file"] = path.join(openapiDir, "catalog.json");
108
267
  }
109
- // Validate that either --wsdl or --catalog is provided, but not both
110
- if (!openapiArgv.wsdl && !openapiArgv.catalog) {
111
- console.error("Error: either --wsdl or --catalog must be provided for openapi generation.");
112
- process.exit(1);
268
+ if (openapiArgv["wsdl-source"] && openapiArgv["catalog-file"]) {
269
+ handleCLIError("provide only one of --wsdl-source or --catalog-file, not both");
113
270
  }
114
- if (openapiArgv.wsdl && openapiArgv.catalog) {
115
- console.error("Error: provide only one of --wsdl or --catalog, not both.");
116
- process.exit(1);
271
+ const servers = parseServers(openapiArgv["openapi-servers"]);
272
+ const outBase = path.resolve(String(openapiArgv["openapi-file"]));
273
+ const openApiOptions = buildOpenApiOptionsFromArgv(openapiArgv, format, servers);
274
+ const result = await generateOpenAPI({
275
+ ...openApiOptions,
276
+ catalogFile: openapiArgv["catalog-file"],
277
+ outFile: outBase,
278
+ wsdl: openapiArgv["wsdl-source"],
279
+ });
280
+ // Report success
281
+ reportOpenApiSuccess(result);
282
+ process.exit(0);
283
+ }
284
+ /**
285
+ * Command handler for "gateway" subcommand
286
+ *
287
+ * This branch handles the Fastify gateway generation mode, which creates production-ready
288
+ * Fastify route scaffolding and JSON Schema validation from OpenAPI 3.1 specifications.
289
+ */
290
+ if (rawArgs[0] === "gateway") {
291
+ const { generateGateway } = await import("./gateway/generateGateway.js");
292
+ const gatewayArgv = await yargs(rawArgs.slice(1))
293
+ .version(false)
294
+ .scriptName("wsdl-tsc gateway")
295
+ .usage("$0 --openapi-file <file> --client-dir <dir> --gateway-dir <dir> --gateway-service-name <slug> --gateway-version-prefix <slug> [options]")
296
+ .option("openapi-file", {
297
+ type: "string",
298
+ demandOption: true,
299
+ desc: "Path to OpenAPI 3.1 JSON/YAML file"
300
+ })
301
+ .option("client-dir", {
302
+ type: "string",
303
+ demandOption: true,
304
+ desc: "Path to client directory (where client.ts is located)"
305
+ })
306
+ .option("gateway-dir", {
307
+ type: "string",
308
+ demandOption: true,
309
+ desc: "Output directory for gateway code"
310
+ })
311
+ .option("gateway-version-prefix", {
312
+ type: "string",
313
+ demandOption: true,
314
+ desc: "Version prefix for URN generation (e.g. v1, v2, urn:1.0.2:schema)"
315
+ })
316
+ .option("gateway-service-name", {
317
+ type: "string",
318
+ demandOption: true,
319
+ desc: "Service identifier for URN generation"
320
+ })
321
+ .option("import-extensions", {
322
+ type: "string",
323
+ choices: ["js", "ts", "bare"],
324
+ default: "js",
325
+ desc: "Import-extension mode for generated TypeScript modules",
326
+ })
327
+ .option("gateway-default-status-codes", {
328
+ type: "string",
329
+ desc: "Comma-separated status codes to backfill with default response (default: 200,400,401,403,404,409,422,429,500,502,503,504)"
330
+ })
331
+ .strict()
332
+ .help()
333
+ .parse();
334
+ // Parse default response status codes
335
+ let defaultResponseStatusCodes;
336
+ if (gatewayArgv["gateway-default-status-codes"]) {
337
+ try {
338
+ defaultResponseStatusCodes = parseStatusCodes(String(gatewayArgv["gateway-default-status-codes"]), "--gateway-default-status-codes");
339
+ }
340
+ catch (err) {
341
+ handleCLIError(err);
342
+ }
117
343
  }
118
- // Parse server URLs from comma-separated string
119
- const servers = (openapiArgv.servers ? String(openapiArgv.servers).split(",").map(s => s.trim()).filter(Boolean) : []);
120
- // Determine output format, with backwards compatibility for the --yaml flag
121
- const inferredFormat = openapiArgv.format || (openapiArgv.yaml ? "yaml" : undefined);
122
- // Call the OpenAPI generator with the parsed options
123
- await generateOpenAPI({
124
- wsdl: openapiArgv.wsdl,
125
- catalogFile: openapiArgv.catalog,
126
- outFile: openapiArgv.out,
127
- title: openapiArgv.title,
128
- version: openapiArgv.version,
129
- servers,
130
- basePath: openapiArgv.basePath,
131
- pathStyle: openapiArgv.pathStyle,
132
- defaultMethod: openapiArgv.method,
133
- securityConfigFile: openapiArgv.security,
134
- tagsFile: openapiArgv.tags,
135
- opsFile: openapiArgv.ops,
136
- closedSchemas: openapiArgv.closedSchemas,
137
- pruneUnusedSchemas: openapiArgv.pruneUnusedSchemas,
138
- format: inferredFormat,
139
- skipValidate: openapiArgv.validate === false,
140
- tagStyle: openapiArgv["tag-style"],
141
- envelopeNamespace: openapiArgv["envelope-namespace"],
142
- errorNamespace: openapiArgv["error-namespace"],
344
+ const outDir = path.resolve(gatewayArgv["gateway-dir"]);
345
+ const clientDir = path.resolve(gatewayArgv["client-dir"]);
346
+ await generateGateway({
347
+ openapiFile: gatewayArgv["openapi-file"],
348
+ outDir,
349
+ clientDir,
350
+ versionSlug: gatewayArgv["gateway-version-prefix"],
351
+ serviceSlug: gatewayArgv["gateway-service-name"],
352
+ defaultResponseStatusCodes,
353
+ imports: gatewayArgv["import-extensions"],
143
354
  });
144
- console.log(`✅ OpenAPI generation complete (${inferredFormat || 'json'})`);
355
+ success(`Gateway code generated in ${outDir}`);
145
356
  process.exit(0);
146
357
  }
147
358
  if (rawArgs[0] === "pipeline") {
148
359
  const pipelineArgv = await yargs(rawArgs.slice(1))
149
360
  .version(false)
150
361
  .scriptName("wsdl-tsc pipeline")
151
- .usage("$0 --wsdl <file|url> --out <dir> [options]")
152
- .option("wsdl", { type: "string", demandOption: true, desc: "Path or URL to the WSDL" })
153
- .option("out", { type: "string", demandOption: true, desc: "Output directory for TypeScript artifacts" })
362
+ .usage("$0 --wsdl-source <file|url> [--client-dir <dir>] [--openapi-file <path>] [--gateway-dir <dir> --gateway-service-name <slug> --gateway-version-prefix <slug>] [--catalog-file <path>] [options]")
363
+ .option("wsdl-source", { type: "string", demandOption: true, desc: "Path or URL to the WSDL" })
364
+ // Per-artifact outputs
365
+ .option("client-dir", {
366
+ type: "string",
367
+ desc: "Output directory for generated client (client.ts, types.ts, utils.ts)"
368
+ })
369
+ .option("openapi-file", {
370
+ type: "string",
371
+ desc: "Output base or file for OpenAPI (enables OpenAPI generation when provided)"
372
+ })
373
+ .option("gateway-dir", {
374
+ type: "string",
375
+ desc: "Output directory for gateway code (enables gateway generation when provided)"
376
+ })
377
+ .option("catalog-file", {
378
+ type: "string",
379
+ desc: "Output path for catalog.json (default: tmp/catalog.json)"
380
+ })
154
381
  .option("clean", {
155
382
  type: "boolean",
156
383
  default: false,
157
- desc: "Remove existing contents of --out before generation (safety: will refuse if --out is project root)"
384
+ desc: "Remove existing contents of the client output directory before generation (safety: will refuse if it resolves to project root)"
158
385
  })
159
386
  // Compiler flags
160
- .option("imports", { type: "string", choices: ["js", "ts", "bare"], default: "js" })
161
- .option("catalog", { type: "boolean", default: true })
162
- .option("attributes-key", { type: "string", default: "$attributes" })
163
- .option("int64-as", { type: "string", choices: ["string", "number", "bigint"], default: "string" })
164
- .option("bigint-as", { type: "string", choices: ["string", "number"], default: "string" })
165
- .option("decimal-as", { type: "string", choices: ["string", "number"], default: "string" })
166
- .option("date-as", { type: "string", choices: ["string", "Date"], default: "string" })
167
- .option("choice", { type: "string", choices: ["all-optional", "union"], default: "all-optional" })
168
- .option("fail-on-unresolved", { type: "boolean", default: false })
169
- .option("nillable-as-optional", { type: "boolean", default: false })
387
+ .option("import-extensions", { type: "string", choices: ["js", "ts", "bare"], default: "js" })
388
+ .option("client-attributes-key", { type: "string", default: "$attributes" })
389
+ .option("client-class-name", { type: "string" })
390
+ .option("client-bigint-as", { type: "string", choices: ["string", "number"], default: "string" })
391
+ .option("client-choice-mode", { type: "string", choices: ["all-optional", "union"], default: "all-optional" })
392
+ .option("client-date-as", { type: "string", choices: ["string", "Date"], default: "string" })
393
+ .option("client-decimal-as", { type: "string", choices: ["string", "number"], default: "string" })
394
+ .option("client-fail-on-unresolved", { type: "boolean", default: false })
395
+ .option("client-int64-as", { type: "string", choices: ["string", "number", "bigint"], default: "string" })
396
+ .option("client-nillable-as-optional", { type: "boolean", default: false })
170
397
  // OpenAPI flags
171
- .option("openapi-out", { type: "string", desc: "Output base or file for OpenAPI (if omitted chooses openapi.json)" })
172
- .option("format", { type: "string", choices: ["json", "yaml", "both"], desc: "OpenAPI output format (default json)" })
173
- .option("yaml", { type: "boolean", default: false, desc: "[DEPRECATED] Use --format yaml or both" })
174
- .option("validate", { type: "boolean", default: true, desc: "Validate OpenAPI output" })
175
- .option("no-validate", { type: "boolean", default: false, desc: "Alias for --validate=false" })
176
- .option("tag-style", { type: "string", choices: ["default", "first", "service"], default: "default" })
177
- .option("servers", { type: "string" })
178
- .option("basePath", { type: "string" })
179
- .option("pathStyle", { type: "string", choices: ["kebab", "asis", "lower"], default: "kebab" })
180
- .option("method", { type: "string", choices: ["post", "get", "put", "patch", "delete"], default: "post" })
181
- .option("security", { type: "string" })
182
- .option("tags", { type: "string" })
183
- .option("ops", { type: "string" })
184
- .option("closedSchemas", { type: "boolean", default: false })
185
- .option("pruneUnusedSchemas", { type: "boolean", default: false })
186
- // Envelope feature flags
187
- .option("envelope-namespace", { type: "string" })
188
- .option("error-namespace", { type: "string" })
398
+ .option("openapi-format", {
399
+ type: "string",
400
+ choices: ["json", "yaml", "both"],
401
+ desc: "OpenAPI output format (default json)"
402
+ })
403
+ .option("openapi-title", { type: "string" })
404
+ .option("openapi-version-tag", { type: "string" })
405
+ .option("openapi-tag-style", { type: "string", choices: ["default", "first", "service"], default: "default" })
406
+ .option("openapi-servers", { type: "string" })
407
+ .option("openapi-base-path", { type: "string" })
408
+ .option("openapi-path-style", { type: "string", choices: ["kebab", "asis", "lower"], default: "kebab" })
409
+ .option("openapi-default-method", {
410
+ type: "string",
411
+ choices: ["post", "get", "put", "patch", "delete"],
412
+ default: "post"
413
+ })
414
+ .option("openapi-security-file", { type: "string" })
415
+ .option("openapi-tags-file", { type: "string" })
416
+ .option("openapi-ops-file", { type: "string" })
417
+ .option("openapi-closed-schemas", { type: "boolean", default: false })
418
+ .option("openapi-prune-unused-schemas", { type: "boolean", default: false })
419
+ .option("openapi-envelope-namespace", { type: "string" })
420
+ .option("openapi-error-namespace", { type: "string" })
421
+ // Gateway flags
422
+ .option("gateway-service-name", {
423
+ type: "string",
424
+ desc: "Service name for gateway URN generation (required when --gateway-dir is provided)"
425
+ })
426
+ .option("gateway-version-prefix", {
427
+ type: "string",
428
+ desc: "Version prefix for gateway URN generation (required when --gateway-dir is provided)"
429
+ })
430
+ .option("gateway-default-status-codes", {
431
+ type: "string",
432
+ desc: "Comma-separated status codes for gateway (default: 200,400,401,403,404,409,422,429,500,502,503,504)"
433
+ })
189
434
  .strict()
190
435
  .help()
191
436
  .parse();
192
- if (pipelineArgv.yaml && !pipelineArgv.format) {
193
- console.warn("[deprecation] --yaml is deprecated; use --format yaml or --format both");
437
+ const format = pipelineArgv["openapi-format"] ?? "json";
438
+ const clientOut = pipelineArgv["client-dir"];
439
+ const openapiOut = pipelineArgv["openapi-file"];
440
+ const gatewayOut = pipelineArgv["gateway-dir"];
441
+ const catalogOutArg = pipelineArgv["catalog-file"];
442
+ if (!clientOut && !openapiOut && !gatewayOut) {
443
+ handleCLIError("At least one of --catalog-file, --client-dir, --openapi-file, or --gateway-dir must be provided for pipeline generation.");
444
+ }
445
+ // Determine catalog output path (always required since we always compile WSDL)
446
+ let catalogOut;
447
+ if (catalogOutArg) {
448
+ catalogOut = path.resolve(catalogOutArg);
449
+ }
450
+ else if (clientOut) {
451
+ // Default to client-dir/catalog.json
452
+ catalogOut = path.join(path.resolve(clientOut), "catalog.json");
453
+ }
454
+ else if (openapiOut) {
455
+ // Default to openapi-file-dir/catalog.json
456
+ const openapiDir = path.dirname(path.resolve(openapiOut));
457
+ catalogOut = path.join(openapiDir, "catalog.json");
194
458
  }
195
- if (pipelineArgv["no-validate"]) {
196
- pipelineArgv.validate = false;
459
+ else if (gatewayOut) {
460
+ // Default to gateway-dir/catalog.json
461
+ catalogOut = path.join(path.resolve(gatewayOut), "catalog.json");
197
462
  }
198
- if (pipelineArgv.clean) {
199
- const resolvedOut = path.resolve(String(pipelineArgv.out));
463
+ else {
464
+ // Fallback to tmp/catalog.json (should rarely happen due to validation above)
465
+ catalogOut = path.resolve("tmp/catalog.json");
466
+ }
467
+ // Handle --clean flag for client output
468
+ if (pipelineArgv.clean && clientOut) {
469
+ const clientOutDir = path.resolve(clientOut);
200
470
  const projectRoot = path.resolve(process.cwd());
201
- if (resolvedOut === projectRoot) {
202
- console.error("Refusing to clean project root. Choose a subdirectory for --out.");
203
- process.exit(2);
471
+ if (clientOutDir === projectRoot) {
472
+ handleCLIError("Refusing to clean project root. Choose a subdirectory for --client-dir.", 2);
204
473
  }
205
- if (fs.existsSync(resolvedOut)) {
206
- fs.rmSync(resolvedOut, { recursive: true, force: true });
474
+ if (fs.existsSync(clientOutDir)) {
475
+ fs.rmSync(clientOutDir, { recursive: true, force: true });
207
476
  }
208
477
  }
209
- const servers = (pipelineArgv.servers ? String(pipelineArgv.servers).split(",").map(s => s.trim()).filter(Boolean) : []);
210
- const format = pipelineArgv.format || (pipelineArgv.yaml ? "yaml" : undefined);
211
- await runGenerationPipeline({
212
- wsdl: pipelineArgv.wsdl,
213
- outDir: pipelineArgv.out,
214
- compiler: {
215
- imports: pipelineArgv.imports,
216
- catalog: pipelineArgv.catalog,
217
- attributesKey: pipelineArgv["attributes-key"],
218
- primitive: {
219
- int64As: pipelineArgv["int64-as"],
220
- bigIntegerAs: pipelineArgv["bigint-as"],
221
- decimalAs: pipelineArgv["decimal-as"],
222
- dateAs: pipelineArgv["date-as"],
223
- },
224
- choice: pipelineArgv.choice,
225
- failOnUnresolved: pipelineArgv["fail-on-unresolved"],
226
- nillableAsOptional: pipelineArgv["nillable-as-optional"],
227
- },
228
- openapi: {
229
- outFile: pipelineArgv["openapi-out"],
230
- format,
231
- skipValidate: pipelineArgv.validate === false,
232
- tagStyle: pipelineArgv["tag-style"],
233
- servers,
234
- basePath: pipelineArgv.basePath,
235
- pathStyle: pipelineArgv.pathStyle,
236
- defaultMethod: pipelineArgv.method,
237
- securityConfigFile: pipelineArgv.security,
238
- tagsFile: pipelineArgv.tags,
239
- opsFile: pipelineArgv.ops,
240
- closedSchemas: pipelineArgv.closedSchemas,
241
- pruneUnusedSchemas: pipelineArgv.pruneUnusedSchemas,
242
- envelopeNamespace: pipelineArgv["envelope-namespace"],
243
- errorNamespace: pipelineArgv["error-namespace"],
478
+ const servers = parseServers(pipelineArgv["openapi-servers"]);
479
+ // Validate gateway requirements
480
+ validateGatewayRequirements(gatewayOut, openapiOut, pipelineArgv["gateway-service-name"], pipelineArgv["gateway-version-prefix"]);
481
+ // Parse gateway default response status codes if provided
482
+ let gatewayDefaultResponseStatusCodes;
483
+ if (pipelineArgv["gateway-default-status-codes"]) {
484
+ try {
485
+ gatewayDefaultResponseStatusCodes = parseStatusCodes(String(pipelineArgv["gateway-default-status-codes"]), "--gateway-default-status-codes");
486
+ }
487
+ catch (err) {
488
+ handleCLIError(err);
244
489
  }
490
+ }
491
+ // Build options using helpers
492
+ const compilerOptions = buildCompilerOptionsFromArgv(pipelineArgv);
493
+ const openApiOptions = openapiOut
494
+ ? buildOpenApiOptionsFromArgv(pipelineArgv, format, servers)
495
+ : undefined;
496
+ await runGenerationPipeline({
497
+ wsdl: pipelineArgv["wsdl-source"],
498
+ clientOutDir: clientOut ? path.resolve(clientOut) : undefined,
499
+ catalogOut,
500
+ compiler: compilerOptions,
501
+ openapi: openApiOptions ? {
502
+ ...openApiOptions,
503
+ outFile: path.resolve(openapiOut),
504
+ } : undefined,
505
+ gateway: gatewayOut ? {
506
+ defaultResponseStatusCodes: gatewayDefaultResponseStatusCodes,
507
+ outDir: path.resolve(gatewayOut),
508
+ serviceSlug: pipelineArgv["gateway-service-name"],
509
+ versionSlug: pipelineArgv["gateway-version-prefix"],
510
+ } : undefined,
245
511
  });
246
- console.log(`✅ Pipeline complete (format=${format || 'json'})`);
247
512
  process.exit(0);
248
513
  }
249
- // ---------- Original generation CLI (unchanged) ----------
250
- const argv = await yargs(rawArgs)
251
- .scriptName("wsdl-tsc")
252
- .option("wsdl", {
253
- type: "string",
254
- demandOption: true,
255
- desc: "Path or URL to the WSDL"
256
- })
257
- .option("out", {
258
- type: "string",
259
- demandOption: true,
260
- desc: "Output directory for generated files"
261
- })
262
- .option("imports", {
263
- type: "string",
264
- choices: ["js", "ts", "bare"],
265
- default: "js",
266
- desc: "Intra-generated import specifiers: 'js', 'ts', or 'bare' (no extension). Default is 'js'.",
267
- })
268
- .option("catalog", {
269
- type: "boolean",
270
- default: false,
271
- desc: "Emit catalog.json file with complied catalog object for introspection. Default is false.",
272
- })
273
- .option("attributes-key", {
274
- type: "string",
275
- default: "$attributes",
276
- desc: "Key used by runtime marshaller for XML attributes",
277
- })
278
- .option("client-name", {
279
- type: "string",
280
- desc: "Override the generated client class name (exact export name). If not provided, it will be derived from the WSDL name or 'GeneratedSOAPClient' will be used.",
281
- })
282
- // Primitive mapping knobs (safe defaults)
283
- .option("int64-as", {
284
- type: "string",
285
- choices: ["string", "number", "bigint"],
286
- default: "string",
287
- desc: "How to map xs:long/xs:unsignedLong",
288
- })
289
- .option("bigint-as", {
290
- type: "string",
291
- choices: ["string", "number"],
292
- default: "string",
293
- desc: "How to map xs:integer family (positive/nonNegative/etc.)",
294
- })
295
- .option("decimal-as", {
296
- type: "string",
297
- choices: ["string", "number"],
298
- default: "string",
299
- desc: "How to map xs:decimal (money/precision)",
300
- })
301
- .option("date-as", {
302
- type: "string",
303
- choices: ["string", "Date"],
304
- default: "string",
305
- desc: "How to map date/time/duration types",
306
- })
307
- .option("choice", {
308
- type: "string",
309
- choices: ["all-optional", "union"],
310
- default: "all-optional",
311
- desc: "Representation of <choice> elements: all-optional properties or discriminated union",
312
- })
313
- .option("fail-on-unresolved", {
314
- type: "boolean",
315
- default: true,
316
- desc: "Emit errors if any type references cannot be resolved in the WSDL schema",
317
- })
318
- .option("nillable-as-optional", {
319
- type: "boolean",
320
- default: false,
321
- desc: "Emit nillable elements as optional properties in types.",
322
- })
323
- .strict()
324
- .help()
325
- .parse();
326
- const outDir = path.resolve(String(argv.out));
327
- // Load & compile
328
- const catalog = await loadWsdl(String(argv.wsdl));
329
- const compiled = compileCatalog(catalog, {
330
- wsdl: argv.wsdl,
331
- out: argv.out,
332
- imports: argv.imports,
333
- catalog: argv["catalog"],
334
- primitive: {
335
- int64As: argv["int64-as"],
336
- bigIntegerAs: argv["bigint-as"],
337
- decimalAs: argv["decimal-as"],
338
- dateAs: argv["date-as"],
339
- },
340
- choice: argv.choice,
341
- failOnUnresolved: argv["fail-on-unresolved"],
342
- attributesKey: argv["attributes-key"],
343
- clientName: argv["client-name"],
344
- nillableAsOptional: argv["nillable-as-optional"],
345
- });
346
- // Report counts of types and operations for user visibility
347
- console.log(`Schemas discovered: ${catalog.schemas.length}`);
348
- console.log(`Compiled types: ${compiled.types.length}`);
349
- console.log(`Operations: ${compiled.operations.length}`);
350
- // Ensure output directory exists
351
- fs.mkdirSync(outDir, { recursive: true });
352
- // Emit files
353
- emitClient(path.join(outDir, "client.ts"), compiled);
354
- emitTypes(path.join(outDir, "types.ts"), compiled);
355
- emitUtils(path.join(outDir, "utils.ts"), compiled);
356
- // Emit catalog if requested
357
- if (compiled.options.catalog) {
358
- emitCatalog(path.join(outDir, "catalog.json"), compiled);
359
- }
360
- console.log(`✅ Generated TypeScript client in ${outDir}`);