@coze-arch/cli 0.1.8-alpha.382639 → 0.1.8-alpha.53dd57

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.
@@ -0,0 +1,380 @@
1
+ #!/usr/bin/env bash
2
+ # Install dependencies on local disk and expose them to the source tree by symlink.
3
+ set -euo pipefail
4
+
5
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)"
6
+ if [ "$(basename "$(dirname "$SCRIPT_DIR")")" = ".cozeproj" ]; then
7
+ PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd -P)"
8
+ else
9
+ PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd -P)"
10
+ fi
11
+
12
+ command -v pnpm >/dev/null 2>&1 || {
13
+ echo "[node_modules] pnpm is required" >&2
14
+ exit 1
15
+ }
16
+
17
+ DRIVE_ROOT="${COZE_DRIVE_ROOT:-/Coze/Drive}"
18
+ if [ -d "$DRIVE_ROOT" ]; then
19
+ DRIVE_ROOT="$(cd "$DRIVE_ROOT" && pwd -P)"
20
+ else
21
+ DRIVE_ROOT="${DRIVE_ROOT%/}"
22
+ fi
23
+ case "$PROJECT_ROOT" in
24
+ "$DRIVE_ROOT"|"$DRIVE_ROOT"/*) ;;
25
+ *)
26
+ (cd "$PROJECT_ROOT" && pnpm install "$@")
27
+ exit 0
28
+ ;;
29
+ esac
30
+
31
+ NM_ROOT="/tmp/nm"
32
+ PROJECT_ID="$(basename "$PROJECT_ROOT")-$(printf '%s' "$PROJECT_ROOT" | cksum | awk '{print $1}')"
33
+ mkdir -p "$NM_ROOT"
34
+ WORK_DIR="$NM_ROOT/$PROJECT_ID"
35
+ case "$WORK_DIR" in
36
+ "$DRIVE_ROOT"|"$DRIVE_ROOT"/*)
37
+ echo "[node_modules] local dependency directory must not be on Coze Drive: $WORK_DIR" >&2
38
+ exit 1
39
+ ;;
40
+ esac
41
+
42
+ LOCK_DIR="$NM_ROOT/$PROJECT_ID.lock"
43
+ LOCK_TIMEOUT_SECONDS="${COZE_NODE_MODULES_LOCK_TIMEOUT_SECONDS:-120}"
44
+ case "$LOCK_TIMEOUT_SECONDS" in
45
+ ''|*[!0-9]*|0)
46
+ echo "[node_modules] COZE_NODE_MODULES_LOCK_TIMEOUT_SECONDS must be a positive integer" >&2
47
+ exit 1
48
+ ;;
49
+ esac
50
+
51
+ lock_owner_is_alive() {
52
+ local owner_pid=""
53
+ [ -f "$LOCK_DIR/pid" ] || return 1
54
+ IFS= read -r owner_pid < "$LOCK_DIR/pid" || return 1
55
+ case "$owner_pid" in
56
+ ''|*[!0-9]*) return 1 ;;
57
+ esac
58
+ kill -0 "$owner_pid" 2>/dev/null
59
+ }
60
+
61
+ release_lock() {
62
+ local owner_pid=""
63
+ [ -f "$LOCK_DIR/pid" ] || return 0
64
+ IFS= read -r owner_pid < "$LOCK_DIR/pid" || true
65
+ if [ "$owner_pid" = "$$" ]; then
66
+ rm -f "$LOCK_DIR/pid"
67
+ rmdir "$LOCK_DIR" 2>/dev/null || true
68
+ fi
69
+ }
70
+
71
+ lock_started_at="$(date +%s)"
72
+ missing_owner_checks=0
73
+ while ! mkdir "$LOCK_DIR" 2>/dev/null; do
74
+ if lock_owner_is_alive; then
75
+ missing_owner_checks=0
76
+ else
77
+ missing_owner_checks=$((missing_owner_checks + 1))
78
+ if [ "$missing_owner_checks" -ge 2 ]; then
79
+ stale_lock_dir="$LOCK_DIR.stale.$$"
80
+ if mv "$LOCK_DIR" "$stale_lock_dir" 2>/dev/null; then
81
+ echo "[node_modules] Recovering stale dependency preparation lock."
82
+ rm -rf "$stale_lock_dir"
83
+ fi
84
+ missing_owner_checks=0
85
+ continue
86
+ fi
87
+ fi
88
+
89
+ lock_now="$(date +%s)"
90
+ if [ $((lock_now - lock_started_at)) -ge "$LOCK_TIMEOUT_SECONDS" ]; then
91
+ echo "[node_modules] Timed out waiting ${LOCK_TIMEOUT_SECONDS}s for dependency preparation lock: $LOCK_DIR" >&2
92
+ exit 1
93
+ fi
94
+ echo "[node_modules] Waiting for another dependency preparation for this project..."
95
+ sleep 1
96
+ done
97
+ printf '%s\n' "$$" > "$LOCK_DIR/pid"
98
+ trap release_lock EXIT
99
+
100
+ mkdir -p "$WORK_DIR"
101
+ INSTALL_STATE_FILE="$WORK_DIR/.install-state"
102
+
103
+ # Previous versions mirrored the whole project into WORK_DIR. That gives pnpm
104
+ # and framework dev servers two paths for the same source tree, which breaks
105
+ # file watching and on-demand compilation. Keep only managed local outputs.
106
+ cleanup_legacy_workspace() {
107
+ local workspace_dir="$1" entry name
108
+ if [ -L "$workspace_dir" ] || { [ -e "$workspace_dir" ] && [ ! -d "$workspace_dir" ]; }; then
109
+ rm -rf "$workspace_dir"
110
+ return
111
+ fi
112
+ if [ ! -d "$workspace_dir" ]; then
113
+ return 0
114
+ fi
115
+
116
+ while IFS= read -r -d '' entry; do
117
+ name="$(basename "$entry")"
118
+ [ "$name" = "node_modules" ] || rm -rf "$entry"
119
+ done < <(find "$workspace_dir" -mindepth 1 -maxdepth 1 -print0)
120
+ }
121
+
122
+ # Preserve existing project caches during dependency preparation.
123
+ cleanup_legacy_shadow_project() {
124
+ local entry name
125
+ while IFS= read -r -d '' entry; do
126
+ name="$(basename "$entry")"
127
+ case "$name" in
128
+ node_modules|client|server|caches|.install-state) continue ;;
129
+ esac
130
+ rm -rf "$entry"
131
+ done < <(find "$WORK_DIR" -mindepth 1 -maxdepth 1 -print0)
132
+
133
+ for workspace in client server; do
134
+ cleanup_legacy_workspace "$WORK_DIR/$workspace"
135
+ done
136
+ }
137
+
138
+ link_install_entry() {
139
+ local source_path="$1" target_path="$2"
140
+ rm -rf "$target_path"
141
+ if [ -e "$source_path" ]; then
142
+ ln -s "$source_path" "$target_path"
143
+ fi
144
+ }
145
+
146
+ hash_install_input() {
147
+ if command -v sha256sum >/dev/null 2>&1; then
148
+ sha256sum | awk '{print $1}'
149
+ elif command -v shasum >/dev/null 2>&1; then
150
+ shasum -a 256 | awk '{print $1}'
151
+ else
152
+ cksum | awk '{print $1 "-" $2}'
153
+ fi
154
+ }
155
+
156
+ append_file_to_install_fingerprint() {
157
+ local relative_path="$1"
158
+ local source_path="$PROJECT_ROOT/$relative_path"
159
+ printf 'path=%s\n' "$relative_path"
160
+ if [ -f "$source_path" ]; then
161
+ cat "$source_path"
162
+ else
163
+ printf '<missing>\n'
164
+ fi
165
+ }
166
+
167
+ append_directory_to_install_fingerprint() {
168
+ local relative_path="$1"
169
+ local source_path="$PROJECT_ROOT/$relative_path"
170
+ local file
171
+ printf 'path=%s\n' "$relative_path"
172
+ if [ ! -d "$source_path" ]; then
173
+ printf '<missing>\n'
174
+ return
175
+ fi
176
+
177
+ while IFS= read -r file; do
178
+ printf 'file=%s\n' "${file#"${PROJECT_ROOT}/"}"
179
+ cat "$file"
180
+ done < <(find -P "$source_path" -type f -print | LC_ALL=C sort)
181
+ }
182
+
183
+ # Everything that can change the resolved dependency tree. The leading version marker
184
+ # invalidates every stored state when this scheme itself changes.
185
+ calculate_install_fingerprint() {
186
+ local filename workspace argument
187
+ {
188
+ printf 'prepare-node-modules-install-state=1\n'
189
+ printf 'node-version=%s\n' "$(node --version)"
190
+ printf 'pnpm-version=%s\n' "$(pnpm --version)"
191
+ printf 'node-env=%s\n' "${NODE_ENV:-}"
192
+ printf 'npm-config-production=%s\n' "${npm_config_production:-}"
193
+ printf 'npm-config-production-upper=%s\n' "${NPM_CONFIG_PRODUCTION:-}"
194
+ printf 'pnpm-config-production=%s\n' "${PNPM_CONFIG_PRODUCTION:-}"
195
+ for argument in "$@"; do
196
+ printf 'install-argument=%s\n' "$argument"
197
+ done
198
+ for filename in package.json pnpm-lock.yaml pnpm-workspace.yaml .npmrc; do
199
+ append_file_to_install_fingerprint "$filename"
200
+ done
201
+ for workspace in client server; do
202
+ append_file_to_install_fingerprint "$workspace/package.json"
203
+ done
204
+ append_directory_to_install_fingerprint patches
205
+ } | hash_install_input
206
+ }
207
+
208
+ install_requires_refresh() {
209
+ local argument
210
+ for argument in "$@"; do
211
+ case "$argument" in
212
+ --force|-f|--lockfile-only|--fix-lockfile) return 0 ;;
213
+ esac
214
+ done
215
+ return 1
216
+ }
217
+
218
+ # Reuse skips pnpm install entirely, so it must not skip side effects that live outside
219
+ # node_modules. preinstall is exempt: it only guards which package manager may run, and
220
+ # there is no install to guard when the tree is already in place.
221
+ package_has_install_lifecycle() {
222
+ local package_path="$1"
223
+ node - "$package_path" <<'NODE'
224
+ const fs = require('node:fs');
225
+ const [packagePath] = process.argv.slice(2);
226
+
227
+ try {
228
+ const packageJson = JSON.parse(fs.readFileSync(packagePath, 'utf8'));
229
+ const scripts = packageJson && typeof packageJson.scripts === 'object' && packageJson.scripts
230
+ ? packageJson.scripts
231
+ : {};
232
+ const hasLifecycle = ['install', 'postinstall', 'prepare'].some(
233
+ name => typeof scripts[name] === 'string' && scripts[name].trim() !== '',
234
+ );
235
+
236
+ process.exit(hasLifecycle ? 0 : 1);
237
+ } catch {
238
+ process.exit(0);
239
+ }
240
+ NODE
241
+ }
242
+
243
+ install_reuse_is_safe() {
244
+ local workspace package_path
245
+ for package_path in "$PROJECT_ROOT/.pnpmfile.cjs" "$PROJECT_ROOT/.pnpmfile.js"; do
246
+ if [ -e "$package_path" ]; then
247
+ echo "[node_modules] Reinstalling because pnpm hooks are present: $package_path"
248
+ return 1
249
+ fi
250
+ done
251
+
252
+ if [ -f "$PROJECT_ROOT/.npmrc" ] && grep -Eq '^[[:space:]]*pnpmfile[[:space:]]*=' "$PROJECT_ROOT/.npmrc"; then
253
+ echo "[node_modules] Reinstalling because .npmrc configures a pnpm hook."
254
+ return 1
255
+ fi
256
+
257
+ for workspace in . client server; do
258
+ package_path="$PROJECT_ROOT/$workspace/package.json"
259
+ [ -f "$package_path" ] || continue
260
+ if package_has_install_lifecycle "$package_path"; then
261
+ echo "[node_modules] Reinstalling because $package_path has an install lifecycle script."
262
+ return 1
263
+ fi
264
+ done
265
+ return 0
266
+ }
267
+
268
+ dependencies_are_ready() {
269
+ local workspace
270
+ [ -f "$WORK_DIR/node_modules/.modules.yaml" ] || return 1
271
+ for workspace in client server; do
272
+ if [ -f "$PROJECT_ROOT/$workspace/package.json" ] &&
273
+ [ ! -f "$WORK_DIR/$workspace/node_modules/.modules.yaml" ]; then
274
+ return 1
275
+ fi
276
+ done
277
+ return 0
278
+ }
279
+
280
+ # 判断顺序按代价从小到大排。指纹要读云盘上的 lockfile 和整个 patches/,所以放最后:只有其余
281
+ # 条件都满足、确实可能复用时才付这份开销。build.sh 和 prepare.sh 不设开关,第一行就返回了。
282
+ install_can_be_reused() {
283
+ local installed_fingerprint=""
284
+
285
+ [ "${COZE_REUSE_NODE_MODULES:-}" = "1" ] || return 1
286
+ install_requires_refresh "$@" && return 1
287
+ [ -f "$INSTALL_STATE_FILE" ] || return 1
288
+ IFS= read -r installed_fingerprint < "$INSTALL_STATE_FILE" || return 1
289
+ dependencies_are_ready || return 1
290
+ install_reuse_is_safe || return 1
291
+ [ "$installed_fingerprint" = "$(calculate_install_fingerprint "$@")" ] || return 1
292
+ return 0
293
+ }
294
+
295
+ write_install_state() {
296
+ local fingerprint="$1" temporary_state
297
+ temporary_state="$(mktemp "$WORK_DIR/.install-state.XXXXXX")"
298
+ printf '%s\n' "$fingerprint" > "$temporary_state"
299
+ mv -f "$temporary_state" "$INSTALL_STATE_FILE"
300
+ }
301
+
302
+ copy_install_metadata() {
303
+ local filename source_file target_file workspace
304
+ for filename in pnpm-lock.yaml pnpm-workspace.yaml .npmrc; do
305
+ source_file="$PROJECT_ROOT/$filename"
306
+ target_file="$WORK_DIR/$filename"
307
+ rm -f "$target_file"
308
+ if [ -f "$source_file" ]; then
309
+ cp "$source_file" "$target_file"
310
+ fi
311
+ done
312
+
313
+ link_install_entry "$PROJECT_ROOT/package.json" "$WORK_DIR/package.json"
314
+ link_install_entry "$PROJECT_ROOT/patches" "$WORK_DIR/patches"
315
+ link_install_entry "$PROJECT_ROOT/scripts" "$WORK_DIR/scripts"
316
+
317
+ for workspace in client server; do
318
+ if [ -f "$PROJECT_ROOT/$workspace/package.json" ]; then
319
+ mkdir -p "$WORK_DIR/$workspace"
320
+ link_install_entry "$PROJECT_ROOT/$workspace/package.json" "$WORK_DIR/$workspace/package.json"
321
+ link_install_entry "$PROJECT_ROOT/$workspace/scripts" "$WORK_DIR/$workspace/scripts"
322
+ else
323
+ rm -rf "$WORK_DIR/$workspace"
324
+ fi
325
+ done
326
+ }
327
+
328
+ link_node_modules() {
329
+ local source_dir="$1" target_dir="$2"
330
+ local current=""
331
+ mkdir -p "$target_dir"
332
+ if [ -L "$source_dir/node_modules" ]; then
333
+ current="$(readlink "$source_dir/node_modules")"
334
+ case "$current" in
335
+ "$NM_ROOT"/*/node_modules|"$NM_ROOT"/*/*/node_modules) ;;
336
+ *)
337
+ echo "[node_modules] refusing to replace unmanaged symlink: $source_dir/node_modules" >&2
338
+ exit 1
339
+ ;;
340
+ esac
341
+ rm "$source_dir/node_modules"
342
+ elif [ -e "$source_dir/node_modules" ]; then
343
+ rm -rf "$source_dir/node_modules"
344
+ fi
345
+ ln -s "$target_dir" "$source_dir/node_modules"
346
+ }
347
+
348
+ cleanup_legacy_shadow_project
349
+ copy_install_metadata
350
+
351
+ if install_can_be_reused "$@"; then
352
+ echo "[node_modules] Reusing unchanged dependencies in $WORK_DIR"
353
+ else
354
+ # A stale state file must not survive a failed install.
355
+ rm -f "$INSTALL_STATE_FILE"
356
+ echo "[node_modules] Installing dependencies in $WORK_DIR"
357
+ if ! (cd "$WORK_DIR" && pnpm install "$@"); then
358
+ exit 1
359
+ fi
360
+
361
+ if [ -f "$WORK_DIR/pnpm-lock.yaml" ]; then
362
+ temporary_lockfile="$(mktemp "$PROJECT_ROOT/.pnpm-lock.yaml.XXXXXX")"
363
+ cp "$WORK_DIR/pnpm-lock.yaml" "$temporary_lockfile"
364
+ chmod 0644 "$temporary_lockfile"
365
+ mv -f "$temporary_lockfile" "$PROJECT_ROOT/pnpm-lock.yaml"
366
+ fi
367
+
368
+ # pnpm may have rewritten the lockfile, so record the fingerprint of the tree as it
369
+ # now stands. Otherwise the next run would see a changed input and reinstall.
370
+ write_install_state "$(calculate_install_fingerprint "$@")"
371
+ fi
372
+
373
+ link_node_modules "$PROJECT_ROOT" "$WORK_DIR/node_modules"
374
+ for workspace in client server; do
375
+ if [ -f "$PROJECT_ROOT/$workspace/package.json" ]; then
376
+ link_node_modules "$PROJECT_ROOT/$workspace" "$WORK_DIR/$workspace/node_modules"
377
+ fi
378
+ done
379
+
380
+ echo "[node_modules] Dependencies ready"
@@ -13,7 +13,7 @@ cd "${COZE_WORKSPACE_PATH}"
13
13
 
