@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.
@@ -0,0 +1,148 @@
1
+ /**
2
+ * 环境变量加载
3
+ *
4
+ * 从项目目录加载 .env 文件,仅白名单变量注入浏览器。
5
+ * 从 compat/env.js 迁移(95% 可复用)。
6
+ *
7
+ * 文件加载顺序(从低到高):
8
+ * .env → .env.local → .env.[mode] → .env.[mode].local
9
+ *
10
+ * 浏览器变量白名单:
11
+ * VUE_APP_*, NODE_ENV, ENV, BASE_URL
12
+ */
13
+
14
+ import fs from 'node:fs';
15
+ import path from 'node:path';
16
+
17
+ /**
18
+ * 简易 .env 文件解析器(不依赖 dotenv)
19
+ * 支持:KEY=VALUE, KEY="VALUE", KEY='VALUE', 注释行 #
20
+ *
21
+ * @param {string} filePath
22
+ * @returns {Record<string, string>}
23
+ */
24
+ export function parseEnvFile(filePath) {
25
+ if (!fs.existsSync(filePath)) return {};
26
+
27
+ const content = fs.readFileSync(filePath, 'utf8');
28
+ const result = {};
29
+
30
+ for (const rawLine of content.split(/\r?\n/)) {
31
+ const line = rawLine.trim();
32
+ if (!line || line.startsWith('#')) continue;
33
+
34
+ const eqIndex = line.indexOf('=');
35
+ if (eqIndex === -1) continue;
36
+
37
+ const key = line.slice(0, eqIndex).trim();
38
+ let value = line.slice(eqIndex + 1).trim();
39
+
40
+ // 去除引号
41
+ if ((value.startsWith('"') && value.endsWith('"')) ||
42
+ (value.startsWith("'") && value.endsWith("'"))) {
43
+ value = value.slice(1, -1);
44
+ }
45
+
46
+ result[key] = value;
47
+ }
48
+
49
+ return result;
50
+ }
51
+
52
+ /**
53
+ * 按优先级合并多个 .env 文件
54
+ *
55
+ * @param {string} projectRoot - 项目根目录
56
+ * @param {string} mode - 构建模式
57
+ * @returns {Record<string, string>}
58
+ */
59
+ export function loadEnvFiles(projectRoot, mode) {
60
+ const baseEnv = parseEnvFile(path.join(projectRoot, '.env'));
61
+ const localEnv = parseEnvFile(path.join(projectRoot, '.env.local'));
62
+ const modeEnv = parseEnvFile(path.join(projectRoot, `.env.${mode}`));
63
+ const modeLocalEnv = parseEnvFile(path.join(projectRoot, `.env.${mode}.local`));
64
+
65
+ return { ...baseEnv, ...localEnv, ...modeEnv, ...modeLocalEnv };
66
+ }
67
+
68
+ const VUE_APP_PREFIX = 'VUE_APP_';
69
+
70
+ /**
71
+ * production-like mode 集合
72
+ */
73
+ const PRODUCTION_LIKE_MODES = new Set([
74
+ 'production', 'prod', 'staging', 'stage', 'release',
75
+ ]);
76
+
77
+ /**
78
+ * 生成 Vite define 配置
79
+ *
80
+ * 映射规则:
81
+ * - 所有 VUE_APP_* → process.env.VUE_APP_*
82
+ * - NODE_ENV → process.env.NODE_ENV(production-like mode 时为 "production")
83
+ * - ENV → process.env.ENV
84
+ * - VUE_APP_PLATFORM → process.env.VUE_APP_PLATFORM(target 优先)
85
+ *
86
+ * @param {object} options
87
+ * @param {string} options.projectRoot - 项目根目录
88
+ * @param {string} options.mode - 构建模式
89
+ * @param {string} [options.target] - CLI 或环境变量指定的 target
90
+ * @param {string} [options.defaultTarget] - 默认 target
91
+ * @returns {{ define: Record<string, string>, target: string, envVars: Record<string, string> }}
92
+ */
93
+ export function buildDefine(options = {}) {
94
+ const {
95
+ projectRoot,
96
+ mode = 'development',
97
+ target,
98
+ defaultTarget = 'DEFAULT',
99
+ base = '/',
100
+ } = options;
101
+ const envVars = loadEnvFiles(projectRoot, mode);
102
+
103
+ const define = {};
104
+
105
+ // 白名单注入 VUE_APP_* 变量
106
+ for (const [key, value] of Object.entries(envVars)) {
107
+ if (key.startsWith(VUE_APP_PREFIX)) {
108
+ define[`process.env.${key}`] = JSON.stringify(value);
109
+ }
110
+ }
111
+
112
+ // 标准变量
113
+ define['process.env.NODE_ENV'] = JSON.stringify(
114
+ PRODUCTION_LIKE_MODES.has(mode) ? 'production' : 'development'
115
+ );
116
+ define['process.env.ENV'] = JSON.stringify(envVars.ENV || mode);
117
+ define['process.env.BASE_URL'] = JSON.stringify(base);
118
+
119
+ // VUE_APP_PLATFORM(target)优先级:CLI > 环境变量 > 默认值
120
+ const resolvedTarget = target ||
121
+ process.env.VUE_APP_PLATFORM ||
122
+ envVars.VUE_APP_PLATFORM ||
123
+ defaultTarget;
124
+ define['process.env.VUE_APP_PLATFORM'] = JSON.stringify(resolvedTarget);
125
+
126
+ // 确保 VUE_APP_BASE_API 存在(mock 服务器需要)
127
+ if (!define['process.env.VUE_APP_BASE_API']) {
128
+ define['process.env.VUE_APP_BASE_API'] = JSON.stringify(envVars.VUE_APP_BASE_API || '');
129
+ }
130
+
131
+ return { define, target: resolvedTarget, envVars };
132
+ }
133
+
134
+ /**
135
+ * 获取解析后的 target 值(不生成 define)
136
+ *
137
+ * @param {object} options
138
+ * @returns {string}
139
+ */
140
+ export function resolveTarget(options = {}) {
141
+ const { projectRoot, mode = 'development', target, defaultTarget = 'DEFAULT' } = options;
142
+ const envVars = loadEnvFiles(projectRoot, mode);
143
+
144
+ return target ||
145
+ process.env.VUE_APP_PLATFORM ||
146
+ envVars.VUE_APP_PLATFORM ||
147
+ defaultTarget;
148
+ }
@@ -0,0 +1,65 @@
1
+ /**
2
+ * 深度合并工具
3
+ *
4
+ * 将多个配置源按优先级合并。规则:
5
+ * - undefined 值不覆盖已定义值
6
+ * - 对象深度合并(不替换)
7
+ * - 数组替换(不拼接)
8
+ * - null 显式清空
9
+ */
10
+
11
+ /**
12
+ * 深度合并多个配置对象。后面的覆盖前面的。
13
+ *
14
+ * @param {...object} sources - 从低到高优先级的配置源
15
+ * @returns {object}
16
+ */
17
+ export function deepMerge(...sources) {
18
+ const target = {};
19
+
20
+ for (const source of sources) {
21
+ if (!source || typeof source !== 'object') continue;
22
+ mergeInto(target, source);
23
+ }
24
+
25
+ return target;
26
+ }
27
+
28
+ /**
29
+ * 将 source 合并到 target(修改 target)
30
+ */
31
+ function mergeInto(target, source) {
32
+ for (const key of Object.keys(source)) {
33
+ const sourceValue = source[key];
34
+ const targetValue = target[key];
35
+
36
+ // null 显式清空
37
+ if (sourceValue === null) {
38
+ target[key] = null;
39
+ continue;
40
+ }
41
+
42
+ // undefined → 跳过
43
+ if (sourceValue === undefined) {
44
+ continue;
45
+ }
46
+
47
+ // 双方都是普通对象 → 深度合并
48
+ if (isPlainObject(targetValue) && isPlainObject(sourceValue)) {
49
+ target[key] = mergeInto({ ...targetValue }, sourceValue);
50
+ continue;
51
+ }
52
+
53
+ // 否则直接替换(包括数组)
54
+ target[key] = sourceValue;
55
+ }
56
+
57
+ return target;
58
+ }
59
+
60
+ /**
61
+ * 检查值是否为普通对象(非数组、非 null)
62
+ */
63
+ function isPlainObject(value) {
64
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
65
+ }
@@ -0,0 +1,87 @@
1
+ /**
2
+ * 归一化项目配置 schema
3
+ *
4
+ * 所有配置来源(vue.config.js、package.json inflyVite、infly.vite.config.js、CLI 参数)
5
+ * 合并为统一的配置对象。此模块定义默认值和 schema。
6
+ */
7
+
8
+ /**
9
+ * 创建默认配置(包层级安全默认值)
10
+ *
11
+ * @param {object} context - { command, mode, target, projectRoot, isBuild, isServe }
12
+ * @returns {object} 默认配置
13
+ */
14
+ export function createDefaultConfig(context) {
15
+ const { command, mode, target, projectRoot, isBuild, isServe } = context;
16
+
17
+ return {
18
+ // 构建输出
19
+ outputDir: null, // 必须由 vue.config.js 或 infly.vite.config.js 提供
20
+ mainEntry: 'src/main.js',
21
+ htmlTemplate: 'public/index.html',
22
+ assetsDir: 'static',
23
+ base: '/',
24
+ productionSourceMap: false,
25
+
26
+ // 开发服务器
27
+ server: {
28
+ port: null, // 从 vue.config.js devServer.port 或配置提供
29
+ // 单项目直接启动默认开放局域网访问;注册表启动仍可显式覆盖为 localhost。
30
+ host: '0.0.0.0',
31
+ proxy: {},
32
+ open: false,
33
+ strictPort: false,
34
+ },
35
+
36
+ // HTML
37
+ html: {
38
+ title: undefined,
39
+ favicon: 'favicon.ico',
40
+ },
41
+
42
+ // CSS / Sass
43
+ css: {
44
+ additionalData: '',
45
+ preprocessorOptions: {},
46
+ devSourcemap: true,
47
+ },
48
+
49
+ // 模块解析
50
+ resolve: {
51
+ alias: {},
52
+ },
53
+
54
+ // Mock
55
+ mock: {
56
+ enabled: false,
57
+ entry: 'mock/mock-server.js',
58
+ },
59
+
60
+ // 构建策略
61
+ strictCompatibility: false,
62
+
63
+ // 从 vue.config.js 推断的兼容性字段
64
+ vueConfig: {
65
+ configureWebpack: null, // null=不存在, object=普通对象, 'function'=函数形式
66
+ chainWebpack: false,
67
+ transpileDependencies: [],
68
+ },
69
+
70
+ // 上下文(传给 infly.vite.config.js 函数)
71
+ context: {
72
+ command,
73
+ mode,
74
+ target,
75
+ projectRoot,
76
+ isBuild,
77
+ isServe,
78
+ },
79
+ };
80
+ }
81
+
82
+ /**
83
+ * 检查配置对象是否包含有效输出目录
84
+ */
85
+ export function hasOutputDir(config) {
86
+ return config.outputDir !== null && typeof config.outputDir === 'string' && config.outputDir.length > 0;
87
+ }
@@ -0,0 +1,152 @@
1
+ import path from 'node:path';
2
+
3
+ import { createViteConfig } from '../factory/create-vite-config.mjs';
4
+ import { loadEnvFiles } from './env.mjs';
5
+
6
+ function resolveTarget(app, requestedTarget) {
7
+ const target = requestedTarget || app.defaultTarget;
8
+ const targetConfig = app.targets?.[target];
9
+ if (!targetConfig) {
10
+ throw new Error(
11
+ `应用 "${app.id}" 不存在 target "${target}";可用值: ${Object.keys(app.targets || {}).join(', ')}`,
12
+ );
13
+ }
14
+ return { target, targetConfig };
15
+ }
16
+
17
+ function resolveOutputDir(app, targetConfig, rootDir) {
18
+ if (!app.outputRoot || !targetConfig.outputDir) {
19
+ throw new Error(`${app.id} 缺少登记的输出目录`);
20
+ }
21
+
22
+ const outputRoot = path.resolve(rootDir, app.outputRoot);
23
+ const outputDir = path.resolve(rootDir, targetConfig.outputDir);
24
+ const relative = path.relative(outputRoot, outputDir);
25
+ if (!relative || relative.startsWith('..') || path.isAbsolute(relative)) {
26
+ throw new Error(`拒绝不安全的输出目录: ${outputDir}`);
27
+ }
28
+
29
+ const registered = Object.values(app.targets || {}).some(
30
+ (entry) => path.resolve(rootDir, entry.outputDir) === outputDir,
31
+ );
32
+ if (!registered) {
33
+ throw new Error(`输出目录未登记: ${outputDir}`);
34
+ }
35
+ return outputDir;
36
+ }
37
+
38
+ function createProxy(proxy = {}, envVars = {}) {
39
+ const dynamic = proxy.__dynamic;
40
+ if (dynamic) {
41
+ if (typeof dynamic !== 'object' || !dynamic.prefixEnv || !dynamic.target) {
42
+ throw new Error('动态代理缺少 prefixEnv 或 target 配置');
43
+ }
44
+ const prefix = envVars[dynamic.prefixEnv];
45
+ if (!prefix) return {};
46
+ const escaped = prefix.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
47
+ const entry = { ...dynamic };
48
+ delete entry.prefixEnv;
49
+ return {
50
+ [prefix]: {
51
+ ...entry,
52
+ rewrite: (requestPath) => requestPath.replace(new RegExp(`^${escaped}`), ''),
53
+ },
54
+ };
55
+ }
56
+
57
+ return Object.fromEntries(Object.entries(proxy).map(([prefix, rawEntry]) => {
58
+ const entry = { ...rawEntry };
59
+ if (entry.pathRewrite) {
60
+ const pathRewrite = { ...entry.pathRewrite };
61
+ entry.rewrite = (requestPath) => Object.entries(pathRewrite).reduce(
62
+ (result, [pattern, replacement]) => result.replace(new RegExp(pattern), replacement),
63
+ requestPath,
64
+ );
65
+ delete entry.pathRewrite;
66
+ }
67
+ return [prefix, entry];
68
+ }));
69
+ }
70
+
71
+ function createAliases(aliases = {}, rootDir) {
72
+ return Object.fromEntries(
73
+ Object.entries(aliases).map(([name, replacement]) => [
74
+ name,
75
+ path.resolve(rootDir, replacement),
76
+ ]),
77
+ );
78
+ }
79
+
80
+ export function createRegisteredConfig(app, options = {}) {
81
+ const rootDir = path.resolve(options.rootDir || process.cwd());
82
+ const command = options.command || 'build';
83
+ const mode = options.mode || (command === 'serve' ? 'development' : 'production');
84
+ const { target, targetConfig } = resolveTarget(app, options.target);
85
+ const projectRoot = path.resolve(rootDir, app.subProjectPath);
86
+ const envVars = loadEnvFiles(projectRoot, mode);
87
+ const theme = options.theme || targetConfig.theme || 'default';
88
+ const additionalData = app.css?.additionalData
89
+ ? app.css.additionalData.replace(/\$\{theme\}/g, theme)
90
+ : '';
91
+
92
+ return {
93
+ outputDir: resolveOutputDir(app, targetConfig, rootDir),
94
+ mainEntry: app.mainEntry || 'src/main.js',
95
+ htmlTemplate: app.htmlTemplate || 'public/index.html',
96
+ assetsDir: 'static',
97
+ base: targetConfig.base || app.base || '/',
98
+ productionSourceMap: app.productionSourceMap ?? false,
99
+ server: {
100
+ port: options.port || targetConfig.port || app.defaultPort || 3000,
101
+ host: options.host || 'localhost',
102
+ proxy: createProxy(app.proxy, envVars),
103
+ open: options.open || false,
104
+ strictPort: options.strictPort || false,
105
+ },
106
+ html: {
107
+ title: targetConfig.title || app.title || app.id,
108
+ favicon: targetConfig.favicon || app.favicon?.path || 'favicon.ico',
109
+ },
110
+ css: {
111
+ additionalData,
112
+ preprocessorOptions: {},
113
+ devSourcemap: true,
114
+ sassModern: app.css?.sassModern === true,
115
+ },
116
+ resolve: {
117
+ alias: createAliases(app.aliases, rootDir),
118
+ },
119
+ compat: {
120
+ emptyModuleStubs: (app.compat?.emptyModuleStubs || []).map((entry) => ({
121
+ id: path.resolve(projectRoot, entry.path),
122
+ exports: entry.exports || [],
123
+ })),
124
+ },
125
+ mock: {
126
+ enabled: command === 'serve' && app.mock !== false,
127
+ entry: 'mock/mock-server.js',
128
+ },
129
+ build: {
130
+ rollupOptions: {
131
+ external: app.external || [],
132
+ },
133
+ },
134
+ strictCompatibility: false,
135
+ workspace: {
136
+ rootDir,
137
+ appSourcePaths: options.appSourcePaths || [app.subProjectPath],
138
+ },
139
+ context: {
140
+ command,
141
+ mode,
142
+ target,
143
+ projectRoot,
144
+ isBuild: command === 'build',
145
+ isServe: command === 'serve',
146
+ },
147
+ };
148
+ }
149
+
150
+ export function createRegisteredViteConfig(app, options = {}) {
151
+ return createViteConfig(createRegisteredConfig(app, options));
152
+ }
@@ -0,0 +1,122 @@
1
+ /**
2
+ * 严格兼容模式验证
3
+ *
4
+ * 规范 §9.5:strictCompatibility 为 true 时必须检测并失败:
5
+ * 1. 存在未确认的 chainWebpack
6
+ * 2. configureWebpack 是函数
7
+ * 3. outputDir 无法确定
8
+ * 4. main entry 或 HTML 模板不存在
9
+ * 5. 多个项目配置文件同时存在
10
+ * 6. target 不在显式 targets 中
11
+ * 7. Vite 输出目录等于项目根目录、磁盘根目录或用户主目录
12
+ */
13
+
14
+ import fs from 'node:fs';
15
+ import path from 'node:path';
16
+ import os from 'node:os';
17
+
18
+ /**
19
+ * 执行严格兼容检查
20
+ *
21
+ * @param {object} config - 归一化后的完整配置
22
+ * @param {object} options - { hasChainWebpack, hasFunctionConfigureWebpack, projectRoot, targets }
23
+ * @returns {{ valid: boolean, errors: string[] }}
24
+ */
25
+ export function validateStrictCompatibility(config, options = {}) {
26
+ const errors = [];
27
+
28
+ // 1. chainWebpack 存在
29
+ if (options.hasChainWebpack) {
30
+ errors.push(
31
+ '项目使用了 chainWebpack,无法自动转换为 Vite 配置。' +
32
+ '请在 infly.vite.config.js 中显式提供等价的 Vite 配置,或关闭严格兼容模式。'
33
+ );
34
+ }
35
+
36
+ // 2. configureWebpack 是函数
37
+ if (options.hasFunctionConfigureWebpack) {
38
+ errors.push(
39
+ 'configureWebpack 是函数形式,无法自动转换为 Vite 配置。' +
40
+ '请在 infly.vite.config.js 中显式提供等价的 Vite 配置,或关闭严格兼容模式。'
41
+ );
42
+ }
43
+
44
+ // 3. outputDir 无法确定
45
+ if (!config.outputDir || typeof config.outputDir !== 'string') {
46
+ errors.push(
47
+ '无法确定 outputDir。请在 vue.config.js 中设置 outputDir,' +
48
+ '或在 infly.vite.config.js 中显式提供。'
49
+ );
50
+ }
51
+
52
+ // 4. main entry 或 HTML 模板不存在
53
+ if (options.projectRoot) {
54
+ const mainEntry = path.resolve(options.projectRoot, config.mainEntry || 'src/main.js');
55
+ if (!fs.existsSync(mainEntry)) {
56
+ errors.push(`入口文件不存在: ${mainEntry}`);
57
+ }
58
+
59
+ const htmlTemplate = path.resolve(options.projectRoot, config.htmlTemplate || 'public/index.html');
60
+ if (!fs.existsSync(htmlTemplate)) {
61
+ errors.push(`HTML 模板不存在: ${htmlTemplate}`);
62
+ }
63
+ }
64
+
65
+ // 5. 多个项目配置文件(在 discover 阶段检测)
66
+
67
+ // 6. target 验证 - target 在配置中应该存在
68
+ if (options.targets && config.context && config.context.target) {
69
+ const target = config.context.target;
70
+ if (!options.targets.includes(target)) {
71
+ errors.push(
72
+ `未知 target "${target}"。可用值: ${options.targets.join(', ')}`
73
+ );
74
+ }
75
+ }
76
+
77
+ // 7. outputDir 安全检查
78
+ if (config.outputDir && options.projectRoot) {
79
+ const outDir = path.resolve(options.projectRoot, config.outputDir);
80
+ const projectRoot = path.resolve(options.projectRoot);
81
+ const rootDir = path.parse(projectRoot).root;
82
+
83
+ if (outDir === projectRoot) {
84
+ errors.push(`输出目录不能等于项目根目录: ${outDir}`);
85
+ }
86
+ if (outDir === rootDir || outDir === rootDir.slice(0, -1)) {
87
+ errors.push(`输出目录不能为磁盘根目录: ${outDir}`);
88
+ }
89
+ if (outDir === os.homedir()) {
90
+ errors.push(`输出目录不能为用户主目录: ${outDir}`);
91
+ }
92
+ }
93
+
94
+ return {
95
+ valid: errors.length === 0,
96
+ errors,
97
+ };
98
+ }
99
+
100
+ /**
101
+ * 非严格模式诊断(仅产生警告,不失败)
102
+ *
103
+ * @param {object} config
104
+ * @param {object} options
105
+ * @returns {string[]}
106
+ */
107
+ export function diagnoseCompatIssues(config, options = {}) {
108
+ const warnings = [];
109
+
110
+ // chainWebpack 不再产生非严格模式诊断:其行为已由内置默认能力覆盖,
111
+ // 仅严格兼容模式(validateStrictCompatibility)继续校验失败。
112
+
113
+ if (options.hasFunctionConfigureWebpack) {
114
+ warnings.push('[诊断] configureWebpack 是函数形式:纯 JavaScript 配置无法可靠映射为 Vite。请显式提供 Vite 插件或配置。');
115
+ }
116
+
117
+ if (!config.outputDir) {
118
+ warnings.push('[诊断] outputDir 未设置;将输出到默认 dist 目录。');
119
+ }
120
+
121
+ return warnings;
122
+ }