@pipelab/plugin-core 1.0.1-beta.2

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.
Files changed (41) hide show
  1. package/.turbo/turbo-build.log +364 -0
  2. package/.turbo/turbo-lint.log +36 -0
  3. package/CHANGELOG.md +22 -0
  4. package/LICENSE +110 -0
  5. package/LICENSE.md +110 -0
  6. package/README.md +3 -0
  7. package/dist/chunk-JTKJZQYV-CRyylFtW.mjs +1153 -0
  8. package/dist/chunk-JTKJZQYV-CRyylFtW.mjs.map +1 -0
  9. package/dist/chunk-JTKJZQYV-DwALyGe5.cjs +1186 -0
  10. package/dist/emscripten-module-BreoZ8PK.cjs +548 -0
  11. package/dist/emscripten-module-DVxR1x9z.mjs +549 -0
  12. package/dist/emscripten-module-DVxR1x9z.mjs.map +1 -0
  13. package/dist/emscripten-module-VHMWRAJ2-CZxK3CFi.cjs +373 -0
  14. package/dist/emscripten-module-VHMWRAJ2-Czcf6BHN.mjs +374 -0
  15. package/dist/emscripten-module-VHMWRAJ2-Czcf6BHN.mjs.map +1 -0
  16. package/dist/ffi-B2-9g6wM.cjs +143 -0
  17. package/dist/ffi-D46mojcp.cjs +143 -0
  18. package/dist/ffi-DjjqisTa.mjs +144 -0
  19. package/dist/ffi-DjjqisTa.mjs.map +1 -0
  20. package/dist/ffi-SCExi0U-.mjs +144 -0
  21. package/dist/ffi-SCExi0U-.mjs.map +1 -0
  22. package/dist/index.cjs +64710 -0
  23. package/dist/index.d.cts +160 -0
  24. package/dist/index.d.cts.map +1 -0
  25. package/dist/index.d.mts +160 -0
  26. package/dist/index.d.mts.map +1 -0
  27. package/dist/index.mjs +64571 -0
  28. package/dist/index.mjs.map +1 -0
  29. package/dist/module-6F3E5H7Y-BVGmvvK_.cjs +6 -0
  30. package/dist/module-6F3E5H7Y-DHjYta_R.mjs +2 -0
  31. package/package.json +55 -0
  32. package/src/archive-utils.ts +137 -0
  33. package/src/create-plugin.ts +8 -0
  34. package/src/custom-errors.ts +11 -0
  35. package/src/fs-utils.ts +31 -0
  36. package/src/index.ts +10 -0
  37. package/src/node-utils.ts +52 -0
  38. package/src/pipelab.ts +103 -0
  39. package/src/utils.ts +251 -0
  40. package/tsconfig.json +9 -0
  41. package/tsdown.config.ts +8 -0
