neutralinojs icon indicating copy to clipboard operation
neutralinojs copied to clipboard

Not running WebSocket on HotReloading

Open jaronwanderley opened this issue 2 years ago • 2 comments

Describe the bug Hot relaoding works, but without Neutralino API

Uncaught DOMException: Failed to construct 'WebSocket': The URL 'ws://localhost:undefined' is invalid.
    at a (http://localhost:3000/neutralino.js:1:580)
    at Object.e.init (http://localhost:3000/neutralino.js:1:11526)
    at http://localhost:3000/src/main.js?t=1653880987265:6:19

I think is something related to the port to websocket but I not find anything about in the docs.

To Reproduce Steps to reproduce the behavior:

  1. I follow the instructions in https://neutralino.js.org/docs/how-to/use-a-frontend-library/
  2. But using Vue + Vite
  3. When run vite build && neu run Websocket works but not hot reloading
  4. When run vite + neu run --frontend-lib-dev hot relaoding works but not Websocket

Expected behavior Work hot reloading e Neutralino API.

Specifications

  • OS: Windows 10 x64
  • Neutralinojs version: v4.6.0
  • Neutralinojs client library version: v3.5.0
  • Neutralinojs CLI version: v9.3.1

Additional context neutralino.config.json

{
  "applicationId": "js.neutralino.zero",
  "version": "1.0.0",
  "defaultMode": "window",
  "documentRoot": "/output",
  "url": "/",
  "enableServer": true,
  "enableNativeAPI": true,
  "nativeAllowList": [
    "app.*",
    "window.*"
  ],
  "modes": {
    "window": {
      "title": "myapp",
      "width": 800,
      "height": 500,
      "minWidth": 400,
      "minHeight": 200,
      "icon": "/output/icon.png"
    }
  },
  "cli": {
    "binaryName": "myapp",
    "resourcesPath": "/output",
    "extensionsPath": "/extensions/",
    "clientLibrary": "/public/neutralino.js",
    "binaryVersion": "4.6.0",
    "clientVersion": "3.5.0",
    "frontendLibrary": {
        "patchFile": "/output/index.html",
        "devUrl": "http://localhost:3000"
    }
  }
}

vite.config.js

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [vue()],
  build: {
    outDir: './output'
  },
})

neutralinojs.log

INFO 2022-05-30 00:53:56,683 Auth info was exported to ./.tmp/auth_info.json api\debug\debug.cpp:14 jbwanderley2@STFSAOC045831-L

/.tmp/auth_info.json

{"accessToken":"Ovg9BLuujZi1b3vNgRu0x2wQGTSSG7SV4tgTCS3uGCw-wUNh","port":54428}

jaronwanderley avatar May 30 '22 03:05 jaronwanderley

Inspecting further I found that the problem is that the window variables are not being set (NL_PORT, NL_TOKEN, NL_ARGS) I tried setting it up myself verifying if is in DEV mode and running the Neutralino API and it worked. For NL_PORT and NL_TOKEN I used the values ​​in /.tmp/auth_info.json, for NL_ARGS I ran it once in the build and got it.

// main.js
...
import authInfo from '../.tmp/auth_info.json'
if (import.meta.env.DEV) {
  const {accessToken, port} = authInfo
  window.NL_PORT = port
  window.NL_TOKEN = accessToken
  window.NL_ARGS = [
    'bin\\neutralino-win_x64.exe',
    '',
    '--load-dir-res',
    '--path=.',
    '--export-auth-info',
    '--neu-dev-extension',
    '--neu-dev-auto-reload',
    '--window-enable-inspector'
  ]
}
window.Neutralino.init()

Using this code running on hot reload works.

jaronwanderley avatar May 30 '22 04:05 jaronwanderley

I've just run into the same issue - was trying to uncover a method in Neutralino to generate the globals, but will pinch your fix insteaad. Thanks a bunch! Changed it to a dynamic import though, as a static import will fail when trying to build without launching from neu.

