only-dev 1.0.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.
package/README.md ADDED
@@ -0,0 +1,201 @@
1
+ # only-dev
2
+
3
+ [![npm version](https://img.shields.io/npm/v/only-dev.svg)](https://www.npmjs.com/package/only-dev)
4
+ [![npm downloads](https://img.shields.io/npm/dm/only-dev.svg)](https://www.npmjs.com/package/only-dev)
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
6
+ [![Node.js Version](https://img.shields.io/badge/node-%3E%3D14.0.0-brightgreen.svg)](https://nodejs.org/)
7
+
8
+ 开发环境专用文件替换插件,支持 Vite、Webpack 和 Rollup。允许你在开发时使用 `.only-dev` 目录中的文件替换原始文件,而无需修改源代码。
9
+
10
+ ## 功能特性
11
+
12
+ - ✅ 支持 Vite、Webpack 和 Rollup
13
+ - ✅ 热模块替换 (HMR) 支持
14
+ - ✅ 文件模式过滤(include/exclude)
15
+ - ✅ 自动监听文件变化
16
+ - ✅ 开发文件与原始文件映射管理
17
+
18
+ ## 安装
19
+
20
+ ```bash
21
+ npm install only-dev
22
+ # 或
23
+ yarn add only-dev
24
+ # 或
25
+ pnpm add only-dev
26
+ ```
27
+
28
+ ## 使用方法
29
+
30
+ ### Vite
31
+
32
+ 在 `vite.config.js` 中配置:
33
+
34
+ ```javascript
35
+ import { defineConfig } from 'vite';
36
+ import onlyDev from 'only-dev/vite';
37
+ // 或
38
+ // import onlyDev from 'only-dev';
39
+
40
+ export default defineConfig({
41
+ plugins: [
42
+ onlyDev({
43
+ enabled: true, // 是否启用插件,默认 true
44
+ devDir: '.only-dev', // 开发文件目录,默认 '.only-dev'
45
+ debug: false, // 是否开启调试日志,默认 false
46
+ exclude: [], // 排除的文件模式,默认 []
47
+ include: ['**/*'] // 包含的文件模式,默认 ['**/*']
48
+ }),
49
+ // ... 其他插件
50
+ ]
51
+ });
52
+ ```
53
+
54
+ ### Webpack
55
+
56
+ 在 `webpack.config.js` 中配置:
57
+
58
+ ```javascript
59
+ const OnlyDevWebpackPlugin = require('only-dev/webpack');
60
+
61
+ module.exports = {
62
+ plugins: [
63
+ new OnlyDevWebpackPlugin({
64
+ enabled: true, // 是否启用插件,默认 true
65
+ devDir: '.only-dev', // 开发文件目录,默认 '.only-dev'
66
+ debug: false, // 是否开启调试日志,默认 false
67
+ exclude: [], // 排除的文件模式,默认 []
68
+ include: ['**/*'] // 包含的文件模式,默认 ['**/*']
69
+ }),
70
+ // ... 其他插件
71
+ ]
72
+ };
73
+ ```
74
+
75
+ ### Rollup
76
+
77
+ 在 `rollup.config.js` 中配置:
78
+
79
+ ```javascript
80
+ import onlyDev from 'only-dev/rollup';
81
+ // 或
82
+ // import { rollup as onlyDev } from 'only-dev';
83
+
84
+ export default {
85
+ plugins: [
86
+ onlyDev({
87
+ enabled: true, // 是否启用插件,默认 true
88
+ devDir: '.only-dev', // 开发文件目录,默认 '.only-dev'
89
+ debug: false, // 是否开启调试日志,默认 false
90
+ exclude: [], // 排除的文件模式,默认 []
91
+ include: ['**/*'] // 包含的文件模式,默认 ['**/*']
92
+ }),
93
+ // ... 其他插件
94
+ ]
95
+ };
96
+ ```
97
+
98
+ ## 工作原理
99
+
100
+ 1. 插件会在项目根目录下查找 `.only-dev` 目录
101
+ 2. 当导入文件时,插件会检查是否存在对应的开发文件
102
+ 3. 如果存在,则使用开发文件替换原始文件
103
+ 4. 开发文件的变化会触发热模块替换 (HMR)
104
+
105
+ ## 目录结构示例
106
+
107
+ ```
108
+ project-root/
109
+ ├── src/
110
+ │ └── components/
111
+ │ └── Button.vue
112
+ ├── .only-dev/ # 开发文件目录
113
+ │ └── src/
114
+ │ └── components/
115
+ │ └── Button.vue # 这个文件会替换 src/components/Button.vue
116
+ └── vite.config.js
117
+ ```
118
+
119
+ ## 配置选项
120
+
121
+ ### enabled
122
+
123
+ - 类型: `boolean`
124
+ - 默认值: `true`
125
+ - 说明: 是否启用插件
126
+
127
+ ### devDir
128
+
129
+ - 类型: `string`
130
+ - 默认值: `'.only-dev'`
131
+ - 说明: 开发文件目录,相对于项目根目录
132
+
133
+ ### debug
134
+
135
+ - 类型: `boolean`
136
+ - 默认值: `false`
137
+ - 说明: 是否开启调试日志,开启后会输出详细的文件替换信息
138
+
139
+ ### exclude
140
+
141
+ - 类型: `string[]`
142
+ - 默认值: `[]`
143
+ - 说明: 排除的文件模式(使用 glob patterns),匹配的文件不会被替换
144
+
145
+ 示例:
146
+ ```javascript
147
+ exclude: ['**/*.test.js', '**/node_modules/**']
148
+ ```
149
+
150
+ ### include
151
+
152
+ - 类型: `string[]`
153
+ - 默认值: `['**/*']`
154
+ - 说明: 包含的文件模式(使用 glob patterns),只有匹配的文件才会被替换
155
+
156
+ 示例:
157
+ ```javascript
158
+ include: ['src/**/*.vue', 'src/**/*.js']
159
+ ```
160
+
161
+ ## 使用场景
162
+
163
+ - 开发时临时修改组件或工具函数,而不影响源代码
164
+ - 调试特定功能时替换部分文件
165
+ - 快速测试不同的实现方案
166
+ - 在开发环境中使用 mock 数据
167
+
168
+ ## 注意事项
169
+
170
+ 1. `.only-dev` 目录中的文件结构应该与原始文件结构保持一致
171
+ 2. 开发文件的变化会触发热更新,但可能需要手动刷新浏览器
172
+ 3. 构建生产版本时,插件会自动忽略 `.only-dev` 目录中的文件
173
+ 4. 建议将 `.only-dev` 目录添加到 `.gitignore` 中
174
+
175
+ ## API (Vite)
176
+
177
+ 插件还提供了一些额外的 API(仅 Vite 版本):
178
+
179
+ ```javascript
180
+ import onlyDev from 'only-dev/vite';
181
+
182
+ const plugin = onlyDev({ debug: true });
183
+
184
+ // 在插件配置后访问 API
185
+ plugin.api.getDevFileMap(); // 获取开发文件映射表
186
+ plugin.api.getOriginalFiles(); // 获取原始文件内容缓存
187
+ plugin.api.restoreOriginalFile(path); // 恢复原始文件
188
+ ```
189
+
190
+ ## 贡献
191
+
192
+ 欢迎提交 Issue 和 Pull Request!
193
+
194
+ ## 许可证
195
+
196
+ MIT
197
+
198
+ ## 相关链接
199
+
200
+ - [GitHub 仓库](https://github.com/xujiehui/only-dev)
201
+ - [问题反馈](https://github.com/xujiehui/only-dev/issues)
package/index.js ADDED
@@ -0,0 +1,7 @@
1
+ // index.js
2
+ // 主入口文件,默认导出 Vite 插件
3
+
4
+ export { default } from './vite.js';
5
+ export { default as vite } from './vite.js';
6
+ export { default as rollup } from './rollup.js';
7
+
package/package.json ADDED
@@ -0,0 +1,55 @@
1
+ {
2
+ "name": "only-dev",
3
+ "version": "1.0.0",
4
+ "description": "开发环境专用文件替换插件,支持 Vite、Webpack 和 Rollup",
5
+ "type": "module",
6
+ "main": "index.js",
7
+ "types": "types.d.ts",
8
+ "exports": {
9
+ ".": "./index.js",
10
+ "./vite": "./vite.js",
11
+ "./webpack": "./webpack.cjs",
12
+ "./rollup": "./rollup.js"
13
+ },
14
+ "keywords": [
15
+ "vite",
16
+ "webpack",
17
+ "rollup",
18
+ "plugin",
19
+ "dev",
20
+ "development",
21
+ "hot-reload",
22
+ "hmr"
23
+ ],
24
+ "author": "xujiehui",
25
+ "license": "MIT",
26
+ "repository": {
27
+ "type": "git",
28
+ "url": "https://github.com/xujiehui/only-dev.git"
29
+ },
30
+ "homepage": "https://github.com/xujiehui/only-dev#readme",
31
+ "bugs": {
32
+ "url": "https://github.com/xujiehui/only-dev/issues"
33
+ },
34
+ "engines": {
35
+ "node": ">=14.0.0"
36
+ },
37
+ "peerDependencies": {
38
+ "vite": "^3.0.0 || ^4.0.0 || ^5.0.0",
39
+ "webpack": "^5.0.0",
40
+ "rollup": "^3.0.0 || ^4.0.0"
41
+ },
42
+ "dependencies": {
43
+ "minimatch": "^10.0.0"
44
+ },
45
+ "devDependencies": {},
46
+ "files": [
47
+ "index.js",
48
+ "vite.js",
49
+ "webpack.cjs",
50
+ "rollup.js",
51
+ "types.d.ts",
52
+ "README.md"
53
+ ]
54
+ }
55
+
package/rollup.js ADDED
@@ -0,0 +1,239 @@
1
+ // rollup.js
2
+ import fs from 'fs';
3
+ import path from 'path';
4
+ import minimatch from 'minimatch';
5
+
6
+ export default function onlyDevRollupPlugin(options = {}) {
7
+ const {
8
+ enabled = true,
9
+ devDir = '.only-dev',
10
+ debug = false,
11
+ exclude = [],
12
+ include = ['**/*']
13
+ } = options;
14
+
15
+ let root = '';
16
+ const fileMap = new Map();
17
+ const originalFiles = new Map();
18
+ const devPathToOriginalPath = new Map();
19
+
20
+ return {
21
+ name: 'only-dev',
22
+
23
+ buildStart(opts) {
24
+ root = opts.input ? path.dirname(Array.isArray(opts.input) ? opts.input[0] : opts.input) : process.cwd();
25
+
26
+ if (debug) {
27
+ console.log(`[only-dev] Rollup 插件已加载,根目录: ${root}`);
28
+ }
29
+ },
30
+
31
+ resolveId(source, importer) {
32
+ if (!enabled) return null;
33
+
34
+ try {
35
+ // 解析导入路径
36
+ let resolvedPath;
37
+
38
+ if (path.isAbsolute(source)) {
39
+ resolvedPath = source;
40
+ } else if (importer) {
41
+ resolvedPath = path.resolve(path.dirname(importer), source);
42
+ } else {
43
+ resolvedPath = path.resolve(root, source);
44
+ }
45
+
46
+ // 规范化路径
47
+ resolvedPath = path.resolve(resolvedPath);
48
+
49
+ // 检查是否有对应的开发文件
50
+ const devPath = getDevPath(resolvedPath);
51
+ if (devPath && fs.existsSync(devPath)) {
52
+ const normalizedResolved = normalizePath(resolvedPath);
53
+ const normalizedDevPath = normalizePath(devPath);
54
+
55
+ if (debug) {
56
+ console.log(`[only-dev] 解析: ${path.relative(root, resolvedPath)} -> ${path.relative(root, devPath)}`);
57
+ }
58
+
59
+ fileMap.set(normalizedResolved, normalizedDevPath);
60
+ devPathToOriginalPath.set(normalizedDevPath, normalizedResolved);
61
+
62
+ return devPath;
63
+ }
64
+
65
+ return null;
66
+ } catch (err) {
67
+ if (debug) {
68
+ console.error('[only-dev] 解析错误:', err);
69
+ }
70
+ return null;
71
+ }
72
+ },
73
+
74
+ load(id) {
75
+ if (!enabled) return null;
76
+
77
+ const normalizedId = normalizePath(id);
78
+
79
+ // 如果已经在 .only-dev 目录中,直接读取
80
+ if (normalizedId.includes(`/${devDir}/`)) {
81
+ try {
82
+ if (!fs.existsSync(id)) {
83
+ if (debug) {
84
+ console.warn(`[only-dev] load: 开发文件不存在: ${path.relative(root || process.cwd(), id)}`);
85
+ }
86
+ return null;
87
+ }
88
+
89
+ const originalPath = getOriginalPath(normalizedId);
90
+ const normalizedOriginalPath = normalizePath(originalPath);
91
+
92
+ // 读取最新的文件内容
93
+ const content = fs.readFileSync(id, 'utf-8');
94
+
95
+ // 缓存原始文件内容(仅在首次加载时)
96
+ if (!originalFiles.has(normalizedOriginalPath) && fs.existsSync(originalPath)) {
97
+ originalFiles.set(normalizedOriginalPath, fs.readFileSync(originalPath, 'utf-8'));
98
+ }
99
+
100
+ // 更新映射关系
101
+ fileMap.set(normalizedOriginalPath, normalizedId);
102
+ devPathToOriginalPath.set(normalizedId, normalizedOriginalPath);
103
+
104
+ if (debug) {
105
+ console.log(`[only-dev] load: 加载开发文件: ${path.relative(root || process.cwd(), id)}`);
106
+ }
107
+
108
+ return content;
109
+ } catch (err) {
110
+ if (debug) {
111
+ console.error('[only-dev] load: 读取文件错误:', err);
112
+ }
113
+ return null;
114
+ }
115
+ }
116
+
117
+ // 检查是否有对应的 .only-dev 文件
118
+ const devPath = getDevPath(id);
119
+ if (devPath && fs.existsSync(devPath)) {
120
+ try {
121
+ const normalizedDevPath = normalizePath(devPath);
122
+ const normalizedOriginalId = normalizePath(id);
123
+
124
+ // 读取最新的文件内容
125
+ const content = fs.readFileSync(devPath, 'utf-8');
126
+
127
+ // 更新映射关系
128
+ fileMap.set(normalizedOriginalId, normalizedDevPath);
129
+ devPathToOriginalPath.set(normalizedDevPath, normalizedOriginalId);
130
+
131
+ // 缓存原始文件内容(仅在首次加载时)
132
+ if (!originalFiles.has(normalizedOriginalId) && fs.existsSync(id)) {
133
+ originalFiles.set(normalizedOriginalId, fs.readFileSync(id, 'utf-8'));
134
+ }
135
+
136
+ if (debug) {
137
+ console.log(`[only-dev] load: 使用开发文件 (原始: ${path.relative(root || process.cwd(), id)}, 开发: ${path.relative(root || process.cwd(), devPath)})`);
138
+ }
139
+
140
+ return content;
141
+ } catch (err) {
142
+ if (debug) {
143
+ console.error('[only-dev] load: 读取开发文件错误:', err);
144
+ }
145
+ return null;
146
+ }
147
+ }
148
+
149
+ return null;
150
+ },
151
+
152
+ watchChange(id) {
153
+ if (!enabled) return;
154
+
155
+ const normalizedFile = normalizePath(id);
156
+ const devDirNormalized = normalizePath(path.join(root || process.cwd(), devDir));
157
+
158
+ // 检查是否是 .only-dev 目录下的文件
159
+ if (normalizedFile.startsWith(devDirNormalized + '/') || normalizedFile.includes(`/${devDir}/`)) {
160
+ const originalPath = getOriginalPath(normalizedFile);
161
+ const normalizedOriginalPath = normalizePath(originalPath);
162
+
163
+ if (debug) {
164
+ console.log(`[only-dev] 开发文件变更: ${path.relative(root || process.cwd(), id)}`);
165
+ console.log(`[only-dev] 原始路径: ${path.relative(root || process.cwd(), originalPath)}`);
166
+ }
167
+
168
+ // 更新映射关系
169
+ devPathToOriginalPath.set(normalizedFile, normalizedOriginalPath);
170
+ fileMap.set(normalizedOriginalPath, normalizedFile);
171
+ }
172
+ },
173
+
174
+ buildEnd() {
175
+ if (debug && enabled) {
176
+ console.log(`[only-dev] 构建完成,共使用 ${fileMap.size} 个开发文件`);
177
+ }
178
+ }
179
+ };
180
+
181
+ function getDevPath(originalPath) {
182
+ const currentRoot = root || process.cwd();
183
+ if (!originalPath.startsWith(currentRoot)) return null;
184
+
185
+ const relativePath = path.relative(currentRoot, originalPath);
186
+ const devPath = path.join(currentRoot, devDir, relativePath);
187
+
188
+ // 检查是否在排除列表中
189
+ if (exclude.some(pattern => minimatch(relativePath, pattern))) {
190
+ return null;
191
+ }
192
+
193
+ // 检查是否在包含列表中
194
+ if (include.length > 0 && !include.some(pattern => minimatch(relativePath, pattern))) {
195
+ return null;
196
+ }
197
+
198
+ return devPath;
199
+ }
200
+
201
+ function getOriginalPath(devPath) {
202
+ const normalizedDevPath = normalizePath(devPath);
203
+ const currentRoot = root || process.cwd();
204
+ const devDirNormalized = normalizePath(path.join(currentRoot, devDir));
205
+
206
+ // 如果不在 .only-dev 目录中,直接返回
207
+ if (!normalizedDevPath.startsWith(devDirNormalized + '/') && !normalizedDevPath.includes(`/${devDir}/`)) {
208
+ return devPath;
209
+ }
210
+
211
+ // 从映射中查找(优先使用缓存)
212
+ if (devPathToOriginalPath.has(normalizedDevPath)) {
213
+ return devPathToOriginalPath.get(normalizedDevPath);
214
+ }
215
+
216
+ // 通过路径计算原始路径
217
+ let relativeFromDev;
218
+ const devDirIndex = normalizedDevPath.indexOf(`/${devDir}/`);
219
+ if (devDirIndex !== -1) {
220
+ relativeFromDev = normalizedDevPath.substring(devDirIndex + devDir.length + 2);
221
+ } else if (normalizedDevPath.startsWith(devDirNormalized + '/')) {
222
+ relativeFromDev = normalizedDevPath.substring(devDirNormalized.length + 1);
223
+ } else {
224
+ return devPath;
225
+ }
226
+
227
+ const originalPath = normalizePath(path.join(currentRoot, relativeFromDev));
228
+
229
+ // 更新映射关系
230
+ devPathToOriginalPath.set(normalizedDevPath, originalPath);
231
+
232
+ return originalPath;
233
+ }
234
+
235
+ function normalizePath(filePath) {
236
+ return filePath.replace(/\\/g, '/');
237
+ }
238
+ }
239
+
package/types.d.ts ADDED
@@ -0,0 +1,84 @@
1
+ // types.d.ts
2
+ import { Plugin } from 'vite';
3
+
4
+ export interface OnlyDevOptions {
5
+ /**
6
+ * 是否启用插件
7
+ * @default true
8
+ */
9
+ enabled?: boolean;
10
+
11
+ /**
12
+ * 开发文件目录
13
+ * @default '.only-dev'
14
+ */
15
+ devDir?: string;
16
+
17
+ /**
18
+ * 是否开启调试日志
19
+ * @default false
20
+ */
21
+ debug?: boolean;
22
+
23
+ /**
24
+ * 排除的文件模式(glob patterns)
25
+ * @default []
26
+ */
27
+ exclude?: string[];
28
+
29
+ /**
30
+ * 包含的文件模式(glob patterns)
31
+ * @default ['**/*']
32
+ */
33
+ include?: string[];
34
+ }
35
+
36
+ export interface OnlyDevApi {
37
+ /**
38
+ * 获取开发文件映射表
39
+ */
40
+ getDevFileMap: () => Map<string, string>;
41
+
42
+ /**
43
+ * 获取原始文件内容缓存
44
+ */
45
+ getOriginalFiles: () => Map<string, string>;
46
+
47
+ /**
48
+ * 恢复原始文件
49
+ * @param filePath 文件路径
50
+ * @returns 是否成功恢复
51
+ */
52
+ restoreOriginalFile: (filePath: string) => boolean;
53
+ }
54
+
55
+ /**
56
+ * Vite 插件
57
+ */
58
+ export declare function onlyDevVitePlugin(options?: OnlyDevOptions): Plugin & {
59
+ api: OnlyDevApi;
60
+ };
61
+
62
+ /**
63
+ * Webpack 插件类
64
+ */
65
+ export declare class OnlyDevWebpackPlugin {
66
+ constructor(options?: OnlyDevOptions);
67
+ apply(compiler: any): void;
68
+ }
69
+
70
+ /**
71
+ * Rollup 插件
72
+ */
73
+ export declare function onlyDevRollupPlugin(options?: OnlyDevOptions): {
74
+ name: string;
75
+ buildStart: (opts: any) => void;
76
+ resolveId: (source: string, importer?: string) => string | null;
77
+ load: (id: string) => string | null;
78
+ watchChange: (id: string) => void;
79
+ buildEnd: () => void;
80
+ };
81
+
82
+ declare const _default: typeof onlyDevVitePlugin;
83
+ export default _default;
84
+
package/vite.js ADDED
@@ -0,0 +1,381 @@
1
+ // vite.js
2
+ import fs from 'fs';
3
+ import path from 'path';
4
+ import { normalizePath } from 'vite';
5
+ import minimatch from 'minimatch';
6
+
7
+ export default function onlyDevPlugin(options = {}) {
8
+ const {
9
+ enabled = true,
10
+ devDir = '.only-dev',
11
+ debug = false,
12
+ exclude = [],
13
+ include = ['**/*']
14
+ } = options;
15
+
16
+ let root = '';
17
+ let config;
18
+ const fileMap = new Map();
19
+ const originalFiles = new Map();
20
+ const devPathToOriginalPath = new Map();
21
+
22
+ return {
23
+ name: 'only-dev',
24
+ enforce: 'pre',
25
+
26
+ configResolved(resolvedConfig) {
27
+ root = resolvedConfig.root;
28
+ config = resolvedConfig;
29
+
30
+ if (debug) {
31
+ console.log(`[only-dev] 插件已加载,根目录: ${root}`);
32
+ }
33
+ },
34
+
35
+ configureServer(server) {
36
+ if (!enabled) return;
37
+
38
+ // 监听 .only-dev 目录变化
39
+ const devDirPath = path.join(root, devDir);
40
+
41
+ // 检查目录是否存在
42
+ if (!fs.existsSync(devDirPath)) {
43
+ if (debug) {
44
+ console.log(`[only-dev] 开发目录不存在: ${devDirPath}`);
45
+ }
46
+ return;
47
+ }
48
+
49
+ // 使用 Vite 的 watcher 来监听文件变化
50
+ const watcher = server.watcher;
51
+
52
+ // 添加 .only-dev 目录到监听列表
53
+ watcher.add(devDirPath);
54
+
55
+ // 注意:我们不在 watcher.on('change') 中处理,而是使用 handleHotUpdate hook
56
+ // 这样可以确保 Vite 正确处理文件变化
57
+ },
58
+
59
+ handleHotUpdate(ctx) {
60
+ if (!enabled) return;
61
+
62
+ const { file, server } = ctx;
63
+ const normalizedFile = normalizePath(file);
64
+
65
+ // 检查是否是 .only-dev 目录下的文件(使用更精确的路径匹配)
66
+ const devDirNormalized = normalizePath(path.join(root, devDir));
67
+ if (!normalizedFile.startsWith(devDirNormalized + '/') && !normalizedFile.includes(`/${devDir}/`)) {
68
+ return;
69
+ }
70
+
71
+ const originalPath = getOriginalPath(normalizedFile);
72
+ const normalizedOriginalPath = normalizePath(originalPath);
73
+
74
+ if (debug) {
75
+ console.log(`[only-dev] handleHotUpdate: 开发文件变更: ${path.relative(root, file)}`);
76
+ console.log(`[only-dev] handleHotUpdate: 原始路径: ${path.relative(root, originalPath)}`);
77
+ }
78
+
79
+ // 更新 devPathToOriginalPath 映射
80
+ devPathToOriginalPath.set(normalizedFile, normalizedOriginalPath);
81
+
82
+ // 更新 fileMap 映射
83
+ fileMap.set(normalizedOriginalPath, normalizedFile);
84
+
85
+ // 查找对应的模块 - 优先通过 dev 路径查找(因为 resolveId 已经将 ID 改成了 dev 路径)
86
+ let module = server.moduleGraph.getModuleById(normalizedFile);
87
+
88
+ // 如果找不到,尝试通过原始路径查找
89
+ if (!module) {
90
+ module = server.moduleGraph.getModuleById(normalizedOriginalPath);
91
+ }
92
+
93
+ // 如果还是找不到,尝试通过 fileMap 查找所有可能的模块
94
+ if (!module) {
95
+ for (const [id, devPath] of fileMap.entries()) {
96
+ const normalizedDevPath = normalizePath(devPath);
97
+ if (normalizedDevPath === normalizedFile || normalizePath(id) === normalizedOriginalPath) {
98
+ // 尝试通过原始 ID 查找
99
+ module = server.moduleGraph.getModuleById(id);
100
+ if (module) break;
101
+ // 尝试通过 dev 路径查找
102
+ module = server.moduleGraph.getModuleById(normalizedDevPath);
103
+ if (module) break;
104
+ }
105
+ }
106
+ }
107
+
108
+ // 如果还是找不到,尝试通过 URL 查找
109
+ if (!module) {
110
+ const relativePath = path.relative(root, originalPath).replace(/\\/g, '/');
111
+ const url = relativePath.startsWith('/') ? relativePath : `/${relativePath}`;
112
+ module = server.moduleGraph.urlToModuleMap.get(url);
113
+ }
114
+
115
+ // 如果还是找不到,尝试遍历所有模块查找匹配的
116
+ if (!module) {
117
+ for (const mod of server.moduleGraph.idToModuleMap.values()) {
118
+ const modId = normalizePath(mod.id);
119
+ if (modId === normalizedOriginalPath ||
120
+ modId === normalizedFile ||
121
+ modId.includes(`/${devDir}/`) && getOriginalPath(modId) === normalizedOriginalPath) {
122
+ module = mod;
123
+ break;
124
+ }
125
+ }
126
+ }
127
+
128
+ if (module) {
129
+ if (debug) {
130
+ console.log(`[only-dev] handleHotUpdate: 找到模块: ${module.id}`);
131
+ console.log(`[only-dev] handleHotUpdate: 模块 URL: ${module.url}`);
132
+ }
133
+
134
+ // 确保映射关系是最新的
135
+ const devPath = getDevPath(normalizedOriginalPath);
136
+ if (devPath && fs.existsSync(devPath)) {
137
+ const normalizedDevPath = normalizePath(devPath);
138
+
139
+ // 更新映射关系
140
+ fileMap.set(normalizedOriginalPath, normalizedDevPath);
141
+ devPathToOriginalPath.set(normalizedDevPath, normalizedOriginalPath);
142
+
143
+ if (debug) {
144
+ console.log(`[only-dev] handleHotUpdate: 更新映射: ${normalizedOriginalPath} -> ${normalizedDevPath}`);
145
+ }
146
+ }
147
+
148
+ // 使模块失效,触发重新加载
149
+ // 传入空的 Set 表示不递归失效依赖,让 Vite 的 HMR 机制自动处理
150
+ server.moduleGraph.invalidateModule(module, new Set());
151
+
152
+ // 清除模块的转换缓存,确保重新加载时使用最新内容
153
+ if (module.transformResult) {
154
+ module.transformResult = null;
155
+ }
156
+
157
+ if (debug) {
158
+ console.log(`[only-dev] handleHotUpdate: 已使模块失效: ${module.id}`);
159
+ }
160
+
161
+ // 返回模块列表,让 Vite 处理 HMR
162
+ // Vite 会自动向上查找 HMR 边界(如 Vue 组件),只更新必要的部分
163
+ return [module];
164
+ } else {
165
+ if (debug) {
166
+ console.log(`[only-dev] handleHotUpdate: 未找到模块`);
167
+ console.log(`[only-dev] handleHotUpdate: 尝试查找的路径: ${normalizedOriginalPath}, ${normalizedFile}`);
168
+ console.log(`[only-dev] handleHotUpdate: 模块图大小: ${server.moduleGraph.idToModuleMap.size}`);
169
+ }
170
+ // 返回 undefined 会触发全量刷新
171
+ return undefined;
172
+ }
173
+ },
174
+
175
+ async resolveId(source, importer, options) {
176
+ if (!enabled) return null;
177
+
178
+ try {
179
+ const resolved = await this.resolve(source, importer, {
180
+ skipSelf: true,
181
+ ...options
182
+ });
183
+
184
+ if (!resolved) return null;
185
+
186
+ const devPath = getDevPath(resolved.id);
187
+ if (devPath && fs.existsSync(devPath)) {
188
+ const normalizedResolved = normalizePath(resolved.id);
189
+ const normalizedDevPath = normalizePath(devPath);
190
+
191
+ if (debug) {
192
+ console.log(`[only-dev] 解析: ${path.relative(root, resolved.id)} -> ${path.relative(root, devPath)}`);
193
+ }
194
+
195
+ fileMap.set(normalizedResolved, normalizedDevPath);
196
+ devPathToOriginalPath.set(normalizedDevPath, normalizedResolved);
197
+
198
+ return normalizedDevPath;
199
+ }
200
+
201
+ return null;
202
+ } catch (err) {
203
+ if (debug) {
204
+ console.error('[only-dev] 解析错误:', err);
205
+ }
206
+ return null;
207
+ }
208
+ },
209
+
210
+ async load(id) {
211
+ if (!enabled) return null;
212
+
213
+ const normalizedId = normalizePath(id);
214
+
215
+ // 如果已经在 .only-dev 目录中,直接读取(热更新时会走这里)
216
+ if (normalizedId.includes(`/${devDir}/`)) {
217
+ try {
218
+ // 检查文件是否存在
219
+ if (!fs.existsSync(id)) {
220
+ if (debug) {
221
+ console.warn(`[only-dev] load: 开发文件不存在: ${path.relative(root, id)}`);
222
+ }
223
+ return null;
224
+ }
225
+
226
+ const originalPath = getOriginalPath(normalizedId);
227
+ const normalizedOriginalPath = normalizePath(originalPath);
228
+
229
+ // 读取最新的文件内容(不缓存,确保热更新时获取最新内容)
230
+ const content = fs.readFileSync(id, 'utf-8');
231
+
232
+ // 缓存原始文件内容(仅在首次加载时)
233
+ if (!originalFiles.has(normalizedOriginalPath) && fs.existsSync(originalPath)) {
234
+ originalFiles.set(normalizedOriginalPath, fs.readFileSync(originalPath, 'utf-8'));
235
+ }
236
+
237
+ // 更新映射关系
238
+ fileMap.set(normalizedOriginalPath, normalizedId);
239
+ devPathToOriginalPath.set(normalizedId, normalizedOriginalPath);
240
+
241
+ if (debug) {
242
+ console.log(`[only-dev] load: 加载开发文件: ${path.relative(root, id)}`);
243
+ }
244
+
245
+ return content;
246
+ } catch (err) {
247
+ if (debug) {
248
+ console.error('[only-dev] load: 读取文件错误:', err);
249
+ }
250
+ return null;
251
+ }
252
+ }
253
+
254
+ // 检查是否有对应的 .only-dev 文件
255
+ const devPath = getDevPath(id);
256
+ if (devPath && fs.existsSync(devPath)) {
257
+ try {
258
+ const normalizedDevPath = normalizePath(devPath);
259
+ const normalizedOriginalId = normalizePath(id);
260
+
261
+ // 读取最新的文件内容
262
+ const content = fs.readFileSync(devPath, 'utf-8');
263
+
264
+ // 更新映射关系
265
+ fileMap.set(normalizedOriginalId, normalizedDevPath);
266
+ devPathToOriginalPath.set(normalizedDevPath, normalizedOriginalId);
267
+
268
+ // 缓存原始文件内容(仅在首次加载时)
269
+ if (!originalFiles.has(normalizedOriginalId) && fs.existsSync(id)) {
270
+ originalFiles.set(normalizedOriginalId, fs.readFileSync(id, 'utf-8'));
271
+ }
272
+
273
+ if (debug) {
274
+ console.log(`[only-dev] load: 使用开发文件 (原始: ${path.relative(root, id)}, 开发: ${path.relative(root, devPath)})`);
275
+ }
276
+
277
+ return content;
278
+ } catch (err) {
279
+ if (debug) {
280
+ console.error('[only-dev] load: 读取开发文件错误:', err);
281
+ }
282
+ return null;
283
+ }
284
+ }
285
+
286
+ return null;
287
+ },
288
+
289
+ async transform(code, id) {
290
+ if (!enabled) return null;
291
+
292
+ const normalizedId = normalizePath(id);
293
+
294
+ // 检查是否来自 .only-dev 目录
295
+ if (fileMap.has(normalizedId) || devPathToOriginalPath.has(normalizedId) || normalizedId.includes(`/${devDir}/`)) {
296
+ if (debug) {
297
+ const from = normalizedId.includes(`/${devDir}/`)
298
+ ? '直接' : '映射';
299
+ console.log(`[only-dev] ${from}使用开发文件转换: ${path.relative(root, id)}`);
300
+ }
301
+ }
302
+
303
+ return null;
304
+ },
305
+
306
+ generateBundle() {
307
+ if (debug && enabled) {
308
+ console.log(`[only-dev] 构建完成,共使用 ${fileMap.size} 个开发文件`);
309
+ }
310
+ },
311
+
312
+ // 提供额外的工具函数
313
+ api: {
314
+ getDevFileMap: () => new Map(fileMap),
315
+ getOriginalFiles: () => new Map(originalFiles),
316
+ restoreOriginalFile: (filePath) => {
317
+ const content = originalFiles.get(filePath);
318
+ if (content) {
319
+ fs.writeFileSync(filePath, content);
320
+ return true;
321
+ }
322
+ return false;
323
+ }
324
+ }
325
+ };
326
+
327
+ function getDevPath(originalPath) {
328
+ if (!originalPath.startsWith(root)) return null;
329
+
330
+ const relativePath = path.relative(root, originalPath);
331
+ const devPath = path.join(root, devDir, relativePath);
332
+
333
+ // 检查是否在排除列表中
334
+ if (exclude.some(pattern => minimatch(relativePath, pattern))) {
335
+ return null;
336
+ }
337
+
338
+ // 检查是否在包含列表中
339
+ // 如果 include 为空数组,表示不限制(包含所有文件)
340
+ // 如果 include 不为空,文件必须匹配至少一个模式
341
+ if (include.length > 0 && !include.some(pattern => minimatch(relativePath, pattern))) {
342
+ return null;
343
+ }
344
+
345
+ return devPath;
346
+ }
347
+
348
+ function getOriginalPath(devPath) {
349
+ const normalizedDevPath = normalizePath(devPath);
350
+ const devDirNormalized = normalizePath(path.join(root, devDir));
351
+
352
+ // 如果不在 .only-dev 目录中,直接返回
353
+ if (!normalizedDevPath.startsWith(devDirNormalized + '/') && !normalizedDevPath.includes(`/${devDir}/`)) {
354
+ return devPath;
355
+ }
356
+
357
+ // 从映射中查找(优先使用缓存)
358
+ if (devPathToOriginalPath.has(normalizedDevPath)) {
359
+ return devPathToOriginalPath.get(normalizedDevPath);
360
+ }
361
+
362
+ // 通过路径计算原始路径
363
+ let relativeFromDev;
364
+ const devDirIndex = normalizedDevPath.indexOf(`/${devDir}/`);
365
+ if (devDirIndex !== -1) {
366
+ relativeFromDev = normalizedDevPath.substring(devDirIndex + devDir.length + 2);
367
+ } else if (normalizedDevPath.startsWith(devDirNormalized + '/')) {
368
+ relativeFromDev = normalizedDevPath.substring(devDirNormalized.length + 1);
369
+ } else {
370
+ return devPath;
371
+ }
372
+
373
+ const originalPath = normalizePath(path.join(root, relativeFromDev));
374
+
375
+ // 更新映射关系
376
+ devPathToOriginalPath.set(normalizedDevPath, originalPath);
377
+
378
+ return originalPath;
379
+ }
380
+ }
381
+
package/webpack.cjs ADDED
@@ -0,0 +1,270 @@
1
+ // webpack.js
2
+ const fs = require('fs');
3
+ const path = require('path');
4
+ const minimatch = require('minimatch');
5
+
6
+ class OnlyDevWebpackPlugin {
7
+ constructor(options = {}) {
8
+ this.options = {
9
+ enabled: options.enabled !== false,
10
+ devDir: options.devDir || '.only-dev',
11
+ debug: options.debug || false,
12
+ exclude: options.exclude || [],
13
+ include: options.include || ['**/*']
14
+ };
15
+
16
+ this.root = '';
17
+ this.fileMap = new Map();
18
+ this.originalFiles = new Map();
19
+ this.devPathToOriginalPath = new Map();
20
+ }
21
+
22
+ apply(compiler) {
23
+ if (!this.options.enabled) return;
24
+
25
+ const { devDir, debug } = this.options;
26
+
27
+ // 获取项目根目录
28
+ this.root = compiler.context || compiler.options.context || process.cwd();
29
+
30
+ if (debug) {
31
+ console.log(`[only-dev] Webpack 插件已加载,根目录: ${this.root}`);
32
+ }
33
+
34
+ // 监听文件变化
35
+ compiler.hooks.normalModuleFactory.tap('OnlyDevWebpackPlugin', (normalModuleFactory) => {
36
+ // 在解析模块时替换路径
37
+ normalModuleFactory.hooks.beforeResolve.tapAsync('OnlyDevWebpackPlugin', (data, callback) => {
38
+ if (!data.request) {
39
+ return callback();
40
+ }
41
+
42
+ // 解析请求路径
43
+ const request = data.request;
44
+ const context = data.context || this.root;
45
+
46
+ // 尝试解析为绝对路径
47
+ let resolvedPath;
48
+ try {
49
+ resolvedPath = require.resolve(request, { paths: [context] });
50
+ } catch (err) {
51
+ // 如果解析失败,尝试使用路径拼接
52
+ if (path.isAbsolute(request)) {
53
+ resolvedPath = request;
54
+ } else {
55
+ resolvedPath = path.resolve(context, request);
56
+ }
57
+ }
58
+
59
+ // 检查是否有对应的开发文件
60
+ const devPath = this.getDevPath(resolvedPath);
61
+ if (devPath && fs.existsSync(devPath)) {
62
+ const normalizedResolved = this.normalizePath(resolvedPath);
63
+ const normalizedDevPath = this.normalizePath(devPath);
64
+
65
+ if (debug) {
66
+ console.log(`[only-dev] 解析: ${path.relative(this.root, resolvedPath)} -> ${path.relative(this.root, devPath)}`);
67
+ }
68
+
69
+ this.fileMap.set(normalizedResolved, normalizedDevPath);
70
+ this.devPathToOriginalPath.set(normalizedDevPath, normalizedResolved);
71
+
72
+ // 修改请求路径
73
+ data.request = devPath;
74
+ }
75
+
76
+ callback();
77
+ });
78
+
79
+ // 在创建模块时读取开发文件内容
80
+ normalModuleFactory.hooks.module.tap('OnlyDevWebpackPlugin', (module, data) => {
81
+ const resource = module.resource;
82
+ if (!resource) return module;
83
+
84
+ const normalizedResource = this.normalizePath(resource);
85
+
86
+ // 如果已经在 .only-dev 目录中,确保内容是最新的
87
+ if (normalizedResource.includes(`/${devDir}/`)) {
88
+ const originalPath = this.getOriginalPath(normalizedResource);
89
+ const normalizedOriginalPath = this.normalizePath(originalPath);
90
+
91
+ // 缓存原始文件内容(仅在首次加载时)
92
+ if (!this.originalFiles.has(normalizedOriginalPath) && fs.existsSync(originalPath)) {
93
+ this.originalFiles.set(normalizedOriginalPath, fs.readFileSync(originalPath, 'utf-8'));
94
+ }
95
+
96
+ // 更新映射关系
97
+ this.fileMap.set(normalizedOriginalPath, normalizedResource);
98
+ this.devPathToOriginalPath.set(normalizedResource, normalizedOriginalPath);
99
+
100
+ if (debug) {
101
+ console.log(`[only-dev] 加载开发文件: ${path.relative(this.root, resource)}`);
102
+ }
103
+ } else {
104
+ // 检查是否有对应的开发文件
105
+ const devPath = this.getDevPath(resource);
106
+ if (devPath && fs.existsSync(devPath)) {
107
+ const normalizedDevPath = this.normalizePath(devPath);
108
+ const normalizedResource = this.normalizePath(resource);
109
+
110
+ // 更新映射关系
111
+ this.fileMap.set(normalizedResource, normalizedDevPath);
112
+ this.devPathToOriginalPath.set(normalizedDevPath, normalizedResource);
113
+
114
+ // 缓存原始文件内容(仅在首次加载时)
115
+ if (!this.originalFiles.has(normalizedResource) && fs.existsSync(resource)) {
116
+ this.originalFiles.set(normalizedResource, fs.readFileSync(resource, 'utf-8'));
117
+ }
118
+
119
+ if (debug) {
120
+ console.log(`[only-dev] 使用开发文件 (原始: ${path.relative(this.root, resource)}, 开发: ${path.relative(this.root, devPath)})`);
121
+ }
122
+ }
123
+ }
124
+
125
+ return module;
126
+ });
127
+ });
128
+
129
+ // 监听文件变化,触发热更新
130
+ compiler.hooks.afterCompile.tap('OnlyDevWebpackPlugin', (compilation) => {
131
+ const devDirPath = path.join(this.root, devDir);
132
+
133
+ if (!fs.existsSync(devDirPath)) {
134
+ if (debug) {
135
+ console.log(`[only-dev] 开发目录不存在: ${devDirPath}`);
136
+ }
137
+ return;
138
+ }
139
+
140
+ // 添加开发目录到文件依赖中,以便监听变化
141
+ compilation.fileDependencies.add(devDirPath);
142
+
143
+ // 递归添加所有开发文件
144
+ this.addDirectoryToDependencies(devDirPath, compilation);
145
+ });
146
+
147
+ // 监听文件变化
148
+ compiler.hooks.watchRun.tapAsync('OnlyDevWebpackPlugin', (compilation, callback) => {
149
+ const changedFiles = compilation.modifiedFiles || new Set();
150
+
151
+ for (const file of changedFiles) {
152
+ const normalizedFile = this.normalizePath(file);
153
+ const devDirNormalized = this.normalizePath(path.join(this.root, devDir));
154
+
155
+ // 检查是否是 .only-dev 目录下的文件
156
+ if (normalizedFile.startsWith(devDirNormalized + '/') || normalizedFile.includes(`/${devDir}/`)) {
157
+ const originalPath = this.getOriginalPath(normalizedFile);
158
+ const normalizedOriginalPath = this.normalizePath(originalPath);
159
+
160
+ if (debug) {
161
+ console.log(`[only-dev] 开发文件变更: ${path.relative(this.root, file)}`);
162
+ console.log(`[only-dev] 原始路径: ${path.relative(this.root, originalPath)}`);
163
+ }
164
+
165
+ // 更新映射关系
166
+ this.devPathToOriginalPath.set(normalizedFile, normalizedOriginalPath);
167
+ this.fileMap.set(normalizedOriginalPath, normalizedFile);
168
+
169
+ // 使相关模块失效,触发重新编译
170
+ if (compilation.moduleGraph) {
171
+ const module = compilation.moduleGraph.getModuleByDependency(
172
+ compilation.moduleGraph.getResolvedModule(normalizedOriginalPath)
173
+ );
174
+ if (module) {
175
+ compilation.moduleGraph.invalidateModule(module);
176
+ }
177
+ }
178
+ }
179
+ }
180
+
181
+ callback();
182
+ });
183
+
184
+ // 构建完成时的日志
185
+ compiler.hooks.done.tap('OnlyDevWebpackPlugin', () => {
186
+ if (debug && this.options.enabled) {
187
+ console.log(`[only-dev] 构建完成,共使用 ${this.fileMap.size} 个开发文件`);
188
+ }
189
+ });
190
+ }
191
+
192
+ addDirectoryToDependencies(dirPath, compilation) {
193
+ try {
194
+ const entries = fs.readdirSync(dirPath, { withFileTypes: true });
195
+
196
+ for (const entry of entries) {
197
+ const fullPath = path.join(dirPath, entry.name);
198
+
199
+ if (entry.isDirectory()) {
200
+ this.addDirectoryToDependencies(fullPath, compilation);
201
+ } else {
202
+ compilation.fileDependencies.add(fullPath);
203
+ }
204
+ }
205
+ } catch (err) {
206
+ if (this.options.debug) {
207
+ console.error(`[only-dev] 添加依赖错误:`, err);
208
+ }
209
+ }
210
+ }
211
+
212
+ getDevPath(originalPath) {
213
+ if (!originalPath.startsWith(this.root)) return null;
214
+
215
+ const relativePath = path.relative(this.root, originalPath);
216
+ const devPath = path.join(this.root, this.options.devDir, relativePath);
217
+
218
+ // 检查是否在排除列表中
219
+ if (this.options.exclude.some(pattern => minimatch(relativePath, pattern))) {
220
+ return null;
221
+ }
222
+
223
+ // 检查是否在包含列表中
224
+ if (this.options.include.length > 0 && !this.options.include.some(pattern => minimatch(relativePath, pattern))) {
225
+ return null;
226
+ }
227
+
228
+ return devPath;
229
+ }
230
+
231
+ getOriginalPath(devPath) {
232
+ const normalizedDevPath = this.normalizePath(devPath);
233
+ const devDirNormalized = this.normalizePath(path.join(this.root, this.options.devDir));
234
+
235
+ // 如果不在 .only-dev 目录中,直接返回
236
+ if (!normalizedDevPath.startsWith(devDirNormalized + '/') && !normalizedDevPath.includes(`/${this.options.devDir}/`)) {
237
+ return devPath;
238
+ }
239
+
240
+ // 从映射中查找(优先使用缓存)
241
+ if (this.devPathToOriginalPath.has(normalizedDevPath)) {
242
+ return this.devPathToOriginalPath.get(normalizedDevPath);
243
+ }
244
+
245
+ // 通过路径计算原始路径
246
+ let relativeFromDev;
247
+ const devDirIndex = normalizedDevPath.indexOf(`/${this.options.devDir}/`);
248
+ if (devDirIndex !== -1) {
249
+ relativeFromDev = normalizedDevPath.substring(devDirIndex + this.options.devDir.length + 2);
250
+ } else if (normalizedDevPath.startsWith(devDirNormalized + '/')) {
251
+ relativeFromDev = normalizedDevPath.substring(devDirNormalized.length + 1);
252
+ } else {
253
+ return devPath;
254
+ }
255
+
256
+ const originalPath = this.normalizePath(path.join(this.root, relativeFromDev));
257
+
258
+ // 更新映射关系
259
+ this.devPathToOriginalPath.set(normalizedDevPath, originalPath);
260
+
261
+ return originalPath;
262
+ }
263
+
264
+ normalizePath(filePath) {
265
+ return filePath.replace(/\\/g, '/');
266
+ }
267
+ }
268
+
269
+ module.exports = OnlyDevWebpackPlugin;
270
+