Skip to content

Commit

Permalink
feat: support running of frontend libraries from NeutralinoApp class (#…
Browse files Browse the repository at this point in the history
…15)

* feat: support running of frontend libraries from NeutralinoApp class

* fix: fixed bug

* chore: removed chalk as deps

* feat: allow app to run after build

* feat: fixed bug

* chore: fixed auth_info fetching in build mode

* fix: changed extension id to "js.node-neutralino.projectRunner" to allow the projectRunner to be run after build

* chore: use `js.neutralino.devtools` extensionId in dev to allow tests to be passed
  • Loading branch information
viralgupta authored Aug 17, 2024
1 parent 7c4c550 commit 5c84f79
Show file tree
Hide file tree
Showing 22 changed files with 320 additions and 33 deletions.
110 changes: 110 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
"description": "Wrapper project for Neutralinojs backend",
"entry point": "./src/index.js",
"main": "./src/index.js",
"types": "./types/index.d.ts",
"license": "MIT",
"author": {
"name": "Viral-Gupta and Neutralinojs community",
Expand All @@ -31,6 +30,9 @@
"browser"
],
"dependencies": {
"recursive-readdir": "^2.2.3",
"spawn-command": "^0.0.3",
"tcp-port-used": "^1.0.2",
"uuid": "^9.0.1",
"websocket": "^1.0.34"
}
Expand Down
2 changes: 1 addition & 1 deletion spec/app.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ describe('Test NeutralinoApp Class', function () {

process.stdout.write = originalWrite;

assert.ok(output.includes('--load-dir-res --path=. --export-auth-info --neu-dev-extension --url=/ --window-width=500 --window-height=500 --window-hidden=true --window-enable-inspector=false'));
assert.ok(output.includes('--load-dir-res --path=. --neu-dev-extension --export-auth-info --enable-extensions=true --url=/ --window-width=500 --window-height=500 --window-hidden=true --window-enable-inspector=false'));
});

