Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Linaria + Next.js webpack configuration #318

Closed
orpheus opened this issue Feb 22, 2019 · 19 comments
Closed

Linaria + Next.js webpack configuration #318

orpheus opened this issue Feb 22, 2019 · 19 comments

Comments

@orpheus
Copy link

orpheus commented Feb 22, 2019

Using react and nextjs. The div element gets the className: ci74fup but no styles are applied. What am I missing?
Am I limited to using only the react-helpers?

import React from 'react'
import Head from 'next/head'
import { css } from 'linaria';

const container = css`
	background-color: red;
	`;

export default () => {
	return (
	<>
		<Head><title>CSS Playground</title></Head>
		<div className={container}>
			Hello World
		</div>
	</>
	)
}

.babelrc
{ "presets": ["next/babel", "linaria/babel"] }

linaria-issue-1

@satya164
Copy link
Member

satya164 commented Feb 22, 2019

Have you added linaria/loader, css-loader etc. to your webpack config?

@orpheus
Copy link
Author

orpheus commented Feb 22, 2019

@satya164 Didn't think to since it's hidden with next.js. Extending webpack..

Following the next.js rules on how to extend webpack config I did this:

// next.config.js is not transformed by Babel. So you can only use javascript features supported by your version of Node.js.
const MiniCssExtractPlugin = require('mini-css-extract-plugin');

module.exports = {
	webpack: (config, {buildId, dev, isServer, defaultLoaders}) => {
		// Perform customizations to webpack config
		// Important: return the modified config
		config.module.rules.push({
				loader: 'linaria/loader',
				options: {
					sourceMap: process.env.NODE_ENV !== 'production',
				},
			},
			{
				test: /\.css$/,
				use: [
					MiniCssExtractPlugin.loader,
					{
						loader: 'css-loader',
						options: {
							sourceMap: process.env.NODE_ENV !== 'production',
						},
					},
				],
			},)
		config.plugins.push(new MiniCssExtractPlugin({
			filename: 'styles.css',
		}))
		return config
	},
	webpackDevMiddleware: config => {
		// Perform customizations to webpack dev middleware config
		// Important: return the modified config
		return config
	}
}

this is the compile error I get:

 ERROR  Failed to compile with 2 errors11:21:44 AM

 error  in ./node_modules/next/dist/build/webpack/loaders/next-client-pages-loader.js?page=%2F_app&absolutePagePath=next%2Fdist%2Fpages%2F_app

Module build failed (from ./node_modules/linaria/loader.js):
TypeError: Cannot read property 'replace' of undefined
    at Object.loader (/home/orpheus/code/personal/pentaimag/node_modules/linaria/lib/loader.js:35:174)

 error  in ./node_modules/next/dist/build/webpack/loaders/next-client-pages-loader.js?page=%2F_error&absolutePagePath=next%2Fdist%2Fpages%2F_error

Module build failed (from ./node_modules/linaria/loader.js):
TypeError: Cannot read property 'replace' of undefined
    at Object.loader (/home/orpheus/code/personal/pentaimag/node_modules/linaria/lib/loader.js:35:174)

@orpheus
Copy link
Author

orpheus commented Feb 22, 2019

I'm not placing linaria/loader in the right spot. Lemme fix this and try again.

Make sure that linaria/loader is included after babel-loader.

@orpheus
Copy link
Author

orpheus commented Feb 22, 2019

@satya164 Adding it correctly did the trick. Feels a little hacky but that's next.js for you I guess. Here's the confg setup:

next.config.js

// next.config.js is not transformed by Babel. So you can only use javascript features supported by your version of Node.js.
const MiniCssExtractPlugin = require('mini-css-extract-plugin');

module.exports = {
	webpack: (config, {buildId, dev, isServer, defaultLoaders}) => {
		// Perform customizations to webpack config
		// Important: return the modified config
		
		let nextBabelLoader = config.module.rules[0].use
		config.module.rules[0].use = [nextBabelLoader,
			{
				loader: 'linaria/loader',
				options: {
					sourceMap: process.env.NODE_ENV !== 'production',
				},
			}]
		config.module.rules.push(
			{
				test: /\.css$/,
				use: [
					MiniCssExtractPlugin.loader,
					{
						loader: 'css-loader',
						options: {
							sourceMap: process.env.NODE_ENV !== 'production',
						},
					},
				],
			},)
		config.plugins.push(new MiniCssExtractPlugin({
			filename: 'styles.css',
		}))
		if (isServer) {
			console.log('my webpack config: ', config.module.rules)
			console.log(config.module.rules[0].use)
		}
		
		return config
	},
	webpackDevMiddleware: config => {
		// Perform customizations to webpack dev middleware config
		// Important: return the modified config
		return config
	}
}

