rsnext/packages/next/build/babel/plugins/next-page-config.ts
Guy Bedford 64850a8348
ncc Babel inlining (#18768)
This adds inlining for Babel and the Babel plugins used in next.

This is based to the PR at https://github.com/vercel/next.js/pull/18823.

The approach is to make one large bundle and then separate out the individual packages from that in order to avoid duplications.

In the first attempt the Babel bundle size was 10MB... using "resolutions" in the Yarn workspace to reduce the duplicated packages this was brought down to a 2.8MB bundle for Babel and all the used plugins which is exactly the expected file size here.

This will thus add a 2.8MB download size to the next package, but save downloading any babel dependencies separately, removing a large number of package dependencies from the overall install.
2020-11-05 14:23:01 +00:00

200 lines
6.9 KiB
TypeScript

import {
NodePath,
PluginObj,
types as BabelTypes,
} from 'next/dist/compiled/babel/core'
import { PageConfig } from 'next/types'
import { STRING_LITERAL_DROP_BUNDLE } from '../../../next-server/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://err.sh/vercel/next.js/invalid-page-config`
}
interface ConfigState {
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: ConfigState) {
path.traverse(
{
ExportDeclaration(exportPath, exportState) {
if (
BabelTypes.isExportNamedDeclaration(exportPath) &&
(exportPath.node as BabelTypes.ExportNamedDeclaration).specifiers?.some(
(specifier) => {
return specifier.exported.name === 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 (specifier.exported.name === 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
}
if (!BabelTypes.isObjectExpression(declaration.init)) {
const got = declaration.init
? declaration.init.type
: 'undefined'
throw new Error(
errorMessage(
exportState,
`Expected object but got ${got}`
)
)
}
for (const prop of declaration.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
)
},
},
},
}
}