@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
package/dist/index.js
ADDED
|
@@ -0,0 +1,1342 @@
|
|
|
1
|
+
//#region src/capabilities/host-capability.ts
|
|
2
|
+
/**
|
|
3
|
+
* Every capability a Host may expose. A port or plugin declares which it
|
|
4
|
+
* requires, a host advertises which it provides, and negotiation compares the
|
|
5
|
+
* two before anything binds.
|
|
6
|
+
*/
|
|
7
|
+
const ALL_CAPABILITIES = [
|
|
8
|
+
"fs",
|
|
9
|
+
"process",
|
|
10
|
+
"net",
|
|
11
|
+
"clock",
|
|
12
|
+
"random",
|
|
13
|
+
"secrets",
|
|
14
|
+
"log",
|
|
15
|
+
"io"
|
|
16
|
+
];
|
|
17
|
+
//#endregion
|
|
18
|
+
//#region src/dotenv/dotenv-files.ts
|
|
19
|
+
/**
|
|
20
|
+
* The dotenv files read when `venn.toml` does not name its own.
|
|
21
|
+
*
|
|
22
|
+
* The order is the precedence, lowest first: what everyone shares, then what
|
|
23
|
+
* this environment adds, then what this machine keeps to itself. The `.local`
|
|
24
|
+
* ones are what a `.gitignore` is for.
|
|
25
|
+
*/
|
|
26
|
+
const DOTENV_CONVENTION = [
|
|
27
|
+
".env",
|
|
28
|
+
".env.${name}",
|
|
29
|
+
".env.local",
|
|
30
|
+
".env.${name}.local"
|
|
31
|
+
];
|
|
32
|
+
/**
|
|
33
|
+
* Which files to read, in order, for one environment.
|
|
34
|
+
*
|
|
35
|
+
* The runner and the editor both ask this, so they cannot disagree about where
|
|
36
|
+
* a value lives.
|
|
37
|
+
*
|
|
38
|
+
* @param args.configured - `[env] files` from `venn.toml`. Empty or absent
|
|
39
|
+
* falls back to {@link DOTENV_CONVENTION}.
|
|
40
|
+
* @param args.name - the selected environment, substituted for `${name}`.
|
|
41
|
+
*/
|
|
42
|
+
function dotenvFiles(args) {
|
|
43
|
+
return (args.configured?.length ? args.configured : DOTENV_CONVENTION).map((each) => each.replaceAll("${name}", args.name));
|
|
44
|
+
}
|
|
45
|
+
//#endregion
|
|
46
|
+
//#region src/dotenv/parse-dotenv.ts
|
|
47
|
+
/**
|
|
48
|
+
* Read a `.env` file: `NAME=value`, one per line.
|
|
49
|
+
*
|
|
50
|
+
* Deliberately small. It understands comments, blank lines, an `export `
|
|
51
|
+
* prefix, and quotes around a value that needs them. No variable expansion:
|
|
52
|
+
* `${OTHER}` is left alone, because Venn already interpolates in its own
|
|
53
|
+
* strings and two syntaxes for one idea is how people get surprised.
|
|
54
|
+
*
|
|
55
|
+
* @returns every name found, later lines winning over earlier ones.
|
|
56
|
+
*/
|
|
57
|
+
function parseDotenv(content) {
|
|
58
|
+
const out = {};
|
|
59
|
+
for (const line of content.split(/\r?\n/)) {
|
|
60
|
+
const entry = readLine(line);
|
|
61
|
+
if (entry) out[entry.name] = entry.value;
|
|
62
|
+
}
|
|
63
|
+
return out;
|
|
64
|
+
}
|
|
65
|
+
const LINE = /^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)$/;
|
|
66
|
+
function readLine(line) {
|
|
67
|
+
if (/^\s*(#|$)/.test(line)) return void 0;
|
|
68
|
+
const match = LINE.exec(line);
|
|
69
|
+
if (!match?.[1]) return void 0;
|
|
70
|
+
return {
|
|
71
|
+
name: match[1],
|
|
72
|
+
value: unquote((match[2] ?? "").trim())
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
/** A quoted value keeps its spaces and its `#`; an unquoted one stops at a comment. */
|
|
76
|
+
function unquote(raw) {
|
|
77
|
+
const quoted = /^(['"])([\s\S]*)\1$/.exec(raw);
|
|
78
|
+
if (quoted) return quoted[2];
|
|
79
|
+
return (raw.split(" #")[0] ?? "").trim();
|
|
80
|
+
}
|
|
81
|
+
//#endregion
|
|
82
|
+
//#region src/errors/venn-error.ts
|
|
83
|
+
/**
|
|
84
|
+
* The single error type that crosses a contracts boundary.
|
|
85
|
+
*
|
|
86
|
+
* Every failure carries a stable `VNxxxx` code, so conformance suites assert on
|
|
87
|
+
* `.code` and never on prose. Messages stay free to improve.
|
|
88
|
+
*/
|
|
89
|
+
var VennError = class extends Error {
|
|
90
|
+
code;
|
|
91
|
+
detail;
|
|
92
|
+
constructor(args) {
|
|
93
|
+
super(args.message);
|
|
94
|
+
this.name = "VennError";
|
|
95
|
+
this.code = args.code;
|
|
96
|
+
this.detail = args.detail;
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
//#endregion
|
|
100
|
+
//#region src/errors/contract-codes.ts
|
|
101
|
+
/** VN2010: a port requires capabilities the host does not provide. */
|
|
102
|
+
function hostMissingCapability(args) {
|
|
103
|
+
return new VennError({
|
|
104
|
+
code: "VN2010",
|
|
105
|
+
message: `Port "${args.portId}" requires capability ${quote(args.missing)}, which this host does not provide. Present: ${list(args.present)}.`,
|
|
106
|
+
detail: { ...args }
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
/** VN2011: an implementation is missing one or more methods the port declares. */
|
|
110
|
+
function portShapeMismatch(args) {
|
|
111
|
+
return new VennError({
|
|
112
|
+
code: "VN2011",
|
|
113
|
+
message: `Implementation of "${args.portId}" is missing method(s): ${args.missing.join(", ")}.`,
|
|
114
|
+
detail: { ...args }
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
/** VN2012: code reached a capability the host declared unavailable. */
|
|
118
|
+
function capabilityUnavailable(args) {
|
|
119
|
+
return new VennError({
|
|
120
|
+
code: "VN2012",
|
|
121
|
+
message: `Capability "${args.capability}" is not available on this host (called "${args.method}").`,
|
|
122
|
+
detail: { ...args }
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
function quote(caps) {
|
|
126
|
+
return caps.map((c) => `"${c}"`).join(", ");
|
|
127
|
+
}
|
|
128
|
+
function list(caps) {
|
|
129
|
+
return caps.length === 0 ? "(none)" : caps.join(", ");
|
|
130
|
+
}
|
|
131
|
+
//#endregion
|
|
132
|
+
//#region src/logger/console-logger.ts
|
|
133
|
+
/** Logger backed by the global console, so it works in a Worker and in Node. */
|
|
134
|
+
function createConsoleLogger() {
|
|
135
|
+
return { log(entry) {
|
|
136
|
+
const line = `[${entry.level}] ${entry.message}`;
|
|
137
|
+
if (entry.level === "error") console.error(line);
|
|
138
|
+
else console.log(line);
|
|
139
|
+
} };
|
|
140
|
+
}
|
|
141
|
+
//#endregion
|
|
142
|
+
//#region src/logger/memory-logger.ts
|
|
143
|
+
/** The double: keeps every entry in `entries` instead of printing it. */
|
|
144
|
+
function createMemoryLogger() {
|
|
145
|
+
const entries = [];
|
|
146
|
+
return {
|
|
147
|
+
entries,
|
|
148
|
+
log(entry) {
|
|
149
|
+
entries.push(entry);
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
//#endregion
|
|
154
|
+
//#region src/ports/clock/clock.port.ts
|
|
155
|
+
/**
|
|
156
|
+
* The {@link Clock} contract. Implementations: `system-clock`, `virtual-clock`.
|
|
157
|
+
*
|
|
158
|
+
* `advance` and `setTime` are absent on purpose: they are how a test drives a
|
|
159
|
+
* virtual clock, not something a caller of the port may reach for.
|
|
160
|
+
*/
|
|
161
|
+
const ClockPort = {
|
|
162
|
+
id: "venn.port.clock",
|
|
163
|
+
version: 1,
|
|
164
|
+
requires: ["clock"],
|
|
165
|
+
methods: ["now", "sleep"]
|
|
166
|
+
};
|
|
167
|
+
//#endregion
|
|
168
|
+
//#region src/ports/clock/system-clock.ts
|
|
169
|
+
/** Wall-clock time, on the `Date` and `setTimeout` globals. */
|
|
170
|
+
function createSystemClock() {
|
|
171
|
+
return {
|
|
172
|
+
now: () => Date.now(),
|
|
173
|
+
sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms))
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
//#endregion
|
|
177
|
+
//#region src/ports/clock/virtual-clock.ts
|
|
178
|
+
/**
|
|
179
|
+
* The double. `sleep` advances internal time and resolves at once, so a test
|
|
180
|
+
* about a timeout costs nothing to run.
|
|
181
|
+
*
|
|
182
|
+
* @param args.start - the epoch this clock begins at. Defaults to 0.
|
|
183
|
+
*/
|
|
184
|
+
function createVirtualClock(args = {}) {
|
|
185
|
+
let time = args.start ?? 0;
|
|
186
|
+
const advance = (ms) => {
|
|
187
|
+
time += Math.max(0, ms);
|
|
188
|
+
};
|
|
189
|
+
return {
|
|
190
|
+
now: () => time,
|
|
191
|
+
sleep: async (ms) => advance(ms),
|
|
192
|
+
advance,
|
|
193
|
+
setTime: (epochMs) => {
|
|
194
|
+
time = epochMs;
|
|
195
|
+
}
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
//#endregion
|
|
199
|
+
//#region src/ports/file-system/file-system.errors.ts
|
|
200
|
+
/** VN8010: a read or a remove targeted a path that does not exist. */
|
|
201
|
+
function fsNotFound(args) {
|
|
202
|
+
return new VennError({
|
|
203
|
+
code: "VN8010",
|
|
204
|
+
message: `File not found: "${args.path}".`,
|
|
205
|
+
detail: { path: args.path }
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
//#endregion
|
|
209
|
+
//#region src/ports/file-system/file-system.port.ts
|
|
210
|
+
/**
|
|
211
|
+
* The {@link FileSystem} contract. Implementations: `node-fs`, `memory-fs`.
|
|
212
|
+
*/
|
|
213
|
+
const FileSystemPort = {
|
|
214
|
+
id: "venn.port.filesystem",
|
|
215
|
+
version: 1,
|
|
216
|
+
requires: ["fs"],
|
|
217
|
+
methods: [
|
|
218
|
+
"read",
|
|
219
|
+
"write",
|
|
220
|
+
"exists",
|
|
221
|
+
"remove",
|
|
222
|
+
"list"
|
|
223
|
+
]
|
|
224
|
+
};
|
|
225
|
+
//#endregion
|
|
226
|
+
//#region src/ports/file-system/memory-fs.ts
|
|
227
|
+
/** The double: byte-exact, isolated per instance, no disk. */
|
|
228
|
+
function createMemoryFs() {
|
|
229
|
+
const files = /* @__PURE__ */ new Map();
|
|
230
|
+
return {
|
|
231
|
+
async read(path) {
|
|
232
|
+
const bytes = files.get(path);
|
|
233
|
+
if (!bytes) throw fsNotFound({ path });
|
|
234
|
+
return bytes.slice();
|
|
235
|
+
},
|
|
236
|
+
async write(path, bytes) {
|
|
237
|
+
files.set(path, bytes.slice());
|
|
238
|
+
},
|
|
239
|
+
async exists(path) {
|
|
240
|
+
return files.has(path);
|
|
241
|
+
},
|
|
242
|
+
async remove(path) {
|
|
243
|
+
if (!files.delete(path)) throw fsNotFound({ path });
|
|
244
|
+
},
|
|
245
|
+
async list(path) {
|
|
246
|
+
return listUnder([...files.keys()], path);
|
|
247
|
+
}
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* What a directory holds, worked out from the paths written.
|
|
252
|
+
*
|
|
253
|
+
* There are no directories here, only paths, so a directory is whatever a path
|
|
254
|
+
* implies one to be. For everything the language does with a directory that is
|
|
255
|
+
* the same answer the real file system gives, which is what lets the TCK ask
|
|
256
|
+
* both the same questions.
|
|
257
|
+
*/
|
|
258
|
+
function listUnder(paths, directory) {
|
|
259
|
+
const prefix = directory === "" || directory === "." ? "" : `${directory.replace(/\/+$/, "")}/`;
|
|
260
|
+
const seen = /* @__PURE__ */ new Map();
|
|
261
|
+
for (const path of paths) {
|
|
262
|
+
if (!path.startsWith(prefix)) continue;
|
|
263
|
+
const rest = path.slice(prefix.length);
|
|
264
|
+
const slash = rest.indexOf("/");
|
|
265
|
+
if (rest !== "") seen.set(slash < 0 ? rest : rest.slice(0, slash), slash >= 0);
|
|
266
|
+
}
|
|
267
|
+
return [...seen].map(([name, isDirectory]) => ({
|
|
268
|
+
name,
|
|
269
|
+
directory: isDirectory
|
|
270
|
+
}));
|
|
271
|
+
}
|
|
272
|
+
//#endregion
|
|
273
|
+
//#region src/ports/lock-provider/fake-lock.ts
|
|
274
|
+
/** The double: grants immediately and serialises nothing. */
|
|
275
|
+
function createFakeLock() {
|
|
276
|
+
return { acquire: async () => () => {} };
|
|
277
|
+
}
|
|
278
|
+
//#endregion
|
|
279
|
+
//#region src/ports/lock-provider/in-process-lock.ts
|
|
280
|
+
/**
|
|
281
|
+
* The real one: a chained-promise mutex per name. Waiters are served in the
|
|
282
|
+
* order they asked, and the chain lives only in this process.
|
|
283
|
+
*/
|
|
284
|
+
function createInProcessLock() {
|
|
285
|
+
const tails = /* @__PURE__ */ new Map();
|
|
286
|
+
return { acquire: (name) => acquire(tails, name) };
|
|
287
|
+
}
|
|
288
|
+
async function acquire(tails, name) {
|
|
289
|
+
const previous = tails.get(name) ?? Promise.resolve();
|
|
290
|
+
let release = () => {};
|
|
291
|
+
const held = new Promise((resolve) => {
|
|
292
|
+
release = resolve;
|
|
293
|
+
});
|
|
294
|
+
tails.set(name, previous.then(() => held));
|
|
295
|
+
await previous;
|
|
296
|
+
return release;
|
|
297
|
+
}
|
|
298
|
+
//#endregion
|
|
299
|
+
//#region src/ports/lock-provider/lock-provider.port.ts
|
|
300
|
+
/**
|
|
301
|
+
* The {@link LockProvider} contract. Implementations: `in-process-lock`,
|
|
302
|
+
* `fake-lock`.
|
|
303
|
+
*
|
|
304
|
+
* Requires nothing: a mutex is bookkeeping, so it binds even on a host with no
|
|
305
|
+
* capabilities at all.
|
|
306
|
+
*/
|
|
307
|
+
const LockProviderPort = {
|
|
308
|
+
id: "venn.port.lock",
|
|
309
|
+
version: 1,
|
|
310
|
+
requires: [],
|
|
311
|
+
methods: ["acquire"]
|
|
312
|
+
};
|
|
313
|
+
//#endregion
|
|
314
|
+
//#region src/ports/process-provider/fake-process.ts
|
|
315
|
+
/**
|
|
316
|
+
* The double: a scripted process that never touches the OS.
|
|
317
|
+
*
|
|
318
|
+
* Output is streamed before the wait resolves, exactly as the real one does it,
|
|
319
|
+
* so a caller that shows progress is exercised here and not only against a real
|
|
320
|
+
* machine.
|
|
321
|
+
*
|
|
322
|
+
* @param args.exitCode - what every run reports. Defaults to 0.
|
|
323
|
+
* @param args.output - what every run writes. Defaults to nothing.
|
|
324
|
+
*/
|
|
325
|
+
function createFakeProcess(args = {}) {
|
|
326
|
+
const code = args.exitCode ?? 0;
|
|
327
|
+
const output = args.output ?? "";
|
|
328
|
+
return { spawn: (spawned) => {
|
|
329
|
+
if (output !== "") spawned.onOutput?.(output);
|
|
330
|
+
return handle(code, output);
|
|
331
|
+
} };
|
|
332
|
+
}
|
|
333
|
+
function handle(code, output) {
|
|
334
|
+
return {
|
|
335
|
+
pid: 0,
|
|
336
|
+
wait: async () => ({
|
|
337
|
+
code,
|
|
338
|
+
output
|
|
339
|
+
}),
|
|
340
|
+
kill: () => {}
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
//#endregion
|
|
344
|
+
//#region src/ports/process-provider/process-provider.port.ts
|
|
345
|
+
/**
|
|
346
|
+
* The {@link ProcessProvider} contract. Implementations: `node-spawn`,
|
|
347
|
+
* `fake-process`.
|
|
348
|
+
*/
|
|
349
|
+
const ProcessProviderPort = {
|
|
350
|
+
id: "venn.port.process",
|
|
351
|
+
version: 1,
|
|
352
|
+
requires: ["process"],
|
|
353
|
+
methods: ["spawn"]
|
|
354
|
+
};
|
|
355
|
+
//#endregion
|
|
356
|
+
//#region src/ports/random/fixed-random.ts
|
|
357
|
+
/** The double: always the same value, and always `min` from `int`. */
|
|
358
|
+
function createFixedRandom(args = {}) {
|
|
359
|
+
const value = args.value ?? 0;
|
|
360
|
+
return {
|
|
361
|
+
next: () => value,
|
|
362
|
+
int: (min) => min
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
//#endregion
|
|
366
|
+
//#region src/ports/random/random.port.ts
|
|
367
|
+
/**
|
|
368
|
+
* The {@link Random} contract. Implementations: `seeded-random`,
|
|
369
|
+
* `fixed-random`.
|
|
370
|
+
*/
|
|
371
|
+
const RandomPort = {
|
|
372
|
+
id: "venn.port.random",
|
|
373
|
+
version: 1,
|
|
374
|
+
requires: ["random"],
|
|
375
|
+
methods: ["next", "int"]
|
|
376
|
+
};
|
|
377
|
+
//#endregion
|
|
378
|
+
//#region src/ports/random/seeded-random.ts
|
|
379
|
+
/**
|
|
380
|
+
* The real one: a mulberry32 PRNG, seeded once per worker. Same seed, same
|
|
381
|
+
* sequence, so a run reproduces.
|
|
382
|
+
*/
|
|
383
|
+
function createSeededRandom(args) {
|
|
384
|
+
let state = args.seed >>> 0;
|
|
385
|
+
const next = () => {
|
|
386
|
+
state = state + 1831565813 | 0;
|
|
387
|
+
let t = Math.imul(state ^ state >>> 15, 1 | state);
|
|
388
|
+
t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t;
|
|
389
|
+
return ((t ^ t >>> 14) >>> 0) / 4294967296;
|
|
390
|
+
};
|
|
391
|
+
return {
|
|
392
|
+
next,
|
|
393
|
+
int: (min, max) => min + Math.floor(next() * (max - min + 1))
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
//#endregion
|
|
397
|
+
//#region src/ports/secret-provider/secret.ts
|
|
398
|
+
/** What every serialised secret collapses to. */
|
|
399
|
+
const REDACTED = "‹redigido›";
|
|
400
|
+
/**
|
|
401
|
+
* Wrap a raw value so it cannot leak through `toString` or `toJSON`.
|
|
402
|
+
*
|
|
403
|
+
* @param args.reveal - the raw value, reachable only via `Secret.reveal()`.
|
|
404
|
+
*/
|
|
405
|
+
function makeSecret(args) {
|
|
406
|
+
return {
|
|
407
|
+
reveal: () => args.reveal,
|
|
408
|
+
toString: () => REDACTED,
|
|
409
|
+
toJSON: () => REDACTED
|
|
410
|
+
};
|
|
411
|
+
}
|
|
412
|
+
//#endregion
|
|
413
|
+
//#region src/ports/secret-provider/env-secrets.ts
|
|
414
|
+
/**
|
|
415
|
+
* The real one: secrets from `process.env`.
|
|
416
|
+
*
|
|
417
|
+
* The `process` global is probed rather than imported, so this file stays
|
|
418
|
+
* neutral and reports every name as absent in a Worker instead of throwing.
|
|
419
|
+
*/
|
|
420
|
+
function createEnvSecrets() {
|
|
421
|
+
const env = typeof process === "undefined" ? {} : process.env;
|
|
422
|
+
return {
|
|
423
|
+
get: (name) => {
|
|
424
|
+
const value = env[name];
|
|
425
|
+
return value === void 0 ? void 0 : makeSecret({ reveal: value });
|
|
426
|
+
},
|
|
427
|
+
has: (name) => env[name] !== void 0
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
//#endregion
|
|
431
|
+
//#region src/ports/secret-provider/memory-secrets.ts
|
|
432
|
+
/**
|
|
433
|
+
* The double: secrets given by name, wrapped so they still redact.
|
|
434
|
+
*
|
|
435
|
+
* @param args.values - the raw values, keyed by secret name.
|
|
436
|
+
*/
|
|
437
|
+
function createMemorySecrets(args) {
|
|
438
|
+
const values = args.values;
|
|
439
|
+
return {
|
|
440
|
+
get: (name) => name in values ? makeSecret({ reveal: values[name] }) : void 0,
|
|
441
|
+
has: (name) => name in values
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
//#endregion
|
|
445
|
+
//#region src/ports/secret-provider/secret-provider.port.ts
|
|
446
|
+
/**
|
|
447
|
+
* The {@link SecretProvider} contract. Implementations: `env-secrets`,
|
|
448
|
+
* `memory-secrets`.
|
|
449
|
+
*/
|
|
450
|
+
const SecretProviderPort = {
|
|
451
|
+
id: "venn.port.secrets",
|
|
452
|
+
version: 1,
|
|
453
|
+
requires: ["secrets"],
|
|
454
|
+
methods: ["get", "has"]
|
|
455
|
+
};
|
|
456
|
+
//#endregion
|
|
457
|
+
//#region src/host/create-test-host.ts
|
|
458
|
+
/**
|
|
459
|
+
* A host of doubles, every capability granted. The default for tests.
|
|
460
|
+
*
|
|
461
|
+
* @param overrides - replaces individual members, e.g. a real clock.
|
|
462
|
+
*/
|
|
463
|
+
function createTestHost(overrides = {}) {
|
|
464
|
+
return {
|
|
465
|
+
fs: createMemoryFs(),
|
|
466
|
+
proc: createFakeProcess(),
|
|
467
|
+
clock: createVirtualClock(),
|
|
468
|
+
random: createSeededRandom({ seed: 1 }),
|
|
469
|
+
secrets: createMemorySecrets({ values: {} }),
|
|
470
|
+
log: createMemoryLogger(),
|
|
471
|
+
lock: createInProcessLock(),
|
|
472
|
+
caps: ALL_CAPABILITIES,
|
|
473
|
+
...overrides
|
|
474
|
+
};
|
|
475
|
+
}
|
|
476
|
+
//#endregion
|
|
477
|
+
//#region src/host/unavailable.ts
|
|
478
|
+
/**
|
|
479
|
+
* A stand-in for a port the host cannot provide, such as `process` in a Worker.
|
|
480
|
+
*
|
|
481
|
+
* @param args.capability - the capability the host lacks, named in the error.
|
|
482
|
+
* @param args.methods - the port's declared methods, usually `SomePort.methods`.
|
|
483
|
+
* @returns an object shaped like `T` whose every method throws VN2012 when
|
|
484
|
+
* called, rather than failing as a `TypeError` mid-run.
|
|
485
|
+
*/
|
|
486
|
+
function unavailable(args) {
|
|
487
|
+
const bag = {};
|
|
488
|
+
for (const method of args.methods) bag[method] = () => {
|
|
489
|
+
throw capabilityUnavailable({
|
|
490
|
+
capability: args.capability,
|
|
491
|
+
method
|
|
492
|
+
});
|
|
493
|
+
};
|
|
494
|
+
return bag;
|
|
495
|
+
}
|
|
496
|
+
//#endregion
|
|
497
|
+
//#region src/host/create-worker-host.ts
|
|
498
|
+
/**
|
|
499
|
+
* Host for a Web Worker: an in-memory file system, no `process`, no `net`.
|
|
500
|
+
*
|
|
501
|
+
* `proc` is a stand-in whose every method throws VN2012, so reaching for a
|
|
502
|
+
* subprocess in the editor fails with a diagnostic instead of a `TypeError`.
|
|
503
|
+
*/
|
|
504
|
+
function createWorkerHost() {
|
|
505
|
+
return {
|
|
506
|
+
fs: createMemoryFs(),
|
|
507
|
+
proc: unavailable({
|
|
508
|
+
capability: "process",
|
|
509
|
+
methods: ProcessProviderPort.methods
|
|
510
|
+
}),
|
|
511
|
+
clock: createSystemClock(),
|
|
512
|
+
random: createSeededRandom({ seed: 1 }),
|
|
513
|
+
secrets: createMemorySecrets({ values: {} }),
|
|
514
|
+
log: createMemoryLogger(),
|
|
515
|
+
lock: createInProcessLock(),
|
|
516
|
+
caps: [
|
|
517
|
+
"fs",
|
|
518
|
+
"clock",
|
|
519
|
+
"random",
|
|
520
|
+
"secrets",
|
|
521
|
+
"log"
|
|
522
|
+
]
|
|
523
|
+
};
|
|
524
|
+
}
|
|
525
|
+
//#endregion
|
|
526
|
+
//#region src/host/create-host.ts
|
|
527
|
+
/**
|
|
528
|
+
* The neutral host assemblers.
|
|
529
|
+
*
|
|
530
|
+
* `node` is deliberately absent: it pulls `node:*`, so it lives behind
|
|
531
|
+
* `@venn-lang/contracts/node` as `createNodeHost` and this entry stays Worker-safe.
|
|
532
|
+
*/
|
|
533
|
+
const createHost = {
|
|
534
|
+
worker: createWorkerHost,
|
|
535
|
+
test: createTestHost
|
|
536
|
+
};
|
|
537
|
+
//#endregion
|
|
538
|
+
//#region src/port/missing-capabilities.ts
|
|
539
|
+
/**
|
|
540
|
+
* The gap between what is required and what the host offers.
|
|
541
|
+
*
|
|
542
|
+
* @returns the required capabilities absent from `caps`, in the order required.
|
|
543
|
+
*/
|
|
544
|
+
function missingCapabilities(args) {
|
|
545
|
+
const present = new Set(args.caps);
|
|
546
|
+
return args.requires.filter((cap) => !present.has(cap));
|
|
547
|
+
}
|
|
548
|
+
//#endregion
|
|
549
|
+
//#region src/port/assert-capabilities.ts
|
|
550
|
+
/**
|
|
551
|
+
* Checks the host against what the port requires.
|
|
552
|
+
*
|
|
553
|
+
* @throws VennError VN2010 when a required capability is absent.
|
|
554
|
+
*/
|
|
555
|
+
function assertCapabilities(args) {
|
|
556
|
+
const missing = missingCapabilities({
|
|
557
|
+
requires: args.port.requires,
|
|
558
|
+
caps: args.caps
|
|
559
|
+
});
|
|
560
|
+
if (missing.length === 0) return;
|
|
561
|
+
throw hostMissingCapability({
|
|
562
|
+
portId: args.port.id,
|
|
563
|
+
missing,
|
|
564
|
+
present: args.caps
|
|
565
|
+
});
|
|
566
|
+
}
|
|
567
|
+
//#endregion
|
|
568
|
+
//#region src/port/assert-port-shape.ts
|
|
569
|
+
/**
|
|
570
|
+
* Checks the implementation against the methods the port declares.
|
|
571
|
+
*
|
|
572
|
+
* @throws VennError VN2011 when a declared method is missing or is not callable.
|
|
573
|
+
*/
|
|
574
|
+
function assertPortShape(args) {
|
|
575
|
+
const missing = missingMethods(args.port, args.impl);
|
|
576
|
+
if (missing.length === 0) return;
|
|
577
|
+
throw portShapeMismatch({
|
|
578
|
+
portId: args.port.id,
|
|
579
|
+
missing
|
|
580
|
+
});
|
|
581
|
+
}
|
|
582
|
+
function missingMethods(port, impl) {
|
|
583
|
+
const bag = impl;
|
|
584
|
+
return port.methods.filter((m) => typeof bag?.[m] !== "function");
|
|
585
|
+
}
|
|
586
|
+
//#endregion
|
|
587
|
+
//#region src/port/bind-port.ts
|
|
588
|
+
/**
|
|
589
|
+
* The single loader entry. Negotiates capabilities, checks the implementation's
|
|
590
|
+
* shape, then hands it back typed as `T`.
|
|
591
|
+
*
|
|
592
|
+
* Both checks run before anything is bound, so a mismatch is reported at start
|
|
593
|
+
* up rather than as a `TypeError` in the middle of a test.
|
|
594
|
+
*
|
|
595
|
+
* @param args.port - the descriptor to bind against.
|
|
596
|
+
* @param args.impl - the candidate implementation, untrusted.
|
|
597
|
+
* @param args.caps - the capabilities the host advertises.
|
|
598
|
+
* @returns `args.impl` typed as `T`.
|
|
599
|
+
* @throws VennError VN2010 when the host lacks a required capability, VN2011
|
|
600
|
+
* when the implementation is missing a declared method.
|
|
601
|
+
*/
|
|
602
|
+
function bindPort(args) {
|
|
603
|
+
assertCapabilities({
|
|
604
|
+
port: args.port,
|
|
605
|
+
caps: args.caps
|
|
606
|
+
});
|
|
607
|
+
assertPortShape({
|
|
608
|
+
port: args.port,
|
|
609
|
+
impl: args.impl
|
|
610
|
+
});
|
|
611
|
+
return args.impl;
|
|
612
|
+
}
|
|
613
|
+
//#endregion
|
|
614
|
+
//#region src/ports/console/console.port.ts
|
|
615
|
+
/**
|
|
616
|
+
* The {@link Console} contract. Implementations: `node-console`,
|
|
617
|
+
* `memory-console`.
|
|
618
|
+
*/
|
|
619
|
+
const ConsolePort = {
|
|
620
|
+
id: "venn.port.console",
|
|
621
|
+
version: 1,
|
|
622
|
+
requires: ["io"],
|
|
623
|
+
methods: [
|
|
624
|
+
"write",
|
|
625
|
+
"writeError",
|
|
626
|
+
"readLine",
|
|
627
|
+
"args"
|
|
628
|
+
]
|
|
629
|
+
};
|
|
630
|
+
//#endregion
|
|
631
|
+
//#region src/ports/console/memory-console.ts
|
|
632
|
+
/**
|
|
633
|
+
* The double: records what was written, reads from a scripted input.
|
|
634
|
+
*
|
|
635
|
+
* @param args.input - the lines `readLine` hands back, in order.
|
|
636
|
+
* @param args.argv - what `args()` reports.
|
|
637
|
+
*/
|
|
638
|
+
function createMemoryConsole(args = {}) {
|
|
639
|
+
const state = {
|
|
640
|
+
out: "",
|
|
641
|
+
err: ""
|
|
642
|
+
};
|
|
643
|
+
const input = [...args.input ?? []];
|
|
644
|
+
return {
|
|
645
|
+
write: (text) => {
|
|
646
|
+
state.out += text;
|
|
647
|
+
},
|
|
648
|
+
writeError: (text) => {
|
|
649
|
+
state.err += text;
|
|
650
|
+
},
|
|
651
|
+
readLine: () => Promise.resolve(input.length > 0 ? input.shift() ?? null : null),
|
|
652
|
+
args: () => args.argv ?? [],
|
|
653
|
+
get out() {
|
|
654
|
+
return state.out;
|
|
655
|
+
},
|
|
656
|
+
get err() {
|
|
657
|
+
return state.err;
|
|
658
|
+
}
|
|
659
|
+
};
|
|
660
|
+
}
|
|
661
|
+
//#endregion
|
|
662
|
+
//#region src/ports/manifest-provider/read/scalars.ts
|
|
663
|
+
function asRecord(value) {
|
|
664
|
+
return typeof value === "object" && value !== null ? value : {};
|
|
665
|
+
}
|
|
666
|
+
function asStringMap(value) {
|
|
667
|
+
const out = {};
|
|
668
|
+
for (const [key, item] of Object.entries(asRecord(value))) out[key] = String(item);
|
|
669
|
+
return out;
|
|
670
|
+
}
|
|
671
|
+
function asList(value) {
|
|
672
|
+
return Array.isArray(value) ? value.map(String) : [];
|
|
673
|
+
}
|
|
674
|
+
function asRecords(value) {
|
|
675
|
+
return Array.isArray(value) ? value.map(asRecord) : [];
|
|
676
|
+
}
|
|
677
|
+
function asString(value) {
|
|
678
|
+
return value === void 0 || value === null ? void 0 : String(value);
|
|
679
|
+
}
|
|
680
|
+
function asNumber(value) {
|
|
681
|
+
const parsed = Number(value);
|
|
682
|
+
return value === void 0 || Number.isNaN(parsed) ? void 0 : parsed;
|
|
683
|
+
}
|
|
684
|
+
function asBoolean(value) {
|
|
685
|
+
if (value === void 0) return void 0;
|
|
686
|
+
return value === true || value === "true";
|
|
687
|
+
}
|
|
688
|
+
//#endregion
|
|
689
|
+
//#region src/ports/manifest-provider/read/read-dependencies.ts
|
|
690
|
+
/**
|
|
691
|
+
* A dependency table, in either spelling TOML allows.
|
|
692
|
+
*
|
|
693
|
+
* `zod = "^4"` and `zod = { version = "^4", optional = true }` say the same
|
|
694
|
+
* thing about the version, so both read into one shape rather than two the rest
|
|
695
|
+
* of the code would have to keep apart.
|
|
696
|
+
*/
|
|
697
|
+
function readDependencies(value) {
|
|
698
|
+
return Object.entries(asRecord(value)).map(([name, spec]) => readOne(name, spec));
|
|
699
|
+
}
|
|
700
|
+
function readOne(name, spec) {
|
|
701
|
+
if (typeof spec === "string") return {
|
|
702
|
+
name,
|
|
703
|
+
version: spec,
|
|
704
|
+
fromWorkspace: false,
|
|
705
|
+
optional: false
|
|
706
|
+
};
|
|
707
|
+
const table = asRecord(spec);
|
|
708
|
+
return {
|
|
709
|
+
name,
|
|
710
|
+
version: asString(table.version),
|
|
711
|
+
path: asString(table.path),
|
|
712
|
+
fromWorkspace: asBoolean(table.workspace) ?? false,
|
|
713
|
+
optional: asBoolean(table.optional) ?? false
|
|
714
|
+
};
|
|
715
|
+
}
|
|
716
|
+
//#endregion
|
|
717
|
+
//#region src/ports/manifest-provider/read/read-package.ts
|
|
718
|
+
/** `[package]`: who this is. A missing table reads as a nameless package. */
|
|
719
|
+
function readPackage(data) {
|
|
720
|
+
const table = asRecord(data.package);
|
|
721
|
+
return {
|
|
722
|
+
name: String(table.name ?? ""),
|
|
723
|
+
version: asString(table.version),
|
|
724
|
+
description: asString(table.description),
|
|
725
|
+
license: asString(table.license),
|
|
726
|
+
authors: asList(table.authors),
|
|
727
|
+
edition: asString(table.edition)
|
|
728
|
+
};
|
|
729
|
+
}
|
|
730
|
+
/**
|
|
731
|
+
* The same table, reduced to what a member may inherit from its workspace.
|
|
732
|
+
*
|
|
733
|
+
* A name is never inherited, being the one thing that must differ between two
|
|
734
|
+
* members, so `[workspace.package]` carrying one is read as carrying none.
|
|
735
|
+
*/
|
|
736
|
+
function readInheritable(data) {
|
|
737
|
+
const table = asRecord(data);
|
|
738
|
+
const found = {};
|
|
739
|
+
for (const key of [
|
|
740
|
+
"version",
|
|
741
|
+
"description",
|
|
742
|
+
"license",
|
|
743
|
+
"edition"
|
|
744
|
+
]) {
|
|
745
|
+
const value = asString(table[key]);
|
|
746
|
+
if (value !== void 0) found[key] = value;
|
|
747
|
+
}
|
|
748
|
+
const authors = asList(table.authors);
|
|
749
|
+
return authors.length > 0 ? {
|
|
750
|
+
...found,
|
|
751
|
+
authors
|
|
752
|
+
} : found;
|
|
753
|
+
}
|
|
754
|
+
//#endregion
|
|
755
|
+
//#region src/ports/manifest-provider/read/read-profiles.ts
|
|
756
|
+
/**
|
|
757
|
+
* What each build is for, and what that costs.
|
|
758
|
+
*
|
|
759
|
+
* `dev` reports what it finds and carries on, because a project being worked on
|
|
760
|
+
* is half-written most of the time. `release` refuses to build over a problem.
|
|
761
|
+
* Neither has to be written in a manifest for the defaults to apply.
|
|
762
|
+
*/
|
|
763
|
+
const DEFAULT_PROFILES = {
|
|
764
|
+
dev: { strict: false },
|
|
765
|
+
release: { strict: true }
|
|
766
|
+
};
|
|
767
|
+
/** `[profile.<name>]`, merged over {@link DEFAULT_PROFILES}. */
|
|
768
|
+
function readProfiles(data) {
|
|
769
|
+
const out = { ...DEFAULT_PROFILES };
|
|
770
|
+
for (const [name, table] of Object.entries(asRecord(data.profile))) out[name] = {
|
|
771
|
+
...out[name],
|
|
772
|
+
...readProfile(asRecord(table))
|
|
773
|
+
};
|
|
774
|
+
return out;
|
|
775
|
+
}
|
|
776
|
+
function readProfile(table) {
|
|
777
|
+
const found = {};
|
|
778
|
+
for (const key of ["strict"]) {
|
|
779
|
+
const value = asBoolean(table[key]);
|
|
780
|
+
if (value !== void 0) found[key] = value;
|
|
781
|
+
}
|
|
782
|
+
return found;
|
|
783
|
+
}
|
|
784
|
+
//#endregion
|
|
785
|
+
//#region src/ports/manifest-provider/read/read-targets.ts
|
|
786
|
+
/** Where a `lib` target starts when the manifest does not say. */
|
|
787
|
+
const LIB_ROOT = "src/lib.vn";
|
|
788
|
+
/** Where the default `bin` target starts when the manifest does not say. */
|
|
789
|
+
const MAIN_ROOT = "src/main.vn";
|
|
790
|
+
/** Where additional `bin` targets are looked for by convention. */
|
|
791
|
+
const BIN_DIR = "src/bin";
|
|
792
|
+
/**
|
|
793
|
+
* `[lib]` and `[[bin]]`: what this package builds.
|
|
794
|
+
*
|
|
795
|
+
* Only what the manifest *declares*. The conventional roots are filled in by
|
|
796
|
+
* whoever can look at the disk, because a convention is a claim about files
|
|
797
|
+
* existing and this reader is pure. A manifest declaring nothing is the common
|
|
798
|
+
* case, not an empty one.
|
|
799
|
+
*/
|
|
800
|
+
function readTargets(data, packageName) {
|
|
801
|
+
const found = [];
|
|
802
|
+
if (data.lib !== void 0) found.push(readLib(asRecord(data.lib), packageName));
|
|
803
|
+
for (const entry of asRecords(data.bin)) found.push(readBin(entry, packageName));
|
|
804
|
+
return found;
|
|
805
|
+
}
|
|
806
|
+
function readLib(table, packageName) {
|
|
807
|
+
return {
|
|
808
|
+
kind: "lib",
|
|
809
|
+
name: asString(table.name) ?? packageName,
|
|
810
|
+
path: asString(table.path) ?? "src/lib.vn"
|
|
811
|
+
};
|
|
812
|
+
}
|
|
813
|
+
function readBin(table, packageName) {
|
|
814
|
+
const name = asString(table.name) ?? packageName;
|
|
815
|
+
return {
|
|
816
|
+
kind: "bin",
|
|
817
|
+
name,
|
|
818
|
+
path: asString(table.path) ?? `src/bin/${name}.vn`
|
|
819
|
+
};
|
|
820
|
+
}
|
|
821
|
+
//#endregion
|
|
822
|
+
//#region src/ports/manifest-provider/read/read-tooling.ts
|
|
823
|
+
const MANAGERS = /* @__PURE__ */ new Set([
|
|
824
|
+
"pnpm",
|
|
825
|
+
"npm",
|
|
826
|
+
"bun",
|
|
827
|
+
"yarn"
|
|
828
|
+
]);
|
|
829
|
+
/**
|
|
830
|
+
* `[tooling]`: which package manager runs underneath.
|
|
831
|
+
*
|
|
832
|
+
* An unrecognised name falls back to pnpm rather than failing, so a manifest
|
|
833
|
+
* written against a newer toolchain still opens.
|
|
834
|
+
*/
|
|
835
|
+
function readTooling(data) {
|
|
836
|
+
const name = asString(asRecord(data.tooling).manager);
|
|
837
|
+
return { manager: name && MANAGERS.has(name) ? name : "pnpm" };
|
|
838
|
+
}
|
|
839
|
+
//#endregion
|
|
840
|
+
//#region src/ports/manifest-provider/read/read-workspace.ts
|
|
841
|
+
/**
|
|
842
|
+
* `[workspace]`, or undefined when the manifest is a plain package.
|
|
843
|
+
*
|
|
844
|
+
* A root may also be a package, so `[workspace]` and `[package]` are read
|
|
845
|
+
* separately and never merged.
|
|
846
|
+
*/
|
|
847
|
+
function readWorkspace(data) {
|
|
848
|
+
if (data.workspace === void 0) return void 0;
|
|
849
|
+
const table = asRecord(data.workspace);
|
|
850
|
+
return {
|
|
851
|
+
members: asList(table.members),
|
|
852
|
+
exclude: asList(table.exclude),
|
|
853
|
+
defaultMembers: asList(table["default-members"]),
|
|
854
|
+
package: readInheritable(asRecord(table.package)),
|
|
855
|
+
dependencies: readDependencies(table.dependencies)
|
|
856
|
+
};
|
|
857
|
+
}
|
|
858
|
+
//#endregion
|
|
859
|
+
//#region src/ports/manifest-provider/default-manifest.ts
|
|
860
|
+
/**
|
|
861
|
+
* A manifest with nothing declared: what a project without a `venn.toml` is.
|
|
862
|
+
*
|
|
863
|
+
* The one place stating what a project gets for saying nothing at all, so a
|
|
864
|
+
* caller who wants one field need not spell the other dozen.
|
|
865
|
+
*
|
|
866
|
+
* @param overrides - fields to state explicitly. `name` and `version` fall back
|
|
867
|
+
* to `[package]` before falling back to the defaults.
|
|
868
|
+
*/
|
|
869
|
+
function defaultManifest(overrides = {}) {
|
|
870
|
+
const pkg = {
|
|
871
|
+
name: "",
|
|
872
|
+
authors: [],
|
|
873
|
+
...overrides.package
|
|
874
|
+
};
|
|
875
|
+
return {
|
|
876
|
+
name: overrides.name ?? pkg.name,
|
|
877
|
+
version: overrides.version ?? pkg.version ?? "0.0.0",
|
|
878
|
+
package: pkg,
|
|
879
|
+
targets: [],
|
|
880
|
+
dependencies: [],
|
|
881
|
+
devDependencies: [],
|
|
882
|
+
patch: [],
|
|
883
|
+
profiles: { ...DEFAULT_PROFILES },
|
|
884
|
+
tooling: { manager: "pnpm" },
|
|
885
|
+
env: {},
|
|
886
|
+
envFiles: [],
|
|
887
|
+
paths: {},
|
|
888
|
+
format: {},
|
|
889
|
+
...overrides
|
|
890
|
+
};
|
|
891
|
+
}
|
|
892
|
+
//#endregion
|
|
893
|
+
//#region src/ports/manifest-provider/edit/table-span.ts
|
|
894
|
+
/**
|
|
895
|
+
* Where a `[table]` is written, or undefined when the file has none.
|
|
896
|
+
*
|
|
897
|
+
* Found by reading lines rather than by re-serialising the parse tree. A
|
|
898
|
+
* manifest carries its author's comments, blank lines and chosen order, and
|
|
899
|
+
* rebuilding it from a tree would throw all of that away.
|
|
900
|
+
*/
|
|
901
|
+
function tableSpan(lines, name) {
|
|
902
|
+
const header = lines.findIndex((line) => line.trim() === `[${name}]`);
|
|
903
|
+
if (header < 0) return void 0;
|
|
904
|
+
let to = header + 1;
|
|
905
|
+
for (let at = header + 1; at < lines.length; at++) {
|
|
906
|
+
if (lines[at]?.trimStart().startsWith("[")) break;
|
|
907
|
+
if (lines[at]?.trim() !== "") to = at + 1;
|
|
908
|
+
}
|
|
909
|
+
return {
|
|
910
|
+
header,
|
|
911
|
+
from: header + 1,
|
|
912
|
+
to
|
|
913
|
+
};
|
|
914
|
+
}
|
|
915
|
+
/** The key a `key = value` line writes, or undefined for a comment or a blank. */
|
|
916
|
+
function keyOf(line) {
|
|
917
|
+
const text = line.trim();
|
|
918
|
+
if (text === "" || text.startsWith("#") || text.startsWith("[")) return void 0;
|
|
919
|
+
const equals = text.indexOf("=");
|
|
920
|
+
return equals < 0 ? void 0 : text.slice(0, equals).trim().replace(/^["']|["']$/g, "");
|
|
921
|
+
}
|
|
922
|
+
//#endregion
|
|
923
|
+
//#region src/ports/manifest-provider/edit/edit-dependency.ts
|
|
924
|
+
/** The table an edit touches unless told otherwise. */
|
|
925
|
+
const DEPENDENCIES = "dependencies";
|
|
926
|
+
/**
|
|
927
|
+
* The manifest with one dependency written in, everything else untouched.
|
|
928
|
+
*
|
|
929
|
+
* Entries are kept in name order, so two people adding two packages produce two
|
|
930
|
+
* one-line diffs rather than a conflict. A name already there is replaced where
|
|
931
|
+
* it stands, because an upgrade should not move the line.
|
|
932
|
+
*
|
|
933
|
+
* @returns the whole manifest as text. A missing table is appended at the end.
|
|
934
|
+
*/
|
|
935
|
+
function addDependency(args) {
|
|
936
|
+
const table = args.table ?? "dependencies";
|
|
937
|
+
const lines = args.text.split("\n");
|
|
938
|
+
const entry = `${args.name} = "${args.version}"`;
|
|
939
|
+
const span = tableSpan(lines, table);
|
|
940
|
+
if (!span) return appended(args.text, table, entry);
|
|
941
|
+
const at = lines.findIndex((line, index) => inside(span, index) && keyOf(line) === args.name);
|
|
942
|
+
if (at >= 0) return replaced(lines, at, entry);
|
|
943
|
+
return replaced(lines, insertionPoint(lines, span, args.name), entry, "insert");
|
|
944
|
+
}
|
|
945
|
+
/**
|
|
946
|
+
* The manifest without that dependency.
|
|
947
|
+
*
|
|
948
|
+
* @returns the text unchanged when the name is not there: absent is already the
|
|
949
|
+
* wanted state.
|
|
950
|
+
*/
|
|
951
|
+
function removeDependency(args) {
|
|
952
|
+
const table = args.table ?? "dependencies";
|
|
953
|
+
const lines = args.text.split("\n");
|
|
954
|
+
const span = tableSpan(lines, table);
|
|
955
|
+
if (!span) return args.text;
|
|
956
|
+
const at = lines.findIndex((line, index) => inside(span, index) && keyOf(line) === args.name);
|
|
957
|
+
if (at < 0) return args.text;
|
|
958
|
+
return [...lines.slice(0, at), ...lines.slice(at + 1)].join("\n");
|
|
959
|
+
}
|
|
960
|
+
function inside(span, index) {
|
|
961
|
+
return index >= span.from && index < span.to;
|
|
962
|
+
}
|
|
963
|
+
/** Where the name belongs, in order, among the entries already there. */
|
|
964
|
+
function insertionPoint(lines, span, name) {
|
|
965
|
+
for (let at = span.from; at < span.to; at++) {
|
|
966
|
+
const key = keyOf(lines[at] ?? "");
|
|
967
|
+
if (key && key > name) return at;
|
|
968
|
+
}
|
|
969
|
+
return span.to;
|
|
970
|
+
}
|
|
971
|
+
function replaced(lines, at, entry, how = "replace") {
|
|
972
|
+
const after = how === "insert" ? lines.slice(at) : lines.slice(at + 1);
|
|
973
|
+
return [
|
|
974
|
+
...lines.slice(0, at),
|
|
975
|
+
entry,
|
|
976
|
+
...after
|
|
977
|
+
].join("\n");
|
|
978
|
+
}
|
|
979
|
+
/** A table the manifest does not have yet, written at the end where it reads. */
|
|
980
|
+
function appended(text, table, entry) {
|
|
981
|
+
return `${text.endsWith("\n") ? text : `${text}\n`}\n[${table}]\n${entry}\n`;
|
|
982
|
+
}
|
|
983
|
+
//#endregion
|
|
984
|
+
//#region src/ports/manifest-provider/manifest.port.ts
|
|
985
|
+
/**
|
|
986
|
+
* The {@link ManifestProvider} contract. Implementations: `toml-manifest`,
|
|
987
|
+
* `memory-manifest`.
|
|
988
|
+
*
|
|
989
|
+
* Requires nothing: reading the file is the caller's job, and what arrives here
|
|
990
|
+
* is already text.
|
|
991
|
+
*/
|
|
992
|
+
const ManifestProviderPort = {
|
|
993
|
+
id: "venn.port.manifest",
|
|
994
|
+
version: 1,
|
|
995
|
+
requires: [],
|
|
996
|
+
methods: ["load"]
|
|
997
|
+
};
|
|
998
|
+
//#endregion
|
|
999
|
+
//#region src/ports/manifest-provider/memory-manifest.ts
|
|
1000
|
+
/**
|
|
1001
|
+
* The double: a preset manifest, with no file involved.
|
|
1002
|
+
*
|
|
1003
|
+
* Takes only what the caller cares about. The rest is what a project that
|
|
1004
|
+
* declared nothing would get, which is exactly what the real provider gives a
|
|
1005
|
+
* `venn.toml` silent on those tables.
|
|
1006
|
+
*/
|
|
1007
|
+
function createMemoryManifest(args) {
|
|
1008
|
+
const manifest = defaultManifest(args.manifest);
|
|
1009
|
+
return { load: () => manifest };
|
|
1010
|
+
}
|
|
1011
|
+
//#endregion
|
|
1012
|
+
//#region src/ports/manifest-provider/resolve-alias.ts
|
|
1013
|
+
/**
|
|
1014
|
+
* Split a `#alias/rest` import specifier against `[paths]` from `venn.toml`.
|
|
1015
|
+
*
|
|
1016
|
+
* @returns undefined when the specifier names no configured alias, which the
|
|
1017
|
+
* caller reads as a relative or bare path.
|
|
1018
|
+
*/
|
|
1019
|
+
function resolveAlias(args) {
|
|
1020
|
+
const alias = Object.keys(args.paths).find((key) => args.spec.startsWith(`${key}/`));
|
|
1021
|
+
if (!alias) return void 0;
|
|
1022
|
+
return {
|
|
1023
|
+
dir: args.paths[alias],
|
|
1024
|
+
rest: args.spec.slice(alias.length + 1)
|
|
1025
|
+
};
|
|
1026
|
+
}
|
|
1027
|
+
//#endregion
|
|
1028
|
+
//#region src/ports/manifest-provider/toml/read-value.ts
|
|
1029
|
+
/** A cursor over `text`, positioned at the start. */
|
|
1030
|
+
function cursor(text) {
|
|
1031
|
+
return {
|
|
1032
|
+
text,
|
|
1033
|
+
index: 0
|
|
1034
|
+
};
|
|
1035
|
+
}
|
|
1036
|
+
/**
|
|
1037
|
+
* One TOML value: a string, a number, a bool, an array, or an inline table.
|
|
1038
|
+
*
|
|
1039
|
+
* Reads by walking rather than by splitting, because a comma can sit inside a
|
|
1040
|
+
* value: splitting turns `["a,b"]` into two items and leaves
|
|
1041
|
+
* `{ version = "^4" }` as the text it was written in.
|
|
1042
|
+
*/
|
|
1043
|
+
function readValue(cur) {
|
|
1044
|
+
skipSpace(cur);
|
|
1045
|
+
const ch = cur.text[cur.index];
|
|
1046
|
+
if (ch === "\"" || ch === "'") return readString(cur);
|
|
1047
|
+
if (ch === "[") return readArray(cur);
|
|
1048
|
+
if (ch === "{") return readInlineTable(cur);
|
|
1049
|
+
return readBare(cur);
|
|
1050
|
+
}
|
|
1051
|
+
function skipSpace(cur) {
|
|
1052
|
+
while (/\s/.test(cur.text[cur.index] ?? "")) cur.index++;
|
|
1053
|
+
}
|
|
1054
|
+
/** A basic string reads escapes; a literal one, in single quotes, does not. */
|
|
1055
|
+
function readString(cur) {
|
|
1056
|
+
const quote = cur.text[cur.index];
|
|
1057
|
+
cur.index++;
|
|
1058
|
+
let out = "";
|
|
1059
|
+
while (cur.index < cur.text.length && cur.text[cur.index] !== quote) if (quote === "\"" && cur.text[cur.index] === "\\") out += unescaped(cur);
|
|
1060
|
+
else out += cur.text[cur.index++];
|
|
1061
|
+
cur.index++;
|
|
1062
|
+
return out;
|
|
1063
|
+
}
|
|
1064
|
+
const ESCAPES = {
|
|
1065
|
+
n: "\n",
|
|
1066
|
+
t: " ",
|
|
1067
|
+
r: "\r",
|
|
1068
|
+
"\\": "\\",
|
|
1069
|
+
"\"": "\""
|
|
1070
|
+
};
|
|
1071
|
+
function unescaped(cur) {
|
|
1072
|
+
const code = cur.text[cur.index + 1] ?? "";
|
|
1073
|
+
cur.index += 2;
|
|
1074
|
+
return ESCAPES[code] ?? code;
|
|
1075
|
+
}
|
|
1076
|
+
function readArray(cur) {
|
|
1077
|
+
cur.index++;
|
|
1078
|
+
const out = [];
|
|
1079
|
+
while (!atClose(cur, "]")) {
|
|
1080
|
+
out.push(readValue(cur));
|
|
1081
|
+
skipSeparator(cur);
|
|
1082
|
+
}
|
|
1083
|
+
cur.index++;
|
|
1084
|
+
return out;
|
|
1085
|
+
}
|
|
1086
|
+
function readInlineTable(cur) {
|
|
1087
|
+
cur.index++;
|
|
1088
|
+
const out = {};
|
|
1089
|
+
while (!atClose(cur, "}")) {
|
|
1090
|
+
const key = readKey(cur);
|
|
1091
|
+
if (key === void 0) break;
|
|
1092
|
+
out[key] = readValue(cur);
|
|
1093
|
+
skipSeparator(cur);
|
|
1094
|
+
}
|
|
1095
|
+
cur.index++;
|
|
1096
|
+
return out;
|
|
1097
|
+
}
|
|
1098
|
+
function readKey(cur) {
|
|
1099
|
+
skipSpace(cur);
|
|
1100
|
+
const equals = cur.text.indexOf("=", cur.index);
|
|
1101
|
+
if (equals < 0) return void 0;
|
|
1102
|
+
const raw = cur.text.slice(cur.index, equals).trim();
|
|
1103
|
+
cur.index = equals + 1;
|
|
1104
|
+
return raw.replace(/^["']|["']$/g, "");
|
|
1105
|
+
}
|
|
1106
|
+
function atClose(cur, close) {
|
|
1107
|
+
skipSpace(cur);
|
|
1108
|
+
return cur.index >= cur.text.length || cur.text[cur.index] === close;
|
|
1109
|
+
}
|
|
1110
|
+
function skipSeparator(cur) {
|
|
1111
|
+
skipSpace(cur);
|
|
1112
|
+
if (cur.text[cur.index] === ",") cur.index++;
|
|
1113
|
+
}
|
|
1114
|
+
/** Anything unquoted: `true`, `12`, `1.5`, or a word the manifest gives meaning. */
|
|
1115
|
+
function readBare(cur) {
|
|
1116
|
+
const start = cur.index;
|
|
1117
|
+
while (cur.index < cur.text.length && !",]}".includes(cur.text[cur.index] ?? "")) cur.index++;
|
|
1118
|
+
const raw = cur.text.slice(start, cur.index).trim();
|
|
1119
|
+
if (raw === "true" || raw === "false") return raw === "true";
|
|
1120
|
+
const num = Number(raw);
|
|
1121
|
+
return raw !== "" && !Number.isNaN(num) ? num : raw;
|
|
1122
|
+
}
|
|
1123
|
+
//#endregion
|
|
1124
|
+
//#region src/ports/manifest-provider/toml/sections.ts
|
|
1125
|
+
/** The table `[path]` names, created along the way if it is not there yet. */
|
|
1126
|
+
function enterSection(root, path) {
|
|
1127
|
+
let node = root;
|
|
1128
|
+
for (const key of keysOf(path)) {
|
|
1129
|
+
node[key] ??= {};
|
|
1130
|
+
node = node[key];
|
|
1131
|
+
}
|
|
1132
|
+
return node;
|
|
1133
|
+
}
|
|
1134
|
+
/**
|
|
1135
|
+
* `[[bin]]`: one more of something there may be several of.
|
|
1136
|
+
*
|
|
1137
|
+
* Returns the table just appended, so two `[[bin]]` sections give two bins
|
|
1138
|
+
* rather than one bin written twice.
|
|
1139
|
+
*/
|
|
1140
|
+
function enterTableArray(root, path) {
|
|
1141
|
+
const keys = keysOf(path);
|
|
1142
|
+
const last = keys.pop();
|
|
1143
|
+
if (!last) return root;
|
|
1144
|
+
const parent = enterSection(root, keys.join("."));
|
|
1145
|
+
const found = Array.isArray(parent[last]) ? parent[last] : [];
|
|
1146
|
+
parent[last] = found;
|
|
1147
|
+
const table = {};
|
|
1148
|
+
found.push(table);
|
|
1149
|
+
return table;
|
|
1150
|
+
}
|
|
1151
|
+
function keysOf(path) {
|
|
1152
|
+
return path.split(".").map((part) => part.trim().replace(/^["']|["']$/g, "")).filter((part) => part !== "");
|
|
1153
|
+
}
|
|
1154
|
+
//#endregion
|
|
1155
|
+
//#region src/ports/manifest-provider/toml/parse-toml.ts
|
|
1156
|
+
/**
|
|
1157
|
+
* A TOML reader for `venn.toml`: sections, nested `[a.b]`, arrays of tables
|
|
1158
|
+
* (`[[bin]]`), `key = value`, strings, numbers, bools, arrays and inline tables.
|
|
1159
|
+
*
|
|
1160
|
+
* Not a full TOML parser. No dates, no multi-line strings, no dotted keys.
|
|
1161
|
+
* `venn.toml` is our own format, so the subset is a decision rather than a
|
|
1162
|
+
* shortfall.
|
|
1163
|
+
*
|
|
1164
|
+
* @returns the root table. Malformed lines are skipped, never thrown on.
|
|
1165
|
+
*/
|
|
1166
|
+
function parseToml(content) {
|
|
1167
|
+
const root = {};
|
|
1168
|
+
let section = root;
|
|
1169
|
+
for (const raw of content.split(/\r?\n/)) {
|
|
1170
|
+
const line = stripComment(raw).trim();
|
|
1171
|
+
if (line === "") continue;
|
|
1172
|
+
if (line.startsWith("[")) section = openSection(root, line);
|
|
1173
|
+
else assign(section, line);
|
|
1174
|
+
}
|
|
1175
|
+
return root;
|
|
1176
|
+
}
|
|
1177
|
+
/** `[[bin]]` appends another table, `[bin]` opens the one and only. */
|
|
1178
|
+
function openSection(root, line) {
|
|
1179
|
+
if (line.startsWith("[[")) return enterTableArray(root, line.slice(2, line.indexOf("]]")));
|
|
1180
|
+
return enterSection(root, line.slice(1, line.indexOf("]")));
|
|
1181
|
+
}
|
|
1182
|
+
function stripComment(line) {
|
|
1183
|
+
let quote;
|
|
1184
|
+
for (let i = 0; i < line.length; i++) {
|
|
1185
|
+
const ch = line[i];
|
|
1186
|
+
if (ch === "\"" || ch === "'") quote = quote === ch ? void 0 : quote ?? ch;
|
|
1187
|
+
else if (ch === "#" && !quote) return line.slice(0, i);
|
|
1188
|
+
}
|
|
1189
|
+
return line;
|
|
1190
|
+
}
|
|
1191
|
+
function assign(section, line) {
|
|
1192
|
+
const eq = line.indexOf("=");
|
|
1193
|
+
if (eq < 0) return;
|
|
1194
|
+
const key = line.slice(0, eq).trim().replace(/^["']|["']$/g, "");
|
|
1195
|
+
section[key] = readValue(cursor(line.slice(eq + 1)));
|
|
1196
|
+
}
|
|
1197
|
+
//#endregion
|
|
1198
|
+
//#region src/ports/manifest-provider/toml-docs.ts
|
|
1199
|
+
const COMMENT = /^\s*#\s?(.*)$/;
|
|
1200
|
+
const KEY = /^\s*([A-Za-z_][\w-]*)\s*=/;
|
|
1201
|
+
/**
|
|
1202
|
+
* The comment block written directly above a key, read as that key's
|
|
1203
|
+
* documentation. The same idea as `##` above a declaration in a `.vn`.
|
|
1204
|
+
*
|
|
1205
|
+
* A blank line breaks the block, so a comment separated from a key belongs to
|
|
1206
|
+
* nobody and is never attributed to the next one down.
|
|
1207
|
+
*
|
|
1208
|
+
* @returns documentation keyed by the key it sits above.
|
|
1209
|
+
*/
|
|
1210
|
+
function tomlDocs(content) {
|
|
1211
|
+
const docs = {};
|
|
1212
|
+
let block = [];
|
|
1213
|
+
for (const line of content.split(/\r?\n/)) {
|
|
1214
|
+
const comment = COMMENT.exec(line);
|
|
1215
|
+
if (comment) {
|
|
1216
|
+
block.push(comment[1] ?? "");
|
|
1217
|
+
continue;
|
|
1218
|
+
}
|
|
1219
|
+
const key = KEY.exec(line)?.[1];
|
|
1220
|
+
if (key && block.length > 0) docs[key] = block.join("\n").trim();
|
|
1221
|
+
block = [];
|
|
1222
|
+
}
|
|
1223
|
+
return docs;
|
|
1224
|
+
}
|
|
1225
|
+
//#endregion
|
|
1226
|
+
//#region src/ports/manifest-provider/read-run-settings.ts
|
|
1227
|
+
/**
|
|
1228
|
+
* How the project runs, as against what it is: environments, path aliases and
|
|
1229
|
+
* formatting.
|
|
1230
|
+
*/
|
|
1231
|
+
function readRunSettings(data) {
|
|
1232
|
+
return {
|
|
1233
|
+
env: readEnv(data.env),
|
|
1234
|
+
envFiles: asList(asRecord(data.env).files),
|
|
1235
|
+
paths: asStringMap(data.paths),
|
|
1236
|
+
format: readFormat(data.format)
|
|
1237
|
+
};
|
|
1238
|
+
}
|
|
1239
|
+
function readFormat(value) {
|
|
1240
|
+
const table = asRecord(value);
|
|
1241
|
+
return {
|
|
1242
|
+
indent: asNumber(table.indent),
|
|
1243
|
+
tabs: asBoolean(table.tabs),
|
|
1244
|
+
organize: asBoolean(table.organize),
|
|
1245
|
+
sort: asBoolean(table.sort)
|
|
1246
|
+
};
|
|
1247
|
+
}
|
|
1248
|
+
/** `files` configures where to read from; every other key names an environment. */
|
|
1249
|
+
const RESERVED = /* @__PURE__ */ new Set(["files"]);
|
|
1250
|
+
function readEnv(value) {
|
|
1251
|
+
const out = {};
|
|
1252
|
+
for (const [name, vars] of Object.entries(asRecord(value))) if (!RESERVED.has(name)) out[name] = asStringMap(vars);
|
|
1253
|
+
return out;
|
|
1254
|
+
}
|
|
1255
|
+
//#endregion
|
|
1256
|
+
//#region src/ports/manifest-provider/toml-manifest.ts
|
|
1257
|
+
/**
|
|
1258
|
+
* The real one: parse `venn.toml` into a {@link Manifest}.
|
|
1259
|
+
*
|
|
1260
|
+
* Parsing happens once, at construction, so repeated `load()` calls are free
|
|
1261
|
+
* and always agree.
|
|
1262
|
+
*
|
|
1263
|
+
* @param args.content - the manifest as written.
|
|
1264
|
+
*/
|
|
1265
|
+
function createTomlManifest(args) {
|
|
1266
|
+
const manifest = toManifest(parseToml(args.content));
|
|
1267
|
+
return { load: () => manifest };
|
|
1268
|
+
}
|
|
1269
|
+
function toManifest(data) {
|
|
1270
|
+
const pkg = readPackage(data);
|
|
1271
|
+
return {
|
|
1272
|
+
name: pkg.name,
|
|
1273
|
+
version: pkg.version ?? "0.0.0",
|
|
1274
|
+
package: pkg,
|
|
1275
|
+
targets: readTargets(data, pkg.name),
|
|
1276
|
+
dependencies: readDependencies(data.dependencies),
|
|
1277
|
+
devDependencies: readDependencies(data["dev-dependencies"]),
|
|
1278
|
+
patch: readDependencies(data.patch),
|
|
1279
|
+
profiles: readProfiles(data),
|
|
1280
|
+
tooling: readTooling(data),
|
|
1281
|
+
workspace: readWorkspace(data),
|
|
1282
|
+
...readRunSettings(data)
|
|
1283
|
+
};
|
|
1284
|
+
}
|
|
1285
|
+
//#endregion
|
|
1286
|
+
//#region src/ports/signal-source/fake-signals.ts
|
|
1287
|
+
/**
|
|
1288
|
+
* The double: a signal arrives because a test said so. No process, no operating
|
|
1289
|
+
* system, no risk of stopping the test runner along with the code under test.
|
|
1290
|
+
*/
|
|
1291
|
+
function createFakeSignals() {
|
|
1292
|
+
const handlers = /* @__PURE__ */ new Map();
|
|
1293
|
+
return {
|
|
1294
|
+
get listening() {
|
|
1295
|
+
return [...handlers].filter(([, set]) => set.size > 0).map(([signal]) => signal);
|
|
1296
|
+
},
|
|
1297
|
+
on: (signal, handler) => subscribe({
|
|
1298
|
+
handlers,
|
|
1299
|
+
signal,
|
|
1300
|
+
handler
|
|
1301
|
+
}),
|
|
1302
|
+
raise: (signal) => {
|
|
1303
|
+
for (const handler of [...handlers.get(signal) ?? []]) handler(signal);
|
|
1304
|
+
}
|
|
1305
|
+
};
|
|
1306
|
+
}
|
|
1307
|
+
function subscribe(args) {
|
|
1308
|
+
const set = args.handlers.get(args.signal) ?? /* @__PURE__ */ new Set();
|
|
1309
|
+
set.add(args.handler);
|
|
1310
|
+
args.handlers.set(args.signal, set);
|
|
1311
|
+
return () => {
|
|
1312
|
+
set.delete(args.handler);
|
|
1313
|
+
};
|
|
1314
|
+
}
|
|
1315
|
+
//#endregion
|
|
1316
|
+
//#region src/ports/signal-source/signal-source.port.ts
|
|
1317
|
+
/**
|
|
1318
|
+
* The {@link SignalSource} contract. Implementations: `node-signals`,
|
|
1319
|
+
* `fake-signals`.
|
|
1320
|
+
*/
|
|
1321
|
+
const SignalSourcePort = {
|
|
1322
|
+
id: "venn.port.signals",
|
|
1323
|
+
version: 1,
|
|
1324
|
+
requires: ["process"],
|
|
1325
|
+
methods: ["on"]
|
|
1326
|
+
};
|
|
1327
|
+
//#endregion
|
|
1328
|
+
//#region src/ports/signal-source/signal-source.types.ts
|
|
1329
|
+
/**
|
|
1330
|
+
* The ways a system asks a program to stop. `SIGBREAK` is Windows' Ctrl+Break,
|
|
1331
|
+
* `SIGHUP` is the terminal itself going away.
|
|
1332
|
+
*/
|
|
1333
|
+
const ALL_SIGNALS = [
|
|
1334
|
+
"SIGINT",
|
|
1335
|
+
"SIGTERM",
|
|
1336
|
+
"SIGBREAK",
|
|
1337
|
+
"SIGHUP"
|
|
1338
|
+
];
|
|
1339
|
+
//#endregion
|
|
1340
|
+
export { ALL_CAPABILITIES, ALL_SIGNALS, BIN_DIR, ClockPort, ConsolePort, DEFAULT_PROFILES, DEPENDENCIES, DOTENV_CONVENTION, FileSystemPort, LIB_ROOT, LockProviderPort, MAIN_ROOT, ManifestProviderPort, ProcessProviderPort, REDACTED, RandomPort, SecretProviderPort, SignalSourcePort, VennError, addDependency, assertCapabilities, assertPortShape, bindPort, capabilityUnavailable, createConsoleLogger, createEnvSecrets, createFakeLock, createFakeProcess, createFakeSignals, createFixedRandom, createHost, createInProcessLock, createMemoryConsole, createMemoryFs, createMemoryLogger, createMemoryManifest, createMemorySecrets, createSeededRandom, createSystemClock, createTestHost, createTomlManifest, createVirtualClock, createWorkerHost, defaultManifest, dotenvFiles, fsNotFound, hostMissingCapability, makeSecret, missingCapabilities, parseDotenv, parseToml, portShapeMismatch, readInheritable, removeDependency, resolveAlias, tomlDocs, unavailable };
|
|
1341
|
+
|
|
1342
|
+
//# sourceMappingURL=index.js.map
|