@orpheus orpheus closed this as completed Feb 22, 2019
@orpheus orpheus changed the title basic syntax with react not working Linaria + Next.js webpack configuration Feb 22, 2019
@OrcaXS
Copy link

OrcaXS commented Feb 23, 2019

@satya164 Adding it correctly did the trick. Feels a little hacky but that's next.js for you I guess. Here's the confg setup:

next.config.js

// next.config.js is not transformed by Babel. So you can only use javascript features supported by your version of Node.js.
const MiniCssExtractPlugin = require('mini-css-extract-plugin');

module.exports = {
	webpack: (config, {buildId, dev, isServer, defaultLoaders}) => {
		// Perform customizations to webpack config
		// Important: return the modified config
		
		let nextBabelLoader = config.module.rules[0].use
		config.module.rules[0].use = [nextBabelLoader,
			{
				loader: 'linaria/loader',
				options: {
					sourceMap: process.env.NODE_ENV !== 'production',
				},
			}]
		config.module.rules.push(
			{
				test: /\.css$/,
				use: [
					MiniCssExtractPlugin.loader,
					{
						loader: 'css-loader',
						options: {
							sourceMap: process.env.NODE_ENV !== 'production',
						},
					},
				],
			},)
		config.plugins.push(new MiniCssExtractPlugin({
			filename: 'styles.css',
		}))
		if (isServer) {
			console.log('my webpack config: ', config.module.rules)
			console.log(config.module.rules[0].use)
		}
		
		return config
	},
	webpackDevMiddleware: config => {
		// Perform customizations to webpack dev middleware config
		// Important: return the modified config
		return config
	}
}

Following your way, I find it working only in dev but not in the production build.
Am I missing something?

@LukasBombach
Copy link

LukasBombach commented Mar 29, 2020

hey @orpheus thanks for your answer, I could simplify this even:

add linaria

yarn add -E linaria

.babelrc

{
  "presets": ["next/babel", "linaria/babel"]
}

next.config.js

const withCSS = require("@zeit/next-css");

module.exports = withCSS({
  webpack(config, { isServer }) {
    config.module.rules[0].use = [
      config.module.rules[0].use,
      {
        loader: "linaria/loader",
        options: {
          sourceMap: process.env.NODE_ENV !== "production"
        }
      }
    ];
    return config;
  }
});

I made a tutorial: https://gist.github.com/LukasBombach/dc46546919ecf71b2aa68688ee767e4e

@trongthanh
Copy link
Contributor

Thank @LukasBombach. Your config is really nice and clean.

However, it doesn't work with latest Next.js 9.4 for next dev mode. It is because during dev mode, config.module.rules[0].use is an Array for the client side build. Something like this:

use: [
  '.../node_modules/@next/react-refresh-utils/loader.js',
  { loader: 'next-babel-loader', options: [Object] }
]

So I have adjusted the config a little to cater for both dev and production build:

const withCSS = require('@zeit/next-css');

module.exports = withCSS({
  webpack(config) {
    // retrieve the rule without knowing its order
    const jsLoaderRule = config.module.rules.find(
      (rule) => rule.test instanceof RegExp && rule.test.test('.js')
    );
    const linariaLoader = {
      loader: 'linaria/loader',
      options: {
        sourceMap: process.env.NODE_ENV !== 'production',
      },
    };
    if (Array.isArray(jsLoaderRule.use)) {
      jsLoaderRule.use.push(linariaLoader);
    } else {
      jsLoaderRule.use = [jsLoaderRule.use, linariaLoader];
    }
    return config;
  },
});

@adamhenson
Copy link

adamhenson commented Aug 8, 2020

UPDATE

No need to read the below. Seems that I was doing too much. A good example does exist, but finding it is difficult. Fortunately it seems to be on the radar.


Since there isn't a solid example of Next.js with Linaria from my Googling, I've pieced together a config based on this issue and the Linaria Webpack documentation. I'm getting the following error logged in my terminal and in the browser the HTML body is being hidden by Next. I see a class in the HTML but seems to be undefined.

next: 9.5.1
linaria: 1.3.3

It compiles successfully, but below is the error:

event - compiled successfully
event - build page: /
wait  - compiling...
error - ./.linaria-cache/pages/index.linaria.css
Error: Didn't get a result from child compiler
Could not find files for / in .next/build-manifest.json

