@drej/workflow 1.1.0

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.
@@ -0,0 +1,229 @@
1
+ import { Drej, ExecCodeOptions, ExecOptions, SandboxOptions } from "drej";
2
+
3
+ //#region src/sandbox-builder.d.ts
4
+ /** An operation queued on a SandboxBuilder and executed later. */
5
+ type SandboxOp = {
6
+ kind: "exec";
7
+ cmd: string;
8
+ opts: ExecOptions;
9
+ } | {
10
+ kind: "execCode";
11
+ code: string;
12
+ opts: ExecCodeOptions;
13
+ } | {
14
+ kind: "writeFile";
15
+ path: string;
16
+ content: string;
17
+ } | {
18
+ kind: "readFile";
19
+ path: string;
20
+ as: string;
21
+ } | {
22
+ kind: "deleteFile";
23
+ path: string;
24
+ } | {
25
+ kind: "moveFile";
26
+ from: string;
27
+ to: string;
28
+ } | {
29
+ kind: "checkpoint";
30
+ name?: string;
31
+ } | {
32
+ kind: "retry";
33
+ maxAttempts: number;
34
+ fn: (sb: SandboxBuilder) => void;
35
+ opts: RetryOptions;
36
+ } | {
37
+ kind: "when";
38
+ pred: WhenPredicate;
39
+ then: (sb: SandboxBuilder) => void;
40
+ else?: (sb: SandboxBuilder) => void;
41
+ } | {
42
+ kind: "forEach";
43
+ items: unknown[];
44
+ fn: (sb: SandboxBuilder, item: unknown, index: number) => void;
45
+ opts: ForEachOptions;
46
+ };
47
+ interface RetryOptions {
48
+ backoff?: "fixed" | "exponential";
49
+ delayMs?: number;
50
+ }
51
+ type WhenPredicate = (ctx: {
52
+ stdout: string;
53
+ exitCode: number;
54
+ vars: Record<string, unknown>;
55
+ }) => boolean;
56
+ interface ForEachOptions {
57
+ concurrency?: number;
58
+ }
59
+ /**
60
+ * Synchronous lazy builder for sandbox operations.
61
+ *
62
+ * All methods queue an operation and return `this` — no awaits, no async.
63
+ * The queue is flushed when `WorkflowBuilder.pipe()` or `.result()` is called.
64
+ * This gives a single `await` at the end regardless of workflow complexity.
65
+ *
66
+ * @example
67
+ * ```ts
68
+ * await workflow(client)
69
+ * .sandbox({ image: "node:22", resources: { cpu: "500m", memory: "256Mi" } }, (sb) => {
70
+ * sb.exec("npm ci")
71
+ * sb.checkpoint()
72
+ * sb.retry(3, (sb) => sb.exec("npm test"), { backoff: "exponential" })
73
+ * })
74
+ * .pipe(process.stdout);
75
+ * ```
76
+ */
77
+ declare class SandboxBuilder {
78
+ readonly _ops: SandboxOp[];
79
+ /** Queue a shell command. */
80
+ exec(cmd: string, opts?: ExecOptions): this;
81
+ /** Queue a code execution (Python/JS/TS via execd code interpreter). */
82
+ execCode(code: string, opts?: ExecCodeOptions): this;
83
+ /** Queue a file write into the sandbox. */
84
+ writeFile(path: string, content: string): this;
85
+ /** Queue a file read from the sandbox, stored in `vars[as]`. */
86
+ readFile(path: string, as: string): this;
87
+ /** Queue a file deletion inside the sandbox. */
88
+ deleteFile(path: string): this;
89
+ /** Queue a file move/rename inside the sandbox. */
90
+ moveFile(from: string, to: string): this;
91
+ /** Queue a checkpoint (snapshot). */
92
+ checkpoint(name?: string): this;
93
+ /**
94
+ * Retry an inner builder callback up to `maxAttempts` times on failure.
95
+ *
96
+ * @example
97
+ * ```ts
98
+ * sb.retry(3, (sb) => sb.exec("npm test"), { backoff: "exponential" })
99
+ * ```
100
+ */
101
+ retry(maxAttempts: number, fn: (sb: SandboxBuilder) => void, opts?: RetryOptions): this;
102
+ /**
103
+ * Conditionally execute one of two builder callbacks based on a predicate.
104
+ *
105
+ * The predicate receives `{ stdout, exitCode, vars }` from the last exec.
106
+ *
107
+ * @example
108
+ * ```ts
109
+ * sb.when(
110
+ * (ctx) => ctx.exitCode === 0,
111
+ * (sb) => sb.exec("echo success"),
112
+ * (sb) => sb.exec("echo failure"),
113
+ * )
114
+ * ```
115
+ */
116
+ when(pred: WhenPredicate, then: (sb: SandboxBuilder) => void, otherwise?: (sb: SandboxBuilder) => void): this;
117
+ /**
118
+ * Execute a builder callback for each item in `items`, optionally in parallel.
119
+ *
120
+ * @example
121
+ * ```ts
122
+ * sb.forEach(["a", "b", "c"], (sb, item) => sb.exec(`echo ${item}`))
123
+ * ```
124
+ */
125
+ forEach(items: unknown[], fn: (sb: SandboxBuilder, item: unknown, index: number) => void, opts?: ForEachOptions): this;
126
+ }
127
+ //#endregion
128
+ //#region src/workflow-builder.d.ts
129
+ interface WorkflowResult {
130
+ /** Concatenated stdout from all sandboxes in the workflow. */
131
+ stdout: string;
132
+ /** Named values captured by `sb.readFile(path, as)`. */
133
+ vars: Record<string, unknown>;
134
+ }
135
+ /** A single step in a `.sequence()` call. */
136
+ interface SequenceStep {
137
+ image: SandboxOptions["image"];
138
+ resources: SandboxOptions["resources"];
139
+ env?: SandboxOptions["env"];
140
+ timeout?: SandboxOptions["timeout"];
141
+ name?: string;
142
+ run: (sb: SandboxBuilder, prevResult?: WorkflowResult) => void;
143
+ }
144
+ /**
145
+ * Lazy workflow builder. Collects stages and executes them all when `.pipe()` or
146
+ * `.result()` is awaited. Sandboxes are created via the `Drej` client internally
147
+ * — the user never manages sandbox lifecycle when using this layer.
148
+ *
149
+ * @example
150
+ * ```ts
151
+ * import { workflow } from "@drej/workflow";
152
+ *
153
+ * await workflow(client)
154
+ * .sandbox({ image: "node:22", resources: { cpu: "500m", memory: "256Mi" } }, (sb) => {
155
+ * sb.exec("npm ci")
156
+ * sb.checkpoint()
157
+ * sb.retry(3, (sb) => sb.exec("npm test"), { backoff: "exponential" })
158
+ * })
159
+ * .pipe(process.stdout);
160
+ * ```
161
+ */
162
+ declare class WorkflowBuilder {
163
+ private readonly _client;
164
+ private readonly _stages;
165
+ constructor(client: Drej);
166
+ /**
167
+ * Add a sandbox stage. The `fn` callback receives a `SandboxBuilder` to
168
+ * queue operations synchronously. Sandboxes across multiple `.sandbox()` calls
169
+ * run sequentially, top-to-bottom.
170
+ */
171
+ sandbox(opts: SandboxOptions, fn: (sb: SandboxBuilder) => void): this;
172
+ /**
173
+ * Run the same operation across multiple sandbox configurations in parallel.
174
+ * All sandboxes receive the same `fn` callback.
175
+ *
176
+ * @example
177
+ * ```ts
178
+ * await workflow(client)
179
+ * .parallel(
180
+ * [{ image: "node:20" }, { image: "node:22" }, { image: "node:24" }],
181
+ * (sb) => sb.exec("npm test"),
182
+ * )
183
+ * .pipe(process.stdout);
184
+ * ```
185
+ */
186
+ parallel(configs: SandboxOptions[], fn: (sb: SandboxBuilder) => void): this;
187
+ /**
188
+ * Run sandboxes in sequence, passing the previous stage's result to the next.
189
+ *
190
+ * @example
191
+ * ```ts
192
+ * await workflow(client)
193
+ * .sequence([
194
+ * { image: "node:22", run: (sb) => sb.exec("npm run build") },
195
+ * { image: "ubuntu:22.04", run: (sb, prev) => sb.exec("./deploy.sh") },
196
+ * ])
197
+ * .pipe(process.stdout);
198
+ * ```
199
+ */
200
+ sequence(steps: SequenceStep[]): this;
201
+ /** Execute the workflow and pipe stdout to a writable. */
202
+ pipe(writable: {
203
+ write(chunk: string): unknown;
204
+ }): Promise<void>;
205
+ /** Execute the workflow and return the full result. */
206
+ result(): Promise<WorkflowResult>;
207
+ private _execute;
208
+ private _runSandbox;
209
+ private _runParallel;
210
+ private _runSequence;
211
+ }
212
+ /**
213
+ * Create a workflow builder attached to a `Drej` client.
214
+ *
215
+ * @example
216
+ * ```ts
217
+ * import { workflow } from "@drej/workflow";
218
+ *
219
+ * await workflow(client)
220
+ * .sandbox({ image: "node:22", resources: { cpu: "500m", memory: "256Mi" } }, (sb) => {
221
+ * sb.exec("npm ci")
222
+ * sb.exec("npm test")
223
+ * })
224
+ * .pipe(process.stdout);
225
+ * ```
226
+ */
227
+ declare function workflow(client: Drej): WorkflowBuilder;
228
+ //#endregion
229
+ export { type ForEachOptions, type RetryOptions, SandboxBuilder, type SandboxOp, type SequenceStep, type WhenPredicate, WorkflowBuilder, type WorkflowResult, workflow };
package/dist/index.mjs ADDED
@@ -0,0 +1,423 @@
1
+ //#region src/sandbox-builder.ts
2
+ /**
3
+ * Synchronous lazy builder for sandbox operations.
4
+ *
5
+ * All methods queue an operation and return `this` — no awaits, no async.
6
+ * The queue is flushed when `WorkflowBuilder.pipe()` or `.result()` is called.
7
+ * This gives a single `await` at the end regardless of workflow complexity.
8
+ *
9
+ * @example
10
+ * ```ts
11
+ * await workflow(client)
12
+ * .sandbox({ image: "node:22", resources: { cpu: "500m", memory: "256Mi" } }, (sb) => {
13
+ * sb.exec("npm ci")
14
+ * sb.checkpoint()
15
+ * sb.retry(3, (sb) => sb.exec("npm test"), { backoff: "exponential" })
16
+ * })
17
+ * .pipe(process.stdout);
18
+ * ```
19
+ */
20
+ var SandboxBuilder = class {
21
+ _ops = [];
22
+ /** Queue a shell command. */
23
+ exec(cmd, opts = {}) {
24
+ this._ops.push({
25
+ kind: "exec",
26
+ cmd,
27
+ opts
28
+ });
29
+ return this;
30
+ }
31
+ /** Queue a code execution (Python/JS/TS via execd code interpreter). */
32
+ execCode(code, opts = {}) {
33
+ this._ops.push({
34
+ kind: "execCode",
35
+ code,
36
+ opts
37
+ });
38
+ return this;
39
+ }
40
+ /** Queue a file write into the sandbox. */
41
+ writeFile(path, content) {
42
+ this._ops.push({
43
+ kind: "writeFile",
44
+ path,
45
+ content
46
+ });
47
+ return this;
48
+ }
49
+ /** Queue a file read from the sandbox, stored in `vars[as]`. */
50
+ readFile(path, as) {
51
+ this._ops.push({
52
+ kind: "readFile",
53
+ path,
54
+ as
55
+ });
56
+ return this;
57
+ }
58
+ /** Queue a file deletion inside the sandbox. */
59
+ deleteFile(path) {
60
+ this._ops.push({
61
+ kind: "deleteFile",
62
+ path
63
+ });
64
+ return this;
65
+ }
66
+ /** Queue a file move/rename inside the sandbox. */
67
+ moveFile(from, to) {
68
+ this._ops.push({
69
+ kind: "moveFile",
70
+ from,
71
+ to
72
+ });
73
+ return this;
74
+ }
75
+ /** Queue a checkpoint (snapshot). */
76
+ checkpoint(name) {
77
+ this._ops.push({
78
+ kind: "checkpoint",
79
+ name
80
+ });
81
+ return this;
82
+ }
83
+ /**
84
+ * Retry an inner builder callback up to `maxAttempts` times on failure.
85
+ *
86
+ * @example
87
+ * ```ts
88
+ * sb.retry(3, (sb) => sb.exec("npm test"), { backoff: "exponential" })
89
+ * ```
90
+ */
91
+ retry(maxAttempts, fn, opts = {}) {
92
+ this._ops.push({
93
+ kind: "retry",
94
+ maxAttempts,
95
+ fn,
96
+ opts
97
+ });
98
+ return this;
99
+ }
100
+ /**
101
+ * Conditionally execute one of two builder callbacks based on a predicate.
102
+ *
103
+ * The predicate receives `{ stdout, exitCode, vars }` from the last exec.
104
+ *
105
+ * @example
106
+ * ```ts
107
+ * sb.when(
108
+ * (ctx) => ctx.exitCode === 0,
109
+ * (sb) => sb.exec("echo success"),
110
+ * (sb) => sb.exec("echo failure"),
111
+ * )
112
+ * ```
113
+ */
114
+ when(pred, then, otherwise) {
115
+ this._ops.push({
116
+ kind: "when",
117
+ pred,
118
+ then,
119
+ else: otherwise
120
+ });
121
+ return this;
122
+ }
123
+ /**
124
+ * Execute a builder callback for each item in `items`, optionally in parallel.
125
+ *
126
+ * @example
127
+ * ```ts
128
+ * sb.forEach(["a", "b", "c"], (sb, item) => sb.exec(`echo ${item}`))
129
+ * ```
130
+ */
131
+ forEach(items, fn, opts = {}) {
132
+ this._ops.push({
133
+ kind: "forEach",
134
+ items,
135
+ fn,
136
+ opts
137
+ });
138
+ return this;
139
+ }
140
+ };
141
+ /** Flush a SandboxBuilder's op queue against a live Sandbox. */
142
+ async function flushOps(sandbox, ops, ctx) {
143
+ for (const op of ops) switch (op.kind) {
144
+ case "exec": {
145
+ const handle = sandbox.exec(op.cmd, op.opts);
146
+ if (ctx.sink) {
147
+ for await (const chunk of handle.stdout()) {
148
+ ctx.stdout += chunk;
149
+ ctx.sink.write(chunk);
150
+ }
151
+ ctx.exitCode = (await handle.result()).exitCode;
152
+ } else {
153
+ const result = await handle;
154
+ ctx.stdout += result.stdout;
155
+ ctx.exitCode = result.exitCode;
156
+ }
157
+ break;
158
+ }
159
+ case "execCode": {
160
+ const handle = sandbox.execCode(op.code, op.opts);
161
+ if (ctx.sink) {
162
+ for await (const chunk of handle.stdout()) {
163
+ ctx.stdout += chunk;
164
+ ctx.sink.write(chunk);
165
+ }
166
+ ctx.exitCode = (await handle.result()).exitCode;
167
+ } else {
168
+ const result = await handle;
169
+ ctx.stdout += result.stdout;
170
+ ctx.exitCode = result.exitCode;
171
+ }
172
+ break;
173
+ }
174
+ case "writeFile":
175
+ await sandbox.writeFile(op.path, op.content);
176
+ break;
177
+ case "readFile": {
178
+ const content = await sandbox.readFile(op.path);
179
+ ctx.vars[op.as] = content;
180
+ break;
181
+ }
182
+ case "deleteFile":
183
+ await sandbox.deleteFile(op.path);
184
+ break;
185
+ case "moveFile":
186
+ await sandbox.moveFile(op.from, op.to);
187
+ break;
188
+ case "checkpoint":
189
+ await sandbox.checkpoint(op.name);
190
+ break;
191
+ case "retry":
192
+ await flushRetry(sandbox, op.fn, op.maxAttempts, op.opts, ctx);
193
+ break;
194
+ case "when": {
195
+ const branch = op.pred(ctx) ? op.then : op["else"];
196
+ if (branch) {
197
+ const inner = new SandboxBuilder();
198
+ branch(inner);
199
+ await flushOps(sandbox, inner._ops, ctx);
200
+ }
201
+ break;
202
+ }
203
+ case "forEach":
204
+ await flushForEach(sandbox, op.items, op.fn, op.opts, ctx);
205
+ break;
206
+ }
207
+ }
208
+ async function flushRetry(sandbox, fn, maxAttempts, opts, ctx) {
209
+ let lastErr;
210
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
211
+ if (attempt > 0) {
212
+ const base = opts.delayMs ?? 1e3;
213
+ const delay = opts.backoff === "exponential" ? base * 2 ** (attempt - 1) : base;
214
+ await new Promise((r) => setTimeout(r, delay));
215
+ }
216
+ try {
217
+ const inner = new SandboxBuilder();
218
+ fn(inner);
219
+ await flushOps(sandbox, inner._ops, ctx);
220
+ return;
221
+ } catch (err) {
222
+ lastErr = err;
223
+ }
224
+ }
225
+ throw lastErr;
226
+ }
227
+ async function flushForEach(sandbox, items, fn, opts, ctx) {
228
+ const concurrency = opts.concurrency ?? 1;
229
+ if (concurrency <= 1) for (let i = 0; i < items.length; i++) {
230
+ const inner = new SandboxBuilder();
231
+ fn(inner, items[i], i);
232
+ await flushOps(sandbox, inner._ops, ctx);
233
+ }
234
+ else {
235
+ let idx = 0;
236
+ async function worker() {
237
+ while (idx < items.length) {
238
+ const i = idx++;
239
+ const inner = new SandboxBuilder();
240
+ fn(inner, items[i], i);
241
+ await flushOps(sandbox, inner._ops, { ...ctx });
242
+ }
243
+ }
244
+ const workers = Array.from({ length: Math.min(concurrency, items.length) }, worker);
245
+ await Promise.all(workers);
246
+ }
247
+ }
248
+ //#endregion
249
+ //#region src/workflow-builder.ts
250
+ /**
251
+ * Lazy workflow builder. Collects stages and executes them all when `.pipe()` or
252
+ * `.result()` is awaited. Sandboxes are created via the `Drej` client internally
253
+ * — the user never manages sandbox lifecycle when using this layer.
254
+ *
255
+ * @example
256
+ * ```ts
257
+ * import { workflow } from "@drej/workflow";
258
+ *
259
+ * await workflow(client)
260
+ * .sandbox({ image: "node:22", resources: { cpu: "500m", memory: "256Mi" } }, (sb) => {
261
+ * sb.exec("npm ci")
262
+ * sb.checkpoint()
263
+ * sb.retry(3, (sb) => sb.exec("npm test"), { backoff: "exponential" })
264
+ * })
265
+ * .pipe(process.stdout);
266
+ * ```
267
+ */
268
+ var WorkflowBuilder = class {
269
+ _client;
270
+ _stages = [];
271
+ constructor(client) {
272
+ this._client = client;
273
+ }
274
+ /**
275
+ * Add a sandbox stage. The `fn` callback receives a `SandboxBuilder` to
276
+ * queue operations synchronously. Sandboxes across multiple `.sandbox()` calls
277
+ * run sequentially, top-to-bottom.
278
+ */
279
+ sandbox(opts, fn) {
280
+ this._stages.push({
281
+ type: "sandbox",
282
+ opts,
283
+ fn
284
+ });
285
+ return this;
286
+ }
287
+ /**
288
+ * Run the same operation across multiple sandbox configurations in parallel.
289
+ * All sandboxes receive the same `fn` callback.
290
+ *
291
+ * @example
292
+ * ```ts
293
+ * await workflow(client)
294
+ * .parallel(
295
+ * [{ image: "node:20" }, { image: "node:22" }, { image: "node:24" }],
296
+ * (sb) => sb.exec("npm test"),
297
+ * )
298
+ * .pipe(process.stdout);
299
+ * ```
300
+ */
301
+ parallel(configs, fn) {
302
+ this._stages.push({
303
+ type: "parallel",
304
+ configs,
305
+ fn
306
+ });
307
+ return this;
308
+ }
309
+ /**
310
+ * Run sandboxes in sequence, passing the previous stage's result to the next.
311
+ *
312
+ * @example
313
+ * ```ts
314
+ * await workflow(client)
315
+ * .sequence([
316
+ * { image: "node:22", run: (sb) => sb.exec("npm run build") },
317
+ * { image: "ubuntu:22.04", run: (sb, prev) => sb.exec("./deploy.sh") },
318
+ * ])
319
+ * .pipe(process.stdout);
320
+ * ```
321
+ */
322
+ sequence(steps) {
323
+ this._stages.push({
324
+ type: "sequence",
325
+ steps
326
+ });
327
+ return this;
328
+ }
329
+ /** Execute the workflow and pipe stdout to a writable. */
330
+ async pipe(writable) {
331
+ await this._execute(writable);
332
+ }
333
+ /** Execute the workflow and return the full result. */
334
+ async result() {
335
+ return this._execute(void 0);
336
+ }
337
+ async _execute(sink) {
338
+ const combined = {
339
+ stdout: "",
340
+ vars: {}
341
+ };
342
+ for (const stage of this._stages) if (stage.type === "sandbox") {
343
+ const result = await this._runSandbox(stage.opts, stage.fn, sink);
344
+ combined.stdout += result.stdout;
345
+ Object.assign(combined.vars, result.vars);
346
+ } else if (stage.type === "parallel") {
347
+ const results = await this._runParallel(stage.configs, stage.fn, sink);
348
+ for (const r of results) {
349
+ combined.stdout += r.stdout;
350
+ Object.assign(combined.vars, r.vars);
351
+ }
352
+ } else if (stage.type === "sequence") {
353
+ const result = await this._runSequence(stage.steps, sink);
354
+ combined.stdout += result.stdout;
355
+ Object.assign(combined.vars, result.vars);
356
+ }
357
+ return combined;
358
+ }
359
+ async _runSandbox(opts, fn, sink) {
360
+ const sb = new SandboxBuilder();
361
+ fn(sb);
362
+ const sandbox = await this._client.sandbox(opts);
363
+ try {
364
+ const ctx = {
365
+ stdout: "",
366
+ exitCode: 0,
367
+ vars: {},
368
+ sink
369
+ };
370
+ await flushOps(sandbox, sb._ops, ctx);
371
+ return {
372
+ stdout: ctx.stdout,
373
+ vars: ctx.vars
374
+ };
375
+ } finally {
376
+ await sandbox.close();
377
+ }
378
+ }
379
+ async _runParallel(configs, fn, sink) {
380
+ return Promise.all(configs.map((opts) => this._runSandbox(opts, fn, sink)));
381
+ }
382
+ async _runSequence(steps, sink) {
383
+ const combined = {
384
+ stdout: "",
385
+ vars: {}
386
+ };
387
+ let prev;
388
+ for (const step of steps) {
389
+ const opts = {
390
+ image: step.image,
391
+ resources: step.resources,
392
+ env: step.env,
393
+ timeout: step.timeout,
394
+ name: step.name
395
+ };
396
+ const result = await this._runSandbox(opts, (sb) => step.run(sb, prev), sink);
397
+ combined.stdout += result.stdout;
398
+ Object.assign(combined.vars, result.vars);
399
+ prev = result;
400
+ }
401
+ return combined;
402
+ }
403
+ };
404
+ /**
405
+ * Create a workflow builder attached to a `Drej` client.
406
+ *
407
+ * @example
408
+ * ```ts
409
+ * import { workflow } from "@drej/workflow";
410
+ *
411
+ * await workflow(client)
412
+ * .sandbox({ image: "node:22", resources: { cpu: "500m", memory: "256Mi" } }, (sb) => {
413
+ * sb.exec("npm ci")
414
+ * sb.exec("npm test")
415
+ * })
416
+ * .pipe(process.stdout);
417
+ * ```
418
+ */
419
+ function workflow(client) {
420
+ return new WorkflowBuilder(client);
421
+ }
422
+ //#endregion
423
+ export { SandboxBuilder, WorkflowBuilder, workflow };
package/package.json ADDED
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "@drej/workflow",
3
+ "version": "1.1.0",
4
+ "files": [
5
+ "dist"
6
+ ],
7
+ "type": "module",
8
+ "main": "./dist/index.mjs",
9
+ "types": "./dist/index.d.mts",
10
+ "exports": {
11
+ ".": {
12
+ "import": "./dist/index.mjs",
13
+ "types": "./dist/index.d.mts"
14
+ }
15
+ },
16
+ "publishConfig": {
17
+ "access": "public"
18
+ },
19
+ "scripts": {
20
+ "build": "tsdown",
21
+ "test": "vitest run",
22
+ "test:watch": "vitest"
23
+ },
24
+ "dependencies": {
25
+ "drej": "workspace:*"
26
+ },
27
+ "devDependencies": {
28
+ "bun-types": "1.3.14",
29
+ "tsdown": "0.22.3",
30
+ "typescript": "6.0.3",
31
+ "vitest": "4.1.9"
32
+ }
33
+ }