UNPKG

21.5 kBMarkdownView Raw
1
2# Using TypeScript Project References with ts-loader and webpack
3
4Project References were added to TypeScript in 3.0. The benefits of using project references include:
5
6* Better code organisation
7* Logical separation between components
8* Faster build times
9
10If you are using TypeScript in your web project you can also use project references to improve your code and build workflow. This article describes some of the ways to set up your project to use references. I am using ts-loader to transpile the TypeScript code to JavaScript and webpack to bundle code.
11
12An example repo using the configuration above is available at the link below:
13
14[https://github.com/appzuka/project-references-example](https://github.com/appzuka/project-references-example)
15
16There are 2 stages to using project references in your project:
17
181. Configure and build the project references
191. Setup your codebase to consume the compiled projects
20
21To gain an understanding of how project references work, for the first part of this guide we will use <code>tsc</code> to build the project references. Later on, we will configure ts-loader to do this automatically.
22
23### Configure and build the project references
24
25This stage just involves following the directions from the TypeScript documentation:
26[https://www.typescriptlang.org/docs/handbook/project-references.html](https://www.typescriptlang.org/docs/handbook/project-references.html)
27
28There are a few points to note:
29
301. Referenced projects must have the new composite setting enabled.
311. Each referenced project has its own <code>tsconfig.json</code>
321. There will be a root level <code>tsconfig.json</code> which includes the lower level projects as references. Building this will build all subprojects.
331. You should be using configuration file inheritance (<code>{ “extends”: …}</code>) to avoid duplication in your config.
341. You need to use <code>tsc --build</code> to compile the project.
351. When you compile the project <code>tsc --build</code> will create a file called tsconfig.tsbuildinfo that contains the signatures and timestamps of all files required to build the project. On subsequent builds TypeScript will use that information to detect the least costly way to type-check and emit changes to your project.
361. There is no need to use the incremental compiler option. <code>tsc --build</code> will generate and use tsconfig.tsbuildinfo anyway.
371. If you delete your compiled code and re-run <code>tsc --build</code> the code will **not **be rebuilt unless you also delete the <code>tsconfig.tsbuildinfo</code> file. Use the <code>tsc --build --clean</code> command to do this for you.
38
391. If you set the <code>declaration</code> and <code>declarationMap</code> settings in <code>tsconfig.json</code> the <code>outDir</code> folder will contain <code>.d.ts</code> and <code>.d.ts.map</code> files alongside the transpiled JavaScript. When you consume the compiled project you should consume the <code>outDir</code> folder, not the <code>src</code>. Even though your root project is in TypeScript it can use full syntax checking without the subproject’s TypeScript source because the <code>outDir</code> folder contains the definitions in the <code>.d.ts</code> file. Vscode (and many other code editors and IDEs) will be able to find the definitions and perform syntax checking in the editor just as if you were not using project references and importing the TypeScript source directly.
40
41### Project Structure
42
43The TypeScript implementation of project references allows you to structure the project in almost any way you wish. Just configure the input and output folders in tsconfig.json to your needs and TypeScript will build it for you.
44
45For a web project you might like a structure similar to the one below. You could put all your project references in a packages folder with the top-level project code in src:
46```
47 tsconfig.json
48 tsconfig-base.json
49 src
50 - (source code for the main project)
51 dist
52 - main.js (final bundle produced by webpack)
53 packages
54 - reference1
55 - tsconfig.json (inherits from tsconfig-base.json)
56 - src
57 - lib
58 - reference2
59 - tsconfig.json (inherits from tsconfig-base.json)
60 - src
61 - lib
62```
63Each project reference has its own <code>tsconfig.json</code> with the source code for each package in a <code>src</code> subfolder. When the project is built the compiled JavaScript for each project will be in its <code>lib</code> subfolder.
64
65The source code for your main project is in a top-level <code>src</code> folder and the final bundle will be in a top-level <code>dist</code> folder. The top-level <code>src</code> folder is not a referenced project — it is normal TypeScript source that webpack will bundle. It imports from the <code>lib</code> folders of the referenced projects built by <code>tsc</code>.
66
67This structure works well because:
68
69* Having packages grouped together under a packages folder organises your codebase nicely.
70* Other tools such as yarn workspaces and lerna use and understand this organisation.
71* Each package is fully self-contained in its own folder. It contains the source, compiled code, tsconfig.json and (optionally) its own <code>package.json</code> which describes how the package is used.
72* You can drop the package into another project, import it with a simple statement and everything will be linked up.
73
74This is just one way to structure your project. Some other options include:
75
76* Not putting the projects references in a packages folder. They could all be at the top level, or a different folder, or nested folders.
77* The output folder of each project does not have to be in a lib folder of that project. You could have a top-level lib folder which contains the output of all projects.
78
79Almost any structure is compatible with project references. You have freedom to specify the paths of the referenced projects and their outputs in the <code>tsconfig.json</code> files. You will import the compiled JavaScript files into the main project and some structures make this easier than others, but you have the freedom to choose what works for you.
80
81### Test Build your Projects
82
83You should now check that the building of the projects is successful and produces the code you expect.
84
85In each project reference folder execute <code>tsc --build</code>, check there are no errors and the output is as you expect. Use <code>tsc --build --clean</code> to remove the output and repeat. You can use <code>tsc --build --verbose</code> to see what <code>tsc</code> is doing.
86
87If you have a top-level <code>tsconfig.json</code> similar to:
88```
89 {
90 "files": ["src/index.ts"],
91 "references": [
92 { "path": "./reference1" },
93 { "path": "./reference2" }
94 ]
95 }
96```
97Then executing <code>tsc --build</code> in the top-level will compile all of your subprojects with one command. The build process is smart and can manage dependencies between subprojects.
98
99In the final step of this guide we will get ts-loader to do the build automatically when called from webpack, but for now, just make sure that the build process works when using <code>tsc --build</code> manually.
100
101### Setup your codebase to consume the project
102
103Now your subprojects are built you can use them in your root project. Let’s say your reference1 project exports a number:
104```
105 // packages/reference1/src/index.ts
106
107 export const Meaning = 42;
108```
109After building the reference with <code>tsc --build</code> the compiled JavaScript will be found in <code>packages/reference1/lib/index.js</code>. In your root project you need to import this. There are several ways you can do this. Let’s start with a naive approach that will work but has severe downsides:
110```
111 // src/index.ts
112
113 // Don't do this!
114 import { Meaning } from '../packages/reference1/lib';
115```
116This will work because TypeScript and webpack will both find the file. The downsides are:
117
118* The organisation of your root project and components are now intertwined. If you change the internal structure of your subproject you will need to update every import statement in the entire project.
119
120* The import location will depend on the location on the source file. For example, if you want to do the same import from a subfolder in your root project you will need to replace <code>../packages/reference1/lib</code> with <code>../../packages/reference1/lib</code>. If you re-organise your project structure you will need to fix every import.
121
122The solution to this is module resolution — how TypeScript and webpack resolve the targets of import statements. You can read about this at the links below:
123
124* [https://www.typescriptlang.org/docs/handbook/module-resolution.html](https://www.typescriptlang.org/docs/handbook/module-resolution.html)
125* [https://webpack.js.org/concepts/module-resolution](https://webpack.js.org/concepts/module-resolution)
126
127Module resolution is nothing new and it is not part of project references, but understanding it will be a huge help getting everything working. Some points to note:
128
129* TypeScript and webpack can use different methods to resolve modules. It will help if you can set them up so they are using the same method. (See the example below using alias in webpack and/or tsconfig-paths-webpack-plugin.)
130* Resolution works differently for relative (<code>./reference1</code>) and absolute (<code>reference1</code>) imports.
131* TypeScript has 2 strategies for module resolution: <code>classic</code> and <code>node</code>. You probably want to use <code>node</code>.
132* You can use a webpack plugin <code>tsconfig-paths-webpack-plugin</code> so that you just need to define paths in your <code>tsconfig.json</code> and then don’t need to repeat these in your webpack config.
133
134Using the example above, we would like to just import from <code>packages/reference</code> and have TypeScript and webpack both know that this refers to the actual location.
135```
136 // src/index.ts
137
138 // Better!
139 import { Meaning } from 'packages/reference1/lib';
140```
141We can achieve this using the paths configuration in <code>tsconfig.json</code> (or better, in <code>tsconfig-base.json</code> so the settings are made once and inherited by all projects):
142```
143 {
144 "compilerOptions": {
145 "baseUrl": ".", // This must be specified if "paths" is.
146 "paths": {
147 "packages/*": ["packages/*"]
148 }
149 }
150 }
151```
152Now TypeScript understands that when it sees <code>packages/reference1</code> in an import statement, it should look in <code>./packages/reference1</code>. The path is relative to the root <code>tsconfig.json</code> so it does not matter where the source file which imports this is located.
153
154Unless you are using tsconfig-paths-webpack-plugin you may need to include a corresponding resolve-alias setting in your <code>webpack.config.js</code>:
155```
156 const path = require('path');
157
158 module.exports = {
159 modules: [
160 "node_modules",
161 path.resolve(__dirname)
162 ],
163 resolve: {
164 alias: {
165 packages: path.resolve(__dirname, 'packages/'),
166 }
167 }
168 };
169```
170(In this case the <code>path.resolve(__dirname)</code> in the modules section accomplishes the same thing, but depending on your project structure you may need an alias.)
171
172If you are getting module not found errors when you build, knowing whether these are coming from TypeScript or webpack will help you to resolve the issue.
173
174Errors which come from TypeScript when you build the project look similar to the following:
175```
176 ERROR in ...project-references-demo/src/index.tsx
177 ./src/index.tsx
178 [tsl] ERROR in ...project-references-demo/src/index.tsx(1,27)
179 TS2307: Cannot find module 'mypackages/zoo' or its corresponding type declarations.
180```
181Note the <code>[tsl]</code> in the message and also the TypeScript error code <code>TS2307</code>. This indicates that the error was passed to webpack by ts-loader when it tried to transpile the file. You can also check whether errors are coming from TypeScript by building your project manually with <code>tsc</code> and checking whether it reports errors.
182
183Errors from webpack look similar to the following:
184```
185 ERROR in ./src/index.tsx
186 Module not found: Error: Can't resolve 'mypackages/zoo' in '...project-references-demo/src'
187 @ ./src/index.tsx 6:12-37
188```
189If you just get these errors it indicates that <code>tsconfig.json</code> is correctly configured and TypeScript is able to resolve your modules, but webpack is not. Look into the resolve section of <code>webpack.config.js</code> and check whether you need to add an alias.
190
191You can use module resolution to make your project work with project references even if your structure is very different from that outlined here. As long as webpack and TypeScript can find the built code it will work.
192
193### Can you import the TypeScript Source instead of the JavaScript?
194
195You can import the TypeScript source from your projects, but you probably should not. If you do set up your project to import the TypeScript, webpack will bundle your project just fine, but then you are not using project references. You have succeeded in organising your codebase but you are not getting the advantage of reducing build time by using the compiled files in <code>lib</code>. In fact, you are slowing down your build by requiring tsc or ts-loader to build the reference and then not using it.
196
197If your project is large you could see a significant benefit from pre-building large sections of code. If your project is not so large you may prefer to just structure your codebase and skip project references.
198
199### Using ts-loader to build project references
200
201Up to this point, we ran <code>tsc --build</code> on its own and then used webpack and ts-loader to build the whole project, importing the references. You can configure ts-loader to build the references for you, which simplifies the build process.
202
203The top-level project in <code>src</code> is TypeScript code, so you will already be using ts-loader to load the TypeScript source into webpack. Just add <code>projectReferences: true</code> to the ts-loader configuration and you no longer need to run <code>tsc</code> in a separate process:
204```
205 // webpack.config.js
206
207 "module": {
208 "rules": [
209 {
210 "test": /\.tsx?$/,
211 "exclude": /node_modules/,
212 "use": {
213 "loader": "ts-loader",
214 "options": {
215 "projectReferences": true
216 }
217 }
218 }
219 ]
220 }
221```
222When webpack uses ts-loader to process a TypeScript file ts-loader will now check whether any of your project references need rebuilding and rebuild them before webpack proceeds if necessary. This includes when webpack is in watch mode as used by webpack-dev-server.
223
224Setting <code>projectReferences: true</code> in ts-loader alone will not magically convert your code to use project references. All it does is to run <code>tsc --build</code> as part of the build process. You need to configure project references and structure your project to use them as described here.
225
226If you have come this far congratulations — you are now using TypeScript project references in your web project. You can stop here, but in the next section of this guide there are some tips to clean up the project further and create a library of reusable, version-controlled components.
227
228### Using package.json
229
230We can clean this up further by including a <code>package.json</code> in the project reference subfolder. If this contains the following:
231```
232 //packages/reference1/package.json
233
234 {
235 "name": "reference1",
236 "version": "1.0.0",
237 "description": "Project Reference1",
238 "main": "lib/index.js",
239 "directories": {
240 "lib": "lib"
241 },
242 "license": "ISC"
243 }
244```
245then you can just import as follows:
246```
247 // src/index.ts
248
249 import { Meaning } from 'packages/reference1';
250```
251The module setting in <code>package.json</code> tells the bundler to import from <code>lib/index.js</code> when it sees the import statement above.
252
253### Using node_modules
254
255In the above approach we need to add paths to <code>tsconfig.json</code> so that the module resolution knows where to find our package. But the module resolution system automatically looks in <code>node_modules</code>, so if we link our reference in <code>node_modules</code> we won’t need the paths and aliases:
256```
257 ln -s ../packages/reference1 node_modules/reference1
258 node_modules/reference1 -> packages/reference1
259```
260It probably makes sense to use npm scopes:
261```
262 ln -f ../../packages/reference1 node_modules/@myscope/reference1
263 node_modules/@myscope/reference1 -> packages/reference1
264````
265then you can consume the code with:
266```
267 // src/index.ts
268
269 import { Meaning } from '@myscope/reference1';
270```
271So 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 you use Yarn workspaces.
272
273### Using Yarn Workspaces
274
275If you use yarn workspaces the <code>node_modules</code> links will automatically be created for you when you execute <code>yarn install</code>. Simply include the following in your root level <code>package.json</code>:
276``
277 {
278 "private": true,
279 "workspaces": ["packages/*"]
280 }
281``
282In the subproject’s <code>package.json</code> you should use the name of the package you want to be linked in node_modules:
283```
284 //packages/reference1/package.json
285
286 {
287 "name": "@myscope/reference1",
288 "version": "1.0.0",
289 "module": "lib/index.js
290 }
291```
292When you run yarn install the links in <code>node_modules</code> will be created for you.
293
294You 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.
295
296### Building a Component Library
297
298A 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:
299
300* 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.
301* 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.
302
303A 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 <code>yarn add @myscope/reference1</code>.
304
305Versioning works exactly the same as any other npm package. You specify in the consuming project’s <code>tsconfig.json</code> what version changes are acceptable:
306```
307 "@myscope/reference1": "1.0.1", // Only version 1.0.1 can be used
308 "@myscope/reference1": "~1.0.1", // Patch updates are acceptable
309 "@myscope/reference1": "^1.0.1", // Minor version changes are OK
310```
311You 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 <code>package.json</code>. When you are ready to update you can use the same yarn tools you would use to update any package (<code>yarn outdated / upgrade / upgrade-interactive</code> or the npm equivalents).
312
313If you want to keep your packages private you can set up your own private npm repository with [Verdaccio](https://verdaccio.org/) or you can use [Github Packages](https://github.com/features/packages)
314
315### Lerna
316
317If your project references are complex and have their own scripts for testing and building you could use [Lerna](https://lerna.js.org/). 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:
318```
319 lerna run --scope=reference1 test
320```
321The same command without the <code>--scope</code> argument would execute the test scripts in all subprojects.
322
323Yarn 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.
324
325### Build Times in Development
326
327Using 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.
328
329So 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.
330
331
332
\No newline at end of file