未验证 提交 ea1f45a3 编写于 作者: J Joe Haddad 提交者: GitHub

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

Fixes #12291 

![image](https://user-images.githubusercontent.com/616428/85499698-176b4900-b5b0-11ea-8a5d-a7f0b4c20307.png)
上级 1b5ca788
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
}
}
},
},
}
}
......@@ -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) {
......
export default ({ Component, pageProps }) => <Component {...pageProps} />
export default function CustomApp({ Component, pageProps }) {
return <Component {...pageProps} />
}
import Child from '../components/Child'
export default function () {
return <Child />
}
import Child from '../components/Child'
export default function A() {
return <Child />
}
export default function () {
return <div />
}
/* 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)
})
})
......@@ -38,6 +38,7 @@ const babel = async (
return callback
},
callback,
emitWarning() {},
query: {
// babel opts
babelrc: false,
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册