@hzab/vite-config 0.0.2-alpha.0 → 0.0.3-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. package/CHANGELOG.md +95 -83
  2. package/LICENSE +21 -21
  3. package/README.md +184 -173
  4. package/dist/create-app-vite-config.cjs +160 -27
  5. package/dist/create-app-vite-config.cjs.map +1 -1
  6. package/dist/create-app-vite-config.js +160 -27
  7. package/dist/create-app-vite-config.js.map +1 -1
  8. package/dist/create-library-vite-config.cjs +39 -4
  9. package/dist/create-library-vite-config.cjs.map +1 -1
  10. package/dist/create-library-vite-config.js +38 -3
  11. package/dist/create-library-vite-config.js.map +1 -1
  12. package/dist/create-package-vitest-config.cjs.map +1 -1
  13. package/dist/create-package-vitest-config.js.map +1 -1
  14. package/dist/create-vite-config.cjs +160 -27
  15. package/dist/create-vite-config.cjs.map +1 -1
  16. package/dist/create-vite-config.js +160 -27
  17. package/dist/create-vite-config.js.map +1 -1
  18. package/dist/index.cjs +198 -30
  19. package/dist/index.cjs.map +1 -1
  20. package/dist/index.d.cts +1 -1
  21. package/dist/index.d.ts +1 -1
  22. package/dist/index.js +199 -31
  23. package/dist/index.js.map +1 -1
  24. package/dist/plugins/copy.cjs.map +1 -1
  25. package/dist/plugins/copy.js.map +1 -1
  26. package/dist/plugins/hash-deploy.cjs +150 -25
  27. package/dist/plugins/hash-deploy.cjs.map +1 -1
  28. package/dist/plugins/hash-deploy.d.cts +16 -5
  29. package/dist/plugins/hash-deploy.d.ts +16 -5
  30. package/dist/plugins/hash-deploy.js +150 -25
  31. package/dist/plugins/hash-deploy.js.map +1 -1
  32. package/dist/plugins/less-tilde.cjs.map +1 -1
  33. package/dist/plugins/less-tilde.d.cts +17 -2
  34. package/dist/plugins/less-tilde.d.ts +17 -2
  35. package/dist/plugins/less-tilde.js.map +1 -1
  36. package/dist/plugins/lifecycle.cjs.map +1 -1
  37. package/dist/plugins/lifecycle.js.map +1 -1
  38. package/dist/plugins/public-assets.cjs.map +1 -1
  39. package/dist/plugins/public-assets.js.map +1 -1
  40. package/dist/plugins/public-config.cjs.map +1 -1
  41. package/dist/plugins/public-config.js.map +1 -1
  42. package/dist/plugins/react-compiler.cjs.map +1 -1
  43. package/dist/plugins/react-compiler.js.map +1 -1
  44. package/dist/plugins/relative-base.cjs.map +1 -1
  45. package/dist/plugins/relative-base.js.map +1 -1
  46. package/dist/plugins/stub-less.cjs.map +1 -1
  47. package/dist/plugins/stub-less.js.map +1 -1
  48. package/dist/plugins/svg-react.cjs.map +1 -1
  49. package/dist/plugins/svg-react.js.map +1 -1
  50. package/dist/types.d.cts +19 -8
  51. package/dist/types.d.ts +19 -8
  52. package/dist/vitest.cjs.map +1 -1
  53. package/dist/vitest.js.map +1 -1
  54. package/docs/README.md +15 -15
  55. package/docs/abt-management-ui/vite.config.mts +226 -226
  56. package/docs/api.md +404 -366
  57. package/docs/migration-from-webpack.md +723 -722
  58. package/docs/sync-public-path.md +52 -52
  59. package/docs/vite-config-integration.md +477 -476
  60. package/docs/vite-pnpm-integration.md +279 -279
  61. package/docs/vs-ccc-vite-config.md +26 -26
  62. package/docs/webpack-gap.md +108 -107
  63. package/package.json +1 -1
  64. package/src/client.d.ts +9 -9
  65. package/src/create-app-vite-config.ts +236 -218
  66. package/src/create-library-vite-config.ts +248 -199
  67. package/src/create-package-vitest-config.ts +54 -54
  68. package/src/create-vite-config.ts +22 -22
  69. package/src/index.ts +41 -42
  70. package/src/plugins/copy.ts +25 -25
  71. package/src/plugins/hash-deploy.ts +239 -199
  72. package/src/plugins/hash-dir-name.ts +212 -0
  73. package/src/plugins/less-tilde.ts +72 -44
  74. package/src/plugins/lifecycle.ts +40 -40
  75. package/src/plugins/public-assets.ts +28 -28
  76. package/src/plugins/public-config.ts +34 -34
  77. package/src/plugins/react-compiler.ts +32 -32
  78. package/src/plugins/relative-base.ts +20 -20
  79. package/src/plugins/stub-less.ts +26 -26
  80. package/src/plugins/svg-react.ts +90 -90
  81. package/src/svg-react-runtime.ts +4 -4
  82. package/src/svg-react.d.ts +6 -6
  83. package/src/types.ts +325 -314
  84. package/src/vitest-types.ts +20 -20
  85. package/src/vitest.ts +2 -2
  86. package/templates/src/vite-env.d.ts +2 -2
  87. package/templates/tsconfig.json +12 -12
  88. package/templates/vite.config.ts +26 -26
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/plugins/hash-deploy.ts"],"names":["fs","path","execFileSync","repositoryRoot"],"mappings":"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","file":"hash-deploy.cjs","sourcesContent":["import { execFileSync } from \"node:child_process\";\r\nimport fs from \"node:fs\";\r\nimport path from \"node:path\";\r\n\r\nimport type { PluginOption } from \"vite\";\r\n\r\n/** @public */\r\nexport type HashTimestampFormat = \"epoch\" | \"datetime\";\r\n\r\n/** @public */\r\nexport interface HashBuildDirNameOptions {\r\n repositoryRoot?: string;\r\n now?: number;\r\n /** @defaultValue \"epoch\" */\r\n timestampFormat?: HashTimestampFormat;\r\n}\r\n\r\n/** @public */\r\nexport interface HashCleanupOptions {\r\n /**\r\n * 每次成功构建后保留的 Hash 版本总数(含本次构建)。\r\n * 必须为大于等于 1 的整数。\r\n */\r\n keep: number;\r\n}\r\n\r\n/** @public */\r\nexport interface HashDeployPluginOptions {\r\n rootDir: string;\r\n /** hash 子目录名,如 `{commit}-{timestamp}` 或 `{commit}-YYYY-MM-DD-HH-mm-ss` */\r\n buildDirStr: string;\r\n /**\r\n * 与最终 Vite `base` 一致的资源前缀。\r\n * - 默认相对:`./{buildDirStr}/`\r\n * - 自定义 CDN:`https://cdn.example.com/app/{buildDirStr}/`\r\n */\r\n publicPath: string;\r\n /** 旧 Hash 目录清理策略;未传时永久保留,由部署系统负责清理。 */\r\n cleanup?: HashCleanupOptions;\r\n}\r\n\r\nconst HASH_BUILD_DIR_PATTERN = /^(?:[0-9a-f]{12}|local)-(\\d{4}-\\d{2}-\\d{2}-\\d{2}-\\d{2}-\\d{2}|\\d+)$/;\r\n\r\nfunction parseHashTimestamp(value: string): number {\r\n if (/^\\d+$/.test(value)) return Number(value);\r\n const [year = 0, month = 1, day = 1, hour = 0, minute = 0, second = 0] = value\r\n .split(\"-\")\r\n .map(Number);\r\n return new Date(year, month - 1, day, hour, minute, second).getTime();\r\n}\r\n\r\nfunction padTwo(value: number): string {\r\n return String(value).padStart(2, \"0\");\r\n}\r\n\r\nfunction formatHashTimestamp(now: number, format: HashTimestampFormat = \"epoch\"): string {\r\n if (format !== \"datetime\") return String(now);\r\n const date = new Date(now);\r\n return [\r\n date.getFullYear(),\r\n padTwo(date.getMonth() + 1),\r\n padTwo(date.getDate()),\r\n padTwo(date.getHours()),\r\n padTwo(date.getMinutes()),\r\n padTwo(date.getSeconds()),\r\n ].join(\"-\");\r\n}\r\n\r\nfunction cleanupOldHashBuilds(deployRoot: string, currentBuildDir: string, keep: number): void {\r\n const oldBuilds = fs\r\n .readdirSync(deployRoot, { withFileTypes: true })\r\n .flatMap((entry) => {\r\n if (!entry.isDirectory() || entry.name === currentBuildDir) return [];\r\n const match = HASH_BUILD_DIR_PATTERN.exec(entry.name);\r\n return match ? [{ name: entry.name, timestamp: parseHashTimestamp(match[1]!) }] : [];\r\n })\r\n .sort((left, right) => right.timestamp - left.timestamp || right.name.localeCompare(left.name));\r\n\r\n for (const build of oldBuilds.slice(Math.max(keep - 1, 0))) {\r\n fs.rmSync(path.join(deployRoot, build.name), { recursive: true });\r\n }\r\n}\r\n\r\n/**\r\n * 构建结束后写 `dist/hash.txt`,并把 hash 子目录中的 `index.html` 复制到 `dist/index.html`。\r\n * 相对 `base` 时为根入口补上 hash 前缀;绝对 `base` 时产物已含完整 URL,原样复制即可。\r\n * 配置 `cleanup.keep` 时,成功写入根入口后只保留指定数量的最新 Hash 版本。\r\n * @public\r\n */\r\nexport function hashDeployPlugin(options: HashDeployPluginOptions): PluginOption {\r\n const { rootDir, buildDirStr, publicPath, cleanup } = options;\r\n if (cleanup && (!Number.isInteger(cleanup.keep) || cleanup.keep < 1)) {\r\n throw new Error(\"[hash-deploy] cleanup.keep must be an integer greater than or equal to 1.\");\r\n }\r\n let outputDir: string | undefined;\r\n let deployRoot: string | undefined;\r\n\r\n return {\r\n name: \"hash-deploy\",\r\n enforce: \"post\",\r\n configResolved(config) {\r\n const resolvedRoot = path.resolve(config.root);\r\n const expectedRoot = path.resolve(rootDir);\r\n if (resolvedRoot !== expectedRoot) {\r\n throw new Error(\r\n `[hash-deploy] Vite root \"${resolvedRoot}\" must match rootDir \"${expectedRoot}\".`,\r\n );\r\n }\r\n // 相对 hash:工厂 publicPath 为 `./{hash}/`,但 Vite base 必须保持 `./`。\r\n const expectedBase = publicPath.startsWith(\"./\") ? \"./\" : publicPath;\r\n if (config.base !== expectedBase) {\r\n throw new Error(\r\n `[hash-deploy] Vite base \"${config.base}\" must remain \"${expectedBase}\" during hash deployment.`,\r\n );\r\n }\r\n\r\n outputDir = path.resolve(resolvedRoot, config.build.outDir);\r\n if (path.basename(outputDir) !== buildDirStr) {\r\n throw new Error(\r\n `[hash-deploy] build.outDir \"${config.build.outDir}\" must end with the hash directory \"${buildDirStr}\".`,\r\n );\r\n }\r\n deployRoot = path.dirname(outputDir);\r\n },\r\n writeBundle: {\r\n sequential: true,\r\n order: \"post\",\r\n handler() {\r\n if (!outputDir || !deployRoot) {\r\n throw new Error(\"[hash-deploy] Vite configuration was not resolved before writeBundle.\");\r\n }\r\n\r\n const sourceIndex = path.join(outputDir, \"index.html\");\r\n if (!fs.existsSync(sourceIndex)) {\r\n throw new Error(`[hash-deploy] Generated entry was not found: \"${sourceIndex}\".`);\r\n }\r\n\r\n fs.mkdirSync(deployRoot, { recursive: true });\r\n fs.writeFileSync(path.join(deployRoot, \"hash.txt\"), buildDirStr);\r\n const sourceHtml = fs.readFileSync(sourceIndex, \"utf8\");\r\n // 相对 base(`./{hash}/`)时子目录 HTML 中的资源均相对于自身,根入口需统一补 hash;\r\n // 绝对 base 时 Vite 已写出完整 URL,直接复制。\r\n const rootHtml = publicPath.startsWith(\"./\")\r\n ? sourceHtml.replace(/(\\b(?:href|src)\\s*=\\s*[\"'])\\.\\//g, `$1${publicPath}`)\r\n : sourceHtml;\r\n fs.writeFileSync(path.join(deployRoot, \"index.html\"), rootHtml);\r\n if (cleanup) {\r\n cleanupOldHashBuilds(deployRoot, buildDirStr, cleanup.keep);\r\n }\r\n },\r\n },\r\n };\r\n}\r\n\r\n/**\r\n * 从指定仓库读取短 commit hash;非 git 环境回退 `local`。\r\n * @public\r\n */\r\nexport function getCommitHash(repositoryRoot: string = process.cwd()): string {\r\n try {\r\n return execFileSync(\"git\", [\"rev-parse\", \"HEAD\"], {\r\n cwd: repositoryRoot,\r\n encoding: \"utf8\",\r\n stdio: [\"ignore\", \"pipe\", \"ignore\"],\r\n })\r\n .trim()\r\n .slice(0, 12);\r\n } catch {\r\n return \"local\";\r\n }\r\n}\r\n\r\n/**\r\n * 生成 hash 构建目录名;保留仅传时间戳的旧调用方式。\r\n * @public\r\n */\r\nexport function createHashBuildDirName(now?: number): string;\r\n/** @public */\r\nexport function createHashBuildDirName(repositoryRoot: string, now?: number): string;\r\n/** @public */\r\nexport function createHashBuildDirName(options: HashBuildDirNameOptions): string;\r\nexport function createHashBuildDirName(\r\n repositoryRootOrNowOrOptions?: string | number | HashBuildDirNameOptions,\r\n now: number = Date.now(),\r\n): string {\r\n if (repositoryRootOrNowOrOptions != null && typeof repositoryRootOrNowOrOptions === \"object\") {\r\n const {\r\n repositoryRoot,\r\n now: optionsNow = Date.now(),\r\n timestampFormat = \"epoch\",\r\n } = repositoryRootOrNowOrOptions;\r\n return `${getCommitHash(repositoryRoot)}-${formatHashTimestamp(optionsNow, timestampFormat)}`;\r\n }\r\n const repositoryRoot =\r\n typeof repositoryRootOrNowOrOptions === \"string\" ? repositoryRootOrNowOrOptions : undefined;\r\n const timestamp =\r\n typeof repositoryRootOrNowOrOptions === \"number\" ? repositoryRootOrNowOrOptions : now;\r\n return `${getCommitHash(repositoryRoot)}-${formatHashTimestamp(timestamp)}`;\r\n}\r\n"]}
1
+ {"version":3,"sources":["../../src/plugins/hash-dir-name.ts","../../src/plugins/hash-deploy.ts"],"names":["fs","path","execFileSync","repositoryRoot"],"mappings":";;;;;;;;;;;;;;AAqBA,IAAM,eAAe,MAAc,yCAAA;AAEnC,IAAM,gBAAA,GAAmB,EAAE,IAAA,EAAM,CAAA,EAAG,IAAI,CAAA,EAAG,EAAA,EAAI,CAAA,EAAG,EAAA,EAAI,GAAG,EAAA,EAAI,CAAA,EAAG,EAAA,EAAI,CAAA,EAAG,KAAK,CAAA,EAAE;AAK9E,IAAM,eAAA,GAAkB;AAAA,EACtB,IAAA,EAAM,MAAA;AAAA,EACN,EAAA,EAAI,OAAA;AAAA,EACJ,EAAA,EAAI,KAAA;AAAA,EACJ,EAAA,EAAI,MAAA;AAAA,EACJ,EAAA,EAAI,QAAA;AAAA,EACJ,EAAA,EAAI,QAAA;AAAA,EACJ,GAAA,EAAK;AACP,CAAA;AAIA,IAAM,WAAA,GAAc,CAAC,WAAA,EAAa,MAAA,EAAQ,MAAM,IAAA,EAAM,IAAA,EAAM,IAAA,EAAM,IAAA,EAAM,KAAK,CAAA;AAE7E,IAAM,gBAAA,GAAmB,CAAC,MAAA,EAAQ,GAAG,WAAW,CAAA;AAGzC,IAAM,6BAAA,GAAgC,gBAAA;AAG7C,IAAM,wBAAA,GAA2B,cAAA;AAEjC,IAAM,GAAA,GAAM,CAAC,KAAA,EAAe,MAAA,GAAS,CAAA,KAAc,OAAO,KAAK,CAAA,CAAE,QAAA,CAAS,MAAA,EAAQ,GAAG,CAAA;AAErF,SAAS,aAAa,KAAA,EAAuB;AAC3C,EAAA,OAAO,KAAA,CAAM,OAAA,CAAQ,qBAAA,EAAuB,MAAM,CAAA;AACpD;AAEA,SAAS,mBAAmB,OAAA,EAAuB;AACjD,EAAA,IAAI,OAAA,CAAQ,IAAA,EAAK,KAAM,EAAA,EAAI;AACzB,IAAA,MAAM,IAAI,MAAM,uDAAuD,CAAA;AAAA,EACzE;AACF;AAGA,SAAS,iBAAA,CAAkB,SAAiB,OAAA,EAAuB;AACjE,EAAA,MAAM,SACJ,wBAAA,CAAyB,IAAA,CAAK,OAAO,CAAA,IACrC,OAAA,CAAQ,SAAS,IAAI,CAAA,IACrB,QAAQ,UAAA,CAAW,GAAG,KACtB,OAAA,CAAQ,QAAA,CAAS,GAAG,CAAA,IACpB,OAAA,KAAY,QAAQ,IAAA,EAAK;AAE3B,EAAA,IAAI,MAAA,EAAQ;AACV,IAAA,MAAM,IAAI,KAAA;AAAA,MACR,CAAA,2BAAA,EAA8B,OAAO,CAAA,qCAAA,EAAwC,OAAO,CAAA,EAAA;AAAA,KACtF;AAAA,EACF;AACF;AAMO,SAAS,kBAAkB,OAAA,EAIvB;AACT,EAAA,MAAM,EAAE,OAAA,EAAS,UAAA,EAAY,MAAM,IAAA,CAAK,GAAA,IAAM,GAAI,OAAA;AAClD,EAAA,kBAAA,CAAmB,OAAO,CAAA;AAE1B,EAAA,MAAM,IAAA,GAAO,IAAI,IAAA,CAAK,GAAG,CAAA;AACzB,EAAA,MAAM,MAAA,GAAiC;AAAA,IACrC,MAAM,UAAA,IAAc,OAAA;AAAA,IACpB,SAAA,EAAW,OAAO,GAAG,CAAA;AAAA,IACrB,IAAA,EAAM,MAAA,CAAO,IAAA,CAAK,WAAA,EAAa,CAAA;AAAA,IAC/B,EAAA,EAAI,GAAA,CAAI,IAAA,CAAK,QAAA,KAAa,CAAC,CAAA;AAAA,IAC3B,EAAA,EAAI,GAAA,CAAI,IAAA,CAAK,OAAA,EAAS,CAAA;AAAA,IACtB,EAAA,EAAI,GAAA,CAAI,IAAA,CAAK,QAAA,EAAU,CAAA;AAAA,IACvB,EAAA,EAAI,GAAA,CAAI,IAAA,CAAK,UAAA,EAAY,CAAA;AAAA,IACzB,EAAA,EAAI,GAAA,CAAI,IAAA,CAAK,UAAA,EAAY,CAAA;AAAA,IACzB,GAAA,EAAK,GAAA,CAAI,IAAA,CAAK,eAAA,IAAmB,CAAC;AAAA,GACpC;AAEA,EAAA,IAAI,WAAA,GAAc,KAAA;AAElB,EAAA,MAAM,UAAU,OAAA,CAAQ,OAAA,CAAQ,YAAA,EAAa,EAAG,CAAC,KAAA,KAAU;AACzD,IAAA,IAAK,WAAA,CAAkC,QAAA,CAAS,KAAK,CAAA,EAAG,WAAA,GAAc,IAAA;AACtE,IAAA,OAAO,MAAA,CAAO,KAAK,CAAA,IAAK,KAAA;AAAA,EAC1B,CAAC,CAAA;AAGD,EAAA,IAAI,CAAC,WAAA,EAAa;AAChB,IAAA,MAAM,IAAI,KAAA;AAAA,MACR,8BAA8B,OAAO,CAAA,+CAAA,EAAkD,gBAAA,CAAiB,IAAA,CAAK,IAAI,CAAC,CAAA,CAAA;AAAA,KACpH;AAAA,EACF;AAEA,EAAA,iBAAA,CAAkB,SAAS,OAAO,CAAA;AAElC,EAAA,OAAO,OAAA;AACT;AAMO,SAAS,aAAa,OAAA,EAA0B;AACrD,EAAA,IAAI,OAAA,CAAQ,IAAA,EAAK,KAAM,EAAA,EAAI,OAAO,KAAA;AAElC,EAAA,OAAO,CAAC,GAAG,OAAA,CAAQ,SAAS,YAAA,EAAc,CAAC,CAAA,CAAE,IAAA;AAAA,IAAK,CAAC,KAAA,KAChD,WAAA,CAAkC,QAAA,CAAS,KAAA,CAAM,CAAC,CAAC;AAAA,GACtD;AACF;AAiBO,SAAS,0BAA0B,OAAA,EAAqC;AAC7E,EAAA,kBAAA,CAAmB,OAAO,CAAA;AAE1B,EAAA,IAAI,MAAA,GAAS,GAAA;AACb,EAAA,IAAI,MAAA,GAAS,CAAA;AACb,EAAA,IAAI,UAAA,GAAa,CAAA;AACjB,EAAA,IAAI,UAAA,GAAa,EAAA;AACjB,EAAA,MAAM,aAA0B,EAAC;AAEjC,EAAA,KAAA,MAAW,KAAA,IAAS,OAAA,CAAQ,QAAA,CAAS,YAAA,EAAc,CAAA,EAAG;AACpD,IAAA,MAAM,QAAQ,KAAA,CAAM,KAAA;AACpB,IAAA,MAAA,IAAU,YAAA,CAAa,OAAA,CAAQ,KAAA,CAAM,MAAA,EAAQ,KAAK,CAAC,CAAA;AAEnD,IAAA,MAAM,KAAA,GAAQ,MAAM,CAAC,CAAA;AACrB,IAAA,IAAI,UAAU,MAAA,EAAQ;AACpB,MAAA,MAAA,IAAU,wBAAA;AAAA,IACZ,CAAA,MAAA,IAAW,UAAU,WAAA,EAAa;AAChC,MAAA,MAAA,IAAU,aAAA;AACV,MAAA,UAAA,GAAa,UAAA;AACb,MAAA,UAAA,IAAc,CAAA;AAAA,IAChB,CAAA,MAAO;AACL,MAAA,MAAM,KAAA,GAAQ,iBAAiB,KAAkB,CAAA;AACjD,MAAA,MAAA,IAAU,QAAQ,KAAK,CAAA,EAAA,CAAA;AACvB,MAAA,UAAA,CAAW,KAAK,KAAkB,CAAA;AAClC,MAAA,UAAA,IAAc,CAAA;AAAA,IAChB;AAEA,IAAA,MAAA,GAAS,QAAQ,KAAA,CAAM,MAAA;AAAA,EACzB;AACA,EAAA,MAAA,IAAU,GAAG,YAAA,CAAa,OAAA,CAAQ,KAAA,CAAM,MAAM,CAAC,CAAC,CAAA,CAAA,CAAA;AAEhD,EAAA,MAAM,KAAA,GAAQ,IAAI,MAAA,CAAO,MAAM,CAAA;AAE/B,EAAA,OAAO;AAAA,IACL,KAAA;AAAA,IACA,eAAe,IAAA,EAAkC;AAC/C,MAAA,MAAM,OAAA,GAAU,KAAA,CAAM,IAAA,CAAK,IAAI,CAAA;AAC/B,MAAA,IAAI,CAAC,SAAS,OAAO,MAAA;AAErB,MAAA,IAAI,aAAa,CAAA,EAAG;AAClB,QAAA,MAAM,KAAA,GAAQ,QAAQ,UAAU,CAAA;AAChC,QAAA,OAAO,KAAA,IAAS,IAAA,GAAO,MAAA,GAAY,MAAA,CAAO,KAAK,CAAA;AAAA,MACjD;AACA,MAAA,IAAI,UAAA,CAAW,MAAA,KAAW,CAAA,EAAG,OAAO,MAAA;AAEpC,MAAA,MAAM,QAA2C,EAAC;AAClD,MAAA,UAAA,CAAW,OAAA,CAAQ,CAAC,KAAA,EAAO,QAAA,KAAa;AACtC,QAAA,MAAM,GAAA,GAAM,OAAA,CAAQ,QAAA,GAAW,CAAC,CAAA;AAChC,QAAA,IAAI,GAAA,IAAO,MAAM,KAAA,CAAM,eAAA,CAAgB,KAAK,CAAC,CAAA,GAAI,OAAO,GAAG,CAAA;AAAA,MAC7D,CAAC,CAAA;AAED,MAAA,OAAO,IAAI,IAAA;AAAA,QACT,MAAM,IAAA,IAAQ,IAAA;AAAA,QAAA,CACb,KAAA,CAAM,SAAS,CAAA,IAAK,CAAA;AAAA,QACrB,MAAM,GAAA,IAAO,CAAA;AAAA,QACb,MAAM,IAAA,IAAQ,CAAA;AAAA,QACd,MAAM,MAAA,IAAU,CAAA;AAAA,QAChB,MAAM,MAAA,IAAU,CAAA;AAAA,QAChB,MAAM,WAAA,IAAe;AAAA,QACrB,OAAA,EAAQ;AAAA,IACZ;AAAA,GACF;AACF;;;AChMA,IAAM,uBAAA,GAA0B,IAAI,EAAA,GAAK,GAAA;AAkDzC,SAAS,oBAAA,CACP,UAAA,EACA,eAAA,EACA,IAAA,EACA,OAAA,EACM;AACN,EAAA,MAAM,GAAA,GAAM,KAAK,GAAA,EAAI;AACrB,EAAA,MAAM,SAAA,GAAYA,mBAAA,CACf,WAAA,CAAY,UAAA,EAAY,EAAE,aAAA,EAAe,IAAA,EAAM,CAAA,CAC/C,OAAA,CAAQ,CAAC,KAAA,KAAU;AAClB,IAAA,IAAI,CAAC,MAAM,WAAA,EAAY,IAAK,MAAM,IAAA,KAAS,eAAA,SAAwB,EAAC;AACpE,IAAA,MAAM,SAAA,GAAY,OAAA,CAAQ,cAAA,CAAe,KAAA,CAAM,IAAI,CAAA;AACnD,IAAA,IAAI,SAAA,IAAa,IAAA,EAAM,OAAO,EAAC;AAE/B,IAAA,IAAI,GAAA,GAAM,SAAA,GAAY,uBAAA,EAAyB,OAAO,EAAC;AAEvD,IAAA,OAAO,CAAC,EAAE,IAAA,EAAM,KAAA,CAAM,IAAA,EAAM,WAAW,CAAA;AAAA,EACzC,CAAC,CAAA,CACA,IAAA,CAAK,CAAC,MAAM,KAAA,KAAU,KAAA,CAAM,SAAA,GAAY,IAAA,CAAK,aAAa,KAAA,CAAM,IAAA,CAAK,aAAA,CAAc,IAAA,CAAK,IAAI,CAAC,CAAA;AAEhG,EAAA,KAAA,MAAW,KAAA,IAAS,UAAU,KAAA,CAAM,IAAA,CAAK,IAAI,IAAA,GAAO,CAAA,EAAG,CAAC,CAAC,CAAA,EAAG;AAC1D,IAAAA,mBAAA,CAAG,MAAA,CAAOC,qBAAA,CAAK,IAAA,CAAK,UAAA,EAAY,KAAA,CAAM,IAAI,CAAA,EAAG,EAAE,SAAA,EAAW,IAAA,EAAM,CAAA;AAAA,EAClE;AACF;AAQO,SAAS,iBAAiB,OAAA,EAAgD;AAC/E,EAAA,MAAM,EAAE,OAAA,EAAS,WAAA,EAAa,mBAAA,EAAqB,UAAA,EAAY,SAAQ,GAAI,OAAA;AAC3E,EAAA,IAAI,OAAA,KAAY,CAAC,MAAA,CAAO,SAAA,CAAU,QAAQ,IAAI,CAAA,IAAK,OAAA,CAAQ,IAAA,GAAO,CAAA,CAAA,EAAI;AACpE,IAAA,MAAM,IAAI,MAAM,2EAA2E,CAAA;AAAA,EAC7F;AAEA,EAAA,MAAM,8BAA8B,mBAAA,IAAuB,6BAAA;AAC3D,EAAA,IAAI,OAAA,IAAW,CAAC,YAAA,CAAa,2BAA2B,CAAA,EAAG;AAEzD,IAAA,MAAM,IAAI,MAAM,yEAAyE,CAAA;AAAA,EAC3F;AAEA,EAAA,MAAM,cAAA,GAAiB,0BAA0B,2BAA2B,CAAA;AAC5E,EAAA,IAAI,WAAW,CAAC,cAAA,CAAe,KAAA,CAAM,IAAA,CAAK,WAAW,CAAA,EAAG;AAGtD,IAAA,MAAM,IAAI,KAAA;AAAA,MACR,CAAA,2BAAA,EAA8B,WAAW,CAAA,sCAAA,EAAyC,2BAA2B,CAAA,EAAA;AAAA,KAC/G;AAAA,EACF;AAEA,EAAA,IAAI,SAAA;AACJ,EAAA,IAAI,UAAA;AAEJ,EAAA,OAAO;AAAA,IACL,IAAA,EAAM,aAAA;AAAA,IACN,OAAA,EAAS,MAAA;AAAA,IACT,eAAe,MAAA,EAAQ;AACrB,MAAA,MAAM,YAAA,GAAeA,qBAAA,CAAK,OAAA,CAAQ,MAAA,CAAO,IAAI,CAAA;AAC7C,MAAA,MAAM,YAAA,GAAeA,qBAAA,CAAK,OAAA,CAAQ,OAAO,CAAA;AACzC,MAAA,IAAI,iBAAiB,YAAA,EAAc;AACjC,QAAA,MAAM,IAAI,KAAA;AAAA,UACR,CAAA,yBAAA,EAA4B,YAAY,CAAA,sBAAA,EAAyB,YAAY,CAAA,EAAA;AAAA,SAC/E;AAAA,MACF;AAEA,MAAA,MAAM,YAAA,GAAe,UAAA,CAAW,UAAA,CAAW,IAAI,IAAI,IAAA,GAAO,UAAA;AAC1D,MAAA,IAAI,MAAA,CAAO,SAAS,YAAA,EAAc;AAChC,QAAA,MAAM,IAAI,KAAA;AAAA,UACR,CAAA,yBAAA,EAA4B,MAAA,CAAO,IAAI,CAAA,eAAA,EAAkB,YAAY,CAAA,yBAAA;AAAA,SACvE;AAAA,MACF;AAEA,MAAA,SAAA,GAAYA,qBAAA,CAAK,OAAA,CAAQ,YAAA,EAAc,MAAA,CAAO,MAAM,MAAM,CAAA;AAC1D,MAAA,IAAIA,qBAAA,CAAK,QAAA,CAAS,SAAS,CAAA,KAAM,WAAA,EAAa;AAC5C,QAAA,MAAM,IAAI,KAAA;AAAA,UACR,CAAA,4BAAA,EAA+B,MAAA,CAAO,KAAA,CAAM,MAAM,uCAAuC,WAAW,CAAA,EAAA;AAAA,SACtG;AAAA,MACF;AACA,MAAA,UAAA,GAAaA,qBAAA,CAAK,QAAQ,SAAS,CAAA;AAAA,IACrC,CAAA;AAAA,IACA,WAAA,EAAa;AAAA,MACX,UAAA,EAAY,IAAA;AAAA,MACZ,KAAA,EAAO,MAAA;AAAA,MACP,OAAA,GAAU;AACR,QAAA,IAAI,CAAC,SAAA,IAAa,CAAC,UAAA,EAAY;AAC7B,UAAA,MAAM,IAAI,MAAM,uEAAuE,CAAA;AAAA,QACzF;AAEA,QAAA,MAAM,WAAA,GAAcA,qBAAA,CAAK,IAAA,CAAK,SAAA,EAAW,YAAY,CAAA;AACrD,QAAA,IAAI,CAACD,mBAAA,CAAG,UAAA,CAAW,WAAW,CAAA,EAAG;AAC/B,UAAA,MAAM,IAAI,KAAA,CAAM,CAAA,8CAAA,EAAiD,WAAW,CAAA,EAAA,CAAI,CAAA;AAAA,QAClF;AAEA,QAAAA,mBAAA,CAAG,SAAA,CAAU,UAAA,EAAY,EAAE,SAAA,EAAW,MAAM,CAAA;AAC5C,QAAAA,mBAAA,CAAG,cAAcC,qBAAA,CAAK,IAAA,CAAK,UAAA,EAAY,UAAU,GAAG,WAAW,CAAA;AAC/D,QAAA,MAAM,UAAA,GAAaD,mBAAA,CAAG,YAAA,CAAa,WAAA,EAAa,MAAM,CAAA;AAGtD,QAAA,MAAM,QAAA,GAAW,UAAA,CAAW,UAAA,CAAW,IAAI,CAAA,GACvC,UAAA,CAAW,OAAA,CAAQ,kCAAA,EAAoC,CAAA,EAAA,EAAK,UAAU,CAAA,CAAE,CAAA,GACxE,UAAA;AACJ,QAAAA,mBAAA,CAAG,cAAcC,qBAAA,CAAK,IAAA,CAAK,UAAA,EAAY,YAAY,GAAG,QAAQ,CAAA;AAC9D,QAAA,IAAI,OAAA,EAAS;AACX,UAAA,oBAAA,CAAqB,UAAA,EAAY,WAAA,EAAa,OAAA,CAAQ,IAAA,EAAM,cAAc,CAAA;AAAA,QAC5E;AAAA,MACF;AAAA;AACF,GACF;AACF;AAMO,SAAS,aAAA,CAAc,cAAA,GAAyB,OAAA,CAAQ,GAAA,EAAI,EAAW;AAC5E,EAAA,IAAI;AACF,IAAA,OAAOC,0BAAA,CAAa,KAAA,EAAO,CAAC,WAAA,EAAa,MAAM,CAAA,EAAG;AAAA,MAChD,GAAA,EAAK,cAAA;AAAA,MACL,QAAA,EAAU,MAAA;AAAA,MACV,KAAA,EAAO,CAAC,QAAA,EAAU,MAAA,EAAQ,QAAQ;AAAA,KACnC,CAAA,CACE,IAAA,EAAK,CACL,KAAA,CAAM,GAAG,EAAE,CAAA;AAAA,EAChB,CAAA,CAAA,MAAQ;AACN,IAAA,OAAO,OAAA;AAAA,EACT;AACF;AAaO,SAAS,sBAAA,CACd,4BAAA,EACA,GAAA,GAAc,IAAA,CAAK,KAAI,EACf;AACR,EAAA,IAAI,4BAAA,IAAgC,IAAA,IAAQ,OAAO,4BAAA,KAAiC,QAAA,EAAU;AAC5F,IAAA,MAAM;AAAA,MACJ,cAAA,EAAAC,eAAAA;AAAA,MACA,GAAA,EAAK,UAAA,GAAa,IAAA,CAAK,GAAA,EAAI;AAAA,MAC3B;AAAA,KACF,GAAI,4BAAA;AAEJ,IAAA,OAAO,iBAAA,CAAkB;AAAA,MACvB,SAAS,WAAA,IAAe,6BAAA;AAAA,MACxB,UAAA,EAAY,cAAcA,eAAc,CAAA;AAAA,MACxC,GAAA,EAAK;AAAA,KACN,CAAA;AAAA,EACH;AAEA,EAAA,MAAM,cAAA,GACJ,OAAO,4BAAA,KAAiC,QAAA,GAAW,4BAAA,GAA+B,MAAA;AACpF,EAAA,MAAM,SAAA,GACJ,OAAO,4BAAA,KAAiC,QAAA,GAAW,4BAAA,GAA+B,GAAA;AAEpF,EAAA,OAAO,iBAAA,CAAkB;AAAA,IACvB,OAAA,EAAS,6BAAA;AAAA,IACT,UAAA,EAAY,cAAc,cAAc,CAAA;AAAA,IACxC,GAAA,EAAK;AAAA,GACN,CAAA;AACH","file":"hash-deploy.cjs","sourcesContent":["/**\n * Hash 子目录名模式串 → 实际目录名,以及清理历史目录用的匹配器。\n *\n * 与 `@hzab/webpack-config` 的 `hashDirName` 共用同一套 token 语义(两个包各自实现,不互相依赖):\n * token 展开为值,其余字符(含 `-`)原样保留。\n * - `hash` 12 位 commit(非 git 仓库回退 `local`)\n * - `timestamp` 毫秒时间戳(`Date.now()`)\n * - `YYYY` / `MM` / `DD` / `HH` / `mm` / `ss` / `SSS` 本地时区格式化时间\n *\n * 例(now = 2026-09-17 15:30:52.123,commit = a1b2c3d4e5f6):\n * - `hash-timestamp` -> `a1b2c3d4e5f6-1789630252123`\n * - `hash-timestamp-YYYYMMDDHHmmssSSS` -> `a1b2c3d4e5f6-1789630252123-20260917153052123`\n * - `hash-YYYYMMDDHHmmssSSS` -> `a1b2c3d4e5f6-20260917153052123`\n *\n * 约束:模式串必须至少含一个时间 token(`timestamp` 或日期 token)。只含 `hash` 的目录名会让\n * 同一 commit 的多次构建互相覆盖,因此被拒绝。\n *\n * 只做纯字符串计算(无 I/O),命名与清理排序共用同一套 token 定义。\n */\n\n/** token 交替顺序:长 token 在前,避免被短 token 抢先切分。每次取新实例,避免共享 `lastIndex`。 */\nconst tokenPattern = (): RegExp => /YYYY|SSS|MM|DD|HH|mm|ss|timestamp|hash/g;\n\nconst DATE_TOKEN_WIDTH = { YYYY: 4, MM: 2, DD: 2, HH: 2, mm: 2, ss: 2, SSS: 3 } as const;\n\ntype DateToken = keyof typeof DATE_TOKEN_WIDTH;\n\n/** 日期 token → `Date` 字段 */\nconst DATE_TOKEN_SLOT = {\n YYYY: \"year\",\n MM: \"month\",\n DD: \"day\",\n HH: \"hour\",\n mm: \"minute\",\n ss: \"second\",\n SSS: \"millisecond\",\n} as const satisfies Record<DateToken, string>;\n\ntype DateSlot = (typeof DATE_TOKEN_SLOT)[DateToken];\n\nconst TIME_TOKENS = [\"timestamp\", \"YYYY\", \"MM\", \"DD\", \"HH\", \"mm\", \"ss\", \"SSS\"] as const;\n\nconst AVAILABLE_TOKENS = [\"hash\", ...TIME_TOKENS];\n\n/** 默认模式串:与历史默认目录名 `{commit}-{Date.now()}` 等价 */\nexport const DEFAULT_HASH_DIR_NAME_PATTERN = \"hash-timestamp\";\n\n/** 目录名不允许出现的字符(路径分隔符、Windows 保留字符) */\nconst ILLEGAL_DIR_CHAR_PATTERN = /[\\\\/:*?\"<>|]/;\n\nconst pad = (value: number, length = 2): string => String(value).padStart(length, \"0\");\n\nfunction escapeRegExp(value: string): string {\n return value.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n}\n\nfunction assertPatternValue(pattern: string): void {\n if (pattern.trim() === \"\") {\n throw new Error(\"[vite-config] hashDirName must be a non-empty string.\");\n }\n}\n\n/** 展开后的目录名必须留在 deployRoot 之下(模式串不能借字面文本跳出产物根目录) */\nfunction assertSafeDirName(dirName: string, pattern: string): void {\n const unsafe =\n ILLEGAL_DIR_CHAR_PATTERN.test(dirName) ||\n dirName.includes(\"..\") ||\n dirName.startsWith(\".\") ||\n dirName.endsWith(\".\") ||\n dirName !== dirName.trim();\n\n if (unsafe) {\n throw new Error(\n `[vite-config] hashDirName \"${pattern}\" resolves to unsafe directory name \"${dirName}\".`,\n );\n }\n}\n\n/**\n * 把目录名模式串展开为目录名。\n * `timestamp` 与日期 token 共用同一个 `now`,保证描述同一时刻。\n */\nexport function expandHashDirName(options: {\n pattern: string;\n commitHash: string;\n now?: number;\n}): string {\n const { pattern, commitHash, now = Date.now() } = options;\n assertPatternValue(pattern);\n\n const date = new Date(now);\n const values: Record<string, string> = {\n hash: commitHash || \"local\",\n timestamp: String(now),\n YYYY: String(date.getFullYear()),\n MM: pad(date.getMonth() + 1),\n DD: pad(date.getDate()),\n HH: pad(date.getHours()),\n mm: pad(date.getMinutes()),\n ss: pad(date.getSeconds()),\n SSS: pad(date.getMilliseconds(), 3),\n };\n\n let timeMatched = false;\n // 单次 replace:替换结果不会被二次扫描,避免 commit / 时间戳里的字符被再次展开\n const dirName = pattern.replace(tokenPattern(), (token) => {\n if ((TIME_TOKENS as readonly string[]).includes(token)) timeMatched = true;\n return values[token] ?? token;\n });\n\n // 只含 commit(如 \"hash\")的目录名会让同一 commit 的多次构建互相覆盖,直接拒绝\n if (!timeMatched) {\n throw new Error(\n `[vite-config] hashDirName \"${pattern}\" must contain a time token; available tokens: ${AVAILABLE_TOKENS.join(\", \")}.`,\n );\n }\n\n assertSafeDirName(dirName, pattern);\n\n return dirName;\n}\n\n/**\n * 模式串是否含可排序的时间 token(`timestamp` 或日期 token)。\n * 清理器按目录名里的时间判断新旧,没有时间 token 就无从排序,必须让调用方提前报错。\n */\nexport function hasTimeToken(pattern: string): boolean {\n if (pattern.trim() === \"\") return false;\n\n return [...pattern.matchAll(tokenPattern())].some((match) =>\n (TIME_TOKENS as readonly string[]).includes(match[0]),\n );\n}\n\n/**\n * 由模式串编译出的历史目录匹配器:只认本模式串生成的目录名,并给出可排序时间戳。\n */\nexport interface HashDirNameMatcher {\n regex: RegExp;\n /** 目录名 → 时间戳(毫秒);目录名不匹配本模式串时返回 `undefined` */\n parseTimestamp(name: string): number | undefined;\n}\n\n/**\n * 编译模式串:字面文本转义为 `^...$`,token 转为捕获组。\n * - `timestamp` 存在时直接取其毫秒值排序;\n * - 否则按日期 token 回推本地时间(缺省字段为 1970-01-01 00:00:00.000)。\n * 模式串必须含时间 token(由 `expandHashDirName` 保证),故时间戳总是可解析。\n */\nexport function compileHashDirNameMatcher(pattern: string): HashDirNameMatcher {\n assertPatternValue(pattern);\n\n let source = \"^\";\n let cursor = 0;\n let groupIndex = 1;\n let epochGroup = -1;\n const dateTokens: DateToken[] = [];\n\n for (const match of pattern.matchAll(tokenPattern())) {\n const index = match.index;\n source += escapeRegExp(pattern.slice(cursor, index));\n\n const token = match[0];\n if (token === \"hash\") {\n source += \"(?:[0-9a-f]{12}|local)\";\n } else if (token === \"timestamp\") {\n source += \"(\\\\d{1,15})\";\n epochGroup = groupIndex;\n groupIndex += 1;\n } else {\n const width = DATE_TOKEN_WIDTH[token as DateToken];\n source += `(\\\\d{${width}})`;\n dateTokens.push(token as DateToken);\n groupIndex += 1;\n }\n\n cursor = index + token.length;\n }\n source += `${escapeRegExp(pattern.slice(cursor))}$`;\n\n const regex = new RegExp(source);\n\n return {\n regex,\n parseTimestamp(name: string): number | undefined {\n const matched = regex.exec(name);\n if (!matched) return undefined;\n\n if (epochGroup > 0) {\n const epoch = matched[epochGroup];\n return epoch == null ? undefined : Number(epoch);\n }\n if (dateTokens.length === 0) return undefined;\n\n const slots: Partial<Record<DateSlot, number>> = {};\n dateTokens.forEach((token, position) => {\n const raw = matched[position + 1];\n if (raw != null) slots[DATE_TOKEN_SLOT[token]] = Number(raw);\n });\n\n return new Date(\n slots.year ?? 1970,\n (slots.month ?? 1) - 1,\n slots.day ?? 1,\n slots.hour ?? 0,\n slots.minute ?? 0,\n slots.second ?? 0,\n slots.millisecond ?? 0,\n ).getTime();\n },\n };\n}\n","import { execFileSync } from \"node:child_process\";\nimport fs from \"node:fs\";\nimport path from \"node:path\";\n\nimport type { PluginOption } from \"vite\";\n\nimport {\n compileHashDirNameMatcher,\n DEFAULT_HASH_DIR_NAME_PATTERN,\n expandHashDirName,\n hasTimeToken,\n} from \"./hash-dir-name.ts\";\nimport type { HashDirNameMatcher } from \"./hash-dir-name.ts\";\n\n/**\n * 近期构建保护窗口:目录名时间戳距今不足该时长的目录不参与清理。\n * 同机并发构建时,先完成的构建不应删掉另一个进程正在写的目录。\n * 判据只用目录名里的时间戳,不用文件系统 mtime——CI 恢复缓存 / 解压产物会改写 mtime。\n */\nconst RECENT_BUILD_PROTECT_MS = 5 * 60 * 1000;\n\n/** @public */\nexport interface HashBuildDirNameOptions {\n repositoryRoot?: string;\n now?: number;\n /**\n * Hash 子目录名模式串;不传时用默认 `hash-timestamp`(等价历史默认 `{commit}-{毫秒时间戳}`)。\n * token 大小写敏感:`hash` = 12 位 commit(非 git 回退 `local`)、`timestamp` = 毫秒时间戳、\n * `YYYY`/`MM`/`DD`/`HH`/`mm`/`ss`/`SSS` = 本地时区格式化时间;其余字符(含 `-`)为字面文本。\n * 模式串必须至少含一个时间 token(`timestamp` 或日期 token),只含 `hash` 会被拒绝。\n * 例:`hash-timestamp`、`hash-timestamp-YYYYMMDDHHmmssSSS`、`hash-YYYYMMDDHHmmssSSS`。\n */\n hashDirName?: string;\n}\n\n/** @public */\nexport interface HashCleanupOptions {\n /**\n * 每次成功构建后保留的 Hash 版本总数(含本次构建)。\n * 必须为大于等于 1 的整数。\n */\n keep: number;\n}\n\n/** @public */\nexport interface HashDeployPluginOptions {\n rootDir: string;\n /** hash 子目录名,如 `{commit}-{timestamp}` 或 `{commit}-YYYY-MM-DD-HH-mm-ss` */\n buildDirStr: string;\n /**\n * 生成 `buildDirStr` 的模式串(来自工厂的 `hashDirName`);供 `cleanup` 精确识别历史目录,\n * 且必须与实际 `buildDirStr` 匹配。未传时用默认模式串 `hash-timestamp`。\n */\n buildDirNamePattern?: string;\n /**\n * 与最终 Vite `base` 一致的资源前缀。\n * - 默认相对:`./{buildDirStr}/`\n * - 自定义 CDN:`https://cdn.example.com/app/{buildDirStr}/`\n */\n publicPath: string;\n /** 旧 Hash 目录清理策略;未传时永久保留,由部署系统负责清理。 */\n cleanup?: HashCleanupOptions;\n}\n\n/**\n * 只保留最近 `keep` 个 Hash 版本(含本次构建)。\n * 只识别 `matcher`(模式串)生成的目录名;不匹配的目录(如 `uploads-*`)一律不动。\n * 目录名时间戳落在保护窗口内的目录视为并发构建产物,跳过清理。\n */\nfunction cleanupOldHashBuilds(\n deployRoot: string,\n currentBuildDir: string,\n keep: number,\n matcher: HashDirNameMatcher,\n): void {\n const now = Date.now();\n const oldBuilds = fs\n .readdirSync(deployRoot, { withFileTypes: true })\n .flatMap((entry) => {\n if (!entry.isDirectory() || entry.name === currentBuildDir) return [];\n const timestamp = matcher.parseTimestamp(entry.name);\n if (timestamp == null) return [];\n // 保护窗口内的目录(含因时钟偏差落在未来的目录)保持不动\n if (now - timestamp < RECENT_BUILD_PROTECT_MS) return [];\n\n return [{ name: entry.name, timestamp }];\n })\n .sort((left, right) => right.timestamp - left.timestamp || right.name.localeCompare(left.name));\n\n for (const build of oldBuilds.slice(Math.max(keep - 1, 0))) {\n fs.rmSync(path.join(deployRoot, build.name), { recursive: true });\n }\n}\n\n/**\n * 构建结束后写 `dist/hash.txt`,并把 hash 子目录中的 `index.html` 复制到 `dist/index.html`。\n * 相对 `base` 时为根入口补上 hash 前缀;绝对 `base` 时产物已含完整 URL,原样复制即可。\n * 配置 `cleanup.keep` 时,成功写入根入口后只保留指定数量的最新 Hash 版本。\n * @public\n */\nexport function hashDeployPlugin(options: HashDeployPluginOptions): PluginOption {\n const { rootDir, buildDirStr, buildDirNamePattern, publicPath, cleanup } = options;\n if (cleanup && (!Number.isInteger(cleanup.keep) || cleanup.keep < 1)) {\n throw new Error(\"[hash-deploy] cleanup.keep must be an integer greater than or equal to 1.\");\n }\n\n const resolvedBuildDirNamePattern = buildDirNamePattern ?? DEFAULT_HASH_DIR_NAME_PATTERN;\n if (cleanup && !hasTimeToken(resolvedBuildDirNamePattern)) {\n // 没有可排序的时间 token 时无法判断历史目录新旧,直接报错,否则会静默堆积\n throw new Error(\"[hash-deploy] cleanup requires a buildDirNamePattern with a time token.\");\n }\n\n const cleanupMatcher = compileHashDirNameMatcher(resolvedBuildDirNamePattern);\n if (cleanup && !cleanupMatcher.regex.test(buildDirStr)) {\n // 配了清理但模式串与实际目录名不符时直接报错,否则历史目录会静默堆积,\n // 与「保留最近 N 个」的预期相反。\n throw new Error(\n `[hash-deploy] buildDirStr \"${buildDirStr}\" does not match buildDirNamePattern \"${resolvedBuildDirNamePattern}\".`,\n );\n }\n\n let outputDir: string | undefined;\n let deployRoot: string | undefined;\n\n return {\n name: \"hash-deploy\",\n enforce: \"post\",\n configResolved(config) {\n const resolvedRoot = path.resolve(config.root);\n const expectedRoot = path.resolve(rootDir);\n if (resolvedRoot !== expectedRoot) {\n throw new Error(\n `[hash-deploy] Vite root \"${resolvedRoot}\" must match rootDir \"${expectedRoot}\".`,\n );\n }\n // 相对 hash:工厂 publicPath 为 `./{hash}/`,但 Vite base 必须保持 `./`。\n const expectedBase = publicPath.startsWith(\"./\") ? \"./\" : publicPath;\n if (config.base !== expectedBase) {\n throw new Error(\n `[hash-deploy] Vite base \"${config.base}\" must remain \"${expectedBase}\" during hash deployment.`,\n );\n }\n\n outputDir = path.resolve(resolvedRoot, config.build.outDir);\n if (path.basename(outputDir) !== buildDirStr) {\n throw new Error(\n `[hash-deploy] build.outDir \"${config.build.outDir}\" must end with the hash directory \"${buildDirStr}\".`,\n );\n }\n deployRoot = path.dirname(outputDir);\n },\n writeBundle: {\n sequential: true,\n order: \"post\",\n handler() {\n if (!outputDir || !deployRoot) {\n throw new Error(\"[hash-deploy] Vite configuration was not resolved before writeBundle.\");\n }\n\n const sourceIndex = path.join(outputDir, \"index.html\");\n if (!fs.existsSync(sourceIndex)) {\n throw new Error(`[hash-deploy] Generated entry was not found: \"${sourceIndex}\".`);\n }\n\n fs.mkdirSync(deployRoot, { recursive: true });\n fs.writeFileSync(path.join(deployRoot, \"hash.txt\"), buildDirStr);\n const sourceHtml = fs.readFileSync(sourceIndex, \"utf8\");\n // 相对 base(`./{hash}/`)时子目录 HTML 中的资源均相对于自身,根入口需统一补 hash;\n // 绝对 base 时 Vite 已写出完整 URL,直接复制。\n const rootHtml = publicPath.startsWith(\"./\")\n ? sourceHtml.replace(/(\\b(?:href|src)\\s*=\\s*[\"'])\\.\\//g, `$1${publicPath}`)\n : sourceHtml;\n fs.writeFileSync(path.join(deployRoot, \"index.html\"), rootHtml);\n if (cleanup) {\n cleanupOldHashBuilds(deployRoot, buildDirStr, cleanup.keep, cleanupMatcher);\n }\n },\n },\n };\n}\n\n/**\n * 从指定仓库读取短 commit hash;非 git 环境回退 `local`。\n * @public\n */\nexport function getCommitHash(repositoryRoot: string = process.cwd()): string {\n try {\n return execFileSync(\"git\", [\"rev-parse\", \"HEAD\"], {\n cwd: repositoryRoot,\n encoding: \"utf8\",\n stdio: [\"ignore\", \"pipe\", \"ignore\"],\n })\n .trim()\n .slice(0, 12);\n } catch {\n return \"local\";\n }\n}\n\n/**\n * 生成 hash 构建目录名;保留仅传时间戳的旧调用方式。\n * 目录名一律由模式串展开,`hashDirName` 不传时用默认 `hash-timestamp`\n * (等价历史默认 `{commit}-{毫秒时间戳}`)。\n * @public\n */\nexport function createHashBuildDirName(now?: number): string;\n/** @public */\nexport function createHashBuildDirName(repositoryRoot: string, now?: number): string;\n/** @public */\nexport function createHashBuildDirName(options: HashBuildDirNameOptions): string;\nexport function createHashBuildDirName(\n repositoryRootOrNowOrOptions?: string | number | HashBuildDirNameOptions,\n now: number = Date.now(),\n): string {\n if (repositoryRootOrNowOrOptions != null && typeof repositoryRootOrNowOrOptions === \"object\") {\n const {\n repositoryRoot,\n now: optionsNow = Date.now(),\n hashDirName,\n } = repositoryRootOrNowOrOptions;\n\n return expandHashDirName({\n pattern: hashDirName ?? DEFAULT_HASH_DIR_NAME_PATTERN,\n commitHash: getCommitHash(repositoryRoot),\n now: optionsNow,\n });\n }\n\n const repositoryRoot =\n typeof repositoryRootOrNowOrOptions === \"string\" ? repositoryRootOrNowOrOptions : undefined;\n const timestamp =\n typeof repositoryRootOrNowOrOptions === \"number\" ? repositoryRootOrNowOrOptions : now;\n\n return expandHashDirName({\n pattern: DEFAULT_HASH_DIR_NAME_PATTERN,\n commitHash: getCommitHash(repositoryRoot),\n now: timestamp,\n });\n}\n"]}
@@ -1,13 +1,17 @@
1
1
  import { PluginOption } from 'vite';
