@lark-apaas/coding-templates 0.1.43-alpha.1 → 0.1.43-alpha.3

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lark-apaas/coding-templates",
3
- "version": "0.1.43-alpha.1",
3
+ "version": "0.1.43-alpha.3",
4
4
  "description": "OpenClaw project templates for mclaw CLI",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -13,7 +13,7 @@
13
13
  "dev": "concurrently -k -n vite,health 'vite --port 8001 --host 0.0.0.0' 'node scripts/mock-health.cjs'",
14
14
  "build": "bash scripts/build.sh",
15
15
  "typecheck": "tsc -p tsconfig.app.json",
16
- "lint": "concurrently npm:typecheck npm:lint:eslint",
16
+ "lint": "npm run typecheck && npm run lint:eslint",
17
17
  "lint:eslint": "eslint client/src"
18
18
  },
19
19
  "dependencies": {
@@ -44,7 +44,7 @@
44
44
  "@radix-ui/react-toggle": "^1.1.9",
45
45
  "@radix-ui/react-toggle-group": "^1.1.10",
46
46
  "@radix-ui/react-tooltip": "^1.1.18",
47
- "@lark-apaas/client-toolkit-lite": "^1.1.0",
47
+ "@lark-apaas/client-toolkit-lite": "1.1.7-alpha.5",
48
48
  "@formkit/auto-animate": "^0.9.0",
49
49
  "@gsap/react": "^2.1.2",
50
50
  "framer-motion": "^12.38.0",
@@ -79,7 +79,7 @@
79
79
  "@types/react": "^19",
80
80
  "@types/react-dom": "^19",
81
81
  "@lark-apaas/coding-presets": "^1.0.0",
82
- "@lark-apaas/coding-vite-preset": "^1.0.0",
82
+ "@lark-apaas/coding-vite-preset": "1.0.7-alpha.1",
83
83
  "concurrently": "^9",
84
84
  "eslint": "^9",
85
85
  "eslint-plugin-react-hooks": "^7",
@@ -0,0 +1,219 @@
1
+ #!/usr/bin/env bash
2
+ # 测沙箱(或本机)模板从 0 到"预览可访问"的耗时拆解。
3
+ # 默认只测 dev 启动那段(不跑 npm install),符合沙箱里依赖已装好的常见场景。
4
+ #
5
+ # 用法:
6
+ # bash scripts/measure-boot.sh # 默认:只测 dev 启动(warm)
7
+ # bash scripts/measure-boot.sh --cold # 冷启动:清光 vite 预打包缓存
8
+ # bash scripts/measure-boot.sh --with-install # 顺带跑一次 npm install 测装包耗时
9
+ # bash scripts/measure-boot.sh --with-install --cold # 模拟首次冷启(清 node_modules + .vite)
10
+ # bash scripts/measure-boot.sh --port 18001 # 临时换端口
11
+ # bash scripts/measure-boot.sh --base /app/whatever # 临时换 CLIENT_BASE_PATH
12
+ #
13
+ # 兼容:--skip-install 等同于不传(默认就是跳过),保留作为 no-op 防止旧脚本/手势失败。
14
+ #
15
+ # 输出:每段耗时 + 总耗时,写到 ./logs/boot-timing.log(同时打到 stdout)
16
+
17
+ set -uo pipefail
18
+ T1=0; T2=0
19
+ DEV_PID=0
20
+
21
+ WITH_INSTALL=0
22
+ COLD=0
23
+ PORT=""
24
+ BASE="/app/probe"
25
+ LOG_DIR="./logs"
26
+ LOG_FILE="$LOG_DIR/boot-timing.log"
27
+
28
+ while [ $# -gt 0 ]; do
29
+ case "$1" in
30
+ --with-install) WITH_INSTALL=1; shift ;;
31
+ --skip-install) shift ;; # no-op,兼容旧手势;默认就是跳过
32
+ --cold) COLD=1; shift ;;
33
+ --port) PORT="$2"; shift 2 ;;
34
+ --base) BASE="$2"; shift 2 ;;
35
+ *) echo "未知参数: $1"; exit 1 ;;
36
+ esac
37
+ done
38
+
39
+ mkdir -p "$LOG_DIR"
40
+ : > "$LOG_FILE"
41
+
42
+ now_ms() {
43
+ # GNU date 支持 %N,macOS 的 BSD date 不支持——优先用 python3 兜底,避免 sandbox 上 BSD 镜像出错。
44
+ if command -v python3 >/dev/null 2>&1; then
45
+ python3 -c 'import time; print(int(time.time()*1000))'
46
+ else
47
+ local raw="$(date +%s%3N)"
48
+ case "$raw" in
49
+ *N) echo $(($(date +%s) * 1000));; # date 不支持 %N,降级到秒级精度
50
+ *) echo "$raw";;
51
+ esac
52
+ fi
53
+ }
54
+ log() {
55
+ local msg="[$(date +%H:%M:%S)] $*"
56
+ echo "$msg" | tee -a "$LOG_FILE"
57
+ }
58
+
59
+ # 自动从 package.json 提取 vite 端口
60
+ if [ -z "$PORT" ]; then
61
+ PORT=$(grep -oE -- '--port[[:space:]]+[0-9]+' package.json 2>/dev/null | head -1 | awk '{print $2}')
62
+ PORT="${PORT:-8001}"
63
+ fi
64
+
65
+ export CLIENT_BASE_PATH="$BASE"
66
+
67
+ log "=============================================="
68
+ log "Boot timing — port=$PORT base=$BASE with-install=$WITH_INSTALL cold=$COLD"
69
+ log "=============================================="
70
+
71
+ # ── 冷启动:清光本地缓存(不计入耗时,T_TOTAL_START 之前完成)────────
72
+ if [ "$COLD" = "1" ]; then
73
+ log "[cold] 清缓存以模拟冷启动..."
74
+ # vite 依赖预打包缓存(永远要清,热启动的最大区别就在这)
75
+ if [ -d node_modules/.vite ]; then
76
+ log " - rm -rf node_modules/.vite"
77
+ rm -rf node_modules/.vite
78
+ fi
79
+ # 本脚本上轮日志
80
+ if [ -d "$LOG_DIR" ]; then
81
+ log " - 清 $LOG_DIR/dev.log(避免 grep 命中旧 ready 行)"
82
+ : > "$LOG_DIR/dev.log" 2>/dev/null || true
83
+ fi
84
+ # 项目级 dist(一般不影响 dev,但顺手)
85
+ [ -d dist ] && rm -rf dist
86
+ # 默认保留 node_modules(典型冷启动对照:装好依赖、只清 .vite)
87
+ # 仅 --with-install 模式才清 node_modules 触发 npm install 重装
88
+ if [ "$WITH_INSTALL" = "1" ]; then
89
+ if [ -d node_modules ]; then
90
+ log " - rm -rf node_modules(--with-install 模式:清依赖以模拟首次冷启)"
91
+ rm -rf node_modules
92
+ fi
93
+ # npm 全局缓存留着(不动 /tmp/.npmcache / ~/.npm),不然太慢且不真实
94
+ else
95
+ log " - 保留 node_modules(默认模式:只清 .vite 缓存)"
96
+ fi
97
+ log "[cold] 缓存清理完成"
98
+ fi
99
+
100
+ T_TOTAL_START=$(now_ms)
101
+
102
+ # ── 阶段 1: npm install ──────────────────────────────────────────────
103
+ if [ "$WITH_INSTALL" = "1" ]; then
104
+ log "[1/4] npm install ..."
105
+ T1=$(now_ms)
106
+ npm install --silent > "$LOG_DIR/install.log" 2>&1
107
+ RC=$?
108
+ T2=$(now_ms)
109
+ if [ $RC -ne 0 ]; then
110
+ log " ❌ npm install 失败(exit=$RC,详见 $LOG_DIR/install.log)"
111
+ exit 1
112
+ fi
113
+ log " ✅ 耗时 $((T2 - T1)) ms"
114
+ else
115
+ log "[1/4] 跳过 npm install(默认;用 --with-install 启用)"
116
+ fi
117
+
118
+ # ── 阶段 2: 启动 dev(进程拉起到出现 \"VITE ready\" 提示)──────────────
119
+ log "[2/4] npm run dev — 等 vite ready 提示 ..."
120
+
121
+ # 杀光占用目标端口的所有进程(不依赖进程名匹配,沙箱里 vite 可能被各种 wrapper 拉起)
122
+ kill_port() {
123
+ local p="$1"
124
+ local pids
125
+ pids=$(lsof -tiTCP:"$p" -sTCP:LISTEN 2>/dev/null | tr '\n' ' ')
126
+ if [ -n "$pids" ]; then
127
+ log " 清理 :$p 上的进程: $pids"
128
+ # shellcheck disable=SC2086
129
+ kill -9 $pids 2>/dev/null || true
130
+ fi
131
+ }
132
+ kill_port "$PORT"
133
+ kill_port 3000
134
+ # 再保险一道:按名字
135
+ pkill -f 'concurrently.*vite' 2>/dev/null || true
136
+ pkill -f 'mock-health' 2>/dev/null || true
137
+ sleep 1
138
+
139
+ # 二次校验:端口必须真的空了
140
+ if lsof -tiTCP:"$PORT" -sTCP:LISTEN >/dev/null 2>&1; then
141
+ log " ❌ 清理后 :$PORT 仍被占用,请手动 kill 后重试"
142
+ log " lsof -i :$PORT"
143
+ exit 1
144
+ fi
145
+
146
+ # 截断 log 文件确保 grep 命中的是本次启动的输出(不是旧 log 残留)
147
+ : > "$LOG_DIR/dev.log"
148
+
149
+ T3=$(now_ms)
150
+ nohup npm run dev > "$LOG_DIR/dev.log" 2>&1 &
151
+ DEV_PID=$!
152
+
153
+ # 等 vite ready 提示(最多 60s)
154
+ T_READY=""
155
+ for i in $(seq 1 600); do
156
+ if grep -q 'VITE.*ready' "$LOG_DIR/dev.log" 2>/dev/null; then
157
+ T_READY=$(now_ms)
158
+ break
159
+ fi
160
+ # 短路:如果 dev 进程已经退出且没出现 ready,提早失败
161
+ if ! kill -0 "$DEV_PID" 2>/dev/null; then
162
+ log " ❌ dev 进程提前退出,看 $LOG_DIR/dev.log"
163
+ tail -20 "$LOG_DIR/dev.log" | sed 's/^/ /' | tee -a "$LOG_FILE"
164
+ exit 1
165
+ fi
166
+ sleep 0.1
167
+ done
168
+
169
+ if [ -z "$T_READY" ]; then
170
+ log " ❌ 60s 内未看到 vite ready 提示,看 $LOG_DIR/dev.log"
171
+ kill ${DEV_PID} 2>/dev/null
172
+ exit 1
173
+ fi
174
+ log " ✅ 耗时 $((T_READY - T3)) ms"
175
+
176
+ # ── 阶段 3: 端口可 TCP 连接 ──────────────────────────────────────────
177
+ log "[3/4] 端口 :$PORT 接受 TCP 连接 ..."
178
+ T4=$(now_ms)
179
+ T_TCP=""
180
+ for i in $(seq 1 100); do
181
+ if (echo > /dev/tcp/127.0.0.1/$PORT) 2>/dev/null; then
182
+ T_TCP=$(now_ms)
183
+ break
184
+ fi
185
+ sleep 0.05
186
+ done
187
+ if [ -z "$T_TCP" ]; then
188
+ log " ❌ 5s 内端口不可连接"
189
+ kill ${DEV_PID} 2>/dev/null
190
+ exit 1
191
+ fi
192
+ log " ✅ 耗时 $((T_TCP - T4)) ms(自 ready 后再 $((T_TCP - T_READY)) ms)"
193
+
194
+ # ── 阶段 4: 首屏 HTML 200 ─────────────────────────────────────────────
195
+ log "[4/4] curl 首屏(含 base normalize rewrite 路径)..."
196
+ T5=$(now_ms)
197
+ URL="http://127.0.0.1:$PORT${BASE}?probe=1"
198
+ HTTP_CODE=$(curl -sS -o "$LOG_DIR/first.html" -w '%{http_code}' --max-time 10 "$URL" 2>&1)
199
+ T6=$(now_ms)
200
+ SIZE=$(wc -c < "$LOG_DIR/first.html" 2>/dev/null | tr -d ' ')
201
+
202
+ log " URL=$URL"
203
+ log " HTTP=$HTTP_CODE size=${SIZE}B 耗时 $((T6 - T5)) ms"
204
+
205
+ if [ "$HTTP_CODE" != "200" ]; then
206
+ log " ⚠️ 非 200,看 $LOG_DIR/first.html"
207
+ fi
208
+
209
+ # ── 汇总 ──────────────────────────────────────────────────────────────
210
+ T_TOTAL_END=$(now_ms)
211
+ log "=============================================="
212
+ log "总耗时 $((T_TOTAL_END - T_TOTAL_START)) ms"
213
+ log "dev 进程 PID=${DEV_PID}(kill 后退出:kill ${DEV_PID})"
214
+ log " install: $([ $WITH_INSTALL = 1 ] && echo "$((T2 - T1)) ms" || echo skipped)"
215
+ log " vite ready: $((T_READY - T3)) ms"
216
+ log " tcp accept after ready: $((T_TCP - T_READY)) ms"
217
+ log " first html curl: $((T6 - T5)) ms"
218
+ log "=============================================="
219
+ log "详细日志:$LOG_FILE / $LOG_DIR/dev.log"
@@ -1,5 +1,56 @@
1
1
  import path from 'path'
