diff --git a/README.md b/README.md index 25b87d5..4371501 100644 --- a/README.md +++ b/README.md @@ -108,7 +108,7 @@ Compared to the previous approach, this method decouples the React Fast Refresh - Type: [Rspack.RuleSetCondition](https://rspack.dev/config/module#condition) - Default: `/\.([cm]js|[jt]sx?|flow)$/i` -Include files to be processed by the plugin. The value is the same as the `rule.test` option in Rspack. +Include files to be processed by the plugin. The value is the same as the [rule.test](https://rspack.dev/config/module#ruletest) option in Rspack. ```js new ReactRefreshPlugin({ @@ -121,7 +121,7 @@ new ReactRefreshPlugin({ - Type: [Rspack.RuleSetCondition](https://rspack.dev/config/module#condition) - Default: `/node_modules/` -Exclude files from being processed by the plugin. The value is the same as the `rule.exclude` option in Rspack. +Exclude files from being processed by the plugin. The value is the same as the [rule.exclude](https://rspack.dev/config/module#ruleexclude) option in Rspack. ```js new ReactRefreshPlugin({ @@ -129,6 +129,21 @@ new ReactRefreshPlugin({ }); ``` +### resourceQuery + +- Type: [Rspack.RuleSetCondition](https://rspack.dev/config/module#condition) +- Default: `undefined` + +Can be used to exclude certain resources from being processed by the plugin by the resource query. The value is the same as the [rule.resourceQuery](https://rspack.dev/config/module#ruleresourcequery) option in Rspack. + +For example, to exclude all resources with the `raw` query, such as `import rawTs from './ReactComponent.ts?raw';`, use the following: + +```js +new ReactRefreshPlugin({ + resourceQuery: { not: /raw/ }, +}); +``` + ### forceEnable - Type: `boolean` diff --git a/src/index.ts b/src/index.ts index a8cbbb2..7d55db1 100644 --- a/src/index.ts +++ b/src/index.ts @@ -86,6 +86,7 @@ class ReactRefreshRspackPlugin { // biome-ignore lint: exists or: [this.options.exclude!, [...runtimePaths]].filter(Boolean), }, + resourceQuery: this.options.resourceQuery, use: ReactRefreshRspackPlugin.loader, }); } diff --git a/src/options.ts b/src/options.ts index 588f130..eabbaf0 100644 --- a/src/options.ts +++ b/src/options.ts @@ -16,14 +16,29 @@ export type PluginOptions = { * Include files to be processed by the plugin. * The value is the same as the `rule.test` option in Rspack. * @default /\.([cm]js|[jt]sx?|flow)$/i + * @see https://rspack.dev/config/module#ruletest */ include?: RuleSetCondition | null; /** * Exclude files from being processed by the plugin. * The value is the same as the `rule.exclude` option in Rspack. * @default /node_modules/ + * @see https://rspack.dev/config/module#ruleexclude */ exclude?: RuleSetCondition | null; + /** + * Can be used to exclude certain resources from being processed by + * the plugin by the resource query. + * @see https://rspack.dev/config/module#ruleresourcequery + * + * @example + * To exclude all resources with the `raw` query, such as + * `import rawTs from './ReactComponent.ts?raw';`, use the following: + * ```ts + * { resourceQuery: { not: /raw/ } } + * ``` + */ + resourceQuery?: RuleSetCondition; /** * Sets a namespace for the React Refresh runtime. * It is most useful when multiple instances of React Refresh is running diff --git a/test/fixtures/query/foo.js b/test/fixtures/query/foo.js new file mode 100644 index 0000000..2651774 --- /dev/null +++ b/test/fixtures/query/foo.js @@ -0,0 +1 @@ +module.exports = 'foo'; diff --git a/test/fixtures/query/index.js b/test/fixtures/query/index.js new file mode 100644 index 0000000..5a6fd64 --- /dev/null +++ b/test/fixtures/query/index.js @@ -0,0 +1 @@ +require('./foo?raw'); diff --git a/test/test.spec.ts b/test/test.spec.ts index 7cd3c03..effcf59 100644 --- a/test/test.spec.ts +++ b/test/test.spec.ts @@ -1,199 +1,224 @@ -import assert from "node:assert" -import fs from "node:fs" -import path from "node:path" -import { type Stats, rspack } from "@rspack/core" -import ReactRefreshPlugin, { type PluginOptions } from "@rspack/plugin-react-refresh" +import assert from 'node:assert'; +import fs from 'node:fs'; +import path from 'node:path'; +import { type Stats, rspack } from '@rspack/core'; +import ReactRefreshPlugin, { + type PluginOptions, +} from '@rspack/plugin-react-refresh'; type Outputs = { - reactRefresh: string, - fixture: string, - runtime: string, - vendor: string -} + reactRefresh: string; + fixture: string; + runtime: string; + vendor: string; +}; -type CompilationCallback = (error: Error | null, stats: Stats | undefined, outputs: Outputs) => void +type CompilationCallback = ( + error: Error | null, + stats: Stats | undefined, + outputs: Outputs, +) => void; -const uniqueName = "ReactRefreshLibrary"; +const uniqueName = 'ReactRefreshLibrary'; -const compileWithReactRefresh = (fixturePath: string, refreshOptions: PluginOptions, callback: CompilationCallback) => { - let dist = path.join(fixturePath, "dist"); - rspack( - { - mode: "development", - context: fixturePath, - entry: { - fixture: path.join(fixturePath, "index.js") - }, - output: { - path: dist, - uniqueName - }, - plugins: [new ReactRefreshPlugin(refreshOptions)], - optimization: { - runtimeChunk: { - name: "runtime" - }, - splitChunks: { - cacheGroups: { - reactRefresh: { - test: /[\\/](react-refresh|rspack-plugin-react-refresh\/client|react-refresh-webpack-plugin)[\\/]/, - name: "react-refresh", - chunks: "all", - priority: -1000 - }, - foo: { - test: /[\\/]node_modules[\\/]foo/, - name: "vendor", - chunks: "all", - priority: -500, - enforce: true - } - } - } - } - }, - (error, stats) => { - expect(error).toBeFalsy(); - assert(stats, "stats is not defined"); - const statsJson = stats.toJson({ all: true }); - expect(statsJson.errors).toHaveLength(0); - expect(statsJson.warnings).toHaveLength(0); - callback(error, stats, { - reactRefresh: fs.readFileSync( - path.join(fixturePath, "dist", "react-refresh.js"), - "utf-8" - ), - fixture: fs.readFileSync( - path.join(fixturePath, "dist", "fixture.js"), - "utf-8" - ), - runtime: fs.readFileSync( - path.join(fixturePath, "dist", "runtime.js"), - "utf-8" - ), - vendor: fs.readFileSync( - path.join(fixturePath, "dist", "vendor.js"), - "utf-8" - ) - }); - } - ); +const compileWithReactRefresh = ( + fixturePath: string, + refreshOptions: PluginOptions, + callback: CompilationCallback, +) => { + let dist = path.join(fixturePath, 'dist'); + rspack( + { + mode: 'development', + context: fixturePath, + entry: { + fixture: path.join(fixturePath, 'index.js'), + }, + output: { + path: dist, + uniqueName, + }, + plugins: [new ReactRefreshPlugin(refreshOptions)], + optimization: { + runtimeChunk: { + name: 'runtime', + }, + splitChunks: { + cacheGroups: { + reactRefresh: { + test: /[\\/](react-refresh|rspack-plugin-react-refresh\/client|react-refresh-webpack-plugin)[\\/]/, + name: 'react-refresh', + chunks: 'all', + priority: -1000, + }, + foo: { + test: /[\\/]foo/, + name: 'vendor', + chunks: 'all', + priority: -500, + enforce: true, + }, + }, + }, + }, + }, + (error, stats) => { + expect(error).toBeFalsy(); + assert(stats, 'stats is not defined'); + const statsJson = stats.toJson({ all: true }); + expect(statsJson.errors).toHaveLength(0); + expect(statsJson.warnings).toHaveLength(0); + callback(error, stats, { + reactRefresh: fs.readFileSync( + path.join(fixturePath, 'dist', 'react-refresh.js'), + 'utf-8', + ), + fixture: fs.readFileSync( + path.join(fixturePath, 'dist', 'fixture.js'), + 'utf-8', + ), + runtime: fs.readFileSync( + path.join(fixturePath, 'dist', 'runtime.js'), + 'utf-8', + ), + vendor: fs.readFileSync( + path.join(fixturePath, 'dist', 'vendor.js'), + 'utf-8', + ), + }); + }, + ); }; -describe("react-refresh-rspack-plugin", () => { - it("should exclude node_modules when compiling with default options", done => { - compileWithReactRefresh( - path.join(__dirname, "fixtures/default"), - {}, - (_, __, { reactRefresh, fixture, runtime, vendor }) => { - expect(vendor).not.toContain("function $RefreshReg$"); - done(); - } - ); - }); +describe('react-refresh-rspack-plugin', () => { + it('should exclude node_modules when compiling with default options', (done) => { + compileWithReactRefresh( + path.join(__dirname, 'fixtures/default'), + {}, + (_, __, { reactRefresh, fixture, runtime, vendor }) => { + expect(vendor).not.toContain('function $RefreshReg$'); + done(); + }, + ); + }); + + it('should include non node_modules when compiling with default options', (done) => { + compileWithReactRefresh( + path.join(__dirname, 'fixtures/default'), + {}, + (_, __, { fixture }) => { + expect(fixture).toContain('function $RefreshReg$'); + done(); + }, + ); + }); - it("should include non node_modules when compiling with default options", done => { - compileWithReactRefresh( - path.join(__dirname, "fixtures/default"), - {}, - (_, __, { fixture }) => { - expect(fixture).toContain("function $RefreshReg$"); - done(); - } - ); - }); + it('should add library to make sure work in Micro-Frontend', (done) => { + compileWithReactRefresh( + path.join(__dirname, 'fixtures/default'), + {}, + (_, __, { reactRefresh }) => { + expect(reactRefresh).toContain(uniqueName); + done(); + }, + ); + }); - it("should add library to make sure work in Micro-Frontend", done => { - compileWithReactRefresh( - path.join(__dirname, "fixtures/default"), - {}, - (_, __, { reactRefresh }) => { - expect(reactRefresh).toContain(uniqueName); - done(); - } - ); - }); + it('should include selected file when compiling', (done) => { + compileWithReactRefresh( + path.join(__dirname, 'fixtures/custom'), + { + exclude: null, + include: path.join(__dirname, 'fixtures/node_modules/foo'), + }, + (_, __, { vendor }) => { + expect(vendor).toContain('function $RefreshReg$'); + done(); + }, + ); + }); - it("should include selected file when compiling", done => { - compileWithReactRefresh( - path.join(__dirname, "fixtures/custom"), - { - exclude: null, - include: path.join(__dirname, "fixtures/node_modules/foo") - }, - (_, __, { vendor }) => { - expect(vendor).toContain("function $RefreshReg$"); - done(); - } - ); - }); + it('should exclude selected file when compiling', (done) => { + compileWithReactRefresh( + path.join(__dirname, 'fixtures/custom'), + { + exclude: path.join(__dirname, 'fixtures/custom/index.js'), + }, + (_, __, { fixture }) => { + expect(fixture).not.toContain('function $RefreshReg$'); + done(); + }, + ); + }); - it("should exclude selected file when compiling", done => { - compileWithReactRefresh( - path.join(__dirname, "fixtures/custom"), - { - exclude: path.join(__dirname, "fixtures/custom/index.js") - }, - (_, __, { reactRefresh, fixture, runtime, vendor }) => { - expect(fixture).not.toContain("function $RefreshReg$"); - done(); - } - ); - }); + it('should exclude selected file via `resourceQuery` when compiling', (done) => { + compileWithReactRefresh( + path.join(__dirname, 'fixtures/query'), + { + resourceQuery: { not: /raw/ }, + }, + (_, __, { vendor }) => { + expect(vendor).not.toContain('function $RefreshReg$'); + done(); + }, + ); + }); - it("should allow custom inject loader when compiling", done => { - expect(ReactRefreshPlugin.loader).toBe("builtin:react-refresh-loader"); - compileWithReactRefresh( - path.join(__dirname, "fixtures/custom"), - { - injectLoader: false, - }, - (_, __, { reactRefresh, fixture, runtime, vendor }) => { - expect(fixture).not.toContain("function $RefreshReg$"); - done(); - } - ); - }); + it('should allow custom inject loader when compiling', (done) => { + expect(ReactRefreshPlugin.loader).toBe('builtin:react-refresh-loader'); + compileWithReactRefresh( + path.join(__dirname, 'fixtures/custom'), + { + injectLoader: false, + }, + (_, __, { reactRefresh, fixture, runtime, vendor }) => { + expect(fixture).not.toContain('function $RefreshReg$'); + done(); + }, + ); + }); - it("should allow custom inject entry when compiling", done => { - compileWithReactRefresh( - path.join(__dirname, "fixtures/custom"), - { - injectEntry: false, - }, - (_, __, { reactRefresh, fixture, runtime, vendor }) => { - expect(reactRefresh).not.toContain("RefreshRuntime.injectIntoGlobalHook(safeThis)"); - done(); - } - ); - }); + it('should allow custom inject entry when compiling', (done) => { + compileWithReactRefresh( + path.join(__dirname, 'fixtures/custom'), + { + injectEntry: false, + }, + (_, __, { reactRefresh, fixture, runtime, vendor }) => { + expect(reactRefresh).not.toContain( + 'RefreshRuntime.injectIntoGlobalHook(safeThis)', + ); + done(); + }, + ); + }); - it("should always exclude react-refresh related modules", done => { - compileWithReactRefresh( - path.join(__dirname, "fixtures/custom"), - { - exclude: null - }, - (_, __, { reactRefresh, fixture, runtime, vendor }) => { - expect(reactRefresh).not.toContain("function $RefreshReg$"); - done(); - } - ); - }); + it('should always exclude react-refresh related modules', (done) => { + compileWithReactRefresh( + path.join(__dirname, 'fixtures/custom'), + { + exclude: null, + }, + (_, __, { reactRefresh, fixture, runtime, vendor }) => { + expect(reactRefresh).not.toContain('function $RefreshReg$'); + done(); + }, + ); + }); - it("should include entries for webpack-hot-middleware", done => { - compileWithReactRefresh( - path.join(__dirname, "fixtures/custom"), - { - overlay: { - sockIntegration: 'whm' - } - }, - (_, __, { fixture }) => { - expect(fixture).toContain("webpack-hot-middleware/client"); - expect(fixture).toContain("WHMEventSource.js"); - done(); - } - ); - }); + it('should include entries for webpack-hot-middleware', (done) => { + compileWithReactRefresh( + path.join(__dirname, 'fixtures/custom'), + { + overlay: { + sockIntegration: 'whm', + }, + }, + (_, __, { fixture }) => { + expect(fixture).toContain('webpack-hot-middleware/client'); + expect(fixture).toContain('WHMEventSource.js'); + done(); + }, + ); + }); });