@flun/node-mobile-app 3.1.5 → 4.0.1

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.
@@ -1,122 +1,123 @@
1
- import fs from 'fs';
2
- import path from 'path';
3
- import crypto from 'crypto';
4
- import { ensureDir, removeDir } from './utils.js';
5
-
6
- const FINGERPRINT_FILE = '.build-fingerprint.json',
7
- hashUserDeps = userProjectDir => {
8
- const h = crypto.createHash('sha256'), pkgPath = path.join(userProjectDir, 'package.json');
9
- if (fs.existsSync(pkgPath)) {
10
- try {
11
- const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
12
- h.update(JSON.stringify(pkg.dependencies || {})), h.update('|'), h.update(pkg.type || '');
13
- } catch { h.update('package.json-read-error'); }
14
- }
15
- else h.update('no-package');
16
- return h.digest('hex');
17
- },
18
- writeFp = (file, fp) => fs.writeFileSync(file, JSON.stringify(fp, null, 2), 'utf-8'),
19
- clearCommonCaches = (buildDir, userProjectDir) => {
20
- removeDir(path.join(buildDir, 'android', 'build'));
21
- removeDir(path.join(buildDir, 'android', 'app', 'build'));
22
- removeDir(path.join(userProjectDir, 'node_modules', '@flun', 'nodejs-mobile-react-native', 'android', 'build'));
23
- },
24
- hashTemplate = templateDir => {
25
- const h = crypto.createHash('sha256'),
26
- walk = d => {
27
- let entries;
28
- try { entries = fs.readdirSync(d, { withFileTypes: true }); } catch { return; }
29
- entries.sort((a, b) => a.name.localeCompare(b.name));
30
- for (const e of entries) {
31
- if (e.name === 'node_modules' || e.name === '.git') continue;
32
- const full = path.join(d, e.name), rel = path.relative(templateDir, full).replace(/\\/g, '/');
33
- if (e.isDirectory()) { h.update('D:' + rel + '|'); walk(full); }
34
- else if (e.isFile()) {
35
- h.update('F:' + rel + '|');
36
- try { h.update(fs.readFileSync(full)); } catch { h.update('err'); }
37
- h.update('|');
38
- }
39
- }
40
- };
41
- walk(templateDir);
42
- return h.digest('hex');
43
- },
44
- computeFingerprint = (userProjectDir, config, pkgRoot) => {
45
- const abiFilters = [...(config.android.abiFilters || [])].sort(), libnode = {},
46
- pluginDir = path.join(userProjectDir, 'node_modules', '@flun', 'nodejs-mobile-react-native'),
47
- libnodeBin = path.join(pluginDir, 'android', 'libnode', 'bin');
48
-
49
- if (fs.existsSync(libnodeBin)) {
50
- for (const arch of fs.readdirSync(libnodeBin)) {
51
- const so = path.join(libnodeBin, arch, 'libnode.so');
52
- if (fs.existsSync(so)) {
53
- const stat = fs.statSync(so);
54
- libnode[arch] = { size: stat.size, mtime: Math.floor(stat.mtimeMs) };
55
- }
56
- }
57
- }
58
-
59
- let pluginVersion = null, cliVersion = null;
60
- const pluginPkg = path.join(pluginDir, 'package.json'), cliPkg = path.join(pkgRoot, 'package.json');
61
- if (fs.existsSync(pluginPkg)) try { pluginVersion = JSON.parse(fs.readFileSync(pluginPkg, 'utf-8')).version; } catch { };
62
- if (fs.existsSync(cliPkg)) try { cliVersion = JSON.parse(fs.readFileSync(cliPkg, 'utf-8')).version; } catch { };
63
-
64
- const depsHash = hashUserDeps(userProjectDir), a = config.android || {},
65
- appInfo = {
66
- appName: config.appName || null,
67
- appId: config.appId || null,
68
- versionCode: config.versionCode || null,
69
- versionName: config.versionName || null,
70
- minSdkVersion: a.minSdkVersion || null,
71
- targetSdkVersion: a.targetSdkVersion || null,
72
- compileSdkVersion: a.compileSdkVersion || null,
73
- buildToolsVersion: a.buildToolsVersion || null,
74
- usesCleartextTraffic: a.usesCleartextTraffic !== false,
75
- permissions: [...(a.permissions || [])].sort(),
76
- };
77
-
78
- const templateHash = hashTemplate(path.join(pkgRoot, 'template'));
79
- return { abiFilters, libnode, pluginVersion, cliVersion, depsHash, appInfo, templateHash };
80
- },
81
- checkAndResetCache = (buildDir, userProjectDir, config, pkgRoot) => {
82
- ensureDir(buildDir);
83
-
84
- const fpFile = path.join(buildDir, FINGERPRINT_FILE), newFp = computeFingerprint(userProjectDir, config, pkgRoot);
85
- let oldFp = null;
86
- if (fs.existsSync(fpFile)) try { oldFp = JSON.parse(fs.readFileSync(fpFile, 'utf-8')); } catch { };
87
- if (!oldFp) return console.log(' ✓ 全新构建,无缓存可清'), writeFp(fpFile, newFp);
88
-
89
- const abiChanged = JSON.stringify(oldFp.abiFilters) !== JSON.stringify(newFp.abiFilters),
90
- libnodeChanged = JSON.stringify(oldFp.libnode) !== JSON.stringify(newFp.libnode),
91
- pluginChanged = oldFp.pluginVersion !== newFp.pluginVersion,
92
- cliChanged = oldFp.cliVersion !== newFp.cliVersion, depsChanged = oldFp.depsHash !== newFp.depsHash,
93
- appInfoChanged = JSON.stringify(oldFp.appInfo) !== JSON.stringify(newFp.appInfo),
94
- templateChanged = oldFp.templateHash !== newFp.templateHash,
95
- structural = abiChanged || libnodeChanged || pluginChanged || cliChanged || templateChanged;
96
-
97
- if (structural) {
98
- const reasons = [];
99
- if (abiChanged) reasons.push('ABI 配置');
100
- if (libnodeChanged) reasons.push('libnode.so');
101
- if (pluginChanged) reasons.push('插件版本');
102
- if (cliChanged) reasons.push('CLI 版本');
103
- if (templateChanged) reasons.push('模板文件');
104
- if (depsChanged) reasons.push('用户依赖');
105
- if (appInfoChanged) reasons.push('应用信息');
106
- console.log(' ⚠️ 检测到变化(' + reasons.join('、') + '),清理全部缓存');
107
- clearCommonCaches(buildDir, userProjectDir);
108
- removeDir(path.join(userProjectDir, 'node_modules', '@flun', 'nodejs-mobile-react-native', 'android', '.cxx'));
109
- }
110
- else if (depsChanged || appInfoChanged) {
111
- const reasons = [];
112
- if (depsChanged) reasons.push('用户依赖');
113
- if (appInfoChanged) reasons.push('应用信息');
114
- console.log(' ⚠️ 检测到变化(' + reasons.join('、') + '),清理 nodejs-assets 缓存 + app/build');
115
- clearCommonCaches(buildDir, userProjectDir);
116
- }
117
- else console.log(' ✓ 缓存有效,跳过清理');
118
-
119
- writeFp(fpFile, newFp);
120
- };
121
-
122
- export { computeFingerprint, checkAndResetCache };
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import crypto from 'crypto';
4
+ import { ensureDir, removeDir } from './utils.js';
5
+ import chalk from 'chalk';
6
+
7
+ const FINGERPRINT_FILE = '.build-fingerprint.json',
8
+ hashUserDeps = userProjectDir => {
9
+ const h = crypto.createHash('sha256'), pkgPath = path.join(userProjectDir, 'package.json');
10
+ if (fs.existsSync(pkgPath)) {
11
+ try {
12
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
13
+ h.update(JSON.stringify(pkg.dependencies || {})), h.update('|'), h.update(pkg.type || '');
14
+ } catch { h.update('package.json-read-error'); }
15
+ }
16
+ else h.update('no-package');
17
+ return h.digest('hex');
18
+ },
19
+ writeFp = (file, fp) => fs.writeFileSync(file, JSON.stringify(fp, null, 2), 'utf-8'),
20
+ clearCommonCaches = (buildDir, userProjectDir) => {
21
+ removeDir(path.join(buildDir, 'android', 'build'));
22
+ removeDir(path.join(buildDir, 'android', 'app', 'build'));
23
+ removeDir(path.join(userProjectDir, 'node_modules', '@flun', 'nodejs-mobile-react-native', 'android', 'build'));
24
+ },
25
+ hashTemplate = templateDir => {
26
+ const h = crypto.createHash('sha256'),
27
+ walk = d => {
28
+ let entries;
29
+ try { entries = fs.readdirSync(d, { withFileTypes: true }); } catch { return; }
30
+ entries.sort((a, b) => a.name.localeCompare(b.name));
31
+ for (const e of entries) {
32
+ if (e.name === 'node_modules' || e.name === '.git') continue;
33
+ const full = path.join(d, e.name), rel = path.relative(templateDir, full).replace(/\\/g, '/');
34
+ if (e.isDirectory()) { h.update('D:' + rel + '|'); walk(full); }
35
+ else if (e.isFile()) {
36
+ h.update('F:' + rel + '|');
37
+ try { h.update(fs.readFileSync(full)); } catch { h.update('err'); }
38
+ h.update('|');
39
+ }
40
+ }
41
+ };
42
+ walk(templateDir);
43
+ return h.digest('hex');
44
+ },
45
+ computeFingerprint = (userProjectDir, config, pkgRoot) => {
46
+ const abiFilters = [...(config.android.abiFilters || [])].sort(), libnode = {},
47
+ pluginDir = path.join(userProjectDir, 'node_modules', '@flun', 'nodejs-mobile-react-native'),
48
+ libnodeBin = path.join(pluginDir, 'android', 'libnode', 'bin');
49
+
50
+ if (fs.existsSync(libnodeBin)) {
51
+ for (const arch of fs.readdirSync(libnodeBin)) {
52
+ const so = path.join(libnodeBin, arch, 'libnode.so');
53
+ if (fs.existsSync(so)) {
54
+ const stat = fs.statSync(so);
55
+ libnode[arch] = { size: stat.size, mtime: Math.floor(stat.mtimeMs) };
56
+ }
57
+ }
58
+ }
59
+
60
+ let pluginVersion = null, cliVersion = null;
61
+ const pluginPkg = path.join(pluginDir, 'package.json'), cliPkg = path.join(pkgRoot, 'package.json');
62
+ if (fs.existsSync(pluginPkg)) try { pluginVersion = JSON.parse(fs.readFileSync(pluginPkg, 'utf-8')).version; } catch { };
63
+ if (fs.existsSync(cliPkg)) try { cliVersion = JSON.parse(fs.readFileSync(cliPkg, 'utf-8')).version; } catch { };
64
+
65
+ const depsHash = hashUserDeps(userProjectDir), a = config.android || {},
66
+ appInfo = {
67
+ appName: config.appName || null,
68
+ appId: config.appId || null,
69
+ versionCode: config.versionCode || null,
70
+ versionName: config.versionName || null,
71
+ minSdkVersion: a.minSdkVersion || null,
72
+ targetSdkVersion: a.targetSdkVersion || null,
73
+ compileSdkVersion: a.compileSdkVersion || null,
74
+ buildToolsVersion: a.buildToolsVersion || null,
75
+ usesCleartextTraffic: a.usesCleartextTraffic !== false,
76
+ permissions: [...(a.permissions || [])].sort(),
77
+ };
78
+
79
+ const templateHash = hashTemplate(path.join(pkgRoot, 'template'));
80
+ return { abiFilters, libnode, pluginVersion, cliVersion, depsHash, appInfo, templateHash };
81
+ },
82
+ checkAndResetCache = (buildDir, userProjectDir, config, pkgRoot) => {
83
+ ensureDir(buildDir);
84
+
85
+ const fpFile = path.join(buildDir, FINGERPRINT_FILE), newFp = computeFingerprint(userProjectDir, config, pkgRoot);
86
+ let oldFp = null;
87
+ if (fs.existsSync(fpFile)) try { oldFp = JSON.parse(fs.readFileSync(fpFile, 'utf-8')); } catch { };
88
+ if (!oldFp) return console.log(chalk.green(' ✓ 全新构建,无缓存可清')), writeFp(fpFile, newFp);
89
+
90
+ const abiChanged = JSON.stringify(oldFp.abiFilters) !== JSON.stringify(newFp.abiFilters),
91
+ libnodeChanged = JSON.stringify(oldFp.libnode) !== JSON.stringify(newFp.libnode),
92
+ pluginChanged = oldFp.pluginVersion !== newFp.pluginVersion,
93
+ cliChanged = oldFp.cliVersion !== newFp.cliVersion, depsChanged = oldFp.depsHash !== newFp.depsHash,
94
+ appInfoChanged = JSON.stringify(oldFp.appInfo) !== JSON.stringify(newFp.appInfo),
95
+ templateChanged = oldFp.templateHash !== newFp.templateHash,
96
+ structural = abiChanged || libnodeChanged || pluginChanged || cliChanged || templateChanged;
97
+
98
+ if (structural) {
99
+ const reasons = [];
100
+ if (abiChanged) reasons.push('ABI 配置');
101
+ if (libnodeChanged) reasons.push('libnode.so');
102
+ if (pluginChanged) reasons.push('插件版本');
103
+ if (cliChanged) reasons.push('CLI 版本');
104
+ if (templateChanged) reasons.push('模板文件');
105
+ if (depsChanged) reasons.push('用户依赖');
106
+ if (appInfoChanged) reasons.push('应用信息');
107
+ console.log(chalk.yellow(' ⚠️ 检测到变化(' + reasons.join('、') + '),清理全部缓存'));
108
+ clearCommonCaches(buildDir, userProjectDir);
109
+ removeDir(path.join(userProjectDir, 'node_modules', '@flun', 'nodejs-mobile-react-native', 'android', '.cxx'));
110
+ }
111
+ else if (depsChanged || appInfoChanged) {
112
+ const reasons = [];
113
+ if (depsChanged) reasons.push('用户依赖');
114
+ if (appInfoChanged) reasons.push('应用信息');
115
+ console.log(chalk.yellow(' ⚠️ 检测到变化(' + reasons.join('、') + '),清理 nodejs-assets 缓存 + app/build'));
116
+ clearCommonCaches(buildDir, userProjectDir);
117
+ }
118
+ else console.log(chalk.gray(' ✓ 缓存有效,跳过清理'));
119
+
120
+ writeFp(fpFile, newFp);
121
+ };
122
+
123
+ export { computeFingerprint, checkAndResetCache };
package/src/generate.js CHANGED
@@ -1,104 +1,105 @@
1
- import fs from 'fs';
2
- import path from 'path';
3
- import { writeIfChanged } from './utils.js';
4
-
5
- /**
6
- * 生成 main.js(桥接代码,用户不需要手写)
7
- *
8
- * 上报机制(通过 rn-bridge channel 发消息给 App.tsx):
9
- * - 'Node started on <URL>' server.js 成功调用 listen
10
- * - 'Node failed: <原因>' 加载异常或 5 秒内未捕获 listen
11
- */
12
- const generateMainJs = (targetDir, config, moduleType) => {
13
- const entry = config.serverPath.startsWith('./') ? config.serverPath : './' + config.serverPath,
14
- entryLabel = config.serverPath, hb = config.advanced.heartbeatInterval,
15
- body = `(async () => {
16
- const rn_bridge = require('rn-bridge');
17
- const net = require('net'), https = require('https'), USER_ENTRY = ${JSON.stringify(entry)},
18
- ENTRY_LABEL = ${JSON.stringify(entryLabel)}, START_TIMEOUT_MS = 5000;
19
-
20
- let resolved = false;
21
- function emitUrl(url) {
22
- if (resolved) return;
23
- resolved = true;
24
- console.log('[node-mobile-app] server URL: ' + url);
25
- rn_bridge.channel.send('Node started on ' + url);
26
- }
27
-
28
- function emitFailure(reason, detail) {
29
- if (resolved) return;
30
- resolved = true;
31
- const msg = detail ? (reason + ' -- ' + detail) : reason;
32
- console.log('[node-mobile-app] failed: ' + msg);
33
- rn_bridge.channel.send('Node failed: ' + msg);
34
- }
35
-
36
- rn_bridge.channel.send('Node was initialized.');
37
- process.on('uncaughtException', (err) => {
38
- console.error('未捕获的异常:', err);
39
- emitFailure('未捕获异常', err && err.message);
40
- });
41
- process.on('unhandledRejection', (reason) => {
42
- console.error('未处理的 Promise rejection:', reason);
43
- emitFailure('未处理的 Promise rejection', String(reason));
44
- });
45
-
46
- // 拦截 net.Server.prototype.listen,捕获协议和真实端口
47
- const origListen = net.Server.prototype.listen;
48
- net.Server.prototype.listen = function (...args) {
49
- const server = this;
50
- if (!resolved) {
51
- server.once('listening', () => {
52
- try {
53
- const addr = server.address(), port = (addr && typeof addr === 'object') ? addr.port : addr,
54
- protocol = (server instanceof https.Server) ? 'https' : 'http';
55
- emitUrl(protocol + '://127.0.0.1:' + port);
56
- } catch (e) { /* 等超时 */ }
57
- });
58
- }
59
- return origListen.apply(this, args);
60
- };
61
-
62
- // 5 秒超时兜底
63
- setTimeout(() => {
64
- if (resolved) return;
65
- emitFailure('Node 服务 5 秒内未启动', '请检查 ' + ENTRY_LABEL + ' 是否调用了 listen()');
66
- }, START_TIMEOUT_MS);
67
-
68
- // 加载用户入口
69
- (async () => {
70
- try {
71
- await import(USER_ENTRY);
72
- } catch (err) {
73
- emitFailure('加载 ' + ENTRY_LABEL + ' 失败', err && err.message);
74
- }
75
- })();
76
-
77
- ${config.advanced.keepAlive === false ? '// keepAlive: false — 不发送心跳' : "setInterval(() => rn_bridge.channel.send('heartbeat'), " + hb + ");"}
78
-
79
- rn_bridge.channel.on('message', (msg) => {
80
- rn_bridge.channel.send('Echo: ' + msg);
81
- });
82
- })();`,
83
- header = moduleType === 'module'
84
- ? `import { createRequire } from 'module';\nconst require = createRequire(import.meta.url);\n\n` : '',
85
- file = path.join(targetDir, 'main.js'), changed = writeIfChanged(file, header + body);
86
- console.log(changed ? ' ✓ 已生成 main.js' : ' ✓ main.js 无变化');
87
- },
88
- generateRuntime = (buildDir, config) => {
89
- const data = {
90
- showLoading: config.webview.showLoading,
91
- loadingText: config.webview.loadingText,
92
- webview: {
93
- enableJavaScript: config.webview.enableJavaScript,
94
- enableDomStorage: config.webview.enableDomStorage,
95
- allowFileAccess: config.webview.allowFileAccess,
96
- mixedContentMode: config.webview.mixedContentMode,
97
- backgroundColor: config.webview.backgroundColor
98
- },
99
- }, content = `// 自动生成,请勿手改\nexport default ${JSON.stringify(data, null, 2)} as const;\n`,
100
- changed = writeIfChanged(path.join(buildDir, 'mobileApp.runtime.ts'), content);
101
- console.log(changed ? ' ✓ 已生成 mobileApp.runtime.ts' : ' ✓ mobileApp.runtime.ts 无变化');
102
- };
103
-
104
- export { generateMainJs, generateRuntime }
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { writeIfChanged } from './utils.js';
4
+ import chalk from 'chalk';
5
+
6
+ /**
7
+ * 生成 main.js(桥接代码,用户不需要手写)
8
+ *
9
+ * 上报机制(通过 rn-bridge channel 发消息给 App.tsx):
10
+ * - 'Node started on <URL>' server.js 成功调用 listen
11
+ * - 'Node failed: <原因>' 加载异常或 5 秒内未捕获 listen
12
+ */
13
+ const generateMainJs = (targetDir, config, moduleType) => {
14
+ const entry = config.serverPath.startsWith('./') ? config.serverPath : './' + config.serverPath,
15
+ entryLabel = config.serverPath, hb = config.advanced.heartbeatInterval,
16
+ body = `(async () => {
17
+ const rn_bridge = require('rn-bridge');
18
+ const net = require('net'), https = require('https'), USER_ENTRY = ${JSON.stringify(entry)},
19
+ ENTRY_LABEL = ${JSON.stringify(entryLabel)}, START_TIMEOUT_MS = 5000;
20
+
21
+ let resolved = false;
22
+ function emitUrl(url) {
23
+ if (resolved) return;
24
+ resolved = true;
25
+ console.log('[node-mobile-app] server URL: ' + url);
26
+ rn_bridge.channel.send('Node started on ' + url);
27
+ }
28
+
29
+ function emitFailure(reason, detail) {
30
+ if (resolved) return;
31
+ resolved = true;
32
+ const msg = detail ? (reason + ' -- ' + detail) : reason;
33
+ console.log('[node-mobile-app] failed: ' + msg);
34
+ rn_bridge.channel.send('Node failed: ' + msg);
35
+ }
36
+
37
+ rn_bridge.channel.send('Node was initialized.');
38
+ process.on('uncaughtException', (err) => {
39
+ console.error('未捕获的异常:', err);
40
+ emitFailure('未捕获异常', err && err.message);
41
+ });
42
+ process.on('unhandledRejection', (reason) => {
43
+ console.error('未处理的 Promise rejection:', reason);
44
+ emitFailure('未处理的 Promise rejection', String(reason));
45
+ });
46
+
47
+ // 拦截 net.Server.prototype.listen,捕获协议和真实端口
48
+ const origListen = net.Server.prototype.listen;
49
+ net.Server.prototype.listen = function (...args) {
50
+ const server = this;
51
+ if (!resolved) {
52
+ server.once('listening', () => {
53
+ try {
54
+ const addr = server.address(), port = (addr && typeof addr === 'object') ? addr.port : addr,
55
+ protocol = (server instanceof https.Server) ? 'https' : 'http';
56
+ emitUrl(protocol + '://127.0.0.1:' + port);
57
+ } catch (e) { /* 等超时 */ }
58
+ });
59
+ }
60
+ return origListen.apply(this, args);
61
+ };
62
+
63
+ // 5 秒超时兜底
64
+ setTimeout(() => {
65
+ if (resolved) return;
66
+ emitFailure('Node 服务 5 秒内未启动', '请检查 ' + ENTRY_LABEL + ' 是否调用了 listen()');
67
+ }, START_TIMEOUT_MS);
68
+
69
+ // 加载用户入口
70
+ (async () => {
71
+ try {
72
+ await import(USER_ENTRY);
73
+ } catch (err) {
74
+ emitFailure('加载 ' + ENTRY_LABEL + ' 失败', err && err.message);
75
+ }
76
+ })();
77
+
78
+ ${config.advanced.keepAlive === false ? '// keepAlive: false — 不发送心跳' : "setInterval(() => rn_bridge.channel.send('heartbeat'), " + hb + ");"}
79
+
80
+ rn_bridge.channel.on('message', (msg) => {
81
+ rn_bridge.channel.send('Echo: ' + msg);
82
+ });
83
+ })();`,
84
+ header = moduleType === 'module'
85
+ ? `import { createRequire } from 'module';\nconst require = createRequire(import.meta.url);\n\n` : '',
86
+ file = path.join(targetDir, 'main.js'), changed = writeIfChanged(file, header + body);
87
+ console.log(chalk.green(changed ? ' ✓ 已生成 main.js' : ' ✓ main.js 无变化'));
88
+ },
89
+ generateRuntime = (buildDir, config) => {
90
+ const data = {
91
+ showLoading: config.webview.showLoading,
92
+ loadingText: config.webview.loadingText,
93
+ webview: {
94
+ enableJavaScript: config.webview.enableJavaScript,
95
+ enableDomStorage: config.webview.enableDomStorage,
96
+ allowFileAccess: config.webview.allowFileAccess,
97
+ mixedContentMode: config.webview.mixedContentMode,
98
+ backgroundColor: config.webview.backgroundColor
99
+ },
100
+ }, content = `// 自动生成,请勿手改\nexport default ${JSON.stringify(data, null, 2)} as const;\n`,
101
+ changed = writeIfChanged(path.join(buildDir, 'mobileApp.runtime.ts'), content);
102
+ console.log(chalk.green(changed ? ' ✓ 已生成 mobileApp.runtime.ts' : ' ✓ mobileApp.runtime.ts 无变化'));
103
+ };
104
+
105
+ export { generateMainJs, generateRuntime }