1tube 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,34 @@
1
+ /**
2
+ * The `1tube/test` contract every runner binding implements.
3
+ *
4
+ * Edge function tests import `test`, `describe`, hooks and assertions from
5
+ * `1tube/test` instead of a specific runner, so the same files run under
6
+ * `bun test` today and under another runtime's runner (node:test, a workerd
7
+ * pool) later. Keep this surface to what every runner can express: no test
8
+ * context argument, no runner-specific options.
9
+ */
10
+ export type TestFn = () => void | Promise<unknown>;
11
+ export type HookFn = () => void | Promise<unknown>;
12
+ export interface TestOptions {
13
+ /** Per-test timeout in milliseconds (runner default when omitted). */
14
+ timeout?: number;
15
+ /** Register but do not run. */
16
+ skip?: boolean;
17
+ /** Run only tests marked `only` (where the runner supports focusing). */
18
+ only?: boolean;
19
+ }
20
+ export interface TestFunction {
21
+ (name: string, fn: TestFn, options?: TestOptions): void;
22
+ skip(name: string, fn: TestFn, options?: TestOptions): void;
23
+ only(name: string, fn: TestFn, options?: TestOptions): void;
24
+ /** A placeholder for a test that is not written yet. */
25
+ todo(name: string): void;
26
+ }
27
+ export interface DescribeFunction {
28
+ (name: string, fn: () => void): void;
29
+ skip(name: string, fn: () => void): void;
30
+ only(name: string, fn: () => void): void;
31
+ }
32
+ export type Hook = (fn: HookFn) => void;
33
+ /** Which runner the active binding drives. */
34
+ export type TestRuntime = "bun" | "node";
@@ -0,0 +1,10 @@
1
+ /**
2
+ * The `1tube/test` contract every runner binding implements.
3
+ *
4
+ * Edge function tests import `test`, `describe`, hooks and assertions from
5
+ * `1tube/test` instead of a specific runner, so the same files run under
6
+ * `bun test` today and under another runtime's runner (node:test, a workerd
7
+ * pool) later. Keep this surface to what every runner can express: no test
8
+ * context argument, no runner-specific options.
9
+ */
10
+ export {};
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Runtime-agnostic assertions for `1tube/test`.
3
+ *
4
+ * Pure TypeScript with no runtime imports, so the same assertions run under
5
+ * `bun test`, `node --test`, or a workerd test pool. The surface follows the
6
+ * familiar `assertX(actual, expected, msg?)` shape (the one Supabase-style
7
+ * edge function tests are written against), so test files only change their
8
+ * import line when moving between runners.
9
+ */
10
+ export declare class AssertionError extends Error {
11
+ name: string;
12
+ constructor(message: string);
13
+ }
14
+ type AnyConstructor = abstract new (...args: never[]) => unknown;
15
+ type ErrorClass<E extends Error = Error> = abstract new (...args: never[]) => E;
16
+ /** Readable, deterministic rendering of a value for failure messages. */
17
+ export declare function format(value: unknown): string;
18
+ /**
19
+ * Deep structural equality: same constructor, same own enumerable keys
20
+ * (strings and symbols), recursively equal values. `NaN` equals `NaN`;
21
+ * `0` and `-0` differ. Maps and Sets compare by content, order-insensitive.
22
+ */
23
+ export declare function equal(a: unknown, b: unknown): boolean;
24
+ /** Assert that `expr` is truthy. */
25
+ export declare function assert(expr: unknown, msg?: string): asserts expr;
26
+ /** Assert that `expr` is falsy. */
27
+ export declare function assertFalse(expr: unknown, msg?: string): void;
28
+ /** Deep equality (see {@link equal}). */
29
+ export declare function assertEquals<T>(actual: T, expected: T, msg?: string): void;
30
+ export declare function assertNotEquals<T>(actual: T, expected: T, msg?: string): void;
31
+ /** Identity (`Object.is`). */
32
+ export declare function assertStrictEquals<T>(actual: unknown, expected: T, msg?: string): asserts actual is T;
33
+ export declare function assertNotStrictEquals<T>(actual: T, expected: T, msg?: string): void;
34
+ /** |actual - expected| <= tolerance (default 1e-7). */
35
+ export declare function assertAlmostEquals(actual: number, expected: number, tolerance?: number, msg?: string): void;
36
+ /** Not `null` and not `undefined`. */
37
+ export declare function assertExists<T>(actual: T, msg?: string): asserts actual is NonNullable<T>;
38
+ export declare function assertInstanceOf<T extends AnyConstructor>(actual: unknown, expectedType: T, msg?: string): asserts actual is InstanceType<T>;
39
+ export declare function assertNotInstanceOf<T extends AnyConstructor>(actual: unknown, unexpectedType: T, msg?: string): void;
40
+ export declare function assertStringIncludes(actual: string, expected: string, msg?: string): void;
41
+ export declare function assertMatch(actual: string, expected: RegExp, msg?: string): void;
42
+ export declare function assertNotMatch(actual: string, expected: RegExp, msg?: string): void;
43
+ /** Every element of `expected` is deep-equal to some element of `actual`. */
44
+ export declare function assertArrayIncludes<T>(actual: ArrayLike<T>, expected: ArrayLike<T>, msg?: string): void;
45
+ /** `actual` contains (recursively) every key/value of `expected`. */
46
+ export declare function assertObjectMatch(actual: Record<PropertyKey, unknown>, expected: Record<PropertyKey, unknown>, msg?: string): void;
47
+ export declare function assertGreater<T>(actual: T, expected: T, msg?: string): void;
48
+ export declare function assertGreaterOrEqual<T>(actual: T, expected: T, msg?: string): void;
49
+ export declare function assertLess<T>(actual: T, expected: T, msg?: string): void;
50
+ export declare function assertLessOrEqual<T>(actual: T, expected: T, msg?: string): void;
51
+ /** Assert `fn` throws; returns the thrown value. */
52
+ export declare function assertThrows(fn: () => unknown, msg?: string): unknown;
53
+ export declare function assertThrows<E extends Error = Error>(fn: () => unknown, ErrorClass: ErrorClass<E>, msgIncludes?: string, msg?: string): E;
54
+ /** Assert the promise (or `fn()`'s promise) rejects; resolves to the reason. */
55
+ export declare function assertRejects(fn: (() => PromiseLike<unknown>) | PromiseLike<unknown>, msg?: string): Promise<unknown>;
56
+ export declare function assertRejects<E extends Error = Error>(fn: (() => PromiseLike<unknown>) | PromiseLike<unknown>, ErrorClass: ErrorClass<E>, msgIncludes?: string, msg?: string): Promise<E>;
57
+ /** Unconditionally fail. */
58
+ export declare function fail(msg?: string): never;
59
+ export declare function unreachable(msg?: string): never;
60
+ export declare function unimplemented(msg?: string): never;
61
+ export {};
@@ -0,0 +1,430 @@
1
+ /**
2
+ * Runtime-agnostic assertions for `1tube/test`.
3
+ *
4
+ * Pure TypeScript with no runtime imports, so the same assertions run under
5
+ * `bun test`, `node --test`, or a workerd test pool. The surface follows the
6
+ * familiar `assertX(actual, expected, msg?)` shape (the one Supabase-style
7
+ * edge function tests are written against), so test files only change their
8
+ * import line when moving between runners.
9
+ */
10
+ export class AssertionError extends Error {
11
+ name = "AssertionError";
12
+ constructor(message) {
13
+ super(message);
14
+ }
15
+ }
16
+ // ---------------------------------------------------------------------------
17
+ // Formatting
18
+ // ---------------------------------------------------------------------------
19
+ /** Readable, deterministic rendering of a value for failure messages. */
20
+ export function format(value) {
21
+ const seen = new WeakSet();
22
+ const indent = (s) => s.replace(/\n/g, "\n ");
23
+ const render = (v) => {
24
+ switch (typeof v) {
25
+ case "string":
26
+ return JSON.stringify(v);
27
+ case "bigint":
28
+ return `${v}n`;
29
+ case "symbol":
30
+ case "undefined":
31
+ case "boolean":
32
+ return String(v);
33
+ case "number":
34
+ return Object.is(v, -0) ? "-0" : String(v);
35
+ case "function":
36
+ return `[Function: ${v.name || "anonymous"}]`;
37
+ }
38
+ if (v === null)
39
+ return "null";
40
+ const obj = v;
41
+ if (seen.has(obj))
42
+ return "[Circular]";
43
+ if (obj instanceof Date)
44
+ return `Date(${Number.isNaN(obj.getTime()) ? "Invalid" : obj.toISOString()})`;
45
+ if (obj instanceof RegExp)
46
+ return String(obj);
47
+ if (obj instanceof Error)
48
+ return `${obj.name}: ${obj.message}`;
49
+ if (typeof URL !== "undefined" && obj instanceof URL)
50
+ return `URL(${obj.href})`;
51
+ seen.add(obj);
52
+ try {
53
+ if (Array.isArray(obj)) {
54
+ if (obj.length === 0)
55
+ return "[]";
56
+ return `[\n ${obj.map((x) => indent(render(x))).join(",\n ")},\n]`;
57
+ }
58
+ if (ArrayBuffer.isView(obj) && !(obj instanceof DataView)) {
59
+ const items = Array.from(obj, (x) => render(x));
60
+ return `${obj.constructor.name}(${items.length}) [${items.join(", ")}]`;
61
+ }
62
+ if (obj instanceof Map) {
63
+ if (obj.size === 0)
64
+ return "Map(0) {}";
65
+ const rows = [...obj].map(([k, x]) => `${indent(render(k))} => ${indent(render(x))}`);
66
+ return `Map(${obj.size}) {\n ${rows.join(",\n ")},\n}`;
67
+ }
68
+ if (obj instanceof Set) {
69
+ if (obj.size === 0)
70
+ return "Set(0) {}";
71
+ return `Set(${obj.size}) {\n ${[...obj].map((x) => indent(render(x))).join(",\n ")},\n}`;
72
+ }
73
+ const keys = [
74
+ ...Object.keys(obj),
75
+ ...Object.getOwnPropertySymbols(obj).filter((s) => Object.prototype.propertyIsEnumerable.call(obj, s)),
76
+ ];
77
+ const proto = Object.getPrototypeOf(obj);
78
+ const tag = proto === null
79
+ ? "[Object: null prototype] "
80
+ : proto !== Object.prototype && proto?.constructor?.name
81
+ ? `${proto.constructor.name} `
82
+ : "";
83
+ if (keys.length === 0)
84
+ return `${tag}{}`;
85
+ const rows = keys.map((k) => {
86
+ const name = typeof k === "symbol"
87
+ ? `[${String(k)}]`
88
+ : /^[A-Za-z_$][\w$]*$/.test(k)
89
+ ? k
90
+ : JSON.stringify(k);
91
+ return `${name}: ${indent(render(obj[k]))}`;
92
+ });
93
+ return `${tag}{\n ${rows.join(",\n ")},\n}`;
94
+ }
95
+ finally {
96
+ seen.delete(obj);
97
+ }
98
+ };
99
+ return render(value);
100
+ }
101
+ /** Line-level diff of two formatted values (`-` actual, `+` expected). */
102
+ function diff(actual, expected) {
103
+ const a = format(actual).split("\n");
104
+ const b = format(expected).split("\n");
105
+ // LCS over lines: values printed for assertion failures are small.
106
+ const lcs = Array.from({ length: a.length + 1 }, () => new Array(b.length + 1).fill(0));
107
+ for (let i = a.length - 1; i >= 0; i--) {
108
+ for (let j = b.length - 1; j >= 0; j--) {
109
+ lcs[i][j] = a[i] === b[j] ? lcs[i + 1][j + 1] + 1 : Math.max(lcs[i + 1][j], lcs[i][j + 1]);
110
+ }
111
+ }
112
+ const out = [];
113
+ let i = 0;
114
+ let j = 0;
115
+ while (i < a.length || j < b.length) {
116
+ if (i < a.length && j < b.length && a[i] === b[j]) {
117
+ out.push(` ${a[i]}`);
118
+ i++;
119
+ j++;
120
+ }
121
+ else if (j < b.length && (i >= a.length || lcs[i][j + 1] >= lcs[i + 1][j])) {
122
+ out.push(`+ ${b[j++]}`);
123
+ }
124
+ else {
125
+ out.push(`- ${a[i++]}`);
126
+ }
127
+ }
128
+ return out.join("\n");
129
+ }
130
+ const suffix = (msg) => (msg ? `: ${msg}` : "");
131
+ // ---------------------------------------------------------------------------
132
+ // Equality
133
+ // ---------------------------------------------------------------------------
134
+ /**
135
+ * Deep structural equality: same constructor, same own enumerable keys
136
+ * (strings and symbols), recursively equal values. `NaN` equals `NaN`;
137
+ * `0` and `-0` differ. Maps and Sets compare by content, order-insensitive.
138
+ */
139
+ export function equal(a, b) {
140
+ const seen = new Map();
141
+ const eq = (x, y) => {
142
+ if (Object.is(x, y))
143
+ return true;
144
+ if (typeof x !== "object" || typeof y !== "object" || x === null || y === null) {
145
+ return false;
146
+ }
147
+ if (seen.get(x) === y)
148
+ return true;
149
+ if (Object.getPrototypeOf(x) !== Object.getPrototypeOf(y))
150
+ return false;
151
+ seen.set(x, y);
152
+ if (x instanceof Date)
153
+ return Object.is(x.getTime(), y.getTime());
154
+ if (x instanceof RegExp)
155
+ return String(x) === String(y);
156
+ if (typeof URL !== "undefined" && x instanceof URL)
157
+ return x.href === y.href;
158
+ if (x instanceof WeakMap || x instanceof WeakSet || x instanceof WeakRef) {
159
+ throw new TypeError("cannot compare WeakMap, WeakSet or WeakRef instances");
160
+ }
161
+ if (x instanceof Number || x instanceof String || x instanceof Boolean) {
162
+ if (!Object.is(x.valueOf(), y.valueOf()))
163
+ return false;
164
+ }
165
+ if (x instanceof ArrayBuffer) {
166
+ return eq(new Uint8Array(x), new Uint8Array(y));
167
+ }
168
+ if (x instanceof DataView) {
169
+ return eq(new Uint8Array(x.buffer, x.byteOffset, x.byteLength), new Uint8Array(y.buffer, y.byteOffset, y.byteLength));
170
+ }
171
+ if (ArrayBuffer.isView(x)) {
172
+ const xa = x;
173
+ const ya = y;
174
+ if (xa.length !== ya.length)
175
+ return false;
176
+ for (let i = 0; i < xa.length; i++)
177
+ if (!Object.is(xa[i], ya[i]))
178
+ return false;
179
+ return true;
180
+ }
181
+ if (x instanceof Map) {
182
+ const ym = y;
183
+ if (x.size !== ym.size)
184
+ return false;
185
+ const unmatched = [...ym];
186
+ for (const [k, v] of x) {
187
+ const idx = unmatched.findIndex(([k2, v2]) => eq(k, k2) && eq(v, v2));
188
+ if (idx === -1)
189
+ return false;
190
+ unmatched.splice(idx, 1);
191
+ }
192
+ return true;
193
+ }
194
+ if (x instanceof Set) {
195
+ const ys = y;
196
+ if (x.size !== ys.size)
197
+ return false;
198
+ const unmatched = [...ys];
199
+ for (const v of x) {
200
+ const idx = unmatched.findIndex((v2) => eq(v, v2));
201
+ if (idx === -1)
202
+ return false;
203
+ unmatched.splice(idx, 1);
204
+ }
205
+ return true;
206
+ }
207
+ if (x instanceof Error) {
208
+ const ye = y;
209
+ if (x.name !== ye.name || x.message !== ye.message)
210
+ return false;
211
+ }
212
+ const keysOf = (o) => [
213
+ ...Object.keys(o),
214
+ ...Object.getOwnPropertySymbols(o).filter((s) => Object.prototype.propertyIsEnumerable.call(o, s)),
215
+ ];
216
+ const xk = keysOf(x);
217
+ const yk = keysOf(y);
218
+ if (xk.length !== yk.length)
219
+ return false;
220
+ const rec = (o, k) => o[k];
221
+ for (const k of xk) {
222
+ if (!Object.prototype.hasOwnProperty.call(y, k))
223
+ return false;
224
+ if (!eq(rec(x, k), rec(y, k)))
225
+ return false;
226
+ }
227
+ return true;
228
+ };
229
+ return eq(a, b);
230
+ }
231
+ // ---------------------------------------------------------------------------
232
+ // Assertions
233
+ // ---------------------------------------------------------------------------
234
+ /** Assert that `expr` is truthy. */
235
+ export function assert(expr, msg = "") {
236
+ if (!expr)
237
+ throw new AssertionError(msg || "Expected value to be truthy");
238
+ }
239
+ /** Assert that `expr` is falsy. */
240
+ export function assertFalse(expr, msg = "") {
241
+ if (expr)
242
+ throw new AssertionError(msg || `Expected value to be falsy, got ${format(expr)}`);
243
+ }
244
+ /** Deep equality (see {@link equal}). */
245
+ export function assertEquals(actual, expected, msg) {
246
+ if (equal(actual, expected))
247
+ return;
248
+ throw new AssertionError(`Values are not equal${suffix(msg)}\n\n [Diff] Actual / Expected\n\n${diff(actual, expected)}\n`);
249
+ }
250
+ export function assertNotEquals(actual, expected, msg) {
251
+ if (!equal(actual, expected))
252
+ return;
253
+ throw new AssertionError(`Expected actual: ${format(actual)} not to be: ${format(expected)}${suffix(msg)}`);
254
+ }
255
+ /** Identity (`Object.is`). */
256
+ export function assertStrictEquals(actual, expected, msg) {
257
+ if (Object.is(actual, expected))
258
+ return;
259
+ const sameRendering = format(actual) === format(expected);
260
+ throw new AssertionError(sameRendering
261
+ ? `Values have the same structure but are not reference-equal${suffix(msg)}\n\n ${format(actual).replace(/\n/g, "\n ")}\n`
262
+ : `Values are not strictly equal${suffix(msg)}\n\n [Diff] Actual / Expected\n\n${diff(actual, expected)}\n`);
263
+ }
264
+ export function assertNotStrictEquals(actual, expected, msg) {
265
+ if (!Object.is(actual, expected))
266
+ return;
267
+ throw new AssertionError(`Expected "actual" to not be strictly equal to: ${format(expected)}${suffix(msg)}`);
268
+ }
269
+ /** |actual - expected| <= tolerance (default 1e-7). */
270
+ export function assertAlmostEquals(actual, expected, tolerance = 1e-7, msg) {
271
+ if (Object.is(actual, expected))
272
+ return;
273
+ const delta = Math.abs(expected - actual);
274
+ if (delta <= tolerance)
275
+ return;
276
+ throw new AssertionError(`Expected actual: "${actual}" to be close to "${expected}": delta "${delta}" is greater than "${tolerance}"${suffix(msg)}`);
277
+ }
278
+ /** Not `null` and not `undefined`. */
279
+ export function assertExists(actual, msg) {
280
+ if (actual === undefined || actual === null) {
281
+ throw new AssertionError(msg || `Expected actual: "${actual}" to not be null or undefined`);
282
+ }
283
+ }
284
+ export function assertInstanceOf(actual, expectedType, msg) {
285
+ if (actual instanceof expectedType)
286
+ return;
287
+ const got = actual === null
288
+ ? "null"
289
+ : typeof actual === "object"
290
+ ? (Object.getPrototypeOf(actual)?.constructor?.name ?? "Object")
291
+ : typeof actual;
292
+ throw new AssertionError(`Expected object to be an instance of "${expectedType.name}" but was "${got}"${suffix(msg)}`);
293
+ }
294
+ export function assertNotInstanceOf(actual, unexpectedType, msg) {
295
+ if (!(actual instanceof unexpectedType))
296
+ return;
297
+ throw new AssertionError(`Expected object to not be an instance of "${unexpectedType.name}"${suffix(msg)}`);
298
+ }
299
+ export function assertStringIncludes(actual, expected, msg) {
300
+ if (typeof actual === "string" && actual.includes(expected))
301
+ return;
302
+ throw new AssertionError(msg || `Expected actual: ${format(actual)} to contain: ${format(expected)}`);
303
+ }
304
+ export function assertMatch(actual, expected, msg) {
305
+ expected.lastIndex = 0;
306
+ if (expected.test(actual))
307
+ return;
308
+ throw new AssertionError(msg || `Expected actual: ${format(actual)} to match: ${expected}`);
309
+ }
310
+ export function assertNotMatch(actual, expected, msg) {
311
+ expected.lastIndex = 0;
312
+ if (!expected.test(actual))
313
+ return;
314
+ throw new AssertionError(msg || `Expected actual: ${format(actual)} to not match: ${expected}`);
315
+ }
316
+ /** Every element of `expected` is deep-equal to some element of `actual`. */
317
+ export function assertArrayIncludes(actual, expected, msg) {
318
+ const missing = [];
319
+ for (const e of Array.from(expected)) {
320
+ if (!Array.from(actual).some((a) => equal(a, e)))
321
+ missing.push(e);
322
+ }
323
+ if (missing.length === 0)
324
+ return;
325
+ throw new AssertionError(msg || `Expected actual: ${format(actual)} to include: ${format(expected)}\nmissing: ${format(missing)}`);
326
+ }
327
+ /** `actual` contains (recursively) every key/value of `expected`. */
328
+ export function assertObjectMatch(actual, expected, msg) {
329
+ const pick = (a, e) => {
330
+ if (Array.isArray(e) && Array.isArray(a))
331
+ return a.map((x, i) => (i < e.length ? pick(x, e[i]) : x));
332
+ if (e && typeof e === "object" && a && typeof a === "object" && !(e instanceof Date) && !(e instanceof RegExp)) {
333
+ const out = {};
334
+ for (const k of Reflect.ownKeys(e)) {
335
+ if (k in a)
336
+ out[k] = pick(a[k], e[k]);
337
+ }
338
+ return out;
339
+ }
340
+ return a;
341
+ };
342
+ assertEquals(pick(actual, expected), expected, msg);
343
+ }
344
+ export function assertGreater(actual, expected, msg) {
345
+ if (actual > expected)
346
+ return;
347
+ throw new AssertionError(msg || `Expect ${format(actual)} > ${format(expected)}`);
348
+ }
349
+ export function assertGreaterOrEqual(actual, expected, msg) {
350
+ if (actual >= expected)
351
+ return;
352
+ throw new AssertionError(msg || `Expect ${format(actual)} >= ${format(expected)}`);
353
+ }
354
+ export function assertLess(actual, expected, msg) {
355
+ if (actual < expected)
356
+ return;
357
+ throw new AssertionError(msg || `Expect ${format(actual)} < ${format(expected)}`);
358
+ }
359
+ export function assertLessOrEqual(actual, expected, msg) {
360
+ if (actual <= expected)
361
+ return;
362
+ throw new AssertionError(msg || `Expect ${format(actual)} <= ${format(expected)}`);
363
+ }
364
+ function checkError(err, ErrorClass, msgIncludes, msg) {
365
+ if (!ErrorClass)
366
+ return;
367
+ if (!(err instanceof ErrorClass)) {
368
+ throw new AssertionError(`Expected error to be instance of "${ErrorClass.name}", but was "${err instanceof Error ? err.constructor.name : typeof err}"${suffix(msg)}`);
369
+ }
370
+ if (msgIncludes !== undefined && !String(err.message).includes(msgIncludes)) {
371
+ throw new AssertionError(`Expected error message to include ${format(msgIncludes)}, but got ${format(err.message)}${suffix(msg)}`);
372
+ }
373
+ }
374
+ export function assertThrows(fn, classOrMsg, msgIncludes, msg) {
375
+ const ErrorClass = typeof classOrMsg === "function" ? classOrMsg : undefined;
376
+ if (typeof classOrMsg === "string")
377
+ msg = classOrMsg;
378
+ let thrown = false;
379
+ let error;
380
+ try {
381
+ fn();
382
+ }
383
+ catch (err) {
384
+ thrown = true;
385
+ error = err;
386
+ }
387
+ if (!thrown)
388
+ throw new AssertionError(`Expected function to throw${suffix(msg)}`);
389
+ checkError(error, ErrorClass, msgIncludes, msg);
390
+ return error;
391
+ }
392
+ export async function assertRejects(fn, classOrMsg, msgIncludes, msg) {
393
+ const ErrorClass = typeof classOrMsg === "function" ? classOrMsg : undefined;
394
+ if (typeof classOrMsg === "string")
395
+ msg = classOrMsg;
396
+ let p;
397
+ try {
398
+ p = typeof fn === "function" ? fn() : fn;
399
+ }
400
+ catch {
401
+ // A synchronous throw is not a rejection (an async function never does this).
402
+ throw new AssertionError(`Function throws when expected to reject${suffix(msg)}`);
403
+ }
404
+ if (!p || typeof p.then !== "function") {
405
+ throw new AssertionError(`Function returns a non-promise when expected to reject${suffix(msg)}`);
406
+ }
407
+ let rejected = false;
408
+ let error;
409
+ try {
410
+ await p;
411
+ }
412
+ catch (err) {
413
+ rejected = true;
414
+ error = err;
415
+ }
416
+ if (!rejected)
417
+ throw new AssertionError(`Expected function to reject${suffix(msg)}`);
418
+ checkError(error, ErrorClass, msgIncludes, msg);
419
+ return error;
420
+ }
421
+ /** Unconditionally fail. */
422
+ export function fail(msg) {
423
+ throw new AssertionError(`Failed assertion${suffix(msg)}`);
424
+ }
425
+ export function unreachable(msg) {
426
+ throw new AssertionError(msg ?? "Unreachable");
427
+ }
428
+ export function unimplemented(msg) {
429
+ throw new AssertionError(msg ?? "Unimplemented");
430
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * `1tube/test` everywhere else (the default export condition): drives the
3
+ * standard `node:test` runner, which Node runs natively and other runtimes
4
+ * implement for compatibility.
5
+ */
6
+ import type { DescribeFunction, Hook, TestFunction, TestRuntime } from "./api.ts";
7
+ export * from "./assert.ts";
8
+ export type * from "./api.ts";
9
+ export declare const runtime: TestRuntime;
10
+ export declare const test: TestFunction;
11
+ export declare const describe: DescribeFunction;
12
+ export declare const beforeAll: Hook;
13
+ export declare const afterAll: Hook;
14
+ export declare const beforeEach: Hook;
15
+ export declare const afterEach: Hook;
@@ -0,0 +1,29 @@
1
+ /**
2
+ * `1tube/test` everywhere else (the default export condition): drives the
3
+ * standard `node:test` runner, which Node runs natively and other runtimes
4
+ * implement for compatibility.
5
+ */
6
+ import * as node from "node:test";
7
+ export * from "./assert.js";
8
+ export const runtime = "node";
9
+ // node:test passes a context (and a `done` callback to 2-arity functions);
10
+ // the 1tube contract has neither, so wrap to keep the arity at zero.
11
+ const register = (name, fn, options) => node.test(name, { timeout: options.timeout, skip: options.skip, only: options.only }, async () => {
12
+ await fn();
13
+ });
14
+ export const test = Object.assign((name, fn, options = {}) => void register(name, fn, options), {
15
+ skip: (name, fn, options = {}) => void register(name, fn, { ...options, skip: true }),
16
+ only: (name, fn, options = {}) => void register(name, fn, { ...options, only: true }),
17
+ todo: (name) => void node.test(name, { todo: true }),
18
+ });
19
+ export const describe = Object.assign((name, fn) => void node.describe(name, fn), {
20
+ skip: (name, fn) => void node.describe(name, { skip: true }, fn),
21
+ only: (name, fn) => void node.describe(name, { only: true }, fn),
22
+ });
23
+ const hook = (register) => (fn) => register(async () => {
24
+ await fn();
25
+ });
26
+ export const beforeAll = hook((fn) => node.before(fn));
27
+ export const afterAll = hook((fn) => node.after(fn));
28
+ export const beforeEach = hook((fn) => node.beforeEach(fn));
29
+ export const afterEach = hook((fn) => node.afterEach(fn));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "1tube",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "private": false,
5
5
  "description": "Self-hosted Supabase Edge Functions gateway on Bun, plus an ESLint plugin for consumers",
6
6
  "license": "MIT",
@@ -29,6 +29,12 @@
29
29
  "import": "./dist/warmup-client.js",
30
30
  "default": "./dist/warmup-client.js"
31
31
  },
