Switch to Workspaces and Project References

If you want to take advantage of the performance benefits of TypeScript project references, it is recommended to use package manager workspaces for local project linking. If you are currently using TypeScript path aliases for project linking, follow the steps in this guide to switch to workspaces project linking and enable TypeScript project references.

Enable Package Manager Workspaces

Follow the specific instructions for your package manager to enable workspaces project linking.

package.json
1{ 2 "workspaces": ["apps/*", "libs/*"] 3} 4

Defining the workspaces property in the root package.json file lets npm know to look for other package.json files in the specified folders. With this configuration in place, all the dependencies for the individual projects will be installed in the root node_modules folder when npm install is run in the root folder. Also, the projects themselves will be linked in the root node_modules folder to be accessed as if they were npm packages.

If you reference a local library project with its own build task, you should include the library in the devDependencies of the application's package.json with * specified as the library's version. * tells npm to use whatever version of the project is available.

/apps/my-app/package.json
1{ 2 "devDependencies": { 3 "@my-org/some-project": "*" 4 } 5} 6

Update Root TypeScript Configuration

The root tsconfig.base.json should contain a compilerOptions property and no other properties. compilerOptions.composite and compilerOptions.declaration should be set to true. compilerOptions.paths and compilerOptions.rootDir should not be set.

Note: Before you delete the paths property, copy the project paths for use as references in the tsconfig.json file.

