Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 62 additions & 1 deletion e2e/cases/server/preview/index.test.ts
Original file line number Diff line number Diff line change
@@ -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 }) => {
Expand Down Expand Up @@ -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('<html>SSR fallback</html>');
});
};
},
},
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('<html>SSR fallback</html>');

const serverBundleRes = await fetch(
`http://localhost:${result.port}/index.js`,
);
expect(serverBundleRes.status).toBe(404);
expect(await serverBundleRes.text()).toBe('<html>SSR fallback</html>');
Comment thread
chenjiahan marked this conversation as resolved.
});
4 changes: 2 additions & 2 deletions packages/core/src/server/assets-middleware/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -314,7 +314,7 @@ export const assetsMiddleware = async ({
}
};

const instance = createMiddleware(
const instance = createAssetsMiddleware(
context,
ready,
outputFileSystem,
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/server/assets-middleware/middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
13 changes: 5 additions & 8 deletions packages/core/src/server/devServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import {
getPublicPathFromCompiler,
isMultiCompiler,
} from '../helpers/compiler';
import { getPathnameFromUrl } from '../helpers/path';
import { onBeforeRestartServer, restartDevServer } from '../restart';
import type {
CreateCompiler,
Expand Down Expand Up @@ -36,14 +35,14 @@ import {
getRoutes,
getServerTerminator,
printServerURLs,
removeBasePath,
type RsbuildServerBase,
resolvePort,
type StartDevServerResult,
} from './helper';
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';
Expand Down Expand Up @@ -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();
Expand Down
76 changes: 58 additions & 18 deletions packages/core/src/server/previewServer.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -16,6 +18,7 @@ import {
getAddressUrls,
getRoutes,
getServerTerminator,
isUrlPathUnderBase,
printServerURLs,
type RsbuildServerBase,
resolvePort,
Expand All @@ -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,
Expand All @@ -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);
Expand Down Expand Up @@ -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, {
Comment thread
chenjiahan marked this conversation as resolved.
etag: true,
Expand All @@ -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);
Comment thread
chenjiahan marked this conversation as resolved.
}
});
});
};

Expand Down
23 changes: 23 additions & 0 deletions packages/core/src/server/publicPathnames.ts
Original file line number Diff line number Diff line change
@@ -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,
);
};
Loading