diff --git a/package.json b/package.json index c620cd22..c774c2bb 100644 --- a/package.json +++ b/package.json @@ -48,6 +48,7 @@ "typescript": "^4.6.3" }, "dependencies": { + "@types/glob": "8.0.0", "@types/node": "^17.0.24", "@types/supports-color": "^8.1.0", "@types/tape": "^4.13.2", @@ -58,6 +59,7 @@ "chalk": "^4.1.1", "eslint": "^8.13.0", "eslint-plugin-prettier": "^4.0.0", + "glob": "8.0.3", "sanitize-filename": "^1.6.3", "soap": "^0.43.0", "supports-color": "^8.1.1", diff --git a/src/cli.ts b/src/cli.ts index 2be18600..3a22ac86 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -1,8 +1,11 @@ #!/usr/bin/env node import yargs from "yargs"; import path from "path"; +import glob from "glob"; +import { promisify } from "util"; import { Logger } from "./utils/logger"; import { parseAndGenerate, Options } from "./index"; +import { isUrl } from "./utils/url"; import packageJson from "../package.json"; const conf = yargs(process.argv.slice(2)) @@ -125,20 +128,27 @@ if (conf._ === undefined || conf._.length === 0) { const outDir = path.resolve(conf.o); let errorsCount = 0; - const matches = conf._ as string[]; - - if (matches.length > 1) { - Logger.debug(matches.map((m) => path.resolve(m)).join("\n")); - Logger.log(`Found ${matches.length} wsdl files`); + const patterns = conf._ as string[]; + Logger.debug(`patterns: ${patterns}`); + const wsdlUris: string[] = []; + for (const pattern of patterns) { + if (isUrl(pattern)) { + wsdlUris.push(pattern); + } else { + const matches = await promisify(glob)(pattern); + wsdlUris.push(...matches); + } + } + if (wsdlUris.length > 1) { + Logger.log(`Found ${wsdlUris.length} wsdl URIs`); + Logger.debug(wsdlUris.join("\n")); } - for (const match of matches) { - const wsdlPath = path.resolve(match); - const wsdlName = path.basename(wsdlPath); - Logger.log(`Generating soap client from "${wsdlName}"`); + for (const wsdlUri of wsdlUris) { + Logger.log(`Generating soap client from "${wsdlUri}"`); try { - await parseAndGenerate(wsdlPath, path.join(outDir), options); + await parseAndGenerate(wsdlUri, path.join(outDir), options); } catch (err) { - Logger.error(`Error occured while generating client "${wsdlName}"`); + Logger.error(`Error occured while generating client for "${wsdlUri}"`); Logger.error(`\t${err}`); errorsCount += 1; } diff --git a/src/generator.ts b/src/generator.ts index 05a15981..4593abd1 100644 --- a/src/generator.ts +++ b/src/generator.ts @@ -28,12 +28,14 @@ const defaultOptions: GeneratorOptions = { function addSafeImport( imports: OptionalKind[], moduleSpecifier: string, - namedImport: string + namedImport: string, + isTypeOnly: boolean = false ) { if (!imports.find((imp) => imp.moduleSpecifier == moduleSpecifier)) { imports.push({ moduleSpecifier, namedImports: [{ name: namedImport }], + isTypeOnly }); } } @@ -105,7 +107,7 @@ function generateDefinitionFile( } // If a property is of the same type as its parent type, don't add import if (prop.ref.name !== definition.name) { - addSafeImport(definitionImports, `./${prop.ref.name}`, prop.ref.name); + addSafeImport(definitionImports, `./${prop.ref.name}.js`, prop.ref.name, true); } definitionProperties.push(createProperty(prop.name, prop.ref.name, prop.sourceName, prop.isArray)); } @@ -177,14 +179,16 @@ export async function generate( ); addSafeImport( clientImports, - `./definitions/${method.paramDefinition.name}`, - method.paramDefinition.name + `./definitions/${method.paramDefinition.name}.js`, + method.paramDefinition.name, + true ); } addSafeImport( portImports, - `../definitions/${method.paramDefinition.name}`, - method.paramDefinition.name + `../definitions/${method.paramDefinition.name}.js`, + method.paramDefinition.name, + true ); } if (method.returnDefinition !== null) { @@ -200,14 +204,16 @@ export async function generate( ); addSafeImport( clientImports, - `./definitions/${method.returnDefinition.name}`, - method.returnDefinition.name + `./definitions/${method.returnDefinition.name}.js`, + method.returnDefinition.name, + true ); } addSafeImport( portImports, - `../definitions/${method.returnDefinition.name}`, - method.returnDefinition.name + `../definitions/${method.returnDefinition.name}.js`, + method.returnDefinition.name, + true ); } // TODO: Deduplicate PortMethods @@ -230,7 +236,7 @@ export async function generate( }); } // End of PortMethod if (!mergedOptions.emitDefinitionsOnly) { - addSafeImport(serviceImports, `../ports/${port.name}`, port.name); + addSafeImport(serviceImports, `../ports/${port.name}.js`, port.name, true); servicePorts.push({ name: sanitizePropName(port.name), isReadonly: true, @@ -252,7 +258,7 @@ export async function generate( } // End of Port if (!mergedOptions.emitDefinitionsOnly) { - addSafeImport(clientImports, `./services/${service.name}`, service.name); + addSafeImport(clientImports, `./services/${service.name}.js`, service.name, true); clientServices.push({ name: sanitizePropName(service.name), type: service.name }); serviceFile.addImportDeclarations(serviceImports); @@ -314,12 +320,12 @@ export async function generate( { isRestParameter: true, name: "args", - type: "Parameters", + type: "Parameters extends [any, ...infer Rest] ? Rest : never", }, ], returnType: `Promise<${parsedWsdl.name}Client>`, // TODO: `any` keyword is very dangerous }); - createClientDeclaration.setBodyText("return soapCreateClientAsync(args[0], args[1], args[2]) as any;"); + createClientDeclaration.setBodyText(`return soapCreateClientAsync("${parsedWsdl.wsdlUri}", ...args) as any;`); Logger.log(`Writing Client file: ${path.resolve(path.join(outDir, "client"))}.ts`); clientFile.saveSync(); } @@ -333,7 +339,8 @@ export async function generate( indexFile.addExportDeclarations( allDefinitions.map((def) => ({ namedExports: [def.name], - moduleSpecifier: `./definitions/${def.name}`, + moduleSpecifier: `./definitions/${def.name}.js`, + isTypeOnly: true })) ); if (!mergedOptions.emitDefinitionsOnly) { @@ -342,19 +349,21 @@ export async function generate( indexFile.addExportDeclarations([ { namedExports: ["createClientAsync", `${parsedWsdl.name}Client`], - moduleSpecifier: "./client", + moduleSpecifier: "./client.js", }, ]); indexFile.addExportDeclarations( parsedWsdl.services.map((service) => ({ namedExports: [service.name], - moduleSpecifier: `./services/${service.name}`, + moduleSpecifier: `./services/${service.name}.js`, + isTypeOnly: true, })) ); indexFile.addExportDeclarations( parsedWsdl.ports.map((port) => ({ namedExports: [port.name], - moduleSpecifier: `./ports/${port.name}`, + moduleSpecifier: `./ports/${port.name}.js`, + isTypeOnly: true, })) ); } diff --git a/src/index.ts b/src/index.ts index 596f7a5a..06413d26 100644 --- a/src/index.ts +++ b/src/index.ts @@ -69,7 +69,7 @@ export const defaultOptions: Options = { }; export async function parseAndGenerate( - wsdlPath: string, + wsdlUri: string, outDir: string, options: Partial = {} ): Promise { @@ -95,7 +95,7 @@ export async function parseAndGenerate( // Logger.debug(`Options: ${JSON.stringify(mergedOptions, null, 2)}`); const timeParseStart = process.hrtime(); - const parsedWsdl = await parseWsdl(wsdlPath, mergedOptions); + const parsedWsdl = await parseWsdl(wsdlUri, mergedOptions); Logger.debug(`Parser time: ${timeElapsed(process.hrtime(timeParseStart))}ms`); const timeGenerateStart = process.hrtime(); diff --git a/src/models/parsed-wsdl.ts b/src/models/parsed-wsdl.ts index 9fc2df74..0f2a009e 100644 --- a/src/models/parsed-wsdl.ts +++ b/src/models/parsed-wsdl.ts @@ -90,15 +90,16 @@ const defaultOptions: Options = { export class ParsedWsdl { /** - * Name is always uppercased filename of wsdl without an extension. + * Name is always uppercased filename of wsdl without an extension + * or the last URL path component without the query string. * Used to generate client name of interface * @example "MyClient" */ name: string; - /** Original wsdl filename */ - wsdlFilename: string; - /** Absolute basepath or url */ - wsdlPath: string; + /** Original wsdl basename */ + wsdlBasename: string; + /** Relative path or url */ + wsdlUri: string; definitions: Array = []; ports: Array = []; diff --git a/src/parser.ts b/src/parser.ts index f830cde3..a54f33b8 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -6,6 +6,7 @@ import { changeCase } from "./utils/change-case"; import { stripExtension } from "./utils/file"; import { reservedKeywords } from "./utils/javascript"; import { Logger } from "./utils/logger"; +import { isUrl, stripQuery } from "./utils/url"; interface ParserOptions { modelNamePreffix: string; @@ -220,16 +221,16 @@ function parseDefinition( // TODO: Add comments for services, ports, methods and client /** * Parse WSDL to domain model `ParsedWsdl` - * @param wsdlPath - path or url to wsdl file + * @param wsdlUri - path or url to wsdl file */ -export async function parseWsdl(wsdlPath: string, options: Partial): Promise { +export async function parseWsdl(wsdlUri: string, options: Partial): Promise { const mergedOptions: ParserOptions = { ...defaultOptions, ...options, }; return new Promise((resolve, reject) => { open_wsdl( - wsdlPath, + wsdlUri, { namespaceArrayElements: false, ignoredNamespaces: ["tns", "targetNamespace", "typeNamespace"] }, function (err, wsdl) { if (err) { @@ -240,12 +241,17 @@ export async function parseWsdl(wsdlPath: string, options: Partial = []; diff --git a/src/utils/url.ts b/src/utils/url.ts new file mode 100644 index 00000000..85882553 --- /dev/null +++ b/src/utils/url.ts @@ -0,0 +1,10 @@ +/** + * @example isUrl("https://domain.com") -> true + */ +export function isUrl(uri: string): boolean { + return /^https?:\/\//.test(uri); +} + +export function stripQuery(url: string): string { + return url.replace(/^(.*?)\?.*$/, "$1"); +}