diff --git a/e2e/cases/server/cors/index.test.ts b/e2e/cases/server/cors/index.test.ts new file mode 100644 index 0000000000..9765657f93 --- /dev/null +++ b/e2e/cases/server/cors/index.test.ts @@ -0,0 +1,70 @@ +import { build, dev } from '@e2e/helper'; +import { expect, test } from '@playwright/test'; + +test('should include CORS headers by default for dev server', async ({ + page, + request, +}) => { + const rsbuild = await dev({ + cwd: __dirname, + page, + }); + + const response = await request.get(`http://localhost:${rsbuild.port}`); + expect(response.headers()['access-control-allow-origin']).toEqual('*'); + + await rsbuild.close(); +}); + +test('should include CORS headers by default for preview server', async ({ + page, + request, +}) => { + const rsbuild = await build({ + cwd: __dirname, + page, + }); + + const response = await request.get(`http://localhost:${rsbuild.port}`); + expect(response.headers()['access-control-allow-origin']).toEqual('*'); + + await rsbuild.close(); +}); + +test('should allow to disable CORS', async ({ page, request }) => { + const rsbuild = await build({ + cwd: __dirname, + page, + rsbuildConfig: { + server: { + cors: false, + }, + }, + }); + + const response = await request.get(`http://localhost:${rsbuild.port}`); + expect(response.headers()).not.toHaveProperty('access-control-allow-origin'); + + await rsbuild.close(); +}); + +test('should allow to configure CORS', async ({ page, request }) => { + const rsbuild = await build({ + cwd: __dirname, + page, + rsbuildConfig: { + server: { + cors: { + origin: 'https://example.com', + }, + }, + }, + }); + + const response = await request.get(`http://localhost:${rsbuild.port}`); + expect(response.headers()['access-control-allow-origin']).toEqual( + 'https://example.com', + ); + + await rsbuild.close(); +}); diff --git a/e2e/cases/server/cors/src/index.js b/e2e/cases/server/cors/src/index.js new file mode 100644 index 0000000000..03a16d74b4 --- /dev/null +++ b/e2e/cases/server/cors/src/index.js @@ -0,0 +1 @@ +console.log('Hello Rsbuild!'); diff --git a/packages/core/package.json b/packages/core/package.json index ffa9513ac2..43e1534575 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -68,6 +68,7 @@ "commander": "^12.1.0", "connect": "3.7.0", "connect-history-api-fallback": "^2.0.0", + "cors": "^2.8.5", "css-loader": "7.1.2", "deepmerge": "^4.3.1", "dotenv": "16.4.7", diff --git a/packages/core/prebundle.config.mjs b/packages/core/prebundle.config.mjs index b9743c9e21..3e104b4595 100644 --- a/packages/core/prebundle.config.mjs +++ b/packages/core/prebundle.config.mjs @@ -41,6 +41,7 @@ export default { 'mrmime', 'tinyglobby', 'chokidar', + 'cors', { name: 'picocolors', beforeBundle({ depPath }) { diff --git a/packages/core/src/config.ts b/packages/core/src/config.ts index 6a5d70e3b9..fe33849230 100644 --- a/packages/core/src/config.ts +++ b/packages/core/src/config.ts @@ -85,6 +85,7 @@ const getDefaultServerConfig = (): NormalizedServerConfig => ({ compress: true, printUrls: true, strictPort: false, + cors: true, }); let swcHelpersPath: string; diff --git a/packages/core/src/server/getDevMiddlewares.ts b/packages/core/src/server/getDevMiddlewares.ts index e4f95ab8eb..02062f1822 100644 --- a/packages/core/src/server/getDevMiddlewares.ts +++ b/packages/core/src/server/getDevMiddlewares.ts @@ -108,6 +108,15 @@ const applyDefaultMiddlewares = async ({ next(); }); + if (server.cors) { + const { default: corsMiddleware } = await import( + '../../compiled/cors/index.js' + ); + middlewares.push( + corsMiddleware(typeof server.cors === 'boolean' ? {} : server.cors), + ); + } + // dev proxy handler, each proxy has own handler if (server.proxy) { const { middlewares: proxyMiddlewares, upgrade } = diff --git a/packages/core/src/server/prodServer.ts b/packages/core/src/server/prodServer.ts index e13e430e00..f8f6e842d5 100644 --- a/packages/core/src/server/prodServer.ts +++ b/packages/core/src/server/prodServer.ts @@ -56,7 +56,7 @@ export class RsbuildProdServer { } private async applyDefaultMiddlewares() { - const { headers, proxy, historyApiFallback, compress, base } = + const { headers, proxy, historyApiFallback, compress, base, cors } = this.options.serverConfig; if (logger.level === 'verbose') { @@ -82,6 +82,15 @@ export class RsbuildProdServer { }); } + if (cors) { + const { default: corsMiddleware } = await import( + '../../compiled/cors/index.js' + ); + this.middlewares.use( + corsMiddleware(typeof cors === 'boolean' ? {} : cors), + ); + } + if (proxy) { const { middlewares, upgrade } = await createProxyMiddleware(proxy); diff --git a/packages/core/src/types/config.ts b/packages/core/src/types/config.ts index 364e4b0bb9..2dc0896333 100644 --- a/packages/core/src/types/config.ts +++ b/packages/core/src/types/config.ts @@ -13,6 +13,7 @@ import type { rspack, } from '@rspack/core'; import type { ChokidarOptions } from '../../compiled/chokidar/index.js'; +import type cors from '../../compiled/cors/index.js'; import type { Options as HttpProxyOptions, Filter as ProxyFilter, @@ -384,6 +385,15 @@ export interface ServerConfig { target?: string | string[]; before?: () => Promise | void; }; + /** + * Configure CORS for the dev server or preview server. + * - true: enable CORS with default options. + * - false: disable CORS. + * - object: enable CORS with the specified options. + * @default true + * @link https://github.com/expressjs/cors + */ + cors?: boolean | cors.CorsOptions; /** * Configure proxy rules for the dev server or preview server to proxy requests to the specified service. */ @@ -410,6 +420,7 @@ export type NormalizedServerConfig = ServerConfig & | 'printUrls' | 'open' | 'base' + | 'cors' > >; diff --git a/packages/core/tests/__snapshots__/environments.test.ts.snap b/packages/core/tests/__snapshots__/environments.test.ts.snap index 98e7bd1ad2..3bf50c3de3 100644 --- a/packages/core/tests/__snapshots__/environments.test.ts.snap +++ b/packages/core/tests/__snapshots__/environments.test.ts.snap @@ -112,6 +112,7 @@ exports[`environment config > should normalize environment config correctly 1`] "server": { "base": "/", "compress": true, + "cors": true, "host": "0.0.0.0", "htmlFallback": "index", "open": false, @@ -253,6 +254,7 @@ exports[`environment config > should normalize environment config correctly 2`] "server": { "base": "/", "compress": true, + "cors": true, "host": "0.0.0.0", "htmlFallback": "index", "open": false, @@ -394,6 +396,7 @@ exports[`environment config > should print environment config when inspect confi "server": { "base": "/", "compress": true, + "cors": true, "host": "0.0.0.0", "htmlFallback": "index", "open": false, @@ -531,6 +534,7 @@ exports[`environment config > should print environment config when inspect confi "server": { "base": "/", "compress": true, + "cors": true, "host": "0.0.0.0", "htmlFallback": "index", "open": false, @@ -688,6 +692,7 @@ exports[`environment config > should support modify environment config by api.mo "server": { "base": "/", "compress": true, + "cors": true, "host": "0.0.0.0", "htmlFallback": "index", "open": false, @@ -826,6 +831,7 @@ exports[`environment config > should support modify environment config by api.mo "server": { "base": "/", "compress": true, + "cors": true, "host": "0.0.0.0", "htmlFallback": "index", "open": false, @@ -964,6 +970,7 @@ exports[`environment config > should support modify environment config by api.mo "server": { "base": "/", "compress": true, + "cors": true, "host": "0.0.0.0", "htmlFallback": "index", "open": false, @@ -1104,6 +1111,7 @@ exports[`environment config > should support modify single environment config by "server": { "base": "/", "compress": true, + "cors": true, "host": "0.0.0.0", "htmlFallback": "index", "open": false, @@ -1242,6 +1250,7 @@ exports[`environment config > should support modify single environment config by "server": { "base": "/", "compress": true, + "cors": true, "host": "0.0.0.0", "htmlFallback": "index", "open": false, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 39a2e9d2b0..3d895ec8b4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -625,6 +625,9 @@ importers: connect-history-api-fallback: specifier: ^2.0.0 version: 2.0.0 + cors: + specifier: ^2.8.5 + version: 2.8.5 css-loader: specifier: 7.1.2 version: 7.1.2(@rspack/core@1.1.6(@swc/helpers@0.5.15))(webpack@5.97.1) @@ -3553,6 +3556,10 @@ packages: core-js@3.39.0: resolution: {integrity: sha512-raM0ew0/jJUqkJ0E6e8UDtl+y/7ktFivgWvqw8dNSQeNWoSDLvQ1H/RN3aPXB9tBd4/FhyR4RDPGhsNIMsAn7g==} + cors@2.8.5: + resolution: {integrity: sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==} + engines: {node: '>= 0.10'} + cosmiconfig@8.3.6: resolution: {integrity: sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==} engines: {node: '>=14'} @@ -9336,6 +9343,11 @@ snapshots: core-js@3.39.0: {} + cors@2.8.5: + dependencies: + object-assign: 4.1.1 + vary: 1.1.2 + cosmiconfig@8.3.6(typescript@5.7.2): dependencies: import-fresh: 3.3.0