nodejs-sea 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2023 thanhlcm
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,87 @@
1
+ <p align="center">
2
+ <a href="https://github.com/thanhlcm90/nodejs-sea" target="blank"><img src="https://nodejs.org/static/logos/nodejsDark.svg" width="120" alt="NodeJS Logo" /></a>
3
+ </p>
4
+ <h1 align="center">NestJS SEA</h1>
5
+
6
+ <p align="center">
7
+ CLI for NodeJS single executable applications.
8
+ <p align="center">
9
+ <a href="https://www.npmjs.com/package/nestjs-auditlog" target="_blank"><img alt="npm version" src="https://img.shields.io/npm/v/nestjs-auditlog" /></a>
10
+ <a href="https://www.npmjs.com/package/nestjs-auditlog" target="_blank"><img alt="NPM" src="https://img.shields.io/npm/l/nestjs-auditlog" /></a>
11
+ <a href="https://www.npmjs.com/package/nestjs-auditlog" target="_blank"><img alt="npm downloads" src="https://img.shields.io/npm/dm/nestjs-auditlog" /></a>
12
+ <a href="https://coveralls.io/github/thanhlcm90/nestjs-auditlog?branch=main" target="_blank"><img alt="coverage" src="https://coveralls.io/repos/github/thanhlcm90/nestjs-auditlog/badge.svg?branch=main" /></a>
13
+ </p>
14
+ </p>
15
+
16
+ ## Table of Contents
17
+
18
+ - [Description](#description)
19
+ - [API document](#api-document)
20
+ - [Installation](#installation)
21
+ - [Example](#example)
22
+ - [CLI usage](#cli-usage)
23
+ - [Contact and Feedback](#contact-and-feedback)
24
+ - [License](#license)
25
+
26
+ ## Description
27
+
28
+ This feature allows the distribution of a Node.js application conveniently to a system that does not have Node.js installed.
29
+
30
+ Node.js supports the creation of <a href="https://nodejs.org/api/single-executable-applications.html" target="blank">single executable applications</a> by allowing the injection of a blob prepared by Node.js, which can contain a bundled script, into the node binary. During start up, the program checks if anything has been injected. If the blob is found, it executes the script in the blob. Otherwise Node.js operates as it normally does.
31
+
32
+ The single executable application feature currently only supports running a single embedded script using the CommonJS module system.
33
+
34
+ Users can create a single executable application from their bundled script with the node binary itself and any tool which can inject resources into the binary.
35
+
36
+ **To use this CLI, you must use Node.js 18+.**
37
+
38
+ ## API document
39
+
40
+ You can visit the full API documents <a href="https://thanhlcm90.github.io/nodejs-sea">in here</a>
41
+
42
+ ## Installation
43
+
44
+ You can install the library using npm:
45
+
46
+ ```
47
+ npm install nodejs-sea
48
+ ```
49
+
50
+ With yarn
51
+
52
+ ```
53
+ yarn add nodejs-sea
54
+ ```
55
+
56
+ ## Example
57
+
58
+ To build the single executable applications from source, please create the folder `sea`, and put the `config.json` file
59
+
60
+ ```json
61
+ {
62
+ "main": "sea/dist/server-out.js",
63
+ "output": "sea/dist/viactapp.blob",
64
+ "copyFiles": [],
65
+ "esbuild": {}
66
+ }
67
+ ```
68
+
69
+ Run build script with `npx`
70
+
71
+ ```
72
+ npx nodejs-sea sea
73
+ ```
74
+
75
+ ## CLI usage
76
+
77
+ ## Contact and Feedback
78
+
79
+ If you have any ideas, comments, or questions, don't hesitate to contact me
80
+
81
+ Best regards,
82
+
83
+ Daniel Le
84
+
85
+ ## License
86
+
87
+ This library is licensed under the MIT License. See the [LICENSE](LICENSE) file for more details.
package/lib/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/lib/cli.js ADDED
@@ -0,0 +1,304 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ var child_process = require('child_process');
5
+ var fs = require('fs');
6
+ var path = require('path');
7
+ var clipanion = require('clipanion');
8
+ var rimraf = require('rimraf');
9
+ var chalk = require('chalk');
10
+ var cliProgress = require('cli-progress');
11
+ var crypto = require('crypto');
12
+ var promises = require('stream/promises');
13
+ var url = require('url');
14
+ var zlib = require('zlib');
15
+ var nv = require('@pkgjs/nv');
16
+ var tar = require('tar');
17
+ var undici = require('undici');
18
+
19
+ function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }
20
+
21
+ var fs__default = /*#__PURE__*/_interopDefaultLegacy(fs);
22
+ var path__default = /*#__PURE__*/_interopDefaultLegacy(path);
23
+ var chalk__default = /*#__PURE__*/_interopDefaultLegacy(chalk);
24
+ var cliProgress__default = /*#__PURE__*/_interopDefaultLegacy(cliProgress);
25
+ var crypto__default = /*#__PURE__*/_interopDefaultLegacy(crypto);
26
+ var zlib__default = /*#__PURE__*/_interopDefaultLegacy(zlib);
27
+ var nv__default = /*#__PURE__*/_interopDefaultLegacy(nv);
28
+ var tar__default = /*#__PURE__*/_interopDefaultLegacy(tar);
29
+
30
+ class LoggerImpl {
31
+ constructor() {
32
+ this.currentStep = "";
33
+ this.cliProgress = null;
34
+ }
35
+ stepStarting(info) {
36
+ if (this.currentStep) {
37
+ this.stepCompleted();
38
+ }
39
+ this.currentStep = info;
40
+ console.warn(`${chalk__default["default"].yellow("→")} ${info} ...`);
41
+ }
42
+ _stepDone() {
43
+ this.currentStep = "";
44
+ if (this.cliProgress) {
45
+ this.cliProgress.stop();
46
+ this.cliProgress = null;
47
+ }
48
+ }
49
+ stepCompleted() {
50
+ const doneText = this.currentStep;
51
+ this._stepDone();
52
+ console.warn(chalk__default["default"].green(` ✓ Completed: ${doneText}`));
53
+ }
54
+ stepFailed(err) {
55
+ this._stepDone();
56
+ console.warn(chalk__default["default"].red(` ✖ Failed: ${err.message}`));
57
+ }
58
+ startProgress(maximum) {
59
+ this.cliProgress = new cliProgress__default["default"].SingleBar({}, cliProgress__default["default"].Presets.shades_classic);
60
+ this.cliProgress.start(maximum, 0);
61
+ }
62
+ doProgress(current) {
63
+ if (this.cliProgress) {
64
+ this.cliProgress.update(current);
65
+ }
66
+ }
67
+ }
68
+
69
+ class NodeUtils {
70
+ // Download and unpack a tarball containing the code for a specific Node.js version.
71
+ async getNodeSourceForVersion(range, dir, logger, retries = 2) {
72
+ var _a, _b;
73
+ logger.stepStarting(`Looking for Node.js version matching ${JSON.stringify(range)}`);
74
+ let inputIsFileUrl = false;
75
+ try {
76
+ inputIsFileUrl = new URL(range).protocol === "file:";
77
+ }
78
+ catch {
79
+ /* not a valid URL */
80
+ }
81
+ if (inputIsFileUrl) {
82
+ logger.stepStarting(`Extracting tarball from ${range} to ${dir}`);
83
+ fs__default["default"].mkdirSync(dir, { recursive: true });
84
+ await promises.pipeline(fs.createReadStream(url.fileURLToPath(range)), zlib__default["default"].createGunzip(), tar__default["default"].x({
85
+ cwd: dir,
86
+ }));
87
+ logger.stepCompleted();
88
+ const filesInDir = fs__default["default"].readdirSync(dir, { withFileTypes: true });
89
+ const dirsInDir = filesInDir.filter((f) => f.isDirectory());
90
+ if (dirsInDir.length !== 1) {
91
+ throw new Error("Node.js tarballs should contain exactly one directory");
92
+ }
93
+ return path__default["default"].join(dir, dirsInDir[0].name);
94
+ }
95
+ let releaseBaseUrl;
96
+ let version;
97
+ if (range.match(/-nightly\d+/)) {
98
+ version = range.startsWith("v") ? range : `v${range}`;
99
+ releaseBaseUrl = `https://nodejs.org/download/nightly/${version}`;
100
+ }
101
+ else {
102
+ const ver = (await nv__default["default"](range)).pop();
103
+ if (!ver) {
104
+ throw new Error(`No node version found for ${range}`);
105
+ }
106
+ version = `v${ver.version}`;
107
+ releaseBaseUrl = `https://nodejs.org/download/release/${version}`;
108
+ }
109
+ const cachedName = `node-${version}-linux-x64`;
110
+ const tarballName = `${cachedName}.tar.gz`;
111
+ const cachedTarballPath = path__default["default"].join(dir, tarballName);
112
+ const cachedNodePath = path__default["default"].join(dir, cachedName, "bin", "node");
113
+ let hasCachedTarball = false;
114
+ try {
115
+ hasCachedTarball = fs__default["default"].statSync(cachedTarballPath).size > 0;
116
+ }
117
+ catch { }
118
+ if (hasCachedTarball) {
119
+ const shaSumsUrl = `${releaseBaseUrl}/SHASUMS256.txt`;
120
+ logger.stepStarting(`Verifying existing tarball via ${shaSumsUrl}`);
121
+ const [expectedSha, realSha] = await Promise.all([
122
+ (async () => {
123
+ var _a;
124
+ try {
125
+ const shaSums = await undici.request(shaSumsUrl);
126
+ if (shaSums.statusCode !== 200)
127
+ return;
128
+ const text = await shaSums.body.text();
129
+ for (const line of text.split("\n")) {
130
+ if (line.trim().endsWith(tarballName)) {
131
+ return (_a = line.match(/^([0-9a-fA-F]+)\b/)) === null || _a === void 0 ? void 0 : _a[0];
132
+ }
133
+ }
134
+ }
135
+ catch { }
136
+ return null;
137
+ })(),
138
+ (async () => {
139
+ const hash = crypto__default["default"].createHash("sha256");
140
+ await promises.pipeline(fs.createReadStream(cachedTarballPath), hash);
141
+ return hash.digest("hex");
142
+ })(),
143
+ ]);
144
+ if (expectedSha === realSha) {
145
+ logger.stepStarting("Unpacking existing tarball");
146
+ }
147
+ else {
148
+ logger.stepFailed(new Error(`SHA256 mismatch: got ${realSha}, expected ${expectedSha}`));
149
+ hasCachedTarball = false;
150
+ }
151
+ }
152
+ let tarballStream;
153
+ let tarballWritePromise;
154
+ if (hasCachedTarball) {
155
+ const hasNodePath = fs__default["default"].statSync(cachedNodePath).size > 0;
156
+ if (hasNodePath) {
157
+ return cachedNodePath;
158
+ }
159
+ tarballStream = fs.createReadStream(cachedTarballPath);
160
+ }
161
+ else {
162
+ const url = `${releaseBaseUrl}/${tarballName}`;
163
+ logger.stepStarting(`Downloading from ${url}`);
164
+ const tarball = await undici.request(url);
165
+ if (tarball.statusCode !== 200) {
166
+ throw new Error(`Could not download Node.js source tarball: ${tarball.statusCode}`);
167
+ }
168
+ logger.stepStarting(`Unpacking tarball to ${dir}`);
169
+ fs__default["default"].mkdirSync(dir, { recursive: true });
170
+ const contentLength = +((_a = tarball.headers["content-length"]) !== null && _a !== void 0 ? _a : 0);
171
+ if (contentLength) {
172
+ logger.startProgress(contentLength);
173
+ let downloaded = 0;
174
+ (_b = tarball.body) === null || _b === void 0 ? void 0 : _b.on("data", (chunk) => {
175
+ downloaded += chunk.length;
176
+ logger.doProgress(downloaded);
177
+ });
178
+ }
179
+ tarballStream = tarball.body;
180
+ // It is important that this happens in the same tick as the streaming
181
+ // unpack below in order not to lose any data.
182
+ tarballWritePromise = promises.pipeline(tarballStream, fs.createWriteStream(cachedTarballPath));
183
+ }
184
+ // Streaming unpack. This will create the directory `${dir}/node-${version}`
185
+ // with the Node.js source tarball contents in it.
186
+ try {
187
+ await Promise.all([
188
+ promises.pipeline(tarballStream, zlib__default["default"].createGunzip(), tar__default["default"].x({
189
+ cwd: dir,
190
+ })),
191
+ tarballWritePromise,
192
+ ]);
193
+ }
194
+ catch (err) {
195
+ if (retries > 0) {
196
+ logger.stepFailed(err);
197
+ logger.stepStarting("Re-trying");
198
+ return await this.getNodeSourceForVersion(range, dir, logger, retries - 1);
199
+ }
200
+ throw err;
201
+ }
202
+ logger.stepCompleted();
203
+ return cachedNodePath;
204
+ }
205
+ }
206
+
207
+ const currentWorkingDirectory = process.cwd();
208
+ const esbuild = require("esbuild");
209
+ class PackCommand extends clipanion.Command {
210
+ constructor() {
211
+ super(...arguments);
212
+ this.input = clipanion.Option.String(`-s,--sea-config`, {
213
+ description: `Path of the sea config file. Default is sea/config.json`,
214
+ });
215
+ this.nodeVersion = clipanion.Option.String(`-n,--node-version`, {
216
+ description: `Node.js version or semver version range. Default is 22.11.0`,
217
+ });
218
+ this.clean = clipanion.Option.String(`-c,--clean`, {
219
+ description: `Node.js version or semver version range. Default is true`,
220
+ });
221
+ }
222
+ /**
223
+ * run command
224
+ *
225
+ * @param command
226
+ * @returns
227
+ */
228
+ async execCommand(command, args) {
229
+ const child = child_process.spawn(command, args, {
230
+ cwd: currentWorkingDirectory,
231
+ shell: true,
232
+ stdio: `inherit`,
233
+ });
234
+ const result = await new Promise((resolve, reject) => {
235
+ child.on(`close`, (code, signal) => resolve(code !== null && code !== void 0 ? code : 1));
236
+ });
237
+ if (result !== 0)
238
+ throw new clipanion.UsageError(`Command failed`);
239
+ }
240
+ async execute() {
241
+ var _a, _b, _c, _d;
242
+ const logger = new LoggerImpl();
243
+ const nodeUtils = new NodeUtils();
244
+ const tmpdir = path__default["default"].join(currentWorkingDirectory, "node_modules/.cache/nodejs-sea");
245
+ const configFilePath = (_a = this.input) !== null && _a !== void 0 ? _a : "sea/config.json";
246
+ const configContent = fs__default["default"].readFileSync(configFilePath).toString();
247
+ const nodeVersion = (_b = this.nodeVersion) !== null && _b !== void 0 ? _b : "22.11.0";
248
+ let config;
249
+ try {
250
+ config = JSON.parse(configContent);
251
+ }
252
+ catch (err) { }
253
+ if (!config.main || !config.output) {
254
+ throw new clipanion.UsageError("Sea config is not correct");
255
+ }
256
+ const outputPath = config.output.split("/").slice(0, -1).join("/");
257
+ const copyFiles = (_c = config.copyFiles) !== null && _c !== void 0 ? _c : [];
258
+ const esbuildConfig = config.esbuild;
259
+ const nodeSourcePath = await nodeUtils.getNodeSourceForVersion(nodeVersion, tmpdir, logger);
260
+ const appPath = path__default["default"].join(outputPath, "app");
261
+ logger.stepStarting("Cleaning dist directory");
262
+ await rimraf.rimraf(outputPath, { glob: false });
263
+ fs__default["default"].mkdirSync(outputPath, { recursive: true });
264
+ logger.stepCompleted();
265
+ if (Array.isArray(copyFiles) && copyFiles.length > 0) {
266
+ logger.stepStarting("Copy files");
267
+ for (const f of copyFiles) {
268
+ await this.execCommand("cp", ["-rf", f, `${outputPath}/`]);
269
+ }
270
+ logger.stepCompleted();
271
+ }
272
+ if (esbuildConfig) {
273
+ logger.stepStarting("Run esbuild");
274
+ await esbuild.build(esbuildConfig);
275
+ logger.stepCompleted();
276
+ }
277
+ logger.stepStarting("Inject to NodeJS Single Execute Application");
278
+ await this.execCommand("node", ["--experimental-sea-config", configFilePath]);
279
+ await this.execCommand("cp", [nodeSourcePath, appPath]);
280
+ await this.execCommand("npx", [
281
+ "postject",
282
+ appPath,
283
+ "NODE_SEA_BLOB",
284
+ config.output,
285
+ "--sentinel-fuse",
286
+ "NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2",
287
+ ]);
288
+ logger.stepCompleted();
289
+ // clear temp file
290
+ const clean = (_d = this.clean) !== null && _d !== void 0 ? _d : true;
291
+ if (clean) {
292
+ logger.stepStarting("Remove generated files");
293
+ const cleanFiles = [config.output];
294
+ if (esbuildConfig === null || esbuildConfig === void 0 ? void 0 : esbuildConfig.outfile) {
295
+ cleanFiles.push(path__default["default"].resolve(currentWorkingDirectory, esbuildConfig.outfile));
296
+ }
297
+ await this.execCommand("rm", cleanFiles);
298
+ logger.stepCompleted();
299
+ }
300
+ }
301
+ }
302
+ clipanion.runExit({
303
+ binaryLabel: ``,
304
+ }, [PackCommand]);
package/lib/cli.mjs ADDED
@@ -0,0 +1,291 @@
1
+ #!/usr/bin/env node
2
+ import { spawn } from 'child_process';
3
+ import fs, { createReadStream, createWriteStream } from 'fs';
4
+ import path from 'path';
5
+ import { runExit, Command, Option, UsageError } from 'clipanion';
6
+ import { rimraf } from 'rimraf';
7
+ import chalk from 'chalk';
8
+ import cliProgress from 'cli-progress';
9
+ import crypto from 'crypto';
10
+ import { pipeline } from 'stream/promises';
11
+ import { fileURLToPath } from 'url';
12
+ import zlib from 'zlib';
13
+ import nv from '@pkgjs/nv';
14
+ import tar from 'tar';
15
+ import { request } from 'undici';
16
+
17
+ class LoggerImpl {
18
+ constructor() {
19
+ this.currentStep = "";
20
+ this.cliProgress = null;
21
+ }
22
+ stepStarting(info) {
23
+ if (this.currentStep) {
24
+ this.stepCompleted();
25
+ }
26
+ this.currentStep = info;
27
+ console.warn(`${chalk.yellow("→")} ${info} ...`);
28
+ }
29
+ _stepDone() {
30
+ this.currentStep = "";
31
+ if (this.cliProgress) {
32
+ this.cliProgress.stop();
33
+ this.cliProgress = null;
34
+ }
35
+ }
36
+ stepCompleted() {
37
+ const doneText = this.currentStep;
38
+ this._stepDone();
39
+ console.warn(chalk.green(` ✓ Completed: ${doneText}`));
40
+ }
41
+ stepFailed(err) {
42
+ this._stepDone();
43
+ console.warn(chalk.red(` ✖ Failed: ${err.message}`));
44
+ }
45
+ startProgress(maximum) {
46
+ this.cliProgress = new cliProgress.SingleBar({}, cliProgress.Presets.shades_classic);
47
+ this.cliProgress.start(maximum, 0);
48
+ }
49
+ doProgress(current) {
50
+ if (this.cliProgress) {
51
+ this.cliProgress.update(current);
52
+ }
53
+ }
54
+ }
55
+
56
+ class NodeUtils {
57
+ // Download and unpack a tarball containing the code for a specific Node.js version.
58
+ async getNodeSourceForVersion(range, dir, logger, retries = 2) {
59
+ var _a, _b;
60
+ logger.stepStarting(`Looking for Node.js version matching ${JSON.stringify(range)}`);
61
+ let inputIsFileUrl = false;
62
+ try {
63
+ inputIsFileUrl = new URL(range).protocol === "file:";
64
+ }
65
+ catch {
66
+ /* not a valid URL */
67
+ }
68
+ if (inputIsFileUrl) {
69
+ logger.stepStarting(`Extracting tarball from ${range} to ${dir}`);
70
+ fs.mkdirSync(dir, { recursive: true });
71
+ await pipeline(createReadStream(fileURLToPath(range)), zlib.createGunzip(), tar.x({
72
+ cwd: dir,
73
+ }));
74
+ logger.stepCompleted();
75
+ const filesInDir = fs.readdirSync(dir, { withFileTypes: true });
76
+ const dirsInDir = filesInDir.filter((f) => f.isDirectory());
77
+ if (dirsInDir.length !== 1) {
78
+ throw new Error("Node.js tarballs should contain exactly one directory");
79
+ }
80
+ return path.join(dir, dirsInDir[0].name);
81
+ }
82
+ let releaseBaseUrl;
83
+ let version;
84
+ if (range.match(/-nightly\d+/)) {
85
+ version = range.startsWith("v") ? range : `v${range}`;
86
+ releaseBaseUrl = `https://nodejs.org/download/nightly/${version}`;
87
+ }
88
+ else {
89
+ const ver = (await nv(range)).pop();
90
+ if (!ver) {
91
+ throw new Error(`No node version found for ${range}`);
92
+ }
93
+ version = `v${ver.version}`;
94
+ releaseBaseUrl = `https://nodejs.org/download/release/${version}`;
95
+ }
96
+ const cachedName = `node-${version}-linux-x64`;
97
+ const tarballName = `${cachedName}.tar.gz`;
98
+ const cachedTarballPath = path.join(dir, tarballName);
99
+ const cachedNodePath = path.join(dir, cachedName, "bin", "node");
100
+ let hasCachedTarball = false;
101
+ try {
102
+ hasCachedTarball = fs.statSync(cachedTarballPath).size > 0;
103
+ }
104
+ catch { }
105
+ if (hasCachedTarball) {
106
+ const shaSumsUrl = `${releaseBaseUrl}/SHASUMS256.txt`;
107
+ logger.stepStarting(`Verifying existing tarball via ${shaSumsUrl}`);
108
+ const [expectedSha, realSha] = await Promise.all([
109
+ (async () => {
110
+ var _a;
111
+ try {
112
+ const shaSums = await request(shaSumsUrl);
113
+ if (shaSums.statusCode !== 200)
114
+ return;
115
+ const text = await shaSums.body.text();
116
+ for (const line of text.split("\n")) {
117
+ if (line.trim().endsWith(tarballName)) {
118
+ return (_a = line.match(/^([0-9a-fA-F]+)\b/)) === null || _a === void 0 ? void 0 : _a[0];
119
+ }
120
+ }
121
+ }
122
+ catch { }
123
+ return null;
124
+ })(),
125
+ (async () => {
126
+ const hash = crypto.createHash("sha256");
127
+ await pipeline(createReadStream(cachedTarballPath), hash);
128
+ return hash.digest("hex");
129
+ })(),
130
+ ]);
131
+ if (expectedSha === realSha) {
132
+ logger.stepStarting("Unpacking existing tarball");
133
+ }
134
+ else {
135
+ logger.stepFailed(new Error(`SHA256 mismatch: got ${realSha}, expected ${expectedSha}`));
136
+ hasCachedTarball = false;
137
+ }
138
+ }
139
+ let tarballStream;
140
+ let tarballWritePromise;
141
+ if (hasCachedTarball) {
142
+ const hasNodePath = fs.statSync(cachedNodePath).size > 0;
143
+ if (hasNodePath) {
144
+ return cachedNodePath;
145
+ }
146
+ tarballStream = createReadStream(cachedTarballPath);
147
+ }
148
+ else {
149
+ const url = `${releaseBaseUrl}/${tarballName}`;
150
+ logger.stepStarting(`Downloading from ${url}`);
151
+ const tarball = await request(url);
152
+ if (tarball.statusCode !== 200) {
153
+ throw new Error(`Could not download Node.js source tarball: ${tarball.statusCode}`);
154
+ }
155
+ logger.stepStarting(`Unpacking tarball to ${dir}`);
156
+ fs.mkdirSync(dir, { recursive: true });
157
+ const contentLength = +((_a = tarball.headers["content-length"]) !== null && _a !== void 0 ? _a : 0);
158
+ if (contentLength) {
159
+ logger.startProgress(contentLength);
160
+ let downloaded = 0;
161
+ (_b = tarball.body) === null || _b === void 0 ? void 0 : _b.on("data", (chunk) => {
162
+ downloaded += chunk.length;
163
+ logger.doProgress(downloaded);
164
+ });
165
+ }
166
+ tarballStream = tarball.body;
167
+ // It is important that this happens in the same tick as the streaming
168
+ // unpack below in order not to lose any data.
169
+ tarballWritePromise = pipeline(tarballStream, createWriteStream(cachedTarballPath));
170
+ }
171
+ // Streaming unpack. This will create the directory `${dir}/node-${version}`
172
+ // with the Node.js source tarball contents in it.
173
+ try {
174
+ await Promise.all([
175
+ pipeline(tarballStream, zlib.createGunzip(), tar.x({
176
+ cwd: dir,
177
+ })),
178
+ tarballWritePromise,
179
+ ]);
180
+ }
181
+ catch (err) {
182
+ if (retries > 0) {
183
+ logger.stepFailed(err);
184
+ logger.stepStarting("Re-trying");
185
+ return await this.getNodeSourceForVersion(range, dir, logger, retries - 1);
186
+ }
187
+ throw err;
188
+ }
189
+ logger.stepCompleted();
190
+ return cachedNodePath;
191
+ }
192
+ }
193
+
194
+ const currentWorkingDirectory = process.cwd();
195
+ const esbuild = require("esbuild");
196
+ class PackCommand extends Command {
197
+ constructor() {
198
+ super(...arguments);
199
+ this.input = Option.String(`-s,--sea-config`, {
200
+ description: `Path of the sea config file. Default is sea/config.json`,
201
+ });
202
+ this.nodeVersion = Option.String(`-n,--node-version`, {
203
+ description: `Node.js version or semver version range. Default is 22.11.0`,
204
+ });
205
+ this.clean = Option.String(`-c,--clean`, {
206
+ description: `Node.js version or semver version range. Default is true`,
207
+ });
208
+ }
209
+ /**
210
+ * run command
211
+ *
212
+ * @param command
213
+ * @returns
214
+ */
215
+ async execCommand(command, args) {
216
+ const child = spawn(command, args, {
217
+ cwd: currentWorkingDirectory,
218
+ shell: true,
219
+ stdio: `inherit`,
220
+ });
221
+ const result = await new Promise((resolve, reject) => {
222
+ child.on(`close`, (code, signal) => resolve(code !== null && code !== void 0 ? code : 1));
223
+ });
224
+ if (result !== 0)
225
+ throw new UsageError(`Command failed`);
226
+ }
227
+ async execute() {
228
+ var _a, _b, _c, _d;
229
+ const logger = new LoggerImpl();
230
+ const nodeUtils = new NodeUtils();
231
+ const tmpdir = path.join(currentWorkingDirectory, "node_modules/.cache/nodejs-sea");
232
+ const configFilePath = (_a = this.input) !== null && _a !== void 0 ? _a : "sea/config.json";
233
+ const configContent = fs.readFileSync(configFilePath).toString();
234
+ const nodeVersion = (_b = this.nodeVersion) !== null && _b !== void 0 ? _b : "22.11.0";
235
+ let config;
236
+ try {
237
+ config = JSON.parse(configContent);
238
+ }
239
+ catch (err) { }
240
+ if (!config.main || !config.output) {
241
+ throw new UsageError("Sea config is not correct");
242
+ }
243
+ const outputPath = config.output.split("/").slice(0, -1).join("/");
244
+ const copyFiles = (_c = config.copyFiles) !== null && _c !== void 0 ? _c : [];
245
+ const esbuildConfig = config.esbuild;
246
+ const nodeSourcePath = await nodeUtils.getNodeSourceForVersion(nodeVersion, tmpdir, logger);
247
+ const appPath = path.join(outputPath, "app");
248
+ logger.stepStarting("Cleaning dist directory");
249
+ await rimraf(outputPath, { glob: false });
250
+ fs.mkdirSync(outputPath, { recursive: true });
251
+ logger.stepCompleted();
252
+ if (Array.isArray(copyFiles) && copyFiles.length > 0) {
253
+ logger.stepStarting("Copy files");
254
+ for (const f of copyFiles) {
255
+ await this.execCommand("cp", ["-rf", f, `${outputPath}/`]);
256
+ }
257
+ logger.stepCompleted();
258
+ }
259
+ if (esbuildConfig) {
260
+ logger.stepStarting("Run esbuild");
261
+ await esbuild.build(esbuildConfig);
262
+ logger.stepCompleted();
263
+ }
264
+ logger.stepStarting("Inject to NodeJS Single Execute Application");
265
+ await this.execCommand("node", ["--experimental-sea-config", configFilePath]);
266
+ await this.execCommand("cp", [nodeSourcePath, appPath]);
267
+ await this.execCommand("npx", [
268
+ "postject",
269
+ appPath,
270
+ "NODE_SEA_BLOB",
271
+ config.output,
272
+ "--sentinel-fuse",
273
+ "NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2",
274
+ ]);
275
+ logger.stepCompleted();
276
+ // clear temp file
277
+ const clean = (_d = this.clean) !== null && _d !== void 0 ? _d : true;
278
+ if (clean) {
279
+ logger.stepStarting("Remove generated files");
280
+ const cleanFiles = [config.output];
281
+ if (esbuildConfig === null || esbuildConfig === void 0 ? void 0 : esbuildConfig.outfile) {
282
+ cleanFiles.push(path.resolve(currentWorkingDirectory, esbuildConfig.outfile));
283
+ }
284
+ await this.execCommand("rm", cleanFiles);
285
+ logger.stepCompleted();
286
+ }
287
+ }
288
+ }
289
+ runExit({
290
+ binaryLabel: ``,
291
+ }, [PackCommand]);
@@ -0,0 +1,17 @@
1
+ export interface Logger {
2
+ stepStarting(info: string): void;
3
+ stepCompleted(): void;
4
+ stepFailed(err: Error): void;
5
+ startProgress(maximum: number): void;
6
+ doProgress(current: number): void;
7
+ }
8
+ export declare class LoggerImpl implements Logger {
9
+ private currentStep;
10
+ private cliProgress;
11
+ stepStarting(info: string): void;
12
+ _stepDone(): void;
13
+ stepCompleted(): void;
14
+ stepFailed(err: Error): void;
15
+ startProgress(maximum: number): void;
16
+ doProgress(current: number): void;
17
+ }
@@ -0,0 +1,4 @@
1
+ import { Logger } from "./logger";
2
+ export declare class NodeUtils {
3
+ getNodeSourceForVersion(range: string, dir: string, logger: Logger, retries?: number): Promise<string>;
4
+ }
package/package.json ADDED
@@ -0,0 +1,84 @@
1
+ {
2
+ "name": "nodejs-sea",
3
+ "version": "1.0.0",
4
+ "description": "A powerful package for NodeJS single executable applications (SEA), support good for NestJS framework",
5
+ "main": "lib/index.js",
6
+ "license": "MIT",
7
+ "author": "Daniel Le <thanhlcm90@gmail.com>",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "https://github.com/thanhlcm90/nodejs-sea"
11
+ },
12
+ "homepage": "https://github.com/thanhlcm90/nodejs-sea#readme",
13
+ "keywords": ["node.js", "binary", "packaging", "shipping", "sea"],
14
+ "bin": {
15
+ "sea": "lib/cli.js"
16
+ },
17
+ "scripts": {
18
+ "precommit": "lint-staged",
19
+ "prepare": "husky",
20
+ "prepack": "rm -rf lib && rollup -c",
21
+ "test": "sea -s test/sea/config.json",
22
+ "version": "standard-version",
23
+ "build": "rollup -c"
24
+ },
25
+ "engines": {
26
+ "node": ">=18"
27
+ },
28
+ "devDependencies": {
29
+ "@rollup/plugin-commonjs": "^22.0.2",
30
+ "@rollup/plugin-node-resolve": "^14.1.0",
31
+ "@rollup/plugin-typescript": "^8.5.0",
32
+ "@types/cli-progress": "^3.11.6",
33
+ "@types/node": "^18.7.15",
34
+ "@types/tar": "^6.1.2",
35
+ "@typescript-eslint/eslint-plugin": "^5.0.1",
36
+ "@typescript-eslint/parser": "^5.0.1",
37
+ "@yarnpkg/eslint-config": "^1.0.0-rc.20",
38
+ "eslint": "^7.8.0",
39
+ "eslint-config-prettier": "^6.11.0",
40
+ "eslint-plugin-eslint-comments": "^3.2.0",
41
+ "eslint-plugin-import": "^2.29.1",
42
+ "eslint-plugin-prettier": "^4.2.1",
43
+ "eslint-plugin-promise": "^6.1.1",
44
+ "husky": "^9.1.7",
45
+ "lint-staged": "^15.2.11",
46
+ "prettier": "2.3.2",
47
+ "rollup": "^2.79.0",
48
+ "rollup-plugin-multi-input": "^1.3.1",
49
+ "rollup-plugin-preserve-shebang": "^1.0.1",
50
+ "rollup-plugin-terser": "^7.0.2",
51
+ "terser": "^5.15.0",
52
+ "ts-node": "^10.9.1",
53
+ "typescript": "^4.8.2"
54
+ },
55
+ "dependencies": {
56
+ "@pkgjs/nv": "^0.2.2",
57
+ "cli-progress": "^3.12.0",
58
+ "clipanion": "^3.2.0-rc.12",
59
+ "esbuild": "^0.24.0",
60
+ "postject": "^1.0.0-alpha.6",
61
+ "rimraf": "^6.0.1",
62
+ "tar": "^7.4.3",
63
+ "undici": "^7.2.0"
64
+ },
65
+ "publishConfig": {
66
+ "main": "lib/index"
67
+ },
68
+ "files": ["/lib"],
69
+ "lint-staged": {
70
+ "*.{js,jsx,ts,tsx}": ["prettier --ignore-path .eslintignore --write", "git add --force"],
71
+ "{*.json,.{babelrc,eslintrc,prettierrc,stylelintrc}}": [
72
+ "prettier --ignore-path .eslintignore --parser json --write",
73
+ "git add --force"
74
+ ]
75
+ },
76
+ "commitlint": {
77
+ "extends": ["@commitlint/config-conventional"]
78
+ },
79
+ "husky": {
80
+ "hooks": {
81
+ "pre-commit": "npm run precommit"
82
+ }
83
+ }
84
+ }