14
14
  echo "Installing dependencies..."
15
15
  if [[ "${COZE_WEB_LOCAL_MIRROR:-}" != "1" ]]; then
16
- pnpm install --prefer-frozen-lockfile --prefer-offline
16
+ bash "$COZE_WORKSPACE_PATH/scripts/prepare-node-modules.sh" --prefer-frozen-lockfile --prefer-offline
17
17
  fi
18
18
 
19
19
  echo "Preparing Nuxt project..."
@@ -8,7 +8,7 @@ build = ["bash", ".cozeproj/scripts/dev_build.sh"]
8
8
  build_win = ["powershell.exe", "-NoProfile", "-ExecutionPolicy", "Bypass", "-File", ".cozeproj/scripts/dev_build.ps1"]
9
9
  run = ["bash", ".cozeproj/scripts/dev_run.sh"]
10
10
  run_win = ["powershell.exe", "-NoProfile", "-ExecutionPolicy", "Bypass", "-File", ".cozeproj/scripts/dev_run.ps1"]
11
- deps = ["git"] # -> apt install git
11
+ deps = ["git", "rsync"] # -> apt install git rsync
12
12
  pack = ["bash", ".cozeproj/scripts/pack.sh"]
13
13
  pack_win = ["powershell.exe", "-NoProfile", "-ExecutionPolicy", "Bypass", "-File", ".cozeproj/scripts/pack.ps1"]
