Skip to content

Instantly share code, notes, and snippets.

@schickling
Created January 19, 2026 07:58
Show Gist options
  • Select an option

  • Save schickling/c6484f40be38b250fab23e677461a3e2 to your computer and use it in GitHub Desktop.

Select an option

Save schickling/c6484f40be38b250fab23e677461a3e2 to your computer and use it in GitHub Desktop.
Playwright duplicate loading investigation

Playwright duplicate loading in dotdot setups

Status

Active investigation. The root cause is understood but the final workaround is not selected.

Problem statement

Playwright throws Error: Requiring @playwright/test second time when a shared config or helper package is linked via file: and both the consumer and the linked package resolve their own physical copies of @playwright/test. The Playwright runner enforces a singleton load and errors on the second physical path.

In dotdot, this is complicated by symlinked peer repos: Node resolves modules from the real path of the linked repo, not the consumer, so runtime dependencies must exist in the linked repo’s node_modules.

What we learned

  • Dotdot links peer repos via file: into the consumer’s node_modules, but Node resolution follows the real path of the symlink target.
  • If the peer repo does not have node_modules, runtime deps like effect fail to resolve when the Playwright config imports shared code.
  • Adding node_modules to the peer repo fixes runtime deps but also installs @playwright/test, creating a second physical Playwright copy and triggering the duplicate-load error.
  • A working workaround is to route configs through @overeng/utils/node/playwright/config/mod which returns a plain config object without importing @playwright/test at runtime.

Constraints

  • @playwright/test must stay a dev dependency in shared packages (for local testing).
  • NODE_OPTIONS=--preserve-symlinks does not work here because Node refuses to strip TS types under node_modules.
  • Playwright’s guard is hard-coded; there is no supported env flag to disable it.
  • The workaround relies on a config helper that returns a plain object, so consumers must import it via @overeng/utils/node/playwright/config/mod.

Reproduction

Repro repo: /Users/schickling/Code/overengineeringstudio/workspace3/peer-playwright-duplicate

shared-utils/  # imports @playwright/test
consumer/      # imports shared-utils via file:
cd shared-utils
bun install

cd ../consumer
bun install

bunx playwright test

Expected error:

Error: Requiring @playwright/test second time

Related issues

Proposed dotdot extension: shared Playwright install

This idea would allow dotdot to avoid duplicate Playwright installs while keeping @playwright/test as a dev dependency in shared packages.

Spec sketch

  • Add a new optional field to dotdot.json (root + repo-level):
    • sharedDeps: map of package name to version range
  • dotdot sync writes a dotdot.shared-deps.json manifest in the workspace root.
  • dotdot install installs shared deps into a workspace-level store:
    • e.g. .dotdot/shared-node-modules/node_modules/<package>
  • dotdot exec injects NODE_PATH pointing to the shared store so Node/Bun resolve the shared deps first.
  • dotdot exec optionally enforces that shared deps are NOT installed in peer repos (detect and warn).

Requirements

  • Must work with ESM resolution for Node and Bun.
  • Must ensure a single physical install path for singleton-sensitive packages (Playwright).
  • Must keep peer repo installs isolated for all other deps.

Constraints

  • NODE_OPTIONS=--preserve-symlinks fails for TS sources under node_modules.
  • Playwright guard cannot be disabled; single physical path is required.
  • Bun recommends isolated installs in dotdot setups (no auto-hoisting).

Open questions

  • Does Playwright’s loader honor NODE_PATH for ESM imports when run via bunx playwright?
  • Can dotdot inject NODE_PATH consistently for all test/dev commands (including IDE integrations)?
  • Should shared deps be installed per workspace or per repo group?
  • How should dotdot handle version conflicts across repos for shared deps?
  • How do we ensure shared deps are never duplicated in peer repos (warn vs auto-prune)?

Other candidate solutions

  • Install shared packages as tarballs/registry packages (avoid file: symlink resolution).
  • Inline Playwright config in consumers (avoid shared helper import).

References

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment