rsnext/packages/next/build/babel/plugins/next-page-config.ts
Hannes Bornö 8aa3620a16
Babel & next-swc: Fix exporting page config with AsExpression (#32702)
fixes #20626

No changes needed in next-swc, see comments from @kdy1 below.

Co-authored-by: JJ Kasper <22380829+ijjk@users.noreply.github.com>
2022-02-05 21:15:49 +00:00

214 lines
7.3 KiB
TypeScript

import {
NodePath,
PluginObj,
PluginPass,
types as BabelTypes,
Visitor,
} from 'next/dist/compiled/babel/core'
import { PageConfig } from 'next/types'
import { STRING_LITERAL_DROP_BUNDLE } from '../../../shared/lib/constants'
const CONFIG_KEY = 'config'
// replace program path with just a variable with the drop identifier
function replaceBundle(path: any, t: typeof BabelTypes): void {
path.parentPath.replaceWith(
t.program(
[
t.variableDeclaration('const', [
t.variableDeclarator(
t.identifier(STRING_LITERAL_DROP_BUNDLE),
t.stringLiteral(`${STRING_LITERAL_DROP_BUNDLE} ${Date.now()}`)
),
]),
],
[]
)
)
}
function errorMessage(state: any, details: string): string {
const pageName =
(state.filename || '').split(state.cwd || '').pop() || 'unknown'
return `Invalid page config export found. ${details} in file ${pageName}. See: https://nextjs.org/docs/messages/invalid-page-config`
}
interface ConfigState extends PluginPass {
bundleDropped?: boolean
}
// config to parsing pageConfig for client bundles
export default function nextPageConfig({
types: t,
}: {
types: typeof BabelTypes
}): PluginObj {
return {
visitor: {
Program: {
enter(path, state) {
path.traverse(
{
ExportDeclaration(exportPath, exportState) {
if (
BabelTypes.isExportNamedDeclaration(exportPath) &&
(
exportPath.node as BabelTypes.ExportNamedDeclaration
).specifiers?.some((specifier) => {
return (
(t.isIdentifier(specifier.exported)
? specifier.exported.name
: specifier.exported.value) === CONFIG_KEY
)
}) &&
BabelTypes.isStringLiteral(
(exportPath.node as BabelTypes.ExportNamedDeclaration)
.source
)
) {
throw new Error(
errorMessage(
exportState,
'Expected object but got export from'
)
)
}
},
ExportNamedDeclaration(
exportPath: NodePath<BabelTypes.ExportNamedDeclaration>,
exportState: any
) {
if (
exportState.bundleDropped ||
(!exportPath.node.declaration &&
exportPath.node.specifiers.length === 0)
) {
return
}
const config: PageConfig = {}
const declarations: BabelTypes.VariableDeclarator[] = [
...((
exportPath.node
.declaration as BabelTypes.VariableDeclaration
)?.declarations || []),
exportPath.scope.getBinding(CONFIG_KEY)?.path
.node as BabelTypes.VariableDeclarator,
].filter(Boolean)
for (const specifier of exportPath.node.specifiers) {
if (
(t.isIdentifier(specifier.exported)
? specifier.exported.name
: specifier.exported.value) === CONFIG_KEY
) {
// export {} from 'somewhere'
if (BabelTypes.isStringLiteral(exportPath.node.source)) {
throw new Error(
errorMessage(
exportState,
`Expected object but got import`
)
)
// import hello from 'world'
// export { hello as config }
} else if (
BabelTypes.isIdentifier(
(specifier as BabelTypes.ExportSpecifier).local
)
) {
if (
BabelTypes.isImportSpecifier(
exportPath.scope.getBinding(
(specifier as BabelTypes.ExportSpecifier).local.name
)?.path.node
)
) {
throw new Error(
errorMessage(
exportState,
`Expected object but got import`
)
)
}
}
}
}
for (const declaration of declarations) {
if (
!BabelTypes.isIdentifier(declaration.id, {
name: CONFIG_KEY,
})
) {
continue
}
let { init } = declaration
if (BabelTypes.isTSAsExpression(init)) {
init = init.expression
}
if (!BabelTypes.isObjectExpression(init)) {
const got = init ? init.type : 'undefined'
throw new Error(
errorMessage(
exportState,
`Expected object but got ${got}`
)
)
}
for (const prop of init.properties) {
if (BabelTypes.isSpreadElement(prop)) {
throw new Error(
errorMessage(
exportState,
`Property spread is not allowed`
)
)
}
const { name } = prop.key as BabelTypes.Identifier
if (BabelTypes.isIdentifier(prop.key, { name: 'amp' })) {
if (!BabelTypes.isObjectProperty(prop)) {
throw new Error(
errorMessage(
exportState,
`Invalid property "${name}"`
)
)
}
if (
!BabelTypes.isBooleanLiteral(prop.value) &&
!BabelTypes.isStringLiteral(prop.value)
) {
throw new Error(
errorMessage(
exportState,
`Invalid value for "${name}"`
)
)
}
config.amp = prop.value.value as PageConfig['amp']
}
}
}
if (config.amp === true) {
if (!exportState.file?.opts?.caller.isDev) {
// don't replace bundle in development so HMR can track
// dependencies and trigger reload when they are changed
replaceBundle(exportPath, t)
}
exportState.bundleDropped = true
return
}
},
},
state
)
},
},
} as Visitor<ConfigState>,
}
}