← Back to Blog

A mini JS tokenizer for safe config editing

·10 min read

To inject an alias into vite.config.ts or next.config.mjs, pyrpc does not parse the file into an AST and pretty-print it back. That would reformat your carefully indented config and stomp comments. Instead it runs a hand-rolled tokenizer that knows just enough about JS strings and comments to find the right place and splice one line in.

The core primitive: skipping strings and comments

Everything builds on one function. Given an index into the source, it asks: am I at the start of a string or comment? If yes, it advances past the whole thing and returns the new index:

def _skip_strings_and_comments(content, index):
    c = content[index]
    if c in ('"', "'"):
        # walk to the matching close quote, honoring backslash escapes
    if c == "`":
        # template literal: must also handle nested ${...}
    if c == "/" and next is "/":  # line comment → skip to newline
    if c == "/" and next is "*":  # block comment → skip to */

The deceptively hard part is the backtick branch. A template literal can contain ${ ... } interpolations, and inside those, more strings. The implementation recurses: when it hits ${, it jumps past the { and calls itself to skip whatever the interpolation contains. That is a mini-parser, and it exists for one reason: a { inside a string must never be mistaken for an object brace.

Finding the config object

def _find_object_after(content, start):
    # advance while skipping strings/comments; return the first '{'

After locating defineConfig( or export default, this finds the first top-level { that is not inside a string or comment. For Vite that is the options object passed to defineConfig. For Next.js it is the object after export default or after const nextConfig.

Matching braces without an AST

_match_braces walks from the opening brace, maintaining a depth counter. Strings and comments are skipped via the tokenizer, so a { or } inside a string never disturbs the count. The function returns the index of the matching close brace. This is the entire secret: you do not need a full parser to edit a config if all you do is locate one object boundary by balanced braces.

Why not a real parser

  • Output preservation. Parsing and re-printing reformats. A splice preserves every byte you did not touch, your formatting, comments, and weird spacing survive.
  • Dependency surface. A full JS parser in Python (or a Node subprocess) is heavy for a tool whose core job is RPC type generation.
  • Failure is checkable. If the mini-parser cannot find a matching structure, it returns None, and the caller surfaces a warning instead of corrupting the file.

The boundary of the approach

The tokenizer understands strings, comments, and balanced braces. It does not understand arrow functions, TypeScript generics, or object spread, it does not need to. Its contract is narrower: find the outermost object literal belonging to the config export and report where its braces sit. Staying inside that contract is what keeps the edit safe, and refusing outside it is what keeps it honest.