@burger-api/cli 0.7.0 → 0.9.1

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.
@@ -2,15 +2,15 @@
2
2
  * Build Commands
3
3
  *
4
4
  * Two commands for packaging your Burger API project:
5
- * 1. `burger-api build <file>` - Bundle to single JS file
6
- * 2. `burger-api build:executable <file>` - Compile to standalone executable
5
+ * 1. `burger-api build <file>` Bundle to .build/bundle/
6
+ * 2. `burger-api build:exec <file>` Compile to .build/executable/
7
7
  *
8
- * These are wrappers around Bun's build commands with sensible defaults.
8
+ * Both use build-time (AOT) route discovery no filesystem scanning at runtime.
9
9
  */
10
10
 
11
11
  import { Command } from 'commander';
12
- import { existsSync, readFileSync } from 'fs';
13
- import { join } from 'path';
12
+ import { existsSync } from 'fs';
13
+ import { dirname, resolve } from 'path';
14
14
  import {
15
15
  spinner,
16
16
  success,
@@ -20,9 +20,11 @@ import {
20
20
  formatSize,
21
21
  dim,
22
22
  } from '../utils/logger';
23
+ import { runVirtualEntryBuild } from '../utils/build/pipeline';
24
+ import { getProjectName } from '../utils/build/project';
23
25
 
24
26
  /**
25
- * Build command options
27
+ * Options for the `build` command.
26
28
  */
27
29
  interface BuildCommandOptions {
28
30
  outfile: string;
@@ -32,7 +34,7 @@ interface BuildCommandOptions {
32
34
  }
33
35
 
34
36
  /**
35
- * Build executable command options
37
+ * Options for the `build:exec` command.
36
38
  */
37
39
  interface BuildExecutableOptions {
38
40
  outfile?: string;
@@ -42,74 +44,81 @@ interface BuildExecutableOptions {
42
44
  }
43
45
 
44
46
  /**
45
- * Create the "build" command
46
- * Bundles your code into a single JavaScript file
47
+ * `burger-api build <file>`
48
+ *
49
+ * Bundles your project into .build/bundle/ using AOT route discovery.
50
+ *
51
+ * Output:
52
+ * .build/bundle/
53
+ * app.js — Bun server (run with: bun .build/bundle/app.js)
54
+ * index.html — HTML pages (flat, one per page route)
55
+ * style-[hash].css — CSS assets (flat)
56
+ * app-[hash].js — JS chunks (flat)
57
+ *
58
+ * API-only projects: app.js is a self-contained single file.
59
+ * Projects with HTML pages: deploy the entire .build/bundle/ directory.
47
60
  */
48
61
  export const buildCommand = new Command('build')
49
- .description('Bundle your project to a single JavaScript file')
50
- .argument('<file>', 'Entry file to build (e.g., index.ts)')
51
- .option('--outfile <path>', 'Output file path', '.build/bundle.js')
62
+ .description('Bundle your project into .build/bundle/')
63
+ .argument(
64
+ '<file>',
65
+ 'Entry file (used for compatibility; config from burger.config.ts or conventions)'
66
+ )
67
+ .option('--outfile <path>', 'Output bundle path', '.build/bundle/app.js')
52
68
  .option('--minify', 'Minify the output')
53
69
  .option(
54
70
  '--sourcemap <type>',
55
71
  'Generate sourcemaps (inline, linked, or none)'
56
72
  )
57
- .option('--target <target>', 'Target environment (e.g., bun, node)')
73
+ .option('--target <env>', 'Target environment (default: bun)')
58
74
  .action(async (file: string, options: BuildCommandOptions) => {
59
- // Check if the input file exists
60
- if (!existsSync(file)) {
61
- logError(`File not found: ${file}`);
75
+ const cwd = process.cwd();
76
+ const entryPath = resolve(cwd, file);
77
+ if (!existsSync(entryPath)) {
78
+ logError(`Entry file not found: ${file}`);
79
+ info('Make sure you are in the project directory.');
62
80
  process.exit(1);
63
81
  }
64
82
 
65
83
  const spin = spinner('Building project...');
66
84
 
67
85
  try {
68
- // Build the command arguments for Bun
69
- const args = ['build', file];
70
-
71
- // Add output file
72
- args.push('--outfile', options.outfile);
73
-
74
- // Add optional flags
75
- if (options.minify) {
76
- args.push('--minify');
77
- }
78
-
79
- if (options.sourcemap) {
80
- args.push('--sourcemap', options.sourcemap);
81
- }
82
-
83
- // Always target bun by default since BurgerAPI uses Bun builtins
84
- args.push('--target', options.target || 'bun');
85
-
86
- // Run the build using Bun.spawn
87
- const proc = Bun.spawn(['bun', ...args], {
88
- stdout: 'pipe',
89
- stderr: 'pipe',
86
+ info('Build-time route discovery enabled.');
87
+
88
+ const { success: ok, hasPages } = await runVirtualEntryBuild({
89
+ cwd,
90
+ entryFile: file,
91
+ outfile: options.outfile,
92
+ target: options.target || 'bun',
93
+ minify: options.minify,
94
+ sourcemap: options.sourcemap,
90
95
  });
91
96
 
92
- // Wait for it to complete
93
- const exitCode = await proc.exited;
94
-
95
- if (exitCode !== 0) {
96
- // Read error output
97
- const errorText = await new Response(proc.stderr).text();
97
+ if (!ok) {
98
98
  spin.stop('Build failed', true);
99
- logError(errorText || 'Build process failed');
99
+ logError(
100
+ 'Bun.build failed. Check that api/page directories and route files are valid.'
101
+ );
100
102
  process.exit(1);
101
103
  }
102
104
 
103
- // Get file size
104
- const outputFile = Bun.file(options.outfile);
105
- const size = outputFile.size;
105
+ const size = Bun.file(options.outfile).size;
106
+ const bundleDir = dirname(options.outfile);
106
107
 
107
108
  spin.stop('Build completed successfully!');
108
109
  newline();
109
- success(`Output: ${options.outfile}`);
110
- info(`Size: ${formatSize(size)}`);
110
+ success(`Bundle: ${options.outfile} (${formatSize(size)})`);
111
111
  newline();
112
- dim('Run your bundle with: bun ' + options.outfile);
112
+ if (hasPages) {
113
+ info(`Pages and assets are in: ${bundleDir}/`);
114
+ dim(
115
+ 'Deploy the entire directory — HTML pages depend on their chunks.'
116
+ );
117
+ dim(`Run: bun ${options.outfile}`);
118
+ } else {
119
+ info('API-only bundle — self-contained single file.');
120
+ dim(`Run anywhere: bun ${options.outfile}`);
121
+ }
113
122
  newline();
114
123
  } catch (err) {
115
124
  spin.stop('Build failed', true);
@@ -119,109 +128,92 @@ export const buildCommand = new Command('build')
119
128
  });
120
129
 
121
130
  /**
122
- * Create the "build:executable" command
123
- * Compiles your code to a standalone executable
131
+ * `burger-api build:exec <file>`
132
+ *
133
+ * Compiles your project into a standalone binary in .build/executable/.
134
+ * The binary is fully self-contained — no Bun installation required on the target.
135
+ * All routes, pages, and assets are embedded inside the binary.
124
136
  */
125
- export const buildExecutableCommand = new Command('build:executable')
126
- .description('Compile your project to a standalone executable')
127
- .argument('<file>', 'Entry file to compile (e.g., index.ts)')
128
- .option('--outfile <path>', 'Output file path')
137
+ export const buildExecutableCommand = new Command('build:exec')
138
+ .description(
139
+ 'Compile your project to a standalone executable in .build/executable/'
140
+ )
141
+ .argument(
142
+ '<file>',
143
+ 'Entry file (used for compatibility; config from burger.config.ts or conventions)'
144
+ )
145
+ .option('--outfile <path>', 'Output executable path')
129
146
  .option(
130
- '--target <target>',
147
+ '--target <platform>',
131
148
  'Target platform (bun-windows-x64, bun-linux-x64, bun-darwin-arm64)'
132
149
  )
133
150
  .option('--minify', 'Minify the output (enabled by default)', true)
134
151
  .option('--no-bytecode', 'Disable bytecode compilation')
135
152
  .action(async (file: string, options: BuildExecutableOptions) => {
136
- // Check if the input file exists
137
- if (!existsSync(file)) {
138
- logError(`File not found: ${file}`);
153
+ const cwd = process.cwd();
154
+ const entryPath = resolve(cwd, file);
155
+ if (!existsSync(entryPath)) {
156
+ logError(`Entry file not found: ${file}`);
157
+ info('Make sure you are in the project directory.');
139
158
  process.exit(1);
140
159
  }
141
160
 
142
- // Determine output filename
143
161
  let outfile = options.outfile;
144
162
  if (!outfile) {
145
- // Get project name from package.json or use basename
146
- const projectName = getProjectName();
147
-
148
- // Add platform-specific extension
149
- // Check if targeting Windows or if we're on Windows without a specific target
163
+ const projectName = getProjectName(cwd);
150
164
  const isWindows =
151
165
  options.target?.includes('windows') ||
152
166
  (!options.target && process.platform === 'win32');
153
-
154
- if (isWindows) {
155
- outfile = `.build/${projectName}.exe`;
156
- } else {
157
- outfile = `.build/${projectName}`;
158
- }
167
+ outfile = isWindows
168
+ ? `.build/executable/${projectName}.exe`
169
+ : `.build/executable/${projectName}`;
159
170
  }
160
171
 
161
172
  const spin = spinner('Compiling to executable...');
162
173
 
163
174
  try {
164
- // Build the command arguments for Bun
165
- const args = ['build', file, '--compile'];
166
-
167
- // Add output file
168
- args.push('--outfile', outfile);
169
-
170
- // Add target platform
171
- if (options.target) {
172
- args.push('--target', options.target);
173
- }
174
-
175
- // Add minify (on by default)
176
- if (options.minify) {
177
- args.push('--minify');
178
- }
179
-
180
- // Add bytecode (on by default, unless --no-bytecode is passed)
181
- if (options.bytecode !== false) {
182
- args.push('--bytecode');
183
- }
184
-
175
+ info('Build-time route discovery enabled.');
185
176
  spin.update('Compiling... (this may take a minute)');
186
177
 
187
- // Run the build using Bun.spawn
188
- const proc = Bun.spawn(['bun', ...args], {
189
- stdout: 'pipe',
190
- stderr: 'pipe',
178
+ const { success: ok, outputs } = await runVirtualEntryBuild({
179
+ cwd,
180
+ entryFile: file,
181
+ outfile,
182
+ target: options.target,
183
+ minify: options.minify,
184
+ bytecode: options.bytecode !== false,
185
+ compile: true,
191
186
  });
192
187
 
193
- // Wait for it to complete
194
- const exitCode = await proc.exited;
195
-
196
- if (exitCode !== 0) {
197
- // Read error output
198
- const errorText = await new Response(proc.stderr).text();
188
+ if (!ok) {
199
189
  spin.stop('Compilation failed', true);
200
- logError(errorText || 'Compilation process failed');
190
+ logError(
191
+ 'Bun.build failed. Check that api/page directories and route files are valid.'
192
+ );
201
193
  process.exit(1);
202
194
  }
203
195
 
204
- // Get file size - check if file exists first
205
- let size = 0;
206
- if (existsSync(outfile)) {
207
- const executableFile = Bun.file(outfile);
208
- size = executableFile.size;
209
- }
196
+ const size = existsSync(outfile)
197
+ ? Bun.file(outfile).size
198
+ : (outputs[0]?.size ?? 0);
210
199
 
211
200
  spin.stop('Compilation completed successfully!');
212
201
  newline();
213
202
  success(`Executable: ${outfile}`);
214
- if (size > 0) {
215
- info(`Size: ${formatSize(size)}`);
216
- }
203
+ if (size > 0) info(`Size: ${formatSize(size)}`);
204
+ newline();
205
+ info(
206
+ 'Standalone binary — copy it anywhere, no Bun required on the target.'
207
+ );
208
+ dim(
209
+ 'All routes, pages, and assets are embedded inside the binary.'
210
+ );
217
211
  newline();
218
- info('Your standalone executable is ready to run!');
219
-
220
212
  if (process.platform !== 'win32') {
221
- dim(`Make it executable: chmod +x ${outfile}`);
222
- dim(`Run it: ./${outfile}`);
213
+ dim(`Make executable: chmod +x ${outfile}`);
214
+ dim(`Run: ./${outfile}`);
223
215
  } else {
224
- dim(`Run it: ${outfile}`);
216
+ dim(`Run: ${outfile}`);
225
217
  }
226
218
  newline();
227
219
  } catch (err) {
@@ -230,21 +222,3 @@ export const buildExecutableCommand = new Command('build:executable')
230
222
  process.exit(1);
231
223
  }
232
224
  });
233
-
234
- /**
235
- * Get the project name from package.json
236
- * Falls back to 'app' if not found
237
- */
238
- function getProjectName(): string {
239
- try {
240
- const packageJsonPath = join(process.cwd(), 'package.json');
241
- if (existsSync(packageJsonPath)) {
242
- const content = readFileSync(packageJsonPath, 'utf-8');
243
- const packageJson = JSON.parse(content);
244
- return packageJson?.name || 'app';
245
- }
246
- } catch (err) {
247
- // Ignore errors
248
- }
249
- return 'app';
250
- }
@@ -82,6 +82,7 @@ export const createCommand = new Command('create')
82
82
  info('Creating project with the following configuration:');
83
83
  newline();
84
84
  console.log(` Name: ${projectName}`);
85
+ console.log(` Config File: burger.config.ts`);
85
86
  if (options.useApi) {
86
87
  console.log(` API Routes: ${options.apiDir || 'api'}`);
87
88
  }
@@ -106,10 +107,13 @@ export const createCommand = new Command('create')
106
107
  console.log(` 2. Start the development server:`);
107
108
  command('bun run dev');
108
109
  newline();
109
- console.log(` 3. Open your browser:`);
110
+ console.log(` 3. Edit config if needed:`);
111
+ command('burger.config.ts');
112
+ newline();
113
+ console.log(` 4. Open your browser:`);
110
114
  console.log(` ${highlight('http://localhost:4000')}`);
111
115
  newline();
112
- console.log(` 4. Add middleware (optional):`);
116
+ console.log(` 5. Add middleware (optional):`);
113
117
  command('burger-api add cors logger');
114
118
  newline();
115
119
  success('Happy coding!');
package/src/index.ts CHANGED
@@ -10,6 +10,8 @@
10
10
  * This makes it easy to add new commands and provide helpful error messages.
11
11
  */
12
12
 
13
+ import { readFileSync } from 'fs';
14
+ import { join } from 'path';
13
15
  import { Command } from 'commander';
14
16
  import { createCommand } from './commands/create';
15
17
  import { addCommand } from './commands/add';
@@ -18,6 +20,22 @@ import { buildCommand, buildExecutableCommand } from './commands/build';
18
20
  import { serveCommand } from './commands/serve';
19
21
  import { showBanner } from './utils/logger';
20
22
 
23
+ /**
24
+ * Read CLI version from package.json (single source of truth for publish).
25
+ * @returns The version of the CLI.
26
+ */
27
+ function getVersion(): string {
28
+ try {
29
+ const pkgPath = join(import.meta.dir, '..', 'package.json');
30
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8')) as {
31
+ version?: string;
32
+ };
33
+ return pkg.version ?? '0.0.0';
34
+ } catch {
35
+ return '0.0.0';
36
+ }
37
+ }
38
+
21
39
  /**
22
40
  * Create the main CLI program
23
41
  * This is what runs when someone types 'burger-api' in their terminal
@@ -28,7 +46,7 @@ const program = new Command();
28
46
  program
29
47
  .name('burger-api')
30
48
  .description('Simple tool to work with BurgerAPI projects')
31
- .version('0.7.0');
49
+ .version(getVersion());
32
50
 
33
51
  // Add all our commands to the CLI
34
52
  // Each command is defined in its own file for better organization
@@ -41,7 +59,7 @@ program.addCommand(serveCommand); // Run development server
41
59
 
42
60
  // Show banner + help when no command is provided
43
61
  program.action(() => {
44
- showBanner();
62
+ showBanner(getVersion());
45
63
  program.help();
46
64
  });
47
65
 
@@ -51,3 +51,15 @@ export interface GitHubFile {
51
51
  download_url?: string;
52
52
  size: number;
53
53
  }
54
+
55
+ /**
56
+ * Build-time configuration for Burger API (conventions or burger.config.ts).
57
+ * Used by the CLI when generating the virtual entry and scanning routes.
58
+ */
59
+ export interface BuildConfig {
60
+ apiDir: string;
61
+ pageDir: string;
62
+ apiPrefix: string;
63
+ pagePrefix: string;
64
+ debug?: boolean;
65
+ }
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Build config resolution: conventions-first with optional burger.config.ts
3
+ *
4
+ * Used by the CLI build pipeline to discover apiDir, pageDir, and prefixes
5
+ * without parsing the user's entry file.
6
+ */
7
+
8
+ import { existsSync } from 'fs';
9
+ import { join } from 'path';
10
+ import type { BuildConfig } from '../types/index';
11
+
12
+ const CONVENTION_DEFAULTS: BuildConfig = {
13
+ apiDir: './src/api',
14
+ pageDir: './src/pages',
15
+ apiPrefix: '/api',
16
+ pagePrefix: '/',
17
+ debug: false,
18
+ };
19
+
20
+ const CONFIG_NAMES = ['burger.config.ts', 'burger.config.js'];
21
+
22
+ /**
23
+ * Resolve build configuration from the project directory.
24
+ * Uses convention defaults; overrides with burger.config.ts / burger.config.js if present.
25
+ *
26
+ * @param cwd - Project root (e.g. process.cwd())
27
+ * @returns BuildConfig with resolved paths and prefixes
28
+ */
29
+ export async function resolveBuildConfig(cwd: string): Promise<BuildConfig> {
30
+ let configPath: string | null = null;
31
+ for (const name of CONFIG_NAMES) {
32
+ const candidate = join(cwd, name);
33
+ if (existsSync(candidate)) {
34
+ configPath = candidate;
35
+ break;
36
+ }
37
+ }
38
+
39
+ if (!configPath) {
40
+ return { ...CONVENTION_DEFAULTS };
41
+ }
42
+
43
+ try {
44
+ const mod = await import(configPath);
45
+ const user = mod.default ?? mod;
46
+ if (!user || typeof user !== 'object') {
47
+ return { ...CONVENTION_DEFAULTS };
48
+ }
49
+ return mergeBuildConfig(CONVENTION_DEFAULTS, user);
50
+ } catch (err) {
51
+ console.warn(
52
+ `[burger-api] Could not load ${configPath}: ${err instanceof Error ? err.message : String(err)}. Using convention defaults.`
53
+ );
54
+ return { ...CONVENTION_DEFAULTS };
55
+ }
56
+ }
57
+
58
+ function mergeBuildConfig(
59
+ defaults: BuildConfig,
60
+ user: Record<string, unknown>
61
+ ): BuildConfig {
62
+ return {
63
+ apiDir:
64
+ typeof user.apiDir === 'string' ? user.apiDir : defaults.apiDir,
65
+ pageDir:
66
+ typeof user.pageDir === 'string' ? user.pageDir : defaults.pageDir,
67
+ apiPrefix:
68
+ typeof user.apiPrefix === 'string'
69
+ ? user.apiPrefix
70
+ : defaults.apiPrefix,
71
+ pagePrefix:
72
+ typeof user.pagePrefix === 'string'
73
+ ? user.pagePrefix
74
+ : defaults.pagePrefix,
75
+ debug:
76
+ typeof user.debug === 'boolean' ? user.debug : defaults.debug,
77
+ };
78
+ }