From 031a1af44b192a38167e3fd02aca8113879a624e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Cruz?= Date: Fri, 22 Nov 2019 15:54:10 +0000 Subject: [PATCH 1/2] feat: add next plugin that deals with webpack workarounds The canvas package, which is a dependency of jsdom, has a native binding which causes the following error during Next.js build process: "Module did not self-register". We circuvent that by using a null-loader. See https://github.com/zeit/next.js/issues/7894 The ws package, which is also a dependency of jsdom, tries to optionally load some dependencies. This produces a warning in webpack that we want to avoid. We circuvent that by adding them to externals. --- README.md | 18 +++++++++--- package-lock.json | 70 ++++++++++++++++++++++++++++++++++++++++++-- package.json | 2 +- plugin.js | 3 ++ src/plugin.js | 41 ++++++++++++++++++++++++++ src/plugin.test.js | 72 ++++++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 198 insertions(+), 8 deletions(-) create mode 100644 plugin.js create mode 100644 src/plugin.js create mode 100644 src/plugin.test.js diff --git a/README.md b/README.md index 3686b74..20c04c3 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,17 @@ All the polyfilling will be taken care by this library automatically, so that yo ## Setup -#### 1. Create a root folder named `intl` with the following structure: +#### 1 Add the plugin to your `next.config.js` + +```js +const withNextIntl = require('@moxy/next-intl/plugin'); + +module.exports = withNextIntl()({ ...nextConfig }); +``` + +This plugin will make some [modifications](src/plugin.js) to your webpack config to circuvent a few issues related to JSDOM, which is a runtime dependency of `react-intl` for the server. + +#### 2. Create a root folder named `intl` with the following structure: ``` intl/ @@ -71,7 +81,7 @@ The `messages/en-US.json` file contains the messages for the `en-US` locale: } ``` -#### 2. Include `` in `pages/_document.js`: +#### 3. Include `` in `pages/_document.js`: ```js import React from 'react'; @@ -96,7 +106,7 @@ export default class MyDocument extends Document { } ``` -#### 3. Wrap your app with `withNextIntlSetup` in `pages/_app.js`: +#### 4. Wrap your app with `withNextIntlSetup` in `pages/_app.js`: ```js import React from 'react'; @@ -131,7 +141,7 @@ class MyApp extends App { export default withNextIntlSetup(nextIntlConfig)(MyApp); ``` -#### 4. Ready! +#### 5. Ready! You may now use [`react-intl`](https://www.npmjs.com/package/react-intl) as you normally would. Moreover, you will receive the current locale in your pages' `getInitialProps` static function. diff --git a/package-lock.json b/package-lock.json index ce13626..36a1444 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1982,6 +1982,16 @@ "uri-js": "^4.2.2" } }, + "ajv-errors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ajv-errors/-/ajv-errors-1.0.1.tgz", + "integrity": "sha512-DCRfO/4nQ+89p/RK43i8Ezd41EqdGIU4ld7nGF8OQ14oc/we5rEntLCUa7+jrn3nn83BosfwZA0wb4pon2o8iQ==" + }, + "ajv-keywords": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.4.1.tgz", + "integrity": "sha512-RO1ibKvd27e6FEShVFfPALuHI3WjSVNeK5FIsmme/LYRNxjKuNj+Dt7bucLa6NdSv3JcVTyMlm9kGR84z1XpaQ==" + }, "ansi-escapes": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.2.0.tgz", @@ -2453,6 +2463,11 @@ "tweetnacl": "^0.14.3" } }, + "big.js": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", + "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==" + }, "binary-extensions": { "version": "1.13.1", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.13.1.tgz", @@ -3909,6 +3924,11 @@ "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", "dev": true }, + "emojis-list": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-2.1.0.tgz", + "integrity": "sha1-TapNnbAPmBmIDHn6RXrlsJof04k=" + }, "end-of-stream": { "version": "1.4.4", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", @@ -5742,9 +5762,9 @@ "dev": true }, "handlebars": { - "version": "4.5.2", - "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.5.2.tgz", - "integrity": "sha512-29Zxv/cynYB7mkT1rVWQnV7mGX6v7H/miQ6dbEpYTKq5eJBN7PsRB+ViYJlcT6JINTSu4dVB9kOqEun78h6Exg==", + "version": "4.5.3", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.5.3.tgz", + "integrity": "sha512-3yPecJoJHK/4c6aZhSvxOyG4vJKDshV36VHp0iVCDVh7o9w2vwi3NSnL2MMPj3YdduqaBcu7cGbggJQM0br9xA==", "dev": true, "requires": { "neo-async": "^2.6.0", @@ -7710,6 +7730,31 @@ "strip-bom": "^3.0.0" } }, + "loader-utils": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.2.3.tgz", + "integrity": "sha512-fkpz8ejdnEMG3s37wGL07iSBDg99O9D5yflE9RGNH3hRdx9SOwYfnGYdZOUIZitN8E+E2vkq3MUMYMvPYl5ZZA==", + "requires": { + "big.js": "^5.2.2", + "emojis-list": "^2.0.0", + "json5": "^1.0.1" + }, + "dependencies": { + "json5": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", + "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", + "requires": { + "minimist": "^1.2.0" + } + }, + "minimist": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" + } + } + }, "locate-path": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", @@ -8248,6 +8293,15 @@ "integrity": "sha1-l33/1xdgErnsMNKjnbXPcqBDnt0=", "dev": true }, + "null-loader": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/null-loader/-/null-loader-3.0.0.tgz", + "integrity": "sha512-hf5sNLl8xdRho4UPBOOeoIwT3WhjYcMUQm0zj44EhD6UscMAz72o2udpoDFBgykucdEDGIcd6SXbc/G6zssbzw==", + "requires": { + "loader-utils": "^1.2.3", + "schema-utils": "^1.0.0" + } + }, "number-is-nan": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", @@ -9360,6 +9414,16 @@ "object-assign": "^4.1.1" } }, + "schema-utils": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz", + "integrity": "sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g==", + "requires": { + "ajv": "^6.1.0", + "ajv-errors": "^1.0.0", + "ajv-keywords": "^3.1.0" + } + }, "semver": { "version": "5.7.1", "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", diff --git a/package.json b/package.json index 83243e3..e7c815a 100644 --- a/package.json +++ b/package.json @@ -50,6 +50,7 @@ "hoist-non-react-statics": "^3.3.1", "jsdom": "^15.2.1", "memoize-one": "^5.1.1", + "null-loader": "^3.0.0", "p-cancelable": "^2.0.0", "pico-signals": "^1.0.0", "prop-types": "^15.7.2", @@ -66,7 +67,6 @@ "@commitlint/config-conventional": "^8.1.0", "@moxy/jest-config": "^1.1.0", "@testing-library/react": "^9.3.2", - "babel-jest": "^24.9.0", "babel-preset-moxy": "^3.2.0", "delay": "^4.3.0", "eslint": "^6.6.0", diff --git a/plugin.js b/plugin.js new file mode 100644 index 0000000..adb89b2 --- /dev/null +++ b/plugin.js @@ -0,0 +1,3 @@ +/* eslint-disable prefer-import/prefer-import-over-require */ + +module.exports = require('./lib/plugin'); diff --git a/src/plugin.js b/src/plugin.js new file mode 100644 index 0000000..2f565d3 --- /dev/null +++ b/src/plugin.js @@ -0,0 +1,41 @@ +const castArray = (value) => { + if (Array.isArray(value)) { + return value; + } + + return value != null ? [value] : []; +}; + +const withNextIntl = () => (nextConfig = {}) => ({ + ...nextConfig, + webpack: (config, options) => { + const { isServer } = options; + + if (isServer) { + // The canvas package, which is a dependency of jsdom, has a native binding which causes + // the following error during Next.js build process: "Module did not self-register" + // See https://github.com/zeit/next.js/issues/7894 + // We circuvent that by using a null-loader + config.module.rules.unshift({ + test: require.resolve('canvas'), + loader: require.resolve('null-loader'), + }); + + // The ws package, which is also a dependency of jsdom, tries to optionally load some dependencies + // This produces a warning in webpack that we want to avoid + config.externals = [ + 'bufferutil', + 'utf-8-validate', + ...castArray(config.externals), + ]; + } + + if (typeof nextConfig.webpack === 'function') { + return nextConfig.webpack(config, options); + } + + return config; + }, +}); + +export default withNextIntl; diff --git a/src/plugin.test.js b/src/plugin.test.js new file mode 100644 index 0000000..96ce1ca --- /dev/null +++ b/src/plugin.test.js @@ -0,0 +1,72 @@ +const nextIntlPlugin = require('./plugin'); + +const webpackOptions = { + isServer: true, +}; + +const createWebpackConfig = () => ({ + module: { + rules: [ + { + test: 'foo', + loader: 'foo-loader', + }, + ], + }, + externals: () => {}, +}); + +it('should add a rule for canvas that uses null-loader', () => { + const config = nextIntlPlugin()().webpack(createWebpackConfig(), webpackOptions); + + const rule = config.module.rules[0]; + + expect(rule.test).toBe(require.resolve('canvas')); + expect(rule.loader).toBe(require.resolve('null-loader')); +}); + +it('should add no canvas rule when not server', () => { + const config = nextIntlPlugin()().webpack(createWebpackConfig(), { ...webpackOptions, isServer: false }); + + expect(config.module.rules[0].test).toBe('foo'); +}); + +it('should add ws\'s optional dependencies to externals', () => { + const config = nextIntlPlugin()().webpack(createWebpackConfig(), webpackOptions); + + expect(config.externals).toHaveLength(3); + expect(config.externals[0]).toBe('bufferutil'); + expect(config.externals[1]).toBe('utf-8-validate'); + expect(typeof config.externals[2]).toBe('function'); +}); + +it('should add ws\'s optional dependencies to externals (already an array)', () => { + const originalConfig = { + ...createWebpackConfig(), + externals: [() => {}], + }; + + const config = nextIntlPlugin()().webpack(originalConfig, webpackOptions); + + expect(config.externals).toHaveLength(3); + expect(config.externals[0]).toBe('bufferutil'); + expect(config.externals[1]).toBe('utf-8-validate'); + expect(typeof config.externals[2]).toBe('function'); +}); + +it('should leave externals untouched when not server', () => { + const config = nextIntlPlugin()().webpack(createWebpackConfig(), { ...webpackOptions, isServer: false }); + + expect(typeof config.externals).toBe('function'); +}); + +it('should call nextConfig webpack if defined', () => { + const nextConfig = { + webpack: jest.fn(() => 'foo'), + }; + + const config = nextIntlPlugin()(nextConfig).webpack(createWebpackConfig(), webpackOptions); + + expect(nextConfig.webpack).toHaveBeenCalledTimes(1); + expect(config).toBe('foo'); +}); From 8f58c3d689d08a09c350923b91b74f6097d60ce8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Cruz?= Date: Fri, 22 Nov 2019 16:15:19 +0000 Subject: [PATCH 2/2] chore: update README.md Co-Authored-By: Ivo Lima Silva --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 20c04c3..cde554a 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,7 @@ const withNextIntl = require('@moxy/next-intl/plugin'); module.exports = withNextIntl()({ ...nextConfig }); ``` -This plugin will make some [modifications](src/plugin.js) to your webpack config to circuvent a few issues related to JSDOM, which is a runtime dependency of `react-intl` for the server. +This plugin will make some [modifications](src/plugin.js) to your webpack config to circuvent a few issues related to `jsdom`, which is a runtime dependency of `react-intl` for the server. #### 2. Create a root folder named `intl` with the following structure: