node.jstypescriptyarn-workspaces

How to use relative paths in typescript shared package with Yarn workspaces


When using Yarn workspaces with one TS module depending on another, node isn't looking in the /dist folder, so cannot find the js files it needs to run.

More detail:

I'm using Yarn workspaces with the following structure:

tsconfig.json
package.json
packages/
  shared/
    index.ts
    package.json
    tsconfig.json
    interfaces/
      index.ts
apps/
  server/
    index.ts
    package.json
    tsconfig.json

Within apps/server, I want to import something from shared/interfaces/index.js, so I write import X from 'shared/interfaces'.

However, the way my typescript is compiled, the shared package in the node_modules folder once installed looks like this:

dist/
  index.js
  index.d.ts
  interfaces/
    index.js
    index.d.ts
interfaces/
  index.ts
index.ts
index.js

So if I import from just the index i.e. import X from 'shared', it's all fine. However, if I try to do import Y from 'shared/interfaces' I get an ERR_MODULE_NOT_FOUND error (because it seems node is trying to pick up interfaces/index.js instead of 'knowing' to look for dist/interfaces/index.js instead.

How can I get node to look in the dist folder for a relative import?

My files

Root package.json

{
  "name": "supperclub",
  "version": "1.0.0",
  "main": "index.js",
  "license": "MIT",
  "private": true,
  "workspaces": ["apps/*", "packages/*"],
  "devDependencies": {
  }
}

Root tsconfig.json

{
  "references": [
    {
      "path": "apps/server"
    },
    {
      "path": "packages/shared"
    }
  ],
  "compilerOptions": {
    "target": "es2016",       
    "module": "commonjs", 
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true,
    "strict": true, 
    "skipLibCheck": true,
  }
}

Shared package.json

{
  "name": "shared",
  "version": "1.0.0",
  "main": "dist/index.js",
  "types": "dist/index.d.ts",
  "license": "MIT",
  "scripts": {
    "compile": "tsc --build"
  },
  "files": ["dist/"],
  "module": "CommonJS"
}

Shared tsconfig.json

{
  "compilerOptions": {
    "composite": true,
    "target": "es2016",
    "module": "commonjs",
    "esModuleInterop": true, 
    "forceConsistentCasingInFileNames": true,
    "strict": true,
    "skipLibCheck": true,
    "outDir": "./dist",
    "declaration": true,
    "declarationMap": true,
    "sourceMap": true,
  }
}

server package.json

{
  "name": "server",
  "version": "1.0.0",
  "main": "dist/index.js",
  "license": "MIT",
  "type": "module",
  "engines": {
    "node": ">=14.20.1 <19"
  },
  "scripts": {
    "dev": "nodemon index.js",
  },
  "devDependencies": {
    "typescript": "^4.5.4"
  },
  "dependencies": {
    "supper-club-shared": "*",
  },
  "references": [
    {
      "path": "../../packages/supper-club-shared"
    }
  ]
}

server tsconfig.json

{
  "compilerOptions": {
   
    "target": "es2017",
    "module": "es6",
    "moduleResolution": "node",
    "baseUrl": "./",
    "resolveJsonModule": true,
    "sourceMap": true,
    "outDir": "./dist",
    "esModuleInterop": true,
    "strict": true,
    "skipLibCheck": true,
    "composite": true
  },
  "references": [{"path": "../../packages/supper-club-shared"}],
  "include": ["src/**/*"]
}


Solution

  • Per nodejs docs you can use subpath exports as described here.

    "exports": {
        ".": "./dist/index.js",
        "./interfaces": "./dist/interfaces/index.js"
      },
    

    so on packages.json you might include the following:

    {
      "name": "server",
      "version": "1.0.0",
      "main": "dist/index.js",
     "exports": {
        ".": "./dist/index.js",
        "./interfaces": "./dist/interfaces/index.js"
      },
      "license": "MIT",
      "type": "module",
      "engines": {
        "node": ">=14.20.1 <19"
      },
      "scripts": {
        "dev": "nodemon index.js",
      },
      "devDependencies": {
        "typescript": "^4.5.4"
      },
      "dependencies": {
        "supper-club-shared": "*",
      },
      "references": [
        {
          "path": "../../packages/supper-club-shared"
        }
      ]
    }
    

    Then you can import:

    import .... from '{LIBNAME}/{MODULENAME}'