diff --git a/packages/next/build/webpack/config/blocks/css/index.ts b/packages/next/build/webpack/config/blocks/css/index.ts index 5ebfbf8e62aac..38154cb787e9e 100644 --- a/packages/next/build/webpack/config/blocks/css/index.ts +++ b/packages/next/build/webpack/config/blocks/css/index.ts @@ -456,6 +456,28 @@ export const css = curry(async function css( ], }) ) + fns.push( + loader({ + oneOf: [ + markRemovable({ + // A global SASS import always has side effects. Webpack will tree + // shake the CSS without this option if the issuer claims to have + // no side-effects. + // See https://github.com/webpack/webpack/issues/6571 + sideEffects: true, + test: regexSassGlobal, + use: [ + require.resolve('../../../loaders/next-flight-css-dev-loader'), + ...getGlobalCssLoader( + ctx, + lazyPostCSSInitializer, + sassPreprocessors + ), + ], + }), + ], + }) + ) fns.push( loader({ oneOf: [ @@ -470,6 +492,24 @@ export const css = curry(async function css( ], }) ) + fns.push( + loader({ + oneOf: [ + markRemovable({ + sideEffects: false, + test: regexSassModules, + use: [ + require.resolve('../../../loaders/next-flight-css-dev-loader'), + ...getCssModuleLoader( + ctx, + lazyPostCSSInitializer, + sassPreprocessors + ), + ], + }), + ], + }) + ) } else { fns.push( loader({ diff --git a/packages/next/build/webpack/loaders/next-flight-client-entry-loader.ts b/packages/next/build/webpack/loaders/next-flight-client-entry-loader.ts index b35b000bdffac..ac8cd9b7c23f8 100644 --- a/packages/next/build/webpack/loaders/next-flight-client-entry-loader.ts +++ b/packages/next/build/webpack/loaders/next-flight-client-entry-loader.ts @@ -25,7 +25,7 @@ export default async function transformSource(this: any): Promise { // Filter out css files on the server .filter((request) => (isServer ? !regexCSS.test(request) : true)) .map((request) => - request.endsWith('.css') + regexCSS.test(request) ? `(() => import(/* webpackMode: "lazy" */ ${JSON.stringify(request)}))` : `import(/* webpackMode: "eager" */ ${JSON.stringify(request)})` ) diff --git a/packages/next/build/webpack/loaders/next-flight-css-dev-loader.ts b/packages/next/build/webpack/loaders/next-flight-css-dev-loader.ts index 30947cd00dd62..8bfd62b9f38f1 100644 --- a/packages/next/build/webpack/loaders/next-flight-css-dev-loader.ts +++ b/packages/next/build/webpack/loaders/next-flight-css-dev-loader.ts @@ -5,7 +5,7 @@ */ export function pitch(this: any) { - const content = this.fs.readFileSync(this.resource) + const content = this.fs.readFileSync(this.resourcePath) this.data.__checksum = ( typeof content === 'string' ? Buffer.from(content) : content ).toString('hex') @@ -14,7 +14,7 @@ export function pitch(this: any) { const NextServerCSSLoader = function (this: any, content: string) { this.cacheable && this.cacheable() - const isCSSModule = this.resource.match(/\.module\.css$/) + const isCSSModule = this.resourcePath.match(/\.module\.(css|sass|scss)$/) if (isCSSModule) { return ( content + diff --git a/packages/next/build/webpack/loaders/utils.ts b/packages/next/build/webpack/loaders/utils.ts index dbaf39b0508ac..d098ba9681a09 100644 --- a/packages/next/build/webpack/loaders/utils.ts +++ b/packages/next/build/webpack/loaders/utils.ts @@ -11,5 +11,4 @@ export function isClientComponentModule(mod: { return hasClientDirective || imageRegex.test(mod.resource) } -// TODO-APP: ensure .scss / .sass also works. -export const regexCSS = /\.css(\?.*)?$/ +export const regexCSS = /\.(css|scss|sass)(\?.*)?$/ diff --git a/test/e2e/app-dir/app/app/css/sass-client/global.sass b/test/e2e/app-dir/app/app/css/sass-client/global.sass new file mode 100644 index 0000000000000..febf593dd789c --- /dev/null +++ b/test/e2e/app-dir/app/app/css/sass-client/global.sass @@ -0,0 +1,3 @@ +#sass-client-layout + color: brown + diff --git a/test/e2e/app-dir/app/app/css/sass-client/global.scss b/test/e2e/app-dir/app/app/css/sass-client/global.scss new file mode 100644 index 0000000000000..d9a5d9d523b9d --- /dev/null +++ b/test/e2e/app-dir/app/app/css/sass-client/global.scss @@ -0,0 +1,3 @@ +#scss-client-layout { + color: burlywood; +} diff --git a/test/e2e/app-dir/app/app/css/sass-client/inner/global.sass b/test/e2e/app-dir/app/app/css/sass-client/inner/global.sass new file mode 100644 index 0000000000000..6cf30f33ff606 --- /dev/null +++ b/test/e2e/app-dir/app/app/css/sass-client/inner/global.sass @@ -0,0 +1,3 @@ +#sass-client-page + color: wheat + diff --git a/test/e2e/app-dir/app/app/css/sass-client/inner/global.scss b/test/e2e/app-dir/app/app/css/sass-client/inner/global.scss new file mode 100644 index 0000000000000..05c93b6448efd --- /dev/null +++ b/test/e2e/app-dir/app/app/css/sass-client/inner/global.scss @@ -0,0 +1,3 @@ +#scss-client-page { + color: tomato; +} diff --git a/test/e2e/app-dir/app/app/css/sass-client/inner/page.js b/test/e2e/app-dir/app/app/css/sass-client/inner/page.js new file mode 100644 index 0000000000000..ad208ca88190d --- /dev/null +++ b/test/e2e/app-dir/app/app/css/sass-client/inner/page.js @@ -0,0 +1,19 @@ +'use client' + +import './global.scss' +import './global.sass' +import sass from './styles.module.sass' +import scss from './styles.module.scss' + +export default function Page() { + return ( + <> +
+ sass client page +
+
+ scss client page +
+ + ) +} diff --git a/test/e2e/app-dir/app/app/css/sass-client/inner/styles.module.sass b/test/e2e/app-dir/app/app/css/sass-client/inner/styles.module.sass new file mode 100644 index 0000000000000..e1f5b5d0e487f --- /dev/null +++ b/test/e2e/app-dir/app/app/css/sass-client/inner/styles.module.sass @@ -0,0 +1,2 @@ +.mod + background-color: indigo diff --git a/test/e2e/app-dir/app/app/css/sass-client/inner/styles.module.scss b/test/e2e/app-dir/app/app/css/sass-client/inner/styles.module.scss new file mode 100644 index 0000000000000..e84d60fe479cf --- /dev/null +++ b/test/e2e/app-dir/app/app/css/sass-client/inner/styles.module.scss @@ -0,0 +1,3 @@ +.mod { + background-color: aqua; +} diff --git a/test/e2e/app-dir/app/app/css/sass-client/layout.js b/test/e2e/app-dir/app/app/css/sass-client/layout.js new file mode 100644 index 0000000000000..fa6713af90e2f --- /dev/null +++ b/test/e2e/app-dir/app/app/css/sass-client/layout.js @@ -0,0 +1,20 @@ +'use client' + +import './global.scss' +import './global.sass' +import sass from './styles.module.sass' +import scss from './styles.module.scss' + +export default function Layout({ children }) { + return ( + <> +
+ sass client layout +
+
+ scss client layout +
+ {children} + + ) +} diff --git a/test/e2e/app-dir/app/app/css/sass-client/styles.module.sass b/test/e2e/app-dir/app/app/css/sass-client/styles.module.sass new file mode 100644 index 0000000000000..725bb78d164a4 --- /dev/null +++ b/test/e2e/app-dir/app/app/css/sass-client/styles.module.sass @@ -0,0 +1,2 @@ +.mod + background-color: darksalmon diff --git a/test/e2e/app-dir/app/app/css/sass-client/styles.module.scss b/test/e2e/app-dir/app/app/css/sass-client/styles.module.scss new file mode 100644 index 0000000000000..185167dd39fd2 --- /dev/null +++ b/test/e2e/app-dir/app/app/css/sass-client/styles.module.scss @@ -0,0 +1,3 @@ +.mod { + background-color: darkred; +} diff --git a/test/e2e/app-dir/app/app/css/sass/global.sass b/test/e2e/app-dir/app/app/css/sass/global.sass new file mode 100644 index 0000000000000..f692b6266a6d9 --- /dev/null +++ b/test/e2e/app-dir/app/app/css/sass/global.sass @@ -0,0 +1,3 @@ +#sass-server-layout + color: brown + diff --git a/test/e2e/app-dir/app/app/css/sass/global.scss b/test/e2e/app-dir/app/app/css/sass/global.scss new file mode 100644 index 0000000000000..1f8677ff440e5 --- /dev/null +++ b/test/e2e/app-dir/app/app/css/sass/global.scss @@ -0,0 +1,3 @@ +#scss-server-layout { + color: burlywood; +} diff --git a/test/e2e/app-dir/app/app/css/sass/inner/global.sass b/test/e2e/app-dir/app/app/css/sass/inner/global.sass new file mode 100644 index 0000000000000..509da5744d11c --- /dev/null +++ b/test/e2e/app-dir/app/app/css/sass/inner/global.sass @@ -0,0 +1,3 @@ +#sass-server-page + color: wheat + diff --git a/test/e2e/app-dir/app/app/css/sass/inner/global.scss b/test/e2e/app-dir/app/app/css/sass/inner/global.scss new file mode 100644 index 0000000000000..8cce88945a2c1 --- /dev/null +++ b/test/e2e/app-dir/app/app/css/sass/inner/global.scss @@ -0,0 +1,3 @@ +#scss-server-page { + color: tomato; +} diff --git a/test/e2e/app-dir/app/app/css/sass/inner/page.js b/test/e2e/app-dir/app/app/css/sass/inner/page.js new file mode 100644 index 0000000000000..fdb01e5056c86 --- /dev/null +++ b/test/e2e/app-dir/app/app/css/sass/inner/page.js @@ -0,0 +1,17 @@ +import './global.scss' +import './global.sass' +import sass from './styles.module.sass' +import scss from './styles.module.scss' + +export default function Page() { + return ( + <> +
+ sass server page +
+
+ scss server page +
+ + ) +} diff --git a/test/e2e/app-dir/app/app/css/sass/inner/styles.module.sass b/test/e2e/app-dir/app/app/css/sass/inner/styles.module.sass new file mode 100644 index 0000000000000..e1f5b5d0e487f --- /dev/null +++ b/test/e2e/app-dir/app/app/css/sass/inner/styles.module.sass @@ -0,0 +1,2 @@ +.mod + background-color: indigo diff --git a/test/e2e/app-dir/app/app/css/sass/inner/styles.module.scss b/test/e2e/app-dir/app/app/css/sass/inner/styles.module.scss new file mode 100644 index 0000000000000..e84d60fe479cf --- /dev/null +++ b/test/e2e/app-dir/app/app/css/sass/inner/styles.module.scss @@ -0,0 +1,3 @@ +.mod { + background-color: aqua; +} diff --git a/test/e2e/app-dir/app/app/css/sass/layout.js b/test/e2e/app-dir/app/app/css/sass/layout.js new file mode 100644 index 0000000000000..a0b928bdf9d86 --- /dev/null +++ b/test/e2e/app-dir/app/app/css/sass/layout.js @@ -0,0 +1,18 @@ +import './global.scss' +import './global.sass' +import sass from './styles.module.sass' +import scss from './styles.module.scss' + +export default function Layout({ children }) { + return ( + <> +
+ sass server layout +
+
+ scss server layout +
+ {children} + + ) +} diff --git a/test/e2e/app-dir/app/app/css/sass/styles.module.sass b/test/e2e/app-dir/app/app/css/sass/styles.module.sass new file mode 100644 index 0000000000000..725bb78d164a4 --- /dev/null +++ b/test/e2e/app-dir/app/app/css/sass/styles.module.sass @@ -0,0 +1,2 @@ +.mod + background-color: darksalmon diff --git a/test/e2e/app-dir/app/app/css/sass/styles.module.scss b/test/e2e/app-dir/app/app/css/sass/styles.module.scss new file mode 100644 index 0000000000000..185167dd39fd2 --- /dev/null +++ b/test/e2e/app-dir/app/app/css/sass/styles.module.scss @@ -0,0 +1,3 @@ +.mod { + background-color: darkred; +} diff --git a/test/e2e/app-dir/index.test.ts b/test/e2e/app-dir/index.test.ts index 13380335692b8..6ab9690183bbb 100644 --- a/test/e2e/app-dir/index.test.ts +++ b/test/e2e/app-dir/index.test.ts @@ -30,6 +30,7 @@ describe('app dir', () => { dependencies: { react: 'experimental', 'react-dom': 'experimental', + sass: 'latest', }, skipStart: true, }) @@ -1374,6 +1375,144 @@ describe('app dir', () => { }) }) }) + describe('sass support', () => { + describe('server layouts', () => { + it('should support global sass/scss inside server layouts', async () => { + const browser = await webdriver(next.url, '/css/sass/inner') + // .sass + expect( + await browser.eval( + `window.getComputedStyle(document.querySelector('#sass-server-layout')).color` + ) + ).toBe('rgb(165, 42, 42)') + // .scss + expect( + await browser.eval( + `window.getComputedStyle(document.querySelector('#scss-server-layout')).color` + ) + ).toBe('rgb(222, 184, 135)') + }) + + it('should support sass/scss modules inside server layouts', async () => { + const browser = await webdriver(next.url, '/css/sass/inner') + // .sass + expect( + await browser.eval( + `window.getComputedStyle(document.querySelector('#sass-server-layout')).backgroundColor` + ) + ).toBe('rgb(233, 150, 122)') + // .scss + expect( + await browser.eval( + `window.getComputedStyle(document.querySelector('#scss-server-layout')).backgroundColor` + ) + ).toBe('rgb(139, 0, 0)') + }) + }) + + describe('server pages', () => { + it('should support global sass/scss inside server pages', async () => { + const browser = await webdriver(next.url, '/css/sass/inner') + // .sass + expect( + await browser.eval( + `window.getComputedStyle(document.querySelector('#sass-server-page')).color` + ) + ).toBe('rgb(245, 222, 179)') + // .scss + expect( + await browser.eval( + `window.getComputedStyle(document.querySelector('#scss-server-page')).color` + ) + ).toBe('rgb(255, 99, 71)') + }) + + it('should support sass/scss modules inside server pages', async () => { + const browser = await webdriver(next.url, '/css/sass/inner') + // .sass + expect( + await browser.eval( + `window.getComputedStyle(document.querySelector('#sass-server-page')).backgroundColor` + ) + ).toBe('rgb(75, 0, 130)') + // .scss + expect( + await browser.eval( + `window.getComputedStyle(document.querySelector('#scss-server-page')).backgroundColor` + ) + ).toBe('rgb(0, 255, 255)') + }) + }) + + describe('client layouts', () => { + it('should support global sass/scss inside client layouts', async () => { + const browser = await webdriver(next.url, '/css/sass-client/inner') + // .sass + expect( + await browser.eval( + `window.getComputedStyle(document.querySelector('#sass-client-layout')).color` + ) + ).toBe('rgb(165, 42, 42)') + // .scss + expect( + await browser.eval( + `window.getComputedStyle(document.querySelector('#scss-client-layout')).color` + ) + ).toBe('rgb(222, 184, 135)') + }) + + it('should support sass/scss modules inside client layouts', async () => { + const browser = await webdriver(next.url, '/css/sass-client/inner') + // .sass + expect( + await browser.eval( + `window.getComputedStyle(document.querySelector('#sass-client-layout')).backgroundColor` + ) + ).toBe('rgb(233, 150, 122)') + // .scss + expect( + await browser.eval( + `window.getComputedStyle(document.querySelector('#scss-client-layout')).backgroundColor` + ) + ).toBe('rgb(139, 0, 0)') + }) + }) + }) + + describe('client pages', () => { + it('should support global sass/scss inside client pages', async () => { + const browser = await webdriver(next.url, '/css/sass-client/inner') + await waitFor(5000) + // .sass + expect( + await browser.eval( + `window.getComputedStyle(document.querySelector('#sass-client-page')).color` + ) + ).toBe('rgb(245, 222, 179)') + // .scss + expect( + await browser.eval( + `window.getComputedStyle(document.querySelector('#scss-client-page')).color` + ) + ).toBe('rgb(255, 99, 71)') + }) + + it('should support sass/scss modules inside client pages', async () => { + const browser = await webdriver(next.url, '/css/sass-client/inner') + // .sass + expect( + await browser.eval( + `window.getComputedStyle(document.querySelector('#sass-client-page')).backgroundColor` + ) + ).toBe('rgb(75, 0, 130)') + // .scss + expect( + await browser.eval( + `window.getComputedStyle(document.querySelector('#scss-client-page')).backgroundColor` + ) + ).toBe('rgb(0, 255, 255)') + }) + }) ;(isDev ? describe.skip : describe)('Subresource Integrity', () => { function fetchWithPolicy(policy: string | null) { return fetchViaHTTP(next.url, '/dashboard', undefined, { diff --git a/test/e2e/app-dir/vercel-analytics.test.ts b/test/e2e/app-dir/vercel-analytics.test.ts index f6862ac4aa33d..08b6b1252e47c 100644 --- a/test/e2e/app-dir/vercel-analytics.test.ts +++ b/test/e2e/app-dir/vercel-analytics.test.ts @@ -21,6 +21,7 @@ describe('vercel analytics', () => { dependencies: { react: 'experimental', 'react-dom': 'experimental', + sass: 'latest', }, skipStart: true, env: {