Skip to content

Globals.d.ts

Features

  • AXIOS_BASE for Axios's baseURL
  • AXIOS_HEADERS for Axios's headers

See FileManager#getEnvSource for more information about how this works in the background.

ts
import type { KubbFile, Plugin, PluginFactoryOptions, ResolveNameParams } from '@kubb/core'
import type { Exclude, Include, Override, ResolvePathOptions } from '@kubb/plugin-oas'
import type { Client, Operations } from './components/index.ts'

type Templates = {
  operations?: typeof Operations.templates | false
  client?: typeof Client.templates | false
}

export type Options = {
  output?: {
    /**
     * Output to save the clients.
     * @default `"clients"``
     */
    path: string
    /**
     * Name to be used for the `export * as {{exportAs}} from './'`
     */
    exportAs?: string
    /**
     * Add an extension to the generated imports and exports, default it will not use an extension
     */
    extName?: KubbFile.Extname
    /**
     * Define what needs to exported, here you can also disable the export of barrel files
     * @default `'barrel'`
     */
    exportType?: 'barrel' | 'barrelNamed' | false
  }
  /**
   * Group the clients based on the provided name.
   */
  group?: {
    /**
     * Tag will group based on the operation tag inside the Swagger file
     */
    type: 'tag'
    /**
     * Relative path to save the grouped clients.
     *
     * `{{tag}}` will be replaced by the current tagName.
     * @example `${output}/{{tag}}Controller` => `clients/PetController`
     * @default `${output}/{{tag}}Controller`
     */
    output?: string
    /**
     * Name to be used for the `export * as {{exportAs}} from './`
     * @default `"{{tag}}Service"`
     */
    exportAs?: string
  }
  /**
   * Array containing exclude parameters to exclude/skip tags/operations/methods/paths.
   */
  exclude?: Array<Exclude>
  /**
   * Array containing include parameters to include tags/operations/methods/paths.
   */
  include?: Array<Include>
  /**
   * Array containing override parameters to override `options` based on tags/operations/methods/paths.
   */
  override?: Array<Override<ResolvedOptions>>
  client?: {
    /**
     * Path to the client import path that will be used to do the API calls.
     * It will be used as `import client from '${client.importPath}'`.
     * It allow both relative and absolute path.
     * the path will be applied as is, so relative path shoule be based on the file being generated.
     * @default '@kubb/swagger-client/client'
     */
    importPath?: string
  }
  /**
   * ReturnType that needs to be used when calling client().
   *
   * `Data` will return ResponseConfig[data].
   *
   * `Full` will return ResponseConfig.
   * @default `'data'`
   * @private
   */
  dataReturnType?: 'data' | 'full'
  /**
   * How to pass your pathParams.
   *
   * `object` will return the pathParams as an object.
   *
   * `inline` will return the pathParams as comma separated params.
   * @default `'inline'`
   * @private
   */
  pathParamsType?: 'object' | 'inline'
  transformers?: {
    /**
     * Customize the names based on the type that is provided by the plugin.
     */
    name?: (name: ResolveNameParams['name'], type?: ResolveNameParams['type']) => string
  }
  /**
   * Make it possible to override one of the templates
   */
  templates?: Partial<Templates>
}

type ResolvedOptions = {
  client: Required<NonNullable<Options['client']>>
  dataReturnType: NonNullable<Options['dataReturnType']>
  pathParamsType: NonNullable<Options['pathParamsType']>
  templates: NonNullable<Templates>
}

export type FileMeta = {
  pluginKey?: Plugin['key']
  tag?: string
}

export type PluginClient = PluginFactoryOptions<'plugin-client', Options, ResolvedOptions, never, ResolvePathOptions>

declare module '@kubb/core' {
  export interface _Register {
    ['@kubb/swagger-client']: PluginClient
  }
}
ts
/// <reference no-default-lib="true" />
/// <reference lib="esnext" />

type Environments = import('./src/types.ts').Environments