2
2
 
3
- /** @public */
4
- type HashTimestampFormat = "epoch" | "datetime";
5
3
  /** @public */
6
4
  interface HashBuildDirNameOptions {
7
5
  repositoryRoot?: string;
8
6
  now?: number;
9
- /** @defaultValue "epoch" */
10
- timestampFormat?: HashTimestampFormat;
7
+ /**
8
+ * Hash 子目录名模式串;不传时用默认 `hash-timestamp`(等价历史默认 `{commit}-{毫秒时间戳}`)。
9
+ * token 大小写敏感:`hash` = 12 位 commit(非 git 回退 `local`)、`timestamp` = 毫秒时间戳、
10
+ * `YYYY`/`MM`/`DD`/`HH`/`mm`/`ss`/`SSS` = 本地时区格式化时间;其余字符(含 `-`)为字面文本。
11
+ * 模式串必须至少含一个时间 token(`timestamp` 或日期 token),只含 `hash` 会被拒绝。
12
+ * 例:`hash-timestamp`、`hash-timestamp-YYYYMMDDHHmmssSSS`、`hash-YYYYMMDDHHmmssSSS`。
13
+ */
14
+ hashDirName?: string;
11
15
  }
12
16
  /** @public */
13
17
  interface HashCleanupOptions {
@@ -22,6 +26,11 @@ interface HashDeployPluginOptions {
22
26
  rootDir: string;
23
27
  /** hash 子目录名,如 `{commit}-{timestamp}` 或 `{commit}-YYYY-MM-DD-HH-mm-ss` */
24
28
  buildDirStr: string;
29
+ /**
30
+ * 生成 `buildDirStr` 的模式串(来自工厂的 `hashDirName`);供 `cleanup` 精确识别历史目录,
31
+ * 且必须与实际 `buildDirStr` 匹配。未传时用默认模式串 `hash-timestamp`。
32
+ */
33
+ buildDirNamePattern?: string;
25
34
  /**
26
35
  * 与最终 Vite `base` 一致的资源前缀。
27
36
  * - 默认相对:`./{buildDirStr}/`
@@ -45,6 +54,8 @@ declare function hashDeployPlugin(options: HashDeployPluginOptions): PluginOptio
45
54
  declare function getCommitHash(repositoryRoot?: string): string;
46
55
  /**
47
56
  * 生成 hash 构建目录名;保留仅传时间戳的旧调用方式。
57
+ * 目录名一律由模式串展开,`hashDirName` 不传时用默认 `hash-timestamp`
58
+ * (等价历史默认 `{commit}-{毫秒时间戳}`)。
48
59
  * @public
49
60
  */
50
61
  declare function createHashBuildDirName(now?: number): string;
@@ -53,4 +64,4 @@ declare function createHashBuildDirName(repositoryRoot: string, now?: number): s
53
64
  /** @public */
54
65
  declare function createHashBuildDirName(options: HashBuildDirNameOptions): string;
55
66
 
56
- export { type HashBuildDirNameOptions, type HashCleanupOptions, type HashDeployPluginOptions, type HashTimestampFormat, createHashBuildDirName, getCommitHash, hashDeployPlugin };
67
+ export { type HashBuildDirNameOptions, type HashCleanupOptions, type HashDeployPluginOptions, createHashBuildDirName, getCommitHash, hashDeployPlugin };
@@ -1,13 +1,17 @@
1
1
  import { PluginOption } from 'vite';
2
2
 
3
- /** @public */
4
- type HashTimestampFormat = "epoch" | "datetime";
5
3
  /** @public */
6
4
  interface HashBuildDirNameOptions {
7
5
  repositoryRoot?: string;
8
6
  now?: number;
9
- /** @defaultValue "epoch" */
10
- timestampFormat?: HashTimestampFormat;
7
+ /**
8
+ * Hash 子目录名模式串;不传时用默认 `hash-timestamp`(等价历史默认 `{commit}-{毫秒时间戳}`)。
9
+ * token 大小写敏感:`hash` = 12 位 commit(非 git 回退 `local`)、`timestamp` = 毫秒时间戳、
10
+ * `YYYY`/`MM`/`DD`/`HH`/`mm`/`ss`/`SSS` = 本地时区格式化时间;其余字符(含 `-`)为字面文本。
11
+ * 模式串必须至少含一个时间 token(`timestamp` 或日期 token),只含 `hash` 会被拒绝。
12
+ * 例:`hash-timestamp`、`hash-timestamp-YYYYMMDDHHmmssSSS`、`hash-YYYYMMDDHHmmssSSS`。
13
+ */
14
+ hashDirName?: string;
11
15
  }
12
16
  /** @public */
13
17
  interface HashCleanupOptions {
@@ -22,6 +26,11 @@ interface HashDeployPluginOptions {
22
26
  rootDir: string;
23
27
  /** hash 子目录名,如 `{commit}-{timestamp}` 或 `{commit}-YYYY-MM-DD-HH-mm-ss` */
24
28
  buildDirStr: string;
29
+ /**
30
+ * 生成 `buildDirStr` 的模式串(来自工厂的 `hashDirName`);供 `cleanup` 精确识别历史目录,
31
+ * 且必须与实际 `buildDirStr` 匹配。未传时用默认模式串 `hash-timestamp`。
32
+ */
33
+ buildDirNamePattern?: string;
25
34
  /**
26
35
  * 与最终 Vite `base` 一致的资源前缀。
27
36
  * - 默认相对:`./{buildDirStr}/`
@@ -45,6 +54,8 @@ declare function hashDeployPlugin(options: HashDeployPluginOptions): PluginOptio
45
54
  declare function getCommitHash(repositoryRoot?: string): string;
46
55
  /**
47
56
  * 生成 hash 构建目录名;保留仅传时间戳的旧调用方式。
57
+ * 目录名一律由模式串展开,`hashDirName` 不传时用默认 `hash-timestamp`
58
+ * (等价历史默认 `{commit}-{毫秒时间戳}`)。
48
59
  * @public
49
60
  */
50
61
  declare function createHashBuildDirName(now?: number): string;
@@ -53,4 +64,4 @@ declare function createHashBuildDirName(repositoryRoot: string, now?: number): s
53
64
  /** @public */
54
65
  declare function createHashBuildDirName(options: HashBuildDirNameOptions): string;
55
66
 
56
- export { type HashBuildDirNameOptions, type HashCleanupOptions, type HashDeployPluginOptions, type HashTimestampFormat, createHashBuildDirName, getCommitHash, hashDeployPlugin };
67
+ export { type HashBuildDirNameOptions, type HashCleanupOptions, type HashDeployPluginOptions, createHashBuildDirName, getCommitHash, hashDeployPlugin };
@@ -3,42 +3,159 @@ import fs from 'fs';
3
3
  import path from 'path';
4
4
 
5
5
  // src/plugins/hash-deploy.ts
6
- var HASH_BUILD_DIR_PATTERN = /^(?:[0-9a-f]{12}|local)-(\d{4}-\d{2}-\d{2}-\d{2}-\d{2}-\d{2}|\d+)$/;
7
- function parseHashTimestamp(value) {
8
- if (/^\d+$/.test(value)) return Number(value);
9
- const [year = 0, month = 1, day = 1, hour = 0, minute = 0, second = 0] = value.split("-").map(Number);
10
- return new Date(year, month - 1, day, hour, minute, second).getTime();
6
+
7
+ // src/plugins/hash-dir-name.ts
8
+ var tokenPattern = () => /YYYY|SSS|MM|DD|HH|mm|ss|timestamp|hash/g;
9
+ var DATE_TOKEN_WIDTH = { YYYY: 4, MM: 2, DD: 2, HH: 2, mm: 2, ss: 2, SSS: 3 };
10
+ var DATE_TOKEN_SLOT = {
11
+ YYYY: "year",
12
+ MM: "month",
13
+ DD: "day",
14
+ HH: "hour",
15
+ mm: "minute",
16
+ ss: "second",
17
+ SSS: "millisecond"
18
+ };
19
+ var TIME_TOKENS = ["timestamp", "YYYY", "MM", "DD", "HH", "mm", "ss", "SSS"];
20
+ var AVAILABLE_TOKENS = ["hash", ...TIME_TOKENS];
21
+ var DEFAULT_HASH_DIR_NAME_PATTERN = "hash-timestamp";
22
+ var ILLEGAL_DIR_CHAR_PATTERN = /[\\/:*?"<>|]/;
23
+ var pad = (value, length = 2) => String(value).padStart(length, "0");
24
+ function escapeRegExp(value) {
25
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
11
26
  }
12
- function padTwo(value) {
13
- return String(value).padStart(2, "0");
27
+ function assertPatternValue(pattern) {
28
+ if (pattern.trim() === "") {
29
+ throw new Error("[vite-config] hashDirName must be a non-empty string.");
30
+ }
14
31
  }
15
- function formatHashTimestamp(now, format = "epoch") {
16
- if (format !== "datetime") return String(now);
32
+ function assertSafeDirName(dirName, pattern) {
33
+ const unsafe = ILLEGAL_DIR_CHAR_PATTERN.test(dirName) || dirName.includes("..") || dirName.startsWith(".") || dirName.endsWith(".") || dirName !== dirName.trim();
34
+ if (unsafe) {
35
+ throw new Error(
36
+ `[vite-config] hashDirName "${pattern}" resolves to unsafe directory name "${dirName}".`
37
+ );
38
+ }
39
+ }
40
+ function expandHashDirName(options) {
41
+ const { pattern, commitHash, now = Date.now() } = options;
42
+ assertPatternValue(pattern);
17
43
  const date = new Date(now);
18
- return [
19
- date.getFullYear(),
20
- padTwo(date.getMonth() + 1),
21
- padTwo(date.getDate()),
22
- padTwo(date.getHours()),
23
- padTwo(date.getMinutes()),
24
- padTwo(date.getSeconds())
25
- ].join("-");
44
+ const values = {
45
+ hash: commitHash || "local",
46
+ timestamp: String(now),
47
+ YYYY: String(date.getFullYear()),
48
+ MM: pad(date.getMonth() + 1),
49
+ DD: pad(date.getDate()),
50
+ HH: pad(date.getHours()),
51
+ mm: pad(date.getMinutes()),
52
+ ss: pad(date.getSeconds()),
53
+ SSS: pad(date.getMilliseconds(), 3)
54
+ };
55
+ let timeMatched = false;
56
+ const dirName = pattern.replace(tokenPattern(), (token) => {
57
+ if (TIME_TOKENS.includes(token)) timeMatched = true;
58
+ return values[token] ?? token;
59
+ });
60
+ if (!timeMatched) {
61
+ throw new Error(
62
+ `[vite-config] hashDirName "${pattern}" must contain a time token; available tokens: ${AVAILABLE_TOKENS.join(", ")}.`
63
+ );
64
+ }
65
+ assertSafeDirName(dirName, pattern);
66
+ return dirName;
67
+ }
68
+ function hasTimeToken(pattern) {
69
+ if (pattern.trim() === "") return false;
70
+ return [...pattern.matchAll(tokenPattern())].some(
71
+ (match) => TIME_TOKENS.includes(match[0])
72
+ );
73
+ }
74
+ function compileHashDirNameMatcher(pattern) {
75
+ assertPatternValue(pattern);
76
+ let source = "^";
77
+ let cursor = 0;
78
+ let groupIndex = 1;
79
+ let epochGroup = -1;
80
+ const dateTokens = [];
81
+ for (const match of pattern.matchAll(tokenPattern())) {
82
+ const index = match.index;
83
+ source += escapeRegExp(pattern.slice(cursor, index));
84
+ const token = match[0];
85
+ if (token === "hash") {
86
+ source += "(?:[0-9a-f]{12}|local)";
87
+ } else if (token === "timestamp") {
88
+ source += "(\\d{1,15})";
89
+ epochGroup = groupIndex;
90
+ groupIndex += 1;
91
+ } else {
92
+ const width = DATE_TOKEN_WIDTH[token];
93
+ source += `(\\d{${width}})`;
94
+ dateTokens.push(token);
95
+ groupIndex += 1;
96
+ }
97
+ cursor = index + token.length;
98
+ }
99
+ source += `${escapeRegExp(pattern.slice(cursor))}$`;
100
+ const regex = new RegExp(source);
101
+ return {
102
+ regex,
103
+ parseTimestamp(name) {
104
+ const matched = regex.exec(name);
105
+ if (!matched) return void 0;
106
+ if (epochGroup > 0) {
107
+ const epoch = matched[epochGroup];
108
+ return epoch == null ? void 0 : Number(epoch);
109
+ }
110
+ if (dateTokens.length === 0) return void 0;
111
+ const slots = {};
112
+ dateTokens.forEach((token, position) => {
113
+ const raw = matched[position + 1];
114
+ if (raw != null) slots[DATE_TOKEN_SLOT[token]] = Number(raw);
115
+ });
116
+ return new Date(
117
+ slots.year ?? 1970,
118
+ (slots.month ?? 1) - 1,
119
+ slots.day ?? 1,
120
+ slots.hour ?? 0,
121
+ slots.minute ?? 0,
122
+ slots.second ?? 0,
123
+ slots.millisecond ?? 0
124
+ ).getTime();
125
+ }
126
+ };
26
127
  }
27
- function cleanupOldHashBuilds(deployRoot, currentBuildDir, keep) {
128
+
129
+ // src/plugins/hash-deploy.ts
130
+ var RECENT_BUILD_PROTECT_MS = 5 * 60 * 1e3;
131
+ function cleanupOldHashBuilds(deployRoot, currentBuildDir, keep, matcher) {
132
+ const now = Date.now();
28
133
  const oldBuilds = fs.readdirSync(deployRoot, { withFileTypes: true }).flatMap((entry) => {
29
134
  if (!entry.isDirectory() || entry.name === currentBuildDir) return [];
30
- const match = HASH_BUILD_DIR_PATTERN.exec(entry.name);
31
- return match ? [{ name: entry.name, timestamp: parseHashTimestamp(match[1]) }] : [];
135
+ const timestamp = matcher.parseTimestamp(entry.name);
136
+ if (timestamp == null) return [];
137
+ if (now - timestamp < RECENT_BUILD_PROTECT_MS) return [];
138
+ return [{ name: entry.name, timestamp }];
32
139
  }).sort((left, right) => right.timestamp - left.timestamp || right.name.localeCompare(left.name));
33
140
  for (const build of oldBuilds.slice(Math.max(keep - 1, 0))) {
34
141
  fs.rmSync(path.join(deployRoot, build.name), { recursive: true });
35
142
  }
36
143
  }
37
144
  function hashDeployPlugin(options) {
38
- const { rootDir, buildDirStr, publicPath, cleanup } = options;
145
+ const { rootDir, buildDirStr, buildDirNamePattern, publicPath, cleanup } = options;
39
146
  if (cleanup && (!Number.isInteger(cleanup.keep) || cleanup.keep < 1)) {
40
147
  throw new Error("[hash-deploy] cleanup.keep must be an integer greater than or equal to 1.");
41
148
  }
149
+ const resolvedBuildDirNamePattern = buildDirNamePattern ?? DEFAULT_HASH_DIR_NAME_PATTERN;
150
+ if (cleanup && !hasTimeToken(resolvedBuildDirNamePattern)) {
151
+ throw new Error("[hash-deploy] cleanup requires a buildDirNamePattern with a time token.");
152
+ }
153
+ const cleanupMatcher = compileHashDirNameMatcher(resolvedBuildDirNamePattern);
154
+ if (cleanup && !cleanupMatcher.regex.test(buildDirStr)) {
155
+ throw new Error(
156
+ `[hash-deploy] buildDirStr "${buildDirStr}" does not match buildDirNamePattern "${resolvedBuildDirNamePattern}".`
157
+ );
158
+ }
42
159
  let outputDir;
43
160
  let deployRoot;
44
161
  return {
@@ -83,7 +200,7 @@ function hashDeployPlugin(options) {
83
200
  const rootHtml = publicPath.startsWith("./") ? sourceHtml.replace(/(\b(?:href|src)\s*=\s*["'])\.\//g, `$1${publicPath}`) : sourceHtml;
84
201
  fs.writeFileSync(path.join(deployRoot, "index.html"), rootHtml);
85
202
  if (cleanup) {
86
- cleanupOldHashBuilds(deployRoot, buildDirStr, cleanup.keep);
203
+ cleanupOldHashBuilds(deployRoot, buildDirStr, cleanup.keep, cleanupMatcher);
87
204
  }
88
205
  }
89
206
  }
@@ -105,13 +222,21 @@ function createHashBuildDirName(repositoryRootOrNowOrOptions, now = Date.now())
105
222
  const {
106
223
  repositoryRoot: repositoryRoot2,
107
224
  now: optionsNow = Date.now(),
108
- timestampFormat = "epoch"
225
+ hashDirName
109
226
  } = repositoryRootOrNowOrOptions;
110
- return `${getCommitHash(repositoryRoot2)}-${formatHashTimestamp(optionsNow, timestampFormat)}`;
227
+ return expandHashDirName({
228
+ pattern: hashDirName ?? DEFAULT_HASH_DIR_NAME_PATTERN,
229
+ commitHash: getCommitHash(repositoryRoot2),
230
+ now: optionsNow
231
+ });
111
232
  }
112
233
  const repositoryRoot = typeof repositoryRootOrNowOrOptions === "string" ? repositoryRootOrNowOrOptions : void 0;
113
234
  const timestamp = typeof repositoryRootOrNowOrOptions === "number" ? repositoryRootOrNowOrOptions : now;
114
- return `${getCommitHash(repositoryRoot)}-${formatHashTimestamp(timestamp)}`;
235
+ return expandHashDirName({
236
+ pattern: DEFAULT_HASH_DIR_NAME_PATTERN,
237
+ commitHash: getCommitHash(repositoryRoot),
238
+ now: timestamp
239
+ });
115
240
  }
116
241
 
117
242
  export { createHashBuildDirName, getCommitHash, hashDeployPlugin };
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/plugins/hash-deploy.ts"],"names":["repositoryRoot"],"mappings":"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","file":"hash-deploy.js","sourcesContent":["import { execFileSync } from \"node:child_process\";\r\nimport fs from \"node:fs\";\r\nimport path from \"node:path\";\r\n\r\nimport type { PluginOption } from \"vite\";\r\n\r\n/** @public */\r\nexport type HashTimestampFormat = \"epoch\" | \"datetime\";\r\n\r\n/** @public */\r\nexport interface HashBuildDirNameOptions {\r\n repositoryRoot?: string;\r\n now?: number;\r\n /** @defaultValue \"epoch\" */\r\n timestampFormat?: HashTimestampFormat;\r\n}\r\n\r\n/** @public */\r\nexport interface HashCleanupOptions {\r\n /**\r\n * 每次成功构建后保留的 Hash 版本总数(含本次构建)。\r\n * 必须为大于等于 1 的整数。\r\n */\r\n keep: number;\r\n}\r\n\r\n/** @public */\r\nexport interface HashDeployPluginOptions {\r\n rootDir: string;\r\n /** hash 子目录名,如 `{commit}-{timestamp}` 或 `{commit}-YYYY-MM-DD-HH-mm-ss` */\r\n buildDirStr: string;\r\n /**\r\n * 与最终 Vite `base` 一致的资源前缀。\r\n * - 默认相对:`./{buildDirStr}/`\r\n * - 自定义 CDN:`https://cdn.example.com/app/{buildDirStr}/`\r\n */\r\n publicPath: string;\r\n /** 旧 Hash 目录清理策略;未传时永久保留,由部署系统负责清理。 */\r\n cleanup?: HashCleanupOptions;\r\n}\r\n\r\nconst HASH_BUILD_DIR_PATTERN = /^(?:[0-9a-f]{12}|local)-(\\d{4}-\\d{2}-\\d{2}-\\d{2}-\\d{2}-\\d{2}|\\d+)$/;\r\n\r\nfunction parseHashTimestamp(value: string): number {\r\n if (/^\\d+$/.test(value)) return Number(value);\r\n const [year = 0, month = 1, day = 1, hour = 0, minute = 0, second = 0] = value\r\n .split(\"-\")\r\n .map(Number);\r\n return new Date(year, month - 1, day, hour, minute, second).getTime();\r\n}\r\n\r\nfunction padTwo(value: number): string {\r\n return String(value).padStart(2, \"0\");\r\n}\r\n\r\nfunction formatHashTimestamp(now: number, format: HashTimestampFormat = \"epoch\"): string {\r\n if (format !== \"datetime\") return String(now);\r\n const date = new Date(now);\r\n return [\r\n date.getFullYear(),\r\n padTwo(date.getMonth() + 1),\r\n padTwo(date.getDate()),\r\n padTwo(date.getHours()),\r\n padTwo(date.getMinutes()),\r\n padTwo(date.getSeconds()),\r\n ].join(\"-\");\r\n}\r\n\r\nfunction cleanupOldHashBuilds(deployRoot: string, currentBuildDir: string, keep: number): void {\r\n const oldBuilds = fs\r\n .readdirSync(deployRoot, { withFileTypes: true })\r\n .flatMap((entry) => {\r\n if (!entry.isDirectory() || entry.name === currentBuildDir) return [];\r\n const match = HASH_BUILD_DIR_PATTERN.exec(entry.name);\r\n return match ? [{ name: entry.name, timestamp: parseHashTimestamp(match[1]!) }] : [];\r\n })\r\n .sort((left, right) => right.timestamp - left.timestamp || right.name.localeCompare(left.name));\r\n\r\n for (const build of oldBuilds.slice(Math.max(keep - 1, 0))) {\r\n fs.rmSync(path.join(deployRoot, build.name), { recursive: true });\r\n }\r\n}\r\n\r\n/**\r\n * 构建结束后写 `dist/hash.txt`,并把 hash 子目录中的 `index.html` 复制到 `dist/index.html`。\r\n * 相对 `base` 时为根入口补上 hash 前缀;绝对 `base` 时产物已含完整 URL,原样复制即可。\r\n * 配置 `cleanup.keep` 时,成功写入根入口后只保留指定数量的最新 Hash 版本。\r\n * @public\r\n */\r\nexport function hashDeployPlugin(options: HashDeployPluginOptions): PluginOption {\r\n const { rootDir, buildDirStr, publicPath, cleanup } = options;\r\n if (cleanup && (!Number.isInteger(cleanup.keep) || cleanup.keep < 1)) {\r\n throw new Error(\"[hash-deploy] cleanup.keep must be an integer greater than or equal to 1.\");\r\n }\r\n let outputDir: string | undefined;\r\n let deployRoot: string | undefined;\r\n\r\n return {\r\n name: \"hash-deploy\",\r\n enforce: \"post\",\r\n configResolved(config) {\r\n const resolvedRoot = path.resolve(config.root);\r\n const expectedRoot = path.resolve(rootDir);\r\n if (resolvedRoot !== expectedRoot) {\r\n throw new Error(\r\n `[hash-deploy] Vite root \"${resolvedRoot}\" must match rootDir \"${expectedRoot}\".`,\r\n );\r\n }\r\n // 相对 hash:工厂 publicPath 为 `./{hash}/`,但 Vite base 必须保持 `./`。\r\n const expectedBase = publicPath.startsWith(\"./\") ? \"./\" : publicPath;\r\n if (config.base !== expectedBase) {\r\n throw new Error(\r\n `[hash-deploy] Vite base \"${config.base}\" must remain \"${expectedBase}\" during hash deployment.`,\r\n );\r\n }\r\n\r\n outputDir = path.resolve(resolvedRoot, config.build.outDir);\r\n if (path.basename(outputDir) !== buildDirStr) {\r\n throw new Error(\r\n `[hash-deploy] build.outDir \"${config.build.outDir}\" must end with the hash directory \"${buildDirStr}\".`,\r\n );\r\n }\r\n deployRoot = path.dirname(outputDir);\r\n },\r\n writeBundle: {\r\n sequential: true,\r\n order: \"post\",\r\n handler() {\r\n if (!outputDir || !deployRoot) {\r\n throw new Error(\"[hash-deploy] Vite configuration was not resolved before writeBundle.\");\r\n }\r\n\r\n const sourceIndex = path.join(outputDir, \"index.html\");\r\n if (!fs.existsSync(sourceIndex)) {\r\n throw new Error(`[hash-deploy] Generated entry was not found: \"${sourceIndex}\".`);\r\n }\r\n\r\n fs.mkdirSync(deployRoot, { recursive: true });\r\n fs.writeFileSync(path.join(deployRoot, \"hash.txt\"), buildDirStr);\r\n const sourceHtml = fs.readFileSync(sourceIndex, \"utf8\");\r\n // 相对 base(`./{hash}/`)时子目录 HTML 中的资源均相对于自身,根入口需统一补 hash;\r\n // 绝对 base 时 Vite 已写出完整 URL,直接复制。\r\n const rootHtml = publicPath.startsWith(\"./\")\r\n ? sourceHtml.replace(/(\\b(?:href|src)\\s*=\\s*[\"'])\\.\\//g, `$1${publicPath}`)\r\n : sourceHtml;\r\n fs.writeFileSync(path.join(deployRoot, \"index.html\"), rootHtml);\r\n if (cleanup) {\r\n cleanupOldHashBuilds(deployRoot, buildDirStr, cleanup.keep);\r\n }\r\n },\r\n },\r\n };\r\n}\r\n\r\n/**\r\n * 从指定仓库读取短 commit hash;非 git 环境回退 `local`。\r\n * @public\r\n */\r\nexport function getCommitHash(repositoryRoot: string = process.cwd()): string {\r\n try {\r\n return execFileSync(\"git\", [\"rev-parse\", \"HEAD\"], {\r\n cwd: repositoryRoot,\r\n encoding: \"utf8\",\r\n stdio: [\"ignore\", \"pipe\", \"ignore\"],\r\n })\r\n .trim()\r\n .slice(0, 12);\r\n } catch {\r\n return \"local\";\r\n }\r\n}\r\n\r\n/**\r\n * 生成 hash 构建目录名;保留仅传时间戳的旧调用方式。\r\n * @public\r\n */\r\nexport function createHashBuildDirName(now?: number): string;\r\n/** @public */\r\nexport function createHashBuildDirName(repositoryRoot: string, now?: number): string;\r\n/** @public */\r\nexport function createHashBuildDirName(options: HashBuildDirNameOptions): string;\r\nexport function createHashBuildDirName(\r\n repositoryRootOrNowOrOptions?: string | number | HashBuildDirNameOptions,\r\n now: number = Date.now(),\r\n): string {\r\n if (repositoryRootOrNowOrOptions != null && typeof repositoryRootOrNowOrOptions === \"object\") {\r\n const {\r\n repositoryRoot,\r\n now: optionsNow = Date.now(),\r\n timestampFormat = \"epoch\",\r\n } = repositoryRootOrNowOrOptions;\r\n return `${getCommitHash(repositoryRoot)}-${formatHashTimestamp(optionsNow, timestampFormat)}`;\r\n }\r\n const repositoryRoot =\r\n typeof repositoryRootOrNowOrOptions === \"string\" ? repositoryRootOrNowOrOptions : undefined;\r\n const timestamp =\r\n typeof repositoryRootOrNowOrOptions === \"number\" ? repositoryRootOrNowOrOptions : now;\r\n return `${getCommitHash(repositoryRoot)}-${formatHashTimestamp(timestamp)}`;\r\n}\r\n"]}
1
+ {"version":3,"sources":["../../src/plugins/hash-dir-name.ts","../../src/plugins/hash-deploy.ts"],"names":["repositoryRoot"],"mappings":"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","file":"hash-deploy.js","sourcesContent":["/**\n * Hash 子目录名模式串 → 实际目录名,以及清理历史目录用的匹配器。\n *\n * 与 `@hzab/webpack-config` 的 `hashDirName` 共用同一套 token 语义(两个包各自实现,不互相依赖):\n * token 展开为值,其余字符(含 `-`)原样保留。\n * - `hash` 12 位 commit(非 git 仓库回退 `local`)\n * - `timestamp` 毫秒时间戳(`Date.now()`)\n * - `YYYY` / `MM` / `DD` / `HH` / `mm` / `ss` / `SSS` 本地时区格式化时间\n *\n * 例(now = 2026-09-17 15:30:52.123,commit = a1b2c3d4e5f6):\n * - `hash-timestamp` -> `a1b2c3d4e5f6-1789630252123`\n * - `hash-timestamp-YYYYMMDDHHmmssSSS` -> `a1b2c3d4e5f6-1789630252123-20260917153052123`\n * - `hash-YYYYMMDDHHmmssSSS` -> `a1b2c3d4e5f6-20260917153052123`\n *\n * 约束:模式串必须至少含一个时间 token(`timestamp` 或日期 token)。只含 `hash` 的目录名会让\n * 同一 commit 的多次构建互相覆盖,因此被拒绝。\n *\n * 只做纯字符串计算(无 I/O),命名与清理排序共用同一套 token 定义。\n */\n\n/** token 交替顺序:长 token 在前,避免被短 token 抢先切分。每次取新实例,避免共享 `lastIndex`。 */\nconst tokenPattern = (): RegExp => /YYYY|SSS|MM|DD|HH|mm|ss|timestamp|hash/g;\n\nconst DATE_TOKEN_WIDTH = { YYYY: 4, MM: 2, DD: 2, HH: 2, mm: 2, ss: 2, SSS: 3 } as const;\n\ntype DateToken = keyof typeof DATE_TOKEN_WIDTH;\n\n/** 日期 token → `Date` 字段 */\nconst DATE_TOKEN_SLOT = {\n YYYY: \"year\",\n MM: \"month\",\n DD: \"day\",\n HH: \"hour\",\n mm: \"minute\",\n ss: \"second\",\n SSS: \"millisecond\",\n} as const satisfies Record<DateToken, string>;\n\ntype DateSlot = (typeof DATE_TOKEN_SLOT)[DateToken];\n\nconst TIME_TOKENS = [\"timestamp\", \"YYYY\", \"MM\", \"DD\", \"HH\", \"mm\", \"ss\", \"SSS\"] as const;\n\nconst AVAILABLE_TOKENS = [\"hash\", ...TIME_TOKENS];\n\n/** 默认模式串:与历史默认目录名 `{commit}-{Date.now()}` 等价 */\nexport const DEFAULT_HASH_DIR_NAME_PATTERN = \"hash-timestamp\";\n\n/** 目录名不允许出现的字符(路径分隔符、Windows 保留字符) */\nconst ILLEGAL_DIR_CHAR_PATTERN = /[\\\\/:*?\"<>|]/;\n\nconst pad = (value: number, length = 2): string => String(value).padStart(length, \"0\");\n\nfunction escapeRegExp(value: string): string {\n return value.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n}\n\nfunction assertPatternValue(pattern: string): void {\n if (pattern.trim() === \"\") {\n throw new Error(\"[vite-config] hashDirName must be a non-empty string.\");\n }\n}\n\n/** 展开后的目录名必须留在 deployRoot 之下(模式串不能借字面文本跳出产物根目录) */\nfunction assertSafeDirName(dirName: string, pattern: string): void {\n const unsafe =\n ILLEGAL_DIR_CHAR_PATTERN.test(dirName) ||\n dirName.includes(\"..\") ||\n dirName.startsWith(\".\") ||\n dirName.endsWith(\".\") ||\n dirName !== dirName.trim();\n\n if (unsafe) {\n throw new Error(\n `[vite-config] hashDirName \"${pattern}\" resolves to unsafe directory name \"${dirName}\".`,\n );\n }\n}\n\n/**\n * 把目录名模式串展开为目录名。\n * `timestamp` 与日期 token 共用同一个 `now`,保证描述同一时刻。\n */\nexport function expandHashDirName(options: {\n pattern: string;\n commitHash: string;\n now?: number;\n}): string {\n const { pattern, commitHash, now = Date.now() } = options;\n assertPatternValue(pattern);\n\n const date = new Date(now);\n const values: Record<string, string> = {\n hash: commitHash || \"local\",\n timestamp: String(now),\n YYYY: String(date.getFullYear()),\n MM: pad(date.getMonth() + 1),\n DD: pad(date.getDate()),\n HH: pad(date.getHours()),\n mm: pad(date.getMinutes()),\n ss: pad(date.getSeconds()),\n SSS: pad(date.getMilliseconds(), 3),\n };\n\n let timeMatched = false;\n // 单次 replace:替换结果不会被二次扫描,避免 commit / 时间戳里的字符被再次展开\n const dirName = pattern.replace(tokenPattern(), (token) => {\n if ((TIME_TOKENS as readonly string[]).includes(token)) timeMatched = true;\n return values[token] ?? token;\n });\n\n // 只含 commit(如 \"hash\")的目录名会让同一 commit 的多次构建互相覆盖,直接拒绝\n if (!timeMatched) {\n throw new Error(\n `[vite-config] hashDirName \"${pattern}\" must contain a time token; available tokens: ${AVAILABLE_TOKENS.join(\", \")}.`,\n );\n }\n\n assertSafeDirName(dirName, pattern);\n\n return dirName;\n}\n\n/**\n * 模式串是否含可排序的时间 token(`timestamp` 或日期 token)。\n * 清理器按目录名里的时间判断新旧,没有时间 token 就无从排序,必须让调用方提前报错。\n */\nexport function hasTimeToken(pattern: string): boolean {\n if (pattern.trim() === \"\") return false;\n\n return [...pattern.matchAll(tokenPattern())].some((match) =>\n (TIME_TOKENS as readonly string[]).includes(match[0]),\n );\n}\n\n/**\n * 由模式串编译出的历史目录匹配器:只认本模式串生成的目录名,并给出可排序时间戳。\n */\nexport interface HashDirNameMatcher {\n regex: RegExp;\n /** 目录名 → 时间戳(毫秒);目录名不匹配本模式串时返回 `undefined` */\n parseTimestamp(name: string): number | undefined;\n}\n\n/**\n * 编译模式串:字面文本转义为 `^...$`,token 转为捕获组。\n * - `timestamp` 存在时直接取其毫秒值排序;\n * - 否则按日期 token 回推本地时间(缺省字段为 1970-01-01 00:00:00.000)。\n * 模式串必须含时间 token(由 `expandHashDirName` 保证),故时间戳总是可解析。\n */\nexport function compileHashDirNameMatcher(pattern: string): HashDirNameMatcher {\n assertPatternValue(pattern);\n\n let source = \"^\";\n let cursor = 0;\n let groupIndex = 1;\n let epochGroup = -1;\n const dateTokens: DateToken[] = [];\n\n for (const match of pattern.matchAll(tokenPattern())) {\n const index = match.index;\n source += escapeRegExp(pattern.slice(cursor, index));\n\n const token = match[0];\n if (token === \"hash\") {\n source += \"(?:[0-9a-f]{12}|local)\";\n } else if (token === \"timestamp\") {\n source += \"(\\\\d{1,15})\";\n epochGroup = groupIndex;\n groupIndex += 1;\n } else {\n const width = DATE_TOKEN_WIDTH[token as DateToken];\n source += `(\\\\d{${width}})`;\n dateTokens.push(token as DateToken);\n groupIndex += 1;\n }\n\n cursor = index + token.length;\n }\n source += `${escapeRegExp(pattern.slice(cursor))}$`;\n\n const regex = new RegExp(source);\n\n return {\n regex,\n parseTimestamp(name: string): number | undefined {\n const matched = regex.exec(name);\n if (!matched) return undefined;\n\n if (epochGroup > 0) {\n const epoch = matched[epochGroup];\n return epoch == null ? undefined : Number(epoch);\n }\n if (dateTokens.length === 0) return undefined;\n\n const slots: Partial<Record<DateSlot, number>> = {};\n dateTokens.forEach((token, position) => {\n const raw = matched[position + 1];\n if (raw != null) slots[DATE_TOKEN_SLOT[token]] = Number(raw);\n });\n\n return new Date(\n slots.year ?? 1970,\n (slots.month ?? 1) - 1,\n slots.day ?? 1,\n slots.hour ?? 0,\n slots.minute ?? 0,\n slots.second ?? 0,\n slots.millisecond ?? 0,\n ).getTime();\n },\n };\n}\n","import { execFileSync } from \"node:child_process\";\nimport fs from \"node:fs\";\nimport path from \"node:path\";\n\nimport type { PluginOption } from \"vite\";\n\nimport {\n compileHashDirNameMatcher,\n DEFAULT_HASH_DIR_NAME_PATTERN,\n expandHashDirName,\n hasTimeToken,\n} from \"./hash-dir-name.ts\";\nimport type { HashDirNameMatcher } from \"./hash-dir-name.ts\";\n\n/**\n * 近期构建保护窗口:目录名时间戳距今不足该时长的目录不参与清理。\n * 同机并发构建时,先完成的构建不应删掉另一个进程正在写的目录。\n * 判据只用目录名里的时间戳,不用文件系统 mtime——CI 恢复缓存 / 解压产物会改写 mtime。\n */\nconst RECENT_BUILD_PROTECT_MS = 5 * 60 * 1000;\n\n/** @public */\nexport interface HashBuildDirNameOptions {\n repositoryRoot?: string;\n now?: number;\n /**\n * Hash 子目录名模式串;不传时用默认 `hash-timestamp`(等价历史默认 `{commit}-{毫秒时间戳}`)。\n * token 大小写敏感:`hash` = 12 位 commit(非 git 回退 `local`)、`timestamp` = 毫秒时间戳、\n * `YYYY`/`MM`/`DD`/`HH`/`mm`/`ss`/`SSS` = 本地时区格式化时间;其余字符(含 `-`)为字面文本。\n * 模式串必须至少含一个时间 token(`timestamp` 或日期 token),只含 `hash` 会被拒绝。\n * 例:`hash-timestamp`、`hash-timestamp-YYYYMMDDHHmmssSSS`、`hash-YYYYMMDDHHmmssSSS`。\n */\n hashDirName?: string;\n}\n\n/** @public */\nexport interface HashCleanupOptions {\n /**\n * 每次成功构建后保留的 Hash 版本总数(含本次构建)。\n * 必须为大于等于 1 的整数。\n */\n keep: number;\n}\n\n/** @public */\nexport interface HashDeployPluginOptions {\n rootDir: string;\n /** hash 子目录名,如 `{commit}-{timestamp}` 或 `{commit}-YYYY-MM-DD-HH-mm-ss` */\n buildDirStr: string;\n /**\n * 生成 `buildDirStr` 的模式串(来自工厂的 `hashDirName`);供 `cleanup` 精确识别历史目录,\n * 且必须与实际 `buildDirStr` 匹配。未传时用默认模式串 `hash-timestamp`。\n */\n buildDirNamePattern?: string;\n /**\n * 与最终 Vite `base` 一致的资源前缀。\n * - 默认相对:`./{buildDirStr}/`\n * - 自定义 CDN:`https://cdn.example.com/app/{buildDirStr}/`\n */\n publicPath: string;\n /** 旧 Hash 目录清理策略;未传时永久保留,由部署系统负责清理。 */\n cleanup?: HashCleanupOptions;\n}\n\n/**\n * 只保留最近 `keep` 个 Hash 版本(含本次构建)。\n * 只识别 `matcher`(模式串)生成的目录名;不匹配的目录(如 `uploads-*`)一律不动。\n * 目录名时间戳落在保护窗口内的目录视为并发构建产物,跳过清理。\n */\nfunction cleanupOldHashBuilds(\n deployRoot: string,\n currentBuildDir: string,\n keep: number,\n matcher: HashDirNameMatcher,\n): void {\n const now = Date.now();\n const oldBuilds = fs\n .readdirSync(deployRoot, { withFileTypes: true })\n .flatMap((entry) => {\n if (!entry.isDirectory() || entry.name === currentBuildDir) return [];\n const timestamp = matcher.parseTimestamp(entry.name);\n if (timestamp == null) return [];\n // 保护窗口内的目录(含因时钟偏差落在未来的目录)保持不动\n if (now - timestamp < RECENT_BUILD_PROTECT_MS) return [];\n\n return [{ name: entry.name, timestamp }];\n })\n .sort((left, right) => right.timestamp - left.timestamp || right.name.localeCompare(left.name));\n\n for (const build of oldBuilds.slice(Math.max(keep - 1, 0))) {\n fs.rmSync(path.join(deployRoot, build.name), { recursive: true });\n }\n}\n\n/**\n * 构建结束后写 `dist/hash.txt`,并把 hash 子目录中的 `index.html` 复制到 `dist/index.html`。\n * 相对 `base` 时为根入口补上 hash 前缀;绝对 `base` 时产物已含完整 URL,原样复制即可。\n * 配置 `cleanup.keep` 时,成功写入根入口后只保留指定数量的最新 Hash 版本。\n * @public\n */\nexport function hashDeployPlugin(options: HashDeployPluginOptions): PluginOption {\n const { rootDir, buildDirStr, buildDirNamePattern, publicPath, cleanup } = options;\n if (cleanup && (!Number.isInteger(cleanup.keep) || cleanup.keep < 1)) {\n throw new Error(\"[hash-deploy] cleanup.keep must be an integer greater than or equal to 1.\");\n }\n\n const resolvedBuildDirNamePattern = buildDirNamePattern ?? DEFAULT_HASH_DIR_NAME_PATTERN;\n if (cleanup && !hasTimeToken(resolvedBuildDirNamePattern)) {\n // 没有可排序的时间 token 时无法判断历史目录新旧,直接报错,否则会静默堆积\n throw new Error(\"[hash-deploy] cleanup requires a buildDirNamePattern with a time token.\");\n }\n\n const cleanupMatcher = compileHashDirNameMatcher(resolvedBuildDirNamePattern);\n if (cleanup && !cleanupMatcher.regex.test(buildDirStr)) {\n // 配了清理但模式串与实际目录名不符时直接报错,否则历史目录会静默堆积,\n // 与「保留最近 N 个」的预期相反。\n throw new Error(\n `[hash-deploy] buildDirStr \"${buildDirStr}\" does not match buildDirNamePattern \"${resolvedBuildDirNamePattern}\".`,\n );\n }\n\n let outputDir: string | undefined;\n let deployRoot: string | undefined;\n\n return {\n name: \"hash-deploy\",\n enforce: \"post\",\n configResolved(config) {\n const resolvedRoot = path.resolve(config.root);\n const expectedRoot = path.resolve(rootDir);\n if (resolvedRoot !== expectedRoot) {\n throw new Error(\n `[hash-deploy] Vite root \"${resolvedRoot}\" must match rootDir \"${expectedRoot}\".`,\n );\n }\n // 相对 hash:工厂 publicPath 为 `./{hash}/`,但 Vite base 必须保持 `./`。\n const expectedBase = publicPath.startsWith(\"./\") ? \"./\" : publicPath;\n if (config.base !== expectedBase) {\n throw new Error(\n `[hash-deploy] Vite base \"${config.base}\" must remain \"${expectedBase}\" during hash deployment.`,\n );\n }\n\n outputDir = path.resolve(resolvedRoot, config.build.outDir);\n if (path.basename(outputDir) !== buildDirStr) {\n throw new Error(\n `[hash-deploy] build.outDir \"${config.build.outDir}\" must end with the hash directory \"${buildDirStr}\".`,\n );\n }\n deployRoot = path.dirname(outputDir);\n },\n writeBundle: {\n sequential: true,\n order: \"post\",\n handler() {\n if (!outputDir || !deployRoot) {\n throw new Error(\"[hash-deploy] Vite configuration was not resolved before writeBundle.\");\n }\n\n const sourceIndex = path.join(outputDir, \"index.html\");\n if (!fs.existsSync(sourceIndex)) {\n throw new Error(`[hash-deploy] Generated entry was not found: \"${sourceIndex}\".`);\n }\n\n fs.mkdirSync(deployRoot, { recursive: true });\n fs.writeFileSync(path.join(deployRoot, \"hash.txt\"), buildDirStr);\n const sourceHtml = fs.readFileSync(sourceIndex, \"utf8\");\n // 相对 base(`./{hash}/`)时子目录 HTML 中的资源均相对于自身,根入口需统一补 hash;\n // 绝对 base 时 Vite 已写出完整 URL,直接复制。\n const rootHtml = publicPath.startsWith(\"./\")\n ? sourceHtml.replace(/(\\b(?:href|src)\\s*=\\s*[\"'])\\.\\//g, `$1${publicPath}`)\n : sourceHtml;\n fs.writeFileSync(path.join(deployRoot, \"index.html\"), rootHtml);\n if (cleanup) {\n cleanupOldHashBuilds(deployRoot, buildDirStr, cleanup.keep, cleanupMatcher);\n }\n },\n },\n };\n}\n\n/**\n * 从指定仓库读取短 commit hash;非 git 环境回退 `local`。\n * @public\n */\nexport function getCommitHash(repositoryRoot: string = process.cwd()): string {\n try {\n return execFileSync(\"git\", [\"rev-parse\", \"HEAD\"], {\n cwd: repositoryRoot,\n encoding: \"utf8\",\n stdio: [\"ignore\", \"pipe\", \"ignore\"],\n })\n .trim()\n .slice(0, 12);\n } catch {\n return \"local\";\n }\n}\n\n/**\n * 生成 hash 构建目录名;保留仅传时间戳的旧调用方式。\n * 目录名一律由模式串展开,`hashDirName` 不传时用默认 `hash-timestamp`\n * (等价历史默认 `{commit}-{毫秒时间戳}`)。\n * @public\n */\nexport function createHashBuildDirName(now?: number): string;\n/** @public */\nexport function createHashBuildDirName(repositoryRoot: string, now?: number): string;\n/** @public */\nexport function createHashBuildDirName(options: HashBuildDirNameOptions): string;\nexport function createHashBuildDirName(\n repositoryRootOrNowOrOptions?: string | number | HashBuildDirNameOptions,\n now: number = Date.now(),\n): string {\n if (repositoryRootOrNowOrOptions != null && typeof repositoryRootOrNowOrOptions === \"object\") {\n const {\n repositoryRoot,\n now: optionsNow = Date.now(),\n hashDirName,\n } = repositoryRootOrNowOrOptions;\n\n return expandHashDirName({\n pattern: hashDirName ?? DEFAULT_HASH_DIR_NAME_PATTERN,\n commitHash: getCommitHash(repositoryRoot),\n now: optionsNow,\n });\n }\n\n const repositoryRoot =\n typeof repositoryRootOrNowOrOptions === \"string\" ? repositoryRootOrNowOrOptions : undefined;\n const timestamp =\n typeof repositoryRootOrNowOrOptions === \"number\" ? repositoryRootOrNowOrOptions : now;\n\n return expandHashDirName({\n pattern: DEFAULT_HASH_DIR_NAME_PATTERN,\n commitHash: getCommitHash(repositoryRoot),\n now: timestamp,\n });\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/plugins/less-tilde.ts"],"names":[],"mappings":";;;AAaO,SAAS,yBAAA,GAA4B;AAC1C,EAAA,OAAO;AAAA,IACL,OAAA,CAAQ,MAAW,aAAA,EAAoB;AAAA,MACrC,MAAM,gBAAA,SAAyB,IAAA,CAAK,WAAA,CAAY;AAAA,QAC9C,SAAS,QAAA,EAAkB;AACzB,UAAA,OAAO,QAAA,CAAS,WAAW,GAAG,CAAA;AAAA,QAChC;AAAA,QACA,QAAA,CAAS,QAAA,EAAkB,gBAAA,EAA0B,OAAA,EAAc,WAAA,EAAkB;AAEnF,UAAA,OAAO,KAAA,CAAM,SAAS,QAAA,CAAS,KAAA,CAAM,CAAC,CAAA,EAAG,gBAAA,EAAkB,SAAS,WAAW,CAAA;AAAA,QACjF;AAAA;AAEF,MAAA,aAAA,CAAc,cAAA,CAAe,IAAI,gBAAA,EAAkB,CAAA;AAAA,IACrD;AAAA,GACF;AACF;AAOO,SAAS,iBAAiB,IAAA,EAAoB;AACnD,EAAA,MAAM,YAAA,GAAe,MAAM,YAAA,KAAiB,IAAA;AAC5C,EAAA,OAAO;AAAA,IACL,GAAI,MAAM,cAAA,IAAkB,IAAA,GAAO,EAAE,cAAA,EAAgB,IAAA,CAAK,cAAA,EAAe,GAAI,EAAC;AAAA,IAC9E,GAAI,MAAM,UAAA,IAAc,IAAA,GAAO,EAAE,UAAA,EAAY,IAAA,CAAK,UAAA,EAAW,GAAI,EAAC;AAAA,IAClE,GAAI,MAAM,iBAAA,IAAqB,IAAA,GAAO,EAAE,iBAAA,EAAmB,IAAA,CAAK,iBAAA,EAAkB,GAAI,EAAC;AAAA,IACvF,GAAI,eAAe,EAAE,OAAA,EAAS,CAAC,yBAAA,EAA2B,CAAA,EAAE,GAAI;AAAC,GACnE;AACF","file":"less-tilde.cjs","sourcesContent":["import type { LessOptions } from \"../types.ts\";\r\n\r\n/**\r\n * Less 插件:把 webpack 遗留的 `~pkg/...` 导入解析到 node_modules。\r\n *\r\n * Less 自身不认识 `~`(那是 webpack `less-loader` 的约定:从 node_modules 解析);Vite 的\r\n * Less 预处理器也不处理 `~`,会回退到 Less 默认 FileManager 去查找 `node_modules/~pkg/...` 而失败。\r\n * 本插件注册一个 FileManager:仅当导入以 `~` 开头时拦截,去掉前缀后交给默认 FileManager 解析\r\n * (Vite 恒向 `less.render` 注入 `paths: [\"node_modules\"]`)。\r\n *\r\n * 第三方包(如 formily 系)自带 `@import \"~antd/...\"` 时尤其需要——消费方无法修改 node_modules。\r\n * @public\r\n */\r\nexport function lessTildeResolutionPlugin() {\r\n return {\r\n install(less: any, pluginManager: any) {\r\n class TildeFileManager extends less.FileManager {\r\n supports(filename: string) {\r\n return filename.startsWith(\"~\");\r\n }\r\n loadFile(filename: string, currentDirectory: string, options: any, environment: any) {\r\n // `~pkg/...` → `pkg/...`,让默认 FileManager 从 node_modules 解析\r\n return super.loadFile(filename.slice(1), currentDirectory, options, environment);\r\n }\r\n }\r\n pluginManager.addFileManager(new TildeFileManager());\r\n },\r\n };\r\n}\r\n\r\n/**\r\n * 组装 `css.preprocessorOptions.less`:映射 webpack `less-loader` 选项,\r\n * `less.resolveTilde: true` 时注入 `~` 解析插件(默认关闭)。\r\n * @public\r\n */\r\nexport function buildLessOptions(less?: LessOptions) {\r\n const resolveTilde = less?.resolveTilde === true;\r\n return {\r\n ...(less?.additionalData != null ? { additionalData: less.additionalData } : {}),\r\n ...(less?.modifyVars != null ? { modifyVars: less.modifyVars } : {}),\r\n ...(less?.javascriptEnabled != null ? { javascriptEnabled: less.javascriptEnabled } : {}),\r\n ...(resolveTilde ? { plugins: [lessTildeResolutionPlugin()] } : {}),\r\n };\r\n}\r\n"]}
1
+ {"version":3,"sources":["../../src/plugins/less-tilde.ts"],"names":[],"mappings":";;;AAoCO,SAAS,yBAAA,GAA4B;AAC1C,EAAA,OAAO;AAAA,IACL,OAAA,CAAQ,MAAsB,aAAA,EAAkC;AAAA,MAC9D,MAAM,gBAAA,SAAyB,IAAA,CAAK,WAAA,CAAY;AAAA,QACrC,SAAS,QAAA,EAAkB;AAClC,UAAA,OAAO,QAAA,CAAS,WAAW,GAAG,CAAA;AAAA,QAChC;AAAA,QACS,QAAA,CACP,QAAA,EACA,gBAAA,EACA,OAAA,EACA,WAAA,EACA;AAEA,UAAA,OAAO,KAAA,CAAM,SAAS,QAAA,CAAS,KAAA,CAAM,CAAC,CAAA,EAAG,gBAAA,EAAkB,SAAS,WAAW,CAAA;AAAA,QACjF;AAAA;AAEF,MAAA,aAAA,CAAc,cAAA,CAAe,IAAI,gBAAA,EAAkB,CAAA;AAAA,IACrD;AAAA,GACF;AACF;AAOO,SAAS,iBAAiB,IAAA,EAAoB;AACnD,EAAA,MAAM,YAAA,GAAe,MAAM,YAAA,KAAiB,IAAA;AAC5C,EAAA,OAAO;AAAA,IACL,GAAI,MAAM,cAAA,IAAkB,IAAA,GAAO,EAAE,cAAA,EAAgB,IAAA,CAAK,cAAA,EAAe,GAAI,EAAC;AAAA,IAC9E,GAAI,MAAM,UAAA,IAAc,IAAA,GAAO,EAAE,UAAA,EAAY,IAAA,CAAK,UAAA,EAAW,GAAI,EAAC;AAAA,IAClE,GAAI,MAAM,iBAAA,IAAqB,IAAA,GAAO,EAAE,iBAAA,EAAmB,IAAA,CAAK,iBAAA,EAAkB,GAAI,EAAC;AAAA,IACvF,GAAI,eAAe,EAAE,OAAA,EAAS,CAAC,yBAAA,EAA2B,CAAA,EAAE,GAAI;AAAC,GACnE;AACF","file":"less-tilde.cjs","sourcesContent":["import type { LessOptions } from \"../types.ts\";\n\n/**\n * less 插件注入的 FileManager 实例表面类型。less@4 不自带 TS 类型,按实际使用的最小面声明。\n */\ninterface LessFileManagerInstance {\n supports(filename: string): boolean;\n loadFile(\n filename: string,\n currentDirectory: string,\n options: unknown,\n environment: unknown,\n ): unknown;\n}\n\n/** less.js 传给插件 `install` 的 less 环境实例表面。 */\ninterface LessPluginHost {\n FileManager: new (...args: unknown[]) => LessFileManagerInstance;\n}\n\n/** less.js 插件管理器表面。 */\ninterface LessPluginManager {\n addFileManager(manager: LessFileManagerInstance): void;\n}\n\n/**\n * Less 插件:把 webpack 遗留的 `~pkg/...` 导入解析到 node_modules。\n *\n * Less 自身不认识 `~`(那是 webpack `less-loader` 的约定:从 node_modules 解析);Vite 的\n * Less 预处理器也不处理 `~`,会回退到 Less 默认 FileManager 去查找 `node_modules/~pkg/...` 而失败。\n * 本插件注册一个 FileManager:仅当导入以 `~` 开头时拦截,去掉前缀后交给默认 FileManager 解析\n * (Vite 恒向 `less.render` 注入 `paths: [\"node_modules\"]`)。\n *\n * 第三方包(如 formily 系)自带 `@import \"~antd/...\"` 时尤其需要——消费方无法修改 node_modules。\n * @public\n */\nexport function lessTildeResolutionPlugin() {\n return {\n install(less: LessPluginHost, pluginManager: LessPluginManager) {\n class TildeFileManager extends less.FileManager {\n override supports(filename: string) {\n return filename.startsWith(\"~\");\n }\n override loadFile(\n filename: string,\n currentDirectory: string,\n options: unknown,\n environment: unknown,\n ) {\n // `~pkg/...` → `pkg/...`,让默认 FileManager 从 node_modules 解析\n return super.loadFile(filename.slice(1), currentDirectory, options, environment);\n }\n }\n pluginManager.addFileManager(new TildeFileManager());\n },\n };\n}\n\n/**\n * 组装 `css.preprocessorOptions.less`:映射 webpack `less-loader` 选项,\n * `less.resolveTilde: true` 时注入 `~` 解析插件(默认关闭)。\n * @public\n */\nexport function buildLessOptions(less?: LessOptions) {\n const resolveTilde = less?.resolveTilde === true;\n return {\n ...(less?.additionalData != null ? { additionalData: less.additionalData } : {}),\n ...(less?.modifyVars != null ? { modifyVars: less.modifyVars } : {}),\n ...(less?.javascriptEnabled != null ? { javascriptEnabled: less.javascriptEnabled } : {}),\n ...(resolveTilde ? { plugins: [lessTildeResolutionPlugin()] } : {}),\n };\n}\n"]}
@@ -5,6 +5,21 @@ import 'vite-plugin-static-copy';
5
5
  import './hash-deploy.cjs';
6
6
  import './svg-react.cjs';
7
7
 
8
+ /**
9
+ * less 插件注入的 FileManager 实例表面类型。less@4 不自带 TS 类型,按实际使用的最小面声明。
10
+ */
11
+ interface LessFileManagerInstance {
12
+ supports(filename: string): boolean;
13
+ loadFile(filename: string, currentDirectory: string, options: unknown, environment: unknown): unknown;
14
+ }
15
+ /** less.js 传给插件 `install` 的 less 环境实例表面。 */
16
+ interface LessPluginHost {
17
+ FileManager: new (...args: unknown[]) => LessFileManagerInstance;
18
+ }
19
+ /** less.js 插件管理器表面。 */
20
+ interface LessPluginManager {
21
+ addFileManager(manager: LessFileManagerInstance): void;
22
+ }
8
23
  /**
9
24
  * Less 插件:把 webpack 遗留的 `~pkg/...` 导入解析到 node_modules。
10
25
  *
@@ -17,7 +32,7 @@ import './svg-react.cjs';
17
32
  * @public
18
33
  */
19
34
  declare function lessTildeResolutionPlugin(): {
20
- install(less: any, pluginManager: any): void;
35
+ install(less: LessPluginHost, pluginManager: LessPluginManager): void;
21
36
  };
22
37
  /**
23
38
  * 组装 `css.preprocessorOptions.less`:映射 webpack `less-loader` 选项,
@@ -26,7 +41,7 @@ declare function lessTildeResolutionPlugin(): {
26
41
  */
27
42
  declare function buildLessOptions(less?: LessOptions): {
28
43
  plugins?: {
29
- install(less: any, pluginManager: any): void;
44
+ install(less: LessPluginHost, pluginManager: LessPluginManager): void;
30
45
  }[] | undefined;
31
46
  javascriptEnabled?: boolean | undefined;
32
47
  modifyVars?: Record<string, unknown> | undefined;
@@ -5,6 +5,21 @@ import 'vite-plugin-static-copy';
5
5
  import './hash-deploy.js';
6
6
  import './svg-react.js';
7
7
 
8
+ /**
9
+ * less 插件注入的 FileManager 实例表面类型。less@4 不自带 TS 类型,按实际使用的最小面声明。
10
+ */
11
+ interface LessFileManagerInstance {
12
+ supports(filename: string): boolean;
13
+ loadFile(filename: string, currentDirectory: string, options: unknown, environment: unknown): unknown;
14
+ }
15
+ /** less.js 传给插件 `install` 的 less 环境实例表面。 */
16
+ interface LessPluginHost {
17
+ FileManager: new (...args: unknown[]) => LessFileManagerInstance;
18
+ }
19
+ /** less.js 插件管理器表面。 */
20
+ interface LessPluginManager {
21
+ addFileManager(manager: LessFileManagerInstance): void;
22
+ }
8
23
  /**
9
24
  * Less 插件:把 webpack 遗留的 `~pkg/...` 导入解析到 node_modules。
10
25
  *
@@ -17,7 +32,7 @@ import './svg-react.js';
17
32
  * @public
18
33
  */
19
34
  declare function lessTildeResolutionPlugin(): {
20
- install(less: any, pluginManager: any): void;
35
+ install(less: LessPluginHost, pluginManager: LessPluginManager): void;
21
36
  };
22
37
  /**
23
38
  * 组装 `css.preprocessorOptions.less`:映射 webpack `less-loader` 选项,
@@ -26,7 +41,7 @@ declare function lessTildeResolutionPlugin(): {
26
41
  */
27
42
  declare function buildLessOptions(less?: LessOptions): {
28
43
  plugins?: {
29
- install(less: any, pluginManager: any): void;
44
+ install(less: LessPluginHost, pluginManager: LessPluginManager): void;
30
45
  }[] | undefined;
31
46
  javascriptEnabled?: boolean | undefined;
32
47
  modifyVars?: Record<string, unknown> | undefined;
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/plugins/less-tilde.ts"],"names":[],"mappings":";AAaO,SAAS,yBAAA,GAA4B;AAC1C,EAAA,OAAO;AAAA,IACL,OAAA,CAAQ,MAAW,aAAA,EAAoB;AAAA,MACrC,MAAM,gBAAA,SAAyB,IAAA,CAAK,WAAA,CAAY;AAAA,QAC9C,SAAS,QAAA,EAAkB;AACzB,UAAA,OAAO,QAAA,CAAS,WAAW,GAAG,CAAA;AAAA,QAChC;AAAA,QACA,QAAA,CAAS,QAAA,EAAkB,gBAAA,EAA0B,OAAA,EAAc,WAAA,EAAkB;AAEnF,UAAA,OAAO,KAAA,CAAM,SAAS,QAAA,CAAS,KAAA,CAAM,CAAC,CAAA,EAAG,gBAAA,EAAkB,SAAS,WAAW,CAAA;AAAA,QACjF;AAAA;AAEF,MAAA,aAAA,CAAc,cAAA,CAAe,IAAI,gBAAA,EAAkB,CAAA;AAAA,IACrD;AAAA,GACF;AACF;AAOO,SAAS,iBAAiB,IAAA,EAAoB;AACnD,EAAA,MAAM,YAAA,GAAe,MAAM,YAAA,KAAiB,IAAA;AAC5C,EAAA,OAAO;AAAA,IACL,GAAI,MAAM,cAAA,IAAkB,IAAA,GAAO,EAAE,cAAA,EAAgB,IAAA,CAAK,cAAA,EAAe,GAAI,EAAC;AAAA,IAC9E,GAAI,MAAM,UAAA,IAAc,IAAA,GAAO,EAAE,UAAA,EAAY,IAAA,CAAK,UAAA,EAAW,GAAI,EAAC;AAAA,IAClE,GAAI,MAAM,iBAAA,IAAqB,IAAA,GAAO,EAAE,iBAAA,EAAmB,IAAA,CAAK,iBAAA,EAAkB,GAAI,EAAC;AAAA,IACvF,GAAI,eAAe,EAAE,OAAA,EAAS,CAAC,yBAAA,EAA2B,CAAA,EAAE,GAAI;AAAC,GACnE;AACF","file":"less-tilde.js","sourcesContent":["import type { LessOptions } from \"../types.ts\";\r\n\r\n/**\r\n * Less 插件:把 webpack 遗留的 `~pkg/...` 导入解析到 node_modules。\r\n *\r\n * Less 自身不认识 `~`(那是 webpack `less-loader` 的约定:从 node_modules 解析);Vite 的\r\n * Less 预处理器也不处理 `~`,会回退到 Less 默认 FileManager 去查找 `node_modules/~pkg/...` 而失败。\r\n * 本插件注册一个 FileManager:仅当导入以 `~` 开头时拦截,去掉前缀后交给默认 FileManager 解析\r\n * (Vite 恒向 `less.render` 注入 `paths: [\"node_modules\"]`)。\r\n *\r\n * 第三方包(如 formily 系)自带 `@import \"~antd/...\"` 时尤其需要——消费方无法修改 node_modules。\r\n * @public\r\n */\r\nexport function lessTildeResolutionPlugin() {\r\n return {\r\n install(less: any, pluginManager: any) {\r\n class TildeFileManager extends less.FileManager {\r\n supports(filename: string) {\r\n return filename.startsWith(\"~\");\r\n }\r\n loadFile(filename: string, currentDirectory: string, options: any, environment: any) {\r\n // `~pkg/...` → `pkg/...`,让默认 FileManager 从 node_modules 解析\r\n return super.loadFile(filename.slice(1), currentDirectory, options, environment);\r\n }\r\n }\r\n pluginManager.addFileManager(new TildeFileManager());\r\n },\r\n };\r\n}\r\n\r\n/**\r\n * 组装 `css.preprocessorOptions.less`:映射 webpack `less-loader` 选项,\r\n * `less.resolveTilde: true` 时注入 `~` 解析插件(默认关闭)。\r\n * @public\r\n */\r\nexport function buildLessOptions(less?: LessOptions) {\r\n const resolveTilde = less?.resolveTilde === true;\r\n return {\r\n ...(less?.additionalData != null ? { additionalData: less.additionalData } : {}),\r\n ...(less?.modifyVars != null ? { modifyVars: less.modifyVars } : {}),\r\n ...(less?.javascriptEnabled != null ? { javascriptEnabled: less.javascriptEnabled } : {}),\r\n ...(resolveTilde ? { plugins: [lessTildeResolutionPlugin()] } : {}),\r\n };\r\n}\r\n"]}
1
+ {"version":3,"sources":["../../src/plugins/less-tilde.ts"],"names":[],"mappings":";AAoCO,SAAS,yBAAA,GAA4B;AAC1C,EAAA,OAAO;AAAA,IACL,OAAA,CAAQ,MAAsB,aAAA,EAAkC;AAAA,MAC9D,MAAM,gBAAA,SAAyB,IAAA,CAAK,WAAA,CAAY;AAAA,QACrC,SAAS,QAAA,EAAkB;AAClC,UAAA,OAAO,QAAA,CAAS,WAAW,GAAG,CAAA;AAAA,QAChC;AAAA,QACS,QAAA,CACP,QAAA,EACA,gBAAA,EACA,OAAA,EACA,WAAA,EACA;AAEA,UAAA,OAAO,KAAA,CAAM,SAAS,QAAA,CAAS,KAAA,CAAM,CAAC,CAAA,EAAG,gBAAA,EAAkB,SAAS,WAAW,CAAA;AAAA,QACjF;AAAA;AAEF,MAAA,aAAA,CAAc,cAAA,CAAe,IAAI,gBAAA,EAAkB,CAAA;AAAA,IACrD;AAAA,GACF;AACF;AAOO,SAAS,iBAAiB,IAAA,EAAoB;AACnD,EAAA,MAAM,YAAA,GAAe,MAAM,YAAA,KAAiB,IAAA;AAC5C,EAAA,OAAO;AAAA,IACL,GAAI,MAAM,cAAA,IAAkB,IAAA,GAAO,EAAE,cAAA,EAAgB,IAAA,CAAK,cAAA,EAAe,GAAI,EAAC;AAAA,IAC9E,GAAI,MAAM,UAAA,IAAc,IAAA,GAAO,EAAE,UAAA,EAAY,IAAA,CAAK,UAAA,EAAW,GAAI,EAAC;AAAA,IAClE,GAAI,MAAM,iBAAA,IAAqB,IAAA,GAAO,EAAE,iBAAA,EAAmB,IAAA,CAAK,iBAAA,EAAkB,GAAI,EAAC;AAAA,IACvF,GAAI,eAAe,EAAE,OAAA,EAAS,CAAC,yBAAA,EAA2B,CAAA,EAAE,GAAI;AAAC,GACnE;AACF","file":"less-tilde.js","sourcesContent":["import type { LessOptions } from \"../types.ts\";\n\n/**\n * less 插件注入的 FileManager 实例表面类型。less@4 不自带 TS 类型,按实际使用的最小面声明。\n */\ninterface LessFileManagerInstance {\n supports(filename: string): boolean;\n loadFile(\n filename: string,\n currentDirectory: string,\n options: unknown,\n environment: unknown,\n ): unknown;\n}\n\n/** less.js 传给插件 `install` 的 less 环境实例表面。 */\ninterface LessPluginHost {\n FileManager: new (...args: unknown[]) => LessFileManagerInstance;\n}\n\n/** less.js 插件管理器表面。 */\ninterface LessPluginManager {\n addFileManager(manager: LessFileManagerInstance): void;\n}\n\n/**\n * Less 插件:把 webpack 遗留的 `~pkg/...` 导入解析到 node_modules。\n *\n * Less 自身不认识 `~`(那是 webpack `less-loader` 的约定:从 node_modules 解析);Vite 的\n * Less 预处理器也不处理 `~`,会回退到 Less 默认 FileManager 去查找 `node_modules/~pkg/...` 而失败。\n * 本插件注册一个 FileManager:仅当导入以 `~` 开头时拦截,去掉前缀后交给默认 FileManager 解析\n * (Vite 恒向 `less.render` 注入 `paths: [\"node_modules\"]`)。\n *\n * 第三方包(如 formily 系)自带 `@import \"~antd/...\"` 时尤其需要——消费方无法修改 node_modules。\n * @public\n */\nexport function lessTildeResolutionPlugin() {\n return {\n install(less: LessPluginHost, pluginManager: LessPluginManager) {\n class TildeFileManager extends less.FileManager {\n override supports(filename: string) {\n return filename.startsWith(\"~\");\n }\n override loadFile(\n filename: string,\n currentDirectory: string,\n options: unknown,\n environment: unknown,\n ) {\n // `~pkg/...` → `pkg/...`,让默认 FileManager 从 node_modules 解析\n return super.loadFile(filename.slice(1), currentDirectory, options, environment);\n }\n }\n pluginManager.addFileManager(new TildeFileManager());\n },\n };\n}\n\n/**\n * 组装 `css.preprocessorOptions.less`:映射 webpack `less-loader` 选项,\n * `less.resolveTilde: true` 时注入 `~` 解析插件(默认关闭)。\n * @public\n */\nexport function buildLessOptions(less?: LessOptions) {\n const resolveTilde = less?.resolveTilde === true;\n return {\n ...(less?.additionalData != null ? { additionalData: less.additionalData } : {}),\n ...(less?.modifyVars != null ? { modifyVars: less.modifyVars } : {}),\n ...(less?.javascriptEnabled != null ? { javascriptEnabled: less.javascriptEnabled } : {}),\n ...(resolveTilde ? { plugins: [lessTildeResolutionPlugin()] } : {}),\n };\n}\n"]}