@flun/node-mobile-app 4.1.1 → 4.2.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 +17 -27
- package/README.md +67 -13
- package/bin/cli.js +3 -3
- package/mobileAppConfig.js +17 -4
- package/package.json +1 -1
- package/scripts/copy-files.js +3 -2
- package/src/commands/build.js +105 -99
- package/src/commands/clean.js +12 -6
- package/src/commands/test.js +141 -132
- package/src/config.js +88 -52
- package/src/copy.js +302 -215
- package/src/fingerprint.js +96 -77
- package/src/generate.js +33 -21
- package/src/install.js +176 -162
- package/src/optimize-node-modules.js +5 -4
- package/src/patcher.js +438 -358
- package/src/platform-ios.js +122 -106
- package/src/utils.js +357 -254
- package/template/.prettierrc.js +8 -2
- package/template/App.tsx +98 -87
- package/template/android/gradle/wrapper/gradle-wrapper.properties +1 -1
- package/template/babel.config.js +8 -2
- package/template/eslint.config.js +8 -2
- package/template/metro.config.js +9 -3
- package//350/207/252/345/256/232/344/271/211libnode/346/214/207/345/215/227.md +2 -2
package/src/copy.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import
|
|
1
|
+
import chalk from 'chalk';
|
|
2
2
|
import fs from 'fs';
|
|
3
|
+
import { createRequire } from 'module';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import { resolveSdkPath } from './config.js';
|
|
3
6
|
import {
|
|
4
|
-
|
|
7
|
+
buildEslintConfigSource, copyFileIfChanged, ensureDir, isExcluded, readJson, removeDir, writeIfChanged
|
|
5
8
|
} from './utils.js';
|
|
6
|
-
import { resolveSdkPath } from './config.js';
|
|
7
|
-
import { createRequire } from 'module';
|
|
8
|
-
import chalk from 'chalk';
|
|
9
9
|
|
|
10
10
|
const BUILD_DIR_NAME = 'node-mobile-app-build',
|
|
11
11
|
TEMPLATE_EXCLUDE = [
|
|
@@ -13,22 +13,6 @@ const BUILD_DIR_NAME = 'node-mobile-app-build',
|
|
|
13
13
|
'app/build/', 'app/.cxx/',
|
|
14
14
|
'Pods/', 'local.properties', '*.bak', 'gradle.properties.bak',
|
|
15
15
|
],
|
|
16
|
-
/**
|
|
17
|
-
* 内部:递归覆盖式复制模板,跳过 keepSet 命中的文件
|
|
18
|
-
*/
|
|
19
|
-
copyDirOverwrite = (src, dst, exclude, buildDir, keepSet) => {
|
|
20
|
-
ensureDir(dst);
|
|
21
|
-
for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
|
|
22
|
-
if (isExcluded(entry.name, exclude)) continue;
|
|
23
|
-
const s = path.join(src, entry.name), d = path.join(dst, entry.name);
|
|
24
|
-
if (entry.isDirectory()) copyDirOverwrite(s, d, exclude, buildDir, keepSet);
|
|
25
|
-
else if (entry.isFile()) {
|
|
26
|
-
const rel = path.relative(buildDir, d).replace(/\\/g, '/');
|
|
27
|
-
if (keepSet.has(rel)) continue;
|
|
28
|
-
copyFileIfChanged(s, d);
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
},
|
|
32
16
|
/**
|
|
33
17
|
* 内部:增量复制用户项目到 nodejs-project
|
|
34
18
|
* 保留 node_modules 等由其他步骤生成的产物
|
|
@@ -53,48 +37,19 @@ const BUILD_DIR_NAME = 'node-mobile-app-build',
|
|
|
53
37
|
}
|
|
54
38
|
},
|
|
55
39
|
/**
|
|
56
|
-
*
|
|
57
|
-
*/
|
|
58
|
-
getBuildDir = userProjectDir => path.join(userProjectDir, BUILD_DIR_NAME),
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* 把 buildDir/package.json 的依赖版本同步为主包 dependencies 的版本
|
|
62
|
-
* - 依赖名单来自 buildDir/package.json(模板声明需要哪些依赖)
|
|
63
|
-
* - 版本号来自 pkgRoot/package.json 的 dependencies(单一数据源)
|
|
64
|
-
* - 若主包缺少某个依赖,抛错中断(避免模板占位值被真正使用)
|
|
40
|
+
* 内部:递归覆盖式复制模板,跳过 keepSet 命中的文件
|
|
65
41
|
*/
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
// 移除仅用于模板占位说明的 _comment 字段,避免带进最终 build 工程
|
|
78
|
-
if ('_comment' in buildPkg) delete buildPkg._comment, needWrite = true;
|
|
79
|
-
for (const name of Object.keys(buildPkg.dependencies)) {
|
|
80
|
-
const v = mainDeps[name];
|
|
81
|
-
if (!v) { missing.push(name); continue; }
|
|
82
|
-
if (buildPkg.dependencies[name] !== v) buildPkg.dependencies[name] = v, depsUpdated++, needWrite = true;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
if (needWrite) {
|
|
86
|
-
writeIfChanged(buildPkgPath, JSON.stringify(buildPkg, null, 2) + '\n');
|
|
87
|
-
if (depsUpdated > 0)
|
|
88
|
-
console.log(chalk.green(' ✓ 已同步 ' + depsUpdated + ' 个依赖版本到 ' + BUILD_DIR_NAME + '/package.json'));
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
if (missing.length > 0) {
|
|
92
|
-
throw new Error(
|
|
93
|
-
'template/package.json 声明的以下依赖在主包 dependencies 中不存在:\n' +
|
|
94
|
-
' ' + missing.join(', ') + '\n' +
|
|
95
|
-
' 请在 @flun/node-mobile-app 的 package.json 中补上对应声明(版本号随意,\n' +
|
|
96
|
-
' 但必须存在,以便 syncTemplateDependencies 覆盖模板里的占位值);'
|
|
97
|
-
);
|
|
42
|
+
copyDirOverwrite = (src, dst, exclude, buildDir, keepSet) => {
|
|
43
|
+
ensureDir(dst);
|
|
44
|
+
for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
|
|
45
|
+
if (isExcluded(entry.name, exclude)) continue;
|
|
46
|
+
const s = path.join(src, entry.name), d = path.join(dst, entry.name);
|
|
47
|
+
if (entry.isDirectory()) copyDirOverwrite(s, d, exclude, buildDir, keepSet);
|
|
48
|
+
else if (entry.isFile()) {
|
|
49
|
+
const rel = path.relative(buildDir, d).replace(/\\/g, '/');
|
|
50
|
+
if (keepSet.has(rel)) continue;
|
|
51
|
+
copyFileIfChanged(s, d);
|
|
52
|
+
}
|
|
98
53
|
}
|
|
99
54
|
},
|
|
100
55
|
/**
|
|
@@ -143,179 +98,311 @@ const BUILD_DIR_NAME = 'node-mobile-app-build',
|
|
|
143
98
|
if (linked > 0) console.log(chalk.green(' ✓ 已兜底链接 ' + linked + ' 个嵌套依赖到用户根 node_modules'));
|
|
144
99
|
},
|
|
145
100
|
/**
|
|
146
|
-
*
|
|
147
|
-
*
|
|
148
|
-
*
|
|
149
|
-
* - 若模板里删除了文件,旧副本残留由 CLI 版本指纹触发全量重建处理
|
|
101
|
+
* 探测镜像是否可达,HEAD 请求,3 秒超时
|
|
102
|
+
* @param {string} url 待探测地址
|
|
103
|
+
* @returns {Promise<boolean>} 是否可达
|
|
150
104
|
*/
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
copyDirOverwrite(templateDir, buildDir, TEMPLATE_EXCLUDE, buildDir, keepSet);
|
|
159
|
-
fs.writeFileSync(path.join(buildDir, 'eslint.config.js'), buildEslintConfigSource(), 'utf-8');
|
|
160
|
-
syncTemplateDependencies(pkgRoot, buildDir);
|
|
161
|
-
linkNestedDeps(pkgRoot, buildDir);
|
|
162
|
-
console.log(chalk.green(' ✓ 模板已更新到 ' + BUILD_DIR_NAME + '/'));
|
|
163
|
-
if (keepSet.size > 0) console.log(chalk.gray(' 保留文件: ' + Array.from(keepSet).join(', ')));
|
|
105
|
+
probeUrl = async url => {
|
|
106
|
+
const ctrl = new AbortController(), timer = setTimeout(() => ctrl.abort(), 3000);
|
|
107
|
+
try {
|
|
108
|
+
const res = await fetch(url, { method: 'HEAD', signal: ctrl.signal, redirect: 'follow' });
|
|
109
|
+
return res.ok;
|
|
110
|
+
} catch { return false; }
|
|
111
|
+
finally { clearTimeout(timer); }
|
|
164
112
|
},
|
|
165
113
|
/**
|
|
166
|
-
*
|
|
167
|
-
*
|
|
114
|
+
* 解析或探测 Gradle 下载地址,写入 gradle-wrapper.properties
|
|
115
|
+
* - 用户配置 android.gradleDistributionUrl 时直接使用,失败不回退
|
|
116
|
+
* - 未配置时从模板解析版本号,按腾讯/阿里/官方顺序探测,官方兜底
|
|
117
|
+
* @param {string} buildDir 构建目录
|
|
118
|
+
* @param {object} config 用户配置
|
|
119
|
+
* @returns {Promise<void>}
|
|
168
120
|
*/
|
|
169
|
-
|
|
170
|
-
const
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
(config.allowScripts && Object.keys(config.allowScripts).length) ? config.allowScripts : { node: true };
|
|
187
|
-
writeIfChanged(path.join(targetDir, 'package.json'), JSON.stringify(out, null, 2) + '\n');
|
|
188
|
-
|
|
189
|
-
console.log(chalk.green(' ✓ 已同步用户项目到 nodejs-assets/nodejs-project'));
|
|
190
|
-
console.log(chalk.gray(' 模块类型: ' + (out.type === 'module' ? 'ESM' : 'CJS')));
|
|
191
|
-
console.log(chalk.gray(' 生产依赖: ' + Object.keys(out.dependencies || {}).length + ' 个'));
|
|
192
|
-
|
|
193
|
-
return { pkg: out, moduleType: out.type === 'module' ? 'module' : 'commonjs', targetDir };
|
|
194
|
-
},
|
|
195
|
-
/**
|
|
196
|
-
* 生成 <buildDir>/android/local.properties(Gradle 需要 sdk.dir)
|
|
197
|
-
*/
|
|
198
|
-
writeLocalProperties = (buildDir, config) => {
|
|
199
|
-
const sdkPath = resolveSdkPath(config);
|
|
200
|
-
if (!sdkPath)
|
|
201
|
-
return console.warn(chalk.yellow(' ⚠️ 未找到 Android SDK,请在 mobileAppConfig.js 里配置 android.sdkPath'));
|
|
202
|
-
|
|
203
|
-
const localProps = path.join(buildDir, 'android', 'local.properties'),
|
|
204
|
-
escaped = sdkPath.replace(/\\/g, '\\\\').replace(/:/g, '\\:');
|
|
205
|
-
fs.writeFileSync(localProps, `sdk.dir=${escaped}\n`, 'utf-8');
|
|
206
|
-
console.log(chalk.green(' ✓ 写入 sdk.dir: ' + sdkPath));
|
|
207
|
-
},
|
|
208
|
-
/**
|
|
209
|
-
* 确保用户项目根存在 serverPath 指向的入口文件
|
|
210
|
-
* 不存在时从 CLI 内置 cli-assets/server.js 复制一份
|
|
211
|
-
*/
|
|
212
|
-
ensureUserEntry = (userProjectDir, config, pkgRoot) => {
|
|
213
|
-
const entryPath = path.join(userProjectDir, config.serverPath);
|
|
214
|
-
if (fs.existsSync(entryPath)) return { created: false, path: entryPath };
|
|
215
|
-
|
|
216
|
-
const fallback = path.join(pkgRoot, 'cli-assets', 'server.js');
|
|
217
|
-
if (!fs.existsSync(fallback)) throw new Error('入口文件不存在: ' + config.serverPath + '(CLI 内置模板也缺失)');
|
|
218
|
-
|
|
219
|
-
ensureDir(path.dirname(entryPath)), fs.copyFileSync(fallback, entryPath);
|
|
220
|
-
console.log(chalk.green(' ✓ 生成默认入口文件: ' + config.serverPath));
|
|
221
|
-
console.log(chalk.gray(' (因为用户项目里没有这个文件,从 CLI 内置模板复制)'));
|
|
222
|
-
|
|
223
|
-
return { created: true, path: entryPath };
|
|
224
|
-
},
|
|
225
|
-
/**
|
|
226
|
-
* 确保用户项目根存在 mobileAppConfig.js
|
|
227
|
-
* 不存在时从 CLI 包根复制一份(带注释的模板)
|
|
228
|
-
*/
|
|
229
|
-
ensureUserConfig = (userProjectDir, pkgRoot) => {
|
|
230
|
-
const configPath = path.join(userProjectDir, 'mobileAppConfig.js');
|
|
231
|
-
if (fs.existsSync(configPath)) return { created: false, path: configPath };
|
|
232
|
-
|
|
233
|
-
const fallback = path.join(pkgRoot, 'mobileAppConfig.js');
|
|
234
|
-
if (!fs.existsSync(fallback)) throw new Error('CLI 缺少 mobileAppConfig.js 模板');
|
|
235
|
-
|
|
236
|
-
fs.copyFileSync(fallback, configPath);
|
|
237
|
-
console.log(chalk.green(' ✓ 生成默认配置文件: mobileAppConfig.js'));
|
|
238
|
-
console.log(chalk.gray(' (因为用户项目里没有这个文件,从 CLI 内置模板复制)'));
|
|
239
|
-
|
|
240
|
-
return { created: true, path: configPath };
|
|
241
|
-
},
|
|
242
|
-
/**
|
|
243
|
-
* 按 config.android.abiFilters 修改 buildDir 里的 app/build.gradle:
|
|
244
|
-
* - defaultConfig.ndk.abiFilters
|
|
245
|
-
* - gradle.properties 的 reactNativeArchitectures
|
|
246
|
-
* 插件 build.gradle 读的是 project(":app").android.defaultConfig.ndk.abiFilters,
|
|
247
|
-
* 所以必须改这里,改插件自身的 abiFilters 无效;
|
|
248
|
-
*/
|
|
249
|
-
patchAndroidAbi = (buildDir, config) => {
|
|
250
|
-
const file = path.join(buildDir, 'android', 'app', 'build.gradle');
|
|
251
|
-
if (!fs.existsSync(file)) return console.warn(chalk.yellow(' ⚠️ 未找到 app/build.gradle,跳过 ABI patch'));
|
|
252
|
-
|
|
253
|
-
const abiList = (config.android.abiFilters || []).map((x) => `"${x}"`).join(', ');
|
|
254
|
-
let c = fs.readFileSync(file, 'utf-8');
|
|
255
|
-
const before = c;
|
|
256
|
-
|
|
257
|
-
// defaultConfig 里的 ndk { abiFilters ... }
|
|
258
|
-
c = c.replace(/(defaultConfig\s*\{[\s\S]*?ndk\s*\{\s*abiFilters\s+)("[^"]*"(?:\s*,\s*"[^"]*")*)/, `$1${abiList}`);
|
|
259
|
-
// 兜底:没有 ndk {} 块时,插到 defaultConfig 里
|
|
260
|
-
if (!/ndk\s*\{\s*abiFilters/.test(c))
|
|
261
|
-
c = c.replace(/(defaultConfig\s*\{)/, `$1\n ndk {\n abiFilters ${abiList}\n }`);
|
|
262
|
-
// 不 patch splits.abi.include:AGP 禁止它和 ndk.abiFilters 同时存在
|
|
263
|
-
if (c !== before) writeIfChanged(file, c), console.log(chalk.green(` ✓ 已设置 app ndk.abiFilters = [${abiList}]`));
|
|
264
|
-
|
|
265
|
-
// 同步 gradle.properties 的 reactNativeArchitectures
|
|
266
|
-
const propsFile = path.join(buildDir, 'android', 'gradle.properties');
|
|
267
|
-
if (fs.existsSync(propsFile)) {
|
|
268
|
-
let p = fs.readFileSync(propsFile, 'utf-8');
|
|
269
|
-
const abiPlain = (config.android.abiFilters || []).join(','), before2 = p;
|
|
270
|
-
p = p.replace(/^reactNativeArchitectures=.*/gm, `reactNativeArchitectures=${abiPlain}`);
|
|
271
|
-
if (p !== before2) writeIfChanged(propsFile, p),
|
|
272
|
-
console.log(chalk.green(` ✓ 已设置 reactNativeArchitectures = ${abiPlain}`));
|
|
121
|
+
patchGradleDistributionUrl = async (buildDir, config) => {
|
|
122
|
+
const file = path.join(buildDir, 'android', 'gradle', 'wrapper', 'gradle-wrapper.properties');
|
|
123
|
+
if (!fs.existsSync(file)) return;
|
|
124
|
+
const src = fs.readFileSync(file, 'utf-8'),
|
|
125
|
+
configured = config && config.android && config.android.gradleDistributionUrl;
|
|
126
|
+
let url = configured;
|
|
127
|
+
if (!url) {
|
|
128
|
+
const m = src.match(/gradle-(\d+\.\d+(?:\.\d+)?)-bin\.zip/);
|
|
129
|
+
if (!m) return console.warn(chalk.yellow(' ⚠️ 无法从模板解析 Gradle 版本号,保留原地址'));
|
|
130
|
+
const v = m[1], candidates = [
|
|
131
|
+
'https://mirrors.cloud.tencent.com/gradle/gradle-' + v + '-bin.zip',
|
|
132
|
+
'https://mirrors.aliyun.com/gradle/distributions/v' + v + '/gradle-' + v + '-bin.zip',
|
|
133
|
+
'https://services.gradle.org/distributions/gradle-' + v + '-bin.zip',
|
|
134
|
+
];
|
|
135
|
+
for (const c of candidates) if (await probeUrl(c)) { url = c; break; }
|
|
136
|
+
if (!url) url = candidates[candidates.length - 1],
|
|
137
|
+
console.warn(chalk.yellow(' ⚠️ 三个 Gradle 源均不可达,回落官方地址'));
|
|
273
138
|
}
|
|
139
|
+
const escaped = url.replace(/:/g, '\\:'),
|
|
140
|
+
next = src.replace(/^distributionUrl=.*$/m, 'distributionUrl=' + escaped);
|
|
141
|
+
if (next !== src) writeIfChanged(file, next);
|
|
142
|
+
if (configured) console.log(chalk.green(' ✓ 已应用自定义 Gradle 下载地址: ' + url));
|
|
274
143
|
},
|
|
275
144
|
/**
|
|
276
|
-
*
|
|
277
|
-
*
|
|
278
|
-
*
|
|
145
|
+
* 把 buildDir/package.json 的依赖版本同步为主包 dependencies 的版本
|
|
146
|
+
* - 依赖名单来自 buildDir/package.json(模板声明需要哪些依赖)
|
|
147
|
+
* - 版本号来自 pkgRoot/package.json 的 dependencies(单一数据源)
|
|
148
|
+
* - 若主包缺少某个依赖,抛错中断(避免模板占位值被真正使用)
|
|
279
149
|
*/
|
|
280
|
-
|
|
281
|
-
const extras = config.extraRnDependencies || [];
|
|
282
|
-
if (extras.length === 0) return;
|
|
283
|
-
|
|
150
|
+
syncTemplateDependencies = (pkgRoot, buildDir) => {
|
|
284
151
|
const buildPkgPath = path.join(buildDir, 'package.json');
|
|
285
|
-
if (!fs.existsSync(buildPkgPath))
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
const buildPkg = readJson(buildPkgPath),
|
|
289
|
-
if (!buildPkg.dependencies)
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
152
|
+
if (!fs.existsSync(buildPkgPath)) return;
|
|
153
|
+
const mainPkgPath = path.join(pkgRoot, 'package.json');
|
|
154
|
+
if (!fs.existsSync(mainPkgPath)) return;
|
|
155
|
+
const buildPkg = readJson(buildPkgPath), mainPkg = readJson(mainPkgPath);
|
|
156
|
+
if (!buildPkg.dependencies) return;
|
|
157
|
+
|
|
158
|
+
const mainDeps = mainPkg.dependencies || {}, missing = [];
|
|
159
|
+
let depsUpdated = 0, needWrite = false;
|
|
160
|
+
|
|
161
|
+
// 移除仅用于模板占位说明的 _comment 字段,避免带进最终 build 工程
|
|
162
|
+
if ('_comment' in buildPkg) delete buildPkg._comment, needWrite = true;
|
|
163
|
+
for (const name of Object.keys(buildPkg.dependencies)) {
|
|
164
|
+
const v = mainDeps[name];
|
|
165
|
+
if (!v) { missing.push(name); continue; }
|
|
166
|
+
if (buildPkg.dependencies[name] !== v) buildPkg.dependencies[name] = v, depsUpdated++, needWrite = true;
|
|
300
167
|
}
|
|
301
168
|
|
|
302
|
-
if (
|
|
169
|
+
if (needWrite) {
|
|
303
170
|
writeIfChanged(buildPkgPath, JSON.stringify(buildPkg, null, 2) + '\n');
|
|
304
|
-
|
|
171
|
+
if (depsUpdated > 0)
|
|
172
|
+
console.log(chalk.green(' ✓ 已同步 ' + depsUpdated + ' 个依赖版本到 ' + BUILD_DIR_NAME + '/package.json'));
|
|
305
173
|
}
|
|
306
174
|
|
|
307
175
|
if (missing.length > 0) {
|
|
308
176
|
throw new Error(
|
|
309
|
-
'
|
|
177
|
+
'template/package.json 声明的以下依赖在主包 dependencies 中不存在:\n' +
|
|
310
178
|
' ' + missing.join(', ') + '\n' +
|
|
311
|
-
'
|
|
312
|
-
'
|
|
313
|
-
' 然后重新运行 node-mobile-app test'
|
|
179
|
+
' 请在 @flun/node-mobile-app 的 package.json 中补上对应声明(版本号随意,\n' +
|
|
180
|
+
' 但必须存在,以便 syncTemplateDependencies 覆盖模板里的占位值);'
|
|
314
181
|
);
|
|
315
182
|
}
|
|
316
183
|
};
|
|
317
184
|
|
|
185
|
+
/**
|
|
186
|
+
* 把 CLI 包里的 template/ 逐文件覆盖到 build 目录
|
|
187
|
+
* - 只覆盖模板里存在的文件,不删除 buildDir 里已有的其他内容(保留 Gradle 增量缓存)
|
|
188
|
+
* - 模板文件被改写时自动同步;新增文件自动添加
|
|
189
|
+
* - 若模板里删除了文件,旧副本残留由 CLI 版本指纹触发全量重建处理
|
|
190
|
+
* >查看定义:@see {@link copyTemplate}
|
|
191
|
+
* @param {string} pkgRoot CLI 包根目录
|
|
192
|
+
* @param {string} buildDir 构建目录
|
|
193
|
+
* @param {object} config 用户配置
|
|
194
|
+
* @returns {Promise<void>}
|
|
195
|
+
*/
|
|
196
|
+
const copyTemplate = async (pkgRoot, buildDir, config) => {
|
|
197
|
+
ensureDir(buildDir);
|
|
198
|
+
|
|
199
|
+
const templateDir = path.join(pkgRoot, 'template');
|
|
200
|
+
if (!fs.existsSync(templateDir)) throw new Error('CLI 包缺少 template/ 目录: ' + templateDir);
|
|
201
|
+
|
|
202
|
+
const keepSet = new Set((config && config.keepFiles) || []);
|
|
203
|
+
copyDirOverwrite(templateDir, buildDir, TEMPLATE_EXCLUDE, buildDir, keepSet);
|
|
204
|
+
await patchGradleDistributionUrl(buildDir, config);
|
|
205
|
+
fs.writeFileSync(path.join(buildDir, 'eslint.config.js'), buildEslintConfigSource(), 'utf-8');
|
|
206
|
+
syncTemplateDependencies(pkgRoot, buildDir);
|
|
207
|
+
linkNestedDeps(pkgRoot, buildDir);
|
|
208
|
+
console.log(chalk.green(' ✓ 模板已更新到 ' + BUILD_DIR_NAME + '/'));
|
|
209
|
+
if (keepSet.size > 0) console.log(chalk.gray(' 保留文件: ' + Array.from(keepSet).join(', ')));
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* 复制用户 Node 项目到 <buildDir>/nodejs-assets/nodejs-project
|
|
214
|
+
* 复制后重写 package.json:只保留 dependencies,剔除 devDependencies;用 config 的 allowScripts
|
|
215
|
+
* >查看定义:@see {@link copyUserProject}
|
|
216
|
+
* @param {string} userProjectDir 用户项目目录
|
|
217
|
+
* @param {string} buildDir 构建目录
|
|
218
|
+
* @param {object} config 用户配置
|
|
219
|
+
* @returns {object} 含 pkg、moduleType、targetDir
|
|
220
|
+
*/
|
|
221
|
+
const copyUserProject = (userProjectDir, buildDir, config) => {
|
|
222
|
+
const targetDir = path.join(buildDir, 'nodejs-assets', 'nodejs-project');
|
|
223
|
+
ensureDir(targetDir);
|
|
224
|
+
|
|
225
|
+
const exclude = [
|
|
226
|
+
...config.excludeFiles, BUILD_DIR_NAME + '/',
|
|
227
|
+
'mobileAppConfig.js', 'mobileAppConfig.mjs', 'mobileAppConfig.cjs'
|
|
228
|
+
];
|
|
229
|
+
|
|
230
|
+
// 增量复制:内容变化的文件才覆盖,保留 node_modules 和其他未变文件
|
|
231
|
+
copyDirIncremental(userProjectDir, targetDir, exclude);
|
|
232
|
+
// 黑名单式过滤:只删 devDependencies 和 allowScripts,其余字段原样保留
|
|
233
|
+
const userPkgPath = path.join(userProjectDir, 'package.json');
|
|
234
|
+
if (!fs.existsSync(userPkgPath)) throw new Error('用户项目缺少 package.json');
|
|
235
|
+
const pkg = readJson(userPkgPath), out = { ...pkg };
|
|
236
|
+
delete out.devDependencies;
|
|
237
|
+
out.allowScripts =
|
|
238
|
+
(config.allowScripts && Object.keys(config.allowScripts).length) ? config.allowScripts : { node: true };
|
|
239
|
+
writeIfChanged(path.join(targetDir, 'package.json'), JSON.stringify(out, null, 2) + '\n');
|
|
240
|
+
|
|
241
|
+
console.log(chalk.green(' ✓ 已同步用户项目到 nodejs-assets/nodejs-project'));
|
|
242
|
+
console.log(chalk.gray(' 模块类型: ' + (out.type === 'module' ? 'ESM' : 'CJS')));
|
|
243
|
+
console.log(chalk.gray(' 生产依赖: ' + Object.keys(out.dependencies || {}).length + ' 个'));
|
|
244
|
+
|
|
245
|
+
return { pkg: out, moduleType: out.type === 'module' ? 'module' : 'commonjs', targetDir };
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* 确保用户项目根存在 mobileAppConfig.js
|
|
250
|
+
* 不存在时从 CLI 包根复制一份(带注释的模板)
|
|
251
|
+
* >查看定义:@see {@link ensureUserConfig}
|
|
252
|
+
* @param {string} userProjectDir 用户项目目录
|
|
253
|
+
* @param {string} pkgRoot CLI 包根目录
|
|
254
|
+
* @returns {object} 含 created、path
|
|
255
|
+
*/
|
|
256
|
+
const ensureUserConfig = (userProjectDir, pkgRoot) => {
|
|
257
|
+
const configPath = path.join(userProjectDir, 'mobileAppConfig.js');
|
|
258
|
+
if (fs.existsSync(configPath)) return { created: false, path: configPath };
|
|
259
|
+
|
|
260
|
+
const fallback = path.join(pkgRoot, 'mobileAppConfig.js');
|
|
261
|
+
if (!fs.existsSync(fallback)) throw new Error('CLI 缺少 mobileAppConfig.js 模板');
|
|
262
|
+
|
|
263
|
+
fs.copyFileSync(fallback, configPath);
|
|
264
|
+
console.log(chalk.green(' ✓ 生成默认配置文件: mobileAppConfig.js'));
|
|
265
|
+
console.log(chalk.gray(' (因为用户项目里没有这个文件,从 CLI 内置模板复制)'));
|
|
266
|
+
|
|
267
|
+
return { created: true, path: configPath };
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* 确保用户项目根存在 serverPath 指向的入口文件
|
|
272
|
+
* 不存在时从 CLI 内置 cli-assets/server.js 复制一份
|
|
273
|
+
* >查看定义:@see {@link ensureUserEntry}
|
|
274
|
+
* @param {string} userProjectDir 用户项目目录
|
|
275
|
+
* @param {object} config 用户配置
|
|
276
|
+
* @param {string} pkgRoot CLI 包根目录
|
|
277
|
+
* @returns {object} 含 created、path
|
|
278
|
+
*/
|
|
279
|
+
const ensureUserEntry = (userProjectDir, config, pkgRoot) => {
|
|
280
|
+
const entryPath = path.join(userProjectDir, config.serverPath);
|
|
281
|
+
if (fs.existsSync(entryPath)) return { created: false, path: entryPath };
|
|
282
|
+
|
|
283
|
+
const fallback = path.join(pkgRoot, 'cli-assets', 'server.js');
|
|
284
|
+
if (!fs.existsSync(fallback)) throw new Error('入口文件不存在: ' + config.serverPath + '(CLI 内置模板也缺失)');
|
|
285
|
+
|
|
286
|
+
ensureDir(path.dirname(entryPath)), fs.copyFileSync(fallback, entryPath);
|
|
287
|
+
console.log(chalk.green(' ✓ 生成默认入口文件: ' + config.serverPath));
|
|
288
|
+
console.log(chalk.gray(' (因为用户项目里没有这个文件,从 CLI 内置模板复制)'));
|
|
289
|
+
|
|
290
|
+
return { created: true, path: entryPath };
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* 获取 build 目录路径:<userProject>/node-mobile-app-build
|
|
295
|
+
* >查看定义:@see {@link getBuildDir}
|
|
296
|
+
* @param {string} userProjectDir 用户项目目录
|
|
297
|
+
* @returns {string} build 目录绝对路径
|
|
298
|
+
*/
|
|
299
|
+
const getBuildDir = userProjectDir => path.join(userProjectDir, BUILD_DIR_NAME);
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* 按 config.android.abiFilters 修改 buildDir 里的 app/build.gradle:
|
|
303
|
+
* - defaultConfig.ndk.abiFilters
|
|
304
|
+
* - gradle.properties 的 reactNativeArchitectures
|
|
305
|
+
* 插件 build.gradle 读的是 project(":app").android.defaultConfig.ndk.abiFilters,
|
|
306
|
+
* 所以必须改这里,改插件自身的 abiFilters 无效;
|
|
307
|
+
* >查看定义:@see {@link patchAndroidAbi}
|
|
308
|
+
* @param {string} buildDir 构建目录
|
|
309
|
+
* @param {object} config 用户配置
|
|
310
|
+
* @returns {void}
|
|
311
|
+
*/
|
|
312
|
+
const patchAndroidAbi = (buildDir, config) => {
|
|
313
|
+
const file = path.join(buildDir, 'android', 'app', 'build.gradle');
|
|
314
|
+
if (!fs.existsSync(file)) return console.warn(chalk.yellow(' ⚠️ 未找到 app/build.gradle,跳过 ABI patch'));
|
|
315
|
+
|
|
316
|
+
const abiList = (config.android.abiFilters || []).map((x) => `"${x}"`).join(', ');
|
|
317
|
+
let c = fs.readFileSync(file, 'utf-8');
|
|
318
|
+
const before = c;
|
|
319
|
+
|
|
320
|
+
// defaultConfig 里的 ndk { abiFilters ... }
|
|
321
|
+
c = c.replace(/(defaultConfig\s*\{[\s\S]*?ndk\s*\{\s*abiFilters\s+)("[^"]*"(?:\s*,\s*"[^"]*")*)/, `$1${abiList}`);
|
|
322
|
+
// 兜底:没有 ndk {} 块时,插到 defaultConfig 里
|
|
323
|
+
if (!/ndk\s*\{\s*abiFilters/.test(c))
|
|
324
|
+
c = c.replace(/(defaultConfig\s*\{)/, `$1\n ndk {\n abiFilters ${abiList}\n }`);
|
|
325
|
+
// 不 patch splits.abi.include:AGP 禁止它和 ndk.abiFilters 同时存在
|
|
326
|
+
if (c !== before) writeIfChanged(file, c), console.log(chalk.green(` ✓ 已设置 app ndk.abiFilters = [${abiList}]`));
|
|
327
|
+
|
|
328
|
+
// 同步 gradle.properties 的 reactNativeArchitectures
|
|
329
|
+
const propsFile = path.join(buildDir, 'android', 'gradle.properties');
|
|
330
|
+
if (fs.existsSync(propsFile)) {
|
|
331
|
+
let p = fs.readFileSync(propsFile, 'utf-8');
|
|
332
|
+
const abiPlain = (config.android.abiFilters || []).join(','), before2 = p;
|
|
333
|
+
p = p.replace(/^reactNativeArchitectures=.*/gm, `reactNativeArchitectures=${abiPlain}`);
|
|
334
|
+
if (p !== before2) writeIfChanged(propsFile, p),
|
|
335
|
+
console.log(chalk.green(` ✓ 已设置 reactNativeArchitectures = ${abiPlain}`));
|
|
336
|
+
}
|
|
337
|
+
};
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* 把用户在 mobileAppConfig.js 里声明的 extraRnDependencies 注入 buildDir/package.json
|
|
341
|
+
* 版本号从用户根 node_modules 读实际装的版本
|
|
342
|
+
* 用户没装的会中断并提示安装命令
|
|
343
|
+
* >查看定义:@see {@link patchExtraRnDependencies}
|
|
344
|
+
* @param {string} buildDir 构建目录
|
|
345
|
+
* @param {string} userProjectDir 用户项目目录
|
|
346
|
+
* @param {object} config 用户配置
|
|
347
|
+
* @returns {void}
|
|
348
|
+
*/
|
|
349
|
+
const patchExtraRnDependencies = (buildDir, userProjectDir, config) => {
|
|
350
|
+
const extras = config.extraRnDependencies || [];
|
|
351
|
+
if (extras.length === 0) return;
|
|
352
|
+
|
|
353
|
+
const buildPkgPath = path.join(buildDir, 'package.json');
|
|
354
|
+
if (!fs.existsSync(buildPkgPath))
|
|
355
|
+
return console.warn(chalk.yellow(' ⚠️ 未找到 buildDir/package.json,跳过 extraRnDependencies'));
|
|
356
|
+
|
|
357
|
+
const buildPkg = readJson(buildPkgPath), missing = [];
|
|
358
|
+
if (!buildPkg.dependencies) buildPkg.dependencies = {};
|
|
359
|
+
let added = 0;
|
|
360
|
+
for (const name of extras) {
|
|
361
|
+
const pkgJson = path.join(userProjectDir, 'node_modules', name, 'package.json');
|
|
362
|
+
if (!fs.existsSync(pkgJson)) {
|
|
363
|
+
missing.push(name); continue;
|
|
364
|
+
}
|
|
365
|
+
try {
|
|
366
|
+
const v = JSON.parse(fs.readFileSync(pkgJson, 'utf-8')).version;
|
|
367
|
+
if (buildPkg.dependencies[name] !== v) buildPkg.dependencies[name] = v, added++;
|
|
368
|
+
} catch { missing.push(name); }
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
if (added > 0) {
|
|
372
|
+
writeIfChanged(buildPkgPath, JSON.stringify(buildPkg, null, 2) + '\n');
|
|
373
|
+
console.log(chalk.green(` ✓ 已注入 ${added} 个额外 RN 依赖到 buildDir/package.json`));
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
if (missing.length > 0) {
|
|
377
|
+
throw new Error(
|
|
378
|
+
'extraRnDependencies 里以下包未安装:\n' +
|
|
379
|
+
' ' + missing.join(', ') + '\n' +
|
|
380
|
+
' 请先在项目根执行:\n' +
|
|
381
|
+
' npm install ' + missing.join(' ') + '\n' +
|
|
382
|
+
' 然后重新运行 node-mobile-app test'
|
|
383
|
+
);
|
|
384
|
+
}
|
|
385
|
+
};
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* 生成 <buildDir>/android/local.properties(Gradle 需要 sdk.dir)
|
|
389
|
+
* >查看定义:@see {@link writeLocalProperties}
|
|
390
|
+
* @param {string} buildDir 构建目录
|
|
391
|
+
* @param {object} config 用户配置
|
|
392
|
+
* @returns {void}
|
|
393
|
+
*/
|
|
394
|
+
const writeLocalProperties = (buildDir, config) => {
|
|
395
|
+
const sdkPath = resolveSdkPath(config);
|
|
396
|
+
if (!sdkPath)
|
|
397
|
+
return console.warn(chalk.yellow(' ⚠️ 未找到 Android SDK,请在 mobileAppConfig.js 里配置 android.sdkPath'));
|
|
398
|
+
|
|
399
|
+
const localProps = path.join(buildDir, 'android', 'local.properties'),
|
|
400
|
+
escaped = sdkPath.replace(/\\/g, '\\\\').replace(/:/g, '\\:');
|
|
401
|
+
fs.writeFileSync(localProps, `sdk.dir=${escaped}\n`, 'utf-8');
|
|
402
|
+
console.log(chalk.green(' ✓ 写入 sdk.dir: ' + sdkPath));
|
|
403
|
+
};
|
|
404
|
+
|
|
318
405
|
export {
|
|
319
|
-
|
|
320
|
-
patchExtraRnDependencies
|
|
406
|
+
copyTemplate, copyUserProject, ensureUserConfig, ensureUserEntry, getBuildDir, patchAndroidAbi,
|
|
407
|
+
patchExtraRnDependencies, writeLocalProperties
|
|
321
408
|
};
|