diff --git a/packages/next/build/webpack-config.ts b/packages/next/build/webpack-config.ts index 5f9d502d507a..2e707e03d28a 100644 --- a/packages/next/build/webpack-config.ts +++ b/packages/next/build/webpack-config.ts @@ -1237,6 +1237,7 @@ export default async function getBaseWebpackConfig( dev ? '' : appDir ? '-[chunkhash]' : '-[contenthash]' }.js`, library: isClient || isEdgeServer ? '_N_E' : undefined, + uniqueName: config.experimental.webpackCssExperiment ? '_N_E' : undefined, libraryTarget: isClient || isEdgeServer ? 'assign' : 'commonjs2', hotUpdateChunkFilename: 'static/webpack/[id].[fullhash].hot-update.js', hotUpdateMainFilename: @@ -1751,6 +1752,21 @@ export default async function getBaseWebpackConfig( ].filter<[Feature, boolean]>(Boolean as any) ) ), + ...(config.experimental.webpackCssExperiment + ? [ + new webpack.ids.DeterministicModuleIdsPlugin({ + maxLength: 5, + failOnConflict: true, + fixedLength: true, + test: (m) => m.type.startsWith('css'), + }), + new webpack.experiments.ids.SyncModuleIdsPlugin({ + test: (m) => m.type.startsWith('css'), + path: path.resolve(distDir, 'module-ids.json'), + mode: 'create', + }), + ] + : []), ].filter(Boolean as any as ExcludesFalse), } @@ -1802,6 +1818,7 @@ export default async function getBaseWebpackConfig( ...config.experimental.urlImports, } : undefined, + css: config.experimental.webpackCssExperiment, } webpack5Config.module!.parser = { @@ -2162,9 +2179,11 @@ export default async function getBaseWebpackConfig( } const hasUserCssConfig = - webpackConfig.module?.rules.some( - (rule) => canMatchCss(rule.test) || canMatchCss(rule.include) - ) ?? false + (config.experimental.webpackCssExperiment || + webpackConfig.module?.rules.some( + (rule) => canMatchCss(rule.test) || canMatchCss(rule.include) + )) ?? + false if (hasUserCssConfig) { // only show warning for one build diff --git a/packages/next/build/webpack/config/blocks/css/index.ts b/packages/next/build/webpack/config/blocks/css/index.ts index 1177e746a649..f33f42335dd0 100644 --- a/packages/next/build/webpack/config/blocks/css/index.ts +++ b/packages/next/build/webpack/config/blocks/css/index.ts @@ -502,6 +502,25 @@ export const css = curry(async function css( ) } + if (ctx.experimental.webpackCssExperiment) { + fns.push( + loader({ + oneOf: [ + { + test: regexSassModules, + use: require.resolve('next/dist/compiled/sass-loader'), + type: 'css/module', + }, + { + test: regexSassGlobal, + use: require.resolve('next/dist/compiled/sass-loader'), + type: 'css', + }, + ], + }) + ) + } + const fn = pipe(...fns) return fn(config) }) diff --git a/packages/next/server/config-shared.ts b/packages/next/server/config-shared.ts index 416fc7cb01da..48ce096fd2e9 100644 --- a/packages/next/server/config-shared.ts +++ b/packages/next/server/config-shared.ts @@ -98,6 +98,7 @@ export interface ExperimentalConfig { // Use Record as critters doesn't export its Option type // https://github.com/GoogleChromeLabs/critters/blob/a590c05f9197b656d2aeaae9369df2483c26b072/packages/critters/src/index.d.ts optimizeCss?: boolean | Record + webpackCssExperiment?: boolean nextScriptWorkers?: boolean scrollRestoration?: boolean externalDir?: boolean diff --git a/packages/next/types/webpack.d.ts b/packages/next/types/webpack.d.ts index 5cbda79547b0..141671232f54 100644 --- a/packages/next/types/webpack.d.ts +++ b/packages/next/types/webpack.d.ts @@ -161,6 +161,7 @@ declare module 'webpack4' { /** Optimization options */ optimization?: Options.Optimization experiments?: { + css: boolean | { exportsOnly?: boolean } layers: boolean } } @@ -244,6 +245,8 @@ declare module 'webpack4' { libraryExport?: string | string[] /** If output.libraryTarget is set to umd and output.library is set, setting this to true will name the AMD module. */ umdNamedDefine?: boolean + /** A unique name of the webpack build to avoid multiple webpack runtimes to conflict when using globals. */ + uniqueName?: string /** The output directory as absolute path. */ path?: string /** Include comments with information about the modules. */ @@ -319,6 +322,7 @@ declare module 'webpack4' { generator?: { asset?: any } + type?: any } interface Resolve { @@ -571,6 +575,8 @@ declare module 'webpack4' { | 'json' | 'webassembly/experimental' | 'asset/resource' + | 'css' + | 'css/module' /** * Match the resource path of the module */ @@ -2071,6 +2077,87 @@ declare module 'webpack4' { namespace dependencies {} + namespace ids { + class DeterministicModuleIdsPlugin extends Plugin { + constructor(options?: { + /** + * context relative to which module identifiers are computed + */ + context?: string + /** + * selector function for modules + */ + test?: (arg0: Module) => boolean + /** + * maximum id length in digits (used as starting point) + */ + maxLength?: number + /** + * hash salt for ids + */ + salt?: number + /** + * do not increase the maxLength to find an optimal id space size + */ + fixedLength?: boolean + /** + * throw an error when id conflicts occur (instead of rehashing) + */ + failOnConflict?: boolean + }) + options: { + /** + * context relative to which module identifiers are computed + */ + context?: string + /** + * selector function for modules + */ + test?: (arg0: Module) => boolean + /** + * maximum id length in digits (used as starting point) + */ + maxLength?: number + /** + * hash salt for ids + */ + salt?: number + /** + * do not increase the maxLength to find an optimal id space size + */ + fixedLength?: boolean + /** + * throw an error when id conflicts occur (instead of rehashing) + */ + failOnConflict?: boolean + } + } + } + namespace experiments { + namespace ids { + class SyncModuleIdsPlugin extends Plugin { + constructor(options: { + /** + * path to file + */ + path: string + /** + * context for module names + */ + context?: string + /** + * selector for modules + */ + test: (arg0: Module) => boolean + /** + * operation mode (defaults to merge) + */ + mode?: 'read' | 'create' | 'merge' | 'update' + }) + } + } + } + namespace loader { interface Loader extends Function { ( diff --git a/test/e2e/css-webpack-experiment/index.test.ts b/test/e2e/css-webpack-experiment/index.test.ts new file mode 100644 index 000000000000..dd7d2b3fff33 --- /dev/null +++ b/test/e2e/css-webpack-experiment/index.test.ts @@ -0,0 +1,90 @@ +import { createNext } from 'e2e-utils' +import { NextInstance } from 'test/lib/next-modes/base' +import webdriver from 'next-webdriver' + +describe('CSS webpack Experiment', () => { + describe('with basic CSS', () => { + let next: NextInstance + + beforeAll(async () => { + next = await createNext({ + files: { + 'styles/global.css': ` + p { + color: green; + font-size: 2rem; + } + `, + 'styles/styles.module.css': ` + .hello { + color: red; + } + `, + 'styles/styles.module.sass': ` + $color: blue + + .hello + color: $color + `, + 'pages/_app.js': ` + import '../styles/global.css'; + export default function MyApp({ Component, pageProps }) { + return + } + `, + 'pages/index.js': ` + export default function Page() { + return

hello world

+ } + `, + 'pages/css-modules.js': ` + import * as styles from '../styles/styles.module.css'; + export default function Page() { + return

hello world

+ } + `, + 'pages/sass.js': ` + import * as styles from '../styles/styles.module.sass'; + export default function Page() { + return

hello world

+ } + `, + }, + nextConfig: { + experimental: { + webpackCssExperiment: true, + }, + }, + dependencies: { + sass: '1.52.3', + }, + }) + }) + + afterAll(() => next.destroy()) + + it('should work with basic CSS', async () => { + const browser = await webdriver(next.url, `/`) + const element = await browser.elementByCss('p') + const color = await element.getComputedCss('color') + + expect(color).toBe('rgb(0, 128, 0)') + }) + + it('should work with CSS modules', async () => { + const browser = await webdriver(next.url, `/css-modules`) + const element = await browser.elementByCss('p') + const color = await element.getComputedCss('color') + + expect(color).toBe('rgb(255, 0, 0)') + }) + + it('should work with SASS', async () => { + const browser = await webdriver(next.url, `/sass`) + const element = await browser.elementByCss('p') + const color = await element.getComputedCss('color') + + expect(color).toBe('rgb(0, 0, 255)') + }) + }) +})