build.ts 3.7 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394
  1. /**
  2. * User-owned tsdown configuration wrappers and child-process invocation.
  3. *
  4. * @module @deepseek-ai/dsh-scripts/build
  5. */
  6. import { createRequire } from 'node:module'
  7. import { existsSync, readFileSync, readdirSync } from 'node:fs'
  8. import { dirname, resolve } from 'node:path'
  9. import type { UserConfig } from 'tsdown'
  10. import { NodeCommandRunner, type CommandRunner } from '@deepseek-ai/dsh-helper'
  11. function hasLocalPluginPackages(root: string): boolean {
  12. const directory = resolve(root, 'plugins')
  13. return existsSync(directory) && readdirSync(directory, { withFileTypes: true }).some(
  14. item => item.isDirectory() && existsSync(resolve(directory, item.name, 'package.json')),
  15. )
  16. }
  17. function hasTsdownConfig(root: string): boolean {
  18. const hasConfigFile = [
  19. 'tsdown.config.ts', 'tsdown.config.mts', 'tsdown.config.cts',
  20. 'tsdown.config.js', 'tsdown.config.mjs', 'tsdown.config.cjs',
  21. 'tsdown.config.json',
  22. ]
  23. .some(name => existsSync(resolve(root, name)))
  24. if (hasConfigFile) return true
  25. let manifestText: string
  26. try {
  27. manifestText = readFileSync(resolve(root, 'package.json'), 'utf8')
  28. } catch (error) {
  29. if ((error as NodeJS.ErrnoException).code === 'ENOENT') return false
  30. throw error
  31. }
  32. const manifest: unknown = JSON.parse(manifestText)
  33. return manifest !== null && !Array.isArray(manifest) && typeof manifest === 'object'
  34. && Object.hasOwn(manifest, 'tsdown')
  35. }
  36. /**
  37. * Preserve the developer's root config and append a separate workspace pass
  38. * when generated local plugin packages exist.
  39. * @param tsdownConfig - developer-owned root tsdown config.
  40. * @returns root tsdown config and optional local-plugin workspace pass.
  41. */
  42. export function ProjectBuild(tsdownConfig: UserConfig): UserConfig[] {
  43. if (tsdownConfig.workspace !== undefined) {
  44. throw new Error('ProjectBuild owns workspace discovery; remove config.workspace')
  45. }
  46. const root = resolve(tsdownConfig.cwd ?? process.cwd())
  47. return hasLocalPluginPackages(root)
  48. ? [{ ...tsdownConfig }, { workspace: { include: ['plugins/*'] } }]
  49. : [{ ...tsdownConfig }]
  50. }
  51. /**
  52. * Preserve a local plugin package's developer-owned tsdown config.
  53. * @param tsdownConfig - developer-owned plugin tsdown config.
  54. * @returns validated tsdown config copy.
  55. */
  56. export function PluginBuild(tsdownConfig: UserConfig): UserConfig {
  57. if (tsdownConfig.workspace !== undefined) throw new Error('PluginBuild does not accept nested workspace config')
  58. return { ...tsdownConfig }
  59. }
  60. function resolveTsdownBin(cwd: string): string {
  61. const require = createRequire(resolve(cwd, 'package.json'))
  62. let manifestPath: string
  63. try {
  64. manifestPath = require.resolve('tsdown/package.json')
  65. } catch (error) {
  66. throw new Error(`dsh-sdk build requires tsdown in this project: ${String(error)}`)
  67. }
  68. const manifest = JSON.parse(readFileSync(manifestPath, 'utf8')) as { bin?: unknown }
  69. const bin = typeof manifest.bin === 'string'
  70. ? manifest.bin
  71. : manifest.bin && typeof manifest.bin === 'object'
  72. ? (manifest.bin as Record<string, unknown>).tsdown
  73. : undefined
  74. if (typeof bin !== 'string') throw new Error('installed tsdown package has no executable')
  75. return resolve(dirname(manifestPath), bin)
  76. }
  77. /** Invoke the project's installed tsdown, forwarding all build arguments. */
  78. export async function runProjectBuild(
  79. args: readonly string[],
  80. cwd: string = process.cwd(),
  81. runner: CommandRunner = new NodeCommandRunner(),
  82. ): Promise<void> {
  83. if (!hasTsdownConfig(cwd)) return
  84. const result = await runner.run(process.execPath, [resolveTsdownBin(cwd), ...args], resolve(cwd))
  85. if (result.signal) throw new Error(`tsdown was killed by ${result.signal}`)
  86. if (result.exitCode !== 0) throw new Error(`tsdown exited with code ${String(result.exitCode)}`)
  87. }