index.ts 9.6 KB
Newer Older
1 2
import curry from 'lodash.curry'
import path from 'path'
3
import webpack, { Configuration } from 'webpack'
4
import MiniCssExtractPlugin from '../../../plugins/mini-css-extract-plugin'
5
import { loader, plugin } from '../../helpers'
6
import { ConfigurationContext, ConfigurationFn, pipe } from '../../utils'
7
import { getCssModuleLoader, getGlobalCssLoader } from './loaders'
8
import {
9
  getCustomDocumentError,
10 11 12 13
  getGlobalImportError,
  getGlobalModuleImportError,
  getLocalModuleImportError,
} from './messages'
14 15
import { getPostCssPlugins } from './plugins'

16 17 18 19
// RegExps for all Style Sheet variants
const regexLikeCss = /\.(css|scss|sass)$/

// RegExps for Style Sheets
J
Joe Haddad 已提交
20 21 22
const regexCssGlobal = /(?<!\.module)\.css$/
const regexCssModules = /\.module\.css$/

23 24 25 26
// RegExps for Syntactically Awesome Style Sheets
const regexSassGlobal = /(?<!\.module)\.(scss|sass)$/
const regexSassModules = /\.module\.(scss|sass)$/

27 28
export const css = curry(async function css(
  enabled: boolean,
29
  scssEnabled: boolean,
30 31 32 33 34 35 36
  ctx: ConfigurationContext,
  config: Configuration
) {
  if (!enabled) {
    return config
  }

37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62
  const sassPreprocessors: webpack.RuleSetUseItem[] = [
    // First, process files with `sass-loader`: this inlines content, and
    // compiles away the proprietary syntax.
    {
      loader: require.resolve('sass-loader'),
      options: {
        // Source maps are required so that `resolve-url-loader` can locate
        // files original to their source directory.
        sourceMap: true,
      },
    },
    // Then, `sass-loader` will have passed-through CSS imports as-is instead
    // of inlining them. Because they were inlined, the paths are no longer
    // correct.
    // To fix this, we use `resolve-url-loader` to rewrite the CSS
    // imports to real file paths.
    {
      loader: require.resolve('resolve-url-loader'),
      options: {
        // Source maps are not required here, but we may as well emit
        // them.
        sourceMap: true,
      },
    },
  ]

63 64 65 66 67 68 69 70 71 72 73 74
  const fns: ConfigurationFn[] = [
    loader({
      oneOf: [
        {
          // Impossible regex expression
          test: /a^/,
          loader: 'noop-loader',
          options: { __next_css_remove: true },
        },
      ],
    }),
  ]
75

76 77
  const postCssPlugins = await getPostCssPlugins(
    ctx.rootDirectory,
78
    ctx.isProduction,
79 80 81 82 83
    // TODO: In the future, we should stop supporting old CSS setups and
    // unconditionally inject ours. When that happens, we should remove this
    // function argument.
    true
  )
84 85 86 87 88 89 90

  // CSS cannot be imported in _document. This comes before everything because
  // global CSS nor CSS modules work in said file.
  fns.push(
    loader({
      oneOf: [
        {
91
          test: regexLikeCss,
92 93 94 95 96 97 98 99 100 101 102 103 104 105
          // Use a loose regex so we don't have to crawl the file system to
          // find the real file name (if present).
          issuer: { test: /pages[\\/]_document\./ },
          use: {
            loader: 'error-loader',
            options: {
              reason: getCustomDocumentError(),
            },
          },
        },
      ],
    })
  )

106 107 108 109 110 111 112 113 114 115 116 117
  // CSS Modules support must be enabled on the server and client so the class
  // names are availble for SSR or Prerendering.
  fns.push(
    loader({
      oneOf: [
        {
          // CSS Modules should never have side effects. This setting will
          // allow unused CSS to be removed from the production build.
          // We ensure this by disallowing `:global()` CSS at the top-level
          // via the `pure` mode in `css-loader`.
          sideEffects: false,
          // CSS Modules are activated via this specific extension.
J
Joe Haddad 已提交
118
          test: regexCssModules,
119 120 121 122 123 124
          // CSS Modules are only supported in the user's application. We're
          // not yet allowing CSS imports _within_ `node_modules`.
          issuer: {
            include: [ctx.rootDirectory],
            exclude: /node_modules/,
          },
125
          use: getCssModuleLoader(ctx, postCssPlugins),
126 127 128 129
        },
      ],
    })
  )
130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154
  if (scssEnabled) {
    fns.push(
      loader({
        oneOf: [
          // Opt-in support for Sass (using .scss or .sass extensions).
          {
            // Sass Modules should never have side effects. This setting will
            // allow unused Sass to be removed from the production build.
            // We ensure this by disallowing `:global()` Sass at the top-level
            // via the `pure` mode in `css-loader`.
            sideEffects: false,
            // Sass Modules are activated via this specific extension.
            test: regexSassModules,
            // Sass Modules are only supported in the user's application. We're
            // not yet allowing Sass imports _within_ `node_modules`.
            issuer: {
              include: [ctx.rootDirectory],
              exclude: /node_modules/,
            },
            use: getCssModuleLoader(ctx, postCssPlugins, sassPreprocessors),
          },
        ],
      })
    )
  }
155 156 157 158 159 160

  // Throw an error for CSS Modules used outside their supported scope
  fns.push(
    loader({
      oneOf: [
        {
161 162 163 164
          test: [
            regexCssModules,
            (scssEnabled && regexSassModules) as RegExp,
          ].filter(Boolean),
165 166 167
          use: {
            loader: 'error-loader',
            options: {
168
              reason: getLocalModuleImportError(),
169 170 171 172 173 174 175
            },
          },
        },
      ],
    })
  )

176 177 178
  if (ctx.isServer) {
    fns.push(
      loader({
J
Joe Haddad 已提交
179
        oneOf: [
180 181 182 183 184 185 186
          {
            test: [
              regexCssGlobal,
              (scssEnabled && regexSassGlobal) as RegExp,
            ].filter(Boolean),
            use: require.resolve('ignore-loader'),
          },
J
Joe Haddad 已提交
187
        ],
188 189 190 191 192 193 194 195 196 197 198 199
      })
    )
  } else if (ctx.customAppFile) {
    fns.push(
      loader({
        oneOf: [
          {
            // A global CSS 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,
J
Joe Haddad 已提交
200
            test: regexCssGlobal,
201
            issuer: { include: ctx.customAppFile },
202
            use: getGlobalCssLoader(ctx, postCssPlugins),
203 204 205 206
          },
        ],
      })
    )
207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224
    if (scssEnabled) {
      fns.push(
        loader({
          oneOf: [
            {
              // A global Sass import always has side effects. Webpack will tree
              // shake the Sass without this option if the issuer claims to have
              // no side-effects.
              // See https://github.com/webpack/webpack/issues/6571
              sideEffects: true,
              test: regexSassGlobal,
              issuer: { include: ctx.customAppFile },
              use: getGlobalCssLoader(ctx, postCssPlugins, sassPreprocessors),
            },
          ],
        })
      )
    }
225 226
  }

227 228 229 230 231
  // Throw an error for Global CSS used inside of `node_modules`
  fns.push(
    loader({
      oneOf: [
        {
232 233 234 235
          test: [
            regexCssGlobal,
            (scssEnabled && regexSassGlobal) as RegExp,
          ].filter(Boolean),
236 237 238 239 240 241 242 243 244 245 246 247
          issuer: { include: [/node_modules/] },
          use: {
            loader: 'error-loader',
            options: {
              reason: getGlobalModuleImportError(),
            },
          },
        },
      ],
    })
  )

248 249 250 251 252
  // Throw an error for Global CSS used outside of our custom <App> file
  fns.push(
    loader({
      oneOf: [
        {
253 254 255 256
          test: [
            regexCssGlobal,
            (scssEnabled && regexSassGlobal) as RegExp,
          ].filter(Boolean),
257 258 259 260 261 262 263 264 265 266 267 268 269 270
          use: {
            loader: 'error-loader',
            options: {
              reason: getGlobalImportError(
                ctx.customAppFile &&
                  path.relative(ctx.rootDirectory, ctx.customAppFile)
              ),
            },
          },
        },
      ],
    })
  )

271 272 273 274 275 276 277 278
  if (ctx.isClient) {
    // Automatically transform references to files (i.e. url()) into URLs
    // e.g. url(./logo.svg)
    fns.push(
      loader({
        oneOf: [
          {
            // This should only be applied to CSS files
279
            issuer: { test: regexLikeCss },
280 281 282 283 284 285 286 287 288 289
            // Exclude extensions that webpack handles by default
            exclude: [/\.(js|mjs|jsx|ts|tsx)$/, /\.html$/, /\.json$/],
            use: {
              // `file-loader` always emits a URL reference, where `url-loader`
              // might inline the asset as a data URI
              loader: require.resolve('file-loader'),
              options: {
                // Hash the file for immutable cacheability
                name: 'static/media/[name].[hash].[ext]',
              },
290 291
            },
          },
292 293 294 295
        ],
      })
    )
  }
296

297 298 299 300 301 302 303
  if (ctx.isClient && ctx.isProduction) {
    // Extract CSS as CSS file(s) in the client-side production bundle.
    fns.push(
      plugin(
        new MiniCssExtractPlugin({
          filename: 'static/css/[contenthash].css',
          chunkFilename: 'static/css/[contenthash].css',
J
Joe Haddad 已提交
304
          // Next.js guarantees that CSS order "doesn't matter", due to imposed
305 306 307 308 309
          // restrictions:
          // 1. Global CSS can only be defined in a single entrypoint (_app)
          // 2. CSS Modules generate scoped class names by default and cannot
          //    include Global CSS (:global() selector).
          //
J
Joe Haddad 已提交
310 311 312
          // While not a perfect guarantee (e.g. liberal use of `:global()`
          // selector), this assumption is required to code-split CSS.
          //
313 314 315 316 317 318 319 320
          // If this warning were to trigger, it'd be unactionable by the user,
          // but also not valid -- so we disable it.
          ignoreOrder: true,
        })
      )
    )
  }

321 322 323
  const fn = pipe(...fns)
  return fn(config)
})