@pipelab/core-node 1.0.0-beta.2 → 1.0.0-beta.21
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 +1 -1
- package/CHANGELOG.md +164 -0
- package/dist/config-Bi0ORcTK.mjs +2 -0
- package/dist/config-CFgGRD9U.mjs +265 -0
- package/dist/config-CFgGRD9U.mjs.map +1 -0
- package/dist/index.d.mts +74 -53
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +1705 -576
- package/dist/index.mjs.map +1 -1
- package/package.json +9 -5
- package/scratch/simulate-updates.ts +120 -0
- package/src/config.test.ts +232 -0
- package/src/config.ts +111 -50
- package/src/context.ts +117 -5
- package/src/handler-func.ts +2 -77
- package/src/handlers/build-history.ts +60 -90
- package/src/handlers/config.ts +265 -55
- package/src/handlers/engine.ts +32 -35
- package/src/handlers/history.ts +0 -40
- package/src/handlers/index.ts +4 -0
- package/src/handlers/migration.ts +621 -0
- package/src/handlers/plugins.ts +305 -0
- package/src/handlers/system.ts +7 -2
- package/src/index.ts +9 -2
- package/src/plugins-registry.ts +280 -38
- package/src/presets/c3toSteam.ts +13 -7
- package/src/presets/demo.ts +9 -9
- package/src/presets/list.ts +0 -6
- package/src/presets/moreToCome.ts +3 -2
- package/src/presets/newProject.ts +3 -2
- package/src/presets/test-c3-offline.ts +15 -6
- package/src/presets/test-c3-unzip.ts +19 -8
- package/src/runner.ts +2 -8
- package/src/server.ts +45 -4
- package/src/types/runner.ts +2 -30
- package/src/utils/fs-extras.ts +6 -24
- package/src/utils/github.test.ts +211 -0
- package/src/utils/github.ts +90 -10
- package/src/utils/remote.test.ts +209 -0
- package/src/utils/remote.ts +325 -87
- package/src/utils/storage.ts +2 -1
- package/src/utils.ts +20 -24
- package/src/migrations.ts +0 -72
- package/src/presets/if.ts +0 -69
- package/src/presets/loop.ts +0 -65
package/src/utils/remote.ts
CHANGED
|
@@ -1,16 +1,48 @@
|
|
|
1
1
|
import { dirname, delimiter, join } from "node:path";
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
2
|
+
import {
|
|
3
|
+
mkdir,
|
|
4
|
+
readdir,
|
|
5
|
+
readFile,
|
|
6
|
+
writeFile,
|
|
7
|
+
access,
|
|
8
|
+
chmod,
|
|
9
|
+
rm,
|
|
10
|
+
cp,
|
|
11
|
+
rename,
|
|
12
|
+
} from "node:fs/promises";
|
|
13
|
+
import { existsSync, constants, statSync, readdirSync } from "node:fs";
|
|
14
|
+
import dns from "node:dns/promises";
|
|
5
15
|
import pacote from "pacote";
|
|
6
16
|
import semver from "semver";
|
|
7
|
-
import { isDev, projectRoot, PipelabContext } from "../context";
|
|
17
|
+
import { isDev, projectRoot, PipelabContext, CacheFolder } from "../context";
|
|
8
18
|
import { execa } from "execa";
|
|
9
19
|
import { sendStartupProgress } from "../server";
|
|
10
|
-
import { downloadFile, extractZip, extractTarGz
|
|
20
|
+
import { downloadFile, extractZip, extractTarGz } from "./fs-extras";
|
|
11
21
|
|
|
12
|
-
|
|
13
|
-
|
|
22
|
+
import { DEFAULT_NODE_VERSION, DEFAULT_PNPM_VERSION } from "@pipelab/constants";
|
|
23
|
+
|
|
24
|
+
function isPackageComplete(packageDir: string): boolean {
|
|
25
|
+
return existsSync(join(packageDir, "package.json"));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function isNodeJSComplete(nodePath: string): boolean {
|
|
29
|
+
try {
|
|
30
|
+
return existsSync(nodePath) && statSync(nodePath).size > 0;
|
|
31
|
+
} catch {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function isDependenciesInstalledSync(packageDir: string): boolean {
|
|
37
|
+
const nodeModulesPath = join(packageDir, "node_modules");
|
|
38
|
+
if (!existsSync(nodeModulesPath)) return false;
|
|
39
|
+
try {
|
|
40
|
+
const files = readdirSync(nodeModulesPath);
|
|
41
|
+
return files.length > 0;
|
|
42
|
+
} catch {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
14
46
|
|
|
15
47
|
/**
|
|
16
48
|
* In-memory lock to prevent concurrent operations on the same resource (e.g., downloading Node.js).
|
|
@@ -37,6 +69,27 @@ async function withLock<T>(key: string, fn: () => Promise<T>): Promise<T> {
|
|
|
37
69
|
return promise;
|
|
38
70
|
}
|
|
39
71
|
|
|
72
|
+
let isOnlineCached: boolean | null = null;
|
|
73
|
+
let lastCheckTime = 0;
|
|
74
|
+
|
|
75
|
+
export async function isOnline(): Promise<boolean> {
|
|
76
|
+
const now = Date.now();
|
|
77
|
+
if (isOnlineCached !== null && now - lastCheckTime < 10000) {
|
|
78
|
+
return isOnlineCached;
|
|
79
|
+
}
|
|
80
|
+
try {
|
|
81
|
+
await Promise.race([
|
|
82
|
+
dns.lookup("registry.npmjs.org"),
|
|
83
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error("timeout")), 1500)),
|
|
84
|
+
]);
|
|
85
|
+
isOnlineCached = true;
|
|
86
|
+
} catch {
|
|
87
|
+
isOnlineCached = false;
|
|
88
|
+
}
|
|
89
|
+
lastCheckTime = now;
|
|
90
|
+
return isOnlineCached;
|
|
91
|
+
}
|
|
92
|
+
|
|
40
93
|
export type FetchOptions = {
|
|
41
94
|
installDeps?: boolean;
|
|
42
95
|
signal?: AbortSignal;
|
|
@@ -57,12 +110,16 @@ export async function fetchPackage(
|
|
|
57
110
|
isLocal?: boolean;
|
|
58
111
|
entryPoint?: string;
|
|
59
112
|
}> {
|
|
113
|
+
const start = Date.now();
|
|
60
114
|
// 0. Check for local monorepo package in development
|
|
61
115
|
if (isDev && projectRoot && process.env.PIPELAB_FORCE_NPM !== "true") {
|
|
62
116
|
if (packageName.startsWith("@pipelab/")) {
|
|
117
|
+
const localStart = Date.now();
|
|
63
118
|
const local = await tryResolveMonorepoPackage(packageName);
|
|
64
119
|
if (local) {
|
|
65
|
-
console.log(
|
|
120
|
+
console.log(
|
|
121
|
+
`[Fetcher] ${packageName}: Resolved to local source at ${local.packageDir} (${Date.now() - localStart}ms)`,
|
|
122
|
+
);
|
|
66
123
|
return {
|
|
67
124
|
...local,
|
|
68
125
|
resolvedVersion: "workspace",
|
|
@@ -75,66 +132,182 @@ export async function fetchPackage(
|
|
|
75
132
|
const baseDir = ctx.getPackagesPath(packageName);
|
|
76
133
|
let resolvedVersion: string;
|
|
77
134
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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;
|
|
90
|
-
const versions = Object.keys(packument.versions);
|
|
91
|
-
const range = versionOrRange || "latest";
|
|
135
|
+
let resolvedVersionOrRange = versionOrRange;
|
|
136
|
+
if (resolvedVersionOrRange === "local") {
|
|
137
|
+
resolvedVersionOrRange = "latest";
|
|
138
|
+
}
|
|
92
139
|
|
|
93
|
-
|
|
94
|
-
|
|
140
|
+
console.log(`[Fetcher] Resolving ${packageName}@${resolvedVersionOrRange || "latest"}...`);
|
|
141
|
+
const resolveStart = Date.now();
|
|
95
142
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
)
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
143
|
+
const online = await isOnline();
|
|
144
|
+
if (!online) {
|
|
145
|
+
console.warn(
|
|
146
|
+
`[Fetcher] ${packageName}: offline mode detected (${Date.now() - resolveStart}ms), trying local fallback...`,
|
|
147
|
+
);
|
|
148
|
+
const fallbackStart = Date.now();
|
|
149
|
+
const fallbackVersion = await tryLocalFallback(
|
|
150
|
+
resolvedVersionOrRange,
|
|
151
|
+
new Error("Offline"),
|
|
152
|
+
baseDir,
|
|
153
|
+
packageName,
|
|
154
|
+
);
|
|
106
155
|
if (fallbackVersion) {
|
|
107
156
|
resolvedVersion = fallbackVersion;
|
|
157
|
+
console.log(
|
|
158
|
+
`[Fetcher] ${packageName}: Resolved to local fallback ${resolvedVersion} (${Date.now() - fallbackStart}ms)`,
|
|
159
|
+
);
|
|
108
160
|
} else {
|
|
109
|
-
throw
|
|
161
|
+
throw new Error(`Offline and no local fallback version available for ${packageName}`);
|
|
162
|
+
}
|
|
163
|
+
} else {
|
|
164
|
+
try {
|
|
165
|
+
// 1. Resolve version/range using npm with session-wide memoization and disk cache
|
|
166
|
+
const cachePath = ctx.getCachePath(CacheFolder.Pacote);
|
|
167
|
+
let packumentPromise = packumentRequests.get(packageName);
|
|
168
|
+
if (!packumentPromise) {
|
|
169
|
+
packumentPromise = pacote.packument(packageName, { cache: cachePath });
|
|
170
|
+
packumentRequests.set(packageName, packumentPromise);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const packument = await packumentPromise;
|
|
174
|
+
const versions = Object.keys(packument.versions);
|
|
175
|
+
const range = resolvedVersionOrRange || "latest";
|
|
176
|
+
|
|
177
|
+
// Prioritize tags (like 'latest', 'beta', etc.) over semver ranges
|
|
178
|
+
let foundVersion = packument["dist-tags"]?.[range] || semver.maxSatisfying(versions, range);
|
|
179
|
+
|
|
180
|
+
// If we are in a non-latest releaseTag channel (like "beta" or "dev"),
|
|
181
|
+
// and we are resolving "latest", we prefer the releaseTag version if it exists
|
|
182
|
+
// and is newer than (or equal to) the latest stable version.
|
|
183
|
+
if (range === "latest" && ctx.releaseTag && ctx.releaseTag !== "latest") {
|
|
184
|
+
const releaseTagVersion = packument["dist-tags"]?.[ctx.releaseTag];
|
|
185
|
+
if (releaseTagVersion && semver.valid(releaseTagVersion)) {
|
|
186
|
+
if (
|
|
187
|
+
!foundVersion ||
|
|
188
|
+
(semver.valid(foundVersion) && semver.gte(releaseTagVersion, foundVersion))
|
|
189
|
+
) {
|
|
190
|
+
console.log(
|
|
191
|
+
`[Fetcher] Using release tag "${ctx.releaseTag}" (${releaseTagVersion}) instead of "latest" (${foundVersion || "none"}) for ${packageName}`,
|
|
192
|
+
);
|
|
193
|
+
foundVersion = releaseTagVersion;
|
|
194
|
+
} else if (foundVersion) {
|
|
195
|
+
console.warn(
|
|
196
|
+
`[Fetcher] Tag "${ctx.releaseTag}" (${releaseTagVersion}) is older than "latest" (${foundVersion}) for ${packageName}, keeping "latest"`,
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (!foundVersion) {
|
|
203
|
+
throw new Error(
|
|
204
|
+
`Package ${packageName}@${range} not found on npm (available tags: ${Object.keys(packument["dist-tags"] || {}).join(", ")})`,
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
resolvedVersion = foundVersion;
|
|
208
|
+
console.log(
|
|
209
|
+
`[Fetcher] ${packageName}: Resolved to v${resolvedVersion} via npm (${Date.now() - resolveStart}ms)`,
|
|
210
|
+
);
|
|
211
|
+
} catch (error) {
|
|
212
|
+
console.warn(
|
|
213
|
+
`[Fetcher] ${packageName}: remote resolution failed (${Date.now() - resolveStart}ms), trying local fallback...`,
|
|
214
|
+
);
|
|
215
|
+
const fallbackStart = Date.now();
|
|
216
|
+
const fallbackVersion = await tryLocalFallback(
|
|
217
|
+
resolvedVersionOrRange,
|
|
218
|
+
error,
|
|
219
|
+
baseDir,
|
|
220
|
+
packageName,
|
|
221
|
+
);
|
|
222
|
+
if (fallbackVersion) {
|
|
223
|
+
resolvedVersion = fallbackVersion;
|
|
224
|
+
console.log(
|
|
225
|
+
`[Fetcher] ${packageName}: Resolved to local fallback ${resolvedVersion} (${Date.now() - fallbackStart}ms)`,
|
|
226
|
+
);
|
|
227
|
+
} else {
|
|
228
|
+
throw error;
|
|
229
|
+
}
|
|
110
230
|
}
|
|
111
231
|
}
|
|
112
232
|
|
|
113
|
-
const cachePath =
|
|
233
|
+
const cachePath = ctx.getCachePath(CacheFolder.Pacote);
|
|
114
234
|
const packageDir = join(baseDir, resolvedVersion);
|
|
115
235
|
|
|
116
|
-
|
|
117
|
-
|
|
236
|
+
const checkStart = Date.now();
|
|
237
|
+
// If the package already exists and we don't need to install dependencies (or they are already installed), return immediately
|
|
238
|
+
const isInstalled = options?.installDeps
|
|
239
|
+
? isPackageComplete(packageDir) && isDependenciesInstalledSync(packageDir)
|
|
240
|
+
: isPackageComplete(packageDir);
|
|
241
|
+
const checkDuration = Date.now() - checkStart;
|
|
242
|
+
|
|
243
|
+
if (isInstalled) {
|
|
244
|
+
console.log(
|
|
245
|
+
`[Fetcher] ${packageName}@${resolvedVersion}: Already installed (check took ${checkDuration}ms, fetchPackage took ${Date.now() - start}ms)`,
|
|
246
|
+
);
|
|
118
247
|
return { packageDir, resolvedVersion };
|
|
119
248
|
}
|
|
120
249
|
|
|
121
250
|
const lockKey = `package:${packageName}:${resolvedVersion}`;
|
|
122
251
|
return withLock(lockKey, async () => {
|
|
123
|
-
if (!
|
|
252
|
+
if (!isPackageComplete(packageDir)) {
|
|
124
253
|
console.log(`[Fetcher] ${packageName}@${resolvedVersion}: Downloading to ${packageDir}...`);
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
254
|
+
const downloadStart = Date.now();
|
|
255
|
+
|
|
256
|
+
const tempDir = join(
|
|
257
|
+
baseDir,
|
|
258
|
+
`.tmp-${resolvedVersion}-${Math.random().toString(36).slice(2)}`,
|
|
259
|
+
);
|
|
260
|
+
await mkdir(tempDir, { recursive: true });
|
|
261
|
+
try {
|
|
262
|
+
await pacote.extract(`${packageName}@${resolvedVersion}`, tempDir, {
|
|
263
|
+
cache: cachePath,
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
// Resolve entryPoint from package.json inside the temp directory first to ensure everything works
|
|
267
|
+
await resolveEntryPoint(tempDir, packageName);
|
|
268
|
+
|
|
269
|
+
// Remove existing incomplete packageDir if any
|
|
270
|
+
if (existsSync(packageDir)) {
|
|
271
|
+
await rm(packageDir, { recursive: true, force: true }).catch(() => {});
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Atomically rename the temp directory to the target packageDir
|
|
275
|
+
try {
|
|
276
|
+
await rename(tempDir, packageDir);
|
|
277
|
+
} catch (err: any) {
|
|
278
|
+
if (isPackageComplete(packageDir)) {
|
|
279
|
+
console.log(`[Fetcher] Destination ${packageDir} already exists and is valid.`);
|
|
280
|
+
} else {
|
|
281
|
+
throw err;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
console.log(
|
|
285
|
+
`[Fetcher] ${packageName}@${resolvedVersion}: Downloaded and extracted in ${Date.now() - downloadStart}ms`,
|
|
286
|
+
);
|
|
287
|
+
} catch (err) {
|
|
288
|
+
await rm(tempDir, { recursive: true, force: true }).catch(() => {});
|
|
289
|
+
throw err;
|
|
290
|
+
}
|
|
129
291
|
}
|
|
130
292
|
|
|
131
293
|
// 2. Resolve entry point from package.json for downloaded package
|
|
294
|
+
const entryStart = Date.now();
|
|
132
295
|
const entryPoint = await resolveEntryPoint(packageDir, packageName);
|
|
296
|
+
console.log(
|
|
297
|
+
`[Fetcher] ${packageName}@${resolvedVersion}: Resolved entry point in ${Date.now() - entryStart}ms`,
|
|
298
|
+
);
|
|
133
299
|
|
|
134
300
|
if (options?.installDeps) {
|
|
301
|
+
const depsStart = Date.now();
|
|
135
302
|
await installDependencies(packageDir, packageName, options);
|
|
303
|
+
console.log(
|
|
304
|
+
`[Fetcher] ${packageName}@${resolvedVersion}: Installed dependencies in ${Date.now() - depsStart}ms`,
|
|
305
|
+
);
|
|
136
306
|
}
|
|
137
307
|
|
|
308
|
+
console.log(
|
|
309
|
+
`[Fetcher] ${packageName}@${resolvedVersion}: FetchPackage complete in ${Date.now() - start}ms`,
|
|
310
|
+
);
|
|
138
311
|
return { packageDir, resolvedVersion, entryPoint };
|
|
139
312
|
});
|
|
140
313
|
}
|
|
@@ -181,7 +354,7 @@ export async function runPnpm(
|
|
|
181
354
|
...process.env,
|
|
182
355
|
NODE_ENV: "production",
|
|
183
356
|
PATH: nodePath ? `${dirname(nodePath)}${delimiter}${process.env.PATH}` : process.env.PATH,
|
|
184
|
-
PNPM_HOME:
|
|
357
|
+
PNPM_HOME: ctx.getPnpmPath(),
|
|
185
358
|
PNPM_ONLY_ALLOW_TRUSTED_DEPENDENCIES: "false",
|
|
186
359
|
...extraEnv,
|
|
187
360
|
},
|
|
@@ -192,17 +365,21 @@ export async function runPnpm(
|
|
|
192
365
|
* Installs a specific version of Node.js if not already present.
|
|
193
366
|
*/
|
|
194
367
|
export async function ensureNodeJS(context: PipelabContext, version = DEFAULT_NODE_VERSION) {
|
|
368
|
+
const checkStart = Date.now();
|
|
195
369
|
const isWindows = process.platform === "win32";
|
|
196
370
|
const nodeDir = context.getThirdPartyPath("node", version);
|
|
197
371
|
const finalNodePath = join(nodeDir, isWindows ? "node.exe" : "bin/node");
|
|
198
372
|
|
|
199
|
-
if (
|
|
373
|
+
if (isNodeJSComplete(finalNodePath)) {
|
|
374
|
+
console.log(
|
|
375
|
+
`[Environment] Node.js check took ${Date.now() - checkStart}ms (found at ${finalNodePath})`,
|
|
376
|
+
);
|
|
200
377
|
return finalNodePath;
|
|
201
378
|
}
|
|
202
379
|
|
|
203
380
|
const lockKey = `node:${version}`;
|
|
204
381
|
return withLock(lockKey, async () => {
|
|
205
|
-
if (
|
|
382
|
+
if (isNodeJSComplete(finalNodePath)) return finalNodePath;
|
|
206
383
|
|
|
207
384
|
const arch = process.arch === "x64" ? "x64" : process.arch === "arm64" ? "arm64" : "x86";
|
|
208
385
|
const platform = isWindows ? "win" : process.platform === "darwin" ? "osx" : "linux";
|
|
@@ -211,15 +388,18 @@ export async function ensureNodeJS(context: PipelabContext, version = DEFAULT_NO
|
|
|
211
388
|
|
|
212
389
|
const fileName = `node-v${version}-${downloadPlatform}-${arch}.${extension}`;
|
|
213
390
|
const downloadUrl = `https://nodejs.org/dist/v${version}/${fileName}`;
|
|
214
|
-
const tempDir = await
|
|
391
|
+
const tempDir = await context.createTempFolder("node-download-");
|
|
215
392
|
const archivePath = join(tempDir, fileName);
|
|
216
393
|
|
|
217
394
|
sendStartupProgress(`Downloading Node.js v${version}...`);
|
|
218
395
|
console.log(`Downloading Node.js from ${downloadUrl}...`);
|
|
396
|
+
const dlStart = Date.now();
|
|
219
397
|
await downloadFile(downloadUrl, archivePath);
|
|
398
|
+
console.log(`[Environment] Node.js download took ${Date.now() - dlStart}ms`);
|
|
220
399
|
|
|
221
400
|
sendStartupProgress(`Extracting Node.js v${version}...`);
|
|
222
401
|
console.log(`Extracting Node.js to ${tempDir}...`);
|
|
402
|
+
const extStart = Date.now();
|
|
223
403
|
const extractTempDir = join(tempDir, "extracted");
|
|
224
404
|
await mkdir(extractTempDir, { recursive: true });
|
|
225
405
|
|
|
@@ -234,12 +414,42 @@ export async function ensureNodeJS(context: PipelabContext, version = DEFAULT_NO
|
|
|
234
414
|
if (!nodeSubDir) throw new Error(`Could not find extracted Node.js directory`);
|
|
235
415
|
|
|
236
416
|
const sourceDir = join(extractTempDir, nodeSubDir);
|
|
237
|
-
|
|
238
|
-
await
|
|
239
|
-
|
|
240
|
-
|
|
417
|
+
const parentDir = dirname(nodeDir);
|
|
418
|
+
await mkdir(parentDir, { recursive: true });
|
|
419
|
+
|
|
420
|
+
const tempNodeDir = join(
|
|
421
|
+
parentDir,
|
|
422
|
+
`.tmp-node-${version}-${Math.random().toString(36).slice(2)}`,
|
|
423
|
+
);
|
|
424
|
+
await mkdir(tempNodeDir, { recursive: true });
|
|
425
|
+
|
|
426
|
+
try {
|
|
427
|
+
await cp(sourceDir, tempNodeDir, { recursive: true });
|
|
428
|
+
if (!isWindows) {
|
|
429
|
+
const tempNodePath = join(tempNodeDir, "bin/node");
|
|
430
|
+
await chmod(tempNodePath, 0o755).catch(() => {});
|
|
431
|
+
}
|
|
241
432
|
|
|
242
|
-
|
|
433
|
+
if (existsSync(nodeDir)) {
|
|
434
|
+
await rm(nodeDir, { recursive: true, force: true }).catch(() => {});
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
try {
|
|
438
|
+
await rename(tempNodeDir, nodeDir);
|
|
439
|
+
} catch (err: any) {
|
|
440
|
+
if (isNodeJSComplete(finalNodePath)) {
|
|
441
|
+
console.log(`[Fetcher] Node.js directory already exists and is valid.`);
|
|
442
|
+
} else {
|
|
443
|
+
throw err;
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
console.log(`[Environment] Node.js extraction took ${Date.now() - extStart}ms`);
|
|
447
|
+
} finally {
|
|
448
|
+
await rm(tempNodeDir, { recursive: true, force: true }).catch(() => {});
|
|
449
|
+
await rm(tempDir, { recursive: true, force: true }).catch(() => {});
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
console.log(`[Environment] Node.js set up complete in ${Date.now() - checkStart}ms`);
|
|
243
453
|
return finalNodePath;
|
|
244
454
|
});
|
|
245
455
|
}
|
|
@@ -248,10 +458,14 @@ export async function ensureNodeJS(context: PipelabContext, version = DEFAULT_NO
|
|
|
248
458
|
* Installs the PNPM package from npm if not already present.
|
|
249
459
|
*/
|
|
250
460
|
export async function ensurePNPM(context: PipelabContext, version = DEFAULT_PNPM_VERSION) {
|
|
461
|
+
const checkStart = Date.now();
|
|
251
462
|
const pnpmDir = context.getPackagesPath("pnpm", version);
|
|
252
463
|
const pnpmPath = join(pnpmDir, "bin", "pnpm.cjs");
|
|
253
464
|
|
|
254
465
|
if (existsSync(pnpmPath)) {
|
|
466
|
+
console.log(
|
|
467
|
+
`[Environment] PNPM check took ${Date.now() - checkStart}ms (found at ${pnpmPath})`,
|
|
468
|
+
);
|
|
255
469
|
return pnpmPath;
|
|
256
470
|
}
|
|
257
471
|
|
|
@@ -262,44 +476,56 @@ export async function ensurePNPM(context: PipelabContext, version = DEFAULT_PNPM
|
|
|
262
476
|
const { packageDir } = await fetchPackage("pnpm", version, {
|
|
263
477
|
context,
|
|
264
478
|
});
|
|
479
|
+
console.log(`[Environment] PNPM set up complete in ${Date.now() - checkStart}ms`);
|
|
265
480
|
return join(packageDir, "bin", "pnpm.cjs");
|
|
266
481
|
});
|
|
267
482
|
}
|
|
268
483
|
|
|
269
484
|
async function installDependencies(packageDir: string, packageName: string, options: FetchOptions) {
|
|
485
|
+
const start = Date.now();
|
|
270
486
|
const nodeModulesPath = join(packageDir, "node_modules");
|
|
271
487
|
|
|
272
|
-
if (
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
if (files.length === 0) {
|
|
276
|
-
console.warn(
|
|
277
|
-
`[Fetcher] ${packageName}: node_modules exists but is empty. Re-installing...`,
|
|
278
|
-
);
|
|
279
|
-
} else {
|
|
280
|
-
console.log(`[Fetcher] ${packageName}: Dependencies already installed, skipping.`);
|
|
281
|
-
return;
|
|
282
|
-
}
|
|
283
|
-
} catch (e) {
|
|
284
|
-
// Continue to install if readdir fails
|
|
285
|
-
}
|
|
488
|
+
if (isDependenciesInstalledSync(packageDir)) {
|
|
489
|
+
console.log(`[Fetcher] ${packageName}: Dependencies already installed, skipping.`);
|
|
490
|
+
return;
|
|
286
491
|
}
|
|
287
492
|
|
|
288
|
-
|
|
493
|
+
// To prevent other processes from seeing a partially populated node_modules,
|
|
494
|
+
// we can create a temp directory next to packageDir, run pnpm there, and rename node_modules
|
|
495
|
+
const tempDir = `${packageDir}.tmp-deps-${Math.random().toString(36).slice(2)}`;
|
|
496
|
+
await mkdir(tempDir, { recursive: true });
|
|
289
497
|
try {
|
|
290
|
-
|
|
498
|
+
// Copy package.json to tempDir so pnpm can install dependencies
|
|
499
|
+
await cp(join(packageDir, "package.json"), join(tempDir, "package.json"));
|
|
500
|
+
|
|
501
|
+
console.log(`[Fetcher] ${packageName}: Ensuring dependencies are installed...`);
|
|
502
|
+
const pnpmStart = Date.now();
|
|
503
|
+
const { all } = await runPnpm(tempDir, {
|
|
291
504
|
signal: options.signal,
|
|
292
505
|
context: options.context,
|
|
293
506
|
});
|
|
507
|
+
console.log(`[Fetcher] ${packageName}: pnpm install command took ${Date.now() - pnpmStart}ms`);
|
|
294
508
|
|
|
295
509
|
if (all) console.log(`[Fetcher] ${packageName}: Installation trace:\n${all}`);
|
|
296
|
-
|
|
510
|
+
|
|
511
|
+
// Atomically rename node_modules from tempDir to packageDir/node_modules
|
|
512
|
+
const tempNodeModules = join(tempDir, "node_modules");
|
|
513
|
+
if (existsSync(nodeModulesPath)) {
|
|
514
|
+
await rm(nodeModulesPath, { recursive: true, force: true }).catch(() => {});
|
|
515
|
+
}
|
|
516
|
+
const renameStart = Date.now();
|
|
517
|
+
await rename(tempNodeModules, nodeModulesPath);
|
|
518
|
+
console.log(
|
|
519
|
+
`[Fetcher] ${packageName}: Dependencies installed successfully (rename took ${Date.now() - renameStart}ms, total installDependencies took ${Date.now() - start}ms).`,
|
|
520
|
+
);
|
|
297
521
|
} catch (err: any) {
|
|
298
522
|
console.error(
|
|
299
523
|
`[Fetcher] ${packageName}: CRITICAL ERROR during dependency installation: ${err.message}`,
|
|
300
524
|
);
|
|
301
525
|
if (err.all) console.error(`[Fetcher] ${packageName}: Error details:\n${err.all}`);
|
|
302
526
|
throw new Error(`Failed to install dependencies for ${packageName}. See logs for details.`);
|
|
527
|
+
} finally {
|
|
528
|
+
await rm(tempDir, { recursive: true, force: true }).catch(() => {});
|
|
303
529
|
}
|
|
304
530
|
}
|
|
305
531
|
|
|
@@ -444,30 +670,42 @@ async function crawlMonorepoPackages(): Promise<Record<string, string>> {
|
|
|
444
670
|
return cache;
|
|
445
671
|
}
|
|
446
672
|
|
|
447
|
-
async function
|
|
673
|
+
async function tryLocalFallback(
|
|
674
|
+
versionOrRange: string | undefined,
|
|
675
|
+
_error: unknown,
|
|
676
|
+
baseDir: string,
|
|
677
|
+
logPrefix: string,
|
|
678
|
+
): Promise<string | null> {
|
|
448
679
|
if (!existsSync(baseDir)) return null;
|
|
449
680
|
try {
|
|
450
681
|
const entries = await readdir(baseDir, { withFileTypes: true });
|
|
451
|
-
const
|
|
682
|
+
const localVersions = entries
|
|
452
683
|
.filter((e) => e.isDirectory() || e.isSymbolicLink())
|
|
453
684
|
.map((e) => e.name)
|
|
454
|
-
.
|
|
455
|
-
return versions[0] || null;
|
|
456
|
-
} catch {
|
|
457
|
-
return null;
|
|
458
|
-
}
|
|
459
|
-
}
|
|
685
|
+
.filter((name) => !!semver.valid(name));
|
|
460
686
|
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
)
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
687
|
+
if (localVersions.length === 0) return null;
|
|
688
|
+
|
|
689
|
+
const range = versionOrRange || "latest";
|
|
690
|
+
|
|
691
|
+
if (range === "latest") {
|
|
692
|
+
const sorted = localVersions.sort((a, b) => semver.rcompare(a, b));
|
|
693
|
+
const latestLocal = sorted[0] || null;
|
|
694
|
+
if (latestLocal) {
|
|
695
|
+
console.info(`[Fetcher] ${logPrefix}: Using locally cached latest version: ${latestLocal}`);
|
|
696
|
+
return latestLocal;
|
|
697
|
+
}
|
|
698
|
+
} else {
|
|
699
|
+
const matched = semver.maxSatisfying(localVersions, range);
|
|
700
|
+
if (matched) {
|
|
701
|
+
console.info(
|
|
702
|
+
`[Fetcher] ${logPrefix}: Using locally cached matching version: ${matched} for range ${range}`,
|
|
703
|
+
);
|
|
704
|
+
return matched;
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
} catch (e) {
|
|
708
|
+
console.warn(`[Fetcher] ${logPrefix}: Error during local fallback resolution:`, e);
|
|
471
709
|
}
|
|
472
710
|
return null;
|
|
473
711
|
}
|
package/src/utils/storage.ts
CHANGED
|
@@ -12,7 +12,8 @@ export class JsonFileStorage {
|
|
|
12
12
|
private logger = useLogger().logger;
|
|
13
13
|
|
|
14
14
|
constructor(fileName: string, context: PipelabContext) {
|
|
15
|
-
this.filePath =
|
|
15
|
+
this.filePath = context.getConfigPath(fileName);
|
|
16
|
+
|
|
16
17
|
const dir = path.dirname(this.filePath);
|
|
17
18
|
if (!fs.existsSync(dir)) {
|
|
18
19
|
try {
|
package/src/utils.ts
CHANGED
|
@@ -1,24 +1,27 @@
|
|
|
1
1
|
import { nanoid } from "nanoid";
|
|
2
|
-
import {
|
|
3
|
-
|
|
2
|
+
import {
|
|
3
|
+
usePlugins,
|
|
4
|
+
RendererPluginDefinition,
|
|
5
|
+
processGraph,
|
|
6
|
+
useLogger,
|
|
7
|
+
BuildHistoryEntry,
|
|
8
|
+
Variable,
|
|
9
|
+
AppConfig,
|
|
10
|
+
transformUrl,
|
|
11
|
+
} from "@pipelab/shared";
|
|
4
12
|
import { downloadFile, DownloadHooks } from "./utils/fs-extras";
|
|
5
13
|
import { access, chmod, mkdir, rm, writeFile, readdir, cp } from "node:fs/promises";
|
|
6
14
|
import { dirname, join } from "node:path";
|
|
7
15
|
import { pathToFileURL } from "node:url";
|
|
8
16
|
import { isDev, projectRoot, PipelabContext } from "./context";
|
|
9
17
|
import { constants, existsSync } from "node:fs";
|
|
10
|
-
import { processGraph } from "@pipelab/shared";
|
|
11
18
|
import { handleActionExecute } from "./handler-func";
|
|
12
|
-
import { useLogger } from "@pipelab/shared";
|
|
13
19
|
import { BuildHistoryStorage } from "./handlers/build-history";
|
|
14
|
-
import type { BuildHistoryEntry } from "@pipelab/shared";
|
|
15
|
-
import type { Variable } from "@pipelab/shared";
|
|
16
20
|
|
|
17
|
-
import { ensure,
|
|
21
|
+
import { ensure, extractTarGz, extractZip, zipFolder } from "./utils/fs-extras";
|
|
18
22
|
import { fetchPipelabAsset } from "./utils/remote";
|
|
19
23
|
import { loadPipelabPlugin } from "./plugins-registry";
|
|
20
|
-
import {
|
|
21
|
-
import { AppConfig } from "@pipelab/shared";
|
|
24
|
+
import { setupSettingsConfigFile } from "./config";
|
|
22
25
|
|
|
23
26
|
export const getFinalPlugins = () => {
|
|
24
27
|
const { plugins } = usePlugins();
|
|
@@ -29,13 +32,6 @@ export const getFinalPlugins = () => {
|
|
|
29
32
|
for (const plugin of plugins.value) {
|
|
30
33
|
const finalNodes = [];
|
|
31
34
|
|
|
32
|
-
const transformUrl = (url: string) => {
|
|
33
|
-
if (url.startsWith("file://")) {
|
|
34
|
-
return url.replace("file://", "media://");
|
|
35
|
-
}
|
|
36
|
-
return url;
|
|
37
|
-
};
|
|
38
|
-
|
|
39
35
|
const finalIcon =
|
|
40
36
|
plugin.icon?.type === "image"
|
|
41
37
|
? {
|
|
@@ -106,6 +102,7 @@ export const executeGraphWithHistory = async ({
|
|
|
106
102
|
projectName,
|
|
107
103
|
projectPath,
|
|
108
104
|
pipelineId,
|
|
105
|
+
cachePath,
|
|
109
106
|
startTime,
|
|
110
107
|
status: "running",
|
|
111
108
|
logs: [],
|
|
@@ -124,7 +121,7 @@ export const executeGraphWithHistory = async ({
|
|
|
124
121
|
}
|
|
125
122
|
|
|
126
123
|
const logs: any[] = [];
|
|
127
|
-
const sandboxPath = await
|
|
124
|
+
const sandboxPath = await ctx.createTempFolder("pipeline-sandbox-");
|
|
128
125
|
const { logger } = useLogger();
|
|
129
126
|
let completedSteps = 0;
|
|
130
127
|
let failedSteps = 0;
|
|
@@ -152,7 +149,7 @@ export const executeGraphWithHistory = async ({
|
|
|
152
149
|
}
|
|
153
150
|
|
|
154
151
|
logger().info(`[Sandbox] Execution sandbox created at: ${sandboxPath}`);
|
|
155
|
-
const settingsFile = await
|
|
152
|
+
const settingsFile = await setupSettingsConfigFile(ctx);
|
|
156
153
|
const config = await settingsFile.getConfig();
|
|
157
154
|
const shouldCleanup = true;
|
|
158
155
|
|
|
@@ -251,18 +248,17 @@ export const executeGraphWithHistory = async ({
|
|
|
251
248
|
|
|
252
249
|
throw error;
|
|
253
250
|
} finally {
|
|
254
|
-
// Apply retention policy regardless of outcome
|
|
255
|
-
if (!shouldDisableHistory) {
|
|
256
|
-
// Don't await, let it run in the background
|
|
257
|
-
buildHistoryStorage.applyRetentionPolicy();
|
|
258
|
-
}
|
|
259
|
-
|
|
260
251
|
if (shouldCleanup) {
|
|
261
252
|
try {
|
|
262
253
|
await rm(sandboxPath, { recursive: true, force: true });
|
|
263
254
|
} catch (e) {
|
|
264
255
|
console.warn(`Failed to cleanup sandbox at ${sandboxPath}:`, e);
|
|
265
256
|
}
|
|
257
|
+
try {
|
|
258
|
+
await rm(cachePath, { recursive: true, force: true });
|
|
259
|
+
} catch (e) {
|
|
260
|
+
console.warn(`Failed to cleanup cache at ${cachePath}:`, e);
|
|
261
|
+
}
|
|
266
262
|
}
|
|
267
263
|
}
|
|
268
264
|
};
|