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.
@@ -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> Explicit deno.json path for the build import map
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
 
@@ -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
+ }
@@ -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> explicit deno.json 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> Explicit deno.json path for the import map.
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
- * 1TUBE_DENO_CONFIG explicit deno.json[c] for the import map
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["1TUBE_DENO_CONFIG"] ??
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({
@@ -1,8 +1,9 @@
1
1
  /**
2
- * Deno import-map support (`imports` + `scopes` from deno.json[c]).
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/deno.json`
5
- * (or a repo-root deno.json). Bun has no notion of that file, so the compat
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 deno.json locations for a functions project, most specific
63
- * last-checked: cwd first (repo-root config), then the functions dir
64
- * (Supabase's `supabase/functions/deno.json`).
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 = [join(cwd, "deno.json"), join(cwd, "deno.jsonc")];
74
+ const candidates = IMPORT_MAP_FILES.map((f) => join(cwd, f));
71
75
  if (functionsDir) {
72
- candidates.push(join(functionsDir, "deno.json"));
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> Host deno.json[c] whose import map functions use
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["1TUBE_DENO_CONFIG"] = configAbs;
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] : ["deno.json", "deno.jsonc"].map(resolveToAbs)),
269
+ ...(configAbs ? [configAbs] : IMPORT_MAP_FILES.map(resolveToAbs)),
263
270
  ...PROJECT_DEP_FILES.map(resolveToAbs),
264
271
  ];
265
272
 
@@ -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";