Describe the bug
With jsc.transform.legacyDecorator, the statements SWC generates to apply decorators have no mappings in the source map. That covers the _ts_decorate([ line, the ], Target.prototype, "member", null) line and, in this repro, the decorator expression inside them.
When a decorator throws, the stack frame for that call can't be mapped back to the source:
- Node with
--enable-source-maps falls back to the nearest earlier mapping, so the frame points at the wrong line (here the class's closing }, line 12).
- Tools that look up only the frame's own line leave it at its position in the output.
tsc maps each part: __decorate([ to the decorated member, each decorator expression to its own position, and the closing line to the member.
Input code
function Log(label: string): MethodDecorator {
return () => {
throw new Error(`bad label ${label}`)
}
}
class Greeter {
greet() {}
@Log('first')
one() {}
}
Config
{
"jsc": {
"parser": { "syntax": "typescript", "decorators": true },
"transform": { "legacyDecorator": true },
"target": "es2022"
},
"sourceMaps": true
}
Link to the code that reproduces this issue
https://play.swc.rs/?version=1.16.2&code=H4sIAAAAAAAAEyWOSwrDMBBD93MKLQqxrxBo6aKlm%2FYOcZLJB4IHxhOyCL57sbOSkNBD0x4HWyXiK7PbQs9bi2S6xtm3%2BLEtMr54EA0mipMAZds1wnncHzUAbFE5EPnAW1XUdX0YUVm4nVVz5wnIlImGLaSEjzIbX8S5eOdxZiLgWY4006rJmjKSyFeX6Q%2FZuJPWrAAAAA%3D%3D&config=H4sIAAAAAAAAE22NuwoCQQxF%2B%2FmKS2oLmXJqWz8ixLgoujMkWXBY9t%2FFxy4WlpfDuWdOAF1dqGBOAECNzdW2DZD3MfhBBRS9qYtdWtBupSeVahzVnArCJn2D5cMpjEc%2FV7v%2FHt50YOmHVfzrsQ0ar6Z63udM6cvI62SiR25bb3kCI8StFMUAAAA%3D
A script that compares the mappings SWC and tsc emit for the same file:
// npm i @swc/core@1.16.2 typescript@6.0.3 @jridgewell/trace-mapping
import { readFileSync } from 'node:fs'
import { transformSync } from '@swc/core'
import ts from 'typescript'
import { TraceMap, eachMapping } from '@jridgewell/trace-mapping'
const source = readFileSync('input.ts', 'utf8')
const swc = transformSync(source, {
filename: 'input.ts',
sourceMaps: true,
jsc: {
parser: { syntax: 'typescript', decorators: true },
transform: { legacyDecorator: true },
target: 'es2022',
},
})
const tsc = ts.transpileModule(source, {
fileName: 'input.ts',
compilerOptions: { experimentalDecorators: true, sourceMap: true, target: ts.ScriptTarget.ES2022 },
})
function show(name, code, map) {
const byLine = new Map()
eachMapping(new TraceMap(map), m => {
if (!byLine.has(m.generatedLine)) byLine.set(m.generatedLine, [])
byLine.get(m.generatedLine).push(`${m.generatedColumn}->${m.originalLine}:${m.originalColumn}`)
})
console.log(`--- ${name}`)
code.split('\n').forEach((text, i) => {
if (!/decorate|Log\(|\], Greeter/.test(text)) return
console.log(`${String(i + 1).padStart(3)} ${(byLine.get(i + 1)?.join(' ') ?? '(no mapping)').padEnd(40)} ${text.trim()}`)
})
}
show('swc', swc.code, swc.map)
show('tsc', tsc.outputText, tsc.sourceMapText)
Output (generated column -> original line:column):
--- swc
23 (no mapping) _ts_decorate([
24 (no mapping) Log('first')
25 (no mapping) ], Greeter.prototype, "one", null);
--- tsc
17 0->11:2 __decorate([
18 4->10:3 7->10:6 8->10:7 15->10:14 16->10:15 Log('first')
19 34->11:10 ], Greeter.prototype, "one", null);
(Lines for the helper functions are left out; neither compiler maps its helper, which is expected.)
Expected behavior
The generated decorator application maps back to the source, as tsc's does: the _ts_decorate( call to the decorated member (or its decorators), and each decorator expression to its own position. With the output from both compilers written next to its map and run with node --enable-source-maps, the frame of the decorator call should point at the decorated member:
tsc: at <anonymous> (input.ts:11:3)
Actual behavior
The same file compiled by SWC and run with node --enable-source-maps:
Error: bad label first
at <anonymous> (input.ts:3:11)
at _ts_decorate (swc.mjs:8:45)
at <anonymous> (input.ts:12:1)
input.ts:12:1 is the closing } of the class, the last mapping before the unmapped _ts_decorate([ line. In a file with more code between the class and the decorator calls, or in a bundle, the frame can land on an unrelated line or even an unrelated file.
The same happens with class and parameter decorators and with decoratorMetadata: true. For @C class Greeter { @M greet(@P name: string) {} }, none of the generated _ts_decorate([, M,, _ts_param(0, P),, _ts_metadata(...), ], Greeter.prototype, "greet", null);, Greeter = _ts_decorate([, C or ], Greeter); lines has a mapping.
Version
1.16.2
Additional context
- @swc/core 1.16.2 (darwin-arm64), Node.js 26.4.0; comparison against TypeScript 6.0.3.
- A test runner that transforms with SWC reports a failing decorator's code frame on the wrong line for the same reason.
Describe the bug
With
jsc.transform.legacyDecorator, the statements SWC generates to apply decorators have no mappings in the source map. That covers the_ts_decorate([line, the], Target.prototype, "member", null)line and, in this repro, the decorator expression inside them.When a decorator throws, the stack frame for that call can't be mapped back to the source:
--enable-source-mapsfalls back to the nearest earlier mapping, so the frame points at the wrong line (here the class's closing}, line 12).tscmaps each part:__decorate([to the decorated member, each decorator expression to its own position, and the closing line to the member.Input code
Config
{ "jsc": { "parser": { "syntax": "typescript", "decorators": true }, "transform": { "legacyDecorator": true }, "target": "es2022" }, "sourceMaps": true }Link to the code that reproduces this issue
https://play.swc.rs/?version=1.16.2&code=H4sIAAAAAAAAEyWOSwrDMBBD93MKLQqxrxBo6aKlm%2FYOcZLJB4IHxhOyCL57sbOSkNBD0x4HWyXiK7PbQs9bi2S6xtm3%2BLEtMr54EA0mipMAZds1wnncHzUAbFE5EPnAW1XUdX0YUVm4nVVz5wnIlImGLaSEjzIbX8S5eOdxZiLgWY4006rJmjKSyFeX6Q%2FZuJPWrAAAAA%3D%3D&config=H4sIAAAAAAAAE22NuwoCQQxF%2B%2FmKS2oLmXJqWz8ixLgoujMkWXBY9t%2FFxy4WlpfDuWdOAF1dqGBOAECNzdW2DZD3MfhBBRS9qYtdWtBupSeVahzVnArCJn2D5cMpjEc%2FV7v%2FHt50YOmHVfzrsQ0ar6Z63udM6cvI62SiR25bb3kCI8StFMUAAAA%3D
A script that compares the mappings SWC and
tscemit for the same file:Output (generated column -> original line:column):
(Lines for the helper functions are left out; neither compiler maps its helper, which is expected.)
Expected behavior
The generated decorator application maps back to the source, as
tsc's does: the_ts_decorate(call to the decorated member (or its decorators), and each decorator expression to its own position. With the output from both compilers written next to its map and run withnode --enable-source-maps, the frame of the decorator call should point at the decorated member:Actual behavior
The same file compiled by SWC and run with
node --enable-source-maps:input.ts:12:1is the closing}of the class, the last mapping before the unmapped_ts_decorate([line. In a file with more code between the class and the decorator calls, or in a bundle, the frame can land on an unrelated line or even an unrelated file.The same happens with class and parameter decorators and with
decoratorMetadata: true. For@C class Greeter { @M greet(@P name: string) {} }, none of the generated_ts_decorate([,M,,_ts_param(0, P),,_ts_metadata(...),], Greeter.prototype, "greet", null);,Greeter = _ts_decorate([,Cor], Greeter);lines has a mapping.Version
1.16.2
Additional context