Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 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
9 changes: 8 additions & 1 deletion packages/core/src/cli/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { type RsbuildInstance, createRsbuild } from '@rsbuild/core';
import { composeRsbuildEnvironments, pruneEnvironments } from '../config';
import type { RslibConfig } from '../types/config';
import type { BuildOptions } from './commands';
import { onBeforeRestartServer } from './restart';

export async function build(
config: RslibConfig,
Expand All @@ -14,9 +15,15 @@ export async function build(
},
});

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

if (options.watch) {
onBeforeRestartServer(buildInstance.close);
} else {
await buildInstance.close();
}

return rsbuildInstance;
}
37 changes: 28 additions & 9 deletions packages/core/src/cli/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { build } from './build';
import { loadRslibConfig } from './init';
import { inspect } from './inspect';
import { startMFDevServer } from './mf';
import { watchFilesForRestart } from './restart';

export type CommonOptions = {
root?: string;
Expand Down Expand Up @@ -62,11 +63,20 @@ export function runCli(): void {
.description('build the library for production')
.action(async (options: BuildOptions) => {
try {
const rslibConfig = await loadRslibConfig(options);
await build(rslibConfig, {
lib: options.lib,
watch: options.watch,
});
const cliBuild = async () => {
const { content: rslibConfig, filePath } =
await loadRslibConfig(options);

await build(rslibConfig, options);

if (options.watch) {
watchFilesForRestart([filePath], async () => {
await cliBuild();
});
}
};

await cliBuild();
} catch (err) {
logger.error('Failed to build.');
logger.error(err);
Expand All @@ -90,7 +100,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,9 +118,18 @@ export function runCli(): void {
.description('start Rsbuild dev server of Module Federation format')
.action(async (options: CommonOptions) => {
try {
const rslibConfig = await loadRslibConfig(options);
// TODO: support lib option in mf dev server
await startMFDevServer(rslibConfig);
const mfDev = async () => {
const { content: rslibConfig, filePath } =
await loadRslibConfig(options);
// TODO: support lib option in mf dev server
await startMFDevServer(rslibConfig);

watchFilesForRestart([filePath], async () => {
await mfDev();
});
};

await mfDev();
} catch (err) {
logger.error('Failed to start mf dev.');
logger.error(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;
}
5 changes: 4 additions & 1 deletion packages/core/src/cli/mf.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { createRsbuild, mergeRsbuildConfig } from '@rsbuild/core';
import type { RsbuildConfig, RsbuildInstance } from '@rsbuild/core';
import { composeCreateRsbuildConfig } from '../config';
import type { RslibConfig } from '../types';
import { onBeforeRestartServer } from './restart';

export async function startMFDevServer(
config: RslibConfig,
Expand All @@ -27,7 +28,9 @@ async function initMFRsbuild(
const rsbuildInstance = await createRsbuild({
rsbuildConfig: mfRsbuildConfig.config,
});
await rsbuildInstance.startDevServer();
const devServer = await rsbuildInstance.startDevServer();

onBeforeRestartServer(devServer.server.close);
return rsbuildInstance;
}

Expand Down
76 changes: 76 additions & 0 deletions packages/core/src/cli/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';
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lazy import

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/**'],
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

chokidar v4 does not support glob, this will not work

});

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 = [];
};
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
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
Loading
Loading