import { NodePath, PluginObj, types as BabelTypes, } from 'next/dist/compiled/babel/core' import { SERVER_PROPS_SSG_CONFLICT } from '../../../lib/constants' import { SERVER_PROPS_ID, STATIC_PROPS_ID, } from '../../../next-server/lib/constants' export const EXPORT_NAME_GET_STATIC_PROPS = 'getStaticProps' export const EXPORT_NAME_GET_STATIC_PATHS = 'getStaticPaths' export const EXPORT_NAME_GET_SERVER_PROPS = 'getServerSideProps' const ssgExports = new Set([ EXPORT_NAME_GET_STATIC_PROPS, EXPORT_NAME_GET_STATIC_PATHS, EXPORT_NAME_GET_SERVER_PROPS, // legacy methods added so build doesn't fail from importing // server-side only methods `unstable_getStaticProps`, `unstable_getStaticPaths`, `unstable_getServerProps`, `unstable_getServerSideProps`, ]) type PluginState = { refs: Set> isPrerender: boolean isServerProps: boolean done: boolean } function decorateSsgExport( t: typeof BabelTypes, path: NodePath, state: PluginState ): void { const gsspName = state.isPrerender ? STATIC_PROPS_ID : SERVER_PROPS_ID const gsspId = t.identifier(gsspName) const addGsspExport = ( exportPath: | NodePath | NodePath ): void => { if (state.done) { return } state.done = true const [pageCompPath] = exportPath.replaceWithMultiple([ t.exportNamedDeclaration( t.variableDeclaration( // We use 'var' instead of 'let' or 'const' for ES5 support. Since // this runs in `Program#exit`, no ES2015 transforms (preset env) // will be ran against this code. 'var', [t.variableDeclarator(gsspId, t.booleanLiteral(true))] ), [t.exportSpecifier(gsspId, gsspId)] ), exportPath.node, ]) exportPath.scope.registerDeclaration( pageCompPath as NodePath ) } path.traverse({ ExportDefaultDeclaration(exportDefaultPath) { addGsspExport(exportDefaultPath) }, ExportNamedDeclaration(exportNamedPath) { addGsspExport(exportNamedPath) }, }) } const isDataIdentifier = (name: string, state: PluginState): boolean => { if (ssgExports.has(name)) { if (name === EXPORT_NAME_GET_SERVER_PROPS) { if (state.isPrerender) { throw new Error(SERVER_PROPS_SSG_CONFLICT) } state.isServerProps = true } else { if (state.isServerProps) { throw new Error(SERVER_PROPS_SSG_CONFLICT) } state.isPrerender = true } return true } return false } export default function nextTransformSsg({ types: t, }: { types: typeof BabelTypes }): PluginObj { function getIdentifier( path: | NodePath | NodePath | NodePath ): NodePath | null { const parentPath = path.parentPath if (parentPath.type === 'VariableDeclarator') { const pp = parentPath as NodePath const name = pp.get('id') return name.node.type === 'Identifier' ? (name as NodePath) : null } if (parentPath.type === 'AssignmentExpression') { const pp = parentPath as NodePath const name = pp.get('left') return name.node.type === 'Identifier' ? (name as NodePath) : null } if (path.node.type === 'ArrowFunctionExpression') { return null } return path.node.id && path.node.id.type === 'Identifier' ? (path.get('id') as NodePath) : null } function isIdentifierReferenced( ident: NodePath ): boolean { const b = ident.scope.getBinding(ident.node.name) if (b?.referenced) { // Functions can reference themselves, so we need to check if there's a // binding outside the function scope or not. if (b.path.type === 'FunctionDeclaration') { return !b.constantViolations .concat(b.referencePaths) // Check that every reference is contained within the function: .every((ref) => ref.findParent((p) => p === b.path)) } return true } return false } function markFunction( path: | NodePath | NodePath | NodePath, state: PluginState ): void { const ident = getIdentifier(path) if (ident?.node && isIdentifierReferenced(ident)) { state.refs.add(ident) } } function markImport( path: | NodePath | NodePath | NodePath, state: PluginState ): void { const local = path.get('local') as NodePath if (isIdentifierReferenced(local)) { state.refs.add(local) } } return { visitor: { Program: { enter(path, state) { state.refs = new Set>() state.isPrerender = false state.isServerProps = false state.done = false path.traverse( { VariableDeclarator(variablePath, variableState) { if (variablePath.node.id.type === 'Identifier') { const local = variablePath.get('id') as NodePath< BabelTypes.Identifier > if (isIdentifierReferenced(local)) { variableState.refs.add(local) } } else if (variablePath.node.id.type === 'ObjectPattern') { const pattern = variablePath.get('id') as NodePath< BabelTypes.ObjectPattern > const properties = pattern.get('properties') properties.forEach((p) => { const local = p.get( p.node.type === 'ObjectProperty' ? 'value' : p.node.type === 'RestElement' ? 'argument' : (function () { throw new Error('invariant') })() ) as NodePath if (isIdentifierReferenced(local)) { variableState.refs.add(local) } }) } else if (variablePath.node.id.type === 'ArrayPattern') { const pattern = variablePath.get('id') as NodePath< BabelTypes.ArrayPattern > const elements = pattern.get('elements') elements.forEach((e) => { let local: NodePath if (e.node?.type === 'Identifier') { local = e as NodePath } else if (e.node?.type === 'RestElement') { local = e.get('argument') as NodePath< BabelTypes.Identifier > } else { return } if (isIdentifierReferenced(local)) { variableState.refs.add(local) } }) } }, FunctionDeclaration: markFunction, FunctionExpression: markFunction, ArrowFunctionExpression: markFunction, ImportSpecifier: markImport, ImportDefaultSpecifier: markImport, ImportNamespaceSpecifier: markImport, ExportNamedDeclaration(exportNamedPath, exportNamedState) { const specifiers = exportNamedPath.get('specifiers') if (specifiers.length) { specifiers.forEach((s) => { if ( isDataIdentifier( t.isIdentifier(s.node.exported) ? s.node.exported.name : s.node.exported.value, exportNamedState ) ) { s.remove() } }) if (exportNamedPath.node.specifiers.length < 1) { exportNamedPath.remove() } return } const decl = exportNamedPath.get('declaration') as NodePath< | BabelTypes.FunctionDeclaration | BabelTypes.VariableDeclaration > if (decl == null || decl.node == null) { return } switch (decl.node.type) { case 'FunctionDeclaration': { const name = decl.node.id!.name if (isDataIdentifier(name, exportNamedState)) { exportNamedPath.remove() } break } case 'VariableDeclaration': { const inner = decl.get('declarations') as NodePath< BabelTypes.VariableDeclarator >[] inner.forEach((d) => { if (d.node.id.type !== 'Identifier') { return } const name = d.node.id.name if (isDataIdentifier(name, exportNamedState)) { d.remove() } }) break } default: { break } } }, }, state ) if (!state.isPrerender && !state.isServerProps) { return } const refs = state.refs let count: number function sweepFunction( sweepPath: | NodePath | NodePath | NodePath ): void { const ident = getIdentifier(sweepPath) if ( ident?.node && refs.has(ident) && !isIdentifierReferenced(ident) ) { ++count if ( t.isAssignmentExpression(sweepPath.parentPath) || t.isVariableDeclarator(sweepPath.parentPath) ) { sweepPath.parentPath.remove() } else { sweepPath.remove() } } } function sweepImport( sweepPath: | NodePath | NodePath | NodePath ): void { const local = sweepPath.get('local') as NodePath< BabelTypes.Identifier > if (refs.has(local) && !isIdentifierReferenced(local)) { ++count sweepPath.remove() if ( (sweepPath.parent as BabelTypes.ImportDeclaration).specifiers .length === 0 ) { sweepPath.parentPath.remove() } } } do { ;(path.scope as any).crawl() count = 0 path.traverse({ // eslint-disable-next-line no-loop-func VariableDeclarator(variablePath) { if (variablePath.node.id.type === 'Identifier') { const local = variablePath.get('id') as NodePath< BabelTypes.Identifier > if (refs.has(local) && !isIdentifierReferenced(local)) { ++count variablePath.remove() } } else if (variablePath.node.id.type === 'ObjectPattern') { const pattern = variablePath.get('id') as NodePath< BabelTypes.ObjectPattern > const beforeCount = count const properties = pattern.get('properties') properties.forEach((p) => { const local = p.get( p.node.type === 'ObjectProperty' ? 'value' : p.node.type === 'RestElement' ? 'argument' : (function () { throw new Error('invariant') })() ) as NodePath if (refs.has(local) && !isIdentifierReferenced(local)) { ++count p.remove() } }) if ( beforeCount !== count && pattern.get('properties').length < 1 ) { variablePath.remove() } } else if (variablePath.node.id.type === 'ArrayPattern') { const pattern = variablePath.get('id') as NodePath< BabelTypes.ArrayPattern > const beforeCount = count const elements = pattern.get('elements') elements.forEach((e) => { let local: NodePath if (e.node?.type === 'Identifier') { local = e as NodePath } else if (e.node?.type === 'RestElement') { local = e.get('argument') as NodePath< BabelTypes.Identifier > } else { return } if (refs.has(local) && !isIdentifierReferenced(local)) { ++count e.remove() } }) if ( beforeCount !== count && pattern.get('elements').length < 1 ) { variablePath.remove() } } }, FunctionDeclaration: sweepFunction, FunctionExpression: sweepFunction, ArrowFunctionExpression: sweepFunction, ImportSpecifier: sweepImport, ImportDefaultSpecifier: sweepImport, ImportNamespaceSpecifier: sweepImport, }) } while (count) decorateSsgExport(t, path, state) }, }, }, } }