diff --git a/e2e/cases/server/public-dir/publicDir.test.ts b/e2e/cases/server/public-dir/publicDir.test.ts index 86ec67fe75..759d66fd58 100644 --- a/e2e/cases/server/public-dir/publicDir.test.ts +++ b/e2e/cases/server/public-dir/publicDir.test.ts @@ -170,6 +170,85 @@ test('should serve publicDir for preview server correctly', async ({ await rsbuild.close(); }); +test('should copy publicDir to the environment distDir when multiple environments', async () => { + await fse.outputFile(join(__dirname, 'public', 'test-temp-file.txt'), 'a'); + + const rsbuild = await build({ + cwd, + rsbuildConfig: { + environments: { + web1: { + output: { + distPath: { + root: 'dist-build-web-1', + }, + }, + }, + web2: { + output: { + distPath: { + root: 'dist-build-web-2', + }, + }, + }, + }, + }, + }); + const files = await rsbuild.unwrapOutputJSON(); + const filenames = Object.keys(files); + + expect( + filenames.some((filename) => + filename.includes('dist-build-web-1/test-temp-file.txt'), + ), + ).toBeTruthy(); + expect( + filenames.some((filename) => + filename.includes('dist-build-web-2/test-temp-file.txt'), + ), + ).toBeTruthy(); +}); + +test('should copy publicDir to root dist when environment dist path has a parent-child relationship', async () => { + await fse.outputFile(join(__dirname, 'public', 'test-temp-file.txt'), 'a'); + fse.removeSync(join(__dirname, 'dist-build-web')); + + const rsbuild = await build({ + cwd, + rsbuildConfig: { + environments: { + web1: { + output: { + distPath: { + root: 'dist-build-web/1', + }, + }, + }, + web2: { + output: { + distPath: { + root: 'dist-build-web', + }, + }, + }, + }, + }, + }); + const files = await rsbuild.unwrapOutputJSON(); + const filenames = Object.keys(files); + + expect( + filenames.some((filename) => + filename.includes('dist-build-web/test-temp-file.txt'), + ), + ).toBeTruthy(); + expect( + filenames.some((filename) => + filename.includes('dist-build-web/1/test-temp-file.txt'), + ), + ).toBeFalsy(); +}); + test('should serve publicDir for preview server with assetPrefix correctly', async ({ page, }) => { diff --git a/packages/core/src/helpers/path.ts b/packages/core/src/helpers/path.ts index 92071b0f16..a152e8d840 100644 --- a/packages/core/src/helpers/path.ts +++ b/packages/core/src/helpers/path.ts @@ -45,3 +45,17 @@ export const pathnameParse = (publicPath: string): string => { return publicPath; } }; + +/** dedupe and remove nested paths */ +export const dedupeNestedPaths = (paths: string[]): string[] => { + return paths + .sort((p1, p2) => (p2.length > p1.length ? -1 : 1)) + .reduce((prev, curr) => { + const isSub = prev.find((p) => curr.startsWith(p) || curr === p); + if (isSub) { + return prev; + } + + return prev.concat(curr); + }, []); +}; diff --git a/packages/core/src/plugins/cleanOutput.ts b/packages/core/src/plugins/cleanOutput.ts index a154c1047d..f06f7b3d99 100644 --- a/packages/core/src/plugins/cleanOutput.ts +++ b/packages/core/src/plugins/cleanOutput.ts @@ -1,6 +1,6 @@ import { join, sep } from 'node:path'; import { RSBUILD_OUTPUTS_PATH } from '../constants'; -import { color, emptyDir } from '../helpers'; +import { color, dedupeNestedPaths, emptyDir } from '../helpers'; import { logger } from '../logger'; import type { EnvironmentContext, RsbuildPlugin } from '../types'; @@ -12,19 +12,6 @@ const isStrictSubdir = (parent: string, child: string) => { return parentDir !== childDir && childDir.startsWith(parentDir); }; -export const dedupeCleanPaths = (paths: string[]): string[] => { - return paths - .sort((p1, p2) => (p2.length > p1.length ? -1 : 1)) - .reduce((prev, curr) => { - const isSub = prev.find((p) => curr.startsWith(p) || curr === p); - if (isSub) { - return prev; - } - - return prev.concat(curr); - }, []); -}; - export const pluginCleanOutput = (): RsbuildPlugin => ({ name: 'rsbuild:clean-output', @@ -92,7 +79,7 @@ export const pluginCleanOutput = (): RsbuildPlugin => ({ .concat(getRsbuildCleanPath()) .filter((p): p is string => !!p); - await Promise.all(dedupeCleanPaths(cleanPaths).map((p) => emptyDir(p))); + await Promise.all(dedupeNestedPaths(cleanPaths).map((p) => emptyDir(p))); }; api.onBeforeBuild(async ({ isFirstCompile, environments }) => { diff --git a/packages/core/src/plugins/server.ts b/packages/core/src/plugins/server.ts index 6ef1f784f2..38482f6a33 100644 --- a/packages/core/src/plugins/server.ts +++ b/packages/core/src/plugins/server.ts @@ -1,6 +1,7 @@ import fs from 'node:fs'; import { isAbsolute, join } from 'node:path'; import { normalizePublicDirs } from '../config'; +import { dedupeNestedPaths } from '../helpers'; import { open } from '../server/open'; import type { OnAfterStartDevServerFn, RsbuildPlugin } from '../types'; @@ -23,7 +24,7 @@ export const pluginServer = (): RsbuildPlugin => ({ api.onAfterStartDevServer(onStartServer); api.onAfterStartProdServer(onStartServer); - api.onBeforeBuild(async ({ isFirstCompile }) => { + api.onBeforeBuild(async ({ isFirstCompile, environments }) => { if (!isFirstCompile) { return; } @@ -45,14 +46,22 @@ export const pluginServer = (): RsbuildPlugin => ({ continue; } + const distPaths = dedupeNestedPaths( + Object.values(environments).map((e) => e.distPath), + ); + try { // async errors will missing error stack on copy, move // https://github.com/jprichardson/node-fs-extra/issues/769 - await fs.promises.cp(normalizedPath, api.context.distPath, { - recursive: true, - // dereference symlinks - dereference: true, - }); + await Promise.all( + distPaths.map((distPath) => + fs.promises.cp(normalizedPath, distPath, { + recursive: true, + // dereference symlinks + dereference: true, + }), + ), + ); } catch (err) { if (err instanceof Error) { err.message = `Copy public dir (${normalizedPath}) to dist failed:\n${err.message}`; diff --git a/packages/core/tests/cleanOutput.test.ts b/packages/core/tests/cleanOutput.test.ts deleted file mode 100644 index 56c3be423c..0000000000 --- a/packages/core/tests/cleanOutput.test.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { dedupeCleanPaths } from '../src/plugins/cleanOutput'; - -describe('cleanOutput', () => { - test('should dedupeCleanPaths correctly', async () => { - expect( - dedupeCleanPaths([ - 'package/to/root/dist/web1', - 'package/to/root/dist/web2', - 'package/to/root/dist', - ]), - ).toEqual(['package/to/root/dist']); - - expect( - dedupeCleanPaths([ - 'package/to/root', - 'package/to/root/dist/web2', - 'package/to/root/dist', - ]), - ).toEqual(['package/to/root']); - - expect( - dedupeCleanPaths([ - 'package/to/root/dist/web1', - 'package/to/root/dist/web2', - 'package/to/root/dist/web3', - 'package/to/root/dist/web3', - ]), - ).toEqual([ - 'package/to/root/dist/web1', - 'package/to/root/dist/web2', - 'package/to/root/dist/web3', - ]); - }); -}); diff --git a/packages/core/tests/helpers.test.ts b/packages/core/tests/helpers.test.ts index c547efb17e..6381c17265 100644 --- a/packages/core/tests/helpers.test.ts +++ b/packages/core/tests/helpers.test.ts @@ -1,6 +1,6 @@ import { sep } from 'node:path'; import { ensureAssetPrefix, pick, prettyTime } from '../src/helpers'; -import { getCommonParentPath } from '../src/helpers/path'; +import { dedupeNestedPaths, getCommonParentPath } from '../src/helpers/path'; import type { InternalContext } from '../src/internal'; import { getRoutes, normalizeUrl } from '../src/server/helper'; @@ -204,3 +204,34 @@ describe('getCommonParentPath', () => { expect(result).toBe(normalize('/home/user/project/dist1')); }); }); + +test('should dedupeNestedPaths correctly', async () => { + expect( + dedupeNestedPaths([ + 'package/to/root/dist/web1', + 'package/to/root/dist/web2', + 'package/to/root/dist', + ]), + ).toEqual(['package/to/root/dist']); + + expect( + dedupeNestedPaths([ + 'package/to/root', + 'package/to/root/dist/web2', + 'package/to/root/dist', + ]), + ).toEqual(['package/to/root']); + + expect( + dedupeNestedPaths([ + 'package/to/root/dist/web1', + 'package/to/root/dist/web2', + 'package/to/root/dist/web3', + 'package/to/root/dist/web3', + ]), + ).toEqual([ + 'package/to/root/dist/web1', + 'package/to/root/dist/web2', + 'package/to/root/dist/web3', + ]); +}); diff --git a/website/docs/en/config/server/public-dir.mdx b/website/docs/en/config/server/public-dir.mdx index 2b80c92a52..d811dafce0 100644 --- a/website/docs/en/config/server/public-dir.mdx +++ b/website/docs/en/config/server/public-dir.mdx @@ -84,6 +84,13 @@ Note that setting the value of `copyOnBuild` to false means that when you run `r During dev builds, if you need to copy some static assets to the output directory, you can use the [output.copy](/config/output/copy) option instead. ::: +#### Multiple environments + +When performing [multi-environment builds](/guide/advanced/environments), Rsbuild copies files from the public directory to the output directory of each environment. If there are nested output directories, files will only be copied to the root of the output directory. For example: + +- The distDir of the `web` environment is `dist`, and the distDir of the `web1` environment is `dist/web1`. Due to the nested relationship between `dist` and `dist/web1`, at this time, the public directory files are only copied to the `dist` directory. +- The distDir of the `esm` environment is `dist/esm`, and the distDir of the `cjs` environment is `dist/cjs`. Since there is no nesting relationship between `dist/esm` and `dist/cjs`, at this time, the public directory files will be copied to the `dist/esm` and `dist/cjs` directories respectively. + ### watch - **Type:** `boolean` diff --git a/website/docs/zh/config/server/public-dir.mdx b/website/docs/zh/config/server/public-dir.mdx index 5d8908ce7b..0feccd72bd 100644 --- a/website/docs/zh/config/server/public-dir.mdx +++ b/website/docs/zh/config/server/public-dir.mdx @@ -85,6 +85,13 @@ export default { 在 dev 构建时,如果你需要拷贝一些静态资源到构建产物目录,可以使用 [output.copy](/config/output/copy) 选项代替。 ::: +#### 多环境 + +当执行 [多环境构建](/guide/advanced/environments) 时,Rsbuild 会将文件从 public 目录拷贝到各个环境的输出目录下。如果输出目录存在嵌套的情况,则只会拷贝文件到输出目录的根目录下。例如: + +- `web` 环境的构建产物目录设置为 `dist`,`web1` 环境的构建产物目录设置为 `dist/web1`。由于 `dist` 和 `dist/web1` 存在嵌套关系,此时,public 目录文件仅复制到 `dist` 目录下。 +- `esm` 环境的构建产物目录设置为 `dist/esm`,`cjs` 环境的构建产物目录设置为 `dist/cjs`。由于 `dist/esm` 和 `dist/cjs` 不存在嵌套关系,此时,public 目录文件将分别复制到 `dist/esm` 和 `dist/cjs` 目录下。 + ### watch - **类型:** `boolean`