When pyrpc dev needs to know what frontend framework you use, it does not ask. It looks at your filesystem. The presence of a next.config.ts or avite.config.js is a far more reliable signal than anything a user might type, and the detection logic is tiny enough to live in one function.
The signature table
Detection starts from a flat mapping of config filenames to canonical framework labels:
_FRAMEWORK_SIGNATURES: list[tuple[str, str]] = [
("next.config.ts", "Next.js"),
("next.config.js", "Next.js"),
("next.config.mjs", "Next.js"),
("nuxt.config.ts", "Nuxt"),
("nuxt.config.js", "Nuxt"),
("svelte.config.js", "Svelte"),
("svelte.config.ts", "Svelte"),
("vite.config.ts", "Vite"),
("vite.config.js", "Vite"),
("astro.config.mjs", "Astro"),
]Every framework above has exactly one unavoidable config file, the file its own tooling requires at the project root. That makes the table stable: it will only grow when a framework adds a new supported config extension, and it will never produce false positives the way scanning package.json dependencies would.
Checking one directory
Checking a single directory is a linear scan of the table:
def _detect_framework(root: str) -> str | None: """Return framework_label if a known config file is found.""" for filename, label in _FRAMEWORK_SIGNATURES: if (Path(root) / filename).exists(): return label return None
The order matters. next.config.ts is checked before next.config.jsbecause a project that has both is using TypeScript and should be labeled accordingly. It also does no content parsing, existence is the signal, which keeps the check cheap and immune to config-file syntax changes.
Walking the tree, pruning as you go
The setup wizard and dev --yes need to find frontends anywhere in the project, not just at the root. _find_frontend_projects does a full tree walk, but it prunes the directories it descends into while walking, so it never even entersnode_modules or your virtualenv:
def _find_frontend_projects(root: str) -> list[tuple[str, str]]:
_skip = {"node_modules", "__pycache__", ".venv", "venv", "env", "dist", "build", ".git", ".next"}
projects = []
for dirpath, dirnames, filenames in os.walk(root):
dirnames[:] = [d for d in dirnames if d not in _skip and not d.startswith(".")]
fw = _detect_framework(dirpath)
if fw:
rel = os.path.relpath(dirpath, root)
if rel == ".":
projects.append((".", fw))
else:
projects.append((f"./{rel}", fw))
return projectsTwo details are easy to miss. First, dirnames[:] = [...] mutates the listos.walk uses to plan its descent, pruned directories are neverstat-ed, let alone scanned. Second, the skip set includes".next" (Next.js build output) and any dot-directory, which matters because a.next directory inside the tree might otherwise contain something that looks like a config. Results are normalized to "./rel" form (or "." for the root itself) so they can be written straight into pyrpc.json.
Where the signal feeds in
Detection powers three behaviors:
- The wizard, pre-fills client root and framework when exactly one project is found, or offers multi-select when several are.
dev --yes, with exactly one detected project, config is written with no prompts at all.- Error quality, with several projects and no explicit
--client,dev --yesrefuses to guess and lists the candidates with the exact flag to disambiguate.
The framework label ends up as a single informational field in pyrpc.json. It is a record of the setup decision, not a runtime dependency, the generated types and the tsconfig alias work the same regardless of framework, which is exactly why the detection can afford to be a lightweight heuristic.
Read the full changelogfor the complete list of changes.

pyRPC