@coze-arch/cli 0.0.1-alpha.e8683e → 0.0.1-alpha.e89608

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.
Files changed (128) hide show
  1. package/README.md +1 -0
  2. package/lib/__templates__/expo/.cozeproj/scripts/dev_run.sh +25 -16
  3. package/lib/__templates__/expo/.cozeproj/scripts/server_dev_run.sh +9 -8
  4. package/lib/__templates__/expo/README.md +2 -2
  5. package/lib/__templates__/expo/client/app/+not-found.tsx +30 -0
  6. package/lib/__templates__/expo/client/app.config.ts +2 -2
  7. package/lib/__templates__/expo/client/components/Screen.tsx +2 -2
  8. package/lib/__templates__/expo/client/eslint.config.mjs +21 -1
  9. package/lib/__templates__/expo/client/hooks/useSafeRouter.ts +152 -0
  10. package/lib/__templates__/expo/client/metro.config.js +3 -0
  11. package/lib/__templates__/expo/client/package.json +36 -34
  12. package/lib/__templates__/expo/client/screens/demo/index.tsx +3 -3
  13. package/lib/__templates__/expo/client/scripts/install-missing-deps.js +10 -10
  14. package/lib/__templates__/expo/eslint-plugins/forbid-emoji/index.js +9 -0
  15. package/lib/__templates__/expo/eslint-plugins/forbid-emoji/rule.js +112 -0
  16. package/lib/__templates__/expo/eslint-plugins/forbid-emoji/tech.md +94 -0
  17. package/lib/__templates__/expo/eslint-plugins/react-native/index.js +9 -0
  18. package/lib/__templates__/expo/eslint-plugins/react-native/rule.js +64 -0
  19. package/lib/__templates__/expo/package.json +3 -0
  20. package/lib/__templates__/expo/patches/expo@54.0.33.patch +45 -0
  21. package/lib/__templates__/expo/pnpm-lock.yaml +1318 -2636
  22. package/lib/__templates__/expo/server/package.json +9 -7
  23. package/lib/__templates__/expo/server/src/index.ts +1 -0
  24. package/lib/__templates__/expo/template.config.js +56 -0
  25. package/lib/__templates__/native-static/.coze +11 -0
  26. package/lib/__templates__/native-static/index.html +33 -0
  27. package/lib/__templates__/native-static/styles/main.css +136 -0
  28. package/lib/__templates__/native-static/template.config.js +22 -0
  29. package/lib/__templates__/nextjs/README.md +5 -0
  30. package/lib/__templates__/nextjs/eslint.config.mjs +5 -0
  31. package/lib/__templates__/nextjs/next.config.ts +1 -2
  32. package/lib/__templates__/nextjs/package.json +5 -6
  33. package/lib/__templates__/nextjs/pnpm-lock.yaml +2096 -956
  34. package/lib/__templates__/nextjs/scripts/build.sh +4 -1
  35. package/lib/__templates__/nextjs/scripts/dev.sh +1 -2
  36. package/lib/__templates__/nextjs/scripts/start.sh +1 -1
  37. package/lib/__templates__/nextjs/src/app/layout.tsx +1 -1
  38. package/lib/__templates__/nextjs/src/app/page.tsx +18 -60
  39. package/lib/__templates__/nextjs/src/server.ts +35 -0
  40. package/lib/__templates__/nextjs/template.config.js +49 -14
  41. package/lib/__templates__/nextjs/tsconfig.json +1 -1
  42. package/lib/__templates__/nuxt-vue/.coze +12 -0
  43. package/lib/__templates__/nuxt-vue/README.md +73 -0
  44. package/lib/__templates__/nuxt-vue/_gitignore +24 -0
  45. package/lib/__templates__/nuxt-vue/_npmrc +23 -0
  46. package/lib/__templates__/nuxt-vue/app/app.vue +6 -0
  47. package/lib/__templates__/nuxt-vue/app/pages/index.vue +23 -0
  48. package/lib/__templates__/nuxt-vue/assets/css/main.css +24 -0
  49. package/lib/__templates__/nuxt-vue/nuxt.config.ts +116 -0
  50. package/lib/__templates__/nuxt-vue/package.json +35 -0
  51. package/lib/__templates__/nuxt-vue/pnpm-lock.yaml +8759 -0
  52. package/lib/__templates__/nuxt-vue/postcss.config.mjs +8 -0
  53. package/lib/__templates__/nuxt-vue/public/favicon.ico +0 -0
  54. package/lib/__templates__/nuxt-vue/public/robots.txt +2 -0
  55. package/lib/__templates__/nuxt-vue/scripts/build.sh +14 -0
  56. package/lib/__templates__/nuxt-vue/scripts/dev.sh +31 -0
  57. package/lib/__templates__/nuxt-vue/scripts/prepare.sh +14 -0
  58. package/lib/__templates__/nuxt-vue/scripts/start.sh +15 -0
  59. package/lib/__templates__/nuxt-vue/server/api/hello.ts +10 -0
  60. package/lib/__templates__/nuxt-vue/server/middleware/logger.ts +10 -0
  61. package/lib/__templates__/nuxt-vue/server/routes/health.ts +10 -0
  62. package/lib/__templates__/nuxt-vue/tailwind.config.js +13 -0
  63. package/lib/__templates__/nuxt-vue/template.config.js +87 -0
  64. package/lib/__templates__/nuxt-vue/tsconfig.json +18 -0
  65. package/lib/__templates__/taro/.coze +14 -0
  66. package/lib/__templates__/taro/.cozeproj/scripts/deploy_build.sh +19 -0
  67. package/lib/__templates__/taro/.cozeproj/scripts/deploy_run.sh +14 -0
  68. package/lib/__templates__/taro/.cozeproj/scripts/dev_build.sh +2 -0
  69. package/lib/__templates__/taro/.cozeproj/scripts/dev_run.sh +151 -0
  70. package/lib/__templates__/taro/.cozeproj/scripts/init_env.sh +5 -0
  71. package/lib/__templates__/taro/.cozeproj/scripts/pack.sh +24 -0
  72. package/lib/__templates__/taro/README.md +751 -0
  73. package/lib/__templates__/taro/_gitignore +40 -0
  74. package/lib/__templates__/taro/_npmrc +18 -0
  75. package/lib/__templates__/taro/babel.config.js +12 -0
  76. package/lib/__templates__/taro/config/dev.ts +9 -0
  77. package/lib/__templates__/taro/config/index.ts +223 -0
  78. package/lib/__templates__/taro/config/prod.ts +34 -0
  79. package/lib/__templates__/taro/eslint.config.mjs +80 -0
  80. package/lib/__templates__/taro/key/private.appid.key +0 -0
  81. package/lib/__templates__/taro/package.json +107 -0
  82. package/lib/__templates__/taro/patches/@tarojs__plugin-mini-ci@4.1.9.patch +30 -0
  83. package/lib/__templates__/taro/pnpm-lock.yaml +23100 -0
  84. package/lib/__templates__/taro/pnpm-workspace.yaml +2 -0
  85. package/lib/__templates__/taro/project.config.json +15 -0
  86. package/lib/__templates__/taro/server/nest-cli.json +10 -0
  87. package/lib/__templates__/taro/server/package.json +40 -0
  88. package/lib/__templates__/taro/server/src/app.controller.ts +23 -0
  89. package/lib/__templates__/taro/server/src/app.module.ts +10 -0
  90. package/lib/__templates__/taro/server/src/app.service.ts +8 -0
  91. package/lib/__templates__/taro/server/src/interceptors/http-status.interceptor.ts +23 -0
  92. package/lib/__templates__/taro/server/src/main.ts +49 -0
  93. package/lib/__templates__/taro/server/tsconfig.json +24 -0
  94. package/lib/__templates__/taro/src/app.config.ts +11 -0
  95. package/lib/__templates__/taro/src/app.css +52 -0
  96. package/lib/__templates__/taro/src/app.tsx +9 -0
  97. package/lib/__templates__/taro/src/index.html +39 -0
  98. package/lib/__templates__/taro/src/network.ts +39 -0
  99. package/lib/__templates__/taro/src/pages/index/index.config.ts +3 -0
  100. package/lib/__templates__/taro/src/pages/index/index.css +1 -0
  101. package/lib/__templates__/taro/src/pages/index/index.tsx +33 -0
  102. package/lib/__templates__/taro/src/presets/dev-debug.ts +23 -0
  103. package/lib/__templates__/taro/src/presets/h5-container.tsx +15 -0
  104. package/lib/__templates__/taro/src/presets/h5-navbar.tsx +201 -0
  105. package/lib/__templates__/taro/src/presets/h5-styles.ts +142 -0
  106. package/lib/__templates__/taro/src/presets/index.tsx +18 -0
  107. package/lib/__templates__/taro/stylelint.config.mjs +4 -0
  108. package/lib/__templates__/taro/template.config.js +68 -0
  109. package/lib/__templates__/taro/tsconfig.json +29 -0
  110. package/lib/__templates__/taro/types/global.d.ts +32 -0
  111. package/lib/__templates__/templates.json +75 -0
  112. package/lib/__templates__/vite/README.md +189 -11
  113. package/lib/__templates__/vite/_gitignore +1 -0
  114. package/lib/__templates__/vite/eslint.config.mjs +6 -1
  115. package/lib/__templates__/vite/package.json +20 -3
  116. package/lib/__templates__/vite/pnpm-lock.yaml +944 -1551
  117. package/lib/__templates__/vite/scripts/build.sh +4 -1
  118. package/lib/__templates__/vite/scripts/dev.sh +2 -2
  119. package/lib/__templates__/vite/scripts/start.sh +3 -3
  120. package/lib/__templates__/vite/server/index.ts +57 -0
  121. package/lib/__templates__/vite/server/routes/index.ts +31 -0
  122. package/lib/__templates__/vite/server/vite.ts +79 -0
  123. package/lib/__templates__/vite/src/main.ts +17 -47
  124. package/lib/__templates__/vite/template.config.js +49 -14
  125. package/lib/__templates__/vite/tsconfig.json +4 -3
  126. package/lib/__templates__/vite/vite.config.ts +1 -0
  127. package/lib/cli.js +651 -173
  128. package/package.json +7 -3
