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
24 changes: 24 additions & 0 deletions e2e/cases/output/clean-dist-path/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import fse from 'fs-extra';

const cwd = __dirname;
const testDistFile = join(cwd, 'dist/test.json');
const testDeepDistFile = join(cwd, 'dist/foo/bar/test.json');

test('should clean dist path by default', async () => {
await fse.outputFile(testDistFile, `{ "test": 1 }`);
Expand Down Expand Up @@ -60,3 +61,26 @@ test('should allow to disable cleanDistPath', async () => {

fs.rmSync(testDistFile, { force: true });
});

test('should allow to use `cleanDistPath.keep` to keep some files', async () => {
await fse.outputFile(testDistFile, `{ "test": 1 }`);
await fse.outputFile(testDeepDistFile, `{ "test": 1 }`);

await build({
cwd,
rsbuildConfig: {
output: {
cleanDistPath: {
keep: [/dist[\\/]test.json/, /dist[\\/]foo[\\/]bar[\\/]test.json/],
},
},
},
});

expect(fs.existsSync(testDistFile)).toBeTruthy();
expect(fs.existsSync(testDeepDistFile)).toBeTruthy();

await build({ cwd });
expect(fs.existsSync(testDistFile)).toBeFalsy();
expect(fs.existsSync(testDeepDistFile)).toBeFalsy();
});
35 changes: 27 additions & 8 deletions packages/core/src/helpers/fs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,18 +63,37 @@ export async function fileExistsByCompilation(
});
}