I found this issue from mini-css-extract-plugin, but seems like it would have impacted others here if it was in fact the cause.

Would appreciate any help or hints!

This is the code I've pieced together

next.config.js

const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const withCSS = require('@zeit/next-css');

module.exports = withCSS({
  webpack(config) {
    // https://github.com/callstack/linaria/issues/318
    // retrieve the rule without knowing its order
    const jsLoaderRule = config.module.rules.find(
      (rule) => rule.test instanceof RegExp && rule.test.test('.js')
    );
    const linariaLoader = {
      loader: 'linaria/loader',
      options: {
        sourceMap: process.env.NODE_ENV !== 'production',
      },
    };
    if (Array.isArray(jsLoaderRule.use)) {
      jsLoaderRule.use.push(linariaLoader);
    } else {
      jsLoaderRule.use = [jsLoaderRule.use, linariaLoader];
    }

    // https://github.com/callstack/linaria/blob/master/docs/BUNDLERS_INTEGRATION.md#webpack
    config.module.rules.push({
      test: /\.css$/,
      use: [
        {
          loader: MiniCssExtractPlugin.loader,
          options: {
            hmr: process.env.NODE_ENV !== 'production',
          },
        },
        {
          loader: 'css-loader',
          options: {
            sourceMap: process.env.NODE_ENV !== 'production',
          },
        },
      ],
    });

    config.plugins.push(
      new MiniCssExtractPlugin({
        filename: process.env.NODE_ENV !== 'production'
          ? 'styles.css'
          : 'styles-[contenthash].css',
      }),
    );

    return config;
  },
});

pages/index.js

import { styled } from 'linaria/react';
import Head from 'next/head';

const Header = styled.h1`
  font-family: sans-serif;
  font-size: 48px;
  color: #f15f79;
`;

export default () => (
  <>
    <Head>
      <title>Create Next App</title>
      <link rel="icon" href="/favicon.ico" />
    </Head>
    <Header>Hello world!</Header>
  </>
);

@Mizumaki
Copy link

For me, this library works well

https://github.com/Mistereo/next-linaria

@Nish17g
Copy link

Nish17g commented Oct 25, 2022

Thank @LukasBombach. Your config is really nice and clean.

However, it doesn't work with latest Next.js 9.4 for next dev mode. It is because during dev mode, config.module.rules[0].use is an Array for the client side build. Something like this:

use: [
  '.../node_modules/@next/react-refresh-utils/loader.js',
  { loader: 'next-babel-loader', options: [Object] }
]

So I have adjusted the config a little to cater for both dev and production build:

const withCSS = require('@zeit/next-css');

module.exports = withCSS({
  webpack(config) {
    // retrieve the rule without knowing its order
    const jsLoaderRule = config.module.rules.find(
      (rule) => rule.test instanceof RegExp && rule.test.test('.js')
    );
    const linariaLoader = {
      loader: 'linaria/loader',
      options: {
        sourceMap: process.env.NODE_ENV !== 'production',
      },
    };
    if (Array.isArray(jsLoaderRule.use)) {
      jsLoaderRule.use.push(linariaLoader);
    } else {
      jsLoaderRule.use = [jsLoaderRule.use, linariaLoader];
    }
    return config;
  },
});

Is this still relevant with Nest js 12 .I am getting the following error while using this configuration in next.config.js
ValidationError: Invalid configuration object. Webpack has been initialized using a configuration object that does not match the API schema.

  • configuration[0].module.rules[0] should be one of these:
    ["..." | object { assert?, compiler?, dependency?, descriptionData?, enforce?, exclude?, generator?, include?, issuer?, issuerLayer?, layer?, loader?, mimetype?, oneOf?, options?, parser?, realResource?, resolve?, resource?, resourceFragment?, resourceQuery?, rules?, scheme?, sideEffects?, test?, type?, use? }, ...]
    -> A rule.
    Details:
    • configuration[0].module.rules[0].use[0] should be one of these:
      object { ident?, loader?, options? } | function | non-empty string
      -> A description of an applied loader.
      Details:
      • configuration[0].module.rules[0].use[0] should be an object:
        object { ident?, loader?, options? }
      • configuration[0].module.rules[0].use[0] should be an instance of function.
      • configuration[0].module.rules[0].use[0] should be a non-empty string.
        -> A loader request.

@LukasBombach
Copy link

I created a new repo recently based on this comment here #589 (comment) by @anulman

This is for Next.js 12 with Linaria 4 + it has Inline CSS Extraction

