@pipelab/core-node 1.0.0-beta.1 → 1.0.0-beta.11

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.
@@ -1,7 +1,17 @@
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 {
4
+ mkdir,
5
+ readdir,
6
+ readFile,
7
+ writeFile,
8
+ access,
9
+ chmod,
10
+ rm,
11
+ cp,
12
+ rename,
13
+ } from "node:fs/promises";
14
+ import { existsSync, constants, statSync, readdirSync } from "node:fs";
5
15
  import pacote from "pacote";
6
16
  import semver from "semver";
7
17
  import { isDev, projectRoot, PipelabContext } from "../context";
@@ -12,6 +22,29 @@ import { downloadFile, extractZip, extractTarGz, generateTempFolder } from "./fs
12
22
  export const DEFAULT_NODE_VERSION = "24.14.1";
13
23
  export const DEFAULT_PNPM_VERSION = "10.12.0";
14
24
 
25
+ function isPackageComplete(packageDir: string): boolean {
26
+ return existsSync(join(packageDir, "package.json"));
27
+ }
28
+
29
+ function isNodeJSComplete(nodePath: string): boolean {
30
+ try {
31
+ return existsSync(nodePath) && statSync(nodePath).size > 0;
32
+ } catch {
33
+ return false;
34
+ }
35
+ }
36
+
37
+ function isDependenciesInstalledSync(packageDir: string): boolean {
38
+ const nodeModulesPath = join(packageDir, "node_modules");
39
+ if (!existsSync(nodeModulesPath)) return false;
40
+ try {
41
+ const files = readdirSync(nodeModulesPath);
42
+ return files.length > 0;
43
+ } catch {
44
+ return false;
45
+ }
46
+ }
47
+
15
48
  /**
16
49
  * In-memory lock to prevent concurrent operations on the same resource (e.g., downloading Node.js).
17
50
  */
