Skip to content

feat: use tokens for more accurate sourcemaps (alternative approach) - #235

Merged
Rich-Harris merged 10 commits into
mainfrom
tokens-alt
Sep 26, 2026
Merged

Rich-Harris merged 10 commits into
mainfrom
tokens-alt

Conversation

@Rich-Harris

Copy link
Copy Markdown
Member

This is an alternative to #213, designed to be a bit less labour-intensive — rather than all the visitor code in ts needing to be token-aware, it stores token state on the context, and extracts tokens (if provided) inside context.write.

There's a little bit of footwork involved in matching openers with closers, but other than that it's fairly simple — on visiting a node with a loc property, we set the current frame to that node's boundaries, and advance through the array within that frame each time we consume a token. If visiting a node without loc, token extraction is disabled.

I prefer this because if we update visitor code we don't need to remember to carefully use write_at and write_before and locate and enclosing etc — it's all taken care of us. As far as I can tell it's equivalently reliable. @leonidaz thoughts?

leonidaz and others added 9 commits September 24, 2026 09:25
The root visitor maps every node's start and end, which covers tokens that
open a node. Tokens inside a node have no node boundary to anchor them: the
brackets of a computed key, the parentheses of parameter lists, calls and
control-flow heads, the braces of import and export lists, static blocks,
switch and enum bodies, template interpolations, and every keyword written
after a node's first token (`function` after `async`, `else`, `finally`,
`while` in `do … while`, `default` after `export`, `type` after `import`,
class and member modifiers, `extends`, `implements`, `in`, `of`, `as`,
`satisfies`).

A new `tokens` option takes the parser's token list (Acorn's `onToken`, Babel's
`tokens`, ESLint-style `ast.tokens`). The tokens of interest are indexed once,
sorted if the parser emitted them out of order, and each one is found by a
bounded binary search from a position the AST does know. Without tokens they
stay unmapped.

Closing delimiters that end a node (`)` of a call, `]` of an array or computed
member, `}` of an object, switch, enum or interface body) are mapped from the
node's end without tokens, as BlockStatement already does.

Printed output is unchanged; 61 sample maps gain segments.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A decorated class's decorators are printed before `export`, so the root visitor
cannot map the export node's start where it is written. Emit that start once the
decorators are out: the export node begins at `export` with both Acorn and
typescript-estree, whichever side of it the decorators were written on.

Instead of clearing the declaration's decorators and location while it is
printed, remember that its decorators were printed. The root visitor then maps
its start when that start is the `class` keyword (typescript-estree, decorators
written before `export`) and skips it when it is a decorator (Acorn, see
sveltejs/acorn-typescript#117), and the keyword lookup can locate `class`,
`abstract` and the rest from the tokens because the node keeps its location.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Deferred from the first version of this branch until #212 gave the catch
clause its own visitor.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@pkg-svelte-dev

pkg-svelte-dev Bot commented Sep 25, 2026 •

Copy link
Copy Markdown

Install the latest version of esrap from 0353368:

pnpm add https://pkg.svelte.dev/esrap/c/0353368b17cefa5577abc245b88f8dbd6d011151

Open in pkg.svelte.dev: https://pkg.svelte.dev/repos/esrap/pr/235

@changeset-bot

changeset-bot Bot commented Sep 25, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 0353368

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
esrap Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@leonidaz

Copy link
Copy Markdown
Contributor

yeah, this is definitely a better direction 👍

Comment thread src/context.js
this.#commands.push(content);
this.location(node.loc.end.line, node.loc.end.column);
} else if (token && value) {
const start = content.indexOf(value);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it not possible for this to false positive and exit early if the same string appears more than once in the source?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no, this is purely about handling whitespace — content can be a string like " potato " in which case value is "potato", and this just yoinks out the leading " " so that they're treated separately (and potato is treated as a token) despite it being a single context.write(...) call. is just for convenience

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants