@techspokes/typescript-wsdl-client 0.2.0 → 0.2.5

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
@@ -11,96 +11,93 @@
11
11
  [![TechSpokes Org](https://img.shields.io/badge/org-techspokes-181717?logo=github)](https://github.com/techspokes)
12
12
  [![Sponsor TechSpokes](https://img.shields.io/badge/sponsor-GitHub-blue?logo=github-sponsors)](https://github.com/sponsors/TechSpokes)
13
13
 
14
- **TypeScript WSDL → SOAP client generator.**
15
- Reads WSDL/XSD (with imports) and emits a small, typed client you can compile into your app.
14
+ ## Introduction
16
15
 
17
- * `xs:attribute` support (attributes become first-class fields, not bags)
18
- * `complexType` (`sequence` / `all` / `choice`)
19
- * `simpleContent` / `complexContent` (`extension` + `restriction`)
20
- * Inline **anonymous** types (auto-named, stable)
21
- * Global `element @ref` resolution
22
- * Duplicate/local-name **merge** across schemas/namespaces
23
- * Deterministic metadata (`ATTR_SPEC`, `PROP_META`) for clean JSON ⇄ SOAP mapping
24
- * ESM **and** CommonJS friendly output
16
+ **TypeScript WSDL Client** is a generator that converts WSDL/XSD files into a fully-typed SOAP client for TypeScript. It simplifies SOAP integration by generating deterministic, type-safe code that works seamlessly with modern TypeScript and Node.js environments.
25
17
 
26
- Vendor: **[TechSpokes](https://www.techspokes.com)** · Contact: **[contact page](https://www.techspokes.com/contact/)"**
27
- Maintainer: **Serge Liatko** ([@sergeliatko](https://github.com/sergeliatko)) · GitHub org: [@techspokes](https://github.com/techspokes)
18
+ Key features:
19
+ - **Typed SOAP client**: Generates TypeScript interfaces and runtime code.
20
+ - **Deterministic metadata**: Ensures clean JSON ⇄ SOAP mapping.
21
+ - **ESM and CommonJS support**: Compatible with modern and legacy module systems.
22
+ - **Customizable mappings**: Control how XML primitives (e.g., `xs:decimal`, `xs:date`) are mapped.
23
+
24
+ Vendor: **[TechSpokes](https://www.techspokes.com)**
25
+ Maintainer: **Serge Liatko** ([@sergeliatko](https://github.com/sergeliatko))
28
26
 
29
27
  ---
30
28
 
31
- ## Quick start
29
+ ## Installation
32
30
 
33
- Install the generator (dev-time) in your project:
31
+ Install the generator as a development dependency:
34
32
 
35
33
  ```bash
36
- npm i -D typescript-wsdl-client
37
- # your app will use node-soap at runtime:
34
+ npm i -D @techspokes/typescript-wsdl-client
35
+ # Your app will use node-soap at runtime:
38
36
  npm i soap
39
37
  ```
40
38
 
41
- Generate into your app (recommended under `src/generated/...`):
39
+ ---
40
+
41
+ ## Quick Start
42
+
43
+ ### Generate a SOAP Client
44
+
45
+ Run the following command to generate a client from your WSDL file:
42
46
 
43
47
  ```bash
44
48
  npx wsdl-tsc --wsdl ./spec/wsdl/MyService.wsdl --out ./src/generated/my --imports js --ops-ts
45
49
  ```
46
50
 
47
- Use the generated client:
51
+ ### Use the Generated Client
48
52
 
49
53
  ```ts
50
- // ESM / NodeNext app
51
54
  import { createSoapClient } from "./generated/my/runtime.js";
52
- import { GeneratedSoapClient } from "./generated/my/client.js";
55
+ import { MyServiceSoapClient } from "./generated/my/client.js";
56
+ import soap from "soap";
53
57
 
54
- const soap = await createSoapClient({ wsdlUrl: "https://example.com/MyService?wsdl" });
55
- const client = new GeneratedSoapClient(soap, "$attributes"); // attributes key optional
58
+ const security = new soap.WSSecurity("user", "pass");
59
+ const soapClient = await createSoapClient({
60
+ wsdlUrl: "https://example.com/MyService?wsdl",
61
+ security,
62
+ });
56
63
 
57
- const rs = await client.MyOperation({
64
+ const client = new MyServiceSoapClient(soapClient, "$attributes");
65
+ const response = await client.MyOperation({
58
66
  MyOperationRQ: {
59
- MyAttribute: "passed as attribute",
60
- MyElement: "passed as child element",
61
- // ...other fields
62
- // attributes appear as top-level props; child elements as nested objects
63
- }
67
+ MyElement: {
68
+ MyAttribute: "value",
69
+ ChildElementA: "valueA",
70
+ },
71
+ },
64
72
  });
65
73
 
66
- console.log(rs);
74
+ console.log(response);
67
75
  ```
68
76
 
69
- > The generator always emits TypeScript sources (`*.ts`). You compile them with your app.
70
-
71
77
  ---
72
78
 
73
- ## CLI
79
+ ## Features
74
80
 
75
- ```
76
- wsdl-tsc --wsdl <path-or-url> --out <dir> [options]
77
- ```
81
+ - **Attributes and Child Elements**: Supports both attributes and nested elements in SOAP requests.
82
+ - **Literal Text Values**: Handles mixed content (attributes + text).
83
+ - **Security Integration**: Works with `node-soap` security instances (e.g., `WSSecurity`, `BasicAuthSecurity`).
84
+ - **WS-Policy Hints**: Provides inline hints for security configuration based on WSDL policies.
78
85
 
79
- ### Local development
80
-
81
- By default, `npx wsdl-tsc` invokes the published npm version. To run the CLI from your local source (with your latest changes), use one of these approaches:
86
+ ---
82
87
 
83
- ```bash
84
- # Directly via tsx (requires tsx in devDependencies)
85
- npx tsx src/cli.ts --wsdl <path-or-url> --out <dir> [options]
88
+ ## CLI Usage
86
89
 
87
- # Via npm script
88
- git clone ... then:
89
- npm install
90
- git checkout <branch>
91
- npm run dev -- --wsdl <path-or-url> --out <dir> [options]
90
+ The CLI is the primary way to generate SOAP clients.
92
91
 
93
- # Using npm link to symlink your working copy
94
- npm link
92
+ ```bash
95
93
  wsdl-tsc --wsdl <path-or-url> --out <dir> [options]
96
94
  ```
97
95
 
98
- **Required**
99
-
100
- * `--wsdl` WSDL path or URL
101
- * `--out` — output directory (created if missing)
96
+ ### Required Flags
97
+ - `--wsdl`: Path or URL to the WSDL file.
98
+ - `--out`: Output directory for the generated files.
102
99
 
103
- **Options**
100
+ ### Options
104
101
 
105
102
  | Flag | Type | Choices | Default | Description |
106
103
  |---------------------|-----------|--------------------------------|--------------|------------------------------------------------------------------|
@@ -112,153 +109,62 @@ wsdl-tsc --wsdl <path-or-url> --out <dir> [options]
112
109
  | `--decimal-as` | string | string, number | string | How to map xs:decimal (money/precision) |
113
110
  | `--date-as` | string | string, Date | string | How to map date/time/duration types |
114
111
 
115
- **Primitive mapping (safe defaults)**
116
-
117
- Defaults are **string-first** to avoid precision & timezone surprises:
118
-
119
- * `xs:decimal` → `string` (money/precision safe)
120
- * 64-bit integers → `string` (you can opt into `bigint` or `number`)
121
- * dates/times → `string` (transport-friendly, no implicit tz conversion)
112
+ ---
122
113
 
123
- Override these defaults using the CLI flags above as needed for your use case.
114
+ ## Generated Files
124
115
 
125
- # What gets generated
116
+ The generator produces the following files in the output directory:
126
117
 
127
118
  ```
128
119
  <out>/
129
- types.ts # interfaces + type aliases (with @xsd JSDoc: original XML type/occurs)
130
- client.ts # thin operation wrapper (calls into runtime)
131
- runtime.ts # small SOAP runtime: createSoapClient, toSoapArgs, fromSoapResult
132
- meta.ts # ATTR_SPEC, CHILD_TYPE, PROP_META for JSON ⇄ SOAP mapping
133
- operations.ts # compiled operation metadata (name, soapAction, etc.)
134
- ```
135
-
136
- Example: if `User` has `@Id` and `@CreatedAt`, you’ll see:
137
-
138
- ```ts
139
- interface User {
140
- Id?: string;
141
- CreatedAt?: string; // or Date if you chose --date-as Date
142
- }
143
- ```
144
-
145
- ---
146
-
147
- ## Using in different app module systems
148
-
149
- ### ESM / NodeNext (recommended)
150
-
151
- Service `tsconfig.json`:
152
-
153
- ```json
154
- {
155
- "compilerOptions": {
156
- "target": "ES2022",
157
- "module": "NodeNext",
158
- "moduleResolution": "NodeNext",
159
- "strict": true,
160
- "esModuleInterop": true,
161
- "skipLibCheck": true
162
- }
163
- }
120
+ types.ts # TypeScript interfaces and type aliases
121
+ client.ts # Thin wrapper for SOAP operations
122
+ runtime.ts # SOAP runtime utilities
123
+ meta.ts # Metadata for JSON ⇄ SOAP mapping
124
+ operations.ts # Operation metadata (optional, based on --ops-ts)
164
125
  ```
165
126
 
166
- Generate with `--imports js` and import `./client.js`, `./runtime.js`.
167
-
168
- ### CommonJS
169
-
170
- Keep your app `module: "CommonJS"` and generate with `--imports bare` (imports like `./runtime`).
171
- TypeScript will compile to `require("./runtime")` cleanly.
172
-
173
127
  ---
174
128
 
175
- ## Recommended workflow
129
+ ## Advanced Usage
176
130
 
177
- * **Vendor** your WSDL(s) in `spec/wsdl/` for reproducible builds.
178
- * Generate into `src/generated/<service>/` and **commit the generated files** (deterministic CI/Docker).
179
- * Build your app (the generated code compiles with it).
131
+ ### Programmatic API
180
132
 
181
- **Example scripts (app `package.json`):**
182
-
183
- ```json
184
- {
185
- "scripts": {
186
- "codegen:my": "wsdl-tsc --wsdl ./spec/wsdl/MyService.wsdl --out ./src/generated/my --imports js --ops-ts",
187
- "prebuild": "npm run codegen:my",
188
- "build": "tsc -p tsconfig.json"
189
- }
190
- }
191
- ```
192
-
193
- ---
194
-
195
- ## Programmatic API (optional)
133
+ You can use the generator programmatically for custom workflows:
196
134
 
197
135
  ```ts
198
- import { compileCatalog, xsdToTsPrimitive, type CompilerOptions } from "typescript-wsdl-client";
199
- import { loadWsdlCatalog } from "typescript-wsdl-client/internal-or-your-loader"; // if you expose it
136
+ import { compileCatalog } from "@techspokes/typescript-wsdl-client";
200
137
 
201
138
  const catalog = await loadWsdlCatalog("./spec/wsdl/MyService.wsdl");
202
139
  const compiled = compileCatalog(catalog, {
203
- primitive: { decimalAs: "string", dateAs: "string" }
140
+ primitive: { decimalAs: "string", dateAs: "string" },
204
141
  });
205
- // …write your own emitters or use the built-ins in the CLI.
206
- ```
207
-
208
- ---
209
-
210
- ## Primitive mapping rationale
211
-
212
- Defaults are **string-first** to avoid precision & timezone surprises:
213
-
214
- * `xs:decimal` → `string` (money/precision safe)
215
- * 64-bit integers → `string` (you can opt into `bigint` or `number`)
216
- * dates/times → `string` (transport-friendly, no implicit tz conversion)
217
-
218
- Change per your needs with the CLI flags above.
219
-
220
- ---
221
142
 
222
- ## Minimal test you can run
223
-
224
- ```bash
225
- # generate from a local WSDL
226
- npx wsdl-tsc --wsdl ./spec/wsdl/MyService.wsdl --out ./tmp --imports js --ops-ts
227
-
228
- # quick typecheck of generated output (NodeNext)
229
- npx tsc --noEmit --module NodeNext --moduleResolution NodeNext --target ES2022 ./tmp/*.ts
143
+ // Use the compiled output as needed.
230
144
  ```
231
145
 
232
146
  ---
233
147
 
234
148
  ## Troubleshooting
235
149
 
236
- * **“I don’t see `runtime.ts`”** — You should. Ensure you’re on a recent version and check that the output directory is writable.
237
- * **“Cannot find './runtime.js'”** Your app must be `module: "NodeNext"` when using `--imports js`.
238
- Use `--imports bare` for CommonJS apps.
239
- * **“node-soap not found”** — Install it in your **app**: `npm i soap`.
150
+ - **Missing `runtime.ts`**: Ensure the output directory is writable and you're using the latest version.
151
+ - **Module system issues**: Use `--imports js` for ESM/NodeNext or `--imports bare` for CommonJS.
152
+ - **Security warnings**: Configure `node-soap` security (e.g., `WSSecurity`) as needed.
240
153
 
241
154
  ---
242
155
 
243
- ## Contributing
244
-
245
- Issues and PRs welcome. Please include a **minimal WSDL/XSD** fixture that reproduces the case.
246
- Node 20+ supported.
156
+ ## Roadmap
247
157
 
248
- - See CONTRIBUTING.md for setup and workflow.
249
- - See CODE_OF_CONDUCT.md for community expectations.
250
- - Security reports: see SECURITY.md.
158
+ Please see the [ROADMAP.md](ROADMAP.md) for planned features and improvements.
251
159
 
252
160
  ---
253
161
 
254
- ## Community
162
+ ## Contributing
255
163
 
256
- - Contributing guide: CONTRIBUTING.md
257
- - Code of Conduct: CODE_OF_CONDUCT.md
258
- - Security policy: SECURITY.md
259
- - Support: SUPPORT.md
260
- - Roadmap: ROADMAP.md
261
- - Changelog: CHANGELOG.md
164
+ We welcome contributions! Please see the following resources:
165
+ - [CONTRIBUTING.md](CONTRIBUTING.md): Development workflow and guidelines.
166
+ - [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md): Community expectations.
167
+ - [SECURITY.md](SECURITY.md): Reporting vulnerabilities.
262
168
 
263
169
  ---
264
170
 
@@ -275,9 +181,10 @@ MIT © TechSpokes.
275
181
  - Your Name Here!
276
182
 
277
183
  **Gold Sponsors**
278
- - [Your Name or Company](https://your-link-here.com)
184
+ - [Your Name or Company (with a link) Here!](https://your-link-here.com)
279
185
 
280
186
  **Platinum Sponsors**
281
- - [Your Name or Company](https://your-link-here.com) – 30-min one-to-one video meeting on AI, business automations, vacation rentals industry, development, tools, or a subject of your choice.
187
+ - [Your Name or Company (with a link) Here!](https://your-link-here.com)
188
+ - **AND** 30-min one-to-one video meeting on AI, business automations, vacation rentals industry, development, tools, or a subject of your choice.
282
189
 
283
190
  Want to see your name or company here? [Become a sponsor!](https://github.com/sponsors/TechSpokes)
package/dist/cli.js CHANGED
@@ -37,6 +37,10 @@ const argv = await yargs(hideBin(process.argv))
37
37
  type: "string",
38
38
  default: "$attributes",
39
39
  desc: "Key used by runtime marshaller for XML attributes",
40
+ })
41
+ .option("client-name", {
42
+ type: "string",
43
+ desc: "Override the generated client class name (exact export name)",
40
44
  })
41
45
  // Primitive mapping knobs (safe defaults)
42
46
  .option("int64-as", {
@@ -94,5 +98,6 @@ emitClient(path.join(outDir, "client.ts"), compiled, {
94
98
  // @ts-ignore runtime-only options for emitter
95
99
  importExt,
96
100
  attributesKey: String(argv["attributes-key"]),
101
+ clientName: argv["client-name"],
97
102
  });
98
103
  console.log(`✅ Generated TypeScript client in ${outDir}`);
@@ -22,6 +22,21 @@ export type CompiledType = {
22
22
  declaredType: string;
23
23
  }>;
24
24
  jsdoc?: string;
25
+ base?: string;
26
+ localAttrs?: Array<{
27
+ name: string;
28
+ tsType: string;
29
+ use?: "required" | "optional";
30
+ declaredType: string;
31
+ }>;
32
+ localElems?: Array<{
33
+ name: string;
34
+ tsType: string;
35
+ min: number;
36
+ max: number | "unbounded";
37
+ nillable?: boolean;
38
+ declaredType: string;
39
+ }>;
25
40
  };
26
41
  export type CompiledAlias = {
27
42
  name: string;
@@ -43,8 +58,11 @@ export type CompiledCatalog = {
43
58
  soapAction: string;
44
59
  inputElement?: QName;
45
60
  outputElement?: QName;
61
+ security?: string[];
46
62
  }>;
47
63
  wsdlTargetNS: string;
64
+ serviceName?: string;
65
+ wsdlUri: string;
48
66
  };
49
67
  /**
50
68
  * Compile a WSDL catalog into an internal representation (CompiledCatalog).
@@ -1 +1 @@
1
- {"version":3,"file":"schemaCompiler.d.ts","sourceRoot":"","sources":["../../src/compiler/schemaCompiler.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AACpD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AAU3D,MAAM,MAAM,KAAK,GAAG;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC;AAElD,MAAM,MAAM,YAAY,GAAG;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,KAAK,CAAC;QACX,IAAI,EAAE,MAAM,CAAC;QACb,MAAM,EAAE,MAAM,CAAC;QACf,GAAG,CAAC,EAAE,UAAU,GAAG,UAAU,CAAC;QAC9B,YAAY,EAAE,MAAM,CAAC;KACtB,CAAC,CAAC;IACH,KAAK,EAAE,KAAK,CAAC;QACX,IAAI,EAAE,MAAM,CAAC;QACb,MAAM,EAAE,MAAM,CAAC;QACf,GAAG,EAAE,MAAM,CAAC;QACZ,GAAG,EAAE,MAAM,GAAG,WAAW,CAAC;QAC1B,QAAQ,CAAC,EAAE,OAAO,CAAC;QACnB,YAAY,EAAE,MAAM,CAAC;KACtB,CAAC,CAAC;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF,MAAM,MAAM,aAAa,GAAG;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF,MAAM,MAAM,eAAe,GAAG;IAC5B,KAAK,EAAE,YAAY,EAAE,CAAC;IACtB,OAAO,EAAE,aAAa,EAAE,CAAC;IACzB,IAAI,EAAE;QACJ,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;QACnC,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;QAClD,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC,CAAC;KAC/C,CAAC;IACF,UAAU,EAAE,KAAK,CAAC;QAChB,IAAI,EAAE,MAAM,CAAC;QACb,UAAU,EAAE,MAAM,CAAC;QACnB,YAAY,CAAC,EAAE,KAAK,CAAC;QACrB,aAAa,CAAC,EAAE,KAAK,CAAC;KACvB,CAAC,CAAC;IACH,YAAY,EAAE,MAAM,CAAC;CACtB,CAAC;AAsBF;;;;;;;;;GASG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,WAAW,EAAE,KAAK,EAAE,eAAe,GAAG,eAAe,CA+exF"}
1
+ {"version":3,"file":"schemaCompiler.d.ts","sourceRoot":"","sources":["../../src/compiler/schemaCompiler.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AACpD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AAU3D,MAAM,MAAM,KAAK,GAAG;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC;AAElD,MAAM,MAAM,YAAY,GAAG;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,KAAK,CAAC;QACX,IAAI,EAAE,MAAM,CAAC;QACb,MAAM,EAAE,MAAM,CAAC;QACf,GAAG,CAAC,EAAE,UAAU,GAAG,UAAU,CAAC;QAC9B,YAAY,EAAE,MAAM,CAAC;KACtB,CAAC,CAAC;IACH,KAAK,EAAE,KAAK,CAAC;QACX,IAAI,EAAE,MAAM,CAAC;QACb,MAAM,EAAE,MAAM,CAAC;QACf,GAAG,EAAE,MAAM,CAAC;QACZ,GAAG,EAAE,MAAM,GAAG,WAAW,CAAC;QAC1B,QAAQ,CAAC,EAAE,OAAO,CAAC;QACnB,YAAY,EAAE,MAAM,CAAC;KACtB,CAAC,CAAC;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;IAEd,UAAU,CAAC,EAAE,KAAK,CAAC;QACjB,IAAI,EAAE,MAAM,CAAC;QACb,MAAM,EAAE,MAAM,CAAC;QACf,GAAG,CAAC,EAAE,UAAU,GAAG,UAAU,CAAC;QAC9B,YAAY,EAAE,MAAM,CAAC;KACtB,CAAC,CAAC;IAEH,UAAU,CAAC,EAAE,KAAK,CAAC;QACjB,IAAI,EAAE,MAAM,CAAC;QACb,MAAM,EAAE,MAAM,CAAC;QACf,GAAG,EAAE,MAAM,CAAC;QACZ,GAAG,EAAE,MAAM,GAAG,WAAW,CAAC;QAC1B,QAAQ,CAAC,EAAE,OAAO,CAAC;QACnB,YAAY,EAAE,MAAM,CAAC;KACtB,CAAC,CAAC;CACJ,CAAC;AAEF,MAAM,MAAM,aAAa,GAAG;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF,MAAM,MAAM,eAAe,GAAG;IAC5B,KAAK,EAAE,YAAY,EAAE,CAAC;IACtB,OAAO,EAAE,aAAa,EAAE,CAAC;IACzB,IAAI,EAAE;QACJ,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;QACnC,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;QAClD,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC,CAAC;KAC/C,CAAC;IACF,UAAU,EAAE,KAAK,CAAC;QAChB,IAAI,EAAE,MAAM,CAAC;QACb,UAAU,EAAE,MAAM,CAAC;QACnB,YAAY,CAAC,EAAE,KAAK,CAAC;QACrB,aAAa,CAAC,EAAE,KAAK,CAAC;QACtB,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;KACrB,CAAC,CAAC;IACH,YAAY,EAAE,MAAM,CAAC;IAErB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,OAAO,EAAE,MAAM,CAAC;CACjB,CAAC;AA6DF;;;;;;;;;GASG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,WAAW,EAAE,KAAK,EAAE,eAAe,GAAG,eAAe,CA6gBxF"}
@@ -13,6 +13,56 @@ function makeInlineTypeName(parentTypeName, propName, _max) {
13
13
  }
14
14
  return `${base}Anon`;
15
15
  }
16
+ // --- Minimal WS-Policy scanning (inline policies only; PolicyReference not resolved) ---
17
+ function collectSecurityFromPolicyNodes(policyNodes) {
18
+ const found = new Set();
19
+ const targets = new Set([
20
+ "UsernameToken",
21
+ "TransportBinding",
22
+ "HttpsToken",
23
+ "TransportToken",
24
+ "AsymmetricBinding",
25
+ "SymmetricBinding",
26
+ "SignedSupportingTokens",
27
+ "X509Token",
28
+ ]);
29
+ const visit = (n) => {
30
+ if (n == null || typeof n !== "object")
31
+ return;
32
+ for (const [k, v] of Object.entries(n)) {
33
+ if (k.startsWith("@_"))
34
+ continue; // attribute
35
+ // localName match (prefix-agnostic) by checking tail after ':' or whole key if no ':'
36
+ const ln = k.includes(":") ? k.split(":").pop() : k;
37
+ if (targets.has(ln)) {
38
+ switch (ln) {
39
+ case "UsernameToken":
40
+ found.add("usernameToken");
41
+ break;
42
+ case "HttpsToken":
43
+ case "TransportBinding":
44
+ case "TransportToken":
45
+ found.add("https");
46
+ break;
47
+ case "X509Token":
48
+ found.add("x509");
49
+ break;
50
+ case "AsymmetricBinding":
51
+ case "SymmetricBinding":
52
+ case "SignedSupportingTokens":
53
+ found.add("messageSecurity");
54
+ break;
55
+ }
56
+ }
57
+ // recurse
58
+ if (v && typeof v === "object")
59
+ visit(v);
60
+ }
61
+ };
62
+ for (const p of policyNodes)
63
+ visit(p);
64
+ return Array.from(found);
65
+ }
16
66
  /**
17
67
  * Compile a WSDL catalog into an internal representation (CompiledCatalog).
18
68
  * Steps:
@@ -198,23 +248,18 @@ export function compileCatalog(cat, _opts) {
198
248
  // resolves type refs or @ref, applies min/max occurrence and nillable flags
199
249
  const collectParticles = (ownerTypeName, node) => {
200
250
  const out = [];
201
- const groups = [
202
- ...getChildrenWithLocalName(node, "sequence"),
203
- ...getChildrenWithLocalName(node, "all"),
204
- ...getChildrenWithLocalName(node, "choice"),
205
- ];
206
- for (const grp of groups) {
207
- for (const e of getChildrenWithLocalName(grp, "element")) {
251
+ // process a compositor or element container recursively
252
+ const recurse = (groupNode) => {
253
+ // handle direct element children
254
+ for (const e of getChildrenWithLocalName(groupNode, "element")) {
208
255
  const nameOrRef = e["@_name"] || e["@_ref"];
209
- if (!nameOrRef) {
256
+ if (!nameOrRef)
210
257
  continue;
211
- }
212
258
  const propName = e["@_name"];
213
259
  const min = e["@_minOccurs"] ? Number(e["@_minOccurs"]) : 1;
214
260
  const maxAttr = e["@_maxOccurs"];
215
261
  const max = maxAttr === "unbounded" ? "unbounded" : maxAttr ? Number(maxAttr) : 1;
216
262
  const nillable = e["@_nillable"] === "true";
217
- // inline complexType: create a nested interface with a generated name
218
263
  const inlineComplex = getFirstWithLocalName(e, "complexType");
219
264
  const inlineSimple = getFirstWithLocalName(e, "simpleType");
220
265
  if (inlineComplex) {
@@ -223,19 +268,24 @@ export function compileCatalog(cat, _opts) {
223
268
  out.push({ name: propName || nameOrRef, tsType: rec.name, min, max, nillable, declaredType: `{${schemaNS}}${rec.name}` });
224
269
  }
225
270
  else if (inlineSimple) {
226
- // inline simpleType (e.g., list or enumeration)
227
271
  const r = compileSimpleTypeNode(inlineSimple, schemaNS, prefixes);
228
272
  out.push({ name: propName || nameOrRef, tsType: r.tsType, min, max, nillable, declaredType: r.declared });
229
273
  }
230
274
  else {
231
- // named type or ref: resolve via QName
232
275
  const t = e["@_type"] || e["@_ref"];
233
276
  const q = t ? resolveQName(t, schemaNS, prefixes) : { ns: XS, local: "string" };
234
277
  const r = resolveTypeRef(q, schemaNS, prefixes);
235
278
  out.push({ name: propName || nameOrRef, tsType: r.tsType, min, max, nillable, declaredType: r.declared });
236
279
  }
237
280
  }
238
- }
281
+ // recurse into nested compositor groups
282
+ for (const comp of ["sequence", "all", "choice"]) {
283
+ for (const sub of getChildrenWithLocalName(groupNode, comp)) {
284
+ recurse(sub);
285
+ }
286
+ }
287
+ };
288
+ recurse(node);
239
289
  return out;
240
290
  };
241
291
  // Result accumulators
@@ -248,21 +298,26 @@ export function compileCatalog(cat, _opts) {
248
298
  const res = getFirstWithLocalName(complexContent, "restriction");
249
299
  const node = ext || res;
250
300
  if (node) {
301
+ // handle complexContent extension: record base and separate local additions
302
+ let baseName;
251
303
  const baseAttr = node["@_base"];
252
304
  if (baseAttr) {
253
305
  const baseQ = resolveQName(baseAttr, schemaNS, prefixes);
254
306
  const baseRec = findComplexRec(baseQ);
255
307
  if (baseRec) {
256
- const base = getOrCompileComplex(baseRec.node["@_name"], baseRec.node, baseRec.tns, baseRec.prefixes);
257
- // inherit base
258
- mergeAttrs(attrs, base.attrs);
259
- mergeElems(elems, base.elems);
308
+ const baseType = getOrCompileComplex(baseRec.node["@_name"], baseRec.node, baseRec.tns, baseRec.prefixes);
309
+ baseName = baseType.name;
310
+ // inherit base members
311
+ attrs.push(...baseType.attrs);
312
+ elems.push(...baseType.elems);
260
313
  }
261
314
  }
262
- // local additions/overrides
263
- mergeAttrs(attrs, collectAttributes(node));
264
- mergeElems(elems, collectParticles(outName, node));
265
- const result = { name: outName, ns: schemaNS, attrs, elems };
315
+ // collect local additions/overrides
316
+ const locals = collectAttributes(node);
317
+ const localElems = collectParticles(outName, node);
318
+ attrs.push(...locals);
319
+ elems.push(...localElems);
320
+ const result = { name: outName, ns: schemaNS, attrs, elems, base: baseName, localAttrs: locals, localElems };
266
321
  compiledMap.set(key, result);
267
322
  inProgress.delete(key);
268
323
  return result;
@@ -469,11 +524,38 @@ export function compileCatalog(cat, _opts) {
469
524
  return { name, soapAction: bOps.get(name) || "", inputElement, outputElement };
470
525
  })
471
526
  .filter((x) => x != null));
527
+ // --- WS-Policy: scan for security requirements (inline policies only) ---
528
+ const bindingPolicies = getChildrenWithLocalName(soapBinding || {}, "Policy");
529
+ const bindingSec = collectSecurityFromPolicyNodes(bindingPolicies);
530
+ for (const op of ops) {
531
+ const bo = opsNodes.find(o => o?.["@_name"] === op.name) || {};
532
+ const opPolicies = getChildrenWithLocalName(bo, "Policy");
533
+ const opSec = collectSecurityFromPolicyNodes(opPolicies);
534
+ const secSet = new Set([...bindingSec, ...opSec]);
535
+ op.security = Array.from(secSet);
536
+ }
537
+ // --- Service discovery (for client naming) ---
538
+ let serviceName;
539
+ const soapBindingName = soapBinding?.["@_name"];
540
+ const serviceDefs = normalizeArray(defs?.["wsdl:service"] || defs?.["service"]);
541
+ const serviceUsingBinding = serviceDefs.find(s => {
542
+ const ports = getChildrenWithLocalName(s, "port");
543
+ return ports.some(p => {
544
+ const bq = p?.["@_binding"];
545
+ if (!bq || !soapBindingName)
546
+ return false;
547
+ const q = resolveQName(bq, tns, cat.prefixMap);
548
+ return q.local === soapBindingName; // match by local name
549
+ });
550
+ });
551
+ serviceName = serviceUsingBinding?.["@_name"] || serviceDefs[0]?.["@_name"];
472
552
  return {
473
553
  types: typesList,
474
554
  aliases: aliasList,
475
555
  meta: { attrSpec, childType, propMeta },
476
556
  operations: ops,
477
557
  wsdlTargetNS: defs?.["@_targetNamespace"] || "",
558
+ serviceName,
559
+ wsdlUri: cat.wsdlUri,
478
560
  };
479
561
  }
package/dist/config.d.ts CHANGED
@@ -23,6 +23,11 @@ export type CompilerOptions = {
23
23
  * Attribute bag key for the runtime mapper (node-soap).
24
24
  */
25
25
  attributesKey?: string;
26
+ /**
27
+ * Optional override for the generated client class name.
28
+ * If provided, the emitter will export this exact class name.
29
+ */
30
+ clientName?: string;
26
31
  /**
27
32
  * Controls low-level mapping of XSD primitives to TypeScript types. Safe defaults are provided.
28
33
  */
@@ -1 +1 @@
1
- {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAE5D;;GAEG;AACH,MAAM,MAAM,eAAe,GAAG;IAC1B;;OAEG;IACH,MAAM,CAAC,EAAE,cAAc,GAAG,OAAO,CAAC;IAClC;;OAEG;IACH,MAAM,CAAC,EAAE,QAAQ,GAAG,MAAM,CAAC;IAC3B;;OAEG;IACH,KAAK,CAAC,EAAE,QAAQ,GAAG,QAAQ,CAAC;IAC5B;;OAEG;IACH,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B;;OAEG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB;;OAEG;IACH,SAAS,CAAC,EAAE,gBAAgB,CAAC;CAChC,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,cAAc,EAAE,eAM5B,CAAC"}
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAE5D;;GAEG;AACH,MAAM,MAAM,eAAe,GAAG;IAC1B;;OAEG;IACH,MAAM,CAAC,EAAE,cAAc,GAAG,OAAO,CAAC;IAClC;;OAEG;IACH,MAAM,CAAC,EAAE,QAAQ,GAAG,MAAM,CAAC;IAC3B;;OAEG;IACH,KAAK,CAAC,EAAE,QAAQ,GAAG,QAAQ,CAAC;IAC5B;;OAEG;IACH,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B;;OAEG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB;;;OAGG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;OAEG;IACH,SAAS,CAAC,EAAE,gBAAgB,CAAC;CAChC,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,cAAc,EAAE,eAM5B,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"clientEmitter.d.ts","sourceRoot":"","sources":["../../src/emit/clientEmitter.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,+BAA+B,CAAC;AACrE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAGpD,wBAAgB,UAAU,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,eAAe,EAAE,IAAI,EAAE,eAAe,GAAG;IAAE,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE,QAmCpH"}
1
+ {"version":3,"file":"clientEmitter.d.ts","sourceRoot":"","sources":["../../src/emit/clientEmitter.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,+BAA+B,CAAC;AACrE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAGpD,wBAAgB,UAAU,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,eAAe,EAAE,IAAI,EAAE,eAAe,GAAG;IAAE,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE,QAmEpH"}
@@ -7,7 +7,27 @@ export function emitClient(outFile, compiled, opts) {
7
7
  lines.push(`import { ATTR_SPEC, CHILD_TYPE, PROP_META } from "./meta${ext}";`);
8
8
  lines.push(`import type * as T from "./types${ext}";`);
9
9
  lines.push("");
10
- lines.push(`export class GeneratedSoapClient {`);
10
+ // Derive class name: prefer explicit override, else WSDL service name, else base filename, else default
11
+ const overrideName = (opts.clientName || "").trim();
12
+ const svcName = compiled.serviceName && pascal(compiled.serviceName);
13
+ const fileBase = (() => {
14
+ const uri = compiled.wsdlUri || "";
15
+ // extract last path segment and strip extension for both URL and file path
16
+ const seg = uri.split(/[\\/]/).pop() || "";
17
+ const noExt = seg.replace(/\.[^.]+$/, "");
18
+ return noExt ? pascal(noExt) : "";
19
+ })();
20
+ const className = overrideName || ((svcName || fileBase) ? `${svcName || fileBase}SoapClient` : "GeneratedSoapClient");
21
+ // Helpers for emitting safe method names
22
+ const isValidIdent = (name) => /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(name);
23
+ const reserved = new Set([
24
+ "break", "case", "catch", "class", "const", "continue", "debugger", "default", "delete",
25
+ "do", "else", "enum", "export", "extends", "false", "finally", "for", "function", "if",
26
+ "import", "in", "instanceof", "new", "null", "return", "super", "switch", "this", "throw",
27
+ "true", "try", "typeof", "var", "void", "while", "with", "as", "implements", "interface",
28
+ "let", "package", "private", "protected", "public", "static", "yield", "constructor"
29
+ ]);
30
+ lines.push(`export class ${className} {`);
11
31
  lines.push(` constructor(private source: string | any, private attributesKey: string = "${opts.attributesKey || "$attributes"}") {}`);
12
32
  lines.push(` async _client() {`);
13
33
  lines.push(` if (typeof this.source === 'string') return createSoapClient({ wsdlUrl: this.source });`);
@@ -21,14 +41,24 @@ export function emitClient(outFile, compiled, opts) {
21
41
  const outTs = outTypeName ? `T.${outTypeName}` : `any`;
22
42
  const inMetaKey = inTypeName ?? m;
23
43
  const outMetaKey = outTypeName ?? m;
24
- lines.push(` /** SOAPAction: ${op.soapAction} */`);
25
- lines.push(` async ${m}(args: ${inTs}): Promise<${outTs}> {`);
44
+ const secHints = Array.isArray(op.security) && op.security.length ? op.security : [];
45
+ const methodHeader = (isValidIdent(m) && !reserved.has(m))
46
+ ? ` async ${m}(args: ${inTs}): Promise<${outTs}> {`
47
+ : ` async [${JSON.stringify(m)}](args: ${inTs}): Promise<${outTs}> {`;
48
+ const clientCall = (isValidIdent(m) && !reserved.has(m))
49
+ ? `c.${m}`
50
+ : `c[${JSON.stringify(m)}]`;
51
+ lines.push(` /** SOAPAction: ${op.soapAction}${secHints.length ? `\n * Security (WSDL policy hint): ${secHints.join(", ")}` : ""} */`);
52
+ lines.push(methodHeader);
26
53
  lines.push(` const c: any = await this._client();`);
54
+ if (secHints.length) {
55
+ lines.push(` if (!c || !c.security) { console.warn("[wsdl-client] Operation '${m}' may require security: ${secHints.join(", ")}. Configure client.setSecurity(...) or pass { security } to createSoapClient()."); }`);
56
+ }
27
57
  lines.push(` const meta = { ATTR_SPEC, CHILD_TYPE, PROP_META } as const;`);
28
- lines.push(` const soapArgs = toSoapArgs(args as any, "${inMetaKey}", meta, this.attributesKey);`);
58
+ lines.push(` const soapArgs = toSoapArgs(args as any, ${JSON.stringify(inMetaKey)}, meta, this.attributesKey);`);
29
59
  lines.push(` return new Promise((resolve, reject) => {`);
30
- lines.push(` c['${m}'](soapArgs, (err: any, result: any) => {`);
31
- lines.push(` if (err) reject(err); else resolve(fromSoapResult(result, "${outMetaKey}", meta, this.attributesKey));`);
60
+ lines.push(` ${clientCall}(soapArgs, (err: any, result: any) => {`);
61
+ lines.push(` if (err) reject(err); else resolve(fromSoapResult(result, ${JSON.stringify(outMetaKey)}, meta, this.attributesKey));`);
32
62
  lines.push(` });`);
33
63
  lines.push(` });`);
34
64
  lines.push(` }`);
@@ -1 +1 @@
1
- {"version":3,"file":"runtimeEmitter.d.ts","sourceRoot":"","sources":["../../src/emit/runtimeEmitter.ts"],"names":[],"mappings":"AAGA,wBAAgB,WAAW,CAAC,OAAO,EAAE,MAAM,QAuF1C"}
1
+ {"version":3,"file":"runtimeEmitter.d.ts","sourceRoot":"","sources":["../../src/emit/runtimeEmitter.ts"],"names":[],"mappings":"AAGA,wBAAgB,WAAW,CAAC,OAAO,EAAE,MAAM,QAoF1C"}
@@ -9,6 +9,7 @@ export type CreateSoapClientOptions = {
9
9
  endpoint?: string;
10
10
  wsdlOptions?: Record<string, any>;
11
11
  requestOptions?: Record<string, any>;
12
+ security?: any; // pass an instance of a node-soap security class, e.g., new soap.WSSecurity("user","pass")
12
13
  }
13
14
  export type AttrSpec = Record<string, readonly string[]>;
14
15
  export type ChildType = Record<string, Readonly<Record<string, string>>>;
@@ -17,12 +18,8 @@ export type PropMeta = Record<string, Readonly<Record<string, any>>>;
17
18
  export async function createSoapClient(opts: CreateSoapClientOptions): Promise<any> {
18
19
  const client = await soap.createClientAsync(opts.wsdlUrl, opts.wsdlOptions || {});
19
20
  if (opts.endpoint) client.setEndpoint(opts.endpoint);
20
- if (opts.requestOptions) {
21
- const { wsdlUrl, endpoint, wsdlOptions, ...req } = opts as any;
22
- if (client.setSecurity && req) {
23
- // security is caller's responsibility (if needed)
24
- }
25
- }
21
+ if (opts.security) client.setSecurity(opts.security);
22
+ // security and any request-specific configuration are the caller's responsibility
26
23
  return client;
27
24
  }
28
25
 
@@ -1 +1 @@
1
- {"version":3,"file":"typesEmitter.d.ts","sourceRoot":"","sources":["../../src/emit/typesEmitter.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,+BAA+B,CAAC;AAErE;;;;;;;GAOG;AACH,wBAAgB,SAAS,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,eAAe,QA+FnE"}
1
+ {"version":3,"file":"typesEmitter.d.ts","sourceRoot":"","sources":["../../src/emit/typesEmitter.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAC,eAAe,EAAe,MAAM,+BAA+B,CAAC;AAEjF;;;;;;;GAOG;AACH,wBAAgB,SAAS,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,eAAe,QA2InE"}
@@ -11,37 +11,71 @@ export function emitTypes(outFile, compiled) {
11
11
  const lines = [];
12
12
  // Convenience lookups
13
13
  const typeNames = new Set(compiled.types.map((t) => t.name));
14
+ const isValidIdent = (name) => /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(name);
15
+ const emitPropName = (name) => (isValidIdent(name) ? name : JSON.stringify(name));
14
16
  //
15
17
  // 1) Named simple types (aliases) first
16
18
  //
19
+ // sort aliases by name to ensure consistent order
20
+ compiled.aliases.sort((a, b) => a.name.localeCompare(b.name));
17
21
  for (const a of compiled.aliases) {
18
- const ann = a.jsdoc ? ` /** @xsd ${a.jsdoc} */\n` : "";
22
+ const ann = a.jsdoc ? `/** @xsd ${a.jsdoc} */\n` : "";
19
23
  lines.push(`${ann}export type ${a.name} = ${a.tsType};`);
20
24
  lines.push("");
21
25
  }
22
26
  //
23
27
  // 2) Complex types as interfaces
24
28
  //
29
+ // sort types by name to ensure consistent order
30
+ compiled.types.sort((a, b) => a.name.localeCompare(b.name));
25
31
  for (const t of compiled.types) {
26
- // Detect mis-mapped simpleContent extension:
27
- // single "$value" whose tsType is another named interface ⇒ extend that interface
32
+ // Detect complexContent extension via compiled metadata or mis-mapped simpleContent extension
33
+ const complexBase = t.base;
34
+ // Detect mis-mapped simpleContent extension: single "$value" whose tsType is another named interface
28
35
  const valueElems = (t.elems || []).filter((e) => e.name === "$value" &&
29
36
  (e.max === 1 || e.max === undefined) &&
30
37
  typeof e.tsType === "string" &&
31
38
  typeNames.has(e.tsType));
32
- const isSimpleContentExtension = (t.elems?.length || 0) === 1 && valueElems.length === 1;
33
- const baseName = isSimpleContentExtension ? valueElems[0].tsType : undefined;
34
- // Header
39
+ const isSimpleContentExtension = !complexBase && (t.elems?.length || 0) === 1 && valueElems.length === 1;
40
+ const baseName = complexBase ?? (isSimpleContentExtension ? valueElems[0].tsType : undefined);
41
+ // Header: extend base type if applicable
35
42
  if (baseName) {
36
43
  lines.push(`export interface ${t.name} extends ${baseName} {`);
37
44
  }
38
45
  else {
39
46
  lines.push(`export interface ${t.name} {`);
40
47
  }
48
+ // Prepare lists: for complexContent extension use only local additions
49
+ const attrsToEmit = complexBase ? (t.localAttrs || []) : (t.attrs || []);
50
+ // Elements list similar
51
+ let elementsToEmit = complexBase ? (t.localElems || []) : (t.elems || []);
52
+ // SimpleContent extension special handling drops synthetic $value
53
+ if (isSimpleContentExtension && !complexBase) {
54
+ const idx = elementsToEmit.findIndex(e => e.name === "$value");
55
+ if (idx >= 0)
56
+ elementsToEmit.splice(idx, 1);
57
+ }
41
58
  //
42
59
  // Attributes — with JSDoc on every attribute
43
60
  //
44
- for (const a of t.attrs || []) {
61
+ if (0 < attrsToEmit.length) {
62
+ // add attributes header comment
63
+ lines.push("");
64
+ lines.push(" /**");
65
+ lines.push((1 === t.attrs.length) ? " * Attribute." : " * Attributes.");
66
+ lines.push(" */");
67
+ // Sort the elements with the following logic: required first (sorted a-z), then optional (sorted a-z)
68
+ attrsToEmit.sort((a, b) => {
69
+ // Required attributes come before optional attributes
70
+ if (a.use === "required" && b.use !== "required")
71
+ return -1; // `a` is required, b is optional
72
+ if (a.use !== "required" && b.use === "required")
73
+ return 1; // `a` is optional, b is required
74
+ // Within the same group (required or optional), sort alphabetically
75
+ return a.name.localeCompare(b.name);
76
+ });
77
+ }
78
+ for (const a of attrsToEmit) {
45
79
  const opt = a.use === "required" ? "" : "?";
46
80
  const annObj = {
47
81
  kind: "attribute",
@@ -49,40 +83,54 @@ export function emitTypes(outFile, compiled) {
49
83
  use: a.use || "optional",
50
84
  };
51
85
  const ann = ` /** @xsd ${JSON.stringify(annObj)} */`;
86
+ lines.push("");
52
87
  lines.push(ann);
53
- lines.push(` ${a.name}${opt}: ${a.tsType};`);
88
+ lines.push(` ${emitPropName(a.name)}${opt}: ${a.tsType};`);
54
89
  }
55
90
  //
56
91
  // Elements — with JSDoc on every element
57
92
  //
58
- const elementsToEmit = [...(t.elems || [])];
59
- // If we detected simpleContent extension, drop the synthetic $value (we're extending instead).
60
- if (isSimpleContentExtension) {
61
- const idx = elementsToEmit.findIndex((e) => e.name === "$value");
62
- if (idx >= 0)
63
- elementsToEmit.splice(idx, 1);
93
+ // elementsToEmit already prepared above
94
+ if (0 < elementsToEmit.length) {
95
+ // add elements header comment
96
+ lines.push("");
97
+ lines.push(" /**");
98
+ lines.push((1 === elementsToEmit.length) ? " * Child element." : " * Children elements.");
99
+ lines.push(" */");
100
+ // Sort the elements with the following logic: required first (sorted a-z), then optional (sorted a-z), and finally "$value" if present.
101
+ elementsToEmit.sort((a, b) => {
102
+ // Special case: $value is always last
103
+ if (a.name === "$value")
104
+ return 1;
105
+ if (b.name === "$value")
106
+ return -1;
107
+ // Required elements come before optional elements
108
+ if (a.min !== 0 && b.min === 0)
109
+ return -1; // `a` is required, b is optional
110
+ if (a.min === 0 && b.min !== 0)
111
+ return 1; // `a` is optional, b is required
112
+ // Within the same group (required or optional), sort alphabetically
113
+ return a.name.localeCompare(b.name);
114
+ });
64
115
  }
65
- // Ensure "$value" is last if present
66
- elementsToEmit.sort((a, b) => {
67
- if (a.name === "$value" && b.name !== "$value")
68
- return 1;
69
- if (a.name !== "$value" && b.name === "$value")
70
- return -1;
71
- return 0;
72
- });
73
116
  for (const e of elementsToEmit) {
74
- const isArray = e.max === "unbounded" ||
75
- (typeof e.max === "number" && e.max > 1);
117
+ const isArray = e.max === "unbounded" || (e.max > 1);
76
118
  const arr = isArray ? "[]" : "";
77
119
  const opt = (e.min ?? 0) === 0 ? "?" : "";
78
120
  const annObj = {
79
- kind: "element",
121
+ // if a.name === "$value", the kind should be "scalar value"
122
+ kind: e.name === "$value" ? "scalar value" : "element",
80
123
  type: e.declaredType,
81
124
  occurs: { min: e.min, max: e.max, nillable: e.nillable ?? false },
82
125
  };
126
+ // if the a.name === "$value" and we have more child elements, add an empty line before the annotation
127
+ if ((e.name === "$value") && (1 < elementsToEmit.length)) {
128
+ lines.push("");
129
+ }
83
130
  const ann = ` /** @xsd ${JSON.stringify(annObj)} */`;
131
+ lines.push("");
84
132
  lines.push(ann);
85
- lines.push(` ${e.name}${opt}: ${e.tsType}${arr};`);
133
+ lines.push(` ${emitPropName(e.name)}${opt}: ${e.tsType}${arr};`);
86
134
  }
87
135
  lines.push("}");
88
136
  lines.push("");
@@ -1 +1 @@
1
- {"version":3,"file":"xml.d.ts","sourceRoot":"","sources":["../../src/util/xml.ts"],"names":[],"mappings":"AAAA,uDAAuD;AACvD,wBAAgB,cAAc,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,GAAG,CAAC,EAAE,GAAG,SAAS,GAAG,IAAI,GAAG,CAAC,EAAE,CAGpE;AAED,0EAA0E;AAC1E,wBAAgB,wBAAwB,CAAC,IAAI,EAAE,GAAG,EAAE,KAAK,EAAE,MAAM,GAAG,GAAG,EAAE,CASxE;AAED,gFAAgF;AAChF,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,GAAG,EAAE,KAAK,EAAE,MAAM,GAAG,GAAG,GAAG,SAAS,CAK/E;AAED,6DAA6D;AAC7D,wBAAgB,MAAM,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAExC;AAED,wBAAgB,YAAY,CAC1B,KAAK,EAAE,MAAM,EACb,SAAS,EAAE,MAAM,EACjB,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAC/B;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAS/B"}
1
+ {"version":3,"file":"xml.d.ts","sourceRoot":"","sources":["../../src/util/xml.ts"],"names":[],"mappings":"AAAA,uDAAuD;AACvD,wBAAgB,cAAc,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,GAAG,CAAC,EAAE,GAAG,SAAS,GAAG,IAAI,GAAG,CAAC,EAAE,CAGpE;AAED,0EAA0E;AAC1E,wBAAgB,wBAAwB,CAAC,IAAI,EAAE,GAAG,EAAE,KAAK,EAAE,MAAM,GAAG,GAAG,EAAE,CASxE;AAED,gFAAgF;AAChF,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,GAAG,EAAE,KAAK,EAAE,MAAM,GAAG,GAAG,GAAG,SAAS,CAK/E;AAED,6DAA6D;AAC7D,wBAAgB,MAAM,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CA6BxC;AAED,wBAAgB,YAAY,CAC1B,KAAK,EAAE,MAAM,EACb,SAAS,EAAE,MAAM,EACjB,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAC/B;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAS/B"}
package/dist/util/xml.js CHANGED
@@ -25,7 +25,35 @@ export function getFirstWithLocalName(node, local) {
25
25
  }
26
26
  /** Simple PascalCase helper used across emitters/parsers. */
27
27
  export function pascal(s) {
28
- return s.replace(/(^|[_\-\s])(\w)/g, (_, __, c) => c.toUpperCase()).replace(/[^A-Za-z0-9]/g, "");
28
+ const raw = String(s ?? "");
29
+ // Split on underscores to preserve them literally
30
+ const segments = raw.split("_");
31
+ const cased = segments.map(seg => {
32
+ // Uppercase letters after common separators (start, space, dash, dot, colon, slash)
33
+ const up = seg.replace(/(^|[-\s.:\/])([A-Za-z0-9_$])/g, (_m, _sep, c) => String(c).toUpperCase());
34
+ // Remove disallowed identifier characters but preserve A-Z, a-z, 0-9, _ and $
35
+ return up.replace(/[^A-Za-z0-9_$]/g, "");
36
+ });
37
+ let out = cased.join("_");
38
+ if (!out)
39
+ out = "_"; // fallback
40
+ if (/^[0-9]/.test(out))
41
+ out = `_${out}`; // ensure valid identifier start
42
+ // guard against TypeScript reserved keywords when the identifier equals them exactly
43
+ const reserved = [
44
+ "break", "case", "catch", "class", "const", "continue", "debugger", "default", "delete",
45
+ "do", "else", "enum", "export", "extends", "false", "finally", "for", "function", "if",
46
+ "import", "in", "instanceof", "new", "null", "return", "super", "switch", "this", "throw",
47
+ "true", "try", "typeof", "var", "void", "while", "with", "as", "implements", "interface",
48
+ "let", "package", "private", "protected", "public", "static", "yield", "any", "boolean",
49
+ "constructor", "declare", "get", "module", "require", "number", "set", "string", "symbol",
50
+ "type", "from", "of"
51
+ ];
52
+ const lower = out.toLowerCase();
53
+ if (reserved.includes(lower)) {
54
+ out = `_${out}`;
55
+ }
56
+ return out;
29
57
  }
30
58
  export function resolveQName(qname, defaultNS, prefixes) {
31
59
  if (!qname)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@techspokes/typescript-wsdl-client",
3
- "version": "0.2.0",
3
+ "version": "0.2.5",
4
4
  "description": "TypeScript WSDL → SOAP client generator with full xs:attribute support, complex types, sequences, inheritance, and namespace-collision merging.",
5
5
  "keywords": [
6
6
  "wsdl",