electron-react-boilerplate: Electron 28 breaks build: ERR_UNKNOWN_FILE_EXTENSION – Unknown file extension ".ts" for …

Prerequisites

  • Using npm
  • Using an up-to-date main branch
  • Using latest version of devtools. Check the docs for how to update
  • Tried solutions mentioned in #400
  • For issue in production release, add devtools output of DEBUG_PROD=true npm run build && npm start

Expected Behavior

Updating electron should make the app start.

Current Behavior

I updated Electron to 28.0.0, then ran npm install. Now:

➜ npm start

> start
> ts-node ./.erb/scripts/check-port-in-use.js && npm run start:renderer


> start:renderer
> cross-env NODE_ENV=development TS_NODE_TRANSPILE_ONLY=true webpack serve --config ./.erb/configs/webpack.config.renderer.dev.ts

Starting preload.js builder...
Starting Main Process...
<i> [webpack-dev-server] Project is running at:
<i> [webpack-dev-server] Loopback: http://localhost:1212/
<i> [webpack-dev-server] On Your Network (IPv4): http://192.168.0.119:1212/
<i> [webpack-dev-server] On Your Network (IPv6): http://[fe80::1]:1212/
<i> [webpack-dev-server] Content not from webpack is served from '/Users/werner/Documents/Software/electron-react-boilerplate/public' directory
<i> [webpack-dev-server] 404s will fallback to '/index.html'

> start:main
> cross-env NODE_ENV=development electronmon -r ts-node/register/transpile-only .


> start:preload
> cross-env NODE_ENV=development TS_NODE_TRANSPILE_ONLY=true webpack --config ./.erb/configs/webpack.config.preload.dev.ts

[electronmon] waiting for a change to restart it
App threw an error during load
TypeError [ERR_UNKNOWN_FILE_EXTENSION]: Unknown file extension ".ts" for /Users/werner/Documents/Software/electron-react-boilerplate/src/main/main.ts
    at new NodeError (node:internal/errors:405:5)
    at Object.getFileProtocolModuleFormat [as file:] (node:internal/modules/esm/get_format:80:11)
    at defaultGetFormat (node:internal/modules/esm/get_format:125:36)
    at defaultLoad (node:internal/modules/esm/load:89:20)
    at nextLoad (node:internal/modules/esm/loader:163:28)
    at ESMLoader.load (node:internal/modules/esm/loader:603:26)
    at ESMLoader.moduleProvider (node:internal/modules/esm/loader:457:22)
    at new ModuleJob (node:internal/modules/esm/module_job:64:26)
    at ESMLoader.#createModuleJob (node:internal/modules/esm/loader:480:17)
    at ESMLoader.getModuleJob (node:internal/modules/esm/loader:434:34)
[electronmon] uncaught exception occured
[electronmon] waiting for any change to restart the app

Steps to Reproduce

See above.

Possible Solution (Not obligatory)

Not sure what the issue is. I know that Node v18.19.0 broke ts-node with a similar error message, see https://github.com/TypeStrong/ts-node/issues/1997

Latest known good version for Electron is 27.1.3.

Context

N/A

Your Environment

  • Node version : v18.19.0
  • electron-react-boilerplate version or branch : main
  • Operating System and version : macOS
  • Link to your project : N/A

About this issue

  • Original URL
  • State: open
  • Created 6 months ago
  • Reactions: 13
  • Comments: 31 (3 by maintainers)

Most upvoted comments

I ran into this issue yesterday and I was able to get it working with the following changes:

  1. In the start script, replace ts-node with tsx.
  2. In the start:main script, add NODE_OPTION flag and minor changes to the electronmon arguments.

Here is what the updated version of these scripts look like:

"start": "tsx ./.erb/scripts/check-port-in-use.js && npm run start:renderer" "start:main": "cross-env NODE_ENV=development NODE_OPTIONS=\"--loader tsx\" electronmon ."

I haven’t exhaustively checked for the side-effects but the development environment is working fine i.e. the files are getting transpiled and on code change, the latest changes are loaded automatically.

A few points to note:

  1. My postinstall script still uses ts-node. Will get around to this soon.
  2. My package script still uses ts-node. Will get around to this soon.
  3. I had to install tsx as a dev dependency.

My Environment

Node version : v20.5.0 Electron: 28.2.3 electron-react-boilerplate version or branch : main Operating System and version : macOS

I hope this helps developers who stumble onto this thread in future.

Because webpack needs to use ts-node when using typescript as the configuration language: webpack-configuration-languages. And currently it does not support ESModule, this will be a hindrance when we move fully to ESModule.

