NodeJS SEA
NodeJS "Single Executable Applications"1 are standalone CLI tools that embed bundled scripts in a special standalone copy of the NodeJS binary.
SheetJS is a JavaScript library for reading and writing data from spreadsheets.
This demo uses NodeJS SEA and SheetJS to create a standalone CLI tool for parsing spreadsheets and generating CSV rows.
It is strongly recommended to install NodeJS on systems using SheetJS libraries in command-line tools. This workaround should only be considered if a standalone binary is considered desirable.
Great open source software grows with user tests and reports. Any issues should be reported to the NodeJS single-executable project for further diagnosis.
Integration Details
The SheetJS NodeJS module can be required from NodeJS SEA base scripts.
NodeJS SEA does not support ECMAScript Modules!
A CommonJS script is conveniently included in the SheetJS NodeJS module package.
At a high level, single-executable applications are constructed in four steps:
-
Pre-process an existing NodeJS script, creating a SEA bundle.
-
Copy the NodeJS binary and remove any signatures.
-
Inject the SEA bundle into the unsigned NodeJS binary.
-
Re-sign the binary.
macOS and Windows enforce digital signatures. Both operating systems will warn users if a signed program is modified.
Existing signatures should be removed before injecting the SEA bundle. After injecting the SEA bundle, the binary should be resigned.
Script Requirements
Scripts that exclusively use SheetJS libraries and NodeJS built-in modules can
be bundled using NodeJS SEA. Due to limitations in the SEA bundler, a special
require
function must be created manually:
const { createRequire } = require('node:module');
require = createRequire(__filename);
const { readFile, utils } = require("xlsx");
For example, the following script accepts one command line argument, parses the
specified file using the SheetJS readFile
method2, generates CSV text from
the first worksheet using sheet_to_csv
3, and prints to terminal:
// For NodeJS SEA, the CommonJS `require` must be used
const { createRequire } = require('node:module');
require = createRequire(__filename);
const { readFile, utils } = require("xlsx");
// argv[2] is the first argument to the script
const filename = process.argv[2];
// read file
const wb = readFile(filename);
// generate CSV of first sheet
const ws = wb.Sheets[wb.SheetNames[0]];
const csv = utils.sheet_to_csv(ws);
// print to terminal
console.log(csv);
SEA Bundles
SEA Bundles are blobs that represent the script and supporting libraries.
Configuration
SEA configuration is specified using a special JSON file. Assuming no special assets are bundled with the script, there are two relevant fields:
main
is a relative path to the entry script.output
is a relative path to the output file (typically ending in.blob
)
For example, the following configuration specifies sheet2csv.js
as the entry
script and sheet2csv.blob
as the output blob:
{
"main": "sheet2csv.js",
"output": "sheet2csv.blob"
}
Construction
The main node
program, with the command-line flag --experimental-sea-config
,
will generate a SEA bundle:
node --experimental-sea-config sheet2csv.json
The bundle will be written to the file specified in the output
field of the
SEA configuration file.
Injection
A special postject
utility is used to add the SEA bundle to the NodeJS binary.
The specific command depends on the operating system.
On macOS, assuming the copy of the NodeJS binary is named sheet2csv
and the
SEA bundle is named sheet2csv.blob
, the following command injects the bundle:
npx -y postject --sentinel-fuse NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2 --macho-segment-name NODE_SEA sheet2csv NODE_SEA_BLOB sheet2csv.blob
Complete Example
This demo was tested in the following deployments:
Architecture | NodeJS | Date |
---|---|---|
darwin-x64 | 22.2.0 | 2024-05-28 |
darwin-arm | 22.2.0 | 2024-05-29 |
win10-x64 | 20.12.0 | 2024-03-26 |
win11-x64 | 20.13.1 | 2024-05-22 |
win11-arm | 20.14.0 | 2024-06-11 |
linux-x64 | 20.11.1 | 2024-03-18 |
linux-arm | 20.14.0 | 2024-06-10 |
NodeJS on Windows on ARM uses the X64 compatibility layer. It does not generate a native ARM64 binary!
- Ensure NodeJS version 20 or later is installed.
To display the current version, run the following command:
node --version
The major version number starts after the v
and ends before the first .
If the version number is 19 or earlier, upgrade NodeJS before proceeding.
Project Setup
- Create a new project folder:
mkdir sheetjs-sea
cd sheetjs-sea
npm init -y
-
Save the contents of the
sheet2csv.js
code block tosheet2csv.js
in the project folder. -
Install the SheetJS dependency:
- npm
- pnpm
- Yarn
npm i --save https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz
pnpm install --save https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz
yarn add https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz
Script Test
Before building the standalone app, the base script should be tested using the local NodeJS platform.
- Download the test file https://docs.sheetjs.com/pres.numbers:
curl -o pres.numbers https://docs.sheetjs.com/pres.numbers
- Run the script and pass
pres.numbers
as the first argument:
node sheet2csv.js pres.numbers
The script should display CSV contents from the first sheet:
Name,Index
Bill Clinton,42
GeorgeW Bush,43
Barack Obama,44
Donald Trump,45
Joseph Biden,46
SEA Bundle
-
Save the contents of the
sheet2csv.json
code block tosheet2csv.json
in the project folder. -
Generate the SEA bundle:
node --experimental-sea-config sheet2csv.json
SEA Injection
- Create a local copy of the NodeJS binary:
- MacOS
- Windows
- Linux
cp `which node` sheet2csv
- Remove the code signature.
codesign --remove-signature ./sheet2csv
In PowerShell, the Get-Command
command displays the location to node.exe
:
PS C:\sheetjs-sea> get-command node
CommandType Name Version Source
----------- ---- ------- ------
Application node.exe 20.12.0.0 C:\Program Files\nodejs\node.exe
Copy the program (listed in the "Source" column) to sheet2csv.exe
:
copy "C:\Program Files\nodejs\node.exe" sheet2csv.exe
- Remove the code signature.
signtool remove /s .\sheet2csv.exe
signtool
is included in the Windows SDK4.
cp `which node` sheet2csv
- Observe that many Linux distributions do not enforce code signatures.
- Inject the SEA bundle.
- MacOS
- Windows
- Linux
npx -y postject --sentinel-fuse NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2 --macho-segment-name NODE_SEA sheet2csv NODE_SEA_BLOB sheet2csv.blob
- Resign the binary. The following command performs macOS ad-hoc signing:
codesign -s - ./sheet2csv
npx -y postject --sentinel-fuse NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2 sheet2csv.exe NODE_SEA_BLOB sheet2csv.blob
- Resign the binary.
The following sequence generates a self-signed certificate:
$cert = New-SelfSignedCertificate -Type CodeSigning -DnsName www.onlyspans.net -CertStoreLocation Cert:\CurrentUser\My
$pass = ConvertTo-SecureString -String "hunter2" -Force -AsPlainText
Export-PfxCertificate -Cert "cert:\CurrentUser\My\$($cert.Thumbprint)" -FilePath "mycert.pfx" -Password $pass
After creating a cert, sign the binary:
signtool sign /v /f mycert.pfx /p hunter2 /fd SHA256 sheet2csv.exe
npx -y postject --sentinel-fuse NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2 sheet2csv NODE_SEA_BLOB sheet2csv.blob
- Observe that many Linux distributions do not enforce code signatures.
Standalone Test
- Run the command and pass
pres.numbers
as the first argument:
./sheet2csv pres.numbers
The program should display the same CSV contents as the script (from step 5)
- MacOS
- Windows
- Linux
- Validate the binary signature:
codesign -dv ./sheet2csv
Inspecting the output, the following line confirms ad-hoc signing was used:
Signature=adhoc
- Validate the binary signature:
signtool verify sheet2csv.exe
If the certificate is self-signed, there may be an error:
SignTool Error: A certificate chain processed, but terminated in a root
certificate which is not trusted by the trust provider.
This error is expected.
- Observe that many Linux distributions do not enforce code signatures.
Footnotes
-
See "Single Executable Applications" in the NodeJS documentation. ↩
-
See Windows SDK in the Windows Dev Center documentation. ↩