@flun/node-mobile-app 3.1.5 → 4.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.
- package/CHANGELOG.md +33 -5
- package/README.md +559 -546
- package/bin/cli.js +3 -2
- package/package.json +4 -3
- package/src/commands/build.js +115 -114
- package/src/commands/clean.js +4 -3
- package/src/commands/test.js +160 -109
- package/src/config.js +6 -5
- package/src/copy.js +320 -319
- package/src/fingerprint.js +123 -122
- package/src/generate.js +105 -104
- package/src/install.js +317 -215
- package/src/optimize-node-modules.js +55 -56
- package/src/patcher.js +389 -388
- package/src/platform-ios.js +6 -5
- package/src/utils.js +92 -5
package/src/fingerprint.js
CHANGED
|
@@ -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
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
removeDir(path.join(buildDir, 'android', '
|
|
22
|
-
removeDir(path.join(
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
entries.
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
h.update('
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
if (fs.existsSync(
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
if (
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
if (
|
|
101
|
-
if (
|
|
102
|
-
if (
|
|
103
|
-
if (
|
|
104
|
-
if (
|
|
105
|
-
if (
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
if (
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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
|
-
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* - 'Node
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
resolved
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
resolved
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
net.Server.prototype.listen
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
})
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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 }
|