@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/utils.js
CHANGED
|
@@ -1,177 +1,190 @@
|
|
|
1
|
-
import fs from 'fs';
|
|
2
|
-
import path from 'path';
|
|
3
|
-
import { spawnSync } from 'child_process';
|
|
4
|
-
|
|
5
|
-
const ensureDir = dir => {
|
|
6
|
-
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
7
|
-
},
|
|
8
|
-
removeDir = dir => {
|
|
9
|
-
if (fs.existsSync(dir)) fs.rmSync(dir, { recursive: true, force: true });
|
|
10
|
-
},
|
|
11
|
-
isExcluded = (name, patterns) => {
|
|
12
|
-
for (let p of patterns) {
|
|
13
|
-
p = p.replace(/\\/g, '/').replace(/^\.\//, '').replace(/\/$/, '');
|
|
14
|
-
if (p === name) return true;
|
|
15
|
-
if (p.includes('*')) {
|
|
16
|
-
const escaped = p.replace(/[.+?^${}()|[\]\\/]/g, '\\$&').replace(/\*/g, '.*');
|
|
17
|
-
if (new RegExp('^' + escaped + '$').test(name)) return true;
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
return false;
|
|
21
|
-
},
|
|
22
|
-
/**
|
|
23
|
-
* 递归复制,exclude 支持 'node_modules/'、'*.log'、'.git/'
|
|
24
|
-
*/
|
|
25
|
-
copyDir = (src, dest, exclude = []) => {
|
|
26
|
-
ensureDir(dest);
|
|
27
|
-
for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
|
|
28
|
-
const name = entry.name;
|
|
29
|
-
if (isExcluded(name, exclude)) continue;
|
|
30
|
-
const s = path.join(src, name), d = path.join(dest, name);
|
|
31
|
-
if (entry.isDirectory()) copyDir(s, d, exclude);
|
|
32
|
-
else if (entry.isFile()) fs.copyFileSync(s, d);
|
|
33
|
-
}
|
|
34
|
-
},
|
|
35
|
-
readJson = file => JSON.parse(fs.readFileSync(file, 'utf-8')),
|
|
36
|
-
writeJson = (file, obj) => fs.writeFileSync(file, JSON.stringify(obj, null, 2) + '\n', 'utf-8'),
|
|
37
|
-
/**
|
|
38
|
-
* 内容不同才写文件,避免不必要的 mtime 变化触发下游(Gradle/Metro)重编
|
|
39
|
-
* 返回 true 表示写入了,false 表示内容相同跳过
|
|
40
|
-
*/
|
|
41
|
-
writeIfChanged = (file, content) => {
|
|
42
|
-
if (fs.existsSync(file)) {
|
|
43
|
-
try {
|
|
44
|
-
if (fs.readFileSync(file, 'utf-8') === content) return false;
|
|
45
|
-
} catch { /* 读失败就当需要写 */ }
|
|
46
|
-
}
|
|
47
|
-
fs.writeFileSync(file, content, 'utf-8');
|
|
48
|
-
return true;
|
|
49
|
-
},
|
|
50
|
-
/**
|
|
51
|
-
* 大小和 mtime 都一致才跳过复制,否则复制
|
|
52
|
-
* 用于用户项目 → nodejs-project 的增量复制
|
|
53
|
-
*/
|
|
54
|
-
copyFileIfChanged = (src, dst) => {
|
|
55
|
-
if (fs.existsSync(dst)) {
|
|
56
|
-
try {
|
|
57
|
-
const s = fs.statSync(src), d = fs.statSync(dst);
|
|
58
|
-
if (s.size === d.size && Math.floor(s.mtimeMs) <= Math.floor(d.mtimeMs)) return false;
|
|
59
|
-
} catch { /* 出错就复制 */ }
|
|
60
|
-
}
|
|
61
|
-
fs.copyFileSync(src, dst);
|
|
62
|
-
return true;
|
|
63
|
-
},
|
|
64
|
-
// 把单个参数转成适合 Windows cmd.exe 的形式:含空白或 shell 元字符时加双引号。
|
|
65
|
-
quoteShellArg = (s) => {
|
|
66
|
-
const str = String(s);
|
|
67
|
-
if (str === '') return '""';
|
|
68
|
-
return /[\s"&|<>^()]/.test(str) ? '"' + str.replace(/"/g, '""') + '"' : str;
|
|
69
|
-
},
|
|
70
|
-
/**
|
|
71
|
-
* 统一的子进程启动:
|
|
72
|
-
* - Windows:.cmd / .bat 需经 shell;把命令与参数拼成单字符串,
|
|
73
|
-
* 避免 spawnSync(bin, args, { shell: true }) 触发 DEP0190。
|
|
74
|
-
* - 其它平台:不用 shell,参数交由 Node 转义。
|
|
75
|
-
*/
|
|
76
|
-
spawnCmd = (bin, args, baseOpts = {}) => {
|
|
77
|
-
if (process.platform === 'win32') {
|
|
78
|
-
const cmd = [bin, ...args].map(quoteShellArg).join(' ');
|
|
79
|
-
return spawnSync(cmd, { ...baseOpts, shell: true });
|
|
80
|
-
}
|
|
81
|
-
return spawnSync(bin, args, { ...baseOpts, shell: false });
|
|
82
|
-
},
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
const
|
|
104
|
-
if (
|
|
105
|
-
if (
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
const
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
*
|
|
141
|
-
*/
|
|
142
|
-
|
|
143
|
-
const
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
{
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { spawnSync } from 'child_process';
|
|
4
|
+
|
|
5
|
+
const ensureDir = dir => {
|
|
6
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
7
|
+
},
|
|
8
|
+
removeDir = dir => {
|
|
9
|
+
if (fs.existsSync(dir)) fs.rmSync(dir, { recursive: true, force: true });
|
|
10
|
+
},
|
|
11
|
+
isExcluded = (name, patterns) => {
|
|
12
|
+
for (let p of patterns) {
|
|
13
|
+
p = p.replace(/\\/g, '/').replace(/^\.\//, '').replace(/\/$/, '');
|
|
14
|
+
if (p === name) return true;
|
|
15
|
+
if (p.includes('*')) {
|
|
16
|
+
const escaped = p.replace(/[.+?^${}()|[\]\\/]/g, '\\$&').replace(/\*/g, '.*');
|
|
17
|
+
if (new RegExp('^' + escaped + '$').test(name)) return true;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return false;
|
|
21
|
+
},
|
|
22
|
+
/**
|
|
23
|
+
* 递归复制,exclude 支持 'node_modules/'、'*.log'、'.git/'
|
|
24
|
+
*/
|
|
25
|
+
copyDir = (src, dest, exclude = []) => {
|
|
26
|
+
ensureDir(dest);
|
|
27
|
+
for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
|
|
28
|
+
const name = entry.name;
|
|
29
|
+
if (isExcluded(name, exclude)) continue;
|
|
30
|
+
const s = path.join(src, name), d = path.join(dest, name);
|
|
31
|
+
if (entry.isDirectory()) copyDir(s, d, exclude);
|
|
32
|
+
else if (entry.isFile()) fs.copyFileSync(s, d);
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
readJson = file => JSON.parse(fs.readFileSync(file, 'utf-8')),
|
|
36
|
+
writeJson = (file, obj) => fs.writeFileSync(file, JSON.stringify(obj, null, 2) + '\n', 'utf-8'),
|
|
37
|
+
/**
|
|
38
|
+
* 内容不同才写文件,避免不必要的 mtime 变化触发下游(Gradle/Metro)重编
|
|
39
|
+
* 返回 true 表示写入了,false 表示内容相同跳过
|
|
40
|
+
*/
|
|
41
|
+
writeIfChanged = (file, content) => {
|
|
42
|
+
if (fs.existsSync(file)) {
|
|
43
|
+
try {
|
|
44
|
+
if (fs.readFileSync(file, 'utf-8') === content) return false;
|
|
45
|
+
} catch { /* 读失败就当需要写 */ }
|
|
46
|
+
}
|
|
47
|
+
fs.writeFileSync(file, content, 'utf-8');
|
|
48
|
+
return true;
|
|
49
|
+
},
|
|
50
|
+
/**
|
|
51
|
+
* 大小和 mtime 都一致才跳过复制,否则复制
|
|
52
|
+
* 用于用户项目 → nodejs-project 的增量复制
|
|
53
|
+
*/
|
|
54
|
+
copyFileIfChanged = (src, dst) => {
|
|
55
|
+
if (fs.existsSync(dst)) {
|
|
56
|
+
try {
|
|
57
|
+
const s = fs.statSync(src), d = fs.statSync(dst);
|
|
58
|
+
if (s.size === d.size && Math.floor(s.mtimeMs) <= Math.floor(d.mtimeMs)) return false;
|
|
59
|
+
} catch { /* 出错就复制 */ }
|
|
60
|
+
}
|
|
61
|
+
fs.copyFileSync(src, dst);
|
|
62
|
+
return true;
|
|
63
|
+
},
|
|
64
|
+
// 把单个参数转成适合 Windows cmd.exe 的形式:含空白或 shell 元字符时加双引号。
|
|
65
|
+
quoteShellArg = (s) => {
|
|
66
|
+
const str = String(s);
|
|
67
|
+
if (str === '') return '""';
|
|
68
|
+
return /[\s"&|<>^()]/.test(str) ? '"' + str.replace(/"/g, '""') + '"' : str;
|
|
69
|
+
},
|
|
70
|
+
/**
|
|
71
|
+
* 统一的子进程启动:
|
|
72
|
+
* - Windows:.cmd / .bat 需经 shell;把命令与参数拼成单字符串,
|
|
73
|
+
* 避免 spawnSync(bin, args, { shell: true }) 触发 DEP0190。
|
|
74
|
+
* - 其它平台:不用 shell,参数交由 Node 转义。
|
|
75
|
+
*/
|
|
76
|
+
spawnCmd = (bin, args, baseOpts = {}) => {
|
|
77
|
+
if (process.platform === 'win32') {
|
|
78
|
+
const cmd = [bin, ...args].map(quoteShellArg).join(' ');
|
|
79
|
+
return spawnSync(cmd, { ...baseOpts, shell: true });
|
|
80
|
+
}
|
|
81
|
+
return spawnSync(bin, args, { ...baseOpts, shell: false });
|
|
82
|
+
},
|
|
83
|
+
/**
|
|
84
|
+
* 执行命令:默认实时输出(inherit);quiet=true 时捕获输出,成功静默、失败才打印
|
|
85
|
+
*/
|
|
86
|
+
run = (bin, args, opts = {}) => {
|
|
87
|
+
const { quiet, ...restOpts } = opts;
|
|
88
|
+
if (!quiet) {
|
|
89
|
+
const r = spawnCmd(bin, args, { stdio: 'inherit', ...restOpts });
|
|
90
|
+
if (r.status !== 0) throw new Error(`${bin} ${args.join(' ')} 退出码 ${r.status}`);
|
|
91
|
+
return r;
|
|
92
|
+
}
|
|
93
|
+
const r = spawnCmd(bin, args, { encoding: 'utf-8', ...restOpts });
|
|
94
|
+
if (r.status !== 0) {
|
|
95
|
+
if (r.stdout) process.stderr.write(r.stdout);
|
|
96
|
+
if (r.stderr) process.stderr.write(r.stderr);
|
|
97
|
+
throw new Error(`${bin} ${args.join(' ')} 退出码 ${r.status}`);
|
|
98
|
+
}
|
|
99
|
+
return r;
|
|
100
|
+
},
|
|
101
|
+
runQuiet = (bin, args, opts = {}) => spawnCmd(bin, args, { encoding: 'utf-8', ...opts }),
|
|
102
|
+
runSilent = (bin, args, opts = {}) => {
|
|
103
|
+
const r = spawnCmd(bin, args, { encoding: 'utf-8', ...opts });
|
|
104
|
+
if (r.status !== 0) {
|
|
105
|
+
if (r.stdout) process.stderr.write(r.stdout);
|
|
106
|
+
if (r.stderr) process.stderr.write(r.stderr);
|
|
107
|
+
throw new Error(`${bin} ${args.join(' ')} 退出码 ${r.status}`);
|
|
108
|
+
}
|
|
109
|
+
return r;
|
|
110
|
+
},
|
|
111
|
+
/**
|
|
112
|
+
* 定位 adb(配置 > 环境变量 > 默认路径 > PATH)
|
|
113
|
+
*/
|
|
114
|
+
findAdb = config => {
|
|
115
|
+
// 1. 用户显式配置的路径优先
|
|
116
|
+
const configured = config?.android?.adbPath;
|
|
117
|
+
if (configured) {
|
|
118
|
+
if (fs.existsSync(configured)) return configured;
|
|
119
|
+
console.warn('⚠️ config.android.adbPath 不存在,回退到自动查找: ' + configured);
|
|
120
|
+
}
|
|
121
|
+
// 2. 环境变量 ANDROID_HOME / ANDROID_SDK_ROOT
|
|
122
|
+
for (const env of ['ANDROID_HOME', 'ANDROID_SDK_ROOT']) {
|
|
123
|
+
const base = process.env[env];
|
|
124
|
+
if (base) {
|
|
125
|
+
const p = path.join(base, 'platform-tools', process.platform === 'win32' ? 'adb.exe' : 'adb');
|
|
126
|
+
if (fs.existsSync(p)) return p;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
// 3. 默认路径猜测(Windows / macOS / Linux)
|
|
130
|
+
const home = process.env.USERPROFILE || process.env.HOME || '', localAppData = process.env.LOCALAPPDATA || '',
|
|
131
|
+
candidates = [
|
|
132
|
+
path.join(localAppData, 'Android', 'Sdk', 'platform-tools', 'adb.exe'),
|
|
133
|
+
path.join(home, 'Library', 'Android', 'sdk', 'platform-tools', 'adb'),
|
|
134
|
+
path.join(home, 'Android', 'Sdk', 'platform-tools', 'adb')
|
|
135
|
+
];
|
|
136
|
+
for (const c of candidates) if (c && fs.existsSync(c)) return c;
|
|
137
|
+
return 'adb';
|
|
138
|
+
},
|
|
139
|
+
/**
|
|
140
|
+
* 定位 hermesc(Hermes 编译器,用于把 JS bundle 编成字节码)
|
|
141
|
+
*/
|
|
142
|
+
findHermesc = userProjectDir => {
|
|
143
|
+
const base = path.join(userProjectDir, 'node_modules', 'hermes-compiler', 'hermesc'), platform = process.platform;
|
|
144
|
+
let sub;
|
|
145
|
+
if (platform === 'win32') sub = ['win64-bin', 'hermesc.exe'];
|
|
146
|
+
else if (platform === 'darwin') sub = ['osx-bin', 'hermesc'];
|
|
147
|
+
else sub = ['linux64-bin', 'hermesc'];
|
|
148
|
+
const p = path.join(base, ...sub);
|
|
149
|
+
return fs.existsSync(p) ? p : null;
|
|
150
|
+
},
|
|
151
|
+
/**
|
|
152
|
+
* 文件名安全化:去除文件系统非法字符(/ \ : * ? " < > |)
|
|
153
|
+
* 空结果回退 'app'
|
|
154
|
+
*/
|
|
155
|
+
sanitizeFileName = s => {
|
|
156
|
+
const cleaned = String(s).replace(/[\\/:*?"<>|]/g, '_').trim();
|
|
157
|
+
return cleaned || 'app';
|
|
158
|
+
},
|
|
159
|
+
/**
|
|
160
|
+
* 生成 ESLint flat config 源码(替代 @react-native/eslint-config/flat)
|
|
161
|
+
*/
|
|
162
|
+
buildEslintConfigSource = () => `import js from '@eslint/js';
|
|
163
|
+
import tsParser from '@typescript-eslint/parser';
|
|
164
|
+
import tsPlugin from '@typescript-eslint/eslint-plugin';
|
|
165
|
+
import reactPlugin from '@eslint-react/eslint-plugin';
|
|
166
|
+
import reactHooks from 'eslint-plugin-react-hooks';
|
|
167
|
+
import prettier from 'eslint-config-prettier';
|
|
168
|
+
|
|
169
|
+
export default [
|
|
170
|
+
js.configs.recommended,
|
|
171
|
+
{
|
|
172
|
+
files: ['**/*.{js,jsx,ts,tsx}'],
|
|
173
|
+
languageOptions: {
|
|
174
|
+
parser: tsParser,
|
|
175
|
+
parserOptions: { ecmaFeatures: { jsx: true }, sourceType: 'module' },
|
|
176
|
+
},
|
|
177
|
+
plugins: { ...reactPlugin.configs.recommended.plugins, 'react-hooks': reactHooks },
|
|
178
|
+
settings: { ...reactPlugin.configs.recommended.settings },
|
|
179
|
+
rules: {
|
|
180
|
+
...reactPlugin.configs.recommended.rules,
|
|
181
|
+
...reactHooks.configs.recommended.rules,
|
|
182
|
+
},
|
|
183
|
+
},
|
|
184
|
+
{ files: ['**/*.{ts,tsx}'], plugins: { '@typescript-eslint': tsPlugin },
|
|
185
|
+
rules: tsPlugin.configs.recommended.rules }, prettier];`;
|
|
186
|
+
|
|
187
|
+
export {
|
|
188
|
+
ensureDir, removeDir, isExcluded, copyDir, readJson, writeJson, writeIfChanged, copyFileIfChanged, run, runQuiet,
|
|
189
|
+
runSilent, findAdb, findHermesc, sanitizeFileName, buildEslintConfigSource
|
|
177
190
|
};
|