gen-shims.cjs 4.7 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091
  1. /**
  2. * gen-shims.cjs —— 重新生成 shims/*.ts 的转发清单(框架升级五步流程的辅助脚本,README §1.5)。
  3. *
  4. * 用法:node scripts/gen-shims.cjs
  5. * 原理:从 node_modules 安装的框架包(与 vendor 版本表同版本)读运行时公开 API,
  6. * 逐一生成为 `export const X = V.X;` 静态转发行。
  7. *
  8. * 类型策略(关键,决定 V 的静态类型):
  9. * V 收窄为 ESM 命名空间类型(import type * as XNS from 'pkg')。
  10. * 原因与限制:vue 3.5 起 **CJS 运行时导出 ≠ ESM 类型声明**——
  11. * ssrUtils / withAsyncContext / mergeDefaults 等键在运行时(require/UMD 全局)存在,
  12. * 但 ESM 声明文件未导出它们。若按 `typeof globalThis.Vue`(= ESM 类型)收窄,
  13. * 这些键的转发行会 TS 报"Property does not exist"。
  14. * 处理:对"运行时存在但 ESM 类型未声明"的键,用下方 EXCLUDE_* 清单显式跳过——
  15. * 这些键是 SSR/compat 边缘工具,纯浏览器客户端业务用不到,SFC 编译器产物也不会
  16. * import 它们(编译器只 import openBlock/createVNode 等 runtime-core 导出,全在 ESM 类型里)。
  17. * 跳过它们 = shims 只转发"ESM 类型已声明的公开 API",每个转发行类型精确、vue-tsc 零报错。
  18. * 维护纪律:升级框架(README §1.5)后重跑 pnpm typecheck,若出现新的
  19. * "Property 'X' does not exist"报错,把 X 补进对应 EXCLUDE 清单再重新生成本脚本。
  20. *
  21. * 静态逐一转发(而非 export * / default namespace):
  22. * ① 每个导出的值在模块顶层从 UMD 全局取值——dist 不含框架本体;
  23. * ② 模板编译器产物 import 的任何具名导出都能命中。
  24. *
  25. * 纪律:shims/ 是全项目唯一允许出现框架模块名的目录;本脚本是 shims 的唯一事实来源。
  26. */
  27. const fs = require('fs');
  28. const path = require('path');
  29. /**
  30. * 运行时导出但 ESM 类型未声明的键(按包名)。
  31. * vue 3.5:以下 10 个为 SSR/compat 边缘工具,浏览器客户端用不到,跳过。
  32. * 升级框架后按 pnpm typecheck 报错增量维护本清单。
  33. */
  34. const EXCLUDE = {
  35. vue: new Set([
  36. 'ssrUtils',
  37. 'withAsyncContext',
  38. 'mergeDefaults',
  39. 'mergeModels',
  40. 'resolveFilter',
  41. 'compatUtils',
  42. 'ErrorTypeStrings',
  43. 'assertNumber',
  44. 'createPropsRestProxy',
  45. 'initDirectivesForSSR',
  46. ]),
  47. 'vue-router': new Set(),
  48. pinia: new Set(),
  49. };
  50. function gen(pkgName, globalName, outFile) {
  51. const ns = require(pkgName);
  52. const keys = Object.keys(ns);
  53. const validId = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
  54. const excluded = EXCLUDE[pkgName] ?? new Set();
  55. const exportable = keys.filter((k) => validId.test(k) && !k.startsWith('__') && !excluded.has(k));
  56. const header = `/**
  57. * shims/${path.basename(outFile)} —— '${pkgName}' 模块名 → UMD 全局 ${globalName} 的唯一胶水层(README §3.3-1)。
  58. *
  59. * 存在理由:SFC 模板编译器(@vitejs/plugin-vue)产物内部会生成
  60. * 'import { X } from "${pkgName}"' 这类模块名 import,vite define 无法改写模块名(只处理裸标识符),
  61. * 故 vite.config 的 alias 将 '${pkgName}' 重定向到本文件。
  62. *
  63. * 关键实现(保证 dist 不含框架运行时):
  64. * - import type 仅取 ESM 命名空间类型(编译期擦除,不进产物);
  65. * - 运行时值全部取自 globalThis.${globalName}(index.html 引入的 UMD 全局);
  66. * - 编译后只剩"取全局 + 转发"代码,框架本体不进 dist。
  67. *
  68. * 本文件由 scripts/gen-shims.cjs 生成(导出清单来自当前安装版本,跳过 ESM 类型未声明的 SSR/compat 边缘键);
  69. * 框架升级(README §1.5 五步流程)时须重新生成并按 typecheck 报错维护排除清单。
  70. * 纪律:shims/ 是全项目唯一允许出现框架模块名的目录。
  71. */
  72. import type * as ${globalName.toUpperCase().replace(/[^A-Z0-9]/g, '_')}_NS from '${pkgName}';
  73. /** 运行时框架全局(index.html 按序注入;经 unknown 收窄为 ESM 命名空间类型) */
  74. const V: typeof ${globalName.toUpperCase().replace(/[^A-Z0-9]/g, '_')}_NS = (globalThis as unknown as { ${globalName}: typeof ${globalName.toUpperCase().replace(/[^A-Z0-9]/g, '_')}_NS }).${globalName};
  75. `;
  76. const body = exportable
  77. .map((k) => `// eslint-disable-next-line import/no-mutable-exports\nexport const ${k} = V.${k};`)
  78. .join('\n');
  79. fs.writeFileSync(outFile, header + body + '\n');
  80. console.log(`${outFile}: ${exportable.length} exports (excluded ${excluded.size})`);
  81. }
  82. const root = path.join(__dirname, '..');
  83. gen('vue', 'Vue', path.join(root, 'shims', 'vue.ts'));
  84. gen('vue-router', 'VueRouter', path.join(root, 'shims', 'vue-router.ts'));
  85. gen('pinia', 'Pinia', path.join(root, 'shims', 'pinia.ts'));