Skip to content

Commit

Permalink
fix: should copy public dir to environment's dist dir (#4268)
Browse files Browse the repository at this point in the history
Co-authored-by: neverland <chenjiahan.jait@bytedance.com>
  • Loading branch information
9aoy and chenjiahan authored Dec 26, 2024
1 parent 5a31ce8 commit b457159
Show file tree
Hide file tree
Showing 8 changed files with 156 additions and 56 deletions.
79 changes: 79 additions & 0 deletions e2e/cases/server/public-dir/publicDir.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}) => {
Expand Down
14 changes: 14 additions & 0 deletions packages/core/src/helpers/path.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string[]>((prev, curr) => {
const isSub = prev.find((p) => curr.startsWith(p) || curr === p);
if (isSub) {
return prev;
}

return prev.concat(curr);
}, []);
};
17 changes: 2 additions & 15 deletions packages/core/src/plugins/cleanOutput.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -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<string[]>((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',

Expand Down Expand Up @@ -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 }) => {
Expand Down
21 changes: 15 additions & 6 deletions packages/core/src/plugins/server.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -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;
}
Expand All @@ -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}`;
Expand Down
34 changes: 0 additions & 34 deletions packages/core/tests/cleanOutput.test.ts

This file was deleted.

33 changes: 32 additions & 1 deletion packages/core/tests/helpers.test.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand Down Expand Up @@ -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',
]);
});
7 changes: 7 additions & 0 deletions website/docs/en/config/server/public-dir.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down
7 changes: 7 additions & 0 deletions website/docs/zh/config/server/public-dir.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down

0 comments on commit b457159

Please sign in to comment.