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..44a052ad 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", @@ -102,7 +104,7 @@ "@babel/core": "^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", "@symfony/stimulus-bridge": "^3.0.0", "@vue/babel-helper-vue-jsx-merge-props": "^1.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/test/loaders/babel.js b/test/loaders/babel.js index 6fa89e43..ab654f51 100644 --- a/test/loaders/babel.js +++ b/test/loaders/babel.js @@ -75,9 +75,23 @@ describe('loaders/babel', () => { // env, react & foo expect(actualLoaders[0].options.presets).to.have.lengthOf(3); - expect(actualLoaders[0].options.presets).to.include(require.resolve('@babel/preset-react')); + expect(actualLoaders[0].options.presets[0]).to.deep.equal([ + require.resolve('@babel/preset-env'), + { + corejs: null, + modules: false, + targets: {}, + useBuiltIns: false, + }, + ]); + expect(actualLoaders[0].options.presets[1]).to.deep.equal([ + require.resolve('@babel/preset-react'), + { + runtime: 'automatic', + } + ]); // foo is also still there, not overridden - expect(actualLoaders[0].options.presets).to.include('foo'); + expect(actualLoaders[0].options.presets[2]).to.equal('foo'); }); it('getLoaders() with preact', () => { 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"