@venn-lang/contracts 0.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.
- package/LICENSE +21 -0
- package/README.md +254 -0
- package/dist/index.d.ts +620 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1342 -0
- package/dist/index.js.map +1 -0
- package/dist/node.d.mts +287 -0
- package/dist/node.d.mts.map +1 -0
- package/dist/node.mjs +387 -0
- package/dist/node.mjs.map +1 -0
- package/dist/signal-source.types-BI3TYaCi.d.ts +317 -0
- package/dist/signal-source.types-BI3TYaCi.d.ts.map +1 -0
- package/dist/testing.d.ts +84 -0
- package/dist/testing.d.ts.map +1 -0
- package/dist/testing.js +276 -0
- package/dist/testing.js.map +1 -0
- package/package.json +77 -0
- package/src/capabilities/host-capability.ts +18 -0
- package/src/capabilities/index.ts +2 -0
- package/src/conformance/expect-venn-error.ts +18 -0
- package/src/conformance/harness.types.ts +14 -0
- package/src/conformance/index.ts +2 -0
- package/src/dotenv/dotenv-files.ts +30 -0
- package/src/dotenv/index.ts +2 -0
- package/src/dotenv/parse-dotenv.ts +34 -0
- package/src/errors/contract-codes.ts +37 -0
- package/src/errors/index.ts +7 -0
- package/src/errors/venn-error.ts +20 -0
- package/src/host/create-host.ts +16 -0
- package/src/host/create-node-host.ts +29 -0
- package/src/host/create-test-host.ts +28 -0
- package/src/host/create-worker-host.ts +32 -0
- package/src/host/host.types.ts +26 -0
- package/src/host/index.ts +5 -0
- package/src/host/unavailable.ts +23 -0
- package/src/index.ts +10 -0
- package/src/logger/console-logger.ts +12 -0
- package/src/logger/index.ts +3 -0
- package/src/logger/logger.types.ts +18 -0
- package/src/logger/memory-logger.ts +12 -0
- package/src/node.ts +8 -0
- package/src/port/assert-capabilities.ts +18 -0
- package/src/port/assert-port-shape.ts +18 -0
- package/src/port/bind-port.ts +28 -0
- package/src/port/index.ts +5 -0
- package/src/port/missing-capabilities.ts +14 -0
- package/src/port/port.types.ts +36 -0
- package/src/ports/clock/clock.port.ts +15 -0
- package/src/ports/clock/clock.types.ts +17 -0
- package/src/ports/clock/index.ts +4 -0
- package/src/ports/clock/system-clock.ts +9 -0
- package/src/ports/clock/virtual-clock.ts +22 -0
- package/src/ports/console/console.port.ts +13 -0
- package/src/ports/console/console.types.ts +17 -0
- package/src/ports/console/index.ts +3 -0
- package/src/ports/console/memory-console.ts +36 -0
- package/src/ports/console/node-console.ts +80 -0
- package/src/ports/file-system/file-system.errors.ts +10 -0
- package/src/ports/file-system/file-system.port.ts +12 -0
- package/src/ports/file-system/file-system.types.ts +27 -0
- package/src/ports/file-system/index.ts +6 -0
- package/src/ports/file-system/memory-fs.ts +46 -0
- package/src/ports/file-system/node-fs.ts +93 -0
- package/src/ports/index.ts +9 -0
- package/src/ports/lock-provider/fake-lock.ts +6 -0
- package/src/ports/lock-provider/in-process-lock.ts +26 -0
- package/src/ports/lock-provider/index.ts +4 -0
- package/src/ports/lock-provider/lock-provider.port.ts +16 -0
- package/src/ports/lock-provider/lock-provider.types.ts +8 -0
- package/src/ports/manifest-provider/default-manifest.ts +31 -0
- package/src/ports/manifest-provider/edit/edit-dependency.ts +82 -0
- package/src/ports/manifest-provider/edit/index.ts +7 -0
- package/src/ports/manifest-provider/edit/table-span.ts +39 -0
- package/src/ports/manifest-provider/index.ts +31 -0
- package/src/ports/manifest-provider/manifest.port.ts +16 -0
- package/src/ports/manifest-provider/manifest.types.ts +54 -0
- package/src/ports/manifest-provider/memory-manifest.ts +14 -0
- package/src/ports/manifest-provider/project.types.ts +94 -0
- package/src/ports/manifest-provider/read/index.ts +15 -0
- package/src/ports/manifest-provider/read/read-dependencies.ts +27 -0
- package/src/ports/manifest-provider/read/read-package.ts +32 -0
- package/src/ports/manifest-provider/read/read-profiles.ts +32 -0
- package/src/ports/manifest-provider/read/read-targets.ts +37 -0
- package/src/ports/manifest-provider/read/read-tooling.ts +15 -0
- package/src/ports/manifest-provider/read/read-workspace.ts +22 -0
- package/src/ports/manifest-provider/read/scalars.ts +35 -0
- package/src/ports/manifest-provider/read-run-settings.ts +38 -0
- package/src/ports/manifest-provider/resolve-alias.ts +22 -0
- package/src/ports/manifest-provider/toml/index.ts +3 -0
- package/src/ports/manifest-provider/toml/parse-toml.ts +52 -0
- package/src/ports/manifest-provider/toml/read-value.ts +104 -0
- package/src/ports/manifest-provider/toml/sections.ts +39 -0
- package/src/ports/manifest-provider/toml-docs.ts +27 -0
- package/src/ports/manifest-provider/toml-manifest.ts +41 -0
- package/src/ports/process-provider/fake-process.ts +32 -0
- package/src/ports/process-provider/index.ts +10 -0
- package/src/ports/process-provider/node-spawn.ts +52 -0
- package/src/ports/process-provider/process-provider.port.ts +13 -0
- package/src/ports/process-provider/process-provider.types.ts +47 -0
- package/src/ports/random/fixed-random.ts +10 -0
- package/src/ports/random/index.ts +4 -0
- package/src/ports/random/random.port.ts +13 -0
- package/src/ports/random/random.types.ts +10 -0
- package/src/ports/random/seeded-random.ts +19 -0
- package/src/ports/secret-provider/env-secrets.ts +19 -0
- package/src/ports/secret-provider/index.ts +6 -0
- package/src/ports/secret-provider/memory-secrets.ts +15 -0
- package/src/ports/secret-provider/secret-provider.port.ts +13 -0
- package/src/ports/secret-provider/secret-provider.types.ts +17 -0
- package/src/ports/secret-provider/secret.ts +17 -0
- package/src/ports/signal-source/fake-signals.ts +44 -0
- package/src/ports/signal-source/index.ts +11 -0
- package/src/ports/signal-source/node-signals.ts +33 -0
- package/src/ports/signal-source/signal-source.port.ts +13 -0
- package/src/ports/signal-source/signal-source.types.ts +24 -0
- package/src/testing.ts +16 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"testing.js","names":[],"sources":["../src/conformance/expect-venn-error.ts","../src/ports/clock/clock.suite.ts","../src/ports/file-system/file-system.suite.ts","../src/ports/lock-provider/lock-provider.suite.ts","../src/ports/manifest-provider/manifest.suite.ts","../src/ports/process-provider/process-provider.suite.ts","../src/ports/random/random.suite.ts","../src/ports/secret-provider/secret-provider.suite.ts","../src/ports/signal-source/signal-source.suite.ts"],"sourcesContent":["import { expect } from \"vitest\";\n\n/**\n * Asserts that an async operation rejects with a `VennError` whose `.code`\n * matches.\n *\n * Suites assert on the code and never on the message, so wording can be\n * improved without breaking a conformance run.\n *\n * @param args.op - the operation expected to reject.\n * @param args.code - pattern the error code must match, e.g. `/^VN8/`.\n */\nexport async function expectVennError(args: {\n op: () => Promise<unknown>;\n code: RegExp;\n}): Promise<void> {\n await expect(args.op()).rejects.toMatchObject({ code: expect.stringMatching(args.code) });\n}\n","import { describe, expect, it } from \"vitest\";\nimport type { ConformanceSpec } from \"../../conformance/index.js\";\nimport type { Clock } from \"./clock.types.js\";\n\n/** The {@link Clock} TCK. Both the system and virtual clocks satisfy it. */\nexport function clockConformance(spec: ConformanceSpec<Clock>): void {\n describe(`Clock · ${spec.name}`, () => {\n it(\"now() is non-decreasing\", async () => {\n const clock = await spec.factory();\n const first = clock.now();\n expect(clock.now()).toBeGreaterThanOrEqual(first);\n });\n\n it(\"sleep advances now() by about the requested delay\", async () => {\n const clock = await spec.factory();\n const before = clock.now();\n await clock.sleep(20);\n // A real timer may fire a millisecond or two early, so the bound is loose.\n // What must never happen is returning wildly short.\n expect(clock.now()).toBeGreaterThanOrEqual(before + 18);\n });\n });\n}\n","import { fc, test } from \"@fast-check/vitest\";\nimport { describe, expect, it } from \"vitest\";\nimport { type ConformanceSpec, expectVennError } from \"../../conformance/index.js\";\nimport type { FileSystem } from \"./file-system.types.js\";\n\n/** The {@link FileSystem} TCK. Real and double both run it. */\nexport function fileSystemConformance(spec: ConformanceSpec<FileSystem>): void {\n describe(`FileSystem · ${spec.name}`, () => {\n it(\"read returns exactly the bytes written\", async () => {\n const fs = await spec.factory();\n const bytes = new Uint8Array([1, 2, 3, 4]);\n await fs.write(\"a.bin\", bytes);\n expect(await fs.read(\"a.bin\")).toEqual(bytes);\n });\n\n it(\"reading a missing path fails with VN8, never a raw error\", async () => {\n const fs = await spec.factory();\n await expectVennError({ op: () => fs.read(\"missing.bin\"), code: /^VN8/ });\n });\n\n it(\"exists reflects write then remove\", async () => {\n const fs = await spec.factory();\n await fs.write(\"k.bin\", new Uint8Array([0]));\n expect(await fs.exists(\"k.bin\")).toBe(true);\n await fs.remove(\"k.bin\");\n expect(await fs.exists(\"k.bin\")).toBe(false);\n });\n\n /**\n * Bounded in both draws and payload, because `node-fs` runs this against a\n * real disk. Twenty-five draws demonstrate last-write-wins as well as\n * fast-check's default hundred, and the raised timeout means a loaded\n * machine reports a slow disk rather than a failure.\n */\n test.prop([fc.uint8Array({ maxLength: 4096 })], { numRuns: 25 })(\n \"write is last-write-wins\",\n async (bytes) => {\n const fs = await spec.factory();\n await fs.write(\"k.bin\", new Uint8Array([9]));\n await fs.write(\"k.bin\", bytes);\n expect(await fs.read(\"k.bin\")).toEqual(bytes);\n },\n 20_000,\n );\n\n /**\n * A workspace's `members = [\"packages/*\"]` rests on listing, so the two\n * implementations have to agree about it. The double has no directories at\n * all, only paths, which is exactly why this is asked of both.\n */\n it(\"lists what a directory holds, one level deep\", async () => {\n const fs = await spec.factory();\n await fs.write(\"pkg/a/venn.toml\", new Uint8Array([1]));\n await fs.write(\"pkg/b/venn.toml\", new Uint8Array([1]));\n await fs.write(\"pkg/top.txt\", new Uint8Array([1]));\n\n const names = (await fs.list(\"pkg\")).map((entry) => entry.name).sort();\n expect(names).toEqual([\"a\", \"b\", \"top.txt\"]);\n });\n\n it(\"says which of them hold more\", async () => {\n const fs = await spec.factory();\n await fs.write(\"root/inside/deep.txt\", new Uint8Array([1]));\n await fs.write(\"root/flat.txt\", new Uint8Array([1]));\n\n const found = await fs.list(\"root\");\n expect(found.find((entry) => entry.name === \"inside\")?.directory).toBe(true);\n expect(found.find((entry) => entry.name === \"flat.txt\")?.directory).toBe(false);\n });\n\n /** Asking what is inside something that holds nothing has an answer. */\n it(\"reads a path that is not a directory as empty\", async () => {\n const fs = await spec.factory();\n expect(await fs.list(\"nao/existe\")).toEqual([]);\n });\n });\n}\n","import { describe, expect, it } from \"vitest\";\nimport type { ConformanceSpec } from \"../../conformance/index.js\";\nimport type { LockProvider } from \"./lock-provider.types.js\";\n\n/**\n * The {@link LockProvider} TCK. Acquiring, releasing and acquiring again must\n * not deadlock, which is the one law the fake and the real mutex share.\n */\nexport function lockProviderConformance(spec: ConformanceSpec<LockProvider>): void {\n describe(`LockProvider · ${spec.name}`, () => {\n it(\"acquire returns a release, and re-acquiring after release works\", async () => {\n const lock = await spec.factory();\n const release = await lock.acquire(\"k\");\n release();\n const again = await lock.acquire(\"k\");\n again();\n expect(typeof again).toBe(\"function\");\n });\n });\n}\n","import { describe, expect, it } from \"vitest\";\nimport type { ConformanceSpec } from \"../../conformance/index.js\";\nimport type { ManifestProvider } from \"./manifest.types.js\";\n\n/**\n * The {@link ManifestProvider} TCK. Whatever the source, `load()` returns a\n * manifest with a name and an env map, never a partial one.\n */\nexport function manifestProviderConformance(\n spec: ConformanceSpec<ManifestProvider> & { expectedName: string },\n): void {\n describe(`ManifestProvider · ${spec.name}`, () => {\n it(\"loads a manifest with a name and env map\", async () => {\n const manifest = (await spec.factory()).load();\n expect(manifest.name).toBe(spec.expectedName);\n expect(typeof manifest.env).toBe(\"object\");\n });\n });\n}\n","import { describe, expect, it } from \"vitest\";\nimport type { ConformanceSpec } from \"../../conformance/index.js\";\nimport type { ProcessProvider, SpawnArgs } from \"./process-provider.types.js\";\n\n/**\n * The {@link ProcessProvider} TCK.\n *\n * Each implementation is asked to run something that says a known thing and\n * ends with a known code. *What* it is asked to run differs, since a fake has\n * no shell and the real one has no script, but the answer must not.\n */\nexport function processProviderConformance(\n spec: ConformanceSpec<ProcessProvider> & {\n runs: SpawnArgs;\n expected: { code: number; output: string };\n },\n): void {\n describe(`ProcessProvider · ${spec.name}`, () => {\n it(\"waits for the command and reports how it ended\", async () => {\n const proc = await spec.factory();\n\n const result = await proc.spawn(spec.runs).wait();\n\n expect(result.code).toBe(spec.expected.code);\n expect(result.output).toContain(spec.expected.output);\n });\n\n /** Half a minute of silence reads as a command that has hung. */\n it(\"streams what the command writes, as it writes it\", async () => {\n const proc = await spec.factory();\n const seen: string[] = [];\n\n await proc.spawn({ ...spec.runs, onOutput: (chunk) => seen.push(chunk) }).wait();\n\n expect(seen.join(\"\")).toContain(spec.expected.output);\n });\n\n it(\"hands back a handle that can be asked to stop\", async () => {\n const proc = await spec.factory();\n const handle = proc.spawn(spec.runs);\n\n expect(typeof handle.pid).toBe(\"number\");\n expect(() => handle.kill()).not.toThrow();\n });\n });\n}\n","import { describe, expect, it } from \"vitest\";\nimport type { ConformanceSpec } from \"../../conformance/index.js\";\nimport type { Random } from \"./random.types.js\";\n\n/** The {@link Random} TCK: the range laws both seeded and fixed satisfy. */\nexport function randomConformance(spec: ConformanceSpec<Random>): void {\n describe(`Random · ${spec.name}`, () => {\n it(\"next() stays within [0, 1)\", async () => {\n const random = await spec.factory();\n for (let i = 0; i < 100; i++) {\n const value = random.next();\n expect(value).toBeGreaterThanOrEqual(0);\n expect(value).toBeLessThan(1);\n }\n });\n\n it(\"int(min, max) stays within the inclusive range\", async () => {\n const random = await spec.factory();\n for (let i = 0; i < 100; i++) {\n const value = random.int(3, 7);\n expect(value).toBeGreaterThanOrEqual(3);\n expect(value).toBeLessThanOrEqual(7);\n }\n });\n });\n}\n","import { describe, expect, it } from \"vitest\";\nimport type { ConformanceSpec } from \"../../conformance/index.js\";\nimport type { SecretProvider } from \"./secret-provider.types.js\";\n\n/** A secret the implementation under test is known to hold. */\nexport type KnownSecret = { name: string; raw: string };\n\n/**\n * The {@link SecretProvider} TCK. The redaction law is the load-bearing one:\n * an implementation that leaks through `JSON.stringify` fails here.\n */\nexport function secretProviderConformance(\n spec: ConformanceSpec<SecretProvider> & { known: KnownSecret },\n): void {\n describe(`SecretProvider · ${spec.name}`, () => {\n it(\"reveals the known secret\", async () => {\n const secrets = await spec.factory();\n expect(secrets.get(spec.known.name)?.reveal()).toBe(spec.known.raw);\n });\n\n it(\"never leaks the raw value through serialization\", async () => {\n const secrets = await spec.factory();\n const secret = secrets.get(spec.known.name);\n expect(JSON.stringify(secret)).not.toContain(spec.known.raw);\n expect(String(secret)).not.toContain(spec.known.raw);\n });\n });\n}\n","import { describe, expect, it } from \"vitest\";\nimport type { SignalSource, SystemSignal } from \"./signal-source.types.js\";\n\n/** How a suite delivers a signal to the implementation it is testing. */\nexport interface SignalSpec {\n name: string;\n factory(): SignalSource;\n /** Deliver `signal` the way this implementation really receives one. */\n raise(args: { source: SignalSource; signal: SystemSignal }): void;\n /** The signal this implementation can be asked to deliver here. */\n delivers: SystemSignal;\n /** A second signal, subscribed to but never raised, to prove they stay apart. */\n other: SystemSignal;\n}\n\n/**\n * The {@link SignalSource} TCK. The process and the fake both run it, because a\n * double that delivers differently is a test that lies about shutdown.\n */\nexport function signalSourceConformance(spec: SignalSpec): void {\n describe(`SignalSource · ${spec.name}`, () => {\n const listen = () => {\n const source = spec.factory();\n const seen: SystemSignal[] = [];\n return { source, seen, raise: () => spec.raise({ source, signal: spec.delivers }) };\n };\n\n it(\"calls the handler with the signal that arrived\", () => {\n const { source, seen, raise } = listen();\n const off = source.on(spec.delivers, (signal) => seen.push(signal));\n\n raise();\n off();\n\n expect(seen).toEqual([spec.delivers]);\n });\n\n it(\"calls every handler waiting on that signal\", () => {\n const { source, seen, raise } = listen();\n const first = source.on(spec.delivers, () => seen.push(spec.delivers));\n const second = source.on(spec.delivers, () => seen.push(spec.delivers));\n\n raise();\n first();\n second();\n\n expect(seen).toHaveLength(2);\n });\n\n it(\"leaves other signals alone\", () => {\n const { source, seen, raise } = listen();\n const off = source.on(spec.other, (signal) => seen.push(signal));\n\n raise();\n off();\n\n expect(seen).toEqual([]);\n });\n\n it(\"stops delivering once unsubscribed, and unsubscribing twice is harmless\", () => {\n const { source, seen, raise } = listen();\n const off = source.on(spec.delivers, (signal) => seen.push(signal));\n\n off();\n off();\n raise();\n\n expect(seen).toEqual([]);\n });\n\n // A signal is not a one-shot: a program that ignores the second Ctrl+C is a\n // program the user cannot get out of.\n it(\"keeps delivering while the subscription lives\", () => {\n const { source, seen, raise } = listen();\n const off = source.on(spec.delivers, (signal) => seen.push(signal));\n\n raise();\n raise();\n off();\n\n expect(seen).toEqual([spec.delivers, spec.delivers]);\n });\n });\n}\n"],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@venn-lang/contracts",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "The ports the Venn core runs on, their implementations, and the host that carries them.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"venn",
|
|
7
|
+
"testing",
|
|
8
|
+
"e2e",
|
|
9
|
+
"ports",
|
|
10
|
+
"adapters",
|
|
11
|
+
"conformance"
|
|
12
|
+
],
|
|
13
|
+
"homepage": "https://github.com/venn-lang/venn/tree/main/packages/contracts#readme",
|
|
14
|
+
"bugs": "https://github.com/venn-lang/venn/issues",
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": "git+https://github.com/venn-lang/venn.git",
|
|
18
|
+
"directory": "packages/contracts"
|
|
19
|
+
},
|
|
20
|
+
"license": "MIT",
|
|
21
|
+
"author": "Vinicius Borges",
|
|
22
|
+
"type": "module",
|
|
23
|
+
"sideEffects": false,
|
|
24
|
+
"exports": {
|
|
25
|
+
".": {
|
|
26
|
+
"development": "./src/index.ts",
|
|
27
|
+
"types": "./dist/index.d.ts",
|
|
28
|
+
"import": "./dist/index.js",
|
|
29
|
+
"default": "./dist/index.js"
|
|
30
|
+
},
|
|
31
|
+
"./node": {
|
|
32
|
+
"development": "./src/node.ts",
|
|
33
|
+
"types": "./dist/node.d.mts",
|
|
34
|
+
"import": "./dist/node.mjs",
|
|
35
|
+
"default": "./dist/node.mjs"
|
|
36
|
+
},
|
|
37
|
+
"./testing": {
|
|
38
|
+
"development": "./src/testing.ts",
|
|
39
|
+
"types": "./dist/testing.d.ts",
|
|
40
|
+
"import": "./dist/testing.js",
|
|
41
|
+
"default": "./dist/testing.js"
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
"files": [
|
|
45
|
+
"dist",
|
|
46
|
+
"src",
|
|
47
|
+
"!src/**/*.test.ts",
|
|
48
|
+
"!src/**/*.suite.ts"
|
|
49
|
+
],
|
|
50
|
+
"publishConfig": {
|
|
51
|
+
"access": "public"
|
|
52
|
+
},
|
|
53
|
+
"peerDependencies": {
|
|
54
|
+
"@fast-check/vitest": ">=0.4.0",
|
|
55
|
+
"vitest": ">=4.0.0"
|
|
56
|
+
},
|
|
57
|
+
"devDependencies": {
|
|
58
|
+
"@fast-check/vitest": "^0.4.1",
|
|
59
|
+
"@types/node": "^24",
|
|
60
|
+
"tsdown": "^0.22.14",
|
|
61
|
+
"typescript": "^7.0.2",
|
|
62
|
+
"vitest": "^4.1.10"
|
|
63
|
+
},
|
|
64
|
+
"peerDependenciesMeta": {
|
|
65
|
+
"@fast-check/vitest": {
|
|
66
|
+
"optional": true
|
|
67
|
+
},
|
|
68
|
+
"vitest": {
|
|
69
|
+
"optional": true
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
"scripts": {
|
|
73
|
+
"build": "tsdown",
|
|
74
|
+
"test": "vitest run",
|
|
75
|
+
"typecheck": "tsc --noEmit"
|
|
76
|
+
}
|
|
77
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Every capability a Host may expose. A port or plugin declares which it
|
|
3
|
+
* requires, a host advertises which it provides, and negotiation compares the
|
|
4
|
+
* two before anything binds.
|
|
5
|
+
*/
|
|
6
|
+
export const ALL_CAPABILITIES = [
|
|
7
|
+
"fs",
|
|
8
|
+
"process",
|
|
9
|
+
"net",
|
|
10
|
+
"clock",
|
|
11
|
+
"random",
|
|
12
|
+
"secrets",
|
|
13
|
+
"log",
|
|
14
|
+
"io",
|
|
15
|
+
] as const;
|
|
16
|
+
|
|
17
|
+
/** One capability name, drawn from {@link ALL_CAPABILITIES}. */
|
|
18
|
+
export type HostCapability = (typeof ALL_CAPABILITIES)[number];
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { expect } from "vitest";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Asserts that an async operation rejects with a `VennError` whose `.code`
|
|
5
|
+
* matches.
|
|
6
|
+
*
|
|
7
|
+
* Suites assert on the code and never on the message, so wording can be
|
|
8
|
+
* improved without breaking a conformance run.
|
|
9
|
+
*
|
|
10
|
+
* @param args.op - the operation expected to reject.
|
|
11
|
+
* @param args.code - pattern the error code must match, e.g. `/^VN8/`.
|
|
12
|
+
*/
|
|
13
|
+
export async function expectVennError(args: {
|
|
14
|
+
op: () => Promise<unknown>;
|
|
15
|
+
code: RegExp;
|
|
16
|
+
}): Promise<void> {
|
|
17
|
+
await expect(args.op()).rejects.toMatchObject({ code: expect.stringMatching(args.code) });
|
|
18
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Builds a fresh implementation for a single conformance run.
|
|
3
|
+
*
|
|
4
|
+
* Called once per test rather than once per suite, so no test can observe state
|
|
5
|
+
* another test left behind.
|
|
6
|
+
*/
|
|
7
|
+
export type PortFactory<T> = () => T | Promise<T>;
|
|
8
|
+
|
|
9
|
+
/** One implementation under test. */
|
|
10
|
+
export interface ConformanceSpec<T> {
|
|
11
|
+
/** Shown in the test name, e.g. "memory" or "node". */
|
|
12
|
+
readonly name: string;
|
|
13
|
+
readonly factory: PortFactory<T>;
|
|
14
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// biome-ignore-all lint/suspicious/noTemplateCurlyInString: ${name} is a placeholder in a path, not a JavaScript template.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The dotenv files read when `venn.toml` does not name its own.
|
|
5
|
+
*
|
|
6
|
+
* The order is the precedence, lowest first: what everyone shares, then what
|
|
7
|
+
* this environment adds, then what this machine keeps to itself. The `.local`
|
|
8
|
+
* ones are what a `.gitignore` is for.
|
|
9
|
+
*/
|
|
10
|
+
export const DOTENV_CONVENTION: readonly string[] = [
|
|
11
|
+
".env",
|
|
12
|
+
".env.${name}",
|
|
13
|
+
".env.local",
|
|
14
|
+
".env.${name}.local",
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Which files to read, in order, for one environment.
|
|
19
|
+
*
|
|
20
|
+
* The runner and the editor both ask this, so they cannot disagree about where
|
|
21
|
+
* a value lives.
|
|
22
|
+
*
|
|
23
|
+
* @param args.configured - `[env] files` from `venn.toml`. Empty or absent
|
|
24
|
+
* falls back to {@link DOTENV_CONVENTION}.
|
|
25
|
+
* @param args.name - the selected environment, substituted for `${name}`.
|
|
26
|
+
*/
|
|
27
|
+
export function dotenvFiles(args: { configured?: readonly string[]; name: string }): string[] {
|
|
28
|
+
const names = args.configured?.length ? args.configured : DOTENV_CONVENTION;
|
|
29
|
+
return names.map((each) => each.replaceAll("${name}", args.name));
|
|
30
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Read a `.env` file: `NAME=value`, one per line.
|
|
3
|
+
*
|
|
4
|
+
* Deliberately small. It understands comments, blank lines, an `export `
|
|
5
|
+
* prefix, and quotes around a value that needs them. No variable expansion:
|
|
6
|
+
* `${OTHER}` is left alone, because Venn already interpolates in its own
|
|
7
|
+
* strings and two syntaxes for one idea is how people get surprised.
|
|
8
|
+
*
|
|
9
|
+
* @returns every name found, later lines winning over earlier ones.
|
|
10
|
+
*/
|
|
11
|
+
export function parseDotenv(content: string): Record<string, string> {
|
|
12
|
+
const out: Record<string, string> = {};
|
|
13
|
+
for (const line of content.split(/\r?\n/)) {
|
|
14
|
+
const entry = readLine(line);
|
|
15
|
+
if (entry) out[entry.name] = entry.value;
|
|
16
|
+
}
|
|
17
|
+
return out;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const LINE = /^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)$/;
|
|
21
|
+
|
|
22
|
+
function readLine(line: string): { name: string; value: string } | undefined {
|
|
23
|
+
if (/^\s*(#|$)/.test(line)) return undefined;
|
|
24
|
+
const match = LINE.exec(line);
|
|
25
|
+
if (!match?.[1]) return undefined;
|
|
26
|
+
return { name: match[1], value: unquote((match[2] ?? "").trim()) };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** A quoted value keeps its spaces and its `#`; an unquoted one stops at a comment. */
|
|
30
|
+
function unquote(raw: string): string {
|
|
31
|
+
const quoted = /^(['"])([\s\S]*)\1$/.exec(raw);
|
|
32
|
+
if (quoted) return quoted[2] as string;
|
|
33
|
+
return (raw.split(" #")[0] ?? "").trim();
|
|
34
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { HostCapability } from "../capabilities/index.js";
|
|
2
|
+
import { VennError } from "./venn-error.js";
|
|
3
|
+
|
|
4
|
+
/** VN2010: a port requires capabilities the host does not provide. */
|
|
5
|
+
export function hostMissingCapability(args: {
|
|
6
|
+
portId: string;
|
|
7
|
+
missing: readonly HostCapability[];
|
|
8
|
+
present: readonly HostCapability[];
|
|
9
|
+
}): VennError {
|
|
10
|
+
const message =
|
|
11
|
+
`Port "${args.portId}" requires capability ${quote(args.missing)}, ` +
|
|
12
|
+
`which this host does not provide. Present: ${list(args.present)}.`;
|
|
13
|
+
return new VennError({ code: "VN2010", message, detail: { ...args } });
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** VN2011: an implementation is missing one or more methods the port declares. */
|
|
17
|
+
export function portShapeMismatch(args: { portId: string; missing: readonly string[] }): VennError {
|
|
18
|
+
const message = `Implementation of "${args.portId}" is missing method(s): ${args.missing.join(", ")}.`;
|
|
19
|
+
return new VennError({ code: "VN2011", message, detail: { ...args } });
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** VN2012: code reached a capability the host declared unavailable. */
|
|
23
|
+
export function capabilityUnavailable(args: {
|
|
24
|
+
capability: HostCapability;
|
|
25
|
+
method: string;
|
|
26
|
+
}): VennError {
|
|
27
|
+
const message = `Capability "${args.capability}" is not available on this host (called "${args.method}").`;
|
|
28
|
+
return new VennError({ code: "VN2012", message, detail: { ...args } });
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function quote(caps: readonly string[]): string {
|
|
32
|
+
return caps.map((c) => `"${c}"`).join(", ");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function list(caps: readonly string[]): string {
|
|
36
|
+
return caps.length === 0 ? "(none)" : caps.join(", ");
|
|
37
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/** Structured, serializable, redaction-safe detail attached to a {@link VennError}. */
|
|
2
|
+
export type VennErrorDetail = Readonly<Record<string, unknown>>;
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The single error type that crosses a contracts boundary.
|
|
6
|
+
*
|
|
7
|
+
* Every failure carries a stable `VNxxxx` code, so conformance suites assert on
|
|
8
|
+
* `.code` and never on prose. Messages stay free to improve.
|
|
9
|
+
*/
|
|
10
|
+
export class VennError extends Error {
|
|
11
|
+
readonly code: string;
|
|
12
|
+
readonly detail: VennErrorDetail | undefined;
|
|
13
|
+
|
|
14
|
+
constructor(args: { code: string; message: string; detail?: VennErrorDetail }) {
|
|
15
|
+
super(args.message);
|
|
16
|
+
this.name = "VennError";
|
|
17
|
+
this.code = args.code;
|
|
18
|
+
this.detail = args.detail;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { createTestHost } from "./create-test-host.js";
|
|
2
|
+
import { createWorkerHost } from "./create-worker-host.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The neutral host assemblers.
|
|
6
|
+
*
|
|
7
|
+
* `node` is deliberately absent: it pulls `node:*`, so it lives behind
|
|
8
|
+
* `@venn-lang/contracts/node` as `createNodeHost` and this entry stays Worker-safe.
|
|
9
|
+
*/
|
|
10
|
+
export const createHost: {
|
|
11
|
+
readonly worker: typeof createWorkerHost;
|
|
12
|
+
readonly test: typeof createTestHost;
|
|
13
|
+
} = {
|
|
14
|
+
worker: createWorkerHost,
|
|
15
|
+
test: createTestHost,
|
|
16
|
+
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { ALL_CAPABILITIES } from "../capabilities/index.js";
|
|
2
|
+
import { createConsoleLogger } from "../logger/index.js";
|
|
3
|
+
import { createSystemClock } from "../ports/clock/index.js";
|
|
4
|
+
import { createNodeFs } from "../ports/file-system/node-fs.js";
|
|
5
|
+
import { createInProcessLock } from "../ports/lock-provider/index.js";
|
|
6
|
+
import { createNodeSpawn } from "../ports/process-provider/node-spawn.js";
|
|
7
|
+
import { createSeededRandom } from "../ports/random/index.js";
|
|
8
|
+
import { createEnvSecrets } from "../ports/secret-provider/index.js";
|
|
9
|
+
import type { Host } from "./host.types.js";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Host for the CLI: the real file system, secrets from the environment, and
|
|
13
|
+
* every capability.
|
|
14
|
+
*
|
|
15
|
+
* @param args.root - where relative paths resolve. Defaults to the process's
|
|
16
|
+
* current directory.
|
|
17
|
+
*/
|
|
18
|
+
export function createNodeHost(args: { root?: string } = {}): Host {
|
|
19
|
+
return {
|
|
20
|
+
fs: createNodeFs({ root: args.root }),
|
|
21
|
+
proc: createNodeSpawn(),
|
|
22
|
+
clock: createSystemClock(),
|
|
23
|
+
random: createSeededRandom({ seed: 1 }),
|
|
24
|
+
secrets: createEnvSecrets(),
|
|
25
|
+
log: createConsoleLogger(),
|
|
26
|
+
lock: createInProcessLock(),
|
|
27
|
+
caps: ALL_CAPABILITIES,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { ALL_CAPABILITIES } from "../capabilities/index.js";
|
|
2
|
+
import { createMemoryLogger } from "../logger/index.js";
|
|
3
|
+
import { createVirtualClock } from "../ports/clock/index.js";
|
|
4
|
+
import { createMemoryFs } from "../ports/file-system/index.js";
|
|
5
|
+
import { createInProcessLock } from "../ports/lock-provider/index.js";
|
|
6
|
+
import { createFakeProcess } from "../ports/process-provider/index.js";
|
|
7
|
+
import { createSeededRandom } from "../ports/random/index.js";
|
|
8
|
+
import { createMemorySecrets } from "../ports/secret-provider/index.js";
|
|
9
|
+
import type { Host } from "./host.types.js";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* A host of doubles, every capability granted. The default for tests.
|
|
13
|
+
*
|
|
14
|
+
* @param overrides - replaces individual members, e.g. a real clock.
|
|
15
|
+
*/
|
|
16
|
+
export function createTestHost(overrides: Partial<Host> = {}): Host {
|
|
17
|
+
return {
|
|
18
|
+
fs: createMemoryFs(),
|
|
19
|
+
proc: createFakeProcess(),
|
|
20
|
+
clock: createVirtualClock(),
|
|
21
|
+
random: createSeededRandom({ seed: 1 }),
|
|
22
|
+
secrets: createMemorySecrets({ values: {} }),
|
|
23
|
+
log: createMemoryLogger(),
|
|
24
|
+
lock: createInProcessLock(),
|
|
25
|
+
caps: ALL_CAPABILITIES,
|
|
26
|
+
...overrides,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { createMemoryLogger } from "../logger/index.js";
|
|
2
|
+
import { createSystemClock } from "../ports/clock/index.js";
|
|
3
|
+
import { createMemoryFs } from "../ports/file-system/index.js";
|
|
4
|
+
import { createInProcessLock } from "../ports/lock-provider/index.js";
|
|
5
|
+
import type { ProcessProvider } from "../ports/process-provider/index.js";
|
|
6
|
+
import { ProcessProviderPort } from "../ports/process-provider/index.js";
|
|
7
|
+
import { createSeededRandom } from "../ports/random/index.js";
|
|
8
|
+
import { createMemorySecrets } from "../ports/secret-provider/index.js";
|
|
9
|
+
import type { Host } from "./host.types.js";
|
|
10
|
+
import { unavailable } from "./unavailable.js";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Host for a Web Worker: an in-memory file system, no `process`, no `net`.
|
|
14
|
+
*
|
|
15
|
+
* `proc` is a stand-in whose every method throws VN2012, so reaching for a
|
|
16
|
+
* subprocess in the editor fails with a diagnostic instead of a `TypeError`.
|
|
17
|
+
*/
|
|
18
|
+
export function createWorkerHost(): Host {
|
|
19
|
+
return {
|
|
20
|
+
fs: createMemoryFs(),
|
|
21
|
+
proc: unavailable<ProcessProvider>({
|
|
22
|
+
capability: "process",
|
|
23
|
+
methods: ProcessProviderPort.methods,
|
|
24
|
+
}),
|
|
25
|
+
clock: createSystemClock(),
|
|
26
|
+
random: createSeededRandom({ seed: 1 }),
|
|
27
|
+
secrets: createMemorySecrets({ values: {} }),
|
|
28
|
+
log: createMemoryLogger(),
|
|
29
|
+
lock: createInProcessLock(),
|
|
30
|
+
caps: ["fs", "clock", "random", "secrets", "log"],
|
|
31
|
+
};
|
|
32
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { HostCapability } from "../capabilities/index.js";
|
|
2
|
+
import type { Logger } from "../logger/index.js";
|
|
3
|
+
import type { Clock } from "../ports/clock/index.js";
|
|
4
|
+
import type { FileSystem } from "../ports/file-system/index.js";
|
|
5
|
+
import type { LockProvider } from "../ports/lock-provider/index.js";
|
|
6
|
+
import type { ProcessProvider } from "../ports/process-provider/index.js";
|
|
7
|
+
import type { Random } from "../ports/random/index.js";
|
|
8
|
+
import type { SecretProvider } from "../ports/secret-provider/index.js";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Everything the core is allowed to reach: assembled at the entry point, then
|
|
12
|
+
* passed inward. The core receives this and imports nothing else.
|
|
13
|
+
*
|
|
14
|
+
* Three assemblers build one: `createHost.worker`, `createHost.test`, and
|
|
15
|
+
* `createNodeHost` from `@venn-lang/contracts/node`.
|
|
16
|
+
*/
|
|
17
|
+
export interface Host {
|
|
18
|
+
readonly fs: FileSystem;
|
|
19
|
+
readonly proc: ProcessProvider;
|
|
20
|
+
readonly clock: Clock;
|
|
21
|
+
readonly random: Random;
|
|
22
|
+
readonly secrets: SecretProvider;
|
|
23
|
+
readonly log: Logger;
|
|
24
|
+
readonly lock: LockProvider;
|
|
25
|
+
readonly caps: readonly HostCapability[];
|
|
26
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { HostCapability } from "../capabilities/index.js";
|
|
2
|
+
import { capabilityUnavailable } from "../errors/index.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* A stand-in for a port the host cannot provide, such as `process` in a Worker.
|
|
6
|
+
*
|
|
7
|
+
* @param args.capability - the capability the host lacks, named in the error.
|
|
8
|
+
* @param args.methods - the port's declared methods, usually `SomePort.methods`.
|
|
9
|
+
* @returns an object shaped like `T` whose every method throws VN2012 when
|
|
10
|
+
* called, rather than failing as a `TypeError` mid-run.
|
|
11
|
+
*/
|
|
12
|
+
export function unavailable<T>(args: {
|
|
13
|
+
capability: HostCapability;
|
|
14
|
+
methods: readonly string[];
|
|
15
|
+
}): T {
|
|
16
|
+
const bag: Record<string, unknown> = {};
|
|
17
|
+
for (const method of args.methods) {
|
|
18
|
+
bag[method] = () => {
|
|
19
|
+
throw capabilityUnavailable({ capability: args.capability, method });
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
return bag as T;
|
|
23
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// @venn-lang/contracts: the neutral core. Everything reachable from here is
|
|
2
|
+
// Worker-safe. Implementations backed by `node:*` live behind the "./node"
|
|
3
|
+
// subpath, conformance suites behind "./testing".
|
|
4
|
+
export * from "./capabilities/index.js";
|
|
5
|
+
export { DOTENV_CONVENTION, dotenvFiles, parseDotenv } from "./dotenv/index.js";
|
|
6
|
+
export * from "./errors/index.js";
|
|
7
|
+
export * from "./host/index.js";
|
|
8
|
+
export * from "./logger/index.js";
|
|
9
|
+
export * from "./port/index.js";
|
|
10
|
+
export * from "./ports/index.js";
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { Logger } from "./logger.types.js";
|
|
2
|
+
|
|
3
|
+
/** Logger backed by the global console, so it works in a Worker and in Node. */
|
|
4
|
+
export function createConsoleLogger(): Logger {
|
|
5
|
+
return {
|
|
6
|
+
log(entry) {
|
|
7
|
+
const line = `[${entry.level}] ${entry.message}`;
|
|
8
|
+
if (entry.level === "error") console.error(line);
|
|
9
|
+
else console.log(line);
|
|
10
|
+
},
|
|
11
|
+
};
|
|
12
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/** How loudly one entry asks to be heard. */
|
|
2
|
+
export type LogLevel = "debug" | "info" | "warn" | "error";
|
|
3
|
+
|
|
4
|
+
/** One line of log, as data rather than as formatted text. */
|
|
5
|
+
export interface LogEntry {
|
|
6
|
+
readonly level: LogLevel;
|
|
7
|
+
readonly message: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** The minimal structured logger a host exposes. */
|
|
11
|
+
export interface Logger {
|
|
12
|
+
log(entry: LogEntry): void;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** A {@link Logger} that also retains what it was given, for assertions. */
|
|
16
|
+
export interface MemoryLogger extends Logger {
|
|
17
|
+
readonly entries: readonly LogEntry[];
|
|
18
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { LogEntry, MemoryLogger } from "./logger.types.js";
|
|
2
|
+
|
|
3
|
+
/** The double: keeps every entry in `entries` instead of printing it. */
|
|
4
|
+
export function createMemoryLogger(): MemoryLogger {
|
|
5
|
+
const entries: LogEntry[] = [];
|
|
6
|
+
return {
|
|
7
|
+
entries,
|
|
8
|
+
log(entry) {
|
|
9
|
+
entries.push(entry);
|
|
10
|
+
},
|
|
11
|
+
};
|
|
12
|
+
}
|
package/src/node.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// @venn-lang/contracts/node: the corner backed by `node:*`. Only Node consumers (the
|
|
2
|
+
// CLI) import this; the main entry stays neutral so it runs in a Web Worker.
|
|
3
|
+
|
|
4
|
+
export { createNodeHost } from "./host/create-node-host.js";
|
|
5
|
+
export { type ConsoleStreams, createNodeConsole } from "./ports/console/node-console.js";
|
|
6
|
+
export { createNodeFs } from "./ports/file-system/node-fs.js";
|
|
7
|
+
export { createNodeSpawn } from "./ports/process-provider/node-spawn.js";
|
|
8
|
+
export { createNodeSignals, isKnownSignal } from "./ports/signal-source/node-signals.js";
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { HostCapability } from "../capabilities/index.js";
|
|
2
|
+
import { hostMissingCapability } from "../errors/index.js";
|
|
3
|
+
import { missingCapabilities } from "./missing-capabilities.js";
|
|
4
|
+
import type { Port } from "./port.types.js";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Checks the host against what the port requires.
|
|
8
|
+
*
|
|
9
|
+
* @throws VennError VN2010 when a required capability is absent.
|
|
10
|
+
*/
|
|
11
|
+
export function assertCapabilities<T>(args: {
|
|
12
|
+
port: Port<T>;
|
|
13
|
+
caps: readonly HostCapability[];
|
|
14
|
+
}): void {
|
|
15
|
+
const missing = missingCapabilities({ requires: args.port.requires, caps: args.caps });
|
|
16
|
+
if (missing.length === 0) return;
|
|
17
|
+
throw hostMissingCapability({ portId: args.port.id, missing, present: args.caps });
|
|
18
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { portShapeMismatch } from "../errors/index.js";
|
|
2
|
+
import type { Port } from "./port.types.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Checks the implementation against the methods the port declares.
|
|
6
|
+
*
|
|
7
|
+
* @throws VennError VN2011 when a declared method is missing or is not callable.
|
|
8
|
+
*/
|
|
9
|
+
export function assertPortShape<T>(args: { port: Port<T>; impl: unknown }): void {
|
|
10
|
+
const missing = missingMethods(args.port, args.impl);
|
|
11
|
+
if (missing.length === 0) return;
|
|
12
|
+
throw portShapeMismatch({ portId: args.port.id, missing });
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function missingMethods<T>(port: Port<T>, impl: unknown): readonly string[] {
|
|
16
|
+
const bag = impl as Record<string, unknown> | null | undefined;
|
|
17
|
+
return port.methods.filter((m) => typeof bag?.[m] !== "function");
|
|
18
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { HostCapability } from "../capabilities/index.js";
|
|
2
|
+
import { assertCapabilities } from "./assert-capabilities.js";
|
|
3
|
+
import { assertPortShape } from "./assert-port-shape.js";
|
|
4
|
+
import type { Port } from "./port.types.js";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The single loader entry. Negotiates capabilities, checks the implementation's
|
|
8
|
+
* shape, then hands it back typed as `T`.
|
|
9
|
+
*
|
|
10
|
+
* Both checks run before anything is bound, so a mismatch is reported at start
|
|
11
|
+
* up rather than as a `TypeError` in the middle of a test.
|
|
12
|
+
*
|
|
13
|
+
* @param args.port - the descriptor to bind against.
|
|
14
|
+
* @param args.impl - the candidate implementation, untrusted.
|
|
15
|
+
* @param args.caps - the capabilities the host advertises.
|
|
16
|
+
* @returns `args.impl` typed as `T`.
|
|
17
|
+
* @throws VennError VN2010 when the host lacks a required capability, VN2011
|
|
18
|
+
* when the implementation is missing a declared method.
|
|
19
|
+
*/
|
|
20
|
+
export function bindPort<T>(args: {
|
|
21
|
+
port: Port<T>;
|
|
22
|
+
impl: unknown;
|
|
23
|
+
caps: readonly HostCapability[];
|
|
24
|
+
}): T {
|
|
25
|
+
assertCapabilities({ port: args.port, caps: args.caps });
|
|
26
|
+
assertPortShape({ port: args.port, impl: args.impl });
|
|
27
|
+
return args.impl as T;
|
|
28
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { assertCapabilities } from "./assert-capabilities.js";
|
|
2
|
+
export { assertPortShape } from "./assert-port-shape.js";
|
|
3
|
+
export { bindPort } from "./bind-port.js";
|
|
4
|
+
export { missingCapabilities } from "./missing-capabilities.js";
|
|
5
|
+
export type { AnyPort, Port } from "./port.types.js";
|