@pipelab/core-node 1.0.1-latest.38 → 1.0.1-latest.39

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.1-latest.38",
3
+ "version": "1.0.1-latest.39",
4
4
  "private": false,
5
5
  "description": "The Pipelab automation engine for Node.js",
6
6
  "license": "FSL-1.1-MIT",
@@ -39,8 +39,8 @@
39
39
  "type-fest": "4.39.0",
40
40
  "ws": "8.18.3",
41
41
  "yauzl": "2.10.0",
42
- "@pipelab/constants": "1.0.1-latest.34",
43
- "@pipelab/shared": "2.0.1-latest.35"
42
+ "@pipelab/constants": "1.0.1-latest.35",
43
+ "@pipelab/shared": "2.0.1-latest.36"
44
44
  },
45
45
  "devDependencies": {
46
46
  "@types/adm-zip": "0.5.7",
@@ -53,7 +53,7 @@
53
53
  "@types/yauzl": "2.10.3",
54
54
  "tsdown": "0.21.2",
55
55
  "typescript": "^5.0.0",
56
- "@pipelab/tsconfig": "1.0.1-latest.34"
56
+ "@pipelab/tsconfig": "1.0.1-latest.35"
57
57
  },
58
58
  "scripts": {
59
59
  "build": "tsdown",
@@ -53,9 +53,9 @@ export const handleConditionExecute = async (
53
53
  .find((plugin) => plugin.id === pluginId)
54
54
  ?.nodes.find((node: any) => node.node.id === nodeId) as
55
55
  | {
56
- node: Condition;
57
- runner: ConditionRunner<any>;
58
- }
56
+ node: Condition;
57
+ runner: ConditionRunner<any>;
58
+ }
59
59
  | undefined;
60
60
 
61
61
  if (!node) {
@@ -136,9 +136,9 @@ export const handleActionExecute = async (
136
136
  .find((plugin) => plugin.id === pluginId)
137
137
  ?.nodes.find((node: any) => node.node.id === nodeId) as
138
138
  | {
139
- node: Action;
140
- runner: ActionRunner<any>;
141
- }
139
+ node: Action;
140
+ runner: ActionRunner<any>;
141
+ }
142
142
  | undefined;
143
143
 
144
144
  if (!node) {
@@ -149,8 +149,8 @@ export const handleActionExecute = async (
149
149
  };
150
150
  }
151
151
 
152
- const nodePath = await ensureNodeJS("24.14.1", { context: ctx });
153
- const pnpm = await ensurePNPM("10.12.0", { context: ctx });
152
+ const nodePath = await ensureNodeJS(ctx);
153
+ const pnpm = await ensurePNPM(ctx);
154
154
 
155
155
  const outputs: Record<string, unknown> = {};
156
156
 
@@ -1,9 +1,8 @@
1
- import { fetchPipelabPlugin } from "./utils/remote";
1
+ import { ensureNodeJS, ensurePNPM, fetchPipelabPlugin } from "./utils/remote";
2
2
  import { pathToFileURL } from "node:url";
3
- import { join } from "node:path";
4
3
  import { readdir } from "node:fs/promises";
5
4
  import { existsSync } from "node:fs";
6
- import { isDev, projectRoot, PipelabContext } from "./context";
5
+ import { PipelabContext } from "./context";
7
6
  import { sendStartupProgress } from "./server";
8
7
 
9
8
  const DEFAULT_PLUGIN_IDS = [
@@ -54,14 +53,24 @@ export const loadPipelabPlugin = async (id: string, options: { context: PipelabC
54
53
 
55
54
  export const builtInPlugins = async (options: { context: PipelabContext }) => {
56
55
  console.log("[Plugins] Finalizing default plugins list...");
57
- const plugins = [];
58
- for (const id of DEFAULT_PLUGIN_IDS) {
59
- sendStartupProgress(`Loading plugin: ${id}`);
60
- const plugin = await loadPipelabPlugin(id, options);
61
- if (plugin) {
62
- plugins.push(plugin);
63
- }
64
- }
56
+
57
+ // Pre-ensure Node.js and PNPM once in parallel so plugins don't have to wait for them
58
+ sendStartupProgress("Preparing environment...");
59
+ await Promise.all([
60
+ ensureNodeJS(options.context),
61
+ ensurePNPM(options.context),
62
+ ]);
63
+
64
+ const results = await Promise.allSettled(
65
+ DEFAULT_PLUGIN_IDS.map(async (id) => {
66
+ sendStartupProgress(`Loading plugin: ${id}`);
67
+ return loadPipelabPlugin(id, options);
68
+ }),
69
+ );
70
+
71
+ const plugins = results
72
+ .filter((r) => r.status === "fulfilled")
73
+ .map((r) => (r as PromiseFulfilledResult<any>).value);
65
74
 
66
75
  const filtered = plugins.filter(Boolean).flat();
67
76
  console.log(`[Plugins] Successfully loaded ${filtered.length} default plugins`);
@@ -9,10 +9,14 @@ import { execa } from "execa";
9
9
  import { sendStartupProgress } from "../server";
10
10
  import { downloadFile, extractZip, extractTarGz, generateTempFolder } from "./fs-extras";
11
11
 
12
+ export const DEFAULT_NODE_VERSION = "24.14.1";
13
+ export const DEFAULT_PNPM_VERSION = "10.12.0";
14
+
12
15
  /**
13
16
  * In-memory lock to prevent concurrent operations on the same resource (e.g., downloading Node.js).
14
17
  */
15
18
  const activeOperations = new Map<string, Promise<any>>();
19
+ const packumentRequests = new Map<string, Promise<any>>();
16
20
 
17
21
  async function withLock<T>(key: string, fn: () => Promise<T>): Promise<T> {
18
22
  const existing = activeOperations.get(key);
@@ -74,8 +78,15 @@ export async function fetchPackage(
74
78
  console.log(`[Fetcher] Resolving ${packageName}@${versionOrRange || "latest"}...`);
75
79
 
76
80
  try {
77
- // 1. Resolve version/range using npm
78
- const packument = await pacote.packument(packageName);
81
+ // 1. Resolve version/range using npm with session-wide memoization and disk cache
82
+ const cachePath = join(ctx.userDataPath, "cache", "pacote");
83
+ let packumentPromise = packumentRequests.get(packageName);
84
+ if (!packumentPromise) {
85
+ packumentPromise = pacote.packument(packageName, { cache: cachePath });
86
+ packumentRequests.set(packageName, packumentPromise);
87
+ }
88
+
89
+ const packument = await packumentPromise;
79
90
  const versions = Object.keys(packument.versions);
80
91
  const range = versionOrRange || "latest";
81
92
 
@@ -97,32 +108,61 @@ export async function fetchPackage(
97
108
  }
98
109
  }
99
110
 
111
+ const cachePath = join(ctx.userDataPath, "cache", "pacote");
112
+
113
+ // 1. Resolve version/range using npm with session-wide memoization and disk cache
114
+ try {
115
+ let packumentPromise = packumentRequests.get(packageName);
116
+ if (!packumentPromise) {
117
+ packumentPromise = pacote.packument(packageName, { cache: cachePath });
118
+ packumentRequests.set(packageName, packumentPromise);
119
+ }
120
+
121
+ const packument = await packumentPromise;
122
+ const versions = Object.keys(packument.versions);
123
+ const range = versionOrRange || "latest";
124
+
125
+ // Prioritize tags (like 'latest', 'beta', etc.) over semver ranges
126
+ const foundVersion = packument["dist-tags"]?.[range] || semver.maxSatisfying(versions, range);
127
+
128
+ if (!foundVersion) {
129
+ throw new Error(
130
+ `Package ${packageName}@${range} not found on npm (available tags: ${Object.keys(
131
+ packument["dist-tags"] || {},
132
+ ).join(", ")})`,
133
+ );
134
+ }
135
+ resolvedVersion = foundVersion;
136
+ console.log(`[Fetcher] ${packageName}: Resolved to v${resolvedVersion} via npm`);
137
+ } catch (error) {
138
+ console.warn(`[Fetcher] ${packageName}: remote resolution failed, trying local fallback...`);
139
+ const fallbackVersion = await tryLocalFallback(versionOrRange, error, baseDir, packageName);
140
+ if (fallbackVersion) {
141
+ resolvedVersion = fallbackVersion;
142
+ } else {
143
+ throw error;
144
+ }
145
+ }
146
+
147
+ const packageDir = join(baseDir, resolvedVersion);
148
+
149
+ // If the package already exists and we don't need to install dependencies, return immediately
150
+ if (existsSync(packageDir) && !options?.installDeps) {
151
+ return { packageDir, resolvedVersion };
152
+ }
153
+
100
154
  const lockKey = `package:${packageName}:${resolvedVersion}`;
101
155
  return withLock(lockKey, async () => {
102
- const packageDir = join(baseDir, resolvedVersion);
103
156
  if (!existsSync(packageDir)) {
104
157
  console.log(`[Fetcher] ${packageName}@${resolvedVersion}: Downloading to ${packageDir}...`);
105
158
  await mkdir(packageDir, { recursive: true });
106
- await pacote.extract(`${packageName}@${resolvedVersion}`, packageDir);
159
+ await pacote.extract(`${packageName}@${resolvedVersion}`, packageDir, {
160
+ cache: cachePath,
161
+ });
107
162
  }
108
163
 
109
164
  // 2. Resolve entry point from package.json for downloaded package
110
- let entryPoint: string | undefined;
111
- try {
112
- const pkgPath = join(packageDir, "package.json");
113
- if (existsSync(pkgPath)) {
114
- const pkg = JSON.parse(await readFile(pkgPath, "utf-8"));
115
- const main = pkg.module || pkg.main || pkg.publishConfig?.module || pkg.publishConfig?.main;
116
- if (main) {
117
- entryPoint = join(packageDir, main);
118
- } else if (pkg.bin) {
119
- const binFile = typeof pkg.bin === "string" ? pkg.bin : Object.values(pkg.bin)[0];
120
- if (binFile) entryPoint = join(packageDir, binFile as string);
121
- }
122
- }
123
- } catch (e) {
124
- console.warn(`[Fetcher] ${packageName}: Failed to parse package.json for entry point resolution:`, e);
125
- }
165
+ const entryPoint = await resolveEntryPoint(packageDir, packageName);
126
166
 
127
167
  if (options?.installDeps) {
128
168
  await installDependencies(packageDir, packageName, options);
@@ -145,14 +185,16 @@ export async function runPnpm(
145
185
  },
146
186
  ) {
147
187
  const {
148
- args = ["install", "--prod", "--no-lockfile"],
188
+ args = ["install", "--prod", "--no-lockfile", "--prefer-offline"],
149
189
  extraEnv = {},
150
190
  signal,
151
191
  context: ctx,
152
192
  } = options;
153
193
 
154
- const nodePath = await ensureNodeJS("24.14.1", { context: ctx }).catch(() => process.execPath);
155
- const pnpmPath = await ensurePNPM("10.12.0", { context: ctx }).catch(() => "pnpm");
194
+ const [nodePath, pnpmPath] = await Promise.all([
195
+ ensureNodeJS(ctx).catch(() => process.execPath),
196
+ ensurePNPM(ctx).catch(() => "pnpm"),
197
+ ]);
156
198
 
157
199
  const isScript = pnpmPath.endsWith(".cjs") || pnpmPath.endsWith(".js");
158
200
  const command = isScript ? nodePath : pnpmPath;
@@ -175,19 +217,21 @@ export async function runPnpm(
175
217
  /**
176
218
  * Installs a specific version of Node.js if not already present.
177
219
  */
178
- export async function ensureNodeJS(version: string, options: { context: PipelabContext }) {
220
+ export async function ensureNodeJS(
221
+ context: PipelabContext,
222
+ version = DEFAULT_NODE_VERSION,
223
+ ) {
224
+ const isWindows = process.platform === "win32";
225
+ const nodeDir = context.getThirdPartyPath("node", version);
226
+ const finalNodePath = join(nodeDir, isWindows ? "node.exe" : "bin/node");
227
+
228
+ if (existsSync(finalNodePath)) {
229
+ return finalNodePath;
230
+ }
231
+
179
232
  const lockKey = `node:${version}`;
180
233
  return withLock(lockKey, async () => {
181
- const ctx = options.context;
182
- const nodeDir = ctx.getThirdPartyPath("node", version);
183
- const isWindows = process.platform === "win32";
184
- const executableName = isWindows ? "node.exe" : "bin/node";
185
- const finalNodePath = join(nodeDir, executableName);
186
-
187
- try {
188
- await access(finalNodePath, constants.X_OK);
189
- return finalNodePath;
190
- } catch (e) {}
234
+ if (existsSync(finalNodePath)) return finalNodePath;
191
235
 
192
236
  const arch = process.arch === "x64" ? "x64" : process.arch === "arm64" ? "arm64" : "x86";
193
237
  const platform = isWindows ? "win" : process.platform === "darwin" ? "osx" : "linux";
@@ -224,7 +268,7 @@ export async function ensureNodeJS(version: string, options: { context: PipelabC
224
268
  await cp(sourceDir, nodeDir, { recursive: true });
225
269
  await rm(tempDir, { recursive: true, force: true });
226
270
 
227
- if (!isWindows) await chmod(finalNodePath, 0o755).catch(() => {});
271
+ if (!isWindows) await chmod(finalNodePath, 0o755).catch(() => { });
228
272
  return finalNodePath;
229
273
  });
230
274
  }
@@ -232,13 +276,23 @@ export async function ensureNodeJS(version: string, options: { context: PipelabC
232
276
  /**
233
277
  * Installs the PNPM package from npm if not already present.
234
278
  */
235
- export async function ensurePNPM(version = "10.12.0", options: { context: PipelabContext }) {
279
+ export async function ensurePNPM(
280
+ context: PipelabContext,
281
+ version = DEFAULT_PNPM_VERSION,
282
+ ) {
283
+ const pnpmDir = context.getPackagesPath("pnpm", version);
284
+ const pnpmPath = join(pnpmDir, "bin", "pnpm.cjs");
285
+
286
+ if (existsSync(pnpmPath)) {
287
+ return pnpmPath;
288
+ }
289
+
236
290
  const lockKey = `pnpm:${version}`;
237
291
  return withLock(lockKey, async () => {
292
+ if (existsSync(pnpmPath)) return pnpmPath;
238
293
  sendStartupProgress(`Checking PNPM v${version}...`);
239
- const ctx = options.context;
240
294
  const { packageDir } = await fetchPackage("pnpm", version, {
241
- context: ctx,
295
+ context,
242
296
  });
243
297
  return join(packageDir, "bin", "pnpm.cjs");
244
298
  });
@@ -367,7 +421,7 @@ async function tryResolveMonorepoPackage(
367
421
  typeof pkg.bin === "string"
368
422
  ? pkg.bin
369
423
  : pkg.bin?.[packageName.replace("@pipelab/", "")] ||
370
- (pkg.bin ? pkg.bin[Object.keys(pkg.bin)[0]] : undefined);
424
+ (pkg.bin ? pkg.bin[Object.keys(pkg.bin)[0]] : undefined);
371
425
 
372
426
  // In dev, we prefer the "main" field if it points to TS, or a hardcoded src/index.ts
373
427
  const tsSource = join(packageDir, "src", "index.ts");
@@ -414,11 +468,11 @@ async function crawlMonorepoPackages(): Promise<Record<string, string>> {
414
468
  if (pkg.name) {
415
469
  cache[pkg.name] = join(fullDir, entry.name);
416
470
  }
417
- } catch (e) {}
471
+ } catch (e) { }
418
472
  }
419
473
  }
420
474
  }
421
- } catch (e) {}
475
+ } catch (e) { }
422
476
  }
423
477
  return cache;
424
478
  }
@@ -450,3 +504,28 @@ async function tryLocalFallback(
450
504
  }
451
505
  return null;
452
506
  }
507
+
508
+ /**
509
+ * Resolves the entry point of a package by looking at its package.json.
510
+ */
511
+ async function resolveEntryPoint(packageDir: string, packageName: string) {
512
+ try {
513
+ const pkgPath = join(packageDir, "package.json");
514
+ if (!existsSync(pkgPath)) return undefined;
515
+
516
+ const pkg = JSON.parse(await readFile(pkgPath, "utf-8"));
517
+ const main = pkg.module || pkg.main || pkg.publishConfig?.module || pkg.publishConfig?.main;
518
+
519
+ if (main) {
520
+ return join(packageDir, main);
521
+ }
522
+
523
+ if (pkg.bin) {
524
+ const binFile = typeof pkg.bin === "string" ? pkg.bin : Object.values(pkg.bin)[0];
525
+ if (binFile) return join(packageDir, binFile as string);
526
+ }
527
+ } catch (e) {
528
+ console.warn(`[Fetcher] ${packageName}: Failed to resolve entry point:`, e);
529
+ }
530
+ return undefined;
531
+ }
@@ -108,7 +108,7 @@ export class WebSocketServer {
108
108
  });
109
109
 
110
110
  if (!server.listening) {
111
- server.listen(port, () => {
111
+ server.listen(port, "127.0.0.1", () => {
112
112
  this.connectionState = "connected";
113
113
  logger().info(`WebSocket server listening on port ${port}`);
114
114
  this.isReady = true;