32
+ "./test": {
33
+ "types": "./dist/test/node.d.ts",
34
+ "bun": "./src/test/bun.ts",
35
+ "import": "./dist/test/node.js",
36
+ "default": "./dist/test/node.js"
37
+ },
32
38
  "./eslint-plugin": "./eslint-plugin.js",
33
39
  "./package.json": "./package.json"
34
40
  },
@@ -56,7 +62,7 @@
56
62
  "play:workerd": "bun src/cli.ts --backend workerd --dev --hmr --functions ./playground --port 3199",
57
63
  "build": "bun src/cli.ts build",
58
64
  "test": "bun test --timeout 60000",
59
- "build:edge": "bunx tsc --ignoreConfig --target ES2022 --module ESNext --moduleResolution Bundler --declaration --outDir dist src/edge.ts src/edge-caller.ts src/warmup-client.ts",
65
+ "build:edge": "bunx tsc --ignoreConfig --target ES2022 --module ESNext --moduleResolution Bundler --declaration --rewriteRelativeImportExtensions --types node --outDir dist src/edge.ts src/edge-caller.ts src/warmup-client.ts src/test/node.ts",
60
66
  "prepack": "bun run build:edge",
61
67
  "typecheck": "bun check",
62
68
  "smoke:bin": "node ./scripts/npm-cli.js --version",
