47

I'm trying to set up a monorepo using yarn. I'm confused as to how to set up typescript with project references such that things resolve properly.

For example, if I have a folder structure like

/cmd
/client

And I want cmd to depend on client I could have:

cmd/tsconfig.json:

{
  "compilerOptions": {
    "types": ["reflect-metadata", "jest"],
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true,
    "moduleResolution": "node",
    "declaration": true,
    "importHelpers": true,
    "composite": true,
    "target": "esnext"
    "sourceRoot": "src",
    "outDir": "dist"
  },
  "references": [
    {
      "path": "../client"
    }
  ],
  "include": [
    "src/**/*"
  ]
}

with a package.json

{
  "name": "cmd",
  "version": "1.0.0",
  "dependencies": {
    "client": "^1.0.0",
  }
}

In this model both cmd and client get compiled with an outDir and sourceRoot field set in their tsconfig. This means all their compiled javascript goes into the dist/ subfolder of cmd/dist and client/dist

If now I try and reference a class from client into cmd like

import Foo from 'client/src/foo'

The IDE is perfectly happy to resolve this since it seems that its mapped via the typescript references property.

However, the compiled javascript boils down to a

const foo_1 = require("client/src/foo");

However, the actual built javascript is in client/dist/src/foo, so at runtime this never works.

On the flip side, if I don't use sourceRoots and outDirs and have the javascript inlined with the typescript files at the same folder everything does work (but makes the repo dirty and requires custom gitignores to exclude things)

Can anyone shed any light on how to properly set up a typescript 3.x monorepo with yarn workspaces such that things just work?

tk421
  • 5,775
  • 6
  • 23
  • 34