package/README.md CHANGED
@@ -14,6 +14,7 @@ Coze Coding 的项目模板引擎,提供基于前端技术栈的项目初始
14
14
 
15
15
  ```bash
16
16
  rush update
17
+
17
18
  ```
18
19
 
19
20
  ## 使用
@@ -1,7 +1,7 @@
1
1
  ROOT_DIR="$(cd "$(dirname "$0")/../.." && pwd)"
2
2
  PREVIEW_DIR="${COZE_PREVIEW_DIR:-/source/preview}"
3
3
  LOG_DIR="${COZE_LOG_DIR:-$ROOT_DIR/logs}"
4
- LOG_FILE="$LOG_DIR/app.log"
4
+ LOG_CLIENT_FILE="$LOG_DIR/client.log"
5
5
  mkdir -p "$LOG_DIR"
6
6
 
7
7
  # ==================== 配置项 ====================
@@ -15,13 +15,14 @@ EXPO_PORT="5000"
15
15
  WEB_URL="${COZE_PROJECT_DOMAIN_DEFAULT:-http://127.0.0.1:${SERVER_PORT}}"
16
16
  ASSUME_YES="1"
17
17
  EXPO_PUBLIC_BACKEND_BASE_URL="${EXPO_PUBLIC_BACKEND_BASE_URL:-$WEB_URL}"