it('Should test WS / Event Emitter', async function () {
Expand Down
27 changes: 21 additions & 6 deletions src/NeutralinoProcess.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
const { getBinaryName, normalize } = require("./utils.js");
const { getBinaryName, normalize, inBuildMode } = require("./utils.js");
const fs = require("fs");
const path = require("path");
const { spawn } = require("child_process");
const frontendLib = require("./frontendLib.js")

class NeutralinoProcess {
constructor({ url, windowOptions, WebSocketIPC }) {
Expand All @@ -11,21 +12,35 @@ class NeutralinoProcess {
this.neuProcess = null;
}

init() {
async init() {

if (this.WebSocketIPC.ws && this.WebSocketIPC.ws.readyState === this.WebSocketIPC.ws.OPEN) {
console.info("Already connected to the application.");
return;
}

this.WebSocketIPC.startWebsocket()
const frontendLibOptions = this.windowOptions.frontendLibrary;

this.WebSocketIPC.startWebsocket(frontendLibOptions);

if(frontendLibOptions && !inBuildMode()) {
frontendLib.runCommand('devCommand', frontendLibOptions);
await frontendLib.waitForFrontendLibApp(frontendLibOptions);
}

const EXEC_PERMISSION = 0o755;

let outputArgs = " --url=" + normalize(this.url);
let outputArgs = "";
if(!inBuildMode()){
if(frontendLibOptions && frontendLibOptions.devUrl){
outputArgs += " --url=" + frontendLibOptions.devUrl;
} else {
outputArgs += " --url=" + normalize(this.url);
}
}

for (let key in this.windowOptions) {
if (key == "processArgs") continue;
if (key == "processArgs" || key == "frontendLibrary") continue;

let cliKey = key.replace(/[A-Z]|^[a-z]/g, (token) => "-" + token.toLowerCase());

Expand All @@ -46,7 +61,7 @@ class NeutralinoProcess {

let binaryPath = `bin${path.sep}${binaryName}`;

let args = " --load-dir-res --path=. --export-auth-info --neu-dev-extension";
let args = `${inBuildMode() ? "" : " --load-dir-res --path=. --neu-dev-extension"} --export-auth-info --enable-extensions=true`;

if (outputArgs) args += " " + outputArgs;

Expand Down
5 changes: 5 additions & 0 deletions src/constants.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
const constants = {
files: {
authFile: ".tmp/auth_info.json",
buildAuthFile: "bin/.tmp/auth_info.json",
binaries: {
linux: {
x64: "neutralino-linux_x64",
Expand All @@ -16,6 +17,10 @@ const constants = {
x64: "neutralino-win_x64.exe"
}
},
},
misc: {
hotReloadLibPatchRegex: /(<script.*src=")(.*neutralino.js)(".*><\/script>)/g,
hotReloadGlobPatchRegex: /(<script.*src=")(.*__neutralino_globals.js)(".*><\/script>)/g
}
};

Expand Down
126 changes: 126 additions & 0 deletions src/frontendLib.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
const { trimPath } = require('./utils.js');
const spawnCommand = require('spawn-command');
const fs = require('fs');
const process = require('process');
const recursive = require('recursive-readdir');
const tpu = require('tcp-port-used');
const constants = require('./constants.js');

const HOT_REL_LIB_PATCH_REGEX = constants.misc.hotReloadLibPatchRegex;
const HOT_REL_GLOB_PATCH_REGEX = constants.misc.hotReloadGlobPatchRegex;
let originalClientLib = null;
let originalGlobals = null;

async function makeClientLibUrl(port, frontendLibOptions) {
let resourcesPath = frontendLibOptions.resourcesPath.replace(/^\//, '');
let files = await recursive(resourcesPath);
let clientLib = files.find((file) => /neutralino\.js$/.test(file));
if (clientLib) {
clientLib = clientLib.replace(/\\/g, '/'); // Fix path on Windows
}
let url = `http://localhost:${port}`;

if(clientLib) {
clientLib = '/' + clientLib;
if(frontendLibOptions.documentRoot) {
clientLib = clientLib.replace(frontendLibOptions.documentRoot, '/');
}
url += clientLib;
}
return url;
}

function makeGlobalsUrl(port) {
return `http://localhost:${port}/__neutralino_globals.js`;
}

function patchHTMLFile(scriptFile, regex, frontendLibOptions) {
let patchFile = frontendLibOptions.patchFile.replace(/^\//, '');
let html = fs.readFileSync(patchFile, 'utf8');
let matches = regex.exec(html);
if(matches) {
html = html.replace(regex, `$1${scriptFile}$3`);
fs.writeFileSync(patchFile, html);
return matches[2];
}
return null;
}

function getPortByProtocol(protocol) {
switch (protocol) {
case 'http:':
return 80;
case 'https:':
return 443;
case 'ftp:':
return 21;
default:
return -1;
}
}

module.exports.bootstrap = async (port, frontendLibOptions) => {
if(frontendLibOptions.clientLibrary) {
let clientLibUrl = await makeClientLibUrl(port);
originalClientLib = patchHTMLFile(clientLibUrl, HOT_REL_LIB_PATCH_REGEX, frontendLibOptions);
}
let globalsUrl = makeGlobalsUrl(port);
originalGlobals = patchHTMLFile(globalsUrl, HOT_REL_GLOB_PATCH_REGEX, frontendLibOptions);
console.warn('Global variables patch was applied successfully. ' +
'Please avoid sending keyboard interrupts.');
console.log(`You are working with your frontend library's development environment. ` +
'Your frontend-library-based app will run with Neutralino and be able to use the Neutralinojs API.');
}

module.exports.cleanup = ( frontendLibOptions ) => {
if(originalClientLib) {
patchHTMLFile(originalClientLib, HOT_REL_LIB_PATCH_REGEX, frontendLibOptions);
}
if(originalGlobals) {
patchHTMLFile(originalGlobals, HOT_REL_GLOB_PATCH_REGEX, frontendLibOptions);
}
console.log('Global variables patch was reverted.');
}


module.exports.runCommand = (commandKey, frontendLibOptions) => {

if (frontendLibOptions && frontendLibOptions.projectPath && frontendLibOptions[commandKey]) {
return new Promise((resolve, _reject) => {
let projectPath = trimPath(frontendLibOptions.projectPath);
let cmd = frontendLibOptions[commandKey];

console.log(`Running ${commandKey}: ${cmd}...`);
const proc = spawnCommand(cmd, { stdio: 'inherit', cwd: projectPath });
proc.on('exit', (code) => {
console.log(`FrontendLib: ${commandKey} completed with exit code: ${code}`);
resolve();
});
});
}
}

module.exports.waitForFrontendLibApp = async ( frontendLibOptions ) => {
let devUrlString = frontendLibOptions ? frontendLibOptions.devUrl : undefined;
let timeout = frontendLibOptions && frontendLibOptions.waitTimeout | 20000;
let url = new URL(devUrlString);
let portString = url.port;
let port = portString ? Number.parseInt(portString) : getPortByProtocol(url.protocol);
if (port < 0) {
utils.error(`Could not get frontendLibrary port of ${devUrlString} with protocol ${url.protocol}`);
process.exit(1);
}

let inter = setInterval(() => {
console.log(`App will be launched when ${devUrlString} on port ${port} is ready...`);
}, 2500);

try {
await tpu.waitUntilUsedOnHost(port, url.hostname, 200, timeout);
}
catch(e) {
console.error(`Timeout exceeded while waiting till local TCP port: ${port}`);
process.exit(1);
}
clearInterval(inter);
}
Loading

0 comments on commit 5c84f79

Please sign in to comment.