import path from 'path'
import { WEBPACK_LAYERS, type WebpackLayerName } from '../../lib/constants'
import type {
NextConfig,
ExperimentalConfig,
EmotionConfig,
StyledComponentsConfig,
} from '../../server/config-shared'
import type { ResolvedBaseUrl } from '../load-jsconfig'
import { shouldUseReactServerCondition, isWebpackAppPagesLayer } from '../utils'
import { escapeStringRegexp } from '../../shared/lib/escape-regexp'
const nextDirname = path.dirname(require.resolve('next/package.json'))
const nextDistPath = new RegExp(
`${escapeStringRegexp(nextDirname)}[\\/]dist[\\/](shared[\\/]lib|client|pages)`
)
const nodeModulesPath = /[\\/]node_modules[\\/]/
const regeneratorRuntimePath = require.resolve(
'next/dist/compiled/regenerator-runtime'
)
function isTypeScriptFile(filename: string) {
return filename.endsWith('.ts') || filename.endsWith('.tsx')
}
function isCommonJSFile(filename: string) {
return filename.endsWith('.cjs')
}
// Ensure Next.js internals and .cjs files are output as CJS modules,
// By default all modules are output as ESM or will treated as CJS if next-swc/auto-cjs plugin detects file is CJS.
function shouldOutputCommonJs(filename: string) {
return isCommonJSFile(filename) || nextDistPath.test(filename)
}
export function getParserOptions({ filename, jsConfig, ...rest }: any) {
const isTSFile = filename.endsWith('.ts')
const hasTsSyntax = isTypeScriptFile(filename)
const enableDecorators = Boolean(
jsConfig?.compilerOptions?.experimentalDecorators
)
return {
...rest,
syntax: hasTsSyntax ? 'typescript' : 'ecmascript',
dynamicImport: true,
decorators: enableDecorators,
// Exclude regular TypeScript files from React transformation to prevent e.g. generic parameters and angle-bracket type assertion from being interpreted as JSX tags.
[hasTsSyntax ? 'tsx' : 'jsx']: !isTSFile,
importAssertions: true,
}
}
function getBaseSWCOptions({
filename,
jest,
development,
hasReactRefresh,
globalWindow,
esm,
configDir,
modularizeImports,
swcPlugins,
compilerOptions,
resolvedBaseUrl,
jsConfig,
supportedBrowsers,
swcCacheDir,
serverComponents,
serverReferenceHashSalt,
bundleLayer,
isCacheComponents,
cacheHandlers,
useCacheEnabled,
taintEnabled,
trackDynamicImports,
pageExtensions,
}: {
filename: string
jest?: boolean
development: boolean
hasReactRefresh: boolean
globalWindow: boolean
esm: boolean
configDir?: string
modularizeImports?: NextConfig['modularizeImports']
compilerOptions: NextConfig['compiler']
swcPlugins: ExperimentalConfig['swcPlugins']
resolvedBaseUrl?: ResolvedBaseUrl
jsConfig: any
supportedBrowsers: string[] | undefined
swcCacheDir?: string
serverComponents?: boolean
serverReferenceHashSalt: string
bundleLayer?: WebpackLayerName
isCacheComponents?: boolean
cacheHandlers?: NextConfig['cacheHandlers']
useCacheEnabled?: boolean
taintEnabled?: boolean
trackDynamicImports?: boolean
pageExtensions?: string[]
}) {
const isReactServerLayer = shouldUseReactServerCondition(bundleLayer)
const isAppRouterPagesLayer = isWebpackAppPagesLayer(bundleLayer)
const parserConfig = getParserOptions({ filename, jsConfig })
const paths = jsConfig?.compilerOptions?.paths
const enableDecorators = Boolean(
jsConfig?.compilerOptions?.experimentalDecorators
)
const emitDecoratorMetadata = Boolean(
jsConfig?.compilerOptions?.emitDecoratorMetadata
)
const useDefineForClassFields = Boolean(
jsConfig?.compilerOptions?.useDefineForClassFields
)
const plugins = (swcPlugins ?? [])
.filter(Array.isArray)
.map(([name, options]: any) => [
require.resolve(name, configDir ? { paths: [configDir] } : undefined),
options,
])
return {
jsc: {
...(resolvedBaseUrl && paths
? {
baseUrl: resolvedBaseUrl.baseUrl,
paths,
}
: {}),
externalHelpers: !process.versions.pnp && !jest,
parser: parserConfig,
experimental: {
keepImportAttributes: true,
emitAssertForImportAttributes: true,
plugins,
cacheRoot: swcCacheDir,
},
transform: {
// Enables https://github.com/swc-project/swc/blob/0359deb4841be743d73db4536d4a22ac797d7f65/crates/swc_ecma_ext_transforms/src/jest.rs
...(jest
? {
hidden: {
jest: true,
},
}
: {}),
legacyDecorator: enableDecorators,
decoratorMetadata: emitDecoratorMetadata,
useDefineForClassFields: useDefineForClassFields,
react: {
importSource:
jsConfig?.compilerOptions?.jsxImportSource ??
(compilerOptions?.emotion && !isReactServerLayer
? '@emotion/react'
: 'react'),
runtime: 'automatic',
pragmaFrag: 'React.Fragment',
throwIfNamespace: true,
development: !!development,
useBuiltins: true,
refresh: !!hasReactRefresh,
},
optimizer: {
simplify: false,
globals: jest
? undefined
: {
typeofs: {
window: globalWindow ? 'object' : 'undefined',
},
envs: {
NODE_ENV: development ? '"development"' : '"production"',
},
// TODO: handle process.browser to match babel replacing as well
},
},
regenerator: {
importPath: regeneratorRuntimePath,
},
},
},
sourceMaps: jest ? 'inline' : undefined,
removeConsole: compilerOptions?.removeConsole,
// disable "reactRemoveProperties" when "jest" is true
// otherwise the setting from next.config.js will be used
reactRemoveProperties: jest
? false
: compilerOptions?.reactRemoveProperties,
// Map the k-v map to an array of pairs.
modularizeImports: modularizeImports
? Object.fromEntries(
Object.entries(modularizeImports).map(([mod, config]) => [
mod,
{
...config,
transform:
typeof config.transform === 'string'
? config.transform
: Object.entries(config.transform).map(([key, value]) => [
key,
value,
]),
},
])
)
: undefined,
relay: compilerOptions?.relay,
// Always transform styled-jsx and error when `client-only` condition is triggered
styledJsx: compilerOptions?.styledJsx ?? {
useLightningcss: jsConfig?.experimental?.useLightningcss ?? false,
},
// Disable css-in-js libs (without client-only integration) transform on server layer for server components
...(!isReactServerLayer && {
emotion: getEmotionOptions(compilerOptions?.emotion, development),
styledComponents: getStyledComponentsOptions(
compilerOptions?.styledComponents,
development
),
}),
serverComponents:
serverComponents && !jest
? {
isReactServerLayer,
cacheComponentsEnabled: isCacheComponents,
useCacheEnabled,
taintEnabled,
pageExtensions: pageExtensions || [],
}
: undefined,
serverActions:
isAppRouterPagesLayer && !jest
? {
isReactServerLayer,
isDevelopment: development,
useCacheEnabled,
hashSalt: serverReferenceHashSalt,
cacheKinds: ['default', 'remote', 'private'].concat(
cacheHandlers ? Object.keys(cacheHandlers) : []
),
}
: undefined,
// For app router we prefer to bundle ESM,
// On server side of pages router we prefer CJS.
preferEsm: esm,
lintCodemodComments: true,
trackDynamicImports: trackDynamicImports,
debugFunctionName: development,
...(supportedBrowsers && supportedBrowsers.length > 0
? {
cssEnv: {
targets: supportedBrowsers,
},
}
: {}),
}
}
function getStyledComponentsOptions(
styledComponentsConfig: undefined | boolean | StyledComponentsConfig,
development: any
) {
if (!styledComponentsConfig) {
return null
} else if (typeof styledComponentsConfig === 'object') {
return {
...styledComponentsConfig,
displayName: styledComponentsConfig.displayName ?? Boolean(development),
}
} else {
return {
displayName: Boolean(development),
}
}
}
function getEmotionOptions(
emotionConfig: undefined | boolean | EmotionConfig,
development: boolean
) {
if (!emotionConfig) {
return null
}
let autoLabel = !!development
if (typeof emotionConfig === 'object' && emotionConfig.autoLabel) {
switch (emotionConfig.autoLabel) {
case 'never':
autoLabel = false
break
case 'always':
autoLabel = true
break
case 'dev-only':
break
default:
emotionConfig.autoLabel satisfies never
}
}
return {
enabled: true,
autoLabel,
sourcemap: development,
...(typeof emotionConfig === 'object' && {
importMap: emotionConfig.importMap,
labelFormat: emotionConfig.labelFormat,
sourcemap: development && emotionConfig.sourceMap,
}),
}
}
export function getJestSWCOptions({
isServer,
filename,
esm,
modularizeImports,
configDir,
swcPlugins,
compilerOptions,
jsConfig,
resolvedBaseUrl,
pagesDir,
imageConfig,
serverReferenceHashSalt,
}: {
isServer: boolean
filename: string
esm: boolean
configDir?: string
modularizeImports?: NextConfig['modularizeImports']
swcPlugins: ExperimentalConfig['swcPlugins']
compilerOptions: NextConfig['compiler']
jsConfig: any
resolvedBaseUrl?: ResolvedBaseUrl
pagesDir?: string
serverComponents?: boolean
imageConfig?: Partial<NextConfig['images']>
serverReferenceHashSalt: string
}) {
let baseOptions = getBaseSWCOptions({
filename,
jest: true,
development: false,
hasReactRefresh: false,
configDir,
globalWindow: !isServer,
modularizeImports,
swcPlugins,
compilerOptions,
jsConfig,
resolvedBaseUrl,
supportedBrowsers: undefined,
esm,
// Don't apply server layer transformations for Jest
// Disable server / client graph assertions for Jest
bundleLayer: undefined,
serverComponents: false,
serverReferenceHashSalt,
})
// In production, webpack DefinePlugin replaces process.env.__NEXT_IMAGE_OPTS
// with an object literal at compile time. Emulate that here by enabling
// SWC's optimizer globals.envs so the same compile-time replacement happens
// during Jest transforms.
if (imageConfig) {
baseOptions.jsc.transform.optimizer.globals = {
envs: {
...baseOptions.jsc.transform.optimizer.globals?.envs,
__NEXT_IMAGE_OPTS: JSON.stringify(imageConfig),
},
} as any
}
const useCjsModules = shouldOutputCommonJs(filename)
return {
...baseOptions,
env: {
targets: {
// Targets the current version of Node.js
node: process.versions.node,
},
},
module: {
type: esm && !useCjsModules ? 'es6' : 'commonjs',
},
disableNextSsg: true,
pagesDir,
}
}
export function getLoaderSWCOptions({
// This is not passed yet as "paths" resolving is handled by webpack currently.
// resolvedBaseUrl,
filename,
development,
isServer,
pagesDir,
appDir,
isPageFile,
isCacheComponents,
hasReactRefresh,
// The folder containing the next.config.js, used for resolving relative config paths.
configDir,
modularizeImports,
optimizeServerReact,
optimizePackageImports,
swcPlugins,
swcEnvOptions,
compilerOptions,
jsConfig,
supportedBrowsers,
swcCacheDir,
relativeFilePathFromRoot,
serverComponents,
serverReferenceHashSalt,
bundleLayer,
esm,
cacheHandlers,
useCacheEnabled,
taintEnabled,
trackDynamicImports,
pageExtensions,
}: {
filename: string
development: boolean
isServer: boolean
pagesDir?: string
appDir?: string
isPageFile: boolean
hasReactRefresh: boolean
configDir: string
optimizeServerReact?: boolean
modularizeImports: NextConfig['modularizeImports']
isCacheComponents?: boolean
optimizePackageImports?: NonNullable<
NextConfig['experimental']
>['optimizePackageImports']
swcPlugins: ExperimentalConfig['swcPlugins']
swcEnvOptions?: ExperimentalConfig['swcEnvOptions']
compilerOptions: NextConfig['compiler']
jsConfig: any
supportedBrowsers: string[] | undefined
swcCacheDir: string
relativeFilePathFromRoot: string
esm?: boolean
serverComponents?: boolean
serverReferenceHashSalt: string
bundleLayer?: WebpackLayerName
cacheHandlers: NextConfig['cacheHandlers']
useCacheEnabled?: boolean
taintEnabled?: boolean
trackDynamicImports?: boolean
pageExtensions?: string[]
}) {
let baseOptions: any = getBaseSWCOptions({
filename,
development,
globalWindow: !isServer,
hasReactRefresh,
configDir,
modularizeImports,
swcPlugins,
compilerOptions,
jsConfig,
// resolvedBaseUrl,
supportedBrowsers,
swcCacheDir,
bundleLayer,
serverComponents,
serverReferenceHashSalt,
esm: !!esm,
isCacheComponents,
cacheHandlers,
useCacheEnabled,
taintEnabled,
trackDynamicImports,
pageExtensions,
})
baseOptions.fontLoaders = {
fontLoaders: ['next/font/local', 'next/font/google'],
relativeFilePathFromRoot,
}
baseOptions.cjsRequireOptimizer = {
packages: {
'next/server': {
transforms: {
NextRequest: 'next/dist/server/web/spec-extension/request',
NextResponse: 'next/dist/server/web/spec-extension/response',
ImageResponse: 'next/dist/server/web/spec-extension/image-response',
userAgentFromString: 'next/dist/server/web/spec-extension/user-agent',
userAgent: 'next/dist/server/web/spec-extension/user-agent',
},
},
},
}
if (optimizeServerReact && isServer && !development) {
baseOptions.optimizeServerReact = {
optimize_use_state: false,
}
}
// Modularize import optimization for barrel files
if (optimizePackageImports) {
baseOptions.autoModularizeImports = {
packages: optimizePackageImports,
}
}
const isNodeModules = nodeModulesPath.test(filename)
const isAppBrowserLayer = bundleLayer === WEBPACK_LAYERS.appPagesBrowser
const moduleResolutionConfig = shouldOutputCommonJs(filename)
? {
module: {
type: 'commonjs',
},
}
: {}
let options: any
if (isServer) {
options = {
...baseOptions,
...moduleResolutionConfig,
// Disables getStaticProps/getServerSideProps tree shaking on the server compilation for pages
disableNextSsg: true,
isDevelopment: development,
isServerCompiler: isServer,
pagesDir,
appDir,
preferEsm: !!esm,
isPageFile,
env: {
targets: {
// Targets the current version of Node.js
node: process.versions.node,
},
},
}
} else {
options = {
...baseOptions,
...moduleResolutionConfig,
disableNextSsg: !isPageFile,
isDevelopment: development,
isServerCompiler: isServer,
pagesDir,
appDir,
isPageFile,
...(supportedBrowsers && supportedBrowsers.length > 0
? {
env: {
targets: supportedBrowsers,
...swcEnvOptions,
},
}
: {}),
}
if (!options.env) {
// Matches default @babel/preset-env behavior
options.jsc.target = 'es5'
}
}
// For node_modules in app browser layer, we don't need to do any server side transformation.
// Only keep server actions transform to discover server actions from client components.
if (isAppBrowserLayer && isNodeModules) {
options.disableNextSsg = true
options.isPageFile = false
options.optimizeServerReact = undefined
options.cjsRequireOptimizer = undefined
// Disable optimizer for node_modules in app browser layer, to avoid unnecessary replacement.
// e.g. typeof window could result differently in js worker or browser.
if (
options.jsc.transform.optimizer.globals?.typeofs &&
!filename.includes(nextDirname)
) {
delete options.jsc.transform.optimizer.globals.typeofs.window
}
}
return options
}