@pipelab/core-node 1.0.0-beta.6 → 1.0.0-beta.9

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pipelab/core-node",
3
- "version": "1.0.0-beta.6",
3
+ "version": "1.0.0-beta.9",
4
4
  "private": false,
5
5
  "description": "The Pipelab automation engine for Node.js",
6
6
  "license": "FSL-1.1-MIT",
@@ -40,8 +40,8 @@
40
40
  "type-fest": "4.39.0",
41
41
  "ws": "8.18.3",
42
42
  "yauzl": "2.10.0",
43
- "@pipelab/constants": "1.0.0-beta.5",
44
- "@pipelab/shared": "1.0.0-beta.4"
43
+ "@pipelab/constants": "1.0.0-beta.7",
44
+ "@pipelab/shared": "1.0.0-beta.5"
45
45
  },
46
46
  "devDependencies": {
47
47
  "@types/adm-zip": "0.5.7",
@@ -54,7 +54,7 @@
54
54
  "@types/yauzl": "2.10.3",
55
55
  "tsdown": "0.21.2",
56
56
  "typescript": "^5.0.0",
57
- "@pipelab/tsconfig": "1.0.0-beta.4"
57
+ "@pipelab/tsconfig": "1.0.0-beta.5"
58
58
  },
59
59
  "scripts": {
60
60
  "build": "tsdown",
@@ -1,7 +1,7 @@
1
1
  import { dirname, delimiter, join } from "node:path";
2
2
  import { tmpdir } from "node:os";
3
- import { mkdir, readdir, readFile, writeFile, access, chmod, rm, cp } from "node:fs/promises";
4
- import { existsSync, constants } from "node:fs";
3
+ import { mkdir, readdir, readFile, writeFile, access, chmod, rm, cp, rename } from "node:fs/promises";
4
+ import { existsSync, constants, statSync, readdirSync } from "node:fs";
5
5
  import pacote from "pacote";
6
6
  import semver from "semver";
7
7
  import { isDev, projectRoot, PipelabContext } from "../context";
@@ -12,6 +12,29 @@ import { downloadFile, extractZip, extractTarGz, generateTempFolder } from "./fs
12
12
  export const DEFAULT_NODE_VERSION = "24.14.1";
13
13
  export const DEFAULT_PNPM_VERSION = "10.12.0";
14
14
 
15
+ function isPackageComplete(packageDir: string): boolean {
16
+ return existsSync(join(packageDir, "package.json"));
17
+ }
18
+
19
+ function isNodeJSComplete(nodePath: string): boolean {
20
+ try {
21
+ return existsSync(nodePath) && statSync(nodePath).size > 0;
22
+ } catch {
23
+ return false;
24
+ }
25
+ }
26
+
27
+ function isDependenciesInstalledSync(packageDir: string): boolean {
28
+ const nodeModulesPath = join(packageDir, "node_modules");
29
+ if (!existsSync(nodeModulesPath)) return false;
30
+ try {
31
+ const files = readdirSync(nodeModulesPath);
32
+ return files.length > 0;
33
+ } catch {
34
+ return false;
35
+ }
36
+ }
37
+
15
38
  /**
16
39
  * In-memory lock to prevent concurrent operations on the same resource (e.g., downloading Node.js).
17
40
  */
@@ -113,19 +136,49 @@ export async function fetchPackage(
113
136
  const cachePath = join(ctx.userDataPath, "cache", "pacote");
114
137
  const packageDir = join(baseDir, resolvedVersion);
115
138
 
116
- // If the package already exists and we don't need to install dependencies, return immediately
117
- if (existsSync(packageDir) && !options?.installDeps) {
139
+ // If the package already exists and we don't need to install dependencies (or they are already installed), return immediately
140
+ const isInstalled = options?.installDeps
141
+ ? (isPackageComplete(packageDir) && isDependenciesInstalledSync(packageDir))
142
+ : isPackageComplete(packageDir);
143
+
144
+ if (isInstalled) {
118
145
  return { packageDir, resolvedVersion };
119
146
  }
120
147
 
121
148
  const lockKey = `package:${packageName}:${resolvedVersion}`;
122
149
  return withLock(lockKey, async () => {
123
- if (!existsSync(packageDir)) {
150
+ if (!isPackageComplete(packageDir)) {
124
151
  console.log(`[Fetcher] ${packageName}@${resolvedVersion}: Downloading to ${packageDir}...`);
125
- await mkdir(packageDir, { recursive: true });
126
- await pacote.extract(`${packageName}@${resolvedVersion}`, packageDir, {
127
- cache: cachePath,
128
- });
152
+
153
+ const tempDir = join(baseDir, `.tmp-${resolvedVersion}-${Math.random().toString(36).slice(2)}`);
154
+ await mkdir(tempDir, { recursive: true });
155
+ try {
156
+ await pacote.extract(`${packageName}@${resolvedVersion}`, tempDir, {
157
+ cache: cachePath,
158
+ });
159
+
160
+ // Resolve entryPoint from package.json inside the temp directory first to ensure everything works
161
+ await resolveEntryPoint(tempDir, packageName);
162
+
163
+ // Remove existing incomplete packageDir if any
164
+ if (existsSync(packageDir)) {
165
+ await rm(packageDir, { recursive: true, force: true }).catch(() => {});
166
+ }
167
+
168
+ // Atomically rename the temp directory to the target packageDir
169
+ try {
170
+ await rename(tempDir, packageDir);
171
+ } catch (err: any) {
172
+ if (isPackageComplete(packageDir)) {
173
+ console.log(`[Fetcher] Destination ${packageDir} already exists and is valid.`);
174
+ } else {
175
+ throw err;
176
+ }
177
+ }
178
+ } catch (err) {
179
+ await rm(tempDir, { recursive: true, force: true }).catch(() => {});
180
+ throw err;
181
+ }
129
182
  }
130
183
 
131
184
  // 2. Resolve entry point from package.json for downloaded package
@@ -196,13 +249,13 @@ export async function ensureNodeJS(context: PipelabContext, version = DEFAULT_NO
196
249
  const nodeDir = context.getThirdPartyPath("node", version);
197
250
  const finalNodePath = join(nodeDir, isWindows ? "node.exe" : "bin/node");
198
251
 
199
- if (existsSync(finalNodePath)) {
252
+ if (isNodeJSComplete(finalNodePath)) {
200
253
  return finalNodePath;
201
254
  }
202
255
 
203
256
  const lockKey = `node:${version}`;
204
257
  return withLock(lockKey, async () => {
205
- if (existsSync(finalNodePath)) return finalNodePath;
258
+ if (isNodeJSComplete(finalNodePath)) return finalNodePath;
206
259
 
207
260
  const arch = process.arch === "x64" ? "x64" : process.arch === "arm64" ? "arm64" : "x86";
208
261
  const platform = isWindows ? "win" : process.platform === "darwin" ? "osx" : "linux";
@@ -234,12 +287,37 @@ export async function ensureNodeJS(context: PipelabContext, version = DEFAULT_NO
234
287
  if (!nodeSubDir) throw new Error(`Could not find extracted Node.js directory`);
235
288
 
236
289
  const sourceDir = join(extractTempDir, nodeSubDir);
237
- await mkdir(dirname(nodeDir), { recursive: true });
238
- await rm(nodeDir, { recursive: true, force: true });
239
- await cp(sourceDir, nodeDir, { recursive: true });
240
- await rm(tempDir, { recursive: true, force: true });
290
+ const parentDir = dirname(nodeDir);
291
+ await mkdir(parentDir, { recursive: true });
292
+
293
+ const tempNodeDir = join(parentDir, `.tmp-node-${version}-${Math.random().toString(36).slice(2)}`);
294
+ await mkdir(tempNodeDir, { recursive: true });
295
+
296
+ try {
297
+ await cp(sourceDir, tempNodeDir, { recursive: true });
298
+ if (!isWindows) {
299
+ const tempNodePath = join(tempNodeDir, "bin/node");
300
+ await chmod(tempNodePath, 0o755).catch(() => {});
301
+ }
302
+
303
+ if (existsSync(nodeDir)) {
304
+ await rm(nodeDir, { recursive: true, force: true }).catch(() => {});
305
+ }
306
+
307
+ try {
308
+ await rename(tempNodeDir, nodeDir);
309
+ } catch (err: any) {
310
+ if (isNodeJSComplete(finalNodePath)) {
311
+ console.log(`[Fetcher] Node.js directory already exists and is valid.`);
312
+ } else {
313
+ throw err;
314
+ }
315
+ }
316
+ } finally {
317
+ await rm(tempNodeDir, { recursive: true, force: true }).catch(() => {});
318
+ await rm(tempDir, { recursive: true, force: true }).catch(() => {});
319
+ }
241
320
 
242
- if (!isWindows) await chmod(finalNodePath, 0o755).catch(() => {});
243
321
  return finalNodePath;
244
322
  });
245
323
  }
@@ -269,30 +347,33 @@ export async function ensurePNPM(context: PipelabContext, version = DEFAULT_PNPM
269
347
  async function installDependencies(packageDir: string, packageName: string, options: FetchOptions) {
270
348
  const nodeModulesPath = join(packageDir, "node_modules");
271
349
 
272
- if (existsSync(nodeModulesPath)) {
273
- try {
274
- const files = await readdir(nodeModulesPath);
275
- if (files.length === 0) {
276
- console.warn(
277
- `[Fetcher] ${packageName}: node_modules exists but is empty. Re-installing...`,
278
- );
279
- } else {
280
- console.log(`[Fetcher] ${packageName}: Dependencies already installed, skipping.`);
281
- return;
282
- }
283
- } catch (e) {
284
- // Continue to install if readdir fails
285
- }
350
+ if (isDependenciesInstalledSync(packageDir)) {
351
+ console.log(`[Fetcher] ${packageName}: Dependencies already installed, skipping.`);
352
+ return;
286
353
  }
287
354
 
288
- console.log(`[Fetcher] ${packageName}: Ensuring dependencies are installed...`);
355
+ // To prevent other processes from seeing a partially populated node_modules,
356
+ // we can create a temp directory next to packageDir, run pnpm there, and rename node_modules
357
+ const tempDir = `${packageDir}.tmp-deps-${Math.random().toString(36).slice(2)}`;
358
+ await mkdir(tempDir, { recursive: true });
289
359
  try {
290
- const { all } = await runPnpm(packageDir, {
360
+ // Copy package.json to tempDir so pnpm can install dependencies
361
+ await cp(join(packageDir, "package.json"), join(tempDir, "package.json"));
362
+
363
+ console.log(`[Fetcher] ${packageName}: Ensuring dependencies are installed...`);
364
+ const { all } = await runPnpm(tempDir, {
291
365
  signal: options.signal,
292
366
  context: options.context,
293
367
  });
294
368
 
295
369
  if (all) console.log(`[Fetcher] ${packageName}: Installation trace:\n${all}`);
370
+
371
+ // Atomically rename node_modules from tempDir to packageDir/node_modules
372
+ const tempNodeModules = join(tempDir, "node_modules");
373
+ if (existsSync(nodeModulesPath)) {
374
+ await rm(nodeModulesPath, { recursive: true, force: true }).catch(() => {});
375
+ }
376
+ await rename(tempNodeModules, nodeModulesPath);
296
377
  console.log(`[Fetcher] ${packageName}: Dependencies installed successfully.`);
297
378
  } catch (err: any) {
298
379
  console.error(
@@ -300,6 +381,8 @@ async function installDependencies(packageDir: string, packageName: string, opti
300
381
  );
301
382
  if (err.all) console.error(`[Fetcher] ${packageName}: Error details:\n${err.all}`);
302
383
  throw new Error(`Failed to install dependencies for ${packageName}. See logs for details.`);
384
+ } finally {
385
+ await rm(tempDir, { recursive: true, force: true }).catch(() => {});
303
386
  }
304
387
  }
305
388