@flun/node-mobile-app 4.0.2 → 4.0.4
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" +38 -38
- package/CHANGELOG.md +8 -47
- package/bin/cli.js +5 -5
- package/mobileAppConfig.js +18 -18
- package/package.json +1 -1
- package/scripts/copy-files.js +1 -1
- package/src/commands/build.js +1 -1
- package/src/commands/test.js +21 -15
- package/src/config.js +8 -11
- package/src/copy.js +24 -28
- package/src/fingerprint.js +6 -6
- package/src/generate.js +7 -10
- package/src/install.js +13 -13
- package/src/optimize-node-modules.js +10 -8
- package/src/patcher.js +21 -21
- package/src/platform-ios.js +7 -7
- package/src/utils.js +13 -13
- package/template/App.tsx +83 -87
- package/template/index.js +1 -1
- package/template/metro.config.js +1 -3
- package//350/207/252/345/256/232/344/271/211libnode/346/214/207/345/215/227.md +68 -68
package/src/copy.js
CHANGED
|
@@ -61,7 +61,7 @@ const BUILD_DIR_NAME = 'node-mobile-app-build',
|
|
|
61
61
|
* 把 buildDir/package.json 的依赖版本同步为主包 dependencies 的版本
|
|
62
62
|
* - 依赖名单来自 buildDir/package.json(模板声明需要哪些依赖)
|
|
63
63
|
* - 版本号来自 pkgRoot/package.json 的 dependencies(单一数据源)
|
|
64
|
-
* -
|
|
64
|
+
* - 若主包缺少某个依赖,抛错中断(避免模板占位值被真正使用)
|
|
65
65
|
*/
|
|
66
66
|
syncTemplateDependencies = (pkgRoot, buildDir) => {
|
|
67
67
|
const buildPkgPath = path.join(buildDir, 'package.json');
|
|
@@ -74,7 +74,7 @@ const BUILD_DIR_NAME = 'node-mobile-app-build',
|
|
|
74
74
|
const mainDeps = mainPkg.dependencies || {}, missing = [];
|
|
75
75
|
let depsUpdated = 0, needWrite = false;
|
|
76
76
|
|
|
77
|
-
// 移除仅用于模板占位说明的 _comment
|
|
77
|
+
// 移除仅用于模板占位说明的 _comment 字段,避免带进最终 build 工程
|
|
78
78
|
if ('_comment' in buildPkg) delete buildPkg._comment, needWrite = true;
|
|
79
79
|
for (const name of Object.keys(buildPkg.dependencies)) {
|
|
80
80
|
const v = mainDeps[name];
|
|
@@ -91,27 +91,25 @@ const BUILD_DIR_NAME = 'node-mobile-app-build',
|
|
|
91
91
|
throw new Error(
|
|
92
92
|
'template/package.json 声明的以下依赖在主包 dependencies 中不存在:\n' +
|
|
93
93
|
' ' + missing.join(', ') + '\n' +
|
|
94
|
-
' 请在 @flun/node-mobile-app 的 package.json
|
|
95
|
-
'
|
|
94
|
+
' 请在 @flun/node-mobile-app 的 package.json 中补上对应声明(版本号随意,\n' +
|
|
95
|
+
' 但必须存在,以便 syncTemplateDependencies 覆盖模板里的占位值)。'
|
|
96
96
|
);
|
|
97
97
|
}
|
|
98
98
|
},
|
|
99
99
|
/**
|
|
100
|
-
* 把主包 node_modules 里被 npm
|
|
100
|
+
* 把主包 node_modules 里被 npm 嵌套(未提升到用户根)的依赖,按需链接到用户根 node_modules 下,
|
|
101
101
|
* 让 RN CLI / Metro 从用户根也能解析到它们。
|
|
102
|
-
* -
|
|
103
|
-
* - 范围:遍历主包 node_modules 顶层全部条目(含 @scope/
|
|
104
|
-
* - 落点:只在用户根 node_modules 缺失条目时建 symlink
|
|
105
|
-
* -
|
|
102
|
+
* - 判据:从用户根视角解析不到,才建链接;能解析则跳过;
|
|
103
|
+
* - 范围:遍历主包 node_modules 顶层全部条目(含 @scope/ 下每个子项),不列举任何包名;
|
|
104
|
+
* - 落点:只在用户根 node_modules 缺失条目时建 symlink,不覆盖已有内容、不碰 package.json / package-lock.json;
|
|
105
|
+
* - 建链接失败:中断报错,不静默。
|
|
106
106
|
*/
|
|
107
107
|
linkNestedDeps = (pkgRoot, buildDir) => {
|
|
108
108
|
const pkgNm = path.join(pkgRoot, 'node_modules');
|
|
109
109
|
if (!fs.existsSync(pkgNm)) return;
|
|
110
110
|
|
|
111
|
-
const userProjectDir = path.dirname(buildDir),
|
|
112
|
-
|
|
113
|
-
req = createRequire(path.join(userProjectDir, 'index.js')),
|
|
114
|
-
candidates = [];
|
|
111
|
+
const userProjectDir = path.dirname(buildDir), userNm = path.join(userProjectDir, 'node_modules'),
|
|
112
|
+
req = createRequire(path.join(userProjectDir, 'index.js')), candidates = [];
|
|
115
113
|
for (const entry of fs.readdirSync(pkgNm, { withFileTypes: true })) {
|
|
116
114
|
if (!entry.isDirectory() || entry.name.startsWith('.')) continue;
|
|
117
115
|
if (entry.name.startsWith('@')) {
|
|
@@ -123,7 +121,7 @@ const BUILD_DIR_NAME = 'node-mobile-app-build',
|
|
|
123
121
|
|
|
124
122
|
let linked = 0;
|
|
125
123
|
for (const name of candidates) {
|
|
126
|
-
try { req.resolve(name); continue; } catch { /*
|
|
124
|
+
try { req.resolve(name); continue; } catch { /* 解析不到,进入兜底 */ }
|
|
127
125
|
const src = path.join(pkgNm, name);
|
|
128
126
|
if (!fs.existsSync(src)) continue;
|
|
129
127
|
const realSrc = fs.realpathSync(src), dst = path.join(userNm, name);
|
|
@@ -132,8 +130,7 @@ const BUILD_DIR_NAME = 'node-mobile-app-build',
|
|
|
132
130
|
if (occupied) continue;
|
|
133
131
|
ensureDir(path.dirname(dst));
|
|
134
132
|
try {
|
|
135
|
-
fs.symlinkSync(realSrc, dst, 'junction')
|
|
136
|
-
linked++;
|
|
133
|
+
fs.symlinkSync(realSrc, dst, 'junction'), linked++;
|
|
137
134
|
} catch (err) {
|
|
138
135
|
throw new Error(
|
|
139
136
|
'建立兜底链接失败:' + dst + ' -> ' + realSrc + '\n' +
|
|
@@ -146,9 +143,9 @@ const BUILD_DIR_NAME = 'node-mobile-app-build',
|
|
|
146
143
|
},
|
|
147
144
|
/**
|
|
148
145
|
* 把 CLI 包里的 template/ 逐文件覆盖到 build 目录
|
|
149
|
-
* -
|
|
146
|
+
* - 只覆盖模板里存在的文件,不删除 buildDir 里已有的其他内容(保留 Gradle 增量缓存)
|
|
150
147
|
* - 模板文件被改写时自动同步;新增文件自动添加
|
|
151
|
-
* -
|
|
148
|
+
* - 若模板里删除了文件,旧副本残留由 CLI 版本指纹触发全量重建处理
|
|
152
149
|
*/
|
|
153
150
|
copyTemplate = (pkgRoot, buildDir, config) => {
|
|
154
151
|
ensureDir(buildDir);
|
|
@@ -166,7 +163,7 @@ const BUILD_DIR_NAME = 'node-mobile-app-build',
|
|
|
166
163
|
},
|
|
167
164
|
/**
|
|
168
165
|
* 复制用户 Node 项目到 <buildDir>/nodejs-assets/nodejs-project
|
|
169
|
-
* 复制后重写 package.json:只保留 dependencies
|
|
166
|
+
* 复制后重写 package.json:只保留 dependencies,剔除 devDependencies;用 config 的 allowScripts
|
|
170
167
|
*/
|
|
171
168
|
copyUserProject = (userProjectDir, buildDir, config) => {
|
|
172
169
|
const targetDir = path.join(buildDir, 'nodejs-assets', 'nodejs-project');
|
|
@@ -177,7 +174,7 @@ const BUILD_DIR_NAME = 'node-mobile-app-build',
|
|
|
177
174
|
'mobileAppConfig.js', 'mobileAppConfig.mjs', 'mobileAppConfig.cjs'
|
|
178
175
|
];
|
|
179
176
|
|
|
180
|
-
//
|
|
177
|
+
// 增量复制:内容变化的文件才覆盖,保留 node_modules 和其他未变文件
|
|
181
178
|
copyDirIncremental(userProjectDir, targetDir, exclude);
|
|
182
179
|
// 黑名单式过滤:只删 devDependencies 和 allowScripts,其余字段原样保留
|
|
183
180
|
const userPkgPath = path.join(userProjectDir, 'package.json');
|
|
@@ -198,7 +195,7 @@ const BUILD_DIR_NAME = 'node-mobile-app-build',
|
|
|
198
195
|
*/
|
|
199
196
|
writeLocalProperties = (buildDir, config) => {
|
|
200
197
|
const sdkPath = resolveSdkPath(config);
|
|
201
|
-
if (!sdkPath) return console.warn(chalk.yellow(' ⚠️ 未找到 Android SDK
|
|
198
|
+
if (!sdkPath) return console.warn(chalk.yellow(' ⚠️ 未找到 Android SDK,请在 mobileAppConfig.js 里配置 android.sdkPath'));
|
|
202
199
|
|
|
203
200
|
const localProps = path.join(buildDir, 'android', 'local.properties'),
|
|
204
201
|
escaped = sdkPath.replace(/\\/g, '\\\\').replace(/:/g, '\\:');
|
|
@@ -243,12 +240,12 @@ const BUILD_DIR_NAME = 'node-mobile-app-build',
|
|
|
243
240
|
* 按 config.android.abiFilters 修改 buildDir 里的 app/build.gradle:
|
|
244
241
|
* - defaultConfig.ndk.abiFilters
|
|
245
242
|
* - gradle.properties 的 reactNativeArchitectures
|
|
246
|
-
* 插件 build.gradle 读的是 project(":app").android.defaultConfig.ndk.abiFilters
|
|
247
|
-
*
|
|
243
|
+
* 插件 build.gradle 读的是 project(":app").android.defaultConfig.ndk.abiFilters,
|
|
244
|
+
* 所以必须改这里,改插件自身的 abiFilters 无效。
|
|
248
245
|
*/
|
|
249
246
|
patchAndroidAbi = (buildDir, config) => {
|
|
250
247
|
const file = path.join(buildDir, 'android', 'app', 'build.gradle');
|
|
251
|
-
if (!fs.existsSync(file)) return console.warn(chalk.yellow(' ⚠️ 未找到 app/build.gradle
|
|
248
|
+
if (!fs.existsSync(file)) return console.warn(chalk.yellow(' ⚠️ 未找到 app/build.gradle,跳过 ABI patch'));
|
|
252
249
|
|
|
253
250
|
const abiList = (config.android.abiFilters || []).map((x) => `"${x}"`).join(', ');
|
|
254
251
|
let c = fs.readFileSync(file, 'utf-8');
|
|
@@ -256,7 +253,7 @@ const BUILD_DIR_NAME = 'node-mobile-app-build',
|
|
|
256
253
|
|
|
257
254
|
// defaultConfig 里的 ndk { abiFilters ... }
|
|
258
255
|
c = c.replace(/(defaultConfig\s*\{[\s\S]*?ndk\s*\{\s*abiFilters\s+)("[^"]*"(?:\s*,\s*"[^"]*")*)/, `$1${abiList}`);
|
|
259
|
-
// 兜底:没有 ndk {}
|
|
256
|
+
// 兜底:没有 ndk {} 块时,插到 defaultConfig 里
|
|
260
257
|
if (!/ndk\s*\{\s*abiFilters/.test(c))
|
|
261
258
|
c = c.replace(/(defaultConfig\s*\{)/, `$1\n ndk {\n abiFilters ${abiList}\n }`);
|
|
262
259
|
// 不 patch splits.abi.include:AGP 禁止它和 ndk.abiFilters 同时存在
|
|
@@ -289,8 +286,7 @@ const BUILD_DIR_NAME = 'node-mobile-app-build',
|
|
|
289
286
|
for (const name of extras) {
|
|
290
287
|
const pkgJson = path.join(userProjectDir, 'node_modules', name, 'package.json');
|
|
291
288
|
if (!fs.existsSync(pkgJson)) {
|
|
292
|
-
missing.push(name);
|
|
293
|
-
continue;
|
|
289
|
+
missing.push(name); continue;
|
|
294
290
|
}
|
|
295
291
|
try {
|
|
296
292
|
const v = JSON.parse(fs.readFileSync(pkgJson, 'utf-8')).version;
|
|
@@ -317,4 +313,4 @@ const BUILD_DIR_NAME = 'node-mobile-app-build',
|
|
|
317
313
|
export {
|
|
318
314
|
getBuildDir, copyTemplate, copyUserProject, writeLocalProperties, ensureUserEntry, ensureUserConfig, patchAndroidAbi,
|
|
319
315
|
patchExtraRnDependencies
|
|
320
|
-
};
|
|
316
|
+
};
|
package/src/fingerprint.js
CHANGED
|
@@ -24,7 +24,7 @@ const FINGERPRINT_FILE = '.build-fingerprint.json',
|
|
|
24
24
|
},
|
|
25
25
|
clearDepsCaches = buildDir => {
|
|
26
26
|
removeDir(path.join(buildDir, 'nodejs-assets'));
|
|
27
|
-
|
|
27
|
+
fs.rmSync(path.join(buildDir, '.deps-snapshot.json'), { force: true });
|
|
28
28
|
},
|
|
29
29
|
hashTemplate = templateDir => {
|
|
30
30
|
const h = crypto.createHash('sha256'),
|
|
@@ -35,7 +35,7 @@ const FINGERPRINT_FILE = '.build-fingerprint.json',
|
|
|
35
35
|
for (const e of entries) {
|
|
36
36
|
if (e.name === 'node_modules' || e.name === '.git') continue;
|
|
37
37
|
const full = path.join(d, e.name), rel = path.relative(templateDir, full).replace(/\\/g, '/');
|
|
38
|
-
if (e.isDirectory())
|
|
38
|
+
if (e.isDirectory()) h.update('D:' + rel + '|'), walk(full);
|
|
39
39
|
else if (e.isFile()) {
|
|
40
40
|
h.update('F:' + rel + '|');
|
|
41
41
|
try { h.update(fs.readFileSync(full)); } catch { h.update('err'); }
|
|
@@ -108,7 +108,7 @@ const FINGERPRINT_FILE = '.build-fingerprint.json',
|
|
|
108
108
|
if (templateChanged) reasons.push('模板文件');
|
|
109
109
|
if (depsChanged) reasons.push('用户依赖');
|
|
110
110
|
if (appInfoChanged) reasons.push('应用信息');
|
|
111
|
-
console.log(chalk.yellow(' ⚠️ 检测到变化(' + reasons.join('、') + '
|
|
111
|
+
console.log(chalk.yellow(' ⚠️ 检测到变化(' + reasons.join('、') + '),清理 Gradle 缓存、CMake 缓存与依赖缓存'));
|
|
112
112
|
clearGradleCaches(buildDir, userProjectDir);
|
|
113
113
|
removeDir(path.join(userProjectDir, 'node_modules', '@flun', 'nodejs-mobile-react-native', 'android', '.cxx'));
|
|
114
114
|
clearDepsCaches(buildDir);
|
|
@@ -117,13 +117,13 @@ const FINGERPRINT_FILE = '.build-fingerprint.json',
|
|
|
117
117
|
const reasons = [], targets = [];
|
|
118
118
|
if (depsChanged) reasons.push('用户依赖'), targets.push('依赖缓存', 'Gradle 缓存');
|
|
119
119
|
if (appInfoChanged) reasons.push('应用信息'), targets.push('Gradle 缓存');
|
|
120
|
-
console.log(chalk.yellow(' ⚠️ 检测到变化(' + reasons.join('、') + '
|
|
120
|
+
console.log(chalk.yellow(' ⚠️ 检测到变化(' + reasons.join('、') + '),清理 ' + [...new Set(targets)].join('、')));
|
|
121
121
|
clearGradleCaches(buildDir, userProjectDir);
|
|
122
122
|
if (depsChanged) clearDepsCaches(buildDir);
|
|
123
123
|
}
|
|
124
|
-
else console.log(chalk.gray(' ✓
|
|
124
|
+
else console.log(chalk.gray(' ✓ 缓存有效,跳过清理'));
|
|
125
125
|
|
|
126
126
|
writeFp(fpFile, newFp);
|
|
127
127
|
};
|
|
128
128
|
|
|
129
|
-
export { computeFingerprint, checkAndResetCache };
|
|
129
|
+
export { computeFingerprint, checkAndResetCache };
|
package/src/generate.js
CHANGED
|
@@ -4,7 +4,7 @@ import { writeIfChanged } from './utils.js';
|
|
|
4
4
|
import chalk from 'chalk';
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
|
-
* 生成 main.js
|
|
7
|
+
* 生成 main.js(桥接代码,用户不需要手写)
|
|
8
8
|
*
|
|
9
9
|
* 上报机制(通过 rn-bridge channel 发消息给 App.tsx):
|
|
10
10
|
* - 'Node started on <URL>' server.js 成功调用 listen
|
|
@@ -14,9 +14,8 @@ const generateMainJs = (targetDir, config, moduleType) => {
|
|
|
14
14
|
const entry = config.serverPath.startsWith('./') ? config.serverPath : './' + config.serverPath,
|
|
15
15
|
entryLabel = config.serverPath, hb = config.advanced.heartbeatInterval,
|
|
16
16
|
body = `(async () => {
|
|
17
|
-
const rn_bridge = require('rn-bridge')
|
|
18
|
-
|
|
19
|
-
ENTRY_LABEL = ${JSON.stringify(entryLabel)}, START_TIMEOUT_MS = 5000;
|
|
17
|
+
const rn_bridge = require('rn-bridge'), net = require('net'), https = require('https'),
|
|
18
|
+
USER_ENTRY = ${JSON.stringify(entry)}, ENTRY_LABEL = ${JSON.stringify(entryLabel)}, START_TIMEOUT_MS = 5000;
|
|
20
19
|
|
|
21
20
|
let resolved = false;
|
|
22
21
|
function emitUrl(url) {
|
|
@@ -44,7 +43,7 @@ const generateMainJs = (targetDir, config, moduleType) => {
|
|
|
44
43
|
emitFailure('未处理的 Promise rejection', String(reason));
|
|
45
44
|
});
|
|
46
45
|
|
|
47
|
-
// 拦截 net.Server.prototype.listen
|
|
46
|
+
// 拦截 net.Server.prototype.listen,捕获协议和真实端口
|
|
48
47
|
const origListen = net.Server.prototype.listen;
|
|
49
48
|
net.Server.prototype.listen = function (...args) {
|
|
50
49
|
const server = this;
|
|
@@ -77,9 +76,7 @@ const generateMainJs = (targetDir, config, moduleType) => {
|
|
|
77
76
|
|
|
78
77
|
${config.advanced.keepAlive === false ? '// keepAlive: false — 不发送心跳' : "setInterval(() => rn_bridge.channel.send('heartbeat'), " + hb + ");"}
|
|
79
78
|
|
|
80
|
-
rn_bridge.channel.on('message',
|
|
81
|
-
rn_bridge.channel.send('Echo: ' + msg);
|
|
82
|
-
});
|
|
79
|
+
rn_bridge.channel.on('message', msg => rn_bridge.channel.send('Echo: ' + msg));
|
|
83
80
|
})();`,
|
|
84
81
|
header = moduleType === 'module'
|
|
85
82
|
? `import { createRequire } from 'module';\nconst require = createRequire(import.meta.url);\n\n` : '',
|
|
@@ -97,9 +94,9 @@ const generateMainJs = (targetDir, config, moduleType) => {
|
|
|
97
94
|
mixedContentMode: config.webview.mixedContentMode,
|
|
98
95
|
backgroundColor: config.webview.backgroundColor
|
|
99
96
|
},
|
|
100
|
-
}, content = `//
|
|
97
|
+
}, content = `// 自动生成,请勿手改\nexport default ${JSON.stringify(data, null, 2)} as const;\n`,
|
|
101
98
|
changed = writeIfChanged(path.join(buildDir, 'mobileApp.runtime.ts'), content);
|
|
102
99
|
console.log(chalk.green(changed ? ' ✓ 已生成 mobileApp.runtime.ts' : ' ✓ mobileApp.runtime.ts 无变化'));
|
|
103
100
|
};
|
|
104
101
|
|
|
105
|
-
export { generateMainJs, generateRuntime }
|
|
102
|
+
export { generateMainJs, generateRuntime }
|
package/src/install.js
CHANGED
|
@@ -25,7 +25,7 @@ const
|
|
|
25
25
|
},
|
|
26
26
|
/**
|
|
27
27
|
* 内部:判断用户项目的生产依赖里是否含原生模块(binding.gyp)
|
|
28
|
-
* 按用户根 package.json 的 dependencies
|
|
28
|
+
* 按用户根 package.json 的 dependencies 键名,去 userProjectDir/node_modules 下逐个查
|
|
29
29
|
* 不用扫 nodejs-project/node_modules:全新构建时它还不存在
|
|
30
30
|
*/
|
|
31
31
|
hasNativeModules = userProjectDir => {
|
|
@@ -100,7 +100,8 @@ const
|
|
|
100
100
|
const toolchain = path.join(ndkPath, 'toolchains', 'llvm', 'prebuilt', hostTag),
|
|
101
101
|
assetsDir = path.dirname(targetDir), gradleProjectDir = path.join(buildDir, 'android');
|
|
102
102
|
const patchScript = path.join(pluginDir, 'scripts', 'patch-package.js');
|
|
103
|
-
if (fs.existsSync(patchScript)) run(process.execPath, [patchScript, path.join(targetDir, 'node_modules')],
|
|
103
|
+
if (fs.existsSync(patchScript)) run(process.execPath, [patchScript, path.join(targetDir, 'node_modules')],
|
|
104
|
+
{ cwd: targetDir, inherit: true });
|
|
104
105
|
for (const abi of abiFilters) {
|
|
105
106
|
const map = ABI_MAP[abi];
|
|
106
107
|
if (!map) console.error(chalk.red(' [错误] 不支持的 ABI: ' + abi)), process.exit(1);
|
|
@@ -119,7 +120,8 @@ const
|
|
|
119
120
|
RANLIB: path.join(toolchain, 'bin', 'llvm-ranlib' + binSuffix),
|
|
120
121
|
GYP_DEFINES: 'target_arch=' + map.arch + ' v8_target_arch=' + map.arch + ' android_target_arch=' + map.arch +
|
|
121
122
|
' host_os=' + hostOs + ' OS=android android_ndk_path=' + ndkPath.replace(/\\/g, '/'),
|
|
122
|
-
PATH: [path.join(ndkPath, 'prebuilt', hostTag, 'bin'), path.join(toolchain, 'bin'),
|
|
123
|
+
PATH: [path.join(ndkPath, 'prebuilt', hostTag, 'bin'), path.join(toolchain, 'bin'),
|
|
124
|
+
path.join(targetDir, 'node_modules', '.bin'), process.env.PATH].filter(Boolean).join(path.delimiter)
|
|
123
125
|
};
|
|
124
126
|
console.log(chalk.cyan(' ℹ️ 编译原生模块 (' + abi + ')...'));
|
|
125
127
|
try { run('npm', ['rebuild', '--build-from-source', '--no-audit', '--no-fund'], { cwd: targetDir, env, quiet: true }); }
|
|
@@ -154,7 +156,7 @@ const
|
|
|
154
156
|
// 在 "NSString* host = nil;" 那一段之后插入 localhost 放行
|
|
155
157
|
const anchor = ' if ([[challenge protectionSpace] authenticationMethod] == NSURLAuthenticationMethodClientCertificate) {',
|
|
156
158
|
idx = c.indexOf(anchor);
|
|
157
|
-
if (idx === -1) return console.warn(chalk.yellow(' ⚠️ RNCWebViewImpl.m
|
|
159
|
+
if (idx === -1) return console.warn(chalk.yellow(' ⚠️ RNCWebViewImpl.m 里未找到锚点,跳过 iOS SSL 补丁'));
|
|
158
160
|
|
|
159
161
|
const injected = [
|
|
160
162
|
' // LOCALHOST_SSL_BYPASS_IOS: 对 localhost/127.0.0.1 放行自签名证书(仅本地测试用)',
|
|
@@ -172,8 +174,8 @@ const
|
|
|
172
174
|
},
|
|
173
175
|
/**
|
|
174
176
|
* 装用户项目的生产依赖到 <buildDir>/nodejs-assets/nodejs-project/
|
|
175
|
-
* 只装 dependencies
|
|
176
|
-
* 依赖 +
|
|
177
|
+
* 只装 dependencies,不装 devDependencies
|
|
178
|
+
* 依赖 + 环境未变化且上次构建完成时跳过,否则按需重装 / 仅优化
|
|
177
179
|
* 快照记录在 buildDir/.deps-snapshot.json(与桌面版一致)
|
|
178
180
|
*/
|
|
179
181
|
installUserDeps = async (targetDir, userProjectDir, config, buildDir, targetPlatform) => {
|
|
@@ -201,8 +203,7 @@ const
|
|
|
201
203
|
envSame = old.platform === targetPlatform && JSON.stringify(old.abiFilters) === JSON.stringify(abiFilters) &&
|
|
202
204
|
old.runtimeMajor === runtimeMajor,
|
|
203
205
|
lastOptimized = old.optimized !== false, selfHashSame = old.optimizeHash === SELF_HASH,
|
|
204
|
-
installerSame = old.installerHash === installerHash,
|
|
205
|
-
nmExists = fs.existsSync(nodeModulesDir);
|
|
206
|
+
installerSame = old.installerHash === installerHash, nmExists = fs.existsSync(nodeModulesDir);
|
|
206
207
|
if (!depsSame) reasons.push('依赖清单已变化');
|
|
207
208
|
if (!verifiedOk) reasons.push('上次构建未完成');
|
|
208
209
|
if (!envSame) reasons.push('运行环境已变化');
|
|
@@ -240,8 +241,8 @@ const
|
|
|
240
241
|
console.log(chalk.green(' ✓ 用户依赖安装完成'));
|
|
241
242
|
}
|
|
242
243
|
|
|
243
|
-
if (hasNative && targetPlatform === 'android' && shouldInstall)
|
|
244
|
-
|
|
244
|
+
if (hasNative && targetPlatform === 'android' && shouldInstall)
|
|
245
|
+
compileNativeModules(targetDir, userProjectDir, config, buildDir, abiFilters);
|
|
245
246
|
if (shouldOptimize) {
|
|
246
247
|
console.log(chalk.cyan(' [信息] 正在执行优化...'));
|
|
247
248
|
try {
|
|
@@ -260,7 +261,7 @@ const
|
|
|
260
261
|
*/
|
|
261
262
|
patchNodejsMobilePlugin = (userProjectDir, config) => {
|
|
262
263
|
const pluginDir = findPluginDir(userProjectDir);
|
|
263
|
-
if (!pluginDir) return console.warn(chalk.yellow(' ⚠️ 未找到 @flun/nodejs-mobile-react-native
|
|
264
|
+
if (!pluginDir) return console.warn(chalk.yellow(' ⚠️ 未找到 @flun/nodejs-mobile-react-native 插件,跳过 patch'));
|
|
264
265
|
|
|
265
266
|
const file = path.join(pluginDir, 'android', 'build.gradle'),
|
|
266
267
|
abiList = config.android.abiFilters.map((x) => `"${x}"`).join(', ');
|
|
@@ -271,7 +272,6 @@ const
|
|
|
271
272
|
|
|
272
273
|
if (c !== before) fs.writeFileSync(file, c, 'utf-8'), console.log(chalk.green(` ✓ 已设置插件 abiFilters = [${abiList}]`));
|
|
273
274
|
},
|
|
274
|
-
|
|
275
275
|
/**
|
|
276
276
|
* 让 react-native-webview 对 localhost 自签名证书放行
|
|
277
277
|
* Android: patch RNCWebViewClient.java
|
|
@@ -314,4 +314,4 @@ const
|
|
|
314
314
|
patchWebViewSslIos(userProjectDir); // iOS 分支
|
|
315
315
|
};
|
|
316
316
|
|
|
317
|
-
export { installUserDeps, patchNodejsMobilePlugin, patchWebViewSsl };
|
|
317
|
+
export { installUserDeps, patchNodejsMobilePlugin, patchWebViewSsl };
|
|
@@ -464,15 +464,17 @@ const require = createRequire(import.meta.url), subpath = (...parts) => './' + p
|
|
|
464
464
|
return null;
|
|
465
465
|
},
|
|
466
466
|
delFile = p => {
|
|
467
|
-
try {
|
|
467
|
+
try {
|
|
468
|
+
fs.statSync(p).isDirectory() ? fs.rmSync(p, { recursive: true, force: true }) : fs.unlinkSync(p), removed++;
|
|
469
|
+
} catch { }
|
|
468
470
|
},
|
|
469
471
|
processDir = d => {
|
|
470
472
|
let entries;
|
|
471
473
|
try { entries = fs.readdirSync(d, { withFileTypes: true }); } catch { return; }
|
|
472
474
|
for (const e of entries) {
|
|
473
|
-
const full = path.join(d, e.name)
|
|
474
|
-
const expectedPrebuilds = targetPlatform === 'ios' ? ['darwin'] : ['android'],
|
|
475
|
+
const full = path.join(d, e.name), expectedPrebuilds = targetPlatform === 'ios' ? ['darwin'] : ['android'],
|
|
475
476
|
prebuildMatch = full.match(/[\\/]prebuilds[\\/](linux|darwin|win32|android)-/i);
|
|
477
|
+
|
|
476
478
|
if (prebuildMatch && !expectedPrebuilds.includes(prebuildMatch[1].toLowerCase())) { delFile(full); continue; }
|
|
477
479
|
if (e.isDirectory()) { processDir(full); continue; }
|
|
478
480
|
if (JS_RE.test(e.name)) continue;
|
|
@@ -496,7 +498,7 @@ const require = createRequire(import.meta.url), subpath = (...parts) => './' + p
|
|
|
496
498
|
return removed;
|
|
497
499
|
},
|
|
498
500
|
// ==================== 清开发文件 ====================
|
|
499
|
-
pruneDevFiles = async
|
|
501
|
+
pruneDevFiles = async nmDir => {
|
|
500
502
|
let deleted = 0;
|
|
501
503
|
const PATTERNS = [
|
|
502
504
|
/(^|\/)tsconfig\.json$/,
|
|
@@ -521,8 +523,7 @@ const require = createRequire(import.meta.url), subpath = (...parts) => './' + p
|
|
|
521
523
|
walk(full);
|
|
522
524
|
} else {
|
|
523
525
|
const rel = toPosix(path.relative(nmDir, full));
|
|
524
|
-
if (PATTERNS.some(re => re.test(rel)))
|
|
525
|
-
try { fs.unlinkSync(full), deleted++; } catch { }
|
|
526
|
+
if (PATTERNS.some(re => re.test(rel))) try { fs.unlinkSync(full), deleted++; } catch { }
|
|
526
527
|
}
|
|
527
528
|
}
|
|
528
529
|
};
|
|
@@ -536,7 +537,8 @@ const require = createRequire(import.meta.url), subpath = (...parts) => './' + p
|
|
|
536
537
|
for (const f of fs.readdirSync(d, { withFileTypes: true })) {
|
|
537
538
|
const full = path.join(d, f.name);
|
|
538
539
|
if (f.isDirectory()) walk(full);
|
|
539
|
-
else if (full.includes(path.sep + 'obj.target' + path.sep) || f.name === 'Makefile' ||
|
|
540
|
+
else if (full.includes(path.sep + 'obj.target' + path.sep) || f.name === 'Makefile' ||
|
|
541
|
+
f.name.endsWith('.orig') || MID_RE.test(f.name)) try { fs.unlinkSync(full), deleted++; } catch { }
|
|
540
542
|
}
|
|
541
543
|
};
|
|
542
544
|
walk(nmDir);
|
|
@@ -702,4 +704,4 @@ const SELF_HASH = (() => {
|
|
|
702
704
|
} catch { return 'unknown'; }
|
|
703
705
|
})();
|
|
704
706
|
|
|
705
|
-
export { clearVerified, markVerified, optimizeNodeModules, SELF_HASH };
|
|
707
|
+
export { clearVerified, markVerified, optimizeNodeModules, SELF_HASH };
|
package/src/patcher.js
CHANGED
|
@@ -4,7 +4,7 @@ import { writeIfChanged } from './utils.js';
|
|
|
4
4
|
import chalk from 'chalk';
|
|
5
5
|
|
|
6
6
|
// 把字符串中所有非 ASCII 字符转成 \uXXXX(Java Properties 规范)
|
|
7
|
-
// gradle.properties 按 ISO-8859-1
|
|
7
|
+
// gradle.properties 按 ISO-8859-1 读取,无法改为 UTF-8
|
|
8
8
|
const escapeProps = s =>
|
|
9
9
|
String(s).replace(/[^\x00-\x7F]/g, ch => '\\u' + ch.charCodeAt(0).toString(16).padStart(4, '0')),
|
|
10
10
|
/**
|
|
@@ -97,7 +97,7 @@ const escapeProps = s =>
|
|
|
97
97
|
* 把 mobileAppConfig.js 里的 Android 编译配置写入 buildDir:
|
|
98
98
|
* - android/build.gradle 的 ext 块:minSdkVersion / targetSdkVersion / compileSdkVersion / buildToolsVersion
|
|
99
99
|
* - android/app/build.gradle 的 manifestPlaceholders:usesCleartextTraffic
|
|
100
|
-
* - AndroidManifest.xml:permissions
|
|
100
|
+
* - AndroidManifest.xml:permissions(去重,始终含 INTERNET)
|
|
101
101
|
*/
|
|
102
102
|
patchAndroidConfig = (buildDir, config) => {
|
|
103
103
|
const a = config.android || {}, rootGradle = path.join(buildDir, 'android', 'build.gradle'),
|
|
@@ -116,7 +116,7 @@ const escapeProps = s =>
|
|
|
116
116
|
writeIfChanged(rootGradle, c);
|
|
117
117
|
}
|
|
118
118
|
// 2. AndroidManifest.xml: 直接写死 usesCleartextTraffic + tools:replace
|
|
119
|
-
//
|
|
119
|
+
// 不用占位符,因为 AGP release 构建在 targetSdk>=28 时会强制 false
|
|
120
120
|
if (fs.existsSync(manifestFile)) {
|
|
121
121
|
let c = fs.readFileSync(manifestFile, 'utf-8');
|
|
122
122
|
const ct = a.usesCleartextTraffic !== false ? 'true' : 'false';
|
|
@@ -155,8 +155,8 @@ const escapeProps = s =>
|
|
|
155
155
|
},
|
|
156
156
|
/**
|
|
157
157
|
* 处理 Release 签名:
|
|
158
|
-
* - config.android.signing 有值 → 写 keystore 路径到 gradle.properties
|
|
159
|
-
* - 没值 → 不动(release 沿用 debug
|
|
158
|
+
* - config.android.signing 有值 → 写 keystore 路径到 gradle.properties,加 signingConfigs.release 块,release buildType 用它
|
|
159
|
+
* - 没值 → 不动(release 沿用 debug,可装但不可发布)
|
|
160
160
|
*
|
|
161
161
|
* keystore 路径以用户项目根为基准。
|
|
162
162
|
*/
|
|
@@ -164,18 +164,18 @@ const escapeProps = s =>
|
|
|
164
164
|
const signing = config.android && config.android.signing;
|
|
165
165
|
if (!signing || !signing.keystore) return; // 未配置签名 → 保持默认
|
|
166
166
|
|
|
167
|
-
// 启用签名后 4
|
|
167
|
+
// 启用签名后 4 项均为必填,缺项时提前报错,避免 Gradle 报"password incorrect"误导排查
|
|
168
168
|
const _missing = ['storePassword', 'keyAlias', 'keyPassword'].filter((k) => !signing[k]);
|
|
169
169
|
if (_missing.length > 0) {
|
|
170
170
|
throw new Error(
|
|
171
|
-
'android.signing.keystore
|
|
172
|
-
'
|
|
173
|
-
'
|
|
171
|
+
'android.signing.keystore 已配置,但以下字段为空:' + _missing.join(', ') + '\n' +
|
|
172
|
+
' 启用签名后,keystore / storePassword / keyAlias / keyPassword 四项均为必填。\n' +
|
|
173
|
+
' 如仅需本地测试(不签名上架),可注释掉整个 signing 块——模板默认用 debug keystore 签名。'
|
|
174
174
|
);
|
|
175
175
|
}
|
|
176
176
|
|
|
177
177
|
const keystoreAbs = path.resolve(userProjectDir, signing.keystore);
|
|
178
|
-
if (!fs.existsSync(keystoreAbs)) return console.warn(chalk.yellow(' ⚠️ 签名 keystore
|
|
178
|
+
if (!fs.existsSync(keystoreAbs)) return console.warn(chalk.yellow(' ⚠️ 签名 keystore 不存在,跳过签名配置: ' + keystoreAbs));
|
|
179
179
|
|
|
180
180
|
// 1. 写 gradle.properties(追加或覆盖 MYAPP_RELEASE_*)
|
|
181
181
|
const propsFile = path.join(buildDir, 'android', 'gradle.properties');
|
|
@@ -187,8 +187,8 @@ const escapeProps = s =>
|
|
|
187
187
|
c = c.replace(/^\s*MYAPP_RELEASE_KEY_ALIAS=.*\r?\n/gm, '');
|
|
188
188
|
c = c.replace(/^\s*MYAPP_RELEASE_KEY_PASSWORD=.*\r?\n/gm, '');
|
|
189
189
|
// 追加新的
|
|
190
|
-
// gradle.properties 按 ISO-8859-1
|
|
191
|
-
//
|
|
190
|
+
// gradle.properties 按 ISO-8859-1 读取,非 ASCII 字符必须转 \uXXXX 转义,
|
|
191
|
+
// 否则中文路径(如「桌面」)会被读成乱码,导致 keystore 找不到。
|
|
192
192
|
const esc = escapeProps(keystoreAbs.replace(/\\/g, '/'));
|
|
193
193
|
c += `\nMYAPP_RELEASE_STORE_FILE=${esc}\n`;
|
|
194
194
|
c += `MYAPP_RELEASE_STORE_PASSWORD=${escapeProps(signing.storePassword || '')}\n`;
|
|
@@ -197,12 +197,12 @@ const escapeProps = s =>
|
|
|
197
197
|
writeIfChanged(propsFile, c);
|
|
198
198
|
}
|
|
199
199
|
|
|
200
|
-
// 2. app/build.gradle: 加 signingConfigs.release
|
|
200
|
+
// 2. app/build.gradle: 加 signingConfigs.release,并让 buildTypes.release 用它
|
|
201
201
|
const gradleFile = path.join(buildDir, 'android', 'app', 'build.gradle');
|
|
202
202
|
if (fs.existsSync(gradleFile)) {
|
|
203
203
|
let c = fs.readFileSync(gradleFile, 'utf-8');
|
|
204
204
|
|
|
205
|
-
// 加 release
|
|
205
|
+
// 加 release 签名块(先删旧的,避免重复)
|
|
206
206
|
c = c.replace(/\s*release\s*\{\s*storeFile[\s\S]*?\}\r?\n(\s*\}\s*\r?\n)/, '$1');
|
|
207
207
|
// 在 signingConfigs { 之后插入
|
|
208
208
|
c = c.replace(/(signingConfigs\s*\{)/,
|
|
@@ -224,7 +224,7 @@ const escapeProps = s =>
|
|
|
224
224
|
console.log(chalk.green(' ✓ Release 签名已配置: ' + path.basename(keystoreAbs)));
|
|
225
225
|
},
|
|
226
226
|
/**
|
|
227
|
-
* 应用图标:把用户提供的 PNG 缩放到 5 个 Android
|
|
227
|
+
* 应用图标:把用户提供的 PNG 缩放到 5 个 Android 密度,覆盖各 mipmap 目录下的 ic_launcher 与 ic_launcher_round
|
|
228
228
|
* 未配置或文件不存在时保持默认
|
|
229
229
|
*/
|
|
230
230
|
patchIcon = async (buildDir, userProjectDir, config) => {
|
|
@@ -232,7 +232,7 @@ const escapeProps = s =>
|
|
|
232
232
|
if (!iconPath) return;
|
|
233
233
|
|
|
234
234
|
const iconAbs = path.resolve(userProjectDir, iconPath);
|
|
235
|
-
if (!fs.existsSync(iconAbs)) return console.warn(chalk.yellow(' ⚠️
|
|
235
|
+
if (!fs.existsSync(iconAbs)) return console.warn(chalk.yellow(' ⚠️ 图标文件不存在,保持默认: ' + iconAbs));
|
|
236
236
|
|
|
237
237
|
const { Jimp } = await import('jimp'),
|
|
238
238
|
densities = [
|
|
@@ -339,16 +339,16 @@ const escapeProps = s =>
|
|
|
339
339
|
console.log(chalk.green(` ✓ iOS 配置:${appName} (${appId}) v${versionName}(${versionCode}), target ${deploymentTarget}`));
|
|
340
340
|
},
|
|
341
341
|
/**
|
|
342
|
-
* iOS 应用图标:把用户提供的 PNG 缩放到 9
|
|
342
|
+
* iOS 应用图标:把用户提供的 PNG 缩放到 9 个尺寸,写入 AppIcon.appiconset
|
|
343
343
|
* 同时改写 Contents.json 加 filename 字段
|
|
344
|
-
*
|
|
344
|
+
* 未配置或文件不存在时保持默认(无图标,Xcode 会用系统占位)
|
|
345
345
|
*/
|
|
346
346
|
patchIosIcon = async (buildDir, userProjectDir, config) => {
|
|
347
347
|
const iconPath = config.ios && config.ios.icon;
|
|
348
348
|
if (!iconPath) return;
|
|
349
349
|
|
|
350
350
|
const iconAbs = path.resolve(userProjectDir, iconPath);
|
|
351
|
-
if (!fs.existsSync(iconAbs)) return console.warn(chalk.yellow(' ⚠️ iOS
|
|
351
|
+
if (!fs.existsSync(iconAbs)) return console.warn(chalk.yellow(' ⚠️ iOS 图标文件不存在,保持默认: ' + iconAbs));
|
|
352
352
|
|
|
353
353
|
const iconDir = path.join(buildDir, 'ios', 'MyApp', 'Images.xcassets', 'AppIcon.appiconset');
|
|
354
354
|
if (!fs.existsSync(iconDir)) return;
|
|
@@ -378,7 +378,7 @@ const escapeProps = s =>
|
|
|
378
378
|
fs.writeFileSync(path.join(iconDir, v.name), buf);
|
|
379
379
|
}
|
|
380
380
|
|
|
381
|
-
// 改写 Contents.json
|
|
381
|
+
// 改写 Contents.json,给每项加 filename
|
|
382
382
|
writeIfChanged(contentsFile, JSON.stringify(contents, null, 2) + '\n');
|
|
383
383
|
console.log(chalk.green(` ✓ iOS 图标已应用: ${path.basename(iconAbs)}`));
|
|
384
384
|
}
|
|
@@ -386,4 +386,4 @@ const escapeProps = s =>
|
|
|
386
386
|
export {
|
|
387
387
|
resolveAppName, escapeKotlinString, escapeSwiftString, patchAppConfig, patchAndroidConfig, patchSigning,
|
|
388
388
|
patchIcon, patchIosConfig, patchIosIcon
|
|
389
|
-
};
|
|
389
|
+
};
|
package/src/platform-ios.js
CHANGED
|
@@ -11,16 +11,16 @@ const SCHEME = 'MyApp',
|
|
|
11
11
|
/** 检查 xcodebuild 是否存在 */
|
|
12
12
|
checkIosTools = () => {
|
|
13
13
|
const r = run('which', ['xcodebuild'], { stdio: 'pipe' });
|
|
14
|
-
if (r.status !== 0) throw new Error('未找到 xcodebuild
|
|
14
|
+
if (r.status !== 0) throw new Error('未找到 xcodebuild,请安装 Xcode 并运行 xcode-select --install');
|
|
15
15
|
},
|
|
16
16
|
/**
|
|
17
|
-
* 执行 pod install(若 Pods/ 已存在且 Podfile.lock
|
|
17
|
+
* 执行 pod install(若 Pods/ 已存在且 Podfile.lock 未变,cocoapods 会自动跳过)
|
|
18
18
|
*/
|
|
19
19
|
podInstall = iosDir => {
|
|
20
20
|
console.log(chalk.cyan(' pod install ...')), run('pod', ['install'], { cwd: iosDir, inherit: true });
|
|
21
21
|
},
|
|
22
22
|
/**
|
|
23
|
-
* 把 config.ios.signing.certificate 导入临时 keychain
|
|
23
|
+
* 把 config.ios.signing.certificate 导入临时 keychain,供 xcodebuild 签名用
|
|
24
24
|
* 返回 { keychainPath, keychainPassword } 或 null(未配置 / 文件缺失)
|
|
25
25
|
*/
|
|
26
26
|
setupSigningKeychain = (buildDir, userProjectDir, config) => {
|
|
@@ -29,7 +29,7 @@ const SCHEME = 'MyApp',
|
|
|
29
29
|
|
|
30
30
|
const certPath = path.resolve(userProjectDir, signing.certificate);
|
|
31
31
|
if (!fs.existsSync(certPath)) {
|
|
32
|
-
console.warn(chalk.yellow(' ⚠️ iOS
|
|
32
|
+
console.warn(chalk.yellow(' ⚠️ iOS 签名证书不存在,跳过: ' + certPath));
|
|
33
33
|
return null;
|
|
34
34
|
}
|
|
35
35
|
|
|
@@ -81,7 +81,7 @@ const SCHEME = 'MyApp',
|
|
|
81
81
|
|
|
82
82
|
if (teamId) args.push('DEVELOPMENT_TEAM=' + teamId);
|
|
83
83
|
|
|
84
|
-
// 若配置了 certificate
|
|
84
|
+
// 若配置了 certificate,导入临时 keychain 并让 xcodebuild 使用
|
|
85
85
|
const keychain = setupSigningKeychain(buildDir, userProjectDir, config);
|
|
86
86
|
if (keychain) args.push('OTHER_CODE_SIGN_FLAGS=--keychain ' + keychain.keychainPath);
|
|
87
87
|
|
|
@@ -89,7 +89,7 @@ const SCHEME = 'MyApp',
|
|
|
89
89
|
return archivePath;
|
|
90
90
|
},
|
|
91
91
|
/**
|
|
92
|
-
* xcodebuild -exportArchive
|
|
92
|
+
* xcodebuild -exportArchive,导出 .ipa
|
|
93
93
|
* 返回 .ipa 路径
|
|
94
94
|
*/
|
|
95
95
|
xcodebuildExport = (buildDir, config, release, outDir, userProjectDir) => {
|
|
@@ -97,7 +97,7 @@ const SCHEME = 'MyApp',
|
|
|
97
97
|
ensureDir(exportDir);
|
|
98
98
|
const archivePath = path.join(iosDir, 'build', SCHEME + '.xcarchive'), method = release ? 'ad-hoc' : 'development',
|
|
99
99
|
signing = (config.ios && config.ios.signing) || {}, teamId = signing.teamId || '',
|
|
100
|
-
// provisioningProfile 路径 → 取 basename
|
|
100
|
+
// provisioningProfile 路径 → 取 basename(去扩展名),写进 exportOptions.plist
|
|
101
101
|
profilePath = signing.provisioningProfile ? path.resolve(userProjectDir, signing.provisioningProfile) : '',
|
|
102
102
|
profileName = profilePath ? path.basename(profilePath).replace(/\.mobileprovision$/i, '') : '',
|
|
103
103
|
plistPath = path.join(iosDir, 'build', 'exportOptions.plist'),
|