diff --git a/Cargo.lock b/Cargo.lock index d7936e2a3bb..72e5aa40d80 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2392,6 +2392,7 @@ dependencies = [ "rspack_error", "rspack_identifier", "rspack_ids", + "rspack_loader_react_refresh", "rspack_loader_runner", "rspack_loader_sass", "rspack_loader_swc", @@ -2576,6 +2577,16 @@ dependencies = [ "rustc-hash", ] +[[package]] +name = "rspack_loader_react_refresh" +version = "0.1.0" +dependencies = [ + "async-trait", + "rspack_core", + "rspack_error", + "rspack_loader_runner", +] + [[package]] name = "rspack_loader_runner" version = "0.1.0" diff --git a/crates/rspack_binding_options/Cargo.toml b/crates/rspack_binding_options/Cargo.toml index 9c9bd76b1a1..ce947b8f301 100644 --- a/crates/rspack_binding_options/Cargo.toml +++ b/crates/rspack_binding_options/Cargo.toml @@ -11,6 +11,7 @@ rspack_core = { path = "../rspack_core" } rspack_error = { path = "../rspack_error" } rspack_identifier = { path = "../rspack_identifier" } rspack_ids = { path = "../rspack_ids" } +rspack_loader_react_refresh = { path = "../rspack_loader_react_refresh" } rspack_loader_runner = { path = "../rspack_loader_runner" } rspack_loader_sass = { path = "../rspack_loader_sass" } rspack_loader_swc = { path = "../rspack_loader_swc" } diff --git a/crates/rspack_binding_options/src/options/raw_module/mod.rs b/crates/rspack_binding_options/src/options/raw_module/mod.rs index 60024b410ad..1cca38eb209 100644 --- a/crates/rspack_binding_options/src/options/raw_module/mod.rs +++ b/crates/rspack_binding_options/src/options/raw_module/mod.rs @@ -15,6 +15,7 @@ use rspack_core::{ ModuleRuleUseLoader, ModuleType, ParserOptions, ParserOptionsByModuleType, }; use rspack_error::internal_error; +use rspack_loader_react_refresh::REACT_REFRESH_LOADER_IDENTIFIER; use rspack_loader_sass::SASS_LOADER_IDENTIFIER; use rspack_loader_swc::SWC_LOADER_IDENTIFIER; use serde::Deserialize; @@ -44,6 +45,11 @@ pub fn get_builtin_loader(builtin: &str, options: Option<&str>) -> BoxLoader { .with_identifier(builtin.into()), ); } + if builtin.starts_with(REACT_REFRESH_LOADER_IDENTIFIER) { + return Arc::new( + rspack_loader_react_refresh::ReactRefreshLoader::new().with_identifier(builtin.into()), + ); + } unreachable!("Unexpected builtin loader: {builtin}") } diff --git a/crates/rspack_loader_react_refresh/Cargo.toml b/crates/rspack_loader_react_refresh/Cargo.toml new file mode 100644 index 00000000000..c5776208b2a --- /dev/null +++ b/crates/rspack_loader_react_refresh/Cargo.toml @@ -0,0 +1,14 @@ +[package] +edition = "2021" +license = "MIT" +name = "rspack_loader_react_refresh" +repository = "https://github.com/web-infra-dev/rspack" +version = "0.1.0" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +async-trait = { workspace = true } +rspack_core = { path = "../rspack_core" } +rspack_error = { path = "../rspack_error" } +rspack_loader_runner = { path = "../rspack_loader_runner" } diff --git a/crates/rspack_loader_react_refresh/LICENSE b/crates/rspack_loader_react_refresh/LICENSE new file mode 100644 index 00000000000..46310101ad8 --- /dev/null +++ b/crates/rspack_loader_react_refresh/LICENSE @@ -0,0 +1,22 @@ +MIT License + +Copyright (c) 2022-present Bytedance, Inc. and its affiliates. + + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/crates/rspack_loader_react_refresh/src/lib.rs b/crates/rspack_loader_react_refresh/src/lib.rs new file mode 100644 index 00000000000..df3d6163edc --- /dev/null +++ b/crates/rspack_loader_react_refresh/src/lib.rs @@ -0,0 +1,51 @@ +use rspack_core::LoaderRunnerContext; +use rspack_error::{internal_error, Result}; +use rspack_loader_runner::{Identifiable, Identifier, Loader, LoaderContext}; + +pub struct ReactRefreshLoader { + identifier: Identifier, +} + +impl ReactRefreshLoader { + pub fn new() -> Self { + Self { + identifier: REACT_REFRESH_LOADER_IDENTIFIER.into(), + } + } + + /// Panics: + /// Panics if `identifier` passed in is not starting with `builtin:react-refresh-loader`. + pub fn with_identifier(mut self, identifier: Identifier) -> Self { + assert!(identifier.starts_with(REACT_REFRESH_LOADER_IDENTIFIER)); + self.identifier = identifier; + self + } +} + +#[async_trait::async_trait] +impl Loader for ReactRefreshLoader { + async fn run(&self, loader_context: &mut LoaderContext<'_, LoaderRunnerContext>) -> Result<()> { + let Some(content) = std::mem::take(&mut loader_context.content) else { + return Err(internal_error!("Content should be available")) + }; + let mut source = content.try_into_string()?; + source += r#" +function $RefreshReg$(type, id) { + $ReactRefreshRuntime$.register(type, __webpack_module__.id + "_" + id); +} +Promise.resolve().then(function() { + $ReactRefreshRuntime$.refresh(__webpack_module__.id, __webpack_module__.hot); +}); +"#; + loader_context.content = Some(source.into()); + Ok(()) + } +} + +pub const REACT_REFRESH_LOADER_IDENTIFIER: &str = "builtin:react-refresh-loader"; + +impl Identifiable for ReactRefreshLoader { + fn identifier(&self) -> Identifier { + self.identifier + } +} diff --git a/examples/react-refresh/package.json b/examples/react-refresh/package.json index a34b24734f6..ad6729b1d9d 100644 --- a/examples/react-refresh/package.json +++ b/examples/react-refresh/package.json @@ -6,15 +6,19 @@ "private": true, "scripts": { "dev": "rspack serve ", - "build": "rspack build" + "build": "cross-env NODE_ENV=production rspack build" }, "keywords": [], "author": "", "license": "MIT", "dependencies": { - "@rspack/cli": "workspace:*", "react": "18.0.0", - "react-dom": "18.0.0", + "react-dom": "18.0.0" + }, + "devDependencies": { + "@rspack/cli": "workspace:*", + "@rspack/core": "workspace:*", + "@rspack/plugin-react-refresh": "workspace:*", "react-refresh": "0.13.0" } } \ No newline at end of file diff --git a/examples/react-refresh/rspack.config.js b/examples/react-refresh/rspack.config.js index cc6fb651af4..5e798e18184 100644 --- a/examples/react-refresh/rspack.config.js +++ b/examples/react-refresh/rspack.config.js @@ -1,14 +1,49 @@ -/** - * @type {import('@rspack/cli').Configuration} - */ +const rspack = require("@rspack/core") +const ReactRefreshPlugin = require("@rspack/plugin-react-refresh") + +const isProduction = process.env.NODE_ENV === "production" + +/** @type {import('@rspack/cli').Configuration} */ const config = { - mode: "development", - entry: { main: "./src/index.tsx" }, - builtins: { - html: [{ template: "./index.html" }], - define: { - "process.env.NODE_ENV": "'development'" + experiments: { + rspackFuture: { + disableTransformByDefault: true, } - } + }, + mode: isProduction ? "production" : "development", + entry: { main: "./src/index.tsx" }, + devtool: 'source-map', + module: { + rules: [ + { + test: /\.tsx$/, + use: { + loader: "builtin:swc-loader", + options: { + sourceMap: true, + jsc: { + parser: { + syntax: "typescript", + tsx: true + }, + transform: { + react: { + runtime: "automatic", + development: !isProduction, + refresh: !isProduction, + } + } + } + } + } + } + ] + }, + plugins: [ + new rspack.HtmlRspackPlugin({ template: "./index.html" }), + new rspack.DefinePlugin({ "process.env.NODE_ENV": "'development'" }), + !isProduction && new ReactRefreshPlugin(), + ].filter(Boolean) }; + module.exports = config; diff --git a/packages/playground/cases/react/basic-disableTransformByDefault/index.test.ts b/packages/playground/cases/react/basic-disableTransformByDefault/index.test.ts new file mode 100644 index 00000000000..33fefe02749 --- /dev/null +++ b/packages/playground/cases/react/basic-disableTransformByDefault/index.test.ts @@ -0,0 +1,59 @@ +import { test, expect } from "@/fixtures"; + +test("render should work", async ({ page }) => { + expect(await page.textContent(".header")).toBe("Hello World"); + expect(await page.textContent("#lazy-component")).toBe("Lazy Component"); +}); + +test("hmr should work", async ({ page, fileAction, rspack }) => { + expect(await page.textContent("button")).toBe("10"); + await page.click("button"); + expect(await page.textContent("button")).toBe("11"); + expect(await page.textContent(".placeholder")).toBe("__PLACE_HOLDER__"); + fileAction.updateFile("src/App.jsx", content => + content.replace("__PLACE_HOLDER__", "__EDITED__") + ); + await rspack.waitingForHmr(async function () { + return (await page.textContent(".placeholder")) === "__EDITED__"; + }); + expect(await page.textContent("button")).toBe("11"); +}); + +test("context+component should work", async ({ page, fileAction, rspack }) => { + expect(await page.textContent("#context")).toBe("context-value"); + await page.click("#context"); + expect(await page.textContent("#context")).toBe("context-value-click"); + fileAction.updateFile("src/CountProvider.jsx", content => + content.replace("context-value", "context-value-update") + ); + await rspack.waitingForHmr(async function () { + return (await page.textContent("#context")) === "context-value-update"; + }); +}); + +test("ReactRefreshFinder should work", async ({ page }) => { + expect(await page.textContent("#nest-function")).toBe("nest-function"); +}); + +test("update same export name from different module should work", async ({ + page, + fileAction, + rspack +}) => { + expect(await page.textContent(".same-export-name1")).toBe("__NAME_1__"); + expect(await page.textContent(".same-export-name2")).toBe("__NAME_2__"); + fileAction.updateFile("src/SameExportName1.jsx", content => + content.replace("__NAME_1__", "__name_1__") + ); + await rspack.waitingForHmr(async function () { + return (await page.textContent(".same-export-name1")) === "__name_1__"; + }); + expect(await page.textContent(".same-export-name2")).toBe("__NAME_2__"); + fileAction.updateFile("src/SameExportName2.jsx", content => + content.replace("__NAME_2__", "__name_2__") + ); + await rspack.waitingForHmr(async function () { + return (await page.textContent(".same-export-name2")) === "__name_2__"; + }); + expect(await page.textContent(".same-export-name1")).toBe("__name_1__"); +}); diff --git a/packages/playground/cases/react/basic-disableTransformByDefault/rspack.config.js b/packages/playground/cases/react/basic-disableTransformByDefault/rspack.config.js new file mode 100644 index 00000000000..7d9545fcbe8 --- /dev/null +++ b/packages/playground/cases/react/basic-disableTransformByDefault/rspack.config.js @@ -0,0 +1,57 @@ +const rspack = require("@rspack/core"); +const ReactRefreshPlugin = require("@rspack/plugin-react-refresh"); + +/** @type { import('@rspack/core').RspackOptions } */ +module.exports = { + experiments: { + rspackFuture: { + disableTransformByDefault: true + } + }, + context: __dirname, + mode: "development", + module: { + rules: [ + { + test: /\.jsx$/, + use: { + loader: "builtin:swc-loader", + options: { + jsc: { + parser: { + syntax: "ecmascript", + jsx: true + }, + transform: { + react: { + runtime: "automatic", + development: true, + refresh: true + } + } + } + } + } + } + ] + }, + plugins: [ + new rspack.HtmlRspackPlugin({ template: "./src/index.html" }), + new ReactRefreshPlugin() + ], + entry: "./src/index.jsx", + devServer: { + hot: true, + devMiddleware: { + writeToDisk: true + } + }, + cache: false, + stats: "none", + infrastructureLogging: { + debug: false + }, + watchOptions: { + poll: 1000 + } +}; diff --git a/packages/playground/cases/react/basic-disableTransformByDefault/src/App.jsx b/packages/playground/cases/react/basic-disableTransformByDefault/src/App.jsx new file mode 100644 index 00000000000..a72ff3a972a --- /dev/null +++ b/packages/playground/cases/react/basic-disableTransformByDefault/src/App.jsx @@ -0,0 +1,30 @@ +import React from 'react' +import './index.css' +import { ContextComponent } from './CountProvider' +import { ReactRefreshFinder } from './ReactRefreshFinder' +import { SameExportName as SameExportName1 } from './SameExportName1' +import { SameExportName as SameExportName2 } from './SameExportName2' + +const Button = () => { + const [count, setCount] = React.useState(10) + return +} + +const LazyComponent = React.lazy(() => import('./LazyComponent')) + +export const App = () => { + return ( +
+
Hello World
+
}> + + + + ) +} diff --git a/packages/playground/cases/react/basic-disableTransformByDefault/src/CountProvider.jsx b/packages/playground/cases/react/basic-disableTransformByDefault/src/CountProvider.jsx new file mode 100644 index 00000000000..afbe1fcbe34 --- /dev/null +++ b/packages/playground/cases/react/basic-disableTransformByDefault/src/CountProvider.jsx @@ -0,0 +1,19 @@ +import React from 'react'; + +export const CountContext = React.createContext(); + +export function CountProvider({ children }) { + const [count, setCount] = React.useState('context-value'); + return ( + + {children} + + ); +} + +export function ContextComponent() { + const { count, setCount } = React.useContext(CountContext); + return
setCount((count) => count + '-click')}> + {count} +
+} \ No newline at end of file diff --git a/packages/playground/cases/react/basic-disableTransformByDefault/src/LazyComponent.css b/packages/playground/cases/react/basic-disableTransformByDefault/src/LazyComponent.css new file mode 100644 index 00000000000..e69de29bb2d diff --git a/packages/playground/cases/react/basic-disableTransformByDefault/src/LazyComponent.jsx b/packages/playground/cases/react/basic-disableTransformByDefault/src/LazyComponent.jsx new file mode 100644 index 00000000000..65545394690 --- /dev/null +++ b/packages/playground/cases/react/basic-disableTransformByDefault/src/LazyComponent.jsx @@ -0,0 +1,7 @@ +import './LazyComponent.css' + +function LazyComponent () { + return
Lazy Component
+} + +export default LazyComponent diff --git a/packages/playground/cases/react/basic-disableTransformByDefault/src/ReactRefreshFinder.jsx b/packages/playground/cases/react/basic-disableTransformByDefault/src/ReactRefreshFinder.jsx new file mode 100644 index 00000000000..83b0509d747 --- /dev/null +++ b/packages/playground/cases/react/basic-disableTransformByDefault/src/ReactRefreshFinder.jsx @@ -0,0 +1,10 @@ +import React, { useState } from 'react'; + +function CreateReactRefreshFinder() { + return function Component() { + useState(1); + return
nest-function
; + }; +} + +export const ReactRefreshFinder = CreateReactRefreshFinder() \ No newline at end of file diff --git a/packages/playground/cases/react/basic-disableTransformByDefault/src/SameExportName1.jsx b/packages/playground/cases/react/basic-disableTransformByDefault/src/SameExportName1.jsx new file mode 100644 index 00000000000..2a7f2b45f15 --- /dev/null +++ b/packages/playground/cases/react/basic-disableTransformByDefault/src/SameExportName1.jsx @@ -0,0 +1,3 @@ +export function SameExportName() { + return
__NAME_1__
+} diff --git a/packages/playground/cases/react/basic-disableTransformByDefault/src/SameExportName2.jsx b/packages/playground/cases/react/basic-disableTransformByDefault/src/SameExportName2.jsx new file mode 100644 index 00000000000..8bf62ab6bb5 --- /dev/null +++ b/packages/playground/cases/react/basic-disableTransformByDefault/src/SameExportName2.jsx @@ -0,0 +1,3 @@ +export function SameExportName() { + return
__NAME_2__
+} diff --git a/packages/playground/cases/react/basic-disableTransformByDefault/src/index.css b/packages/playground/cases/react/basic-disableTransformByDefault/src/index.css new file mode 100644 index 00000000000..753f48a591f --- /dev/null +++ b/packages/playground/cases/react/basic-disableTransformByDefault/src/index.css @@ -0,0 +1,3 @@ +body { + background-color: rgba(0, 0, 0, 0); +} diff --git a/packages/playground/cases/react/basic-disableTransformByDefault/src/index.html b/packages/playground/cases/react/basic-disableTransformByDefault/src/index.html new file mode 100644 index 00000000000..127a457455b --- /dev/null +++ b/packages/playground/cases/react/basic-disableTransformByDefault/src/index.html @@ -0,0 +1,12 @@ + + + + + + + Document + + +
+ + diff --git a/packages/playground/cases/react/basic-disableTransformByDefault/src/index.jsx b/packages/playground/cases/react/basic-disableTransformByDefault/src/index.jsx new file mode 100644 index 00000000000..52e72c3334e --- /dev/null +++ b/packages/playground/cases/react/basic-disableTransformByDefault/src/index.jsx @@ -0,0 +1,11 @@ +import React from "react"; +import { createRoot } from "react-dom/client"; +import { App } from './App'; +import { CountProvider } from "./CountProvider"; + +const container = createRoot(document.getElementById("root")); +container.render( + + + +); diff --git a/packages/playground/cases/react/class-component-disableTransformByDefault/index.test.ts b/packages/playground/cases/react/class-component-disableTransformByDefault/index.test.ts new file mode 100644 index 00000000000..b1c3c5f0a17 --- /dev/null +++ b/packages/playground/cases/react/class-component-disableTransformByDefault/index.test.ts @@ -0,0 +1,24 @@ +import { test, expect } from "@/fixtures"; + +test("render should work", async ({ page }) => { + expect(await page.textContent(".header")).toBe("Hello World"); +}); + +test("class component hmr should work", async ({ + page, + fileAction, + rspack +}) => { + expect(await page.textContent("button")).toBe("10"); + await page.click("button"); + expect(await page.textContent("button")).toBe("11"); + expect(await page.textContent(".placeholder")).toBe("__PLACE_HOLDER__"); + fileAction.updateFile("src/App.jsx", content => + content.replace("__PLACE_HOLDER__", "__EDITED__") + ); + await rspack.waitingForHmr(async function () { + return (await page.textContent(".placeholder")) === "__EDITED__"; + }); + // class component will not keep local status + expect(await page.textContent("button")).toBe("10"); +}); diff --git a/packages/playground/cases/react/class-component-disableTransformByDefault/rspack.config.js b/packages/playground/cases/react/class-component-disableTransformByDefault/rspack.config.js new file mode 100644 index 00000000000..b3cc8c5472d --- /dev/null +++ b/packages/playground/cases/react/class-component-disableTransformByDefault/rspack.config.js @@ -0,0 +1,54 @@ +const rspack = require("@rspack/core"); +const ReactRefreshPlugin = require("@rspack/plugin-react-refresh"); + +/** @type { import('@rspack/core').RspackOptions } */ +module.exports = { + experiments: { + rspackFuture: { + disableTransformByDefault: true + } + }, + context: __dirname, + mode: "development", + entry: "./src/index.jsx", + module: { + rules: [ + { + test: /\.jsx$/, + use: { + loader: "builtin:swc-loader", + options: { + jsc: { + parser: { + syntax: "ecmascript", + jsx: true + }, + transform: { + react: { + runtime: "automatic", + development: true, + refresh: true + } + } + } + } + } + } + ] + }, + plugins: [ + new rspack.HtmlRspackPlugin({ template: "./src/index.html" }), + new ReactRefreshPlugin() + ], + devServer: { + hot: true + }, + cache: false, + stats: "none", + infrastructureLogging: { + debug: false + }, + watchOptions: { + poll: 1000 + } +}; diff --git a/packages/playground/cases/react/class-component-disableTransformByDefault/src/App.jsx b/packages/playground/cases/react/class-component-disableTransformByDefault/src/App.jsx new file mode 100644 index 00000000000..dd95da73edd --- /dev/null +++ b/packages/playground/cases/react/class-component-disableTransformByDefault/src/App.jsx @@ -0,0 +1,18 @@ +import React from "react"; + +const Button = () => { + const [count, setCount] = React.useState(10); + return ; +}; + +export class App extends React.Component { + render() { + return ( +
+
Hello World
+
+ ); + } +} diff --git a/packages/playground/cases/react/class-component-disableTransformByDefault/src/index.html b/packages/playground/cases/react/class-component-disableTransformByDefault/src/index.html new file mode 100644 index 00000000000..127a457455b --- /dev/null +++ b/packages/playground/cases/react/class-component-disableTransformByDefault/src/index.html @@ -0,0 +1,12 @@ + + + + + + + Document + + +
+ + diff --git a/packages/playground/cases/react/class-component-disableTransformByDefault/src/index.jsx b/packages/playground/cases/react/class-component-disableTransformByDefault/src/index.jsx new file mode 100644 index 00000000000..18f79ce0697 --- /dev/null +++ b/packages/playground/cases/react/class-component-disableTransformByDefault/src/index.jsx @@ -0,0 +1,6 @@ +import React from 'react' +import { createRoot } from 'react-dom/client' +import { App } from './App' + +const container = createRoot(document.getElementById('root')) +container.render() diff --git a/packages/playground/cases/react/tailwindcss-disableTransformByDefault/index.test.ts b/packages/playground/cases/react/tailwindcss-disableTransformByDefault/index.test.ts new file mode 100644 index 00000000000..72eda5d7793 --- /dev/null +++ b/packages/playground/cases/react/tailwindcss-disableTransformByDefault/index.test.ts @@ -0,0 +1,32 @@ +import { test, expect } from "@/fixtures"; + +test("tailwindcss should work when modify js file", async ({ + page, + fileAction, + rspack +}) => { + function getAppFontSize() { + return page.evaluate(() => { + const app = document.querySelector("#app"); + if (!app) { + return ""; + } + return window.getComputedStyle(app).fontSize; + }); + } + + let appFontSize = await getAppFontSize(); + expect(appFontSize).toBe("24px"); + + // update + fileAction.updateFile("src/App.jsx", content => { + return content.replace("text-2xl", "text-3xl"); + }); + await rspack.waitingForHmr(async () => { + const classNames = await page.getAttribute("#app", "class"); + return classNames?.includes("text-3xl") || false; + }); + + appFontSize = await getAppFontSize(); + expect(appFontSize).toBe("30px"); +}); diff --git a/packages/playground/cases/react/tailwindcss-disableTransformByDefault/rspack.config.js b/packages/playground/cases/react/tailwindcss-disableTransformByDefault/rspack.config.js new file mode 100644 index 00000000000..2ea530d10ab --- /dev/null +++ b/packages/playground/cases/react/tailwindcss-disableTransformByDefault/rspack.config.js @@ -0,0 +1,62 @@ +const path = require("path"); +const rspack = require("@rspack/core"); +const ReactRefreshPlugin = require("@rspack/plugin-react-refresh"); + +module.exports = { + experiments: { + rspackFuture: { + disableTransformByDefault: true + } + }, + context: __dirname, + entry: { + main: "./src/main.jsx" + }, + plugins: [ + new rspack.HtmlRspackPlugin({ template: "./src/index.html" }), + new ReactRefreshPlugin() + ], + module: { + rules: [ + { + test: /\.jsx$/, + use: { + loader: "builtin:swc-loader", + options: { + jsc: { + parser: { + syntax: "ecmascript", + jsx: true + }, + transform: { + react: { + runtime: "automatic", + development: true, + refresh: true + } + } + } + } + } + }, + { + test: /\.css$/, + use: [ + { + loader: "postcss-loader", + options: { + postcssOptions: { + plugins: { + tailwindcss: { + config: path.join(__dirname, "./tailwind.config.js") + } + } + } + } + } + ], + type: "css" + } + ] + } +}; diff --git a/packages/playground/cases/react/tailwindcss-disableTransformByDefault/src/App.css b/packages/playground/cases/react/tailwindcss-disableTransformByDefault/src/App.css new file mode 100644 index 00000000000..6300d8a49d6 --- /dev/null +++ b/packages/playground/cases/react/tailwindcss-disableTransformByDefault/src/App.css @@ -0,0 +1,10 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +#root { + max-width: 1280px; + margin: 0 auto; + padding: 2rem; + text-align: center; +} diff --git a/packages/playground/cases/react/tailwindcss-disableTransformByDefault/src/App.jsx b/packages/playground/cases/react/tailwindcss-disableTransformByDefault/src/App.jsx new file mode 100644 index 00000000000..51826c77656 --- /dev/null +++ b/packages/playground/cases/react/tailwindcss-disableTransformByDefault/src/App.jsx @@ -0,0 +1,12 @@ +import React from "react"; +import "./App.css"; + +function App() { + return ( +

+ Hello world! +

+ ); +} + +export default App; diff --git a/packages/playground/cases/react/tailwindcss-disableTransformByDefault/src/index.html b/packages/playground/cases/react/tailwindcss-disableTransformByDefault/src/index.html new file mode 100644 index 00000000000..127a457455b --- /dev/null +++ b/packages/playground/cases/react/tailwindcss-disableTransformByDefault/src/index.html @@ -0,0 +1,12 @@ + + + + + + + Document + + +
+ + diff --git a/packages/playground/cases/react/tailwindcss-disableTransformByDefault/src/main.jsx b/packages/playground/cases/react/tailwindcss-disableTransformByDefault/src/main.jsx new file mode 100644 index 00000000000..d0d585c0881 --- /dev/null +++ b/packages/playground/cases/react/tailwindcss-disableTransformByDefault/src/main.jsx @@ -0,0 +1,5 @@ +import React from "react"; +import ReactDOM from "react-dom/client"; +import App from "./App"; + +ReactDOM.createRoot(document.getElementById("root")).render(); diff --git a/packages/playground/cases/react/tailwindcss-disableTransformByDefault/tailwind.config.js b/packages/playground/cases/react/tailwindcss-disableTransformByDefault/tailwind.config.js new file mode 100644 index 00000000000..c0d9e5130a2 --- /dev/null +++ b/packages/playground/cases/react/tailwindcss-disableTransformByDefault/tailwind.config.js @@ -0,0 +1,9 @@ +/** @type {import('tailwindcss').Config} */ +const path = require("path"); +module.exports = { + content: [path.join(__dirname, "./src/**/*.{html,js,jsx}")], + theme: { + extend: {} + }, + plugins: [] +}; diff --git a/packages/playground/cases/react/worker-disableTransformByDefault/index.test.ts b/packages/playground/cases/react/worker-disableTransformByDefault/index.test.ts new file mode 100644 index 00000000000..e5cf91b389c --- /dev/null +++ b/packages/playground/cases/react/worker-disableTransformByDefault/index.test.ts @@ -0,0 +1,13 @@ +import { test, expect } from "@/fixtures"; + +test("should successfully render the page", async ({ page }) => { + expect(await page.textContent("button")).toBe("+"); + expect(await page.textContent("h1")).toBe("0"); +}); + +// test("worker should work", async ({ page, fileAction, rspack }) => { +// await page.click("button"); +// expect(await page.textContent("h1")).toBe("1"); +// await page.click("button"); +// expect(await page.textContent("h1")).toBe("2"); +// }); diff --git a/packages/playground/cases/react/worker-disableTransformByDefault/rspack.config.js b/packages/playground/cases/react/worker-disableTransformByDefault/rspack.config.js new file mode 100644 index 00000000000..b3cc8c5472d --- /dev/null +++ b/packages/playground/cases/react/worker-disableTransformByDefault/rspack.config.js @@ -0,0 +1,54 @@ +const rspack = require("@rspack/core"); +const ReactRefreshPlugin = require("@rspack/plugin-react-refresh"); + +/** @type { import('@rspack/core').RspackOptions } */ +module.exports = { + experiments: { + rspackFuture: { + disableTransformByDefault: true + } + }, + context: __dirname, + mode: "development", + entry: "./src/index.jsx", + module: { + rules: [ + { + test: /\.jsx$/, + use: { + loader: "builtin:swc-loader", + options: { + jsc: { + parser: { + syntax: "ecmascript", + jsx: true + }, + transform: { + react: { + runtime: "automatic", + development: true, + refresh: true + } + } + } + } + } + } + ] + }, + plugins: [ + new rspack.HtmlRspackPlugin({ template: "./src/index.html" }), + new ReactRefreshPlugin() + ], + devServer: { + hot: true + }, + cache: false, + stats: "none", + infrastructureLogging: { + debug: false + }, + watchOptions: { + poll: 1000 + } +}; diff --git a/packages/playground/cases/react/worker-disableTransformByDefault/src/App.jsx b/packages/playground/cases/react/worker-disableTransformByDefault/src/App.jsx new file mode 100644 index 00000000000..9ccfd1f8ac4 --- /dev/null +++ b/packages/playground/cases/react/worker-disableTransformByDefault/src/App.jsx @@ -0,0 +1,21 @@ +import React, { useState } from "react"; +import Button from './Button' + +let updateRenderTimes; + +const worker = new Worker(new URL("./worker", import.meta.url)); + +worker.onmessage = (e) => { + updateRenderTimes(e.data) +} + +export const App = () => { + const [renderTimes, setRenderTimes] = useState(0) + updateRenderTimes = setRenderTimes; + return ( +
+