declare module 'global' {
  namespace NodeJS {
    export interface ProcessEnv extends Partial<Record<keyof Environments, string>> {}
  }
}
/**
 * `tsconfig.json`
 * @example
"compilerOptions": {
___ "types": ["@kubb/swagger-client/globals"]
}
 * @example implementation
{
___ env?: NodeJS.ProcessEnv
}
*/
declare namespace NodeJS {
  export interface ProcessEnv extends Partial<Record<keyof Environments, string>> {}
}

TypeScript

To get TypeScript support for NodeJS.ProcessEnv(with already the process.env type being set), add @kubb/swagger-client/globals to your tsconfig.json:

typescript
{
  "compilerOptions": {
    "types": [
      "@kubb/swagger-client/globals"
    ]
  }
}

Usage

ts
import crypto from 'node:crypto'
import { extname, resolve } from 'node:path'

import { orderBy } from 'natural-orderby'
import PQueue from 'p-queue'
import { isDeepEqual } from 'remeda'

import { BarrelManager } from './BarrelManager.ts'
import { getRelativePath, read } from './fs/read.ts'
import { write } from './fs/write.ts'
import { searchAndReplace } from './transformers/searchAndReplace.ts'
import { trimExtName } from './transformers/trim.ts'

import type { GreaterThan } from '@kubb/types'
import type { BarrelManagerOptions } from './BarrelManager.ts'
import type { Logger } from './logger.ts'
import transformers from './transformers/index.ts'
import type { Plugin } from './types.ts'
import { getParser } from './utils'

type BasePath<T extends string = string> = `${T}/`

export namespace KubbFile {
  export type Import = {
    /**
     * Import name to be used
     * @example ["useState"]
     * @example "React"
     */
    name:
      | string
      | Array<
          | string
          | {
              propertyName: string
              name?: string
            }
        >
    /**
     * Path for the import
     * @xample '@kubb/core'
     */
    path: string
    /**
     * Add `type` prefix to the import, this will result in: `import type { Type } from './path'`.
     */
    isTypeOnly?: boolean
    /**
     * Add `* as` prefix to the import, this will result in: `import * as path from './path'`.
     */

    isNameSpace?: boolean
    /**
     * When root is set it will get the path with relative getRelativePath(root, path).
     */
    root?: string
  }

  export type Export = {
    /**
     * Export name to be used.
     * @example ["useState"]
     * @example "React"
     */
    name?: string | Array<string>
    /**
     * Path for the import.
     * @xample '@kubb/core'
     */
    path: string
    /**
     * Add `type` prefix to the export, this will result in: `export type { Type } from './path'`.
     */
    isTypeOnly?: boolean
    /**
     * Make it possible to override the name, this will result in: `export * as aliasName from './path'`.
     */
    asAlias?: boolean
  }

  export declare const dataTagSymbol: unique symbol
  export type DataTag<Type, Value> = Type & {
    [dataTagSymbol]: Value
  }

  export type UUID = string
  export type Source = string

  export type Extname = '.ts' | '.js' | '.tsx' | '.json' | `.${string}`

  export type Mode = 'single' | 'split'

  /**
   * Name to be used to dynamicly create the baseName(based on input.path)
   * Based on UNIX basename
   * @link https://nodejs.org/api/path.html#pathbasenamepath-suffix
   */
  export type BaseName = `${string}${Extname}`

  /**
   * Path will be full qualified path to a specified file
   */
  export type Path = string

  export type AdvancedPath<T extends BaseName = BaseName> = `${BasePath}${T}`

  export type OptionalPath = Path | undefined | null

  export type FileMetaBase = {
    pluginKey?: Plugin['key']
  }

