bundle.mjs 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108
  1. /**
  2. * 打成单文件 ESM 产物,供 dsh 以原生 ESM 直接加载。
  3. *
  4. * 只构建插件入口 lib/index.js 一份产物;技能脚本(skills 下各 scripts 目录的 .mjs)
  5. * 是手写薄入口(R21/裁决 A1,2026-09-05)——运行能力经相对引 lib/index.js,构建尾部
  6. * 只做接缝校验,不再内联任何 core 副本进 skills/。
  7. *
  8. * 为什么必须打包(2026-09-01 真机冒烟实证):
  9. * dsh 用原生 ESM `import()` 吃插件入口,而 Node 的 ESM 解析器不补全扩展名;
  10. * 本仓 tsconfig 走 moduleResolution: "Bundler",源码里 229 处相对导入都无扩展名,
  11. * 且 `@webnovel/core` 是源码直引 —— dsh 端没有 node_modules/@webnovel/*。
  12. * 直接把 dsh-local.yml 指向 src/index.ts 会以 ERR_MODULE_NOT_FOUND 加载失败。
  13. * 单文件产物同时解掉这两条:相对导入全部内联,跨包依赖一并内联。
  14. * 形态对齐 dsh 自身插件(@deepseek-ai/dsh-skill-filesystem:main=lib/index.js,产物内零相对导入)。
  15. */
  16. import * as fs from 'node:fs'
  17. import * as path from 'node:path'
  18. import { fileURLToPath } from 'node:url'
  19. import { isBuiltin } from 'node:module'
  20. import * as esbuild from 'esbuild'
  21. import { writeNotices } from '../../../scripts/release/notices.mjs'
  22. import { thinScripts as THIN_SCRIPTS } from './artifact-contract.mjs'
  23. const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
  24. const manifest = JSON.parse(fs.readFileSync(path.join(packageRoot, 'package.json'), 'utf8'))
  25. /** dsh 宿主提供的服务面,不进产物。 */
  26. const EXTERNAL = ['@deepseek-ai/cordis', '@deepseek-ai/dsh-tools', '@deepseek-ai/dsh-llm', '@deepseek-ai/dsh-skill-filesystem', 'node:*']
  27. function checkDependencies(result) {
  28. for (const output of Object.values(result.metafile.outputs)) {
  29. for (const entry of output.imports.filter(entry => entry.external)) {
  30. if (!isBuiltin(entry.path) && !manifest.peerDependencies?.[entry.path] && !manifest.dependencies?.[entry.path]) {
  31. throw new Error(`未声明产物运行依赖: ${entry.path}`)
  32. }
  33. }
  34. }
  35. const shared = /node_modules\/(?:@deepseek-ai\/(?:cordis|dsh-tools|dsh-llm|dsh-skill-filesystem)\/|react\/)/
  36. for (const input of Object.keys(result.metafile.inputs)) {
  37. if (shared.test(input.replaceAll('\\', '/'))) throw new Error(`共享运行时被内联: ${input}`)
  38. }
  39. }
  40. async function buildEntry(entry, outfile) {
  41. const result = await esbuild.build({
  42. entryPoints: [entry],
  43. outfile,
  44. bundle: true,
  45. platform: 'node',
  46. target: 'node22.19',
  47. format: 'esm',
  48. external: EXTERNAL,
  49. sourcemap: false,
  50. legalComments: 'inline',
  51. logLevel: 'warning',
  52. metafile: true,
  53. // 内联的 CJS 依赖(yaml@2.9.0)内部 require('process') 会落到 esbuild 的 __require
  54. // 垫片上并在运行时抛 "Dynamic require of X is not supported"(2026-09-01 真机实证)。
  55. // 注入真 require 让这类调用回到 Node 内置解析。
  56. banner: {
  57. js: [
  58. "import { createRequire as __createRequire } from 'node:module'",
  59. 'const require = __createRequire(import.meta.url)',
  60. ].join('\n'),
  61. },
  62. })
  63. const bytes = fs.statSync(outfile).size
  64. checkDependencies(result)
  65. const inlined = Object.keys(result.metafile.inputs).length
  66. console.log(`[bundle] ${path.relative(packageRoot, outfile)} ok (${(bytes / 1024).toFixed(1)} KB, 内联 ${inlined} 个模块)`)
  67. const leftover = /from\s*["']\.[^"']*["']/.exec(fs.readFileSync(outfile, 'utf8'))
  68. if (leftover !== null) throw new Error(`产物残留相对导入:${leftover[0]}`)
  69. console.log(`[bundle] 自检:产物内零相对导入 (${path.relative(packageRoot, outfile)})`)
  70. return result
  71. }
  72. const hostBuild = await buildEntry(path.join(packageRoot, 'src', 'index.ts'), path.join(packageRoot, 'lib', 'index.js'))
  73. // DSH's native client module loader supplies the shared React runtime.
  74. const clientBuild = await esbuild.build({
  75. entryPoints: [path.join(packageRoot, 'src', 'client', 'index.tsx')],
  76. outfile: path.join(packageRoot, 'lib', 'client.js'),
  77. platform: 'browser', format: 'cjs', bundle: true, external: ['react'], target: 'chrome110',
  78. loader: { '.css': 'text' }, minify: true, legalComments: 'inline', metafile: true,
  79. banner: { js: `window.__ModuleLoader__.load({ id: ${JSON.stringify(manifest.name)}, factory: (require) => { const module = { exports: {} }; const exports = module.exports;` },
  80. footer: { js: 'return module.exports; } });' },
  81. })
  82. checkDependencies(clientBuild)
  83. console.log('[bundle] lib/client.js ok (DSH native client module)')
  84. /**
  85. * 技能脚本薄入口接缝校验(R21/裁决 A1)。
  86. *
  87. * 脚本产物形态已定:skills 下各 scripts 目录的 .mjs 是**手写薄入口**(进 git),运行能力
  88. * 经相对引插件根 `lib/index.js`,不再由本构建内联 core 副本。这里逐个校验薄入口与 lib
  89. * 的接缝(入口存在/引 lib/lib 导出对应能力),防止「删了 re-export、脚本只在运行时才炸」。
  90. */
  91. const libText = fs.readFileSync(path.join(packageRoot, 'lib', 'index.js'), 'utf8')
  92. for (const [rel, fn] of THIN_SCRIPTS) {
  93. const abs = path.join(packageRoot, ...rel.split('/'))
  94. if (!fs.existsSync(abs)) throw new Error(`脚本薄入口缺失:${rel}`)
  95. const text = fs.readFileSync(abs, 'utf8')
  96. if (!text.includes('lib/index.js')) throw new Error(`脚本薄入口未引 lib/index.js(违背裁决 A1):${rel}`)
  97. if (!libText.includes(fn)) throw new Error(`lib/index.js 未导出 ${fn},薄入口将无法加载:${rel}`)
  98. }
  99. console.log(`[bundle] 脚本薄入口接缝校验 ok (${THIN_SCRIPTS.length} 个薄入口 -> lib/index.js)`)
  100. writeNotices(packageRoot, [hostBuild, clientBuild])