@hzab/vite-config 0.0.2-alpha.0

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 (151) hide show
  1. package/CHANGELOG.md +83 -0
  2. package/LICENSE +21 -0
  3. package/README.md +173 -0
  4. package/dist/create-app-vite-config.cjs +426 -0
  5. package/dist/create-app-vite-config.cjs.map +1 -0
  6. package/dist/create-app-vite-config.d.cts +19 -0
  7. package/dist/create-app-vite-config.d.ts +19 -0
  8. package/dist/create-app-vite-config.js +415 -0
  9. package/dist/create-app-vite-config.js.map +1 -0
  10. package/dist/create-library-vite-config.cjs +281 -0
  11. package/dist/create-library-vite-config.cjs.map +1 -0
  12. package/dist/create-library-vite-config.d.cts +31 -0
  13. package/dist/create-library-vite-config.d.ts +31 -0
  14. package/dist/create-library-vite-config.js +268 -0
  15. package/dist/create-library-vite-config.js.map +1 -0
  16. package/dist/create-package-vitest-config.cjs +73 -0
  17. package/dist/create-package-vitest-config.cjs.map +1 -0
  18. package/dist/create-package-vitest-config.d.cts +12 -0
  19. package/dist/create-package-vitest-config.d.ts +12 -0
  20. package/dist/create-package-vitest-config.js +67 -0
  21. package/dist/create-package-vitest-config.js.map +1 -0
  22. package/dist/create-vite-config.cjs +438 -0
  23. package/dist/create-vite-config.cjs.map +1 -0
  24. package/dist/create-vite-config.d.cts +17 -0
  25. package/dist/create-vite-config.d.ts +17 -0
  26. package/dist/create-vite-config.js +427 -0
  27. package/dist/create-vite-config.js.map +1 -0
  28. package/dist/index.cjs +631 -0
  29. package/dist/index.cjs.map +1 -0
  30. package/dist/index.d.cts +16 -0
  31. package/dist/index.d.ts +16 -0
  32. package/dist/index.js +603 -0
  33. package/dist/index.js.map +1 -0
  34. package/dist/plugins/copy.cjs +16 -0
  35. package/dist/plugins/copy.cjs.map +1 -0
  36. package/dist/plugins/copy.d.cts +18 -0
  37. package/dist/plugins/copy.d.ts +18 -0
  38. package/dist/plugins/copy.js +13 -0
  39. package/dist/plugins/copy.js.map +1 -0
  40. package/dist/plugins/hash-deploy.cjs +128 -0
  41. package/dist/plugins/hash-deploy.cjs.map +1 -0
  42. package/dist/plugins/hash-deploy.d.cts +56 -0
  43. package/dist/plugins/hash-deploy.d.ts +56 -0
  44. package/dist/plugins/hash-deploy.js +119 -0
  45. package/dist/plugins/hash-deploy.js.map +1 -0
  46. package/dist/plugins/less-tilde.cjs +32 -0
  47. package/dist/plugins/less-tilde.cjs.map +1 -0
  48. package/dist/plugins/less-tilde.d.cts +36 -0
  49. package/dist/plugins/less-tilde.d.ts +36 -0
  50. package/dist/plugins/less-tilde.js +29 -0
  51. package/dist/plugins/less-tilde.js.map +1 -0
  52. package/dist/plugins/lifecycle.cjs +28 -0
  53. package/dist/plugins/lifecycle.cjs.map +1 -0
  54. package/dist/plugins/lifecycle.d.cts +20 -0
  55. package/dist/plugins/lifecycle.d.ts +20 -0
  56. package/dist/plugins/lifecycle.js +26 -0
  57. package/dist/plugins/lifecycle.js.map +1 -0
  58. package/dist/plugins/public-assets.cjs +28 -0
  59. package/dist/plugins/public-assets.cjs.map +1 -0
  60. package/dist/plugins/public-assets.d.cts +17 -0
  61. package/dist/plugins/public-assets.d.ts +17 -0
  62. package/dist/plugins/public-assets.js +26 -0
  63. package/dist/plugins/public-assets.js.map +1 -0
  64. package/dist/plugins/public-config.cjs +31 -0
  65. package/dist/plugins/public-config.cjs.map +1 -0
  66. package/dist/plugins/public-config.d.cts +20 -0
  67. package/dist/plugins/public-config.d.ts +20 -0
  68. package/dist/plugins/public-config.js +29 -0
  69. package/dist/plugins/public-config.js.map +1 -0
  70. package/dist/plugins/react-compiler.cjs +22 -0
  71. package/dist/plugins/react-compiler.cjs.map +1 -0
  72. package/dist/plugins/react-compiler.d.cts +24 -0
  73. package/dist/plugins/react-compiler.d.ts +24 -0
  74. package/dist/plugins/react-compiler.js +16 -0
  75. package/dist/plugins/react-compiler.js.map +1 -0
  76. package/dist/plugins/relative-base.cjs +19 -0
  77. package/dist/plugins/relative-base.cjs.map +1 -0
  78. package/dist/plugins/relative-base.d.cts +9 -0
  79. package/dist/plugins/relative-base.d.ts +9 -0
  80. package/dist/plugins/relative-base.js +17 -0
  81. package/dist/plugins/relative-base.js.map +1 -0
  82. package/dist/plugins/stub-less.cjs +26 -0
  83. package/dist/plugins/stub-less.cjs.map +1 -0
  84. package/dist/plugins/stub-less.d.cts +9 -0
  85. package/dist/plugins/stub-less.d.ts +9 -0
  86. package/dist/plugins/stub-less.js +24 -0
  87. package/dist/plugins/stub-less.js.map +1 -0
  88. package/dist/plugins/svg-react.cjs +70 -0
  89. package/dist/plugins/svg-react.cjs.map +1 -0
  90. package/dist/plugins/svg-react.d.cts +28 -0
  91. package/dist/plugins/svg-react.d.ts +28 -0
  92. package/dist/plugins/svg-react.js +63 -0
  93. package/dist/plugins/svg-react.js.map +1 -0
  94. package/dist/svg-react-runtime.cjs +4 -0
  95. package/dist/svg-react-runtime.cjs.map +1 -0
  96. package/dist/svg-react-runtime.d.cts +2 -0
  97. package/dist/svg-react-runtime.d.ts +2 -0
  98. package/dist/svg-react-runtime.js +3 -0
  99. package/dist/svg-react-runtime.js.map +1 -0
  100. package/dist/types.cjs +4 -0
  101. package/dist/types.cjs.map +1 -0
  102. package/dist/types.d.cts +291 -0
  103. package/dist/types.d.ts +291 -0
  104. package/dist/types.js +3 -0
  105. package/dist/types.js.map +1 -0
  106. package/dist/vitest-types.cjs +4 -0
  107. package/dist/vitest-types.cjs.map +1 -0
  108. package/dist/vitest-types.d.cts +22 -0
  109. package/dist/vitest-types.d.ts +22 -0
  110. package/dist/vitest-types.js +3 -0
  111. package/dist/vitest-types.js.map +1 -0
  112. package/dist/vitest.cjs +73 -0
  113. package/dist/vitest.cjs.map +1 -0
  114. package/dist/vitest.d.cts +4 -0
  115. package/dist/vitest.d.ts +4 -0
  116. package/dist/vitest.js +67 -0
  117. package/dist/vitest.js.map +1 -0
  118. package/docs/README.md +15 -0
  119. package/docs/abt-management-ui/vite.config.mts +226 -0
  120. package/docs/api.md +366 -0
  121. package/docs/migration-from-webpack.md +722 -0
  122. package/docs/sync-public-path.md +52 -0
  123. package/docs/vite-config-integration.md +476 -0
  124. package/docs/vite-pnpm-integration.md +279 -0
  125. package/docs/vs-ccc-vite-config.md +26 -0
  126. package/docs/webpack-gap.md +107 -0
  127. package/package.json +113 -0
  128. package/src/client.d.ts +9 -0
  129. package/src/create-app-vite-config.ts +218 -0
  130. package/src/create-library-vite-config.ts +199 -0
  131. package/src/create-package-vitest-config.ts +54 -0
  132. package/src/create-vite-config.ts +22 -0
  133. package/src/index.ts +42 -0
  134. package/src/plugins/copy.ts +25 -0
  135. package/src/plugins/hash-deploy.ts +199 -0
  136. package/src/plugins/less-tilde.ts +44 -0
  137. package/src/plugins/lifecycle.ts +40 -0
  138. package/src/plugins/public-assets.ts +28 -0
  139. package/src/plugins/public-config.ts +34 -0
  140. package/src/plugins/react-compiler.ts +32 -0
  141. package/src/plugins/relative-base.ts +20 -0
  142. package/src/plugins/stub-less.ts +26 -0
  143. package/src/plugins/svg-react.ts +90 -0
  144. package/src/svg-react-runtime.ts +4 -0
  145. package/src/svg-react.d.ts +6 -0
  146. package/src/types.ts +314 -0
  147. package/src/vitest-types.ts +20 -0
  148. package/src/vitest.ts +2 -0
  149. package/templates/src/vite-env.d.ts +2 -0
  150. package/templates/tsconfig.json +12 -0
  151. package/templates/vite.config.ts +26 -0