https://github.com/LukasBombach/nextjs-linaria

You can check out the next config here

https://github.com/LukasBombach/nextjs-linaria/blob/main/next.config.js

const _ = require("lodash");

const BABEL_LOADER_STRING = "babel/loader";
const makeLinariaLoaderConfig = babelOptions => ({
  loader: require.resolve("@linaria/webpack-loader"),
  options: {
    sourceMap: true,
    extension: ".linaria.module.css",
    babelOptions: _.omit(
      babelOptions,
      "isServer",
      "distDir",
      "pagesDir",
      "development",
      "hasReactRefresh",
      "hasJsxRuntime"
    ),
  },
});

let injectedBabelLoader = false;
function crossRules(rules) {
  for (const rule of rules) {
    if (typeof rule.loader === "string" && rule.loader.includes("css-loader")) {
      if (rule.options && rule.options.modules && typeof rule.options.modules.getLocalIdent === "function") {
        const nextGetLocalIdent = rule.options.modules.getLocalIdent;
        rule.options.modules.mode = "local";
        rule.options.modules.auto = true;
        rule.options.modules.exportGlobals = true;
        rule.options.modules.exportOnlyLocals = true;
        rule.options.modules.getLocalIdent = (context, _, exportName, options) => {
          if (context.resourcePath.includes(".linaria.module.css")) {
            return exportName;
          }
          return nextGetLocalIdent(context, _, exportName, options);
        };
      }
    }

    if (typeof rule.use === "object") {
      if (Array.isArray(rule.use)) {
        const babelLoaderIndex = rule.use.findIndex(
          ({ loader }) => typeof loader === "string" && loader.includes(BABEL_LOADER_STRING)
        );

        const babelLoaderItem = rule.use[babelLoaderIndex];

        if (babelLoaderIndex !== -1) {
          rule.use = [
            ...rule.use.slice(0, babelLoaderIndex),
            babelLoaderItem,
            makeLinariaLoaderConfig(babelLoaderItem.options),
            ...rule.use.slice(babelLoaderIndex + 2),
          ];
          injectedBabelLoader = true;
        }
      } else if (typeof rule.use.loader === "string" && rule.use.loader.includes(BABEL_LOADER_STRING)) {
        rule.use = [rule.use, makeLinariaLoaderConfig(rule.use.options)];
        injectedBabelLoader = true;
      }

      crossRules(Array.isArray(rule.use) ? rule.use : [rule.use]);
    }

    if (Array.isArray(rule.oneOf)) {
      crossRules(rule.oneOf);
    }
  }
}

function withLinaria(nextConfig = {}) {
  return {
    ...nextConfig,
    webpack(config, options) {
      crossRules(config.module.rules);

      if (!injectedBabelLoader) {
        config.module.rules.push({
          test: /\.(tsx?|jsx?)$/,
          exclude: /node_modules/,
          use: [
            {
              loader: "babel-loader",
              options: linariaWebpackLoaderConfig.options.babelOptions,
            },
            linariaWebpackLoaderConfig,
          ],
        });
      }

      if (typeof nextConfig.webpack === "function") {
        return nextConfig.webpack(config, options);
      }

      return config;
    },
  };
}

/** @type {import('next').NextConfig} */
const nextConfig = {
  reactStrictMode: true,
  swcMinify: true,
};

module.exports = withLinaria(nextConfig);

@jonataslaw
Copy link

I created a new repo recently based on this comment here #589 (comment) by @anulman

This is for Next.js 12 with Linaria 4 + it has Inline CSS Extraction

https://github.com/LukasBombach/nextjs-linaria

You can check out the next config here

https://github.com/LukasBombach/nextjs-linaria/blob/main/next.config.js

const _ = require("lodash");

const BABEL_LOADER_STRING = "babel/loader";
const makeLinariaLoaderConfig = babelOptions => ({
  loader: require.resolve("@linaria/webpack-loader"),
  options: {
    sourceMap: true,
    extension: ".linaria.module.css",
    babelOptions: _.omit(
      babelOptions,
      "isServer",
      "distDir",
      "pagesDir",
      "development",
      "hasReactRefresh",
      "hasJsxRuntime"
    ),
  },
});

