@appthreat/caxa 0.0.1

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.
@@ -0,0 +1,14 @@
1
+ {
2
+ "scripts": {
3
+ "prepare": "tsc",
4
+ "start": "npm run prepare && node ./build/index.mjs"
5
+ },
6
+ "dependencies": {
7
+ "express": "^4.18.2"
8
+ },
9
+ "devDependencies": {
10
+ "@types/express": "^4.17.14",
11
+ "@types/node": "^18.8.5",
12
+ "typescript": "^4.8.4"
13
+ }
14
+ }
@@ -0,0 +1,11 @@
1
+ import express from "express";
2
+
3
+ const app = express();
4
+
5
+ app.get<{}, string, {}, {}, {}>("/", (req, res) => {
6
+ res.send("Hello World");
7
+ });
8
+
9
+ app.listen(4000, () => {
10
+ console.log("Server started at http://localhost:4000");
11
+ });
@@ -0,0 +1,19 @@
1
+ {
2
+ "compilerOptions": {
3
+ "rootDir": "./source/",
4
+ "outDir": "./build/",
5
+
6
+ "module": "ESNext",
7
+ "moduleResolution": "NodeNext",
8
+ "esModuleInterop": true,
9
+ "lib": ["ESNext", "DOM", "DOM.Iterable"],
10
+ "skipLibCheck": true,
11
+ "target": "ESNext",
12
+
13
+ "declaration": true,
14
+ "declarationMap": true,
15
+ "sourceMap": true,
16
+
17
+ "strict": true
18
+ }
19
+ }
package/package.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "@appthreat/caxa",
3
+ "version": "0.0.1",
4
+ "description": "Package Node.js applications into executable binaries",
5
+ "author": "Team AppThreat <cloud@appthreat.com>",
6
+ "homepage": "https://github.com/appthreat/caxa",
7
+ "bugs": "https://github.com/appthreat/caxa/issues",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/AppThreat/caxa.git"
11
+ },
12
+ "license": "MIT",
13
+ "keywords": [
14
+ "packing",
15
+ "deployment",
16
+ "binary"
17
+ ],
18
+ "exports": "./build/index.mjs",
19
+ "types": "./build/index.d.mts",
20
+ "bin": "./build/index.mjs",
21
+ "scripts": {
22
+ "prepare": "cd ./source/ && tsc",
23
+ "prepare:stubs": "shx rm -f stubs/stub--win32--x64 && cross-env CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -ldflags \"-s -w -extldflags=-Wl,-z,now,-z,relro\" -o stubs/stub--win32--x64 stubs/stub.go && shx echo >> stubs/stub--win32--x64 && shx echo CAXACAXACAXA >> stubs/stub--win32--x64 && shx rm -f stubs/stub--darwin--x64 && cross-env CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -ldflags \"-s -w -extldflags=-Wl,-z,now,-z,relro\" -o stubs/stub--darwin--x64 stubs/stub.go && shx echo >> stubs/stub--darwin--x64 && shx echo CAXACAXACAXA >> stubs/stub--darwin--x64 && shx rm -f stubs/stub--darwin--arm64 && cross-env CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 go build -ldflags \"-s -w -extldflags=-Wl,-z,now,-z,relro\" -o stubs/stub--darwin--arm64 stubs/stub.go && shx echo >> stubs/stub--darwin--arm64 && shx echo CAXACAXACAXA >> stubs/stub--darwin--arm64 && shx rm -f stubs/stub--linux--x64 && cross-env CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags \"-s -w -extldflags=-Wl,-z,now,-z,relro\" -o stubs/stub--linux--x64 stubs/stub.go && shx echo >> stubs/stub--linux--x64 && shx echo CAXACAXACAXA >> stubs/stub--linux--x64 && shx rm -f stubs/stub--linux--arm64 && cross-env CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -ldflags \"-s -w -extldflags=-Wl,-z,now,-z,relro\" -o stubs/stub--linux--arm64 stubs/stub.go && shx echo >> stubs/stub--linux--arm64 && shx echo CAXACAXACAXA >> stubs/stub--linux--arm64 && shx rm -f stubs/stub--linux--arm && cross-env CGO_ENABLED=0 GOOS=linux GOARCH=arm go build -ldflags \"-s -w -extldflags=-Wl,-z,now,-z,relro\" -o stubs/stub--linux--arm stubs/stub.go && shx echo >> stubs/stub--linux--arm && shx echo CAXACAXACAXA >> stubs/stub--linux--arm",
24
+ "test": "prettier --check \"source/**/*.mts\" --end-of-line auto"
25
+ },
26
+ "dependencies": {
27
+ "archiver": "^7.0.1",
28
+ "commander": "^12.0.0",
29
+ "crypto-random-string": "^5.0.0",
30
+ "dedent": "^1.5.1",
31
+ "execa": "^8.0.1",
32
+ "fs-extra": "^11.2.0",
33
+ "globby": "^14.0.1"
34
+ },
35
+ "devDependencies": {
36
+ "@types/archiver": "^6.0.2",
37
+ "@types/dedent": "^0.7.2",
38
+ "@types/fs-extra": "^11.0.4",
39
+ "@types/node": "^20.12.2",
40
+ "cross-env": "^7.0.3",
41
+ "prettier": "^3.2.5",
42
+ "shx": "^0.3.4",
43
+ "typescript": "^5.4.3"
44
+ }
45
+ }
@@ -0,0 +1,337 @@
1
+ #!/usr/bin/env node
2
+
3
+ import path from "node:path";
4
+ import url from "node:url";
5
+ import os from "node:os";
6
+ import stream from "node:stream/promises";
7
+ import fs from "fs-extra";
8
+ import { globbySync } from "globby";
9
+ import { execa, execaCommand } from "execa";
10
+ import cryptoRandomString from "crypto-random-string";
11
+ import bash from "dedent";
12
+ import archiver from "archiver";
13
+ import * as commander from "commander";
14
+ import dedent from "dedent";
15
+ import process from "node:process";
16
+
17
+ // Some default excludes
18
+ const defaultExcludes = [
19
+ ".*",
20
+ ".git/**",
21
+ ".vscode/**",
22
+ "**/*/*.env",
23
+ "docs/**",
24
+ "test/**",
25
+ ];
26
+
27
+ export default async function caxa({
28
+ input,
29
+ output,
30
+ command,
31
+ force = true,
32
+ exclude = defaultExcludes,
33
+ filter = (() => {
34
+ const pathsToExclude = globbySync(exclude, {
35
+ expandDirectories: false,
36
+ onlyFiles: false,
37
+ }).map((pathToExclude: string) => path.normalize(pathToExclude));
38
+ return (pathToCopy: string) =>
39
+ !pathsToExclude.includes(path.normalize(pathToCopy));
40
+ })(),
41
+ dedupe = true,
42
+ prepareCommand,
43
+ prepare = async (buildDirectory: string) => {
44
+ if (prepareCommand === undefined) return;
45
+ await execaCommand(prepareCommand, { cwd: buildDirectory, shell: true });
46
+ },
47
+ includeNode = true,
48
+ stub = url.fileURLToPath(
49
+ new URL(
50
+ `../stubs/stub--${process.platform}--${process.arch}`,
51
+ import.meta.url,
52
+ ),
53
+ ),
54
+ identifier = path.join(
55
+ path.basename(path.basename(path.basename(output, ".exe"), ".app"), ".sh"),
56
+ cryptoRandomString({ length: 10, type: "alphanumeric" }).toLowerCase(),
57
+ ),
58
+ removeBuildDirectory = true,
59
+ uncompressionMessage,
60
+ }: {
61
+ input: string;
62
+ output: string;
63
+ command: string[];
64
+ force?: boolean;
65
+ exclude?: string[];
66
+ filter?: fs.CopyFilterSync | fs.CopyFilterAsync;
67
+ dedupe?: boolean;
68
+ prepareCommand?: string;
69
+ prepare?: (buildDirectory: string) => Promise<void>;
70
+ includeNode?: boolean;
71
+ stub?: string;
72
+ identifier?: string;
73
+ removeBuildDirectory?: boolean;
74
+ uncompressionMessage?: string;
75
+ }): Promise<void> {
76
+ if (!(await fs.pathExists(input)) || !(await fs.lstat(input)).isDirectory())
77
+ throw new Error(`Input isn’t a directory: ‘${input}’.`);
78
+ if ((await fs.pathExists(output)) && !force)
79
+ throw new Error(`Output already exists: ‘${output}’.`);
80
+ if (process.platform === "win32" && !output.endsWith(".exe"))
81
+ throw new Error("Windows executable must end in ‘.exe’.");
82
+
83
+ const buildDirectory = path.join(
84
+ os.tmpdir(),
85
+ "caxa/builds",
86
+ cryptoRandomString({ length: 10, type: "alphanumeric" }).toLowerCase(),
87
+ );
88
+ await fs.copy(input, buildDirectory, { filter });
89
+ if (dedupe)
90
+ await execa("npm", ["dedupe", "--production"], { cwd: buildDirectory });
91
+ await prepare(buildDirectory);
92
+ if (includeNode) {
93
+ const node = path.join(
94
+ buildDirectory,
95
+ "node_modules/.bin",
96
+ path.basename(process.execPath),
97
+ );
98
+ await fs.ensureDir(path.dirname(node));
99
+ await fs.copyFile(process.execPath, node);
100
+ }
101
+
102
+ await fs.ensureDir(path.dirname(output));
103
+ await fs.remove(output);
104
+
105
+ if (output.endsWith(".app")) {
106
+ if (process.platform !== "darwin")
107
+ throw new Error(
108
+ "macOS Application Bundles (.app) are supported in macOS only.",
109
+ );
110
+ await fs.ensureDir(path.join(output, "Contents/Resources"));
111
+ await fs.move(
112
+ buildDirectory,
113
+ path.join(output, "Contents/Resources/application"),
114
+ );
115
+ await fs.ensureDir(path.join(output, "Contents/MacOS"));
116
+ const name = path.basename(output, ".app");
117
+ await fs.writeFile(
118
+ path.join(output, "Contents/MacOS", name),
119
+ bash`
120
+ #!/usr/bin/env sh
121
+ open "$(dirname "$0")/../Resources/${name}"
122
+ ` + "\n",
123
+ { mode: 0o755 },
124
+ );
125
+ await fs.writeFile(
126
+ path.join(output, "Contents/Resources", name),
127
+ bash`
128
+ #!/usr/bin/env sh
129
+ ${command
130
+ .map(
131
+ (part) =>
132
+ `"${part.replace(
133
+ /\{\{\s*caxa\s*\}\}/g,
134
+ `$(dirname "$0")/application`,
135
+ )}"`,
136
+ )
137
+ .join(" ")}
138
+ ` + "\n",
139
+ { mode: 0o755 },
140
+ );
141
+ } else if (output.endsWith(".sh")) {
142
+ if (process.platform === "win32")
143
+ throw new Error("The Shell Stub (.sh) isn’t supported in Windows.");
144
+ let stub =
145
+ bash`
146
+ #!/usr/bin/env sh
147
+ export CAXA_TEMPORARY_DIRECTORY="$(dirname $(mktemp))/caxa"
148
+ export CAXA_EXTRACTION_ATTEMPT=-1
149
+ while true
150
+ do
151
+ export CAXA_EXTRACTION_ATTEMPT=$(( CAXA_EXTRACTION_ATTEMPT + 1 ))
152
+ export CAXA_LOCK="$CAXA_TEMPORARY_DIRECTORY/locks/${identifier}/$CAXA_EXTRACTION_ATTEMPT"
153
+ export CAXA_APPLICATION_DIRECTORY="$CAXA_TEMPORARY_DIRECTORY/applications/${identifier}/$CAXA_EXTRACTION_ATTEMPT"
154
+ if [ -d "$CAXA_APPLICATION_DIRECTORY" ]
155
+ then
156
+ if [ -d "$CAXA_LOCK" ]
157
+ then
158
+ continue
159
+ else
160
+ break
161
+ fi
162
+ else
163
+ ${
164
+ uncompressionMessage === undefined
165
+ ? bash``
166
+ : bash`echo "${uncompressionMessage}" >&2`
167
+ }
168
+ mkdir -p "$CAXA_LOCK"
169
+ mkdir -p "$CAXA_APPLICATION_DIRECTORY"
170
+ tail -n+{{caxa-number-of-lines}} "$0" | tar -xz -C "$CAXA_APPLICATION_DIRECTORY"
171
+ rmdir "$CAXA_LOCK"
172
+ break
173
+ fi
174
+ done
175
+ exec ${command
176
+ .map(
177
+ (commandPart) =>
178
+ `"${commandPart.replace(
179
+ /\{\{\s*caxa\s*\}\}/g,
180
+ `"$CAXA_APPLICATION_DIRECTORY"`,
181
+ )}"`,
182
+ )
183
+ .join(" ")} "$@"
184
+ ` + "\n";
185
+ stub = stub.replace(
186
+ "{{caxa-number-of-lines}}",
187
+ String(stub.split("\n").length),
188
+ );
189
+ await fs.writeFile(output, stub, { mode: 0o755 });
190
+ await appendTarballOfBuildDirectoryToOutput();
191
+ } else {
192
+ if (!(await fs.pathExists(stub)))
193
+ throw new Error(
194
+ `Stub not found (your operating system / architecture may be unsupported): ‘${stub}’`,
195
+ );
196
+ await fs.copyFile(stub, output);
197
+ await fs.chmod(output, 0o755);
198
+ await appendTarballOfBuildDirectoryToOutput();
199
+ await fs.appendFile(
200
+ output,
201
+ "\n" + JSON.stringify({ identifier, command, uncompressionMessage }),
202
+ );
203
+ }
204
+
205
+ if (removeBuildDirectory) await fs.remove(buildDirectory);
206
+
207
+ async function appendTarballOfBuildDirectoryToOutput(): Promise<void> {
208
+ const archive = archiver("tar", { gzip: true });
209
+ const archiveStream = fs.createWriteStream(output, { flags: "a" });
210
+ archive.pipe(archiveStream);
211
+ archive.directory(buildDirectory, false);
212
+ await archive.finalize();
213
+ await stream.finished(archiveStream);
214
+ }
215
+ }
216
+
217
+ if (url.fileURLToPath(import.meta.url) === (await fs.realpath(process.argv[1])))
218
+ await commander.program
219
+ .name("caxa")
220
+ .description("Package Node.js applications into executable binaries")
221
+ .requiredOption(
222
+ "-i, --input <input>",
223
+ "[Required] The input directory to package.",
224
+ )
225
+ .requiredOption(
226
+ "-o, --output <output>",
227
+ "[Required] The path where the executable will be produced. On Windows, must end in ‘.exe’. In macOS and Linux, may have no extension to produce regular binary. In macOS and Linux, may end in ‘.sh’ to use the Shell Stub, which is a bit smaller, but depends on some tools being installed on the end-user machine, for example, ‘tar’, ‘tail’, and so forth. In macOS, may end in ‘.app’ to generate a macOS Application Bundle.",
228
+ )
229
+ .option("-F, --no-force", "[Advanced] Don’t overwrite output if it exists.")
230
+ .option(
231
+ "-e, --exclude <path...>",
232
+ `[Advanced] Paths to exclude from the build. The paths are passed to https://github.com/sindresorhus/globby and paths that match will be excluded. [Super-Advanced, Please don’t use] If you wish to emulate ‘--include’, you may use ‘--exclude "*" ".*" "!path-to-include" ...’. The problem with ‘--include’ is that if you change your project structure but forget to change the caxa invocation, then things will subtly fail only in the packaged version.`,
233
+ )
234
+ .option(
235
+ "-D, --no-dedupe",
236
+ "[Advanced] Don’t run ‘npm dedupe --production’ on the build directory.",
237
+ )
238
+ .option(
239
+ "-p, --prepare-command <command>",
240
+ "[Advanced] Command to run on the build directory after ‘npm dedupe --production’, before packaging.",
241
+ )
242
+ .option(
243
+ "-N, --no-include-node",
244
+ "[Advanced] Don’t copy the Node.js executable to ‘{{caxa}}/node_modules/.bin/node’.",
245
+ )
246
+ .option("-s, --stub <path>", "[Advanced] Path to the stub.")
247
+ .option(
248
+ "--identifier <identifier>",
249
+ "[Advanced] Build identifier, which is part of the path in which the application will be unpacked.",
250
+ )
251
+ .option(
252
+ "-B, --no-remove-build-directory",
253
+ "[Advanced] Remove the build directory after the build.",
254
+ )
255
+ .option(
256
+ "-m, --uncompression-message <message>",
257
+ "[Advanced] A message to show when uncompressing, for example, ‘This may take a while to run the first time, please wait...’.",
258
+ )
259
+ .argument(
260
+ "<command...>",
261
+ "The command to run and optional arguments to pass to the command every time the executable is called. Paths must be absolute. The ‘{{caxa}}’ placeholder is substituted for the folder from which the package runs. The ‘node’ executable is available at ‘{{caxa}}/node_modules/.bin/node’. Use double quotes to delimit the command and each argument.",
262
+ )
263
+ .version(
264
+ JSON.parse(
265
+ await fs.readFile(new URL("../package.json", import.meta.url), "utf8"),
266
+ ).version,
267
+ )
268
+ .addHelpText(
269
+ "after",
270
+ "\n" +
271
+ dedent`
272
+ Examples:
273
+ Windows:
274
+ > caxa --input "examples/echo-command-line-parameters" --output "echo-command-line-parameters.exe" -- "{{caxa}}/node_modules/.bin/node" "{{caxa}}/index.mjs" "some" "embedded arguments" "--an-option-thats-part-of-the-command"
275
+
276
+ macOS/Linux:
277
+ $ caxa --input "examples/echo-command-line-parameters" --output "echo-command-line-parameters" -- "{{caxa}}/node_modules/.bin/node" "{{caxa}}/index.mjs" "some" "embedded arguments" "--an-option-thats-part-of-the-command"
278
+
279
+ macOS/Linux (Shell Stub):
280
+ $ caxa --input "examples/echo-command-line-parameters" --output "echo-command-line-parameters.sh" -- "{{caxa}}/node_modules/.bin/node" "{{caxa}}/index.mjs" "some" "embedded arguments" "--an-option-thats-part-of-the-command"
281
+
282
+ macOS (Application Bundle):
283
+ $ caxa --input "examples/echo-command-line-parameters" --output "Echo Command Line Parameters.app" -- "{{caxa}}/node_modules/.bin/node" "{{caxa}}/index.mjs" "some" "embedded arguments" "--an-option-thats-part-of-the-command"
284
+ `,
285
+ )
286
+ .action(
287
+ async (
288
+ command: string[],
289
+ {
290
+ input,
291
+ output,
292
+ force,
293
+ exclude = [],
294
+ dedupe,
295
+ prepareCommand,
296
+ includeNode,
297
+ stub,
298
+ identifier,
299
+ removeBuildDirectory,
300
+ uncompressionMessage,
301
+ }: {
302
+ input: string;
303
+ output: string;
304
+ force?: boolean;
305
+ exclude?: string[];
306
+ dedupe?: boolean;
307
+ prepareCommand?: string;
308
+ includeNode?: boolean;
309
+ stub?: string;
310
+ identifier?: string;
311
+ removeBuildDirectory?: boolean;
312
+ uncompressionMessage?: string;
313
+ },
314
+ ) => {
315
+ try {
316
+ await caxa({
317
+ input,
318
+ output,
319
+ command,
320
+ force,
321
+ exclude,
322
+ dedupe,
323
+ prepareCommand,
324
+ includeNode,
325
+ stub,
326
+ identifier,
327
+ removeBuildDirectory,
328
+ uncompressionMessage,
329
+ });
330
+ } catch (error: any) {
331
+ console.error(error.message);
332
+ process.exit(1);
333
+ }
334
+ },
335
+ )
336
+ .showHelpAfterError()
337
+ .parseAsync();
@@ -0,0 +1,19 @@
1
+ {
2
+ "compilerOptions": {
3
+ "rootDir": "./",
4
+ "outDir": "../build/",
5
+
6
+ "module": "NodeNext",
7
+ "moduleResolution": "NodeNext",
8
+ "esModuleInterop": true,
9
+ "lib": ["ESNext", "DOM", "DOM.Iterable"],
10
+ "skipLibCheck": true,
11
+ "target": "es2022",
12
+
13
+ "declaration": true,
14
+ "declarationMap": true,
15
+ "sourceMap": true,
16
+
17
+ "strict": true
18
+ }
19
+ }