@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 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.keystore` | `string` | — | 签名 keystore 路径(相对你的项目根);不填则用模板自带的 debug keystore |
246
+ | `signing` | `object` | — | 自定义签名配置。**不填则用模板自带的 debug keystore 签名**(可安装测试,不可上架);填则以下 4 项均为必填 |
247
+ | `signing.keystore` | `string` | — | keystore 路径(相对你的项目根) |
247
248
  | `signing.storePassword` | `string` | — | keystore 密码 |
248
- | `signing.keyAlias` | `string` | — | key 别名 |
249
- | `signing.keyPassword` | `string` | — | key 密码 |
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
 
@@ -81,12 +81,12 @@ export default {
81
81
  // keyAlias: 'androiddebugkey'
82
82
  // keyPassword: 'android'
83
83
  //
84
- // 自定义签名:取消下面注释,填入你的 keystore 信息
84
+ // 取消下方整块注释即视为「启用签名」,此时 4 项均为必填(缺项会在构建前报错)
85
85
  signing: {
86
- // keystore: './build/release.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.7",
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
- "@babel/eslint-parser": "^7.29.9",
58
- "@typescript-eslint/eslint-plugin": "^8.70.1",
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-plugin-react-native": "^5.0.0",
65
- "@flun/nodejs-mobile-react-native": "^2.0.5"
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
- const esc = keystoreAbs.replace(/\\/g, '/');
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 = spawnSync(bin, args, { stdio: 'inherit', shell: true, ...opts });
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 = {}) => spawnSync(bin, args, { encoding: 'utf-8', shell: true, ...opts }),
88
+ runQuiet = (bin, args, opts = {}) => spawnCmd(bin, args, { encoding: 'utf-8', ...opts }),
70
89
  runSilent = (bin, args, opts = {}) => {
71
- const r = spawnSync(bin, args, { encoding: 'utf-8', shell: true, ...opts });
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-react';
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: babelParser,
145
- parserOptions: { requireConfigFile: false, babelOptions: { presets: ['module:@react-native/babel-preset'] } },
161
+ parser: tsParser,
162
+ parserOptions: { ecmaFeatures: { jsx: true }, sourceType: 'module' },
146
163
  },
147
- plugins: { react: reactPlugin, 'react-hooks': reactHooks, 'react-native': reactNative },
148
- settings: { react: { version: 'detect' } },
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}'], languageOptions: { parser: tsParser }, plugins: { '@typescript-eslint': tsPlugin },
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