endform 0.49.0-beta.1 → 0.49.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.
Files changed (2) hide show
  1. package/bin/endform +92 -42
  2. package/package.json +5 -5
package/bin/endform CHANGED
@@ -9,7 +9,7 @@
9
9
  const child_process = require("node:child_process");
10
10
  const path = require("node:path");
11
11
  const fs = require("node:fs");
12
- const https = require("node:https");
12
+ const { pipeline } = require("node:stream/promises");
13
13
 
14
14
  // Define supported platforms and architectures.
15
15
  const supportedPlatforms = ["darwin", "linux"];
@@ -49,8 +49,8 @@ const version = packageJson.version;
49
49
  // Define download URLs and binary names
50
50
  const MAC_ARM_URL = `https://cli.endform.dev/${version}/endform-aarch64-apple-darwin/endform`;
51
51
  const MAC_X86_URL = `https://cli.endform.dev/${version}/endform-x86_64-apple-darwin/endform`;
52
- const LINUX_ARM_URL = `https://cli.endform.dev/${version}/endform-aarch64-unknown-linux-gnu/endform`;
53
- const LINUX_X86_URL = `https://cli.endform.dev/${version}/endform-x86_64-unknown-linux-gnu/endform`;
52
+ const LINUX_ARM_URL = `https://cli.endform.dev/${version}/endform-aarch64-unknown-linux-musl/endform`;
53
+ const LINUX_X86_URL = `https://cli.endform.dev/${version}/endform-x86_64-unknown-linux-musl/endform`;
54
54
 
55
55
  // Get the appropriate URL and binary name based on platform and architecture
56
56
  const getDownloadInfo = () => {
@@ -67,53 +67,98 @@ const getDownloadInfo = () => {
67
67
  url: arch === "arm64" ? LINUX_ARM_URL : LINUX_X86_URL,
68
68
  binaryName:
69
69
  arch === "arm64"
70
- ? "endform-aarch64-unknown-linux-gnu"
71
- : "endform-x86_64-unknown-linux-gnu",
70
+ ? "endform-aarch64-unknown-linux-musl"
71
+ : "endform-x86_64-unknown-linux-musl",
72
72
  };
73
73
  };
74
74
 
75
- // Function to download the binary
76
- const downloadBinary = () => {
75
+ const downloadBinary = async ({ force } = {}) => {
77
76
  const { url, binaryName } = getDownloadInfo();
78
77
  const currentDir = __dirname;
79
78
  const binaryPath = path.join(currentDir, binaryName);
80
79
 
80
+ // Fast path: binary already present.
81
+ if (!force && fs.existsSync(binaryPath)) return binaryPath;
82
+
81
83
  console.log(
82
84
  "Endform was not installed as an optional dependency, preparing on the fly...",
83
85
  );
84
86
 
85
- return new Promise((resolve, reject) => {
86
- const file = fs.createWriteStream(binaryPath);
87
- https
88
- .get(url, (response) => {
89
- if (response.statusCode !== 200) {
90
- file.close();
91
- fs.unlink(binaryPath, () => {});
92
- reject(
93
- new Error(
94
- `Failed to download binary: ${response.statusCode} ${response.statusMessage}`,
95
- ),
96
- );
97
- return;
98
- }
99
- response.pipe(file);
100
- file.on("finish", () => {
101
- file.close();
102
- // Make the binary executable
103
- fs.chmodSync(binaryPath, "755");
104
- resolve(binaryPath);
105
- });
106
- file.on("error", (err) => {
107
- fs.unlink(binaryPath, () => {});
108
- reject(err);
109
- });
110
- })
111
- .on("error", (err) => {
112
- fs.unlink(binaryPath, () => {}); // Clean up binary file on error
113
- console.error("Error downloading binary:", err);
114
- reject(err);
115
- });
116
- });
87
+ const tmpPath = path.join(
88
+ currentDir,
89
+ `${binaryName}.${process.pid}.${Date.now()}.tmp`,
90
+ );
91
+
92
+ const response = await fetch(url);
93
+ if (!response.ok) {
94
+ throw new Error(
95
+ `Failed to download binary: ${response.status} ${response.statusText}`,
96
+ );
97
+ }
98
+
99
+ if (!response.body) {
100
+ throw new Error("Failed to download binary: empty response body");
101
+ }
102
+
103
+ const file = fs.createWriteStream(tmpPath, { mode: 0o755 });
104
+ try {
105
+ await pipeline(response.body, file);
106
+ } finally {
107
+ // `pipeline` will close the stream on success/errors, but be defensive.
108
+ try {
109
+ file.close();
110
+ } catch {}
111
+ }
112
+
113
+ try {
114
+ fs.renameSync(tmpPath, binaryPath);
115
+ fs.chmodSync(binaryPath, 0o755);
116
+ return binaryPath;
117
+ } catch (err) {
118
+ fs.unlink(tmpPath, () => {});
119
+ throw err;
120
+ }
121
+ };
122
+
123
+ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
124
+
125
+ const isTextFileBusyError = (err) => {
126
+ if (!err) return false;
127
+ if (err.code === "ETXTBSY") return true;
128
+
129
+ // On some platforms Node may surface a numeric errno.
130
+ if (typeof err.errno === "number" && err.errno === -26) return true;
131
+ if (typeof err.message === "string" && err.message.includes("ETXTBSY")) {
132
+ return true;
133
+ }
134
+
135
+ return false;
136
+ };
137
+
138
+ const execFileSyncWithRetry = async (binaryPath, args, options) => {
139
+ const maxAttempts = 5;
140
+ let delayMs = 50;
141
+
142
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
143
+ try {
144
+ child_process.execFileSync(binaryPath, args, options);
145
+ return;
146
+ } catch (err) {
147
+ const shouldRetry = isTextFileBusyError(err) && attempt < maxAttempts;
148
+ if (!shouldRetry) throw err;
149
+
150
+ console.error(
151
+ `Endform binary was busy (ETXTBSY). Re-downloading and retrying (attempt ${attempt}/${maxAttempts})...`,
152
+ );
153
+
154
+ try {
155
+ await downloadBinary({ force: true });
156
+ } catch {}
157
+
158
+ await sleep(delayMs);
159
+ delayMs *= 2;
160
+ }
161
+ }
117
162
  };
