diff --git a/package-lock.json b/package-lock.json index 3570dee9..e5fd8141 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23075,7 +23075,6 @@ "@types/content-type": "^1.1.6", "@types/stringify-object": "^4.0.3", "@vitest/coverage-v8": "^0.34.4", - "camelcase": "^8.0.0", "stringify-object": "^5.0.0", "typescript": "^5.2.2", "vitest": "^0.34.5" @@ -23088,18 +23087,6 @@ "oas": "^24.0.0" } }, - "packages/httpsnippet-client-api/node_modules/camelcase": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-8.0.0.tgz", - "integrity": "sha512-8WB3Jcas3swSvjIeA2yvCJ+Miyz5l1ZmB6HFb9R1317dt9LCQoswg/BGrmAmkWVEszSrrg4RwmO46qIm2OEnSA==", - "dev": true, - "engines": { - "node": ">=16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "packages/httpsnippet-client-api/node_modules/is-obj": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-3.0.0.tgz", diff --git a/packages/api/src/lib/suggestedOperations.ts b/packages/api/src/lib/suggestedOperations.ts index 481feb9a..31d38493 100644 --- a/packages/api/src/lib/suggestedOperations.ts +++ b/packages/api/src/lib/suggestedOperations.ts @@ -116,8 +116,9 @@ export async function buildCodeSnippetForOperation(oas: Oas, operation: Operatio { api: { definition: oas.getDefinition(), - identifier: opts.identifier, + packageName: opts.identifier, registryURI: opts.identifier, + variableName: opts.identifier, }, }, ); diff --git a/packages/httpsnippet-client-api/package.json b/packages/httpsnippet-client-api/package.json index c50572a2..2870b3ab 100644 --- a/packages/httpsnippet-client-api/package.json +++ b/packages/httpsnippet-client-api/package.json @@ -49,7 +49,6 @@ "@types/content-type": "^1.1.6", "@types/stringify-object": "^4.0.3", "@vitest/coverage-v8": "^0.34.4", - "camelcase": "^8.0.0", "stringify-object": "^5.0.0", "typescript": "^5.2.2", "vitest": "^0.34.5" diff --git a/packages/httpsnippet-client-api/src/index.ts b/packages/httpsnippet-client-api/src/index.ts index 52ba2e46..287e36c9 100644 --- a/packages/httpsnippet-client-api/src/index.ts +++ b/packages/httpsnippet-client-api/src/index.ts @@ -4,7 +4,6 @@ import type { Operation } from 'oas/operation'; import type { HttpMethods, OASDocument } from 'oas/types'; import { CodeBuilder } from '@readme/httpsnippet/helpers/code-builder'; -import camelCase from 'camelcase'; // eslint-disable-line import/no-extraneous-dependencies import contentType from 'content-type'; import Oas from 'oas'; import { matchesMimeType } from 'oas/utils'; @@ -36,6 +35,42 @@ function stringify(obj: any, opts = {}) { return stringifyObject(obj, { indent: ' ', ...opts }); } +/** + * Convert a string that might contain spaces or special characters to one that can safely be used + * as a TypeScript interface or enum name. + * + * This function has been adapted and slighty modified from `json-schema-to-typescript`. This + * function also exists in `api` TS codegen. + * + * @license MIT + * @see {@link https://github.com/bcherny/json-schema-to-typescript} + */ +function toSafeString(str: string) { + // identifiers in javaScript/ts: + // First character: a-zA-Z | _ | $ + // Rest: a-zA-Z | _ | $ | 0-9 + + // remove accents, umlauts, ... by their basic latin letters + return ( + str + // if the string starts with a number, prefix it with character that typescript can accept + // https://github.com/bcherny/json-schema-to-typescript/issues/489 + .replace(/^(\d){1}/, '$$1') + // replace chars which are not valid for typescript identifiers with whitespace + .replace(/(^\s*[^a-zA-Z_$])|([^a-zA-Z_$\d])/g, ' ') + // uppercase leading underscores followed by lowercase + .replace(/^_[a-z]/g, (match: string) => match.toUpperCase()) + // remove non-leading underscores followed by lowercase (convert snake_case) + .replace(/_[a-z]/g, (match: string) => match.substr(1, match.length).toUpperCase()) + // uppercase letters after digits, dollars + .replace(/([\d$]+[a-zA-Z])/g, (match: string) => match.toUpperCase()) + // uppercase first letter after whitespace + .replace(/\s+([a-zA-Z])/g, (match: string) => match.toUpperCase().trim()) + // remove remaining whitespace + .replace(/\s/g, '') + ); +} + function buildAuthSnippet(sdkVariable: string, authKey: string | string[]) { // Auth key will be an array for Basic auth cases. if (Array.isArray(authKey)) { @@ -102,11 +137,11 @@ interface APIOptions { /** * The string to identify this SDK as. This is used in the `import sdk from ''` - * sample as well as the the variable name we attach the SDK to. + * sample. * * @example `@api/developers` */ - identifier?: string; + packageName?: string; /** * The URI that is used to download this API definition from `npx api install`. @@ -114,6 +149,13 @@ interface APIOptions { * @example `@developers/v2.0#17273l2glm9fq4l5` */ registryURI: string; + + /** + * The variable name to use when importing an SDK. If not present we will default to `sdk`. + * + * @example `readme` + */ + variableName?: string; }; escapeBrackets?: boolean; indent?: string | false; @@ -151,20 +193,22 @@ const client: Client = { ); } - let sdkPackageName; - let sdkVariable: string; - if (opts.api.identifier) { - sdkPackageName = opts.api.identifier; + let sdkPackageName: string; + if (opts.api.packageName) { + sdkPackageName = opts.api.packageName; + } else { + const registryUUID = getProjectPrefixFromRegistryUUID(opts.api.registryURI); + sdkPackageName = registryUUID || 'company-name'; + } - sdkVariable = camelCase(opts.api.identifier); + let sdkVariable = sdkPackageName; + if (opts.api.variableName) { + sdkVariable = toSafeString(opts.api.variableName); if (isReservedOrBuiltinsLC(sdkVariable)) { // If this identifier is a reserved JS word then we should prefix it with an underscore so // this snippet can be valid code. sdkVariable = `_${sdkVariable}`; } - } else { - sdkPackageName = getProjectPrefixFromRegistryUUID(opts.api.registryURI); - sdkVariable = 'sdk'; } const operationSlugs = foundOperation.url.slugs; diff --git a/packages/httpsnippet-client-api/test/index.test.ts b/packages/httpsnippet-client-api/test/index.test.ts index d834bf82..6d7fd7f9 100644 --- a/packages/httpsnippet-client-api/test/index.test.ts +++ b/packages/httpsnippet-client-api/test/index.test.ts @@ -141,15 +141,16 @@ describe('httpsnippet-client-api', () => { const code = await new HTTPSnippet(mock.har).convert('node', 'api', { api: { definition: mock.definition, - identifier: 'developers', + packageName: 'developers', registryURI: '@developers/v2.0#17273l2glm9fq4l5', + variableName: 'developersSDK', }, }); - expect(code).toStrictEqual(`import developers from '@api/developers'; + expect(code).toStrictEqual(`import developersSDK from '@api/developers'; -developers.auth('123'); -developers.findPetsByStatus({status: 'available', accept: 'application/xml'}) +developersSDK.auth('123'); +developersSDK.findPetsByStatus({status: 'available', accept: 'application/xml'}) .then(({ data }) => console.log(data)) .catch(err => console.error(err));`); }); @@ -160,15 +161,16 @@ developers.findPetsByStatus({status: 'available', accept: 'application/xml'}) const code = await new HTTPSnippet(mock.har).convert('node', 'api', { api: { definition: mock.definition, - identifier: 'metro-transit', + packageName: 'metro-transit', registryURI: '@metro-transit/v2.0#17273l2glm9fq4l5', + variableName: 'metro-transit-SDK', }, }); - expect(code).toStrictEqual(`import metroTransit from '@api/metro-transit'; + expect(code).toStrictEqual(`import metroTransitSDK from '@api/metro-transit'; -metroTransit.auth('123'); -metroTransit.findPetsByStatus({status: 'available', accept: 'application/xml'}) +metroTransitSDK.auth('123'); +metroTransitSDK.findPetsByStatus({status: 'available', accept: 'application/xml'}) .then(({ data }) => console.log(data)) .catch(err => console.error(err));`); }); diff --git a/packages/httpsnippet-client-api/tsup.config.ts b/packages/httpsnippet-client-api/tsup.config.ts index 3b77e424..1e311d39 100644 --- a/packages/httpsnippet-client-api/tsup.config.ts +++ b/packages/httpsnippet-client-api/tsup.config.ts @@ -17,7 +17,6 @@ export default defineConfig((options: Options) => ({ // treat them as external dependencies as CJS libraries can't load ESM code that uses `export`. // `noExternal` will instead treeshake these dependencies down and include them in our compiled // dists. - 'camelcase', 'stringify-object', ],