ooshhub avatar Jun 17 '22 10:06 ooshhub

To prevent including the code during Vite build (and also prevent TS errors when the file doesn't exists), I did a virtual module with Vite that apply this little piece of code

vite.config.ts

import type { Plugin, ResolvedConfig } from "vite";
import { defineConfig } from 'vite';
import path from "node:path";

// Re-define constants in `development` mode.
// <https://github.com/neutralinojs/neutralinojs/issues/909>.
const neutralino_dev = (): Plugin => {
  let config: ResolvedConfig;
  const virtualModuleId = "virtual:neutralino-dev"
  const resolvedVirtualModuleId = "\0" + virtualModuleId;

  return {
    name: "neutralino-dev",

    configResolved (resolvedConfig) {
      // store the resolved config
      config = resolvedConfig
    },
    
    resolveId (id) {
      if (id === virtualModuleId) {
        return resolvedVirtualModuleId;
      }
    },
    load (id) {
      if (id === resolvedVirtualModuleId) {
        if (config.mode !== "development") {
          console.info("[neutralino] don't apply dev code.");
          return "";
        }

        console.info("[neutralino] apply dev code.");
        return `
          const authInfo = await import("${path.join(__dirname, ".tmp", "auth_info.json")}");
          window.NL_PORT = authInfo.port;
          window.NL_TOKEN = authInfo.accessToken;
        `
      }
    }
  }
};

export default defineConfig({
  plugins: [
    // Will only apply development code on development mode.
    neutralino_dev()
  ]
});

index.tsx

import "virtual:neutralino-dev";

// ...render(...);

Neutralino.init();

EDIT: I made a new version of my virtual plugin to actually import the patched neutralino.js file !

const neutralino_dev = (): Plugin => {
  let config: ResolvedConfig;
  const virtualModuleId = "virtual:neutralino-dev";
  const resolvedVirtualModuleId = "\0" + virtualModuleId;

  return {
    name: "neutralino-dev",

    configResolved (resolvedConfig) {
      config = resolvedConfig;
    },

    resolveId (id) {
      if (id === virtualModuleId) {
        return resolvedVirtualModuleId;
      }
    },
    load (id) {
      if (id === resolvedVirtualModuleId) {
        let code: string;

        if (config.mode === "development") {
          code = `
            const authInfo = await import("${path.join(__dirname, ".tmp", "auth_info.json")}");
            const neutralinoJsFileUrl = \`http://localhost:\${authInfo.port}/neutralino.js\`;
          `;
        }
        else {
          code = `
            const neutralinoJsFileUrl = "/neutralino.js";
          `;
        }

        return code + `
          const neutralinoScript = document.createElement("script");
          neutralinoScript.setAttribute("src", neutralinoJsFileUrl);
          document.body.appendChild(neutralinoScript);
        `;
      }
    }
  };
};

Note that you need to remove <script src="neutralino.js"></script> from your index.html since this would be added dynamically by the virtual plugin. Maybe I should write a build plugin so it directly imports it in the index.html file.

Also, you need to keep

"frontendLibrary": {
  "patchFile": "/index.html",
  "devUrl": "http://localhost:3000"
}

in your Neutralino configuration (here my Vite development server port was on 3000, change it depending to yours)

Enjoy !

Vexcited avatar Jan 03 '23 21:01 Vexcited

Maybe I should write a build plugin so it directly imports it in the index.html file.

So I did it, because it's a little bit cleaner and even for the build, it's better.

Also keep in mind that the neutralino.js file is in /public/neutralino.js. If it's anywhere else, you'll need to change the paths.

index.html

...
<body>
  ...
  <script src="neutralino.js"></script>
</body>

neutralino.config.json

{
  "applicationId": "...",
  "version": "...",
  "defaultMode": "window",
  "port": 0,
  "documentRoot": "/dist/",
  "url": "/",
  "enableServer": true,
  "enableNativeAPI": true,
  "exportAuthInfo": true,
  "modes": {
    "window": {
      "..."
    }
  },
  "cli": {
    "binaryName": "...",
    "resourcesPath": "/dist/",
    "extensionsPath": "/extensions/",
    "clientLibrary": "/public/neutralino.js",
    "binaryVersion": "...",
    "clientVersion": "...",
    "frontendLibrary": {
      "patchFile": "/index.html",
      "devUrl": "http://localhost:3000"
    }
  }
}

vite.config.ts

import type { Plugin, ResolvedConfig } from "vite";
import { defineConfig } from "vite";
import fs from "node:fs/promises";
import path from "node:path";

// <https://github.com/neutralinojs/neutralinojs/issues/909>.
const neutralino = (): Plugin => {
  let config: ResolvedConfig;

  return {
    name: "neutralino",

    configResolved (resolvedConfig) {
      config = resolvedConfig;
    },

    async transformIndexHtml (html) {
      if (config.mode === "development") {
        const auth_info_file = await fs.readFile(path.join(__dirname, ".tmp", "auth_info.json"), {
          encoding: "utf-8"
        });

        const auth_info = JSON.parse(auth_info_file);
        const port = auth_info.port;

        return html.replace(
          "<script src=\"neutralino.js\"></script>",
          `<script src="http://localhost:${port}/neutralino.js"></script>`
        );
      }

      return html;
    }
  };
};

export default defineConfig({
  plugins: [
    neutralino()
  ],

  server: {
    port: 3000
  }
});

Parts of my package.json, here I use concurrently to run Neutralino and Vite at the same time.

{
  "type": "module",
  "scripts": {
    "neu": "neu",
    "dev": "concurrently \"vite\" \"neu run --frontend-lib-dev -- --window-enable-inspector=true\"",
    "build": "vite build && neu build --release",
    "preview": "vite build && neu run"
  }
}

You don't have to touch your source files, the neutralino.js file will be updated directly inside the index.html.

Hope it helps some of you, now you have every variables available in your environment (NL_*) !

Vexcited avatar Jan 08 '23 07:01 Vexcited

I've faced the same issue with my nuxtjs client. Have you have any help for about nuxtjs?

hbt272 avatar Jan 18 '23 07:01 hbt272

Add <script src="__neutralino_globals.js"></script> in index.html file fixed the problem for me. The neu process will automatically know to serve the globals there.

NL_PORT as well as other globals are sent to the browser window via __neutralino_globals.js. After a few tests, I fount it out how it works:

  1. When you run in "HMR" mode, neu run --frontend-lib-dev, the neu process searches the html file cli.frontendLibrary.patchFile for <script src="(somepath)__neutralino_globals.js"></script> and modify it into <script src="http://localhost:(anotherport)/(somepath)__neutralino_globals.js"></script>. The program then host the globals there. Notice (anotherport) is different from other assets. If you force to quit in "HMR" mode neu run --frontend-lib-dev, like ctrl+C at neu cli command line, the restoration will not happen and a broken index.html file will be left there.
  2. When you run neu run, the neu process serve __neutralino_globals.js at the same port from other static assets. So no port replacement would happen.

bumprat avatar Apr 02 '23 15:04 bumprat

I had to write a vite plugin like vexcited to add localhost/port to __neutralino_globals.js script tag. Not sure why neu isn't patching it in the patch file like bumprat explained. But after fighting it for too long at least it works.

Thanks everyone.

// <https://github.com/neutralinojs/neutralinojs/issues/909>.
const neutralino = () => {
  let config;

  return {
    name: "neutralino",

    configResolved (resolvedConfig) {
      config = resolvedConfig;
    },

    async transformIndexHtml (html) {
      if (config.mode === "development") {
        const auth_info_file = await fs.readFile(path.join(__dirname, "../.tmp", "auth_info.json"), {
          encoding: "utf-8"
        });

        const auth_info = JSON.parse(auth_info_file);
        const port = auth_info.port;
        return html.replace(
          "<script src=\"__neutralino_globals.js\"></script>",
          `<script src="http://localhost:${port}/__neutralino_globals.js"></script>`
        );
      }
      return html;
    }
  };
};

cyrusbowman avatar Apr 03 '23 01:04 cyrusbowman

Okay after struggling to get extensions websocket to connect I found that localhost was coming back to ipv6 loopback of ::1. This was causes the neu hotload patch to fail as well.

I was getting:

[1] neu: INFO Hot reload patch was reverted.
[1] neu: INFO Hot reload patch was reverted.
[1] neu: INFO Hot reload patch was reverted.
[1] neu: INFO Hot reload patch was reverted.
...

I commented out ipv6 loopback ::1 in /etc/hosts and all is well now. No need for vite plugin.

Probably just need to restart my computer. Sigh. I'm on osx monterey if someone else happens to have this obscure issue.

cyrusbowman avatar Apr 03 '23 02:04 cyrusbowman

I have a similar problem that after manually reloading the page (by pressing F5 in the developer tools) the NL_TOKEN is an empty string and neutralino native api is no longer authentified.. When i restart the whole Neutralino App, the token is set again. Is this intended for security reasons or is it a bug?

ratatoeskr666 avatar Jul 11 '23 21:07 ratatoeskr666

After looking at the neutralino js lib source code, I found a solution:

const storedToken = sessionStorage.getItem('NL_TOKEN');
if (storedToken) window.NL_TOKEN = storedToken;

Neutralino.init();

PS: I'm using a quiet hacky setup for angular, so maybe the issue exist because of that in the first place

ratatoeskr666 avatar Jul 12 '23 08:07 ratatoeskr666

Add <script src="__neutralino_globals.js"></script> in index.html file fixed the problem for me. The neu process will automatically know to serve the globals there.

NL_PORT as well as other globals are sent to the browser window via __neutralino_globals.js. After a few tests, I fount it out how it works:

  1. When you run in "HMR" mode, neu run --frontend-lib-dev, the neu process searches the html file cli.frontendLibrary.patchFile for <script src="(somepath)__neutralino_globals.js"></script> and modify it into <script src="http://localhost:(anotherport)/(somepath)__neutralino_globals.js"></script>. The program then host the globals there. Notice (anotherport) is different from other assets. If you force to quit in "HMR" mode neu run --frontend-lib-dev, like ctrl+C at neu cli command line, the restoration will not happen and a broken index.html file will be left there.
  2. When you run neu run, the neu process serve __neutralino_globals.js at the same port from other static assets. So no port replacement would happen.

How to quit HMR mode without ctrl+c?

Ihasimbola avatar Oct 26 '23 14:10 Ihasimbola

Closing this issue since the latest framework loads global variables separately via a dedicated JavaScript snippet. Please check this tutorial for more details: https://neutralino.js.org/docs/getting-started/using-frontend-libraries

Thanks :tada:

shalithasuranga avatar Nov 25 '23 12:11 shalithasuranga

src in index.html is updated slower than page is loaded. Therefore, the page always tries to load ws-url with previous port

JustPilz avatar Jan 06 '24 01:01 JustPilz

I wonder if the guide is outdated. I'm trying to set up vite with React, but I have two issues:

  1. The neutralino window doesn't open, even though the server is running and I can access it in my browser, it just keeps waiting, then it timeouts but doesn't kill the vite server, so it stays as a zombie. If I try to run again, it will spawn a new instance in a new port.
  2. The neutralino.js fails with some kind of parsing error:
Uncaught DOMException: An invalid or illegal string was specified
    d index.js:1
    node_modules @neutralinojs_lib.js:413
    <anonymous> main.tsx:13

gosukiwi avatar Jan 14 '24 20:01 gosukiwi

I made template with Neutralinojs + React + TS + Vite + HMR: https://github.com/JustPilz/neu-react-ts-vite-template It works for me

JustPilz avatar Jan 28 '24 03:01 JustPilz