Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 18 additions & 5 deletions packages/next/src/build/swc/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,14 +39,19 @@ function shouldOutputCommonJs(filename: string) {
export function getParserOptions({ filename, jsConfig, ...rest }: any) {
const isTSFile = filename.endsWith('.ts')
const hasTsSyntax = isTypeScriptFile(filename)
const enableDecorators = Boolean(
const enableLegacyDecorators = Boolean(
jsConfig?.compilerOptions?.experimentalDecorators
)
return {
...rest,
syntax: hasTsSyntax ? 'typescript' : 'ecmascript',
dynamicImport: true,
decorators: enableDecorators,
// Always enable decorator parsing for TypeScript files so that both
// legacy (@experimentalDecorators) and stage-3/TS5 decorators are parsed.
// Without this, SWC rejects @decorator syntax at the parser level before
// any transform runs — making TS5 decorators impossible even when the
// transform is correctly configured (fixes #48360).
decorators: hasTsSyntax || enableLegacyDecorators,
// 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,
Expand Down Expand Up @@ -106,7 +111,7 @@ function getBaseSWCOptions({
const isAppRouterPagesLayer = isWebpackAppPagesLayer(bundleLayer)
const parserConfig = getParserOptions({ filename, jsConfig })
const paths = jsConfig?.compilerOptions?.paths
const enableDecorators = Boolean(
const enableLegacyDecorators = Boolean(
jsConfig?.compilerOptions?.experimentalDecorators
)
const emitDecoratorMetadata = Boolean(
Expand Down Expand Up @@ -147,8 +152,16 @@ function getBaseSWCOptions({
},
}
: {}),
legacyDecorator: enableDecorators,
decoratorMetadata: emitDecoratorMetadata,
// When experimentalDecorators is set, use the legacy (stage 1) transform.
// Otherwise, use the stage-3/TS5 Ecma decorator proposal (decoratorVersion "2022-03").
// This matches Turbopack's behavior in crates/next-core/src/transform_options.rs
// which selects DecoratorsKind::Legacy vs DecoratorsKind::Ecma (fixes #48360).
...(enableLegacyDecorators
? { legacyDecorator: true }
: { decoratorVersion: '2022-03' as const }),
decoratorMetadata: enableLegacyDecorators
? emitDecoratorMetadata
: false,
useDefineForClassFields: useDefineForClassFields,
react: {
importSource:
Expand Down
13 changes: 8 additions & 5 deletions packages/next/src/build/webpack-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -543,12 +543,15 @@ export default async function getBaseWebpackConfig(
pnp: Boolean(process.versions.pnp),
optimizeServerReact: Boolean(config.experimental.optimizeServerReact),
modularizeImports: config.modularizeImports,
decorators: Boolean(
jsConfig?.compilerOptions?.experimentalDecorators
),
// Always parse decorators in TypeScript; the transform layer handles
// legacy vs stage-3 selection. Without this, TS5 decorators fail at
// parse time (fixes #48360).
decorators: true,
emitDecoratorMetadata: Boolean(
jsConfig?.compilerOptions?.emitDecoratorMetadata
),
jsConfig?.compilerOptions?.experimentalDecorators
)
? Boolean(jsConfig?.compilerOptions?.emitDecoratorMetadata)
: false,
regeneratorRuntimePath: require.resolve(
'next/dist/compiled/regenerator-runtime'
),
Expand Down
22 changes: 22 additions & 0 deletions test/production/ts5-decorators/app/pages/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// Stage-3/TS5 decorator — no experimentalDecorators flag required
function logged<T extends (...args: any[]) => any>(
target: T,
context: ClassMethodDecoratorContext
) {
return function (this: any, ...args: Parameters<T>): ReturnType<T> {
console.log(`Calling ${String(context.name)}`)
return target.apply(this, args)
}
}

class Greeter {
@logged
greet(name: string) {
return `Hello, ${name}!`
}
}

export default function Home() {
const g = new Greeter()
return <p id="result">{g.greet('world')}</p>
}
14 changes: 14 additions & 0 deletions test/production/ts5-decorators/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { join } from 'path'
import { nextTestSetup } from 'e2e-utils'

describe('TypeScript 5 stage-3 decorators', () => {
const { next } = nextTestSetup({
files: join(__dirname, 'app'),
})

it('should build and render a page using TS5 decorators', async () => {
const browser = await next.browser('/')
const text = await browser.elementByCss('#result').text()
expect(text).toBe('Hello, world!')
})
})