devshorts
  • 8,572
  • 4
  • 50
  • 73
  • 1
    Sidenote: I used [lerna](https://github.com/lerna/lerna) to setup a monorepo and found it quite worthile and it should also work with yarn. (I used it with npm.) – k0pernikus Sep 10 '19 at 11:53
  • I remember dealing with something like this. If you want to post a repository link, that reproduces the problem, I'll take a look. – Burt_Harris Oct 01 '19 at 02:43
  • 1
    take a look at this repo. They have done it quit well. look through it https://github.com/apollographql/apollo-client – m.akbari Oct 01 '19 at 11:03

2 Answers2

105

I created a Github Repository to make it easier to follow the following code description:


Code Description

TypeScript Project References make it possible to compile a TypeScript project that consist of multiple smaller TypeScript projects, each project having a tsconfig.json file. (Source: Project References Documentation)


TypeScript Setup

We have a root tsconfig.json file that only manages its sub-projects. The references property specifies the directories that each contain a valid tsconfig.json file. If we now build the project with the --build option (tsc --build tsconfig.json) then we specified the projects which should be compiled, but we didn't specified the build order in which the projects should be compiled.

{
  "references": [
    { "path": "./client" },
    { "path": "./cmd" }
  ],
  "files": [],
  "include": [],
  "exclude": ["**/node_modules"]
}

To correctly specify the build order we need to add a references property to the cmd/tsconfig.json file. This tells the compiler that it first needs to compile client/ before we compile cmd/:

cmd/tsconfig.json:

{
  "extends": "../tsconfig.packages.json",
  "compilerOptions": {
    "rootDir": "src",
    "outDir": "dist"
  },
  "references": [
    {
      "path": "../client"
    }
  ]
}

Build order

client/
  ^
  |
  |
 cmd/

Node Setup

Best practice is that each sub-project has its own package.json file with the main property and the name set. In our example both packages (cmd/ and client/) have a main property pointing to the index.js file in the TypeScript outDir directory (cmd/dist/index.js and client/dist/index.js).

Project structure:

tsconfig.json
cmd/
    tsconfig.json
    package.json
    src/
        index.ts
    dist/  #artifacts
        index.js
client/
    tsconfig.json
    package.json
    src/
        index.ts
    dist/  #artifacts
        index.js

client/packages.json

{
  "name": "client",
  "version": "1.0.0",
  "main": "dist/index",
  ...
}

It is important that we add the client/ as dependency to the cmd/packages.json so the module resolution algorithm can find the client/dist/index.js when we import it in our TypeScript code import Foo from 'client';:

cmd/packages.json

{
  "name": "cmd",
  "version": "1.0.0",
  "main": "dist/index",
  "dependencies": {
    "client": "1.0.0" // important
  }
}

cmd/src/index.ts

import Foo from 'client';

console.log(Foo())

Yarn Setup

The yarn setup is easy. Yarn adds all packages under node_modules instead of:

  • cmd/node_modules
  • client/node_modules

To enable yarn workspaces add the workspaces property and the private: true property to the <root>/package.json file.

<root>/package.json

{
  "private": true,
  "workspaces": [
    "cmd",
    "client"
  ],
  "name": "yarn_workplace",
  "version": "1.0.0"
  ...
}

The cmd/ and client/ packages are symlinked under the <root>/node_modules/ directory:

yarn node_modules


Notes

  • To enable code navigation one has to first build the project
  • Every package lives on its own. The cmd/ package uses the definition file client/dist/index.d.ts for type information instead of using the the TypeScript files directly.
Community
  • 1
  • 1
a1300
  • 2,633
  • 2
  • 14
  • 18
  • @devshorts is this working for you? Or are you missing something in my solution? – a1300 Oct 09 '19 at 05:34
  • 2
    great answer. since I posted the question i've already long since set it up this way as well. My original problem was not understanding that you gotta have the 'dist' folder and if you want the imports to be clean you need to export them from the index module. – devshorts Jan 13 '21 at 19:49
  • Very nice writeup! I did not find a use for the root-level tsconfig file. Calling tsc on it doesn't build the packages, and it seems redundant unless you want to use it as a base config to inherit from. Since each package still needs their own references setting I don't see that the purpose of the root-level references is. – Thijs Koerselman Jun 20 '21 at 12:50
  • If you add a "sub" folder with an "index.ts" in the "src" of the "client" project, and you try to import it from the other project, you get the following error: "TS2307: Cannot find module 'client/sub'". Would you know how to fix it? – Finickyflame Oct 22 '21 at 15:22
  • @Finickyflame how are you importing the package? Is the importing project a JavaScript or TypeScript project? – a1300 Oct 23 '21 at 17:13
  • @a1300 it's a typescript project. I just took your example and tried to add sub dependencies. I was able to make it work by using the "compilerOptions:paths" instead of using "references" in the tsconfig.json – Finickyflame Oct 25 '21 at 14:25
-1

I've set up a monorepo I with some configurations I've always use in my projects using Yarn Workspaces and Typescript in this repository.

I don't know if this set up solves your problem, but you don't need to specify your packages at Typescript config. When you use Yarn Workspaces, it links all your packages in root node_modules that you defined in your workspace package property in your root package.json:

"workspaces": {
    "packages": [
        "packages/**/*"
    ],
    "nohoist": []
}

After yarn install, the root node_modules has client and cmd as linked folder.

enter image description here

With this configuration, you can simply import any package in any package inside the Workspace. For instance:

// cmd/src/index.ts

import { name } from 'client';

const otherName = 'cmd' + name;

console.log(otherName);
Pedro Arantes
  • 5,113
  • 5
  • 25
  • 60
  • 1
    Build won't work from cmd if you didn't previously build the client. error TS2307: Cannot find module 'client'. – gremo Oct 01 '19 at 19:30
  • Did you run `yarn` at the root before try building? – Pedro Arantes Oct 01 '19 at 19:41
  • Yes, of course. Remove all "dist" dir and try yourself. – gremo Oct 01 '19 at 19:51
  • I see. With the configuration I propose, you must not remove `dist` folders, it's part of your code. Maybe I completely misunderstood the question and if `dist` shouldn't be part of the repository, my solution won't work. – Pedro Arantes Oct 01 '19 at 20:13