@infly/vue2-vite 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/LICENSE +15 -0
- package/README.md +307 -0
- package/bin/infly-vue2-vite.mjs +12 -0
- package/bin/vue-cli-service.mjs +135 -0
- package/package.json +71 -0
- package/src/cli/arguments.mjs +197 -0
- package/src/cli/node-version.mjs +46 -0
- package/src/cli/run.mjs +122 -0
- package/src/compat/assets.mjs +165 -0
- package/src/compat/commonjs.mjs +93 -0
- package/src/compat/empty-stub.mjs +58 -0
- package/src/compat/html.mjs +244 -0
- package/src/compat/import-interop.mjs +200 -0
- package/src/compat/jsx.mjs +142 -0
- package/src/compat/mock.mjs +125 -0
- package/src/compat/require-context.mjs +157 -0
- package/src/compat/router.mjs +62 -0
- package/src/compat/sass-export.mjs +112 -0
- package/src/compat/sass-importer.mjs +30 -0
- package/src/compat/svg-icons.mjs +35 -0
- package/src/config/discover.mjs +261 -0
- package/src/config/env.mjs +148 -0
- package/src/config/merge.mjs +65 -0
- package/src/config/project-config.mjs +87 -0
- package/src/config/registered.mjs +152 -0
- package/src/config/validation.mjs +122 -0
- package/src/config/vue-cli-reader.mjs +207 -0
- package/src/factory/build-policy.mjs +39 -0
- package/src/factory/create-vite-config.mjs +648 -0
- package/src/factory/manual-chunks.mjs +55 -0
- package/src/factory/output-names.mjs +62 -0
- package/src/index.mjs +55 -0
- package/src/runner/build.mjs +43 -0
- package/src/runner/serve.mjs +44 -0
- package/src/runner/signals.mjs +29 -0
- package/src/runner/test.mjs +190 -0
- package/src/runner/workspace.cjs +346 -0
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI 参数解析
|
|
3
|
+
*
|
|
4
|
+
* 支持的命令和参数(规范 §6.3):
|
|
5
|
+
* serve, build, test
|
|
6
|
+
* --mode value, --mode=value
|
|
7
|
+
* --target value, --target=value
|
|
8
|
+
* --port value, --port=value
|
|
9
|
+
* --host value, --host=value
|
|
10
|
+
* --open, --strictPort
|
|
11
|
+
* --no-module, --infly-vite
|
|
12
|
+
* --watch(test 专属)
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
export const SUPPORTED_COMMANDS = new Set(['serve', 'build', 'test']);
|
|
16
|
+
|
|
17
|
+
/** serve 的已知长选项名称(不含 -- 前缀) */
|
|
18
|
+
const KNOWN_SERVE_OPTIONS = new Set([
|
|
19
|
+
'mode', 'target', 'port', 'host',
|
|
20
|
+
'open', 'strictPort', 'no-module', 'infly-vite',
|
|
21
|
+
]);
|
|
22
|
+
|
|
23
|
+
/** build 的已知长选项名称 */
|
|
24
|
+
const KNOWN_BUILD_OPTIONS = new Set([
|
|
25
|
+
'mode', 'target', 'no-module', 'infly-vite',
|
|
26
|
+
]);
|
|
27
|
+
|
|
28
|
+
/** test 的已知长选项名称 */
|
|
29
|
+
const KNOWN_TEST_OPTIONS = new Set([
|
|
30
|
+
'mode', 'watch', 'no-module', 'infly-vite',
|
|
31
|
+
]);
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* 解析命令行参数
|
|
35
|
+
*
|
|
36
|
+
* @param {string[]} argv - process.argv.slice(2)
|
|
37
|
+
* @returns {{
|
|
38
|
+
* command: 'serve'|'build'|'test',
|
|
39
|
+
* mode: string|undefined,
|
|
40
|
+
* target: string|undefined,
|
|
41
|
+
* serveOptions: { port?: number, host?: string, open?: boolean, strictPort?: boolean },
|
|
42
|
+
* watch: boolean,
|
|
43
|
+
* passthrough: string[]
|
|
44
|
+
* }}
|
|
45
|
+
*/
|
|
46
|
+
export function parseArguments(argv) {
|
|
47
|
+
if (argv.length === 0) {
|
|
48
|
+
throw new Error('缺少命令。支持的命令: serve, build, test');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const [command, ...args] = argv;
|
|
52
|
+
|
|
53
|
+
if (!SUPPORTED_COMMANDS.has(command)) {
|
|
54
|
+
throw new Error(`未知命令 "${command}";只支持 serve、build、test`);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
let mode;
|
|
58
|
+
let target;
|
|
59
|
+
let watch = false;
|
|
60
|
+
const serveOptions = {};
|
|
61
|
+
const passthrough = [];
|
|
62
|
+
|
|
63
|
+
const knownOptions = command === 'serve'
|
|
64
|
+
? KNOWN_SERVE_OPTIONS
|
|
65
|
+
: command === 'test'
|
|
66
|
+
? KNOWN_TEST_OPTIONS
|
|
67
|
+
: KNOWN_BUILD_OPTIONS;
|
|
68
|
+
|
|
69
|
+
for (let i = 0; i < args.length; i++) {
|
|
70
|
+
const arg = args[i];
|
|
71
|
+
|
|
72
|
+
// --key=value 形式
|
|
73
|
+
const eqIdx = arg.indexOf('=');
|
|
74
|
+
if (arg.startsWith('--') && eqIdx > 2) {
|
|
75
|
+
const key = arg.slice(2, eqIdx);
|
|
76
|
+
const value = arg.slice(eqIdx + 1);
|
|
77
|
+
|
|
78
|
+
if (!knownOptions.has(key)) {
|
|
79
|
+
throw new Error(`未知 ${command} 参数: ${arg}`);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
switch (key) {
|
|
83
|
+
case 'mode':
|
|
84
|
+
mode = value;
|
|
85
|
+
break;
|
|
86
|
+
case 'target':
|
|
87
|
+
target = value;
|
|
88
|
+
break;
|
|
89
|
+
case 'port':
|
|
90
|
+
serveOptions.port = parsePort(value, arg);
|
|
91
|
+
break;
|
|
92
|
+
case 'host':
|
|
93
|
+
serveOptions.host = value;
|
|
94
|
+
break;
|
|
95
|
+
// --infly-vite 和 --no-module 不期望有值,但 = 形式报错
|
|
96
|
+
case 'infly-vite':
|
|
97
|
+
case 'no-module':
|
|
98
|
+
case 'watch':
|
|
99
|
+
throw new Error(`参数 ${key} 不接受值: ${arg}`);
|
|
100
|
+
default:
|
|
101
|
+
// test 命令的未识别选项透传给 vitest CLI
|
|
102
|
+
if (command === 'test') {
|
|
103
|
+
passthrough.push(arg);
|
|
104
|
+
} else {
|
|
105
|
+
throw new Error(`未知 ${command} 参数: ${arg}`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// --key value 形式
|
|
112
|
+
if (arg.startsWith('--')) {
|
|
113
|
+
const key = arg.slice(2);
|
|
114
|
+
|
|
115
|
+
// test 命令的未识别选项透传给 vitest CLI,不拦截
|
|
116
|
+
if (!knownOptions.has(key)) {
|
|
117
|
+
if (command === 'test') {
|
|
118
|
+
passthrough.push(arg);
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
throw new Error(`未知 ${command} 参数: ${arg}`);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
switch (key) {
|
|
125
|
+
case 'mode': {
|
|
126
|
+
const value = args[++i];
|
|
127
|
+
if (!value) throw new Error('--mode 缺少值');
|
|
128
|
+
mode = value;
|
|
129
|
+
break;
|
|
130
|
+
}
|
|
131
|
+
case 'target': {
|
|
132
|
+
const value = args[++i];
|
|
133
|
+
if (!value) throw new Error('--target 缺少值');
|
|
134
|
+
target = value;
|
|
135
|
+
break;
|
|
136
|
+
}
|
|
137
|
+
case 'port': {
|
|
138
|
+
const value = args[++i];
|
|
139
|
+
if (!value) throw new Error('--port 缺少值');
|
|
140
|
+
serveOptions.port = parsePort(value, '--port');
|
|
141
|
+
break;
|
|
142
|
+
}
|
|
143
|
+
case 'host': {
|
|
144
|
+
const value = args[++i];
|
|
145
|
+
if (!value) throw new Error('--host 缺少值');
|
|
146
|
+
serveOptions.host = value;
|
|
147
|
+
break;
|
|
148
|
+
}
|
|
149
|
+
case 'open':
|
|
150
|
+
serveOptions.open = true;
|
|
151
|
+
break;
|
|
152
|
+
case 'strictPort':
|
|
153
|
+
serveOptions.strictPort = true;
|
|
154
|
+
break;
|
|
155
|
+
case 'infly-vite':
|
|
156
|
+
// 消费但不操作(薄分发器也应先过滤,这里作为安全兜底)
|
|
157
|
+
break;
|
|
158
|
+
case 'no-module':
|
|
159
|
+
// 消费后忽略(历史 Vue CLI 参数)
|
|
160
|
+
break;
|
|
161
|
+
case 'watch':
|
|
162
|
+
// 仅 test 命令;其他命令下由 knownOptions 拦截
|
|
163
|
+
watch = true;
|
|
164
|
+
break;
|
|
165
|
+
default:
|
|
166
|
+
// test 命令的未识别选项透传给 vitest CLI
|
|
167
|
+
if (command === 'test') {
|
|
168
|
+
passthrough.push(arg);
|
|
169
|
+
} else {
|
|
170
|
+
throw new Error(`未知 ${command} 参数: ${arg}`);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// 非选项参数
|
|
177
|
+
passthrough.push(arg);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// mode 默认值
|
|
181
|
+
if (!mode) {
|
|
182
|
+
mode = command === 'serve' ? 'development' : command === 'test' ? 'test' : 'production';
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return { command, mode, target, serveOptions, watch, passthrough };
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* 解析端口号
|
|
190
|
+
*/
|
|
191
|
+
function parsePort(value, fullArg) {
|
|
192
|
+
const port = Number(value);
|
|
193
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
194
|
+
throw new Error(`无效端口: ${fullArg} ${value}`);
|
|
195
|
+
}
|
|
196
|
+
return port;
|
|
197
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Node 版本检查
|
|
3
|
+
*
|
|
4
|
+
* Vite 7 要求 Node ^20.19.0 或 >=22.12.0。
|
|
5
|
+
* 版本不满足时在加载 Vite 之前退出,提供明确的错误信息。
|
|
6
|
+
*
|
|
7
|
+
* 退出码:3(规范 §14)
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const MIN_NODE_MAJOR = 20;
|
|
11
|
+
const MIN_NODE_MINOR = 19;
|
|
12
|
+
const ALT_NODE_MAJOR = 22;
|
|
13
|
+
const ALT_NODE_MINOR = 12;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @returns {{ ok: true } | { ok: false, message: string }}
|
|
17
|
+
*/
|
|
18
|
+
export function checkNodeVersion() {
|
|
19
|
+
const version = process.versions.node;
|
|
20
|
+
const parts = version.split('.').map(Number);
|
|
21
|
+
|
|
22
|
+
if (parts.length < 2) {
|
|
23
|
+
return {
|
|
24
|
+
ok: false,
|
|
25
|
+
message: `@infly/vue2-vite 无法解析 Node 版本: ${version}。Vite 7 要求 Node ^20.19.0 或 >=22.12.0。`,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const major = parts[0];
|
|
30
|
+
const minor = parts[1];
|
|
31
|
+
|
|
32
|
+
const ok =
|
|
33
|
+
(major === MIN_NODE_MAJOR && minor >= MIN_NODE_MINOR) ||
|
|
34
|
+
(major >= ALT_NODE_MAJOR && (major !== ALT_NODE_MAJOR || minor >= ALT_NODE_MINOR));
|
|
35
|
+
|
|
36
|
+
if (ok) {
|
|
37
|
+
return { ok: true };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return {
|
|
41
|
+
ok: false,
|
|
42
|
+
message:
|
|
43
|
+
`@infly/vue2-vite 当前 Node 为 ${version};Vite 7 要求 Node ^${MIN_NODE_MAJOR}.${MIN_NODE_MINOR}.0 或 >=${ALT_NODE_MAJOR}.${ALT_NODE_MINOR}.0。\n` +
|
|
44
|
+
`请切换到 Node 22.23.1 后重试。默认 Webpack 命令未受影响。`,
|
|
45
|
+
};
|
|
46
|
+
}
|
package/src/cli/run.mjs
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI 编排入口
|
|
3
|
+
*
|
|
4
|
+
* 退出码(规范 §14):
|
|
5
|
+
* 0 - 成功
|
|
6
|
+
* 2 - 参数错误
|
|
7
|
+
* 3 - Node 版本不满足
|
|
8
|
+
* 4 - 配置错误
|
|
9
|
+
* 5 - 兼容检查失败
|
|
10
|
+
* 10 - Vite serve 启动失败
|
|
11
|
+
* 11 - Vite build 失败
|
|
12
|
+
* 12 - vitest 测试失败(透传 vitest 退出码)
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { parseArguments } from './arguments.mjs';
|
|
16
|
+
import { checkNodeVersion } from './node-version.mjs';
|
|
17
|
+
import { discoverConfig } from '../config/discover.mjs';
|
|
18
|
+
import { createViteConfig } from '../factory/create-vite-config.mjs';
|
|
19
|
+
import { serve } from '../runner/serve.mjs';
|
|
20
|
+
import { build, dryRun } from '../runner/build.mjs';
|
|
21
|
+
import { runTest } from '../runner/test.mjs';
|
|
22
|
+
|
|
23
|
+
export const EXIT_CODES = {
|
|
24
|
+
SUCCESS: 0,
|
|
25
|
+
ARGUMENT_ERROR: 2,
|
|
26
|
+
NODE_VERSION_ERROR: 3,
|
|
27
|
+
CONFIG_ERROR: 4,
|
|
28
|
+
COMPAT_ERROR: 5,
|
|
29
|
+
SERVE_ERROR: 10,
|
|
30
|
+
BUILD_ERROR: 11,
|
|
31
|
+
TEST_ERROR: 12,
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* @param {string[]} argv
|
|
36
|
+
* @param {string} cwd
|
|
37
|
+
*/
|
|
38
|
+
export async function run(argv = process.argv.slice(2), cwd = process.cwd()) {
|
|
39
|
+
let parsed;
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
// 1. 检查 Node 版本
|
|
43
|
+
const nodeCheck = checkNodeVersion();
|
|
44
|
+
if (!nodeCheck.ok) {
|
|
45
|
+
console.error(nodeCheck.message);
|
|
46
|
+
process.exit(EXIT_CODES.NODE_VERSION_ERROR);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// 2. 解析参数
|
|
50
|
+
try {
|
|
51
|
+
parsed = parseArguments(argv);
|
|
52
|
+
} catch (err) {
|
|
53
|
+
console.error(`@infly/vue2-vite 参数错误: ${err.message}`);
|
|
54
|
+
process.exit(EXIT_CODES.ARGUMENT_ERROR);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const { command, mode, target, serveOptions, watch, passthrough } = parsed;
|
|
58
|
+
|
|
59
|
+
// 3. 加载项目配置
|
|
60
|
+
const { config, meta } = await discoverConfig({
|
|
61
|
+
command,
|
|
62
|
+
mode,
|
|
63
|
+
target,
|
|
64
|
+
projectRoot: cwd,
|
|
65
|
+
cliOptions: serveOptions,
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
// 4. 输出诊断信息
|
|
69
|
+
for (const warning of meta.vueCliWarnings || []) {
|
|
70
|
+
console.warn(`@infly/vue2-vite ${warning}`);
|
|
71
|
+
}
|
|
72
|
+
for (const diagnosis of meta.diagnoses || []) {
|
|
73
|
+
console.warn(`@infly/vue2-vite ${diagnosis}`);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// 5. 严格兼容检查
|
|
77
|
+
if (!meta.validation.valid) {
|
|
78
|
+
for (const error of meta.validation.errors) {
|
|
79
|
+
console.error(`@infly/vue2-vite 兼容检查失败: ${error}`);
|
|
80
|
+
}
|
|
81
|
+
process.exit(EXIT_CODES.COMPAT_ERROR);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// 6. Dry-run 模式
|
|
85
|
+
if (process.env.INFLY_VITE_DRY_RUN === 'true' && command === 'build') {
|
|
86
|
+
dryRun(config);
|
|
87
|
+
process.exit(EXIT_CODES.SUCCESS);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// 7. 生成 Vite 配置
|
|
91
|
+
const viteConfig = createViteConfig(config);
|
|
92
|
+
|
|
93
|
+
// 8. 执行命令
|
|
94
|
+
if (command === 'serve') {
|
|
95
|
+
await serve(viteConfig);
|
|
96
|
+
// Vite 的 HTTP/WebSocket 句柄会保持进程运行。这里不能主动 exit,
|
|
97
|
+
// 否则 server.listen() 刚成功就会立即终止开发服务。
|
|
98
|
+
return EXIT_CODES.SUCCESS;
|
|
99
|
+
} else if (command === 'test') {
|
|
100
|
+
const exitCode = await runTest(viteConfig, config, {
|
|
101
|
+
projectRoot: cwd,
|
|
102
|
+
watch,
|
|
103
|
+
passthrough,
|
|
104
|
+
});
|
|
105
|
+
process.exit(exitCode === 0 ? EXIT_CODES.SUCCESS : EXIT_CODES.TEST_ERROR);
|
|
106
|
+
} else {
|
|
107
|
+
await build(viteConfig);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// 正常退出
|
|
111
|
+
process.exit(EXIT_CODES.SUCCESS);
|
|
112
|
+
} catch (err) {
|
|
113
|
+
console.error(`@infly/vue2-vite ${err.stack || err.message}`);
|
|
114
|
+
if (parsed?.command === 'serve') {
|
|
115
|
+
process.exit(EXIT_CODES.SERVE_ERROR);
|
|
116
|
+
} else if (parsed?.command === 'test') {
|
|
117
|
+
process.exit(EXIT_CODES.TEST_ERROR);
|
|
118
|
+
} else {
|
|
119
|
+
process.exit(EXIT_CODES.BUILD_ERROR);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 动态资源 require → Vite import 转换
|
|
3
|
+
*
|
|
4
|
+
* 将 require('@/assets/logo.png') 等动态/静态资源引用
|
|
5
|
+
* 转换为 Vite 的 import + ?url 查询方式。
|
|
6
|
+
*
|
|
7
|
+
* 从 compat/assets.js 迁移(80% 可复用)。
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import fs from 'node:fs';
|
|
11
|
+
import path from 'node:path';
|
|
12
|
+
|
|
13
|
+
const ASSET_EXTENSION = /\.(?:avif|bmp|eot|gif|ico|jpe?g|png|svg|ttf|webp|woff2?)(?:[?#].*)?$/i;
|
|
14
|
+
const ASSET_DIRECTORY = /(?:^|\/)(?:assets?|images?|icons?)(?:\/|$)/i;
|
|
15
|
+
|
|
16
|
+
function normalizeAssetPath(assetPath) {
|
|
17
|
+
return assetPath.startsWith('@/') ? `/src/${assetPath.slice(2)}` : assetPath;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function isAssetRequest(prefix, suffix = '') {
|
|
21
|
+
const request = `${prefix}${suffix}`;
|
|
22
|
+
return ASSET_EXTENSION.test(request) || ASSET_DIRECTORY.test(request);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* 转换源文件中的 require(资源路径) 调用
|
|
27
|
+
*
|
|
28
|
+
* @param {string} source - 源文件内容
|
|
29
|
+
* @param {object} [options] - { resolveFiles? }
|
|
30
|
+
* @returns {{ code: string, map: null } | null}
|
|
31
|
+
*/
|
|
32
|
+
export function transformAssetRequires(source, options = {}) {
|
|
33
|
+
if (!/\brequire\s*\(/.test(source)) return null;
|
|
34
|
+
|
|
35
|
+
let code = source;
|
|
36
|
+
let converted = 0;
|
|
37
|
+
const declarations = [];
|
|
38
|
+
const importDeclarations = [];
|
|
39
|
+
const importedAssets = new Map();
|
|
40
|
+
|
|
41
|
+
function getAssetImport(importPath) {
|
|
42
|
+
if (importedAssets.has(importPath)) return importedAssets.get(importPath);
|
|
43
|
+
const importName = `__vite_asset_url_${importedAssets.size + 1}`;
|
|
44
|
+
importedAssets.set(importPath, importName);
|
|
45
|
+
importDeclarations.push(`import ${importName} from ${JSON.stringify(`${importPath}?url`)};`);
|
|
46
|
+
return importName;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function createReplacement(prefix, expression, suffix = '') {
|
|
50
|
+
if (!isAssetRequest(prefix, suffix)) return null;
|
|
51
|
+
|
|
52
|
+
converted++;
|
|
53
|
+
const normalizedPrefix = normalizeAssetPath(prefix);
|
|
54
|
+
const globPattern = expression
|
|
55
|
+
? `${normalizedPrefix}*${suffix}`
|
|
56
|
+
: `${normalizedPrefix}${suffix}`;
|
|
57
|
+
const modulesName = `__vite_asset_modules_${converted}`;
|
|
58
|
+
const resolverName = `__vite_resolve_asset_${converted}`;
|
|
59
|
+
|
|
60
|
+
const resolvedFiles = options.resolveFiles ? options.resolveFiles(globPattern) : null;
|
|
61
|
+
const modulesDeclaration = resolvedFiles
|
|
62
|
+
? `const ${modulesName} = { ${resolvedFiles.map(
|
|
63
|
+
({ key, importPath: ip }) => `${JSON.stringify(key)}: ${getAssetImport(ip)}`
|
|
64
|
+
).join(', ')} };`
|
|
65
|
+
: `const ${modulesName} = import.meta.glob(${JSON.stringify(globPattern)}, { eager: true, query: '?url', import: 'default' });`;
|
|
66
|
+
|
|
67
|
+
declarations.push(
|
|
68
|
+
modulesDeclaration,
|
|
69
|
+
`function ${resolverName}(key) {`,
|
|
70
|
+
` const asset = ${modulesName}[key];`,
|
|
71
|
+
` if (!asset) throw new Error('[infly-vue2] Cannot resolve asset: ' + key);`,
|
|
72
|
+
' return asset;',
|
|
73
|
+
'}',
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
const keyExpression = expression
|
|
77
|
+
? `${JSON.stringify(normalizedPrefix)} + (${expression.trim()}) + ${JSON.stringify(suffix)}`
|
|
78
|
+
: JSON.stringify(`${normalizedPrefix}${suffix}`);
|
|
79
|
+
return `${resolverName}(${keyExpression})`;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// 模板字符串形式:require(`...`)
|
|
83
|
+
code = code.replace(
|
|
84
|
+
/\brequire\s*\(\s*`([^`$]*)\$\{([^{}]+)\}([^`]*)`\s*\)/g,
|
|
85
|
+
(match, prefix, expression, suffix) => createReplacement(prefix, expression, suffix) || match,
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
// 拼接形式:require('...' + variable)
|
|
89
|
+
code = code.replace(
|
|
90
|
+
/\brequire\s*\(\s*(['"])([^'"]+)\1\s*\+\s*([\w$]+(?:\.[\w$]+)*)\s*\)/g,
|
|
91
|
+
(match, _, prefix, expression) => createReplacement(prefix, expression) || match,
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
// 静态形式:require('...')
|
|
95
|
+
code = code.replace(
|
|
96
|
+
/\brequire\s*\(\s*(['"])([^'"]+)\1\s*\)/g,
|
|
97
|
+
(match, _, request) => createReplacement(request, null) || match,
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
if (converted === 0) return null;
|
|
101
|
+
return { code: `${importDeclarations.join('\n')}\n${declarations.join('\n')}\n${code}`, map: null };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function wildcardToRegExp(filePattern) {
|
|
105
|
+
const escaped = filePattern
|
|
106
|
+
.split('*')
|
|
107
|
+
.map((part) => part.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))
|
|
108
|
+
.join('.*');
|
|
109
|
+
return new RegExp(`^${escaped}$`);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* 创建资源文件解析器(用于优化已知文件的静态映射)
|
|
114
|
+
*/
|
|
115
|
+
function createAssetFileResolver(projectRoot, id) {
|
|
116
|
+
const cleanId = id.replace(/[?#].*$/, '');
|
|
117
|
+
|
|
118
|
+
return (globPattern) => {
|
|
119
|
+
const separatorIndex = globPattern.lastIndexOf('/');
|
|
120
|
+
if (separatorIndex < 0) return [];
|
|
121
|
+
const requestDirectory = globPattern.slice(0, separatorIndex + 1);
|
|
122
|
+
const filePattern = globPattern.slice(separatorIndex + 1);
|
|
123
|
+
let fileDirectory;
|
|
124
|
+
|
|
125
|
+
if (requestDirectory.startsWith('/src/')) {
|
|
126
|
+
fileDirectory = path.resolve(projectRoot, 'src', requestDirectory.slice('/src/'.length));
|
|
127
|
+
} else if (requestDirectory.startsWith('./') || requestDirectory.startsWith('../')) {
|
|
128
|
+
fileDirectory = path.resolve(path.dirname(cleanId), requestDirectory);
|
|
129
|
+
} else {
|
|
130
|
+
return [];
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
try {
|
|
134
|
+
const matcher = wildcardToRegExp(filePattern);
|
|
135
|
+
return fs.readdirSync(fileDirectory, { withFileTypes: true })
|
|
136
|
+
.filter((entry) => entry.isFile() && matcher.test(entry.name))
|
|
137
|
+
.map((entry) => ({
|
|
138
|
+
key: `${requestDirectory}${entry.name}`,
|
|
139
|
+
importPath: `${requestDirectory}${entry.name}`,
|
|
140
|
+
}))
|
|
141
|
+
.sort((left, right) => left.key.localeCompare(right.key));
|
|
142
|
+
} catch (_) {
|
|
143
|
+
return [];
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* 创建资源 require 兼容 Vite 插件
|
|
150
|
+
*
|
|
151
|
+
* @param {string} projectRoot - 项目根目录
|
|
152
|
+
* @returns {import('vite').Plugin}
|
|
153
|
+
*/
|
|
154
|
+
export function assetRequirePlugin(projectRoot) {
|
|
155
|
+
return {
|
|
156
|
+
name: 'infly-vue2:asset-require',
|
|
157
|
+
enforce: 'post',
|
|
158
|
+
transform(code, id) {
|
|
159
|
+
if (id.includes('node_modules') || id.includes('.vite') || id.includes('.cache')) return null;
|
|
160
|
+
if (code.includes('<template') && code.includes('</template>')) return null;
|
|
161
|
+
const resolveFiles = createAssetFileResolver(projectRoot, id);
|
|
162
|
+
return transformAssetRequires(code, { resolveFiles });
|
|
163
|
+
},
|
|
164
|
+
};
|
|
165
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CJS → ESM 受控转换
|
|
3
|
+
*
|
|
4
|
+
* 将项目源码中的 require()/module.exports 转换为 ESM import/export,
|
|
5
|
+
* 同时处理动态 require 模板字符串。
|
|
6
|
+
*
|
|
7
|
+
* 从 factory.js transformCommonJsModule 提取(独立化)。
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* 转换源文件中的 CommonJS 调用
|
|
12
|
+
*
|
|
13
|
+
* @param {string} source - 源文件内容
|
|
14
|
+
* @returns {{ code: string, map: null } | null}
|
|
15
|
+
*/
|
|
16
|
+
export function transformCommonJsModule(source) {
|
|
17
|
+
const hasRequire = /\brequire\s*\(/.test(source);
|
|
18
|
+
const hasModuleExports = source.includes('module.exports');
|
|
19
|
+
if (!hasRequire && !hasModuleExports) return null;
|
|
20
|
+
|
|
21
|
+
let code = source;
|
|
22
|
+
let converted = 0;
|
|
23
|
+
|
|
24
|
+
// 解构 require:const { a, b } = require('...')
|
|
25
|
+
code = code.replace(
|
|
26
|
+
/^(const|let|var)\s+\{\s*([^}]+)\s*\}\s*=\s*require\((['"])([^'"]+)\3\)/gm,
|
|
27
|
+
(_, decl, names, __, reqPath) => {
|
|
28
|
+
converted++;
|
|
29
|
+
const moduleName = `__cjs_r${converted}`;
|
|
30
|
+
return `import * as ${moduleName} from '${reqPath}';\n${decl} { ${names} } = ${moduleName}.default || ${moduleName};`;
|
|
31
|
+
}
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
// 简单 require:const x = require('...')
|
|
35
|
+
code = code.replace(
|
|
36
|
+
/^(const|let|var)\s+(\w+)\s*=\s*require\((['"])([^'"]+)\3\)/gm,
|
|
37
|
+
(_, decl, varName, __, reqPath) => {
|
|
38
|
+
converted++;
|
|
39
|
+
const moduleName = `__cjs_r${converted}`;
|
|
40
|
+
return `import * as ${moduleName} from '${reqPath}';\n${decl} ${varName} = ${moduleName}.default || ${moduleName};`;
|
|
41
|
+
}
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
// 动态模板字符串 require:const loader = (x) => require(`./path/${x}.js`)
|
|
45
|
+
code = code.replace(
|
|
46
|
+
/^(const|let|var)\s+(\w+)\s*=\s*\(\s*(\w+)\s*\)\s*=>\s*require\(\s*`([^`$]*)\$\{\s*\3\s*\}([^`]*)`\s*\)\s*;?/gm,
|
|
47
|
+
(match, decl, loaderName, parameter, prefix, suffix) => {
|
|
48
|
+
if (!prefix.startsWith('.')) return match;
|
|
49
|
+
converted++;
|
|
50
|
+
const modulesName = `__cjs_dynamic_${converted}`;
|
|
51
|
+
const keyName = `__cjs_key_${converted}`;
|
|
52
|
+
const loadedName = `__cjs_loaded_${converted}`;
|
|
53
|
+
const globPattern = `${prefix}*${suffix}.{js,cjs}`;
|
|
54
|
+
return [
|
|
55
|
+
`${decl} ${modulesName} = import.meta.glob(${JSON.stringify(globPattern)}, { eager: true });`,
|
|
56
|
+
`${decl} ${loaderName} = (${parameter}) => {`,
|
|
57
|
+
` const ${keyName} = ${JSON.stringify(prefix)} + ${parameter} + ${JSON.stringify(suffix)};`,
|
|
58
|
+
` const ${loadedName} = ${modulesName}[${keyName}] || ${modulesName}[${keyName} + '.js'] || ${modulesName}[${keyName} + '.cjs'];`,
|
|
59
|
+
` if (!${loadedName}) throw new Error('[infly-vue2] Cannot resolve dynamic module: ' + ${keyName});`,
|
|
60
|
+
` return ${loadedName}.default || ${loadedName};`,
|
|
61
|
+
'};',
|
|
62
|
+
].join('\n');
|
|
63
|
+
}
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
// 未覆盖的动态 require → 留给其他兼容插件
|
|
67
|
+
if (/\brequire\s*\(/.test(code)) return null;
|
|
68
|
+
|
|
69
|
+
// module.exports → export default(避免 __cjs_module 包含 "module" 子串误判)
|
|
70
|
+
if (hasModuleExports) {
|
|
71
|
+
code = code.replace(/\bmodule\.exports\b/g, '__cjs_wrp.exports');
|
|
72
|
+
code = `const __cjs_wrp = { exports: {} };\n${code}\nexport default __cjs_wrp.exports;`;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return converted > 0 || hasModuleExports ? { code, map: null } : null;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* 创建 CJS → ESM 兼容 Vite 插件
|
|
80
|
+
*
|
|
81
|
+
* @returns {import('vite').Plugin}
|
|
82
|
+
*/
|
|
83
|
+
export function commonJsPlugin() {
|
|
84
|
+
return {
|
|
85
|
+
name: 'infly-vue2:commonjs',
|
|
86
|
+
enforce: 'pre',
|
|
87
|
+
transform(code, id) {
|
|
88
|
+
if (id.includes('node_modules') || id.includes('.vite') || id.includes('.cache')) return null;
|
|
89
|
+
const cjsResult = transformCommonJsModule(code);
|
|
90
|
+
return cjsResult || null;
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
function normalizeId(id) {
|
|
2
|
+
return id.replace(/\\/g, '/').replace(/[?#].*$/, '');
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* 为显式登记的空模块生成固定导出;对项目源码中的其他空 JS 模块,
|
|
7
|
+
* 使用 Rollup syntheticNamedExports 兼容旧项目的具名导入。
|
|
8
|
+
*
|
|
9
|
+
* @param {{ modules?: Array<{ id: string, exports?: string[] }> }} options
|
|
10
|
+
*/
|
|
11
|
+
export function emptyStubPlugin(options = {}) {
|
|
12
|
+
const modules = new Map(
|
|
13
|
+
(options.modules || []).map((entry) => [
|
|
14
|
+
normalizeId(entry.id),
|
|
15
|
+
Array.isArray(entry.exports) ? entry.exports : [],
|
|
16
|
+
]),
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
return {
|
|
20
|
+
name: 'infly-vue2:empty-stub',
|
|
21
|
+
enforce: 'pre',
|
|
22
|
+
transform(code, id) {
|
|
23
|
+
const cleanId = normalizeId(id);
|
|
24
|
+
const namedExports = modules.get(cleanId);
|
|
25
|
+
const explicitlyConfigured = modules.has(cleanId);
|
|
26
|
+
if (!explicitlyConfigured && (!cleanId.endsWith('.js') || cleanId.includes('/node_modules/'))) {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
const lines = code.trim().split('\n');
|
|
30
|
+
const isEmpty = lines.every((line) => !line.trim() || line.trim().startsWith('//'));
|
|
31
|
+
if (!isEmpty) return null;
|
|
32
|
+
|
|
33
|
+
if (!explicitlyConfigured) {
|
|
34
|
+
this?.warn?.(`[infly-vue2] 空模块使用具名导入兼容: ${cleanId}`);
|
|
35
|
+
return {
|
|
36
|
+
code: [
|
|
37
|
+
"const __component = Object.freeze({ name: 'InflyEmptyModuleStub', render: (h) => h() });",
|
|
38
|
+
'const __stub = new Proxy(__component, {',
|
|
39
|
+
' get: (target, key) => key in target ? target[key] : __component,',
|
|
40
|
+
'});',
|
|
41
|
+
'export default __stub;',
|
|
42
|
+
].join('\n'),
|
|
43
|
+
map: null,
|
|
44
|
+
syntheticNamedExports: true,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
code: [
|
|
50
|
+
"const __stub = Object.freeze({ name: 'InflyEmptyModuleStub', render: (h) => h() });",
|
|
51
|
+
'export default __stub;',
|
|
52
|
+
...namedExports.map((name) => `export const ${name} = __stub;`),
|
|
53
|
+
].join('\n'),
|
|
54
|
+
map: null,
|
|
55
|
+
};
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
}
|