import { $, useBash, usePwsh } from "zx" import { System } from "#Source/environment/index.ts" import type { ResolveFriendlyOptions } from "#Source/file-system/index.ts" import { findClosestFilePath, getFileContentInJson, resolveStartDirectory, toPathObject, } from "#Source/file-system/index.ts" const executeRawCommand = (command: string): string => command const normalizeExportCommand = (command: string): string => { if (command.includes("dotenvx run") === true && command.includes("--quiet") === false) { return command.replace("dotenvx run", "dotenvx run --quiet") } return command } export interface ExportSqlOptions extends ResolveFriendlyOptions { exportCommand?: string | undefined exportScript?: string | undefined } /** * @description 导出 Drizzle schema 对应的 SQL 字符串。 * * 当前实现支持两种入口: * * - `exportCommand`:直接执行调用方提供的原始命令字符串。 * - `exportScript`:先从最近的 `package.json` 中读取脚本内容,再按原样执行。 * * 这段逻辑在 Windows + VS Code + Vitest 集成测试场景里踩过一次很具体的问题: * 来自 `exportScript` 的 `drizzle-kit export` 命令,在终端里直接执行可以成功, * 但从 VS Code 的 Vitest 测试集成触发时会失败。后面排查确认,问题在于这里把 * `package.json` 里的脚本字符串交给 `zx` 去执行时,Windows 下的 shell 解析 * 路径和终端里的实际路径不一致。 * * 在当前开发环境里,显式调用 `usePowerShell()` 之后,这条执行链才稳定下来。 * * 这里还有一个设计边界要保留:`exportScript` 现在表达的是“脚本名”,而不是 * “可被本函数解析的结构化任务”。如果后续为了跨平台稳定性去硬解析脚本内容,就会 * 把 `exportScript` 的格式绑死,这并不理想。因此,更合适的演进方向不是改变 * `exportScript` 的语义,而是在未来新增一个结构化入口,让需要稳定执行模型的 * 场景显式选择它,同时保留 `exportScript` 的灵活性。 * * 候选设计如下: * * ```ts * type ExportSqlOptions = * | { exportScript: string } * | { exportCommand: string } * | { * exportTask: { * command: string * args: string[] * cwd?: string * envFilePath?: string * } * } * ``` * * 这样的分层可以同时满足两类需求: * * - 继续允许 `exportScript` 复用任意 package script,不限制脚本内部长什么样。 * - 给 VS Code 测试集成、CI、Windows 多 shell 环境这类对进程模型更敏感的场景,提供一个更可预测的执行入口。 */ export const exportSql = async (options: ExportSqlOptions): Promise => { if (options.exportCommand === undefined && options.exportScript === undefined) { throw new Error( "exportCommand 和 exportScript 不能同时为 undefined,请至少提供一个用于执行导出 SQL 操作的命令或脚本。", ) } const startDirectory = resolveStartDirectory({ startPath: options.startPath }) let preparedExportCommand: string | undefined = options.exportCommand if (preparedExportCommand === undefined) { const preparedExportScript = options.exportScript! const packageJsonFilePath = findClosestFilePath({ startPath: startDirectory, targetBase: "package.json", }) if (packageJsonFilePath === undefined) { throw new Error(`未找到 package.json 文件,无法读取 ${preparedExportScript}。`) } const packageJson = getFileContentInJson(packageJsonFilePath) if (typeof packageJson !== "object" || packageJson === null) { throw new Error(`Invalid package.json content in ${packageJsonFilePath}`) } if ("scripts" in packageJson === false) { throw new Error(`package.json 文件中缺少 scripts 字段,无法读取 ${preparedExportScript}。`) } if (typeof packageJson.scripts !== "object" || packageJson.scripts === null) { throw new Error(`Invalid scripts field in package.json at ${packageJsonFilePath}`) } if (preparedExportScript in packageJson.scripts === false) { throw new Error( `package.json 文件中的 scripts 字段缺少 ${preparedExportScript},无法读取用于导出 SQL 的命令。`, ) } const scripts = packageJson.scripts as Record if (typeof scripts[preparedExportScript] !== "string") { throw new Error( `package.json 文件中的 scripts 字段的 ${preparedExportScript} 不是一个字符串,无法读取用于导出 SQL 的命令。`, ) } preparedExportCommand = scripts[preparedExportScript] } preparedExportCommand = normalizeExportCommand(preparedExportCommand) const drizzleConfigFilePath = findClosestFilePath({ startPath: startDirectory, targetBase: "drizzle.config.ts", }) if (drizzleConfigFilePath === undefined) { throw new Error( `未找到 drizzle.config.ts 文件,无法执行导出 SQL 操作。请确保在 ${startDirectory} 及其父级目录中存在 drizzle.config.ts 文件。`, ) } const drizzleKitCliWorkingDirectory = toPathObject(drizzleConfigFilePath).dir System.useSystems({ windows: () => { usePwsh() }, default: () => { useBash() }, }) const exportResult = await $({ cwd: drizzleKitCliWorkingDirectory, nothrow: true, quote: executeRawCommand, })`${preparedExportCommand}` if (exportResult.ok === false) { const errorMessage = exportResult.stderr.trim() || exportResult.stdout.trim() throw new Error(`执行 drizzle-kit export 命令失败,错误信息:${errorMessage}`) } const exportedSql = exportResult.stdout.trim() return exportedSql }