Skip to content

Legacy decorators: the generated _ts_decorate(...) calls have no source map mappings #12429

Description

@l7aromeo

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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions