Using multiple plugins implies getting several nested function calls in next.config.ts. They become hard to read pretty quickly, after adding just 3-4 plugins. Should not there be some recommended compose pattern that would make plugins flat?
E.g.:
module.exports = withTypescript(
withCss(
withBundleAnalyzer({
distDir: "../.next",
analyzeServer: ["server", "both"].includes(process.env.BUNDLE_ANALYZE),
analyzeBrowser: ["browser", "both"].includes(process.env.BUNDLE_ANALYZE),
}),
),
);
↓
const compose = require("@zeit/next/compose-plugins");
module.exports = compose(
withTypescript,
withCss,
withBundleAnalyzer,
)({
distDir: "../.next",
analyzeServer: ["server", "both"].includes(process.env.BUNDLE_ANALYZE),
analyzeBrowser: ["browser", "both"].includes(process.env.BUNDLE_ANALYZE),
});
I've seen variations of compose() in a few places such as
Perhaps, next plugins could also use a similar pattern? We could even make curried calls to plugins so that options were closer to where they belong:
module.exports = compose(
generateWithTypescript({
/* typescript plugin options */
}),
generateWithCss({
/* css plugin options */
}),
generateWithBundleAnalyzer({
analyzeServer: ["server", "both"].includes(process.env.BUNDLE_ANALYZE),
analyzeBrowser: ["browser", "both"].includes(process.env.BUNDLE_ANALYZE),
}),
)({
distDir: "../.next",
});
or:
module.exports = compose(
withTypescript({
/* typescript plugin options */
}),
withCss({
/* css plugin options */
}),
withBundleAnalyzer({
analyzeServer: ["server", "both"].includes(process.env.BUNDLE_ANALYZE),
analyzeBrowser: ["browser", "both"].includes(process.env.BUNDLE_ANALYZE),
}),
{
distDir: "../.next",
},
);
A choice between the above designs depends on whether we want the options generated by one plugin to be passed to another plugin and be morphed by it.
I know it's probably possible to do just this, but aren't there limitations?
module.exports = {
...withTypescript({
/* typescript plugin options */
}),
...withCss({
/* css plugin options */
}),
...withBundleAnalyzer({
analyzeServer: ["server", "both"].includes(process.env.BUNDLE_ANALYZE),
analyzeBrowser: ["browser", "both"].includes(process.env.BUNDLE_ANALYZE),
}),
...{
distDir: "../.next",
},
};
Composed rather nested plugins look a bit more readable. Git diffs are smaller too once any of the plugins is added or removed. It'd be good to discuss plugin composition options in this issue and agree on some implementation of compose() if it's at all needed.
WDYT?
If compose accepts both objects and functions, we can avoid making generate***Plugin(), yet tick all other checkboxes:
module.exports = compose(
withTypescript,
{
/* typescript plugin options */
},
withCss,
{
/* css plugin options */
},
withBundleAnalyzer,
{
analyzeServer: ["server", "both"].includes(process.env.BUNDLE_ANALYZE),
analyzeBrowser: ["browser", "both"].includes(process.env.BUNDLE_ANALYZE),
},
{
distDir: "../.next",
},
);
The resulting config will be built from bottom to top and compose will be a right-to-left reducer that does roughly this:
const _ = require("lodash");
const compose = (...args) =>
_.reduceRight(
args,
(result, arg) => (_.isFunction(arg) ? arg(result) : { ...result, ...arg }),
{},
);
🤔
Thanks for sharing the link @prateekrastogi – next-compose-plugins looks awesome and solves the issue I've been trying to raise! 🎉
@kachkaev Although, I suggested that link, I ended up using https://github.com/JerryCauser/next-compose in my code, mainly due to stylistic considerations, as I found it to be more in line with ideal functional composition.
Agree that next-compose looks slightly cleaner, but it's too late – I've already installed next-compose-plugins 😅