@flun/node-mobile-app 2.1.2 → 3.1.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/Android /346/236/204/345/273/272/346/214/207/345/215/227.md" +1 -1
- package/CHANGELOG.md +40 -23
- package/README.md +36 -7
- package/bin/cli.js +11 -10
- package/mobileAppConfig.js +10 -0
- package/package.json +15 -13
- package/src/commands/build.js +19 -14
- package/src/commands/clean.js +6 -2
- package/src/commands/test.js +26 -25
- package/src/config.js +6 -1
- package/src/copy.js +8 -23
- package/src/fingerprint.js +28 -8
- package/src/install.js +100 -55
- package/src/optimize-node-modules.js +696 -0
- package/src/patcher.js +21 -24
- package/src/platform-ios.js +139 -141
- package/src/utils.js +189 -176
package/src/fingerprint.js
CHANGED
|
@@ -21,6 +21,26 @@ const FINGERPRINT_FILE = '.build-fingerprint.json',
|
|
|
21
21
|
removeDir(path.join(buildDir, 'android', 'app', 'build'));
|
|
22
22
|
removeDir(path.join(userProjectDir, 'node_modules', '@flun', 'nodejs-mobile-react-native', 'android', 'build'));
|
|
23
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
|
+
},
|
|
24
44
|
computeFingerprint = (userProjectDir, config, pkgRoot) => {
|
|
25
45
|
const abiFilters = [...(config.android.abiFilters || [])].sort(), libnode = {},
|
|
26
46
|
pluginDir = path.join(userProjectDir, 'node_modules', '@flun', 'nodejs-mobile-react-native'),
|
|
@@ -36,12 +56,9 @@ const FINGERPRINT_FILE = '.build-fingerprint.json',
|
|
|
36
56
|
}
|
|
37
57
|
}
|
|
38
58
|
|
|
39
|
-
let pluginVersion = null;
|
|
40
|
-
const pluginPkg = path.join(pluginDir, 'package.json');
|
|
59
|
+
let pluginVersion = null, cliVersion = null;
|
|
60
|
+
const pluginPkg = path.join(pluginDir, 'package.json'), cliPkg = path.join(pkgRoot, 'package.json');
|
|
41
61
|
if (fs.existsSync(pluginPkg)) try { pluginVersion = JSON.parse(fs.readFileSync(pluginPkg, 'utf-8')).version; } catch { };
|
|
42
|
-
|
|
43
|
-
let cliVersion = null;
|
|
44
|
-
const cliPkg = path.join(pkgRoot, 'package.json');
|
|
45
62
|
if (fs.existsSync(cliPkg)) try { cliVersion = JSON.parse(fs.readFileSync(cliPkg, 'utf-8')).version; } catch { };
|
|
46
63
|
|
|
47
64
|
const depsHash = hashUserDeps(userProjectDir), a = config.android || {},
|
|
@@ -58,7 +75,8 @@ const FINGERPRINT_FILE = '.build-fingerprint.json',
|
|
|
58
75
|
permissions: [...(a.permissions || [])].sort(),
|
|
59
76
|
};
|
|
60
77
|
|
|
61
|
-
|
|
78
|
+
const templateHash = hashTemplate(path.join(pkgRoot, 'template'));
|
|
79
|
+
return { abiFilters, libnode, pluginVersion, cliVersion, depsHash, appInfo, templateHash };
|
|
62
80
|
},
|
|
63
81
|
checkAndResetCache = (buildDir, userProjectDir, config, pkgRoot) => {
|
|
64
82
|
ensureDir(buildDir);
|
|
@@ -66,14 +84,15 @@ const FINGERPRINT_FILE = '.build-fingerprint.json',
|
|
|
66
84
|
const fpFile = path.join(buildDir, FINGERPRINT_FILE), newFp = computeFingerprint(userProjectDir, config, pkgRoot);
|
|
67
85
|
let oldFp = null;
|
|
68
86
|
if (fs.existsSync(fpFile)) try { oldFp = JSON.parse(fs.readFileSync(fpFile, 'utf-8')); } catch { };
|
|
69
|
-
if (!oldFp) return console.log(' ✓
|
|
87
|
+
if (!oldFp) return console.log(' ✓ 全新构建,无缓存可清'), writeFp(fpFile, newFp);
|
|
70
88
|
|
|
71
89
|
const abiChanged = JSON.stringify(oldFp.abiFilters) !== JSON.stringify(newFp.abiFilters),
|
|
72
90
|
libnodeChanged = JSON.stringify(oldFp.libnode) !== JSON.stringify(newFp.libnode),
|
|
73
91
|
pluginChanged = oldFp.pluginVersion !== newFp.pluginVersion,
|
|
74
92
|
cliChanged = oldFp.cliVersion !== newFp.cliVersion, depsChanged = oldFp.depsHash !== newFp.depsHash,
|
|
75
93
|
appInfoChanged = JSON.stringify(oldFp.appInfo) !== JSON.stringify(newFp.appInfo),
|
|
76
|
-
|
|
94
|
+
templateChanged = oldFp.templateHash !== newFp.templateHash,
|
|
95
|
+
structural = abiChanged || libnodeChanged || pluginChanged || cliChanged || templateChanged;
|
|
77
96
|
|
|
78
97
|
if (structural) {
|
|
79
98
|
const reasons = [];
|
|
@@ -81,6 +100,7 @@ const FINGERPRINT_FILE = '.build-fingerprint.json',
|
|
|
81
100
|
if (libnodeChanged) reasons.push('libnode.so');
|
|
82
101
|
if (pluginChanged) reasons.push('插件版本');
|
|
83
102
|
if (cliChanged) reasons.push('CLI 版本');
|
|
103
|
+
if (templateChanged) reasons.push('模板文件');
|
|
84
104
|
if (depsChanged) reasons.push('用户依赖');
|
|
85
105
|
if (appInfoChanged) reasons.push('应用信息');
|
|
86
106
|
console.log(' ⚠️ 检测到变化(' + reasons.join('、') + '),清理全部缓存');
|
package/src/install.js
CHANGED
|
@@ -1,53 +1,47 @@
|
|
|
1
1
|
import path from 'path';
|
|
2
2
|
import fs from 'fs';
|
|
3
|
-
import crypto from 'crypto';
|
|
4
3
|
import { run } from './utils.js';
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
},
|
|
4
|
+
import { clearVerified, optimizeNodeModules, SELF_HASH } from './optimize-node-modules.js';
|
|
5
|
+
const
|
|
6
|
+
/**
|
|
7
|
+
* 内部:从 libnode 的 node_version.h 读取运行时 Node 主版本号
|
|
8
|
+
* 读不到直接报错退出(说明 libnode 目录不完整,继续执行必失败)
|
|
9
|
+
*/
|
|
10
|
+
resolveRuntimeMajor = (userProjectDir, targetPlatform) => {
|
|
11
|
+
const sub = targetPlatform === 'ios' ? 'ios' : 'android',
|
|
12
|
+
header = path.join(userProjectDir, 'node_modules', '@flun', 'nodejs-mobile-react-native',
|
|
13
|
+
sub, 'libnode', 'include', 'node', 'node_version.h');
|
|
14
|
+
if (!fs.existsSync(header)) {
|
|
15
|
+
console.error(' [错误] 未找到 node_version.h: ' + header);
|
|
16
|
+
console.error(' 说明 libnode 目录不完整,请重新安装依赖。'), process.exit(1);
|
|
17
|
+
}
|
|
18
|
+
const m = fs.readFileSync(header, 'utf-8').match(/#define\s+NODE_MAJOR_VERSION\s+(\d+)/);
|
|
19
|
+
if (!m) console.error(' [错误] 无法解析 node_version.h 中的 NODE_MAJOR_VERSION'), process.exit(1);
|
|
20
|
+
return m[1];
|
|
21
|
+
},
|
|
24
22
|
/**
|
|
25
23
|
* 内部:判断用户项目的生产依赖里是否含原生模块(binding.gyp)
|
|
26
24
|
* 按用户根 package.json 的 dependencies 键名,去 userProjectDir/node_modules 下逐个查
|
|
27
|
-
* 不用扫 nodejs-project/node_modules
|
|
25
|
+
* 不用扫 nodejs-project/node_modules:全新构建时它还不存在
|
|
28
26
|
*/
|
|
29
|
-
hasNativeModules =
|
|
27
|
+
hasNativeModules = userProjectDir => {
|
|
30
28
|
const pkgPath = path.join(userProjectDir, 'package.json');
|
|
31
29
|
if (!fs.existsSync(pkgPath)) return false;
|
|
32
30
|
let deps = {};
|
|
33
31
|
try {
|
|
34
32
|
deps = JSON.parse(fs.readFileSync(pkgPath, 'utf-8')).dependencies || {};
|
|
35
|
-
} catch {
|
|
36
|
-
|
|
37
|
-
}
|
|
33
|
+
} catch { return false; }
|
|
34
|
+
|
|
38
35
|
const nodeModulesDir = path.join(userProjectDir, 'node_modules');
|
|
39
|
-
for (const name of Object.keys(deps))
|
|
36
|
+
for (const name of Object.keys(deps))
|
|
40
37
|
if (fs.existsSync(path.join(nodeModulesDir, name, 'binding.gyp'))) return true;
|
|
41
|
-
}
|
|
42
38
|
return false;
|
|
43
39
|
},
|
|
44
40
|
/**
|
|
45
41
|
* 内部:定位 @flun/nodejs-mobile-react-native 插件目录
|
|
46
42
|
*/
|
|
47
43
|
findPluginDir = userProjectDir => {
|
|
48
|
-
const candidates = [
|
|
49
|
-
path.join(userProjectDir, 'node_modules', '@flun', 'nodejs-mobile-react-native'),
|
|
50
|
-
];
|
|
44
|
+
const candidates = [path.join(userProjectDir, 'node_modules', '@flun', 'nodejs-mobile-react-native')];
|
|
51
45
|
for (const c of candidates) if (fs.existsSync(path.join(c, 'android', 'build.gradle'))) return c;
|
|
52
46
|
return null;
|
|
53
47
|
},
|
|
@@ -55,9 +49,7 @@ const hashDeps = (pkgPath, hasNative, forceRebuild) => {
|
|
|
55
49
|
* 内部:iOS 的 WebView SSL 补丁
|
|
56
50
|
*/
|
|
57
51
|
patchWebViewSslIos = userProjectDir => {
|
|
58
|
-
const file = path.join(
|
|
59
|
-
userProjectDir, 'node_modules', 'react-native-webview', 'apple', 'RNCWebViewImpl.m'
|
|
60
|
-
);
|
|
52
|
+
const file = path.join(userProjectDir, 'node_modules', 'react-native-webview', 'apple', 'RNCWebViewImpl.m');
|
|
61
53
|
if (!fs.existsSync(file)) return;
|
|
62
54
|
|
|
63
55
|
let c = fs.readFileSync(file, 'utf-8');
|
|
@@ -85,29 +77,81 @@ const hashDeps = (pkgPath, hasNative, forceRebuild) => {
|
|
|
85
77
|
/**
|
|
86
78
|
* 装用户项目的生产依赖到 <buildDir>/nodejs-assets/nodejs-project/
|
|
87
79
|
* 只装 dependencies,不装 devDependencies
|
|
88
|
-
*
|
|
80
|
+
* 依赖 + 环境未变化且上次构建完成时跳过;否则按需重装 / 仅优化
|
|
81
|
+
* 快照记录在 buildDir/.deps-snapshot.json(与桌面版一致)
|
|
89
82
|
*/
|
|
90
|
-
installUserDeps = (targetDir, userProjectDir, config) => {
|
|
83
|
+
installUserDeps = async (targetDir, userProjectDir, config, buildDir, targetPlatform) => {
|
|
91
84
|
const assetsDir = path.dirname(targetDir), bnmFile = path.join(assetsDir, 'BUILD_NATIVE_MODULES.txt'),
|
|
92
|
-
|
|
93
|
-
forceRebuild = process.env.NODE_MOBILE_FORCE_REBUILD === '1', hasNative = hasNativeModules(userProjectDir)
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
85
|
+
nodeModulesDir = path.join(targetDir, 'node_modules'), snapshotPath = path.join(buildDir, '.deps-snapshot.json'),
|
|
86
|
+
forceRebuild = process.env.NODE_MOBILE_FORCE_REBUILD === '1', hasNative = hasNativeModules(userProjectDir),
|
|
87
|
+
optimizeCfg = config.optimize || {}, thisOptimize = optimizeCfg.enabled !== false,
|
|
88
|
+
abiFilters = [...(config.android.abiFilters || [])].sort(),
|
|
89
|
+
runtimeMajor = resolveRuntimeMajor(userProjectDir, targetPlatform);
|
|
90
|
+
|
|
91
|
+
fs.writeFileSync(bnmFile, hasNative ? '1' : '0');
|
|
92
|
+
if (hasNative) console.log(' ℹ️ 检测到原生模块(binding.gyp),将编译原生代码');
|
|
93
|
+
|
|
94
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(targetDir, 'package.json'), 'utf-8')), deps = pkg.dependencies || {},
|
|
95
|
+
sortedDeps = Object.keys(deps).sort().reduce((acc, k) => { acc[k] = deps[k]; return acc; }, {}),
|
|
96
|
+
snapshotData = { deps: sortedDeps, type: pkg.type || '', hasNative, forceRebuild };
|
|
97
|
+
|
|
98
|
+
let shouldInstall = true, shouldOptimize = thisOptimize, reasons = [];
|
|
99
|
+
if (fs.existsSync(snapshotPath)) {
|
|
100
|
+
const old = JSON.parse(fs.readFileSync(snapshotPath, 'utf-8')), verifiedOk = old.verified === true,
|
|
101
|
+
depsSame = JSON.stringify({
|
|
102
|
+
deps: old.deps, type: old.type, hasNative: old.hasNative, forceRebuild: old.forceRebuild
|
|
103
|
+
}) === JSON.stringify(snapshotData),
|
|
104
|
+
envSame = old.platform === targetPlatform && JSON.stringify(old.abiFilters) === JSON.stringify(abiFilters) &&
|
|
105
|
+
old.runtimeMajor === runtimeMajor,
|
|
106
|
+
lastOptimized = old.optimized !== false, selfHashSame = old.optimizeHash === SELF_HASH,
|
|
107
|
+
nmExists = fs.existsSync(nodeModulesDir);
|
|
108
|
+
if (!depsSame) reasons.push('依赖清单已变化');
|
|
109
|
+
if (!verifiedOk) reasons.push('上次构建未完成');
|
|
110
|
+
if (!envSame) reasons.push('运行环境已变化');
|
|
111
|
+
if (!nmExists) reasons.push('缓存目录缺失');
|
|
112
|
+
if (lastOptimized && !thisOptimize) reasons.push('优化已由开启转为关闭');
|
|
113
|
+
if (thisOptimize && !selfHashSame && lastOptimized) reasons.push('优化程序已更新');
|
|
114
|
+
if (reasons.length > 0) {
|
|
115
|
+
shouldInstall = true, shouldOptimize = thisOptimize;
|
|
116
|
+
console.log(' ℹ️ 需要重新安装依赖(原因: ' + reasons.join(', ') + ')');
|
|
117
|
+
}
|
|
118
|
+
else if (thisOptimize && !lastOptimized)
|
|
119
|
+
shouldInstall = false, shouldOptimize = true, console.log(' ℹ️ 依赖与环境未变化,仅需优化');
|
|
120
|
+
else {
|
|
121
|
+
shouldInstall = false, shouldOptimize = false, console.log(' ✓ 依赖与环境未变化,跳过安装与优化');
|
|
122
|
+
await clearVerified(buildDir);
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
} else console.log(' ℹ️ 需要重新安装依赖(原因: 缓存快照缺失)');
|
|
126
|
+
|
|
127
|
+
if (shouldInstall) {
|
|
128
|
+
if (fs.existsSync(nodeModulesDir))
|
|
129
|
+
console.log(' 清理旧 node_modules...'), fs.rmSync(nodeModulesDir, { recursive: true, force: true });
|
|
130
|
+
|
|
131
|
+
try { fs.rmSync(snapshotPath, { force: true }); } catch { }
|
|
132
|
+
const env = { ...process.env };
|
|
133
|
+
if (hasNative) env.NODEJS_MOBILE_BUILD_NATIVE_MODULES = '1';
|
|
134
|
+
console.log(' 安装用户生产依赖(nodejs-assets/nodejs-project)...');
|
|
135
|
+
run('npm', ['install', '--omit=dev', '--no-audit', '--no-fund'], { cwd: targetDir, env, inherit: true });
|
|
136
|
+
const snap = {
|
|
137
|
+
...snapshotData, platform: targetPlatform, abiFilters, runtimeMajor,
|
|
138
|
+
optimizeHash: SELF_HASH, optimized: false, verified: false
|
|
139
|
+
};
|
|
140
|
+
fs.writeFileSync(snapshotPath, JSON.stringify(snap, null, 2));
|
|
141
|
+
console.log(' ✓ 用户依赖安装完成');
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (shouldOptimize) {
|
|
145
|
+
try {
|
|
146
|
+
await optimizeNodeModules(targetDir, {
|
|
147
|
+
targetPlatform, abiFilters, runtimeMajor, exclude: optimizeCfg.exclude || [], snapshotDir: buildDir
|
|
148
|
+
});
|
|
149
|
+
} catch (err) {
|
|
150
|
+
console.error(' [错误] node_modules 优化失败:' + err.message);
|
|
151
|
+
console.error(' [提示] 可在 mobileAppConfig.js 中设置 optimize: { enabled: false } 关闭优化,');
|
|
152
|
+
console.error(' 或用 optimize: { exclude: ["包名"] } 排除问题包。'), process.exit(1);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
111
155
|
},
|
|
112
156
|
/**
|
|
113
157
|
* 修改 @flun/nodejs-mobile-react-native 插件 build.gradle 的 abiFilters
|
|
@@ -151,7 +195,8 @@ const hashDeps = (pkgPath, hasNative, forceRebuild) => {
|
|
|
151
195
|
if (cancelIndex !== -1) {
|
|
152
196
|
const newCode = [
|
|
153
197
|
'// LOCALHOST_SSL_BYPASS: 对 localhost/127.0.0.1 放行自签名证书(仅本地测试用)',
|
|
154
|
-
' if (failingUrl != null
|
|
198
|
+
' if (failingUrl != null',
|
|
199
|
+
' && (failingUrl.startsWith("https://127.0.0.1") || failingUrl.startsWith("https://localhost"))) {',
|
|
155
200
|
' handler.proceed();',
|
|
156
201
|
' return;',
|
|
157
202
|
' }',
|