@pipelab/core-node 1.0.1-latest.37 → 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/CHANGELOG.md +18 -0
- package/dist/index.d.mts +6 -7
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +86 -45
- package/dist/index.mjs.map +1 -1
- package/package.json +4 -4
- package/src/handler-func.ts +8 -8
- package/src/plugins-registry.ts +20 -11
- package/src/server.ts +9 -4
- package/src/utils/remote.ts +120 -41
- package/src/websocket-server.ts +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pipelab/core-node",
|
|
3
|
-
"version": "1.0.1-latest.
|
|
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.
|
|
43
|
-
"@pipelab/shared": "2.0.1-latest.
|
|
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.
|
|
56
|
+
"@pipelab/tsconfig": "1.0.1-latest.35"
|
|
57
57
|
},
|
|
58
58
|
"scripts": {
|
|
59
59
|
"build": "tsdown",
|
package/src/handler-func.ts
CHANGED
|
@@ -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
|
-
|
|
57
|
-
|
|
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
|
-
|
|
140
|
-
|
|
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(
|
|
153
|
-
const pnpm = await ensurePNPM(
|
|
152
|
+
const nodePath = await ensureNodeJS(ctx);
|
|
153
|
+
const pnpm = await ensurePNPM(ctx);
|
|
154
154
|
|
|
155
155
|
const outputs: Record<string, unknown> = {};
|
|
156
156
|
|
package/src/plugins-registry.ts
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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`);
|
package/src/server.ts
CHANGED
|
@@ -29,6 +29,13 @@ export const sendStartupProgress = (message: string) => {
|
|
|
29
29
|
});
|
|
30
30
|
};
|
|
31
31
|
|
|
32
|
+
export const sendStartupReady = () => {
|
|
33
|
+
console.log(`[Startup Progress] Ready!`);
|
|
34
|
+
webSocketServer.broadcast("startup:progress", {
|
|
35
|
+
type: "ready",
|
|
36
|
+
});
|
|
37
|
+
};
|
|
38
|
+
|
|
32
39
|
export async function serveCommand(options: ServeOptions, version: string, _dirname: string) {
|
|
33
40
|
if (!options.userData) throw new Error("userDataPath is required for serveCommand");
|
|
34
41
|
const context = new PipelabContext({
|
|
@@ -59,7 +66,7 @@ export async function serveCommand(options: ServeOptions, version: string, _dirn
|
|
|
59
66
|
response.writeHead(404, { "Content-Type": "text/plain" });
|
|
60
67
|
response.end(
|
|
61
68
|
`Error: UI directory not found at ${rawAssetFolder}.\n` +
|
|
62
|
-
|
|
69
|
+
"Please run 'pnpm build' in apps/ui to generate the distribution.",
|
|
63
70
|
);
|
|
64
71
|
return;
|
|
65
72
|
}
|
|
@@ -86,9 +93,7 @@ export async function serveCommand(options: ServeOptions, version: string, _dirn
|
|
|
86
93
|
});
|
|
87
94
|
registerMigrationHandlers(context);
|
|
88
95
|
|
|
89
|
-
|
|
90
|
-
process.send({ type: "ready" });
|
|
91
|
-
}
|
|
96
|
+
sendStartupReady();
|
|
92
97
|
|
|
93
98
|
return server;
|
|
94
99
|
}
|
package/src/utils/remote.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
|
155
|
-
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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
|
|
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
|
-
|
|
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
|
+
}
|
package/src/websocket-server.ts
CHANGED
|
@@ -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;
|