@techspokes/typescript-wsdl-client 0.10.1 → 0.10.4

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/README.md CHANGED
@@ -35,7 +35,7 @@ npx wsdl-tsc pipeline \
35
35
  --gateway-dir ./tmp/gateway \
36
36
  --gateway-service-name weather \
37
37
  --gateway-version-prefix v1 \
38
- --generate-app
38
+ --init-app
39
39
  ```
40
40
 
41
41
  This parses the WSDL, generates a typed SOAP client, creates an OpenAPI 3.1 spec, builds Fastify gateway handlers, and creates a runnable application.
@@ -43,7 +43,7 @@ This parses the WSDL, generates a typed SOAP client, creates an OpenAPI 3.1 spec
43
43
  ### Run and Test
44
44
 
45
45
  ```bash
46
- cd tmp/app && cp .env.example .env && npx tsx server.js
46
+ cd tmp/app && npm install && cp .env.example .env && npm start
47
47
  ```
48
48
 
49
49
  ```bash
@@ -13,6 +13,7 @@
13
13
  * @property {string} [prefix] - Route prefix (default: "")
14
14
  * @property {boolean} [logger] - Enable Fastify logger (default: true)
15
15
  * @property {"copy"|"reference"} [openapiMode] - How to handle OpenAPI file (default: "copy")
16
+ * @property {boolean} [force] - Overwrite existing scaffold files (default: false)
16
17
  */
