chore: replace hand-written xxhashjs.d.ts with @types/xxhashjs
小橘 <xiaoju@shazhou.work>
This commit is contained in:
@@ -0,0 +1,31 @@
|
|||||||
|
import { createRoleModerator, END, type Role } from "@uncaged/workflow";
|
||||||
|
|
||||||
|
type Roles = {
|
||||||
|
greeter: { greeting: string };
|
||||||
|
};
|
||||||
|
|
||||||
|
export const descriptor = {
|
||||||
|
description: "A simple hello world workflow",
|
||||||
|
roles: {
|
||||||
|
greeter: {
|
||||||
|
description: "Generates a greeting",
|
||||||
|
schema: {
|
||||||
|
type: "object",
|
||||||
|
properties: { greeting: { type: "string" } },
|
||||||
|
required: ["greeting"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const greeter: Role<Roles["greeter"]> = async (ctx) => ({
|
||||||
|
content: "Hello, " + ctx.start.content,
|
||||||
|
meta: { greeting: "Hello!" },
|
||||||
|
});
|
||||||
|
|
||||||
|
export default createRoleModerator<Roles>({
|
||||||
|
roles: { greeter },
|
||||||
|
moderator(ctx) {
|
||||||
|
return ctx.steps.length === 0 ? "greeter" : END;
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"name": "@uncaged/workflow-examples",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"dependencies": {
|
||||||
|
"@uncaged/workflow": "workspace:*"
|
||||||
|
}
|
||||||
|
}
|
||||||
+2
-1
@@ -2,7 +2,8 @@
|
|||||||
"name": "@uncaged/workflow-monorepo",
|
"name": "@uncaged/workflow-monorepo",
|
||||||
"private": true,
|
"private": true,
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
"packages/*"
|
"packages/*",
|
||||||
|
"examples"
|
||||||
],
|
],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "bun run --filter '*' build",
|
"build": "bun run --filter '*' build",
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import type { ParsedAddArgv } from "../src/add-argv.js";
|
||||||
|
|
||||||
|
export const MINIMAL_DESCRIPTOR_YAML = `description: "fixture"
|
||||||
|
roles: {}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export function addCliArgs(name: string, filePath: string): ParsedAddArgv {
|
||||||
|
return { name, filePath, descriptorPath: null, typesPath: null };
|
||||||
|
}
|
||||||
@@ -2,9 +2,11 @@ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|||||||
import { mkdir, mkdtemp, readFile, rm, unlink, writeFile } from "node:fs/promises";
|
import { mkdir, mkdtemp, readFile, rm, unlink, writeFile } from "node:fs/promises";
|
||||||
import { tmpdir } from "node:os";
|
import { tmpdir } from "node:os";
|
||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
|
|
||||||
import { getRegisteredWorkflow, readWorkflowRegistry } from "@uncaged/workflow";
|
import { getRegisteredWorkflow, readWorkflowRegistry } from "@uncaged/workflow";
|
||||||
|
|
||||||
|
import { addCliArgs, MINIMAL_DESCRIPTOR_YAML } from "./bundle-fixture.js";
|
||||||
import { cmdAdd } from "../src/cmd-add.js";
|
import { cmdAdd } from "../src/cmd-add.js";
|
||||||
import { cmdHistory } from "../src/cmd-history.js";
|
import { cmdHistory } from "../src/cmd-history.js";
|
||||||
import { cmdList, formatListLines } from "../src/cmd-list.js";
|
import { cmdList, formatListLines } from "../src/cmd-list.js";
|
||||||
@@ -47,8 +49,9 @@ export default async function* (input) {
|
|||||||
`,
|
`,
|
||||||
"utf8",
|
"utf8",
|
||||||
);
|
);
|
||||||
|
await writeFile(join(bundleDir, "demo.yaml"), MINIMAL_DESCRIPTOR_YAML, "utf8");
|
||||||
|
|
||||||
const added = await cmdAdd(storageRoot, "solve-issue", bundlePath);
|
const added = await cmdAdd(storageRoot, addCliArgs("solve-issue", bundlePath));
|
||||||
expect(added.ok).toBe(true);
|
expect(added.ok).toBe(true);
|
||||||
|
|
||||||
const listed = await cmdList(storageRoot);
|
const listed = await cmdList(storageRoot);
|
||||||
@@ -88,10 +91,83 @@ export default async function* (input) {
|
|||||||
'import x from "./local";\nexport default async function* (input) { return { returnCode: 0, summary: input.prompt }; }\n',
|
'import x from "./local";\nexport default async function* (input) { return { returnCode: 0, summary: input.prompt }; }\n',
|
||||||
"utf8",
|
"utf8",
|
||||||
);
|
);
|
||||||
const r = await cmdAdd(storageRoot, "solve-issue", bundlePath);
|
const r = await cmdAdd(storageRoot, addCliArgs("solve-issue", bundlePath));
|
||||||
expect(r.ok).toBe(false);
|
expect(r.ok).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("add rejects .esm.js without companion YAML", async () => {
|
||||||
|
const bundlePath = join(storageRoot, "solo.esm.js");
|
||||||
|
await writeFile(
|
||||||
|
bundlePath,
|
||||||
|
`export default async function* (input) {
|
||||||
|
yield { role: "x", content: input.prompt, meta: {} };
|
||||||
|
return { returnCode: 0, summary: "ok" };
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
"utf8",
|
||||||
|
);
|
||||||
|
const r = await cmdAdd(storageRoot, addCliArgs("solo", bundlePath));
|
||||||
|
expect(r.ok).toBe(false);
|
||||||
|
if (r.ok) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
expect(r.error).toContain("descriptor YAML not found");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("add from .ts builds bundle + yaml + d.ts and registers hash", async () => {
|
||||||
|
const helloTs = fileURLToPath(new URL("../../../examples/hello-world.ts", import.meta.url));
|
||||||
|
const added = await cmdAdd(storageRoot, addCliArgs("hello", helloTs));
|
||||||
|
expect(added.ok).toBe(true);
|
||||||
|
if (!added.ok) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const { hash } = added.value;
|
||||||
|
const bundles = join(storageRoot, "bundles");
|
||||||
|
const esm = await readFile(join(bundles, `${hash}.esm.js`), "utf8");
|
||||||
|
expect(esm.length).toBeGreaterThan(100);
|
||||||
|
const yaml = await readFile(join(bundles, `${hash}.yaml`), "utf8");
|
||||||
|
expect(yaml).toContain("hello world");
|
||||||
|
const dts = await readFile(join(bundles, `${hash}.d.ts`), "utf8");
|
||||||
|
expect(dts).toContain("export type Roles");
|
||||||
|
expect(dts).toContain("WorkflowFn");
|
||||||
|
|
||||||
|
const reg = await readWorkflowRegistry(storageRoot);
|
||||||
|
expect(reg.ok).toBe(true);
|
||||||
|
if (!reg.ok) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const entry = getRegisteredWorkflow(reg.value, "hello");
|
||||||
|
expect(entry).not.toBeNull();
|
||||||
|
if (entry === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
expect(entry.hash).toBe(hash);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("add from .esm.js warns when optional .d.ts is missing", async () => {
|
||||||
|
const bundleDir = join(storageRoot, "src");
|
||||||
|
await mkdir(bundleDir, { recursive: true });
|
||||||
|
const bundlePath = join(bundleDir, "demo.esm.js");
|
||||||
|
await writeFile(
|
||||||
|
bundlePath,
|
||||||
|
`export default async function* (input) {
|
||||||
|
yield { role: "a", content: "x", meta: {} };
|
||||||
|
return { returnCode: 0, summary: "x" };
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
"utf8",
|
||||||
|
);
|
||||||
|
await writeFile(join(bundleDir, "demo.yaml"), MINIMAL_DESCRIPTOR_YAML, "utf8");
|
||||||
|
|
||||||
|
const added = await cmdAdd(storageRoot, addCliArgs("solve-issue", bundlePath));
|
||||||
|
expect(added.ok).toBe(true);
|
||||||
|
if (!added.ok) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
expect(added.value.warnings.length).toBe(1);
|
||||||
|
expect(added.value.warnings[0]).toContain("demo.d.ts");
|
||||||
|
});
|
||||||
|
|
||||||
test("history lists current + prior versions sorted by time descending", async () => {
|
test("history lists current + prior versions sorted by time descending", async () => {
|
||||||
const bundleDir = join(storageRoot, "src");
|
const bundleDir = join(storageRoot, "src");
|
||||||
await mkdir(bundleDir, { recursive: true });
|
await mkdir(bundleDir, { recursive: true });
|
||||||
@@ -107,11 +183,12 @@ export default async function* (input) {
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
await writeFile(bundlePath, v1, "utf8");
|
await writeFile(bundlePath, v1, "utf8");
|
||||||
const add1 = await cmdAdd(storageRoot, "solve-issue", bundlePath);
|
await writeFile(join(bundleDir, "demo.yaml"), MINIMAL_DESCRIPTOR_YAML, "utf8");
|
||||||
|
const add1 = await cmdAdd(storageRoot, addCliArgs("solve-issue", bundlePath));
|
||||||
expect(add1.ok).toBe(true);
|
expect(add1.ok).toBe(true);
|
||||||
await new Promise((r) => setTimeout(r, 15));
|
await new Promise((r) => setTimeout(r, 15));
|
||||||
await writeFile(bundlePath, v2, "utf8");
|
await writeFile(bundlePath, v2, "utf8");
|
||||||
const add2 = await cmdAdd(storageRoot, "solve-issue", bundlePath);
|
const add2 = await cmdAdd(storageRoot, addCliArgs("solve-issue", bundlePath));
|
||||||
expect(add2.ok).toBe(true);
|
expect(add2.ok).toBe(true);
|
||||||
|
|
||||||
const hist = await cmdHistory(storageRoot, "solve-issue");
|
const hist = await cmdHistory(storageRoot, "solve-issue");
|
||||||
@@ -145,14 +222,15 @@ export default async function* (input) {
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
await writeFile(bundlePath, v1, "utf8");
|
await writeFile(bundlePath, v1, "utf8");
|
||||||
const add1 = await cmdAdd(storageRoot, "solve-issue", bundlePath);
|
await writeFile(join(bundleDir, "demo.yaml"), MINIMAL_DESCRIPTOR_YAML, "utf8");
|
||||||
|
const add1 = await cmdAdd(storageRoot, addCliArgs("solve-issue", bundlePath));
|
||||||
expect(add1.ok).toBe(true);
|
expect(add1.ok).toBe(true);
|
||||||
if (!add1.ok) {
|
if (!add1.ok) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const hash1 = add1.value.hash;
|
const hash1 = add1.value.hash;
|
||||||
await writeFile(bundlePath, v2, "utf8");
|
await writeFile(bundlePath, v2, "utf8");
|
||||||
const add2 = await cmdAdd(storageRoot, "solve-issue", bundlePath);
|
const add2 = await cmdAdd(storageRoot, addCliArgs("solve-issue", bundlePath));
|
||||||
expect(add2.ok).toBe(true);
|
expect(add2.ok).toBe(true);
|
||||||
if (!add2.ok) {
|
if (!add2.ok) {
|
||||||
return;
|
return;
|
||||||
@@ -189,7 +267,8 @@ export default async function* (input) {
|
|||||||
`,
|
`,
|
||||||
"utf8",
|
"utf8",
|
||||||
);
|
);
|
||||||
const add1 = await cmdAdd(storageRoot, "solve-issue", bundlePath);
|
await writeFile(join(bundleDir, "demo.yaml"), MINIMAL_DESCRIPTOR_YAML, "utf8");
|
||||||
|
const add1 = await cmdAdd(storageRoot, addCliArgs("solve-issue", bundlePath));
|
||||||
expect(add1.ok).toBe(true);
|
expect(add1.ok).toBe(true);
|
||||||
await writeFile(
|
await writeFile(
|
||||||
bundlePath,
|
bundlePath,
|
||||||
@@ -200,7 +279,7 @@ export default async function* (input) {
|
|||||||
`,
|
`,
|
||||||
"utf8",
|
"utf8",
|
||||||
);
|
);
|
||||||
const add2 = await cmdAdd(storageRoot, "solve-issue", bundlePath);
|
const add2 = await cmdAdd(storageRoot, addCliArgs("solve-issue", bundlePath));
|
||||||
expect(add2.ok).toBe(true);
|
expect(add2.ok).toBe(true);
|
||||||
|
|
||||||
const bad = await cmdRollback(storageRoot, "solve-issue", "0000000000000");
|
const bad = await cmdRollback(storageRoot, "solve-issue", "0000000000000");
|
||||||
@@ -220,7 +299,8 @@ export default async function* (input) {
|
|||||||
`,
|
`,
|
||||||
"utf8",
|
"utf8",
|
||||||
);
|
);
|
||||||
const add1 = await cmdAdd(storageRoot, "solve-issue", bundlePath);
|
await writeFile(join(bundleDir, "demo.yaml"), MINIMAL_DESCRIPTOR_YAML, "utf8");
|
||||||
|
const add1 = await cmdAdd(storageRoot, addCliArgs("solve-issue", bundlePath));
|
||||||
expect(add1.ok).toBe(true);
|
expect(add1.ok).toBe(true);
|
||||||
if (!add1.ok) {
|
if (!add1.ok) {
|
||||||
return;
|
return;
|
||||||
@@ -235,7 +315,7 @@ export default async function* (input) {
|
|||||||
`,
|
`,
|
||||||
"utf8",
|
"utf8",
|
||||||
);
|
);
|
||||||
const add2 = await cmdAdd(storageRoot, "solve-issue", bundlePath);
|
const add2 = await cmdAdd(storageRoot, addCliArgs("solve-issue", bundlePath));
|
||||||
expect(add2.ok).toBe(true);
|
expect(add2.ok).toBe(true);
|
||||||
if (!add2.ok) {
|
if (!add2.ok) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
|||||||
import { tmpdir } from "node:os";
|
import { tmpdir } from "node:os";
|
||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
|
|
||||||
|
import { addCliArgs, MINIMAL_DESCRIPTOR_YAML } from "./bundle-fixture.js";
|
||||||
import { cmdAdd } from "../src/cmd-add.js";
|
import { cmdAdd } from "../src/cmd-add.js";
|
||||||
import { cmdFork } from "../src/cmd-fork.js";
|
import { cmdFork } from "../src/cmd-fork.js";
|
||||||
import { cmdRun } from "../src/cmd-run.js";
|
import { cmdRun } from "../src/cmd-run.js";
|
||||||
@@ -82,8 +83,9 @@ describe("cli fork", () => {
|
|||||||
await mkdir(bundleDir, { recursive: true });
|
await mkdir(bundleDir, { recursive: true });
|
||||||
const bundlePath = join(bundleDir, "demo.esm.js");
|
const bundlePath = join(bundleDir, "demo.esm.js");
|
||||||
await writeFile(bundlePath, threeRoleBundleSource, "utf8");
|
await writeFile(bundlePath, threeRoleBundleSource, "utf8");
|
||||||
|
await writeFile(join(bundleDir, "demo.yaml"), MINIMAL_DESCRIPTOR_YAML, "utf8");
|
||||||
|
|
||||||
const added = await cmdAdd(storageRoot, "solve-issue", bundlePath);
|
const added = await cmdAdd(storageRoot, addCliArgs("solve-issue", bundlePath));
|
||||||
expect(added.ok).toBe(true);
|
expect(added.ok).toBe(true);
|
||||||
if (!added.ok) {
|
if (!added.ok) {
|
||||||
return;
|
return;
|
||||||
@@ -132,8 +134,9 @@ describe("cli fork", () => {
|
|||||||
await mkdir(bundleDir, { recursive: true });
|
await mkdir(bundleDir, { recursive: true });
|
||||||
const bundlePath = join(bundleDir, "demo.esm.js");
|
const bundlePath = join(bundleDir, "demo.esm.js");
|
||||||
await writeFile(bundlePath, threeRoleBundleSource, "utf8");
|
await writeFile(bundlePath, threeRoleBundleSource, "utf8");
|
||||||
|
await writeFile(join(bundleDir, "demo.yaml"), MINIMAL_DESCRIPTOR_YAML, "utf8");
|
||||||
|
|
||||||
const added = await cmdAdd(storageRoot, "solve-issue", bundlePath);
|
const added = await cmdAdd(storageRoot, addCliArgs("solve-issue", bundlePath));
|
||||||
expect(added.ok).toBe(true);
|
expect(added.ok).toBe(true);
|
||||||
if (!added.ok) {
|
if (!added.ok) {
|
||||||
return;
|
return;
|
||||||
@@ -183,8 +186,9 @@ describe("cli fork", () => {
|
|||||||
await mkdir(bundleDir, { recursive: true });
|
await mkdir(bundleDir, { recursive: true });
|
||||||
const bundlePath = join(bundleDir, "demo.esm.js");
|
const bundlePath = join(bundleDir, "demo.esm.js");
|
||||||
await writeFile(bundlePath, threeRoleBundleSource, "utf8");
|
await writeFile(bundlePath, threeRoleBundleSource, "utf8");
|
||||||
|
await writeFile(join(bundleDir, "demo.yaml"), MINIMAL_DESCRIPTOR_YAML, "utf8");
|
||||||
|
|
||||||
const added = await cmdAdd(storageRoot, "solve-issue", bundlePath);
|
const added = await cmdAdd(storageRoot, addCliArgs("solve-issue", bundlePath));
|
||||||
expect(added.ok).toBe(true);
|
expect(added.ok).toBe(true);
|
||||||
if (!added.ok) {
|
if (!added.ok) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { tmpdir } from "node:os";
|
|||||||
import { dirname, join } from "node:path";
|
import { dirname, join } from "node:path";
|
||||||
import { fileURLToPath } from "node:url";
|
import { fileURLToPath } from "node:url";
|
||||||
|
|
||||||
|
import { addCliArgs, MINIMAL_DESCRIPTOR_YAML } from "./bundle-fixture.js";
|
||||||
import { cmdAdd } from "../src/cmd-add.js";
|
import { cmdAdd } from "../src/cmd-add.js";
|
||||||
import { cmdKill } from "../src/cmd-kill.js";
|
import { cmdKill } from "../src/cmd-kill.js";
|
||||||
import { cmdPause } from "../src/cmd-pause.js";
|
import { cmdPause } from "../src/cmd-pause.js";
|
||||||
@@ -113,8 +114,9 @@ describe("cli thread commands", () => {
|
|||||||
await mkdir(bundleDir, { recursive: true });
|
await mkdir(bundleDir, { recursive: true });
|
||||||
const bundlePath = join(bundleDir, "demo.esm.js");
|
const bundlePath = join(bundleDir, "demo.esm.js");
|
||||||
await writeFile(bundlePath, fastBundleSource, "utf8");
|
await writeFile(bundlePath, fastBundleSource, "utf8");
|
||||||
|
await writeFile(join(bundleDir, "demo.yaml"), MINIMAL_DESCRIPTOR_YAML, "utf8");
|
||||||
|
|
||||||
const added = await cmdAdd(storageRoot, "solve-issue", bundlePath);
|
const added = await cmdAdd(storageRoot, addCliArgs("solve-issue", bundlePath));
|
||||||
expect(added.ok).toBe(true);
|
expect(added.ok).toBe(true);
|
||||||
if (!added.ok) {
|
if (!added.ok) {
|
||||||
return;
|
return;
|
||||||
@@ -174,8 +176,9 @@ describe("cli thread commands", () => {
|
|||||||
await mkdir(bundleDir, { recursive: true });
|
await mkdir(bundleDir, { recursive: true });
|
||||||
const bundlePath = join(bundleDir, "demo.esm.js");
|
const bundlePath = join(bundleDir, "demo.esm.js");
|
||||||
await writeFile(bundlePath, slowPlannerBundleSource, "utf8");
|
await writeFile(bundlePath, slowPlannerBundleSource, "utf8");
|
||||||
|
await writeFile(join(bundleDir, "demo.yaml"), MINIMAL_DESCRIPTOR_YAML, "utf8");
|
||||||
|
|
||||||
const added = await cmdAdd(storageRoot, "solve-issue", bundlePath);
|
const added = await cmdAdd(storageRoot, addCliArgs("solve-issue", bundlePath));
|
||||||
expect(added.ok).toBe(true);
|
expect(added.ok).toBe(true);
|
||||||
if (!added.ok) {
|
if (!added.ok) {
|
||||||
return;
|
return;
|
||||||
@@ -204,8 +207,9 @@ describe("cli thread commands", () => {
|
|||||||
await mkdir(bundleDir, { recursive: true });
|
await mkdir(bundleDir, { recursive: true });
|
||||||
const bundlePath = join(bundleDir, "demo.esm.js");
|
const bundlePath = join(bundleDir, "demo.esm.js");
|
||||||
await writeFile(bundlePath, abortablePlannerBundleSource, "utf8");
|
await writeFile(bundlePath, abortablePlannerBundleSource, "utf8");
|
||||||
|
await writeFile(join(bundleDir, "demo.yaml"), MINIMAL_DESCRIPTOR_YAML, "utf8");
|
||||||
|
|
||||||
const added = await cmdAdd(storageRoot, "solve-issue", bundlePath);
|
const added = await cmdAdd(storageRoot, addCliArgs("solve-issue", bundlePath));
|
||||||
expect(added.ok).toBe(true);
|
expect(added.ok).toBe(true);
|
||||||
if (!added.ok) {
|
if (!added.ok) {
|
||||||
return;
|
return;
|
||||||
@@ -243,8 +247,9 @@ describe("cli thread commands", () => {
|
|||||||
await mkdir(bundleDir, { recursive: true });
|
await mkdir(bundleDir, { recursive: true });
|
||||||
const bundlePath = join(bundleDir, "demo.esm.js");
|
const bundlePath = join(bundleDir, "demo.esm.js");
|
||||||
await writeFile(bundlePath, pauseResumeBundleSource, "utf8");
|
await writeFile(bundlePath, pauseResumeBundleSource, "utf8");
|
||||||
|
await writeFile(join(bundleDir, "demo.yaml"), MINIMAL_DESCRIPTOR_YAML, "utf8");
|
||||||
|
|
||||||
const added = await cmdAdd(storageRoot, "solve-issue", bundlePath);
|
const added = await cmdAdd(storageRoot, addCliArgs("solve-issue", bundlePath));
|
||||||
expect(added.ok).toBe(true);
|
expect(added.ok).toBe(true);
|
||||||
if (!added.ok) {
|
if (!added.ok) {
|
||||||
return;
|
return;
|
||||||
@@ -284,8 +289,9 @@ describe("cli thread commands", () => {
|
|||||||
await mkdir(bundleDir, { recursive: true });
|
await mkdir(bundleDir, { recursive: true });
|
||||||
const bundlePath = join(bundleDir, "demo.esm.js");
|
const bundlePath = join(bundleDir, "demo.esm.js");
|
||||||
await writeFile(bundlePath, fastBundleSource, "utf8");
|
await writeFile(bundlePath, fastBundleSource, "utf8");
|
||||||
|
await writeFile(join(bundleDir, "demo.yaml"), MINIMAL_DESCRIPTOR_YAML, "utf8");
|
||||||
|
|
||||||
const added = await cmdAdd(storageRoot, "solve-issue", bundlePath);
|
const added = await cmdAdd(storageRoot, addCliArgs("solve-issue", bundlePath));
|
||||||
expect(added.ok).toBe(true);
|
expect(added.ok).toBe(true);
|
||||||
if (!added.ok) {
|
if (!added.ok) {
|
||||||
return;
|
return;
|
||||||
@@ -313,8 +319,9 @@ describe("cli thread commands", () => {
|
|||||||
await mkdir(bundleDir, { recursive: true });
|
await mkdir(bundleDir, { recursive: true });
|
||||||
const bundlePath = join(bundleDir, "demo.esm.js");
|
const bundlePath = join(bundleDir, "demo.esm.js");
|
||||||
await writeFile(bundlePath, delayedFirstYieldBundleSource, "utf8");
|
await writeFile(bundlePath, delayedFirstYieldBundleSource, "utf8");
|
||||||
|
await writeFile(join(bundleDir, "demo.yaml"), MINIMAL_DESCRIPTOR_YAML, "utf8");
|
||||||
|
|
||||||
const added = await cmdAdd(storageRoot, "solve-issue", bundlePath);
|
const added = await cmdAdd(storageRoot, addCliArgs("solve-issue", bundlePath));
|
||||||
expect(added.ok).toBe(true);
|
expect(added.ok).toBe(true);
|
||||||
if (!added.ok) {
|
if (!added.ok) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -0,0 +1,60 @@
|
|||||||
|
import { err, ok, type Result } from "@uncaged/workflow";
|
||||||
|
|
||||||
|
export type ParsedAddArgv = {
|
||||||
|
name: string;
|
||||||
|
filePath: string;
|
||||||
|
/** Override path to descriptor YAML when adding an `.esm.js` bundle. */
|
||||||
|
descriptorPath: string | null;
|
||||||
|
/** Override path to `.d.ts` when adding an `.esm.js` bundle. */
|
||||||
|
typesPath: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function parseAddArgv(argv: string[]): Result<ParsedAddArgv, string> {
|
||||||
|
let name: string | undefined;
|
||||||
|
let filePath: string | undefined;
|
||||||
|
let descriptorPath: string | null = null;
|
||||||
|
let typesPath: string | null = null;
|
||||||
|
|
||||||
|
let i = 0;
|
||||||
|
while (i < argv.length) {
|
||||||
|
const tok = argv[i];
|
||||||
|
if (tok === "--descriptor") {
|
||||||
|
const value = argv[i + 1];
|
||||||
|
if (value === undefined || value.startsWith("--")) {
|
||||||
|
return err("missing value for --descriptor");
|
||||||
|
}
|
||||||
|
descriptorPath = value;
|
||||||
|
i += 2;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (tok === "--types") {
|
||||||
|
const value = argv[i + 1];
|
||||||
|
if (value === undefined || value.startsWith("--")) {
|
||||||
|
return err("missing value for --types");
|
||||||
|
}
|
||||||
|
typesPath = value;
|
||||||
|
i += 2;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (tok !== undefined && tok.startsWith("--")) {
|
||||||
|
return err(`unknown add flag: ${tok}`);
|
||||||
|
}
|
||||||
|
if (tok === undefined) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (name === undefined) {
|
||||||
|
name = tok;
|
||||||
|
} else if (filePath === undefined) {
|
||||||
|
filePath = tok;
|
||||||
|
} else {
|
||||||
|
return err("too many arguments");
|
||||||
|
}
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === undefined || name === "" || filePath === undefined || filePath === "") {
|
||||||
|
return err("add requires <name> <file>");
|
||||||
|
}
|
||||||
|
|
||||||
|
return ok({ name, filePath, descriptorPath, typesPath });
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { copyFile, mkdir, readFile, stat } from "node:fs/promises";
|
import { mkdir, readFile, stat, writeFile } from "node:fs/promises";
|
||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
|
|
||||||
import { err, ok, type Result } from "@uncaged/workflow";
|
import { err, ok, type Result } from "@uncaged/workflow";
|
||||||
@@ -12,28 +12,39 @@ async function pathExists(path: string): Promise<boolean> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function storeWorkflowBundleCopy(
|
export type BundleFileSource =
|
||||||
storageRoot: string,
|
| { kind: "text"; text: string }
|
||||||
hash: string,
|
| { kind: "path"; path: string };
|
||||||
resolvedSourcePath: string,
|
|
||||||
sourceText: string,
|
|
||||||
): Promise<Result<void, string>> {
|
|
||||||
const bundlesDir = join(storageRoot, "bundles");
|
|
||||||
const destPath = join(bundlesDir, `${hash}.esm.js`);
|
|
||||||
|
|
||||||
|
export type WorkflowBundleStoreInput = {
|
||||||
|
esmJs: BundleFileSource;
|
||||||
|
yaml: BundleFileSource;
|
||||||
|
dts: BundleFileSource | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
async function resolveSourceText(src: BundleFileSource): Promise<Result<string, string>> {
|
||||||
|
if (src.kind === "text") {
|
||||||
|
return ok(src.text);
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
await mkdir(bundlesDir, { recursive: true });
|
return ok(await readFile(src.path, "utf8"));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
const message = e instanceof Error ? e.message : String(e);
|
const message = e instanceof Error ? e.message : String(e);
|
||||||
return err(`failed to store bundle: ${message}`);
|
return err(`failed to read bundle artifact: ${message}`);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function ensureMatchingOrWrite(
|
||||||
|
destPath: string,
|
||||||
|
text: string,
|
||||||
|
label: string,
|
||||||
|
): Promise<Result<void, string>> {
|
||||||
if (!(await pathExists(destPath))) {
|
if (!(await pathExists(destPath))) {
|
||||||
try {
|
try {
|
||||||
await copyFile(resolvedSourcePath, destPath);
|
await writeFile(destPath, text, "utf8");
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
const message = e instanceof Error ? e.message : String(e);
|
const message = e instanceof Error ? e.message : String(e);
|
||||||
return err(`failed to store bundle: ${message}`);
|
return err(`failed to write ${label}: ${message}`);
|
||||||
}
|
}
|
||||||
return ok(undefined);
|
return ok(undefined);
|
||||||
}
|
}
|
||||||
@@ -43,10 +54,67 @@ export async function storeWorkflowBundleCopy(
|
|||||||
existing = await readFile(destPath, "utf8");
|
existing = await readFile(destPath, "utf8");
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
const message = e instanceof Error ? e.message : String(e);
|
const message = e instanceof Error ? e.message : String(e);
|
||||||
return err(`failed to store bundle: ${message}`);
|
return err(`failed to read existing ${label}: ${message}`);
|
||||||
}
|
}
|
||||||
if (existing !== sourceText) {
|
if (existing !== text) {
|
||||||
return err(`bundle hash ${hash} already exists with different contents; refusing to overwrite`);
|
return err(
|
||||||
|
`${label} for this hash already exists with different contents; refusing to overwrite`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return ok(undefined);
|
return ok(undefined);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Store `.esm.js`, `.yaml`, and optional `.d.ts` under `bundles/` keyed by hash. */
|
||||||
|
export async function storeWorkflowBundleArtifacts(
|
||||||
|
storageRoot: string,
|
||||||
|
hash: string,
|
||||||
|
input: WorkflowBundleStoreInput,
|
||||||
|
): Promise<Result<void, string>> {
|
||||||
|
const bundlesDir = join(storageRoot, "bundles");
|
||||||
|
try {
|
||||||
|
await mkdir(bundlesDir, { recursive: true });
|
||||||
|
} catch (e) {
|
||||||
|
const message = e instanceof Error ? e.message : String(e);
|
||||||
|
return err(`failed to store bundle: ${message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const esmText = await resolveSourceText(input.esmJs);
|
||||||
|
if (!esmText.ok) {
|
||||||
|
return esmText;
|
||||||
|
}
|
||||||
|
const yamlText = await resolveSourceText(input.yaml);
|
||||||
|
if (!yamlText.ok) {
|
||||||
|
return yamlText;
|
||||||
|
}
|
||||||
|
|
||||||
|
let dtsText: string | null = null;
|
||||||
|
if (input.dts !== null) {
|
||||||
|
const dtsResolved = await resolveSourceText(input.dts);
|
||||||
|
if (!dtsResolved.ok) {
|
||||||
|
return dtsResolved;
|
||||||
|
}
|
||||||
|
dtsText = dtsResolved.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
const destEsm = join(bundlesDir, `${hash}.esm.js`);
|
||||||
|
const destYaml = join(bundlesDir, `${hash}.yaml`);
|
||||||
|
const destDts = join(bundlesDir, `${hash}.d.ts`);
|
||||||
|
|
||||||
|
const w1 = await ensureMatchingOrWrite(destEsm, esmText.value, "bundle");
|
||||||
|
if (!w1.ok) {
|
||||||
|
return w1;
|
||||||
|
}
|
||||||
|
const w2 = await ensureMatchingOrWrite(destYaml, yamlText.value, "descriptor");
|
||||||
|
if (!w2.ok) {
|
||||||
|
return w2;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dtsText !== null) {
|
||||||
|
const w3 = await ensureMatchingOrWrite(destDts, dtsText, "types");
|
||||||
|
if (!w3.ok) {
|
||||||
|
return w3;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ok(undefined);
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { printCliError, printCliLine } from "./cli-output.js";
|
import { parseAddArgv } from "./add-argv.js";
|
||||||
|
import { printCliError, printCliLine, printCliWarn } from "./cli-output.js";
|
||||||
import { cmdAdd, formatAddSuccess } from "./cmd-add.js";
|
import { cmdAdd, formatAddSuccess } from "./cmd-add.js";
|
||||||
import { cmdFork, parseForkArgv } from "./cmd-fork.js";
|
import { cmdFork, parseForkArgv } from "./cmd-fork.js";
|
||||||
import { cmdHistory } from "./cmd-history.js";
|
import { cmdHistory } from "./cmd-history.js";
|
||||||
@@ -18,7 +19,7 @@ import { parseRunArgv } from "./run-argv.js";
|
|||||||
function usage(): string {
|
function usage(): string {
|
||||||
return [
|
return [
|
||||||
"Usage:",
|
"Usage:",
|
||||||
" uncaged-workflow add <name> <file>",
|
" uncaged-workflow add <name> <file> [--descriptor <path>] [--types <path>]",
|
||||||
" uncaged-workflow list",
|
" uncaged-workflow list",
|
||||||
" uncaged-workflow show <name>",
|
" uncaged-workflow show <name>",
|
||||||
" uncaged-workflow remove <name>",
|
" uncaged-workflow remove <name>",
|
||||||
@@ -37,18 +38,20 @@ function usage(): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function dispatchAdd(storageRoot: string, argv: string[]): Promise<number> {
|
async function dispatchAdd(storageRoot: string, argv: string[]): Promise<number> {
|
||||||
const name = argv[0];
|
const parsed = parseAddArgv(argv);
|
||||||
const file = argv[1];
|
if (!parsed.ok) {
|
||||||
if (name === undefined || file === undefined || argv.length > 2) {
|
printCliError(`${usage()}\n\nerror: ${parsed.error}`);
|
||||||
printCliError(`${usage()}\n\nerror: add requires <name> <file>`);
|
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
const result = await cmdAdd(storageRoot, name, file);
|
const result = await cmdAdd(storageRoot, parsed.value);
|
||||||
if (!result.ok) {
|
if (!result.ok) {
|
||||||
printCliError(result.error);
|
printCliError(result.error);
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
printCliLine(formatAddSuccess(name, file, result.value.hash));
|
for (const w of result.value.warnings) {
|
||||||
|
printCliWarn(w);
|
||||||
|
}
|
||||||
|
printCliLine(formatAddSuccess(parsed.value.name, parsed.value.filePath, result.value.hash));
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,3 +7,8 @@ export function printCliError(line: string): void {
|
|||||||
// biome-ignore lint/suspicious/noConsole: CLI user-facing errors
|
// biome-ignore lint/suspicious/noConsole: CLI user-facing errors
|
||||||
console.error(line);
|
console.error(line);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function printCliWarn(line: string): void {
|
||||||
|
// biome-ignore lint/suspicious/noConsole: CLI user-facing warnings
|
||||||
|
console.warn(line);
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { readFile, stat } from "node:fs/promises";
|
|||||||
import { basename, resolve } from "node:path";
|
import { basename, resolve } from "node:path";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
buildWorkflowFromTypeScript,
|
||||||
err,
|
err,
|
||||||
hashWorkflowBundleBytes,
|
hashWorkflowBundleBytes,
|
||||||
ok,
|
ok,
|
||||||
@@ -12,25 +13,97 @@ import {
|
|||||||
writeWorkflowRegistry,
|
writeWorkflowRegistry,
|
||||||
} from "@uncaged/workflow";
|
} from "@uncaged/workflow";
|
||||||
|
|
||||||
import { storeWorkflowBundleCopy } from "./bundle-store.js";
|
import type { ParsedAddArgv } from "./add-argv.js";
|
||||||
|
import { storeWorkflowBundleArtifacts } from "./bundle-store.js";
|
||||||
import { validateCliWorkflowName } from "./workflow-name.js";
|
import { validateCliWorkflowName } from "./workflow-name.js";
|
||||||
|
|
||||||
|
export type CmdAddSuccess = {
|
||||||
|
hash: string;
|
||||||
|
warnings: ReadonlyArray<string>;
|
||||||
|
};
|
||||||
|
|
||||||
|
function isTypeScriptWorkflow(path: string): boolean {
|
||||||
|
return path.endsWith(".ts");
|
||||||
|
}
|
||||||
|
|
||||||
|
function isEsmBundle(path: string): boolean {
|
||||||
|
return path.endsWith(".esm.js");
|
||||||
|
}
|
||||||
|
|
||||||
|
function defaultDescriptorPath(bundlePath: string): string {
|
||||||
|
return bundlePath.replace(/\.esm\.js$/i, ".yaml");
|
||||||
|
}
|
||||||
|
|
||||||
|
function defaultTypesPath(bundlePath: string): string {
|
||||||
|
return bundlePath.replace(/\.esm\.js$/i, ".d.ts");
|
||||||
|
}
|
||||||
|
|
||||||
|
async function registerHash(
|
||||||
|
storageRoot: string,
|
||||||
|
name: string,
|
||||||
|
hash: string,
|
||||||
|
): Promise<Result<void, string>> {
|
||||||
|
const reg = await readWorkflowRegistry(storageRoot);
|
||||||
|
if (!reg.ok) {
|
||||||
|
return err(reg.error.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
const next = registerWorkflowVersion(reg.value, name, hash, Date.now());
|
||||||
|
const written = await writeWorkflowRegistry(storageRoot, next);
|
||||||
|
if (!written.ok) {
|
||||||
|
return err(written.error.message);
|
||||||
|
}
|
||||||
|
return ok(undefined);
|
||||||
|
}
|
||||||
|
|
||||||
export async function cmdAdd(
|
export async function cmdAdd(
|
||||||
storageRoot: string,
|
storageRoot: string,
|
||||||
name: string,
|
args: ParsedAddArgv,
|
||||||
filePath: string,
|
): Promise<Result<CmdAddSuccess, string>> {
|
||||||
): Promise<Result<{ hash: string }, string>> {
|
const nameOk = validateCliWorkflowName(args.name);
|
||||||
const nameOk = validateCliWorkflowName(name);
|
|
||||||
if (!nameOk.ok) {
|
if (!nameOk.ok) {
|
||||||
return nameOk;
|
return nameOk;
|
||||||
}
|
}
|
||||||
|
|
||||||
let resolvedPath: string;
|
let resolvedPath: string;
|
||||||
try {
|
try {
|
||||||
resolvedPath = resolve(filePath);
|
resolvedPath = resolve(args.filePath);
|
||||||
await stat(resolvedPath);
|
await stat(resolvedPath);
|
||||||
} catch {
|
} catch {
|
||||||
return err(`bundle file not found: ${filePath}`);
|
return err(`file not found: ${args.filePath}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const warnings: string[] = [];
|
||||||
|
|
||||||
|
if (isTypeScriptWorkflow(resolvedPath)) {
|
||||||
|
const built = await buildWorkflowFromTypeScript(resolvedPath);
|
||||||
|
if (!built.ok) {
|
||||||
|
return built;
|
||||||
|
}
|
||||||
|
|
||||||
|
const encoder = new TextEncoder();
|
||||||
|
const bytes = encoder.encode(built.value.esmJsSource);
|
||||||
|
const hash = hashWorkflowBundleBytes(bytes);
|
||||||
|
|
||||||
|
const stored = await storeWorkflowBundleArtifacts(storageRoot, hash, {
|
||||||
|
esmJs: { kind: "text", text: built.value.esmJsSource },
|
||||||
|
yaml: { kind: "text", text: built.value.yamlSource },
|
||||||
|
dts: { kind: "text", text: built.value.dtsSource },
|
||||||
|
});
|
||||||
|
if (!stored.ok) {
|
||||||
|
return stored;
|
||||||
|
}
|
||||||
|
|
||||||
|
const regResult = await registerHash(storageRoot, args.name, hash);
|
||||||
|
if (!regResult.ok) {
|
||||||
|
return regResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ok({ hash, warnings });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isEsmBundle(resolvedPath)) {
|
||||||
|
return err('workflow file must be ".ts" or end with ".esm.js"');
|
||||||
}
|
}
|
||||||
|
|
||||||
let source: string;
|
let source: string;
|
||||||
@@ -49,27 +122,54 @@ export async function cmdAdd(
|
|||||||
return validated;
|
return validated;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const yamlResolved =
|
||||||
|
args.descriptorPath !== null ? resolve(args.descriptorPath) : defaultDescriptorPath(resolvedPath);
|
||||||
|
|
||||||
|
let yamlText: string;
|
||||||
|
try {
|
||||||
|
yamlText = await readFile(yamlResolved, "utf8");
|
||||||
|
} catch {
|
||||||
|
return err(`descriptor YAML not found: ${yamlResolved}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
let dtsSource: { kind: "text"; text: string } | null = null;
|
||||||
|
if (args.typesPath !== null) {
|
||||||
|
const typesResolved = resolve(args.typesPath);
|
||||||
|
try {
|
||||||
|
const text = await readFile(typesResolved, "utf8");
|
||||||
|
dtsSource = { kind: "text", text };
|
||||||
|
} catch {
|
||||||
|
return err(`types file not found: ${typesResolved}`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const typesDefault = defaultTypesPath(resolvedPath);
|
||||||
|
try {
|
||||||
|
const text = await readFile(typesDefault, "utf8");
|
||||||
|
dtsSource = { kind: "text", text };
|
||||||
|
} catch {
|
||||||
|
warnings.push(`optional types file not found (${basename(typesDefault)}); skipped`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const encoder = new TextEncoder();
|
const encoder = new TextEncoder();
|
||||||
const bytes = encoder.encode(source);
|
const bytes = encoder.encode(source);
|
||||||
const hash = hashWorkflowBundleBytes(bytes);
|
const hash = hashWorkflowBundleBytes(bytes);
|
||||||
|
|
||||||
const stored = await storeWorkflowBundleCopy(storageRoot, hash, resolvedPath, source);
|
const stored = await storeWorkflowBundleArtifacts(storageRoot, hash, {
|
||||||
|
esmJs: { kind: "text", text: source },
|
||||||
|
yaml: { kind: "text", text: yamlText },
|
||||||
|
dts: dtsSource,
|
||||||
|
});
|
||||||
if (!stored.ok) {
|
if (!stored.ok) {
|
||||||
return stored;
|
return stored;
|
||||||
}
|
}
|
||||||
|
|
||||||
const reg = await readWorkflowRegistry(storageRoot);
|
const regResult = await registerHash(storageRoot, args.name, hash);
|
||||||
if (!reg.ok) {
|
if (!regResult.ok) {
|
||||||
return err(reg.error.message);
|
return regResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
const next = registerWorkflowVersion(reg.value, name, hash, Date.now());
|
return ok({ hash, warnings });
|
||||||
const written = await writeWorkflowRegistry(storageRoot, next);
|
|
||||||
if (!written.ok) {
|
|
||||||
return err(written.error.message);
|
|
||||||
}
|
|
||||||
|
|
||||||
return ok({ hash });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function formatAddSuccess(name: string, filePath: string, hash: string): string {
|
export function formatAddSuccess(name: string, filePath: string, hash: string): string {
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import { describe, expect, test } from "bun:test";
|
||||||
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
||||||
|
|
||||||
|
import { buildWorkflowFromTypeScript } from "../src/build-pipeline.js";
|
||||||
|
|
||||||
|
describe("buildWorkflowFromTypeScript", () => {
|
||||||
|
test("produces valid ESM bundle text, YAML, and d.ts from hello-world.ts", async () => {
|
||||||
|
const helloTs = fileURLToPath(new URL("../../../examples/hello-world.ts", import.meta.url));
|
||||||
|
const r = await buildWorkflowFromTypeScript(helloTs);
|
||||||
|
expect(r.ok).toBe(true);
|
||||||
|
if (!r.ok) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
expect(r.value.esmJsSource.length).toBeGreaterThan(500);
|
||||||
|
expect(r.value.yamlSource).toContain("hello world");
|
||||||
|
expect(r.value.dtsSource).toContain("greeter");
|
||||||
|
expect(r.value.dtsSource).toContain("greeting: string");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("built bundle default export is executable", async () => {
|
||||||
|
const helloTs = fileURLToPath(new URL("../../../examples/hello-world.ts", import.meta.url));
|
||||||
|
const r = await buildWorkflowFromTypeScript(helloTs);
|
||||||
|
expect(r.ok).toBe(true);
|
||||||
|
if (!r.ok) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const tmp = `/tmp/uncaged-wf-build-test-${Date.now()}.esm.js`;
|
||||||
|
await Bun.write(tmp, r.value.esmJsSource);
|
||||||
|
|
||||||
|
const href = pathToFileURL(tmp).href;
|
||||||
|
const mod = (await import(href)) as { default: unknown };
|
||||||
|
expect(typeof mod.default).toBe("function");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import { describe, expect, test } from "bun:test";
|
||||||
|
|
||||||
|
import { jsonSchemaToTypeString } from "../src/json-schema-to-ts.js";
|
||||||
|
|
||||||
|
describe("jsonSchemaToTypeString", () => {
|
||||||
|
test("maps primitives and object required fields", () => {
|
||||||
|
const schema = {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
plan: { type: "string" },
|
||||||
|
files: { type: "array", items: { type: "string" } },
|
||||||
|
},
|
||||||
|
required: ["plan", "files"],
|
||||||
|
};
|
||||||
|
expect(jsonSchemaToTypeString(schema)).toBe("{ plan: string; files: string[] }");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("marks non-required object properties as nullable union", () => {
|
||||||
|
const schema = {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
n: { type: "number" },
|
||||||
|
},
|
||||||
|
required: [],
|
||||||
|
};
|
||||||
|
expect(jsonSchemaToTypeString(schema)).toBe("{ n: number | null }");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("handles boolean and integer", () => {
|
||||||
|
expect(jsonSchemaToTypeString({ type: "boolean" })).toBe("boolean");
|
||||||
|
expect(jsonSchemaToTypeString({ type: "integer" })).toBe("number");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("handles enum as literal union", () => {
|
||||||
|
expect(jsonSchemaToTypeString({ enum: ["a", "b"] })).toBe(`"a" | "b"`);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
import { access, constants } from "node:fs/promises";
|
||||||
|
import { dirname, join } from "node:path";
|
||||||
|
import { pathToFileURL } from "node:url";
|
||||||
|
|
||||||
|
import { validateWorkflowBundle } from "./bundle-validator.js";
|
||||||
|
import { stringifyWorkflowDescriptor } from "./generate-descriptor.js";
|
||||||
|
import { generateWorkflowBundleTypes } from "./generate-types.js";
|
||||||
|
import { err, ok, type Result } from "./result.js";
|
||||||
|
import {
|
||||||
|
validateWorkflowDescriptor,
|
||||||
|
type WorkflowDescriptor,
|
||||||
|
} from "./workflow-descriptor.js";
|
||||||
|
|
||||||
|
export type BuildPipelineResult = {
|
||||||
|
esmJsSource: string;
|
||||||
|
yamlSource: string;
|
||||||
|
dtsSource: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
async function findPackageRoot(startDir: string): Promise<string> {
|
||||||
|
let dir = startDir;
|
||||||
|
for (;;) {
|
||||||
|
try {
|
||||||
|
await access(join(dir, "package.json"), constants.R_OK);
|
||||||
|
return dir;
|
||||||
|
} catch {
|
||||||
|
const parent = dirname(dir);
|
||||||
|
if (parent === dir) {
|
||||||
|
return startDir;
|
||||||
|
}
|
||||||
|
dir = parent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadDescriptorFromSourceTs(absoluteTsPath: string): Promise<
|
||||||
|
Result<WorkflowDescriptor, string>
|
||||||
|
> {
|
||||||
|
let mod: Record<string, unknown>;
|
||||||
|
try {
|
||||||
|
const href = pathToFileURL(absoluteTsPath).href;
|
||||||
|
// Dynamic import required: user workflow source path resolved at add/build time
|
||||||
|
mod = (await import(href)) as Record<string, unknown>;
|
||||||
|
} catch (e) {
|
||||||
|
const message = e instanceof Error ? e.message : String(e);
|
||||||
|
return err(`failed to import workflow source for descriptor: ${message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const raw = mod.descriptor;
|
||||||
|
return validateWorkflowDescriptor(raw);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bundle a `.ts` workflow entry with Bun, read `export const descriptor`, and emit
|
||||||
|
* companion YAML + `.d.ts` text alongside validated ESM bundle source.
|
||||||
|
*/
|
||||||
|
export async function buildWorkflowFromTypeScript(
|
||||||
|
absoluteTsPath: string,
|
||||||
|
): Promise<Result<BuildPipelineResult, string>> {
|
||||||
|
let rootDir: string;
|
||||||
|
try {
|
||||||
|
rootDir = await findPackageRoot(dirname(absoluteTsPath));
|
||||||
|
} catch (e) {
|
||||||
|
const message = e instanceof Error ? e.message : String(e);
|
||||||
|
return err(`failed to resolve package root: ${message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
let buildResult: Awaited<ReturnType<typeof Bun.build>>;
|
||||||
|
try {
|
||||||
|
buildResult = await Bun.build({
|
||||||
|
entrypoints: [absoluteTsPath],
|
||||||
|
target: "node",
|
||||||
|
format: "esm",
|
||||||
|
external: ["node:*"],
|
||||||
|
root: rootDir,
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
const message = e instanceof Error ? e.message : String(e);
|
||||||
|
return err(`Bun.build failed: ${message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!buildResult.success) {
|
||||||
|
const logs = buildResult.logs.map((l) => l.message).join("; ");
|
||||||
|
return err(`Bun.build failed: ${logs || "unknown error"}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const entry = buildResult.outputs.find((o) => o.kind === "entry-point");
|
||||||
|
if (entry === undefined) {
|
||||||
|
return err("Bun.build produced no entry-point output");
|
||||||
|
}
|
||||||
|
|
||||||
|
let esmJsSource: string;
|
||||||
|
try {
|
||||||
|
esmJsSource = await entry.text();
|
||||||
|
} catch (e) {
|
||||||
|
const message = e instanceof Error ? e.message : String(e);
|
||||||
|
return err(`failed to read bundle output: ${message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const descriptorLoaded = await loadDescriptorFromSourceTs(absoluteTsPath);
|
||||||
|
if (!descriptorLoaded.ok) {
|
||||||
|
return descriptorLoaded;
|
||||||
|
}
|
||||||
|
const descriptor = descriptorLoaded.value;
|
||||||
|
|
||||||
|
const validated = validateWorkflowBundle({
|
||||||
|
filePath: joinVirtualEsmPath(absoluteTsPath),
|
||||||
|
source: esmJsSource,
|
||||||
|
});
|
||||||
|
if (!validated.ok) {
|
||||||
|
return validated;
|
||||||
|
}
|
||||||
|
|
||||||
|
const yamlSource = stringifyWorkflowDescriptor(descriptor);
|
||||||
|
const dtsSource = generateWorkflowBundleTypes(descriptor);
|
||||||
|
|
||||||
|
return ok({ esmJsSource, yamlSource, dtsSource });
|
||||||
|
}
|
||||||
|
|
||||||
|
function joinVirtualEsmPath(absoluteTsPath: string): string {
|
||||||
|
return `${absoluteTsPath}.esm.js`;
|
||||||
|
}
|
||||||
@@ -4,6 +4,8 @@ import type {
|
|||||||
ExportAllDeclaration,
|
ExportAllDeclaration,
|
||||||
ExportDefaultDeclaration,
|
ExportDefaultDeclaration,
|
||||||
ExportNamedDeclaration,
|
ExportNamedDeclaration,
|
||||||
|
ExportSpecifier,
|
||||||
|
FunctionDeclaration,
|
||||||
ImportDeclaration,
|
ImportDeclaration,
|
||||||
Node,
|
Node,
|
||||||
Program,
|
Program,
|
||||||
@@ -66,11 +68,84 @@ function walkAst(node: Node, visit: (n: Node) => void): void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function exportSpecifierIsDefaultReExport(spec: ExportSpecifier): boolean {
|
||||||
|
return spec.exported.type === "Identifier" && spec.exported.name === "default";
|
||||||
|
}
|
||||||
|
|
||||||
function programHasDefaultExport(body: readonly Node[]): boolean {
|
function programHasDefaultExport(body: readonly Node[]): boolean {
|
||||||
for (const stmt of body) {
|
for (const stmt of body) {
|
||||||
if (stmt.type === "ExportDefaultDeclaration") {
|
if (stmt.type === "ExportDefaultDeclaration") {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
if (stmt.type === "ExportNamedDeclaration") {
|
||||||
|
const named = stmt as ExportNamedDeclaration;
|
||||||
|
if (named.source !== null && named.source !== undefined) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for (const spec of named.specifiers) {
|
||||||
|
if (spec.type === "ExportSpecifier" && exportSpecifierIsDefaultReExport(spec)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function findDefaultExportLocalBindingName(program: Program): string | null {
|
||||||
|
for (const stmt of program.body) {
|
||||||
|
if (stmt.type !== "ExportNamedDeclaration") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const named = stmt as ExportNamedDeclaration;
|
||||||
|
if (named.source !== null && named.source !== undefined) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for (const spec of named.specifiers) {
|
||||||
|
if (spec.type !== "ExportSpecifier" || !exportSpecifierIsDefaultReExport(spec)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const loc = spec.local;
|
||||||
|
if (loc.type !== "Identifier") {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return loc.name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function bindingInitializerIsCallable(init: Node): boolean {
|
||||||
|
return (
|
||||||
|
init.type === "FunctionExpression" ||
|
||||||
|
init.type === "ArrowFunctionExpression" ||
|
||||||
|
init.type === "CallExpression"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function programDeclaresCallableExportBinding(program: Program, name: string): boolean {
|
||||||
|
for (const stmt of program.body) {
|
||||||
|
if (stmt.type === "FunctionDeclaration") {
|
||||||
|
const fd = stmt as FunctionDeclaration;
|
||||||
|
const id = fd.id;
|
||||||
|
if (id !== null && id !== undefined && id.type === "Identifier" && id.name === name) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (stmt.type === "VariableDeclaration") {
|
||||||
|
for (const decl of stmt.declarations) {
|
||||||
|
if (decl.id.type !== "Identifier" || decl.id.name !== name) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const init = decl.init;
|
||||||
|
if (init === null || init === undefined) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (bindingInitializerIsCallable(init)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -93,6 +168,11 @@ function defaultExportDeclarationIsCallable(program: Program): boolean {
|
|||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const exportBinding = findDefaultExportLocalBindingName(program);
|
||||||
|
if (exportBinding !== null) {
|
||||||
|
return programDeclaresCallableExportBinding(program, exportBinding);
|
||||||
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import { stringify } from "yaml";
|
||||||
|
|
||||||
|
import type { WorkflowDescriptor } from "./workflow-descriptor.js";
|
||||||
|
|
||||||
|
/** Serialize a validated workflow descriptor to YAML for storage next to the bundle. */
|
||||||
|
export function stringifyWorkflowDescriptor(descriptor: WorkflowDescriptor): string {
|
||||||
|
return stringify(descriptor, { indent: 2, defaultStringType: "QUOTE_DOUBLE" });
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import { jsonSchemaToTypeString } from "./json-schema-to-ts.js";
|
||||||
|
import type { WorkflowDescriptor } from "./workflow-descriptor.js";
|
||||||
|
|
||||||
|
function safePropertyName(name: string): string {
|
||||||
|
return /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(name) ? name : JSON.stringify(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Build the standard workflow bundle `.d.ts` from role JSON Schemas. */
|
||||||
|
export function generateWorkflowBundleTypes(descriptor: WorkflowDescriptor): string {
|
||||||
|
const roleLines: string[] = [];
|
||||||
|
for (const [roleName, role] of Object.entries(descriptor.roles)) {
|
||||||
|
const tsType = jsonSchemaToTypeString(role.schema);
|
||||||
|
roleLines.push(` ${safePropertyName(roleName)}: ${tsType};`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
`import type { WorkflowFn } from "@uncaged/workflow";`,
|
||||||
|
``,
|
||||||
|
`export type Roles = {`,
|
||||||
|
...roleLines,
|
||||||
|
`};`,
|
||||||
|
``,
|
||||||
|
`declare const workflow: WorkflowFn;`,
|
||||||
|
`export default workflow;`,
|
||||||
|
``,
|
||||||
|
].join("\n");
|
||||||
|
}
|
||||||
@@ -6,6 +6,13 @@ export {
|
|||||||
encodeUint64AsCrockford,
|
encodeUint64AsCrockford,
|
||||||
} from "./base32.js";
|
} from "./base32.js";
|
||||||
export { validateWorkflowBundle, type WorkflowBundleValidationInput } from "./bundle-validator.js";
|
export { validateWorkflowBundle, type WorkflowBundleValidationInput } from "./bundle-validator.js";
|
||||||
|
export {
|
||||||
|
buildWorkflowFromTypeScript,
|
||||||
|
type BuildPipelineResult,
|
||||||
|
} from "./build-pipeline.js";
|
||||||
|
export { stringifyWorkflowDescriptor } from "./generate-descriptor.js";
|
||||||
|
export { generateWorkflowBundleTypes } from "./generate-types.js";
|
||||||
|
export { jsonSchemaToTypeString } from "./json-schema-to-ts.js";
|
||||||
export { createRoleModerator } from "./create-role-moderator.js";
|
export { createRoleModerator } from "./create-role-moderator.js";
|
||||||
export {
|
export {
|
||||||
type ExecuteThreadIo,
|
type ExecuteThreadIo,
|
||||||
@@ -66,3 +73,9 @@ export {
|
|||||||
} from "./types.js";
|
} from "./types.js";
|
||||||
export { generateUlid } from "./ulid.js";
|
export { generateUlid } from "./ulid.js";
|
||||||
export { getWorkerHostScriptPath } from "./worker-entry-path.js";
|
export { getWorkerHostScriptPath } from "./worker-entry-path.js";
|
||||||
|
export {
|
||||||
|
validateWorkflowDescriptor,
|
||||||
|
type WorkflowDescriptor,
|
||||||
|
type WorkflowRoleDescriptor,
|
||||||
|
type WorkflowRoleSchema,
|
||||||
|
} from "./workflow-descriptor.js";
|
||||||
|
|||||||
@@ -0,0 +1,83 @@
|
|||||||
|
/**
|
||||||
|
* Convert a JSON Schema subset (object / string / number / integer / boolean / array)
|
||||||
|
* into a TypeScript type string for generated `.d.ts` files.
|
||||||
|
*/
|
||||||
|
export function jsonSchemaToTypeString(schema: unknown): string {
|
||||||
|
return schemaToTs(schema);
|
||||||
|
}
|
||||||
|
|
||||||
|
function schemaToTs(schema: unknown): string {
|
||||||
|
if (schema === null || typeof schema !== "object" || Array.isArray(schema)) {
|
||||||
|
return "unknown";
|
||||||
|
}
|
||||||
|
const s = schema as Record<string, unknown>;
|
||||||
|
|
||||||
|
if ("enum" in s && Array.isArray(s.enum) && s.enum.length > 0) {
|
||||||
|
const literals = s.enum
|
||||||
|
.filter((v): v is string | number | boolean => v !== null && v !== undefined)
|
||||||
|
.map((v) => (typeof v === "string" ? JSON.stringify(v) : String(v)));
|
||||||
|
if (literals.length === 0) {
|
||||||
|
return "unknown";
|
||||||
|
}
|
||||||
|
return literals.join(" | ");
|
||||||
|
}
|
||||||
|
|
||||||
|
const t = s.type;
|
||||||
|
if (t === "string") {
|
||||||
|
return "string";
|
||||||
|
}
|
||||||
|
if (t === "number" || t === "integer") {
|
||||||
|
return "number";
|
||||||
|
}
|
||||||
|
if (t === "boolean") {
|
||||||
|
return "boolean";
|
||||||
|
}
|
||||||
|
if (t === "array") {
|
||||||
|
const items = s.items;
|
||||||
|
if (items === undefined || items === null) {
|
||||||
|
return "unknown[]";
|
||||||
|
}
|
||||||
|
if (Array.isArray(items)) {
|
||||||
|
if (items.length === 0) {
|
||||||
|
return "unknown[]";
|
||||||
|
}
|
||||||
|
const parts = items.map((it) => schemaToTs(it));
|
||||||
|
return `[${parts.join(", ")}]`;
|
||||||
|
}
|
||||||
|
return `${schemaToTs(items)}[]`;
|
||||||
|
}
|
||||||
|
if (t === "object") {
|
||||||
|
const propsRaw = s.properties;
|
||||||
|
const requiredRaw = s.required;
|
||||||
|
const required = new Set<string>(
|
||||||
|
Array.isArray(requiredRaw)
|
||||||
|
? requiredRaw.filter((x): x is string => typeof x === "string")
|
||||||
|
: [],
|
||||||
|
);
|
||||||
|
|
||||||
|
if (propsRaw === null || propsRaw === undefined) {
|
||||||
|
return "Record<string, unknown>";
|
||||||
|
}
|
||||||
|
if (typeof propsRaw !== "object" || Array.isArray(propsRaw)) {
|
||||||
|
return "Record<string, unknown>";
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = propsRaw as Record<string, unknown>;
|
||||||
|
const entries = Object.entries(props);
|
||||||
|
if (entries.length === 0) {
|
||||||
|
return "{}";
|
||||||
|
}
|
||||||
|
|
||||||
|
const parts: string[] = [];
|
||||||
|
for (const [key, subSchema] of entries) {
|
||||||
|
const optional = !required.has(key);
|
||||||
|
const ts = schemaToTs(subSchema);
|
||||||
|
const safeKey = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(key) ? key : JSON.stringify(key);
|
||||||
|
const suffix = optional ? " | null" : "";
|
||||||
|
parts.push(`${safeKey}: ${ts}${suffix}`);
|
||||||
|
}
|
||||||
|
return `{ ${parts.join("; ")} }`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return "unknown";
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
import { err, ok, type Result } from "./result.js";
|
||||||
|
|
||||||
|
/** JSON Schema fragment describing one role's `meta` shape (subset supported by code generation). */
|
||||||
|
export type WorkflowRoleSchema = Record<string, unknown>;
|
||||||
|
|
||||||
|
export type WorkflowRoleDescriptor = {
|
||||||
|
description: string;
|
||||||
|
schema: WorkflowRoleSchema;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Workflow metadata exported as `export const descriptor` from TypeScript sources. */
|
||||||
|
export type WorkflowDescriptor = {
|
||||||
|
description: string;
|
||||||
|
roles: Record<string, WorkflowRoleDescriptor>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function validateWorkflowDescriptor(value: unknown): Result<WorkflowDescriptor, string> {
|
||||||
|
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
||||||
|
return err("descriptor must be a non-array object");
|
||||||
|
}
|
||||||
|
const root = value as Record<string, unknown>;
|
||||||
|
const description = root.description;
|
||||||
|
if (typeof description !== "string") {
|
||||||
|
return err("descriptor.description must be a string");
|
||||||
|
}
|
||||||
|
const rolesRaw = root.roles;
|
||||||
|
if (rolesRaw === null || typeof rolesRaw !== "object" || Array.isArray(rolesRaw)) {
|
||||||
|
return err("descriptor.roles must be a non-array object");
|
||||||
|
}
|
||||||
|
|
||||||
|
const roles: Record<string, WorkflowRoleDescriptor> = {};
|
||||||
|
for (const [roleName, specUnknown] of Object.entries(rolesRaw)) {
|
||||||
|
if (specUnknown === null || typeof specUnknown !== "object" || Array.isArray(specUnknown)) {
|
||||||
|
return err(`descriptor.roles.${roleName} must be a non-array object`);
|
||||||
|
}
|
||||||
|
const spec = specUnknown as Record<string, unknown>;
|
||||||
|
const roleDesc = spec.description;
|
||||||
|
if (typeof roleDesc !== "string") {
|
||||||
|
return err(`descriptor.roles.${roleName}.description must be a string`);
|
||||||
|
}
|
||||||
|
const schema = spec.schema;
|
||||||
|
if (schema === null || typeof schema !== "object" || Array.isArray(schema)) {
|
||||||
|
return err(`descriptor.roles.${roleName}.schema must be a non-array object`);
|
||||||
|
}
|
||||||
|
roles[roleName] = {
|
||||||
|
description: roleDesc,
|
||||||
|
schema: schema as WorkflowRoleSchema,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return ok({ description, roles });
|
||||||
|
}
|
||||||
Vendored
-17
@@ -1,17 +0,0 @@
|
|||||||
declare module "xxhashjs" {
|
|
||||||
type Digest = {
|
|
||||||
toString(radix?: number): string;
|
|
||||||
};
|
|
||||||
|
|
||||||
type Hasher64 = {
|
|
||||||
update(data: Buffer): Hasher64;
|
|
||||||
digest(): Digest;
|
|
||||||
};
|
|
||||||
|
|
||||||
type XXH = {
|
|
||||||
h64(seed: number): Hasher64;
|
|
||||||
};
|
|
||||||
|
|
||||||
const XXH: XXH;
|
|
||||||
export default XXH;
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user