Skip to content

Commit

Permalink
Improve error for invalid page configurations (#10441)
Browse files Browse the repository at this point in the history
* Remove any type and fix edge cases

Removed the "as any" and use the @babel/types typeguards instead. This revealed some edge cases that would just error.

* Remove ts-ignore

Co-authored-by: Joe Haddad <timer150@gmail.com>
  • Loading branch information
Janpot and Timer committed Feb 13, 2020
1 parent 44e40d4 commit 0b12e2e
Show file tree
Hide file tree
Showing 8 changed files with 123 additions and 23 deletions.
54 changes: 39 additions & 15 deletions packages/next/build/babel/plugins/next-page-config.ts
Expand Up @@ -2,7 +2,6 @@ import { NodePath, PluginObj } from '@babel/core'
import * as BabelTypes from '@babel/types'
import { PageConfig } from 'next/types'

const configKeys = new Set(['amp'])
const STRING_LITERAL_DROP_BUNDLE = '__NEXT_DROP_CLIENT_FILE__'

// replace program path with just a variable with the drop identifier
Expand All @@ -26,6 +25,12 @@ function replaceBundle(path: any, t: typeof BabelTypes) {
)
}

function errorMessage(state: any, details: string) {
const pageName =
(state.filename || '').split(state.cwd || '').pop() || 'unknown'
return `Invalid page config export found. ${details} in file ${pageName}. See: https://err.sh/zeit/next.js/invalid-page-config`
}

interface ConfigState {
bundleDropped?: boolean
}
Expand All @@ -50,32 +55,51 @@ export default function nextPageConfig({
return
}

const { declarations } = path.node.declaration as any
const config: PageConfig = {}

if (!declarations) {
if (!BabelTypes.isVariableDeclaration(path.node.declaration)) {
return
}

const { declarations } = path.node.declaration
const config: PageConfig = {}

for (const declaration of declarations) {
if (declaration.id.name !== 'config') {
if (
!BabelTypes.isIdentifier(declaration.id, { name: 'config' })
) {
continue
}

if (declaration.init.type !== 'ObjectExpression') {
const pageName =
(state.filename || '').split(state.cwd || '').pop() ||
'unknown'

if (!BabelTypes.isObjectExpression(declaration.init)) {
const got = declaration.init
? declaration.init.type
: 'undefined'
throw new Error(
`Invalid page config export found. Expected object but got ${declaration.init.type} in file ${pageName}. See: https://err.sh/zeit/next.js/invalid-page-config`
errorMessage(state, `Expected object but got ${got}`)
)
}

for (const prop of declaration.init.properties) {
if (BabelTypes.isSpreadElement(prop)) {
throw new Error(
errorMessage(state, `Property spread is not allowed`)
)
}
const { name } = prop.key
if (configKeys.has(name)) {
// @ts-ignore
config[name] = prop.value.value
if (BabelTypes.isIdentifier(prop.key, { name: 'amp' })) {
if (!BabelTypes.isObjectProperty(prop)) {
throw new Error(
errorMessage(state, `Invalid property "${name}"`)
)
}
if (
!BabelTypes.isBooleanLiteral(prop.value) &&
!BabelTypes.isStringLiteral(prop.value)
) {
throw new Error(
errorMessage(state, `Invalid value for "${name}"`)
)
}
config.amp = prop.value.value as PageConfig['amp']
}
}
}
Expand Down
2 changes: 0 additions & 2 deletions test/integration/page-config/pages/index.js
@@ -1,8 +1,6 @@
import { config as hello } from '../something'
import { config as world } from '../config'

// export const config = 'hello world'

export default () => (
<p>
{hello} {world}
Expand Down
@@ -0,0 +1,3 @@
// export const config = { amp() {} }

export default () => <p>hello world</p>
5 changes: 5 additions & 0 deletions test/integration/page-config/pages/invalid/invalid-value.js
@@ -0,0 +1,5 @@
const amp = 'hybrid'

// export const config = { amp }

export default () => <p>hello ${amp}</p>
3 changes: 3 additions & 0 deletions test/integration/page-config/pages/invalid/no-init.js
@@ -0,0 +1,3 @@
// export let config

export default () => <p>hello world</p>
5 changes: 5 additions & 0 deletions test/integration/page-config/pages/invalid/spread-config.js
@@ -0,0 +1,5 @@
const props = { amp: true }

// export const config = { ...props }

export default () => <p>{props}</p>
3 changes: 3 additions & 0 deletions test/integration/page-config/pages/invalid/string-config.js
@@ -0,0 +1,3 @@
// export const config = 'hello world'

export default () => <p>hello world</p>
71 changes: 65 additions & 6 deletions test/integration/page-config/test/index.test.js
Expand Up @@ -5,28 +5,87 @@ import { join } from 'path'
import { nextBuild } from 'next-test-utils'

const appDir = join(__dirname, '..')
const indexPage = join(appDir, 'pages/index.js')

jasmine.DEFAULT_TIMEOUT_INTERVAL = 1000 * 60 * 2

async function uncommentExport(page) {
const pagePath = join(appDir, 'pages', page)
const origContent = await fs.readFile(pagePath, 'utf8')
const newContent = origContent.replace('// export', 'export')
await fs.writeFile(pagePath, newContent, 'utf8')
return async () => {
await fs.writeFile(pagePath, origContent, 'utf8')
}
}

describe('Page Config', () => {
it('builds without error when export const config is used outside page', async () => {
const { stderr } = await nextBuild(appDir, undefined, { stderr: true })
expect(stderr).not.toMatch(/Failed to compile\./)
})

it('shows valid error on invalid page config', async () => {
const origContent = await fs.readFile(indexPage, 'utf8')
const newContent = origContent.replace('// export', 'export')
await fs.writeFile(indexPage, newContent, 'utf8')
it('shows valid error when page config is a string', async () => {
const reset = await uncommentExport('invalid/string-config.js')

try {
const { stderr } = await nextBuild(appDir, undefined, { stderr: true })
expect(stderr).toMatch(
/https:\/\/err\.sh\/zeit\/next\.js\/invalid-page-config/
)
} finally {
await reset()
}
})

it('shows valid error when page config has no init', async () => {
const reset = await uncommentExport('invalid/no-init.js')

try {
const { stderr } = await nextBuild(appDir, undefined, { stderr: true })
expect(stderr).toMatch(
/https:\/\/err\.sh\/zeit\/next\.js\/invalid-page-config/
)
} finally {
await reset()
}
})

it('shows error when page config has spread properties', async () => {
const reset = await uncommentExport('invalid/spread-config.js')

try {
const { stderr } = await nextBuild(appDir, undefined, { stderr: true })
expect(stderr).toMatch(
/https:\/\/err\.sh\/zeit\/next\.js\/invalid-page-config/
)
} finally {
await reset()
}
})

it('shows error when page config has invalid properties', async () => {
const reset = await uncommentExport('invalid/invalid-property.js')

try {
const { stderr } = await nextBuild(appDir, undefined, { stderr: true })
expect(stderr).toMatch(
/https:\/\/err\.sh\/zeit\/next\.js\/invalid-page-config/
)
} finally {
await reset()
}
})

it('shows error when page config has invalid property value', async () => {
const reset = await uncommentExport('invalid/invalid-value.js')

try {
const { stderr } = await nextBuild(appDir, undefined, { stderr: true })
expect(stderr).toMatch(
/https:\/\/err\.sh\/zeit\/next\.js\/invalid-page-config/
)
} finally {
await fs.writeFile(indexPage, origContent, 'utf8')
await reset()
}
})
})

0 comments on commit 0b12e2e

Please sign in to comment.