shutil.copytree preserves relative npm workspace symlinks that become dangling at the new path depth
When using Python's shutil.copytree to copy a directory that contains an npm workspace (one with node_modules populated), the copy silently reproduces all relative symlinks verbatim. npm workspaces link local packages via relative symlinks inside node_modules (e.g., node_modules/foo -> ../../packages/foo). If the destination directory sits at a different depth in the filesystem than the source, those relative targets no longer resolve — all workspace-linked imports fail at runtime, often with cryptic MODULE_NOT_FOUND errors that give no hint that the root cause is a broken symlink.
The root cause is that shutil.copytree defaults to symlinks=False, meaning it follows symlinks and copies their content — BUT for relative symlinks in node_modules, the copy still records the original relative target path, not the resolved absolute content. The result is a symlink pointing to a path that only made sense relative to the original directory depth.
Fix: exclude node_modules entirely during the copy, then reinstall dependencies at the destination:
import shutil, subprocess
shutil.copytree(
src,
dst,
ignore=shutil.ignore_patterns("node_modules"),
)
subprocess.run(
["npm", "install", "--ignore-scripts"],
cwd=dst, check=True,
)This lets npm recreate the symlinks with paths that are correct relative to the new location.