@@ -57,12 +90,16 @@ export async function fetchPackage(
57
90
  isLocal?: boolean;
58
91
  entryPoint?: string;
59
92
  }> {
93
+ const start = Date.now();
60
94
  // 0. Check for local monorepo package in development
61
95
  if (isDev && projectRoot && process.env.PIPELAB_FORCE_NPM !== "true") {
62
96
  if (packageName.startsWith("@pipelab/")) {
97
+ const localStart = Date.now();
63
98
  const local = await tryResolveMonorepoPackage(packageName);
64
99
  if (local) {
65
- console.log(`[Fetcher] ${packageName}: Resolved to local source at ${local.packageDir}`);
100
+ console.log(
101
+ `[Fetcher] ${packageName}: Resolved to local source at ${local.packageDir} (${Date.now() - localStart}ms)`,
102
+ );
66
103
  return {
67
104
  ...local,
68
105
  resolvedVersion: "workspace",
@@ -76,6 +113,7 @@ export async function fetchPackage(
76
113
  let resolvedVersion: string;
77
114
 
78
115
  console.log(`[Fetcher] Resolving ${packageName}@${versionOrRange || "latest"}...`);
116
+ const resolveStart = Date.now();
79
117
 
80
118
  try {
81
119
  // 1. Resolve version/range using npm with session-wide memoization and disk cache
@@ -94,15 +132,25 @@ export async function fetchPackage(
94
132
  const foundVersion = packument["dist-tags"]?.[range] || semver.maxSatisfying(versions, range);
95
133
 
96
134
  if (!foundVersion) {
97
- throw new Error(`Package ${packageName}@${range} not found on npm (available tags: ${Object.keys(packument["dist-tags"] || {}).join(", ")})`);
135
+ throw new Error(
136
+ `Package ${packageName}@${range} not found on npm (available tags: ${Object.keys(packument["dist-tags"] || {}).join(", ")})`,
137
+ );
98
138
  }
99
139
  resolvedVersion = foundVersion;
100
- console.log(`[Fetcher] ${packageName}: Resolved to v${resolvedVersion} via npm`);
140
+ console.log(
141
+ `[Fetcher] ${packageName}: Resolved to v${resolvedVersion} via npm (${Date.now() - resolveStart}ms)`,
142
+ );
101
143
  } catch (error) {
102
- console.warn(`[Fetcher] ${packageName}: remote resolution failed, trying local fallback...`);
144
+ console.warn(
145
+ `[Fetcher] ${packageName}: remote resolution failed (${Date.now() - resolveStart}ms), trying local fallback...`,
146
+ );
147
+ const fallbackStart = Date.now();
103
148
  const fallbackVersion = await tryLocalFallback(versionOrRange, error, baseDir, packageName);
104
149
  if (fallbackVersion) {
105
150
  resolvedVersion = fallbackVersion;
151
+ console.log(
152
+ `[Fetcher] ${packageName}: Resolved to local fallback ${resolvedVersion} (${Date.now() - fallbackStart}ms)`,
153
+ );
106
154
  } else {
107
155
  throw error;
108
156
  }
@@ -111,28 +159,81 @@ export async function fetchPackage(
111
159
  const cachePath = join(ctx.userDataPath, "cache", "pacote");
112
160
  const packageDir = join(baseDir, resolvedVersion);
113
161
 
114
- // If the package already exists and we don't need to install dependencies, return immediately
115
- if (existsSync(packageDir) && !options?.installDeps) {
162
+ const checkStart = Date.now();
163
+ // If the package already exists and we don't need to install dependencies (or they are already installed), return immediately
164
+ const isInstalled = options?.installDeps
165
+ ? isPackageComplete(packageDir) && isDependenciesInstalledSync(packageDir)
166
+ : isPackageComplete(packageDir);
167
+ const checkDuration = Date.now() - checkStart;
168
+
169
+ if (isInstalled) {
170
+ console.log(
171
+ `[Fetcher] ${packageName}@${resolvedVersion}: Already installed (check took ${checkDuration}ms, fetchPackage took ${Date.now() - start}ms)`,
172
+ );
116
173
  return { packageDir, resolvedVersion };
117
174
  }
118
175
 
119
176
  const lockKey = `package:${packageName}:${resolvedVersion}`;
120
177
  return withLock(lockKey, async () => {
121
- if (!existsSync(packageDir)) {
178
+ if (!isPackageComplete(packageDir)) {
122
179
  console.log(`[Fetcher] ${packageName}@${resolvedVersion}: Downloading to ${packageDir}...`);
123
- await mkdir(packageDir, { recursive: true });
124
- await pacote.extract(`${packageName}@${resolvedVersion}`, packageDir, {
125
- cache: cachePath,
126
- });
180
+ const downloadStart = Date.now();
181
+
182
+ const tempDir = join(
183
+ baseDir,
184
+ `.tmp-${resolvedVersion}-${Math.random().toString(36).slice(2)}`,
185
+ );
186
+ await mkdir(tempDir, { recursive: true });
187
+ try {
188
+ await pacote.extract(`${packageName}@${resolvedVersion}`, tempDir, {
189
+ cache: cachePath,
190
+ });
191
+
192
+ // Resolve entryPoint from package.json inside the temp directory first to ensure everything works
193
+ await resolveEntryPoint(tempDir, packageName);
194
+
195
+ // Remove existing incomplete packageDir if any
196
+ if (existsSync(packageDir)) {
197
+ await rm(packageDir, { recursive: true, force: true }).catch(() => {});
198
+ }
199
+
200
+ // Atomically rename the temp directory to the target packageDir
201
+ try {
202
+ await rename(tempDir, packageDir);
203
+ } catch (err: any) {
204
+ if (isPackageComplete(packageDir)) {
205
+ console.log(`[Fetcher] Destination ${packageDir} already exists and is valid.`);
206
+ } else {
207
+ throw err;
208
+ }
209
+ }
210
+ console.log(
211
+ `[Fetcher] ${packageName}@${resolvedVersion}: Downloaded and extracted in ${Date.now() - downloadStart}ms`,
212
+ );
213
+ } catch (err) {
214
+ await rm(tempDir, { recursive: true, force: true }).catch(() => {});
215
+ throw err;
216
+ }
127
217
  }
128
218
 
129
219
  // 2. Resolve entry point from package.json for downloaded package
220
+ const entryStart = Date.now();
130
221
  const entryPoint = await resolveEntryPoint(packageDir, packageName);
222
+ console.log(
223
+ `[Fetcher] ${packageName}@${resolvedVersion}: Resolved entry point in ${Date.now() - entryStart}ms`,
224
+ );
131
225
 
132
226
  if (options?.installDeps) {
227
+ const depsStart = Date.now();
133
228
  await installDependencies(packageDir, packageName, options);
229
+ console.log(
230
+ `[Fetcher] ${packageName}@${resolvedVersion}: Installed dependencies in ${Date.now() - depsStart}ms`,
231
+ );
134
232
  }
135
233
 
234
+ console.log(
235
+ `[Fetcher] ${packageName}@${resolvedVersion}: FetchPackage complete in ${Date.now() - start}ms`,
236
+ );
136
237
  return { packageDir, resolvedVersion, entryPoint };
137
238
  });
138
239
  }
@@ -150,7 +251,13 @@ export async function runPnpm(
150
251
  },
151
252
  ) {
152
253
  const {
153
- args = ["install", "--prod", "--no-lockfile", "--prefer-offline", "--no-verify-store-integrity"],
254
+ args = [
255
+ "install",
256
+ "--prod",
257
+ "--no-lockfile",
258
+ "--prefer-offline",
259
+ "--no-verify-store-integrity",
260
+ ],
154
261
  extraEnv = {},
155
262
  signal,
156
263
  context: ctx,
@@ -183,21 +290,22 @@ export async function runPnpm(
183
290
  /**
184
291
  * Installs a specific version of Node.js if not already present.
185
292
  */
186
- export async function ensureNodeJS(
187
- context: PipelabContext,
188
- version = DEFAULT_NODE_VERSION,
189
- ) {
293
+ export async function ensureNodeJS(context: PipelabContext, version = DEFAULT_NODE_VERSION) {
294
+ const checkStart = Date.now();
190
295
  const isWindows = process.platform === "win32";
191
296
  const nodeDir = context.getThirdPartyPath("node", version);
192
297
  const finalNodePath = join(nodeDir, isWindows ? "node.exe" : "bin/node");
193
298
 
194
- if (existsSync(finalNodePath)) {
299
+ if (isNodeJSComplete(finalNodePath)) {
300
+ console.log(
301
+ `[Environment] Node.js check took ${Date.now() - checkStart}ms (found at ${finalNodePath})`,
302
+ );
195
303
  return finalNodePath;
196
304
  }
197
305
 
198
306
  const lockKey = `node:${version}`;
199
307
  return withLock(lockKey, async () => {
200
- if (existsSync(finalNodePath)) return finalNodePath;
308
+ if (isNodeJSComplete(finalNodePath)) return finalNodePath;
201
309
 
202
310
  const arch = process.arch === "x64" ? "x64" : process.arch === "arm64" ? "arm64" : "x86";
203
311
  const platform = isWindows ? "win" : process.platform === "darwin" ? "osx" : "linux";
@@ -211,10 +319,13 @@ export async function ensureNodeJS(
211
319
 
212
320
  sendStartupProgress(`Downloading Node.js v${version}...`);
213
321
  console.log(`Downloading Node.js from ${downloadUrl}...`);
322
+ const dlStart = Date.now();
214
323
  await downloadFile(downloadUrl, archivePath);
324
+ console.log(`[Environment] Node.js download took ${Date.now() - dlStart}ms`);
215
325
 
216
326
  sendStartupProgress(`Extracting Node.js v${version}...`);
217
327
  console.log(`Extracting Node.js to ${tempDir}...`);
328
+ const extStart = Date.now();
218
329
  const extractTempDir = join(tempDir, "extracted");
219
330
  await mkdir(extractTempDir, { recursive: true });
220
331
 
@@ -229,12 +340,42 @@ export async function ensureNodeJS(
229
340
  if (!nodeSubDir) throw new Error(`Could not find extracted Node.js directory`);
230
341
 
231
342
  const sourceDir = join(extractTempDir, nodeSubDir);
232
- await mkdir(dirname(nodeDir), { recursive: true });
233
- await rm(nodeDir, { recursive: true, force: true });
234
- await cp(sourceDir, nodeDir, { recursive: true });
235
- await rm(tempDir, { recursive: true, force: true });
343
+ const parentDir = dirname(nodeDir);
344
+ await mkdir(parentDir, { recursive: true });
345
+
346
+ const tempNodeDir = join(
347
+ parentDir,
348
+ `.tmp-node-${version}-${Math.random().toString(36).slice(2)}`,
349
+ );
350
+ await mkdir(tempNodeDir, { recursive: true });
351
+
352
+ try {
353
+ await cp(sourceDir, tempNodeDir, { recursive: true });
354
+ if (!isWindows) {
355
+ const tempNodePath = join(tempNodeDir, "bin/node");
356
+ await chmod(tempNodePath, 0o755).catch(() => {});
357
+ }
358
+
359
+ if (existsSync(nodeDir)) {
360
+ await rm(nodeDir, { recursive: true, force: true }).catch(() => {});
361
+ }
236
362
 
237
- if (!isWindows) await chmod(finalNodePath, 0o755).catch(() => { });
363
+ try {
364
+ await rename(tempNodeDir, nodeDir);
365
+ } catch (err: any) {
366
+ if (isNodeJSComplete(finalNodePath)) {
367
+ console.log(`[Fetcher] Node.js directory already exists and is valid.`);
368
+ } else {
369
+ throw err;
370
+ }
371
+ }
372
+ console.log(`[Environment] Node.js extraction took ${Date.now() - extStart}ms`);
373
+ } finally {
374
+ await rm(tempNodeDir, { recursive: true, force: true }).catch(() => {});
375
+ await rm(tempDir, { recursive: true, force: true }).catch(() => {});
376
+ }
377
+
378
+ console.log(`[Environment] Node.js set up complete in ${Date.now() - checkStart}ms`);
238
379
  return finalNodePath;
239
380
  });
240
381
  }
@@ -242,14 +383,15 @@ export async function ensureNodeJS(
242
383
  /**
243
384
  * Installs the PNPM package from npm if not already present.
244
385
  */
245
- export async function ensurePNPM(
246
- context: PipelabContext,
247
- version = DEFAULT_PNPM_VERSION,
248
- ) {
386
+ export async function ensurePNPM(context: PipelabContext, version = DEFAULT_PNPM_VERSION) {
387
+ const checkStart = Date.now();
249
388
  const pnpmDir = context.getPackagesPath("pnpm", version);
250
389
  const pnpmPath = join(pnpmDir, "bin", "pnpm.cjs");
251
390
 
252
391
  if (existsSync(pnpmPath)) {
392
+ console.log(
393
+ `[Environment] PNPM check took ${Date.now() - checkStart}ms (found at ${pnpmPath})`,
394
+ );
253
395
  return pnpmPath;
254
396
  }
255
397
 
@@ -260,39 +402,56 @@ export async function ensurePNPM(
260
402
  const { packageDir } = await fetchPackage("pnpm", version, {
261
403
  context,
262
404
  });
405
+ console.log(`[Environment] PNPM set up complete in ${Date.now() - checkStart}ms`);
263
406
  return join(packageDir, "bin", "pnpm.cjs");
264
407
  });
265
408
  }
266
409
 
267
410
  async function installDependencies(packageDir: string, packageName: string, options: FetchOptions) {
411
+ const start = Date.now();
268
412
  const nodeModulesPath = join(packageDir, "node_modules");
269
413
 
270
- if (existsSync(nodeModulesPath)) {
271
- try {
272
- const files = await readdir(nodeModulesPath);
273
- if (files.length === 0) {
274
- console.warn(`[Fetcher] ${packageName}: node_modules exists but is empty. Re-installing...`);
275
- }
276
- } catch (e) {
277
- // Continue to install if readdir fails
278
- }
414
+ if (isDependenciesInstalledSync(packageDir)) {
415
+ console.log(`[Fetcher] ${packageName}: Dependencies already installed, skipping.`);
416
+ return;
279
417
  }
280
418
 
281
- console.log(`[Fetcher] ${packageName}: Ensuring dependencies are installed...`);
419
+ // To prevent other processes from seeing a partially populated node_modules,
420
+ // we can create a temp directory next to packageDir, run pnpm there, and rename node_modules
421
+ const tempDir = `${packageDir}.tmp-deps-${Math.random().toString(36).slice(2)}`;
422
+ await mkdir(tempDir, { recursive: true });
282
423
  try {
283
- const { all } = await runPnpm(packageDir, {
424
+ // Copy package.json to tempDir so pnpm can install dependencies
425
+ await cp(join(packageDir, "package.json"), join(tempDir, "package.json"));
426
+
427
+ console.log(`[Fetcher] ${packageName}: Ensuring dependencies are installed...`);
428
+ const pnpmStart = Date.now();
429
+ const { all } = await runPnpm(tempDir, {
284
430
  signal: options.signal,
285
431
  context: options.context,
286
432
  });
433
+ console.log(`[Fetcher] ${packageName}: pnpm install command took ${Date.now() - pnpmStart}ms`);
287
434
 
288
435
  if (all) console.log(`[Fetcher] ${packageName}: Installation trace:\n${all}`);
289
- console.log(`[Fetcher] ${packageName}: Dependencies installed successfully.`);
436
+
437
+ // Atomically rename node_modules from tempDir to packageDir/node_modules
438
+ const tempNodeModules = join(tempDir, "node_modules");
439
+ if (existsSync(nodeModulesPath)) {
440
+ await rm(nodeModulesPath, { recursive: true, force: true }).catch(() => {});
441
+ }
442
+ const renameStart = Date.now();
443
+ await rename(tempNodeModules, nodeModulesPath);
444
+ console.log(
445
+ `[Fetcher] ${packageName}: Dependencies installed successfully (rename took ${Date.now() - renameStart}ms, total installDependencies took ${Date.now() - start}ms).`,
446
+ );
290
447
  } catch (err: any) {
291
448
  console.error(
292
449
  `[Fetcher] ${packageName}: CRITICAL ERROR during dependency installation: ${err.message}`,
293
450
  );
294
451
  if (err.all) console.error(`[Fetcher] ${packageName}: Error details:\n${err.all}`);
295
452
  throw new Error(`Failed to install dependencies for ${packageName}. See logs for details.`);
453
+ } finally {
454
+ await rm(tempDir, { recursive: true, force: true }).catch(() => {});
296
455
  }
297
456
  }
298
457
 
@@ -316,17 +475,14 @@ export async function fetchPipelabPlugin(
316
475
  options: FetchOptions,
317
476
  ): Promise<{ packageDir: string; entryPoint: string; isLocal: boolean }> {
318
477
  const { packageDir, isLocal, entryPoint } = await fetchPackage(pluginName, versionOrRange, {
319
- installDeps: true,
478
+ installDeps: false,
320
479
  ...options,
321
480
  });
322
481
 
323
482
  // Default entry point if not provided by fetchPackage
324
483
  let finalEntryPoint = entryPoint;
325
484
  if (!finalEntryPoint) {
326
- const patterns = [
327
- join(packageDir, "dist", "index.mjs"),
328
- join(packageDir, "index.mjs"),
329
- ];
485
+ const patterns = [join(packageDir, "dist", "index.mjs"), join(packageDir, "index.mjs")];
330
486
  finalEntryPoint = patterns.find((p) => existsSync(p)) || patterns[0];
331
487
  }
332
488
 
@@ -346,10 +502,7 @@ export async function fetchPipelabCli(
346
502
  // Default entry point for CLI if not provided
347
503
  let finalEntryPoint = entryPoint;
348
504
  if (!finalEntryPoint) {
349
- const patterns = [
350
- join(packageDir, "dist", "index.mjs"),
351
- join(packageDir, "index.mjs"),
352
- ];
505
+ const patterns = [join(packageDir, "dist", "index.mjs"), join(packageDir, "index.mjs")];
353
506
  finalEntryPoint = patterns.find((p) => existsSync(p)) || patterns[0];
354
507
  }
355
508
 
@@ -387,7 +540,7 @@ async function tryResolveMonorepoPackage(
387
540
  typeof pkg.bin === "string"
388
541
  ? pkg.bin
389
542
  : pkg.bin?.[packageName.replace("@pipelab/", "")] ||
390
- (pkg.bin ? pkg.bin[Object.keys(pkg.bin)[0]] : undefined);
543
+ (pkg.bin ? pkg.bin[Object.keys(pkg.bin)[0]] : undefined);
391
544
 
392
545
  // In dev, we prefer the "main" field if it points to TS, or a hardcoded src/index.ts
393
546
  const tsSource = join(packageDir, "src", "index.ts");
@@ -434,11 +587,11 @@ async function crawlMonorepoPackages(): Promise<Record<string, string>> {
434
587
  if (pkg.name) {
435
588
  cache[pkg.name] = join(fullDir, entry.name);
436
589
  }
437
- } catch (e) { }
590
+ } catch (e) {}
438
591
  }
439
592
  }
440
593
  }
441
- } catch (e) { }
594
+ } catch (e) {}
442
595
  }
443
596
  return cache;
444
597
  }
package/src/utils.ts CHANGED
@@ -256,7 +256,7 @@ export const executeGraphWithHistory = async ({
256
256
  // Don't await, let it run in the background
257
257
  buildHistoryStorage.applyRetentionPolicy();
258
258
  }
259
-
259
+
260
260
  if (shouldCleanup) {
261
261
  try {
262
262
  await rm(sandboxPath, { recursive: true, force: true });