@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.
@@ -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
- return { abiFilters, libnode, pluginVersion, cliVersion, depsHash, appInfo };
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(' ✓ 首次构建,无缓存可清'), writeFp(fpFile, newFp);
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
- structural = abiChanged || libnodeChanged || pluginChanged || cliChanged;
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
- * 内部:依赖 hash(dependencies + type + 是否含原生模块 + 是否强制重编)
8
- * hasNative 纳入哈希:装/卸原生模块时会触发重装
9
- * forceRebuild 纳入哈希:用户显式要求重编时会强制重装
10
- */
11
- const hashDeps = (pkgPath, hasNative, forceRebuild) => {
12
- const h = crypto.createHash('sha256');
13
- if (fs.existsSync(pkgPath)) {
14
- try {
15
- const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
16
- h.update(JSON.stringify(pkg.dependencies || {})), h.update('|'), h.update(pkg.type || '');
17
- } catch { h.update('read-error') }
18
- }
19
- else h.update('no-package');
20
- h.update('|native=' + (hasNative ? '1' : '0'));
21
- h.update('|force=' + (forceRebuild ? '1' : '0'));
22
- return h.digest('hex');
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 = (userProjectDir) => {
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
- return false;
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
- * 依赖未变化且 node_modules 存在时跳过(用 .deps-hash 记录)
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
- hashFile = path.join(targetDir, '.deps-hash'), nodeModulesDir = path.join(targetDir, 'node_modules'),
93
- forceRebuild = process.env.NODE_MOBILE_FORCE_REBUILD === '1', hasNative = hasNativeModules(userProjectDir);
94
-
95
- fs.writeFileSync(bnmFile, hasNative ? '1' : '0'); // 自动检测是否有原生模块(binding.gyp)
96
- // 计算当前依赖 hash(含 native + force 状态)
97
- const pkgPath = path.join(targetDir, 'package.json'), currentHash = hashDeps(pkgPath, hasNative, forceRebuild),
98
- hasModules = fs.existsSync(nodeModulesDir);
99
- let oldHash = null;
100
- if (fs.existsSync(hashFile)) try { oldHash = fs.readFileSync(hashFile, 'utf-8').trim(); } catch { };
101
- if (hasModules && oldHash === currentHash && !forceRebuild) return console.log(' ✓ 依赖无变化,跳过安装');
102
- if (forceRebuild) console.log(' ℹ️ NODE_MOBILE_FORCE_REBUILD=1,强制重装依赖');
103
-
104
- const env = { ...process.env };
105
- if (hasNative) env.NODEJS_MOBILE_BUILD_NATIVE_MODULES = '1', console.log(' ℹ️ 检测到原生模块(binding.gyp),将编译原生代码');
106
-
107
- console.log(' 安装用户生产依赖(nodejs-assets/nodejs-project)...');
108
- run('npm', ['install', '--omit=dev', '--no-audit', '--no-fund'], { cwd: targetDir, env });
109
- fs.writeFileSync(hashFile, currentHash, 'utf-8');
110
- console.log(' ✓ 用户依赖安装完成');
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 && (failingUrl.startsWith("https://127.0.0.1") || failingUrl.startsWith("https://localhost"))) {',
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
  ' }',