@pipelab/core-node 1.0.1-latest.35 → 1.0.1-latest.37

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.35",
3
+ "version": "1.0.1-latest.37",
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.31",
43
- "@pipelab/shared": "2.0.1-latest.32"
42
+ "@pipelab/constants": "1.0.1-latest.33",
43
+ "@pipelab/shared": "2.0.1-latest.34"
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.31"
56
+ "@pipelab/tsconfig": "1.0.1-latest.33"
57
57
  },
58
58
  "scripts": {
59
59
  "build": "tsdown",
package/src/ipc-core.ts CHANGED
@@ -45,11 +45,18 @@ export const useAPI = () => {
45
45
  requestId,
46
46
  events,
47
47
  };
48
- ws.send(JSON.stringify(response), (error) => {
49
- if (error) {
50
- logger().error("Failed to send WebSocket response:", error);
51
- }
52
- });
48
+
49
+ if (ws.readyState === WSWebSocket.OPEN) {
50
+ ws.send(JSON.stringify(response), (error) => {
51
+ if (error) {
52
+ logger().error("Failed to send WebSocket response:", error);
53
+ }
54
+ });
55
+ } else {
56
+ logger().debug(
57
+ `Cannot send response to ${requestId}: WebSocket is not open (state: ${ws.readyState})`,
58
+ );
59
+ }
53
60
  return Promise.resolve();
54
61
  };
55
62
 
@@ -4,6 +4,7 @@ import { join } from "node:path";
4
4
  import { readdir } from "node:fs/promises";
5
5
  import { existsSync } from "node:fs";
6
6
  import { isDev, projectRoot, PipelabContext } from "./context";
7
+ import { sendStartupProgress } from "./server";
7
8
 
