Typescript project references: Handle third-party dependencies of referenced project
Asked Answered
H

1

4

I have a project with 3 directories client, api, and shared. The shared directory contains typescript types and definitions inside an engine folder that I would like to share with client and api. Additionally, the shared directory requires some third party dependencies as well (specified in its package.json file).

A very basic sample repository describing this scenario can be found here, under the branch project-references. Essentially, server.js in api will import and call a function from the Entity.ts class which in turn, relies on an external library (mathjs).

Now I have been trying to use project references (rather unsuccessfully!) to build my api (I have not looked into the client yet!) so that the shared code will be available in a dist folder within the api for use.

What I have done so far:

  1. I have added the following tsconfig.build-api.json file to shared directory. The outDir points to a dist folder inside api. The idea is to move the compiled shared files to the api's dist folder, maintaining the same directory structure in dist so that my imports won't break. Similarly, I plan on having another tsconfig file for the client.
{
  "compilerOptions": {
    "lib": ["es2015", "es2017", "dom"],
    "module": "commonjs",
    "target": "es6",
    "declaration": true,
    "declarationMap": true,
    "composite": true,
    "moduleResolution": "node",
    "outDir": "../api/dist/shared"
  },
  "include": ["engine"],
  "exclude": ["node_modules", "lib"],
  "references": []
}

  1. I have updated the api's tsconfig.json file to reference the shared directory's tsconfig.build-api.json file
{
  "compilerOptions": {
    // some options
    "outDir": "dist/api/src",
  },
  "references": [
    { "path": "../shared/tsconfig.build-api.json" }
  ],
  // some more options
}
  1. I run tsc --build . from inside the api directory which creates a dist folder with the compiled api and shared code in it. However, when I try to run this built code, I get an error stating that the third party dependencies of the shared code cannot be found.

How can I make sure the third party dependencies of my referenced project (shared) are resolved without any issues inside api?

Note: Without the third party dependencies in my shared code, I could get all of this working without project references by just using relative paths to do the required imports. Thiw was explained here. However, with the external dependencies, I still cannot figure out a way to do this. Research has led to things like monorepositories, learna, yarn workspaces etc etc. But I would like to think this can be solved without relying on external tooling.

Hereof answered 13/4, 2021 at 7:12 Comment(1)
Hello @fsociety, did you find a good way to do this? I'm struggling with a similar problem... #70002616Forefather
D
1

I had to solve the same problem and with several days of head scratching and using whatever I could find online, this is the only solution that I think will work properly. It's rather fragile, in the sense that it needs a very carefully balanced folder structure to work.

I'll use the client-server-common paradigm for the discussion, both the client and the server depending on the common project. Let's start with the topmost, "solution" folder.

Apart from other settings, it needs a tsconfig.json with (to make sure TSC builds both dependent projects):

"references": [
  {
    "path": "./client"
  },
  {
    "path": "./server"
  },
 ]

And a package.json with (to make sure the running code will be able to load the common module):

"imports": {
  "#common/foobar": "./out/common/src/foobar.js"
},

Both the client and the server packages need a tsconfig with -- to make sure that they get output to a common out folder, to help TS (IDE) to find the common module and to depend on it for the build process:

"compilerOptions": {
  "baseUrl": ".",
  "rootDir": "../",
  "outDir": "../out",
  "paths": {
    "#common/*": [
      "../common/src/*.ts"
    ]
  },
},
"references": [
  {
    "path": "../common"
  }
],

Both projects can use and refer to the common in their source code as:

import { foobar } from '#common/foobar';

The common project needs a tsconfig with (also to output to the same folder, and to act as an appropriate TSC --build dependent project):

"compilerOptions": {
  "baseUrl": ".",
  "rootDir": "../",
  "outDir": "../out",
  "composite": true,
  "declaration": true,
  "declarationMap": true,
},
"references": []

The end result is a common out in the project topmost folder, with subfolders for each of the three subprojects. You can go on with packing if needed from there.

I found that if the outDir or rootDir folders have any other structure, even if that comes from the official documentation, the whole scheme falls apart. The biggest hurdle to overcome was that we would need both the source (the compiler and the analysis tools in the IDE) and the final, transpiled code to resolve the common module. While we use the same #common/foobar notation in both cases, the two have different underlying mechanisms: the toolchain uses tsconfig.json while the running code uses package.json. And while the first allows us to look outside the project folder root (with ../), the second does not. That's why I said you needed to accept this out/* structure as is, and modify your packaging or running needs to suit this folder structure, not the other way around: out with the three subfolders will work, while three subfolders with each with its out will not.

And finally, don't forget to call TSC in its build mode:

"scripts": {
  "compile": "tsc -b",

External dependencies are left for the specific case, each subproject needs its own dependencies and the topmost package.json will also need the relevant packages in its node_modules.

Darius answered 25/12, 2022 at 11:32 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.