Using TypeScript Project References with ts-loader and Webpack — Part 2

Nicholas Excell
5 min readSep 3, 2020

--

Split your app into isolated, reusable modules — Adobe Stock #106318228

This guide follows on from Part 1, so if you haven’t read that you already you should go back and read that first.

Where left off, we had a working project using TypeScript project references, webpack and ts-loader. The tips below allow you to:

  • Turn your components into packages which can be imported just like npm packages.
  • Use yarn workspaces to automate this process.
  • Publish your components into an npm repository using semantic versioning.
  • Add Lerna for projects which require extra build or test steps

Using package.json

We can clean this up further by including a package.json in the project reference subfolder. If this contains the following:

//packages/reference1/package.json{
"name": "reference1",
"version": "1.0.0",
"description": "Project Reference1",
"main": "lib/index.js",
"directories": {
"lib": "lib"
},
"license": "ISC"
}

then you can just import as follows:

// src/index.tsimport { Meaning } from 'packages/reference1';

The module setting in package.json tells the bundler to import from lib/index.js when it sees the import statement above.

Using node_modules

In the above approach we need to add paths to tsconfig.json so that the module resolution knows where to find our package. But the module resolution system automatically looks in node_modules, so if we link our reference in node_modules we won’t need the paths and aliases:

ln -s ../packages/reference1 node_modules/reference1
node_modules/reference1 -> packages/reference1

It probably makes sense to use npm scopes:

ln -f ../../packages/reference1 node_modules/@myscope/reference1
node_modules/@myscope/reference1 -> packages/reference1

then you can consume the code with:

// src/index.tsimport { Meaning } from '@myscope/reference1';

So you benefit from not having to configure paths and aliases, but you need to create the links in node_modules after cloning the project, unless …

Using Yarn Workspaces

If you use yarn workspaces the node_modules links will automatically be created for you when you execute yarn install . Simply include the following in your root level package.json :

{
"private": true,
"workspaces": ["packages/*"]
}

In the subproject’s package.json you should use the name of the package you want to be linked in node_modules:

//packages/reference1/package.json{
"name": "@myscope/reference1",
"version": "1.0.0",
"module": "lib/index.js
}

When you run yarn install the links in node_modules will be created for you.

You can now use your project references anywhere in your codebase with a simple import statement, exactly like you import npm modules. If you have a more complex application, for example with client and server applications, you can share modules easily.

Building a Component Library

A common problem in code organisation is how to re-use code in multiple projects. Project references help toward this goal by providing a logical separation between components. This will mean you can drop a component into another project and use it. But there is still the matter of how you do this:

  • You could copy the project reference folder into all top-level projects you want to use it in. This has the disadvantage that you end up with multiple copies of code. If you patch or enhance a component you need to copy the patch to all the other projects, rebuild them and test.
  • Another approach would be to symlink the component into each top-level project. The downside of this is that once you amend the component you could break all of the projects which depend on it.

A smarter solution is to publish the components as npm packages. You can use semantic versioning each time you publish using a version in the format major.minor.patch. You then add the components to other projects using yarn add @myscope/reference1.

Versioning works exactly the same as any other npm package. You specify in the consuming project’s tsconfig.json what version changes are acceptable:

"@myscope/reference1": "1.0.1",   // Only version 1.0.1 can be used
"@myscope/reference1": "~1.0.1", // Patch updates are acceptable
"@myscope/reference1": "^1.0.1", // Minor version changes are OK

You can then update and publish new versions of the component with new version numbers. The other project will not be broken as it will continue to use the version specified in its package.json. When you are ready to update you can use the same yarn tools you would use to update any package (yarn outdated / upgrade / upgrade-interactive or the npm equivalents).

If you want to keep your packages private you can set up your own private npm repository with Verdaccio or you can use Github Packages:

Lerna

If your project references are complex and have their own scripts for testing and building you could use Lerna. This works well with yarn workspaces and the project structure outlined above. If you have a test script in reference1 you could use the following command to execute it:

lerna run --scope=reference1 test

The same command without the --scope argument would execute the test scripts in all subprojects.

Yarn workspaces and Lerna introduce more power but also more complexity in the workflow. They are not required to use project references so it is up to you whether the extra learning curve they introduce is worthwhile.

Build Times in Development

Using ts-loader and webpack-dev-server, when you change a file in one of the project references ts-loader will automatically rebuild the reference and include the change in the new bundle. Rebuilding the reference may take a few seconds. By comparison, when you change a file in the root source (non-reference) webpack will get ts-loader to rebuild just that file and create a new bundle very quickly, typically less than 1 second.

So if you are developing code in a reference and find the few seconds it takes to rebuild the reference too much, you could benefit from importing from the TypeScript source directly. This will be at the expense of longer warm start times as you will not be using the pre-built code for that referenced project.

Example Repository

An example repo using the configuration above is available at the link below:

https://github.com/appzuka/project-references-example

--

--