{renderTimes}

+
+ ); +}; diff --git a/packages/playground/cases/react/worker-disableTransformByDefault/src/Button.jsx b/packages/playground/cases/react/worker-disableTransformByDefault/src/Button.jsx new file mode 100644 index 00000000000..ddaa59b3bee --- /dev/null +++ b/packages/playground/cases/react/worker-disableTransformByDefault/src/Button.jsx @@ -0,0 +1,15 @@ +import React from "react"; + +export default function Button({ onClick }) { + return ; +}; + +Button.count = 0; + +Button.get = () => { + return Button.count; +} + +Button.add = () => { + Button.count += 1; +} diff --git a/packages/playground/cases/react/worker-disableTransformByDefault/src/index.html b/packages/playground/cases/react/worker-disableTransformByDefault/src/index.html new file mode 100644 index 00000000000..127a457455b --- /dev/null +++ b/packages/playground/cases/react/worker-disableTransformByDefault/src/index.html @@ -0,0 +1,12 @@ + + + + + + + Document + + +
+ + diff --git a/packages/playground/cases/react/worker-disableTransformByDefault/src/index.jsx b/packages/playground/cases/react/worker-disableTransformByDefault/src/index.jsx new file mode 100644 index 00000000000..032d1403b0c --- /dev/null +++ b/packages/playground/cases/react/worker-disableTransformByDefault/src/index.jsx @@ -0,0 +1,6 @@ +import React from "react"; +import { createRoot } from "react-dom/client"; +import { App } from './App'; + +const container = createRoot(document.getElementById("root")); +container.render(); diff --git a/packages/playground/cases/react/worker-disableTransformByDefault/src/worker.js b/packages/playground/cases/react/worker-disableTransformByDefault/src/worker.js new file mode 100644 index 00000000000..01baa84f07d --- /dev/null +++ b/packages/playground/cases/react/worker-disableTransformByDefault/src/worker.js @@ -0,0 +1,6 @@ +import Button from "./Button"; + +onmessage = e => { + Button.add(); + postMessage(Button.get()); +}; diff --git a/packages/rspack-plugin-react-refresh/src/index.js b/packages/rspack-plugin-react-refresh/src/index.js index 92b057de0ed..6f24531ebc7 100644 --- a/packages/rspack-plugin-react-refresh/src/index.js +++ b/packages/rspack-plugin-react-refresh/src/index.js @@ -27,9 +27,10 @@ module.exports = class ReactRefreshRspackPlugin { $ReactRefreshRuntime$: reactRefreshPath }).apply(compiler); - compiler.options.module.rules.push({ - include: runtimePaths, - type: "js" + compiler.options.module.rules.unshift({ + exclude: [/node_modules/i, ...runtimePaths], + include: /\.([cm]js|[jt]sx?|flow)$/i, + use: "builtin:react-refresh-loader" }); } }; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5da82ac5a37..a47a436cea1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -563,13 +563,18 @@ importers: examples/react-refresh: specifiers: '@rspack/cli': workspace:* + '@rspack/core': workspace:* + '@rspack/plugin-react-refresh': workspace:* react: 18.0.0 react-dom: 18.0.0 react-refresh: 0.13.0 dependencies: - '@rspack/cli': link:../../packages/rspack-cli react: 18.0.0 react-dom: 18.0.0_react@18.0.0 + devDependencies: + '@rspack/cli': link:../../packages/rspack-cli + '@rspack/core': link:../../packages/rspack + '@rspack/plugin-react-refresh': link:../../packages/rspack-plugin-react-refresh react-refresh: 0.13.0 examples/react-storybook: @@ -23468,7 +23473,6 @@ packages: /react-refresh/0.13.0: resolution: {integrity: sha512-XP8A9BT0CpRBD+NYLLeIhld/RqG9+gktUjW1FkE+Vm7OCinbG1SshcK5tb9ls4kzvjZr9mOQc7HYgBngEyPAXg==} engines: {node: '>=0.10.0'} - dev: false /react-refresh/0.14.0: resolution: {integrity: sha512-wViHqhAd8OHeLS/IRMJjTSDHF3U9eWi62F/MledQGPdJGDhodXJ9PBLNGr6WWL7qlH12Mt3TyTpbS+hGXMjCzQ==}