diff --git a/src/server/PublicAssetManifest.ts b/src/server/PublicAssetManifest.ts index ff31957e8..400e80f8f 100644 --- a/src/server/PublicAssetManifest.ts +++ b/src/server/PublicAssetManifest.ts @@ -375,7 +375,7 @@ export function writePublicAssetManifest( outDir: string, assetManifest: AssetManifest, ): void { - const manifestPath = path.join(outDir, "_assets", "asset-manifest.json"); + const manifestPath = path.join(outDir, "asset-manifest.json"); fs.mkdirSync(path.dirname(manifestPath), { recursive: true }); fs.writeFileSync(manifestPath, `${JSON.stringify(assetManifest, null, 2)}\n`); } diff --git a/src/server/RuntimeAssetManifest.ts b/src/server/RuntimeAssetManifest.ts index ce118946c..a25854feb 100644 --- a/src/server/RuntimeAssetManifest.ts +++ b/src/server/RuntimeAssetManifest.ts @@ -6,7 +6,7 @@ import type { AssetManifest } from "../core/AssetUrls"; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const staticDir = path.join(__dirname, "../../static"); -const manifestPath = path.join(staticDir, "_assets", "asset-manifest.json"); +const manifestPath = path.join(staticDir, "asset-manifest.json"); let cachedManifest: AssetManifest | null = null; diff --git a/update.sh b/update.sh index 4f198f44d..3a69e4dee 100755 --- a/update.sh +++ b/update.sh @@ -53,18 +53,19 @@ EXTRACT_DIR="$(mktemp -d -t openfront-assets-XXXXXX)" trap 'rm -rf "$EXTRACT_DIR"' EXIT TMP_CONTAINER="$(docker create "${GHCR_IMAGE}")" -if ! docker cp "${TMP_CONTAINER}:/usr/src/app/static/_assets" "$EXTRACT_DIR/"; then +if ! docker cp "${TMP_CONTAINER}:/usr/src/app/static" "$EXTRACT_DIR/"; then echo "❌ docker cp failed" docker rm "${TMP_CONTAINER}" > /dev/null 2>&1 || true exit 1 fi docker rm "${TMP_CONTAINER}" > /dev/null -echo "Extracted to $EXTRACT_DIR; top-level contents:" -ls -la "$EXTRACT_DIR/" || true +STATIC_DIR="$EXTRACT_DIR/static" +echo "Extracted to $STATIC_DIR; top-level contents:" +ls -la "$STATIC_DIR/" || true R2_ENDPOINT="https://api.${DOMAIN}" -MANIFEST="$EXTRACT_DIR/_assets/asset-manifest.json" +MANIFEST="$STATIC_DIR/asset-manifest.json" if [ ! -f "$MANIFEST" ]; then echo "❌ Manifest not found at $MANIFEST" exit 1 @@ -101,7 +102,7 @@ if [ -z "$MISSING" ]; then else MISSING_COUNT="$(echo "$MISSING" | wc -l | tr -d ' ')" echo "Uploading $MISSING_COUNT missing asset(s)..." - export R2_ENDPOINT API_KEY EXTRACT_DIR + export R2_ENDPOINT API_KEY STATIC_DIR # KEY from the manifest is URL-encoded per segment (e.g. flags/C%C3%B4te.png). # Files on disk live at the *decoded* path, so decode KEY before reading the # file, then encode the whole decoded path as one URL segment for the POST. @@ -132,7 +133,7 @@ else "$R2_ENDPOINT/game_assets/upload/$ENC" \ -H "X-API-Key: $API_KEY" \ -H "Content-Type: application/octet-stream" \ - --data-binary "@$EXTRACT_DIR/$DECODED" > /dev/null; then + --data-binary "@$STATIC_DIR/$DECODED" > /dev/null; then echo "❌ Failed to upload: $DECODED" >&2 exit 1 fi diff --git a/vite.config.ts b/vite.config.ts index 0a520a93a..9cfbdd148 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -71,13 +71,29 @@ export default defineConfig(({ mode }) => { mobileLogoImageUrl: buildAssetUrl("images/OF.png", assetManifest), }; - const syncHashedPublicAssets = () => ({ + let viteBundleFiles: string[] = []; + const syncHashedPublicAssets = (): Plugin => ({ name: "sync-hashed-public-assets", apply: "build" as const, + writeBundle(_options, bundle) { + viteBundleFiles = Object.keys(bundle); + }, closeBundle() { const outDir = path.join(__dirname, "static"); copyRootPublicFiles(resourcesDir, outDir); + // Run the source→hashed copy first; createHashedPublicAssetFiles iterates + // assetManifest and expects every key to resolve to a file in resources/ + // or proprietary/. Vite's bundle output (assets/...) doesn't, so it's + // merged in after. createHashedPublicAssetFiles(sourceDirs, outDir, assetManifest); + // Track Vite's own bundle output (vendor chunks, JS, CSS, workers under + // static/assets/) in the manifest so the deploy-time R2 upload covers + // them alongside the hashed source assets. Skip non-assets/ emits like + // index.html — those are served by the app, not from R2. + for (const fileName of viteBundleFiles) { + if (!fileName.startsWith("assets/")) continue; + assetManifest[fileName] = `/${fileName}`; + } writePublicAssetManifest(outDir, assetManifest); }, });