@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,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* require.context → import.meta.glob 转换
|
|
3
|
+
*
|
|
4
|
+
* 将 Webpack 的静态 require.context() 调用转换为 Vite 的 import.meta.glob。
|
|
5
|
+
* 动态调用(变量路径/正则)直接报错。
|
|
6
|
+
*
|
|
7
|
+
* 从 compat/glob.js 迁移(80% 可复用)。
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import path from 'node:path';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* require.context 匹配正则
|
|
14
|
+
*/
|
|
15
|
+
const REQUIRE_CONTEXT_PATTERN =
|
|
16
|
+
/const\s+(\w+)\s*=\s*require\.context\(\s*(['"])([^'"]+)\2\s*,\s*(true|false)\s*,\s*(\/.+?\/[gimsuy]*)\s*\)/g;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* 生成 import.meta.glob 的等价代码
|
|
20
|
+
*/
|
|
21
|
+
export function generateGlobReplacement(varName, dir, recursive, regexStr) {
|
|
22
|
+
const globPattern = regexToGlob(regexStr, recursive);
|
|
23
|
+
const actualPattern = recursive ? globPattern : globPattern.replace('**/', '');
|
|
24
|
+
|
|
25
|
+
const hasExcludeIndex = regexStr.includes('?!index');
|
|
26
|
+
const contextPrefix = `${dir.replace(/\/$/, '')}/`;
|
|
27
|
+
const quotedContextPrefix = JSON.stringify(contextPrefix);
|
|
28
|
+
|
|
29
|
+
const lines = [
|
|
30
|
+
`const __${varName}RawModules = import.meta.glob('${dir}/${actualPattern}', { eager: true });`,
|
|
31
|
+
`const __${varName}Modules = {};`,
|
|
32
|
+
`for (const [key, value] of Object.entries(__${varName}RawModules)) {`,
|
|
33
|
+
` const normalizedKey = key.startsWith(${quotedContextPrefix}) ? './' + key.slice(${quotedContextPrefix}.length) : key;`,
|
|
34
|
+
` __${varName}Modules[normalizedKey] = value;`,
|
|
35
|
+
'}',
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
if (hasExcludeIndex) {
|
|
39
|
+
lines.push(
|
|
40
|
+
`const __${varName}Filtered = {};`,
|
|
41
|
+
`for (const [k, v] of Object.entries(__${varName}Modules)) {`,
|
|
42
|
+
` if (!k.endsWith('/index.js') && !k.endsWith('\\\\index.js')) {`,
|
|
43
|
+
` __${varName}Filtered[k] = v;`,
|
|
44
|
+
' }',
|
|
45
|
+
'}',
|
|
46
|
+
`function ${varName}(key) { return __${varName}Filtered[key]; }`,
|
|
47
|
+
`${varName}.keys = () => Object.keys(__${varName}Filtered);`,
|
|
48
|
+
);
|
|
49
|
+
} else {
|
|
50
|
+
lines.push(
|
|
51
|
+
`function ${varName}(key) { return __${varName}Modules[key]; }`,
|
|
52
|
+
`${varName}.keys = () => Object.keys(__${varName}Modules);`,
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return lines.join('\n');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* 将 JS 正则转换为 glob 模式
|
|
61
|
+
*/
|
|
62
|
+
function regexToGlob(regexStr, recursive) {
|
|
63
|
+
const inner = regexStr.replace(/^\/(.*)\/[gimsuy]*$/, '$1');
|
|
64
|
+
|
|
65
|
+
const simpleExt = inner.match(/^\\\.(\w+)\$/);
|
|
66
|
+
if (simpleExt) return `*.${simpleExt[1]}`;
|
|
67
|
+
|
|
68
|
+
let glob = inner.replace(/\\\.\\\//g, '');
|
|
69
|
+
glob = glob.replace(/\.\+/g, '*').replace(/\.\*/g, '*');
|
|
70
|
+
glob = glob.replace(/\^/g, '').replace(/\$/g, '');
|
|
71
|
+
glob = glob.replace(/[()\[\]{}]/g, '');
|
|
72
|
+
|
|
73
|
+
const extMatch = glob.match(/\*\\\.(\w+)/);
|
|
74
|
+
if (extMatch) return `*.${extMatch[1]}`;
|
|
75
|
+
|
|
76
|
+
glob = glob.replace(/\\\\/g, '\\');
|
|
77
|
+
return recursive ? `**/*${glob}` : `*${glob}`;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* 创建 require.context → import.meta.glob 兼容插件
|
|
82
|
+
*
|
|
83
|
+
* @param {string} projectRoot - 项目根目录
|
|
84
|
+
* @param {string[]} [appSourcePaths] - 工作区内所有应用的源码路径,用于跨应用源码转换
|
|
85
|
+
* @param {string[]} [aliasedSourcePaths] - 显式源码别名指向的目录
|
|
86
|
+
* @returns {import('vite').Plugin}
|
|
87
|
+
*/
|
|
88
|
+
export function requireContextPlugin(projectRoot, appSourcePaths = [], aliasedSourcePaths = []) {
|
|
89
|
+
const srcDir = path.resolve(projectRoot, 'src').replace(/\\/g, '/');
|
|
90
|
+
const allowedDirs = [...new Set([
|
|
91
|
+
srcDir,
|
|
92
|
+
...appSourcePaths.map((p) => path.resolve(p, 'src').replace(/\\/g, '/')),
|
|
93
|
+
...aliasedSourcePaths.map((p) => path.resolve(p).replace(/\\/g, '/')),
|
|
94
|
+
])];
|
|
95
|
+
|
|
96
|
+
return {
|
|
97
|
+
name: 'infly-vue2:require-context',
|
|
98
|
+
enforce: 'pre',
|
|
99
|
+
|
|
100
|
+
transform(code, id) {
|
|
101
|
+
const cleanId = id.replace(/[?#].*$/, '');
|
|
102
|
+
const nid = cleanId.replace(/\\/g, '/');
|
|
103
|
+
|
|
104
|
+
if (!cleanId.endsWith('.js')) return null;
|
|
105
|
+
if (!allowedDirs.some((dir) => nid.startsWith(dir))) return null;
|
|
106
|
+
|
|
107
|
+
const codeWithoutComments = code
|
|
108
|
+
.replace(/\/\*[\s\S]*?\*\//g, (comment) => comment.replace(/[^\r\n]/g, ' '))
|
|
109
|
+
.replace(/(^|[^:])\/\/.*$/gm, '$1');
|
|
110
|
+
|
|
111
|
+
if (!codeWithoutComments.includes('require.context')) {
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const errors = [];
|
|
116
|
+
let transformed = code;
|
|
117
|
+
let hasMatch = false;
|
|
118
|
+
|
|
119
|
+
REQUIRE_CONTEXT_PATTERN.lastIndex = 0;
|
|
120
|
+
|
|
121
|
+
transformed = transformed.replace(
|
|
122
|
+
REQUIRE_CONTEXT_PATTERN,
|
|
123
|
+
(match, varName, quote, dir, recursive, regexStr) => {
|
|
124
|
+
hasMatch = true;
|
|
125
|
+
return generateGlobReplacement(varName, dir, recursive === 'true', regexStr);
|
|
126
|
+
}
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
if (!hasMatch && codeWithoutComments.includes('require.context')) {
|
|
130
|
+
const lines = codeWithoutComments.split('\n');
|
|
131
|
+
for (let i = 0; i < lines.length; i++) {
|
|
132
|
+
if (lines[i].includes('require.context')) {
|
|
133
|
+
const testPattern =
|
|
134
|
+
/require\.context\(\s*(['"])([^'"]+)\2\s*,\s*(true|false)\s*,\s*(\/.+?\/[gimsuy]*)\s*\)/;
|
|
135
|
+
if (!testPattern.test(lines[i])) {
|
|
136
|
+
errors.push(
|
|
137
|
+
`动态或无法识别的 require.context 调用在第 ${i + 1} 行:\n` +
|
|
138
|
+
` ${lines[i].trim()}\n` +
|
|
139
|
+
' Vite 兼容层仅支持静态 require.context(dir, recursive, regex) 模式。'
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (errors.length > 0) {
|
|
147
|
+
this.error(errors.join('\n\n'));
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (hasMatch) {
|
|
151
|
+
return { code: transformed, map: null };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return null;
|
|
155
|
+
},
|
|
156
|
+
};
|
|
157
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vue Router 3.x Promise 兼容
|
|
3
|
+
*
|
|
4
|
+
* Vite 提供的 vue-router 3.0.x 中 push/replace 方法只支持回调、不返回 Promise。
|
|
5
|
+
* 此插件在 transpile 阶段为目标文件注入 Promise 包装。
|
|
6
|
+
*
|
|
7
|
+
* 从 compat/router.js 迁移(95% 可复用)。
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const NAVIGATION_METHODS = ['push', 'replace'];
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* 为 VueRouter.prototype.push 和 replace 注入 Promise 支持
|
|
14
|
+
*
|
|
15
|
+
* @param {string} source - 源文件内容
|
|
16
|
+
* @returns {{ code: string, map: null } | null}
|
|
17
|
+
*/
|
|
18
|
+
function transformVueRouterPromiseNavigation(source) {
|
|
19
|
+
let code = source;
|
|
20
|
+
let converted = 0;
|
|
21
|
+
|
|
22
|
+
for (const method of NAVIGATION_METHODS) {
|
|
23
|
+
const pattern = new RegExp(
|
|
24
|
+
`VueRouter\\.prototype\\.${method} = function ${method} \\(location, onComplete, onAbort\\) \\{\\s*` +
|
|
25
|
+
`this\\.history\\.${method}\\(location, onComplete, onAbort\\);?\\s*\\};`
|
|
26
|
+
);
|
|
27
|
+
code = code.replace(pattern, () => {
|
|
28
|
+
converted++;
|
|
29
|
+
return [
|
|
30
|
+
`VueRouter.prototype.${method} = function ${method} (location, onComplete, onAbort) {`,
|
|
31
|
+
` if (!onComplete && !onAbort && typeof Promise !== 'undefined') {`,
|
|
32
|
+
` return new Promise((resolve, reject) => {`,
|
|
33
|
+
` this.history.${method}(location, resolve, reject);`,
|
|
34
|
+
' });',
|
|
35
|
+
' }',
|
|
36
|
+
` this.history.${method}(location, onComplete, onAbort);`,
|
|
37
|
+
'};',
|
|
38
|
+
].join('\n');
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return converted > 0 ? { code, map: null } : null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* 创建 Vue Router Promise 兼容 Vite 插件
|
|
47
|
+
*
|
|
48
|
+
* @returns {import('vite').Plugin}
|
|
49
|
+
*/
|
|
50
|
+
export function vueRouterPromisePlugin() {
|
|
51
|
+
return {
|
|
52
|
+
name: 'infly-vue2:vue-router-promise',
|
|
53
|
+
enforce: 'pre',
|
|
54
|
+
transform(code, id) {
|
|
55
|
+
const normalizedId = id.replace(/\\/g, '/').replace(/[?#].*$/, '');
|
|
56
|
+
if (!normalizedId.endsWith('/node_modules/vue-router/dist/vue-router.esm.js')) {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
return transformVueRouterPromiseNavigation(code);
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sass :export 兼容插件
|
|
3
|
+
*
|
|
4
|
+
* 将 `import styles from "./file.scss"` 转换为 SCSS 导入 + 导出变量的 JS 对象。
|
|
5
|
+
* 从 @infly/libs/adapters/vue2/vite/index.js 迁移(95% 可复用)。
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import fs from 'node:fs';
|
|
9
|
+
import path from 'node:path';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* 解析应用内的 SCSS 导入路径
|
|
13
|
+
*/
|
|
14
|
+
function resolveAppScssImport(importPath, importer, projectRoot) {
|
|
15
|
+
const cleanImporter = importer.replace(/[?#].*$/, '');
|
|
16
|
+
if (importPath.startsWith('@/')) {
|
|
17
|
+
return path.resolve(projectRoot, 'src', importPath.slice(2));
|
|
18
|
+
}
|
|
19
|
+
if (importPath.startsWith('./') || importPath.startsWith('../')) {
|
|
20
|
+
return path.resolve(path.dirname(cleanImporter), importPath);
|
|
21
|
+
}
|
|
22
|
+
if (path.isAbsolute(importPath)) return path.resolve(importPath);
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* 转换 import varName from "./file.scss" → import "./file.scss"; const varName = {...}
|
|
28
|
+
*
|
|
29
|
+
* @param {string} code - 源文件内容
|
|
30
|
+
* @param {string} importer - 导入方文件路径
|
|
31
|
+
* @param {object} options - { projectRoot, readFile? }
|
|
32
|
+
* @returns {string} 转换后的代码
|
|
33
|
+
*/
|
|
34
|
+
export function transformScssExportImports(code, importer, options) {
|
|
35
|
+
const { projectRoot, readFile = (file) => fs.readFileSync(file, 'utf8') } = options;
|
|
36
|
+
|
|
37
|
+
return code.replace(
|
|
38
|
+
/import\s+([\w$]+)\s+from\s+(["'])([^"'?]+\.scss)\2\s*;?/g,
|
|
39
|
+
(statement, binding, quote, importPath) => {
|
|
40
|
+
const resolved = resolveAppScssImport(importPath, importer, projectRoot);
|
|
41
|
+
if (!resolved) return statement;
|
|
42
|
+
|
|
43
|
+
let scss;
|
|
44
|
+
try {
|
|
45
|
+
scss = readFile(resolved);
|
|
46
|
+
} catch (_) {
|
|
47
|
+
return statement;
|
|
48
|
+
}
|
|
49
|
+
if (typeof scss !== 'string') return statement;
|
|
50
|
+
|
|
51
|
+
const exportBlock = scss.match(/:export\s*\{([\s\S]*?)\}/);
|
|
52
|
+
if (!exportBlock) return statement;
|
|
53
|
+
|
|
54
|
+
const variables = new Map();
|
|
55
|
+
for (const match of scss.matchAll(/^\s*\$([\w-]+)\s*:\s*([^;]+);/gm)) {
|
|
56
|
+
variables.set(match[1], match[2].trim());
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const exported = {};
|
|
60
|
+
for (const match of exportBlock[1].matchAll(/([\w-]+)\s*:\s*([^;]+);/g)) {
|
|
61
|
+
const rawValue = match[2].trim();
|
|
62
|
+
const variableName = rawValue.match(/^\$([\w-]+)$/);
|
|
63
|
+
exported[match[1]] = variableName && variables.has(variableName[1])
|
|
64
|
+
? variables.get(variableName[1])
|
|
65
|
+
: rawValue;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return `import ${quote}${importPath}${quote};\nconst ${binding} = ${JSON.stringify(exported)};`;
|
|
69
|
+
},
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* 创建 SCSS :export 兼容 Vite 插件
|
|
75
|
+
*
|
|
76
|
+
* @param {string} projectRoot - 项目根目录
|
|
77
|
+
* @returns {import('vite').Plugin}
|
|
78
|
+
*/
|
|
79
|
+
export function createScssExportCompatPlugin(projectRoot) {
|
|
80
|
+
return {
|
|
81
|
+
name: 'infly-vue2:scss-export-compat',
|
|
82
|
+
enforce: 'pre',
|
|
83
|
+
transform(code, id) {
|
|
84
|
+
if (
|
|
85
|
+
id.includes('node_modules')
|
|
86
|
+
|| (!id.includes('.vue') && !/\.[cm]?[jt]sx?(?:[?#].*)?$/.test(id))
|
|
87
|
+
|| !/import\s+[\w$]+\s+from\s+["'][^"']+\.scss["']/.test(code)
|
|
88
|
+
) return null;
|
|
89
|
+
|
|
90
|
+
const transformed = transformScssExportImports(code, id, { projectRoot });
|
|
91
|
+
return transformed === code ? null : { code: transformed, map: null };
|
|
92
|
+
},
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* 将项目根目录添加到 Sass loadPaths
|
|
98
|
+
*
|
|
99
|
+
* @param {object} css - Vite CSS 配置对象
|
|
100
|
+
* @param {string} projectRoot
|
|
101
|
+
* @returns {object}
|
|
102
|
+
*/
|
|
103
|
+
export function addAppRootSassLoadPath(css, projectRoot) {
|
|
104
|
+
if (!css.preprocessorOptions) css.preprocessorOptions = {};
|
|
105
|
+
if (!css.preprocessorOptions.scss) css.preprocessorOptions.scss = {};
|
|
106
|
+
const loadPaths = css.preprocessorOptions.scss.loadPaths || [];
|
|
107
|
+
const resolvedProjectRoot = path.resolve(projectRoot);
|
|
108
|
+
css.preprocessorOptions.scss.loadPaths = loadPaths.includes(resolvedProjectRoot)
|
|
109
|
+
? loadPaths
|
|
110
|
+
: [...loadPaths, resolvedProjectRoot];
|
|
111
|
+
return css;
|
|
112
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { pathToFileURL } from 'node:url';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* 创建旧项目 Sass `~package/path` 导入兼容器。
|
|
7
|
+
*
|
|
8
|
+
* 工厂和临时 Vitest 配置共用同一实现,避免函数经过 JSON 序列化后丢失。
|
|
9
|
+
*/
|
|
10
|
+
export function createSassTildeImporter(projectRoot) {
|
|
11
|
+
const nodeModulesDir = path.resolve(projectRoot, 'node_modules');
|
|
12
|
+
|
|
13
|
+
return {
|
|
14
|
+
findFileUrl(url) {
|
|
15
|
+
if (!url.startsWith('~')) return null;
|
|
16
|
+
const base = url.substring(1);
|
|
17
|
+
for (const candidate of [
|
|
18
|
+
path.resolve(nodeModulesDir, base),
|
|
19
|
+
path.resolve(nodeModulesDir, base + '.scss'),
|
|
20
|
+
path.resolve(nodeModulesDir, base + '.css'),
|
|
21
|
+
path.resolve(nodeModulesDir, base, '_index.scss'),
|
|
22
|
+
path.resolve(nodeModulesDir, base, 'index.scss'),
|
|
23
|
+
path.resolve(nodeModulesDir, base, 'index.css'),
|
|
24
|
+
]) {
|
|
25
|
+
if (fs.existsSync(candidate)) return pathToFileURL(candidate);
|
|
26
|
+
}
|
|
27
|
+
return null;
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SVG Sprite 图标兼容
|
|
3
|
+
*
|
|
4
|
+
* vite-plugin-svg-icons-ng 包装器。
|
|
5
|
+
* 基于 registries 中的配置自动创建 SVG 图标插件。
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import path from 'node:path';
|
|
9
|
+
import { createSvgIconsPlugin } from 'vite-plugin-svg-icons-ng';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* 为项目创建 SVG icons Vite 插件
|
|
13
|
+
*
|
|
14
|
+
* @param {object} options
|
|
15
|
+
* @param {string} options.projectRoot - 项目根目录
|
|
16
|
+
* @param {string} [options.iconDir] - SVG 图标目录(默认 src/icons/svg)
|
|
17
|
+
* @param {string} [options.symbolId] - SVG symbol ID 格式(默认 icon-[name])
|
|
18
|
+
* @returns {import('vite').Plugin}
|
|
19
|
+
*/
|
|
20
|
+
export function createSvgIconPlugin(options = {}) {
|
|
21
|
+
const {
|
|
22
|
+
projectRoot,
|
|
23
|
+
iconDir = 'src/icons/svg',
|
|
24
|
+
symbolId = 'icon-[name]',
|
|
25
|
+
} = options;
|
|
26
|
+
|
|
27
|
+
const iconDirs = [path.resolve(projectRoot, iconDir)];
|
|
28
|
+
|
|
29
|
+
return createSvgIconsPlugin({
|
|
30
|
+
iconDirs,
|
|
31
|
+
symbolId,
|
|
32
|
+
inject: 'body-last',
|
|
33
|
+
customDomId: '__svg__icons__dom__',
|
|
34
|
+
});
|
|
35
|
+
}
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 配置发现编排器
|
|
3
|
+
*
|
|
4
|
+
* 按优先级(从低到高)加载所有配置源并合并:
|
|
5
|
+
* 1. 包安全默认值
|
|
6
|
+
* 2. 项目 vue.config.js 标准字段
|
|
7
|
+
* 3. package.json 的 inflyVite 字段
|
|
8
|
+
* 4. infly.vite.config.js / .cjs / .mjs
|
|
9
|
+
* 5. CLI 参数
|
|
10
|
+
*
|
|
11
|
+
* 规范 §9.1
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import fs from 'node:fs';
|
|
15
|
+
import path from 'node:path';
|
|
16
|
+
import { createRequire } from 'node:module';
|
|
17
|
+
import { createDefaultConfig } from './project-config.mjs';
|
|
18
|
+
import { loadVueCliConfig, extractAdditionalVueConfigInfo } from './vue-cli-reader.mjs';
|
|
19
|
+
import { loadEnvFiles, resolveTarget, buildDefine } from './env.mjs';
|
|
20
|
+
import { deepMerge } from './merge.mjs';
|
|
21
|
+
import { validateStrictCompatibility, diagnoseCompatIssues } from './validation.mjs';
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* 可选的 infly.vite.config 文件名(按优先级)
|
|
25
|
+
*/
|
|
26
|
+
const CONFIG_FILES = [
|
|
27
|
+
'infly.vite.config.mjs',
|
|
28
|
+
'infly.vite.config.js',
|
|
29
|
+
'infly.vite.config.cjs',
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* @param {object} options
|
|
34
|
+
* @param {string} options.command - 'serve' | 'build'
|
|
35
|
+
* @param {string} options.mode - 模式
|
|
36
|
+
* @param {string} [options.target] - CLI target
|
|
37
|
+
* @param {string} options.projectRoot - 项目根目录
|
|
38
|
+
* @param {object} [options.cliOptions] - CLI 额外选项
|
|
39
|
+
* @returns {Promise<object>} 归一化配置 + 元信息
|
|
40
|
+
*/
|
|
41
|
+
export async function discoverConfig(options = {}) {
|
|
42
|
+
const {
|
|
43
|
+
command = 'build',
|
|
44
|
+
mode = 'production',
|
|
45
|
+
target: cliTarget,
|
|
46
|
+
projectRoot = process.cwd(),
|
|
47
|
+
cliOptions = {},
|
|
48
|
+
} = options;
|
|
49
|
+
|
|
50
|
+
// test 命令复用 build 的配置语义(mode=test,读取 .env.test 等)
|
|
51
|
+
const isBuild = command === 'build' || command === 'test';
|
|
52
|
+
const isServe = command === 'serve';
|
|
53
|
+
|
|
54
|
+
// 1. 包默认值
|
|
55
|
+
const defaults = createDefaultConfig({ command, mode, target: cliTarget, projectRoot, isBuild, isServe });
|
|
56
|
+
|
|
57
|
+
// 2. 环境变量(解析 target)
|
|
58
|
+
const { target: envTarget, define } = buildDefine({
|
|
59
|
+
projectRoot,
|
|
60
|
+
mode,
|
|
61
|
+
target: cliTarget,
|
|
62
|
+
defaultTarget: defaults.context.target || 'DEFAULT',
|
|
63
|
+
});
|
|
64
|
+
const resolvedTarget = cliTarget || envTarget || 'DEFAULT';
|
|
65
|
+
|
|
66
|
+
// 3. vue.config.js 标准字段
|
|
67
|
+
const { vueConfig: vueCli, warnings: vueCliWarnings } = loadVueCliConfig(projectRoot, mode);
|
|
68
|
+
const extraInfo = extractAdditionalVueConfigInfo(projectRoot);
|
|
69
|
+
|
|
70
|
+
// 4. package.json inflyVite 字段
|
|
71
|
+
const pkgConfig = loadPackageJsonConfig(projectRoot);
|
|
72
|
+
|
|
73
|
+
// 5. infly.vite.config.js / .cjs / .mjs
|
|
74
|
+
const { config: fileConfig, errors: fileErrors } = await loadInflyViteConfig(projectRoot, {
|
|
75
|
+
command, mode, target: resolvedTarget, projectRoot, isBuild, isServe,
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
// 开始合并
|
|
79
|
+
const merged = deepMerge(
|
|
80
|
+
defaults,
|
|
81
|
+
// vue.config.js → 标准字段映射
|
|
82
|
+
{
|
|
83
|
+
base: vueCli.base,
|
|
84
|
+
outputDir: vueCli.outputDir,
|
|
85
|
+
assetsDir: vueCli.assetsDir,
|
|
86
|
+
productionSourceMap: vueCli.productionSourceMap,
|
|
87
|
+
server: {
|
|
88
|
+
port: vueCli.server?.port,
|
|
89
|
+
proxy: vueCli.server?.proxy,
|
|
90
|
+
},
|
|
91
|
+
css: {
|
|
92
|
+
additionalData: vueCli.css?.additionalData,
|
|
93
|
+
},
|
|
94
|
+
resolve: {
|
|
95
|
+
alias: vueCli.resolve?.alias,
|
|
96
|
+
},
|
|
97
|
+
html: {
|
|
98
|
+
title: vueCli.html?.title,
|
|
99
|
+
},
|
|
100
|
+
mock: extraInfo.mockEntry ? {
|
|
101
|
+
enabled: true,
|
|
102
|
+
entry: extraInfo.mockEntry,
|
|
103
|
+
} : undefined,
|
|
104
|
+
},
|
|
105
|
+
// package.json inflyVite
|
|
106
|
+
pkgConfig,
|
|
107
|
+
// infly.vite.config.js
|
|
108
|
+
fileConfig || {},
|
|
109
|
+
// CLI 参数
|
|
110
|
+
{
|
|
111
|
+
context: { target: resolvedTarget },
|
|
112
|
+
server: {
|
|
113
|
+
port: cliOptions.port,
|
|
114
|
+
host: cliOptions.host,
|
|
115
|
+
open: cliOptions.open,
|
|
116
|
+
strictPort: cliOptions.strictPort,
|
|
117
|
+
},
|
|
118
|
+
}
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
// Mock 默认值:serve 且存在 mock 入口时启用
|
|
122
|
+
if (merged.mock.entry) {
|
|
123
|
+
const mockFullPath = path.resolve(projectRoot, merged.mock.entry);
|
|
124
|
+
if (fs.existsSync(mockFullPath)) {
|
|
125
|
+
merged.mock.enabled = merged.mock.enabled !== false && isServe;
|
|
126
|
+
} else if (merged.mock.enabled) {
|
|
127
|
+
// mock 明确启用但文件不存在 → 记录但由 runner 决定
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// 严格兼容性验证
|
|
132
|
+
let validation = { valid: true, errors: [] };
|
|
133
|
+
let diagnoses = [];
|
|
134
|
+
|
|
135
|
+
if (merged.strictCompatibility) {
|
|
136
|
+
validation = validateStrictCompatibility(merged, {
|
|
137
|
+
hasChainWebpack: vueCli.hasChainWebpack,
|
|
138
|
+
hasFunctionConfigureWebpack: vueCli.hasFunctionConfigureWebpack,
|
|
139
|
+
projectRoot,
|
|
140
|
+
targets: Object.keys(fileConfig?.targets || {}),
|
|
141
|
+
});
|
|
142
|
+
} else {
|
|
143
|
+
diagnoses = diagnoseCompatIssues(merged, {
|
|
144
|
+
hasChainWebpack: vueCli.hasChainWebpack,
|
|
145
|
+
hasFunctionConfigureWebpack: vueCli.hasFunctionConfigureWebpack,
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// 路径规范化
|
|
150
|
+
if (merged.outputDir && !path.isAbsolute(merged.outputDir)) {
|
|
151
|
+
merged.outputDir = path.resolve(projectRoot, merged.outputDir);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return {
|
|
155
|
+
config: merged,
|
|
156
|
+
env: { define, target: resolvedTarget, envVars: {} },
|
|
157
|
+
meta: {
|
|
158
|
+
vueCliWarnings,
|
|
159
|
+
fileErrors,
|
|
160
|
+
validation,
|
|
161
|
+
diagnoses,
|
|
162
|
+
resolvedTarget,
|
|
163
|
+
},
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* 从 package.json 读取 inflyVite 字段
|
|
169
|
+
*/
|
|
170
|
+
function loadPackageJsonConfig(projectRoot) {
|
|
171
|
+
const pkgPath = path.join(projectRoot, 'package.json');
|
|
172
|
+
if (!fs.existsSync(pkgPath)) return {};
|
|
173
|
+
|
|
174
|
+
try {
|
|
175
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
176
|
+
return pkg.inflyVite || {};
|
|
177
|
+
} catch (_) {
|
|
178
|
+
return {};
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* 加载 infly.vite.config.{mjs,js,cjs}
|
|
184
|
+
*
|
|
185
|
+
* @param {string} projectRoot
|
|
186
|
+
* @param {object} context - { command, mode, target, projectRoot, isBuild, isServe }
|
|
187
|
+
* @returns {Promise<{ config: object|null, errors: string[] }>}
|
|
188
|
+
*/
|
|
189
|
+
async function loadInflyViteConfig(projectRoot, context) {
|
|
190
|
+
// 检测多文件冲突
|
|
191
|
+
const found = CONFIG_FILES.filter((name) =>
|
|
192
|
+
fs.existsSync(path.join(projectRoot, name))
|
|
193
|
+
);
|
|
194
|
+
|
|
195
|
+
if (found.length > 1) {
|
|
196
|
+
return {
|
|
197
|
+
config: null,
|
|
198
|
+
errors: [
|
|
199
|
+
`检测到多个 infly.vite.config 文件: ${found.join(', ')}。请只保留一个。`
|
|
200
|
+
],
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (found.length === 0) {
|
|
205
|
+
return { config: null, errors: [] };
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const configPath = path.join(projectRoot, found[0]);
|
|
209
|
+
const ext = path.extname(found[0]);
|
|
210
|
+
|
|
211
|
+
try {
|
|
212
|
+
let rawConfig;
|
|
213
|
+
|
|
214
|
+
if (ext === '.mjs') {
|
|
215
|
+
// ESM 动态导入
|
|
216
|
+
const imported = await import(`file://${configPath}`);
|
|
217
|
+
rawConfig = imported.default || imported;
|
|
218
|
+
} else if (ext === '.js' || ext === '.cjs') {
|
|
219
|
+
// CJS require
|
|
220
|
+
const projectRequire = createRequire(path.join(projectRoot, 'package.json'));
|
|
221
|
+
rawConfig = projectRequire(configPath);
|
|
222
|
+
} else {
|
|
223
|
+
return { config: null, errors: [`不支持的配置文件扩展名: ${ext}`] };
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// 函数形式
|
|
227
|
+
if (typeof rawConfig === 'function') {
|
|
228
|
+
rawConfig = rawConfig(context);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (!rawConfig || typeof rawConfig !== 'object') {
|
|
232
|
+
return { config: null, errors: [`${found[0]} 必须导出配置对象或函数`] };
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return { config: rawConfig, errors: [] };
|
|
236
|
+
} catch (err) {
|
|
237
|
+
return {
|
|
238
|
+
config: null,
|
|
239
|
+
errors: [`无法加载 ${found[0]}: ${err.message}`],
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* 读取 package.json 信息(用于获取项目名称等元数据)
|
|
246
|
+
*/
|
|
247
|
+
export function readProjectMeta(projectRoot) {
|
|
248
|
+
const pkgPath = path.join(projectRoot, 'package.json');
|
|
249
|
+
if (!fs.existsSync(pkgPath)) return { name: 'unknown', version: '0.0.0' };
|
|
250
|
+
|
|
251
|
+
try {
|
|
252
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
253
|
+
return {
|
|
254
|
+
name: pkg.name || path.basename(projectRoot),
|
|
255
|
+
version: pkg.version || '0.0.0',
|
|
256
|
+
description: pkg.description || '',
|
|
257
|
+
};
|
|
258
|
+
} catch (_) {
|
|
259
|
+
return { name: path.basename(projectRoot), version: '0.0.0' };
|
|
260
|
+
}
|
|
261
|
+
}
|