  export type File<TMeta extends FileMetaBase = FileMetaBase, TBaseName extends BaseName = BaseName> = {
    /**
     * Unique identifier to reuse later
     * @default crypto.randomUUID()
     */
    id?: string
    /**
     * Name to be used to create the path
     * Based on UNIX basename, `${name}.extName`
     * @link https://nodejs.org/api/path.html#pathbasenamepath-suffix
     */
    baseName: TBaseName
    /**
     * Path will be full qualified path to a specified file
     */
    path: AdvancedPath<TBaseName> | Path
    source: Source
    imports?: Import[]
    exports?: Export[]
    /**
     * This will call fileManager.add instead of fileManager.addOrAppend, adding the source when the files already exists
     * This will also ignore the combinefiles utils
     * @default `false`
     */
    override?: boolean
    /**
     * Use extra meta, this is getting used to generate the barrel/index files.
     */
    meta?: TMeta
    /**
     * Override if a file can be exported by the BarrelManager
     * @default true
     */
    exportable?: boolean
    /**
     * This will override `process.env[key]` inside the `source`, see `getFileSource`.
     */
    env?: NodeJS.ProcessEnv
    /**
     * The name of the language being used. This can be TypeScript, JavaScript and still have another ext.
     */
    language?: string
  }

  export type ResolvedFile<TMeta extends FileMetaBase = FileMetaBase, TBaseName extends BaseName = BaseName> = KubbFile.File<TMeta, TBaseName> & {
    /**
     * @default crypto.randomUUID()
     */
    id: UUID
    /**
     * Contains the first part of the baseName, generated based on baseName
     * @link  https://nodejs.org/api/path.html#pathformatpathobject
     */

    name: string
  }
}

type CacheItem = KubbFile.ResolvedFile & {
  cancel?: () => void
}

type AddResult<T extends Array<KubbFile.File>> = Promise<
  Awaited<GreaterThan<T['length'], 1> extends true ? Promise<KubbFile.ResolvedFile[]> : Promise<KubbFile.ResolvedFile>>
>

type AddIndexesProps = {
  /**
   * Root based on root and output.path specified in the config
   */
  root: string
  /**
   * Output for plugin
   */
  output: {
    path: string
    exportAs?: string
    extName?: KubbFile.Extname
    exportType?: 'barrel' | 'barrelNamed' | false
  }
  logger: Logger
  options?: BarrelManagerOptions
  meta?: KubbFile.File['meta']
}

type Options = {
  queue?: PQueue
  task?: (file: KubbFile.ResolvedFile) => Promise<KubbFile.ResolvedFile>
}

export class FileManager {
  #cache: Map<KubbFile.Path, CacheItem[]> = new Map()

  #task: Options['task']
  #queue: PQueue

  constructor({ task = async (file) => file, queue = new PQueue() }: Options = {}) {
    this.#task = task
    this.#queue = queue

    return this
  }

  get files(): Array<KubbFile.File> {
    const files: Array<KubbFile.File> = []
    this.#cache.forEach((item) => {
      files.push(...item.flat(1))
    })

    return files
  }
  get isExecuting(): boolean {
    return this.#queue.size !== 0 && this.#queue.pending !== 0
  }

  async add<T extends Array<KubbFile.File> = Array<KubbFile.File>>(...files: T): AddResult<T> {
    const promises = combineFiles(files).map((file) => {
      if (file.override) {
        return this.#add(file)
      }

      return this.#addOrAppend(file)
    })

    const resolvedFiles = await Promise.all(promises)

    if (files.length > 1) {
      return resolvedFiles as unknown as AddResult<T>
    }

    return resolvedFiles[0] as unknown as AddResult<T>
  }

  async #add(file: KubbFile.File): Promise<KubbFile.ResolvedFile> {
    const controller = new AbortController()
    const resolvedFile: KubbFile.ResolvedFile = {
      id: crypto.randomUUID(),
      name: trimExtName(file.baseName),
      ...file,
    }

    if (resolvedFile.exports?.length) {
      const folder = resolvedFile.path.replace(resolvedFile.baseName, '')

      resolvedFile.exports = resolvedFile.exports.filter((exportItem) => {
        const exportedFile = this.files.find((file) => file.path.includes(resolve(folder, exportItem.path)))

        if (exportedFile) {
          return exportedFile.exportable
        }

        return true
      })
    }

    this.#cache.set(resolvedFile.path, [{ cancel: () => controller.abort(), ...resolvedFile }])

    return this.#queue.add(
      async () => {
        return this.#task?.(resolvedFile)
      },
      { signal: controller.signal },
    ) as Promise<KubbFile.ResolvedFile>
  }

  async #addOrAppend(file: KubbFile.File): Promise<KubbFile.ResolvedFile> {
    const previousCaches = this.#cache.get(file.path)
    const previousCache = previousCaches ? previousCaches.at(previousCaches.length - 1) : undefined

    if (previousCache) {
      this.#cache.delete(previousCache.path)

      return this.#add({
        ...file,
        source: previousCache.source && file.source ? `${previousCache.source}\n${file.source}` : '',
        imports: [...(previousCache.imports || []), ...(file.imports || [])],
        exports: [...(previousCache.exports || []), ...(file.exports || [])],
        env: { ...(previousCache.env || {}), ...(file.env || {}) },
      })
    }
    return this.#add(file)
  }

  async addIndexes({ root, output, meta, logger, options = {} }: AddIndexesProps): Promise<void> {
    const { exportType = 'barrel' } = output
    //        ^?
    if (exportType === false) {
      return undefined
    }

    const pathToBuildFrom = resolve(root, output.path)

    if (transformers.trimExtName(pathToBuildFrom).endsWith('index')) {
      logger.emit('warning', 'Output has the same fileName as the barrelFiles, please disable barrel generation')
      return
    }

    const exportPath = output.path.startsWith('./') ? trimExtName(output.path) : `./${trimExtName(output.path)}`
    const mode = FileManager.getMode(output.path)
    const barrelManager = new BarrelManager({
      extName: output.extName,
      ...options,
    })
    let files = barrelManager.getIndexes(pathToBuildFrom)

    if (!files) {
      return undefined
    }

    if (exportType === 'barrelNamed') {
      files = files.map((file) => {
        if (file.exports) {
          return {
            ...file,
            exports: barrelManager.getNamedExports(pathToBuildFrom, file.exports),
          }
        }
        return file
      })
    }

    await Promise.all(
      files.map((file) => {
        return this.#addOrAppend({
          ...file,
          meta: meta ? meta : file.meta,
        })
      }),
    )

    const rootPath = mode === 'split' ? `${exportPath}/index${output.extName || ''}` : `${exportPath}${output.extName || ''}`
    const rootFile: KubbFile.File = {
      path: resolve(root, 'index.ts'),
      baseName: 'index.ts',
      source: '',
      exports: [
        output.exportAs
          ? {
              name: output.exportAs,
              asAlias: true,
              path: rootPath,
              isTypeOnly: options.isTypeOnly,
            }
          : {
              path: rootPath,
              isTypeOnly: options.isTypeOnly,
            },
      ],
      exportable: true,
    }

    if (exportType === 'barrelNamed' && !output.exportAs && rootFile.exports?.[0]) {
      rootFile.exports = barrelManager.getNamedExport(root, rootFile.exports[0])
    }

    await this.#addOrAppend({
      ...rootFile,
      meta: meta ? meta : rootFile.meta,
    })
  }

  getCacheByUUID(UUID: KubbFile.UUID): KubbFile.File | undefined {
    let cache: KubbFile.File | undefined

    this.#cache.forEach((files) => {
      cache = files.find((item) => item.id === UUID)
    })
    return cache
  }

  get(path: KubbFile.Path): Array<KubbFile.File> | undefined {
    return this.#cache.get(path)
  }

  remove(path: KubbFile.Path): void {
    const cacheItem = this.get(path)
    if (!cacheItem) {
      return
    }

    this.#cache.delete(path)
  }

  async write(...params: Parameters<typeof write>): Promise<string | undefined> {
    return write(...params)
  }

  async read(...params: Parameters<typeof read>): Promise<string> {
    return read(...params)
  }

  // statics

  static async getSource<TMeta extends KubbFile.FileMetaBase = KubbFile.FileMetaBase>(file: KubbFile.File<TMeta>): Promise<string> {
    return getSource<TMeta>(file)
  }

  static combineFiles<TMeta extends KubbFile.FileMetaBase = KubbFile.FileMetaBase>(files: Array<KubbFile.File<TMeta> | null>): Array<KubbFile.File<TMeta>> {
    return combineFiles<TMeta>(files)
  }
  static getMode(path: string | undefined | null): KubbFile.Mode {
    if (!path) {
      return 'split'
    }
    return extname(path) ? 'single' : 'split'
  }

  static get extensions(): Array<KubbFile.Extname> {
    return ['.js', '.ts', '.tsx']
  }

  static isJavascript(baseName: string): boolean {
    return FileManager.extensions.some((extension) => baseName.endsWith(extension))
  }
}

