I had an Angular app that was building fine. Only quirks of this app is that has preserveSymlinks: true
in angular.json
and a local library installed for development using the link:
protocol in package.json
dependencies.
Everything was working until I added SSR following guide in docs by executing
ng add @angular/ssr
Then, ng build
failed with several Typescript errors. Here's an excerpt of those (including one of each kind):
✘ [ERROR] TS2688: Cannot find type definition file for 'express-serve-static-core'. [plugin angular-compiler]
node_modules/@types/express/index.d.ts:8:22:
8 │ /// <reference types="express-serve-static-core" />
╵ ~~~~~~~~~~~~~~~~~~~~~~~~~
// ...
✘ [ERROR] TS2307: Cannot find module 'body-parser' or its corresponding type declarations. [plugin angular-compiler]
node_modules/@types/express/index.d.ts:11:28:
11 │ import * as bodyParser from "body-parser";
╵ ~~~~~~~~~~~~~
// ...
✘ [ERROR] TS7006: Parameter 'req' implicitly has an 'any' type. [plugin angular-compiler]
server.ts:28:19:
28 │ server.get('*', (req, res, next) => {
╵ ~~~
// ...
✘ [ERROR] TS2339: Property 'listen' does not exist on type 'Express'. [plugin angular-compiler]
server.ts:51:9:
51 │ server.listen(port, () => {
╵ ~~~~~~
// ...
✘ [ERROR] Could not resolve "debug"
node_modules/express/lib/view.js:15:20:
15 │ var debug = require('debug')('express:view');
╵ ~~~~~~~
You can mark the path "debug" as external to exclude it from the bundle, which will remove this error and leave the unresolved path in the bundle. You can also surround this "require" call with a try/catch block to handle this failure at run-time instead of bundle-time.
Software used:
17.3.2
(latest when writing this)8.15.6
(latest when writing this)5.4.3
(latest compatible with Angular when writing this)If using pnpm
+ preserveSymlinks
in Angular CLI's build target options + SSR for an Angular application, set node-linker
to hoisted
by adding to an .npmrc
file in the repo (create it if doesn't exist):
node-linker=hoisted
Then, remove node_modules
directory and run pnpm install
again. ng build
works 🎉
You can also use
npm
instead ofpnpm
to solve the issue, but you'll loose thep
ofpnpm
😜
After trying to create a minimal reproducible example app, found out that creating a new Angular app with SSR, pnpm
as package manager was building properly:
pnpm dlx @angular/cli@17 new a17-ssr --ssr --package-manager=pnpm
pnpm run build
So decided to look for differences between both projects.
Eventually found out I had something different in angular.json
. Inside build target options (projects.{name}.architect.build.options
): preserveSymlinks
was set to true
If disabling it, the app builds properly.
However, I need that option in order to link a local NPM dependency/package. This way I can update the dependency locally and immediately see change in the app without having to reinstall the local dependency.
Tried using npm
instead of pnpm
to see if that was a package manager issue. Removed node_modules
, used npm install
and the app built. Even with preserveSymlinks
enabled 🤔 So seems something related to pnpm
installs
After a quick search, found out it is actually related to how pnpm
installs packages. Given dependencies are actually symlinked from node_modules
into a pnpm
's virtual store at node_modules/.pnpm
. And that messes up the build. Unsure why exactly TBH.
To fix it, we can change the way packages are installed by setting node-linker
configuration. See above in TL;DR for how to do it.
Switching to use file:
protocol solved the issue. Also preserveSymlinks
was no longer required in angular.json
workspace configuration. This protocol uses hard-links, so changes to the linked library installed reflect immediately in the app without having to install again. As stated in pnpm
docs it works better with peerDependencies
resolution.
pnp
node-linker
Tried quickly the pnp
node-linker
strategy with symlink
enabled, but build fails due to not found dependencies. Maybe if you properly configure Plug'n'Play it works, but didn't want to spend time on it