@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,244 @@
1
+ /**
2
+ * HTML 模板兼容层
3
+ *
4
+ * 从项目 public/index.html 生成 Vite 临时入口 HTML,
5
+ * 替换 Webpack EJS 标记,注入 Vite module script。
6
+ * 输出到 node_modules/.cache/infly-vue2-vite/[mode]/[target]/index.html。
7
+ *
8
+ * 从 compat/html.js 迁移(60% 可复用)。
9
+ */
10
+
11
+ import fs from 'node:fs';
12
+ import path from 'node:path';
13
+
14
+ /**
15
+ * 已知的 EJS 标记 → 替换函数映射
16
+ */
17
+ export const KNOWN_EJS_TOKENS = {
18
+ 'BASE_URL': (ctx) => ctx.base || '/',
19
+ 'webpackConfig.name': (ctx) => ctx.title || 'App',
20
+ 'htmlWebpackPlugin.options.buildTimestamp': () => new Date().toLocaleString(),
21
+ 'htmlWebpackPlugin.options.buildVersion': (ctx) => ctx.buildVersion || '0.0.0',
22
+ 'htmlWebpackPlugin.options.buildEnv': (ctx) => ctx.buildEnv || 'development',
23
+ 'htmlWebpackPlugin.options.faviconPath': (ctx) => ctx.favicon || 'favicon.ico',
24
+ };
25
+
26
+ const EJS_PATTERN = /<%=\s*([^%]+)\s*%>/g;
27
+
28
+ function resolveBuildVersion(projectRoot) {
29
+ if (process.env.INFLY_VITE_APP_VERSION) {
30
+ return process.env.INFLY_VITE_APP_VERSION;
31
+ }
32
+ try {
33
+ const packageJson = JSON.parse(
34
+ fs.readFileSync(path.join(projectRoot, 'package.json'), 'utf8'),
35
+ );
36
+ if (typeof packageJson.version === 'string' && packageJson.version.length > 0) {
37
+ return packageJson.version;
38
+ }
39
+ } catch (_) {
40
+ // Missing or invalid package metadata falls back to the compatibility default.
41
+ }
42
+ return '0.0.0';
43
+ }
44
+
45
+ /**
46
+ * 替换 HTML 中的 EJS 标记
47
+ *
48
+ * @param {string} html - 原始 HTML
49
+ * @param {object} ctx - 上下文 { title, favicon, buildVersion, buildEnv, base }
50
+ * @returns {{ html: string, errors: string[] }}
51
+ */
52
+ export function replaceEjsTokens(html, ctx = {}) {
53
+ const errors = [];
54
+ let result = html;
55
+
56
+ result = result.replace(EJS_PATTERN, (match, token) => {
57
+ const trimmed = token.trim();
58
+ if (KNOWN_EJS_TOKENS[trimmed]) {
59
+ return KNOWN_EJS_TOKENS[trimmed](ctx);
60
+ }
61
+ errors.push(`未识别的 EJS 标记: ${trimmed} (完整表达式: ${match})`);
62
+ return match;
63
+ });
64
+
65
+ return { html: result, errors };
66
+ }
67
+
68
+ /**
69
+ * 注入 Vite 入口 script 标签
70
+ *
71
+ * @param {string} html
72
+ * @param {string} mainEntry - 如 'src/main.js'
73
+ * @returns {string}
74
+ */
75
+ export function injectViteEntry(html, mainEntry) {
76
+ const entryScript = ` <script type="module" src="/${mainEntry.replace(/\\/g, '/')}"></script>`;
77
+ const bodyCloseIndex = html.lastIndexOf('</body>');
78
+
79
+ if (bodyCloseIndex === -1) {
80
+ return html + '\n' + entryScript + '\n';
81
+ }
82
+
83
+ return html.slice(0, bodyCloseIndex) + '\n' + entryScript + '\n' + html.slice(bodyCloseIndex);
84
+ }
85
+
86
+ /**
87
+ * 注入 window.BUILD_INFO
88
+ *
89
+ * @param {string} html
90
+ * @param {object} ctx - { buildVersion, buildEnv }
91
+ * @returns {string}
92
+ */
93
+ export function injectBuildInfo(html, ctx = {}) {
94
+ const buildInfoScript = [
95
+ '<script>',
96
+ ' window.BUILD_INFO = {',
97
+ ` timestamp: '${new Date().toLocaleString()}',`,
98
+ ` version: '${ctx.buildVersion || '0.0.0'}',`,
99
+ ` env: '${ctx.buildEnv || 'development'}',`,
100
+ ' };',
101
+ '</script>',
102
+ ].join('\n');
103
+
104
+ const headCloseIndex = html.indexOf('</head>');
105
+ if (headCloseIndex === -1) return buildInfoScript + '\n' + html;
106
+ return html.slice(0, headCloseIndex) + '\n' + buildInfoScript + '\n' + html.slice(headCloseIndex);
107
+ }
108
+
109
+ /**
110
+ * 从项目 public/index.html 生成 Vite 入口 HTML
111
+ *
112
+ * @param {object} options
113
+ * @param {string} options.projectRoot - 项目根目录
114
+ * @param {string} [options.htmlTemplate] - HTML 模板路径(默认 public/index.html)
115
+ * @param {string} [options.mainEntry] - 入口文件(默认 src/main.js)
116
+ * @param {string} [options.title] - 页面标题
117
+ * @param {string} [options.favicon] - favicon 路径
118
+ * @param {string} [options.mode] - 构建模式
119
+ * @param {string} [options.target] - 构建 target
120
+ * @returns {{ html: string, cacheDir: string, indexPath: string }}
121
+ */
122
+ export function generateViteHtml(options = {}) {
123
+ const {
124
+ projectRoot,
125
+ htmlTemplate = 'public/index.html',
126
+ mainEntry = 'src/main.js',
127
+ title,
128
+ favicon = 'favicon.ico',
129
+ mode = 'development',
130
+ target = 'DEFAULT',
131
+ base = '/',
132
+ } = options;
133
+
134
+ const templatePath = path.resolve(projectRoot, htmlTemplate);
135
+
136
+ if (!fs.existsSync(templatePath)) {
137
+ throw new Error(`HTML 模板不存在: ${templatePath}`);
138
+ }
139
+
140
+ const normalizedFavicon = favicon.replace(/^\//, '');
141
+
142
+ const ctx = {
143
+ title: title || path.basename(projectRoot),
144
+ favicon: normalizedFavicon,
145
+ buildVersion: resolveBuildVersion(projectRoot),
146
+ buildEnv: mode,
147
+ base,
148
+ };
149
+
150
+ let html = fs.readFileSync(templatePath, 'utf8');
151
+
152
+ // 替换 EJS 标记
153
+ const { html: replaced, errors } = replaceEjsTokens(html, ctx);
154
+ if (errors.length > 0) {
155
+ throw new Error(
156
+ `HTML 模板 "${templatePath}" 包含未识别的 EJS 标记:\n` +
157
+ errors.map((e) => ` - ${e}`).join('\n')
158
+ );
159
+ }
160
+
161
+ html = replaced;
162
+
163
+ // 添加 favicon link
164
+ if (!/<link\s+rel="icon"/i.test(html) && !/<link\s+rel="shortcut icon"/i.test(html)) {
165
+ const faviconLink = ` <link rel="icon" href="${base}${normalizedFavicon}">`;
166
+ const headCloseIdx = html.indexOf('</head>');
167
+ if (headCloseIdx !== -1) {
168
+ html = html.slice(0, headCloseIdx) + faviconLink + '\n' + html.slice(headCloseIdx);
169
+ }
170
+ }
171
+
172
+ // 注入 Vite module script 入口
173
+ html = injectViteEntry(html, mainEntry);
174
+
175
+ // 注入 build 信息
176
+ if (!/window\.BUILD_INFO\s*=/.test(html)) {
177
+ html = injectBuildInfo(html, ctx);
178
+ }
179
+
180
+ // 写入临时缓存目录
181
+ const cacheDir = path.resolve(
182
+ projectRoot, 'node_modules', '.cache', 'infly-vue2-vite', mode, target
183
+ );
184
+ fs.mkdirSync(cacheDir, { recursive: true });
185
+
186
+ const indexPath = path.join(cacheDir, 'index.html');
187
+ fs.writeFileSync(indexPath, html, 'utf8');
188
+
189
+ // 复制 public/ 中的静态资源到缓存目录
190
+ const publicDir = path.join(projectRoot, 'public');
191
+ if (fs.existsSync(publicDir)) {
192
+ copyPublicAssets(publicDir, cacheDir);
193
+ }
194
+
195
+ return { html, cacheDir, indexPath };
196
+ }
197
+
198
+ /**
199
+ * 递归复制 public 目录中的静态资源(跳过 index.html)
200
+ */
201
+ function copyPublicAssets(srcDir, destDir) {
202
+ const entries = fs.readdirSync(srcDir, { withFileTypes: true });
203
+ for (const entry of entries) {
204
+ const srcPath = path.join(srcDir, entry.name);
205
+ const destPath = path.join(destDir, entry.name);
206
+
207
+ if (entry.isDirectory()) {
208
+ fs.mkdirSync(destPath, { recursive: true });
209
+ copyPublicAssets(srcPath, destPath);
210
+ } else if (entry.name !== 'index.html') {
211
+ fs.copyFileSync(srcPath, destPath);
212
+ }
213
+ }
214
+ }
215
+
216
+ /**
217
+ * 检查 HTML 中是否存在未识别的 EJS 标记
218
+ *
219
+ * @param {object} options - { projectRoot, htmlTemplate }
220
+ * @returns {{ valid: boolean, errors: string[] }}
221
+ */
222
+ export function validateEjsTemplate(options = {}) {
223
+ const { projectRoot, htmlTemplate = 'public/index.html' } = options;
224
+ const templatePath = path.resolve(projectRoot, htmlTemplate);
225
+
226
+ if (!fs.existsSync(templatePath)) {
227
+ return { valid: false, errors: [`HTML 模板不存在: ${templatePath}`] };
228
+ }
229
+
230
+ const html = fs.readFileSync(templatePath, 'utf8');
231
+ const errors = [];
232
+ let match;
233
+ const seen = new Set();
234
+
235
+ while ((match = EJS_PATTERN.exec(html)) !== null) {
236
+ const trimmed = match[1].trim();
237
+ if (!KNOWN_EJS_TOKENS[trimmed] && !seen.has(trimmed)) {
238
+ seen.add(trimmed);
239
+ errors.push(`未识别的 EJS 标记 "${trimmed}" 在 ${templatePath}`);
240
+ }
241
+ }
242
+
243
+ return { valid: errors.length === 0, errors };
244
+ }
@@ -0,0 +1,200 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+
4
+ function resolveTargetFile(projectRoot, importerId, importPath) {
5
+ const cleanPath = importPath.replace(/[?#].*$/, '');
6
+ const basePath = cleanPath.startsWith('@/')
7
+ ? path.resolve(projectRoot, 'src', cleanPath.slice(2))
8
+ : path.resolve(path.dirname(importerId.replace(/[?#].*$/, '')), cleanPath);
9
+
10
+ for (const suffix of ['', '.js', '.vue', '/index.js', '/index.vue', '.cjs']) {
11
+ const candidate = basePath + suffix;
12
+ try {
13
+ if (fs.statSync(candidate).isFile()) return candidate;
14
+ } catch (_) {
15
+ // Continue through the supported resolution suffixes.
16
+ }
17
+ }
18
+ return null;
19
+ }
20
+
21
+ function readTarget(projectRoot, importerId, importPath) {
22
+ const target = resolveTargetFile(projectRoot, importerId, importPath);
23
+ if (!target) return null;
24
+ try {
25
+ return fs.readFileSync(target, 'utf8');
26
+ } catch (_) {
27
+ return null;
28
+ }
29
+ }
30
+
31
+ function isCommonJsTarget(projectRoot, importerId, importPath) {
32
+ return readTarget(projectRoot, importerId, importPath)?.includes('module.exports') === true;
33
+ }
34
+
35
+ function isReExportTarget(projectRoot, importerId, importPath) {
36
+ const source = readTarget(projectRoot, importerId, importPath);
37
+ if (!source) return false;
38
+ const lines = source
39
+ .split('\n')
40
+ .map((line) => line.trim())
41
+ .filter((line) => line && !line.startsWith('//'));
42
+ const isReExport = (line) => (
43
+ /^export\s*\{[^}]*\}\s*from\s*['"]/.test(line)
44
+ || /^export\s+\*\s+from\s*['"]/.test(line)
45
+ );
46
+ return lines.length > 0 && (
47
+ lines.every(isReExport)
48
+ || (lines.length <= 20 && lines.slice(0, 5).every(isReExport))
49
+ );
50
+ }
51
+
52
+ function stripJavaScriptComments(source) {
53
+ return source
54
+ .replace(/\/\*[\s\S]*?\*\//g, '')
55
+ .replace(/\/\/.*$/gm, '');
56
+ }
57
+
58
+ function parseNamedImportSpecifiers(names) {
59
+ const cleaned = stripJavaScriptComments(names).trim();
60
+ if (!cleaned) return null;
61
+
62
+ const specifiers = cleaned
63
+ .split(',')
64
+ .map((specifier) => specifier.trim())
65
+ .filter(Boolean)
66
+ .map((specifier) => {
67
+ const match = specifier.match(
68
+ /^([A-Za-z_$][\w$]*)(?:\s+as\s+([A-Za-z_$][\w$]*))?$/,
69
+ );
70
+ if (!match) return null;
71
+ return { imported: match[1], local: match[2] || match[1] };
72
+ });
73
+
74
+ return specifiers.length > 0 && specifiers.every(Boolean) ? specifiers : null;
75
+ }
76
+
77
+ function collectNamedExports(source) {
78
+ const cleanSource = stripJavaScriptComments(source);
79
+ if (/\bexport\s+\*\s+from\s*['"]/.test(cleanSource)) return null;
80
+
81
+ const namedExports = new Set();
82
+ if (/\bexport\s+default\b/.test(cleanSource)) namedExports.add('default');
83
+
84
+ for (const match of cleanSource.matchAll(
85
+ /\bexport\s+(?:async\s+)?(?:function|class)\s+([A-Za-z_$][\w$]*)/g,
86
+ )) {
87
+ namedExports.add(match[1]);
88
+ }
89
+
90
+ for (const match of cleanSource.matchAll(
91
+ /\bexport\s+(?:const|let|var)\s+([^;\n]+)/g,
92
+ )) {
93
+ for (const declaration of match[1].split(',')) {
94
+ const name = declaration.trim().match(/^([A-Za-z_$][\w$]*)/);
95
+ if (name) namedExports.add(name[1]);
96
+ }
97
+ }
98
+
99
+ for (const match of cleanSource.matchAll(/\bexport\s*\{([^}]+)\}/g)) {
100
+ for (const specifier of match[1].split(',')) {
101
+ const parts = specifier.trim().split(/\s+as\s+/);
102
+ const exportedName = (parts[1] || parts[0]).trim();
103
+ if (exportedName) namedExports.add(exportedName);
104
+ }
105
+ }
106
+
107
+ for (const match of cleanSource.matchAll(
108
+ /\bexport\s+\*\s+as\s+([A-Za-z_$][\w$]*)\s+from\s*['"]/g,
109
+ )) {
110
+ namedExports.add(match[1]);
111
+ }
112
+
113
+ return namedExports;
114
+ }
115
+
116
+ function findMissingNamedImports(names, projectRoot, importerId, importPath) {
117
+ const specifiers = parseNamedImportSpecifiers(names);
118
+ if (!specifiers) return null;
119
+
120
+ const target = resolveTargetFile(projectRoot, importerId, importPath);
121
+ if (!target || path.extname(target) === '.vue') return null;
122
+
123
+ const source = readTarget(projectRoot, importerId, importPath);
124
+ if (source === null || isCommonJsTarget(projectRoot, importerId, importPath)) {
125
+ return null;
126
+ }
127
+
128
+ const namedExports = collectNamedExports(source);
129
+ if (!namedExports) return null;
130
+
131
+ const missing = specifiers.filter(({ imported }) => !namedExports.has(imported));
132
+ return missing.length > 0 ? { missing, specifiers } : null;
133
+ }
134
+
135
+ export function transformImportInterop(code, id, projectRoot, warn) {
136
+ if (id.includes('node_modules') || id.includes('.vite') || id.includes('.cache')) return null;
137
+ if (/\.(ts|tsx)(?:[?#].*)?$/.test(id)) return null;
138
+
139
+ let counter = 0;
140
+ let changed = false;
141
+ let result = code.replace(
142
+ /import\s+\{([^}]+)\}\s+from\s+['"](\.\/[^'"]+|\.\.\/[^'"]+|@\/[^'"]+)['"]/g,
143
+ (match, names, importPath) => {
144
+ if (importPath.includes('.vue')) return match;
145
+ if (!isCommonJsTarget(projectRoot, id, importPath)) {
146
+ const missingImports = findMissingNamedImports(names, projectRoot, id, importPath);
147
+ if (!missingImports) return match;
148
+
149
+ const variable = `__infly_named_import_${counter++}`;
150
+ const destructuring = missingImports.specifiers
151
+ .map(({ imported, local }) => imported === local ? imported : `${imported}: ${local}`)
152
+ .join(', ');
153
+ const missingNames = missingImports.missing.map(({ imported }) => imported).join(', ');
154
+ warn?.(
155
+ `[infly-vue2] Missing named export(s) "${missingNames}" from "${importPath}" in "${id}"; using undefined fallback.`,
156
+ );
157
+ changed = true;
158
+ return `import * as ${variable} from '${importPath}';\nconst { ${destructuring} } = ${variable};`;
159
+ }
160
+ const variable = `__infly_interop_${counter++}`;
161
+ changed = true;
162
+ return `import ${variable} from '${importPath}';\nconst { ${names.trim()} } = ${variable}.default || ${variable};`;
163
+ },
164
+ );
165
+
166
+ result = result.replace(
167
+ /import\s+(\w+)\s+from\s+['"](\.\/[^'"]+|\.\.\/[^'"]+|@\/[^'"]+)['"]/g,
168
+ (match, variableName, importPath) => {
169
+ if (variableName.startsWith('__infly_interop_')) return match;
170
+ if (importPath.includes('.vue')) return match;
171
+ if (isReExportTarget(projectRoot, id, importPath)) {
172
+ changed = true;
173
+ return `import * as ${variableName} from '${importPath}';`;
174
+ }
175
+ if (isCommonJsTarget(projectRoot, id, importPath)) {
176
+ const variable = `__infly_interop_${counter++}`;
177
+ changed = true;
178
+ return `import ${variable} from '${importPath}';\nconst ${variableName} = ${variable}.default || ${variable};`;
179
+ }
180
+ return match;
181
+ },
182
+ );
183
+
184
+ return changed ? { code: result, map: null } : null;
185
+ }
186
+
187
+ export function importInteropPlugin(projectRoot) {
188
+ return {
189
+ name: 'infly-vue2:import-interop',
190
+ enforce: 'pre',
191
+ transform(code, id) {
192
+ return transformImportInterop(
193
+ code,
194
+ id,
195
+ projectRoot,
196
+ (message) => this.warn(message),
197
+ );
198
+ },
199
+ };
200
+ }
@@ -0,0 +1,142 @@
1
+ /**
2
+ * Vue 2 JSX 兼容
3
+ *
4
+ * 使用 Babel + @vue/babel-preset-jsx 将 JSX 语法转换为 h() 调用。
5
+ * 同时处理 .js 文件和 .vue 文件的 script 块。
6
+ * 回退方案:Vite 内置 esbuild。
7
+ *
8
+ * 从 factory.js transformJsx 和相关插件提取。
9
+ */
10
+
11
+ import { createRequire } from 'node:module';
12
+ import path from 'node:path';
13
+
14
+ /**
15
+ * 将 JSX 代码转换为 JS
16
+ *
17
+ * @param {string} code - 源代码
18
+ * @param {string} id - 文件路径
19
+ * @param {string} projectRoot - 项目根目录
20
+ * @returns {{ code: string } | null}
21
+ */
22
+ export function transformJsx(code, id, projectRoot) {
23
+ const projectRequire = createRequire(path.join(projectRoot, 'package.json'));
24
+
25
+ // 优先使用 Babel + @vue/babel-preset-jsx
26
+ try {
27
+ const babel = projectRequire('@babel/core');
28
+ const jsxPreset = (() => {
29
+ try {
30
+ return projectRequire('@vue/babel-preset-jsx');
31
+ } catch (_) {
32
+ return null;
33
+ }
34
+ })();
35
+
36
+ if (jsxPreset) {
37
+ const r = babel.transformSync(code, {
38
+ presets: [[jsxPreset, { injectH: true }]],
39
+ filename: id,
40
+ babelrc: false,
41
+ configFile: false,
42
+ compact: false,
43
+ shouldPrintComment: () => false,
44
+ minified: false,
45
+ });
46
+ return { code: r.code };
47
+ }
48
+ } catch (_) {
49
+ // 回退到 esbuild
50
+ }
51
+
52
+ // 回退:Vite 内置 esbuild
53
+ try {
54
+ const viteReq = createRequire(import.meta.resolve('vite'));
55
+ const esbuild = viteReq('esbuild');
56
+ const r = esbuild.transformSync(code, {
57
+ loader: 'jsx',
58
+ jsxFactory: 'h',
59
+ jsxFragment: 'Fragment',
60
+ });
61
+ return { code: r.code };
62
+ } catch (_) {
63
+ return null;
64
+ }
65
+ }
66
+
67
+ /**
68
+ * 判断文件是否为 .vue 且包含 JSX script
69
+ */
70
+ function isJsxCode(code) {
71
+ return code.includes('</') || code.includes('/>');
72
+ }
73
+
74
+ /**
75
+ * 创建 JSX 预处理插件(enforce: pre,在 @vitejs/plugin-vue2 之前运行)
76
+ *
77
+ * @param {string} projectRoot
78
+ * @returns {import('vite').Plugin}
79
+ */
80
+ export function jsxPrePlugin(projectRoot) {
81
+ return {
82
+ name: 'infly-vue2:jsx-pre',
83
+ enforce: 'pre',
84
+ transform(code, id) {
85
+ // 仅处理项目源码中的 .js 文件
86
+ if (!id.endsWith('.js')) return null;
87
+ if (id.includes('node_modules') || id.includes('.cache') || id.includes('.vite')) return null;
88
+ if (!isJsxCode(code)) return null;
89
+ return transformJsx(code, id, projectRoot);
90
+ },
91
+ };
92
+ }
93
+
94
+ /**
95
+ * 创建 .vue script 块 JSX 预处理插件
96
+ *
97
+ * @param {string} projectRoot
98
+ * @returns {import('vite').Plugin}
99
+ */
100
+ export function vueJsxPrePlugin(projectRoot) {
101
+ return {
102
+ name: 'infly-vue2:vue-jsx-pre',
103
+ enforce: 'pre',
104
+ transform(code, id) {
105
+ if (!id.endsWith('.vue') || id.includes('node_modules')) return null;
106
+ if (!code.includes('<script')) return null;
107
+
108
+ let transformed = false;
109
+ const result = code.replace(
110
+ /<script\b([^>]*)>([\s\S]*?)<\/script>/g,
111
+ (match, attrs, content) => {
112
+ if (isJsxCode(content)) {
113
+ const r = transformJsx(content, id, projectRoot);
114
+ if (r) {
115
+ transformed = true;
116
+ return `<script${attrs}>${r.code}</script>`;
117
+ }
118
+ }
119
+ return match;
120
+ }
121
+ );
122
+ return transformed ? { code: result, map: null } : null;
123
+ },
124
+ };
125
+ }
126
+
127
+ /**
128
+ * 创建 JSX 清理插件(在 @vitejs/plugin-vue2 后捕获残留 JSX)
129
+ *
130
+ * @param {string} projectRoot
131
+ * @returns {import('vite').Plugin}
132
+ */
133
+ export function jsxCleanupPlugin(projectRoot) {
134
+ return {
135
+ name: 'infly-vue2:jsx-cleanup',
136
+ transform(code, id) {
137
+ if (id.includes('node_modules') || id.includes('.vite')) return null;
138
+ if (!isJsxCode(code)) return null;
139
+ return transformJsx(code, id, projectRoot);
140
+ },
141
+ };
142
+ }
@@ -0,0 +1,125 @@
1
+ /**
2
+ * Mock 服务器兼容层
3
+ *
4
+ * 将项目的 Express mock-server.js 作为中间件挂载到 Vite dev server。
5
+ * 保留 chokidar 热重载功能。
6
+ *
7
+ * 从 compat/mock.js 迁移(70% 可复用)。
8
+ */
9
+
10
+ import path from 'node:path';
11
+ import fs from 'node:fs';
12
+ import Module, { createRequire } from 'node:module';
13
+ import { fileURLToPath } from 'node:url';
14
+
15
+ const PACKAGE_ROOT = path.resolve(
16
+ path.dirname(fileURLToPath(import.meta.url)),
17
+ '../..',
18
+ );
19
+ const packageRequire = createRequire(import.meta.url);
20
+
21
+ /**
22
+ * 在 Babel 转译下运行回调函数(兼容 CJS mock 代码)
23
+ */
24
+ function withMockCommonJsTranspilation(projectRoot, callback) {
25
+ let register;
26
+ try {
27
+ register = packageRequire('@babel/register');
28
+ } catch (_) {
29
+ throw new Error(
30
+ '@infly/vue2-vite 安装不完整:缺少 @babel/register'
31
+ );
32
+ }
33
+ register({
34
+ babelrc: false,
35
+ configFile: false,
36
+ extensions: ['.js'],
37
+ ignore: [/node_modules/],
38
+ only: [projectRoot],
39
+ presets: [[
40
+ packageRequire.resolve('@babel/preset-env'),
41
+ { targets: { node: 'current' }, modules: 'commonjs' },
42
+ ]],
43
+ });
44
+ return callback();
45
+ }
46
+
47
+ function loadMockModule(mockServerPath, projectRoot) {
48
+ const mockModule = new Module(mockServerPath);
49
+ mockModule.filename = mockServerPath;
50
+ mockModule.paths = [
51
+ ...Module._nodeModulePaths(projectRoot),
52
+ ...Module._nodeModulePaths(PACKAGE_ROOT),
53
+ ];
54
+ mockModule._compile(fs.readFileSync(mockServerPath, 'utf8'), mockServerPath);
55
+ return mockModule.exports;
56
+ }
57
+
58
+ /**
59
+ * 创建 Vite configureServer 钩子,挂载 Express mock 子应用
60
+ */
61
+ export function createMockMiddleware(options = {}) {
62
+ const { projectRoot, mockEntry = 'mock/mock-server.js', enabled = true } = options;
63
+
64
+ if (!enabled) return undefined;
65
+
66
+ const mockServerPath = path.resolve(projectRoot, mockEntry);
67
+
68
+ if (!fs.existsSync(mockServerPath)) {
69
+ console.warn(`[infly-vue2] mock 已启用但文件不存在: ${mockServerPath}`);
70
+ return undefined;
71
+ }
72
+
73
+ return function configureMockServer(server) {
74
+ try {
75
+ // 用包自身的 createRequire 加载 express(@infly/vue2-vite 的依赖)
76
+ const express = packageRequire('express');
77
+ const mockApp = express();
78
+
79
+ // 切换 cwd 到项目目录,使 mock-server 中相对路径正确
80
+ const originalCwd = process.cwd();
81
+ process.chdir(projectRoot);
82
+
83
+ try {
84
+ // mock 入口属于应用,但它的运行依赖由本包提供。组合两侧的
85
+ // node_modules 搜索路径,避免要求业务子仓重复声明构建适配依赖。
86
+ const mockServer = withMockCommonJsTranspilation(
87
+ projectRoot,
88
+ () => loadMockModule(mockServerPath, projectRoot),
89
+ );
90
+ mockServer(mockApp);
91
+
92
+ // 挂载到 Vite dev server
93
+ server.middlewares.use(mockApp);
94
+ } finally {
95
+ process.chdir(originalCwd);
96
+ }
97
+ } catch (error) {
98
+ console.warn(
99
+ `[infly-vue2] 警告:无法加载 mock 服务器 (${mockServerPath}): ${error.message}`
100
+ );
101
+ console.warn(`[infly-vue2] mock 加载失败,dev server 仍可正常使用。`);
102
+ }
103
+ };
104
+ }
105
+
106
+ export function hasMockServer(options = {}) {
107
+ const {
108
+ projectRoot,
109
+ mockEntry = 'mock/mock-server.js',
110
+ enabled = true,
111
+ } = options;
112
+ return enabled && fs.existsSync(path.resolve(projectRoot, mockEntry));
113
+ }
114
+
115
+ /**
116
+ * 创建 mock 兼容 Vite 插件(含 configureServer 钩子)
117
+ */
118
+ export function mockPlugin(options = {}) {
119
+ const configureServer = createMockMiddleware(options);
120
+
121
+ return {
122
+ name: 'infly-vue2:mock',
123
+ configureServer,
124
+ };
125
+ }