2
+ import type { IncomingMessage, ServerResponse } from 'http'
2
3
  import { defineConfig } from '@lark-apaas/coding-vite-preset'
4
+ import type { Plugin, ViteDevServer } from 'vite'
5
+
6
+ /**
7
+ * dev 模式 base-path normalize 中间件
8
+ *
9
+ * 沙箱预览网关把流量原样转给 vite(保留 /app/:appId 前缀)。浏览器首次访问的
10
+ * URL 形如 `/app/app_xxx?sso_token=...`,pathname 缺末尾 /。
11
+ *
12
+ * vite 8 的 baseMiddleware 对"缺末尾 /"的请求不再像 vite 5 那样自动 301,
13
+ * 而是直接返回 404 警告页 "The server is configured with a public base URL..."。
14
+ *
15
+ * 这里做 internal rewrite(不发 301,避免跟沙箱网关形成 round-trip 死循环):
16
+ * 在 vite 内置 baseMiddleware **之前** unshift 一个中间件,把 req.url 改写
17
+ * 成带尾 /,下游 baseMiddleware 继续 strip base → SPA fallback。
18
+ * 浏览器全程不感知 redirect,地址栏可保留缺尾 / 形态(BrowserRouter basename
19
+ * 处理路由)。
20
+ *
21
+ * 必须 stack.unshift 而非 server.middlewares.use(...),否则会排到内置
22
+ * baseMiddleware 之后失效。
23
+ *
24
+ * TODO: 这段已同步到 @lark-apaas/coding-vite-preset,待该包发布并被本模板
25
+ * 依赖升级拉到后,可以删除此 plugin。保留是为了让现存沙箱(用着老 preset
26
+ * 的应用)也能立即生效。
27
+ */
28
+ const baseNormalizePlugin: Plugin = {
29
+ name: 'base-path-normalize',
30
+ configureServer(server: ViteDevServer) {
31
+ const base = (process.env.CLIENT_BASE_PATH || '').replace(/\/+$/, '')
32
+ if (!base) return
33
+ const stack = (
34
+ server.middlewares as unknown as {
35
+ stack: Array<{ route: string; handle: unknown }>
36
+ }
37
+ ).stack
38
+ if (!Array.isArray(stack)) return
39
+ stack.unshift({
40
+ route: '',
41
+ handle: (req: IncomingMessage, _res: ServerResponse, next: () => void) => {
42
+ const url = req.url || ''
43
+ const qIdx = url.indexOf('?')
44
+ const pathname = qIdx >= 0 ? url.slice(0, qIdx) : url
45
+ const search = qIdx >= 0 ? url.slice(qIdx) : ''
46
+ if (pathname === base) {
47
+ req.url = base + '/' + search
48
+ }
49
+ next()
50
+ },
51
+ })
52
+ },
53
+ }
3
54
 
4
55
  export default defineConfig({
5
56
  root: path.resolve(__dirname, 'client'),
@@ -9,4 +60,5 @@ export default defineConfig({
9
60
  '@shared': path.resolve(__dirname, 'shared'),
10
61
  },
11
62
  },
63
+ plugins: [baseNormalizePlugin],
12
64
  })