118
163
 
119
164
  // Main execution function
@@ -129,11 +174,11 @@ const main = async () => {
129
174
  // If not found locally, try to resolve from installed optional dependency
130
175
  try {
131
176
  binaryPath = require.resolve(`${packageName}/${binaryRelativePath}`);
132
- } catch (err) {
177
+ } catch (_err) {
133
178
  // If still not found, download the binary
134
179
  try {
135
180
  binaryPath = await downloadBinary();
136
- } catch (err) {
181
+ } catch (_err) {
137
182
  console.error(
138
183
  `Error: Could not download the Endform binary for ${platform} (${arch}).`,
139
184
  );
@@ -144,10 +189,15 @@ const main = async () => {
144
189
 
145
190
  // Execute the binary and forward any command-line arguments.
146
191
  try {
147
- child_process.execFileSync(binaryPath, process.argv.slice(2), {
192
+ await execFileSyncWithRetry(binaryPath, process.argv.slice(2), {
148
193
  stdio: "inherit",
149
194
  });
150
195
  } catch (err) {
196
+ // err.code is a string (e.g., "ENOENT") when the binary failed to execute,
197
+ // otherwise it's typically undefined (the CLI already printed its own error)
198
+ if (err.message && typeof err.code === "string") {
199
+ console.error(err.message);
200
+ }
151
201
  process.exit(err.status || 1);
152
202
  }
153
203
  } catch (err) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "endform",
3
- "version": "0.49.0-beta.1",
3
+ "version": "0.49.0",
4
4
  "description": "Endform CLI",
5
5
  "repository": "https://github.com/endformdev/npm",
6
6
  "license": "UNLICENSED",
@@ -12,9 +12,9 @@
12
12
  "bin"
13
13
  ],
14
14
  "optionalDependencies": {
15
- "endform-darwin-arm64": "0.49.0-beta.1",
16
- "endform-darwin-x86": "0.49.0-beta.1",
17
- "endform-linux-arm64": "0.49.0-beta.1",
18
- "endform-linux-x86": "0.49.0-beta.1"
15
+ "endform-darwin-arm64": "0.49.0",
16
+ "endform-darwin-x86": "0.49.0",
17
+ "endform-linux-arm64": "0.49.0",
18
+ "endform-linux-x86": "0.49.0"
19
19
  }
20
20
  }