question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

[Feature Request] Allow ES Module import of Node Builtin Modules in Browser Context and/or Make "main" optional when setting "node-main"

See original GitHub issue

As @frank-dspeed explains in #7557, ES Module support is pretty important: as they are not going to implement ESM this is the moment where NW.js can shine when it is able to run Electron apps.

Builtin node modules (e.g. fs) can be mixed with browser APIs easily using require syntax:

(click to show)
<script src="script-that-requires-builtins-and-uses-browser-apis.js"></script>

<script>const test = require("script-that-requires-builtins-and-uses-browser-apis.js");</script>

<script>
    const fs = require("fs");
    const data = fs.readFileSync("data");
    document.querySelector("#data").textContent = data;
</script>

However, there doesn’t seem to be a way to do this easily with ES Modules.

Each of the following fail with: Uncaught TypeError: Failed to resolve module specifier "path". Relative references must start with either "/", "./", or "../":

(click to show)
<script type="module" src="script-imports-builtins-and-uses-browser-apis.mjs"></script>

<script type="module">import test from "script-imports-builtins-and-uses-browser-apis.js";</script>

<script type="module">
    import fs from "fs";
    const data = fs.readFileSync("data");
    document.querySelector("#data").textContent = data;
</script>

and excluding the type="module" each of these fail silently:

(click to show)
<script src="script-imports-builtins-and-uses-browser-apis.mjs"></script>

<script>import test from "script-imports-builtins-and-uses-browser-apis.js";</script>

<script>
    import fs from "fs";
    const data = fs.readFileSync("data");
    document.querySelector("#data").textContent = data;
</script>

The only way importing node builtins seems to work is with a node-main in the package.json e.g. {"node-main":"script-imports-builtins.js"} however the browser APIs are not directly available as node-main code is not in a browser context even if "chromium-args": "--mixed-context" is used. A workaround is to use nw.Window.get() or nw.Window.getAll() to get an NW window and then use e.g. nwWindow.window to get access to its web APIs. This is hacky at best as node-main code gets loaded before the window pointed to by main so a setTimeout is needed to get a window loaded via main.

Currently the best workaround is to create all windows from within the script referenced by node-main (which is actually similar to how electron works) however main is currently still required even if node-main is set. The workaround is to point main to an empty .js file and just use node-main

I propose, that only one of main or node-main should be required and that import is patched to work with node builtins when used from within a browser context.

Issue Analytics

  • State:open
  • Created 3 years ago
  • Reactions:4
  • Comments:18

github_iconTop GitHub Comments

6reactions
CxRescommented, Oct 10, 2022

@sysrage I am sorry, but my experience has been similarly poor as yours!

Here is my tedious workaround to ESM using ESModuleShim library – use below as your entry (main) script and your app proper is invoked in it at the end. It might give you a few ideas:

const { builtinModules: builtins } = require('module');

function generateBridge(moduleName) {
  // eslint-disable-next-line
  const exportContents = Object.keys(require(moduleName))
    .reduce((exportList, exportItem) => `${exportList}  ${exportItem},\n`, '')
  ;

  return `const m = require('${moduleName}');

export default m;
export const {
${exportContents}} = m;
`
  ;
}

window.esmsInitOptions = {
  resolve: async function esmsResolveDecorator(id, parentUrl, defaultResolve) {
    if (builtins.includes(id) /* && id !== 'fs' */) {
      return `/node_builtins/${id}`;
    }

    // Default resolve will handle the typical URL and import map resolution
    return defaultResolve(id, parentUrl);
  },
  fetch: async function esmsFetchDecorator(url) {
    if (url.startsWith('/node_builtins/')) {
      const builtin = generateBridge(url.substring(15));
      return new Response(new Blob([builtin], { type: 'application/javascript' }));
    }
    return fetch(url);
  },
};

function includeShim() {
  const shim = document.createElement('script');
  shim.setAttribute('async', '');
  shim.src = 'node_modules/es-module-shims/dist/es-module-shims.min.js';
  document.body.append(shim);
}

function includeScript(text) {
  const app = document.createElement('script');
  app.type = 'module-shim';
  app.innerText = text;
  document.body.append(app);
}

document.addEventListener('DOMContentLoaded', async function loadApp() {
  includeShim();

  // You cannot use import() statement directly here as the script tag under which
  // this code is run precedes the shim. Hence, this tedious workaround.
  includeScript('import "./src/app.js"');
});

I would suggest not using NW.js for new projects, except in case of niche requirements like code obfuscation etc. since you are unlikely to get a genuine ear to even legitimate issues in this community.

3reactions
edbaaficommented, Dec 7, 2020

FYI - if anyone is interested in the current workaround. Here is what I came up with:

package.json

{
    "name": "App Name",
    "main":"empty.js",
    "node-main":"node-main.mjs",
}

node-main.mjs:

import path from "path";
import load from "./loader.mjs"

//wrap in immediately-called async function 
//unless/until --experimental-top-level-await is supported in nw.js
(async ()=>{

    //add whetever browser APIs you need (and add them in loader.mjs)
    let {window,document,alert,console,setTimeout} = await load("https://google.com");
    
    //now use the APIs which are within the "loaded" context
    document.querySelector("img").setAttribute("src","https://github.githubassets.com/images/modules/logos_page/GitHub-Logo.png");
    
    console.log(path.join("test","join"));
    
    setTimeout(()=>{
        alert("switch location");
        window.location="https://github.com";
    },8000);
    
})()

loader.mjs:

export default function load(url){
   return new Promise((resolve)=>{
        nw.Window.open(url,{}, function(nwWindow) {
            nwWindow.window.addEventListener('DOMContentLoaded', (event) => {

                //reurn whatever browser APIs your user code may need
                resolve({window:nwWindow.window,
                    document:nwWindow.window.document,
                    alert:nwWindow.window.alert,
                    console:nwWindow.window.console,
                    setTimeout:nwWindow.window.setTimeout
                   });
            });
        });

    });
}

empty.js:

//hack as main is required field in manifest file (package.js)
//https://docs.nwjs.io/en/latest/References/Manifest%20Format/#required-fields
Read more comments on GitHub >

github_iconTop Results From Across the Web

Using ES modules in Node.js - LogRocket Blog
Learn about the state of ES modules in Node today, ... use of the import and export keywords instead of the require() function...
Read more >
ECMAScript modules | Node.js v19.3.0 Documentation
Modules are defined using a variety of import and export statements. The following example of an ES module exports a function:
Read more >
Documentation - ECMAScript Modules in Node.js - TypeScript
This setting controls whether .js files are interpreted as ES modules or ... When a .ts file is compiled as an ES module,...
Read more >
JavaScript modules - MDN Web Docs
Import maps allow modules to be imported using bare module names (as in Node.js), and can also simulate importing modules from packages, both ......
Read more >
JavaScript Contexts in NW.js
Scripts loaded by node-main in Manifest file. Global Objects in Node Context. Scripts running in the Node context can use JS builtin objects...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found