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