@vunk/shared

genDtsFiles

生成 d.ts 文件

Test

ts
import path from 'node:path'
import { fixPath } from '@lib-env/build-utils'
import { packagesDir, workRoot } from '@lib-env/path'
import { it } from 'vitest'
import { genDtsFiles } from '../genDtsFiles'

const testable = false

it('genDtsFiles', {
  timeout: 1000 * 60 * 10,
}, async () => {
  if (!testable) {
    return
  }

  await genDtsFiles({
    root: workRoot,
    compilerOptions: {
      outDir: path.resolve(workRoot, 'dist/dts_test'),
      baseUrl: workRoot,
    },
    transform: fixPath,
    globSource: [
      '**/*.ts',
      '**/*.vue',
    ],
    globCwd: path.resolve(packagesDir, 'markdown'),

  })
})

build/morph/__tests__/genDtsFiles.test.ts

Source

ts
import type { Pattern } from 'fast-glob'
import type { CompilerOptions, OutputFile, ProjectOptions, SourceFile } from 'ts-morph'
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
import path from 'node:path'
import { compileScript, parse } from '@vue/compiler-sfc'
import { glob } from 'fast-glob'
import { ModuleResolutionKind, Project, ScriptTarget } from 'ts-morph'
import { JsxEmit } from 'typescript'

export interface GenDtsFilesSettings {

  root: string

  /**
   * @example {
   *  outDir: 'build',
   *  baseUrl: workRoot,
      paths: {
        [`${LIB_NAME}/*`]: ['packages/*'],
        [`${LIB_ALIAS}/*`]: ['packages/*'],
      },
   * }
   */
  compilerOptions: CompilerOptions

  projectOptions?: ProjectOptions

  globCwd?: string
  globSource: Pattern | Pattern[]
  globIgnore?: string[]

  tsConfigFilePath?: string

  transform?: (code: string) => string

  projectEmit?: boolean

}

export async function genDtsFiles (settings: GenDtsFilesSettings) {
  const workRoot = settings.root

  const compilerOptions = settings.compilerOptions

  const defaultTsConfigFilePath = path.resolve(workRoot, 'tsconfig.json')
  const tsConfigFilePath = settings.tsConfigFilePath ?? (
    existsSync(defaultTsConfigFilePath)
      ? defaultTsConfigFilePath
      : undefined
  )

  const globSource = settings.globSource
  const globCwd = settings.globCwd ?? workRoot

  const globIgnore = settings.globIgnore ?? [
    'gulpfile.ts',
    'package.json',
    'node_modules',
    '**/README.md',
    '**/__tests__',
  ]

  const transform = settings.transform ?? (code => code)

  const projectEmit = settings.projectEmit

  const project = new Project({
    compilerOptions: {
      allowJs: true,
      declaration: true,
      emitDeclarationOnly: true,
      noEmitOnError: true,
      strict: false,
      jsx: JsxEmit.Preserve,
      disableSizeLimit: true,
      esModuleInterop: true,
      preserveSymlinks: true,
      moduleResolution: ModuleResolutionKind.Node10,
      target: ScriptTarget.ESNext,
      skipLibCheck: true,
      skipDefaultLibCheck: true,
      baseUrl: workRoot,

      ...compilerOptions,
    },

    tsConfigFilePath,

    skipAddingFilesFromTsConfig: true,
    ...settings.projectOptions,
  })

  const filePaths = await glob(globSource, {
    cwd: globCwd,
    onlyFiles: true,
    absolute: true,
    ignore: globIgnore,
  })

  // 添加全局类型
  project.addSourceFilesAtPaths(
    path.resolve(workRoot, 'typings', './**/*{.d.ts,.ts}'),
  )

  const sourceFiles: SourceFile[] = []
  for (const file of filePaths) {
    // 处理.vue文件成.ts文件
    if (file.endsWith('.vue')) {
      const content = readFileSync(file, 'utf-8')
      const sfc = parse(content)
      const { script, scriptSetup } = sfc.descriptor
      if (script || scriptSetup) {
        let content = script?.content ?? ''
        if (scriptSetup) {
          const compiled = compileScript(sfc.descriptor, {
            id: 'xxx',
          })
          content += compiled.content
        }
        const lang = scriptSetup?.lang || script?.lang || 'js'
        const sourceFile = project.createSourceFile(
          `${path.relative(process.cwd(), file)}.${lang}`,
          content,
        )
        sourceFiles.push(sourceFile)
      }
    }

    if (file.endsWith('.ts')) {
      const sourceFile = project.addSourceFileAtPath(file)
      sourceFiles.push(sourceFile)
    }
  }

  const diagnostics = project.getPreEmitDiagnostics()

  if (diagnostics.length > 0) {
    console.warn(
      project.formatDiagnosticsWithColorAndContext(diagnostics),
    )
  }

  // 发射.d.ts 文件到内存

  if (projectEmit) {
    await project.emit({
      emitOnlyDtsFiles: true,
    })
  }

  const outputFiles: OutputFile[] = []
  for (const sourceFile of sourceFiles) {
    const relativePath = path.relative(workRoot, sourceFile.getFilePath())

    const emitOutput = sourceFile.getEmitOutput()
    const emitFiles = emitOutput.getOutputFiles()

    if (emitFiles.length === 0) {
      console.warn(`没有找到要输出的文件: ${relativePath}`)
      return
    }

    outputFiles.push(...emitFiles)
  }

  for (const outputFile of outputFiles) {
    const filepath = outputFile.getFilePath()

    mkdirSync(path.dirname(filepath), {
      recursive: true,
    })

    writeFileSync(
      filepath,
      transform(outputFile.getText()),
      'utf8',
    )
  }
}

build/morph/genDtsFiles