Skip to content

Instantly share code, notes, and snippets.

@marcus-j-davies
Last active October 23, 2024 11:33
Show Gist options
  • Save marcus-j-davies/a7a9d7193939cc481460aad3e7f8bd03 to your computer and use it in GitHub Desktop.
Save marcus-j-davies/a7a9d7193939cc481460aad3e7f8bd03 to your computer and use it in GitHub Desktop.
Node Red V4 - SFE (Single File Executable)
const esbuild = require('esbuild');
const { cp, readFile, writeFile } = require('fs/promises');
const { exists } = require('fs-extra');
const OutputDIR = './build';
const InputFile = `./main-source.js`;
const OutputFile = `${OutputDIR}/node-red.js`;
const FinalPKG = `${OutputDIR}/package.json`;
const GotSourceFile = './node_modules/got/dist/source/index.js';
const GotPackageFile = './node_modules/got/package.json';
const NRPackageFile = './node_modules/node-red/package.json';
const RequestSourceFile =
'./node_modules/@node-red/nodes/core/network/21-httprequest.js';
const EmbeddedHomePath = 'Home';
/* Updated Later */
let IsFlowBeingEmbedded = false;
/* Things to not include during bundling */
const Externals = [
'@node-red/nodes',
'@node-red/editor-client',
'@node-rs',
'oauth2orize',
'got'
];
/* Native Binding Handling */
const NativeNodeModulesPlugin = {
name: 'native-node-modules',
setup(build) {
build.onResolve({ filter: /\.node$/, namespace: 'file' }, (args) => ({
path: require.resolve(args.path, { paths: [args.resolveDir] }),
namespace: 'node-file'
}));
build.onLoad({ filter: /.*/, namespace: 'node-file' }, (args) => ({
contents: `
import path from ${JSON.stringify(args.path)}
try { module.exports = require(path) }
catch {}
`
}));
build.onResolve({ filter: /\.node$/, namespace: 'node-file' }, (args) => ({
path: args.path,
namespace: 'file'
}));
const opts = build.initialOptions;
opts.loader = opts.loader || {};
opts.loader['.node'] = 'file';
opts.loader['.sh'] = 'binary';
}
};
/* ES Build Got */
const ESBGot = async () => {
const config = {
entryPoints: [GotSourceFile],
bundle: true,
platform: 'node',
target: 'node20',
allowOverwrite: true,
outfile: GotSourceFile
};
await esbuild.build(config);
const Package = require(GotPackageFile);
Package.type = 'commonjs';
await writeFile(GotPackageFile, JSON.stringify(Package, null, 2));
};
/* Main */
const Run = async () => {
// is Flow being embdded
IsFlowBeingEmbedded = await exists(EmbeddedHomePath);
await ESBGot();
const config = {
entryPoints: [InputFile],
plugins: [NativeNodeModulesPlugin],
bundle: true,
platform: 'node',
target: 'node20',
outfile: OutputFile,
external: Externals
};
await esbuild.build(config);
const NRV = require(NRPackageFile).version;
let patchedNR = (await readFile(OutputFile, 'utf-8')).replace(
'= getVersion()',
`= "${NRV}"`
);
if (IsFlowBeingEmbedded) {
patchedNR = patchedNR
.replace('return fs.ensureDir(libFlowsDir)', '')
.replace(
'writePromises.push(util.writeFile(sectionFilename2, sectionContent2, sectionFilename2 + ".backup"));',
''
)
.replace(
'writePromises.push(util.writeFile(sectionFilename, sectionContent, sectionFilename + ".backup"));',
''
)
.replace(
'await fs.ensureDir(fspath.join(settings.userDir, "node_modules"));',
''
);
}
await writeFile(OutputFile, patchedNR);
const PatchedRequest = (await readFile(RequestSourceFile, 'utf-8')).replace(
"const { got } = await import('got')",
"const { got } = require('got')"
);
await writeFile(RequestSourceFile, PatchedRequest);
for (const ext of Externals) {
const path = ext.startsWith('./') ? ext : `node_modules/${ext}`;
if (await exists(path)) {
await cp(path, `${OutputDIR}/${path}`, { recursive: true });
}
}
const PKG = {
name: 'node-red',
bin: 'node-red.js',
pkg: {
assets: ['node_modules/**']
}
};
if (IsFlowBeingEmbedded) {
PKG.pkg.assets.push(`${EmbeddedHomePath}/**`);
await cp(EmbeddedHomePath, `${OutputDIR}/${EmbeddedHomePath}`, {
recursive: true
});
}
await writeFile(FinalPKG, JSON.stringify(PKG, null, 2));
};
Run().catch((err) => {
console.error(err);
process.exit(1);
});
const http = require('http');
const express = require('express');
const RED = require('node-red');
const { exists } = require('fs-extra');
const { join } = require('path');
const Run = async () => {
const app = express();
const server = http.createServer(app);
/* Port */
const port = 1880;
/* Creds Key */
const secret = '#########################'; /* Should be changed to your own */
/* Lock UI */
const disableUI = false; /* Should be set to true, before compiling to an SFE */
/* Think setting.js */
const settings = {
flowFile: 'flows.json',
flowFilePretty: true,
httpAdminRoot: '/',
httpNodeRoot: '/',
userDir: join(process.cwd(), 'Home'),
credentialSecret: secret,
disableEditor: disableUI,
editorTheme: {
projects: {
enabled: false
},
tours: false
}
};
/* Redirect to embedded Home Dir - if we embedded the Home folder in the output */
/* Dont mess with this */
if (await exists('/snapshot/NR/build/Home')) {
settings.userDir = '/snapshot/NR/build/Home';
}
RED.init(server, settings);
app.use(settings.httpAdminRoot, RED.httpAdmin);
app.use(settings.httpNodeRoot, RED.httpNode);
server.listen(port);
RED.start();
};
Run();
{
"name": "node-redex",
"dependencies": {
"@yao-pkg/pkg": "5.12.0",
"esbuild": "0.23.0",
"node-red": "4.0.2"
},
"scripts": {
"design": "node main-source.js",
"build": "node build.js",
"package": "cd ./build && pkg . --compress gzip -t host --targets node20 --output ./dist/node-red"
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment