@flun/node-mobile-app 2.1.1 → 3.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/Android /346/236/204/345/273/272/346/214/207/345/215/227.md" +1 -1
- package/CHANGELOG.md +34 -32
- package/README.md +36 -7
- package/bin/cli.js +11 -10
- package/mobileAppConfig.js +10 -0
- package/package.json +15 -13
- package/scripts/copy-files.js +24 -25
- 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 +698 -0
- package/src/patcher.js +21 -24
- package/src/platform-ios.js +139 -141
- package/src/utils.js +189 -176
package/src/patcher.js
CHANGED
|
@@ -4,26 +4,25 @@ import { writeIfChanged } from './utils.js';
|
|
|
4
4
|
|
|
5
5
|
// 把字符串中所有非 ASCII 字符转成 \uXXXX(Java Properties 规范)
|
|
6
6
|
// gradle.properties 按 ISO-8859-1 读取,无法改为 UTF-8
|
|
7
|
-
const escapeProps =
|
|
8
|
-
String(s).replace(/[^\x00-\x7F]/g,
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
},
|
|
7
|
+
const escapeProps = s =>
|
|
8
|
+
String(s).replace(/[^\x00-\x7F]/g, ch => '\\u' + ch.charCodeAt(0).toString(16).padStart(4, '0')),
|
|
9
|
+
/**
|
|
10
|
+
* 解析最终 appName:
|
|
11
|
+
* 1. config.appName
|
|
12
|
+
* 2. 用户项目 package.json 的 name
|
|
13
|
+
* 3. 'MyApp'
|
|
14
|
+
*/
|
|
15
|
+
resolveAppName = (userProjectDir, config) => {
|
|
16
|
+
if (config.appName) return String(config.appName);
|
|
17
|
+
const pkgPath = path.join(userProjectDir, 'package.json');
|
|
18
|
+
if (fs.existsSync(pkgPath)) {
|
|
19
|
+
try {
|
|
20
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
|
|
21
|
+
if (pkg.name) return String(pkg.name);
|
|
22
|
+
} catch { }
|
|
23
|
+
}
|
|
24
|
+
return 'MyApp';
|
|
25
|
+
},
|
|
27
26
|
/** 通用字符串转义基础:\\, ", \n, \r, \t */
|
|
28
27
|
escapeStringBase = s => String(s)
|
|
29
28
|
.replace(/\\/g, '\\\\')
|
|
@@ -53,8 +52,7 @@ const resolveAppName = (userProjectDir, config) => {
|
|
|
53
52
|
patchAppConfig = (buildDir, userProjectDir, config) => {
|
|
54
53
|
const appName = resolveAppName(userProjectDir, config), appId = config.appId,
|
|
55
54
|
versionCode = config.versionCode, versionName = config.versionName,
|
|
56
|
-
gradleFile = path.join(buildDir, 'android', 'app', 'build.gradle'),
|
|
57
|
-
appJsonFile = path.join(buildDir, 'app.json'),
|
|
55
|
+
gradleFile = path.join(buildDir, 'android', 'app', 'build.gradle'), appJsonFile = path.join(buildDir, 'app.json'),
|
|
58
56
|
stringsFile = path.join(buildDir, 'android', 'app', 'src', 'main', 'res', 'values', 'strings.xml'),
|
|
59
57
|
ktFiles = [
|
|
60
58
|
path.join(buildDir, 'android', 'app', 'src', 'main', 'java', 'com', 'myapp', 'MainActivity.kt'),
|
|
@@ -384,8 +382,7 @@ const resolveAppName = (userProjectDir, config) => {
|
|
|
384
382
|
console.log(` ✓ iOS 图标已应用: ${path.basename(iconAbs)}`);
|
|
385
383
|
}
|
|
386
384
|
|
|
387
|
-
// ===== 导出 =====
|
|
388
385
|
export {
|
|
389
386
|
resolveAppName, escapeKotlinString, escapeSwiftString, patchAppConfig, patchAndroidConfig, patchSigning,
|
|
390
387
|
patchIcon, patchIosConfig, patchIosIcon
|
|
391
|
-
};
|
|
388
|
+
};
|
package/src/platform-ios.js
CHANGED
|
@@ -1,142 +1,140 @@
|
|
|
1
|
-
import path from 'path';
|
|
2
|
-
import fs from 'fs';
|
|
3
|
-
import { run, ensureDir } from './utils.js';
|
|
4
|
-
|
|
5
|
-
const SCHEME = 'MyApp',
|
|
6
|
-
/** iOS 相关命令只能在 macOS 上跑 */
|
|
7
|
-
ensureMac = () => {
|
|
8
|
-
if (process.platform !== 'darwin') throw new Error('iOS 构建只能在 macOS 上执行(当前平台: ' + process.platform + ')');
|
|
9
|
-
},
|
|
10
|
-
/** 检查 xcodebuild 是否存在 */
|
|
11
|
-
checkIosTools = () => {
|
|
12
|
-
const r = run('which', ['xcodebuild'], { stdio: 'pipe' });
|
|
13
|
-
if (r.status !== 0) throw new Error('未找到 xcodebuild,请安装 Xcode 并运行 xcode-select --install');
|
|
14
|
-
},
|
|
15
|
-
/**
|
|
16
|
-
* 执行 pod install(若 Pods/ 已存在且 Podfile.lock 未变,cocoapods 会自动跳过)
|
|
17
|
-
*/
|
|
18
|
-
podInstall = iosDir => {
|
|
19
|
-
console.log(' pod install ...'), run('pod', ['install'], { cwd: iosDir });
|
|
20
|
-
},
|
|
21
|
-
/**
|
|
22
|
-
* 把 config.ios.signing.certificate 导入临时 keychain,供 xcodebuild 签名用
|
|
23
|
-
* 返回 { keychainPath, keychainPassword } 或 null(未配置 / 文件缺失)
|
|
24
|
-
*/
|
|
25
|
-
setupSigningKeychain = (buildDir, userProjectDir, config) => {
|
|
26
|
-
const signing = config.ios && config.ios.signing;
|
|
27
|
-
if (!signing || !signing.certificate) return null;
|
|
28
|
-
|
|
29
|
-
const certPath = path.resolve(userProjectDir, signing.certificate);
|
|
30
|
-
if (!fs.existsSync(certPath)) {
|
|
31
|
-
console.warn(' ⚠️ iOS 签名证书不存在,跳过:', certPath);
|
|
32
|
-
return null;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
const keychainDir = path.join(buildDir, 'ios', 'build', 'signing');
|
|
36
|
-
ensureDir(keychainDir);
|
|
37
|
-
const keychainPath = path.join(keychainDir, 'build.keychain-db'), keychainPassword = 'nmapp' + Date.now();
|
|
38
|
-
|
|
39
|
-
// 清掉同名的旧 keychain(重复构建时)
|
|
40
|
-
if (fs.existsSync(keychainPath)) fs.rmSync(keychainPath, { force: true });
|
|
41
|
-
console.log(' 创建临时 keychain 并导入证书 ...');
|
|
42
|
-
run('security', ['create-keychain', '-p', keychainPassword, keychainPath]);
|
|
43
|
-
run('security', ['set-keychain-settings', '-lut', '3600', keychainPath]);
|
|
44
|
-
run('security', ['unlock-keychain', '-p', keychainPassword, keychainPath]);
|
|
45
|
-
run('security', [
|
|
46
|
-
'import', certPath,
|
|
47
|
-
'-k', keychainPath,
|
|
48
|
-
'-P', signing.certificatePassword || '',
|
|
49
|
-
'-T', '/usr/bin/codesign',
|
|
50
|
-
'-T', '/usr/bin/security'
|
|
51
|
-
]);
|
|
52
|
-
run('security', [
|
|
53
|
-
'set-key-partition-list',
|
|
54
|
-
'-S', 'apple-tool:,apple:,codesign:',
|
|
55
|
-
'-s',
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
const
|
|
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
|
-
const
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
'
|
|
107
|
-
'
|
|
108
|
-
'<
|
|
109
|
-
'<
|
|
110
|
-
' <key>
|
|
111
|
-
' <
|
|
112
|
-
' <key>
|
|
113
|
-
' <
|
|
114
|
-
' <key>
|
|
115
|
-
' <
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
profileName ? '
|
|
119
|
-
profileName ? '
|
|
120
|
-
profileName ? '
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
'-
|
|
131
|
-
'-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
};
|
|
141
|
-
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import { run, ensureDir } from './utils.js';
|
|
4
|
+
|
|
5
|
+
const SCHEME = 'MyApp',
|
|
6
|
+
/** iOS 相关命令只能在 macOS 上跑 */
|
|
7
|
+
ensureMac = () => {
|
|
8
|
+
if (process.platform !== 'darwin') throw new Error('iOS 构建只能在 macOS 上执行(当前平台: ' + process.platform + ')');
|
|
9
|
+
},
|
|
10
|
+
/** 检查 xcodebuild 是否存在 */
|
|
11
|
+
checkIosTools = () => {
|
|
12
|
+
const r = run('which', ['xcodebuild'], { stdio: 'pipe' });
|
|
13
|
+
if (r.status !== 0) throw new Error('未找到 xcodebuild,请安装 Xcode 并运行 xcode-select --install');
|
|
14
|
+
},
|
|
15
|
+
/**
|
|
16
|
+
* 执行 pod install(若 Pods/ 已存在且 Podfile.lock 未变,cocoapods 会自动跳过)
|
|
17
|
+
*/
|
|
18
|
+
podInstall = iosDir => {
|
|
19
|
+
console.log(' pod install ...'), run('pod', ['install'], { cwd: iosDir, inherit: true });
|
|
20
|
+
},
|
|
21
|
+
/**
|
|
22
|
+
* 把 config.ios.signing.certificate 导入临时 keychain,供 xcodebuild 签名用
|
|
23
|
+
* 返回 { keychainPath, keychainPassword } 或 null(未配置 / 文件缺失)
|
|
24
|
+
*/
|
|
25
|
+
setupSigningKeychain = (buildDir, userProjectDir, config) => {
|
|
26
|
+
const signing = config.ios && config.ios.signing;
|
|
27
|
+
if (!signing || !signing.certificate) return null;
|
|
28
|
+
|
|
29
|
+
const certPath = path.resolve(userProjectDir, signing.certificate);
|
|
30
|
+
if (!fs.existsSync(certPath)) {
|
|
31
|
+
console.warn(' ⚠️ iOS 签名证书不存在,跳过:', certPath);
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const keychainDir = path.join(buildDir, 'ios', 'build', 'signing');
|
|
36
|
+
ensureDir(keychainDir);
|
|
37
|
+
const keychainPath = path.join(keychainDir, 'build.keychain-db'), keychainPassword = 'nmapp' + Date.now();
|
|
38
|
+
|
|
39
|
+
// 清掉同名的旧 keychain(重复构建时)
|
|
40
|
+
if (fs.existsSync(keychainPath)) fs.rmSync(keychainPath, { force: true });
|
|
41
|
+
console.log(' 创建临时 keychain 并导入证书 ...');
|
|
42
|
+
run('security', ['create-keychain', '-p', keychainPassword, keychainPath], { quiet: true });
|
|
43
|
+
run('security', ['set-keychain-settings', '-lut', '3600', keychainPath], { quiet: true });
|
|
44
|
+
run('security', ['unlock-keychain', '-p', keychainPassword, keychainPath], { quiet: true });
|
|
45
|
+
run('security', [
|
|
46
|
+
'import', certPath,
|
|
47
|
+
'-k', keychainPath,
|
|
48
|
+
'-P', signing.certificatePassword || '',
|
|
49
|
+
'-T', '/usr/bin/codesign',
|
|
50
|
+
'-T', '/usr/bin/security'
|
|
51
|
+
], { quiet: true });
|
|
52
|
+
run('security', [
|
|
53
|
+
'set-key-partition-list',
|
|
54
|
+
'-S', 'apple-tool:,apple:,codesign:',
|
|
55
|
+
'-s', '-k', keychainPassword, keychainPath
|
|
56
|
+
], { quiet: true });
|
|
57
|
+
|
|
58
|
+
return { keychainPath, keychainPassword };
|
|
59
|
+
},
|
|
60
|
+
/**
|
|
61
|
+
* xcodebuild archive
|
|
62
|
+
* 返回 xcarchive 路径
|
|
63
|
+
*/
|
|
64
|
+
xcodebuildArchive = (buildDir, config, release, userProjectDir) => {
|
|
65
|
+
const iosDir = path.join(buildDir, 'ios'), workspace = path.join(iosDir, SCHEME + '.xcworkspace');
|
|
66
|
+
if (!fs.existsSync(workspace)) throw new Error('未找到 ' + workspace + '(pod install 是否成功?)');
|
|
67
|
+
|
|
68
|
+
const archiveDir = path.join(iosDir, 'build');
|
|
69
|
+
ensureDir(archiveDir);
|
|
70
|
+
const archivePath = path.join(archiveDir, SCHEME + '.xcarchive'), configName = release ? 'Release' : 'Debug',
|
|
71
|
+
teamId = (config.ios && config.ios.signing && config.ios.signing.teamId) || '',
|
|
72
|
+
args = [
|
|
73
|
+
'-workspace', workspace,
|
|
74
|
+
'-scheme', SCHEME,
|
|
75
|
+
'-configuration', configName,
|
|
76
|
+
'-destination', 'generic/platform=iOS',
|
|
77
|
+
'-archivePath', archivePath,
|
|
78
|
+
'archive',
|
|
79
|
+
];
|
|
80
|
+
|
|
81
|
+
if (teamId) args.push('DEVELOPMENT_TEAM=' + teamId);
|
|
82
|
+
|
|
83
|
+
// 若配置了 certificate,导入临时 keychain 并让 xcodebuild 使用
|
|
84
|
+
const keychain = setupSigningKeychain(buildDir, userProjectDir, config);
|
|
85
|
+
if (keychain) args.push('OTHER_CODE_SIGN_FLAGS=--keychain ' + keychain.keychainPath);
|
|
86
|
+
|
|
87
|
+
console.log(' xcodebuild archive ...'), run('xcodebuild', args, { cwd: iosDir, quiet: true });
|
|
88
|
+
return archivePath;
|
|
89
|
+
},
|
|
90
|
+
/**
|
|
91
|
+
* xcodebuild -exportArchive,导出 .ipa
|
|
92
|
+
* 返回 .ipa 路径
|
|
93
|
+
*/
|
|
94
|
+
xcodebuildExport = (buildDir, config, release, outDir, userProjectDir) => {
|
|
95
|
+
const iosDir = path.join(buildDir, 'ios'), exportDir = path.join(iosDir, 'build', 'export');
|
|
96
|
+
ensureDir(exportDir);
|
|
97
|
+
const archivePath = path.join(iosDir, 'build', SCHEME + '.xcarchive'), method = release ? 'ad-hoc' : 'development',
|
|
98
|
+
signing = (config.ios && config.ios.signing) || {}, teamId = signing.teamId || '',
|
|
99
|
+
// provisioningProfile 路径 → 取 basename(去扩展名),写进 exportOptions.plist
|
|
100
|
+
profilePath = signing.provisioningProfile ? path.resolve(userProjectDir, signing.provisioningProfile) : '',
|
|
101
|
+
profileName = profilePath ? path.basename(profilePath).replace(/\.mobileprovision$/i, '') : '',
|
|
102
|
+
plistPath = path.join(iosDir, 'build', 'exportOptions.plist'),
|
|
103
|
+
plist = [
|
|
104
|
+
'<?xml version="1.0" encoding="UTF-8"?>',
|
|
105
|
+
'<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">',
|
|
106
|
+
'<plist version="1.0">',
|
|
107
|
+
'<dict>',
|
|
108
|
+
' <key>method</key>',
|
|
109
|
+
' <string>' + method + '</string>',
|
|
110
|
+
' <key>stripSwiftSymbols</key>',
|
|
111
|
+
' <true/>',
|
|
112
|
+
' <key>compileBitcode</key>',
|
|
113
|
+
' <false/>',
|
|
114
|
+
teamId ? ' <key>teamID</key>' : '',
|
|
115
|
+
teamId ? ' <string>' + teamId + '</string>' : '',
|
|
116
|
+
profileName ? ' <key>provisioningProfiles</key>' : '',
|
|
117
|
+
profileName ? ' <dict>' : '',
|
|
118
|
+
profileName ? ' <key>' + config.appId + '</key>' : '',
|
|
119
|
+
profileName ? ' <string>' + profileName + '</string>' : '',
|
|
120
|
+
profileName ? ' </dict>' : '',
|
|
121
|
+
'</dict>',
|
|
122
|
+
'</plist>',
|
|
123
|
+
].filter(Boolean).join('\n');
|
|
124
|
+
fs.writeFileSync(plistPath, plist, 'utf-8'); // 生成 exportOptions.plist
|
|
125
|
+
|
|
126
|
+
console.log(' xcodebuild -exportArchive ...');
|
|
127
|
+
run('xcodebuild', [
|
|
128
|
+
'-exportArchive',
|
|
129
|
+
'-archivePath', archivePath,
|
|
130
|
+
'-exportOptionsPlist', plistPath,
|
|
131
|
+
'-exportPath', exportDir,
|
|
132
|
+
], { cwd: iosDir, quiet: true });
|
|
133
|
+
|
|
134
|
+
// 找 .ipa
|
|
135
|
+
const files = fs.readdirSync(exportDir).filter(f => f.endsWith('.ipa'));
|
|
136
|
+
if (files.length === 0) throw new Error('导出目录里没有 .ipa: ' + exportDir);
|
|
137
|
+
return path.join(exportDir, files[0]);
|
|
138
|
+
};
|
|
139
|
+
|
|
142
140
|
export { ensureMac, checkIosTools, podInstall, xcodebuildArchive, xcodebuildExport }
|