18
-
18
+ EXPO_PUBLIC_COZE_PROJECT_ID="${COZE_PROJECT_ID:-}"
19
19
 
20
20
  EXPO_PACKAGER_PROXY_URL="${EXPO_PUBLIC_BACKEND_BASE_URL}"
21
- export EXPO_PUBLIC_BACKEND_BASE_URL EXPO_PACKAGER_PROXY_URL
21
+ export EXPO_PUBLIC_BACKEND_BASE_URL EXPO_PACKAGER_PROXY_URL EXPO_PUBLIC_COZE_PROJECT_ID
22
22
  # 运行时变量(为避免 set -u 的未绑定错误,预置为空)
23
23
  SERVER_PID=""
24
24
  EXPO_PID=""
25
+
25
26
  # ==================== 工具函数 ====================
26
27
  check_command() {
27
28
  if ! command -v "$1" &> /dev/null; then
@@ -88,14 +89,17 @@ ensure_port() {
88
89
  pipe_to_log() {
89
90
  local source="${1:-CLIENT}"
90
91
  local raw_log="${2:-}"
91
- local line timestamp ts msg record
92
+ local line clean_line timestamp
92
93
  while IFS= read -r line || [ -n "$line" ]; do
94
+ clean_line=$(printf '%s' "$line" | sed 's/\x1b\[[0-9;]*[mA-Za-z]//g')
95
+ if echo "$clean_line" | grep -qE '(^(Web|iOS|Android) node_modules/.*expo-router.*entry\.js.*%|^(Web|iOS|Android) Bundled [0-9]+ms node_modules/.*expo-router.*entry\.js|Logs will appear in the browser)'; then
96
+ continue
97
+ fi
93
98
  if [ -n "$raw_log" ]; then
94
- echo "$line" >> "$raw_log"
99
+ timestamp=$(date '+%Y-%m-%d %H:%M:%S')
100
+ echo "[$timestamp] $clean_line" >> "$raw_log"
95
101
  fi
96
- line=$(echo "[$source] $line" | sed 's/\x1b\[[0-9;]*[a-zA-Z]//g; s/\x1b\[[0-9;]*m//g')
97
- msg="${line}"
98
- echo "$msg"
102
+ printf '[%s] %s\n' "$source" "$clean_line"
99
103
  done
100
104
  }
101
105
 
@@ -114,11 +118,11 @@ start_expo() {
114
118
  pushd "$ROOT_DIR/client"
115
119
 
116
120
  if [ "$offline" = "1" ]; then
117
- ( EXPO_OFFLINE=1 EXPO_NO_DOCTOR=1 EXPO_PUBLIC_BACKEND_BASE_URL="$EXPO_PUBLIC_BACKEND_BASE_URL" EXPO_PACKAGER_PROXY_URL="$EXPO_PACKAGER_PROXY_URL" \
118
- nohup npx expo start --clear --port "$EXPO_PORT" 2>&1 | pipe_to_log "CLIENT" "$ROOT_DIR/logs/client.log" ) &
121
+ ( EXPO_OFFLINE=1 EXPO_NO_DEPENDENCY_VALIDATION=1 EXPO_PUBLIC_BACKEND_BASE_URL="$EXPO_PUBLIC_BACKEND_BASE_URL" EXPO_PACKAGER_PROXY_URL="$EXPO_PACKAGER_PROXY_URL" EXPO_PUBLIC_COZE_PROJECT_ID="$EXPO_PUBLIC_COZE_PROJECT_ID" \
122
+ nohup npx expo start --clear --port "$EXPO_PORT" 2>&1 | pipe_to_log "CLIENT" "$LOG_CLIENT_FILE" ) &
119
123
  else
120
- ( EXPO_NO_DOCTOR=1 EXPO_PUBLIC_BACKEND_BASE_URL="$EXPO_PUBLIC_BACKEND_BASE_URL" EXPO_PACKAGER_PROXY_URL="$EXPO_PACKAGER_PROXY_URL" \
121
- nohup npx expo start --clear --port "$EXPO_PORT" 2>&1 | pipe_to_log "CLIENT" "$ROOT_DIR/logs/client.log" ) &
124
+ ( EXPO_NO_DEPENDENCY_VALIDATION=1 EXPO_PUBLIC_BACKEND_BASE_URL="$EXPO_PUBLIC_BACKEND_BASE_URL" EXPO_PACKAGER_PROXY_URL="$EXPO_PACKAGER_PROXY_URL" EXPO_PUBLIC_COZE_PROJECT_ID="$EXPO_PUBLIC_COZE_PROJECT_ID" \
125
+ nohup npx expo start --clear --port "$EXPO_PORT" 2>&1 | pipe_to_log "CLIENT" "$LOG_CLIENT_FILE" ) &
122
126
  fi
123
127
  EXPO_PID=$!
124
128
  disown $EXPO_PID 2>/dev/null || true
@@ -129,9 +133,9 @@ start_expo() {
129
133
  detect_expo_fetch_failed() {
130
134
  local timeout="${1:-8}"
131
135
  local waited=0
132
- local log_file="$ROOT_DIR/logs/client.log"
136
+ local log_file="$LOG_CLIENT_FILE"
133
137
  while [ "$waited" -lt "$timeout" ]; do
134
- if [ -f "$log_file" ] && grep -q "TypeError: fetch failed" "$log_file" 2>/dev/null; then
138
+ if [ -f "$log_file" ] && tail -n 100 "$log_file" 2>/dev/null | grep -q "TypeError: fetch failed"; then
135
139
  return 0
136
140
  fi
137
141
  sleep 1
@@ -150,6 +154,12 @@ if [ -f "$PREVIEW_DIR/pre_install.py" ]; then
150
154
  python "$PREVIEW_DIR/pre_install.py" || echo "pre_install.py 执行失败"
151
155
  fi
152
156
 
157
+ echo "检查根目录 post_install.py"
158
+ if [ -f "$PREVIEW_DIR/post_install.py" ]; then
159
+ echo "执行:python $PREVIEW_DIR/post_install.py"
160
+ python "$PREVIEW_DIR/post_install.py" || echo "post_install.py 执行失败"
161
+ fi
162
+
153
163
  echo "==================== 开始启动 ===================="
154
164
  echo "开始执行服务启动脚本(start_dev.sh)..."
155
165
  echo "正在检查依赖命令和目录是否存在..."
@@ -159,8 +169,6 @@ check_command "pnpm"
159
169
  check_command "lsof"
160
170
  check_command "bash"
161
171
 
162
- echo "准备日志目录:$ROOT_DIR/logs"
163
- mkdir -p "$ROOT_DIR/logs"
164
172
  # 端口占用预检查与处理
165
173
  ensure_port SERVER_PORT "$SERVER_PORT"
166
174
  ensure_port EXPO_PORT "$EXPO_PORT"
@@ -188,6 +196,7 @@ fi
188
196
  echo "Expo 环境变量配置:"
189
197
  echo "EXPO_PUBLIC_BACKEND_BASE_URL=${EXPO_PUBLIC_BACKEND_BASE_URL}"
190
198
  echo "EXPO_PACKAGER_PROXY_URL=${EXPO_PACKAGER_PROXY_URL}"
199
+ echo "EXPO_PUBLIC_COZE_PROJECT_ID=${EXPO_PUBLIC_COZE_PROJECT_ID}"
191
200
  if [ -z "${EXPO_PID}" ]; then
192
201
  echo "无法获取 Expo 后台进程 PID"
193
202
  fi
@@ -2,8 +2,8 @@
2
2
 
3
3
  ROOT_DIR="$(cd "$(dirname "$0")/../.." && pwd)"
4
4
  SERVER_DIR="$ROOT_DIR/server"
5
- LOG_DIR="$ROOT_DIR/logs"
6
- LOG_FILE="$LOG_DIR/server.log"
5
+ LOG_DIR="${COZE_LOG_DIR:-$ROOT_DIR/logs}"
6
+ LOG_SERVER_FILE="$LOG_DIR/server.log"
7
7
  SERVER_PORT="${SERVER_PORT:-9091}"
8
8
 
9
9
  mkdir -p "$LOG_DIR"
@@ -11,13 +11,14 @@ mkdir -p "$LOG_DIR"
11
11
  pipe_to_log() {
12
12
  local source="${1:-SERVER}"
13
13
  local raw_log="${2:-}"
14
- local line
14
+ local line clean_line timestamp
15
15
  while IFS= read -r line || [ -n "$line" ]; do
16
+ clean_line=$(printf '%s' "$line" | sed 's/\x1b\[[0-9;]*[mA-Za-z]//g')
16
17
  if [ -n "$raw_log" ]; then
17
- echo "$line" >> "$raw_log"
18
+ timestamp=$(date '+%Y-%m-%d %H:%M:%S')
19
+ echo "[$timestamp] $clean_line" >> "$raw_log"
18
20
  fi
19
- line=$(echo "[$source] $line" | sed 's/\x1b\[[0-9;]*[a-zA-Z]//g; s/\x1b\[[0-9;]*m//g')
20
- echo "$line"
21
+ printf '[%s] %s\n' "$source" "$clean_line"
21
22
  done
22
23
  }
23
24
 
@@ -36,10 +37,10 @@ kill_old_server() {
36
37
  echo "==================== Server Dev Run ===================="
37
38
  echo "Server 目录:$SERVER_DIR"
38
39
  echo "Server 端口:$SERVER_PORT"
39
- echo "日志文件:$LOG_FILE"
40
+ echo "日志文件:$LOG_SERVER_FILE"
40
41
 
41
42
  kill_old_server
42
43
 
43
44
  echo "启动 server 服务..."
44
45
  cd "$SERVER_DIR"
45
- NODE_ENV=development PORT="$SERVER_PORT" npx tsx ./src/index.ts 2>&1 | pipe_to_log "SERVER" "$LOG_FILE"
46
+ NODE_ENV=development PORT="$SERVER_PORT" npx tsx watch ./src/index.ts 2>&1 | pipe_to_log "SERVER" "$LOG_SERVER_FILE"
@@ -15,11 +15,11 @@
15
15
  | └── package.json # 服务端 package.json
16
16
  ├── client/ # React Native 前端代码
17
17
  │ ├── app/ # Expo Router 路由目录(仅路由配置)
18
- │ │ ├── _layout.tsx # 根布局文件(必需)
18
+ │ │ ├── _layout.tsx # 根布局文件(必需,务必阅读)
19
19
  │ │ ├── home.tsx # 首页
20
20
  │ │ └── index.tsx # re-export home.tsx
21
21
  │ ├── screens/ # 页面实现目录(与 app/ 路由对应)
22
- │ │ └── home/
22
+ │ │ └── demo/ # demo 示例页面
23
23
  │ │ ├── index.tsx # 页面组件实现
24
24
  │ │ └── styles.ts # 页面样式
25
25
  │ ├── components/ # 可复用组件
@@ -0,0 +1,30 @@
1
+ import { View, Text, StyleSheet } from 'react-native';
2
+ import { Link } from 'expo-router';
3
+ import { useTheme } from '@/hooks/useTheme';
4
+ import { Spacing } from '@/constants/theme';
5
+
6
+ export default function NotFoundScreen() {
7
+ const { theme } = useTheme();
8
+
9
+ return (
10
+ <View style={[styles.container, { backgroundColor: theme.backgroundRoot }]}>
11
+ <Text>
12
+ 页面不存在
13
+ </Text>
14
+ <Link href="/" style={[styles.gohome]}>
15
+ 返回首页
16
+ </Link>
17
+ </View>
18
+ );
19
+ }
20
+
21
+ const styles = StyleSheet.create({
22
+ container: {
23
+ flex: 1,
24
+ justifyContent: 'center',
25
+ alignItems: 'center',
26
+ },
27
+ gohome: {
28
+ marginTop: Spacing['2xl'],
29
+ },
30
+ });
@@ -1,7 +1,7 @@
1
1
  import { ExpoConfig, ConfigContext } from 'expo/config';
2
2
 
3
- const appName = process.env.EXPO_PUBLIC_COZE_PROJECT_NAME || '应用';
4
- const projectId = process.env.EXPO_PUBLIC_COZE_PROJECT_ID;
3
+ const appName = process.env.COZE_PROJECT_NAME || process.env.EXPO_PUBLIC_COZE_PROJECT_NAME || '应用';
4
+ const projectId = process.env.COZE_PROJECT_ID || process.env.EXPO_PUBLIC_COZE_PROJECT_ID;
5
5
  const slugAppName = projectId ? `app${projectId}` : 'myapp';
6
6
 
7
7
  export default ({ config }: ConfigContext): ExpoConfig => {
@@ -32,7 +32,7 @@ import {
32
32
  *
33
33
  * ## 2. 沉浸式 Header (推荐)
34
34
  * - 场景:Header 背景色/图片需要延伸到状态栏 (如首页、个人中心)。
35
- * - 用法:`<Screen safeAreaEdges={['left', 'right', 'bottom']}>` (去掉 'top')
35
+ * - 用法:`<Screen safeAreaEdges={['left', 'right', 'bottom']}>` (去掉 'top')
36
36
  * - 配合:页面内部 Header 组件必须手动添加 paddingTop:
37
37
  * ```tsx
38
38
  * const insets = useSafeAreaInsets();
@@ -41,7 +41,7 @@ import {
41
41
  *
42
42
  * ## 3. 底部有 TabBar 或 悬浮按钮
43
43
  * - 场景:页面底部有固定导航栏,或者需要精细控制底部留白。
44
- * - 用法:`<Screen safeAreaEdges={['top', 'left', 'right']}>` (去掉 'bottom')
44
+ * - 用法:`<Screen safeAreaEdges={['top', 'left', 'right']}>` (去掉 'bottom')
45
45
  * - 配合:
46
46
  * - 若是滚动页:`<ScrollView contentContainerStyle={{ paddingBottom: insets.bottom + 80 }}>`
47
47
  * - 若是固定页:`<View style={{ paddingBottom: insets.bottom + 60 }}>`
@@ -6,7 +6,9 @@ import reactHooks from 'eslint-plugin-react-hooks';
6
6
  import regexp from 'eslint-plugin-regexp';
7
7
  import pluginImport from 'eslint-plugin-import';
8
8
  import fontawesome6 from '../eslint-plugins/fontawesome6/index.js';
9
- import reanimated from '../eslint-plugins/reanimated/index.js'
9
+ import reanimated from '../eslint-plugins/reanimated/index.js';
10
+ import reactnative from '../eslint-plugins/react-native/index.js';
11
+ import forbidEmoji from '../eslint-plugins/forbid-emoji/index.js';
10
12
 
11
13
  export default [
12
14
  {
@@ -18,6 +20,8 @@ export default [
18
20
  '.expo/**', // 排除 Expo 自动生成的文件
19
21
  'tailwind.config.js', // 排除 Tailwind 配置文件
20
22
  '**/*.d.ts',
23
+ 'eslint.config.*',
24
+ 'metro.config.*',
21
25
  ],
22
26
  },
23
27
  regexp.configs["flat/recommended"],
@@ -57,6 +61,8 @@ export default [
57
61
  import: pluginImport,
58
62
  fontawesome6,
59
63
  reanimated,
64
+ reactnative,
65
+ forbidEmoji,
60
66
  },
61
67
  rules: {
62
68
  // 关闭代码风格规则
@@ -77,6 +83,20 @@ export default [
77
83
  'react/jsx-uses-react': 'off',
78
84
  'fontawesome6/valid-name': 'error',
79
85
  'reanimated/ban-mix-use': 'error',
86
+ 'forbidEmoji/no-emoji': 'error',
87
+ // 禁止使用 via.placeholder.com 服务
88
+ 'no-restricted-syntax': [
89
+ 'error',
90
+ {
91
+ 'selector': 'Literal[value=/via\\.placeholder\\.com/]',
92
+ 'message': 'via.placeholder.com 服务不可用,禁止在代码中使用',
93
+ },
94
+ {
95
+ 'selector': 'TemplateLiteral > TemplateElement[value.raw=/via\\.placeholder\\.com/]',
96
+ 'message': 'via.placeholder.com 服务不可用,禁止在代码中使用',
97
+ },
98
+ ],
99
+ 'reactnative/wrap-horizontal-scrollview-inside-view': ['error'],
80
100
  },
81
101
  },
82
102
 
@@ -0,0 +1,152 @@
1
+ /**
2
+ * 安全路由 Hook - 完全代替原生的 useRouter 和 useLocalSearchParams
3
+ *
4
+ * 提供的 Hook:
5
+ * - useSafeRouter: 代替 useRouter,包含所有路由方法,并对 push/replace/navigate/setParams 进行安全编码
6
+ * - useSafeSearchParams: 代替 useLocalSearchParams,获取路由参数
7
+ *
8
+ * 解决的问题:
9
+ * 1. URI 编解码不对称 - useLocalSearchParams 会自动解码,但 router.push 不会自动编码,
10
+ * 当参数包含 % 等特殊字符时会拿到错误的值
11
+ * 2. 类型丢失 - URL 参数全是 string,Number/Boolean 类型会丢失
12
+ * 3. 嵌套对象无法传递 - URL search params 不支持嵌套结构
13
+ *
14
+ * 解决方案:
15
+ * 采用 Payload 模式,将所有参数打包成 JSON 并 Base64 编码后传递,
16
+ * 接收时再解码还原,确保数据完整性和类型安全。
17
+ *
18
+ * 优点:
19
+ * 1. 自动处理所有特殊字符(如 %、&、=、中文、Emoji 等)
20
+ * 2. 保留数据类型(Number、Boolean 不会变成 String)
21
+ * 3. 支持嵌套对象和数组传递
22
+ * 4. 三端兼容(iOS、Android、Web)
23
+ *
24
+ * 使用方式:
25
+ * ```tsx
26
+ * // 发送端 - 使用 useSafeRouter 代替 useRouter
27
+ * const router = useSafeRouter();
28
+ * router.push('/detail', { id: 123, uri: 'file:///path/%40test.mp3' });
29
+ * router.replace('/home', { tab: 'settings' });
30
+ * router.navigate('/profile', { userId: 456 });
31
+ * router.back();
32
+ * if (router.canGoBack()) { ... }
33
+ * router.setParams({ updated: true });
34
+ *
35
+ * // 接收端 - 使用 useSafeSearchParams 代替 useLocalSearchParams
36
+ * const { id, uri } = useSafeSearchParams<{ id: number; uri: string }>();
37
+ * ```
38
+ */
39
+ import { useMemo } from 'react';
40
+ import { useRouter as useExpoRouter, useLocalSearchParams as useExpoParams } from 'expo-router';
41
+ import { Base64 } from 'js-base64';
42
+
43
+ const PAYLOAD_KEY = '__safeRouterPayload__';
44
+ const LOG_PREFIX = '[SafeRouter]';
45
+
46
+
47
+ const getCurrentParams = (rawParams: Record<string, string | string[]>): Record<string, unknown> => {
48
+ const payload = rawParams[PAYLOAD_KEY];
49
+ if (payload && typeof payload === 'string') {
50
+ const decoded = deserializeParams<Record<string, unknown>>(payload);
51
+ if (decoded && Object.keys(decoded).length > 0) {
52
+ return decoded;
53
+ }
54
+ }
55
+ const { [PAYLOAD_KEY]: _, ...rest } = rawParams;
56
+ return rest as Record<string, unknown>;
57
+ };
58
+
59
+ const serializeParams = (params: Record<string, unknown>): string => {
60
+ try {
61
+ const jsonStr = JSON.stringify(params);
62
+ return Base64.encode(jsonStr);
63
+ } catch (error) {
64
+ console.error(LOG_PREFIX, 'serialize failed:', error instanceof Error ? error.message : 'Unknown error');
65
+ return '';
66
+ }
67
+ };
68
+
69
+ const deserializeParams = <T = Record<string, unknown>>(
70
+ payload: string | string[] | undefined
71
+ ): T | null => {
72
+ if (!payload || typeof payload !== 'string') {
73
+ return null;
74
+ }
75
+ try {
76
+ const jsonStr = Base64.decode(payload);
77
+ return JSON.parse(jsonStr) as T;
78
+ } catch (error) {
79
+ console.error(LOG_PREFIX, 'deserialize failed:', error instanceof Error ? error.message : 'Unknown error');
80
+ return null;
81
+ }
82
+ };
83
+
84
+
85
+ /**
86
+ * 安全路由 Hook,用于页面跳转,代替 useRouter
87
+ * @returns 路由方法(继承 useRouter 所有方法,并对以下方法进行安全编码)
88
+ * - push(pathname, params) - 入栈新页面
89
+ * - replace(pathname, params) - 替换当前页面
90
+ * - navigate(pathname, params) - 智能跳转(已存在则返回,否则 push)
91
+ * - setParams(params) - 更新当前页面参数(合并现有参数)
92
+ */
93
+ export function useSafeRouter() {
94
+ const router = useExpoRouter();
95
+ const rawParams = useExpoParams<Record<string, string | string[]>>();
96
+
97
+ const push = (pathname: string, params: Record<string, unknown> = {}) => {
98
+ const encodedPayload = serializeParams(params);
99
+ router.push({
100
+ pathname: pathname as `/${string}`,
101
+ params: { [PAYLOAD_KEY]: encodedPayload },
102
+ });
103
+ };
104
+
105
+ const replace = (pathname: string, params: Record<string, unknown> = {}) => {
106
+ const encodedPayload = serializeParams(params);
107
+ router.replace({
108
+ pathname: pathname as `/${string}`,
109
+ params: { [PAYLOAD_KEY]: encodedPayload },
110
+ });
111
+ };
112
+
113
+ const navigate = (pathname: string, params: Record<string, unknown> = {}) => {
114
+ const encodedPayload = serializeParams(params);
115
+ router.navigate({
116
+ pathname: pathname as `/${string}`,
117
+ params: { [PAYLOAD_KEY]: encodedPayload },
118
+ });
119
+ };
120
+
121
+ const setParams = (params: Record<string, unknown>) => {
122
+ const currentParams = getCurrentParams(rawParams);
123
+ const mergedParams = { ...currentParams, ...params };
124
+ const encodedPayload = serializeParams(mergedParams);
125
+ router.setParams({ [PAYLOAD_KEY]: encodedPayload });
126
+ };
127
+
128
+ return {
129
+ ...router,
130
+ push,
131
+ replace,
132
+ navigate,
133
+ setParams,
134
+ };
135
+ }
136
+
137
+ /**
138
+ * 安全获取路由参数 Hook,用于接收方,代替 useLocalSearchParams
139
+ * 兼容两种跳转方式:
140
+ * 1. useSafeRouter 跳转 - 自动解码 Payload
141
+ * 2. 外部跳转(深链接、浏览器直接访问等)- 回退到原始参数
142
+ * @returns 解码后的参数对象,类型安全
143
+ */
144
+ export function useSafeSearchParams<T = Record<string, unknown>>(): T {
145
+ const rawParams = useExpoParams<Record<string, string | string[]>>();
146
+
147
+ const decodedParams = useMemo(() => {
148
+ return getCurrentParams(rawParams) as T;
149
+ }, [rawParams]);
150
+
151
+ return decodedParams;
152
+ }
@@ -26,6 +26,9 @@ config.resolver.blockList = [
26
26
  // 4. 通用规则
27
27
  /.*\/__tests__\/.*/, // 排除所有测试目录
28
28
  /.*\.git\/.*/, // 排除 Git 目录
29
+
30
+ // 5. pnpm 临时目录(避免 ENOENT 错误)
31
+ /.*node_modules\/\.pnpm\/.*_tmp_\d+.*/,
29
32
  ];
30
33
 
31
34
  const BACKEND_TARGET = 'http://localhost:9091';
@@ -15,36 +15,38 @@
15
15
  "preset": "jest-expo"
16
16
  },
17
17
  "dependencies": {
18
- "@expo/metro-runtime": "^6.1.2",
19
- "@expo/vector-icons": "^15.0.0",
20
- "@react-native-async-storage/async-storage": "^2.2.0",
21
- "@react-native-community/datetimepicker": "^8.5.0",
22
- "@react-native-community/slider": "^5.0.1",
23
- "@react-native-masked-view/masked-view": "^0.3.2",
24
- "@react-native-picker/picker": "^2.11.0",
18
+ "@expo/metro-runtime": "~6.1.2",
19
+ "@expo/vector-icons": "^15.0.3",
20
+ "@react-native-async-storage/async-storage": "2.2.0",
21
+ "@react-native-community/datetimepicker": "8.4.4",
22
+ "@react-native-community/slider": "5.0.1",
23
+ "@react-native-masked-view/masked-view": "0.3.2",
24
+ "@react-native-picker/picker": "2.11.1",
25
25
  "@react-navigation/bottom-tabs": "^7.2.0",
26
26
  "@react-navigation/native": "^7.0.14",
27
27
  "dayjs": "^1.11.19",
28
- "expo": "^54.0.7",
29
- "expo-auth-session": "^7.0.9",
30
- "expo-av": "~16.0.6",
31
- "expo-blur": "~15.0.6",
28
+ "expo": "54.0.33",
29
+ "expo-auth-session": "~7.0.10",
30
+ "expo-av": "~16.0.8",
31
+ "expo-blur": "~15.0.8",
32
32
  "expo-camera": "~17.0.10",
33
- "expo-constants": "~18.0.8",
34
- "expo-crypto": "^15.0.7",
35
- "expo-font": "~14.0.7",
36
- "expo-haptics": "~15.0.6",
37
- "expo-image-picker": "~17.0.7",
38
- "expo-linear-gradient": "~15.0.6",
39
- "expo-linking": "~8.0.7",
40
- "expo-location": "~19.0.7",
41
- "expo-image": "^3.0.11",
42
- "expo-router": "~6.0.0",
43
- "expo-splash-screen": "~31.0.8",
44
- "expo-status-bar": "~3.0.7",
45
- "expo-symbols": "~1.0.6",
33
+ "expo-constants": "~18.0.13",
34
+ "expo-crypto": "~15.0.8",
35
+ "expo-file-system": "~19.0.21",
36
+ "expo-font": "~14.0.11",
37
+ "expo-haptics": "~15.0.8",
38
+ "expo-image": "~3.0.11",
39
+ "expo-image-picker": "~17.0.10",
40
+ "expo-linear-gradient": "~15.0.8",
41
+ "expo-linking": "~8.0.11",
42
+ "expo-location": "~19.0.8",
43
+ "expo-router": "~6.0.23",
44
+ "expo-splash-screen": "~31.0.13",
45
+ "expo-status-bar": "~3.0.9",
46
+ "expo-symbols": "~1.0.8",
46
47
  "expo-system-ui": "~6.0.9",
47
48
  "expo-web-browser": "~15.0.10",
49
+ "js-base64": "^3.7.7",
48
50
  "react": "19.1.0",
49
51
  "react-dom": "19.1.0",
50
52
  "react-native": "0.81.5",
@@ -52,25 +54,26 @@
52
54
  "react-native-gesture-handler": "~2.28.0",
53
55
  "react-native-keyboard-aware-scroll-view": "^0.9.5",
54
56
  "react-native-modal-datetime-picker": "18.0.0",
55
- "react-native-reanimated": "~4.1.0",
57
+ "react-native-reanimated": "~4.1.1",
56
58
  "react-native-safe-area-context": "~5.6.0",
57
59
  "react-native-screens": "~4.16.0",
58
- "react-native-svg": "15.15.0",
60
+ "react-native-svg": "15.12.1",
59
61
  "react-native-toast-message": "^2.3.3",
60
- "react-native-web": "^0.21.2",
61
- "react-native-webview": "~13.15.0",
62
+ "react-native-web": "~0.21.0",
63
+ "react-native-webview": "13.15.0",
62
64
  "react-native-worklets": "0.5.1",
63
65
  "zod": "^4.2.1"
64
66
  },
65
67
  "devDependencies": {
66
68
  "@babel/core": "^7.25.2",
67
- "babel-plugin-module-resolver": "^5.0.2",
68
- "babel-preset-expo": "^54.0.9",
69
69
  "@eslint/js": "^9.27.0",
70
70
  "@types/jest": "^29.5.12",
71
71
  "@types/react": "~19.1.0",
72
72
  "@types/react-test-renderer": "19.1.0",
73
+ "babel-plugin-module-resolver": "^5.0.2",
74
+ "babel-preset-expo": "^54.0.9",
73
75
  "chalk": "^4.1.2",
76
+ "connect": "^3.7.0",
74
77
  "depcheck": "^1.4.7",
75
78
  "esbuild": "0.27.2",
76
79
  "eslint": "^9.39.2",
@@ -81,13 +84,12 @@
81
84
  "eslint-plugin-react-hooks": "^7.0.1",
82
85
  "eslint-plugin-regexp": "^2.10.0",
83
86
  "globals": "^16.1.0",
87
+ "http-proxy-middleware": "^3.0.5",
84
88
  "jest": "^29.2.1",
85
- "jest-expo": "~54.0.10",
89
+ "jest-expo": "~54.0.17",
86
90
  "react-test-renderer": "19.1.0",
87
91
  "tsx": "^4.21.0",
88
92
  "typescript": "^5.8.3",
89
- "typescript-eslint": "^8.32.1",
90
- "connect": "^3.7.0",
91
- "http-proxy-middleware": "^3.0.5"
93
+ "typescript-eslint": "^8.32.1"
92
94
  }
93
95
  }
@@ -15,10 +15,10 @@ export default function DemoPage() {
15
15
  >
16
16
  <Image
17
17
  style={styles.logo}
18
- source="https://lf-coze-web-cdn.coze.cn/obj/eden-cn/lm-lgvj/ljhwZthlaukjlkulzlp/coze-coding/expo/coze-loading.gif"
18
+ source="https://lf-coze-web-cdn.coze.cn/obj/eden-cn/lm-lgvj/ljhwZthlaukjlkulzlp/coze-coding/icon/coze-coding.gif"
19
19
  ></Image>
20
- <Text style={{...styles.title, color: theme.textPrimary}}>APP 开发中</Text>
21
- <Text style={{...styles.description, color: theme.textSecondary}}>即将为您呈现应用界面</Text>
20
+ <Text style={{...styles.title, color: theme.textPrimary}}>应用开发中</Text>
21
+ <Text style={{...styles.description, color: theme.textSecondary}}>请稍候,界面即将呈现</Text>
22
22
  </View>
23
23
  </Screen>
24
24
  );
@@ -9,7 +9,7 @@ const { execSync } = require('child_process');
9
9
  const fs = require('fs');
10
10
  const path = require('path');
11
11
 
12
- console.log('🔍 检测缺失的依赖...\n');
12
+ console.log('检测缺失的依赖...\n');
13
13
 
14
14
  try {
15
15
  // 运行 depcheck 并获取 JSON 输出
@@ -61,11 +61,11 @@ try {
61
61
  });
62
62
 
63
63
  if (missingPackages.length === 0) {
64
- console.log('✅ 没有发现缺失的依赖!');
64
+ console.log('没有发现缺失的依赖');
65
65
  process.exit(0);
66
66
  }
67
67
 
68
- console.log('📦 发现以下缺失的依赖:');
68
+ console.log('发现以下缺失的依赖:');
69
69
  missingPackages.forEach((pkg, index) => {
70
70
  const files = missing[pkg];
71
71
  console.log(` ${index + 1}. ${pkg}`);
@@ -74,7 +74,7 @@ try {
74
74
  );
75
75
  });
76
76
 
77
- console.log('\n🚀 开始安装...\n');
77
+ console.log('\n开始安装...\n');
78
78
 
79
79
  // 使用 expo install 安装所有缺失的包
80
80
  const packagesToInstall = missingPackages.join(' ');
@@ -84,22 +84,22 @@ try {
84
84
  stdio: 'inherit',
85
85
  });
86
86
 
87
- console.log('\n✅ 所有缺失的依赖已安装完成!');
87
+ console.log('\n所有缺失的依赖已安装完成');
88
88
  } catch (installError) {
89
- console.log('\n⚠️ expo install 失败,尝试使用 npm install...\n');
89
+ console.log('\nexpo install 失败,尝试使用 npm install...\n');
90
90
 
91
91
  execSync(`npm install ${packagesToInstall}`, {
92
92
  stdio: 'inherit',
93
93
  });
94
94
 
95
- console.log('\n所有缺失的依赖已通过 npm 安装完成!');
95
+ console.log('\n所有缺失的依赖已通过 npm 安装完成');
96
96
  }
97
97
  } catch (error) {
98
98
  if (error.message.includes('depcheck')) {
99
- console.error('depcheck 未安装或运行失败');
100
- console.log('💡 尝试运行: npm install -g depcheck');
99
+ console.error('depcheck 未安装或运行失败');
100
+ console.log('尝试运行: npm install -g depcheck');
101
101
  } else {
102
- console.error('发生错误:', error.message);
102
+ console.error('发生错误:', error.message);
103
103
  }
104
104
  process.exit(1);
105
105
  }
@@ -0,0 +1,9 @@
1
+ const noEmoji = require('./rule')
2
+
3
+ const plugin = {
4
+ rules: {
5
+ 'no-emoji': noEmoji,
6
+ },
7
+ }
8
+
9
+ module.exports = plugin