Skip to content

feat: support watch rslib.config to rebuild #489

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
Nov 28, 2024
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
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 { onBeforeRestart } 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) {
onBeforeRestart(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 cliMfDev = async () => {
const { content: rslibConfig, filePath } =
await loadRslibConfig(options);
// TODO: support lib option in mf dev server
await startMFDevServer(rslibConfig);

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

await cliMfDev();
} 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 { onBeforeRestart } 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();

onBeforeRestart(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 onBeforeRestart = (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