SugarMouse
SugarMouse

Reputation: 203

How to use monorepo packages with nestjs using turborepo?

Here's what a real simple monorepo with nestjs using turborepo looks like:

.
├── README.md
├── apps
│   └── nest
│       ├── README.md
│       ├── nest-cli.json
│       ├── package.json
│       ├── src
│       │   ├── app.controller.spec.ts
│       │   ├── app.controller.ts
│       │   ├── app.module.ts
│       │   ├── app.service.ts <---- importing class here
│       │   └── main.ts
│       ├── test
│       │   ├── app.e2e-spec.ts
│       │   └── jest-e2e.json
│       ├── tsconfig.build.json
│       └── tsconfig.json
├── package.json
├── packages
│   └── lib
│       ├── index.ts <------- exporting class here
│       ├── package.json
│       └── tsconfig.json
├── pnpm-lock.yaml
├── pnpm-workspace.yaml
└── turbo.json

A class is defined in packages/lib/index.ts and is then imported in apps/nest/src/app.service.ts.

But doing so leads to the following error when trying to import this index.ts:

nest:dev: 
nest:dev: [1:40:25 AM] Found 0 errors. Watching for file changes.
nest:dev: 
nest:dev: /Users/hercule/Workspace/monorepo-nestjs-package/packages/lib/index.ts:3
nest:dev:   public hello() {
nest:dev:          ^^^^^
nest:dev: 
nest:dev: SyntaxError: Unexpected identifier
nest:dev:     at Object.compileFunction (node:vm:360:18)
nest:dev:     at wrapSafe (node:internal/modules/cjs/loader:1088:15)
nest:dev:     at Module._compile (node:internal/modules/cjs/loader:1123:27)
nest:dev:     at Object.Module._extensions..js (node:internal/modules/cjs/loader:1213:10)
nest:dev:     at Module.load (node:internal/modules/cjs/loader:1037:32)
nest:dev:     at Function.Module._load (node:internal/modules/cjs/loader:878:12)
nest:dev:     at Module.require (node:internal/modules/cjs/loader:1061:19)
nest:dev:     at require (node:internal/modules/cjs/helpers:103:18)
nest:dev:     at Object.<anonymous> (/Users/hercule/Workspace/monorepo-nestjs-package/apps/nest/src/app.service.ts:2:1)
nest:dev:     at Module._compile (node:internal/modules/cjs/loader:1159:14)

Therefore, How do we import a typescript package into nestjs that will be compiled / parsed properly?

Note 1: I tried importing this lib (index.ts) to a nextjs app and angular app, it works without any issue. The problem only arises with nestjs

Note 2: Above's example can be reproduced using following repository: https://github.com/beneccli/monorepo-nestjs-package, once cloned, at root of the project, simply run pnpm i and then pnpm dev (or npm or yarn).

Upvotes: 8

Views: 10578

Answers (3)

Huboh
Huboh

Reputation: 135

So, Nextjs and Angular uses bundlers configured to understand typescript source code from packages in node_modules directory (this is where your internal packages are symlinked and resolved to by node) While NestJS, by default, uses tsc for compilation and tsc doesn't expect typescript source code from packages under node_modules directory.

You can enable this behavior in NestJS apps by configuring it to use webpack instead of the default tsc for compilation with a little bit of setup - I would not recommend this, instead, pre-compile your internal packages to javascript before it’s imported into the NestJS application (this strategy is known as “compiled packages”. Learn more here https://turbo.build/repo/docs/core-concepts/internal-packages#compilation-strategies)

Next.js and Angular apps (though I'm not certain about Angular) use just-in-time packages that export typescript source code because their respective bundlers can handle it. NestJS's default compiler, tsc, isn't set up for that.

You can resolve this by either pre-compiling your internal packages or switching to a Webpack-based setup in your NestJS app package.

1. Using compiled internal packages from the consuming NestJS app package (preferred method)

I prefer this method because it speeds up your build time significantly in a large codebase, as your package's build step will hit cache if its source code is unchanged.

step 1: Update your internal package's package.json

Add build and start:dev scripts to your internal package's package.json:

// packages/helloworld/package.json

{
    "name": "@packages/helloworld",
    "scripts": {
        "build": "tsc --build",
        "start:dev": "tsc --watch"
    },
    "exports": {
        "./*": {
            "types": "./src/*.ts",
            "default": "./dist/*.js"
        }
    }
}

step 2: Add a tsconfig.json to your internal package

// packages/helloworld/tsconfig.json

{
    "compilerOptions": {
        "outDir": "./dist",
        "rootDir": "./src",
        "declaration": true,
        "declarationMap": true,
        "sourceMap": true,
        "module": "CommonJS", 
        "target": "ES2020"
    }
}

step 3: Then, in your NestJS app package, import and use the compiled javascript code directly:

// apps/nestjs/src/app.module.ts

import { MyInternalPackage } from "@packages/helloworld/dir-under-src-dir";

2. Using just-in-time compilation for internal packages by switching your NestJS app’s builder to webpack and using the swc-loader

Alternatively, you can configure your NestJS app to use Webpack with swc-loader, which allows TypeScript code to be transpiled on the fly, similar to the Next.js and Angular apps.

Learn more about using webpack in NestJS here (https://docs.nestjs.com/recipes/swc#monorepo).

step 1: Setup an internal package that exposes a webpack config that you can install and extend in your nestjs application packages. Create a package.json for the new webpack config package:

// packages/webpack-config/package.json

{
    "name": "@packages/webpack-config",
    "files": [
        "webpack.config.js"
    ],
   "peerDependencies": {
        "@nestjs/cli": "^10.1.8"
   }
}

step 2: Install necessary dependencies

npm i -D swc-loader —workspace @packages/webpack-config

step 3: Create a webpage config in the internal package folder so your nest application packages can consume it

// packages/webpack-config/webpack.config.js

const swcDefaultConfig = require("@nestjs/cli/lib/compiler/defaults/swc-defaults").swcDefaultsFactory()
    .swcOptions;

module.exports = {
  node: {
    // required for __dirname to properly resolve
    // Also required for `bull` to work, see https://github.com/OptimalBits/bull/issues/811
    __dirname: true,
  },
  module: {
    rules: [
      {
        test: /\.ts$/,
        exclude: (_) =>
          /node_modules/.test(_) && !/node_modules\/(@package)/.test(_),
        use: {
          loader: "swc-loader",
          options: swcDefaultConfig,
        },
      },
    ],
  },
};

step 4: Configure your NestJS application packages to use your internal Webpack config by installing and extending it.

Update your NestJS application package's nest-cli.json:

// apps/nestjs/nest-cli.json

{
    "compilerOptions": {
        "builder": "webpack"
    }
}

step 5: Create a webpack.config.js in your NestJs application package and extend the internal webpack package:

// apps/nestjs/webpack.config.js
module.exports = require("@packages/webpack-config/webpack.config");

Upvotes: 2

Dang Pham
Dang Pham

Reputation: 51

I clone a repo and see the following things in your packages/lib/package.json :

  • Your packages ( lib exactly ) does not have a build script so I added one.
  • Lib main does not point to the dist/index.js build so I modified the main too.

Final lib's package.json is here

{
  "name": "lib",
  "version": "0.0.0",
  "main": "./dist/index.js",
  "scripts": {
    "build": "tsc"
  },
  "types": "./index.ts",
  "devDependencies": {
    "typescript": "^4.5.2"
  }
}

After those are done, I run the script pnpm run build in the root.

Then I run pnpm dev, and your apps should start working on http://localhost:3000/ with the text Test: Hello World!

The thing behind that is you should have a build script to compile your Typescript code into JS, then have the main to point exactly the compiled index. So that when NestJs use your lib, it should have all the functions and classes in it.

Upvotes: 1

SugarMouse
SugarMouse

Reputation: 203

The solution is actually simple.

Issue was that the dependency was not transpiled from typescript to javascript.

To solve this it is required to tell how to build the dependency and where to find the result files. This is done using the package.json of the lib dependency:

// packages/lib/package.json
{
  "name": "lib",
  "version": "0.0.0",
  "main": "./dist/index", <----- NEW
  "types": "./dist/index", <----- NEW
  "scripts": {
    "build": "tsc --build --force tsconfig.json" <----- NEW
  },
  "devDependencies": {
    "typescript": "^4.5.2"
  }
}

I updated the previously given repository accordingly.

What is important here is to not forget to run pnpm build after pnpm install in order to get the proper built dependency. Then nestjs can be started in either dev or prod.

Upvotes: 0

Related Questions