Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
1 change: 1 addition & 0 deletions packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
"devDependencies": {
"@rslib/tsconfig": "workspace:*",
"@types/fs-extra": "^11.0.4",
"chokidar": "^4.0.1",
"commander": "^12.1.0",
"fs-extra": "^11.2.0",
"memfs": "^4.14.0",
Expand Down
5 changes: 5 additions & 0 deletions packages/core/prebundle.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@ export default {
},
dependencies: [
'commander',
{
name: 'chokidar',
// strip sourcemap comment
prettier: true,
},
{
name: 'rslog',
afterBundle(task) {
Expand Down
1 change: 1 addition & 0 deletions packages/core/rslib.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export default defineConfig({
externals: {
picocolors: '../compiled/picocolors/index.js',
commander: '../compiled/commander/index.js',
chokidar: '../compiled/chokidar/index.js',
rslog: '../compiled/rslog/index.js',
},
},
Expand Down
30 changes: 28 additions & 2 deletions packages/core/src/cli/build.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
import { type RsbuildInstance, createRsbuild } from '@rsbuild/core';
import { composeRsbuildEnvironments, pruneEnvironments } from '../config';
import { onBeforeRestartServer, watchFilesForRestart } from '../restart';
import type { RslibConfig } from '../types/config';
import type { BuildOptions } from './commands';
import { loadRslibConfig } from './init';

export async function build(
config: RslibConfig,
options: Pick<BuildOptions, 'lib' | 'watch'> = {},
options: Pick<BuildOptions, 'lib' | 'watch'> & {
configFilePath?: string;
} = {},
): Promise<RsbuildInstance> {
const environments = await composeRsbuildEnvironments(config);
const rsbuildInstance = await createRsbuild({
Expand All @@ -14,9 +18,31 @@ export async function build(
},
});

await rsbuildInstance.build({
const buildInstance = await rsbuildInstance.build({
watch: options.watch,
});

if (options?.watch) {
const files: string[] = [];

if (options.configFilePath) {
files.push(options.configFilePath);
}

onBeforeRestartServer(buildInstance.close);

watchFilesForRestart(files, async () => {
const { content: rslibConfig, filePath: configFilePath } =
await loadRslibConfig(options);

await build(rslibConfig, {
configFilePath,
...options,
});
});
} else {
await buildInstance.close();
}

return rsbuildInstance;
}
8 changes: 5 additions & 3 deletions packages/core/src/cli/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,10 @@ export function runCli(): void {
.description('build the library for production')
.action(async (options: BuildOptions) => {
try {
const rslibConfig = await loadRslibConfig(options);
const { content: rslibConfig, filePath: configFilePath } =
await loadRslibConfig(options);
await build(rslibConfig, {
configFilePath,
lib: options.lib,
watch: options.watch,
});
Expand All @@ -90,7 +92,7 @@ export function runCli(): void {
.action(async (options: InspectOptions) => {
try {
// TODO: inspect should output Rslib's config
const rslibConfig = await loadRslibConfig(options);
const { content: rslibConfig } = await loadRslibConfig(options);
await inspect(rslibConfig, {
lib: options.lib,
mode: options.mode,
Expand All @@ -108,7 +110,7 @@ export function runCli(): void {
.description('start Rsbuild dev server of Module Federation format')
.action(async (options: CommonOptions) => {
try {
const rslibConfig = await loadRslibConfig(options);
const { content: rslibConfig } = await loadRslibConfig(options);
// TODO: support lib option in mf dev server
await startMFDevServer(rslibConfig);
} catch (err) {
Expand Down
11 changes: 5 additions & 6 deletions packages/core/src/cli/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,16 @@ import type { RslibConfig } from '../types';
import { getAbsolutePath } from '../utils/helper';
import type { CommonOptions } from './commands';

export async function loadRslibConfig(
options: CommonOptions,
): Promise<RslibConfig> {
export async function loadRslibConfig(options: CommonOptions): Promise<{
content: RslibConfig;
filePath: string;
}> {
const cwd = process.cwd();
const root = options.root ? getAbsolutePath(cwd, options.root) : cwd;

const rslibConfig = await loadConfig({
return loadConfig({
cwd: root,
path: options.config,
envMode: options.envMode,
});

return rslibConfig;
}
7 changes: 5 additions & 2 deletions packages/core/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,15 +117,18 @@ export async function loadConfig({
cwd?: string;
path?: string;
envMode?: string;
}): Promise<RslibConfig> {
}): Promise<{
content: RslibConfig;
filePath: string;
}> {
const configFilePath = resolveConfigPath(cwd, path);
const { content } = await loadRsbuildConfig({
cwd: dirname(configFilePath),
path: configFilePath,
envMode,
});

return content as RslibConfig;
return { content: content as RslibConfig, filePath: configFilePath };
}

const composeExternalsWarnConfig = (
Expand Down
76 changes: 76 additions & 0 deletions packages/core/src/restart.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import path from 'node:path';
import chokidar from 'chokidar';
import { color, debounce, isTTY } from './utils/helper';
import { logger } from './utils/logger';

export async function watchFilesForRestart(
files: string[],
restart: () => Promise<void>,
): Promise<void> {
if (!files.length) {
return;
}

const watcher = chokidar.watch(files, {
ignoreInitial: true,
// If watching fails due to read permissions, the errors will be suppressed silently.
ignorePermissionErrors: true,
ignored: ['**/node_modules/**', '**/.git/**', '**/.DS_Store/**'],
});

const callback = debounce(
async (filePath) => {
watcher.close();

await beforeRestart({ filePath });
await restart();
},
// set 300ms debounce to avoid restart frequently
300,
);

watcher.on('add', callback);
watcher.on('change', callback);
watcher.on('unlink', callback);
}

type Cleaner = () => Promise<unknown> | unknown;

let cleaners: Cleaner[] = [];

/**
* Add a cleaner to handle side effects
*/
export const onBeforeRestartServer = (cleaner: Cleaner): void => {
cleaners.push(cleaner);
};

const clearConsole = () => {
if (isTTY() && !process.env.DEBUG) {
process.stdout.write('\x1B[H\x1B[2J');
}
};

const beforeRestart = async ({
filePath,
clear = true,
}: {
filePath?: string;
clear?: boolean;
} = {}): Promise<void> => {
if (clear) {
clearConsole();
}

if (filePath) {
const filename = path.basename(filePath);
logger.info(`Restart because ${color.yellow(filename)} is changed.\n`);
} else {
logger.info('Restarting...\n');
}

for (const cleaner of cleaners) {
await cleaner();
}
cleaners = [];
};
27 changes: 27 additions & 0 deletions packages/core/src/utils/helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -205,4 +205,31 @@ export function checkMFPlugin(config: LibConfig): boolean {
return added;
}

export function debounce<T extends (...args: any[]) => void>(
func: T,
wait: number,
): (...args: Parameters<T>) => void {
let timeoutId: ReturnType<typeof setTimeout> | null = null;

return (...args: Parameters<T>) => {
if (timeoutId !== null) {
clearTimeout(timeoutId);
}

timeoutId = setTimeout(() => {
func(...args);
}, wait);
};
}

/**
* Check if running in a TTY context
*/
export const isTTY = (type: 'stdin' | 'stdout' = 'stdout'): boolean => {
return (
(type === 'stdin' ? process.stdin.isTTY : process.stdout.isTTY) &&
!process.env.CI
);
};

export { color };
16 changes: 8 additions & 8 deletions packages/core/tests/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ describe('Should load config file correctly', () => {
test('Load config.js in cjs project', async () => {
const fixtureDir = join(__dirname, 'fixtures/config/cjs');
const configFilePath = join(fixtureDir, 'rslib.config.js');
const config = await loadConfig({ path: configFilePath });
const { content: config } = await loadConfig({ path: configFilePath });
expect(config).toEqual({
lib: [],
source: {
Expand All @@ -30,7 +30,7 @@ describe('Should load config file correctly', () => {
test('Load config.mjs in cjs project', async () => {
const fixtureDir = join(__dirname, 'fixtures/config/cjs');
const configFilePath = join(fixtureDir, 'rslib.config.mjs');
const config = await loadConfig({ path: configFilePath });
const { content: config } = await loadConfig({ path: configFilePath });
expect(config).toEqual({
lib: [],
source: {
Expand All @@ -47,7 +47,7 @@ describe('Should load config file correctly', () => {
test('Load config.ts in cjs project', async () => {
const fixtureDir = join(__dirname, 'fixtures/config/cjs');
const configFilePath = join(fixtureDir, 'rslib.config.ts');
const config = await loadConfig({ path: configFilePath });
const { content: config } = await loadConfig({ path: configFilePath });
expect(config).toEqual({
lib: [],
source: {
Expand All @@ -64,7 +64,7 @@ describe('Should load config file correctly', () => {
test('Load config.cjs with defineConfig in cjs project', async () => {
const fixtureDir = join(__dirname, 'fixtures/config/cjs');
const configFilePath = join(fixtureDir, 'rslib.config.cjs');
const config = await loadConfig({ path: configFilePath });
const { content: config } = await loadConfig({ path: configFilePath });
expect(config).toEqual({
lib: [],
source: {
Expand All @@ -81,7 +81,7 @@ describe('Should load config file correctly', () => {
test('Load config.js in esm project', async () => {
const fixtureDir = join(__dirname, 'fixtures/config/esm');
const configFilePath = join(fixtureDir, 'rslib.config.js');
const config = await loadConfig({ path: configFilePath });
const { content: config } = await loadConfig({ path: configFilePath });
expect(config).toEqual({
lib: [],
source: {
Expand All @@ -98,7 +98,7 @@ describe('Should load config file correctly', () => {
test('Load config.cjs in esm project', async () => {
const fixtureDir = join(__dirname, 'fixtures/config/esm');
const configFilePath = join(fixtureDir, 'rslib.config.cjs');
const config = await loadConfig({ path: configFilePath });
const { content: config } = await loadConfig({ path: configFilePath });
expect(config).toEqual({
lib: [],
source: {
Expand All @@ -115,7 +115,7 @@ describe('Should load config file correctly', () => {
test('Load config.ts in esm project', async () => {
const fixtureDir = join(__dirname, 'fixtures/config/esm');
const configFilePath = join(fixtureDir, 'rslib.config.ts');
const config = await loadConfig({ path: configFilePath });
const { content: config } = await loadConfig({ path: configFilePath });
expect(config).toEqual({
lib: [],
source: {
Expand All @@ -132,7 +132,7 @@ describe('Should load config file correctly', () => {
test('Load config.mjs with defineConfig in esm project', async () => {
const fixtureDir = join(__dirname, 'fixtures/config/esm');
const configFilePath = join(fixtureDir, 'rslib.config.mjs');
const config = await loadConfig({ path: configFilePath });
const { content: config } = await loadConfig({ path: configFilePath });
expect(config).toMatchObject({
lib: [],
source: {
Expand Down
1 change: 1 addition & 0 deletions packages/core/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"moduleResolution": "Bundler",
"paths": {
"commander": ["./compiled/commander"],
"chokidar": ["./compiled/chokidar"],
"picocolors": ["./compiled/picocolors"],
"rslog": ["./compiled/rslog"]
}
Expand Down
14 changes: 13 additions & 1 deletion packages/plugin-dts/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { fork } from 'node:child_process';
import { type ChildProcess, fork } from 'node:child_process';
import { dirname, extname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { type RsbuildConfig, type RsbuildPlugin, logger } from '@rsbuild/core';
Expand Down Expand Up @@ -60,6 +60,7 @@ export const pluginDts = (options: PluginDtsOptions = {}): RsbuildPlugin => ({

const dtsPromises: Promise<TaskResult>[] = [];
let promisesResult: TaskResult[] = [];
let childProcesses: ChildProcess[] = [];

api.onBeforeEnvironmentCompile(
({ isWatch, isFirstCompile, environment }) => {
Expand All @@ -74,6 +75,8 @@ export const pluginDts = (options: PluginDtsOptions = {}): RsbuildPlugin => ({
stdio: 'inherit',
});

childProcesses.push(childProcess);

// TODO: @microsoft/api-extractor only support single entry to bundle DTS
// use first element of Record<string, string> type entry config
const dtsEntry = processSourceEntry(
Expand Down Expand Up @@ -141,5 +144,14 @@ export const pluginDts = (options: PluginDtsOptions = {}): RsbuildPlugin => ({
},
order: 'post',
});

api.onCloseBuild(() => {
for (const childProcess of childProcesses) {
if (!childProcess.killed) {
childProcess.kill();
}
}
childProcesses = [];
});
},
});
Loading
Loading