From 392fe7ca6cca33cbcb63d016a124f4cd26c00a29 Mon Sep 17 00:00:00 2001 From: neverland Date: Wed, 3 Jun 2026 14:12:00 +0800 Subject: [PATCH] fix(core): serve preview assets from web dist paths --- e2e/cases/server/preview/index.test.ts | 63 ++++++++++++++- .../src/server/assets-middleware/index.ts | 4 +- .../server/assets-middleware/middleware.ts | 2 +- packages/core/src/server/devServer.ts | 13 ++-- packages/core/src/server/previewServer.ts | 76 ++++++++++++++----- packages/core/src/server/publicPathnames.ts | 23 ++++++ 6 files changed, 151 insertions(+), 30 deletions(-) create mode 100644 packages/core/src/server/publicPathnames.ts diff --git a/e2e/cases/server/preview/index.test.ts b/e2e/cases/server/preview/index.test.ts index 2ea34cfa24..1c72db7b7a 100644 --- a/e2e/cases/server/preview/index.test.ts +++ b/e2e/cases/server/preview/index.test.ts @@ -1,4 +1,5 @@ -import { expect, getRandomPort, test } from '@e2e/helper'; +import { basename } from 'node:path'; +import { expect, findFile, getRandomPort, test } from '@e2e/helper'; import type { RsbuildPlugin } from '@rsbuild/core'; test('should preview dist files correctly', async ({ page, buildPreview }) => { @@ -34,3 +35,63 @@ test('should allow plugin to modify preview server config', async ({ const rootEl = page.locator('#root'); await expect(rootEl).toHaveText('Hello Rsbuild!'); }); + +test('should serve multi-environment assets before returned setup middleware', async ({ + buildPreview, +}) => { + const result = await buildPreview({ + config: { + server: { + htmlFallback: false, + setup: ({ server }) => { + return () => { + server.middlewares.use((_req, res) => { + res.statusCode = 404; + res.setHeader('Content-Type', 'text/html; charset=utf-8'); + res.end('SSR fallback'); + }); + }; + }, + }, + environments: { + web: { + output: { + assetPrefix: '/app', + distPath: 'dist/client', + }, + }, + node: { + output: { + target: 'node', + distPath: 'dist/server', + }, + }, + }, + }, + }); + + const assetFile = findFile( + result.getDistFiles(), + /\/dist\/client\/static\/js\/.+\.js$/, + ); + + const res = await fetch( + `http://localhost:${result.port}/app/static/js/${basename(assetFile)}`, + ); + + expect(res.status).toBe(200); + expect(res.headers.get('content-type')).toContain('javascript'); + expect(await res.text()).toContain('Hello Rsbuild!'); + + const siblingPrefixRes = await fetch( + `http://localhost:${result.port}/app2/static/js/${basename(assetFile)}`, + ); + expect(siblingPrefixRes.status).toBe(404); + expect(await siblingPrefixRes.text()).toBe('SSR fallback'); + + const serverBundleRes = await fetch( + `http://localhost:${result.port}/index.js`, + ); + expect(serverBundleRes.status).toBe(404); + expect(await serverBundleRes.text()).toBe('SSR fallback'); +}); diff --git a/packages/core/src/server/assets-middleware/index.ts b/packages/core/src/server/assets-middleware/index.ts index 343e2c535a..c7c8b41027 100644 --- a/packages/core/src/server/assets-middleware/index.ts +++ b/packages/core/src/server/assets-middleware/index.ts @@ -29,7 +29,7 @@ import type { } from '../../types'; import { resolveHostname } from './../hmrFallback'; import type { SocketServer } from '../socketServer'; -import { createMiddleware } from './middleware'; +import { createAssetsMiddleware } from './middleware'; import { setupOutputFileSystem } from './setupOutputFileSystem'; import { resolveWriteToDiskConfig, setupWriteToDisk } from './setupWriteToDisk'; @@ -314,7 +314,7 @@ export const assetsMiddleware = async ({ } }; - const instance = createMiddleware( + const instance = createAssetsMiddleware( context, ready, outputFileSystem, diff --git a/packages/core/src/server/assets-middleware/middleware.ts b/packages/core/src/server/assets-middleware/middleware.ts index 47c7d1bae3..b2d85bd592 100644 --- a/packages/core/src/server/assets-middleware/middleware.ts +++ b/packages/core/src/server/assets-middleware/middleware.ts @@ -140,7 +140,7 @@ function sendError(res: ServerResponse, code: number): void { res.end(document); } -export function createMiddleware( +export function createAssetsMiddleware( context: InternalContext, ready: (callback: () => void) => void, outputFileSystem: Rspack.OutputFileSystem, diff --git a/packages/core/src/server/devServer.ts b/packages/core/src/server/devServer.ts index 153e85b42d..7fc9e67dd8 100644 --- a/packages/core/src/server/devServer.ts +++ b/packages/core/src/server/devServer.ts @@ -5,7 +5,6 @@ import { getPublicPathFromCompiler, isMultiCompiler, } from '../helpers/compiler'; -import { getPathnameFromUrl } from '../helpers/path'; import { onBeforeRestartServer, restartDevServer } from '../restart'; import type { CreateCompiler, @@ -36,7 +35,6 @@ import { getRoutes, getServerTerminator, printServerURLs, - removeBasePath, type RsbuildServerBase, resolvePort, type StartDevServerResult, @@ -44,6 +42,7 @@ import { import { createHttpServer } from './httpServer'; import { notFoundMiddleware, optionsFallbackMiddleware } from './middlewares'; import { open } from './open'; +import { getPublicPathnames } from './publicPathnames'; import { applyServerSetup } from './serverSetup'; import type { ServerMessage } from './socketServer'; import { setupWatchFiles, type WatchFilesResult } from './watchFiles'; @@ -169,12 +168,10 @@ export async function createDevServer< ? compiler.compilers.map(getPublicPathFromCompiler) : [getPublicPathFromCompiler(compiler)]; - const { base } = config.server; - context.publicPathnames = publicPaths - .map(getPathnameFromUrl) - .map((prefix) => - base && base !== '/' ? removeBasePath(prefix, base) : prefix, - ); + context.publicPathnames = getPublicPathnames( + publicPaths, + config.server.base, + ); compiler?.hooks.watchRun.tap('rsbuild:watchRun', () => { resetWaitLastCompileDone(); diff --git a/packages/core/src/server/previewServer.ts b/packages/core/src/server/previewServer.ts index 1ca6d2c005..38e4323607 100644 --- a/packages/core/src/server/previewServer.ts +++ b/packages/core/src/server/previewServer.ts @@ -1,10 +1,12 @@ -import { getPathnameFromUrl } from '../helpers/path'; +import fs from 'node:fs'; +import { isWebTarget } from '../helpers'; import { isVerbose } from '../logger'; import type { InternalContext, NormalizedConfig, PreviewOptions, } from '../types'; +import { createAssetsMiddleware } from './assets-middleware/middleware'; import { isCliShortcutsEnabled, setupCliShortcuts } from './cliShortcuts'; import { registerCleanup, @@ -16,6 +18,7 @@ import { getAddressUrls, getRoutes, getServerTerminator, + isUrlPathUnderBase, printServerURLs, type RsbuildServerBase, resolvePort, @@ -31,11 +34,26 @@ import { optionsFallbackMiddleware, } from './middlewares'; import { open } from './open'; +import { getPublicPathnames } from './publicPathnames'; import { createProxyMiddleware } from './proxy'; import { applyServerSetup } from './serverSetup'; export type RsbuildPreviewServer = RsbuildServerBase; +const getPreviewAssetContext = (context: InternalContext): InternalContext => { + const environmentList = context.environmentList.filter((environment) => + isWebTarget(environment.config.output.target), + ); + + return { + ...context, + environmentList, + publicPathnames: environmentList.map( + (environment) => context.publicPathnames[environment.index], + ), + }; +}; + export async function startPreviewServer( context: InternalContext, config: NormalizedConfig, @@ -52,6 +70,12 @@ export async function startPreviewServer( const serverConfig = config.server; const { host, headers, proxy, historyApiFallback, compress, base, cors } = serverConfig; + + const assetPrefixes = context.environmentList.map( + (environment) => environment.config.output.assetPrefix, + ); + context.publicPathnames = getPublicPathnames(assetPrefixes, base); + const isHttps = Boolean(serverConfig.https); const protocol = isHttps ? 'https' : 'http'; const routes = getRoutes(context); @@ -127,6 +151,13 @@ export async function startPreviewServer( const { default: sirv } = await import( /* webpackChunkName: "sirv" */ 'sirv' ); + const previewAssetContext = getPreviewAssetContext(context); + + const diskAssetsMiddleware = createAssetsMiddleware( + previewAssetContext, + (callback) => callback(), + fs, + ); const assetsMiddleware = sirv(context.distPath, { etag: true, @@ -135,25 +166,34 @@ export async function startPreviewServer( single: serverConfig.htmlFallback === 'index', }); - const assetPrefixes = context.environmentList.map((e) => - getPathnameFromUrl(e.config.output.assetPrefix), - ); + const assetPrefixes = previewAssetContext.publicPathnames; middlewares.use(function staticAssetMiddleware(req, res, next) { - const { url } = req; - const assetPrefix = - url && assetPrefixes.find((prefix) => url.startsWith(prefix)); - - // handling assetPrefix - if (assetPrefix && url?.startsWith(assetPrefix)) { - req.url = url.slice(assetPrefix.length); - assetsMiddleware(req, res, (...args: unknown[]) => { - req.url = url; - next(...args); - }); - } else { - assetsMiddleware(req, res, next); - } + diskAssetsMiddleware(req, res, (err) => { + if (err) { + next(err); + return; + } + + const { url } = req; + const assetPrefix = + url && + assetPrefixes.find( + (prefix) => + prefix && prefix !== '/' && isUrlPathUnderBase(url, prefix), + ); + + // handling assetPrefix + if (assetPrefix) { + req.url = url.slice(assetPrefix.length); + assetsMiddleware(req, res, (...args: unknown[]) => { + req.url = url; + next(...args); + }); + } else { + assetsMiddleware(req, res, next); + } + }); }); }; diff --git a/packages/core/src/server/publicPathnames.ts b/packages/core/src/server/publicPathnames.ts new file mode 100644 index 0000000000..578bd8c5b4 --- /dev/null +++ b/packages/core/src/server/publicPathnames.ts @@ -0,0 +1,23 @@ +import { getPathnameFromUrl } from '../helpers/path'; +import { removeBasePath } from './helper'; + +export const getPublicPathname = (publicPath: string): string => { + if (publicPath === 'auto' || publicPath === '') { + return ''; + } + + return getPathnameFromUrl( + publicPath.endsWith('/') ? publicPath : `${publicPath}/`, + ); +}; + +export const getPublicPathnames = ( + publicPaths: string[], + base: string, +): string[] => { + return publicPaths + .map(getPublicPathname) + .map((prefix) => + base && base !== '/' ? removeBasePath(prefix, base) : prefix, + ); +};