Upgrading to Astro 4.2.4 & Node.js 20.11.0

Upgrading to Astro 4.2.4 & Node.js 20.11.0

The Figures portfolio was created using the Astro version 2.0.10. Astro releases new versions quite fast, so a few months later, we decided to upgrade to version 4.2.0 (which was the newest at the time).

To make this change, I also had to change the Node version used to deploy the website. This is when it started to get a bit more complicated since the new version of Astro requires a different version of Node.

8:21:11 AM: error sharp@0.33.1: The engine "node" is incompatible with this module. 
Expected version "^18.17.0 || ^20.3.0 || >=21.0.0". Got "18.16.0”
4:25:39 PM: error @nanostores/preact@0.5.0: The engine "node" is incompatible with this module.
Expected version "^16.0.0 || ^18.0.0 || >=20.0.0". Got "18.17.0”

After multiple tries, I concluded that the project needed a Node version of at least 20. The problem is that our project uses the script command "process-images": "ts-node --esm ./node/resizeImages.ts", which relies on ts-node and the esm flag, which were broken in Node version 20.

$ npm run build
12:49:50 PM: > forum-new@0.9.8 build
12:49:50 PM: > npm run process-images && astro build
12:49:51 PM: Failed during stage "building site": Build script returned non-zero exit code: 2
12:49:51 PM: > forum-new@0.9.8 process-images
12:49:51 PM: > ts-node --esm ./node/resizeImages.ts
12:49:51 PM: TypeError [ERR_UNKNOWN_FILE_EXTENSION]: Unknown file extension ".ts" for /opt/build/repo/node/resizeImages.ts
12:49:51 PM: at Object.getFileProtocolModuleFormat [as file:] (node:internal/modules/esm/get_format:160:9)
12:49:51 PM: at defaultGetFormat (node:internal/modules/esm/get_format:203:36)
12:49:51 PM: at defaultLoad (node:internal/modules/esm/load:141:22)
12:49:51 PM: at async nextLoad (node:internal/modules/esm/hooks:749:22)
12:49:51 PM: at async nextLoad (node:internal/modules/esm/hooks:749:22)
12:49:51 PM: at async Hooks.load (node:internal/modules/esm/hooks:382:20)
12:49:51 PM: at async handleMessage (node:internal/modules/esm/worker:199:18) {
12:49:51 PM: code: "ERR_UNKNOWN_FILE_EXTENSION"
12:49:51 PM: }
12:49:51 PM: ****
12:49:51 PM: "build.command" failed

As mentioned in the documentation of ts-node: Node's ESM loader hooks are experimental and subject to change. ts-node's ESM support is as stable as possible, but it relies on APIs which node can and will break in new versions of node. Thus it is not recommended for production.

After doing some research on the topic, I found some conversations on GitHub, which explained, that while the maintainers of Node.js are aware that people are using these features, it was always considered an experimental use of Node.js, which is why they made breaking changes, when the need for it arrived.

"As of node v20, loader hooks are executed in a separate isolated thread environment. As a result, they are unable to register the require.extensions hooks in a way that would (in other node versions) make both CJS and ESM work as expected."

"Got some time to dig into this. The issue with ts-node --esm blah.mts seems to have something to do with lateBindHooks. In node before v20, this works fine, but in v20, it doesn't pick them up. Still tracing through to try to figure out why that is."

"The fact is, this broke the ts-node esm loader, which is a widespread way to use TypeScript. It's not a small niche use case that just happens to affect me. Please come to the TypeScript discord and hang out for a few days, I guarantee you'll see someone coming in there to complain that ts-node doesn't work on node 20."

In those GitHub threads, I also found a new way to write the script command: "process-images": "node --loader ts-node/esm ./node/resizeImages.ts", which works with the new Node version.

After this, a new problem occurred with the sharp package:

