cyrilwanner / next-compose-plugins

💡next-compose-plugins provides a cleaner API for enabling and configuring plugins for next.js
MIT License
736 stars 12 forks source link

Custom next config is ignored #22

Open mehmetnyarar opened 5 years ago

mehmetnyarar commented 5 years ago

When I use next-compose-plugins, my custom next config is ignored and I can't get environment variables. When I don't use it, there's no problem.

Am I doing something wrong or is this a bug?

const withPlugins = require('next-compose-plugins')
const withTM = require('next-transpile-modules')
const withCss = require('@zeit/next-css')
const webpack = require('webpack')
module.exports = withPlugins(
  [
    withTM({
      transpileModules: [
        '@my/custompackage'
      ]
    }),
    withCss
  ],
  // Ignored
  {
    webpack: config => {
      const env = {} // some env variables
      config.plugins.push(new webpack.DefinePlugin(env))
      return config
    }
  }
)
Fatxx commented 5 years ago

Same here, webpack doesn't run

dosentmatter commented 4 years ago

Is your app crashing when running the server in dev/prod mode?

I noticed that I had a custom server that had require('./next.config.js') in it. next.config.js doesn't export an object anymore after using next-compose-plugins. It uses phases so it exports a function.

In my case, webpack wasn't running because it was crashing trying to access variables from the exported function, expecting it to be an object. If you still need the next config object, you can see my comment in another post.

const isDev = process.env.NODE_ENV !== 'production';
const { PHASE_DEVELOPMENT_SERVER, PHASE_PRODUCTION_SERVER } = require('next/constants');
const nextConfigModule = require('../next.config');

let nextConfig;
if (typeof nextConfigModule === 'function') {
  nextConfig = nextConfigModule(isDev ? PHASE_DEVELOPMENT_SERVER : PHASE_PRODUCTION_SERVER, {});
} else {
  nextConfig = nextConfigModule;
}

// useless because already know isDev.
nextConfig.serverRuntimeConfig.isDev;

Note that you have to determine isDev to get the correct next config. isDev is probably part of your environment configuration. So if you only need the environment configuration object, you probably would have already gotten it elsewhere (in my case from process.env) and don't need to get the next config.

cyrilwanner commented 4 years ago

@mehmetnyarar the code you have posted should work as you expect it to work. If the solution which @dosentmatter posted does not work, can you share a minimal reproduction example so I can take a look?

larafale commented 4 years ago

same. is there a working work around ?

larafale commented 4 years ago

withTM in that case defines a webpack function in the settings it returns. So maybe the custom webpack function settings passed in withPlugins is already overrided by the above plugin ( withTM ) and thus ignored

antontsvil commented 4 years ago

+1 I'm having this issue in a major way minimal reproduction here, it's just the typescript +css module starter from next.js. If you open next.config.js you'll see we try to log when our custom webpack "hook" runs

module.exports = withPlugins([[withCSS, cssConfig]],{
  webpack: config =>{
    console.log("This never runs :(");  // <<<<<<<<<<<<<<<< Here's the issue
    return config;
  }
});

but if you run the dev script you'll never hit that line of code.

cyrilwanner commented 4 years ago

@antontsvil You define the webpack function twice in your file (once with the css plugin and once at the bottom) and the way next works, they overwrite each other so only one gets executed. You can simply change your next.config.js to only have one webpack definition like this:

const withCSS = require('@zeit/next-css')
const withPlugins = require('next-compose-plugins');
/* With additional configuration on top of CSS Modules */
const cssConfig = {
  cssModules: true,
  cssLoaderOptions: {
    camelCase: true,
    namedExport: true
  },
};

module.exports = withPlugins([[withCSS, cssConfig]],{
  webpack: (config, options) =>{
    console.log("This now runs :)");

    if (!options.isServer) {
      for (let entry of options.defaultLoaders.css) {
        if (entry.loader === 'css-loader') {
          entry.loader = 'typings-for-css-modules-loader'
          break
        }
      }
    }
    return config;
  }
});

