fix: prevent Pascal chunk load failures

This commit is contained in:
134 2026-07-13 11:42:04 +08:00
parent 7ccbc87afd
commit aedf590e1d
6 changed files with 129 additions and 13 deletions

View File

@ -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 \

View File

@ -29,6 +29,8 @@ supabase/.temp/
# Build Outputs
.next/
.next-build/
.next-previous/
out/
build
dist

View File

@ -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"
>
<head>
<Script id="pascal-chunk-recovery" strategy="beforeInteractive">
{chunkRecoveryScript}
</Script>
{process.env.NODE_ENV === 'development' && (
<script async crossOrigin="anonymous" src="//unpkg.com/react-scan/dist/auto.global.js" />
)}

View File

@ -1,10 +1,14 @@
import path from 'node:path'
import { fileURLToPath } from 'node:url'
import type { NextConfig } from 'next'
const configuredBasePath = process.env.NEXT_PUBLIC_PASCAL_BASE_PATH?.trim() ?? ''
const basePath = configuredBasePath ? `/${configuredBasePath.replace(/^\/+|\/+$/g, '')}` : undefined
const pascalRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..')
const nextConfig: NextConfig = {
basePath,
distDir: process.env.PASCAL_NEXT_DIST_DIR?.trim() || '.next',
logging: {
browserToTerminal: true,
},
@ -21,6 +25,7 @@ const nextConfig: NextConfig = {
'@dgreenheck/ez-tree',
],
turbopack: {
root: pascalRoot,
resolveAlias: {
react: './node_modules/react',
three: './node_modules/three',

View File

@ -1,5 +1,6 @@
import { Howl } from 'howler'
import useAudio from '../store/use-audio'
import { withEditorBasePath } from './public-asset-path'
// Per-sound variation config. Playback rate also shifts pitch (one semitone ≈ 1.0595×),
// so a rate range of ~0.88–1.12 reads as a subtle ±2 semitones — enough to kill the
@ -136,14 +137,14 @@ const lastVariation = new Map<SFXName, number>()
// Initialize all sounds
Object.entries(SFX).forEach(([name, config]) => {
const sources = Array.isArray(config.src) ? config.src : [config.src]
const sounds = sources.map(
(src) =>
new Howl({
src: [src],
preload: true,
volume: 0.5, // Will be adjusted by the bus
}),
)
const sounds = sources.map((src) => {
const resolvedSrc = withEditorBasePath(src)
return new Howl({
src: [resolvedSrc],
preload: true,
volume: 0.5, // Will be adjusted by the bus
})
})
sfxCache.set(name as SFXName, sounds)
})

View File

@ -5,7 +5,13 @@
"build": {
"dependsOn": ["^build"],
"inputs": ["$TURBO_DEFAULT$", ".env*"],
"outputs": [".next/**", "!.next/cache/**", "dist/**"],
"outputs": [
".next/**",
"!.next/cache/**",
".next-build/**",
"!.next-build/cache/**",
"dist/**"
],
"env": [
"SKIP_ENV_VALIDATION",
"BETTER_AUTH_SECRET",
@ -28,6 +34,7 @@
"PASCAL_DB_PATH",
"PASCAL_MCP_COMMAND",
"PASCAL_MCP_ENTRY",
"PASCAL_NEXT_DIST_DIR",
"NEXT_PUBLIC_PASCAL_BASE_PATH",
"PASCAL_REQUIRE_HOST_USER"
]