Search code examples
typescriptmonorepo

install locally transpiled typescript package in nextjs


I'm sure this has been asked many times in multiple variations, but I just couldn't get it to work.

I have a somewhat "local" monorepo consisting of three typescript packages:

  • (nestjs-)backend
  • (nextjs-)frontend
  • and a shared package for both.

The shared package does not just declare types, but includes some logic which is why it needs to be transpiled. Because this shared package is only needed for these two apps and not publicly on npm, I decided to locally build and install the local package:

{
    "name": "@someapp/shared",
    "scripts": {
        "build": "tsc -p tsconfig.json"
    },
    "exports": {
        "import": "./dist/src/index.js"
    },
    "files": [
        "dist/src/**/*"
    ],
    "main": "./dist/src/index.js",
    "types": "./dist/src/index.d.ts",
    "dependencies": {
        "typescript": "^4.8.4"
    }
}
{
  "compilerOptions": {
    "composite": true,
    "declaration": true,
    "declarationMap": true,
    "outDir": "./dist",
    "baseUrl": "./",
    "target": "ESNext",
    "moduleResolution": "Node"
  }
}

Now running npm run build in the shared package works and outputs js files with declarations into dist. Installing it from e.g. the frontend via npm i ../shared symlinks the package into node_modules including dist. So far so correct. When building the frontend app, I get an error from webpack:

Module parse failed: Unexpected token (2:18)
You may need an appropriate loader to handle this file type, currently no loaders are configured to process this file.

Clearly, importing exports from the shared package inside the frontend app attempts to import the ts files and not the js files.

What am I doing wrong and how can I fix it?


Solution

  • So after much more trial and error while trying to get it to work in the backend, I stumbled over the fact that nestjs and nextjs are not compatible with pure ES modules, which I was outputting originally (see "target":"ESNext" in tsconfig.json.

    I had to transpile it to both ESM and CJS, but at least CJS with two separate transplations:

    //tsconfig.json acts as a base
    {
      "compilerOptions": {
        "target": "es5",
        "lib": ["dom", "dom.iterable", "esnext"],
        "allowJs": true,
        "skipLibCheck": true,
        "declaration": true,
        "forceConsistentCasingInFileNames": true,
        "esModuleInterop": true,
        "isolatedModules": true
      },
      "include": ["src/**/*.ts"],
      "exclude": ["node_modules"]
    }
    
    //tsconfig.esm.json extends tsconfig.json
    {
      "extends": "./tsconfig.json",
      "compilerOptions": {
        "module": "esnext",
        "moduleResolution": "node",
        "outDir": "dist/esm"
      }
    }
    
    //tsconfig.esm.json extends tsconfig.json
    {
      "extends": "./tsconfig.json",
      "compilerOptions": {
        "module": "commonjs",
        "outDir": "dist/cjs"
      }
    }
    

    Now, when building, transpile both esm and cjs. Plus, tell npm where to look for requires and imports:

    {
        "scripts": {
            "build": "npm run build:esm && npm run build:cjs",
            "build:esm": "tsc -p tsconfig.esm.json",
            "build:cjs": "tsc -p tsconfig.cjs.json"
        },
        "files": [
            "dist"
        ],
        "main": "./dist/cjs/index.js",
        "module": "./dist/esm/index.js",
        "types": "./dist/esm/index.d.ts",
        "exports": {
            ".": {
                "require": "./dist/cjs/index.js",
                "import": "./dist/esm/index.js",
                "types": "./dist/esm/index.d.ts"
            }
        },
    }