14
14
  validate = ["bash", ".cozeproj/scripts/validate.sh"]
@@ -17,4 +17,4 @@ validate_win = ["powershell.exe", "-NoProfile", "-ExecutionPolicy", "Bypass", "-
17
17
  [deploy]
18
18
  build = ["bash", ".cozeproj/scripts/deploy_build.sh"]
19
19
  run = ["bash", ".cozeproj/scripts/deploy_run.sh"]
20
- deps = ["git"] # -> apt install git
20
+ deps = ["git", "rsync"] # -> apt install git rsync
@@ -2,6 +2,9 @@
2
2
  set -Eeuo pipefail
3
3
 
4
4
  ROOT_DIR="$(cd "$(dirname "$0")/../.." && pwd)"
5
+ if [[ "${COZE_TARO_LOCAL_ACTIVE:-}" != "${ROOT_DIR}" ]]; then
6
+ exec node "$ROOT_DIR/.cozeproj/scripts/local-workspace.cjs" build "$@"
7
+ fi
5
8
  COZE_WORKSPACE_PATH="${COZE_WORKSPACE_PATH:-${ROOT_DIR}}"
6
9
  export COZE_WORKSPACE_PATH
7
10
 
@@ -15,7 +18,9 @@ else
15
18
  fi
16
19
  echo "Installing dependencies..."
17
20
  # 安装所有依赖(包含 Taro 核心和 React)
18
- bash "$ROOT_DIR/.cozeproj/scripts/prepare-node-modules.sh" --prefer-frozen-lockfile --prefer-offline
21
+ if [[ "${COZE_TARO_LOCAL_MIRROR:-}" != "1" ]]; then
22
+ bash "$ROOT_DIR/.cozeproj/scripts/prepare-node-modules.sh" --prefer-frozen-lockfile --prefer-offline
23
+ fi
19
24
 
20
25
  echo "Building the Taro project..."
21
26
  pnpm build
@@ -2,4 +2,13 @@
2
2
  set -Eeuo pipefail
3
3
 
4
4
  ROOT_DIR="$(cd "$(dirname "$0")/../.." && pwd)"
5
- bash "$ROOT_DIR/.cozeproj/scripts/prepare-node-modules.sh" --prefer-frozen-lockfile --prefer-offline
5
+ if [[ "${COZE_TARO_LOCAL_ACTIVE:-}" != "${ROOT_DIR}" ]]; then
6
+ exec node "$ROOT_DIR/.cozeproj/scripts/local-workspace.cjs" prepare "$@"
7
+ fi
8
+ COZE_WORKSPACE_PATH="${ROOT_DIR}"
9
+ export COZE_WORKSPACE_PATH
10
+
11
+ cd "${COZE_WORKSPACE_PATH}"
12
+ if [[ "${COZE_TARO_LOCAL_MIRROR:-}" != "1" ]]; then
13
+ bash "$ROOT_DIR/.cozeproj/scripts/prepare-node-modules.sh" --prefer-frozen-lockfile --prefer-offline
14
+ fi
@@ -3,6 +3,9 @@ echo "⚙️ dev_run.sh 开始运行"
3
3
  set -Eeuo pipefail
4
4
 
5
5
  ROOT_DIR="$(cd "$(dirname "$0")/../.." && pwd)"
6
+ if [[ "${COZE_TARO_LOCAL_ACTIVE:-}" != "${ROOT_DIR}" ]]; then
7
+ exec node "$ROOT_DIR/.cozeproj/scripts/local-workspace.cjs" dev "$@"
8
+ fi
6
9
  COZE_WORKSPACE_PATH="${COZE_WORKSPACE_PATH:-${ROOT_DIR}}"
7
10
  export COZE_WORKSPACE_PATH
8
11
  cd "${COZE_WORKSPACE_PATH}"
@@ -10,7 +13,7 @@ cd "${COZE_WORKSPACE_PATH}"
10
13
  # ---------------------------------------------------------
11
14
  # 项目级日志目录
12
15
  # ---------------------------------------------------------
13
- LOG_DIR="${COZE_WORKSPACE_PATH}/logs"
16
+ LOG_DIR="${COZE_LOG_DIR:-${COZE_WORKSPACE_PATH}/logs}"
14
17
  LOG_FILE="${LOG_DIR}/dev.log"
15
18
  PID_FILE="${LOG_DIR}/dev.pid"
16
19
  DEV_PID=""
@@ -119,8 +122,15 @@ process_belongs_to_workspace() {
119
122
 
120
123
  case "${cwd}" in
121
124
  "${COZE_WORKSPACE_PATH}"|"${COZE_WORKSPACE_PATH}"/*) return 0 ;;
122
- *) return 1 ;;
123
125
  esac
126
+
127
+ if [[ -n "${COZE_TARO_SOURCE_PATH:-}" ]]; then
128
+ case "${cwd}" in
129
+ "${COZE_TARO_SOURCE_PATH}"|"${COZE_TARO_SOURCE_PATH}"/*) return 0 ;;
130
+ esac
131
+ fi
132
+
133
+ return 1
124
134
  }
125
135
 
126
136
  process_group_belongs_to_workspace() {
@@ -214,7 +224,9 @@ cleanup_previous_run() {
214
224
  # ---------------------------------------------------------
215
225
  echo "📦 Installing dependencies..."
216
226
  PNPM_BIN="$(command -v pnpm)"
217
- bash "$ROOT_DIR/.cozeproj/scripts/prepare-node-modules.sh" --prefer-frozen-lockfile --prefer-offline
227
+ if [[ "${COZE_TARO_LOCAL_MIRROR:-}" != "1" ]]; then
228
+ bash "$ROOT_DIR/.cozeproj/scripts/prepare-node-modules.sh" --prefer-frozen-lockfile --prefer-offline
229
+ fi
218
230
  echo "✅ Dependencies installed successfully!"
219
231
 
220
232
  # ---------------------------------------------------------
@@ -256,7 +268,7 @@ start_service() {
256
268
  DEV_PID="$(spawn_detached \
257
269
  "${COZE_WORKSPACE_PATH}" \
258
270
  "${LOG_FILE}" \
259
- "${PNPM_BIN}" dev)"
271
+ "$(command -v node)" "$ROOT_DIR/.cozeproj/scripts/local-workspace.cjs" watch "${PNPM_BIN}" dev)"
260
272
  if [[ -z "${DEV_PID}" ]]; then
261
273
  echo "❌ 无法获取 dev 后台进程 PID"
262
274
  return 1
@@ -279,5 +291,45 @@ start_service() {
279
291
  echo "PID file: ${PID_FILE}"
280
292
  }
281
293
 
294
+ warmup_preview() {
295
+ local base="http://127.0.0.1:${PORT}"
296
+ local waited=0
297
+
298
+ if ! command -v curl >/dev/null 2>&1; then
299
+ echo "Warmup skipped: curl is not available."
300
+ return 0
301
+ fi
302
+
303
+ echo "🔥 Warmup: waiting for web server to accept connections..."
304
+ while (( waited < 60 )); do
305
+ if curl -s -o /dev/null --max-time 3 "${base}/"; then
306
+ break
307
+ fi
308
+ sleep 2
309
+ waited=$(( waited + 2 ))
310
+ done
311
+
312
+ local app_config="${COZE_WORKSPACE_PATH}/src/app.config.ts"
313
+ local pages=""
314
+ if [[ -f "${app_config}" ]]; then
315
+ pages=$(grep -oE "[\"']pages/[^\"']+[\"']" "${app_config}" | tr -d "\"'" | sort -u || true)
316
+ fi
317
+
318
+ local p
319
+ for p in "/" "/app.config.ts"; do
320
+ echo "🔥 Warmup ${p}"
321
+ curl -s -o /dev/null --max-time 300 "${base}${p}" || echo "Warmup ${p} timed out or failed (ignored)."
322
+ done
323
+ for p in ${pages}; do
324
+ echo "🔥 Warmup /${p}.tsx"
325
+ curl -s -o /dev/null --max-time 300 "${base}/${p}.tsx" || echo "Warmup /${p}.tsx timed out or failed (ignored)."
326
+ done
327
+ }
328
+
282
329
  echo "Starting HTTP services on port ${PORT} (web) and ${SERVER_PORT} (server)..."
283
330
  start_service
331
+
332
+ if [[ "${COZE_TARO_LOCAL_MIRROR:-}" = "1" && "${COZE_DEV_SKIP_WARMUP:-0}" != "1" ]]; then
333
+ warmup_preview || true
334
+ echo "✅ Warmup finished (or timed out). Preview should load fast now."
335
+ fi