bundler.ts103 lines · main
1import { readdir, readFile, stat } from 'node:fs/promises';
2import { pathToFileURL } from 'node:url';
3import { join, resolve } from 'node:path';
4
5import type { SchemaDef } from '@briven/schema';
6
7/**
8 * Load the user's `briven/schema.ts` from disk and return the resolved
9 * `SchemaDef` value. Uses tsx's programmatic ESM API so we don't need the
10 * user's project to be pre-compiled.
11 *
12 * Returns `null` if the file is missing; throws on a malformed default export.
13 */
14export async function loadProjectSchema(cwd: string): Promise<SchemaDef | null> {
15 const path = resolve(cwd, 'briven', 'schema.ts');
16 try {
17 await stat(path);
18 } catch {
19 return null;
20 }
21
22 const tsx = await import('tsx/esm/api');
23 const mod = (await tsx.tsImport(pathToFileURL(path).href, import.meta.url)) as {
24 default?: unknown;
25 };
26 if (!mod.default || typeof mod.default !== 'object') {
27 throw new Error('briven/schema.ts must have a default export produced by `schema(...)`');
28 }
29
30 // CJS/ESM interop unwrap: when the user's project is CommonJS, tsx loads
31 // `export default schema({...})` through the interop layer and the value
32 // gets double-wrapped as `mod.default.default` (the inner `.default` is the
33 // real schema, the outer one is the synthetic ESM namespace default). Accept
34 // both the plain `mod.default` shape and the double-wrapped one.
35 let candidate = mod.default as Partial<SchemaDef> & { default?: Partial<SchemaDef> };
36 if (
37 candidate.version !== 1 &&
38 candidate.default &&
39 typeof candidate.default === 'object' &&
40 candidate.default.version === 1
41 ) {
42 candidate = candidate.default;
43 }
44
45 if (candidate.version !== 1 || typeof candidate.tables !== 'object') {
46 throw new Error('default export is not a valid briven schema');
47 }
48 return candidate as SchemaDef;
49}
50
51export interface FunctionInfo {
52 readonly names: readonly string[];
53 readonly count: number;
54 /** Map of relative path → file source. Empty if no functions found. */
55 readonly bundle: Readonly<Record<string, string>>;
56}
57
58/**
59 * List the user's function files. A function file is any `.ts` file under
60 * `briven/functions/` — we key deployment identity on the list of file
61 * basenames, which maps 1:1 to endpoint names in the runtime.
62 */
63export async function discoverFunctions(cwd: string): Promise<FunctionInfo> {
64 const dir = resolve(cwd, 'briven', 'functions');
65 const names: string[] = [];
66 try {
67 await walk(dir, dir, names);
68 } catch (err) {
69 if (isNotFound(err)) return { names: [], count: 0, bundle: {} };
70 throw err;
71 }
72 names.sort();
73
74 const bundle: Record<string, string> = {};
75 for (const rel of names) {
76 bundle[rel] = await readFile(resolve(dir, rel), 'utf8');
77 }
78 return { names, count: names.length, bundle };
79}
80
81async function walk(dir: string, root: string, out: string[]): Promise<void> {
82 const entries = await readdir(dir, { withFileTypes: true });
83 for (const entry of entries) {
84 const full = join(dir, entry.name);
85 if (entry.isDirectory()) {
86 await walk(full, root, out);
87 continue;
88 }
89 if (!entry.isFile()) continue;
90 if (!entry.name.endsWith('.ts')) continue;
91 if (entry.name.endsWith('.test.ts') || entry.name.endsWith('.d.ts')) continue;
92 out.push(full.slice(root.length + 1));
93 }
94}
95
96function isNotFound(err: unknown): boolean {
97 return (
98 typeof err === 'object' &&
99 err !== null &&
100 'code' in err &&
101 (err as { code: string }).code === 'ENOENT'
102 );
103}