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.
- package/dist/test/api.d.ts +34 -0
- package/dist/test/api.js +10 -0
- package/dist/test/assert.d.ts +61 -0
- package/dist/test/assert.js +430 -0
- package/dist/test/node.d.ts +15 -0
- package/dist/test/node.js +29 -0
- package/package.json +8 -2
- package/src/backends/workerd/backend.ts +1 -1
- package/src/cli/build.ts +5 -5
- package/src/cli/deno-config.ts +4 -4
- package/src/cli/package.ts +1 -1
- package/src/cli/test.ts +145 -0
- package/src/cli/vercel-build.ts +4 -4
- package/src/compat/environment.ts +3 -2
- package/src/compat/import-map.ts +12 -9
- package/src/launch.ts +10 -3
- package/src/test/api.ts +40 -0
- package/src/test/assert.ts +464 -0
- package/src/test/bun.ts +38 -0
- package/src/test/node.ts +47 -0
- package/src/test/preload.ts +23 -0
package/src/cli/package.ts
CHANGED
|
@@ -588,7 +588,7 @@ Options:
|
|
|
588
588
|
--compat-flag FLAG Add a workerd compatibility flag (repeatable)
|
|
589
589
|
--workerd-env A,B,C Env vars baked into the manifest's allowlist
|
|
590
590
|
--workerd-shared path Shared module path (repeatable)
|
|
591
|
-
--config <path>
|
|
591
|
+
--config <path> Import map path for the build (import_map.json, or deno.json[c])
|
|
592
592
|
--no-config Skip the build import map entirely
|
|
593
593
|
-h, --help Show this help`;
|
|
594
594
|
|
package/src/cli/test.ts
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `1tube test` — run edge function tests with gateway-identical resolution.
|
|
3
|
+
*
|
|
4
|
+
* 1tube test [--functions <dir>] [--config <import map>] [--env-file <f>]…
|
|
5
|
+
* [bun test args / paths…]
|
|
6
|
+
*
|
|
7
|
+
* Test files import from `1tube/test`; this command picks the runner for
|
|
8
|
+
* the current runtime (Bun: `bun test`) and preloads the compat layer so
|
|
9
|
+
* the import map, `npm:` / `jsr:` specifiers and the managed dependency
|
|
10
|
+
* store apply to function code under test. Without explicit paths it runs
|
|
11
|
+
* every test under the functions dir.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { existsSync } from "node:fs";
|
|
15
|
+
import { isAbsolute, relative, resolve } from "node:path";
|
|
16
|
+
import { fileURLToPath } from "node:url";
|
|
17
|
+
|
|
18
|
+
/** `bun test` flags that consume the following token as their value. */
|
|
19
|
+
const BUN_TEST_VALUE_FLAGS = new Set([
|
|
20
|
+
"--timeout",
|
|
21
|
+
"--rerun-each",
|
|
22
|
+
"--retry",
|
|
23
|
+
"-t",
|
|
24
|
+
"--test-name-pattern",
|
|
25
|
+
"--preload",
|
|
26
|
+
"--coverage-reporter",
|
|
27
|
+
"--coverage-dir",
|
|
28
|
+
"--reporter",
|
|
29
|
+
"--reporter-outfile",
|
|
30
|
+
"--seed",
|
|
31
|
+
"--max-concurrency",
|
|
32
|
+
"--path-ignore-patterns",
|
|
33
|
+
]);
|
|
34
|
+
|
|
35
|
+
export interface TestArgs {
|
|
36
|
+
functionsDir: string;
|
|
37
|
+
config?: string;
|
|
38
|
+
depsDir?: string;
|
|
39
|
+
envFiles: string[];
|
|
40
|
+
forwarded: string[];
|
|
41
|
+
hasPaths: boolean;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function parseTestArgs(argv: readonly string[]): TestArgs {
|
|
45
|
+
const out: TestArgs = { functionsDir: "", envFiles: [], forwarded: [], hasPaths: false };
|
|
46
|
+
let functionsDir: string | undefined;
|
|
47
|
+
for (let i = 0; i < argv.length; i++) {
|
|
48
|
+
const tok = argv[i];
|
|
49
|
+
if (tok === "--") {
|
|
50
|
+
for (const rest of argv.slice(i + 1)) {
|
|
51
|
+
out.forwarded.push(rest);
|
|
52
|
+
if (!rest.startsWith("-")) out.hasPaths = true;
|
|
53
|
+
}
|
|
54
|
+
break;
|
|
55
|
+
}
|
|
56
|
+
const eq = tok.startsWith("--") ? tok.indexOf("=") : -1;
|
|
57
|
+
const flag = eq === -1 ? tok : tok.slice(0, eq);
|
|
58
|
+
const value = () => (eq !== -1 ? tok.slice(eq + 1) : argv[++i]);
|
|
59
|
+
switch (flag) {
|
|
60
|
+
case "--functions":
|
|
61
|
+
case "-f":
|
|
62
|
+
functionsDir = value();
|
|
63
|
+
continue;
|
|
64
|
+
case "--config":
|
|
65
|
+
case "--import-map":
|
|
66
|
+
out.config = value();
|
|
67
|
+
continue;
|
|
68
|
+
case "--deps-dir":
|
|
69
|
+
out.depsDir = value();
|
|
70
|
+
continue;
|
|
71
|
+
case "--env-file":
|
|
72
|
+
out.envFiles.push(value());
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
out.forwarded.push(tok);
|
|
76
|
+
if (BUN_TEST_VALUE_FLAGS.has(flag) && eq === -1 && i + 1 < argv.length) {
|
|
77
|
+
out.forwarded.push(argv[++i]);
|
|
78
|
+
} else if (!tok.startsWith("-")) {
|
|
79
|
+
out.hasPaths = true;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
out.functionsDir = functionsDir ??
|
|
83
|
+
(existsSync("supabase/functions") ? "supabase/functions" : "functions");
|
|
84
|
+
return out;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export async function runTest(argv: readonly string[]): Promise<number> {
|
|
88
|
+
if (argv.includes("--help") || argv.includes("-h")) {
|
|
89
|
+
console.log(`Usage: 1tube test [options] [bun test args / paths…]
|
|
90
|
+
|
|
91
|
+
Runs edge function tests (files importing \`1tube/test\`) with the gateway's
|
|
92
|
+
module resolution. Without paths, runs every test under the functions dir.
|
|
93
|
+
|
|
94
|
+
Options:
|
|
95
|
+
--functions, -f <dir> Functions dir (default: supabase/functions, else functions)
|
|
96
|
+
--config <path> Import map (import_map.json, or a deno.json[c] with "imports")
|
|
97
|
+
--deps-dir <path> Managed npm:/jsr: store (default <cwd>/.1tube/deps)
|
|
98
|
+
--env-file <path> Env file loaded into the test process (repeatable)
|
|
99
|
+
-- Pass everything after it to the runner verbatim
|
|
100
|
+
`);
|
|
101
|
+
return 0;
|
|
102
|
+
}
|
|
103
|
+
const args = parseTestArgs(argv);
|
|
104
|
+
const cwd = process.cwd();
|
|
105
|
+
const functionsAbs = resolve(cwd, args.functionsDir);
|
|
106
|
+
if (!existsSync(functionsAbs)) {
|
|
107
|
+
console.error(`[1tube] test: functions dir not found: ${functionsAbs}`);
|
|
108
|
+
return 1;
|
|
109
|
+
}
|
|
110
|
+
const env: Record<string, string | undefined> = {
|
|
111
|
+
...process.env,
|
|
112
|
+
"1TUBE_FUNCTIONS_DIR": functionsAbs,
|
|
113
|
+
};
|
|
114
|
+
if (args.config) {
|
|
115
|
+
const abs = isAbsolute(args.config) ? args.config : resolve(cwd, args.config);
|
|
116
|
+
if (!existsSync(abs)) {
|
|
117
|
+
console.error(`[1tube] test: --config not found: ${abs}`);
|
|
118
|
+
return 1;
|
|
119
|
+
}
|
|
120
|
+
env["1TUBE_IMPORT_MAP"] = abs;
|
|
121
|
+
}
|
|
122
|
+
if (args.depsDir) env["1TUBE_DEPS_DIR"] = resolve(cwd, args.depsDir);
|
|
123
|
+
|
|
124
|
+
const preload = fileURLToPath(new URL("../test/preload.ts", import.meta.url));
|
|
125
|
+
// A path filter starting with "./" is matched as a path, not a substring.
|
|
126
|
+
const rel = relative(cwd, functionsAbs).replace(/\\/g, "/");
|
|
127
|
+
const defaultPath = rel.startsWith("..") || isAbsolute(rel) ? functionsAbs : `./${rel || "."}`;
|
|
128
|
+
const cmd = [
|
|
129
|
+
process.execPath,
|
|
130
|
+
// Like the gateway: only the env files the caller named.
|
|
131
|
+
...(args.envFiles.length > 0
|
|
132
|
+
? args.envFiles.map((f) => `--env-file=${resolve(cwd, f)}`)
|
|
133
|
+
: ["--no-env-file"]),
|
|
134
|
+
"test",
|
|
135
|
+
"--preload",
|
|
136
|
+
preload,
|
|
137
|
+
...args.forwarded,
|
|
138
|
+
...(args.hasPaths ? [] : [defaultPath]),
|
|
139
|
+
];
|
|
140
|
+
const child = Bun.spawn(cmd, { stdin: "inherit", stdout: "inherit", stderr: "inherit", env });
|
|
141
|
+
for (const sig of ["SIGINT", "SIGTERM"] as const) {
|
|
142
|
+
process.on(sig, () => child.kill(sig));
|
|
143
|
+
}
|
|
144
|
+
return await child.exited;
|
|
145
|
+
}
|
package/src/cli/vercel-build.ts
CHANGED
|
@@ -581,7 +581,7 @@ export async function buildVercel(
|
|
|
581
581
|
* --max-duration-cap <s> hard cap for maxDuration (default 800)
|
|
582
582
|
* --deployment-id <id> custom Skew Protection deployment id written to
|
|
583
583
|
* config.json (env: 1TUBE_VERCEL_DEPLOYMENT_ID)
|
|
584
|
-
* --config <path>
|
|
584
|
+
* --config <path> import map path
|
|
585
585
|
* --no-config skip the import map
|
|
586
586
|
*/
|
|
587
587
|
export async function runVercelBuild(args: string[]): Promise<number> {
|
|
@@ -801,8 +801,8 @@ Options:
|
|
|
801
801
|
Bake the same id into the frontend and send it as
|
|
802
802
|
x-deployment-id / ?dpl= on fetch() calls.
|
|
803
803
|
Env fallback: 1TUBE_VERCEL_DEPLOYMENT_ID
|
|
804
|
-
--config <path>
|
|
805
|
-
Default: auto-detect cwd/deno.json[c] then
|
|
806
|
-
<functions-dir>/deno.json[c] (Supabase layout).
|
|
804
|
+
--config <path> Import map path (import_map.json, or deno.json[c]).
|
|
805
|
+
Default: auto-detect cwd/{import_map,deno}.json[c] then
|
|
806
|
+
<functions-dir>/{import_map,deno}.json[c] (Supabase layout).
|
|
807
807
|
--no-config Skip the import map entirely.
|
|
808
808
|
-h, --help Show this help`;
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* whether a function runs locally or ships in a bundle.
|
|
7
7
|
*
|
|
8
8
|
* 1TUBE_DEPS_DIR managed store (default <cwd>/.1tube/deps)
|
|
9
|
-
*
|
|
9
|
+
* 1TUBE_IMPORT_MAP explicit import map (legacy name: 1TUBE_DENO_CONFIG)
|
|
10
10
|
* 1TUBE_DEPS_ISOLATED=1 ignore host node_modules
|
|
11
11
|
* 1TUBE_MINIMUM_DEPENDENCY_AGE min release age for managed installs
|
|
12
12
|
* 1TUBE_RELOAD_REMOTE=1 re-download cached https: modules
|
|
@@ -45,7 +45,8 @@ export function createCompatEnvironment(
|
|
|
45
45
|
cwd,
|
|
46
46
|
opts.depsDir ?? process.env["1TUBE_DEPS_DIR"] ?? join(".1tube", "deps"),
|
|
47
47
|
);
|
|
48
|
-
const configPath = opts.configPath ?? process.env["
|
|
48
|
+
const configPath = opts.configPath ?? process.env["1TUBE_IMPORT_MAP"] ??
|
|
49
|
+
process.env["1TUBE_DENO_CONFIG"] ??
|
|
49
50
|
findDenoConfig(cwd, opts.functionsDir);
|
|
50
51
|
return {
|
|
51
52
|
deps: createDependencyManager({
|
package/src/compat/import-map.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Import-map support (`imports` + `scopes` from an import_map.json, or a
|
|
3
|
+
* deno.json[c] carrying them).
|
|
3
4
|
*
|
|
4
|
-
* Supabase projects keep their aliases in `supabase/functions/
|
|
5
|
-
* (or
|
|
5
|
+
* Supabase projects keep their aliases in `supabase/functions/import_map.json`
|
|
6
|
+
* or `supabase/functions/deno.json` (or at the repo root). Bun has no notion of that file, so the compat
|
|
6
7
|
* plugin and the HMR dep-graph both resolve aliases through this module —
|
|
7
8
|
* the Deno CLI's algorithm reduced to its essentials: exact match first,
|
|
8
9
|
* otherwise the longest trailing-slash prefix, with the most specific
|
|
@@ -58,19 +59,21 @@ export function stripJsonc(text: string): string {
|
|
|
58
59
|
return out.replace(/,\s*([}\]])/g, "$1");
|
|
59
60
|
}
|
|
60
61
|
|
|
62
|
+
/** File names searched for an import map, in priority order. */
|
|
63
|
+
export const IMPORT_MAP_FILES = ["import_map.json", "deno.json", "deno.jsonc"] as const;
|
|
64
|
+
|
|
61
65
|
/**
|
|
62
|
-
* Conventional
|
|
63
|
-
*
|
|
64
|
-
*
|
|
66
|
+
* Conventional import-map locations for a functions project: cwd first
|
|
67
|
+
* (repo-root config), then the functions dir (Supabase's
|
|
68
|
+
* `supabase/functions/import_map.json` or `deno.json`).
|
|
65
69
|
*/
|
|
66
70
|
export function findDenoConfig(
|
|
67
71
|
cwd: string,
|
|
68
72
|
functionsDir?: string,
|
|
69
73
|
): string | undefined {
|
|
70
|
-
const candidates =
|
|
74
|
+
const candidates = IMPORT_MAP_FILES.map((f) => join(cwd, f));
|
|
71
75
|
if (functionsDir) {
|
|
72
|
-
candidates.push(join(functionsDir,
|
|
73
|
-
candidates.push(join(functionsDir, "deno.jsonc"));
|
|
76
|
+
candidates.push(...IMPORT_MAP_FILES.map((f) => join(functionsDir, f)));
|
|
74
77
|
}
|
|
75
78
|
for (const c of candidates) {
|
|
76
79
|
try {
|
package/src/launch.ts
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
* ========
|
|
11
11
|
* build → cli/build.ts (bundle for workerd/vercel)
|
|
12
12
|
* package → cli/package.ts (sign a firmware payload)
|
|
13
|
+
* test → cli/test.ts (run function tests via `1tube/test`)
|
|
13
14
|
* --version → print version
|
|
14
15
|
* --help → usage
|
|
15
16
|
* <else> → serve (the gateway) ← default, may be spelled `serve`
|
|
@@ -35,6 +36,7 @@ import { existsSync, readFileSync, rmSync, statSync } from "node:fs";
|
|
|
35
36
|
import { basename, dirname, isAbsolute, join, resolve } from "node:path";
|
|
36
37
|
import { fileURLToPath } from "node:url";
|
|
37
38
|
import { watchFs } from "./fs-watch.ts";
|
|
39
|
+
import { IMPORT_MAP_FILES } from "./compat/import-map.ts";
|
|
38
40
|
|
|
39
41
|
// ---------------------------------------------------------------------------
|
|
40
42
|
// Top-level dispatch
|
|
@@ -46,6 +48,10 @@ export async function run(argv: readonly string[]): Promise<void> {
|
|
|
46
48
|
const { runBuild } = await import("./cli/build.ts");
|
|
47
49
|
process.exit(await runBuild(argv.slice(1)));
|
|
48
50
|
}
|
|
51
|
+
if (arg0 === "test") {
|
|
52
|
+
const { runTest } = await import("./cli/test.ts");
|
|
53
|
+
process.exit(await runTest(argv.slice(1)));
|
|
54
|
+
}
|
|
49
55
|
if (arg0 === "package") {
|
|
50
56
|
const { runPackage } = await import("./cli/package.ts");
|
|
51
57
|
process.exit(await runPackage(argv.slice(1)));
|
|
@@ -71,13 +77,14 @@ Commands:
|
|
|
71
77
|
serve Run the gateway (default when no command given)
|
|
72
78
|
build Bundle functions for a target (--target workerd|vercel)
|
|
73
79
|
package Build and/or wrap dist/ into a signed .1tube firmware payload
|
|
80
|
+
test Run function tests (files importing 1tube/test) with gateway resolution
|
|
74
81
|
--version Print version and exit
|
|
75
82
|
--help Print this help
|
|
76
83
|
|
|
77
84
|
Serve options (everything else is forwarded to the gateway):
|
|
78
85
|
--deps-dir <path> Managed npm:/jsr: store (default <cwd>/.1tube/deps)
|
|
79
86
|
--refresh-deps Drop the managed store's lockfile for a clean re-resolve
|
|
80
|
-
--config <path>
|
|
87
|
+
--config <path> Import map functions use (import_map.json, or deno.json[c])
|
|
81
88
|
--env-file[=<path>] Env file loaded into the gateway (repeatable)
|
|
82
89
|
--minimum-dependency-age <n> Minimum release age for managed installs (s, or 30m/24h/7d)
|
|
83
90
|
--no-pin Ignore host node_modules; resolve npm: only via the managed store
|
|
@@ -236,7 +243,7 @@ async function runServe(args: ServeArgs): Promise<never> {
|
|
|
236
243
|
...process.env,
|
|
237
244
|
"1TUBE_DEPS_DIR": depsDir,
|
|
238
245
|
};
|
|
239
|
-
if (configAbs) childEnv["
|
|
246
|
+
if (configAbs) childEnv["1TUBE_IMPORT_MAP"] = configAbs;
|
|
240
247
|
if (!args.pin) childEnv["1TUBE_DEPS_ISOLATED"] = "1";
|
|
241
248
|
if (args.minimumDependencyAge !== undefined) {
|
|
242
249
|
childEnv["1TUBE_MINIMUM_DEPENDENCY_AGE"] = args.minimumDependencyAge;
|
|
@@ -259,7 +266,7 @@ async function runServe(args: ServeArgs): Promise<never> {
|
|
|
259
266
|
];
|
|
260
267
|
|
|
261
268
|
const watchPaths = [
|
|
262
|
-
...(configAbs ? [configAbs] :
|
|
269
|
+
...(configAbs ? [configAbs] : IMPORT_MAP_FILES.map(resolveToAbs)),
|
|
263
270
|
...PROJECT_DEP_FILES.map(resolveToAbs),
|
|
264
271
|
];
|
|
265
272
|
|
package/src/test/api.ts
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
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
|
+
|
|
11
|
+
export type TestFn = () => void | Promise<unknown>;
|
|
12
|
+
export type HookFn = () => void | Promise<unknown>;
|
|
13
|
+
|
|
14
|
+
export interface TestOptions {
|
|
15
|
+
/** Per-test timeout in milliseconds (runner default when omitted). */
|
|
16
|
+
timeout?: number;
|
|
17
|
+
/** Register but do not run. */
|
|
18
|
+
skip?: boolean;
|
|
19
|
+
/** Run only tests marked `only` (where the runner supports focusing). */
|
|
20
|
+
only?: boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface TestFunction {
|
|
24
|
+
(name: string, fn: TestFn, options?: TestOptions): void;
|
|
25
|
+
skip(name: string, fn: TestFn, options?: TestOptions): void;
|
|
26
|
+
only(name: string, fn: TestFn, options?: TestOptions): void;
|
|
27
|
+
/** A placeholder for a test that is not written yet. */
|
|
28
|
+
todo(name: string): void;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface DescribeFunction {
|
|
32
|
+
(name: string, fn: () => void): void;
|
|
33
|
+
skip(name: string, fn: () => void): void;
|
|
34
|
+
only(name: string, fn: () => void): void;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export type Hook = (fn: HookFn) => void;
|
|
38
|
+
|
|
39
|
+
/** Which runner the active binding drives. */
|
|
40
|
+
export type TestRuntime = "bun" | "node";
|