Yarn workspaces -- package alias

9.3k Views Asked by At

TL;DR How can I create an alias for a local yarn workspace dependency?

I've tried yarn workspaces before and never succeeded, and I'm giving it another try.

I've set "workspaces": ["packages/*"] in package.json.

For each package, I decided to use the naming convention @-/package-name to prevent naming conflicts and without worrying about having namespaces for internal packages.

When adding packages as dependencies, I've been following a style where I use an interface name for resolution, but point that towards a concrete implementation. This is what I did before using yarn workspaces:

"dependencies": {
  "my-interface-name": "file:some/path/to/packages/some-concrete-implementation"
}

This is basically to allow what I like to call compile-time static dependency injection. And it also means each package can individually name their interface dependencies appropriately to their need and to prevent naming conflicts.

However, I can't figure out how to accomplish this with yarn workspaces. How do I create an alias for my yarn workspaces package @-/some-concrete-implementation called my-interface-name?

What I've already tried with no success:

  • Defining the dependency like "my-interface-name": "@-/some-concrete-implementation"} - for some reason this causes yarn to look for @-/some-concrete-implementation on the npm registry instead of in the local workspace
  • I've also tried to use the workspace protocol: "my-interface-name": "workspace:@-/some-concrete-implementation"} but it still looks for the package on the npm registry!

What I haven't yet tried and could work but removes benefits of using yarn workspaces in the first place:

  • "dependencies": {"my-interface-name": "file:../../node_modules/@-/some-concrete-implementation"}"
2

There are 2 best solutions below

2
On

The workspace: alias protocol (available in pnpm too) seems the direction to take.

I've also tried to use the workspace protocol: "my-interface-name": "workspace:@-/some-concrete-implementation"} but it still looks for the package on the npm registry!

Be sure to have yarn 3 installed, otherwise you'll run into weird issues.

Note that the syntax of "my-interface-name": "workspace:@-/some-concrete-implementation" looks incorrect.

It should be "@xxx/some-concrete-implementation": "workspace:*", assuming the name of linked the package is "name": "@xxx/some-concrete-implementation".

With this in mind you don't even need to create a specific @-/name. With workspace protocol, yarn will ensure it's never downloaded from npm. It becomes an internal workspace dependency.

PS:


Yarn 3 installation

Generally a simple yarn set version 3.0.2 && yarn plugin import workspace-tools) will work.

To avoid pnp current limitation, check the generated config .yarnrc.yml and ensure nmLinker is set to 'node-modules'

# Yarn 2+ supports pnp or regular node_modules installs. Use node-modules one.
nodeLinker: node-modules
plugins:
  - path: .yarn/plugins/@yarnpkg/plugin-workspace-tools.cjs
    spec: "@yarnpkg/plugin-workspace-tools"
yarnPath: .yarn/releases/yarn-3.0.2.cjs

PS: you might want to add this to .gitignore too

.yarn/*
!.yarn/patches
!.yarn/releases
!.yarn/plugins
!.yarn/sdks
!.yarn/versions
.pnp.*

Run a yarn install just after.

About package.json's

Like you did, the root package.json will define the workspace paths:

{
  "name": "monorepo",
  "workspaces": [
    "packages/*"  // Enable package discovery in packages/* directory.
  ],
  // ... 
  "devDependencies": {
    "husky": "7.0.2", // Only what's needed for monorepo management
  }

In your app packages/app/package.json

{
  "name": "my-app",
  "devDependencies": {
    "@types/node": "16.10.1",
    //...
  },
  "dependencies": {
    // Assuming the name of packages/shared is "@your-org/some-concrete-implementation",
    // we explicitly declare the dependency on it through
    // workspace: alias (package-manager perspective)
    "@your-org/some-concrete-implementation": "workspace:*",
  }
}

You consumed package should declare the same name

{
  "name": "@your-org/some-concrete-implementation",
}


Bonus: Typescript aliases

If your project is written in ts, you can even replicate your paths through typescript path mapping. It will allow to include the files just as is (no prior compilation needed).

Following your example, just edit a ./packages/xxx/tsconfig.json in this way

{
  "compilerOptions": {
    // here baseUrl is set at ./src (good practice), can
    // be set to '.'  
    "baseUrl": "./src",
    "paths": {
      // Declare deps here (keep them in sync with what
      // you defined in the package.json)
      // PS: path are relative to baseUrl
      "@your-org/some-concrete-implementation/*": ["../../some-concrete-implementation/src/*"],
      // if you have a barrel in ui-lib 
      "@your-org/some-concrete-implementation": ["../../some-concrete-implementation/src/index"],
    }
  },
}

PS: for non typescript: babel/plugin-module-resolver can be used in a similar manner.

7
On

Have you seen the resolutions package.json key? Is it what you need?

I've used it for aliasing/overriding external packages but the example in the docs shows it working with local packages.

Resolutions

Allows you to override a version of a particular nested dependency. See the Selective Versions Resolutions RFC for the full spec.

{
  "resolutions": {
    "transitive-package-1": "0.0.29",
    "transitive-package-2": "file:./local-forks/transitive-package-2",
    "dependencies-package-1/transitive-package-3": "^2.1.1"
  }
}

From the RFC:

"**/a" denotes all the nested dependencies a of the project.

"a" is an alias for **/a (for retro-compatibility, see below, and because if it wasn't such an alias, it wouldn't mean anything as it would represent one of the non-nested project dependencies, which can't be overridden as explained below).

So, I believe the rule you need is:

"**/my-interface-name": "file:some/path/to/packages/some-concrete-implementation"

// OR equivalent

"my-interface-name": "file:some/path/to/packages/some-concrete-implementation"

I believe it works in the package's package.json. Worst case you can hoist it to the workspace root and make the rule specific to the workspace e.g. "a/b".