function combineFiles<TMeta extends KubbFile.FileMetaBase = KubbFile.FileMetaBase>(files: Array<KubbFile.File<TMeta> | null>): Array<KubbFile.File<TMeta>> {
  return files.filter(Boolean).reduce(
    (acc, file: KubbFile.File<TMeta>) => {
      const prevIndex = acc.findIndex((item) => item.path === file.path)

      if (prevIndex === -1) {
        return [...acc, file]
      }

      const prev = acc[prevIndex]

      if (prev && file.override) {
        acc[prevIndex] = {
          imports: [],
          exports: [],
          ...file,
        }
        return acc
      }

      if (prev) {
        acc[prevIndex] = {
          ...file,
          source: prev.source && file.source ? `${prev.source}\n${file.source}` : '',
          imports: [...(prev.imports || []), ...(file.imports || [])],
          exports: [...(prev.exports || []), ...(file.exports || [])],
          env: { ...(prev.env || {}), ...(file.env || {}) },
        }
      }

      return acc
    },
    [] as Array<KubbFile.File<TMeta>>,
  )
}

export async function getSource<TMeta extends KubbFile.FileMetaBase = KubbFile.FileMetaBase>(file: KubbFile.File<TMeta>): Promise<string> {
  // only use .js, .ts or .tsx files for ESM imports

  if (file.language ? !['typescript', 'javascript'].includes(file.language) : !FileManager.isJavascript(file.baseName)) {
    return file.source
  }

  const parser = await getParser(file.language)

  const exports = file.exports ? combineExports(file.exports) : []
  // imports should be defined and source should contain code or we have imports without them being used
  const imports = file.imports && file.source ? combineImports(file.imports, exports, file.source) : []

  const importNodes = imports
    .filter((item) => {
      const path = item.root ? getRelativePath(item.root, item.path) : item.path
      // trim extName
      return path !== trimExtName(file.path)
    })
    .map((item) => {
      return parser.factory.createImportDeclaration({
        name: item.name,
        path: item.root ? getRelativePath(item.root, item.path) : item.path,
        isTypeOnly: item.isTypeOnly,
      })
    })
  const exportNodes = exports.map((item) =>
    parser.factory.createExportDeclaration({
      name: item.name,
      path: item.path,
      isTypeOnly: item.isTypeOnly,
      asAlias: item.asAlias,
    }),
  )

  const source = [parser.print([...importNodes, ...exportNodes]), getEnvSource(file.source, file.env)].join('\n')

  // do some basic linting with the ts compiler
  return parser.print([], { source, noEmitHelpers: false })
}

export function combineExports(exports: Array<KubbFile.Export>): Array<KubbFile.Export> {
  const combinedExports = orderBy(exports, [(v) => !v.isTypeOnly], ['asc']).reduce(
    (prev, curr) => {
      const name = curr.name
      const prevByPath = prev.findLast((imp) => imp.path === curr.path)
      const prevByPathAndIsTypeOnly = prev.findLast((imp) => imp.path === curr.path && isDeepEqual(imp.name, name) && imp.isTypeOnly)

      if (prevByPathAndIsTypeOnly) {
        // we already have an export that has the same path but uses `isTypeOnly` (export type ...)
        return prev
      }

      const uniquePrev = prev.findLast(
        (imp) => imp.path === curr.path && isDeepEqual(imp.name, name) && imp.isTypeOnly === curr.isTypeOnly && imp.asAlias === curr.asAlias,
      )

      if (uniquePrev || (Array.isArray(name) && !name.length) || (prevByPath?.asAlias && !curr.asAlias)) {
        return prev
      }

      if (!prevByPath) {
        return [
          ...prev,
          {
            ...curr,
            name: Array.isArray(name) ? [...new Set(name)] : name,
          },
        ]
      }

      if (prevByPath && Array.isArray(prevByPath.name) && Array.isArray(curr.name) && prevByPath.isTypeOnly === curr.isTypeOnly) {
        prevByPath.name = [...new Set([...prevByPath.name, ...curr.name])]

        return prev
      }

      return [...prev, curr]
    },
    [] as Array<KubbFile.Export>,
  )

  return orderBy(combinedExports, [(v) => !v.isTypeOnly, (v) => v.asAlias], ['desc', 'desc'])
}

