Once pyrpc knows where the config object starts and ends, the edit itself is a single string splice. It is the smallest change that does the job: prepend the alias as the first property inside the object's braces.
The splice
def _insert_before_close(content, open_idx, close_idx, alias):
inner = content[open_idx + 1:close_idx]
sep = "" if inner.strip() == "" else ", "
return content[:close_idx] + sep + alias + content[close_idx:]Read carefully: the insertion happens at the close brace. The alias line is inserted just before }, after whatever is already inside. If the object is empty, no separator is needed; otherwise a comma-space is added first. Every byte before the close brace is untouched.
Inserting as the last property (rather than the first) is a deliberate choice: it avoids inventing a comma that the first property would need, and it is the least likely position to collide with a trailing comma style. The object may already end with plugin: [foo()], and the splice produces plugin: [foo()], resolve: { alias: { ... } }, valid, minimal, and formatting-neutral.
The idempotency guard
Regeneration happens constantly, every watched procedure edit re-runs codegen. Running the injection again must not produce a duplicate alias:
def _already_aliased(content):
return '"@pyrpc/types"' in content and "__pyrpc.ts" in contentBefore touching anything, the whole file is scanned for both the package name and the target filename. If either marker is missing, it is safe to assume no alias exists yet. The guard is deliberately loose, a project that already wired the alias manually in some other shape is left alone rather than double-patched.
Write only when changed
The final step compares the candidate content to the original and writes the file only if they differ:
if injected != content:
with open(path, "w", encoding="utf-8") as f:
f.write(injected)This keeps the config file's mtime stable across no-op regenerations, which matters for the dev watcher: a constant stream of rewritten-but-identical config files would trigger needless reloads and editor churn.
The two injection shapes
The same splice machinery serves both frameworks via a small difference in the alias snippet:
_VITE_ALIAS = 'resolve: { alias: { "@pyrpc/types": "./__pyrpc.ts" } }'
_NEXT_ALIAS = 'turbopack: { resolveAlias: { "@pyrpc/types": "./__pyrpc.ts" } }'Vite nests under resolve.alias; Turbopack under turbopack.resolveAlias. The object is still closed by the same brace logic, so a nested object literal inside the snippet is not a problem, it is opaque text as far as the tokenizer is concerned.
The takeaway
Editing config files is a precision task. The winning approach here is minimalism: locate one object boundary with a tokenizer, splice one line at the close brace, guard for idempotency, and write only on change. Every decision (from inserting last to skipping unchanged writes) is in service of one goal: a config edit that is correct the hundredth time it runs, not just the first.

pyRPC