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 { AppMeta as SwaggerAppMeta, Exclude, Include, Override, ResolvePathOptions } from '@kubb/swagger'
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
}

type AppMeta = SwaggerAppMeta

export type PluginOptions = PluginFactoryOptions<'swagger-client', Options, ResolvedOptions, never, ResolvePathOptions, AppMeta>

declare module '@kubb/core' {
  export interface _Register {
    ['@kubb/swagger-client']: PluginOptions
  }
}
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
/* eslint-disable @typescript-eslint/no-namespace */
import crypto from 'node:crypto'
import { extname, resolve } from 'node:path'

import { print } from '@kubb/parser'
import * as factory from '@kubb/parser/factory'

import isEqual from 'lodash.isequal'
import { orderBy } from 'natural-orderby'
import PQueue from 'p-queue'

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 { BarrelManager } from './BarrelManager.ts'

import type { GreaterThan } from '@kubb/types'
import type { BarrelManagerOptions } from './BarrelManager.ts'
import type { Plugin } from './types.ts'

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 = 'file' | 'directory'

  /**
   * 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
    /**
     * This will override `process.env[key]` inside the `source`, see `getFileSource`.
     */
    env?: NodeJS.ProcessEnv
  }

  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
  }
  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 }

    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, options = {} }: AddIndexesProps): Promise<void> {
    const { exportType = 'barrel' } = output

    if (exportType === false) {
      return undefined
    }

    const pathToBuildFrom = resolve(root, output.path)
    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 === 'directory' ? `${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,
          },
      ],
    }

    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 getSource<TMeta extends KubbFile.FileMetaBase = KubbFile.FileMetaBase>(file: KubbFile.File<TMeta>): 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 'directory'
    }
    return extname(path) ? 'file' : 'directory'
  }

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

  static isExtensionAllowed(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 function getSource<TMeta extends KubbFile.FileMetaBase = KubbFile.FileMetaBase>(file: KubbFile.File<TMeta>): string {
  if (!FileManager.isExtensionAllowed(file.baseName)) {
    return file.source
  }

  const exports = file.exports ? combineExports(file.exports) : []
  const imports = file.imports ? combineImports(file.imports, exports, file.source) : []

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

  return [print([...importNodes, ...exportNodes]), getEnvSource(file.source, file.env)].join('\n')
}

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 && isEqual(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 && isEqual(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 (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 && isEqual(imp.name, name) && imp.isTypeOnly === curr.isTypeOnly)
    const prevByPathNameAndIsTypeOnly = prev.findLast((imp) => imp.path === curr.path && isEqual(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(new RegExp(`(declare const).*\n`, 'ig'), ''), replaceBy, key })
    }

    return prev
  }, source)
}

Notes

This feature could be useful for:

  • Types support for 'axios' options
  • Prototyping

Released under the MIT License.