I'm thinking if next-compose-plugins should manually parse the webpack definitions, but I'm not sure if that could break some special use-cases..

antontsvil commented 4 years ago

@cyrilwanner thanks for clearing that up. What's odd is that I've since found a workaround for this issue without removing the webpack function in the sassConfig. In my full project my next.config looks like this:

const extendWebpack = require("./extend-webpack");
const withPlugins = require("next-compose-plugins");
const withImages = require("next-optimized-images");
const withFonts = require("next-fonts");
const withSASS = require("@zeit/next-sass");

const SASSConfig = {
  cssModules: true,
  cssLoaderOptions: {
    importLoaders: 1,
    camelCase: true,
    namedExport: true
  },
  webpack(config, options) {
    if (!options.isServer) {
      for (let entry of options.defaultLoaders.sass) {
        if (entry.loader === "css-loader") {
          entry.loader = "typings-for-css-modules-loader";
        }
      }
    }
    return config;
  }
};

module.exports = withPlugins([
  [withSASS, SASSConfig],
  [withFonts],
  [withImages],
  [extendWebpack]
]);

The additional changes to my webpack config are made with this extendWebpack "plugin", which is simply a mock of a plugin structure that accomplishes what I need it to do. It looks like this:

const webpackAliasSupport = require("./lib/dev/webpack-alias-support");
const webpackEnableGlobal = require("./lib/dev/webpack-enable-global-sass");
module.exports = (nextConfig = {}, nextComposePlugins = {}) => {
  return Object.assign({}, nextConfig, {
    webpack: (config, options) => {
      config = webpackEnableGlobal(config);
      return webpackAliasSupport(config, options, nextConfig);
    }
  });
};

I'd like to apply your solution, since I'm sure there is still something wrong with my approach. Unfortunately I think I'm misunderstanding the order of the plugins being executed or something? When I try your suggested approach, the options.defaultLoaders object has no sass key, as if the withSASS plugin hasn't run yet. Am I incorrect in assuming that this final custom next config receives the webpack config at it's final state, after it's been transformed by all preceding plugins?

cyrilwanner commented 4 years ago

Ah, I see. The config gets passed to the plugins before they run so they are aware of the values set there (e.g. the publicPath etc). I think your current solution works well, but I'll see if I find a more convenient way for overwriting the webpack config.

nodkz commented 4 years ago

As a workaround, I wrote simple compose function:

function compose(plugins) {
  let cfg = {};
  return plugins.reduceRight(
    (prevFn, plugin) => {
      // console.log('-->', plugin);
      if (plugin[1]) cfg = { ...cfg, ...plugin[1] };
      return (...args) => plugin[0](prevFn(...args));
    },
    (value) => {
      // console.log('Combined config:', { ...cfg, ...value });
      return { ...cfg, ...value };
    }
  );
}

So your config will have the following view:

const plugins = const plugins = [
  [
    // https://github.com/zeit/next-plugins/tree/master/packages/next-css
    require('@zeit/next-css'),
    {
      cssModules: false,
      cssLoaderOptions: {},
    },
  ],
  [
    // https://github.com/zeit/next-plugins/tree/master/packages/next-sass
    require('@zeit/next-sass'),
    {
      sassLoaderOptions: {},
    },
  ],
];

const nextConfig = {
  distDir: '../dist',
};

module.export = compose(plugins)(nextConfig);
rwieruch commented 4 years ago

@antontsvil You define the webpack function twice in your file (once with the css plugin and once at the bottom) and the way next works, they overwrite each other so only one gets executed. You can simply change your next.config.js to only have one webpack definition like this:

const withCSS = require('@zeit/next-css')
const withPlugins = require('next-compose-plugins');
/* With additional configuration on top of CSS Modules */
const cssConfig = {
  cssModules: true,
  cssLoaderOptions: {
    camelCase: true,
    namedExport: true
  },
};

