@flun/node-mobile-app 1.0.1 → 1.0.2

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 ADDED
@@ -0,0 +1,14 @@
1
+ ## [1.0.2] - 2026-09-21 08:24
2
+
3
+ ### 修复
4
+ - **依赖树瘦身**:删除主包里未使用的 6 个依赖(`@babel/preset-env`、`@babel/runtime`、`@react-native/jest-preset`、`@react-native/new-app-screen`、`@react-native/typescript-config`、`react-native-safe-area-context`),安装体积与安装耗时下降
5
+ - **消除旧依赖链**:移除 `@react-native/eslint-config`,改为内置自写 ESLint flat config(`src/utils.js` 的 `buildEslintConfigSource()`,构建时覆写到 `node-mobile-app-build/eslint.config.js`)。随之消失的旧包:`inflight@1.0.6`、`rimraf@3.0.2`、`glob@7.2.3`、`@humanwhocodes/config-array`、`@humanwhocodes/object-schema`
6
+ - **ESLint 升级到 9**:统一使用 `eslint@^9`,不再出现 eslint 8 / 9 两份并存
7
+ - **模板 lint 清理**:修复 `template/App.tsx` 中逗号表达式触发的 `@typescript-eslint/no-unused-expressions` 报错,模板现在默认 `npx eslint` 零 error 零 warning
8
+
9
+ ### 文档
10
+ - `mobileAppConfig.js` 与 `README.md` 更正签名说明:`build --release` **不配置 `android.signing` 也能出包**,默认沿用模板自带的 debug keystore 签名(可安装、可本地测试,不可上架);需要上架时再填 `keystore` / `storePassword` / `keyAlias` / `keyPassword`
11
+
12
+ ## [1.0.1] - 2026-09-20 11:52
13
+ ### 首发
14
+ - 发布将 Node.js 项目一键打包为 Android / iOS 移动应用(基于 nodejs-mobile-react-native);
package/README.md CHANGED
@@ -1,4 +1,3 @@
1
- 直接给你 README 全文,复制到 `D:\Extend_npm\node-mobile-app\README.md` 即可。
2
1
  # @flun/node-mobile-app
3
2
 
