Skip to content

fix(yarn-plugin-external-workspaces): support purely-internal externals#4117

Draft
Saadnajmi wants to merge 1 commit intomicrosoft:mainfrom
Saadnajmi:fix/yarn-plugin-external-workspaces-purely-internal
Draft

fix(yarn-plugin-external-workspaces): support purely-internal externals#4117
Saadnajmi wants to merge 1 commit intomicrosoft:mainfrom
Saadnajmi:fix/yarn-plugin-external-workspaces-purely-internal

Conversation

@Saadnajmi
Copy link
Copy Markdown
Contributor

@Saadnajmi Saadnajmi commented May 1, 2026

Summary

Two issues prevented @rnx-kit/yarn-plugin-external-workspaces (v0.1.3) from working with externals that exist locally but were never published to npm:

1. getFinderFromJsonConfig reads from a doubly-nested generated.generated

configuration.ts:93-96 reads:

const parsedJson: WorkspaceOutputJson =
  JSON.parse(fs.readFileSync(jsonPath, "utf8"))?.generated || {};
const generated: Partial<WorkspaceOutputGeneratedContent> =
  typeof parsedJson.generated === "object" ? parsedJson.generated : {};

The first line strips one generated layer, then the second line strips another. The README (and the WorkspaceOutputJson type in types.ts) both document the JSON shape as a single layer:

{ "generated": { "version": "1", "repoPath": "...", "workspaces": {...} } }

With that documented shape, parsedJson.generated is undefined, so generated becomes {}, and findPackage always returns null. Externals silently fall through to npm.

Drop the extra ?.generated so the documented shape works as advertised.

2. Resolver chain hard-fails for packages not published to npm

The external: → fallback: → npm: chain (workspace.ts) is wired such that yarn must resolve the npm step before resolution succeeds. The README says:

automatically falling back to npm semver lookups when the local files are not present

— but the design also runs the npm step when the local files are present (for version coalescing). For purely-internal monorepo packages that simply don't exist on npm, the chain throws 404 even though we have a perfectly good local source.

Add a purely-local fast path: when isLocal === true and resolverType === "local", getCandidates short-circuits to a direct external: locator (the fetcher already resolves it via workspace.localPath). getResolutionDependencies returns {} in the same case so yarn doesn't pre-resolve a non-existent chain.

Test plan

Validated against a real Office monorepo workspace consuming three internal externals from a sibling monorepo on disk:

  • yarn install completes cleanly (2066 packages installed in ~38s, no 404s)
  • node_modules/<internal-package> is a symlink to ../..//`
  • dist/index.js (sharedjs's pre-built artifacts) resolves through the symlink
  • No npm registry calls for the purely-internal packages

Without these fixes the install fails at:

<internal-package>s@npm:^1.3.1: Package not found
Response Code: 404 (Not Found)

Notes

  • No tests in the package today; happy to add a npm-404 + local-present regression test if you'd like.
  • The dist/ bundle is gitignored, so the bundled output isn't part of this PR — you'll regenerate at release time.

Two issues prevented the plugin from working with externals that exist
locally but were never published to npm:

1. getFinderFromJsonConfig read 'generated.generated.{...}' from the
   config JSON, but the README + WorkspaceOutputJson type document the
   shape as 'generated.{...}'. The double-nested access caused repoPath
   and workspaces to always be empty, so findPackage always returned null
   and lookups silently fell through.

2. The 'external: -> fallback: -> npm:' resolver chain hard-fails at the
   npm step for packages that don't exist on the registry. The chain
   exists to support semver coalescing against npm when externals also
   live remote, but for purely-internal monorepo packages there is no
   remote version to coalesce against. Add a fast path: when isLocal is
   true and the local resolver is called, skip the chain and return a
   direct external: locator. The fetcher already resolves locally via
   workspace.localPath. getResolutionDependencies returns {} in the same
   case so yarn doesn't pre-resolve a chain that won't terminate.

Validated against an Office monorepo workspace consuming three internal
externals from a sibling monorepo on disk; yarn install completes
cleanly, externals symlink to the sibling repo's package directories,
and dist/index.js resolves through the symlinks.
@Saadnajmi Saadnajmi marked this pull request as draft May 1, 2026 03:00
@Saadnajmi
Copy link
Copy Markdown
Contributor Author

Normally Claude adds itself a co-author, but it didn't here. Regardless, marking as draft till I test more.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant