From aedf590e1d936e326f2ef4a01f68409cd80a51bd Mon Sep 17 00:00:00 2001 From: 134 Date: Mon, 13 Jul 2026 11:42:04 +0800 Subject: [PATCH] fix: prevent Pascal chunk load failures --- scripts/deploy-pascal-production.sh | 67 +++++++++++++++++-- services/pascal-editor/.gitignore | 2 + .../pascal-editor/apps/editor/app/layout.tsx | 42 ++++++++++++ .../pascal-editor/apps/editor/next.config.ts | 5 ++ .../packages/editor/src/lib/sfx-player.ts | 17 ++--- services/pascal-editor/turbo.json | 9 ++- 6 files changed, 129 insertions(+), 13 deletions(-) diff --git a/scripts/deploy-pascal-production.sh b/scripts/deploy-pascal-production.sh index 61526e394..0d7c311c8 100755 --- a/scripts/deploy-pascal-production.sh +++ b/scripts/deploy-pascal-production.sh @@ -213,10 +213,15 @@ sync_pascal_source() { --exclude '.env.production' \ --exclude '.git/' \ --exclude '.next/' \ + --exclude '.next-build/' \ + --exclude '.next-previous/' \ --exclude '.turbo/' \ --exclude 'node_modules/' \ "$PASCAL_SOURCE_DIR/" "$PASCAL_INSTALL_DIR/" +} + +write_pascal_source_version() { if git -C "$MAIN_APP_DIR" rev-parse --verify HEAD >/dev/null 2>&1; then git -C "$MAIN_APP_DIR" rev-parse HEAD >"$PASCAL_INSTALL_DIR/.pascal-source-version" fi @@ -248,10 +253,21 @@ write_pascal_env() { } build_pascal() { + local editor_dir="$PASCAL_INSTALL_DIR/apps/editor" + local current_build="$editor_dir/.next" + local staged_build="$editor_dir/.next-build" + local previous_build="$editor_dir/.next-previous" + mkdir -p "$PASCAL_DATA_DIR" chmod 700 "$PASCAL_DATA_DIR" export PATH="$(dirname "$PASCAL_BUN_BIN"):$PATH" log "Installing and building vendored Pascal service" + + [ "$staged_build" != "$current_build" ] || die "Pascal staged build path is invalid" + [ "$previous_build" != "$current_build" ] || die "Pascal previous build path is invalid" + [ ! -e "$staged_build" ] || rm -r "$staged_build" + [ ! -e "$previous_build" ] || rm -r "$previous_build" + ( cd "$PASCAL_INSTALL_DIR" "$PASCAL_BUN_BIN" install --frozen-lockfile @@ -259,19 +275,55 @@ build_pascal() { # shellcheck disable=SC1091 source .env.production set +a - "$PASCAL_BUN_BIN" x turbo run build --filter=editor --output-logs=errors-only + PASCAL_NEXT_DIST_DIR=.next-build \ + "$PASCAL_BUN_BIN" x turbo run build --filter=editor --output-logs=errors-only --force ) + + [ -f "$staged_build/BUILD_ID" ] || die "Pascal staged build did not produce a BUILD_ID" + if [ -d "$current_build/static" ]; then + log "Retaining static chunks from the active Pascal build" + mkdir -p "$staged_build/static" + rsync -a --ignore-existing "$current_build/static/" "$staged_build/static/" + find "$staged_build/static" -type f -mtime +7 -delete + find "$staged_build/static" -depth -type d -empty -delete + fi +} + +activate_pascal_build() { + local editor_dir="$PASCAL_INSTALL_DIR/apps/editor" + local current_build="$editor_dir/.next" + local staged_build="$editor_dir/.next-build" + local previous_build="$editor_dir/.next-previous" + + [ -f "$staged_build/BUILD_ID" ] || die "Pascal staged build is unavailable" + if [ -e "$current_build" ]; then + mv "$current_build" "$previous_build" + fi + if ! mv "$staged_build" "$current_build"; then + [ ! -e "$previous_build" ] || mv "$previous_build" "$current_build" + die "Could not activate the staged Pascal build" + fi +} + +rollback_pascal_build() { + local editor_dir="$PASCAL_INSTALL_DIR/apps/editor" + local current_build="$editor_dir/.next" + local previous_build="$editor_dir/.next-previous" + + [ -e "$previous_build" ] || return 1 + [ ! -e "$current_build" ] || rm -r "$current_build" + mv "$previous_build" "$current_build" } start_pascal() { require_command pm2 if pm2 describe "$PASCAL_PM2_NAME" >/dev/null 2>&1; then - pm2 restart "$PASCAL_PM2_NAME" + pm2 restart "$PASCAL_PM2_NAME" || return 1 else pm2 start /usr/bin/bash \ --name "$PASCAL_PM2_NAME" \ --cwd "$PASCAL_INSTALL_DIR" \ - -- -lc "set -a; source '$PASCAL_INSTALL_DIR/.env.production'; set +a; export PATH='$(dirname "$PASCAL_BUN_BIN")':\$PATH; exec '$PASCAL_BUN_BIN' run --cwd apps/editor start -- -p '$PASCAL_PORT' -H 127.0.0.1" + -- -lc "set -a; source '$PASCAL_INSTALL_DIR/.env.production'; set +a; export PATH='$(dirname "$PASCAL_BUN_BIN")':\$PATH; exec '$PASCAL_BUN_BIN' run --cwd apps/editor start -- -p '$PASCAL_PORT' -H 127.0.0.1" || return 1 fi pm2 save } @@ -397,10 +449,17 @@ main() { sync_pascal_source write_pascal_env "$secret" build_pascal - start_pascal + activate_pascal_build + if ! start_pascal; then + log "Pascal restart failed; rolling back the previous build" + rollback_pascal_build || true + start_pascal || true + die "Pascal service failed to start after deployment" + fi render_nginx "$secret" sleep 3 check_deployment "$secret" + write_pascal_source_version if is_true "$RUN_AGENT_SMOKE_TEST"; then PASCAL_SMOKE_CONFIRM=1 \ diff --git a/services/pascal-editor/.gitignore b/services/pascal-editor/.gitignore index 2ef6592f0..d88ab7048 100644 --- a/services/pascal-editor/.gitignore +++ b/services/pascal-editor/.gitignore @@ -29,6 +29,8 @@ supabase/.temp/ # Build Outputs .next/ +.next-build/ +.next-previous/ out/ build dist diff --git a/services/pascal-editor/apps/editor/app/layout.tsx b/services/pascal-editor/apps/editor/app/layout.tsx index 043773820..8296a2f63 100644 --- a/services/pascal-editor/apps/editor/app/layout.tsx +++ b/services/pascal-editor/apps/editor/app/layout.tsx @@ -2,9 +2,48 @@ import { Agentation } from 'agentation' import { GeistPixelSquare } from 'geist/font/pixel' import { Barlow } from 'next/font/google' import localFont from 'next/font/local' +import Script from 'next/script' import { ClientBootstrap } from './client-bootstrap' import './globals.css' +const chunkRecoveryScript = ` + (() => { + const marker = 'pascal:chunk-recovery-at' + const recoveryParam = '__pascal_reload' + const chunkError = /ChunkLoadError|Failed to load chunk|Loading chunk .* failed|CSS_CHUNK_LOAD_FAILED/i + + const recover = (event) => { + const reason = event?.reason + const message = [ + event?.message, + reason?.message, + typeof reason === 'string' ? reason : '', + ].filter(Boolean).join(' ') + if (!chunkError.test(message)) return + + const now = Date.now() + const lastRecovery = Number(sessionStorage.getItem(marker) || 0) + if (now - lastRecovery < 60000) return + + sessionStorage.setItem(marker, String(now)) + const url = new URL(window.location.href) + url.searchParams.set(recoveryParam, String(now)) + window.location.replace(url.toString()) + } + + window.addEventListener('error', recover, true) + window.addEventListener('unhandledrejection', recover) + + window.setTimeout(() => { + sessionStorage.removeItem(marker) + const url = new URL(window.location.href) + if (!url.searchParams.has(recoveryParam)) return + url.searchParams.delete(recoveryParam) + window.history.replaceState(window.history.state, '', url.toString()) + }, 10000) + })() +` + const geistSans = localFont({ src: './fonts/GeistVF.woff', variable: '--font-geist-sans', @@ -32,6 +71,9 @@ export default function RootLayout({ lang="zh-CN" > + {process.env.NODE_ENV === 'development' && (