@flun/html-template 4.2.1 → 4.3.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/compile.js +197 -190
- package/customize/hotReloadInjector.js +22 -22
- package/dev-server.js +95 -143
- package/dev.js +12 -1
- package/f-CHANGELOG.md +12 -2
- package/f-README.md +66 -7
- package/index.d.ts +24 -7
- package/package.json +4 -4
- package/services/middleware.js +31 -0
- package/services/templateService.js +147 -3
- package/static/auth.js +1 -1
package/compile.js
CHANGED
|
@@ -5,47 +5,30 @@
|
|
|
5
5
|
* 1. 递归目录复制工具(copyDir)
|
|
6
6
|
* 2. 路由文件处理及入口文件生成
|
|
7
7
|
* - 路由检测(checkUserRoutesExist)
|
|
8
|
-
* - 入口文件生成(generateServerEntry)
|
|
8
|
+
* - 入口文件生成(generateServerEntry)—— 支持沿用开发配置(端口/HTTPS/证书/host)
|
|
9
9
|
* - 依赖管理(mergeDependencies → 返回完整 package.json)
|
|
10
10
|
* 3. 编译模板所有文件(compile)
|
|
11
11
|
* 4. 批量编译主流程(compileAllTemplates)
|
|
12
12
|
* 5. 导出接口与执行编译
|
|
13
13
|
*
|
|
14
|
-
*
|
|
15
|
-
* -
|
|
16
|
-
* -
|
|
17
|
-
* -
|
|
18
|
-
* - 生产环境就绪:自动生成Express服务器和依赖配置
|
|
19
|
-
*
|
|
20
|
-
* 特殊机制:
|
|
21
|
-
* - 编译模式标识:控制包含文件的收集逻辑
|
|
22
|
-
* - 路由功能检测:扫描用户功能文件中的setupRoutes函数
|
|
23
|
-
* - 模块缓存清理:确保路由加载时使用最新代码
|
|
24
|
-
* - Express版本管理:优先使用模板依赖,默认^5.2.1
|
|
14
|
+
* 核心改进:
|
|
15
|
+
* - 复用 parseServerConfig 读取开发阶段配置(端口、HTTPS、证书、主机名)
|
|
16
|
+
* - 生成的 server.js 严格沿用这些配置,不擅自改变默认行为
|
|
17
|
+
* - 若启用 HTTPS 则自动复制证书文件到输出目录/certs
|
|
25
18
|
*/
|
|
26
19
|
import {
|
|
27
|
-
path, fsPromises, CWD, getAvailableTemplates, validateTemplateFile, renderTemplate, processIncludes,
|
|
28
|
-
setCompilationMode, getIncludedFiles, loadUserFeatures, findEntryFile, templatesDir, staticDir, customizeDir
|
|
20
|
+
path, fsPromises, CWD, getAvailableTemplates, parseServerConfig, validateTemplateFile, renderTemplate, processIncludes,
|
|
21
|
+
processVariables, setCompilationMode, getIncludedFiles, loadUserFeatures, findEntryFile, templatesDir, staticDir, customizeDir
|
|
29
22
|
} from './services/templateService.js';
|
|
30
23
|
import PK from './package.json' with { type: 'json' };
|
|
31
24
|
import util from 'util';
|
|
32
25
|
import { exec } from 'child_process';
|
|
33
26
|
import { fileURLToPath, pathToFileURL } from 'url';
|
|
34
27
|
|
|
35
|
-
let cachedPages = [];
|
|
36
|
-
const execPromise = util.promisify(exec),
|
|
28
|
+
let cachedPages = [];
|
|
29
|
+
const __filename = fileURLToPath(import.meta.url), __dirname = path.dirname(__filename), execPromise = util.promisify(exec),
|
|
37
30
|
|
|
38
|
-
// ==================== 1
|
|
39
|
-
/**
|
|
40
|
-
* 目录结构克隆工具(含错误抑制)
|
|
41
|
-
* @param {string} src - 源目录路径
|
|
42
|
-
* @param {string} destDir - 目标目录路径
|
|
43
|
-
*
|
|
44
|
-
* 特性:
|
|
45
|
-
* - 自动创建目标目录结构
|
|
46
|
-
* - 跳过不存在的源目录(不报错)
|
|
47
|
-
* - 保留子目录结构递归复制
|
|
48
|
-
*/
|
|
31
|
+
// ==================== 1. 递归目录复制工具 ====================
|
|
49
32
|
copyDir = async (src, destDir) => {
|
|
50
33
|
try {
|
|
51
34
|
await fsPromises.mkdir(destDir, { recursive: true });
|
|
@@ -60,12 +43,7 @@ const execPromise = util.promisify(exec),
|
|
|
60
43
|
}
|
|
61
44
|
},
|
|
62
45
|
|
|
63
|
-
// ==================== 2
|
|
64
|
-
|
|
65
|
-
/**
|
|
66
|
-
* 检测用户是否定义路由功能(兼容默认导出与具名导出)
|
|
67
|
-
* @returns {Promise<boolean>} 是否存在有效路由
|
|
68
|
-
*/
|
|
46
|
+
// ==================== 2. 路由文件处理及入口文件生成 ====================
|
|
69
47
|
checkUserRoutesExist = async () => {
|
|
70
48
|
try {
|
|
71
49
|
const featuresDir = path.join(CWD, customizeDir);
|
|
@@ -77,9 +55,7 @@ const execPromise = util.promisify(exec),
|
|
|
77
55
|
moduleUrl.search = 'update=' + Date.now();
|
|
78
56
|
const mod = await import(moduleUrl.href), feature = mod.default?.setupRoutes ? mod.default : mod;
|
|
79
57
|
if (typeof feature.setupRoutes === 'function') return true;
|
|
80
|
-
} catch (e) {
|
|
81
|
-
console.warn(`⚠️ 检查路由文件 ${file} 失败:`, e.message);
|
|
82
|
-
}
|
|
58
|
+
} catch (e) { /* ignore */ }
|
|
83
59
|
}
|
|
84
60
|
return false;
|
|
85
61
|
} catch {
|
|
@@ -87,30 +63,23 @@ const execPromise = util.promisify(exec),
|
|
|
87
63
|
}
|
|
88
64
|
},
|
|
89
65
|
|
|
90
|
-
|
|
91
|
-
* 合并用户项目依赖与模板工具依赖,生成完整的 package.json 内容
|
|
92
|
-
* @param {boolean} hasUserRoutes - 是否存在用户自定义路由
|
|
93
|
-
* @returns {Promise<string>} 格式化后的 package.json 字符串
|
|
94
|
-
*/
|
|
95
|
-
mergeDependencies = async hasUserRoutes => {
|
|
66
|
+
mergeDependencies = async (hasUserRoutes) => {
|
|
96
67
|
let basePkg = {}, userDeps = {}, mergedDeps = {};
|
|
97
68
|
const userPkgPath = path.join(CWD, 'package.json');
|
|
98
69
|
try {
|
|
99
70
|
const userPkgRaw = await fsPromises.readFile(userPkgPath, 'utf8'), userPkg = JSON.parse(userPkgRaw);
|
|
100
|
-
basePkg.author = userPkg.author || ''
|
|
71
|
+
basePkg.author = userPkg.author || '';
|
|
72
|
+
basePkg.license = userPkg.license || 'ISC';
|
|
101
73
|
userDeps = userPkg.dependencies || {};
|
|
102
74
|
} catch (err) { }
|
|
103
|
-
|
|
104
75
|
if (hasUserRoutes) {
|
|
105
76
|
const templateDeps = PK.dependencies || {}, excludeList = ['chokidar', 'socket.io', '@flun/html-template'];
|
|
106
77
|
mergedDeps = { ...templateDeps, ...userDeps };
|
|
107
78
|
for (const pkg of excludeList) delete mergedDeps[pkg];
|
|
108
79
|
}
|
|
109
80
|
if (!mergedDeps.express) mergedDeps.express = '^5.2.1';
|
|
110
|
-
|
|
111
81
|
const finalPkg = {
|
|
112
|
-
name: 'dist-server', version: '1.0.0',
|
|
113
|
-
...basePkg,
|
|
82
|
+
name: 'dist-server', version: '1.0.0', ...basePkg,
|
|
114
83
|
type: 'module', main: 'server.js',
|
|
115
84
|
scripts: { dev: 'node server.js' },
|
|
116
85
|
dependencies: mergedDeps,
|
|
@@ -119,11 +88,7 @@ const execPromise = util.promisify(exec),
|
|
|
119
88
|
return JSON.stringify(finalPkg, null, 2);
|
|
120
89
|
},
|
|
121
90
|
|
|
122
|
-
|
|
123
|
-
* 在目标目录中执行 npm install
|
|
124
|
-
* @param {string} targetDir - 目标目录
|
|
125
|
-
*/
|
|
126
|
-
installDependencies = async targetDir => {
|
|
91
|
+
installDependencies = async (targetDir) => {
|
|
127
92
|
console.log('📦 正在安装项目依赖,请稍候...');
|
|
128
93
|
try {
|
|
129
94
|
const { stdout, stderr } = await execPromise('npm install', { cwd: targetDir });
|
|
@@ -137,133 +102,159 @@ const execPromise = util.promisify(exec),
|
|
|
137
102
|
},
|
|
138
103
|
|
|
139
104
|
/**
|
|
140
|
-
* 生成服务端入口文件内容(ESM
|
|
105
|
+
* 生成服务端入口文件内容(ESM 格式),沿用开发配置
|
|
141
106
|
* @param {boolean} hasUserRoutes - 是否存在用户自定义路由
|
|
142
|
-
* @param {string} entryFile -
|
|
143
|
-
* @
|
|
107
|
+
* @param {string} entryFile - 入口文件名
|
|
108
|
+
* @param {object} buildConfig - 编译配置(port, host, httpsEnabled, httpsKeyPath, httpsCertPath)
|
|
109
|
+
* @returns {string} server.js 文件内容
|
|
144
110
|
*/
|
|
145
|
-
generateServerEntry = async (hasUserRoutes, entryFile) => {
|
|
146
|
-
const
|
|
111
|
+
generateServerEntry = async (hasUserRoutes, entryFile, buildConfig) => {
|
|
112
|
+
const { port, host, httpsEnabled, httpsKeyPath, httpsCertPath } = buildConfig,
|
|
113
|
+
|
|
114
|
+
// 基础导入
|
|
115
|
+
baseImports = `import express from 'express';
|
|
147
116
|
import path from 'path';
|
|
148
117
|
import { fileURLToPath, pathToFileURL } from 'url';
|
|
149
|
-
|
|
150
|
-
|
|
118
|
+
import fs from 'fs';
|
|
119
|
+
import http from 'http';
|
|
120
|
+
import https from 'https';
|
|
121
|
+
import { corsMiddleware, trustProxySetting } from './middleware.js';`,
|
|
122
|
+
|
|
123
|
+
// 变量声明
|
|
124
|
+
declarations = `const __filename = fileURLToPath(import.meta.url),
|
|
125
|
+
__dirname = path.dirname(__filename), app = express(), port = ${port}, host = ${JSON.stringify(host)};
|
|
126
|
+
let server, protocol = 'http';`,
|
|
127
|
+
|
|
128
|
+
// 公共中间件
|
|
151
129
|
corsAndSecurity = `
|
|
152
|
-
app.use(
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
res.setHeader('X-Content-Type-Options', 'nosniff');
|
|
159
|
-
res.setHeader('X-Frame-Options', 'SAMEORIGIN');
|
|
160
|
-
res.setHeader('Content-Security-Policy', "frame-ancestors 'self'");
|
|
161
|
-
if (req.method === 'OPTIONS') {
|
|
162
|
-
res.setHeader('Content-Length', '0');
|
|
163
|
-
return res.status(204).end();
|
|
164
|
-
}
|
|
165
|
-
next();
|
|
166
|
-
}), app.set('trust proxy', false);`,
|
|
167
|
-
staticMiddleware = `app.use('/static', express.static(path.join(__dirname, '${staticDir}')));
|
|
130
|
+
app.use(corsMiddleware);
|
|
131
|
+
app.set('trust proxy', trustProxySetting);`,
|
|
132
|
+
|
|
133
|
+
// 静态文件服务
|
|
134
|
+
staticMiddleware = `
|
|
135
|
+
app.use('/static', express.static(path.join(__dirname, '${staticDir}')));
|
|
168
136
|
app.use(express.static(path.join(__dirname, '${templatesDir}')));`,
|
|
169
|
-
defaultRootRoute = `app.get('/', (req, res) => res.redirect('/${entryFile}'));`;
|
|
170
137
|
|
|
171
|
-
|
|
138
|
+
// 默认根路由
|
|
139
|
+
defaultRootRoute = `app.get('/', (req, res) => res.redirect('/${entryFile}'));`,
|
|
140
|
+
httpServerCreation = ` server = http.createServer(app), protocol = 'http';`;
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
// 服务器创建代码(仅赋值,不再重复声明)
|
|
144
|
+
let serverCreationCode;
|
|
145
|
+
if (httpsEnabled && httpsKeyPath && httpsCertPath) {
|
|
146
|
+
const safeKeyPath = JSON.stringify(httpsKeyPath), safeCertPath = JSON.stringify(httpsCertPath);
|
|
147
|
+
serverCreationCode = `
|
|
148
|
+
try {
|
|
149
|
+
const options = {
|
|
150
|
+
key: fs.readFileSync(${safeKeyPath}), cert: fs.readFileSync(${safeCertPath})
|
|
151
|
+
};
|
|
152
|
+
server = https.createServer(options, app);
|
|
153
|
+
protocol = 'https';
|
|
154
|
+
console.log(\`🔒 使用 HTTPS,证书路径: ${safeKeyPath}, ${safeCertPath}\`);
|
|
155
|
+
} catch (err) {
|
|
156
|
+
console.error('HTTPS 证书加载失败,降级为 HTTP:', err.message);
|
|
157
|
+
${httpServerCreation}
|
|
158
|
+
}`;
|
|
159
|
+
} else {
|
|
160
|
+
const warning = httpsEnabled ? `console.warn('⚠️ HTTPS 已启用但未提供证书路径,降级为 HTTP 启动');` : '';
|
|
161
|
+
serverCreationCode = `
|
|
162
|
+
${httpServerCreation}
|
|
163
|
+
${warning}`;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// 有用户路由时
|
|
172
167
|
if (hasUserRoutes) {
|
|
173
|
-
return
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
168
|
+
return `${baseImports}
|
|
169
|
+
${declarations}
|
|
170
|
+
|
|
171
|
+
let allRoutes = [];
|
|
172
|
+
const wrapAppMethods = (app) => {
|
|
173
|
+
const methodsToWrap = ['get', 'post', 'put', 'delete', 'patch', 'options', 'head', 'all'];
|
|
174
|
+
const originals = {};
|
|
175
|
+
methodsToWrap.forEach(method => {
|
|
176
|
+
originals[method] = app[method].bind(app);
|
|
177
|
+
app[method] = function(routePath, ...handlers) {
|
|
178
|
+
allRoutes.push({ method: method.toUpperCase(), path: routePath });
|
|
179
|
+
return originals[method](routePath, ...handlers);
|
|
180
|
+
};
|
|
181
|
+
});
|
|
182
|
+
};
|
|
183
|
+
wrapAppMethods(app);
|
|
184
|
+
|
|
185
|
+
const printRoutes = () => {
|
|
186
|
+
if (allRoutes.length) {
|
|
187
|
+
console.log(\` 🗺️ 检测到 \${ allRoutes.length } 条注册路由\`);
|
|
188
|
+
// allRoutes.forEach(r => console.log(\` \${r.method.padEnd(6)} \${r.path}\`));
|
|
189
|
+
} else {
|
|
190
|
+
console.log(' ℹ️ 未找到任何路由');
|
|
191
|
+
}
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
const loadUserRoutes = async () => {
|
|
195
|
+
const featuresDir = path.join(__dirname, '${customizeDir}');
|
|
196
|
+
if (!fs.existsSync(featuresDir)) {
|
|
197
|
+
return console.log(\` ℹ️ \${featuresDir} 目录不存在,跳过路由加载\`);
|
|
198
|
+
}
|
|
199
|
+
const routeFiles = fs.readdirSync(featuresDir).filter(file => file.endsWith('.js'));
|
|
200
|
+
for (const file of routeFiles) {
|
|
201
|
+
try {
|
|
202
|
+
const modulePath = path.join(featuresDir, file);
|
|
203
|
+
const moduleUrl = pathToFileURL(modulePath);
|
|
204
|
+
moduleUrl.search = 'update=' + Date.now();
|
|
205
|
+
const feature = await import(moduleUrl.href);
|
|
206
|
+
if (typeof feature.default?.setupRoutes === 'function') {
|
|
207
|
+
feature.default.setupRoutes(app);
|
|
208
|
+
console.log(\` ✅ 路由加载文件: \${file}\`);
|
|
209
|
+
} else if (typeof feature?.setupRoutes === 'function') {
|
|
210
|
+
feature.setupRoutes(app);
|
|
211
|
+
console.log(\` ✅ 路由加载文件: \${file}\`);
|
|
212
|
+
}
|
|
213
|
+
} catch (e) {
|
|
214
|
+
console.error(\` ❌ \${file} 加载失败:\`, e.message);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
${corsAndSecurity}
|
|
220
|
+
|
|
221
|
+
const start = async () => {
|
|
222
|
+
await loadUserRoutes();
|
|
223
|
+
${staticMiddleware}
|
|
224
|
+
if (!allRoutes.some(r => r.method === 'GET' && r.path === '/')) ${defaultRootRoute}
|
|
225
|
+
|
|
226
|
+
${serverCreationCode}
|
|
227
|
+
server.listen(port, host, () => {
|
|
228
|
+
console.log(\`\\n🚀 服务已启动: \${protocol}://\${host}:\${port}\`);
|
|
229
|
+
console.log('📡 路由监控:');
|
|
230
|
+
printRoutes();
|
|
231
|
+
});
|
|
232
|
+
};
|
|
233
|
+
start();`;
|
|
232
234
|
}
|
|
233
235
|
|
|
234
|
-
//
|
|
235
|
-
return
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
236
|
+
// 无用户路由(纯静态服务器)
|
|
237
|
+
return `${baseImports}
|
|
238
|
+
${declarations}
|
|
239
|
+
${corsAndSecurity}
|
|
240
|
+
${staticMiddleware}
|
|
241
|
+
${defaultRootRoute}
|
|
242
|
+
${serverCreationCode}
|
|
243
|
+
server.listen(port, host, () => {
|
|
244
|
+
console.log(\`\\n🚀 静态服务器已启动: \${protocol}://\${host}:\${port}\`);
|
|
245
|
+
console.log('📁 当前仅提供静态文件服务(未检测到用户路由)');
|
|
246
|
+
});`;
|
|
245
247
|
},
|
|
246
248
|
|
|
247
|
-
// ==================== 3
|
|
248
|
-
/**
|
|
249
|
-
* @param {string[]} cachedPages - 所有待编译文件(相对于 templatesDir 的路径)
|
|
250
|
-
* @param {string} outputDir - 输出根目录(例如 'dist')
|
|
251
|
-
*
|
|
252
|
-
* 处理阶段:
|
|
253
|
-
* 1. 展平编译(模板继承,包含指令解析,变量占位符替换)
|
|
254
|
-
* 2. 获取所有包含文件并跳过
|
|
255
|
-
* 3. 文件输出到 outputDir/templatesDir/ 下,保持原相对路径结构
|
|
256
|
-
*/
|
|
249
|
+
// ==================== 3. 编译模板文件 ====================
|
|
257
250
|
compile = async (cachedPages, outputDir) => {
|
|
258
251
|
for (const templateFile of cachedPages) {
|
|
259
252
|
try {
|
|
260
253
|
let rendered = await renderTemplate(templateFile);
|
|
261
254
|
rendered = await processIncludes(rendered, templateFile);
|
|
262
255
|
rendered = processVariables(rendered, { currentUrl: `/${templateFile}`, query: {} });
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
if (includedFiles.has(templateFile)) continue; // 跳过被包含的文件
|
|
266
|
-
|
|
256
|
+
const includedFiles = getIncludedFiles();
|
|
257
|
+
if (includedFiles.has(templateFile)) continue;
|
|
267
258
|
const outputPath = path.join(CWD, outputDir, templatesDir, templateFile);
|
|
268
259
|
await fsPromises.mkdir(path.dirname(outputPath), { recursive: true });
|
|
269
260
|
await fsPromises.writeFile(outputPath, rendered);
|
|
@@ -274,76 +265,92 @@ const execPromise = util.promisify(exec),
|
|
|
274
265
|
}
|
|
275
266
|
};
|
|
276
267
|
|
|
277
|
-
// ==================== 4
|
|
268
|
+
// ==================== 4. 批量编译主流程 ====================
|
|
278
269
|
/**
|
|
279
|
-
*
|
|
280
|
-
*
|
|
281
|
-
* @param {string|Object} [options] - 配置项,可以是字符串(输出目录)或对象(支持 outputDir 字段)
|
|
270
|
+
* 全量模板编译与打包,沿用开发阶段配置(端口、HTTPS、证书、主机名)
|
|
271
|
+
* @param {string|Object} [options] - 配置项,可以是字符串(输出目录)或对象(支持 outputDir 字段)
|
|
282
272
|
* @param {string} [options.outputDir='dist'] - 自定义打包输出目录
|
|
283
|
-
*
|
|
284
|
-
* 核心流程:
|
|
285
|
-
* 1. 初始化编译环境(模式标识->缓存清理->验证模板->获取编译文件)
|
|
286
|
-
* 2. 预加载用户自定义变量
|
|
287
|
-
* 3. 创建打包目录,异步编译所有模板文件
|
|
288
|
-
* 4. 路由检测,根据有无路由准备不同的依赖对象,生成入口文件内容、原子写入文件
|
|
289
|
-
* 5. 复制资源、自动安装依赖、恢复非编译模式
|
|
290
|
-
*
|
|
291
|
-
* 特殊处理:
|
|
292
|
-
* - 通过编译模式切换包含文件收集行为
|
|
293
|
-
* - 自动过滤片段文件避免重复输出
|
|
294
|
-
* - 有路由时合并用户依赖,无路由时仅包含 express
|
|
295
|
-
* - 自动安装依赖确保运行环境完整
|
|
296
273
|
*/
|
|
297
274
|
const compileAllTemplates = async (options = {}) => {
|
|
298
275
|
if (typeof options === 'string') options = { outputDir: options };
|
|
299
276
|
const outputDir = options.outputDir || 'dist';
|
|
300
277
|
|
|
301
278
|
try {
|
|
302
|
-
// 1
|
|
279
|
+
// 1. 读取开发阶段配置(与 dev-server 行为完全一致)
|
|
280
|
+
let defaults = {};
|
|
281
|
+
const configFile = path.join(CWD, '.dev-config.json');
|
|
282
|
+
try {
|
|
283
|
+
const content = await fsPromises.readFile(configFile, 'utf8');
|
|
284
|
+
defaults = JSON.parse(content);
|
|
285
|
+
console.log('📋 已读取上次开发配置作为默认值');
|
|
286
|
+
} catch (err) {
|
|
287
|
+
if (err.code !== 'ENOENT') console.warn('⚠️ 读取开发配置失败:', err.message);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const { port, host, httpsEnabled, httpsKeyPath, httpsCertPath } = parseServerConfig({}, defaults);
|
|
291
|
+
console.log(`🔧 沿用开发配置: 端口=${port}, 主机=${host}, HTTPS=${httpsEnabled}`);
|
|
292
|
+
|
|
293
|
+
// 2. 设置编译模式并清空包含文件记录
|
|
303
294
|
setCompilationMode(true), cachedPages = await getAvailableTemplates();
|
|
304
|
-
for (const file of cachedPages) await validateTemplateFile(file);
|
|
295
|
+
for (const file of cachedPages) await validateTemplateFile(file);
|
|
305
296
|
|
|
306
|
-
//
|
|
297
|
+
// 3. 加载用户自定义功能(编译模式)
|
|
307
298
|
await loadUserFeatures(null, true), console.log(`ℹ️ 变量已从${customizeDir}目录加载`);
|
|
308
299
|
|
|
309
|
-
//
|
|
300
|
+
// 4. 创建打包目录
|
|
310
301
|
await fsPromises.rm(outputDir, { recursive: true, force: true });
|
|
311
302
|
await fsPromises.mkdir(outputDir, { recursive: true }), console.log(`📁 已创建输出目录: ${outputDir}`);
|
|
303
|
+
|
|
304
|
+
// 5. 编译模板文件
|
|
312
305
|
await compile(cachedPages, outputDir), console.log(`\n🎉 编译文件完成!`);
|
|
313
306
|
|
|
314
|
-
//
|
|
307
|
+
// 6. 检测路由、生成 package.json 和 server.js
|
|
315
308
|
const hasUserRoutes = await checkUserRoutesExist(), pkgContent = await mergeDependencies(hasUserRoutes),
|
|
316
|
-
entryFile = await findEntryFile(cachedPages),
|
|
309
|
+
entryFile = await findEntryFile(cachedPages), buildConfig = { port, host, httpsEnabled, httpsKeyPath, httpsCertPath };
|
|
310
|
+
// 如果启用了 HTTPS 且证书路径存在;
|
|
311
|
+
if (httpsEnabled && httpsKeyPath && httpsCertPath) {
|
|
312
|
+
console.log(` 证书路径: ${httpsKeyPath}, ${httpsCertPath}`);
|
|
313
|
+
console.warn(' 证书文件将使用原路径,如果部署时证书路径发生变化,请自行调整 server.js 中的证书路径;');
|
|
314
|
+
}
|
|
317
315
|
|
|
316
|
+
const serverContent = await generateServerEntry(hasUserRoutes, entryFile, buildConfig);
|
|
318
317
|
await Promise.all([
|
|
319
318
|
fsPromises.writeFile(path.join(outputDir, 'server.js'), serverContent),
|
|
320
319
|
fsPromises.writeFile(path.join(outputDir, 'package.json'), pkgContent)
|
|
321
320
|
]);
|
|
322
321
|
|
|
323
|
-
//
|
|
322
|
+
// 7. 复制静态资源与用户功能目录
|
|
324
323
|
await copyDir(staticDir, path.join(outputDir, staticDir));
|
|
325
324
|
await copyDir(customizeDir, path.join(outputDir, customizeDir));
|
|
325
|
+
const middlewareSrc = path.join(__dirname, 'services', 'middleware.js'),
|
|
326
|
+
middlewareDest = path.join(outputDir, 'middleware.js');
|
|
327
|
+
await fsPromises.copyFile(middlewareSrc, middlewareDest);
|
|
328
|
+
console.log('✅ 公共中间件已复制到输出目录/middleware.js');
|
|
326
329
|
try {
|
|
327
330
|
await fsPromises.copyFile(path.join(CWD, '.env'), path.join(outputDir, '.env'));
|
|
328
331
|
} catch (err) {
|
|
329
332
|
if (err.code !== 'ENOENT') console.error(`⚠️ 复制 .env 文件失败: ${err.message}`);
|
|
330
333
|
}
|
|
331
|
-
console.log('✅ 资源打包完成')
|
|
334
|
+
console.log('✅ 资源打包完成');
|
|
332
335
|
|
|
333
|
-
|
|
336
|
+
// 8. 安装依赖
|
|
337
|
+
await installDependencies(outputDir);
|
|
338
|
+
|
|
339
|
+
if (hasUserRoutes) console.log('\n🚀 检测到自定义路由,已创建完整服务端入口文件');
|
|
334
340
|
else console.log('\n📄 已生成静态文件服务器(无用户路由)');
|
|
335
341
|
|
|
336
|
-
console.log(`👉 启动服务器命令: cd ${outputDir} && node server.js`)
|
|
342
|
+
console.log(`👉 启动服务器命令: cd ${outputDir} && node server.js`);
|
|
343
|
+
setCompilationMode(false);
|
|
337
344
|
} catch (error) {
|
|
338
345
|
console.error('❌ 编译流程出错:', error.message);
|
|
339
346
|
setCompilationMode(false);
|
|
340
347
|
}
|
|
341
348
|
};
|
|
342
349
|
|
|
343
|
-
// ==================== 5
|
|
350
|
+
// ==================== 5. 导出接口与执行编译 ====================
|
|
344
351
|
export { compileAllTemplates };
|
|
345
352
|
|
|
346
|
-
if (process.argv[1] ===
|
|
353
|
+
if (process.argv[1] === __filename) {
|
|
347
354
|
const customDir = process.argv[2];
|
|
348
355
|
compileAllTemplates(customDir);
|
|
349
356
|
}
|
|
@@ -1,25 +1,25 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
* @param {string} html - 原始 HTML 内容
|
|
4
|
-
* @returns {string} 注入热重载脚本后的 HTML 内容
|
|
5
|
-
*/
|
|
6
|
-
const injectScript = html => {
|
|
7
|
-
if (/hot-reload-socket|socket\.io\.js/.test(html)) return html; // 避免重复注入
|
|
8
|
-
const socketScript = `
|
|
9
|
-
<script src="/socket.io/socket.io.js"></script>
|
|
10
|
-
<script>
|
|
11
|
-
(function() {
|
|
12
|
-
var socket = io();
|
|
13
|
-
socket.on('hot-reload', delay => {
|
|
14
|
-
console.log('[热重载] 检测到文件更改,' + delay + '毫秒后重新加载页面...');
|
|
15
|
-
setTimeout(() => window.location.reload(), delay);
|
|
16
|
-
});
|
|
17
|
-
})();
|
|
18
|
-
</script>
|
|
19
|
-
`;
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
20
3
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
4
|
+
const isProduction = fs.existsSync(path.join(process.cwd(), 'middleware.js')),
|
|
5
|
+
injectScript = html => {
|
|
6
|
+
if (isProduction) return html; // 生产模式(存在 middleware.js)不注入热重载脚本
|
|
7
|
+
if (/hot-reload-socket|socket\.io\.js/.test(html)) return html; // 已经注入过了
|
|
8
|
+
const socketScript = `
|
|
9
|
+
<script src="/socket.io/socket.io.js"></script>
|
|
10
|
+
<script>
|
|
11
|
+
(function() {
|
|
12
|
+
var socket = io();
|
|
13
|
+
socket.on('hot-reload', delay => {
|
|
14
|
+
console.log('[热重载] 检测到文件更改,' + delay + '毫秒后重新加载页面...');
|
|
15
|
+
setTimeout(() => window.location.reload(), delay);
|
|
16
|
+
});
|
|
17
|
+
})();
|
|
18
|
+
</script>
|
|
19
|
+
`;
|
|
20
|
+
|
|
21
|
+
if (html.includes('</body>')) return html.replace('</body>', `${socketScript}</body>`);
|
|
22
|
+
return html + socketScript;
|
|
23
|
+
};
|
|
24
24
|
|
|
25
25
|
export { injectScript };
|