@@ -0,0 +1,199 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+
5
+ import type { PluginOption } from "vite";
6
+
7
+ /** @public */
8
+ export type HashTimestampFormat = "epoch" | "datetime";
9
+
10
+ /** @public */
11
+ export interface HashBuildDirNameOptions {
12
+ repositoryRoot?: string;
13
+ now?: number;
14
+ /** @defaultValue "epoch" */
15
+ timestampFormat?: HashTimestampFormat;
16
+ }
17
+
18
+ /** @public */
19
+ export interface HashCleanupOptions {
20
+ /**
21
+ * 每次成功构建后保留的 Hash 版本总数(含本次构建)。
22
+ * 必须为大于等于 1 的整数。
23
+ */
24
+ keep: number;
25
+ }
26
+
27
+ /** @public */
28
+ export interface HashDeployPluginOptions {
29
+ rootDir: string;
30
+ /** hash 子目录名,如 `{commit}-{timestamp}` 或 `{commit}-YYYY-MM-DD-HH-mm-ss` */
31
+ buildDirStr: string;
32
+ /**
33
+ * 与最终 Vite `base` 一致的资源前缀。
34
+ * - 默认相对:`./{buildDirStr}/`
35
+ * - 自定义 CDN:`https://cdn.example.com/app/{buildDirStr}/`
36
+ */
37
+ publicPath: string;
38
+ /** 旧 Hash 目录清理策略;未传时永久保留,由部署系统负责清理。 */
39
+ cleanup?: HashCleanupOptions;
40
+ }
41
+
42
+ const HASH_BUILD_DIR_PATTERN = /^(?:[0-9a-f]{12}|local)-(\d{4}-\d{2}-\d{2}-\d{2}-\d{2}-\d{2}|\d+)$/;
43
+
44
+ function parseHashTimestamp(value: string): number {
45
+ if (/^\d+$/.test(value)) return Number(value);
46
+ const [year = 0, month = 1, day = 1, hour = 0, minute = 0, second = 0] = value
47
+ .split("-")
48
+ .map(Number);
49
+ return new Date(year, month - 1, day, hour, minute, second).getTime();
50
+ }
51
+
52
+ function padTwo(value: number): string {
53
+ return String(value).padStart(2, "0");
54
+ }
55
+
56
+ function formatHashTimestamp(now: number, format: HashTimestampFormat = "epoch"): string {
57
+ if (format !== "datetime") return String(now);
58
+ const date = new Date(now);
59
+ return [
60
+ date.getFullYear(),
61
+ padTwo(date.getMonth() + 1),
62
+ padTwo(date.getDate()),
63
+ padTwo(date.getHours()),
64
+ padTwo(date.getMinutes()),
65
+ padTwo(date.getSeconds()),
66
+ ].join("-");
67
+ }
68
+
69
+ function cleanupOldHashBuilds(deployRoot: string, currentBuildDir: string, keep: number): void {
70
+ const oldBuilds = fs
71
+ .readdirSync(deployRoot, { withFileTypes: true })
72
+ .flatMap((entry) => {
73
+ if (!entry.isDirectory() || entry.name === currentBuildDir) return [];
74
+ const match = HASH_BUILD_DIR_PATTERN.exec(entry.name);
75
+ return match ? [{ name: entry.name, timestamp: parseHashTimestamp(match[1]!) }] : [];
76
+ })
77
+ .sort((left, right) => right.timestamp - left.timestamp || right.name.localeCompare(left.name));
78
+
79
+ for (const build of oldBuilds.slice(Math.max(keep - 1, 0))) {
80
+ fs.rmSync(path.join(deployRoot, build.name), { recursive: true });
81
+ }
82
+ }
83
+
84
+ /**
85
+ * 构建结束后写 `dist/hash.txt`,并把 hash 子目录中的 `index.html` 复制到 `dist/index.html`。
86
+ * 相对 `base` 时为根入口补上 hash 前缀;绝对 `base` 时产物已含完整 URL,原样复制即可。
87
+ * 配置 `cleanup.keep` 时,成功写入根入口后只保留指定数量的最新 Hash 版本。
88
+ * @public
89
+ */
90
+ export function hashDeployPlugin(options: HashDeployPluginOptions): PluginOption {
91
+ const { rootDir, buildDirStr, publicPath, cleanup } = options;
92
+ if (cleanup && (!Number.isInteger(cleanup.keep) || cleanup.keep < 1)) {
93
+ throw new Error("[hash-deploy] cleanup.keep must be an integer greater than or equal to 1.");
94
+ }
95
+ let outputDir: string | undefined;
96
+ let deployRoot: string | undefined;
97
+
98
+ return {
99
+ name: "hash-deploy",
100
+ enforce: "post",
101
+ configResolved(config) {
102
+ const resolvedRoot = path.resolve(config.root);
103
+ const expectedRoot = path.resolve(rootDir);
104
+ if (resolvedRoot !== expectedRoot) {
105
+ throw new Error(
106
+ `[hash-deploy] Vite root "${resolvedRoot}" must match rootDir "${expectedRoot}".`,
107
+ );
108
+ }
109
+ // 相对 hash:工厂 publicPath 为 `./{hash}/`,但 Vite base 必须保持 `./`。
110
+ const expectedBase = publicPath.startsWith("./") ? "./" : publicPath;
111
+ if (config.base !== expectedBase) {
112
+ throw new Error(
113
+ `[hash-deploy] Vite base "${config.base}" must remain "${expectedBase}" during hash deployment.`,
114
+ );
115
+ }
116
+
117
+ outputDir = path.resolve(resolvedRoot, config.build.outDir);
118
+ if (path.basename(outputDir) !== buildDirStr) {
119
+ throw new Error(
120
+ `[hash-deploy] build.outDir "${config.build.outDir}" must end with the hash directory "${buildDirStr}".`,
121
+ );
122
+ }
123
+ deployRoot = path.dirname(outputDir);
124
+ },
125
+ writeBundle: {
126
+ sequential: true,
127
+ order: "post",
128
+ handler() {
129
+ if (!outputDir || !deployRoot) {
130
+ throw new Error("[hash-deploy] Vite configuration was not resolved before writeBundle.");
131
+ }
132
+
133
+ const sourceIndex = path.join(outputDir, "index.html");
134
+ if (!fs.existsSync(sourceIndex)) {
135
+ throw new Error(`[hash-deploy] Generated entry was not found: "${sourceIndex}".`);
136
+ }
137
+
138
+ fs.mkdirSync(deployRoot, { recursive: true });
139
+ fs.writeFileSync(path.join(deployRoot, "hash.txt"), buildDirStr);
140
+ const sourceHtml = fs.readFileSync(sourceIndex, "utf8");
141
+ // 相对 base(`./{hash}/`)时子目录 HTML 中的资源均相对于自身,根入口需统一补 hash;
142
+ // 绝对 base 时 Vite 已写出完整 URL,直接复制。
143
+ const rootHtml = publicPath.startsWith("./")
144
+ ? sourceHtml.replace(/(\b(?:href|src)\s*=\s*["'])\.\//g, `$1${publicPath}`)
145
+ : sourceHtml;
146
+ fs.writeFileSync(path.join(deployRoot, "index.html"), rootHtml);
147
+ if (cleanup) {
148
+ cleanupOldHashBuilds(deployRoot, buildDirStr, cleanup.keep);
149
+ }
150
+ },
151
+ },
152
+ };
153
+ }
154
+
155
+ /**
156
+ * 从指定仓库读取短 commit hash;非 git 环境回退 `local`。
157
+ * @public
158
+ */
159
+ export function getCommitHash(repositoryRoot: string = process.cwd()): string {
160
+ try {
161
+ return execFileSync("git", ["rev-parse", "HEAD"], {
162
+ cwd: repositoryRoot,
163
+ encoding: "utf8",
164
+ stdio: ["ignore", "pipe", "ignore"],
165
+ })
166
+ .trim()
167
+ .slice(0, 12);
168
+ } catch {
169
+ return "local";
170
+ }
171
+ }
172
+
173
+ /**
174
+ * 生成 hash 构建目录名;保留仅传时间戳的旧调用方式。
175
+ * @public
176
+ */
177
+ export function createHashBuildDirName(now?: number): string;
178
+ /** @public */
179
+ export function createHashBuildDirName(repositoryRoot: string, now?: number): string;
180
+ /** @public */
181
+ export function createHashBuildDirName(options: HashBuildDirNameOptions): string;
182
+ export function createHashBuildDirName(
183
+ repositoryRootOrNowOrOptions?: string | number | HashBuildDirNameOptions,
184
+ now: number = Date.now(),
185
+ ): string {
186
+ if (repositoryRootOrNowOrOptions != null && typeof repositoryRootOrNowOrOptions === "object") {
187
+ const {
188
+ repositoryRoot,
189
+ now: optionsNow = Date.now(),
190
+ timestampFormat = "epoch",
191
+ } = repositoryRootOrNowOrOptions;
192
+ return `${getCommitHash(repositoryRoot)}-${formatHashTimestamp(optionsNow, timestampFormat)}`;
193
+ }
194
+ const repositoryRoot =
195
+ typeof repositoryRootOrNowOrOptions === "string" ? repositoryRootOrNowOrOptions : undefined;
196
+ const timestamp =
197
+ typeof repositoryRootOrNowOrOptions === "number" ? repositoryRootOrNowOrOptions : now;
198
+ return `${getCommitHash(repositoryRoot)}-${formatHashTimestamp(timestamp)}`;
199
+ }
@@ -0,0 +1,44 @@
1
+ import type { LessOptions } from "../types.ts";
2
+
3
+ /**
4
+ * Less 插件:把 webpack 遗留的 `~pkg/...` 导入解析到 node_modules。
5
+ *
6
+ * Less 自身不认识 `~`(那是 webpack `less-loader` 的约定:从 node_modules 解析);Vite 的
7
+ * Less 预处理器也不处理 `~`,会回退到 Less 默认 FileManager 去查找 `node_modules/~pkg/...` 而失败。
8
+ * 本插件注册一个 FileManager:仅当导入以 `~` 开头时拦截,去掉前缀后交给默认 FileManager 解析
9
+ * (Vite 恒向 `less.render` 注入 `paths: ["node_modules"]`)。
10
+ *
11
+ * 第三方包(如 formily 系)自带 `@import "~antd/..."` 时尤其需要——消费方无法修改 node_modules。
12
+ * @public
13
+ */
14
+ export function lessTildeResolutionPlugin() {
15
+ return {
16
+ install(less: any, pluginManager: any) {
17
+ class TildeFileManager extends less.FileManager {
18
+ supports(filename: string) {
19
+ return filename.startsWith("~");
20
+ }
21
+ loadFile(filename: string, currentDirectory: string, options: any, environment: any) {
22
+ // `~pkg/...` → `pkg/...`,让默认 FileManager 从 node_modules 解析
23
+ return super.loadFile(filename.slice(1), currentDirectory, options, environment);
24
+ }
25
+ }
26
+ pluginManager.addFileManager(new TildeFileManager());
27
+ },
28
+ };
29
+ }
30
+
31
+ /**
32
+ * 组装 `css.preprocessorOptions.less`:映射 webpack `less-loader` 选项,
33
+ * `less.resolveTilde: true` 时注入 `~` 解析插件(默认关闭)。
34
+ * @public
35
+ */
36
+ export function buildLessOptions(less?: LessOptions) {
37
+ const resolveTilde = less?.resolveTilde === true;
38
+ return {
39
+ ...(less?.additionalData != null ? { additionalData: less.additionalData } : {}),
40
+ ...(less?.modifyVars != null ? { modifyVars: less.modifyVars } : {}),
41
+ ...(less?.javascriptEnabled != null ? { javascriptEnabled: less.javascriptEnabled } : {}),
42
+ ...(resolveTilde ? { plugins: [lessTildeResolutionPlugin()] } : {}),
43
+ };
44
+ }
@@ -0,0 +1,40 @@
1
+ import type { PluginOption } from "vite";
2
+
3
+ /** @public */
4
+ export interface LifecyclePluginOptions {
5
+ /** 构建开始(Vite `buildStart`)。仅同步。 */
6
+ onRun?: () => void;
7
+ /**
8
+ * 产物写盘成功之后(`writeBundle` sequential post)。
9
+ * 工厂中排在 Hash 根入口写入之后;Hash 写入失败不会调用。
10
+ */
11
+ onDone?: () => void | Promise<void>;
12
+ }
13
+
14
+ /**
15
+ * 构建起止回调。`onDone` 使用 sequential post `writeBundle`,
16
+ * 以便排在 Hash 部署等其它 post 写入之后。
17
+ * @public
18
+ */
19
+ export function lifecyclePlugin(options: LifecyclePluginOptions = {}): PluginOption {
20
+ const { onRun, onDone } = options;
21
+
22
+ return {
23
+ name: "build-lifecycle",
24
+ enforce: "post",
25
+ buildStart() {
26
+ if (typeof onRun === "function") {
27
+ onRun();
28
+ }
29
+ },
30
+ writeBundle: {
31
+ sequential: true,
32
+ order: "post",
33
+ async handler() {
34
+ if (typeof onDone === "function") {
35
+ await onDone();
36
+ }
37
+ },
38
+ },
39
+ };
40
+ }
@@ -0,0 +1,28 @@
1
+ import { copyPlugin } from "./copy.ts";
2
+
3
+ import type { PluginOption } from "vite";
4
+
5
+ /** @public */
6
+ export interface PublicAssetsPluginOptions {
7
+ /**
8
+ * 源目录,相对项目 root。
9
+ * @defaultValue "public"
10
+ */
11
+ dir?: string;
12
+ }
13
+
14
+ /**
15
+ * 将 `public/**` 复制到产物 `public/` 子目录(保留目录结构),替代 Vite 默认 publicDir 根目录散落。
16
+ * @public
17
+ */
18
+ export function publicAssetsPlugin(options: PublicAssetsPluginOptions = {}): PluginOption {
19
+ const { dir = "public" } = options;
20
+ return copyPlugin({
21
+ targets: [
22
+ {
23
+ src: `${dir}/**/*`,
24
+ dest: ".",
25
+ },
26
+ ],
27
+ });
28
+ }
@@ -0,0 +1,34 @@
1
+ import { copyPlugin } from "./copy.ts";
2
+
3
+ import type { PluginOption } from "vite";
4
+
5
+ /** @public */
6
+ export interface PublicConfigPluginOptions {
7
+ /** 源 glob,如 `config/public-config/development/*` */
8
+ sourceGlob: string;
9
+ /**
10
+ * 产物目标目录(可由消费方覆盖)。
11
+ * @defaultValue "config"(站点根 `/config/`)
12
+ * @example "public/config"
13
+ */
14
+ dest?: string;
15
+ }
16
+
17
+ /**
18
+ * 将运行时 public-config 复制到产物目录(默认 `config/`,配合 `window.globalConfig`)。
19
+ * @public
20
+ */
21
+ export function publicConfigPlugin(options: PublicConfigPluginOptions): PluginOption {
22
+ const { sourceGlob, dest = "config" } = options;
23
+ return copyPlugin({
24
+ targets: [
25
+ {
26
+ src: sourceGlob,
27
+ dest,
28
+ rename: {
29
+ stripBase: true,
30
+ },
31
+ },
32
+ ],
33
+ });
34
+ }
@@ -0,0 +1,32 @@
1
+ import babel from "@rolldown/plugin-babel";
2
+ import { reactCompilerPreset } from "@vitejs/plugin-react";
3
+
4
+ import type { ReactCompilerMode } from "../types.ts";
5
+ import type { PluginOption } from "vite";
6
+
7
+ /** @public */
8
+ export interface ReactCompilerPluginOptions {
9
+ command: string;
10
+ /**
11
+ * - `true`:始终启用
12
+ * - `"build-only"`:仅 build,且 `REACT_COMPILER=false` 可关
13
+ * - `false`:关闭
14
+ */
15
+ mode?: ReactCompilerMode;
16
+ }
17
+
18
+ /**
19
+ * React Compiler(babel preset)。默认仅生产构建启用。
20
+ * @public
21
+ */
22
+ export function reactCompilerPlugin(options: ReactCompilerPluginOptions): PluginOption[] {
23
+ const { command, mode = "build-only" } = options;
24
+ if (mode === false) return [];
25
+
26
+ const isBuild = command === "build";
27
+ const enabled = mode === true ? true : isBuild && process.env.REACT_COMPILER !== "false";
28
+
29
+ if (!enabled) return [];
30
+
31
+ return [babel({ presets: [reactCompilerPreset()] })];
32
+ }
@@ -0,0 +1,20 @@
1
+ import type { PluginOption } from "vite";
2
+
3
+ /**
4
+ * hash 部署场景下,把资源路径与 `import.meta.env.BASE_URL` 改写成相对部署目录。
5
+ * @public
6
+ */
7
+ export function relativeBasePlugin(deployPath: string = "./"): PluginOption {
8
+ return {
9
+ name: "relative-base",
10
+ transformIndexHtml(html: string) {
11
+ return html
12
+ .replace(/href="\.?\/assets\//g, `href="${deployPath}assets/`)
13
+ .replace(/src="\.?\/assets\//g, `src="${deployPath}assets/`);
14
+ },
15
+ transform(code: string, id: string) {
16
+ if (id.includes("node_modules")) return null;
17
+ return code.replace(/import\.meta\.env\.BASE_URL/g, `'${deployPath}'`);
18
+ },
19
+ };
20
+ }
@@ -0,0 +1,26 @@
1
+ import type { Plugin } from "vite";
2
+
3
+ const cssModuleStub = `
4
+ const styles = new Proxy({}, {
5
+ get: (_target, property) => typeof property === "string" ? property : undefined,
6
+ });
7
+ export default styles;
8
+ `;
9
+
10
+ /**
11
+ * 单测不解析 less,并为 CSS Modules 提供 identity stub
12
+ * @public
13
+ */
14
+ export function stubLessPlugin(): Plugin {
15
+ return {
16
+ name: "stub-less",
17
+ load(id) {
18
+ if (/\.module\.less(?:$|\?)/.test(id)) {
19
+ return cssModuleStub;
20
+ }
21
+ if (/\.less(?:$|\?)/.test(id)) {
22
+ return "";
23
+ }
24
+ },
25
+ };
26
+ }
@@ -0,0 +1,90 @@
1
+ import fs from "node:fs/promises";
2
+ import { transformWithOxc } from "vite";
3
+
4
+ import type { Config } from "@svgr/core";
5
+ import type { Plugin } from "vite";
6
+
7
+ /**
8
+ * @see https://react-svgr.com/docs/options/
9
+ * @public
10
+ */
11
+ export interface SvgReactOptions {
12
+ /** @defaultValue true */
13
+ svgo?: boolean;
14
+ icon?: boolean;
15
+ [key: string]: unknown;
16
+ }
17
+
18
+ const SVG_EXT_RE = /\.svg$/i;
19
+
20
+ /**
21
+ * `*.svg?react`(及带其它 query 时的 `react` 段)。
22
+ * 不匹配 `?reactive` / webpack 的 `?svgEle`。
23
+ */
24
+ export function isSvgReactId(id: string): boolean {
25
+ const queryStart = id.indexOf("?");
26
+ if (queryStart === -1) return false;
27
+ const file = id.slice(0, queryStart);
28
+ if (!SVG_EXT_RE.test(file)) return false;
29
+ return /(?:^|&)react(?:&|$)/.test(id.slice(queryStart + 1));
30
+ }
31
+
32
+ function missingPeerError(pkg: string, cause: unknown): Error {
33
+ const error = new Error(
34
+ `[svg-react] "${pkg}" is required to transform *.svg?react. Install the optional peer or disable CreateAppViteConfigOptions.svgr.`,
35
+ );
36
+ error.cause = cause;
37
+ return error;
38
+ }
39
+
40
+ async function resolveSvgrConfig(options: SvgReactOptions): Promise<Config> {
41
+ const merged: Config = { svgo: true, ...options };
42
+ if (merged.plugins != null) return merged;
43
+
44
+ const plugins: NonNullable<Config["plugins"]> = [];
45
+ if (merged.svgo !== false) {
46
+ try {
47
+ plugins.push((await import("@svgr/plugin-svgo")).default);
48
+ } catch (cause) {
49
+ throw missingPeerError("@svgr/plugin-svgo", cause);
50
+ }
51
+ }
52
+ try {
53
+ plugins.push((await import("@svgr/plugin-jsx")).default);
54
+ } catch (cause) {
55
+ throw missingPeerError("@svgr/plugin-jsx", cause);
56
+ }
57
+ return { ...merged, plugins };
58
+ }
59
+
60
+ /**
61
+ * 将 `import Icon from "./icon.svg?react"` 编成 React 组件。
62
+ * 普通 `*.svg` 仍走 Vite 默认资源 URL,本插件不处理。
63
+ *
64
+ * `@svgr/core` / `@svgr/plugin-jsx`(以及默认开启 SVGO 时的 `@svgr/plugin-svgo`)
65
+ * 仅在真正命中 `?react` 时动态加载,根入口不会静态依赖它们。
66
+ * @public
67
+ */
68
+ export function svgReactPlugin(options: SvgReactOptions = {}): Plugin {
69
+ return {
70
+ name: "ccc:svg-react",
71
+ enforce: "pre",
72
+ async load(id) {
73
+ if (!isSvgReactId(id)) return;
74
+
75
+ const filePath = id.slice(0, id.indexOf("?"));
76
+ const svg = await fs.readFile(filePath, "utf8");
77
+ let transform;
78
+ try {
79
+ ({ transform } = await import("@svgr/core"));
80
+ } catch (cause) {
81
+ throw missingPeerError("@svgr/core", cause);
82
+ }
83
+ const componentCode = await transform(svg, await resolveSvgrConfig(options), { filePath });
84
+ const result = await transformWithOxc(componentCode, `${filePath}.jsx`, {
85
+ lang: "jsx",
86
+ });
87
+ return { code: result.code, map: result.map };
88
+ },
89
+ };
90
+ }
@@ -0,0 +1,4 @@
1
+ // Runtime counterpart of the `@hzab/vite-config/svg-react` types entry.
2
+ // The ambient types for `*.svg?react` live in `src/svg-react.d.ts`; this module
3
+ // intentionally exports nothing and exists only so the subpath resolves at runtime.
4
+ export {};
@@ -0,0 +1,6 @@
1
+ declare module "*.svg?react" {
2
+ import type { FunctionComponent, SVGProps } from "react";
3
+
4
+ const ReactComponent: FunctionComponent<SVGProps<SVGSVGElement>>;
5
+ export default ReactComponent;
6
+ }