From bea445b5c3e6c13ae8cbe8ac57f7af428270481f Mon Sep 17 00:00:00 2001 From: Jon Ursenbach Date: Wed, 13 Sep 2023 20:29:19 -0700 Subject: [PATCH] refactor(ts): strict mode Co-authored-by: Kanad Gupta --- packages/api/src/cli/codegen/language.ts | 3 +- .../src/cli/codegen/languages/typescript.ts | 25 +++++---- packages/api/src/cli/fetcher.ts | 2 +- packages/api/src/cli/storage.ts | 10 +++- packages/api/tsconfig.json | 4 +- packages/core/src/index.ts | 10 ++-- .../core/src/lib/getJSONSchemaDefaults.ts | 10 ++-- packages/core/src/lib/prepareAuth.ts | 5 +- packages/core/src/lib/prepareParams.ts | 56 +++++++++++-------- packages/core/tsconfig.json | 2 +- 10 files changed, 72 insertions(+), 55 deletions(-) diff --git a/packages/api/src/cli/codegen/language.ts b/packages/api/src/cli/codegen/language.ts index bc3f667d..314334ac 100644 --- a/packages/api/src/cli/codegen/language.ts +++ b/packages/api/src/cli/codegen/language.ts @@ -25,11 +25,12 @@ export default abstract class CodeGeneratorLanguage { userAgent: string; - requiredPackages: Record; + requiredPackages!: Record; constructor(spec: Oas, specPath: string, identifier: string) { this.spec = spec; this.specPath = specPath; + this.identifier = identifier; // User agents should be contextual to the spec in question and the version of `api` that was // used to generate the SDK. For example, this'll look like `petstore/1.0.0 (api/4.2.0)` for diff --git a/packages/api/src/cli/codegen/languages/typescript.ts b/packages/api/src/cli/codegen/languages/typescript.ts index c3f1ea1e..d784b887 100644 --- a/packages/api/src/cli/codegen/languages/typescript.ts +++ b/packages/api/src/cli/codegen/languages/typescript.ts @@ -3,6 +3,7 @@ import type { InstallerOptions } from '../language'; import type Oas from 'oas'; import type { Operation } from 'oas'; import type { HttpMethods, SchemaObject } from 'oas/dist/rmoas.types'; +import type { SemVer } from 'semver'; import type { ClassDeclaration, JSDocStructure, @@ -53,9 +54,9 @@ export default class TSGenerator extends CodeGeneratorLanguage { types: Map; - files: Record; + files?: Record; - sdk: ClassDeclaration; + sdk!: ClassDeclaration; schemas: Record< string, @@ -138,7 +139,7 @@ export default class TSGenerator extends CodeGeneratorLanguage { if (!pkgVersion) { // If the version that's in `info.version` isn't compatible with semver NPM won't be able to // handle it properly so we need to fallback to something it can. - pkgVersion = semver.coerce('0.0.0'); + pkgVersion = semver.coerce('0.0.0') as SemVer; } const pkg: PackageJson = { @@ -221,7 +222,7 @@ export default class TSGenerator extends CodeGeneratorLanguage { sdkSource .getImportDeclarations() .find(id => id.getText() === "import type * as types from './types';") - .remove(); + ?.remove(); } // If this SDK doesn't use the `HTTPMethodRange` interface for handling `2XX` response status @@ -230,7 +231,7 @@ export default class TSGenerator extends CodeGeneratorLanguage { sdkSource .getImportDeclarations() .find(id => id.getText().includes('HTTPMethodRange')) - .replaceWithText("import type { ConfigOptions, FetchResponse } from '@api/core'"); + ?.replaceWithText("import type { ConfigOptions, FetchResponse } from '@api/core'"); } if (this.outputJS) { @@ -656,7 +657,7 @@ sdk.server('https://eu.api.example.com/v14');`), // we should only add a docblock to the first overload we create because IDE Intellisense will // always use that and adding a docblock to all three will bloat the SDK with unused and // unsurfaced method documentation. - docs: shouldAddAltTypedOverloads ? null : Object.keys(docblock).length ? [docblock] : null, + docs: shouldAddAltTypedOverloads ? undefined : Object.keys(docblock).length ? [docblock] : undefined, statements: writer => { /** * @example return this.core.fetch('/pet/findByStatus', 'get', body, metadata); @@ -698,7 +699,7 @@ sdk.server('https://eu.api.example.com/v14');`), { ...parameters.metadata, hasQuestionToken: false }, ], returnType, - docs: Object.keys(docblock).length ? [docblock] : null, + docs: Object.keys(docblock).length ? [docblock] : undefined, }); // Create an overload that just has a single `metadata` parameter. @@ -739,13 +740,13 @@ sdk.server('https://eu.api.example.com/v14');`), */ loadOperationsAndMethods() { const operations: Record = {}; - const methods = new Set(); + const methods = new Set(); // Prepare all of the schemas that we need to process for every operation within this API // definition. Object.entries(this.spec.getPaths()).forEach(([, ops]) => { - Object.entries(ops).forEach(([method, operation]: [HttpMethods, Operation]) => { - methods.add(method); + Object.entries(ops).forEach(([method, operation]: [string, Operation]) => { + methods.add(method as HttpMethods); const operationId = operation.getOperationId({ // This `camelCase` option will clean up any weird characters that might be present in @@ -786,7 +787,7 @@ sdk.server('https://eu.api.example.com/v14');`), transformer: (s: SchemaObject) => { // As our schemas are dereferenced in the `oas` library we don't want to pollute our // codegen'd schemas file with duplicate schemas. - if ('x-readme-ref-name' in s) { + if ('x-readme-ref-name' in s && typeof s['x-readme-ref-name'] !== 'undefined') { const typeName = generateTypeName(s['x-readme-ref-name']); this.addSchemaToExport(s, typeName, typeName); @@ -844,7 +845,7 @@ sdk.server('https://eu.api.example.com/v14');`), transformer: (s: SchemaObject) => { // As our schemas are dereferenced in the `oas` library we don't want to pollute our // codegen'd schemas file with duplicate schemas. - if ('x-readme-ref-name' in s) { + if ('x-readme-ref-name' in s && typeof s['x-readme-ref-name'] !== 'undefined') { const typeName = generateTypeName(s['x-readme-ref-name']); this.addSchemaToExport(s, typeName, `${typeName}`); diff --git a/packages/api/src/cli/fetcher.ts b/packages/api/src/cli/fetcher.ts index 1f189c3b..cd5d6040 100644 --- a/packages/api/src/cli/fetcher.ts +++ b/packages/api/src/cli/fetcher.ts @@ -51,7 +51,7 @@ export default class Fetcher { return undefined; } - return matches.groups.project; + return matches.groups?.project; } async load() { diff --git a/packages/api/src/cli/storage.ts b/packages/api/src/cli/storage.ts index 55f0a882..d2b11b85 100644 --- a/packages/api/src/cli/storage.ts +++ b/packages/api/src/cli/storage.ts @@ -22,7 +22,7 @@ export default class Storage { */ source: string; - identifier: string; + identifier!: string; fetcher: Fetcher; @@ -32,7 +32,9 @@ export default class Storage { this.fetcher = new Fetcher(source); this.source = source; - this.identifier = identifier; + if (identifier) { + this.identifier = identifier; + } // This should default to false so we have awareness if we've looked at the lockfile yet. Storage.lockfile = false; @@ -117,7 +119,9 @@ export default class Storage { if (!isValidForNPM.validForNewPackages) { // `prompts` doesn't support surfacing multiple errors in a `validate` call so we can only // surface the first to the user. - throw new Error(`Identifier cannot be used for an NPM package: ${isValidForNPM.errors[0]}`); + throw new Error( + `Identifier cannot be used for an NPM package: ${isValidForNPM?.errors?.[0] || '[error unavailable]'}`, + ); } return true; diff --git a/packages/api/tsconfig.json b/packages/api/tsconfig.json index c04ee9df..62023664 100644 --- a/packages/api/tsconfig.json +++ b/packages/api/tsconfig.json @@ -6,7 +6,9 @@ "esModuleInterop": true, "lib": ["DOM", "DOM.Iterable", "ES2020"], "noImplicitAny": true, - "outDir": "dist/" + "outDir": "dist/", + "strict": true, + "useUnknownInCatchVariables": false }, "include": ["./src/**/*"] } diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 30d15a53..e7dd1429 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -37,24 +37,24 @@ export type HTTPMethodRange = Exclude; } = false; private config: ConfigOptions = {}; - private userAgent: string; + private userAgent!: string; constructor(spec?: Oas, userAgent?: string) { - this.spec = spec; - this.userAgent = userAgent; + if (spec) this.spec = spec; + if (userAgent) this.userAgent = userAgent; } setSpec(spec: Oas) { diff --git a/packages/core/src/lib/getJSONSchemaDefaults.ts b/packages/core/src/lib/getJSONSchemaDefaults.ts index 0c7cd5d4..cffc20dc 100644 --- a/packages/core/src/lib/getJSONSchemaDefaults.ts +++ b/packages/core/src/lib/getJSONSchemaDefaults.ts @@ -23,16 +23,16 @@ export default function getJSONSchemaDefaults(jsonSchemas: SchemaWrapper[]) { schema: SchemaObject, pointer: string, rootSchema: SchemaObject, - parentPointer: string, - parentKeyword: string, - parentSchema: SchemaObject, - indexProperty: string, + parentPointer?: string, + parentKeyword?: string, + parentSchema?: SchemaObject, + indexProperty?: string | number, ) => { if (!pointer.startsWith('/properties/')) { return; } - if (Array.isArray(parentSchema?.required) && parentSchema.required.includes(indexProperty)) { + if (Array.isArray(parentSchema?.required) && parentSchema?.required.includes(String(indexProperty))) { if (schema.type === 'object' && indexProperty) { defaults[indexProperty] = {}; } diff --git a/packages/core/src/lib/prepareAuth.ts b/packages/core/src/lib/prepareAuth.ts index 5f00ca66..6843e896 100644 --- a/packages/core/src/lib/prepareAuth.ts +++ b/packages/core/src/lib/prepareAuth.ts @@ -1,5 +1,6 @@ /* eslint-disable no-underscore-dangle */ import type { Operation } from 'oas'; +import type { KeyedSecuritySchemeObject } from 'oas/dist/rmoas.types'; export default function prepareAuth(authKey: (number | string)[], operation: Operation) { if (authKey.length === 0) { @@ -58,7 +59,7 @@ export default function prepareAuth(authKey: (number | string)[], operation: Ope ); } - const scheme = schemes.shift(); + const scheme = schemes.shift() as KeyedSecuritySchemeObject; preparedAuth[scheme._key] = { user: authKey[0], pass: authKey.length === 2 ? authKey[1] : '', @@ -76,7 +77,7 @@ export default function prepareAuth(authKey: (number | string)[], operation: Ope .map(([, ps]) => ps.filter(s => usableScheme === s._key)) .reduce((prev, next) => prev.concat(next), []); - const scheme = schemes.shift(); + const scheme = schemes.shift() as KeyedSecuritySchemeObject; switch (scheme.type) { case 'http': if (scheme.scheme === 'basic') { diff --git a/packages/core/src/lib/prepareParams.ts b/packages/core/src/lib/prepareParams.ts index 13f54fad..2e9a72d8 100644 --- a/packages/core/src/lib/prepareParams.ts +++ b/packages/core/src/lib/prepareParams.ts @@ -42,7 +42,7 @@ function digestParameters(parameters: ParameterObject[]): Record { +): Promise<{ base64?: string; buffer?: Buffer; filename: string; paramName?: string } | undefined> { if (typeof file === 'string') { // In order to support relative pathed files, we need to attempt to resolve them. const resolvedFile = path.resolve(file); @@ -103,7 +103,7 @@ function processFile( return resolve({ paramName, - base64: fileMetadata.content.replace(';base64', `;name=${payloadFilename};base64`), + base64: fileMetadata?.content?.replace(';base64', `;name=${payloadFilename};base64`), filename: payloadFilename, buffer: fileMetadata.buffer, }); @@ -118,7 +118,7 @@ function processFile( return { paramName, - base64: base64.replace(';base64', `;name=${payloadFilename};base64`), + base64: base64?.replace(';base64', `;name=${payloadFilename};base64`), filename: payloadFilename, buffer, }; @@ -228,7 +228,7 @@ export default async function prepareParams(operation: Operation, body?: unknown } }); - const intersection = Object.keys(body).filter(value => { + const intersection = Object.keys(body as NonNullable).filter(value => { if (Object.keys(digestedParameters).includes(value)) { return true; } else if (headerParams.has(value)) { @@ -238,10 +238,10 @@ export default async function prepareParams(operation: Operation, body?: unknown return false; }).length; - if (intersection && intersection / Object.keys(body).length > 0.25) { + // If more than 25% of the body intersects with the parameters that we've got on hand, then + // we should treat it as a metadata object and organize into parameters. + if (intersection && intersection / Object.keys(body as NonNullable).length > 0.25) { /* eslint-disable no-param-reassign */ - // If more than 25% of the body intersects with the parameters that we've got on hand, - // then we should treat it as a metadata object and organize into parameters. metadataIntersected = true; metadata = merge(params.body, body) as Record; body = undefined; @@ -304,7 +304,9 @@ export default async function prepareParams(operation: Operation, body?: unknown params.body = fileMetadata.base64; } - params.files[fileMetadata.filename] = fileMetadata.buffer; + if (fileMetadata.buffer && params?.files) { + params.files[fileMetadata.filename] = fileMetadata.buffer; + } }); }); } @@ -336,7 +338,7 @@ export default async function prepareParams(operation: Operation, body?: unknown } else if (param.in === 'header') { // Headers are sent case-insensitive so we need to make sure that we're properly // matching them when detecting what our incoming payload looks like. - metadataHeaderParam = Object.keys(metadata).find(k => k.toLowerCase() === paramName.toLowerCase()); + metadataHeaderParam = Object.keys(metadata).find(k => k.toLowerCase() === paramName.toLowerCase()) || ''; value = metadata[metadataHeaderParam]; } } @@ -348,20 +350,20 @@ export default async function prepareParams(operation: Operation, body?: unknown /* eslint-disable no-param-reassign */ switch (param.in) { case 'path': - params.path[paramName] = value; - delete metadata[paramName]; + (params.path as NonNullable)[paramName] = value; + if (metadata?.[paramName]) delete metadata[paramName]; break; case 'query': - params.query[paramName] = value; - delete metadata[paramName]; + (params.query as NonNullable)[paramName] = value; + if (metadata?.[paramName]) delete metadata[paramName]; break; case 'header': - params.header[paramName.toLowerCase()] = value; - delete metadata[metadataHeaderParam]; + (params.header as NonNullable)[paramName.toLowerCase()] = value; + if (metadataHeaderParam && metadata?.[metadataHeaderParam]) delete metadata[metadataHeaderParam]; break; case 'cookie': - params.cookie[paramName] = value; - delete metadata[paramName]; + (params.cookie as NonNullable)[paramName] = value; + if (metadata?.[paramName]) delete metadata[paramName]; break; default: // no-op } @@ -387,11 +389,17 @@ export default async function prepareParams(operation: Operation, body?: unknown // or specify a custom auth header (maybe we can't handle their auth case right) this is the // only way with this library that they can do that. specialHeaders.forEach(headerName => { - const headerParam = Object.keys(metadata).find(m => m.toLowerCase() === headerName); + const headerParam = Object.keys(metadata || {}).find(m => m.toLowerCase() === headerName); if (headerParam) { - params.header[headerName] = metadata[headerParam] as string; - // eslint-disable-next-line no-param-reassign - delete metadata[headerParam]; + // this if-statement below is a typeguard + if (typeof metadata === 'object') { + // this if-statement below is a typeguard + if (typeof params.header === 'object') { + params.header[headerName] = metadata[headerParam] as string; + } + // eslint-disable-next-line no-param-reassign + delete metadata[headerParam]; + } } }); } @@ -405,7 +413,7 @@ export default async function prepareParams(operation: Operation, body?: unknown } } - ['body', 'cookie', 'files', 'formData', 'header', 'path', 'query'].forEach((type: keyof typeof params) => { + (['body', 'cookie', 'files', 'formData', 'header', 'path', 'query'] as const).forEach((type: keyof typeof params) => { if (type in params && isEmpty(params[type])) { delete params[type]; } diff --git a/packages/core/tsconfig.json b/packages/core/tsconfig.json index 5acf6120..4f40fdab 100644 --- a/packages/core/tsconfig.json +++ b/packages/core/tsconfig.json @@ -7,7 +7,7 @@ "lib": ["DOM", "DOM.Iterable", "ES2020"], "noImplicitAny": true, "outDir": "dist/", - "strict": false + "strict": true }, "include": ["./src/**/*"] }