fix assets in dev

This commit is contained in:
Brandon Thomas
2026-07-12 04:30:54 -04:00
parent 8ad0de7f6a
commit 1aa5189c48
+67 -17
View File
@@ -7,6 +7,70 @@ import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin';
import { execSync } from 'child_process';
import path from 'path';
// Dev-only CDN proxy. Media assets (splats, GLBs, images) are fetched with
// fetch(), but the CDNs only send CORS headers for the production origin —
// not localhost. The web adapter's fetchAsset rewrites absolute CDN URLs to
// `/__cdn/<host>/<path>` in DEV; this middleware forwards them server-side
// (no browser CORS) to whatever host is embedded in the path.
//
// NB: This must be a middleware, not a `server.proxy` entry. The previous
// proxy config used a `router` option to pick the target host from the path,
// but `router` is an http-proxy-middleware feature — Vite's http-proxy
// silently ignores it, so every request went to the hardcoded default target
// and any asset on a different host (e.g. the dev R2 bucket) came back 403.
function cdnProxyPlugin() {
return {
name: 'cdn-proxy',
configureServer(server: {
middlewares: {
use: (
route: string,
handler: (
req: import('http').IncomingMessage,
res: import('http').ServerResponse,
) => void,
) => void;
};
}) {
server.middlewares.use('/__cdn', async (req, res) => {
try {
// Connect strips the `/__cdn` mount prefix from req.url.
const match = (req.url ?? '').match(/^\/([^/?]+)(\/[^?]*)?(\?.*)?$/);
if (!match) {
res.statusCode = 400;
res.end('Bad /__cdn path; expected /__cdn/<host>/<path>');
return;
}
const [, host, assetPath = '/', query = ''] = match;
const upstream = await fetch(`https://${host}${assetPath}${query}`, {
method: req.method === 'HEAD' ? 'HEAD' : 'GET',
headers: req.headers.range
? { range: String(req.headers.range) }
: undefined,
});
res.statusCode = upstream.status;
for (const header of [
'content-type',
'content-length',
'accept-ranges',
'content-range',
'etag',
'cache-control',
]) {
const value = upstream.headers.get(header);
if (value) res.setHeader(header, value);
}
const body = Buffer.from(await upstream.arrayBuffer());
res.end(body);
} catch (e) {
res.statusCode = 502;
res.end(`CDN proxy error: ${e}`);
}
});
},
};
}
// Custom plugin to generate news.json on dev server start
function generateNewsPlugin() {
return {
@@ -38,29 +102,15 @@ export default defineConfig(() => ({
Origin: 'https://api.storyteller.ai',
},
},
// Dev-only CDN proxy. Media assets (splats, GLBs, images) are fetched
// from the CDN with fetch(), but the CDN only sends CORS headers for the
// production origin — not localhost. The web adapter's fetchAsset rewrites
// absolute CDN URLs to `/__cdn/<host>/<path>` in DEV; this forwards them
// server-side (no browser CORS). The target host is taken from the path
// so any cdn-N.fakeyou.com / Cloudflare host works.
'/__cdn': {
target: 'https://cdn-2.fakeyou.com',
changeOrigin: true,
secure: true,
router: (req) => {
const match = req.url?.match(/^\/__cdn\/([^/]+)/);
return match ? `https://${match[1]}` : 'https://cdn-2.fakeyou.com';
},
rewrite: (path) => path.replace(/^\/__cdn\/[^/]+/, ''),
},
// NB: `/__cdn` asset proxying lives in `cdnProxyPlugin` above — it
// needs per-request host routing, which `server.proxy` can't do.
},
},
preview:{
port: 4301,
host: 'localhost',
},
plugins: [generateNewsPlugin(), nxViteTsPaths(), react(), wasm(), topLevelAwait()],
plugins: [cdnProxyPlugin(), generateNewsPlugin(), nxViteTsPaths(), react(), wasm(), topLevelAwait()],
// Uncomment this if you are using workers.
// worker: {
// plugins: [ nxViteTsPaths() ],