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.

pyRPC