Skip to content

Commit

Permalink
feat(server): support dev.watchFiles config (#2145)
Browse files Browse the repository at this point in the history
Co-authored-by: gaoyuan <[email protected]>
Co-authored-by: neverland <[email protected]>
  • Loading branch information
3 people authored Apr 22, 2024
1 parent 047eac7 commit c4973ea
Show file tree
Hide file tree
Showing 14 changed files with 316 additions and 0 deletions.
Empty file.
Empty file.
147 changes: 147 additions & 0 deletions e2e/cases/server/watch-files/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
import path from 'node:path';
import { dev, gotoPage, rspackOnlyTest } from '@e2e/helper';
import { fse } from '@rsbuild/shared';

rspackOnlyTest('should work with string and path to file', async ({ page }) => {
const file = path.join(__dirname, '/assets/example.txt');
const rsbuild = await dev({
cwd: __dirname,
rsbuildConfig: {
dev: {
watchFiles: {
paths: file,
},
},
},
});
await gotoPage(page, rsbuild);

await fse.writeFile(file, 'test');
// check the page is reloaded
await new Promise((resolve) => {
page.waitForURL(page.url()).then(resolve);
});

// reset file
fse.truncateSync(file);
await rsbuild.close();
});

rspackOnlyTest(
'should work with string and path to directory',
async ({ page }) => {
const file = path.join(__dirname, '/assets/example.txt');
const rsbuild = await dev({
cwd: __dirname,
rsbuildConfig: {
dev: {
watchFiles: {
paths: path.join(__dirname, '/assets'),
},
},
},
});
await gotoPage(page, rsbuild);

await fse.writeFile(file, 'test');

await new Promise((resolve) => {
page.waitForURL(page.url()).then(resolve);
});

// reset file
fse.truncateSync(file);
await rsbuild.close();
},
);

rspackOnlyTest('should work with string array directory', async ({ page }) => {
const file = path.join(__dirname, '/assets/example.txt');
const other = path.join(__dirname, '/other/other.txt');
const rsbuild = await dev({
cwd: __dirname,
rsbuildConfig: {
dev: {
watchFiles: {
paths: [
path.join(__dirname, '/assets'),
path.join(__dirname, '/other'),
],
},
},
},
});
await gotoPage(page, rsbuild);

await fse.writeFile(file, 'test');
// check the page is reloaded
await new Promise((resolve) => {
page.waitForURL(page.url()).then(resolve);
});
// reset file
fse.truncateSync(file);

await fse.writeFile(other, 'test');
// check the page is reloaded
await new Promise((resolve) => {
page.waitForURL(page.url()).then(resolve);
});
// reset file
fse.truncateSync(other);

await rsbuild.close();
});

rspackOnlyTest('should work with string and glob', async ({ page }) => {
const file = path.join(__dirname, '/assets/example.txt');
const watchDir = path.join(__dirname, '/assets');
const rsbuild = await dev({
cwd: __dirname,
rsbuildConfig: {
dev: {
watchFiles: {
paths: `${watchDir}/**/*`,
},
},
},
});
await gotoPage(page, rsbuild);

await fse.writeFile(file, 'test');
// check the page is reloaded
await new Promise((resolve) => {
page.waitForURL(page.url()).then(resolve);
});

// reset file
fse.truncateSync(file);
await rsbuild.close();
});

rspackOnlyTest('should work with options', async ({ page }) => {
const file = path.join(__dirname, '/assets/example.txt');
const rsbuild = await dev({
cwd: __dirname,
rsbuildConfig: {
dev: {
watchFiles: {
paths: file,
options: {
usePolling: true,
},
},
},
},
});
await gotoPage(page, rsbuild);

await fse.writeFile(file, 'test');
// check the page is reloaded
await new Promise((resolve) => {
page.waitForURL(page.url()).then(resolve);
});

// reset file
fse.truncateSync(file);
await rsbuild.close();
});
Empty file.
6 changes: 6 additions & 0 deletions e2e/cases/server/watch-files/rsbuild.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { defineConfig } from '@rsbuild/core';
import { pluginReact } from '@rsbuild/plugin-react';

export default defineConfig({
plugins: [pluginReact()],
});
10 changes: 10 additions & 0 deletions e2e/cases/server/watch-files/src/App.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
const App = () => {
return (
<div className="content">
<h1>Rsbuild with React</h1>
<p>Start building amazing things with Rsbuild.</p>
</div>
);
};

export default App;
10 changes: 10 additions & 0 deletions e2e/cases/server/watch-files/src/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';

const root = ReactDOM.createRoot(document.getElementById('root')!);
root.render(
<React.StrictMode>
<App />
</React.StrictMode>,
);
2 changes: 2 additions & 0 deletions packages/core/src/client/hmr/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,8 @@ function onMessage(e: MessageEvent<string>) {
handleSuccess();
break;
case 'static-changed':
window.location.reload();
break;
case 'content-changed':
// Triggered when a file from `contentBase` changed.
window.location.reload();
Expand Down
4 changes: 4 additions & 0 deletions packages/core/src/server/devServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { formatRoutes, getDevOptions, printServerURLs } from './helper';
import { createHttpServer } from './httpServer';
import { notFoundMiddleware } from './middlewares';
import { onBeforeRestartServer } from './restart';
import { setupWatchFiles } from './watchFiles';

export async function createDevServer<
Options extends {
Expand Down Expand Up @@ -123,6 +124,8 @@ export async function createDevServer<

const compileMiddlewareAPI = runCompile ? await startCompile() : undefined;

const fileWatcher = await setupWatchFiles(devConfig, compileMiddlewareAPI);

const devMiddlewares = await getMiddlewares({
pwd: options.context.rootPath,
compileMiddlewareAPI,
Expand Down Expand Up @@ -200,6 +203,7 @@ export async function createDevServer<
close: async () => {
await options.context.hooks.onCloseDevServer.call();
await devMiddlewares.close();
await fileWatcher?.close();
},
};

Expand Down
25 changes: 25 additions & 0 deletions packages/core/src/server/watchFiles.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import type { DevConfig, FSWatcher } from '@rsbuild/shared';
import type { RsbuildDevMiddlewareOptions } from './getDevMiddlewares';

export async function setupWatchFiles(
dev: DevConfig,
compileMiddlewareAPI: RsbuildDevMiddlewareOptions['compileMiddlewareAPI'],
): Promise<FSWatcher | undefined> {
const { watchFiles, hmr, liveReload } = dev;
if (!watchFiles || (!hmr && !liveReload)) {
return;
}

const chokidar = await import('@rsbuild/shared/chokidar');

const { paths, options } = watchFiles;
const watcher = chokidar.watch(paths, options);

watcher.on('change', () => {
if (compileMiddlewareAPI) {
compileMiddlewareAPI.sockWrite('static-changed');
}
});

return watcher;
}
8 changes: 8 additions & 0 deletions packages/shared/src/types/config/dev.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { IncomingMessage, ServerResponse } from 'node:http';
import type { WatchOptions } from '../../../compiled/chokidar';
import type { ArrayOrNot } from '../utils';

export type ProgressBarConfig = {
Expand Down Expand Up @@ -76,6 +77,13 @@ export interface DevConfig {
* Used to control whether the build artifacts of the development environment are written to the disk.
*/
writeToDisk?: boolean | ((filename: string) => boolean);
/**
* This option allows you to configure a list of globs/directories/files to watch for file changes.
*/
watchFiles?: {
paths: string | string[];
options?: WatchOptions;
};
}

export type NormalizedDevConfig = DevConfig &
Expand Down
2 changes: 2 additions & 0 deletions packages/shared/src/types/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import type {
} from './config/dev';
import type { RspackCompiler, RspackMultiCompiler } from './rspack';

export type { FSWatcher } from '../../compiled/chokidar';

export type Middleware = (
req: IncomingMessage,
res: ServerResponse,
Expand Down
51 changes: 51 additions & 0 deletions website/docs/en/config/dev/watch-files.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# dev.watchFiles

- **Type:**

```ts
type WatchFiles = {
paths: string | string[];
// watch options for chokidar
options?: WatchOptions;
};
```

- **Default:** `undefined`

Watch files and directories for changes. When a file changes, the page will be reloaded.

If both `dev.hmr` and `dev.liveReload` are set to false, `watchFiles` will be ignored.

### Example

You can configure a list of globs/directories/files to watch for file changes.

```js
export default {
dev: {
watchFiles: {
// watch a single file
paths: 'public/demo.txt',
// use a glob pattern
paths: 'src/**/*.txt',
// watch multiple file paths
paths: ['src/**/*.txt', 'public/**/*'],
},
},
};
```

You can also specify [chokidar](https://github.com/paulmillr/chokidar#api) watcher options by passing an object with `paths` and `options` properties.

```js
export default {
dev: {
watchFiles: {
paths: 'src/**/*.txt',
options: {
usePolling: false,
},
},
},
};
```
51 changes: 51 additions & 0 deletions website/docs/zh/config/dev/watch-files.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# dev.watchFiles

- **类型:*

```ts
type WatchFiles = {
paths: string | string[];
// chokidar 选项
options?: WatchOptions;
};
```

- **默认值:** `undefined`

监视指定文件和目录的变化。当文件发生变化时,页面将重新加载。

如果 `dev.hmr``dev.liveReload` 都设置为 false,则 `watchFiles` 将被忽略。

### 示例

你可以配置一个 glob 模式 / 目录 / 文件的列表,用于监视文件变化。

```js
export default {
dev: {
watchFiles: {
// 监视单个文件
paths: 'public/demo.txt',
// 使用 glob 模式
paths: 'src/**/*.txt',
// 监视多个文件路径
paths: ['src/**/*.txt', 'public/**/*'],
},
},
};
```

你也可以通过传入一个包含 `paths``options` 属性的对象,来指定 [chokidar](https://github.com/paulmillr/chokidar#api) 选项。

```js
export default {
dev: {
watchFiles: {
paths: 'src/**/*.txt',
options: {
usePolling: false,
},
},
},
};
```

0 comments on commit c4973ea

Please sign in to comment.