@pipelab/core-node 1.0.0-beta.1 → 1.0.0-beta.10
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/.oxfmtrc.json +3 -0
- package/CHANGELOG.md +74 -0
- package/dist/index.d.mts +4 -2
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +145 -55
- package/dist/index.mjs.map +1 -1
- package/package.json +5 -5
- package/scratch/simulate-updates.ts +118 -0
- package/src/config.ts +10 -15
- package/src/context.ts +8 -2
- package/src/handler-func.ts +6 -6
- package/src/handlers/build-history.ts +6 -8
- package/src/handlers/history.ts +6 -8
- package/src/handlers/index.ts +2 -2
- package/src/plugins-registry.ts +25 -20
- package/src/runner.ts +28 -22
- package/src/server.ts +4 -2
- package/src/utils/fs-extras.ts +10 -1
- package/src/utils/github.ts +10 -6
- package/src/utils/remote.ts +133 -49
- package/src/utils.ts +1 -1
package/src/utils/remote.ts
CHANGED
|
@@ -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
|
*/
|
|
@@ -94,7 +117,9 @@ export async function fetchPackage(
|
|
|
94
117
|
const foundVersion = packument["dist-tags"]?.[range] || semver.maxSatisfying(versions, range);
|
|
95
118
|
|
|
96
119
|
if (!foundVersion) {
|
|
97
|
-
throw new Error(
|
|
120
|
+
throw new Error(
|
|
121
|
+
`Package ${packageName}@${range} not found on npm (available tags: ${Object.keys(packument["dist-tags"] || {}).join(", ")})`,
|
|
122
|
+
);
|
|
98
123
|
}
|
|
99
124
|
resolvedVersion = foundVersion;
|
|
100
125
|
console.log(`[Fetcher] ${packageName}: Resolved to v${resolvedVersion} via npm`);
|
|
@@ -111,19 +136,49 @@ export async function fetchPackage(
|
|
|
111
136
|
const cachePath = join(ctx.userDataPath, "cache", "pacote");
|
|
112
137
|
const packageDir = join(baseDir, resolvedVersion);
|
|
113
138
|
|
|
114
|
-
// If the package already exists and we don't need to install dependencies, return immediately
|
|
115
|
-
|
|
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) {
|
|
116
145
|
return { packageDir, resolvedVersion };
|
|
117
146
|
}
|
|
118
147
|
|
|
119
148
|
const lockKey = `package:${packageName}:${resolvedVersion}`;
|
|
120
149
|
return withLock(lockKey, async () => {
|
|
121
|
-
if (!
|
|
150
|
+
if (!isPackageComplete(packageDir)) {
|
|
122
151
|
console.log(`[Fetcher] ${packageName}@${resolvedVersion}: Downloading to ${packageDir}...`);
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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
|
+
}
|
|
127
182
|
}
|
|
128
183
|
|
|
129
184
|
// 2. Resolve entry point from package.json for downloaded package
|
|
@@ -150,7 +205,13 @@ export async function runPnpm(
|
|
|
150
205
|
},
|
|
151
206
|
) {
|
|
152
207
|
const {
|
|
153
|
-
args = [
|
|
208
|
+
args = [
|
|
209
|
+
"install",
|
|
210
|
+
"--prod",
|
|
211
|
+
"--no-lockfile",
|
|
212
|
+
"--prefer-offline",
|
|
213
|
+
"--no-verify-store-integrity",
|
|
214
|
+
],
|
|
154
215
|
extraEnv = {},
|
|
155
216
|
signal,
|
|
156
217
|
context: ctx,
|
|
@@ -183,21 +244,18 @@ export async function runPnpm(
|
|
|
183
244
|
/**
|
|
184
245
|
* Installs a specific version of Node.js if not already present.
|
|
185
246
|
*/
|
|
186
|
-
export async function ensureNodeJS(
|
|
187
|
-
context: PipelabContext,
|
|
188
|
-
version = DEFAULT_NODE_VERSION,
|
|
189
|
-
) {
|
|
247
|
+
export async function ensureNodeJS(context: PipelabContext, version = DEFAULT_NODE_VERSION) {
|
|
190
248
|
const isWindows = process.platform === "win32";
|
|
191
249
|
const nodeDir = context.getThirdPartyPath("node", version);
|
|
192
250
|
const finalNodePath = join(nodeDir, isWindows ? "node.exe" : "bin/node");
|
|
193
251
|
|
|
194
|
-
if (
|
|
252
|
+
if (isNodeJSComplete(finalNodePath)) {
|
|
195
253
|
return finalNodePath;
|
|
196
254
|
}
|
|
197
255
|
|
|
198
256
|
const lockKey = `node:${version}`;
|
|
199
257
|
return withLock(lockKey, async () => {
|
|
200
|
-
if (
|
|
258
|
+
if (isNodeJSComplete(finalNodePath)) return finalNodePath;
|
|
201
259
|
|
|
202
260
|
const arch = process.arch === "x64" ? "x64" : process.arch === "arm64" ? "arm64" : "x86";
|
|
203
261
|
const platform = isWindows ? "win" : process.platform === "darwin" ? "osx" : "linux";
|
|
@@ -229,12 +287,37 @@ export async function ensureNodeJS(
|
|
|
229
287
|
if (!nodeSubDir) throw new Error(`Could not find extracted Node.js directory`);
|
|
230
288
|
|
|
231
289
|
const sourceDir = join(extractTempDir, nodeSubDir);
|
|
232
|
-
|
|
233
|
-
await
|
|
234
|
-
|
|
235
|
-
|
|
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
|
+
}
|
|
236
320
|
|
|
237
|
-
if (!isWindows) await chmod(finalNodePath, 0o755).catch(() => { });
|
|
238
321
|
return finalNodePath;
|
|
239
322
|
});
|
|
240
323
|
}
|
|
@@ -242,10 +325,7 @@ export async function ensureNodeJS(
|
|
|
242
325
|
/**
|
|
243
326
|
* Installs the PNPM package from npm if not already present.
|
|
244
327
|
*/
|
|
245
|
-
export async function ensurePNPM(
|
|
246
|
-
context: PipelabContext,
|
|
247
|
-
version = DEFAULT_PNPM_VERSION,
|
|
248
|
-
) {
|
|
328
|
+
export async function ensurePNPM(context: PipelabContext, version = DEFAULT_PNPM_VERSION) {
|
|
249
329
|
const pnpmDir = context.getPackagesPath("pnpm", version);
|
|
250
330
|
const pnpmPath = join(pnpmDir, "bin", "pnpm.cjs");
|
|
251
331
|
|
|
@@ -267,25 +347,33 @@ export async function ensurePNPM(
|
|
|
267
347
|
async function installDependencies(packageDir: string, packageName: string, options: FetchOptions) {
|
|
268
348
|
const nodeModulesPath = join(packageDir, "node_modules");
|
|
269
349
|
|
|
270
|
-
if (
|
|
271
|
-
|
|
272
|
-
|
|
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
|
-
}
|
|
350
|
+
if (isDependenciesInstalledSync(packageDir)) {
|
|
351
|
+
console.log(`[Fetcher] ${packageName}: Dependencies already installed, skipping.`);
|
|
352
|
+
return;
|
|
279
353
|
}
|
|
280
354
|
|
|
281
|
-
|
|
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 });
|
|
282
359
|
try {
|
|
283
|
-
|
|
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, {
|
|
284
365
|
signal: options.signal,
|
|
285
366
|
context: options.context,
|
|
286
367
|
});
|
|
287
368
|
|
|
288
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);
|
|
289
377
|
console.log(`[Fetcher] ${packageName}: Dependencies installed successfully.`);
|
|
290
378
|
} catch (err: any) {
|
|
291
379
|
console.error(
|
|
@@ -293,6 +381,8 @@ async function installDependencies(packageDir: string, packageName: string, opti
|
|
|
293
381
|
);
|
|
294
382
|
if (err.all) console.error(`[Fetcher] ${packageName}: Error details:\n${err.all}`);
|
|
295
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(() => {});
|
|
296
386
|
}
|
|
297
387
|
}
|
|
298
388
|
|
|
@@ -316,17 +406,14 @@ export async function fetchPipelabPlugin(
|
|
|
316
406
|
options: FetchOptions,
|
|
317
407
|
): Promise<{ packageDir: string; entryPoint: string; isLocal: boolean }> {
|
|
318
408
|
const { packageDir, isLocal, entryPoint } = await fetchPackage(pluginName, versionOrRange, {
|
|
319
|
-
installDeps:
|
|
409
|
+
installDeps: false,
|
|
320
410
|
...options,
|
|
321
411
|
});
|
|
322
412
|
|
|
323
413
|
// Default entry point if not provided by fetchPackage
|
|
324
414
|
let finalEntryPoint = entryPoint;
|
|
325
415
|
if (!finalEntryPoint) {
|
|
326
|
-
const patterns = [
|
|
327
|
-
join(packageDir, "dist", "index.mjs"),
|
|
328
|
-
join(packageDir, "index.mjs"),
|
|
329
|
-
];
|
|
416
|
+
const patterns = [join(packageDir, "dist", "index.mjs"), join(packageDir, "index.mjs")];
|
|
330
417
|
finalEntryPoint = patterns.find((p) => existsSync(p)) || patterns[0];
|
|
331
418
|
}
|
|
332
419
|
|
|
@@ -346,10 +433,7 @@ export async function fetchPipelabCli(
|
|
|
346
433
|
// Default entry point for CLI if not provided
|
|
347
434
|
let finalEntryPoint = entryPoint;
|
|
348
435
|
if (!finalEntryPoint) {
|
|
349
|
-
const patterns = [
|
|
350
|
-
join(packageDir, "dist", "index.mjs"),
|
|
351
|
-
join(packageDir, "index.mjs"),
|
|
352
|
-
];
|
|
436
|
+
const patterns = [join(packageDir, "dist", "index.mjs"), join(packageDir, "index.mjs")];
|
|
353
437
|
finalEntryPoint = patterns.find((p) => existsSync(p)) || patterns[0];
|
|
354
438
|
}
|
|
355
439
|
|
|
@@ -387,7 +471,7 @@ async function tryResolveMonorepoPackage(
|
|
|
387
471
|
typeof pkg.bin === "string"
|
|
388
472
|
? pkg.bin
|
|
389
473
|
: pkg.bin?.[packageName.replace("@pipelab/", "")] ||
|
|
390
|
-
|
|
474
|
+
(pkg.bin ? pkg.bin[Object.keys(pkg.bin)[0]] : undefined);
|
|
391
475
|
|
|
392
476
|
// In dev, we prefer the "main" field if it points to TS, or a hardcoded src/index.ts
|
|
393
477
|
const tsSource = join(packageDir, "src", "index.ts");
|
|
@@ -434,11 +518,11 @@ async function crawlMonorepoPackages(): Promise<Record<string, string>> {
|
|
|
434
518
|
if (pkg.name) {
|
|
435
519
|
cache[pkg.name] = join(fullDir, entry.name);
|
|
436
520
|
}
|
|
437
|
-
} catch (e) {
|
|
521
|
+
} catch (e) {}
|
|
438
522
|
}
|
|
439
523
|
}
|
|
440
524
|
}
|
|
441
|
-
} catch (e) {
|
|
525
|
+
} catch (e) {}
|
|
442
526
|
}
|
|
443
527
|
return cache;
|
|
444
528
|
}
|
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 });
|