tsconfig.base.json
1{ 2 "compilerOptions": { 3 "allowJs": false, 4 "allowSyntheticDefaultImports": true, 5 // ... 6 "paths": { 7 "@myorg/utils": ["libs/utils/src/index.ts"], 8 "@myorg/ui": ["libs/ui/src/index.ts"] 9 } 10 } 11} 12

The root tsconfig.json file should extend tsconfig.base.json and not include any files. It needs to have references for every project in the repository so that editor tooling works correctly.

tsconfig.json
1{ 2 "extends": "./tsconfig.base.json", 3 "files": [] // intentionally empty 4} 5

Register Nx Typescript Plugin

Make sure that the @nx/js plugin is installed in your repository and @nx/js/typescript is registered as a plugin in the nx.json file.

nx.json
1{ 2 "plugins": [ 3 { 4 "plugin": "@nx/js/typescript", 5 "options": { 6 "typecheck": { 7 "targetName": "typecheck" 8 }, 9 "build": { 10 "targetName": "build", 11 "configName": "tsconfig.lib.json", 12 "buildDepsName": "build-deps", 13 "watchDepsName": "watch-deps" 14 } 15 } 16 } 17 ] 18} 19

This plugin will register a sync generator to automatically maintain the project references across the repository.

Create Individual Project package.json files

When using package manager project linking, every project needs to have a package.json file. You can leave all the task configuration in the existing project.json file. For application projects, you only need to specify the name property. For library projects, you should add an exports property that accounts for any TypeScript path aliases that referenced the project. A typical configuration is shown below:

libs/ui/package.json
1{ 2 "name": "@myorg/ui", 3 "exports": { 4 ".": { 5 "types": "./src/index.ts", 6 "import": "./src/index.ts", 7 "default": "./src/index.ts" 8 } 9 } 10} 11
Package Names with Multiple Slashes

The package.json name can only have one / character in it. This is more restrictive than the TypeScript path aliases. So if you have a project that you have been referencing with @myorg/shared/ui, you'll need to make the package.json name be something like @myorg/shared-ui and update all the import statements in your codebase to reference the new name.

Update Individual Project TypeScript Configuration

Each project's tsconfig.json file should extend the tsconfig.base.json file and list references to the project's dependencies. Remove any compilerOptions listed and combine them with the options listed in the tsconfig.lib.json and tsconfig.spec.json files.

The tsconfig.json file's purpose is to provide your IDE with references to the tsconfig.*.json files that define the compilation settings for all the files in the project. In this case, tsconfig.spec.json handles the compilation of the test files and tsconfig.lib.json handles the compilation of the production code.

libs/ui/tsconfig.json
1{ 2 "extends": "../../tsconfig.base.json", 3 "files": [], // intentionally empty 4 "references": [ 5 // All project dependencies 6 // UPDATED BY NX SYNC 7 // This project's other tsconfig.*.json files 8 { 9 "path": "./tsconfig.lib.json" 10 }, 11 { 12 "path": "./tsconfig.spec.json" 13 } 14 ] 15} 16

Each project's tsconfig.lib.json file extends the root tsconfig.base.json file and adds references to the tsconfig.lib.json files of project dependencies. This file should not extend the project's tsconfig.json file because the tsconfig.json file includes a reference to the tsconfig.spec.json file. Keeping the tsconfig.spec.json file unreferenced from the tsconfig.lib.json file makes the typecheck and build tasks faster because the test files do not need to be analyzed. Note that the outDir location needs to be unique across all tsconfig.*.json files so that one task's cached output does not interfere with another task's cached output.

Shared Compiler Options

If there are a lot of shared compilerOptions between tsconfig.lib.json and tsconfig.spec.json, you could create a tsconfig.project.json that contains those shared settings. tsconfig.project.json would extend tsconfig.base.json while tsconfig.lib.json and tsconfig.spec.json would each extend tsconfig.project.json.

libs/ui/tsconfig.lib.json
1{ 2 "extends": "../../tsconfig.base.json", 3 "compilerOptions": { 4 // outDir should be local to the project and not in the same folder as any other tsconfig.*.json 5 "outDir": "./out-tsc/lib" 6 // Any overrides 7 }, 8 "include": ["src/**/*.ts"], 9 "exclude": [ 10 // exclude config and test files 11 ], 12 "references": [ 13 // tsconfig.lib.json files for project dependencies 14 // UPDATED BY NX SYNC 15 ] 16} 17
Task Outputs Within the Project

As part of this migration process, we are moving the task outputs for typecheck and build to be local to the project instead of being output to a root dist folder. This structure is more consistent with a workspaces style repository and helps to keep projects self-contained. It should be possible to continue to send task outputs to a root dist folder, but you'll need to make sure that the outDir and exports paths work correctly for your folder structure.

The project's tsconfig.spec.json does not need to reference project dependencies.

libs/ui/tsconfig.spec.json
1{ 2 "extends": "../../tsconfig.base.json", 3 "compilerOptions": { 4 // outDir should be local to the project and not in the same folder as any other tsconfig.*.json 5 "outDir": "./out-tsc/spec" 6 // Any overrides 7 }, 8 "include": [ 9 // test files 10 ], 11 "references": [ 12 // tsconfig.lib.json for this project 13 { 14 "path": "./tsconfig.lib.json" 15 } 16 ] 17} 18

After creating these tsconfig.*.json files, run nx sync to have Nx automatically add the correct references for each project.

Vite Configuration Updates

If you are using Vite to build a project, you need to update the vite.config.ts file for each project.

  1. Remove the nxViteTsPaths plugin from the plugins array.
  2. Set the build.outDir to ./dist relative to the project's folder.
  3. Make sure the build.lib.name matches the full name of the project, including the organization.
libs/ui/vite.config.ts
1import react from '@vitejs/plugin-react'; 2import dts from 'vite-plugin-dts'; 3import { nxCopyAssetsPlugin } from '@nx/vite/plugins/nx-copy-assets.plugin'; 4 5export default defineConfig({ 6 // ... 7 plugins: [ 8 // any needed plugins, but remove nxViteTsPaths 9 react(), 10 nxCopyAssetsPlugin(['*.md', 'package.json']), 11 dts({ 12 entryRoot: 'src', 13 tsconfigPath: path.join(__dirname, 'tsconfig.lib.json'), 14 }), 15 ], 16 build: { 17 // ... 18 outDir: './dist', 19 // ... 20 lib: { 21 name: '@myorg/ui', 22 // ... 23 }, 24 }, 25}); 26

Future Plans

We realize that this manual migration process is tedious. We are investigating automating parts of this process with generators.