diff --git a/fixtures/react-css-modules/components/App.jsx b/fixtures/react-css-modules/components/App.jsx
new file mode 100644
index 00000000..070c3ca0
--- /dev/null
+++ b/fixtures/react-css-modules/components/App.jsx
@@ -0,0 +1,12 @@
+import './styles.css';
+import './styles.less';
+import './styles.scss';
+import './styles.stylus';
+import stylesCss from './styles.module.css?module';
+import stylesLess from './styles.module.less?module';
+import stylesScss from './styles.module.scss?module';
+import stylesStylus from './styles.module.stylus?module';
+
+export default function App() {
+ return
+}
diff --git a/fixtures/react-css-modules/components/styles.css b/fixtures/react-css-modules/components/styles.css
new file mode 100644
index 00000000..75424513
--- /dev/null
+++ b/fixtures/react-css-modules/components/styles.css
@@ -0,0 +1,3 @@
+.red {
+ color: red;
+}
diff --git a/fixtures/react-css-modules/components/styles.less b/fixtures/react-css-modules/components/styles.less
new file mode 100644
index 00000000..fb48d7ff
--- /dev/null
+++ b/fixtures/react-css-modules/components/styles.less
@@ -0,0 +1,3 @@
+ .justified {
+ text-align: justify;
+ }
diff --git a/fixtures/react-css-modules/components/styles.module.css b/fixtures/react-css-modules/components/styles.module.css
new file mode 100644
index 00000000..ba6b5c49
--- /dev/null
+++ b/fixtures/react-css-modules/components/styles.module.css
@@ -0,0 +1,3 @@
+.italic {
+ font-style: italic;
+}
diff --git a/fixtures/react-css-modules/components/styles.module.less b/fixtures/react-css-modules/components/styles.module.less
new file mode 100644
index 00000000..8874fcf7
--- /dev/null
+++ b/fixtures/react-css-modules/components/styles.module.less
@@ -0,0 +1,3 @@
+ .underline {
+ text-decoration: underline;
+ }
diff --git a/fixtures/react-css-modules/components/styles.module.scss b/fixtures/react-css-modules/components/styles.module.scss
new file mode 100644
index 00000000..81d40c07
--- /dev/null
+++ b/fixtures/react-css-modules/components/styles.module.scss
@@ -0,0 +1,3 @@
+.bold {
+ font-weight: bold;
+}
diff --git a/fixtures/react-css-modules/components/styles.module.stylus b/fixtures/react-css-modules/components/styles.module.stylus
new file mode 100644
index 00000000..4c502130
--- /dev/null
+++ b/fixtures/react-css-modules/components/styles.module.stylus
@@ -0,0 +1,2 @@
+ .rtl
+ direction: rtl;
diff --git a/fixtures/react-css-modules/components/styles.scss b/fixtures/react-css-modules/components/styles.scss
new file mode 100644
index 00000000..3341edd2
--- /dev/null
+++ b/fixtures/react-css-modules/components/styles.scss
@@ -0,0 +1,3 @@
+.large {
+ font-size: 50px;
+}
diff --git a/fixtures/react-css-modules/components/styles.stylus b/fixtures/react-css-modules/components/styles.stylus
new file mode 100644
index 00000000..4d3024cd
--- /dev/null
+++ b/fixtures/react-css-modules/components/styles.stylus
@@ -0,0 +1,2 @@
+ .lowercase
+ text-transform: lowercase
diff --git a/fixtures/react-css-modules/main.js b/fixtures/react-css-modules/main.js
new file mode 100644
index 00000000..e28b9498
--- /dev/null
+++ b/fixtures/react-css-modules/main.js
@@ -0,0 +1,6 @@
+import {createRoot} from 'react-dom/client';
+import App from './components/App';
+
+const root = createRoot(document.getElementById('app'));
+
+root.render();
diff --git a/lib/loaders/babel.js b/lib/loaders/babel.js
index a10ceeb9..8ba0b42d 100644
--- a/lib/loaders/babel.js
+++ b/lib/loaders/babel.js
@@ -70,7 +70,10 @@ module.exports = {
if (webpackConfig.useReact) {
loaderFeatures.ensurePackagesExistAndAreCorrectVersion('react');
- babelConfig.presets.push(require.resolve('@babel/preset-react'));
+ babelConfig.presets.push([require.resolve('@babel/preset-react'), {
+ // TODO: To remove when Babel 8, "automatic" will become the default value
+ runtime: 'automatic',
+ }]);
}
if (webpackConfig.usePreact) {
diff --git a/package.json b/package.json
index 24e97b07..b7b9b00c 100755
--- a/package.json
+++ b/package.json
@@ -48,7 +48,7 @@
"@babel/eslint-parser": "^7.17.0",
"@babel/plugin-transform-react-jsx": "^7.12.11",
"@babel/preset-env": "^7.16.0",
- "@babel/preset-react": "^7.0.0",
+ "@babel/preset-react": "^7.9.0",
"@babel/preset-typescript": "^7.0.0",
"@hotwired/stimulus": "^3.0.0",
"@symfony/mock-module": "file:fixtures/stimulus/mock-module",
@@ -81,6 +81,8 @@
"preact": "^10.5.0",
"preact-compat": "^3.17.0",
"puppeteer": "^23.2.2",
+ "react": "^18.0.0",
+ "react-dom": "^18.0.0",
"sass": "^1.17.0",
"sass-loader": "^16.0.1",
"sinon": "^14.0.0",
diff --git a/test/functional.js b/test/functional.js
index c6701216..4aeac54d 100644
--- a/test/functional.js
+++ b/test/functional.js
@@ -1720,6 +1720,88 @@ module.exports = {
});
});
+ it('React supports CSS/Sass/Less/Stylus modules', (done) => {
+ const appDir = testSetup.createTestAppDir();
+ const config = testSetup.createWebpackConfig(appDir, 'www/build', 'dev');
+ config.enableSingleRuntimeChunk();
+ config.setPublicPath('/build');
+ config.addEntry('main', './react-css-modules/main.js');
+ config.enableReactPreset();
+ config.enableSassLoader();
+ config.enableLessLoader();
+ config.enableStylusLoader();
+ config.configureCssLoader(options => {
+ // Remove hashes from local ident names
+ // since they are not always the same.
+ if (options.modules) {
+ options.modules.localIdentName = '[local]_foo';
+ }
+ });
+
+ // Enable the PostCSS loader so we can use `lang="postcss"`
+ config.enablePostCssLoader();
+ fs.writeFileSync(
+ path.join(appDir, 'postcss.config.js'),
+ `
+module.exports = {
+ plugins: [
+ require('autoprefixer')()
+ ]
+} `
+ );
+
+ testSetup.runWebpack(config, (webpackAssert) => {
+ expect(config.outputPath).to.be.a.directory().with.deep.files([
+ 'main.js',
+ 'main.css',
+ 'manifest.json',
+ 'entrypoints.json',
+ 'runtime.js',
+ ]);
+
+ const expectClassDeclaration = (className) => {
+ webpackAssert.assertOutputFileContains(
+ 'main.css',
+ `.${className} {`
+ );
+ };
+
+ expectClassDeclaration('red'); // Standard CSS
+ expectClassDeclaration('large'); // Standard SCSS
+ expectClassDeclaration('justified'); // Standard Less
+ expectClassDeclaration('lowercase'); // Standard Stylus
+
+ expectClassDeclaration('italic_foo'); // CSS Module
+ expectClassDeclaration('bold_foo'); // SCSS Module
+ expectClassDeclaration('underline_foo'); // Less Module
+ expectClassDeclaration('rtl_foo'); // Stylus Module
+
+ testSetup.requestTestPage(
+ browser,
+ path.join(config.getContext(), 'www'),
+ [
+ 'build/runtime.js',
+ 'build/main.js'
+ ],
+ async({ page }) => {
+ const divClassArray = await page.evaluate(() => Array.from(document.body.querySelector('#app > div').classList.values()));
+
+ expect(divClassArray.includes('red')).to.be.true; // Standard CSS
+ expect(divClassArray.includes('large')).to.be.true; // Standard SCSS
+ expect(divClassArray.includes('justified')).to.be.true; // Standard Less
+ expect(divClassArray.includes('lowercase')).to.be.true; // Standard Stylus
+
+ expect(divClassArray.includes('italic_foo')).to.be.true; // CSS module
+ expect(divClassArray.includes('bold_foo')).to.be.true; // SCSS module
+ expect(divClassArray.includes('underline_foo')).to.be.true; // Less module
+ expect(divClassArray.includes('rtl_foo')).to.be.true; // Stylus module
+
+ done();
+ }
+ );
+ });
+ });
+
it('Preact supports CSS/Sass/Less/Stylus modules', (done) => {
const appDir = testSetup.createTestAppDir();
const config = testSetup.createWebpackConfig(appDir, 'www/build', 'dev');
@@ -1802,7 +1884,6 @@ module.exports = {
});
});
-
it('Vue.js error when using non-activated loaders', (done) => {
const config = createWebpackConfig('www/build', 'dev');
config.setPublicPath('/build');
diff --git a/yarn.lock b/yarn.lock
index a4109172..0199af52 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -975,7 +975,7 @@
"@babel/types" "^7.4.4"
esutils "^2.0.2"
-"@babel/preset-react@^7.0.0":
+"@babel/preset-react@^7.9.0":
version "7.24.7"
resolved "https://registry.yarnpkg.com/@babel/preset-react/-/preset-react-7.24.7.tgz#480aeb389b2a798880bf1f889199e3641cbb22dc"
integrity sha512-AAH4lEkpmzFWrGVlHaxJB7RLH21uPQ9+He+eFLWHmF9IuFQVugz8eAsamaW0DXRrTfco5zj1wWtpdcXJUOfsag==
@@ -4851,7 +4851,7 @@ log-symbols@^4.1.0:
chalk "^4.1.0"
is-unicode-supported "^0.1.0"
-loose-envify@^1.0.0, loose-envify@^1.4.0:
+loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.4.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf"
integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==
@@ -5918,11 +5918,26 @@ raw-body@2.5.2:
iconv-lite "0.4.24"
unpipe "1.0.0"
+react-dom@^18.0.0:
+ version "18.3.1"
+ resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.3.1.tgz#c2265d79511b57d479b3dd3fdfa51536494c5cb4"
+ integrity sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==
+ dependencies:
+ loose-envify "^1.1.0"
+ scheduler "^0.23.2"
+
react-is@^16.13.1:
version "16.13.1"
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
+react@^18.0.0:
+ version "18.3.1"
+ resolved "https://registry.yarnpkg.com/react/-/react-18.3.1.tgz#49ab892009c53933625bd16b2533fc754cab2891"
+ integrity sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==
+ dependencies:
+ loose-envify "^1.1.0"
+
readable-stream@^2.0.1:
version "2.3.8"
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.8.tgz#91125e8042bba1b9887f49345f6277027ce8be9b"
@@ -6183,6 +6198,13 @@ sax@~1.3.0:
resolved "https://registry.yarnpkg.com/sax/-/sax-1.3.0.tgz#a5dbe77db3be05c9d1ee7785dbd3ea9de51593d0"
integrity sha512-0s+oAmw9zLl1V1cS9BtZN7JAd0cW5e0QH4W3LWEK6a4LaLEA2OTpGYWDY+6XasBLtz6wkm3u1xRw95mRuJ59WA==
+scheduler@^0.23.2:
+ version "0.23.2"
+ resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.23.2.tgz#414ba64a3b282892e944cf2108ecc078d115cdc3"
+ integrity sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==
+ dependencies:
+ loose-envify "^1.1.0"
+
schema-utils@^3.0.0, schema-utils@^3.1.1, schema-utils@^3.2.0:
version "3.3.0"
resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-3.3.0.tgz#f50a88877c3c01652a15b622ae9e9795df7a60fe"