@@ -0,0 +1,6 @@
1
+ const require_chunk_JTKJZQYV = require("./chunk-JTKJZQYV-DwALyGe5.cjs");
2
+ require("./index.cjs");
3
+ exports.QuickJSModuleCallbacks = require_chunk_JTKJZQYV.QuickJSModuleCallbacks;
4
+ exports.QuickJSWASMModule = require_chunk_JTKJZQYV.QuickJSWASMModule;
5
+ exports.applyBaseRuntimeOptions = require_chunk_JTKJZQYV.applyBaseRuntimeOptions;
6
+ exports.applyModuleEvalRuntimeOptions = require_chunk_JTKJZQYV.applyModuleEvalRuntimeOptions;
@@ -0,0 +1,2 @@
1
+ import { a as applyModuleEvalRuntimeOptions, i as applyBaseRuntimeOptions, n as QuickJSModuleCallbacks, r as QuickJSWASMModule } from "./chunk-JTKJZQYV-CRyylFtW.mjs";
2
+ export { QuickJSModuleCallbacks, QuickJSWASMModule, applyBaseRuntimeOptions, applyModuleEvalRuntimeOptions };
package/package.json ADDED
@@ -0,0 +1,55 @@
1
+ {
2
+ "name": "@pipelab/plugin-core",
3
+ "version": "1.0.1-beta.2",
4
+ "description": "Core library for building Pipelab plugins",
5
+ "license": "FSL-1.1-MIT",
6
+ "author": "CynToolkit",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://github.com/CynToolkit/pipelab.git",
10
+ "directory": "packages/plugin-core"
11
+ },
12
+ "publishConfig": {
13
+ "access": "public"
14
+ },
15
+ "type": "module",
16
+ "main": "./dist/index.cjs",
17
+ "browser": "./src/index.ts",
18
+ "types": "./dist/index.d.ts",
19
+ "exports": {
20
+ ".": {
21
+ "types": "./dist/index.d.ts",
22
+ "import": "./dist/index.js",
23
+ "require": "./dist/index.cjs",
24
+ "default": "./dist/index.js"
25
+ }
26
+ },
27
+ "dependencies": {
28
+ "@types/node": "^24.12.2",
29
+ "archiver": "7.0.1",
30
+ "electron": "32.1.2",
31
+ "execa": "9.4.1",
32
+ "pacote": "21.5.0",
33
+ "tar": "6.2.1",
34
+ "type-fest": "4.26.1",
35
+ "yauzl": "2.10.0",
36
+ "zod": "4.3.6"
37
+ },
38
+ "devDependencies": {
39
+ "@types/archiver": "7.0.0",
40
+ "@types/pacote": "11.1.8",
41
+ "@types/tar": "7.0.87",
42
+ "@types/yauzl": "2.10.3",
43
+ "tsdown": "0.21.2",
44
+ "typescript": "5.9.3",
45
+ "@pipelab/constants": "1.0.1-beta.3",
46
+ "@pipelab/shared": "2.0.1-beta.4",
47
+ "@pipelab/tsconfig": "1.0.1-beta.3"
48
+ },
49
+ "scripts": {
50
+ "build": "tsdown",
51
+ "format": "oxfmt .",
52
+ "lint": "oxlint ."
53
+ },
54
+ "module": "./dist/index.js"
55
+ }
@@ -0,0 +1,137 @@
1
+ import { mkdir, createReadStream, createWriteStream } from "node:fs";
2
+ import { mkdir as mkdirP } from "node:fs/promises";
3
+ import { join, dirname } from "node:path";
4
+ import zlib from "zlib";
5
+ import tar from "tar";
6
+ import yauzl from "yauzl";
7
+ import archiver from "archiver";
8
+
9
+ /**
10
+ * Extracts a .tar.gz archive.
11
+ * @param archivePath The full path to the .tar.gz file.
12
+ * @param destinationDir The directory to extract contents into.
13
+ * @returns A Promise that resolves when extraction is complete.
14
+ */
15
+ export async function extractTarGz(archivePath: string, destinationDir: string): Promise<void> {
16
+ console.log(`Extracting ${archivePath} to ${destinationDir}...`);
17
+
18
+ // Ensure the destination directory exists
19
+ await mkdirP(destinationDir, { recursive: true });
20
+
21
+ return new Promise((resolve, reject) => {
22
+ const readStream = createReadStream(archivePath);
23
+ const gunzipStream = zlib.createGunzip();
24
+ const extractStream = tar.extract({ cwd: destinationDir });
25
+
26
+ readStream.on("error", reject);
27
+ gunzipStream.on("error", reject);
28
+ extractStream.on("error", reject);
29
+
30
+ extractStream.on("close", () => {
31
+ console.log("Extraction finished.");
32
+ resolve();
33
+ });
34
+
35
+ readStream.pipe(gunzipStream).pipe(extractStream);
36
+ });
37
+ }
38
+
39
+ /**
40
+ * Extracts a .zip archive.
41
+ * @param archivePath The full path to the .zip file.
42
+ * @param destinationDir The directory to extract contents into.
43
+ * @returns A Promise that resolves when extraction is complete.
44
+ */
45
+ export async function extractZip(archivePath: string, destinationDir: string): Promise<void> {
46
+ console.log(`Extracting ${archivePath} to ${destinationDir}...`);
47
+
48
+ // Ensure the destination directory exists
49
+ await mkdirP(destinationDir, { recursive: true });
50
+
51
+ return new Promise((resolve, reject) => {
52
+ yauzl.open(archivePath, { lazyEntries: true }, (err, zipfile) => {
53
+ if (err || !zipfile) {
54
+ return reject(err || new Error("Could not open zip file"));
55
+ }
56
+
57
+ zipfile.on("error", reject);
58
+
59
+ zipfile.readEntry(); // Start reading entries
60
+
61
+ zipfile.on("entry", (entry) => {
62
+ const entryPath = join(destinationDir, entry.fileName);
63
+
64
+ if (/\/$/.test(entry.fileName)) {
65
+ // It's a directory
66
+ mkdirP(entryPath, { recursive: true })
67
+ .then(() => zipfile.readEntry())
68
+ .catch(reject);
69
+ } else {
70
+ // It's a file
71
+ // Ensure parent directory exists (just in case)
72
+ mkdirP(dirname(entryPath), { recursive: true })
73
+ .then(() => {
74
+ zipfile.openReadStream(entry, (err, readStream) => {
75
+ if (err || !readStream) {
76
+ return reject(err || new Error("Could not open read stream"));
77
+ }
78
+
79
+ readStream.on("error", reject);
80
+ const writeStream = createWriteStream(entryPath);
81
+ writeStream.on("error", reject);
82
+ writeStream.on("close", () => {
83
+ zipfile.readEntry();
84
+ });
85
+ readStream.pipe(writeStream);
86
+ });
87
+ })
88
+ .catch(reject);
89
+ }
90
+ });
91
+
92
+ zipfile.on("end", () => {
93
+ console.log("Zip extraction finished.");
94
+ resolve();
95
+ });
96
+ });
97
+ });
98
+ }
99
+
100
+ export const zipFolder = async (from: string, to: string, log: typeof console.log) => {
101
+ const output = createWriteStream(to);
102
+
103
+ const archive = archiver("zip", {
104
+ zlib: { level: 9 }, // Sets the compression level.
105
+ });
106
+
107
+ // eslint-disable-next-line no-async-promise-executor
108
+ return new Promise<string>(async (resolve, reject) => {
109
+ // listen for all archive data to be written
110
+ // 'close' event is fired only when a file descriptor is involved
111
+ output.on("close", function () {
112
+ log(archive.pointer() + " total bytes");
113
+ log("archiver has been finalized and the output file descriptor has closed.");
114
+ resolve(to);
115
+ });
116
+
117
+ // This event is fired when the data source is drained no matter what was the data source.
118
+ // It is not part of this library but part of NodeJS.
119
+ output.on("end", function () {
120
+ log("Data has been drained");
121
+ });
122
+
123
+ // good practice to catch this error and expose it to the user
124
+ archive.on("error", function (err) {
125
+ reject(err);
126
+ });
127
+
128
+ // pipe archive data to the file
129
+ archive.pipe(output);
130
+
131
+ archive.directory(from, false);
132
+
133
+ // finalize the archive (ie we are done appending files but streams have to finish yet)
134
+ // 'close', 'end' or 'finish' may be fired right after calling this method so register to them beforehand
135
+ await archive.finalize();
136
+ });
137
+ };
@@ -0,0 +1,8 @@
1
+ export type Plugin = {
2
+ nodes: Record<string, any>;
3
+ runtime: () => Promise<void>;
4
+ };
5
+
6
+ export const createPlugin = (plugin: Plugin) => {
7
+ return plugin;
8
+ };
@@ -0,0 +1,11 @@
1
+ export class ExternalCommandError extends Error {
2
+ code: number;
3
+
4
+ constructor(message: string, code: number) {
5
+ super(message);
6
+
7
+ this.code = code;
8
+
9
+ Object.setPrototypeOf(this, ExternalCommandError.prototype);
10
+ }
11
+ }
@@ -0,0 +1,31 @@
1
+ import { access, mkdir, realpath, writeFile, mkdtemp } from "node:fs/promises";
2
+ import { dirname, join } from "node:path";
3
+ import { tmpdir } from "node:os";
4
+ import { readdirSync, copyFileSync, statSync } from "node:fs";
5
+
6
+ export const ensure = async (filesPath: string, defaultContent = "{}") => {
7
+ // create parent folder
8
+ await mkdir(dirname(filesPath), {
9
+ recursive: true,
10
+ });
11
+
12
+ // ensure file exist
13
+ try {
14
+ await access(filesPath);
15
+ } catch {
16
+ // File doesn't exist, create it
17
+ await writeFile(filesPath, defaultContent); // json
18
+ }
19
+ };
20
+
21
+ export const generateTempFolder = async (base?: string) => {
22
+ const targetBase = base || tmpdir();
23
+ await mkdir(targetBase, {
24
+ recursive: true,
25
+ });
26
+ const realPath = await realpath(targetBase);
27
+ console.log("join", join(realPath, "pipelab-"));
28
+ const tempFolder = await mkdtemp(join(realPath, "pipelab-"));
29
+
30
+ return tempFolder;
31
+ };
package/src/index.ts ADDED
@@ -0,0 +1,10 @@
1
+ export * from "./pipelab";
2
+ export * from "./create-plugin";
3
+ export * from "./utils";
4
+ export * from "./fs-utils";
5
+ export * from "./archive-utils";
6
+ export * from "./custom-errors";
7
+ export * from "./node-utils";
8
+
9
+ export { z as schema } from "zod";
10
+ export type NoData = { [index in string]?: unknown };
@@ -0,0 +1,52 @@
1
+ import { access } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+
4
+ export type OutputRuntimes = "construct" | "godot";
5
+
6
+ const fileExists = async (path: string): Promise<boolean> => {
7
+ try {
8
+ await access(path);
9
+ return true;
10
+ } catch {
11
+ return false;
12
+ }
13
+ };
14
+
15
+ /**
16
+ * Detects the runtime of an app folder by checking for specific files.
17
+ * This function depends on Node.js APIs and should only be used in Node environments.
18
+ */
19
+ export const detectRuntime = async (
20
+ appFolder: string | undefined,
21
+ ): Promise<OutputRuntimes | undefined> => {
22
+ if (!appFolder) {
23
+ return undefined;
24
+ }
25
+
26
+ const indexExist = await fileExists(join(appFolder, "index.html"));
27
+ const swExist = await fileExists(join(appFolder, "sw.js"));
28
+ const offlineJSON = await fileExists(join(appFolder, "offline.json"));
29
+ const dataJSON = await fileExists(join(appFolder, "data.json"));
30
+ const scriptsFolder = await fileExists(join(appFolder, "scripts"));
31
+ const workermainJs = await fileExists(join(appFolder, "workermain.js"));
32
+
33
+ if (!indexExist) {
34
+ throw new Error("The input folder does not contain an index.html file");
35
+ }
36
+
37
+ let detectedRuntime: OutputRuntimes | undefined = undefined;
38
+
39
+ if (swExist || dataJSON || workermainJs || scriptsFolder) {
40
+ detectedRuntime = "construct";
41
+ }
42
+
43
+ console.log("Detected runtime", detectedRuntime);
44
+
45
+ if (detectedRuntime === "construct" && offlineJSON && swExist) {
46
+ throw new Error(
47
+ "Construct runtime detected, please disable offline capabilties when using HTML5 export. Offline is already supported by default.",
48
+ );
49
+ }
50
+
51
+ return detectedRuntime;
52
+ };
package/src/pipelab.ts ADDED
@@ -0,0 +1,103 @@
1
+ import type { BrowserWindow } from "electron";
2
+ import type {
3
+ Action,
4
+ Condition,
5
+ Loop,
6
+ Expression,
7
+ Event,
8
+ SetOutputActionFn,
9
+ SetOutputLoopFn,
10
+ SetOutputExpressionFn,
11
+ ExtractInputsFromAction,
12
+ ExtractInputsFromCondition,
13
+ ExtractInputsFromLoop,
14
+ ExtractInputsFromEvent,
15
+ ExtractInputsFromExpression,
16
+ } from "@pipelab/shared";
17
+
18
+ export * from "@pipelab/shared";
19
+
20
+ export type RunnerCallbackFnArgument = {
21
+ done: () => void;
22
+ id: string;
23
+ log: (...args: Parameters<(typeof console)["log"]>) => void;
24
+ };
25
+
26
+ export type ActionRunnerData<ACTION extends Action> = {
27
+ log: typeof console.log;
28
+ setOutput: SetOutputActionFn<ACTION>;
29
+ inputs: ExtractInputsFromAction<ACTION>;
30
+ setMeta: (callback: (data: ACTION["meta"]) => ACTION["meta"]) => void;
31
+ meta: ACTION["meta"];
32
+ cwd: string;
33
+ paths: {
34
+ assets: string;
35
+ cache: string;
36
+ pnpm: string;
37
+ node: string;
38
+ userData: string;
39
+ modules: string;
40
+ thirdparty: string;
41
+ };
42
+ api: {
43
+ fetchAsset: (packageName: string, version?: string) => Promise<string>;
44
+ [key: string]: any;
45
+ };
46
+ browserWindow: BrowserWindow;
47
+ abortSignal: AbortSignal;
48
+ };
49
+
50
+ export type ActionRunner<ACTION extends Action> = (data: ActionRunnerData<ACTION>) => Promise<void>;
51
+ export const createActionRunner = <ACTION extends Action>(runner: ActionRunner<ACTION>) => runner;
52
+
53
+ // ---
54
+
55
+ export type ConditionRunner<CONDITION extends Condition> = (data: {
56
+ log: typeof console.log;
57
+ inputs: ExtractInputsFromCondition<CONDITION>;
58
+ setMeta: (callback: (data: CONDITION["meta"]) => CONDITION["meta"]) => void;
59
+ meta: CONDITION["meta"];
60
+ cwd: string;
61
+ }) => Promise<boolean>;
62
+ export const createConditionRunner = <CONDITION extends Condition>(
63
+ runner: ConditionRunner<CONDITION>,
64
+ ) => runner;
65
+
66
+ // ---
67
+
68
+ export type LoopRunner<LOOP extends Loop> = (data: {
69
+ log: typeof console.log;
70
+ setOutput: SetOutputLoopFn<LOOP>;
71
+ inputs: ExtractInputsFromLoop<LOOP>;
72
+ setMeta: (callback: (data: LOOP["meta"]) => LOOP["meta"]) => void;
73
+ meta: LOOP["meta"];
74
+ cwd: string;
75
+ }) => Promise<"step" | "exit">;
76
+ export const createLoopRunner = <LOOP extends Loop>(runner: LoopRunner<LOOP>) => runner;
77
+
78
+ export type ExpressionRunner<EXPRESSION extends Expression> = (data: {
79
+ log: typeof console.log;
80
+ setOutput: SetOutputExpressionFn<EXPRESSION>;
81
+ inputs: ExtractInputsFromExpression<EXPRESSION>;
82
+ setMeta: (callback: (data: EXPRESSION["meta"]) => EXPRESSION["meta"]) => void;
83
+ meta: EXPRESSION["meta"];
84
+ cwd: string;
85
+ }) => Promise<string>;
86
+ export const createExpressionRunner = <EXPRESSION extends Expression>(
87
+ runner: ExpressionRunner<EXPRESSION>,
88
+ ) => runner;
89
+
90
+ export type EventRunner<EVENT extends Event> = (data: {
91
+ log: typeof console.log;
92
+ inputs: ExtractInputsFromEvent<EVENT>;
93
+ setMeta: (callback: (data: EVENT["meta"]) => EVENT["meta"]) => void;
94
+ meta: EVENT["meta"];
95
+ cwd: string;
96
+ }) => Promise<void>;
97
+ export const createEventRunner = <EVENT extends Event>(runner: EventRunner<EVENT>) => runner;
98
+
99
+ export type Runner = ActionRunner<any> | LoopRunner<any> | EventRunner<any> | ConditionRunner<any>;
100
+
101
+ export const sleep = (duration: number) => {
102
+ return new Promise((resolve) => setTimeout(resolve, duration));
103
+ };
package/src/utils.ts ADDED
@@ -0,0 +1,251 @@
1
+ import { execa, Options, Subprocess } from "execa";
2
+ import { ExternalCommandError } from "./custom-errors.js";
3
+ import { join } from "node:path";
4
+ import { access, mkdir, writeFile, rm } from "node:fs/promises";
5
+ import { createWriteStream } from "node:fs";
6
+ import { pipeline } from "node:stream/promises";
7
+ import pacote from "pacote";
8
+
9
+ export const fileExists = async (path: string): Promise<boolean> => {
10
+ try {
11
+ await access(path);
12
+ return true;
13
+ } catch {
14
+ return false;
15
+ }
16
+ };
17
+
18
+ export const runWithLiveLogs = async (
19
+ command: string,
20
+ args: string[],
21
+ execaOptions: Options,
22
+ log: typeof console.log,
23
+ hooks?: {
24
+ onStdout?: (data: string, subprocess: Subprocess) => void;
25
+ onStderr?: (data: string, subprocess: Subprocess) => void;
26
+ onExit?: (code: number) => void;
27
+ },
28
+ ): Promise<void> => {
29
+ return new Promise((resolve, reject) => {
30
+ console.log("command: ", command, args.join(" "));
31
+
32
+ const subprocess = execa(command, args, {
33
+ ...execaOptions,
34
+ stdout: "pipe",
35
+ stderr: "pipe",
36
+ stdin: "pipe",
37
+ });
38
+
39
+ subprocess.stdout.on("data", (data: Buffer) => {
40
+ hooks?.onStdout?.(data.toString(), subprocess);
41
+ });
42
+
43
+ subprocess.stderr?.on("data", (data: Buffer) => {
44
+ hooks?.onStderr?.(data.toString(), subprocess);
45
+ });
46
+
47
+ subprocess.on("error", (error: Error) => {
48
+ console.log("error", error);
49
+ return reject(error);
50
+ });
51
+
52
+ subprocess.on("close", (code: number) => {
53
+ console.log("close", code);
54
+ hooks?.onExit?.(code);
55
+
56
+ if (code === 0) {
57
+ return resolve();
58
+ } else {
59
+ return reject(new Error(`Command exited with non-zero code: ${code}`));
60
+ }
61
+ });
62
+
63
+ subprocess.on("disconnect", () => {
64
+ console.log("disconnect");
65
+ hooks?.onExit?.(0);
66
+ return resolve();
67
+ });
68
+
69
+ subprocess.on("exit", (code: number) => {
70
+ console.log("exit", code);
71
+ hooks?.onExit?.(code);
72
+
73
+ if (code === 0) {
74
+ return resolve();
75
+ } else {
76
+ return reject(new Error(`Command exited with non-zero code: ${code}`));
77
+ }
78
+ });
79
+ });
80
+ };
81
+
82
+ export const runWithLiveLogsPTY = async (
83
+ command: string,
84
+ args: string[],
85
+ execaOptions: Options,
86
+ log: typeof console.log,
87
+ hooks?: {
88
+ onStdout?: (data: string, subprocess: Subprocess) => void;
89
+ onStderr?: (data: string, subprocess: Subprocess) => void;
90
+ onExit?: (code: number) => void;
91
+ onCreated?: (subprocess: Subprocess) => void;
92
+ },
93
+ abortSignal?: AbortSignal,
94
+ ): Promise<void> => {
95
+ return new Promise((resolve, reject) => {
96
+ console.log("command (execa-pty-fallback): ", command, args.join(" "));
97
+
98
+ const subprocess = execa(command, args, {
99
+ ...execaOptions,
100
+ stdout: "pipe",
101
+ stderr: "pipe",
102
+ stdin: "pipe",
103
+ env: {
104
+ ...process.env,
105
+ ...execaOptions.env,
106
+ TERM: "xterm-256color",
107
+ FORCE_STDERR_LOGGING: "1",
108
+ },
109
+ cancelSignal: abortSignal,
110
+ });
111
+
112
+ hooks?.onCreated?.(subprocess);
113
+
114
+ subprocess.stdout?.on("data", (data: Buffer) => {
115
+ hooks?.onStdout?.(data.toString(), subprocess);
116
+ });
117
+
118
+ subprocess.stderr?.on("data", (data: Buffer) => {
119
+ hooks?.onStderr?.(data.toString(), subprocess);
120
+ });
121
+
122
+ subprocess.on("error", (error: Error) => {
123
+ console.log("error", error);
124
+ return reject(error);
125
+ });
126
+
127
+ subprocess.on("exit", (code: number) => {
128
+ console.log("exit", code);
129
+ hooks?.onExit?.(code || 0);
130
+
131
+ if (code === 0) {
132
+ return resolve();
133
+ } else {
134
+ return reject(
135
+ new ExternalCommandError(`Command exited with non-zero code: ${code}`, code || 1),
136
+ );
137
+ }
138
+ });
139
+ });
140
+ };
141
+
142
+ export interface Hooks {
143
+ onProgress?: (data: { progress: number; downloadedSize: number }) => void;
144
+ }
145
+
146
+ /**
147
+ * Downloads a file from a given URL to a specified local path with progress tracking.
148
+ *
149
+ * @param url - The URL of the file to download.
150
+ * @param localPath - The local file path to save the downloaded file.
151
+ * @returns A promise that resolves when the file is downloaded.
152
+ */
153
+ export const downloadFile = async (
154
+ url: string,
155
+ localPath: string,
156
+ hooks?: Hooks,
157
+ abortSignal?: AbortSignal,
158
+ ): Promise<void> => {
159
+ // Fetch the resource
160
+ const response = await fetch(url, {
161
+ signal: abortSignal,
162
+ });
163
+
164
+ // Check if the fetch was successful
165
+ if (!response.ok) {
166
+ throw new Error(`Failed to fetch file: ${response.statusText}`);
167
+ }
168
+
169
+ // Get the total size of the file
170
+ const contentLength = response.headers.get("content-length");
171
+ if (!contentLength) {
172
+ throw new Error("Content-Length header is missing");
173
+ }
174
+ const totalSize = parseInt(contentLength, 10);
175
+
176
+ // Track progress
177
+ let downloadedSize = 0;
178
+ const fileStream = createWriteStream(localPath);
179
+
180
+ // Create a readable stream to monitor progress
181
+ const progressStream = new TransformStream({
182
+ transform(chunk, controller) {
183
+ downloadedSize += chunk.length;
184
+ const progress = (downloadedSize / totalSize) * 100;
185
+ if (hooks?.onProgress) {
186
+ hooks.onProgress({
187
+ progress,
188
+ downloadedSize,
189
+ });
190
+ }
191
+ controller.enqueue(chunk);
192
+ },
193
+ });
194
+
195
+ // Pipe the response through the progress tracker and into the file
196
+ const readable = response.body?.pipeThrough(progressStream);
197
+ if (!readable) {
198
+ throw new Error("Failed to create a readable stream");
199
+ }
200
+
201
+ await pipeline(readable, fileStream);
202
+ };
203
+
204
+ /**
205
+ * Installs an NPM package from the npm registry as a tarball if not already present.
206
+ * @param thirdpartyDir The directory where third-party tools are stored.
207
+ * @param name The name of the package (e.g., 'pnpm' or '@poki/cli').
208
+ * @param version The version of the package.
209
+ * @param options (Optional) configuration for dependency installation.
210
+ * @returns A Promise that resolves to the path of the extracted package (the 'package' subfolder).
211
+ */
212
+ export const ensureNPMPackage = async (
213
+ thirdpartyDir: string,
214
+ name: string,
215
+ version: string,
216
+ options?: {
217
+ nodePath?: string;
218
+ pnpmPath?: string;
219
+ installDeps?: boolean;
220
+ },
221
+ ) => {
222
+ const packageDir = join(thirdpartyDir, name, version);
223
+ const finalPath = join(packageDir, "package");
224
+
225
+ // 1. Check if already installed
226
+ if (await fileExists(finalPath)) {
227
+ return finalPath;
228
+ }
229
+ console.log(`NPM package ${name}@${version} not found at ${finalPath}, installing...`);
230
+
231
+ // 2. Extract using pacote
232
+ console.log(`Extracting ${name}@${version} to ${finalPath}...`);
233
+ await mkdir(packageDir, { recursive: true });
234
+ await pacote.extract(`${name}@${version}`, finalPath);
235
+
236
+ // 3. Install dependencies if requested
237
+ if (options?.installDeps && options.pnpmPath) {
238
+ console.log(
239
+ `Installing dependencies for ${name}@${version} using pnpm at ${options.pnpmPath}...`,
240
+ );
241
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
242
+ // We use the pnpm .cjs bundle directly with node
243
+ const node = options.nodePath || process.execPath;
244
+ await execa(node, [options.pnpmPath, "install", "--prod"], {
245
+ cwd: finalPath,
246
+ stdio: "inherit",
247
+ });
248
+ }
249
+
250
+ return finalPath;
251
+ };
package/tsconfig.json ADDED
@@ -0,0 +1,9 @@
1
+ {
2
+ "extends": "@pipelab/tsconfig/vue.json",
3
+ "compilerOptions": {
4
+ "outDir": "dist",
5
+ "rootDir": "src",
6
+ "types": ["node"]
7
+ },
8
+ "include": ["src"]
9
+ }
@@ -0,0 +1,8 @@
1
+ import { defineConfig } from "tsdown";
2
+
3
+ export default defineConfig({
4
+ entry: ["src/index.ts"],
5
+ format: ["esm", "cjs"],
6
+ dts: true,
7
+ clean: true,
8
+ });