export async function emptyDir(dir: string): Promise<void> {
if (!(await pathExists(dir))) {
export async function emptyDir(
dir: string,
keep: RegExp[] = [],
checkExists = true,
): Promise<void> {
if (checkExists && !(await pathExists(dir))) {
return;
}

try {
for (const file of await fs.promises.readdir(dir)) {
await fs.promises.rm(path.resolve(dir, file), {
recursive: true,
force: true,
});
}
const entries = await fs.promises.readdir(dir, {
withFileTypes: true,
});

await Promise.all(
entries.map(async (entry) => {
const fullPath = path.resolve(dir, entry.name);
if (keep.some((reg) => reg.test(fullPath))) {
return;
}

if (entry.isDirectory()) {
await emptyDir(fullPath, keep, false);
if (!keep.length) {
await fs.promises.rmdir(fullPath);
}
} else {
await fs.promises.unlink(fullPath);
}
}),
);
} catch (err) {
logger.debug(`Failed to empty dir: ${dir}`);
logger.debug(err);
Expand Down
2 changes: 2 additions & 0 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ export type {
Charset,
ClientConfig,
CliShortcut,
CleanDistPath,
CleanDistPathObject,
ConfigChain,
ConfigChainWithContext,
ConsoleType,
Expand Down
119 changes: 82 additions & 37 deletions packages/core/src/plugins/cleanOutput.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
import { join, sep } from 'node:path';
import { RSBUILD_OUTPUTS_PATH } from '../constants';
import { color, dedupeNestedPaths, emptyDir } from '../helpers';
import { color, emptyDir } from '../helpers';
import { logger } from '../logger';
import type { EnvironmentContext, RsbuildPlugin } from '../types';
import type {
CleanDistPath,
CleanDistPathObject,
EnvironmentContext,
RsbuildPlugin,
} from '../types';

const addTrailingSep = (dir: string) => (dir.endsWith(sep) ? dir : dir + sep);

Expand All @@ -12,51 +17,87 @@ const isStrictSubdir = (parent: string, child: string) => {
return parentDir !== childDir && childDir.startsWith(parentDir);
};

const normalizeCleanDistPath = (
userOptions: CleanDistPath,
): CleanDistPathObject => {
const defaultOptions: CleanDistPathObject = {
enable: 'auto',
};

if (typeof userOptions === 'boolean' || userOptions === 'auto') {
return {
...defaultOptions,
enable: userOptions,
};
}

return {
...defaultOptions,
...userOptions,
};
};

type PathInfo = {
path: string;
keep?: RegExp[];
};

export const pluginCleanOutput = (): RsbuildPlugin => ({
name: 'rsbuild:clean-output',

setup(api) {
// should clean rsbuild outputs, such as inspect files
const getRsbuildCleanPath = () => {
// clean Rsbuild outputs files, such as the inspected config files
const getRsbuildOutputPath = (): PathInfo | undefined => {
const { rootPath, distPath } = api.context;
const config = api.getNormalizedConfig();
const cleanPath = join(distPath, RSBUILD_OUTPUTS_PATH);

const { cleanDistPath } = config.output;
const targetPath = join(distPath, RSBUILD_OUTPUTS_PATH);
const { enable } = normalizeCleanDistPath(config.output.cleanDistPath);

if (
cleanDistPath === true ||
(cleanDistPath === 'auto' && isStrictSubdir(rootPath, cleanPath))
enable === true ||
(enable === 'auto' && isStrictSubdir(rootPath, targetPath))
) {
return cleanPath;
return {
path: targetPath,
};
}
return undefined;
};

const getCleanPath = (environment: EnvironmentContext) => {
const getPathInfo = (
environment: EnvironmentContext,
): PathInfo | undefined => {
const { rootPath } = api.context;
const { config, distPath } = environment;

let { cleanDistPath } = config.output;
const { enable, keep } = normalizeCleanDistPath(
config.output.cleanDistPath,
);

// only enable cleanDistPath when the dist path is a subdir of root path
if (cleanDistPath === 'auto') {
cleanDistPath = isStrictSubdir(rootPath, distPath);

if (!cleanDistPath) {
logger.warn(
'The dist path is not a subdir of root path, Rsbuild will not empty it.',
);
logger.warn(
`Please set ${color.yellow('`output.cleanDistPath`')} config manually.`,
);
logger.warn(`Current root path: ${color.dim(rootPath)}`);
logger.warn(`Current dist path: ${color.dim(distPath)}`);
if (enable === 'auto') {
if (isStrictSubdir(rootPath, distPath)) {
return {
path: distPath,
keep,
};
}

logger.warn(
'The dist path is not a subdir of root path, Rsbuild will not empty it.',
);
logger.warn(
`Please set ${color.yellow('`output.cleanDistPath`')} config manually.`,
);
logger.warn(`Current root path: ${color.dim(rootPath)}`);
logger.warn(`Current dist path: ${color.dim(distPath)}`);
return undefined;
}

if (cleanDistPath) {
return distPath;
if (enable === true) {
return {
path: distPath,
keep,
};
}

return undefined;
Expand All @@ -65,21 +106,25 @@ export const pluginCleanOutput = (): RsbuildPlugin => ({
const cleanAll = async (params: {
environments: Record<string, EnvironmentContext>;
}) => {
// dedupe environments by distPath
const environments = Object.values(params.environments).reduce<
Array<EnvironmentContext>
>((total, curr) => {
if (!total.find((t) => t.distPath === curr.distPath)) {
total.push(curr);
EnvironmentContext[]
>((result, curr) => {
if (!result.find((item) => item.distPath === curr.distPath)) {
result.push(curr);
}
return total;
return result;
}, []);

const cleanPaths = environments
.map((e) => getCleanPath(e))
.concat(getRsbuildCleanPath())
.filter((p): p is string => !!p);
const pathInfos: PathInfo[] = [
...environments.map(getPathInfo),
getRsbuildOutputPath(),
].filter((pathInfo): pathInfo is PathInfo => !!pathInfo);

await Promise.all(dedupeNestedPaths(cleanPaths).map((p) => emptyDir(p)));
// Use `for...of` to handle nested directories correctly
for (const pathInfo of pathInfos) {
await emptyDir(pathInfo.path, pathInfo.keep);
}
};

api.onBeforeBuild(async ({ isFirstCompile, environments }) => {
Expand Down
17 changes: 16 additions & 1 deletion packages/core/src/types/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -940,6 +940,20 @@ export type ManifestObjectConfig = {

export type ManifestConfig = string | boolean | ManifestObjectConfig;

export type CleanDistPathObject = {
/**
* Whether to clean the dist path.
* @default 'auto'
*/
enable?: boolean | 'auto';
/**
* The files to keep in the dist path.
*/
keep?: RegExp[];
};

export type CleanDistPath = boolean | 'auto' | CleanDistPathObject;

export interface OutputConfig {
/**
* Specify build target to run in specified environment.
Expand Down Expand Up @@ -998,7 +1012,7 @@ export interface OutputConfig {
* Whether to clean all files in the dist path before starting compilation.
* @default 'auto'
*/
cleanDistPath?: boolean | 'auto';
cleanDistPath?: CleanDistPath;
/**
* Allow to custom CSS Modules options.
*/
Expand Down Expand Up @@ -1089,6 +1103,7 @@ export interface NormalizedOutputConfig extends OutputConfig {
js?: Rspack.Configuration['devtool'];
css: boolean;
};
cleanDistPath: CleanDistPath;
filenameHash: boolean | string;
assetPrefix: string;
dataUriLimit: number | NormalizedDataUriLimit;
Expand Down
Loading