7

I build an Electron application using electron-forge's react-typescript template, meaning it uses electron-prebuilt-compile, which according to the only documentation I can find is supposed to just work.

The index.html contains typescript that just works, as advertised. But I am also using a webview tag with a preload script, in order to display external websites and modify them. This is the code I'm using:

<webview id="webview" preload="./preload.ts" webpreferences="contextIsolation, webSecurity=no" src="https://example.com"></webview>

This preload script is rather involved, and I would love to use typescript for it. But it is apparently parsed as javascript, with any type annotations leading to syntax errors. Is there a trick to make this work with typescript? If I have to call the transpiler manually, how do I integrate this with electron-forge's build process?

tl;dr: preload script is parsed as javascript despite typescript "just working" everywhere else, I would like to use typescript here too

3 Answers 3

5

You can use TypeScript in a preload file (or any file). Just import the ts-node package and configure it before you import any of your TypeScript code.

F.e., make an index.js file containing:

require('./require-hooks')
module.exports = require('./entry') // this is your TypeScript entry point.

then in your require-hooks.js file configure ts-node (ts-node is a require hook that compiles TypeScript on the fly, with caching for subsequent runs):

// ability to require/import TypeScript files
require('ts-node').register({
  typeCheck: false, // faster, no type checking when require'ing files. Use another process to do actual type checking.
  transpileOnly: true, // no type checking, just strip types and output JS.
  files: true,

  // manually supply our own compilerOptions, otherwise if we run this file
  // from another project's location then ts-node will use
  // the compilerOptions from that other location, which may not work.
  compilerOptions: require('./tsconfig.json').compilerOptions,
})

Note, you can put all sorts of require hooks in there, for example so you can do things like require('path/to/file.ts'), require('path/to/file.tsx'), require('path/to/file.jsx'), require('path/to/file.png'), require('path/to/file.mp3'), etc, where you can define hooks for handling certain types of files (similar to Webpack in some way, but hooking into Node's builtin require function). For example, @babel/register is a hook for running JS files through Babel, asset-require-hook is a require hook that allows you to import assets like JPG files, yaml-hook allows you to require .yaml files. EDIT: Even more: css-modules-require-hook for importing CSS modules, and module-alias for creating aliases like with WebPack.

The pirates lib is a popular tool for making your own require hooks, though I also find it easy to make hooks manually without a library. You can, for example, override Module.prototype.require to implement some simple hooks:

const path = require('path')
const url = require('url')
const Module = require('module')
const oldRequire = Module.prototype.require

function toFileURL(filePath) {
  return url.format({
    pathname: filePath,
    protocol: 'file:',
    slashes: true,
  })
}

Module.prototype.require = function(moduleIdentifier) {
  // this simple hook returns a `file://...` URL when you try to `require()`
  // a file with extension obj, svg, png, or jpg.
  if (['.obj', '.png', '.svg', '.jpg'].some(ext => moduleIdentifier.endsWith(ext))) {
    const result = String(toFileURL(path.resolve(path.dirname(this.filename), moduleIdentifier)))

    result.default = result
    return result
  } else {
    return oldRequire.call(this, moduleIdentifier)
  }
}

then in some other file,

const fs = require('fs')
const img = require('path/to/foo.jpg')

console.log(img) // file:///absolute/path/to/foo.jpg
document.createElement('img').src = img

You could even move the call to document.createElement('img').src = img into your overridden require method to do it automatically and return the img element instead of returning a file:// URL. :)

Finally, in your ./entry.ts file that the above index.js file imports, you can have any TypeScript in there.

Sign up to request clarification or add additional context in comments.

Comments

1

Preload script is different kind, you can't directly point typescript in there. Only possible way is make javascript preload script which bootstraps electron compile inside of it (since you are using electron-prebuilt-compile), and require typescript files inside of it. It is somewhat verbose and require additional overhead into, I honestly not strongly recommend for it.

2 Comments

That's somewhat disappointing, but I feared as much. I am somewhat unsure how to bootstrap electron-compile. github.com/electron-userland/electron-forge/wiki/… describes an approach that just ends with Failed to execute 'registerElement' on 'Document': [...] Elements cannot be registered from extensions. Do you have any insight how to do this, or should I rather open a new question for that error?
fixed that problem, contextIsolation was creating the error. Without it,the previously mentioned wiki entry works
1

my solution was to add a 2nd webpack configuration that outputs a single inject.js file.

src/inject.ts transpiles into => dist/inject.js from where it can be required like this:

new BrowserWindow({
    width: 800,
    height: 600,
    webPreferences: {
      nodeIntegration: false,
      enableRemoteModule: false,
      contextIsolation: true,
      sandbox: true,
      preload: path.join(__dirname, 'mercari_inject.js')
    }
});

given a base config webpack.main.config.js like this:

module.exports = {
    ...
    output: {
        path: path.resolve(__dirname, 'dist'),
        filename: '[name].js'
    },
    ...
}

the main config webpack.main.config.js should look like this:

const merge = require('webpack-merge');
const baseConfig = require('./webpack.base.config');

const mainConfig = merge.smart(baseConfig, {...})
const injectScriptConfig = merge.smart(baseConfig, {
  target: 'electron-preload',
  entry: {
    inject: './src/inject.ts',
  },
  module: {
    rules: [
       ... // babel/typescript loaders
    ]
  } 
})

module.exports = [
   mainConfig,
   injectScriptConfig,
]

notice that before the export changes from a single configuration object to an array.

module.exports = {...}
module.exports = [{...}, {...}]

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.