One more thing, it seems that node will no longer support the use of custom ESM Loader(you will get this warning):

ExperimentalWarning: Custom ESM Loaders is an experimental feature and might change at any time

Well it doesn’t “load” typescript sources at runtime, it resolves them in typescript, stores them somewhere and then redirects require() calls to them, most likely achieved by monkey-patching require(). Which is of course not possible in ESM since import is a keyword and not a symbol. The solution is to explicitly have a ts -> js transformation step in your build pipeline and drop ts-node from runtime dependencies.

As mentioned here, after electron v28.0.0 switches to esmodule we will no longer be able to use ts-node to compile ts -> js at runtime. Instead, ts should be compiled into js before running. The simplest way is that we can use webpack or tsc to compile ts into js in advance and write it to a disk file, and then execute electron. to start the service (as far as I know electron-forge uses this method).

  • Write a webpack.config.main.dev.ts for the main process code (the output path must be consistent with the main attribute of package.json
  • Change start:main command
"start:main": "cross-env NODE_ENV=development  TS_NODE_TRANSPILE_ONLY=true webpack --config ./.erb/configs/webpack.config.main.dev.ts && electronmon .",

(If you care about the time-consuming compilation, you can use swc-loader to speed up the compilation process.)

I’ve tried this solution myself and everything works fine. But when I fully migrated to esmodule, webpack started to complain: The webpack.config.ts file could not be recognized. Because webpack does not yet support esmodule when using typescript as the configuration language.

So eventually I started to give up ts-node & webpack and turned to vite and esbuild for a good development experience. At the same time, I also retained the advantages of electron-react-boilerplate. If you are interested you can view the current complete project code: https://github.com/1111mp/nvm-desktop. Currently everything is working fine.

(This comment does not in any way imply that electron-react-boilerplate is obsolete. In fact, all this has nothing to do with electron-react-boilerplate itself, because the problem that causes incompatibility with electron v28.0.0 and above is caused by the tools that its upstream depends on. The design of electron-react-boilerplate is still very good.)

I ran into this issue yesterday and I was able to get it working with the following changes:

  1. In the start script, replace ts-node with tsx.
  2. In the start:main script, add NODE_OPTION flag and minor changes to the electronmon arguments.

Here is what the updated version of these scripts look like:

"start": "tsx ./.erb/scripts/check-port-in-use.js && npm run start:renderer" "start:main": "cross-env NODE_ENV=development NODE_OPTIONS=\"--loader tsx\" electronmon ."

I haven’t exhaustively checked for the side-effects but the development environment is working fine i.e. the files are getting transpiled and on code change, the latest changes are loaded automatically.

A few points to note:

  1. My postinstall script still uses ts-node. Will get around to this soon.
  2. My package script still uses ts-node. Will get around to this soon.
  3. I had to install tsx as a dev dependency.

My Environment

Node version : v20.5.0 Electron: 28.2.3 electron-react-boilerplate version or branch : main Operating System and version : macOS

I hope this helps developers who stumble onto this thread in future.

@slhck, it looks like esbuild-register works as an alternative to ts-node in my quick testing, here’s a branch that makes the necessary changes: https://github.com/dsanders11/electron-react-boilerplate/tree/esbuild

If you want to test it and let me know if that resolves your issues, I could open a PR on this repo with the changes.

Thanks for your comment!

Write a webpack.config.main.dev.ts for the main process code

That sounds reasonable, although it was far from obvious to me how that would look like. I played around a little, and what I did was the following. Create a file ./erb/configs/webpack.config.main.dev.ts:

/**
 * Webpack config for production electron main process
 */

import path from 'path';
import webpack from 'webpack';
import { merge } from 'webpack-merge';
import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer';
import baseConfig from './webpack.config.base';
import webpackPaths from './webpack.paths';
import checkNodeEnv from '../scripts/check-node-env';
import { getContentScriptEntries } from './getContentScriptEntries';

// When an ESLint server is running, we can't set the NODE_ENV so we'll check if it's
// at the dev webpack config is not accidentally run in a production environment
if (process.env.NODE_ENV === 'production') {
  checkNodeEnv('development');
}

const configuration: webpack.Configuration = {
  devtool: 'inline-source-map',

  mode: 'development',

  target: 'electron-main',

  entry: {
    ...getContentScriptEntries(),
    main: path.join(webpackPaths.srcMainPath, 'main.ts'),
    // preload: path.join(webpackPaths.srcMainPath, 'preload.ts'),
    // TODO merge this with the preload-file ...
  },

  output: {
    path: webpackPaths.dllPath,
    filename: '[name].bundle.dev.js',
    library: {
      type: 'umd',
    },
  },

  plugins: [
    // eslint-disable-next-line @typescript-eslint/ban-ts-comment
    // @ts-ignore
    new BundleAnalyzerPlugin({
      analyzerMode: process.env.ANALYZE === 'true' ? 'server' : 'disabled',
      analyzerPort: 8888,
    }),

    new webpack.DefinePlugin({
      'process.type': '"browser"',
    }),
  ],

  /**
   * Disables webpack processing of __dirname and __filename.
   * If you run the bundle in node.js it falls back to these values of node.js.
   * https://github.com/webpack/webpack/issues/2010
   */
  node: {
    __dirname: false,
    __filename: false,
  },
};

export default merge(baseConfig, configuration);

Now change package.json to point to the newly generated bundle JS file:

diff --git a/package.json b/package.json
index 893ca68..d3dbe0f 100644
--- a/package.json
+++ b/package.json
@@ -7,7 +7,7 @@
   "version": "0.9.0",
-  "main": "./src/main/main.ts",
+  "main": "./.erb/dll/main.bundle.dev.js",
   "private": true,
   "scripts": {
     "build": "concurrently \"yarn run build:main\" \"yarn run build:renderer\"",
@@ -24,7 +24,7 @@
     "package:all": "yarn run prepackage && yarn run package:linux && yarn run package:mac && yarn run package:win",
     "rebuild": "electron-rebuild --parallel --types prod,dev,optional --module-dir release/app",
     "start": "ts-node ./.erb/scripts/check-port-in-use.js && yarn run start:renderer",
-    "start:main": "cross-env NODE_ENV=development electronmon --inspect=5858 -r ts-node/register/transpile-only .",
+    "start:main": "cross-env NODE_ENV=development  TS_NODE_TRANSPILE_ONLY=true webpack --config ./.erb/configs/webpack.config.main.dev.ts && electronmon .",
     "start:preload": "cross-env NODE_ENV=development TS_NODE_TRANSPILE_ONLY=true webpack --config ./.erb/configs/webpack.config.preload.dev.ts",
     "start:renderer": "cross-env NODE_ENV=development TS_NODE_TRANSPILE_ONLY=true webpack serve --config ./.erb/configs/webpack.config.renderer.dev.ts",
     "test": "yarn run test:unit && yarn run test:e2e",

I also had to change a few places in my application that looked like this — due to outputting the bundle in .erb/dll now:

diff --git a/src/main/helpers/paths.ts b/src/main/helpers/paths.ts
index 80b9b34..db33e96 100644
--- a/src/main/helpers/paths.ts
+++ b/src/main/helpers/paths.ts
@@ -3,7 +3,7 @@ import path from 'path';
 
 export const RESOURCES_PATH = app.isPackaged
   ? path.join(process.resourcesPath, 'assets')
-  : path.join(__dirname, '../../../assets');
+  : path.join(__dirname, '../../assets');
 
 export function getAssetPath(...paths: string[]) {
   return path.join(RESOURCES_PATH, ...paths);

This works, even with hot-reloading, and I have had no issues launching the app with Electron 29 and Node v18.19.0.

This comment does not in any way imply that electron-react-boilerplate is obsolete

The only thing that irks me is that there is no official comment from the maintainers yet. The package is lagging behind and v27 will be unsupported as of April 16, 2024. Of course, I understand it’s free, open-source software and there’s no guarantee to receive any support whatsoever. But given the various solutions proposed here, an official comment regarding what would be the recommended solution would be good.

For what is worth, I did that migration recently, to the electron-forge boilerplate with the vite-typescript template, and the transition was surprisingly smooth. I hardly had to change anything serious in my code, other than to specify in the configuration that I wanted different directories for main and renderer. I’m not an expert in webpack, but I found the vite configuration much easier to understand and modify, which I was never able to do reliably with webpack.

thanks for the push! I used your repo to help get things working… thanks. Also my dev environment feels much quickier and more snappy. For everyone else, I’ll tag my PR here so y’all can see the file changes

For what is worth, I did that migration recently, to the electron-forge boilerplate with the vite-typescript template, and the transition was surprisingly smooth. I hardly had to change anything serious in my code, other than to specify in the configuration that I wanted different directories for main and renderer. I’m not an expert in webpack, but I found the vite configuration much easier to understand and modify, which I was never able to do reliably with webpack.

Following the great contribution of dsanders11 I had to change more things in the scripts section of the “package.json” file to get everything to work correctly:

"scripts": {
    "build": "concurrently \"npm run build:main\" \"npm run build:renderer\"",
    "build:dll": "cross-env NODE_ENV=development NODE_OPTIONS=\"--loader esbuild-register/loader -r esbuild-register\" webpack --config ./.erb/configs/webpack.config.renderer.dev.dll.ts",
    "build:main": "cross-env NODE_ENV=production NODE_OPTIONS=\"--loader esbuild-register/loader -r esbuild-register\" webpack --config ./.erb/configs/webpack.config.main.prod.ts",
    "build:renderer": "cross-env NODE_ENV=production NODE_OPTIONS=\"--loader esbuild-register/loader -r esbuild-register\" webpack --config ./.erb/configs/webpack.config.renderer.prod.ts",
    "postinstall": "node -r esbuild-register .erb/scripts/check-native-dep.js && electron-builder install-app-deps && npm run build:dll",
    "lint": "cross-env NODE_ENV=development eslint . --ext .js,.jsx,.ts,.tsx",
    "package": "node -r esbuild-register ./.erb/scripts/clean.js dist && npm run build && electron-builder build --publish never && npm run build:dll",
    "rebuild": "electron-rebuild --parallel --types prod,dev,optional --module-dir release/app",
    "start": "node -r esbuild-register ./.erb/scripts/check-port-in-use.js && npm run start:renderer",
    "start:main": "cross-env NODE_ENV=development NODE_OPTIONS=\"--loader esbuild-register/loader -r esbuild-register\" electronmon .",
    "start:preload": "cross-env NODE_ENV=development NODE_OPTIONS=\"--loader esbuild-register/loader -r esbuild-register\" webpack --config ./.erb/configs/webpack.config.preload.dev.ts",
    "start:renderer": "cross-env NODE_ENV=development NODE_OPTIONS=\"--loader esbuild-register/loader -r esbuild-register\" webpack serve --config ./.erb/configs/webpack.config.renderer.dev.ts",
    "test": "jest"
  },

That said, applying your suggested changes I still get:

➜ npm start

> start
> node --import tsx ./.erb/scripts/check-port-in-use.js && npm run start:renderer


> start:renderer
> cross-env NODE_ENV=development TS_NODE_TRANSPILE_ONLY=true webpack serve --config ./.erb/configs/webpack.config.renderer.dev.ts

Starting preload.js builder...
Starting Main Process...
<i> [webpack-dev-server] Project is running at:
<i> [webpack-dev-server] Loopback: http://localhost:1212/
<i> [webpack-dev-server] On Your Network (IPv4): http://192.168.0.119:1212/
<i> [webpack-dev-server] On Your Network (IPv6): http://[fe80::1]:1212/
<i> [webpack-dev-server] Content not from webpack is served from '/Users/werner/Documents/Software/electron-react-boilerplate/public' directory
<i> [webpack-dev-server] 404s will fallback to '/index.html'

> start:main
> cross-env NODE_ENV=development NODE_OPTIONS="--import tsx" electronmon .


> start:preload
> cross-env NODE_ENV=development TS_NODE_TRANSPILE_ONLY=true webpack --config ./.erb/configs/webpack.config.preload.dev.ts

[electronmon] waiting for a change to restart it
App threw an error during load
TypeError [ERR_UNKNOWN_FILE_EXTENSION]: Unknown file extension ".ts" for /Users/werner/Documents/Software/electron-react-boilerplate/src/main/main.ts
    at __node_internal_captureLargerStackTrace (node:internal/errors:496:5)
    at new NodeError (node:internal/errors:405:5)
    at Object.getFileProtocolModuleFormat [as file:] (node:internal/modules/esm/get_format:80:11)
    at defaultGetFormat (node:internal/modules/esm/get_format:125:36)
    at defaultLoad (node:internal/modules/esm/load:89:20)
    at nextLoad (node:internal/modules/esm/loader:163:28)
    at ESMLoader.load (node:internal/modules/esm/loader:603:26)
    at ESMLoader.moduleProvider (node:internal/modules/esm/loader:457:22)
    at new ModuleJob (node:internal/modules/esm/module_job:64:26)
    at #createModuleJob (node:internal/modules/esm/loader:480:17)
    at ESMLoader.getModuleJob (node:internal/modules/esm/loader:434:34)
[electronmon] uncaught exception occured
[electronmon] waiting for any change to restart the app

No, I don’t know enough about the internals… I will post a solution should I find one, but in the meantime I’ll stay on the last 27 release.