@@ -892,7 +892,7 @@ export function createWorkerdBackend(
892
892
  : resolvePath(cwd, opts.configPath);
893
893
  const configPath = existsSync(requestedConfigPath)
894
894
  ? requestedConfigPath
895
- : process.env["1TUBE_DENO_CONFIG"] ??
895
+ : process.env["1TUBE_IMPORT_MAP"] ?? process.env["1TUBE_DENO_CONFIG"] ??
896
896
  findDenoConfig(cwd, functionsDir) ?? requestedConfigPath;
897
897
  const workerdBin = opts.workerdBin ?? process.env["1TUBE_WORKERD_BIN"] ??
898
898
  "workerd";
package/src/cli/build.ts CHANGED
@@ -563,8 +563,8 @@ export async function runBuild(args: string[]): Promise<number> {
563
563
  // Resolve the deno.json import map. Three modes, in priority order:
564
564
  // 1. --config <path> → explicit, must exist (we hard-fail if not)
565
565
  // 2. --no-config → explicit "build with no import map"
566
- // 3. <auto-detect> → cwd/deno.json[c], then
567
- // <functions-dir>/deno.json[c] (Supabase
566
+ // 3. <auto-detect> → cwd/{import_map,deno}.json[c], then
567
+ // <functions-dir>/{import_map,deno}.json[c] (Supabase
568
568
  // layout), otherwise omit.
569
569
  // Auto-detect is the important case: hosts like sciobot-next are Vite/Bun
570
570
  // projects with no root deno.json, and the bundler tolerates a missing
@@ -748,8 +748,8 @@ Options:
748
748
  --workerd-env A,B,C Env vars baked into the manifest's allowlist
749
749
  --workerd-shared path Shared module path (repeatable). Defaults include
750
750
  _shared/profile-cache.ts when present.
751
- --config <path> Explicit deno.json path for the import map.
752
- Default: auto-detect cwd/deno.json[c] then
753
- <functions-dir>/deno.json[c] (Supabase layout).
751
+ --config <path> Import map path (import_map.json, or deno.json[c]).
752
+ Default: auto-detect cwd/{import_map,deno}.json[c] then
753
+ <functions-dir>/{import_map,deno}.json[c] (Supabase layout).
754
754
  --no-config Skip the import map entirely.
755
755
  -h, --help Show this help`;
@@ -8,8 +8,8 @@
8
8
  *
9
9
  * 1. explicit `--config <path>` → must exist (hard error otherwise)
10
10
  * 2. explicit `--no-config` → skip the import map entirely
11
- * 3. auto-detect → cwd/deno.json[c] then
12
- * <functions-dir>/deno.json[c]
11
+ * 3. auto-detect → cwd/{import_map,deno}.json[c] then
12
+ * <functions-dir>/{import_map,deno}.json[c]
13
13
  *
14
14
  * The functions-dir fallback is what makes this work for hosts like
15
15
  * sciobot-next: a Vite/Bun project whose only `deno.json` lives next to the
@@ -63,11 +63,11 @@ export async function resolveDenoConfigPath(
63
63
  : resolvePath(cwd, functionsDir);
64
64
  const found = findDenoConfig(cwd, fnDirAbs);
65
65
  if (found) {
66
- onMessage?.(`using deno config: ${found}`);
66
+ onMessage?.(`using import map: ${found}`);
67
67
  return { configPath: found };
68
68
  }
69
69
  onMessage?.(
70
- `no deno.json / deno.jsonc at ${cwd} or ${fnDirAbs} — bundling ` +
70
+ `no import_map.json / deno.json[c] at ${cwd} or ${fnDirAbs} — bundling ` +
71
71
  `without an import map. Pass --config <path> if you want one.`,
72
72
  );
73
73
  return {};