8
9
  const DEFAULT_PLUGIN_IDS = [
9
10
  "construct",
@@ -53,11 +54,15 @@ export const loadPipelabPlugin = async (id: string, options: { context: PipelabC
53
54
 
54
55
  export const builtInPlugins = async (options: { context: PipelabContext }) => {
55
56
  console.log("[Plugins] Finalizing default plugins list...");
56
- const promises = DEFAULT_PLUGIN_IDS.map(async (id) => {
57
- return loadPipelabPlugin(id, options);
58
- });
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
+ }
59
65
 
60
- const plugins = await Promise.all(promises);
61
66
  const filtered = plugins.filter(Boolean).flat();
62
67
  console.log(`[Plugins] Successfully loaded ${filtered.length} default plugins`);
63
68
  return filtered;
package/src/server.ts CHANGED
@@ -21,6 +21,14 @@ export interface ServeOptions {
21
21
  pnpmPath?: string;
22
22
  }
23
23
 
24
+ export const sendStartupProgress = (message: string) => {
25
+ console.log(`[Startup Progress] ${message}`);
26
+ webSocketServer.broadcast("startup:progress", {
27
+ type: "progress",
28
+ data: { message },
29
+ });
30
+ };
31
+
24
32
  export async function serveCommand(options: ServeOptions, version: string, _dirname: string) {
25
33
  if (!options.userData) throw new Error("userDataPath is required for serveCommand");
26
34
  const context = new PipelabContext({
@@ -69,12 +77,18 @@ export async function serveCommand(options: ServeOptions, version: string, _dirn
69
77
  console.log(`UI available at http://localhost:${options.port}`);
70
78
  }
71
79
 
80
+ // Start the server EARLY so the UI can connect and receive progress updates
81
+ await webSocketServer.start(Number(options.port), server);
82
+
72
83
  await registerAllHandlers({
73
84
  version,
74
85
  context,
75
86
  });
76
87
  registerMigrationHandlers(context);
77
88
 
78
- await webSocketServer.start(Number(options.port), server);
89
+ if (process.send) {
90
+ process.send({ type: "ready" });
91
+ }
92
+
79
93
  return server;
80
94
  }
@@ -6,8 +6,33 @@ import pacote from "pacote";
6
6
  import semver from "semver";
7
7
  import { isDev, projectRoot, PipelabContext } from "../context";
8
8
  import { execa } from "execa";
9
+ import { sendStartupProgress } from "../server";
9
10
  import { downloadFile, extractZip, extractTarGz, generateTempFolder } from "./fs-extras";
10
11
 
12
+ /**
13
+ * In-memory lock to prevent concurrent operations on the same resource (e.g., downloading Node.js).
14
+ */
15
+ const activeOperations = new Map<string, Promise<any>>();
16
+
17
+ async function withLock<T>(key: string, fn: () => Promise<T>): Promise<T> {
18
+ const existing = activeOperations.get(key);
19
+ if (existing) {
20
+ console.log(`[Lock] Waiting for concurrent operation on: ${key}`);
21
+ return existing;
22
+ }
23
+
24
+ const promise = (async () => {
25
+ try {
26
+ return await fn();
27
+ } finally {
28
+ activeOperations.delete(key);
29
+ }
30
+ })();
31
+
32
+ activeOperations.set(key, promise);
33
+ return promise;
34
+ }
35
+
11
36
  export type FetchOptions = {
12
37
  installDeps?: boolean;
13
38
  signal?: AbortSignal;
@@ -72,36 +97,39 @@ export async function fetchPackage(
72
97
  }
73
98
  }
74
99
 
75
- const packageDir = join(baseDir, resolvedVersion);
76
- if (!existsSync(packageDir)) {
77
- console.log(`[Fetcher] ${packageName}@${resolvedVersion}: Downloading to ${packageDir}...`);
78
- await mkdir(packageDir, { recursive: true });
79
- await pacote.extract(`${packageName}@${resolvedVersion}`, packageDir);
80
- }
100
+ const lockKey = `package:${packageName}:${resolvedVersion}`;
101
+ return withLock(lockKey, async () => {
102
+ const packageDir = join(baseDir, resolvedVersion);
103
+ if (!existsSync(packageDir)) {
104
+ console.log(`[Fetcher] ${packageName}@${resolvedVersion}: Downloading to ${packageDir}...`);
105
+ await mkdir(packageDir, { recursive: true });
106
+ await pacote.extract(`${packageName}@${resolvedVersion}`, packageDir);
107
+ }
81
108
 
82
- // 2. Resolve entry point from package.json for downloaded package
83
- let entryPoint: string | undefined;
84
- try {
85
- const pkgPath = join(packageDir, "package.json");
86
- if (existsSync(pkgPath)) {
87
- const pkg = JSON.parse(await readFile(pkgPath, "utf-8"));
88
- const main = pkg.module || pkg.main || pkg.publishConfig?.module || pkg.publishConfig?.main;
89
- if (main) {
90
- entryPoint = join(packageDir, main);
91
- } else if (pkg.bin) {
92
- const binFile = typeof pkg.bin === "string" ? pkg.bin : Object.values(pkg.bin)[0];
93
- if (binFile) entryPoint = join(packageDir, binFile as string);
109
+ // 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
+ }
94
122
  }
123
+ } catch (e) {
124
+ console.warn(`[Fetcher] ${packageName}: Failed to parse package.json for entry point resolution:`, e);
95
125
  }
96
- } catch (e) {
97
- console.warn(`[Fetcher] ${packageName}: Failed to parse package.json for entry point resolution:`, e);
98
- }
99
126
 
100
- if (options?.installDeps) {
101
- await installDependencies(packageDir, packageName, options);
102
- }
127
+ if (options?.installDeps) {
128
+ await installDependencies(packageDir, packageName, options);
129
+ }
103
130
 
104
- return { packageDir, resolvedVersion, entryPoint };
131
+ return { packageDir, resolvedVersion, entryPoint };
132
+ });
105
133
  }
106
134
 
107
135
  /**
@@ -148,63 +176,72 @@ export async function runPnpm(
148
176
  * Installs a specific version of Node.js if not already present.
149
177
  */
150
178
  export async function ensureNodeJS(version: string, options: { context: PipelabContext }) {
151
- const ctx = options.context;
152
- const nodeDir = ctx.getThirdPartyPath("node", version);
153
- const isWindows = process.platform === "win32";
154
- const executableName = isWindows ? "node.exe" : "bin/node";
155
- const finalNodePath = join(nodeDir, executableName);
179
+ const lockKey = `node:${version}`;
180
+ 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);
156
186
 
157
- try {
158
- await access(finalNodePath, constants.X_OK);
159
- return finalNodePath;
160
- } catch (e) {}
187
+ try {
188
+ await access(finalNodePath, constants.X_OK);
189
+ return finalNodePath;
190
+ } catch (e) {}
161
191
 
162
- const arch = process.arch === "x64" ? "x64" : process.arch === "arm64" ? "arm64" : "x86";
163
- const platform = isWindows ? "win" : process.platform === "darwin" ? "osx" : "linux";
164
- const extension = isWindows ? "zip" : "tar.gz";
165
- const downloadPlatform = platform === "osx" ? "darwin" : platform;
192
+ const arch = process.arch === "x64" ? "x64" : process.arch === "arm64" ? "arm64" : "x86";
193
+ const platform = isWindows ? "win" : process.platform === "darwin" ? "osx" : "linux";
194
+ const extension = isWindows ? "zip" : "tar.gz";
195
+ const downloadPlatform = platform === "osx" ? "darwin" : platform;
166
196
 
167
- const fileName = `node-v${version}-${downloadPlatform}-${arch}.${extension}`;
168
- const downloadUrl = `https://nodejs.org/dist/v${version}/${fileName}`;
169
- const tempDir = await generateTempFolder(tmpdir());
170
- const archivePath = join(tempDir, fileName);
197
+ const fileName = `node-v${version}-${downloadPlatform}-${arch}.${extension}`;
198
+ const downloadUrl = `https://nodejs.org/dist/v${version}/${fileName}`;
199
+ const tempDir = await generateTempFolder(tmpdir());
200
+ const archivePath = join(tempDir, fileName);
171
201
 
172
- console.log(`Downloading Node.js from ${downloadUrl}...`);
173
- await downloadFile(downloadUrl, archivePath);
202
+ sendStartupProgress(`Downloading Node.js v${version}...`);
203
+ console.log(`Downloading Node.js from ${downloadUrl}...`);
204
+ await downloadFile(downloadUrl, archivePath);
174
205
 
175
- console.log(`Extracting Node.js to ${tempDir}...`);
176
- const extractTempDir = join(tempDir, "extracted");
177
- await mkdir(extractTempDir, { recursive: true });
206
+ sendStartupProgress(`Extracting Node.js v${version}...`);
207
+ console.log(`Extracting Node.js to ${tempDir}...`);
208
+ const extractTempDir = join(tempDir, "extracted");
209
+ await mkdir(extractTempDir, { recursive: true });
178
210
 
179
- if (extension === "zip") {
180
- await extractZip(archivePath, extractTempDir);
181
- } else {
182
- await extractTarGz(archivePath, extractTempDir);
183
- }
211
+ if (extension === "zip") {
212
+ await extractZip(archivePath, extractTempDir);
213
+ } else {
214
+ await extractTarGz(archivePath, extractTempDir);
215
+ }
184
216
 
185
- const extractedEntries = await readdir(extractTempDir);
186
- const nodeSubDir = extractedEntries.find((entry) => entry.startsWith(`node-v${version}`));
187
- if (!nodeSubDir) throw new Error(`Could not find extracted Node.js directory`);
217
+ const extractedEntries = await readdir(extractTempDir);
218
+ const nodeSubDir = extractedEntries.find((entry) => entry.startsWith(`node-v${version}`));
219
+ if (!nodeSubDir) throw new Error(`Could not find extracted Node.js directory`);
188
220
 
189
- const sourceDir = join(extractTempDir, nodeSubDir);
190
- await mkdir(dirname(nodeDir), { recursive: true });
191
- await rm(nodeDir, { recursive: true, force: true });
192
- await cp(sourceDir, nodeDir, { recursive: true });
193
- await rm(tempDir, { recursive: true, force: true });
221
+ const sourceDir = join(extractTempDir, nodeSubDir);
222
+ await mkdir(dirname(nodeDir), { recursive: true });
223
+ await rm(nodeDir, { recursive: true, force: true });
224
+ await cp(sourceDir, nodeDir, { recursive: true });
225
+ await rm(tempDir, { recursive: true, force: true });
194
226
 
195
- if (!isWindows) await chmod(finalNodePath, 0o755).catch(() => {});
196
- return finalNodePath;
227
+ if (!isWindows) await chmod(finalNodePath, 0o755).catch(() => {});
228
+ return finalNodePath;
229
+ });
197
230
  }
198
231
 
199
232
  /**
200
233
  * Installs the PNPM package from npm if not already present.
201
234
  */
202
235
  export async function ensurePNPM(version = "10.12.0", options: { context: PipelabContext }) {
203
- const ctx = options.context;
204
- const { packageDir } = await fetchPackage("pnpm", version, {
205
- context: ctx,
236
+ const lockKey = `pnpm:${version}`;
237
+ return withLock(lockKey, async () => {
238
+ sendStartupProgress(`Checking PNPM v${version}...`);
239
+ const ctx = options.context;
240
+ const { packageDir } = await fetchPackage("pnpm", version, {
241
+ context: ctx,
242
+ });
243
+ return join(packageDir, "bin", "pnpm.cjs");
206
244
  });
207
- return join(packageDir, "bin", "pnpm.cjs");
208
245
  }
209
246
 
210
247
  async function installDependencies(packageDir: string, packageName: string, options: FetchOptions) {