export function combineImports(imports: Array<KubbFile.Import>, exports: Array<KubbFile.Export>, source?: string): Array<KubbFile.Import> {
  const combinedImports = orderBy(imports, [(v) => !v.isTypeOnly], ['asc']).reduce(
    (prev, curr) => {
      let name = Array.isArray(curr.name) ? [...new Set(curr.name)] : curr.name

      const hasImportInSource = (importName: string) => {
        if (!source) {
          return true
        }

        const checker = (name?: string) => name && !!source.includes(name)

        return checker(importName) || exports.some(({ name }) => (Array.isArray(name) ? name.some(checker) : checker(name)))
      }

      if (curr.path === curr.root) {
        // root and path are the same file, remove the "./" import
        return prev
      }

      if (Array.isArray(name)) {
        name = name.filter((item) => (typeof item === 'string' ? hasImportInSource(item) : hasImportInSource(item.propertyName)))
      }

      const prevByPath = prev.findLast((imp) => imp.path === curr.path && imp.isTypeOnly === curr.isTypeOnly)
      const uniquePrev = prev.findLast((imp) => imp.path === curr.path && isDeepEqual(imp.name, name) && imp.isTypeOnly === curr.isTypeOnly)
      const prevByPathNameAndIsTypeOnly = prev.findLast((imp) => imp.path === curr.path && isDeepEqual(imp.name, name) && imp.isTypeOnly)

      if (prevByPathNameAndIsTypeOnly) {
        // we already have an export that has the same path but uses `isTypeOnly` (import type ...)
        return prev
      }

      if (uniquePrev || (Array.isArray(name) && !name.length)) {
        return prev
      }

      if (!prevByPath) {
        return [
          ...prev,
          {
            ...curr,
            name,
          },
        ]
      }

      if (prevByPath && Array.isArray(prevByPath.name) && Array.isArray(name) && prevByPath.isTypeOnly === curr.isTypeOnly) {
        prevByPath.name = [...new Set([...prevByPath.name, ...name])]

        return prev
      }

      if (!Array.isArray(name) && name && !hasImportInSource(name)) {
        return prev
      }

      return [...prev, curr]
    },
    [] as Array<KubbFile.Import>,
  )

  return orderBy(combinedImports, [(v) => !v.isTypeOnly], ['desc'])
}

function getEnvSource(source: string, env: NodeJS.ProcessEnv | undefined): string {
  if (!env) {
    return source
  }

  const keys = Object.keys(env)

  if (!keys.length) {
    return source
  }

  return keys.reduce((prev, key: string) => {
    const environmentValue = env[key]
    const replaceBy = environmentValue ? `'${environmentValue.replaceAll('"', '')?.replaceAll("'", '')}'` : 'undefined'

    if (key.toUpperCase() !== key) {
      throw new TypeError(`Environment should be in upperCase for ${key}`)
    }

    if (typeof replaceBy === 'string') {
      prev = searchAndReplace({
        text: prev.replaceAll(`process.env.${key}`, replaceBy),
        replaceBy,
        prefix: 'process.env',
        key,
      })
      // removes `declare const ...`
      prev = searchAndReplace({
        text: prev.replaceAll(/(declare const).*\n/gi, ''),
        replaceBy,
        key,
      })
    }

    return prev
  }, source)
}

Notes

This feature could be useful for:

  • Types support for 'axios' options
  • Prototyping

Released under the MIT License.