4
3
  将 Node.js 项目一键打包为 Android / iOS 移动应用。基于 [nodejs-mobile-react-native](https://github.com/nodejs-mobile/nodejs-mobile-react-native)。
@@ -152,14 +151,14 @@ npx node-mobile-app build --release
152
151
 
153
152
  ## 命令说明
154
153
 
155
- | 命令 | 用途 |
156
- | ------------------------------------- | -------------------------------------------------- |
157
- | `npx node-mobile-app test` | 编译并安装到设备(**需连 USB**,走 Metro 热更新) |
158
- | `npx node-mobile-app test --ios` | iOS 版(仅 macOS) |
159
- | `npx node-mobile-app build` | 打 debug APK(**离线可用**) |
160
- | `npx node-mobile-app build --release` | 打 release APK(发布用,需配置 `android.signing`) |
161
- | `npx node-mobile-app build --ios` | 打 IPA(仅 macOS) |
162
- | `npx node-mobile-app clean` | 清理构建缓存 |
154
+ | 命令 | 用途 |
155
+ | ------------------------------------- | ---------------------------------------------------------------------------------- |
156
+ | `npx node-mobile-app test` | 编译并安装到设备(**需连 USB**,走 Metro 热更新) |
157
+ | `npx node-mobile-app test --ios` | iOS 版(仅 macOS) |
158
+ | `npx node-mobile-app build` | 打 debug APK(**离线可用**) |
159
+ | `npx node-mobile-app build --release` | 打 release APK(默认用 debug keystore 签名,可安装;上架需配置 `android.signing`) |
160
+ | `npx node-mobile-app build --ios` | 打 IPA(仅 macOS) |
161
+ | `npx node-mobile-app clean` | 清理构建缓存 |
163
162
 
164
163
  ### 常用选项
165
164
 
@@ -215,47 +214,47 @@ adb install -r ./out/<appName>-<versionName>-debug.apk
215
214
 
216
215
  ### WebView
217
216
 
218
- | 字段 | 类型 | 默认 | 说明 |
219
- | --------------------- | --------- | ------------------- | -------------------------------------------------- |
220
- | `showLoading` | `boolean` | `true` | 是否显示加载指示器 |
221
- | `loadingText` | `string` | `'正在启动服务...'` | 加载提示文字 |
222
- | `enableJavaScript` | `boolean` | `true` | 是否启用 JavaScript |
223
- | `enableDomStorage` | `boolean` | `true` | 是否启用 DOM Storage |
224
- | `allowFileAccess` | `boolean` | `true` | 是否允许访问本地文件 |
225
- | `mixedContentMode` | `string` | `'always'` | 混合内容模式:`never` / `always` / `compatibility` |
217
+ | 字段 | 类型 | 默认 | 说明 |
218
+ | --------------------- | --------- | ------------------- | ------------------------------------------------------------------------------------------- |
219
+ | `showLoading` | `boolean` | `true` | 是否显示加载指示器 |
220
+ | `loadingText` | `string` | `'正在启动服务...'` | 加载提示文字 |
221
+ | `enableJavaScript` | `boolean` | `true` | 是否启用 JavaScript |
222
+ | `enableDomStorage` | `boolean` | `true` | 是否启用 DOM Storage |
223
+ | `allowFileAccess` | `boolean` | `true` | 是否允许访问本地文件 |
224
+ | `mixedContentMode` | `string` | `'always'` | 混合内容模式:`never` / `always` / `compatibility` |
226
225
  | `backgroundColor` | 见下 | `'#ffffff'` | WebView 背景色。支持:固定色 `'#ffffff'` / 跟随系统 `'system'` / 深浅两值 `{ light, dark }` |
227
- | `allowSelfSignedCert` | `boolean` | `false` | 见下文「HTTPS 自签证书」 |
226
+ | `allowSelfSignedCert` | `boolean` | `false` | 见下文「HTTPS 自签证书」 |
228
227
 
229
228
  ### Android
230
229
 
231
- | 字段 | 类型 | 默认 | 说明 |
232
- | ----------------------- | ---------------- | -------------------- | ----------------------------------------------------- |
233
- | `sdkPath` | `string \| null` | `null` | Android SDK 路径(默认自动查找) |
234
- | `adbPath` | `string \| null` | `null` | adb 路径(默认自动查找) |
235
- | `minSdkVersion` | `int` | `29` | 最低 SDK(29=Android 10.0) |
236
- | `targetSdkVersion` | `int` | `36` | 目标 SDK |
237
- | `compileSdkVersion` | `int` | `37` | 编译 SDK |
238
- | `buildToolsVersion` | `string` | `'37.0.0'` | 构建工具版本 |
239
- | `abiFilters` | `string[]` | `['arm64-v8a']` | 打包架构(真机 `arm64-v8a`,模拟器加 `x86_64`) |
240
- | `icon` | `string` | `'./build/icon.png'` | 应用图标(建议 512×512 PNG) |
241
- | `usesCleartextTraffic` | `boolean` | `true` | 是否允许明文 HTTP |
242
- | `permissions` | `string[]` | `[]` | 额外权限(如 `'android.permission.CAMERA'`) |
243
- | `signing.keystore` | `string` | — | 签名 keystore 路径(相对你的项目根) |
244
- | `signing.storePassword` | `string` | — | keystore 密码 |
245
- | `signing.keyAlias` | `string` | — | key 别名 |
246
- | `signing.keyPassword` | `string` | — | key 密码 |
247
- | `buildNativeModules` | `boolean` | `false` | 是否在 `nodejs-project` 里编译原生模块(`bcrypt` 等) |
230
+ | 字段 | 类型 | 默认 | 说明 |
231
+ | ----------------------- | ---------------- | -------------------- | ----------------------------------------------------------------------- |
232
+ | `sdkPath` | `string \| null` | `null` | Android SDK 路径(默认自动查找) |
233
+ | `adbPath` | `string \| null` | `null` | adb 路径(默认自动查找) |
234
+ | `minSdkVersion` | `int` | `29` | 最低 SDK(29=Android 10.0) |
235
+ | `targetSdkVersion` | `int` | `36` | 目标 SDK |
236
+ | `compileSdkVersion` | `int` | `37` | 编译 SDK |
237
+ | `buildToolsVersion` | `string` | `'37.0.0'` | 构建工具版本 |
238
+ | `abiFilters` | `string[]` | `['arm64-v8a']` | 打包架构(真机 `arm64-v8a`,模拟器加 `x86_64`) |
239
+ | `icon` | `string` | `'./build/icon.png'` | 应用图标(建议 512×512 PNG) |
240
+ | `usesCleartextTraffic` | `boolean` | `true` | 是否允许明文 HTTP |
241
+ | `permissions` | `string[]` | `[]` | 额外权限(如 `'android.permission.CAMERA'`) |
242
+ | `signing.keystore` | `string` | — | 签名 keystore 路径(相对你的项目根)。不填则用模板自带的 debug keystore |
243
+ | `signing.storePassword` | `string` | — | keystore 密码 |
244
+ | `signing.keyAlias` | `string` | — | key 别名 |
245
+ | `signing.keyPassword` | `string` | — | key 密码 |
246
+ | `buildNativeModules` | `boolean` | `false` | 是否在 `nodejs-project` 里编译原生模块(`bcrypt` 等) |
248
247
 
249
248
  ### iOS
250
249
 
251
- | 字段 | 类型 | 默认 | 说明 |
252
- | ----------------------------- | -------- | -------------------- | ------------------------------ |
253
- | `deploymentTarget` | `string` | `'15.1'` | 最低 iOS 版本 |
254
- | `icon` | `string` | `'./build/icon.png'` | 应用图标(建议 1024×1024 PNG) |
255
- | `signing.teamId` | `string` | — | Apple Developer Team ID |
256
- | `signing.provisioningProfile` | `string` | — | 描述文件路径(写入 `exportOptions.plist`) |
257
- | `signing.certificate` | `string` | — | P12 证书路径(导入临时 keychain 供 xcodebuild 签名用) |
258
- | `signing.certificatePassword` | `string` | — | P12 密码 |
250
+ | 字段 | 类型 | 默认 | 说明 |
251
+ | ----------------------------- | -------- | -------------------- | ------------------------------------------------------ |
252
+ | `deploymentTarget` | `string` | `'15.1'` | 最低 iOS 版本 |
253
+ | `icon` | `string` | `'./build/icon.png'` | 应用图标(建议 1024×1024 PNG) |
254
+ | `signing.teamId` | `string` | — | Apple Developer Team ID |
255
+ | `signing.provisioningProfile` | `string` | — | 描述文件路径(写入 `exportOptions.plist`) |
256
+ | `signing.certificate` | `string` | — | P12 证书路径(导入临时 keychain 供 xcodebuild 签名用) |
257
+ | `signing.certificatePassword` | `string` | — | P12 密码 |
259
258
 
260
259
  ### 高级
261
260
 
@@ -8,7 +8,7 @@
8
8
  * npx node-mobile-app test 编译并安装到设备(需连 USB,走 Metro 热更新)
9
9
  * npx node-mobile-app test --ios iOS 版(仅 macOS)
10
10
  * npx node-mobile-app build 打 debug APK(离线可用)
11
- * npx node-mobile-app build --release 打 release APK(发布用,需先配置 android.signing)
11
+ * npx node-mobile-app build --release 打 release APK(默认 debug keystore 签名,可安装;上架需配 android.signing)
12
12
  * npx node-mobile-app build --ios 打 IPA(仅 macOS)
13
13
  * npx node-mobile-app clean 清理构建缓存
14
14
  *
@@ -70,7 +70,19 @@ export default {
70
70
  // 'android.permission.CAMERA',
71
71
  // 'android.permission.READ_EXTERNAL_STORAGE',
72
72
  ],
73
- // 签名配置(发布 Release 版本时必填)
73
+ // 签名配置(可选)
74
+ //
75
+ // 不配置也能打 release:模板默认用 debug keystore 签名(可安装、可本地测试,不可上架)。
76
+ // 需要上架时再配置下面任一种:
77
+ //
78
+ // 方式一:Android SDK 自带 debug keystore(本地测试常用)
79
+ // keystore: %USERPROFILE%\.android\debug.keystore (Windows)
80
+ // ~/.android/debug.keystore (macOS/Linux)
81
+ // storePassword: 'android'
82
+ // keyAlias: 'androiddebugkey'
83
+ // keyPassword: 'android'
84
+ //
85
+ // 自定义签名:取消下面注释,填入你的 keystore 信息
74
86
  signing: {
75
87
  // keystore: './build/release.keystore',
76
88
  // storePassword: '',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flun/node-mobile-app",
3
- "version": "1.0.1",
3
+ "version": "1.0.2",
4
4
  "description": "将 Node.js 项目一键打包为 Android / iOS 移动应用(基于 nodejs-mobile-react-native)",
5
5
  "author": "flun <open@flun.top>",
6
6
  "keywords": [
@@ -28,11 +28,12 @@
28
28
  "scripts/",
29
29
  "src/",
30
30
  "template/",
31
- "LICENSE",
32
- "mobileAppConfig.js",
33
- "README.md",
34
31
  "自定义libnode指南.md",
35
32
  "Android 构建指南.md",
33
+ "CHANGELOG.md",
34
+ "README.md",
35
+ "LICENSE",
36
+ "mobileAppConfig.js",
36
37
  "package.json"
37
38
  ],
38
39
  "scripts": {
@@ -40,28 +41,28 @@
40
41
  },
41
42
  "dependencies": {
42
43
  "@babel/core": "^8.0.6",
43
- "@babel/preset-env": "^8.0.6",
44
- "@babel/runtime": "^8.0.5",
45
44
  "@react-native-community/cli": "^20.2.0",
46
45
  "@react-native-community/cli-platform-android": "^20.2.0",
47
46
  "@react-native-community/cli-platform-ios": "^20.2.0",
48
47
  "@react-native/babel-preset": "^0.87.1",
49
- "@react-native/eslint-config": "^0.87.1",
50
- "@react-native/jest-preset": "^0.87.1",
51
48
  "@react-native/metro-config": "^0.87.1",
52
- "@react-native/new-app-screen": "^0.87.1",
53
- "@react-native/typescript-config": "^0.87.1",
54
- "jimp": "^1.6.0",
49
+ "jimp": "^1.6.1",
55
50
  "nodejs-mobile-react-native": "^18.20.4",
56
51
  "react": "^19.3.0",
57
52
  "react-native": "^0.87.1",
58
- "react-native-safe-area-context": "^5.10.0",
59
- "react-native-webview": "^14.0.1"
60
- },
61
- "devDependencies": {
53
+ "react-native-webview": "^14.0.1",
62
54
  "@types/react": "^19.3.0",
63
55
  "prettier": "^3.9.8",
64
- "typescript": "^7.0.2"
56
+ "eslint": "^10.11.0",
57
+ "@eslint/js": "^10.0.1",
58
+ "@babel/eslint-parser": "^7.29.9",
59
+ "@typescript-eslint/eslint-plugin": "^8.70.0",
60
+ "@typescript-eslint/parser": "^8.70.0",
61
+ "eslint-config-prettier": "^10.1.8",
62
+ "eslint-plugin-eslint-comments": "^3.2.0",
63
+ "eslint-plugin-react": "^7.37.5",
64
+ "eslint-plugin-react-hooks": "^7.1.1",
65
+ "eslint-plugin-react-native": "^5.0.0"
65
66
  },
66
67
  "allowScripts": {
67
68
  "node": true,
package/src/copy.js CHANGED
@@ -1,6 +1,8 @@
1
1
  import path from 'path';
2
2
  import fs from 'fs';
3
- import { ensureDir, removeDir, readJson, isExcluded, copyFileIfChanged, writeIfChanged } from './utils.js';
3
+ import {
4
+ ensureDir, removeDir, readJson, isExcluded, copyFileIfChanged, writeIfChanged, buildEslintConfigSource
5
+ } from './utils.js';
4
6
  import { resolveSdkPath } from './config.js';
5
7
 
6
8
  const BUILD_DIR_NAME = 'node-mobile-app-build',
@@ -66,6 +68,7 @@ const BUILD_DIR_NAME = 'node-mobile-app-build',
66
68
 
67
69
  const keepSet = new Set((config && config.keepFiles) || []);
68
70
  copyDirOverwrite(templateDir, buildDir, TEMPLATE_EXCLUDE, buildDir, keepSet);
71
+ fs.writeFileSync(path.join(buildDir, 'eslint.config.js'), buildEslintConfigSource(), 'utf-8');
69
72
  console.log(' ✓ 模板已更新到', BUILD_DIR_NAME + '/');
70
73
  if (keepSet.size > 0) console.log(' 保留文件:', Array.from(keepSet).join(', '));
71
74
  },
@@ -121,8 +124,7 @@ const BUILD_DIR_NAME = 'node-mobile-app-build',
121
124
  const fallback = path.join(pkgRoot, 'cli-assets', 'server.js');
122
125
  if (!fs.existsSync(fallback)) throw new Error('入口文件不存在: ' + config.serverPath + '(CLI 内置模板也缺失)');
123
126
 
124
- ensureDir(path.dirname(entryPath));
125
- fs.copyFileSync(fallback, entryPath);
127
+ ensureDir(path.dirname(entryPath)), fs.copyFileSync(fallback, entryPath);
126
128
  console.log(' ✓ 生成默认入口文件:', config.serverPath);
127
129
  console.log(' (因为用户项目里没有这个文件,从 CLI 内置模板复制)');
128
130
 
@@ -161,17 +163,10 @@ const BUILD_DIR_NAME = 'node-mobile-app-build',
161
163
  const before = c;
162
164
 
163
165
  // defaultConfig 里的 ndk { abiFilters ... }
164
- c = c.replace(
165
- /(defaultConfig\s*\{[\s\S]*?ndk\s*\{\s*abiFilters\s+)("[^"]*"(?:\s*,\s*"[^"]*")*)/,
166
- `$1${abiList}`
167
- );
166
+ c = c.replace(/(defaultConfig\s*\{[\s\S]*?ndk\s*\{\s*abiFilters\s+)("[^"]*"(?:\s*,\s*"[^"]*")*)/, `$1${abiList}`);
168
167
  // 兜底:没有 ndk {} 块时,插到 defaultConfig 里
169
- if (!/ndk\s*\{\s*abiFilters/.test(c)) {
170
- c = c.replace(
171
- /(defaultConfig\s*\{)/,
172
- `$1\n ndk {\n abiFilters ${abiList}\n }`
173
- );
174
- }
168
+ if (!/ndk\s*\{\s*abiFilters/.test(c))
169
+ c = c.replace(/(defaultConfig\s*\{)/, `$1\n ndk {\n abiFilters ${abiList}\n }`);
175
170
  // 不 patch splits.abi.include:AGP 禁止它和 ndk.abiFilters 同时存在
176
171
  if (c !== before) writeIfChanged(file, c), console.log(` ✓ 已设置 app ndk.abiFilters = [${abiList}]`);
177
172
 
package/src/utils.js CHANGED
@@ -123,9 +123,40 @@ const ensureDir = dir => {
123
123
  sanitizeFileName = s => {
124
124
  const cleaned = String(s).replace(/[\\/:*?"<>|]/g, '_').trim();
125
125
  return cleaned || 'app';
126
- };
126
+ },
127
+ /**
128
+ * 生成 ESLint flat config 源码(替代 @react-native/eslint-config/flat)
129
+ */
130
+ buildEslintConfigSource = () => `import js from '@eslint/js';
131
+ import babelParser from '@babel/eslint-parser';
132
+ import tsParser from '@typescript-eslint/parser';
133
+ import tsPlugin from '@typescript-eslint/eslint-plugin';
134
+ import reactPlugin from 'eslint-plugin-react';
135
+ import reactHooks from 'eslint-plugin-react-hooks';
136
+ import reactNative from 'eslint-plugin-react-native';
137
+ import prettier from 'eslint-config-prettier';
138
+
139
+ export default [
140
+ js.configs.recommended,
141
+ {
142
+ files: ['**/*.{js,jsx,ts,tsx}'],
143
+ languageOptions: {
144
+ parser: babelParser,
145
+ parserOptions: { requireConfigFile: false, babelOptions: { presets: ['module:@react-native/babel-preset'] } },
146
+ },
147
+ plugins: { react: reactPlugin, 'react-hooks': reactHooks, 'react-native': reactNative },
148
+ settings: { react: { version: 'detect' } },
149
+ rules: {
150
+ ...reactPlugin.configs.recommended.rules,
151
+ ...reactHooks.configs.recommended.rules,
152
+ 'react/react-in-jsx-scope': 'off',
153
+ 'react/prop-types': 'off',
154
+ },
155
+ },
156
+ { files: ['**/*.{ts,tsx}'], languageOptions: { parser: tsParser }, plugins: { '@typescript-eslint': tsPlugin },
157
+ rules: tsPlugin.configs.recommended.rules }, prettier];`;
127
158
 
128
159
  export {
129
- ensureDir, removeDir, isExcluded, copyDir, readJson, writeJson, writeIfChanged, copyFileIfChanged,
130
- run, runQuiet, runSilent, findAdb, findHermesc, sanitizeFileName
160
+ ensureDir, removeDir, isExcluded, copyDir, readJson, writeJson, writeIfChanged, copyFileIfChanged, run, runQuiet,
161
+ runSilent, findAdb, findHermesc, sanitizeFileName, buildEslintConfigSource
131
162
  };
package/template/App.tsx CHANGED
@@ -50,7 +50,8 @@ const App = () => {
50
50
  if (typeof msg !== 'string') return;
51
51
  if (msg.startsWith('Node started on ')) {
52
52
  const target = msg.slice('Node started on '.length).trim();
53
- setFailedReason(null), setUrl(target);
53
+ setFailedReason(null);
54
+ setUrl(target);
54
55
  } else if (msg.startsWith('Node failed: ')) {
55
56
  const reason = msg.slice('Node failed: '.length).trim();
56
57
  setFailedReason(reason);