@flun/node-mobile-app 2.0.7 → 2.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.
- package/CHANGELOG.md +35 -0
- package/README.md +44 -3
- package/mobileAppConfig.js +5 -5
- package/package.json +5 -7
- package/src/patcher.js +21 -4
- package/src/utils.js +28 -13
- package/template/android/gradle.properties +3 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,4 +1,38 @@
|
|
|
1
1
|
# 变更日志
|
|
2
|
+
|
|
3
|
+
## [2.1.0] - 2026-09-29 10:39
|
|
4
|
+
|
|
5
|
+
### 变更
|
|
6
|
+
|
|
7
|
+
- **ESLint 插件换代**:`@babel/eslint-parser`、`eslint-plugin-react`、`eslint-plugin-react-native` 三个包的最新版均不支持 ESLint 10(分别止步于 9.0.0 / 9.7 / 9),导致安装 `eslint@10` 时被额外拉入一份 `eslint@9.39.5`(npm 的 peer 兼容回退),并触发 npm 的 deprecated 警告。现替换为:
|
|
8
|
+
|
|
9
|
+
| 原 | 新 |
|
|
10
|
+
| ---------------------------------------------------- | --------------------------------------------------- |
|
|
11
|
+
| `@babel/eslint-parser` | `@typescript-eslint/parser`(已支持 ESLint 10) |
|
|
12
|
+
| `eslint-plugin-react` + `eslint-plugin-react-native` | `@eslint-react/eslint-plugin`(原生支持 ESLint 10) |
|
|
13
|
+
|
|
14
|
+
生成的 `eslint.config.js` 随之更新。`eslint-plugin-react-hooks` 保留(其 7.1.x 已支持 ESLint 10)。
|
|
15
|
+
|
|
16
|
+
> **注意**:lint 规则集与原先不同,升级后对同一份代码可能报出不同的检查结果。这只影响静态检查,不影响构建产物与运行。
|
|
17
|
+
|
|
18
|
+
### 修复
|
|
19
|
+
|
|
20
|
+
- **`DEP0190` 弃用警告**:`src/utils.js` 中的 `run` / `runQuiet` / `runSilent` 原先统一用 `spawnSync(bin, args, { shell: true })`。Node 20+ 对「参数数组 + shell」这种组合会提示参数未转义(`DEP0190`),每次执行 CLI 都会在输出末尾出现该警告。现改为:
|
|
21
|
+
- Windows:把命令与参数拼成单个字符串(参数按需加引号)后交给 shell——`.cmd` / `.bat` 仍能执行,且不再触发警告;
|
|
22
|
+
- 其它平台:不使用 shell,参数交由 Node 转义。
|
|
23
|
+
## [2.0.8] - 2026-09-28 22:17
|
|
24
|
+
### 修复
|
|
25
|
+
|
|
26
|
+
- **中文路径构建失败**:三处联动修复。
|
|
27
|
+
- **AGP 拒绝非 ASCII 路径**:`template/android/gradle.properties` 增加 `android.overridePathCheck=true`,允许中文路径构建。
|
|
28
|
+
- **`gradle.properties` 中文乱码**:该文件按 ISO-8859-1 读取,非 ASCII 字符(如路径中的「桌面」)会被读成乱码,导致 keystore 找不到。`patcher.js` 现在把写入的非 ASCII 字符转成 `\uXXXX` 转义(Java Properties 规范)。
|
|
29
|
+
- 影响范围:项目路径含中文(或其它非 ASCII 字符)时,release 构建曾失败于 `Your project path contains non-ASCII characters` 与 `Keystore file ... not found`。
|
|
30
|
+
- **签名配置缺项时报错含糊**:`android.signing.keystore` 已配置、但密码 / 别名 / 别名密码为空时,原先会一路跑到 Gradle 才报 `keystore password was incorrect`,容易误导排查方向。现在 `patcher.js` 在构建前即校验四项,缺项时抛出明确错误,并提示「仅本地测试可注释掉整个 signing 块」。
|
|
31
|
+
|
|
32
|
+
### 变更
|
|
33
|
+
|
|
34
|
+
- **`mobileAppConfig.js` 模板**:`signing` 块内每项标注「必填」,并说明「取消整块注释即视为启用签名」。
|
|
35
|
+
|
|
2
36
|
## [2.0.7] - 2026-09-28 16:05
|
|
3
37
|
### 更新
|
|
4
38
|
- 更新有小问题依赖-> `@flun/nodejs-mobile-react-native` ;
|
|
@@ -28,6 +62,7 @@
|
|
|
28
62
|
- 更新 `@flun/nodejs-mobile-react-native` 依赖;
|
|
29
63
|
|
|
30
64
|
---
|
|
65
|
+
|
|
31
66
|
## [2.0.2] - 2026-09-23 20:25
|
|
32
67
|
|
|
33
68
|
### 修复
|
package/README.md
CHANGED
|
@@ -243,10 +243,51 @@ adb install -r ./out/<appName>-<versionName>-debug.apk
|
|
|
243
243
|
| `icon` | `string` | `'./build/icon.png'` | 应用图标(建议 512×512 PNG) |
|
|
244
244
|
| `usesCleartextTraffic` | `boolean` | `true` | 是否允许明文 HTTP |
|
|
245
245
|
| `permissions` | `string[]` | `[]` | 额外权限(如 `'android.permission.CAMERA'`) |
|
|
246
|
-
| `signing
|
|
246
|
+
| `signing` | `object` | — | 自定义签名配置。**不填则用模板自带的 debug keystore 签名**(可安装测试,不可上架);填则以下 4 项均为必填 |
|
|
247
|
+
| `signing.keystore` | `string` | — | keystore 路径(相对你的项目根) |
|
|
247
248
|
| `signing.storePassword` | `string` | — | keystore 密码 |
|
|
248
|
-
| `signing.keyAlias` | `string` | — |
|
|
249
|
-
| `signing.keyPassword` | `string` | — |
|
|
249
|
+
| `signing.keyAlias` | `string` | — | 密钥别名 |
|
|
250
|
+
| `signing.keyPassword` | `string` | — | 密钥密码(通常与 `storePassword` 相同) |
|
|
251
|
+
|
|
252
|
+
#### 生成自签名 keystore
|
|
253
|
+
|
|
254
|
+
如需自定义签名(上架或长期使用),先生成 keystore 文件。`keytool` 随 JDK 提供:
|
|
255
|
+
|
|
256
|
+
- 若已设 `JAVA_HOME`:`& "$env:JAVA_HOME\bin\keytool.exe"`
|
|
257
|
+
- 否则用 JDK 安装目录下的完整路径,例如 `C:\Program Files\Java\jdk-17.0.20\bin\keytool.exe`
|
|
258
|
+
|
|
259
|
+
```powershell
|
|
260
|
+
# Windows PowerShell
|
|
261
|
+
& "<JDK 安装目录>\bin\keytool.exe" -genkeypair -v `
|
|
262
|
+
-keystore "<你的项目根>\build\release.keystore" `
|
|
263
|
+
-alias testkey `
|
|
264
|
+
-keyalg RSA -keysize 2048 -validity 10000 `
|
|
265
|
+
-storepass <keystore 密码> -keypass <密钥密码> `
|
|
266
|
+
-dname "CN=Test, OU=Dev, O=Test, L=City, S=State, C=CN"
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
```bash
|
|
270
|
+
# macOS / Linux
|
|
271
|
+
keytool -genkeypair -v \
|
|
272
|
+
-keystore "<你的项目根>/build/release.keystore" \
|
|
273
|
+
-alias testkey \
|
|
274
|
+
-keyalg RSA -keysize 2048 -validity 10000 \
|
|
275
|
+
-storepass <keystore 密码> -keypass <密钥密码> \
|
|
276
|
+
-dname "CN=Test, OU=Dev, O=Test, L=City, S=State, C=CN"
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
生成后,把路径、密码、别名填入 `mobileAppConfig.js` 的 `android.signing`:
|
|
280
|
+
|
|
281
|
+
```js
|
|
282
|
+
signing: {
|
|
283
|
+
keystore: './build/release.keystore', // 相对你的项目根
|
|
284
|
+
storePassword: '<keystore 密码>',
|
|
285
|
+
keyAlias: 'testkey',
|
|
286
|
+
keyPassword: '<密钥密码>',
|
|
287
|
+
},
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
> **提示**:`storepass` / `keypass` 可填相同值,对应配置里 `storePassword` / `keyPassword` 也填相同值。四项缺一不可,缺项时构建前会报错。
|
|
250
291
|
|
|
251
292
|
### iOS
|
|
252
293
|
|
package/mobileAppConfig.js
CHANGED
|
@@ -81,12 +81,12 @@ export default {
|
|
|
81
81
|
// keyAlias: 'androiddebugkey'
|
|
82
82
|
// keyPassword: 'android'
|
|
83
83
|
//
|
|
84
|
-
//
|
|
84
|
+
// 取消下方整块注释即视为「启用签名」,此时 4 项均为必填(缺项会在构建前报错)
|
|
85
85
|
signing: {
|
|
86
|
-
// keystore:
|
|
87
|
-
// storePassword: '',
|
|
88
|
-
// keyAlias:
|
|
89
|
-
// keyPassword:
|
|
86
|
+
// keystore: './build/release.keystore', // 必填:keystore 路径(相对用户项目根)
|
|
87
|
+
// storePassword: '', // 必填:keystore 密码
|
|
88
|
+
// keyAlias: '', // 必填:密钥别名
|
|
89
|
+
// keyPassword: '', // 必填:密钥密码(与 storePassword 相同可填同值)
|
|
90
90
|
},
|
|
91
91
|
},
|
|
92
92
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@flun/node-mobile-app",
|
|
3
|
-
"version": "2.0
|
|
3
|
+
"version": "2.1.0",
|
|
4
4
|
"description": "将 Node.js 项目一键打包为 Android / iOS 移动应用,运行时由 @flun/nodejs-mobile-react-native 提供,无需修改你的 Node 代码;",
|
|
5
5
|
"author": "flun <open@flun.top>",
|
|
6
6
|
"keywords": [
|
|
@@ -54,15 +54,13 @@
|
|
|
54
54
|
"prettier": "^3.9.8",
|
|
55
55
|
"eslint": "^10.11.0",
|
|
56
56
|
"@eslint/js": "^10.0.1",
|
|
57
|
-
"@
|
|
58
|
-
"@typescript-eslint/
|
|
59
|
-
"@typescript-eslint/parser": "^8.70.1",
|
|
57
|
+
"@typescript-eslint/eslint-plugin": "^8.71.0",
|
|
58
|
+
"@typescript-eslint/parser": "^8.71.0",
|
|
60
59
|
"eslint-config-prettier": "^10.1.8",
|
|
61
60
|
"eslint-plugin-eslint-comments": "^3.2.0",
|
|
62
|
-
"eslint-plugin-react": "^7.37.5",
|
|
63
61
|
"eslint-plugin-react-hooks": "^7.1.1",
|
|
64
|
-
"eslint-
|
|
65
|
-
"@flun/nodejs-mobile-react-native": "^2.0.
|
|
62
|
+
"@eslint-react/eslint-plugin": "^5.21.3",
|
|
63
|
+
"@flun/nodejs-mobile-react-native": "^2.0.6"
|
|
66
64
|
},
|
|
67
65
|
"allowScripts": {
|
|
68
66
|
"node": true,
|
package/src/patcher.js
CHANGED
|
@@ -2,6 +2,11 @@ import path from 'path';
|
|
|
2
2
|
import fs from 'fs';
|
|
3
3
|
import { writeIfChanged } from './utils.js';
|
|
4
4
|
|
|
5
|
+
// 把字符串中所有非 ASCII 字符转成 \uXXXX(Java Properties 规范)
|
|
6
|
+
// gradle.properties 按 ISO-8859-1 读取,无法改为 UTF-8
|
|
7
|
+
const escapeProps = (s) =>
|
|
8
|
+
String(s).replace(/[^\x00-\x7F]/g, (ch) => '\\u' + ch.charCodeAt(0).toString(16).padStart(4, '0'));
|
|
9
|
+
|
|
5
10
|
/**
|
|
6
11
|
* 解析最终 appName:
|
|
7
12
|
* 1. config.appName
|
|
@@ -160,6 +165,16 @@ const resolveAppName = (userProjectDir, config) => {
|
|
|
160
165
|
const signing = config.android && config.android.signing;
|
|
161
166
|
if (!signing || !signing.keystore) return; // 未配置签名 → 保持默认
|
|
162
167
|
|
|
168
|
+
// 启用签名后 4 项均为必填;缺项时提前报错,避免 Gradle 报"password incorrect"误导排查
|
|
169
|
+
const _missing = ['storePassword', 'keyAlias', 'keyPassword'].filter((k) => !signing[k]);
|
|
170
|
+
if (_missing.length > 0) {
|
|
171
|
+
throw new Error(
|
|
172
|
+
'android.signing.keystore 已配置,但以下字段为空:' + _missing.join(', ') + '\n' +
|
|
173
|
+
' 启用签名后,keystore / storePassword / keyAlias / keyPassword 四项均为必填。\n' +
|
|
174
|
+
' 如仅需本地测试(不签名上架),可注释掉整个 signing 块——模板默认用 debug keystore 签名。'
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
|
|
163
178
|
const keystoreAbs = path.resolve(userProjectDir, signing.keystore);
|
|
164
179
|
if (!fs.existsSync(keystoreAbs)) return console.warn(' ⚠️ 签名 keystore 不存在,跳过签名配置:', keystoreAbs);
|
|
165
180
|
|
|
@@ -173,11 +188,13 @@ const resolveAppName = (userProjectDir, config) => {
|
|
|
173
188
|
c = c.replace(/^\s*MYAPP_RELEASE_KEY_ALIAS=.*\r?\n/gm, '');
|
|
174
189
|
c = c.replace(/^\s*MYAPP_RELEASE_KEY_PASSWORD=.*\r?\n/gm, '');
|
|
175
190
|
// 追加新的
|
|
176
|
-
|
|
191
|
+
// gradle.properties 按 ISO-8859-1 读取,非 ASCII 字符必须转 \uXXXX 转义,
|
|
192
|
+
// 否则中文路径(如「桌面」)会被读成乱码,导致 keystore 找不到。
|
|
193
|
+
const esc = escapeProps(keystoreAbs.replace(/\\/g, '/'));
|
|
177
194
|
c += `\nMYAPP_RELEASE_STORE_FILE=${esc}\n`;
|
|
178
|
-
c += `MYAPP_RELEASE_STORE_PASSWORD=${signing.storePassword || ''}\n`;
|
|
179
|
-
c += `MYAPP_RELEASE_KEY_ALIAS=${signing.keyAlias || ''}\n`;
|
|
180
|
-
c += `MYAPP_RELEASE_KEY_PASSWORD=${signing.keyPassword || ''}\n`;
|
|
195
|
+
c += `MYAPP_RELEASE_STORE_PASSWORD=${escapeProps(signing.storePassword || '')}\n`;
|
|
196
|
+
c += `MYAPP_RELEASE_KEY_ALIAS=${escapeProps(signing.keyAlias || '')}\n`;
|
|
197
|
+
c += `MYAPP_RELEASE_KEY_PASSWORD=${escapeProps(signing.keyPassword || '')}\n`;
|
|
181
198
|
writeIfChanged(propsFile, c);
|
|
182
199
|
}
|
|
183
200
|
|
package/src/utils.js
CHANGED
|
@@ -61,14 +61,33 @@ const ensureDir = dir => {
|
|
|
61
61
|
fs.copyFileSync(src, dst);
|
|
62
62
|
return true;
|
|
63
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
|
+
},
|
|
64
83
|
run = (bin, args, opts = {}) => {
|
|
65
|
-
const r =
|
|
84
|
+
const r = spawnCmd(bin, args, { stdio: 'inherit', ...opts });
|
|
66
85
|
if (r.status !== 0) throw new Error(`${bin} ${args.join(' ')} 退出码 ${r.status}`);
|
|
67
86
|
return r;
|
|
68
87
|
},
|
|
69
|
-
runQuiet = (bin, args, opts = {}) =>
|
|
88
|
+
runQuiet = (bin, args, opts = {}) => spawnCmd(bin, args, { encoding: 'utf-8', ...opts }),
|
|
70
89
|
runSilent = (bin, args, opts = {}) => {
|
|
71
|
-
const r =
|
|
90
|
+
const r = spawnCmd(bin, args, { encoding: 'utf-8', ...opts });
|
|
72
91
|
if (r.status !== 0) {
|
|
73
92
|
if (r.stdout) process.stderr.write(r.stdout);
|
|
74
93
|
if (r.stderr) process.stderr.write(r.stderr);
|
|
@@ -128,12 +147,10 @@ const ensureDir = dir => {
|
|
|
128
147
|
* 生成 ESLint flat config 源码(替代 @react-native/eslint-config/flat)
|
|
129
148
|
*/
|
|
130
149
|
buildEslintConfigSource = () => `import js from '@eslint/js';
|
|
131
|
-
import babelParser from '@babel/eslint-parser';
|
|
132
150
|
import tsParser from '@typescript-eslint/parser';
|
|
133
151
|
import tsPlugin from '@typescript-eslint/eslint-plugin';
|
|
134
|
-
import reactPlugin from 'eslint-plugin
|
|
152
|
+
import reactPlugin from '@eslint-react/eslint-plugin';
|
|
135
153
|
import reactHooks from 'eslint-plugin-react-hooks';
|
|
136
|
-
import reactNative from 'eslint-plugin-react-native';
|
|
137
154
|
import prettier from 'eslint-config-prettier';
|
|
138
155
|
|
|
139
156
|
export default [
|
|
@@ -141,19 +158,17 @@ const ensureDir = dir => {
|
|
|
141
158
|
{
|
|
142
159
|
files: ['**/*.{js,jsx,ts,tsx}'],
|
|
143
160
|
languageOptions: {
|
|
144
|
-
parser:
|
|
145
|
-
parserOptions: {
|
|
161
|
+
parser: tsParser,
|
|
162
|
+
parserOptions: { ecmaFeatures: { jsx: true }, sourceType: 'module' },
|
|
146
163
|
},
|
|
147
|
-
plugins: {
|
|
148
|
-
settings: {
|
|
164
|
+
plugins: { ...reactPlugin.configs.recommended.plugins, 'react-hooks': reactHooks },
|
|
165
|
+
settings: { ...reactPlugin.configs.recommended.settings },
|
|
149
166
|
rules: {
|
|
150
167
|
...reactPlugin.configs.recommended.rules,
|
|
151
168
|
...reactHooks.configs.recommended.rules,
|
|
152
|
-
'react/react-in-jsx-scope': 'off',
|
|
153
|
-
'react/prop-types': 'off',
|
|
154
169
|
},
|
|
155
170
|
},
|
|
156
|
-
{ files: ['**/*.{ts,tsx}'],
|
|
171
|
+
{ files: ['**/*.{ts,tsx}'], plugins: { '@typescript-eslint': tsPlugin },
|
|
157
172
|
rules: tsPlugin.configs.recommended.rules }, prettier];`;
|
|
158
173
|
|
|
159
174
|
export {
|
|
@@ -46,4 +46,6 @@ edgeToEdgeEnabled=true
|
|
|
46
46
|
# Opt out of built-in kotlin and new DSL behavior that ships with AGP 9.
|
|
47
47
|
# Starting from AGP 10.x these opt outs will be removed.
|
|
48
48
|
android.builtInKotlin=false
|
|
49
|
-
android.newDsl=false
|
|
49
|
+
android.newDsl=false
|
|
50
|
+
# 允许非 ASCII 路径(如中文路径)构建;AGP 默认拒绝
|
|
51
|
+
android.overridePathCheck=true
|