module.exports = withPlugins([[withCSS, cssConfig]],{
  webpack: (config, options) =>{
    console.log("This now runs :)");

    if (!options.isServer) {
      for (let entry of options.defaultLoaders.css) {
        if (entry.loader === 'css-loader') {
          entry.loader = 'typings-for-css-modules-loader'
          break
        }
      }
    }
    return config;
  }
});

I'm thinking if next-compose-plugins should manually parse the webpack definitions, but I'm not sure if that could break some special use-cases..

Thanks for this reply. My expectation was different: I thought with every plugin I use in the chain, I can define a dedicated webpack function and these functions would be composed onto each other. Having only one webpack functions for multiple plugins make this a mess over time. For instance, the following would be my desired usage:

const nextConfig = {
  env: {
    BASE_URL: process.env.BASE_URL,
  },
};

const lessWithAntdConfig = {
  lessLoaderOptions: {
    javascriptEnabled: true,
    modifyVars: lessToJS(
      fs.readFileSync(
        path.resolve(__dirname, './assets/antd-custom.less'),
        'utf8'
      )
    ),
  },
  webpack: (config, { isServer }) => {
    if (isServer) {
      const antStyles = /antd\/.*?\/style.*?/;
      const origExternals = [...config.externals];
      config.externals = [
        (context, request, callback) => {
          if (request.match(antStyles)) return callback();
          if (typeof origExternals[0] === 'function') {
            origExternals[0](context, request, callback);
          } else {
            callback();
          }
        },
        ...(typeof origExternals[0] === 'function'
          ? []
          : origExternals),
      ];

      config.module.rules.unshift({
        test: antStyles,
        use: 'null-loader',
      });

      console.log(config.node);
    }

    return config;
  },
};

const mdxConfig = {
  webpack: (config, { isServer }) => {
    if (!isServer) {
      config.node = {
        fs: 'empty',
      };
    }

    return config;
  },
};

const withBundleAnalyzer = bundleAnalyzer({
  enabled: process.env.ANALYZE === 'true',
});

module.exports = withPlugins(
  [
    [withMDX, mdxConfig],
    [withLess, lessWithAntdConfig],
    [withBundleAnalyzer],
  ],
  nextConfig
);

Instead I have to group all webpack functions in one shared webpack function:

const nextConfig = {
  env: {
    BASE_URL: process.env.BASE_URL,
  },
  webpack: (config, { isServer }) => {
    // Less with Antd
    if (isServer) {
      const antStyles = /antd\/.*?\/style.*?/;
      const origExternals = [...config.externals];
      config.externals = [
        (context, request, callback) => {
          if (request.match(antStyles)) return callback();
          if (typeof origExternals[0] === 'function') {
            origExternals[0](context, request, callback);
          } else {
            callback();
          }
        },
        ...(typeof origExternals[0] === 'function'
          ? []
          : origExternals),
      ];

      config.module.rules.unshift({
        test: antStyles,
        use: 'null-loader',
      });
    }

    // MDX
    if (!isServer) {
      config.node = {
        fs: 'empty',
      };
    }

    return config;
  },
};

const lessWithAntdConfig = {
  lessLoaderOptions: {
    javascriptEnabled: true,
    modifyVars: lessToJS(
      fs.readFileSync(
        path.resolve(__dirname, './assets/antd-custom.less'),
        'utf8'
      )
    ),
  },
};

const withBundleAnalyzer = bundleAnalyzer({
  enabled: process.env.ANALYZE === 'true',
});

module.exports = withPlugins(
  [[withLess, lessWithAntdConfig], [withMDX], [withBundleAnalyzer]],
  nextConfig
);

This seems okay for a webpack function for only two plugins, but let's imagine this for 10 plugins. Just my thoughts on this! Thanks for this plugin and your work on it 🎉

kiril-daskalov commented 3 years ago

In fact the config must be passed as array item together with plugins.

const cssConfig = {
  cssModules: true,
  cssLoaderOptions: {
    camelCase: true,
    namedExport: true
  },
};

module.exports = withPlugins([[withCSS, cssConfig]])