let injectedBabelLoader = false;
function crossRules(rules) {
  for (const rule of rules) {
    if (typeof rule.loader === "string" && rule.loader.includes("css-loader")) {
      if (rule.options && rule.options.modules && typeof rule.options.modules.getLocalIdent === "function") {
        const nextGetLocalIdent = rule.options.modules.getLocalIdent;
        rule.options.modules.mode = "local";
        rule.options.modules.auto = true;
        rule.options.modules.exportGlobals = true;
        rule.options.modules.exportOnlyLocals = true;
        rule.options.modules.getLocalIdent = (context, _, exportName, options) => {
          if (context.resourcePath.includes(".linaria.module.css")) {
            return exportName;
          }
          return nextGetLocalIdent(context, _, exportName, options);
        };
      }
    }

    if (typeof rule.use === "object") {
      if (Array.isArray(rule.use)) {
        const babelLoaderIndex = rule.use.findIndex(
          ({ loader }) => typeof loader === "string" && loader.includes(BABEL_LOADER_STRING)
        );

        const babelLoaderItem = rule.use[babelLoaderIndex];

        if (babelLoaderIndex !== -1) {
          rule.use = [
            ...rule.use.slice(0, babelLoaderIndex),
            babelLoaderItem,
            makeLinariaLoaderConfig(babelLoaderItem.options),
            ...rule.use.slice(babelLoaderIndex + 2),
          ];
          injectedBabelLoader = true;
        }
      } else if (typeof rule.use.loader === "string" && rule.use.loader.includes(BABEL_LOADER_STRING)) {
        rule.use = [rule.use, makeLinariaLoaderConfig(rule.use.options)];
        injectedBabelLoader = true;
      }

      crossRules(Array.isArray(rule.use) ? rule.use : [rule.use]);
    }

    if (Array.isArray(rule.oneOf)) {
      crossRules(rule.oneOf);
    }
  }
}

function withLinaria(nextConfig = {}) {
  return {
    ...nextConfig,
    webpack(config, options) {
      crossRules(config.module.rules);

      if (!injectedBabelLoader) {
        config.module.rules.push({
          test: /\.(tsx?|jsx?)$/,
          exclude: /node_modules/,
          use: [
            {
              loader: "babel-loader",
              options: linariaWebpackLoaderConfig.options.babelOptions,
            },
            linariaWebpackLoaderConfig,
          ],
        });
      }

      if (typeof nextConfig.webpack === "function") {
        return nextConfig.webpack(config, options);
      }

      return config;
    },
  };
}

/** @type {import('next').NextConfig} */
const nextConfig = {
  reactStrictMode: true,
  swcMinify: true,
};

module.exports = withLinaria(nextConfig);

It is not working with the lastest nextjs version

@Nish17g
Copy link

Nish17g commented Nov 22, 2022

@LukasBombach Thank you so much for the details .This works like charm but i have run into an issue where i am getting the below error when i try to build the project

Build error occurred
ReferenceError: linariaWebpackLoaderConfig is not defined

@frankandrobot
Copy link

@adamhenson @LukasBombach do your approaches work like styled-components? That is, do they embed the initial (first-render) CSS into the HTML? If not, these approaches will suffer from massive layout shift as the CSS loads after the HTML. (This is the problem that I saw the next-linaria package----it does not embed the linaria CSS into the HTML, so you'll see the page jump.)

And if the answer is no, then this issue needs to be opened---the whole point of next.js is to minimize things like layout shift 😉

@adamhenson
Copy link

adamhenson commented Dec 11, 2022

@frankandrobot My comment was over 2.5 years ago, based on an older version of Next.js, but yes it was working fine eventually. Layout shift for me is unacceptable, hence the point of why I was using Linaria back then (a zero run-time solution). It was working fine for me, but not very well with MUI. I have since stopped using Linaria in favor of Tailwind.

@frankandrobot
Copy link

frankandrobot commented Dec 11, 2022 via email

@LukasBombach
Copy link

@adamhenson @LukasBombach do your approaches work like styled-components? That is, do they embed the initial (first-render) CSS into the HTML? If not, these approaches will suffer from massive layout shift as the CSS loads after the HTML. (This is the problem that I saw the next-linaria package----it does not embed the linaria CSS into the HTML, so you'll see the page jump.)

And if the answer is no, then this issue needs to be opened---the whole point of next.js is to minimize things like layout shift 😉

Linaria or styled components do not inherently implement inlining critical css. But you can make any next app do that by overwriting the way next/head emits its css. In the repo I made I am actually doing that, inlining the css next js would emit as files

@frankandrobot
Copy link

@LukasBombach sweet. Thanks so much. 👍 That repo should probably made as the official example 😄

@baba43
Copy link

baba43 commented Mar 2, 2023

I'd really love to see any official interface instead of having to use low-level code-snippets to make it work.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests