From d9a5abb031fff1fd67bd05f3df849f3d6fcdc753 Mon Sep 17 00:00:00 2001 From: Joe Haddad Date: Wed, 24 Jun 2020 00:16:41 -0400 Subject: [PATCH 1/6] Add no-anon-default-export Babel lint rule --- .../plugins/no-anonymous-default-export.ts | 83 ++++++++++++++++++ .../webpack/loaders/next-babel-loader.js | 19 ++++ .../components/Child.js | 3 + .../no-anon-default-export/pages/both.js | 4 + .../no-anon-default-export/pages/child.js | 4 + .../no-anon-default-export/pages/page.js | 3 + .../no-anon-default-export/test/index.test.js | 86 +++++++++++++++++++ 7 files changed, 202 insertions(+) create mode 100644 packages/next/build/babel/plugins/no-anonymous-default-export.ts create mode 100644 test/integration/no-anon-default-export/components/Child.js create mode 100644 test/integration/no-anon-default-export/pages/both.js create mode 100644 test/integration/no-anon-default-export/pages/child.js create mode 100644 test/integration/no-anon-default-export/pages/page.js create mode 100644 test/integration/no-anon-default-export/test/index.test.js diff --git a/packages/next/build/babel/plugins/no-anonymous-default-export.ts b/packages/next/build/babel/plugins/no-anonymous-default-export.ts new file mode 100644 index 0000000000000..32aa89b6815bb --- /dev/null +++ b/packages/next/build/babel/plugins/no-anonymous-default-export.ts @@ -0,0 +1,83 @@ +import { PluginObj, types as BabelTypes } from '@babel/core' +import chalk from 'next/dist/compiled/chalk' + +export default function NoAnonymousDefaultExport({ + types: t, + ...babel +}: { + types: typeof BabelTypes + caller: (callerCallback: (caller: any) => any) => any +}): PluginObj { + let onWarning: ((reason: string | Error) => void) | null = null + babel.caller((caller) => { + onWarning = caller.onWarning + return '' // Intentionally empty to not invalidate cache + }) + + if (typeof onWarning !== 'function') { + return { visitor: {} } + } + + const warn = onWarning! + return { + visitor: { + ExportDefaultDeclaration(path) { + const def = path.node.declaration + + if ( + !( + def.type === 'ArrowFunctionExpression' || + def.type === 'FunctionDeclaration' + ) + ) { + return + } + + switch (def.type) { + case 'ArrowFunctionExpression': { + warn( + [ + chalk.yellow.bold( + 'Anonymous arrow functions cause Fast Refresh to not preserve local component state.' + ), + 'Please add a name to your function, for example:', + '', + chalk.bold('Before'), + chalk.cyan('export default () =>
;'), + '', + chalk.bold('After'), + chalk.cyan('const Named = () =>
;'), + chalk.cyan('export default Named;'), + ].join('\n') + ) + break + } + case 'FunctionDeclaration': { + const isAnonymous = !Boolean(def.id) + if (isAnonymous) { + warn( + [ + chalk.yellow.bold( + 'Anonymous function declarations cause Fast Refresh to not preserve local component state.' + ), + 'Please add a name to your function, for example:', + '', + chalk.bold('Before'), + chalk.cyan('export default function () { /* ... */ }'), + '', + chalk.bold('After'), + chalk.cyan('export default function Named() { /* ... */ }'), + ].join('\n') + ) + } + break + } + default: { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const _: never = def + } + } + }, + }, + } +} diff --git a/packages/next/build/webpack/loaders/next-babel-loader.js b/packages/next/build/webpack/loaders/next-babel-loader.js index 7bce228ef0da7..b11666674af10 100644 --- a/packages/next/build/webpack/loaders/next-babel-loader.js +++ b/packages/next/build/webpack/loaders/next-babel-loader.js @@ -137,6 +137,18 @@ module.exports = babelLoader.custom((babel) => { options.caller.isModern = isModern options.caller.isDev = development + const emitWarning = this.emitWarning.bind(this) + Object.defineProperty(options.caller, 'onWarning', { + enumerable: false, + writable: false, + value: (options.caller.onWarning = function (reason) { + if (!(reason instanceof Error)) { + reason = new Error(reason) + } + emitWarning(reason) + }), + }) + options.plugins = options.plugins || [] if (hasReactRefresh) { @@ -145,6 +157,13 @@ module.exports = babelLoader.custom((babel) => { { type: 'plugin' } ) options.plugins.unshift(reactRefreshPlugin) + if (!isServer) { + const noAnonymousDefaultExportPlugin = babel.createConfigItem( + [require('../../babel/plugins/no-anonymous-default-export'), {}], + { type: 'plugin' } + ) + options.plugins.unshift(noAnonymousDefaultExportPlugin) + } } if (!isServer && isPageFile) { diff --git a/test/integration/no-anon-default-export/components/Child.js b/test/integration/no-anon-default-export/components/Child.js new file mode 100644 index 0000000000000..7176cf7772111 --- /dev/null +++ b/test/integration/no-anon-default-export/components/Child.js @@ -0,0 +1,3 @@ +export default function () { + return
+} diff --git a/test/integration/no-anon-default-export/pages/both.js b/test/integration/no-anon-default-export/pages/both.js new file mode 100644 index 0000000000000..bc409e375c057 --- /dev/null +++ b/test/integration/no-anon-default-export/pages/both.js @@ -0,0 +1,4 @@ +import Child from '../components/Child' +export default function () { + return +} diff --git a/test/integration/no-anon-default-export/pages/child.js b/test/integration/no-anon-default-export/pages/child.js new file mode 100644 index 0000000000000..06e991078c0ba --- /dev/null +++ b/test/integration/no-anon-default-export/pages/child.js @@ -0,0 +1,4 @@ +import Child from '../components/Child' +export default function A() { + return +} diff --git a/test/integration/no-anon-default-export/pages/page.js b/test/integration/no-anon-default-export/pages/page.js new file mode 100644 index 0000000000000..7176cf7772111 --- /dev/null +++ b/test/integration/no-anon-default-export/pages/page.js @@ -0,0 +1,3 @@ +export default function () { + return
+} diff --git a/test/integration/no-anon-default-export/test/index.test.js b/test/integration/no-anon-default-export/test/index.test.js new file mode 100644 index 0000000000000..e4008893a9573 --- /dev/null +++ b/test/integration/no-anon-default-export/test/index.test.js @@ -0,0 +1,86 @@ +/* eslint-env jest */ + +import fs from 'fs-extra' +import { check, findPort, killApp, launchApp } from 'next-test-utils' +import webdriver from 'next-webdriver' +import { join } from 'path' + +jest.setTimeout(1000 * 30) + +const appDir = join(__dirname, '../') + +describe('no anonymous default export warning', () => { + function getWarningsCount(text) { + return (text.match(/warn {2}-/g) || []).length + } + + beforeEach(async () => { + await fs.remove(join(appDir, '.next')) + }) + + it('show correct warnings for page', async () => { + let stdout = '' + + const appPort = await findPort() + const app = await launchApp(appDir, appPort, { + env: { __NEXT_TEST_WITH_DEVTOOL: true }, + onStdout(msg) { + stdout += msg || '' + }, + }) + + const browser = await webdriver(appPort, '/page') + + const found = await check(() => stdout, /anonymous/i, false) + expect(found).toBeTruthy() + await browser.close() + + expect(getWarningsCount(stdout)).toBe(1) + + await killApp(app) + }) + + it('show correct warnings for child', async () => { + let stdout = '' + + const appPort = await findPort() + const app = await launchApp(appDir, appPort, { + env: { __NEXT_TEST_WITH_DEVTOOL: true }, + onStdout(msg) { + stdout += msg || '' + }, + }) + + const browser = await webdriver(appPort, '/child') + + const found = await check(() => stdout, /anonymous/i, false) + expect(found).toBeTruthy() + await browser.close() + + expect(getWarningsCount(stdout)).toBe(1) + + await killApp(app) + }) + + it('show correct warnings for both', async () => { + let stdout = '' + + const appPort = await findPort() + const app = await launchApp(appDir, appPort, { + env: { __NEXT_TEST_WITH_DEVTOOL: true }, + onStdout(msg) { + stdout += msg || '' + }, + }) + + const browser = await webdriver(appPort, '/both') + + const found = await check(() => stdout, /anonymous/i, false) + expect(found).toBeTruthy() + await browser.close() + + expect(getWarningsCount(stdout)).toBe(2) + + await killApp(app) + }) +}) From 240c6258ad8c7635ae2b01a8babde47acb31c073 Mon Sep 17 00:00:00 2001 From: Joe Haddad Date: Wed, 24 Jun 2020 00:18:24 -0400 Subject: [PATCH 2/6] Test variance --- test/integration/no-anon-default-export/components/Child.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/integration/no-anon-default-export/components/Child.js b/test/integration/no-anon-default-export/components/Child.js index 7176cf7772111..16fd5c268fdc4 100644 --- a/test/integration/no-anon-default-export/components/Child.js +++ b/test/integration/no-anon-default-export/components/Child.js @@ -1,3 +1,3 @@ -export default function () { +export default () => { return
} From c39ead1bd8281c6fce452d5882450806ed7f3dd3 Mon Sep 17 00:00:00 2001 From: Joe Haddad Date: Wed, 24 Jun 2020 00:18:56 -0400 Subject: [PATCH 3/6] Increase timeout --- test/integration/no-anon-default-export/test/index.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/integration/no-anon-default-export/test/index.test.js b/test/integration/no-anon-default-export/test/index.test.js index e4008893a9573..92acb015e897a 100644 --- a/test/integration/no-anon-default-export/test/index.test.js +++ b/test/integration/no-anon-default-export/test/index.test.js @@ -5,7 +5,7 @@ import { check, findPort, killApp, launchApp } from 'next-test-utils' import webdriver from 'next-webdriver' import { join } from 'path' -jest.setTimeout(1000 * 30) +jest.setTimeout(1000 * 60 * 3) const appDir = join(__dirname, '../') From f78d96ec21be752c3393913eaa5c95577e80df71 Mon Sep 17 00:00:00 2001 From: Joe Haddad Date: Tue, 30 Jun 2020 01:17:50 -0400 Subject: [PATCH 4/6] Fix test --- .../no-anon-default-export/test/index.test.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/test/integration/no-anon-default-export/test/index.test.js b/test/integration/no-anon-default-export/test/index.test.js index 92acb015e897a..696e916e7dcc9 100644 --- a/test/integration/no-anon-default-export/test/index.test.js +++ b/test/integration/no-anon-default-export/test/index.test.js @@ -10,8 +10,8 @@ jest.setTimeout(1000 * 60 * 3) const appDir = join(__dirname, '../') describe('no anonymous default export warning', () => { - function getWarningsCount(text) { - return (text.match(/warn {2}-/g) || []).length + function getRegexCount(text, regex) { + return (text.match(regex) || []).length } beforeEach(async () => { @@ -35,7 +35,7 @@ describe('no anonymous default export warning', () => { expect(found).toBeTruthy() await browser.close() - expect(getWarningsCount(stdout)).toBe(1) + expect(getRegexCount(stdout, /not preserve local component state/g)).toBe(1) await killApp(app) }) @@ -57,7 +57,7 @@ describe('no anonymous default export warning', () => { expect(found).toBeTruthy() await browser.close() - expect(getWarningsCount(stdout)).toBe(1) + expect(getRegexCount(stdout, /not preserve local component state/g)).toBe(1) await killApp(app) }) @@ -79,7 +79,7 @@ describe('no anonymous default export warning', () => { expect(found).toBeTruthy() await browser.close() - expect(getWarningsCount(stdout)).toBe(2) + expect(getRegexCount(stdout, /not preserve local component state/g)).toBe(2) await killApp(app) }) From 815d575c67f30591c30b422e6b1d9df531ccbbf4 Mon Sep 17 00:00:00 2001 From: Joe Haddad Date: Tue, 30 Jun 2020 01:21:03 -0400 Subject: [PATCH 5/6] fix test --- test/integration/hydration/pages/_app.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/test/integration/hydration/pages/_app.js b/test/integration/hydration/pages/_app.js index e3c005467a169..cc113aa33cc15 100644 --- a/test/integration/hydration/pages/_app.js +++ b/test/integration/hydration/pages/_app.js @@ -1 +1,3 @@ -export default ({ Component, pageProps }) => +export default function CustomApp({ Component, pageProps }) { + return +} From bc7980744b84ef47fa41e4be4e37fe0ed50232b8 Mon Sep 17 00:00:00 2001 From: Joe Haddad Date: Tue, 30 Jun 2020 01:36:53 -0400 Subject: [PATCH 6/6] fix unit test --- test/unit/next-babel-loader.test.js | 1 + 1 file changed, 1 insertion(+) diff --git a/test/unit/next-babel-loader.test.js b/test/unit/next-babel-loader.test.js index 5f40cbeb3ac56..0544f584f16bc 100644 --- a/test/unit/next-babel-loader.test.js +++ b/test/unit/next-babel-loader.test.js @@ -38,6 +38,7 @@ const babel = async ( return callback }, callback, + emitWarning() {}, query: { // babel opts babelrc: false,