5:19:56 PM: Error: Could not load the "sharp" module using the linux-x64 runtime
5:19:56 PM: Possible solutions:
5:19:56 PM: - Ensure optional dependencies can be installed:
5:19:56 PM: npm install --include=optional sharp
5:19:56 PM: yarn add sharp --ignore-engines
5:19:56 PM: - Ensure your package manager supports multi-platform installation:
5:19:56 PM: See https://sharp.pixelplumbing.com/install#cross-platform
5:19:56 PM: - Add platform-specific dependencies:
5:19:56 PM: npm install --os=linux --cpu=x64 sharp
5:19:56 PM: - Consult the installation documentation:
5:19:56 PM: See https://sharp.pixelplumbing.com/install
5:19:56 PM: at Object.<anonymous> (/opt/build/repo/node_modules/sharp/lib/sharp.js:114:9)
5:19:56 PM: at Module._compile (node:internal/modules/cjs/loader:1376:14)
5:19:56 PM: at Module._extensions..js (node:internal/modules/cjs/loader:1435:10)
5:19:56 PM: at Module.load (node:internal/modules/cjs/loader:1207:32)
5:19:56 PM: at Module._load (node:internal/modules/cjs/loader:1023:12)
5:19:56 PM: at Module.require (node:internal/modules/cjs/loader:1235:19)
5:19:56 PM: at require (node:internal/modules/helpers:176:18)
5:19:56 PM: at Object.<anonymous> (/opt/build/repo/node_modules/sharp/lib/constructor.js:10:1)
5:19:56 PM: at Module._compile (node:internal/modules/cjs/loader:1376:14)
5:19:56 PM: at Module._extensions..js (node:internal/modules/cjs/loader:1435:10)
5:19:56 PM: Node.js v21.1.0
5:19:56 PM: ****
5:19:56 PM: "build.command" failed
5:19:56 PM: ────────────────────────────────────────────────────────────────
5:19:56 PM:
5:19:56 PM: Error message
5:19:56 PM: Command failed with exit code 1: npm run build (**https://ntl.fyi/exit-code-1**)

Unfortunately, the suggested solutions of running yarn add sharp --ignore-engines or npm install --os=linux --cpu=x64 sharp did not give any successful results on netlify, despite working locally. I then realized that running yarn add sharp --ignore-engines did not make any changes to the package.json file. Therefore, doing it locally should not influence the way the project is built on Netlify. I searched for a way to do it there as well.

I modified the build command in the netlify.toml file of the project to npm install --force && npm run build, unfortunately, that failed as well. I then tried to combine multiple options shown by the error message npm install --include=optional sharp && npm install --legacy-peer-deps --force &&e && npm run build, which now led to a new error.

3:11:01 PM: $ npm install --include=optional sharp && npm install --force && npm run build 
3:11:05 PM: Failed during stage "building site": Build script returned non-zero exit code: 2 
3:11:05 PM: npm ERR! code ERESOLVE 
3:11:05 PM: npm ERR! ERESOLVE could not resolve 
3:11:05 PM: npm ERR! 
3:11:05 PM: npm ERR! While resolving: astro-netlify-cms@0.5.4 
3:11:05 PM: npm ERR! Found: astro@4.2.1 
3:11:05 PM: npm ERR! node_modules/astro 
3:11:05 PM: npm ERR! peer astro@"^3.0.0 || ^4.0.0" from @astrojs/tailwind@5.1.0 
3:11:05 PM: npm ERR! node_modules/@astrojs/tailwind 
3:11:05 PM: npm ERR! dev @astrojs/tailwind@"^5.1.0" from the root project 
3:11:05 PM: npm ERR! peer astro@"^1.2.1 || ^2.0.0 || ^3.0.0-beta.0 || ^3.0.0 || ^4.0.0" from @astrolib/seo@1.0.0-beta.5 
3:11:05 PM: npm ERR! node_modules/@astrolib/seo 
3:11:05 PM: npm ERR! dev @astrolib/seo@"^1.0.0-beta.5" from the root project 
3:11:05 PM: npm ERR! 1 more (the root project) 
3:11:05 PM: npm ERR! 
3:11:05 PM: npm ERR! Could not resolve dependency: 
3:11:05 PM: npm ERR! peer astro@"^1.0.0 || ^2.0.0-beta || ^3.0.0-beta" from astro-netlify-cms@0.5.4 
3:11:05 PM: npm ERR! node_modules/astro-netlify-cms 
3:11:05 PM: npm ERR! astro-netlify-cms@"^0.5.3" from the root project 
3:11:05 PM: npm ERR! 3:11:05 PM: npm ERR! Conflicting peer dependency: astro@3.6.4 
3:11:05 PM: npm ERR! node_modules/astro 
3:11:05 PM: npm ERR! peer astro@"^1.0.0 || ^2.0.0-beta || ^3.0.0-beta" from astro-netlify-cms@0.5.4 
3:11:05 PM: npm ERR! node_modules/astro-netlify-cms 
3:11:05 PM: npm ERR! astro-netlify-cms@"^0.5.3" from the root project 
3:11:05 PM: npm ERR! 
3:11:05 PM: npm ERR! Fix the upstream dependency conflict, or retry 
3:11:05 PM: npm ERR! this command with --force or --legacy-peer-deps 
3:11:05 PM: npm ERR! to accept an incorrect (and potentially broken) dependency resolution.

This indicates a conflict between the version of Astro required by different packages in the project. The version of astro-netlify-cms that I had installed (0.5.3) required the Astro version ^1.0.0 || ^2.0.0-beta || ^3.0.0-beta, However, I wanted to use the latest Astro version. I then decided to use the newest version of astro-netlify-cms, which was 0.5.4, to see if it would be compatible with the newest version of Astro. I also checked if any new version of Astro had come out in the meantime (since I switched to other projects in between) and upgraded it to version 4.2.4.

Upgrading to the newest version of astro-netlify-cms did not help, which was predictable since there did not seem to be any major change in the latest astro-netlify-cms vso it seemed pretty unlikely that they would have added the newest Astro version in it.

This means that it’s not possible to use astro-netlify-cms with the newest Astro version. This was good timing since we were thinking of refactoring the way the CMS is implemented (to allow a higher level of customization) for which we would need to remove this package anyway.

I, therefore, refactored the code so that it does not use this package anymore.

This solved the error for this package, but I now had import problems for other packages. The warnings said that I was importing elements that did not exist. Since it worked fine with the previous package versions, I looked at our old package.json file and decided to downgrade the problematic packages (@fontsource/inter and astro-icon).

This brought the building progress further than it had ever been before, as pages actually started building. But this was not the end.

Error: [astro-icon] Unable to load icon "codicon:star-full"!
Error: Unknown builtin plugin "cleanupIDs" specified.

I found a conversation on GitHub that showed that I was not the only one with this problem. One user recommended adding the svgo version 2.8.0, explaining that this would solve a version conflict related to svgo. So I tried it out.

AND. IT. WORKED.

So, that only took approximately 3 days.

Conclusion: if you’re planning on upgrading anything in your project, be ready for update hell.