@flun/html-template 4.0.10

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.
Files changed (59) hide show
  1. package/.env +9 -0
  2. package/LICENSE +15 -0
  3. package/build.js +3 -0
  4. package/compile.js +349 -0
  5. package/copy-files.js +200 -0
  6. package/customize/account.js +726 -0
  7. package/customize/data.json +484 -0
  8. package/customize/functions.js +48 -0
  9. package/customize/hotReloadInjector.js +25 -0
  10. package/customize/routes.js +141 -0
  11. package/customize/users.json +44 -0
  12. package/customize/variables.js +70 -0
  13. package/dev-server.js +344 -0
  14. package/dev.js +4 -0
  15. package/f-CHANGELOG.md +4 -0
  16. package/f-README.md +485 -0
  17. package/index.d.ts +133 -0
  18. package/index.js +4 -0
  19. package/package.json +77 -0
  20. package/restoreDefaults.js +8 -0
  21. package/services/templateService.js +962 -0
  22. package/static/about.css +118 -0
  23. package/static/auth.js +27 -0
  24. package/static/constants.css +138 -0
  25. package/static/img/dark.png +0 -0
  26. package/static/img/favicon.ico +0 -0
  27. package/static/img/light.png +0 -0
  28. package/static/img/top.png +0 -0
  29. package/static/index.css +86 -0
  30. package/static/mouseOrTouch.js +156 -0
  31. package/static/public.css +288 -0
  32. package/static/script.css +318 -0
  33. package/static/script.js +392 -0
  34. package/static/styling.css +874 -0
  35. package/static/styling.js +933 -0
  36. package/static/themeImg.css +10 -0
  37. package/static/themeImg.js +19 -0
  38. package/static/themeModule.js +222 -0
  39. package/static/topImg.css +19 -0
  40. package/static/topImg.js +21 -0
  41. package/static/utils/browser13.js +270 -0
  42. package/static/utils/closebrackets.js +166 -0
  43. package/static/utils/css-lint.js +308 -0
  44. package/static/utils/custom-css-hint.js +876 -0
  45. package/static/utils/foldgutter.js +141 -0
  46. package/static/utils/match-highlighter.js +70 -0
  47. package/templates/about.html +236 -0
  48. package/templates/account/2fa.html +184 -0
  49. package/templates/account/forgot-password.html +226 -0
  50. package/templates/account/login.html +230 -0
  51. package/templates/account/profile.html +977 -0
  52. package/templates/account/register.html +224 -0
  53. package/templates/account/reset-password.html +205 -0
  54. package/templates/account/verify-email.html +163 -0
  55. package/templates/base.html +71 -0
  56. package/templates/footer-content.html +5 -0
  57. package/templates/index.html +140 -0
  58. package/templates/script.html +209 -0
  59. package/templates/test-include.html +11 -0
package/.env ADDED
@@ -0,0 +1,9 @@
1
+ SESSION_SECRET = your-strong-secret-key # session的密钥(请替换成足够复杂且保密的字符串)
2
+ # 开发环境建议使用局域网具体IP,这样可以局域网异机测试;
3
+ # 生产环境建议请用域名如:https://example.com
4
+ APP_URL = http://localhost:7290 # 建议改为具体的ip,以免使用场景受限
5
+ MAIL_HOST = smtp.example.com # 替换成你自己邮箱服务器地址
6
+ MAIL_PORT = 465 # 你自己的邮箱服务器端口
7
+ MAIL_USER = your-email@example.com # 替换成你自己的邮箱地址
8
+ MAIL_PASS = "your-password" # 替换成你自己的邮箱授权码(注意不是邮箱密码,通常需要在邮箱设置里生成授权码)
9
+ PWD = admin123 # 替换成你想要的管理员密码(请务必修改成足够复杂且保密的密码)
package/LICENSE ADDED
@@ -0,0 +1,15 @@
1
+ ISC License
2
+
3
+ Copyright (c) 2026, flun
4
+
5
+ Permission to use, copy, modify, and/or distribute this software for any
6
+ purpose with or without fee is hereby granted, provided that the above
7
+ copyright notice and this permission notice appear in all copies.
8
+
9
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
10
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
11
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
12
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
13
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
14
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
15
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
package/build.js ADDED
@@ -0,0 +1,3 @@
1
+ import { compile } from '@flun/html-template';
2
+
3
+ compile({ outputDir: 'dist' }); // 编译模板->默认参数:目录名 dist;
package/compile.js ADDED
@@ -0,0 +1,349 @@
1
+ /**
2
+ * 模板编译与打包工具
3
+ *
4
+ * 模块结构:
5
+ * 1. 递归目录复制工具(copyDir)
6
+ * 2. 路由文件处理及入口文件生成
7
+ * - 路由检测(checkUserRoutesExist)
8
+ * - 入口文件生成(generateServerEntry)
9
+ * - 依赖管理(mergeDependencies → 返回完整 package.json)
10
+ * 3. 编译模板所有文件(compile)
11
+ * 4. 批量编译主流程(compileAllTemplates)
12
+ * 5. 导出接口与执行编译
13
+ *
14
+ * 核心功能:
15
+ * - 完整的模板编译流水线:模板替换→包含处理→变量替换→文件输出
16
+ * - 智能路由检测与入口生成:自动创建可运行的服务端环境
17
+ * - 资源打包优化:确保路由文件在静态资源前生成
18
+ * - 生产环境就绪:自动生成Express服务器和依赖配置
19
+ *
20
+ * 特殊机制:
21
+ * - 编译模式标识:控制包含文件的收集逻辑
22
+ * - 路由功能检测:扫描用户功能文件中的setupRoutes函数
23
+ * - 模块缓存清理:确保路由加载时使用最新代码
24
+ * - Express版本管理:优先使用模板依赖,默认^5.2.1
25
+ */
26
+ import {
27
+ path, fsPromises, CWD, getAvailableTemplates, validateTemplateFile, renderTemplate, processIncludes, processVariables,
28
+ setCompilationMode, getIncludedFiles, loadUserFeatures, findEntryFile, templatesDir, staticDir, customizeDir, defaultPort
29
+ } from './services/templateService.js';
30
+ import PK from './package.json' with { type: 'json' };
31
+ import util from 'util';
32
+ import { exec } from 'child_process';
33
+ import { fileURLToPath, pathToFileURL } from 'url';
34
+
35
+ let cachedPages = []; // 缓存模板列表
36
+ const execPromise = util.promisify(exec),
37
+
38
+ // ==================== 1.递归目录复制工具 ====================
39
+ /**
40
+ * 目录结构克隆工具(含错误抑制)
41
+ * @param {string} src - 源目录路径
42
+ * @param {string} destDir - 目标目录路径
43
+ *
44
+ * 特性:
45
+ * - 自动创建目标目录结构
46
+ * - 跳过不存在的源目录(不报错)
47
+ * - 保留子目录结构递归复制
48
+ */
49
+ copyDir = async (src, destDir) => {
50
+ try {
51
+ await fsPromises.mkdir(destDir, { recursive: true });
52
+ const entries = await fsPromises.readdir(src, { withFileTypes: true });
53
+ for (const entry of entries) {
54
+ const srcPath = path.join(src, entry.name), destPath = path.join(destDir, entry.name);
55
+ if (entry.isDirectory()) await copyDir(srcPath, destPath);
56
+ else await fsPromises.copyFile(srcPath, destPath);
57
+ }
58
+ } catch (error) {
59
+ if (error.code !== 'ENOENT') console.error(`❌ 复制目录出错: ${src} -> ${destDir}`, error.message);
60
+ }
61
+ },
62
+
63
+ // ==================== 2.路由文件处理及入口文件生成 ====================
64
+
65
+ /**
66
+ * 检测用户是否定义路由功能(兼容默认导出与具名导出)
67
+ * @returns {Promise<boolean>} 是否存在有效路由
68
+ */
69
+ checkUserRoutesExist = async () => {
70
+ try {
71
+ const featuresDir = path.join(CWD, customizeDir);
72
+ await fsPromises.access(featuresDir);
73
+ const files = (await fsPromises.readdir(featuresDir)).filter(f => f.endsWith('.js'));
74
+ for (const file of files) {
75
+ try {
76
+ const modulePath = path.join(featuresDir, file), moduleUrl = pathToFileURL(modulePath);
77
+ moduleUrl.search = 'update=' + Date.now();
78
+ const mod = await import(moduleUrl.href), feature = mod.default?.setupRoutes ? mod.default : mod;
79
+ if (typeof feature.setupRoutes === 'function') return true;
80
+ } catch (e) {
81
+ console.warn(`⚠️ 检查路由文件 ${file} 失败:`, e.message);
82
+ }
83
+ }
84
+ return false;
85
+ } catch {
86
+ return false;
87
+ }
88
+ },
89
+
90
+ /**
91
+ * 合并用户项目依赖与模板工具依赖,生成完整的 package.json 内容
92
+ * @param {boolean} hasUserRoutes - 是否存在用户自定义路由
93
+ * @returns {Promise<string>} 格式化后的 package.json 字符串
94
+ */
95
+ mergeDependencies = async hasUserRoutes => {
96
+ let basePkg = {}, userDeps = {}, mergedDeps = {};
97
+ const userPkgPath = path.join(CWD, 'package.json');
98
+ try {
99
+ const userPkgRaw = await fsPromises.readFile(userPkgPath, 'utf8'), userPkg = JSON.parse(userPkgRaw);
100
+ basePkg.author = userPkg.author || '', basePkg.license = userPkg.license || 'ISC';
101
+ userDeps = userPkg.dependencies || {};
102
+ } catch (err) { }
103
+
104
+ if (hasUserRoutes) {
105
+ const templateDeps = PK.dependencies || {}, excludeList = ['chokidar', 'socket.io', '@flun/html-template'];
106
+ mergedDeps = { ...templateDeps, ...userDeps };
107
+ for (const pkg of excludeList) delete mergedDeps[pkg];
108
+ }
109
+ if (!mergedDeps.express) mergedDeps.express = '^5.2.1';
110
+
111
+ const finalPkg = {
112
+ name: 'dist-server', version: '1.0.0',
113
+ ...basePkg,
114
+ type: 'module', main: 'server.js',
115
+ scripts: { dev: 'node server.js' },
116
+ dependencies: mergedDeps,
117
+ overrides: { 'fast-xml-parser': '^5.3.4' }
118
+ };
119
+ return JSON.stringify(finalPkg, null, 2);
120
+ },
121
+
122
+ /**
123
+ * 在目标目录中执行 npm install
124
+ * @param {string} targetDir - 目标目录
125
+ */
126
+ installDependencies = async targetDir => {
127
+ console.log('📦 正在安装项目依赖,请稍候...');
128
+ try {
129
+ const { stdout, stderr } = await execPromise('npm install', { cwd: targetDir });
130
+ if (stdout) console.log(stdout);
131
+ if (stderr) console.error(stderr);
132
+ console.log('✅ 依赖安装完成');
133
+ } catch (error) {
134
+ console.error('❌ 依赖安装失败:', error.message);
135
+ console.log('💡 请手动进入目标目录执行 npm install');
136
+ }
137
+ },
138
+
139
+ /**
140
+ * 生成服务端入口文件内容(ESM 格式)
141
+ * @param {boolean} hasUserRoutes - 是否存在用户自定义路由
142
+ * @param {string} entryFile - 入口文件名(如 index.html)
143
+ * @returns {Promise<string>} server.js 文件内容
144
+ */
145
+ generateServerEntry = async (hasUserRoutes, entryFile) => {
146
+ const imports = `import express from 'express';
147
+ import path from 'path';
148
+ import { fileURLToPath, pathToFileURL } from 'url';
149
+ const __filename = fileURLToPath(import.meta.url), __dirname = path.dirname(__filename),
150
+ app = express(),port = process.env.PORT || ${defaultPort}`,
151
+ corsAndSecurity = `
152
+ app.use((req, res, next) => {
153
+ res.setHeader('Access-Control-Allow-Origin', '*');
154
+ res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, PATCH, OPTIONS, HEAD');
155
+ res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-Requested-With, Accept, Origin, X-CSRF-Token');
156
+ res.setHeader('Access-Control-Expose-Headers', 'Content-Length, Content-Range');
157
+ res.setHeader('Access-Control-Max-Age', '86400');
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}')));
168
+ app.use(express.static(path.join(__dirname, '${templatesDir}')));`,
169
+ defaultRootRoute = `app.get('/', (req, res) => res.redirect('/${entryFile}'));`;
170
+
171
+ // ----- 有用户路由时的动态加载服务器 -----
172
+ if (hasUserRoutes) {
173
+ return `
174
+ import fs from 'fs';
175
+ ${imports}, allRoutes = [];
176
+
177
+ // 拦截 app 方法,收集路由
178
+ const wrapAppMethods = (app) => {
179
+ const methodsToWrap = ['get', 'post', 'put', 'delete', 'patch', 'options', 'head', 'all'], originals = {};
180
+ methodsToWrap.forEach(method => {
181
+ originals[method] = app[method].bind(app);
182
+ app[method] = function(routePath, ...handlers) {
183
+ allRoutes.push({ method: method.toUpperCase(), path: routePath });
184
+ return originals[method](routePath, ...handlers);
185
+ };
186
+ });
187
+ },
188
+ // 打印已注册路由
189
+ printRoutes = () => {
190
+ if (allRoutes.length) {
191
+ console.log(' 🗺️ 注册路由:');
192
+ allRoutes.forEach(r => console.log(\` \${r.method.padEnd(6)} \${r.path}\`));
193
+ } else console.log(' ℹ️ 未找到任何路由');
194
+ },
195
+ // 动态加载用户自定义路由
196
+ loadUserRoutes = async () => {
197
+ const featuresDir = path.join(__dirname, '${customizeDir}');
198
+ if (!fs.existsSync(featuresDir)) return console.log(\` ℹ️ \${featuresDir}目录不存在,跳过路由加载\`);
199
+
200
+ const routeFiles = fs.readdirSync(featuresDir).filter(file => file.endsWith('.js'));
201
+ for (const file of routeFiles) {
202
+ try {
203
+ // 使用带时间戳的查询参数避免模块缓存,确保每次获取最新内容
204
+ const modulePath = path.join(featuresDir, file), moduleUrl = pathToFileURL(modulePath);
205
+ moduleUrl.search = 'update=' + Date.now();
206
+ const feature = await import(moduleUrl.href);
207
+ if (typeof feature.default?.setupRoutes === 'function')
208
+ feature.default.setupRoutes(app), console.log(\` ✅ 路由加载文件: \${file}\`);
209
+ else if (typeof feature?.setupRoutes === 'function')
210
+ feature.setupRoutes(app), console.log(\` ✅ 路由加载文件: \${file}\`);
211
+ } catch (e) {
212
+ console.error(\` \${file}文件未检测到路由\`, e.message);
213
+ }
214
+ }
215
+ };
216
+
217
+ wrapAppMethods(app);
218
+ ${corsAndSecurity}
219
+
220
+ // 启动流程:先加载路由,再注册静态资源与默认路由
221
+ const start = async () => {
222
+ await loadUserRoutes();
223
+ ${staticMiddleware}
224
+ if (!allRoutes.some(r => r.method === 'GET' && r.path === '/')) ${defaultRootRoute}
225
+ app.listen(port, () => {
226
+ console.log(\`\\n🚀 服务已启动: http://localhost:\${port}\`);
227
+ console.log('📡 路由监控:');
228
+ printRoutes();
229
+ });
230
+ };
231
+ start();`.trim();
232
+ }
233
+
234
+ // ----- 无用户路由时的纯静态服务器 -----
235
+ return `
236
+ ${imports};
237
+
238
+ ${corsAndSecurity}
239
+ ${staticMiddleware}
240
+ ${defaultRootRoute}
241
+ app.listen(port, () => {
242
+ console.log(\`\\n🚀 静态服务器已启动: http://localhost:\${port}\`);
243
+ console.log('📁 当前仅提供静态文件服务(未检测到用户路由)');
244
+ });`.trim();
245
+ },
246
+
247
+ // ==================== 3.编译模板文件 ====================
248
+ /**
249
+ * @param {string[]} cachedPages - 所有待编译文件(相对于 templatesDir 的路径)
250
+ * @param {string} outputDir - 输出根目录(例如 'dist')
251
+ *
252
+ * 处理阶段:
253
+ * 1. 展平编译(模板继承,包含指令解析,变量占位符替换)
254
+ * 2. 获取所有包含文件并跳过
255
+ * 3. 文件输出到 outputDir/templatesDir/ 下,保持原相对路径结构
256
+ */
257
+ compile = async (cachedPages, outputDir) => {
258
+ for (const templateFile of cachedPages) {
259
+ try {
260
+ let rendered = await renderTemplate(templateFile);
261
+ rendered = await processIncludes(rendered, templateFile);
262
+ rendered = processVariables(rendered, { currentUrl: `/${templateFile}`, query: {} });
263
+
264
+ const includedFiles = getIncludedFiles(); // 获取所有包含文件
265
+ if (includedFiles.has(templateFile)) continue; // 跳过被包含的文件
266
+
267
+ const outputPath = path.join(CWD, outputDir, templatesDir, templateFile);
268
+ await fsPromises.mkdir(path.dirname(outputPath), { recursive: true });
269
+ await fsPromises.writeFile(outputPath, rendered);
270
+ console.log(`✅ ${templateFile} ->已编译: ${path.join(outputDir, templatesDir, templateFile)}`);
271
+ } catch (error) {
272
+ console.error(`❌ 编译 ${templateFile} 时出错: ${error.message}`);
273
+ }
274
+ }
275
+ };
276
+
277
+ // ==================== 4.批量编译主流程 ====================
278
+ /**
279
+ * 全量模板编译与打包
280
+ * >查看定义:@see {@link compileAllTemplates}
281
+ * @param {string|Object} [options] - 配置项,可以是字符串(输出目录)或对象(支持 outputDir 字段)
282
+ * @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
+ */
297
+ const compileAllTemplates = async (options = {}) => {
298
+ if (typeof options === 'string') options = { outputDir: options };
299
+ const outputDir = options.outputDir || 'dist';
300
+
301
+ try {
302
+ // 1.设置编译模式并清空包含文件记录
303
+ setCompilationMode(true), cachedPages = await getAvailableTemplates();
304
+ for (const file of cachedPages) await validateTemplateFile(file); // 模板验证
305
+
306
+ // 2.加载用户自定义功能(编译模式)
307
+ await loadUserFeatures(null, true), console.log(`ℹ️ 变量已从${customizeDir}目录加载`);
308
+
309
+ // 3.创建打包目录
310
+ await fsPromises.rm(outputDir, { recursive: true, force: true });
311
+ await fsPromises.mkdir(outputDir, { recursive: true }), console.log(`📁 已创建输出目录: ${outputDir}`);
312
+ await compile(cachedPages, outputDir), console.log(`\n🎉 编译文件完成!`);
313
+
314
+ // 4. 检测是否存在用户路由,生成package.json内容,获取入口文件生成 server.js 内容,并原子写入磁盘
315
+ const hasUserRoutes = await checkUserRoutesExist(), pkgContent = await mergeDependencies(hasUserRoutes),
316
+ entryFile = await findEntryFile(cachedPages), serverContent = await generateServerEntry(hasUserRoutes, entryFile);
317
+
318
+ await Promise.all([
319
+ fsPromises.writeFile(path.join(outputDir, 'server.js'), serverContent),
320
+ fsPromises.writeFile(path.join(outputDir, 'package.json'), pkgContent)
321
+ ]);
322
+
323
+ // 5. 复制静态资源与用户功能目录
324
+ await copyDir(staticDir, path.join(outputDir, staticDir));
325
+ await copyDir(customizeDir, path.join(outputDir, customizeDir));
326
+ try {
327
+ await fsPromises.copyFile(path.join(CWD, '.env'), path.join(outputDir, '.env'));
328
+ } catch (err) {
329
+ if (err.code !== 'ENOENT') console.error(`⚠️ 复制 .env 文件失败: ${err.message}`);
330
+ }
331
+ console.log('✅ 资源打包完成'), await installDependencies(outputDir);
332
+
333
+ if (hasUserRoutes) console.log('\n🚀 检测到自定义路由,已创建完整服务端入口文件');
334
+ else console.log('\n📄 已生成静态文件服务器(无用户路由)');
335
+
336
+ console.log(`👉 启动服务器命令: cd ${outputDir} && node server.js`), setCompilationMode(false); // 设置编译模式为假
337
+ } catch (error) {
338
+ console.error('❌ 编译流程出错:', error.message);
339
+ setCompilationMode(false);
340
+ }
341
+ };
342
+
343
+ // ==================== 5.导出接口与执行编译 ====================
344
+ export { compileAllTemplates };
345
+
346
+ if (process.argv[1] === fileURLToPath(import.meta.url)) {
347
+ const customDir = process.argv[2];
348
+ compileAllTemplates(customDir);
349
+ }
package/copy-files.js ADDED
@@ -0,0 +1,200 @@
1
+ #!/usr/bin/env node
2
+
3
+ import fs from 'fs/promises';
4
+ import path from 'path';
5
+ import { fileURLToPath } from 'url';
6
+
7
+ // 获取 __dirname (仅用于迁移)
8
+ const __filename = fileURLToPath(import.meta.url), __dirname = path.dirname(__filename),
9
+ // 常量定义(直接覆盖文件列表和复制列表)
10
+ alwaysOverwriteFiles = ['f-README.md', 'f-CHANGELOG.md'], filesToCopy = ['templates', 'customize', 'static', '.env',
11
+ 'dev.js', 'build.js', 'restoreDefaults.js', 'f-README.md', 'f-CHANGELOG.md'],
12
+
13
+ // 日志函数
14
+ log = (message, config, isErrorLog = false) => {
15
+ if (isErrorLog) console.log(`❌ ${message}`);
16
+ else if (config.verbose) console.log(`✅ ${message}`); // 非错误日志只在详细模式下显示
17
+ },
18
+
19
+ // 显示帮助信息
20
+ showHelp = () => {
21
+ console.log(`
22
+ 文件复制工具 - 使用说明:
23
+ 默认行为: 跳过已存在的目录,没有的目录执行复制,根目录已有的文件跳过,根目录没有的文件复制
24
+
25
+ 主模式(3选1):
26
+ --overwrite 覆盖所有已存在的包文件和目录
27
+ --skip-files 跳过所有已有文件,所有没有的文件执行复制后创建
28
+ --skip-dirs (默认)
29
+
30
+ 可选参数(2选1):
31
+ --account 启用登录模式(会复制 templates/account 和 customize/account.js)
32
+ --no-account 禁用登录模式,跳过复制上述文件/目录(默认)
33
+ 可选参数:
34
+ --verbose 详细模式,显示操作信息
35
+
36
+ 帮助:
37
+ --help 显示此帮助信息
38
+ `);
39
+ process.exit(0);
40
+ },
41
+
42
+ // 判断是否应该跳过文件
43
+ shouldSkipFile = (destExists, isRootItem, config, shouldAlwaysOverwrite) => {
44
+ if (!destExists || shouldAlwaysOverwrite) return false;
45
+ else if (config.mode === 'skip-files') return true;
46
+ else if (config.mode === 'skip-dirs' && isRootItem) return true;
47
+ return false;
48
+ },
49
+
50
+ // 处理权限错误
51
+ handlePermissionError = (filePath, config) => {
52
+ log(`权限拒绝: ${filePath}`, config, true);
53
+ throw new Error(`权限拒绝: ${filePath}`);
54
+ },
55
+
56
+ // 确保目录存在
57
+ ensureDirectoryExists = async (dirPath, config) => {
58
+ try {
59
+ await fs.mkdir(dirPath, { recursive: true });
60
+ } catch (error) {
61
+ if (error.code === 'EACCES') handlePermissionError(dirPath, config);
62
+ else if (error.code !== 'EEXIST') throw error;
63
+ }
64
+ },
65
+
66
+ // 检查路径是否存在
67
+ pathExists = async path => {
68
+ try {
69
+ await fs.access(path);
70
+ return true;
71
+ } catch {
72
+ return false;
73
+ }
74
+ },
75
+
76
+ // 复制文件或目录
77
+ copyFileOrDir = async (src, dest, isRootItem = true, config) => {
78
+ if (!config.account) {
79
+ const accountDir = path.join(config.packageDir, 'templates', 'account'),
80
+ accountFile = path.join(config.packageDir, 'customize', 'account.js');
81
+ if (src === accountDir || src === accountFile) return log(`跳过account相关:${path.basename(src)}`, config);
82
+ }
83
+
84
+ try {
85
+ if (!await pathExists(src)) throw new Error(`源文件不存在: ${src}`);
86
+ const stat = await fs.stat(src);
87
+ if (stat.isDirectory()) await copyDirectory(src, dest, isRootItem, config);
88
+ else await copyFile(src, dest, isRootItem, config);
89
+ } catch (error) {
90
+ log(`复制失败: ${src} -> ${dest}, ${error.message}`, config, true);
91
+ throw error;
92
+ }
93
+ },
94
+
95
+ // 复制目录
96
+ copyDirectory = async (src, dest, isRootItem = true, config) => {
97
+ const destExists = await pathExists(dest); // 检查目标目录是否存在
98
+ if (destExists && (config.mode === 'skip-dirs')) return log(`跳过已存在目录: ${path.basename(dest)}`, config);
99
+
100
+ await ensureDirectoryExists(dest, config); // 创建目标目录
101
+ const items = await fs.readdir(src); // 读取源目录内容
102
+ log(`复制目录: ${src} -> ${dest} (${items.length} 个项目)`, config);
103
+
104
+ // 并行复制所有项目
105
+ await Promise.all(items.map(item => copyFileOrDir(path.join(src, item), path.join(dest, item), false, config)));
106
+ },
107
+
108
+ // 复制单个文件
109
+ copyFile = async (src, dest, isRootItem = true, config) => {
110
+ // 检查目标文件是否存在
111
+ const destExists = await pathExists(dest), fileName = path.basename(src),
112
+ shouldAlwaysOverwrite = alwaysOverwriteFiles.includes(fileName);
113
+
114
+ // 判断是否应该跳过文件
115
+ if (shouldSkipFile(destExists, isRootItem, config, shouldAlwaysOverwrite)) return log(`跳过已存在文件:${fileName}`, config);
116
+
117
+ // 记录覆盖操作
118
+ if (destExists) {
119
+ if (shouldAlwaysOverwrite) log(`直接覆盖: ${fileName}`, config);
120
+ else if (config.mode === 'overwrite') log(`覆盖文件: ${fileName}`, config);
121
+ }
122
+
123
+ const destDir = path.dirname(dest);
124
+ await ensureDirectoryExists(destDir, config); // 确保目标目录存在
125
+
126
+ // 执行复制
127
+ try {
128
+ await fs.copyFile(src, dest), log(`已复制: ${fileName}`, config);
129
+ } catch (error) {
130
+ if (error.code === 'EACCES') handlePermissionError(dest, config);
131
+ else throw error;
132
+ }
133
+ };
134
+
135
+ /**
136
+ * 运行文件复制
137
+ * >查看定义:@see {@link runCopyFiles}
138
+ * @param {Object} [options] - 配置选项
139
+ * @param {string} [options.mode] - 复制模式: 'overwrite', 'skip-files', 'skip-dirs' (默认: 'skip-dirs')
140
+ * @param {boolean} [options.verbose] - 是否启用详细日志 (默认: false)
141
+ * @param {boolean} [options.account] - 是否启用登录模式 (默认: false)
142
+ * @returns Promise<void>
143
+ */
144
+ const runCopyFiles = async (options = {}) => {
145
+ const config = {
146
+ mode: options.mode || 'skip-dirs',
147
+ verbose: options.verbose ?? false,
148
+ account: options.account ?? false,
149
+ packageDir: __dirname // 包所在目录,用于路径判断
150
+ },
151
+ targetDir = path.resolve(__dirname, '../..'); // 目标目录(项目根目录)
152
+
153
+ console.log('✅ 开始复制文件'); // 关键消息总是显示
154
+ if (config.verbose) {
155
+ console.log(`✅ 包目录: ${__dirname}`);
156
+ console.log(`✅ 目标目录: ${targetDir}`);
157
+ console.log(`✅ 待处理: ${filesToCopy.join(', ')}`);
158
+ console.log(`✅ 模式: ${config.mode}`);
159
+ console.log(`✅ account 模式: ${config.account ? '启用' : '禁用'}`);
160
+ }
161
+
162
+ try {
163
+ // 并行复制所有文件/目录
164
+ await Promise.all(
165
+ filesToCopy.map(item => copyFileOrDir(path.join(__dirname, item), path.join(targetDir, item), true, config)));
166
+
167
+ console.log('✅ 文件复制完成!');
168
+ // 添加专业支持信息(总是显示)
169
+ console.log('\n✅ 专业支持:');
170
+ console.log('✅ • 开发文档: https://www.npmjs.com/package/@flun/html-template');
171
+ console.log('✅ • 技术支持: cn@flun.top');
172
+ console.log('✅ • 企业微信: https://work.weixin.qq.com/kfid/kfc44c370d4ddbac6f0');
173
+ console.log('✅ 安装完成!');
174
+ } catch (error) {
175
+ const errorMsg = `❌ 复制过程中发生错误: ${error.message}`;
176
+ console.log(errorMsg);
177
+ throw new Error(errorMsg);
178
+ }
179
+ };
180
+
181
+ export { runCopyFiles };
182
+ // 如果通过命令行调用
183
+ if (process.argv[1] === __filename) {
184
+ if (process.argv.includes('--help')) showHelp(); // 处理命令行参数
185
+
186
+ // 确定模式
187
+ let mode;
188
+ if (process.argv.includes('--overwrite')) mode = 'overwrite';
189
+ else if (process.argv.includes('--skip-files')) mode = 'skip-files';
190
+ else if (process.argv.includes('--skip-dirs')) mode = 'skip-dirs';
191
+ else mode = 'skip-dirs';
192
+
193
+ let account;
194
+ if (process.argv.includes('--account')) account = true;
195
+ else if (process.argv.includes('--no-account')) account = false;
196
+ else account = false; // 默认
197
+
198
+ runCopyFiles({ mode, verbose: process.argv.includes('--verbose'), account })
199
+ .catch(error => (console.log(`❌ 未处理的错误: ${error.message}`), process.exit(1)));
200
+ }