17
18
  export interface GenerateAppOptions {
18
19
  clientDir: string;
@@ -26,19 +27,24 @@ export interface GenerateAppOptions {
26
27
  prefix?: string;
27
28
  logger?: boolean;
28
29
  openapiMode?: "copy" | "reference";
30
+ force?: boolean;
29
31
  }
30
32
  /**
31
- * Generates a runnable Fastify application
33
+ * Generates a runnable Fastify application scaffold
32
34
  *
33
- * This function orchestrates the complete app generation process:
35
+ * This function orchestrates the complete app scaffold process:
34
36
  * 1. Validates all required inputs exist
35
37
  * 2. Reads catalog.json for metadata
36
38
  * 3. Creates app directory
37
39
  * 4. Generates server.ts with Fastify setup
38
40
  * 5. Generates config.ts with environment loading
39
- * 6. Generates .env.example with configuration template
40
- * 7. Generates README.md with usage instructions
41
- * 8. Optionally copies OpenAPI spec into app directory
41
+ * 6. Generates package.json with dependencies
42
+ * 7. Generates tsconfig.json with TypeScript settings
43
+ * 8. Generates .env.example with configuration template
44
+ * 9. Generates README.md with usage instructions
45
+ * 10. Optionally copies OpenAPI spec into app directory
46
+ *
47
+ * Files that already exist are skipped unless force is true.
42
48
  *
43
49
  * @param {GenerateAppOptions} opts - App generation options
44
50
  * @returns {Promise<void>}
@@ -1 +1 @@
1
- {"version":3,"file":"generateApp.d.ts","sourceRoot":"","sources":["../../src/app/generateApp.ts"],"names":[],"mappings":"AAsBA;;;;;;;;;;;;;;;GAeG;AACH,MAAM,WAAW,kBAAkB;IACjC,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,IAAI,GAAG,IAAI,GAAG,MAAM,CAAC;IAC/B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,GAAG,WAAW,CAAC;CACpC;AA2gBD;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAsB,WAAW,CAAC,IAAI,EAAE,kBAAkB,GAAG,OAAO,CAAC,IAAI,CAAC,CAqCzE"}
1
+ {"version":3,"file":"generateApp.d.ts","sourceRoot":"","sources":["../../src/app/generateApp.ts"],"names":[],"mappings":"AA6BA;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,WAAW,kBAAkB;IACjC,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,IAAI,GAAG,IAAI,GAAG,MAAM,CAAC;IAC/B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,GAAG,WAAW,CAAC;IACnC,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAymBD;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAsB,WAAW,CAAC,IAAI,EAAE,kBAAkB,GAAG,OAAO,CAAC,IAAI,CAAC,CAiDzE"}
@@ -1,23 +1,30 @@
1
1
  // noinspection HttpUrlsUsage
2
2
  /**
3
- * Fastify App Generator
3
+ * Fastify App Scaffold Generator
4
4
  *
5
5
  * This module generates a runnable Fastify application that imports and uses
6
6
  * the generated gateway plugin and SOAP client. The app serves the OpenAPI spec,
7
7
  * health checks, and all gateway routes.
8
8
  *
9
+ * The generated scaffold is intended as a one-time starting point. Developers
10
+ * should customize it freely after generation. Use --force-init to overwrite
11
+ * existing scaffold files.
12
+ *
9
13
  * Core capabilities:
10
14
  * - Generates server.ts with Fastify setup and plugin registration
11
15
  * - Generates config.ts with environment-based configuration
16
+ * - Generates package.json with required dependencies
17
+ * - Generates tsconfig.json with NodeNext/ES2022 settings
12
18
  * - Generates .env.example with required/optional environment variables
13
19
  * - Generates README.md with instructions for running the app
14
20
  * - Optionally copies OpenAPI spec into app directory
15
21
  * - Validates required inputs (client-dir, gateway-dir, openapi-file, catalog-file)
22
+ * - Skip-if-exists protection for scaffold files (override with force option)
16
23
  */
17
24
  import fs from "node:fs";
18
25
  import path from "node:path";
19
26
  import { deriveClientName } from "../util/tools.js";
20
- import { success } from "../util/cli.js";
27
+ import { info, success } from "../util/cli.js";
21
28
  /**
22
29
  * Validates that all required files and directories exist
23
30
  *
@@ -73,16 +80,13 @@ function getExtension(imports) {
73
80
  return "";
74
81
  }
75
82
  /**
76
- * Returns the file extension for executable app files (server, config)
77
- * These always need extensions even with bare imports since they are entry points
83
+ * Returns the file extension for scaffold app files (server, config).
84
+ * Always .ts the scaffold is TypeScript for full type safety.
78
85
  *
79
- * @param {string} imports - Import mode (js, ts, or bare)
80
86
  * @returns {string} - File extension with leading dot
81
87
  */
82
- function getAppFileExtension(imports) {
83
- if (imports === "ts")
84
- return ".ts";
85
- return ".js"; // Use .js for both "js" and "bare" modes
88
+ function getAppFileExtension() {
89
+ return ".ts";
86
90
  }
87
91
  /**
88
92
  * Computes a relative import path from source to target
@@ -105,6 +109,48 @@ function computeRelativeImport(from, to, imports) {
105
109
  }
106
110
  return prefixed;
107
111
  }
112
+ /**
113
+ * Checks if a string is a URL (http:// or https://)
114
+ */
115
+ function isUrl(value) {
116
+ return /^https?:\/\//i.test(value);
117
+ }
118
+ /**
119
+ * Normalizes a file path to POSIX separators
120
+ */
121
+ function toPosix(filePath) {
122
+ return filePath.split(path.sep).join("/");
123
+ }
124
+ /**
125
+ * Resolves a WSDL source path relative to the app directory.
126
+ * URLs are returned as-is. File paths are computed relative to appDir.
127
+ *
128
+ * @param {string} wsdlSource - Original WSDL source from catalog
129
+ * @param {string} appDir - Resolved app output directory
130
+ * @returns {string} - WSDL source suitable for use from the app directory
131
+ */
132
+ function resolveWsdlSourceForApp(wsdlSource, appDir) {
133
+ if (isUrl(wsdlSource))
134
+ return wsdlSource;
135
+ return toPosix(path.relative(appDir, path.resolve(wsdlSource)));
136
+ }
137
+ /**
138
+ * Checks whether a scaffold file should be written.
139
+ * Returns true if the file does not exist or force is enabled.
140
+ * Logs an info message and returns false if the file exists and force is disabled.
141
+ *
142
+ * @param {string} filePath - Absolute path to the file
143
+ * @param {boolean} force - Whether to overwrite existing files
144
+ * @returns {boolean} - Whether the file should be written
145
+ */
146
+ function shouldWriteScaffoldFile(filePath, force) {
147
+ if (!fs.existsSync(filePath))
148
+ return true;
149
+ if (force)
150
+ return true;
151
+ info(`Skipping ${path.basename(filePath)} (already exists, use --force-init to overwrite)`);
152
+ return false;
153
+ }
108
154
  /**
109
155
  * Reads and parses the catalog file
110
156
  *
@@ -135,10 +181,14 @@ function getCatalogWsdlSource(catalog) {
135
181
  * @param {string} appDir - App output directory
136
182
  * @param {GenerateAppOptions} opts - App generation options
137
183
  * @param {string} clientClassName - Derived client class name
184
+ * @param {boolean} force - Whether to overwrite existing files
138
185
  */
139
- function generateServerFile(appDir, opts, clientClassName) {
186
+ function generateServerFile(appDir, opts, clientClassName, force) {
140
187
  const imports = opts.imports || "js";
141
- const ext = getAppFileExtension(imports); // Use getAppFileExtension for executable entry point
188
+ const ext = getAppFileExtension();
189
+ const filePath = path.join(appDir, `server${ext}`);
190
+ if (!shouldWriteScaffoldFile(filePath, force))
191
+ return;
142
192
  const configImport = computeRelativeImport(appDir, path.join(appDir, "config"), imports);
143
193
  const gatewayPluginImport = computeRelativeImport(appDir, path.join(opts.gatewayDir, "plugin"), imports);
144
194
  const clientImport = computeRelativeImport(appDir, path.join(opts.clientDir, "client"), imports);
@@ -149,6 +199,11 @@ function generateServerFile(appDir, opts, clientClassName) {
149
199
  const openapiSpecPath = path.join(__dirname, "openapi.json");
150
200
  const openapiSpec = JSON.parse(fs.readFileSync(openapiSpecPath, "utf-8"));
151
201
 
202
+ // Override OpenAPI server URL at runtime if configured
203
+ if (config.openapiServerUrl) {
204
+ openapiSpec.servers = [{ url: config.openapiServerUrl }];
205
+ }
206
+
152
207
  // Serve OpenAPI specification
153
208
  fastify.get("/openapi.json", async () => {
154
209
  return openapiSpec;
@@ -158,11 +213,16 @@ function generateServerFile(appDir, opts, clientClassName) {
158
213
  const openapiSpecPath = path.resolve(__dirname, "${computeRelativeImport(appDir, opts.openapiFile, "bare")}");
159
214
  const openapiSpec = JSON.parse(fs.readFileSync(openapiSpecPath, "utf-8"));
160
215
 
216
+ // Override OpenAPI server URL at runtime if configured
217
+ if (config.openapiServerUrl) {
218
+ openapiSpec.servers = [{ url: config.openapiServerUrl }];
219
+ }
220
+
161
221
  fastify.get("/openapi.json", async () => {
162
222
  return openapiSpec;
163
223
  });`;
164
224
  const content = `/**
165
- * Generated Fastify Application
225
+ * Fastify Application
166
226
  *
167
227
  * This file bootstraps a Fastify server that:
168
228
  * - Loads configuration from environment variables
@@ -171,7 +231,7 @@ function generateServerFile(appDir, opts, clientClassName) {
171
231
  * - Serves the OpenAPI specification
172
232
  * - Provides health check endpoint
173
233
  *
174
- * Auto-generated - do not edit manually.
234
+ * Scaffolded by wsdl-tsc. Customize freely.
175
235
  */
176
236
  import fs from "node:fs";
177
237
  import path from "node:path";
@@ -245,7 +305,7 @@ main().catch((err) => {
245
305
  process.exit(1);
246
306
  });
247
307
  `;
248
- fs.writeFileSync(path.join(appDir, `server${ext}`), content, "utf-8");
308
+ fs.writeFileSync(filePath, content, "utf-8");
249
309
  }
250
310
  /**
251
311
  * Generates config.ts file
@@ -253,18 +313,35 @@ main().catch((err) => {
253
313
  * @param {string} appDir - App output directory
254
314
  * @param {GenerateAppOptions} opts - App generation options
255
315
  * @param {string|undefined} defaultWsdlSource - Default WSDL source from catalog
316
+ * @param {boolean} force - Whether to overwrite existing files
256
317
  */
257
- function generateConfigFile(appDir, opts, defaultWsdlSource) {
258
- const imports = opts.imports || "js";
259
- const ext = getAppFileExtension(imports); // Use getAppFileExtension for executable entry point
318
+ function generateConfigFile(appDir, opts, defaultWsdlSource, force) {
319
+ const ext = getAppFileExtension();
320
+ const filePath = path.join(appDir, `config${ext}`);
321
+ if (!shouldWriteScaffoldFile(filePath, force))
322
+ return;
260
323
  const defaultHost = opts.host || "127.0.0.1";
261
324
  const defaultPort = opts.port || 3000;
262
325
  const defaultPrefix = opts.prefix || "";
263
326
  const defaultLogger = opts.logger !== false;
264
- // For .js files, we need to generate plain JavaScript (no TypeScript types)
265
- // For .ts files, we can use TypeScript syntax
266
- const isTypeScript = ext === ".ts";
267
- const typeAnnotations = isTypeScript ? `
327
+ // Resolve WSDL source relative to app directory
328
+ const resolvedWsdlSource = defaultWsdlSource
329
+ ? resolveWsdlSourceForApp(defaultWsdlSource, appDir)
330
+ : undefined;
331
+ // For URL sources, use as fallback default. For file sources, require explicit WSDL_SOURCE.
332
+ const wsdlIsUrl = resolvedWsdlSource && isUrl(resolvedWsdlSource);
333
+ const wsdlFallback = wsdlIsUrl ? ` || "${resolvedWsdlSource}"` : "";
334
+ const content = `/**
335
+ * Application Configuration
336
+ *
337
+ * Loads configuration from environment variables with sensible defaults.
338
+ * Configuration precedence:
339
+ * 1. Environment variables (runtime overrides)
340
+ * 2. Hard defaults (defined in this file)
341
+ *
342
+ * Scaffolded by wsdl-tsc. Customize freely.
343
+ */
344
+
268
345
  /**
269
346
  * Application configuration interface
270
347
  */
@@ -274,6 +351,7 @@ export interface AppConfig {
274
351
  port: number;
275
352
  prefix: string;
276
353
  logger: boolean;
354
+ openapiServerUrl: string;
277
355
  }
278
356
 
279
357
  /**
@@ -281,30 +359,12 @@ export interface AppConfig {
281
359
  *
282
360
  * @returns {AppConfig} - Application configuration
283
361
  * @throws {Error} If required configuration is missing
284
- */` : `
285
- /**
286
- * Loads configuration from environment variables
287
- *
288
- * @returns {object} - Application configuration with wsdlSource, host, port, prefix, logger
289
- * @throws {Error} If required configuration is missing
290
- */`;
291
- const content = `/**
292
- * Application Configuration
293
- *
294
- * Loads configuration from environment variables with sensible defaults.
295
- * Configuration precedence:
296
- * 1. Environment variables (runtime overrides)
297
- * 2. Catalog defaults (generation-time recorded values)
298
- * 3. Hard defaults (defined in this file)
299
- *
300
- * Auto-generated - do not edit manually.
301
362
  */
302
- ${typeAnnotations}
303
- export function loadConfig() {
304
- // WSDL source: required from env or catalog default
305
- const wsdlSource = process.env.WSDL_SOURCE${defaultWsdlSource ? ` || "${defaultWsdlSource}"` : ""};
363
+ export function loadConfig(): AppConfig {
364
+ // WSDL source: required from env${wsdlIsUrl ? " or URL default" : ""}
365
+ const wsdlSource = process.env.WSDL_SOURCE${wsdlFallback};
306
366
  if (!wsdlSource) {
307
- throw new Error("WSDL_SOURCE environment variable is required");
367
+ throw new Error("WSDL_SOURCE environment variable is required${resolvedWsdlSource && !wsdlIsUrl ? ` (hint: ${resolvedWsdlSource})` : ""}");
308
368
  }
309
369
 
310
370
  // Host: default to ${defaultHost}
@@ -322,16 +382,20 @@ export function loadConfig() {
322
382
  // Logger: default to ${defaultLogger}
323
383
  const logger = process.env.LOGGER ? process.env.LOGGER === "true" : ${defaultLogger};
324
384
 
385
+ // OpenAPI server URL override (replaces servers in OpenAPI spec at runtime)
386
+ const openapiServerUrl = process.env.OPENAPI_SERVER_URL || "";
387
+
325
388
  return {
326
389
  wsdlSource,
327
390
  host,
328
391
  port,
329
392
  prefix,
330
393
  logger,
394
+ openapiServerUrl,
331
395
  };
332
396
  }
333
397
  `;
334
- fs.writeFileSync(path.join(appDir, `config${ext}`), content, "utf-8");
398
+ fs.writeFileSync(filePath, content, "utf-8");
335
399
  }
336
400
  /**
337
401
  * Generates .env.example file
@@ -339,23 +403,42 @@ export function loadConfig() {
339
403
  * @param {string} appDir - App output directory
340
404
  * @param {GenerateAppOptions} opts - App generation options
341
405
  * @param {string|undefined} defaultWsdlSource - Default WSDL source from catalog
406
+ * @param {boolean} force - Whether to overwrite existing files
342
407
  */
343
- function generateEnvExample(appDir, opts, defaultWsdlSource) {
408
+ function generateEnvExample(appDir, opts, defaultWsdlSource, force) {
409
+ const filePath = path.join(appDir, ".env.example");
410
+ if (!shouldWriteScaffoldFile(filePath, force))
411
+ return;
344
412
  const defaultHost = opts.host || "127.0.0.1";
345
413
  const defaultPort = opts.port || 3000;
346
414
  const defaultPrefix = opts.prefix || "";
347
415
  const defaultLogger = opts.logger !== false;
348
- const content = `# Generated Fastify Application Environment Variables
416
+ // Resolve WSDL source relative to app directory
417
+ const resolvedWsdlSource = defaultWsdlSource
418
+ ? resolveWsdlSourceForApp(defaultWsdlSource, appDir)
419
+ : undefined;
420
+ const wsdlIsUrl = resolvedWsdlSource && isUrl(resolvedWsdlSource);
421
+ let wsdlSection;
422
+ if (wsdlIsUrl) {
423
+ // URL source: show as commented default (it's the fallback in config.ts)
424
+ wsdlSection = `# WSDL source URL (default: ${resolvedWsdlSource})
425
+ #WSDL_SOURCE=${resolvedWsdlSource}`;
426
+ }
427
+ else if (resolvedWsdlSource) {
428
+ // File source: require explicit setting, show hint
429
+ wsdlSection = `# WSDL source (required — set to URL or file path)
430
+ # Generation-time path: ${resolvedWsdlSource}
431
+ WSDL_SOURCE=`;
432
+ }
433
+ else {
434
+ wsdlSection = `# WSDL source (required — set to URL or file path)
435
+ WSDL_SOURCE=`;
436
+ }
437
+ const content = `# Fastify Application Environment Variables
349
438
  #
350
439
  # Copy this file to .env and customize as needed.
351
- # Configuration precedence:
352
- # 1. Environment variables (runtime overrides)
353
- # 2. Catalog defaults (generation-time recorded values)
354
- # 3. Hard defaults (see config file)
355
440
 
356
- # WSDL source (required unless provided in catalog)
357
- ${defaultWsdlSource ? `# Default from catalog: ${defaultWsdlSource}` : "# Required: specify the WSDL URL or local file path"}
358
- ${defaultWsdlSource ? `#WSDL_SOURCE=${defaultWsdlSource}` : `WSDL_SOURCE=`}
441
+ ${wsdlSection}
359
442
 
360
443
  # Server host (default: ${defaultHost})
361
444
  HOST=${defaultHost}
@@ -369,151 +452,162 @@ PREFIX=${defaultPrefix}
369
452
  # Enable Fastify logger (default: ${defaultLogger})
370
453
  LOGGER=${defaultLogger}
371
454
 
455
+ # Override OpenAPI spec server URL at runtime (default: use generation-time value)
456
+ #OPENAPI_SERVER_URL=http://localhost:${defaultPort}
457
+
372
458
  # Optional: SOAP security settings (configure based on your client requirements)
373
459
  # SOAP_USERNAME=
374
460
  # SOAP_PASSWORD=
375
461
  # SOAP_ENDPOINT=
376
462
  `;
377
- fs.writeFileSync(path.join(appDir, ".env.example"), content, "utf-8");
463
+ fs.writeFileSync(filePath, content, "utf-8");
464
+ }
465
+ /**
466
+ * Generates package.json file (skipped if already exists)
467
+ *
468
+ * @param {string} appDir - App output directory
469
+ * @param {boolean} force - Whether to overwrite existing files
470
+ */
471
+ function generatePackageJson(appDir, force) {
472
+ const filePath = path.join(appDir, "package.json");
473
+ if (!shouldWriteScaffoldFile(filePath, force))
474
+ return;
475
+ const pkg = {
476
+ name: "wsdl-gateway-app",
477
+ version: "0.0.1",
478
+ private: true,
479
+ type: "module",
480
+ scripts: {
481
+ start: "tsx server.ts",
482
+ dev: "tsx watch server.ts",
483
+ },
484
+ dependencies: {
485
+ fastify: "^5.7.4",
486
+ "fastify-plugin": "^5.1.0",
487
+ soap: "^1.6.5",
488
+ },
489
+ devDependencies: {
490
+ tsx: "^4.21.0",
491
+ typescript: "^5.9.3",
492
+ },
493
+ };
494
+ fs.writeFileSync(filePath, JSON.stringify(pkg, null, 2) + "\n", "utf-8");
495
+ }
496
+ /**
497
+ * Generates tsconfig.json file (skipped if already exists)
498
+ *
499
+ * @param {string} appDir - App output directory
500
+ * @param {GenerateAppOptions} opts - App generation options
501
+ * @param {boolean} force - Whether to overwrite existing files
502
+ */
503
+ function generateTsConfig(appDir, opts, force) {
504
+ const filePath = path.join(appDir, "tsconfig.json");
505
+ if (!shouldWriteScaffoldFile(filePath, force))
506
+ return;
507
+ // Compute include paths relative to app directory
508
+ const clientInclude = toPosix(path.relative(appDir, opts.clientDir)) + "/**/*.ts";
509
+ const gatewayInclude = toPosix(path.relative(appDir, opts.gatewayDir)) + "/**/*.ts";
510
+ const tsconfig = {
511
+ compilerOptions: {
512
+ module: "NodeNext",
513
+ moduleResolution: "NodeNext",
514
+ target: "ES2022",
515
+ strict: true,
516
+ esModuleInterop: true,
517
+ skipLibCheck: true,
518
+ outDir: "dist",
519
+ rootDir: ".",
520
+ },
521
+ include: [
522
+ "*.ts",
523
+ clientInclude,
524
+ gatewayInclude,
525
+ ],
526
+ };
527
+ fs.writeFileSync(filePath, JSON.stringify(tsconfig, null, 2) + "\n", "utf-8");
378
528
  }
379
529
  /**
380
530
  * Generates README.md file
381
531
  *
382
532
  * @param {string} appDir - App output directory
383
533
  * @param {GenerateAppOptions} opts - App generation options
534
+ * @param {boolean} force - Whether to overwrite existing files
384
535
  */
385
- function generateReadme(appDir, opts) {
386
- const imports = opts.imports || "js";
387
- const ext = getExtension(imports);
388
- const runCommand = ext === ".ts"
389
- ? "npx tsx server.ts"
390
- : "node server.js";
536
+ function generateReadme(appDir, opts, force) {
537
+ const filePath = path.join(appDir, "README.md");
538
+ if (!shouldWriteScaffoldFile(filePath, force))
539
+ return;
391
540
  const content = `# Generated Fastify Application
392
541
 
393
- This application was auto-generated by \`wsdl-tsc app\`.
542
+ This application was scaffolded by \`wsdl-tsc\`. Customize freely.
394
543
 
395
- ## Overview
544
+ ## Quick Start
396
545
 
397
- This Fastify application provides a REST gateway to a SOAP service, automatically bridging between REST endpoints and SOAP operations.
546
+ \`\`\`bash
547
+ npm install
548
+ cp .env.example .env
549
+ # Edit .env — set WSDL_SOURCE to your WSDL URL or file path
550
+ npm start
551
+ \`\`\`
398
552
 
399
553
  ## Structure
400
554
 
401
- - \`server${ext}\` - Main application entry point
402
- - \`config${ext}\` - Configuration loader (environment-based)
555
+ - \`server.ts\` - Main application entry point
556
+ - \`config.ts\` - Configuration loader (environment-based)
557
+ - \`package.json\` - Dependencies and scripts
558
+ - \`tsconfig.json\` - TypeScript configuration
403
559
  - \`.env.example\` - Example environment configuration
404
560
  - \`openapi.json\` - OpenAPI specification${opts.openapiMode === "copy" ? " (copied)" : " (referenced)"}
405
561
 
406
- ## Prerequisites
407
-
408
- - Node.js >= 20.0.0
409
- - Dependencies installed (\`npm install\`)
410
-
411
- ## Quick Start
412
-
413
- 1. **Copy environment template**:
414
- \`\`\`bash
415
- cp .env.example .env
416
- \`\`\`
417
-
418
- 2. **Configure environment**:
419
- Edit \`.env\` and set required variables (especially \`WSDL_SOURCE\` if not provided via catalog).
420
-
421
- 3. **Run the server**:
422
- \`\`\`bash
423
- ${runCommand}
424
- \`\`\`
425
-
426
562
  ## Endpoints
427
563
 
428
- ### Health Check
429
- \`\`\`
430
- GET /health
431
- \`\`\`
432
- Returns: \`{ ok: true }\`
433
-
434
- ### OpenAPI Specification
435
- \`\`\`
436
- GET /openapi.json
437
- \`\`\`
438
- Returns: OpenAPI 3.1 specification document
439
-
440
- ### Gateway Routes
441
- All SOAP operations are exposed as REST endpoints. See \`openapi.json\` for complete API documentation.
564
+ | Endpoint | Method | Description |
565
+ |----------|--------|-------------|
566
+ | \`/health\` | GET | Health check — returns \`{ "ok": true }\` |
567
+ | \`/openapi.json\` | GET | OpenAPI 3.1 specification |
568
+ | All SOAP operations | POST | REST-to-SOAP gateway routes (see openapi.json) |
442
569
 
443
570
  ## Configuration
444
571
 
445
- Configuration is loaded from environment variables with the following precedence:
446
-
447
- 1. Environment variables (runtime overrides)
448
- 2. Catalog defaults (from generation-time)
449
- 3. Hard defaults (in config file)
450
-
451
- ### Environment Variables
452
-
453
572
  | Variable | Default | Description |
454
573
  |----------|---------|-------------|
455
- | \`WSDL_SOURCE\` | (from catalog) | WSDL URL or local file path (required) |
574
+ | \`WSDL_SOURCE\` | (see .env.example) | WSDL URL or local file path (required) |
456
575
  | \`HOST\` | ${opts.host || "127.0.0.1"} | Server bind address |
457
576
  | \`PORT\` | ${opts.port || 3000} | Server listen port |
458
- | \`PREFIX\` | ${opts.prefix || "(empty)"} | Route prefix |
577
+ | \`PREFIX\` | (empty) | Route prefix |
459
578
  | \`LOGGER\` | ${opts.logger !== false} | Enable Fastify logger |
579
+ | \`OPENAPI_SERVER_URL\` | (empty) | Override OpenAPI spec server URL at runtime |
460
580
 
461
581
  ## Development
462
582
 
463
- ### Running with watch mode
464
- \`\`\`bash
465
- npx tsx watch server${ext}
466
- \`\`\`
467
-
468
- ### Testing endpoints
469
583
  \`\`\`bash
470
- # Health check
471
- curl http://localhost:3000/health
472
-
473
- # OpenAPI spec
474
- curl http://localhost:3000/openapi.json
475
- \`\`\`
476
-
477
- ## Troubleshooting
478
-
479
- ### WSDL_SOURCE missing
480
- If you see "WSDL_SOURCE environment variable is required", set it in your \`.env\` file or export it:
481
- \`\`\`bash
482
- export WSDL_SOURCE=path/to/service.wsdl
483
- ${runCommand}
484
- \`\`\`
485
-
486
- ### Port already in use
487
- Change the \`PORT\` in your \`.env\` file or:
488
- \`\`\`bash
489
- PORT=8080 ${runCommand}
584
+ npm run dev # Start with file watching
585
+ curl localhost:3000/health
586
+ curl localhost:3000/openapi.json
490
587
  \`\`\`
491
588
 
492
- ## Notes
493
-
494
- - This app uses the generated client from: \`${opts.clientDir}\`
495
- - Gateway plugin from: \`${opts.gatewayDir}\`
496
- - OpenAPI spec from: \`${opts.openapiFile}\`
497
-
498
589
  ## Generated By
499
590
 
500
- - Tool: [@techspokes/typescript-wsdl-client](https://github.com/TechSpokes/typescript-wsdl-client)
501
- - Command: \`wsdl-tsc app\`
591
+ [@techspokes/typescript-wsdl-client](https://github.com/TechSpokes/typescript-wsdl-client)
502
592
  `;
503
- fs.writeFileSync(path.join(appDir, "README.md"), content, "utf-8");
593
+ fs.writeFileSync(filePath, content, "utf-8");
504
594
  }
505
595
  /**
506
- * Generates a runnable Fastify application
596
+ * Generates a runnable Fastify application scaffold
507
597
  *
508
- * This function orchestrates the complete app generation process:
598
+ * This function orchestrates the complete app scaffold process:
509
599
  * 1. Validates all required inputs exist
510
600
  * 2. Reads catalog.json for metadata
511
601
  * 3. Creates app directory
512
602
  * 4. Generates server.ts with Fastify setup
513
603
  * 5. Generates config.ts with environment loading
514
- * 6. Generates .env.example with configuration template
515
- * 7. Generates README.md with usage instructions
516
- * 8. Optionally copies OpenAPI spec into app directory
604
+ * 6. Generates package.json with dependencies
605
+ * 7. Generates tsconfig.json with TypeScript settings
606
+ * 8. Generates .env.example with configuration template
607
+ * 9. Generates README.md with usage instructions
608
+ * 10. Optionally copies OpenAPI spec into app directory
609
+ *
610
+ * Files that already exist are skipped unless force is true.
517
611
  *
518
612
  * @param {GenerateAppOptions} opts - App generation options
519
613
  * @returns {Promise<void>}
@@ -529,6 +623,7 @@ export async function generateApp(opts) {
529
623
  catalogFile: path.resolve(opts.catalogFile),
530
624
  appDir: path.resolve(opts.appDir),
531
625
  };
626
+ const force = resolvedOpts.force ?? false;
532
627
  // Validate required files and directories
533
628
  validateRequiredFiles(resolvedOpts);
534
629
  // Read catalog for metadata
@@ -537,17 +632,27 @@ export async function generateApp(opts) {
537
632
  const defaultWsdlSource = getCatalogWsdlSource(catalog);
538
633
  // Create app directory
539
634
  fs.mkdirSync(resolvedOpts.appDir, { recursive: true });
540
- // Generate app files
541
- generateServerFile(resolvedOpts.appDir, resolvedOpts, clientClassName);
542
- generateConfigFile(resolvedOpts.appDir, resolvedOpts, defaultWsdlSource);
543
- generateEnvExample(resolvedOpts.appDir, resolvedOpts, defaultWsdlSource);
544
- generateReadme(resolvedOpts.appDir, resolvedOpts);
635
+ // Generate scaffold files (each checks for existing files unless force is set)
636
+ generateServerFile(resolvedOpts.appDir, resolvedOpts, clientClassName, force);
637
+ generateConfigFile(resolvedOpts.appDir, resolvedOpts, defaultWsdlSource, force);
638
+ generatePackageJson(resolvedOpts.appDir, force);
639
+ generateTsConfig(resolvedOpts.appDir, resolvedOpts, force);
640
+ generateEnvExample(resolvedOpts.appDir, resolvedOpts, defaultWsdlSource, force);
641
+ generateReadme(resolvedOpts.appDir, resolvedOpts, force);
545
642
  // Handle OpenAPI file
546
643
  const openapiMode = resolvedOpts.openapiMode || "copy";
547
644
  if (openapiMode === "copy") {
548
645
  const destPath = path.join(resolvedOpts.appDir, "openapi.json");
549
- fs.copyFileSync(resolvedOpts.openapiFile, destPath);
550
- success(`Copied OpenAPI spec to ${destPath}`);
646
+ if (shouldWriteScaffoldFile(destPath, force)) {
647
+ const spec = JSON.parse(fs.readFileSync(resolvedOpts.openapiFile, "utf-8"));
648
+ const host = resolvedOpts.host || "127.0.0.1";
649
+ const port = resolvedOpts.port || 3000;
650
+ const prefix = resolvedOpts.prefix || "";
651
+ const urlHost = ["0.0.0.0", "127.0.0.1", "::", "::1"].includes(host) ? "localhost" : host;
652
+ spec.servers = [{ url: `http://${urlHost}:${port}${prefix}` }];
653
+ fs.writeFileSync(destPath, JSON.stringify(spec, null, 2) + "\n", "utf-8");
654
+ success(`Copied OpenAPI spec to ${destPath}`);
655
+ }
551
656
  }
552
- success(`Generated runnable Fastify app in ${resolvedOpts.appDir}`);
657
+ success(`Scaffolded Fastify app in ${resolvedOpts.appDir}`);
553
658
  }
package/dist/cli.js CHANGED
@@ -457,6 +457,11 @@ if (rawArgs[0] === "app") {
457
457
  choices: ["copy", "reference"],
458
458
  default: "copy",
459
459
  desc: "How to handle OpenAPI file: copy into app dir or reference original"
460
+ })
461
+ .option("force", {
462
+ type: "boolean",
463
+ default: false,
464
+ desc: "Overwrite existing scaffold files"
460
465
  })
461
466
  .strict()
462
467
  .help()
@@ -500,6 +505,7 @@ if (rawArgs[0] === "app") {
500
505
  prefix: String(appArgv.prefix),
501
506
  logger: Boolean(appArgv.logger),
502
507
  openapiMode: appArgv["openapi-mode"],
508
+ force: Boolean(appArgv.force),
503
509
  });
504
510
  process.exit(0);
505
511
  }
@@ -602,11 +608,21 @@ if (rawArgs[0] === "pipeline") {
602
608
  default: false,
603
609
  desc: "Skip generating runtime.ts utilities"
604
610
  })
605
- // App generation flags
611
+ // App scaffold flags
612
+ .option("init-app", {
613
+ type: "boolean",
614
+ default: false,
615
+ desc: "Scaffold a runnable Fastify application (one-time setup, requires --client-dir, --gateway-dir, --openapi-file)"
616
+ })
617
+ .option("force-init", {
618
+ type: "boolean",
619
+ default: false,
620
+ desc: "Overwrite existing scaffold files when using --init-app"
621
+ })
606
622
  .option("generate-app", {
607
623
  type: "boolean",
608
624
  default: false,
609
- desc: "Generate runnable Fastify application (requires --client-dir, --gateway-dir, --openapi-file)"
625
+ hidden: true, // deprecated, use --init-app
610
626
  })
611
627
  .option("app-dir", {
612
628
  type: "string",
@@ -617,6 +633,21 @@ if (rawArgs[0] === "pipeline") {
617
633
  choices: ["copy", "reference"],
618
634
  default: "copy",
619
635
  desc: "How to handle OpenAPI file in app: copy or reference"
636
+ })
637
+ .option("app-host", {
638
+ type: "string",
639
+ default: "127.0.0.1",
640
+ desc: "Default server host for app scaffold"
641
+ })
642
+ .option("app-port", {
643
+ type: "number",
644
+ default: 3000,
645
+ desc: "Default server port for app scaffold"
646
+ })
647
+ .option("app-prefix", {
648
+ type: "string",
649
+ default: "",
650
+ desc: "Route prefix for app scaffold"
620
651
  })
621
652
  .strict()
622
653
  .help()
@@ -662,7 +693,14 @@ if (rawArgs[0] === "pipeline") {
662
693
  fs.rmSync(clientOutDir, { recursive: true, force: true });
663
694
  }
664
695
  }
665
- const servers = parseServers(pipelineArgv["openapi-servers"]);
696
+ // Resolve --init-app (new name) or --generate-app (deprecated alias)
697
+ const initApp = pipelineArgv["init-app"] || pipelineArgv["generate-app"];
698
+ const forceInit = pipelineArgv["force-init"];
699
+ let servers = parseServers(pipelineArgv["openapi-servers"]);
700
+ // Default OpenAPI servers to localhost when scaffolding an app and no explicit servers provided
701
+ if (initApp && servers.length === 0) {
702
+ servers = ["http://localhost:3000"];
703
+ }
666
704
  // Validate gateway requirements
667
705
  validateGatewayRequirements(gatewayOut, openapiOut, pipelineArgv["gateway-service-name"], pipelineArgv["gateway-version-prefix"]);
668
706
  // Parse gateway default response status codes if provided
@@ -680,11 +718,10 @@ if (rawArgs[0] === "pipeline") {
680
718
  const openApiOptions = openapiOut
681
719
  ? buildOpenApiOptionsFromArgv(pipelineArgv, format, servers)
682
720
  : undefined;
683
- // Validate app generation requirements
684
- const generateApp = pipelineArgv["generate-app"];
685
- if (generateApp) {
721
+ // Validate app scaffold requirements
722
+ if (initApp) {
686
723
  if (!clientOut || !gatewayOut || !openapiOut) {
687
- handleCLIError("--generate-app requires --client-dir, --gateway-dir, and --openapi-file to be set");
724
+ handleCLIError("--init-app requires --client-dir, --gateway-dir, and --openapi-file to be set");
688
725
  }
689
726
  }
690
727
  await runGenerationPipeline({
@@ -707,11 +744,15 @@ if (rawArgs[0] === "pipeline") {
707
744
  emitPlugin: pipelineArgv["gateway-skip-plugin"] ? false : undefined,
708
745
  emitRuntime: pipelineArgv["gateway-skip-runtime"] ? false : undefined,
709
746
  } : undefined,
710
- app: generateApp ? {
747
+ app: initApp ? {
711
748
  appDir: pipelineArgv["app-dir"]
712
749
  ? path.resolve(pipelineArgv["app-dir"])
713
750
  : path.join(path.dirname(path.resolve(gatewayOut)), "app"),
714
751
  openapiMode: pipelineArgv["app-openapi-mode"],
752
+ force: forceInit,
753
+ host: pipelineArgv["app-host"],
754
+ port: pipelineArgv["app-port"],
755
+ prefix: pipelineArgv["app-prefix"],
715
756
  } : undefined,
716
757
  });
717
758
  process.exit(0);
@@ -35,6 +35,10 @@ export interface PipelineOptions {
35
35
  app?: {
36
36
  appDir: string;
37
37
  openapiMode?: "copy" | "reference";
38
+ force?: boolean;
39
+ host?: string;
40
+ port?: number;
41
+ prefix?: string;
38
42
  };
39
43
  }
40
44
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"pipeline.d.ts","sourceRoot":"","sources":["../src/pipeline.ts"],"names":[],"mappings":"AAgBA,OAAO,EAAkB,KAAK,sBAAsB,EAAC,MAAM,8BAA8B,CAAC;AAC1F,OAAO,EAAkB,KAAK,sBAAsB,EAAC,MAAM,8BAA8B,CAAC;AAC1F,OAAO,EAAC,KAAK,eAAe,EAAyB,MAAM,aAAa,CAAC;AAGzE;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,EAAE,OAAO,CAAC,eAAe,CAAC,CAAC;IACpC,OAAO,CAAC,EAAE,IAAI,CAAC,sBAAsB,EAAE,MAAM,GAAG,aAAa,GAAG,iBAAiB,CAAC,GAAG;QAAE,OAAO,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAC1G,OAAO,CAAC,EAAE,IAAI,CAAC,sBAAsB,EAAE,aAAa,GAAG,iBAAiB,CAAC,GAAG;QAC1E,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,WAAW,EAAE,MAAM,CAAC;QACpB,WAAW,EAAE,MAAM,CAAC;KACrB,CAAC;IACF,GAAG,CAAC,EAAE;QACJ,MAAM,EAAE,MAAM,CAAC;QACf,WAAW,CAAC,EAAE,MAAM,GAAG,WAAW,CAAC;KACpC,CAAC;CACH;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAsB,qBAAqB,CAAC,IAAI,EAAE,eAAe,GAAG,OAAO,CAAC;IAAE,QAAQ,EAAE,GAAG,CAAC;IAAC,UAAU,CAAC,EAAE,GAAG,CAAC;CAAE,CAAC,CAmHhH"}
1
+ {"version":3,"file":"pipeline.d.ts","sourceRoot":"","sources":["../src/pipeline.ts"],"names":[],"mappings":"AAgBA,OAAO,EAAkB,KAAK,sBAAsB,EAAC,MAAM,8BAA8B,CAAC;AAC1F,OAAO,EAAkB,KAAK,sBAAsB,EAAC,MAAM,8BAA8B,CAAC;AAC1F,OAAO,EAAC,KAAK,eAAe,EAAyB,MAAM,aAAa,CAAC;AAGzE;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,EAAE,OAAO,CAAC,eAAe,CAAC,CAAC;IACpC,OAAO,CAAC,EAAE,IAAI,CAAC,sBAAsB,EAAE,MAAM,GAAG,aAAa,GAAG,iBAAiB,CAAC,GAAG;QAAE,OAAO,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAC1G,OAAO,CAAC,EAAE,IAAI,CAAC,sBAAsB,EAAE,aAAa,GAAG,iBAAiB,CAAC,GAAG;QAC1E,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,WAAW,EAAE,MAAM,CAAC;QACpB,WAAW,EAAE,MAAM,CAAC;KACrB,CAAC;IACF,GAAG,CAAC,EAAE;QACJ,MAAM,EAAE,MAAM,CAAC;QACf,WAAW,CAAC,EAAE,MAAM,GAAG,WAAW,CAAC;QACnC,KAAK,CAAC,EAAE,OAAO,CAAC;QAChB,IAAI,CAAC,EAAE,MAAM,CAAC;QACd,IAAI,CAAC,EAAE,MAAM,CAAC;QACd,MAAM,CAAC,EAAE,MAAM,CAAC;KACjB,CAAC;CACH;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAsB,qBAAqB,CAAC,IAAI,EAAE,eAAe,GAAG,OAAO,CAAC;IAAE,QAAQ,EAAE,GAAG,CAAC;IAAC,UAAU,CAAC,EAAE,GAAG,CAAC;CAAE,CAAC,CAuHhH"}
package/dist/pipeline.js CHANGED
@@ -117,6 +117,10 @@ export async function runGenerationPipeline(opts) {
117
117
  appDir: path.resolve(opts.app.appDir),
118
118
  imports: finalCompiler.imports,
119
119
  openapiMode: opts.app.openapiMode || "copy",
120
+ force: opts.app.force,
121
+ host: opts.app.host,
122
+ port: opts.app.port,
123
+ prefix: opts.app.prefix,
120
124
  });
121
125
  }
122
126
  // Return the compiled catalog and OpenAPI doc for potential further processing
package/docs/AGENTS.md ADDED
@@ -0,0 +1,47 @@
1
+ # Agent Instructions for docs/
2
+
3
+ ## Summary
4
+
5
+ This directory contains human-maintained reference documentation for `@techspokes/typescript-wsdl-client`. Documents here are not generated; agents may edit them but must preserve cross-references, formatting conventions, and consistency with the root README.
6
+
7
+ ## Must-follow rules
8
+
9
+ - Maintain cross-references between documents; when a doc mentions a related topic, link to the relevant file.
10
+ - Use relative links for all internal references (e.g. `[CLI Reference](cli-reference.md)`, `[root README](../README.md)`).
11
+ - Each document must open with an H1 title, a one-line description, and a cross-reference to the root [README.md](../README.md).
12
+ - When adding, renaming, or removing a document, update the Documentation table in the root [README.md](../README.md) — that table is the single source of truth for this directory's contents.
13
+ - Do not duplicate content from root-level files (README.md, CONTRIBUTING.md, CHANGELOG.md); link to them instead.
14
+
15
+ ## Must-read documents
16
+
17
+ - [Root README.md](../README.md): authoritative Documentation table and project overview
18
+ - [Root AGENTS.md](../AGENTS.md): project-wide agent instructions pointing to `.github/copilot-instructions.md` for full detail
19
+
20
+ ## Agent guidelines
21
+
22
+ ### Style conventions
23
+
24
+ - Use fenced code blocks for CLI examples and inline code for flag names (`` `--flag-name` ``).
25
+ - Keep language direct and task-oriented.
26
+ - `architecture.md` targets contributors; all other documents target users.
27
+
28
+ ### Adding a new document
29
+
30
+ 1. Create the file in `docs/` with an H1 title and description.
31
+ 2. Add a row to the Documentation table in the root README.md.
32
+ 3. Add a bullet to the Contents list in `docs/README.md`.
33
+ 4. Cross-reference from related existing documents where appropriate.
34
+
35
+ ### Editing existing documents
36
+
37
+ - Preserve the H1 + description + cross-reference opening pattern.
38
+ - If renaming a file, update the root README.md Documentation table and fix any relative links in other docs.
39
+
40
+ ## Context
41
+
42
+ The `docs/` directory was introduced in v0.8.x to move detailed reference material out of the root README. `architecture.md` covers the internal pipeline for contributors; the remaining documents serve end users integrating with or deploying generated code.
43
+
44
+ ## References
45
+
46
+ - [examples/README.md](../examples/README.md): folder README pattern example
47
+ - [Root README.md Documentation table](../README.md#documentation): authoritative listing of all docs
package/docs/README.md ADDED
@@ -0,0 +1,35 @@
1
+ # Documentation
2
+
3
+ Human-maintained reference documents for `@techspokes/typescript-wsdl-client`. The root [README.md](../README.md) Documentation table is the authoritative index with descriptions; the list below is a quick local reference.
4
+
5
+ ## Contents
6
+
7
+ - [api-reference.md](api-reference.md) – Programmatic TypeScript API
8
+ - [architecture.md](architecture.md) – Internal pipeline for contributors
9
+ - [cli-reference.md](cli-reference.md) – All 6 commands with flags and examples
10
+ - [concepts.md](concepts.md) – Flattening, `$value`, primitives, determinism
11
+ - [configuration.md](configuration.md) – Security, tags, operations config files
12
+ - [gateway-guide.md](gateway-guide.md) – Fastify integration and error handling
13
+ - [generated-code.md](generated-code.md) – Using clients and types
14
+ - [migration.md](migration.md) – Upgrade paths between versions
15
+ - [production.md](production.md) – CI/CD, validation, logging, limitations
16
+ - [troubleshooting.md](troubleshooting.md) – Common issues and debugging
17
+
18
+ ## Conventions
19
+
20
+ - Each document opens with an H1 title and a one-line description.
21
+ - Cross-reference related documents and the root README where relevant.
22
+ - Use fenced code blocks for CLI examples; format flags as inline code (`` `--flag-name` ``).
23
+ - Keep language direct and task-oriented; architecture.md targets contributors, all others target users.
24
+
25
+ ## Related
26
+
27
+ - [Root README](../README.md) – project overview, quick start, and authoritative Documentation table
28
+ - [CONTRIBUTING.md](../CONTRIBUTING.md) – development setup and workflow
29
+ - [CHANGELOG.md](../CHANGELOG.md) – version history
30
+ - [examples/](../examples/) – sample WSDL files and generated output
31
+
32
+ ## Not Here
33
+
34
+ - Generated code samples live in `examples/generated-output/`, not in `docs/`.
35
+ - Installation and dev setup belong in the root README and CONTRIBUTING.md respectively.
@@ -54,7 +54,7 @@ generateGateway.ts orchestrates all gateway file generation. generators.ts conta
54
54
 
55
55
  ### app/
56
56
 
57
- generateApp.ts generates server.js, config.js, and .env.example.
57
+ generateApp.ts scaffolds server.ts, config.ts, package.json, tsconfig.json, .env.example, and README.md.
58
58
 
59
59
  ### Top-level Modules
60
60
 
@@ -54,7 +54,6 @@ The catalog is auto-placed alongside the first available output directory: `{cli
54
54
  | `--client-dir` | Generate TypeScript client in this directory |
55
55
  | `--openapi-file` | Generate OpenAPI spec at this path |
56
56
  | `--gateway-dir` | Generate Fastify gateway in this directory |
57
- | `--generate-app` | Generate runnable app (requires gateway) |
58
57
 
59
58
  ### Client Flags
60
59
 
@@ -79,7 +78,10 @@ The catalog is auto-placed alongside the first available output directory: `{cli
79
78
  | `--openapi-title` | (derived) | API title |
80
79
  | `--openapi-version` | `0.0.0` | API version |
81
80
  | `--openapi-description` | (empty) | API description |
82
- | `--openapi-servers` | `/` | Comma-separated server URLs |
81
+ | `--openapi-servers` | `/` | Comma-separated server URLs (see note below) |
82
+
83
+ > When `--init-app` is used and `--openapi-servers` is not provided, servers default to `http://localhost:3000`. The app scaffold also rewrites the servers array in its local `openapi.json` copy to match the configured port and prefix.
84
+
83
85
  | `--openapi-base-path` | (empty) | Base path prefix |
84
86
  | `--openapi-path-style` | `kebab` | Path transform: kebab, asis, or lower |
85
87
  | `--openapi-method` | `post` | Default HTTP method |
@@ -106,6 +108,18 @@ The catalog is auto-placed alongside the first available output directory: `{cli
106
108
  | `--gateway-skip-plugin` | `false` | Skip plugin.ts generation |
107
109
  | `--gateway-skip-runtime` | `false` | Skip runtime.ts generation |
108
110
 
111
+ ### App Scaffold Flags
112
+
113
+ | Flag | Default | Description |
114
+ |------|---------|-------------|
115
+ | `--init-app` | `false` | Scaffold runnable Fastify app (requires client, gateway, OpenAPI) |
116
+ | `--force-init` | `false` | Overwrite existing scaffold files |
117
+ | `--app-dir` | sibling `app/` | Output directory for app scaffold |
118
+ | `--app-openapi-mode` | `copy` | How to handle OpenAPI file: copy or reference |
119
+ | `--app-host` | `127.0.0.1` | Default server host |
120
+ | `--app-port` | `3000` | Default server port |
121
+ | `--app-prefix` | (empty) | Route prefix |
122
+
109
123
  ### Pipeline Workflow
110
124
 
111
125
  Steps execute in order:
@@ -116,7 +130,7 @@ Steps execute in order:
116
130
  4. Generate client (if --client-dir)
117
131
  5. Generate OpenAPI (if --openapi-file)
118
132
  6. Generate gateway (if --gateway-dir)
119
- 7. Generate app (if --generate-app)
133
+ 7. Scaffold app (if --init-app)
120
134
 
121
135
  All steps share the same parsed WSDL and compiled catalog.
122
136
 
@@ -134,17 +148,17 @@ npx wsdl-tsc pipeline \
134
148
  --gateway-version-prefix v1
135
149
  ```
136
150
 
137
- With app generation:
151
+ With app scaffold:
138
152
 
139
153
  ```bash
140
154
  npx wsdl-tsc pipeline \
141
- --wsdl-source examples/minimal/weather.wsdl \
142
- --client-dir tmp/client \
143
- --openapi-file tmp/openapi.json \
144
- --gateway-dir tmp/gateway \
155
+ --wsdl-source https://example.com/weather?wsdl \
156
+ --client-dir ./generated/client \
157
+ --openapi-file ./generated/openapi.json \
158
+ --gateway-dir ./generated/gateway \
145
159
  --gateway-service-name weather \
146
160
  --gateway-version-prefix v1 \
147
- --generate-app
161
+ --init-app
148
162
  ```
149
163
 
150
164
  Client and OpenAPI only:
@@ -371,7 +385,9 @@ npx wsdl-tsc gateway \
371
385
 
372
386
  ## app
373
387
 
374
- Generate a runnable Fastify application integrating client, gateway, and OpenAPI spec.
388
+ Scaffold a runnable Fastify application integrating client, gateway, and OpenAPI spec. The generated app is TypeScript and includes `package.json` and `tsconfig.json` for immediate use.
389
+
390
+ Scaffold files that already exist are skipped by default. Use `--force` to overwrite.
375
391
 
376
392
  ### Usage
377
393
 
@@ -404,13 +420,16 @@ npx wsdl-tsc app \
404
420
  | `--prefix` | (empty) | Route prefix |
405
421
  | `--logger` | true | Enable Fastify logger |
406
422
  | `--openapi-mode` | copy | copy or reference |
423
+ | `--force` | false | Overwrite existing scaffold files |
407
424
 
408
425
  ### Generated Structure
409
426
 
410
427
  ```text
411
428
  app/
412
- ├── server.js # Main entry point
413
- ├── config.js # Configuration with env support
429
+ ├── server.ts # Main entry point (TypeScript)
430
+ ├── config.ts # Configuration with env support
431
+ ├── package.json # Dependencies (fastify, soap, tsx)
432
+ ├── tsconfig.json # NodeNext/ES2022 configuration
414
433
  ├── .env.example # Environment template
415
434
  ├── README.md # Usage instructions
416
435
  └── openapi.json # OpenAPI spec (when --openapi-mode=copy)
@@ -420,11 +439,12 @@ app/
420
439
 
421
440
  | Variable | Default | Description |
422
441
  |----------|---------|-------------|
423
- | `WSDL_SOURCE` | From catalog or required | WSDL URL or path |
442
+ | `WSDL_SOURCE` | URL fallback or required | WSDL URL or path |
424
443
  | `HOST` | 127.0.0.1 | Server bind address |
425
444
  | `PORT` | 3000 | Server listen port |
426
445
  | `PREFIX` | (empty) | Route prefix |
427
446
  | `LOGGER` | true | Fastify logger |
447
+ | `OPENAPI_SERVER_URL` | (empty) | Override OpenAPI spec server URL at runtime |
428
448
 
429
449
  ### Endpoints
430
450
 
@@ -432,7 +452,17 @@ app/
432
452
  - `GET /openapi.json` returns the OpenAPI specification
433
453
  - All SOAP operations are exposed as REST endpoints
434
454
 
435
- The app can also be generated via pipeline with `--generate-app`.
455
+ ### Quick Start
456
+
457
+ ```bash
458
+ cd app/
459
+ npm install
460
+ cp .env.example .env
461
+ # Edit .env — set WSDL_SOURCE to your WSDL URL
462
+ npm start
463
+ ```
464
+
465
+ The app can also be scaffolded via pipeline with `--init-app`.
436
466
 
437
467
  ## compile
438
468
 
@@ -63,17 +63,18 @@ Route handlers call the SOAP client automatically:
63
63
 
64
64
  ```typescript
65
65
  import type { FastifyInstance } from "fastify";
66
+ import type { GetCityForecastByZIP } from "../../client/types.js";
66
67
  import schema from "../schemas/operations/getcityforecastbyzip.json" with { type: "json" };
67
68
  import { buildSuccessEnvelope } from "../runtime.js";
68
69
 
69
70
  export async function registerRoute_v1_weather_getcityforecastbyzip(fastify: FastifyInstance) {
70
- fastify.route({
71
+ fastify.route<{ Body: GetCityForecastByZIP }>({
71
72
  method: "POST",
72
73
  url: "/get-city-forecast-by-zip",
73
74
  schema,
74
75
  handler: async (request) => {
75
76
  const client = fastify.weatherClient;
76
- const result = await client.GetCityForecastByZIP(request.body);
77
+ const result = await client.GetCityForecastByZIP(request.body as GetCityForecastByZIP);
77
78
  return buildSuccessEnvelope(result.response);
78
79
  },
79
80
  });
@@ -117,6 +118,39 @@ All generated JSON Schemas use deterministic URN identifiers:
117
118
 
118
119
  Example: `urn:services:weather:v1:schemas:models:getcityweatherbyzipresponse`
119
120
 
121
+ ## Multi-Service Setup
122
+
123
+ When integrating multiple SOAP services, each service gets its own client, gateway, and OpenAPI spec. Register each in its own Fastify encapsulation scope to prevent decorator collisions:
124
+
125
+ ```typescript
126
+ import Fastify from "fastify";
127
+ import weatherPlugin from "./generated/weather/gateway/plugin.js";
128
+ import inventoryPlugin from "./generated/inventory/gateway/plugin.js";
129
+ import { Weather } from "./generated/weather/client/client.js";
130
+ import { Inventory } from "./generated/inventory/client/client.js";
131
+
132
+ const app = Fastify({ logger: true });
133
+
134
+ // Each plugin gets its own scope to isolate decorators
135
+ await app.register(async (scope) => {
136
+ await scope.register(weatherPlugin, {
137
+ client: new Weather({ source: "https://example.com/weather?wsdl" }),
138
+ prefix: "/api/weather",
139
+ });
140
+ });
141
+
142
+ await app.register(async (scope) => {
143
+ await scope.register(inventoryPlugin, {
144
+ client: new Inventory({ source: "https://example.com/inventory?wsdl" }),
145
+ prefix: "/api/inventory",
146
+ });
147
+ });
148
+
149
+ await app.listen({ port: 3000 });
150
+ ```
151
+
152
+ See [`examples/fastify-gateway/`](../examples/fastify-gateway/) for a complete example.
153
+
120
154
  ## Contract Assumptions
121
155
 
122
156
  - All request/response bodies must use $ref to components.schemas
@@ -72,3 +72,35 @@ result.GetCityWeatherByZIPResult.Temperature;
72
72
  ```
73
73
 
74
74
  Autocomplete and type checking work across all generated interfaces.
75
+
76
+ ## Gateway Route Handlers
77
+
78
+ When generating a Fastify gateway (`--gateway-dir`), each SOAP operation gets a fully typed route handler. The handler imports the request type from the client, uses Fastify's `Body: T` generic for type inference, and wraps the SOAP response in a standard envelope.
79
+
80
+ ```typescript
81
+ import type { FastifyInstance } from "fastify";
82
+ import type { GetCityForecastByZIP } from "../../client/types.js";
83
+ import schema from "../schemas/operations/getcityforecastbyzip.json" with { type: "json" };
84
+ import { buildSuccessEnvelope } from "../runtime.js";
85
+
86
+ export async function registerRoute_v1_weather_getcityforecastbyzip(fastify: FastifyInstance) {
87
+ fastify.route<{ Body: GetCityForecastByZIP }>({
88
+ method: "POST",
89
+ url: "/get-city-forecast-by-zip",
90
+ schema,
91
+ handler: async (request) => {
92
+ const client = fastify.weatherClient;
93
+ const result = await client.GetCityForecastByZIP(request.body as GetCityForecastByZIP);
94
+ return buildSuccessEnvelope(result.response);
95
+ },
96
+ });
97
+ }
98
+ ```
99
+
100
+ Key features of the generated handlers:
101
+
102
+ - **`Body: T` generic** — Fastify infers `request.body` type from the route generic, enabling IDE autocomplete and compile-time checks
103
+ - **JSON Schema validation** — the `schema` import provides Fastify with request/response validation at runtime, before the handler runs
104
+ - **Envelope wrapping** — `buildSuccessEnvelope()` wraps the raw SOAP response in the standard `{ status, message, data, error }` envelope
105
+
106
+ See [Gateway Guide](gateway-guide.md) for the full architecture and [CLI Reference](cli-reference.md) for generation flags.
package/llms.txt CHANGED
@@ -47,5 +47,7 @@ npx wsdl-tsc pipeline \
47
47
  - README.md: project overview and quick start
48
48
  - CONTRIBUTING.md: development setup, project structure, testing strategy
49
49
  - docs/migration.md: upgrade paths between versions
50
+ - .github/copilot-instructions.md: authoritative detailed agent instructions (referenced by AGENTS.md)
51
+ - docs/README.md: documentation directory index and conventions
50
52
  - npm: https://www.npmjs.com/package/@techspokes/typescript-wsdl-client
51
53
  - GitHub: https://github.com/TechSpokes/typescript-wsdl-client
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@techspokes/typescript-wsdl-client",
3
- "version": "0.10.1",
3
+ "version": "0.10.4",
4
4
  "description": "Generate type-safe TypeScript SOAP clients, OpenAPI 3.1 specs, and production-ready Fastify REST gateways from WSDL/XSD definitions.",
5
5
  "keywords": [
6
6
  "wsdl",
@@ -65,25 +65,26 @@
65
65
  "smoke:client": "npm run smoke:reset && tsx src/cli.ts client --wsdl-source examples/minimal/weather.wsdl --client-dir tmp/client && tsc -p tsconfig.smoke.json",
66
66
  "smoke:openapi": "npm run smoke:reset && tsx src/cli.ts openapi --wsdl-source examples/minimal/weather.wsdl --openapi-file tmp/openapi.json --openapi-format json && tsc -p tsconfig.smoke.json",
67
67
  "smoke:gateway": "npm run smoke:reset && tsx src/cli.ts client --wsdl-source examples/minimal/weather.wsdl --client-dir tmp/client && tsx src/cli.ts openapi --catalog-file tmp/client/catalog.json --openapi-file tmp/openapi.json --openapi-format json && tsx src/cli.ts gateway --openapi-file tmp/openapi.json --client-dir tmp/client --gateway-dir tmp/gateway --gateway-service-name weather --gateway-version-prefix v1 && tsc -p tsconfig.smoke.json",
68
- "smoke:pipeline": "npm run smoke:reset && tsx src/cli.ts pipeline --wsdl-source examples/minimal/weather.wsdl --client-dir tmp/client --openapi-file tmp/openapi.json --gateway-dir tmp/gateway --gateway-service-name weather --gateway-version-prefix v1 --openapi-format json --openapi-servers https://example.com/api --generate-app && tsc -p tsconfig.smoke.json",
69
- "smoke:app": "npm run smoke:reset && tsx src/cli.ts pipeline --wsdl-source examples/minimal/weather.wsdl --client-dir tmp/client --openapi-file tmp/openapi.json --gateway-dir tmp/gateway --gateway-service-name weather --gateway-version-prefix v1 --openapi-format json --openapi-servers https://example.com/api && tsx src/cli.ts app --client-dir tmp/client --gateway-dir tmp/gateway --openapi-file tmp/openapi.json --app-dir tmp/app && tsc -p tsconfig.smoke.json",
70
- "ci": "npm run clean && npm run build && npm run typecheck && npm run smoke:pipeline"
68
+ "smoke:pipeline": "npm run smoke:reset && tsx src/cli.ts pipeline --wsdl-source examples/minimal/weather.wsdl --client-dir tmp/client --openapi-file tmp/openapi.json --gateway-dir tmp/gateway --gateway-service-name weather --gateway-version-prefix v1 --openapi-format json --init-app && tsc -p tsconfig.smoke.json",
69
+ "smoke:app": "npm run smoke:reset && tsx src/cli.ts pipeline --wsdl-source examples/minimal/weather.wsdl --client-dir tmp/client --openapi-file tmp/openapi.json --gateway-dir tmp/gateway --gateway-service-name weather --gateway-version-prefix v1 --openapi-format json --openapi-servers https://example.com/api && tsx src/cli.ts app --client-dir tmp/client --gateway-dir tmp/gateway --openapi-file tmp/openapi.json --app-dir tmp/app --port 8080 && tsc -p tsconfig.smoke.json",
70
+ "ci": "npm run clean && npm run build && npm run typecheck && npm run smoke:pipeline",
71
+ "examples:regenerate": "tsx src/cli.ts pipeline --wsdl-source examples/minimal/weather.wsdl --client-dir examples/generated-output/client --openapi-file examples/generated-output/openapi.json --gateway-dir examples/generated-output/gateway --gateway-service-name weather --gateway-version-prefix v1 --openapi-format json"
71
72
  },
72
73
  "devDependencies": {
73
74
  "@types/js-yaml": "^4.0.9",
74
- "@types/node": "^25.0.2",
75
- "@types/yargs": "^17.0.33",
76
- "fastify": "^5.2.0",
75
+ "@types/node": "^25.2.0",
76
+ "@types/yargs": "^17.0.35",
77
+ "fastify": "^5.7.0",
77
78
  "fastify-plugin": "^5.1.0",
78
- "rimraf": "^6.0.0",
79
- "tsx": "^4.20.0",
80
- "typescript": "^5.6.3"
79
+ "rimraf": "^6.1.0",
80
+ "tsx": "^4.21.0",
81
+ "typescript": "^5.9.0"
81
82
  },
82
83
  "dependencies": {
83
84
  "@apidevtools/swagger-parser": "^12.1.0",
84
- "fast-xml-parser": "^5.2.5",
85
+ "fast-xml-parser": "^5.3.0",
85
86
  "js-yaml": "^4.1.1",
86
- "soap": "^1.3.0",
87
+ "soap": "^1.6.0",
87
88
  "yargs": "^18.0.0"
88
89
  },
89
90
  "funding": {