From de4be13ebf4ac97d6f23f0ec41d362a7eba76915 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 24 Sep 2026 15:43:48 -0400 Subject: [PATCH 1/9] fix: prevent out-of-order mappings with decorators --- .changeset/two-adults-rest.md | 5 +++ src/languages/ts/index.js | 37 ++++++++++++++----- .../ts-decorators-class/expected.ts.map | 2 +- .../ts-decorators-expression/expected.ts.map | 2 +- test/samples/ts-decorators/expected.ts.map | 2 +- 5 files changed, 36 insertions(+), 12 deletions(-) create mode 100644 .changeset/two-adults-rest.md diff --git a/.changeset/two-adults-rest.md b/.changeset/two-adults-rest.md new file mode 100644 index 00000000..61260cba --- /dev/null +++ b/.changeset/two-adults-rest.md @@ -0,0 +1,5 @@ +--- +'esrap': patch +--- + +fix: prevent out-of-order mappings with decorators diff --git a/src/languages/ts/index.js b/src/languages/ts/index.js index 68de647c..c80bf357 100644 --- a/src/languages/ts/index.js +++ b/src/languages/ts/index.js @@ -632,6 +632,7 @@ export default (options = {}) => { if (node.decorators) { for (const decorator of node.decorators) { context.visit(decorator); + context.newline(); } } @@ -734,6 +735,7 @@ export default (options = {}) => { if (node.decorators) { for (const decorator of node.decorators) { context.visit(decorator); + context.newline(); } } @@ -806,6 +808,7 @@ export default (options = {}) => { if (node.decorators) { for (const decorator of node.decorators) { context.visit(decorator); + context.newline(); } } @@ -966,8 +969,9 @@ export default (options = {}) => { node.type in EXPRESSIONS_PRECEDENCE && !BINDINGS.has(node) ); - const start = printed_start(node); - context.location(start.line, start.column); + if (!has_preceding_decorator(node)) { + context.location(node.loc.start.line, node.loc.start.column); + } } visit(node); @@ -1127,7 +1131,6 @@ export default (options = {}) => { Decorator(node, context) { write_decorator(context, node); - context.newline(); }, DoWhileStatement(node, context) { @@ -1177,18 +1180,24 @@ export default (options = {}) => { if (decl.decorators && decl.decorators.length > 0) { for (const decorator of decl.decorators) { context.visit(decorator); + context.newline(); } token(context, 'export', node); context.write(' '); // Temporarily remove decorators so ClassDeclaration doesn't print them again const savedDecorators = decl.decorators; + const loc = decl.loc; decl.decorators = []; - context.visit(node.declaration); + decl.loc = null; + context.visit(decl); decl.decorators = savedDecorators; + decl.loc = loc; + + if (loc) context.location(loc.end.line, loc.end.column); } else { token(context, 'export', node); context.write(' '); - context.visit(node.declaration); + context.visit(decl); } return; } @@ -2726,10 +2735,20 @@ function handle_var_declarator(node, context, no_in) { * decorators (Acorn, typescript-estree), but they are printed first. * @param {TSESTree.Node} node */ -function printed_start(node) { - const start = /** @type {TSESTree.SourceLocation} */ (node.loc).start; - const first = 'decorators' in node ? node.decorators?.[0]?.loc?.start : undefined; - return first && before(first, start) ? first : start; +function has_preceding_decorator(node) { + const node_start = node.loc.start; + let start = node_start; + let n = node; + + if (node.type === 'ExportNamedDeclaration' && node.declaration) { + n = node.declaration; + } + + if ('decorators' in n) { + start = n.decorators?.[0]?.loc?.start ?? node_start; + } + + return before(start, node_start); } /** diff --git a/test/samples/ts-decorators-class/expected.ts.map b/test/samples/ts-decorators-class/expected.ts.map index 23709b48..7d8f96ed 100644 --- a/test/samples/ts-decorators-class/expected.ts.map +++ b/test/samples/ts-decorators-class/expected.ts.map @@ -7,5 +7,5 @@ "sourcesContent": [ "export class Entity {\n\tconstructor(\n\t\tprivate readonly name: string,\n\t\tprivate readonly info: boolean\n\t) {}\n}\n\n@Entity('users', { info: true })\nexport class User {}\n\n@Entity('categories', { info: false })\nclass Category {}\n\n@Entity('tasks', { info: true })\nexport class Task {}\n\nconst u = new User();\nconst c = new Category();\nconst t = new Task();\n" ], - "mappings": "AAAA,MAAM,CAAC,MAAM,MAAM,CAAC,CAAC;CACpB,WAAW;EACV,iBAAiB,IAAY,AAAR,EAAE,MAAM;EAC7B,iBAAiB,IAAa,AAAT,EAAE,OAAO;GAC7B,CAAC,CAAC;AACL,CAAC;;AAGD,AADA,CAAC,MAAM,CAAC,OAAO,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC;AAAA,MAC1B,CADN,MACa,IAAI,CAAC,CAAC,CAAC;;AAEpB,CAAC,MAAM,CAAC,YAAY,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC;AAAA,MAChC,QAAQ,CAAC,CAAC,CAAC;;AAGjB,AADA,CAAC,MAAM,CAAC,OAAO,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC;AAAA,MAC1B,CADN,MACa,IAAI,CAAC,CAAC,CAAC;;AAEpB,KAAK,CAAC,CAAC,GAAG,GAAG,CAAC,IAAI,EAAE,CAAC;AACrB,KAAK,CAAC,CAAC,GAAG,GAAG,CAAC,QAAQ,EAAE,CAAC;AACzB,KAAK,CAAC,CAAC,GAAG,GAAG,CAAC,IAAI,EAAE,CAAC,AACrB" + "mappings": "AAAA,MAAM,CAAC,MAAM,MAAM,CAAC,CAAC;CACpB,WAAW;EACV,iBAAiB,IAAY,AAAR,EAAE,MAAM;EAC7B,iBAAiB,IAAa,AAAT,EAAE,OAAO;GAC7B,CAAC,CAAC;AACL,CAAC;;AAED,CAAC,MAAM,CAAC,OAAO,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC;MAC1B,OAAO,IAAI,CAAC,CAAC,CAAC;;AAEpB,CAAC,MAAM,CAAC,YAAY,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC;MAChC,QAAQ,CAAC,CAAC,CAAC;;AAEjB,CAAC,MAAM,CAAC,OAAO,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC;MAC1B,OAAO,IAAI,CAAC,CAAC,CAAC;;AAEpB,KAAK,CAAC,CAAC,GAAG,GAAG,CAAC,IAAI,EAAE,CAAC;AACrB,KAAK,CAAC,CAAC,GAAG,GAAG,CAAC,QAAQ,EAAE,CAAC;AACzB,KAAK,CAAC,CAAC,GAAG,GAAG,CAAC,IAAI,EAAE,CAAC,AACrB" } \ No newline at end of file diff --git a/test/samples/ts-decorators-expression/expected.ts.map b/test/samples/ts-decorators-expression/expected.ts.map index 2da8b99c..691ff0b1 100644 --- a/test/samples/ts-decorators-expression/expected.ts.map +++ b/test/samples/ts-decorators-expression/expected.ts.map @@ -7,5 +7,5 @@ "sourcesContent": [ "class Fields {\n\tstatic uuid() {\n\t\treturn function (target: any, propertyKey: string | symbol) {\n\t\t\tObject.defineProperty(target, propertyKey, { value: 'random uuid...', writable: true });\n\t\t};\n\t}\n}\n\nclass User {\n\t@Fields.uuid()\n\tid: string;\n}\n\n@(a ? b : c)\nclass Conditional {}\n\n@(a || b)\nclass Logical {}\n\n@dec\nclass Identifier {}\n\n@a.b.c\nclass Member {}\n\n@a()\nclass Call {}\n\nconst u = new User();\nconsole.log(u.id); // will print \"random uuid...\"\n" ], - "mappings": "AAAA,MAAM,MAAM,CAAC,CAAC;CACb,OAAO,IAAI,GAAG,CAAC;EACd,MAAM,CAAC,UAAU,MAAW,AAAL,EAAE,GAAG,EAAE,WAA4B,AAAjB,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;GAC5D,MAAM,CAAC,cAAc,CAAC,MAAM,EAAE,WAAW,EAAE,EAAE,KAAK,EAAE,gBAAgB,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC;EACzF,CAAC,CAAC;CACH,CAAC;AACF,CAAC;;AAED,MAAM,IAAI,CAAC,CAAC;CACX,CAAC,MAAM,CAAC,IAAI,EAAE;CAAA,AACd,EAAE,EAAE,MAAM,CAAC;AACZ,CAAC;;AAED,CAAE,CAAA,CAAC,GAAG,CAAC,GAAG,CAAC,CAAA;AAAC,MACN,WAAW,CAAC,CAAC,CAAC;;AAEpB,CAAE,CAAA,CAAC,IAAI,CAAC,CAAA;AAAC,MACH,OAAO,CAAC,CAAC,CAAC;;AAEhB,CAAC,GAAG;AAAA,MACE,UAAU,CAAC,CAAC,CAAC;;AAEnB,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AAAA,MACA,MAAM,CAAC,CAAC,CAAC;;AAEf,CAAC,CAAC,EAAE;AAAA,MACE,IAAI,CAAC,CAAC,CAAC;;AAEb,KAAK,CAAC,CAAC,GAAG,GAAG,CAAC,IAAI,EAAE,CAAC;;AACrB,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,+BAClB" + "mappings": "AAAA,MAAM,MAAM,CAAC,CAAC;CACb,OAAO,IAAI,GAAG,CAAC;EACd,MAAM,CAAC,UAAU,MAAW,AAAL,EAAE,GAAG,EAAE,WAA4B,AAAjB,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;GAC5D,MAAM,CAAC,cAAc,CAAC,MAAM,EAAE,WAAW,EAAE,EAAE,KAAK,EAAE,gBAAgB,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC;EACzF,CAAC,CAAC;CACH,CAAC;AACF,CAAC;;AAED,MAAM,IAAI,CAAC,CAAC;CACX,CAAC,MAAM,CAAC,IAAI,EAAE;CACd,EAAE,EAAE,MAAM,CAAC;AACZ,CAAC;;AAED,CAAE,CAAA,CAAC,GAAG,CAAC,GAAG,CAAC,CAAA,AAAC;MACN,WAAW,CAAC,CAAC,CAAC;;AAEpB,CAAE,CAAA,CAAC,IAAI,CAAC,CAAA,AAAC;MACH,OAAO,CAAC,CAAC,CAAC;;AAEhB,CAAC,GAAG;MACE,UAAU,CAAC,CAAC,CAAC;;AAEnB,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;MACA,MAAM,CAAC,CAAC,CAAC;;AAEf,CAAC,CAAC,EAAE;MACE,IAAI,CAAC,CAAC,CAAC;;AAEb,KAAK,CAAC,CAAC,GAAG,GAAG,CAAC,IAAI,EAAE,CAAC;;AACrB,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,+BAClB" } \ No newline at end of file diff --git a/test/samples/ts-decorators/expected.ts.map b/test/samples/ts-decorators/expected.ts.map index 9c54876c..b285c7e0 100644 --- a/test/samples/ts-decorators/expected.ts.map +++ b/test/samples/ts-decorators/expected.ts.map @@ -7,5 +7,5 @@ "sourcesContent": [ "function loggedMethod(headMessage = 'LOG:') {\n\treturn function actualDecorator(originalMethod: any, context: ClassMethodDecoratorContext) {\n\t\tconst methodName = String(context.name);\n\n\t\tfunction replacementMethod(this: any, ...args: any[]) {\n\t\t\tconsole.log(`${headMessage} Entering method '${methodName}'.`);\n\t\t\tconst result = originalMethod.call(this, ...args);\n\t\t\tconsole.log(`${headMessage} Exiting method '${methodName}'.`);\n\t\t\treturn result;\n\t\t}\n\n\t\treturn replacementMethod;\n\t};\n}\n\nclass Person {\n\tname: string;\n\tconstructor(name: string) {\n\t\tthis.name = name;\n\t}\n\n\t@loggedMethod('⚠️')\n\tgreet() {\n\t\tconsole.log(`Hello, my name is ${this.name}.`);\n\t}\n}\n\nconst p = new Person('Ron');\np.greet();\n" ], - "mappings": "AAAA,SAAS,YAAY,CAAC,WAAW,GAAG,MAAM,EAAE,CAAC;CAC5C,MAAM,CAAC,SAAS,eAAe,CAAC,cAAmB,AAAL,EAAE,GAAG,EAAE,OAAoC,AAA7B,EAAE,2BAA2B,EAAE,CAAC;EAC3F,KAAK,CAAC,UAAU,GAAG,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;;EAExC,SAAS,iBAAiB,CAAC,IAAS,AAAL,EAAE,GAAG,EAAE,GAAG,IAAI,EAAE,GAAG,EAAE,EAAE,CAAC;GACtD,OAAO,CAAC,GAAG,CAAC,GAAG,WAAW,qBAAqB,UAAU,IAAI,CAAC,CAAC;;GAC/D,KAAK,CAAC,MAAM,GAAG,cAAc,CAAC,IAAI,CAAC,IAAI,EAAE,GAAG,IAAI,CAAC,CAAC;;GAClD,OAAO,CAAC,GAAG,CAAC,GAAG,WAAW,oBAAoB,UAAU,IAAI,CAAC,CAAC;;GAC9D,MAAM,CAAC,MAAM,CAAC;EACf,CAAC;;EAED,MAAM,CAAC,iBAAiB,CAAC;CAC1B,CAAC,CAAC;AACH,CAAC;;AAED,MAAM,MAAM,CAAC,CAAC;CACb,IAAI,EAAE,MAAM,CAAC;;CACb,WAAW,CAAC,IAAY,AAAR,EAAE,MAAM,EAAE,CAAC;EAC1B,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;CAClB,CAAC;;CAED,CAAC,YAAY,CAAC,IAAI,CAAC;CAAA,AACnB,KAAK,GAAG,CAAC;EACR,OAAO,CAAC,GAAG,CAAC,qBAAqB,IAAI,CAAC,IAAI,GAAG,CAAC,CAAC;CAChD,CAAC;AACF,CAAC;;AAED,KAAK,CAAC,CAAC,GAAG,GAAG,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;;AAC5B,CAAC,CAAC,KAAK,EAAE,CAAC,AACV" + "mappings": "AAAA,SAAS,YAAY,CAAC,WAAW,GAAG,MAAM,EAAE,CAAC;CAC5C,MAAM,CAAC,SAAS,eAAe,CAAC,cAAmB,AAAL,EAAE,GAAG,EAAE,OAAoC,AAA7B,EAAE,2BAA2B,EAAE,CAAC;EAC3F,KAAK,CAAC,UAAU,GAAG,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;;EAExC,SAAS,iBAAiB,CAAC,IAAS,AAAL,EAAE,GAAG,EAAE,GAAG,IAAI,EAAE,GAAG,EAAE,EAAE,CAAC;GACtD,OAAO,CAAC,GAAG,CAAC,GAAG,WAAW,qBAAqB,UAAU,IAAI,CAAC,CAAC;;GAC/D,KAAK,CAAC,MAAM,GAAG,cAAc,CAAC,IAAI,CAAC,IAAI,EAAE,GAAG,IAAI,CAAC,CAAC;;GAClD,OAAO,CAAC,GAAG,CAAC,GAAG,WAAW,oBAAoB,UAAU,IAAI,CAAC,CAAC;;GAC9D,MAAM,CAAC,MAAM,CAAC;EACf,CAAC;;EAED,MAAM,CAAC,iBAAiB,CAAC;CAC1B,CAAC,CAAC;AACH,CAAC;;AAED,MAAM,MAAM,CAAC,CAAC;CACb,IAAI,EAAE,MAAM,CAAC;;CACb,WAAW,CAAC,IAAY,AAAR,EAAE,MAAM,EAAE,CAAC;EAC1B,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;CAClB,CAAC;;CAED,CAAC,YAAY,CAAC,IAAI,CAAC;CACnB,KAAK,GAAG,CAAC;EACR,OAAO,CAAC,GAAG,CAAC,qBAAqB,IAAI,CAAC,IAAI,GAAG,CAAC,CAAC;CAChD,CAAC;AACF,CAAC;;AAED,KAAK,CAAC,CAAC,GAAG,GAAG,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;;AAC5B,CAAC,CAAC,KAAK,EAAE,CAAC,AACV" } \ No newline at end of file From b49525ed14555280f52461f8a62f70ed8a5240fb Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 24 Sep 2026 15:54:44 -0400 Subject: [PATCH 2/9] simplify --- src/languages/ts/index.js | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/src/languages/ts/index.js b/src/languages/ts/index.js index c80bf357..58abcc75 100644 --- a/src/languages/ts/index.js +++ b/src/languages/ts/index.js @@ -2736,19 +2736,14 @@ function handle_var_declarator(node, context, no_in) { * @param {TSESTree.Node} node */ function has_preceding_decorator(node) { - const node_start = node.loc.start; - let start = node_start; - let n = node; - - if (node.type === 'ExportNamedDeclaration' && node.declaration) { - n = node.declaration; - } + const n = (node.type === 'ExportNamedDeclaration' && node.declaration) || node; if ('decorators' in n) { - start = n.decorators?.[0]?.loc?.start ?? node_start; + const loc = n.decorators?.[0]?.loc; + return loc ? before(loc.start, node.loc.start) : false; } - return before(start, node_start); + return false; } /** From f2e90377ff64c0a13ff2ad194203570f35e47cde Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 24 Sep 2026 15:56:10 -0400 Subject: [PATCH 3/9] fix comment --- src/languages/ts/index.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/languages/ts/index.js b/src/languages/ts/index.js index 58abcc75..37f6d570 100644 --- a/src/languages/ts/index.js +++ b/src/languages/ts/index.js @@ -2731,8 +2731,8 @@ function handle_var_declarator(node, context, no_in) { } /** - * Where a node's printed output starts. A parameter's `loc` starts after its - * decorators (Acorn, typescript-estree), but they are printed first. + * Whether a node has a decorator whose `start` location precedes that of + * the node itself, in which case we should not emit a mapping yet * @param {TSESTree.Node} node */ function has_preceding_decorator(node) { From ced4eb10ef78e85f1425c9c7fd436006342ebf22 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 24 Sep 2026 16:03:47 -0400 Subject: [PATCH 4/9] simplify --- src/languages/ts/index.js | 27 ++++++++------------------- 1 file changed, 8 insertions(+), 19 deletions(-) diff --git a/src/languages/ts/index.js b/src/languages/ts/index.js index 37f6d570..71e1289c 100644 --- a/src/languages/ts/index.js +++ b/src/languages/ts/index.js @@ -1130,7 +1130,13 @@ export default (options = {}) => { }, Decorator(node, context) { - write_decorator(context, node); + context.write('@'); + // a decorator must be an identifier/member/call (or parenthesized); anything + // else (ternary, logical, assignment, unary, `as`, optional chain…) needs wrapping + const wrap = + /** @type {string} */ (node.expression.type) === 'ChainExpression' || + EXPRESSIONS_PRECEDENCE[node.expression.type] < EXPRESSIONS_PRECEDENCE.CallExpression; + maybe_wrap(context, node.expression, wrap); }, DoWhileStatement(node, context) { @@ -2522,20 +2528,6 @@ function maybe_wrap(context, node, wrap) { } } -/** - * @param {Context} context - * @param {TSESTree.Decorator} node - */ -function write_decorator(context, node) { - context.write('@'); - // a decorator must be an identifier/member/call (or parenthesized); anything - // else (ternary, logical, assignment, unary, `as`, optional chain…) needs wrapping - const wrap = - /** @type {string} */ (node.expression.type) === 'ChainExpression' || - EXPRESSIONS_PRECEDENCE[node.expression.type] < EXPRESSIONS_PRECEDENCE.CallExpression; - maybe_wrap(context, node.expression, wrap); -} - /** * Parameter decorators (`@dec x`) stay on the parameter's line, unlike class * and member decorators, which the `Decorator` visitor puts on their own line @@ -2546,10 +2538,7 @@ function write_parameter_decorators(context, decorators) { if (!decorators) return; for (const decorator of decorators) { - // not visited through the root visitor, so map the node's span here - if (decorator.loc) context.location(decorator.loc.start.line, decorator.loc.start.column); - write_decorator(context, decorator); - if (decorator.loc) context.location(decorator.loc.end.line, decorator.loc.end.column); + context.visit(decorator); context.write(' '); } } From d63d45241aa705bb4c6371e4882601d93c6ee97f Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 24 Sep 2026 16:12:26 -0400 Subject: [PATCH 5/9] tidy up --- src/languages/ts/index.js | 85 ++++++++++++++++++--------------------- 1 file changed, 40 insertions(+), 45 deletions(-) diff --git a/src/languages/ts/index.js b/src/languages/ts/index.js index 71e1289c..190d33ee 100644 --- a/src/languages/ts/index.js +++ b/src/languages/ts/index.js @@ -482,7 +482,7 @@ export default (options = {}) => { * @param {Context} context */ 'ArrayExpression|ArrayPattern': (node, context) => { - if ('decorators' in node) write_parameter_decorators(context, node.decorators); + if ('decorators' in node) inline_decorators(context, node.decorators); context.write('['); sequence( context, @@ -629,12 +629,7 @@ export default (options = {}) => { * @param {Context} context */ 'ClassDeclaration|ClassExpression': (node, context) => { - if (node.decorators) { - for (const decorator of node.decorators) { - context.visit(decorator); - context.newline(); - } - } + block_decorators(context, node.decorators); if (node.declare) context.write('declare '); if (node.abstract) context.write('abstract '); @@ -732,12 +727,7 @@ export default (options = {}) => { * @param {Context} context */ 'MethodDefinition|TSAbstractMethodDefinition': (node, context) => { - if (node.decorators) { - for (const decorator of node.decorators) { - context.visit(decorator); - context.newline(); - } - } + block_decorators(context, node.decorators); // @ts-expect-error `acorn-typescript` and `@typescript-eslint/types` have slightly different type definitions if (node.abstract || node.type === 'TSAbstractMethodDefinition') { @@ -805,12 +795,7 @@ export default (options = {}) => { node, context ) => { - if (node.decorators) { - for (const decorator of node.decorators) { - context.visit(decorator); - context.newline(); - } - } + block_decorators(context, node.decorators); if (node.declare) context.write('declare '); @@ -1030,7 +1015,7 @@ export default (options = {}) => { }, AssignmentPattern(node, context) { - write_parameter_decorators(context, node.decorators); + inline_decorators(context, node.decorators); context.visit(node.left); context.write(' = '); context.visit(node.right); @@ -1182,29 +1167,28 @@ export default (options = {}) => { ExportNamedDeclaration(node, context) { if (node.declaration) { // Check if declaration has decorators (ClassDeclaration, ClassExpression can have them) - const decl = /** @type {any} */ (node.declaration); - if (decl.decorators && decl.decorators.length > 0) { - for (const decorator of decl.decorators) { - context.visit(decorator); - context.newline(); - } - token(context, 'export', node); - context.write(' '); + const d = /** @type {any} */ (node.declaration); + + block_decorators(context, d.decorators); + + token(context, 'export', node); + context.write(' '); + + if (d.decorators && d.decorators.length > 0) { + const { decorators, loc } = d; + // Temporarily remove decorators so ClassDeclaration doesn't print them again - const savedDecorators = decl.decorators; - const loc = decl.loc; - decl.decorators = []; - decl.loc = null; - context.visit(decl); - decl.decorators = savedDecorators; - decl.loc = loc; + d.decorators = []; + d.loc = null; + context.visit(d); + d.decorators = decorators; + d.loc = loc; if (loc) context.location(loc.end.line, loc.end.column); } else { - token(context, 'export', node); - context.write(' '); - context.visit(decl); + context.visit(d); } + return; } @@ -1276,7 +1260,7 @@ export default (options = {}) => { FunctionExpression: shared['FunctionDeclaration|FunctionExpression'], Identifier(node, context) { - write_parameter_decorators(context, node.decorators); + inline_decorators(context, node.decorators); let name = node.name; context.write(name, node); @@ -1465,7 +1449,7 @@ export default (options = {}) => { }, ObjectPattern(node, context) { - write_parameter_decorators(context, node.decorators); + inline_decorators(context, node.decorators); context.write('{'); sequence(context, node.properties, node.loc?.end ?? null, true); context.write('}'); @@ -1953,8 +1937,8 @@ export default (options = {}) => { // property, Acorn to its parameter. Either way they precede the modifiers const parameter = node.parameter; const parameter_decorators = parameter.decorators; - write_parameter_decorators(context, node.decorators); - write_parameter_decorators(context, parameter_decorators); + inline_decorators(context, node.decorators); + inline_decorators(context, parameter_decorators); if (node.accessibility) { context.write(node.accessibility + ' '); @@ -2529,12 +2513,23 @@ function maybe_wrap(context, node, wrap) { } /** - * Parameter decorators (`@dec x`) stay on the parameter's line, unlike class - * and member decorators, which the `Decorator` visitor puts on their own line * @param {Context} context * @param {TSESTree.Decorator[] | undefined} decorators */ -function write_parameter_decorators(context, decorators) { +function block_decorators(context, decorators) { + if (!decorators) return; + + for (const decorator of decorators) { + context.visit(decorator); + context.newline(); + } +} + +/** + * @param {Context} context + * @param {TSESTree.Decorator[] | undefined} decorators + */ +function inline_decorators(context, decorators) { if (!decorators) return; for (const decorator of decorators) { From 6e4f87a8a4d6d6461c3e2f55176dc27fde04af5a Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 24 Sep 2026 22:02:04 -0400 Subject: [PATCH 6/9] fix: print decorators before `export default` (#229) * print decorators before export default * fix * fix * fix * fix * DRY out * tweak * fix * prettier --- src/languages/ts/index.js | 105 +++++++++++------- test/samples/ts-decorators-class/expected.ts | 3 + .../ts-decorators-class/expected.ts.map | 4 +- test/samples/ts-decorators-class/input.ts | 3 + .../ts-parameter-decorators/expected.ts.map | 2 +- 5 files changed, 76 insertions(+), 41 deletions(-) diff --git a/src/languages/ts/index.js b/src/languages/ts/index.js index 190d33ee..7aa3c37e 100644 --- a/src/languages/ts/index.js +++ b/src/languages/ts/index.js @@ -482,7 +482,7 @@ export default (options = {}) => { * @param {Context} context */ 'ArrayExpression|ArrayPattern': (node, context) => { - if ('decorators' in node) inline_decorators(context, node.decorators); + if ('decorators' in node) inline_decorators(context, node); context.write('['); sequence( context, @@ -629,7 +629,7 @@ export default (options = {}) => { * @param {Context} context */ 'ClassDeclaration|ClassExpression': (node, context) => { - block_decorators(context, node.decorators); + block_decorators(context, node); if (node.declare) context.write('declare '); if (node.abstract) context.write('abstract '); @@ -727,7 +727,7 @@ export default (options = {}) => { * @param {Context} context */ 'MethodDefinition|TSAbstractMethodDefinition': (node, context) => { - block_decorators(context, node.decorators); + block_decorators(context, node); // @ts-expect-error `acorn-typescript` and `@typescript-eslint/types` have slightly different type definitions if (node.abstract || node.type === 'TSAbstractMethodDefinition') { @@ -795,7 +795,7 @@ export default (options = {}) => { node, context ) => { - block_decorators(context, node.decorators); + block_decorators(context, node); if (node.declare) context.write('declare '); @@ -1015,7 +1015,7 @@ export default (options = {}) => { }, AssignmentPattern(node, context) { - inline_decorators(context, node.decorators); + inline_decorators(context, node); context.visit(node.left); context.write(' = '); context.visit(node.right); @@ -1154,10 +1154,15 @@ export default (options = {}) => { }, ExportDefaultDeclaration(node, context) { + const d = node.declaration; + + // ClassDeclaration/ClassExpression decorators should be printed before `export` + if ('decorators' in d) block_decorators(context, d); + token(context, 'export', node); context.write(' default '); - context.visit(node.declaration); + visit_without_decorators(context, d); if (node.declaration.type !== 'FunctionDeclaration') { context.write(';'); @@ -1165,30 +1170,16 @@ export default (options = {}) => { }, ExportNamedDeclaration(node, context) { - if (node.declaration) { - // Check if declaration has decorators (ClassDeclaration, ClassExpression can have them) - const d = /** @type {any} */ (node.declaration); + const d = node.declaration; - block_decorators(context, d.decorators); + if (d) { + // ClassDeclaration/ClassExpression decorators should be printed before `export` + if ('decorators' in d) block_decorators(context, d); token(context, 'export', node); context.write(' '); - if (d.decorators && d.decorators.length > 0) { - const { decorators, loc } = d; - - // Temporarily remove decorators so ClassDeclaration doesn't print them again - d.decorators = []; - d.loc = null; - context.visit(d); - d.decorators = decorators; - d.loc = loc; - - if (loc) context.location(loc.end.line, loc.end.column); - } else { - context.visit(d); - } - + visit_without_decorators(context, d); return; } @@ -1260,7 +1251,7 @@ export default (options = {}) => { FunctionExpression: shared['FunctionDeclaration|FunctionExpression'], Identifier(node, context) { - inline_decorators(context, node.decorators); + inline_decorators(context, node); let name = node.name; context.write(name, node); @@ -1449,7 +1440,7 @@ export default (options = {}) => { }, ObjectPattern(node, context) { - inline_decorators(context, node.decorators); + inline_decorators(context, node); context.write('{'); sequence(context, node.properties, node.loc?.end ?? null, true); context.write('}'); @@ -1937,8 +1928,11 @@ export default (options = {}) => { // property, Acorn to its parameter. Either way they precede the modifiers const parameter = node.parameter; const parameter_decorators = parameter.decorators; - inline_decorators(context, node.decorators); - inline_decorators(context, parameter_decorators); + + inline_decorators( + context, + parameter.decorators ? { ...node, decorators: parameter.decorators } : node + ); if (node.accessibility) { context.write(node.accessibility + ' '); @@ -2514,28 +2508,59 @@ function maybe_wrap(context, node, wrap) { /** * @param {Context} context - * @param {TSESTree.Decorator[] | undefined} decorators + * @param {TSESTree.Node & { decorators: TSESTree.Decorator[] | undefined }} node */ -function block_decorators(context, decorators) { - if (!decorators) return; +function block_decorators(context, node) { + if (!node.decorators) return; - for (const decorator of decorators) { + for (const decorator of node.decorators) { context.visit(decorator); context.newline(); } + + if (has_preceding_decorator(node) && node.loc) { + context.location(node.loc.start.line, node.loc.start.column); + } } /** * @param {Context} context - * @param {TSESTree.Decorator[] | undefined} decorators + * @param {TSESTree.Node & { decorators: TSESTree.Decorator[] | undefined }} node */ -function inline_decorators(context, decorators) { - if (!decorators) return; +function inline_decorators(context, node) { + if (!node.decorators) return; - for (const decorator of decorators) { + for (const decorator of node.decorators) { context.visit(decorator); context.write(' '); } + + if (has_preceding_decorator(node) && node.loc) { + context.location(node.loc.start.line, node.loc.start.column); + } +} + +/** + * Visit an exported declaration minus its decorators, which have already been printed + * @param {Context} context + * @param {TSESTree.Node} node + */ +function visit_without_decorators(context, node) { + if ('decorators' in node && node.decorators && node.decorators.length > 0) { + const { decorators, loc } = node; + + // Temporarily remove decorators so ClassDeclaration doesn't print them again + node.decorators = []; + // @ts-expect-error + node.loc = null; + context.visit(node); + node.decorators = decorators; + node.loc = loc; + + if (loc) context.location(loc.end.line, loc.end.column); + } else { + context.visit(node); + } } /** @param {TSESTree.Node} node */ @@ -2720,7 +2745,11 @@ function handle_var_declarator(node, context, no_in) { * @param {TSESTree.Node} node */ function has_preceding_decorator(node) { - const n = (node.type === 'ExportNamedDeclaration' && node.declaration) || node; + let n = (node.type === 'ExportNamedDeclaration' && node.declaration) || node; + + if ('parameter' in n && 'decorators' in n.parameter) { + n = n.parameter; + } if ('decorators' in n) { const loc = n.decorators?.[0]?.loc; diff --git a/test/samples/ts-decorators-class/expected.ts b/test/samples/ts-decorators-class/expected.ts index 41d452cc..106bb5ea 100644 --- a/test/samples/ts-decorators-class/expected.ts +++ b/test/samples/ts-decorators-class/expected.ts @@ -14,6 +14,9 @@ class Category {} @Entity('tasks', { info: true }) export class Task {} +@Entity('default', { info: true }) +export default class Default {}; + const u = new User(); const c = new Category(); const t = new Task(); \ No newline at end of file diff --git a/test/samples/ts-decorators-class/expected.ts.map b/test/samples/ts-decorators-class/expected.ts.map index 7d8f96ed..cfdf6a9b 100644 --- a/test/samples/ts-decorators-class/expected.ts.map +++ b/test/samples/ts-decorators-class/expected.ts.map @@ -5,7 +5,7 @@ "input.js" ], "sourcesContent": [ - "export class Entity {\n\tconstructor(\n\t\tprivate readonly name: string,\n\t\tprivate readonly info: boolean\n\t) {}\n}\n\n@Entity('users', { info: true })\nexport class User {}\n\n@Entity('categories', { info: false })\nclass Category {}\n\n@Entity('tasks', { info: true })\nexport class Task {}\n\nconst u = new User();\nconst c = new Category();\nconst t = new Task();\n" + "export class Entity {\n\tconstructor(\n\t\tprivate readonly name: string,\n\t\tprivate readonly info: boolean\n\t) {}\n}\n\n@Entity('users', { info: true })\nexport class User {}\n\n@Entity('categories', { info: false })\nclass Category {}\n\n@Entity('tasks', { info: true })\nexport class Task {}\n\n@Entity('default', { info: true })\nexport default class Default {}\n\nconst u = new User();\nconst c = new Category();\nconst t = new Task();\n" ], - "mappings": "AAAA,MAAM,CAAC,MAAM,MAAM,CAAC,CAAC;CACpB,WAAW;EACV,iBAAiB,IAAY,AAAR,EAAE,MAAM;EAC7B,iBAAiB,IAAa,AAAT,EAAE,OAAO;GAC7B,CAAC,CAAC;AACL,CAAC;;AAED,CAAC,MAAM,CAAC,OAAO,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC;MAC1B,OAAO,IAAI,CAAC,CAAC,CAAC;;AAEpB,CAAC,MAAM,CAAC,YAAY,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC;MAChC,QAAQ,CAAC,CAAC,CAAC;;AAEjB,CAAC,MAAM,CAAC,OAAO,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC;MAC1B,OAAO,IAAI,CAAC,CAAC,CAAC;;AAEpB,KAAK,CAAC,CAAC,GAAG,GAAG,CAAC,IAAI,EAAE,CAAC;AACrB,KAAK,CAAC,CAAC,GAAG,GAAG,CAAC,QAAQ,EAAE,CAAC;AACzB,KAAK,CAAC,CAAC,GAAG,GAAG,CAAC,IAAI,EAAE,CAAC,AACrB" + "mappings": "AAAA,MAAM,CAAC,MAAM,MAAM,CAAC,CAAC;CACpB,WAAW;EACV,iBAAiB,IAAY,AAAR,EAAE,MAAM;EAC7B,iBAAiB,IAAa,AAAT,EAAE,OAAO;GAC7B,CAAC,CAAC;AACL,CAAC;;AAED,CAAC,MAAM,CAAC,OAAO,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC;MAC1B,OAAO,IAAI,CAAC,CAAC,CAAC;;AAEpB,CAAC,MAAM,CAAC,YAAY,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC;MAChC,QAAQ,CAAC,CAAC,CAAC;;AAEjB,CAAC,MAAM,CAAC,OAAO,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC;MAC1B,OAAO,IAAI,CAAC,CAAC,CAAC;;AAGpB,AADA,CAAC,MAAM,CAAC,SAAS,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC;MAC5B,eAAe,OAAO,CAAC,CAAC,CAAC,CAAA;;AAE/B,KAAK,CAAC,CAAC,GAAG,GAAG,CAAC,IAAI,EAAE,CAAC;AACrB,KAAK,CAAC,CAAC,GAAG,GAAG,CAAC,QAAQ,EAAE,CAAC;AACzB,KAAK,CAAC,CAAC,GAAG,GAAG,CAAC,IAAI,EAAE,CAAC,AACrB" } \ No newline at end of file diff --git a/test/samples/ts-decorators-class/input.ts b/test/samples/ts-decorators-class/input.ts index e196de50..bf69a7bb 100644 --- a/test/samples/ts-decorators-class/input.ts +++ b/test/samples/ts-decorators-class/input.ts @@ -14,6 +14,9 @@ class Category {} @Entity('tasks', { info: true }) export class Task {} +@Entity('default', { info: true }) +export default class Default {} + const u = new User(); const c = new Category(); const t = new Task(); diff --git a/test/samples/ts-parameter-decorators/expected.ts.map b/test/samples/ts-parameter-decorators/expected.ts.map index 74d19e56..66beaf3e 100644 --- a/test/samples/ts-parameter-decorators/expected.ts.map +++ b/test/samples/ts-parameter-decorators/expected.ts.map @@ -7,5 +7,5 @@ "sourcesContent": [ "class Controller {\n\tconstructor(\n\t\t@Inject(CONFIG) private readonly config: Config,\n\t\t@Optional() @Inject(LOGGER) protected logger?: Logger,\n\t\t@Inject(CACHE) public cache = new Map(),\n\t\t@Self() service: Service\n\t) {}\n\n\tfind(@Param('id') id: string, @Query() { limit, offset }: Paging, @Body() [first]: Item[]) {}\n\n\tupdate(\n\t\t@Param('id') id: string,\n\t\t@Body(validation.pipe) body: Item,\n\t\t@(flags ?? defaults) force = false\n\t) {}\n}\n" ], - "mappings": "AAAA,MAAM,UAAU,CAAC,CAAC;CACjB,WAAW;EACM,AAAhB,CAAC,MAAM,CAAC,MAAM,CAAC,kBAAkB,MAAc,AAAR,EAAE,MAAM;EACnB,AAA5B,CAAC,QAAQ,EAAE,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,WAAW,MAAe,CAAR,EAAE,MAAM;EACtC,AAAf,CAAC,MAAM,CAAC,KAAK,CAAC,QAAC,AAAO,KAAK,GAAG,GAAG,CAAC,GAAG,EAAE;EACvC,CAAC,IAAI,EAAE,CAAC,OAAgB,AAAT,EAAE,OAAO;GACvB,CAAC,CAAC;;CAEJ,IAAI;EAAC,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,EAAU,AAAR,EAAE,MAAM;EAAE,CAAC,KAAK,EAAE,GAAG,KAAK,EAAE,MAAM,EAAE,EAAE,MAAM;EAAE,CAAC,IAAI,EAAE,EAAE,KAAK,CAAC,EAAE,IAAI,EAAE;GAAE,CAAC,CAAC;;CAE7F,MAAM;EACL,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,EAAU,AAAR,EAAE,MAAM;EACvB,CAAC,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC,IAAU,AAAN,EAAE,IAAI;EACjC,CAAE,CAAA,KAAK,IAAI,QAAQ,CAAA,AAAC,CAAC,KAAK,GAAG,KAAK;GACjC,CAAC,CAAC;AACL,CAAC,AACD" + "mappings": "AAAA,MAAM,UAAU,CAAC,CAAC;CACjB,WAAW;EACV,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,iBAAiB,MAAc,AAAR,EAAE,MAAM;EAC/C,CAAC,QAAQ,EAAE,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,UAAU,MAAe,CAAR,EAAE,MAAM;EACrD,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,OAAA,AAAO,KAAK,GAAG,GAAG,CAAC,GAAG,EAAE;EACvC,CAAC,IAAI,EAAE,CAAC,OAAgB,AAAT,EAAE,OAAO;GACvB,CAAC,CAAC;;CAEJ,IAAI;EAAC,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,EAAU,AAAR,EAAE,MAAM;EAAE,CAAC,KAAK,EAAE,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,EAAE,MAAM;EAAE,CAAC,IAAI,EAAE,CAAC,CAAC,KAAK,CAAC,EAAE,IAAI,EAAE;GAAE,CAAC,CAAC;;CAE7F,MAAM;EACL,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,EAAU,AAAR,EAAE,MAAM;EACvB,CAAC,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC,IAAU,AAAN,EAAE,IAAI;EACjC,CAAE,CAAA,KAAK,IAAI,QAAQ,CAAA,AAAC,CAAC,KAAK,GAAG,KAAK;GACjC,CAAC,CAAC;AACL,CAAC,AACD" } \ No newline at end of file From 65a3bafae1c231fc74032703960c81b9c38c2b82 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 24 Sep 2026 22:18:12 -0400 Subject: [PATCH 7/9] fix --- src/languages/ts/index.js | 22 ++++++++++++++----- .../ts-decorator-export-comment/expected.ts | 3 +++ .../expected.ts.map | 11 ++++++++++ .../ts-decorator-export-comment/input.ts | 2 ++ .../ts-decorators-class/expected.ts.map | 2 +- 5 files changed, 34 insertions(+), 6 deletions(-) create mode 100644 test/samples/ts-decorator-export-comment/expected.ts create mode 100644 test/samples/ts-decorator-export-comment/expected.ts.map create mode 100644 test/samples/ts-decorator-export-comment/input.ts diff --git a/src/languages/ts/index.js b/src/languages/ts/index.js index 7aa3c37e..ed645468 100644 --- a/src/languages/ts/index.js +++ b/src/languages/ts/index.js @@ -1157,11 +1157,15 @@ export default (options = {}) => { const d = node.declaration; // ClassDeclaration/ClassExpression decorators should be printed before `export` - if ('decorators' in d) block_decorators(context, d); + if ('decorators' in d) block_decorators(context, { ...d, loc: null }); token(context, 'export', node); context.write(' default '); + if (d.loc) { + flush_comments_until(context, null, d.loc.start, true, false); + } + visit_without_decorators(context, d); if (node.declaration.type !== 'FunctionDeclaration') { @@ -1174,11 +1178,15 @@ export default (options = {}) => { if (d) { // ClassDeclaration/ClassExpression decorators should be printed before `export` - if ('decorators' in d) block_decorators(context, d); + if ('decorators' in d) block_decorators(context, { ...d, loc: null }); token(context, 'export', node); context.write(' '); + if (d.loc) { + flush_comments_until(context, null, d.loc.start, true, false); + } + visit_without_decorators(context, d); return; } @@ -2518,7 +2526,7 @@ function block_decorators(context, node) { context.newline(); } - if (has_preceding_decorator(node) && node.loc) { + if (node.loc && has_preceding_decorator(node)) { context.location(node.loc.start.line, node.loc.start.column); } } @@ -2535,7 +2543,7 @@ function inline_decorators(context, node) { context.write(' '); } - if (has_preceding_decorator(node) && node.loc) { + if (node.loc && has_preceding_decorator(node)) { context.location(node.loc.start.line, node.loc.start.column); } } @@ -2553,6 +2561,7 @@ function visit_without_decorators(context, node) { node.decorators = []; // @ts-expect-error node.loc = null; + context.visit(node); node.decorators = decorators; node.loc = loc; @@ -2745,7 +2754,10 @@ function handle_var_declarator(node, context, no_in) { * @param {TSESTree.Node} node */ function has_preceding_decorator(node) { - let n = (node.type === 'ExportNamedDeclaration' && node.declaration) || node; + let n = + ((node.type === 'ExportNamedDeclaration' || node.type === 'ExportDefaultDeclaration') && + node.declaration) || + node; if ('parameter' in n && 'decorators' in n.parameter) { n = n.parameter; diff --git a/test/samples/ts-decorator-export-comment/expected.ts b/test/samples/ts-decorator-export-comment/expected.ts new file mode 100644 index 00000000..d7e4f94b --- /dev/null +++ b/test/samples/ts-decorator-export-comment/expected.ts @@ -0,0 +1,3 @@ +/* c */ +@dec +export declare class X {} \ No newline at end of file diff --git a/test/samples/ts-decorator-export-comment/expected.ts.map b/test/samples/ts-decorator-export-comment/expected.ts.map new file mode 100644 index 00000000..43aa5cf6 --- /dev/null +++ b/test/samples/ts-decorator-export-comment/expected.ts.map @@ -0,0 +1,11 @@ +{ + "version": 3, + "names": [], + "sources": [ + "input.js" + ], + "sourcesContent": [ + "@dec /* c */\nexport declare class X {}\n" + ], + "mappings": "AAAA;AAAA,CAAC,GAAG;MACE,eAAe,CAAC,CAAC,CAAC,CAAC,AACzB" +} \ No newline at end of file diff --git a/test/samples/ts-decorator-export-comment/input.ts b/test/samples/ts-decorator-export-comment/input.ts new file mode 100644 index 00000000..e641e2b7 --- /dev/null +++ b/test/samples/ts-decorator-export-comment/input.ts @@ -0,0 +1,2 @@ +@dec /* c */ +export declare class X {} diff --git a/test/samples/ts-decorators-class/expected.ts.map b/test/samples/ts-decorators-class/expected.ts.map index cfdf6a9b..888060a3 100644 --- a/test/samples/ts-decorators-class/expected.ts.map +++ b/test/samples/ts-decorators-class/expected.ts.map @@ -7,5 +7,5 @@ "sourcesContent": [ "export class Entity {\n\tconstructor(\n\t\tprivate readonly name: string,\n\t\tprivate readonly info: boolean\n\t) {}\n}\n\n@Entity('users', { info: true })\nexport class User {}\n\n@Entity('categories', { info: false })\nclass Category {}\n\n@Entity('tasks', { info: true })\nexport class Task {}\n\n@Entity('default', { info: true })\nexport default class Default {}\n\nconst u = new User();\nconst c = new Category();\nconst t = new Task();\n" ], - "mappings": "AAAA,MAAM,CAAC,MAAM,MAAM,CAAC,CAAC;CACpB,WAAW;EACV,iBAAiB,IAAY,AAAR,EAAE,MAAM;EAC7B,iBAAiB,IAAa,AAAT,EAAE,OAAO;GAC7B,CAAC,CAAC;AACL,CAAC;;AAED,CAAC,MAAM,CAAC,OAAO,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC;MAC1B,OAAO,IAAI,CAAC,CAAC,CAAC;;AAEpB,CAAC,MAAM,CAAC,YAAY,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC;MAChC,QAAQ,CAAC,CAAC,CAAC;;AAEjB,CAAC,MAAM,CAAC,OAAO,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC;MAC1B,OAAO,IAAI,CAAC,CAAC,CAAC;;AAGpB,AADA,CAAC,MAAM,CAAC,SAAS,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC;MAC5B,eAAe,OAAO,CAAC,CAAC,CAAC,CAAA;;AAE/B,KAAK,CAAC,CAAC,GAAG,GAAG,CAAC,IAAI,EAAE,CAAC;AACrB,KAAK,CAAC,CAAC,GAAG,GAAG,CAAC,QAAQ,EAAE,CAAC;AACzB,KAAK,CAAC,CAAC,GAAG,GAAG,CAAC,IAAI,EAAE,CAAC,AACrB" + "mappings": "AAAA,MAAM,CAAC,MAAM,MAAM,CAAC,CAAC;CACpB,WAAW;EACV,iBAAiB,IAAY,AAAR,EAAE,MAAM;EAC7B,iBAAiB,IAAa,AAAT,EAAE,OAAO;GAC7B,CAAC,CAAC;AACL,CAAC;;AAED,CAAC,MAAM,CAAC,OAAO,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC;MAC1B,OAAO,IAAI,CAAC,CAAC,CAAC;;AAEpB,CAAC,MAAM,CAAC,YAAY,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC;MAChC,QAAQ,CAAC,CAAC,CAAC;;AAEjB,CAAC,MAAM,CAAC,OAAO,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC;MAC1B,OAAO,IAAI,CAAC,CAAC,CAAC;;AAEpB,CAAC,MAAM,CAAC,SAAS,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC;MAC5B,eAAe,OAAO,CAAC,CAAC,CAAC,CAAA;;AAE/B,KAAK,CAAC,CAAC,GAAG,GAAG,CAAC,IAAI,EAAE,CAAC;AACrB,KAAK,CAAC,CAAC,GAAG,GAAG,CAAC,QAAQ,EAAE,CAAC;AACzB,KAAK,CAAC,CAAC,GAAG,GAAG,CAAC,IAAI,EAAE,CAAC,AACrB" } \ No newline at end of file From 6036f4eb320c5440d3b4b292b9b0e82316d59974 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 24 Sep 2026 22:23:40 -0400 Subject: [PATCH 8/9] fix test --- test/samples/ts-decorator-export-comment/expected.ts | 3 +-- test/samples/ts-decorator-export-comment/expected.ts.map | 4 ++-- test/samples/ts-decorator-export-comment/input.ts | 4 ++-- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/test/samples/ts-decorator-export-comment/expected.ts b/test/samples/ts-decorator-export-comment/expected.ts index d7e4f94b..41cb9b34 100644 --- a/test/samples/ts-decorator-export-comment/expected.ts +++ b/test/samples/ts-decorator-export-comment/expected.ts @@ -1,3 +1,2 @@ -/* c */ @dec -export declare class X {} \ No newline at end of file +export /* c */ declare class X {} \ No newline at end of file diff --git a/test/samples/ts-decorator-export-comment/expected.ts.map b/test/samples/ts-decorator-export-comment/expected.ts.map index 43aa5cf6..91ed8ecb 100644 --- a/test/samples/ts-decorator-export-comment/expected.ts.map +++ b/test/samples/ts-decorator-export-comment/expected.ts.map @@ -5,7 +5,7 @@ "input.js" ], "sourcesContent": [ - "@dec /* c */\nexport declare class X {}\n" + "@dec\nexport /* c */ declare class X {}\n" ], - "mappings": "AAAA;AAAA,CAAC,GAAG;MACE,eAAe,CAAC,CAAC,CAAC,CAAC,AACzB" + "mappings": "AAAA,CAAC,GAAG;MACE,uBAAuB,CAAC,CAAC,CAAC,CAAC,AACjC" } \ No newline at end of file diff --git a/test/samples/ts-decorator-export-comment/input.ts b/test/samples/ts-decorator-export-comment/input.ts index e641e2b7..478fbb4e 100644 --- a/test/samples/ts-decorator-export-comment/input.ts +++ b/test/samples/ts-decorator-export-comment/input.ts @@ -1,2 +1,2 @@ -@dec /* c */ -export declare class X {} +@dec +export /* c */ declare class X {} From f07d19053f397b616a91b10797dd131dd469eb47 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 24 Sep 2026 22:43:06 -0400 Subject: [PATCH 9/9] fix --- src/languages/ts/index.js | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/src/languages/ts/index.js b/src/languages/ts/index.js index ed645468..a6b3f2d0 100644 --- a/src/languages/ts/index.js +++ b/src/languages/ts/index.js @@ -1157,7 +1157,13 @@ export default (options = {}) => { const d = node.declaration; // ClassDeclaration/ClassExpression decorators should be printed before `export` - if ('decorators' in d) block_decorators(context, { ...d, loc: null }); + if ('decorators' in d) { + block_decorators(context, { + ...d, + // @ts-expect-error we need to prevent a mapping being added for the end of the declaration + loc: null + }); + } token(context, 'export', node); context.write(' default '); @@ -1178,7 +1184,13 @@ export default (options = {}) => { if (d) { // ClassDeclaration/ClassExpression decorators should be printed before `export` - if ('decorators' in d) block_decorators(context, { ...d, loc: null }); + if ('decorators' in d) { + block_decorators(context, { + ...d, + // @ts-expect-error we need to prevent a mapping being added for the end of the declaration + loc: null + }); + } token(context, 'export', node); context.write(' ');