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

Add no-anon-default-export Babel lint rule #14519

Merged
merged 8 commits into from
Jun 30, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 83 additions & 0 deletions packages/next/build/babel/plugins/no-anonymous-default-export.ts
Original file line number Diff line number Diff line change
@@ -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<any> {
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 () => <div />;'),
'',
chalk.bold('After'),
chalk.cyan('const Named = () => <div />;'),
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
}
}
},
},
}
}
19 changes: 19 additions & 0 deletions packages/next/build/webpack/loaders/next-babel-loader.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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) {
Expand Down
4 changes: 3 additions & 1 deletion test/integration/hydration/pages/_app.js
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
export default ({ Component, pageProps }) => <Component {...pageProps} />
export default function CustomApp({ Component, pageProps }) {
return <Component {...pageProps} />
}
3 changes: 3 additions & 0 deletions test/integration/no-anon-default-export/components/Child.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default () => {
return <div />
}
4 changes: 4 additions & 0 deletions test/integration/no-anon-default-export/pages/both.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import Child from '../components/Child'
export default function () {
return <Child />
}
4 changes: 4 additions & 0 deletions test/integration/no-anon-default-export/pages/child.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import Child from '../components/Child'
export default function A() {
return <Child />
}
3 changes: 3 additions & 0 deletions test/integration/no-anon-default-export/pages/page.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default function () {
return <div />
}
86 changes: 86 additions & 0 deletions test/integration/no-anon-default-export/test/index.test.js
Original file line number Diff line number Diff line change
@@ -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 * 60 * 3)

const appDir = join(__dirname, '../')

describe('no anonymous default export warning', () => {
function getRegexCount(text, regex) {
return (text.match(regex) || []).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(getRegexCount(stdout, /not preserve local component state/g)).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(getRegexCount(stdout, /not preserve local component state/g)).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(getRegexCount(stdout, /not preserve local component state/g)).toBe(2)

await killApp(app)
})
})
1 change: 1 addition & 0 deletions test/unit/next-babel-loader.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ const babel = async (
return callback
},
callback,
emitWarning() {},
query: {
// babel opts
babelrc: false,
Expand Down