@burger-api/cli 0.9.1 → 0.9.4
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/CHANGELOG.md +1 -1
- package/package.json +5 -5
- package/src/commands/add.ts +2 -1
- package/src/commands/build.ts +106 -99
- package/src/commands/create.ts +51 -1
- package/src/commands/list.ts +36 -41
- package/src/index.ts +27 -0
- package/src/utils/build/bun.ts +134 -0
- package/src/utils/build/entry.ts +74 -0
- package/src/utils/build/pipeline.ts +90 -0
- package/src/utils/build/project.ts +21 -0
- package/src/utils/config.ts +8 -7
- package/src/utils/github.ts +42 -13
- package/src/utils/logger.ts +21 -0
- package/src/utils/templates.ts +8 -3
package/CHANGELOG.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to the Burger API CLI will be documented in this file.
|
|
4
4
|
|
|
5
|
-
## Version 0.9.
|
|
5
|
+
## Version 0.9.3 - (March 17, 2026)
|
|
6
6
|
|
|
7
7
|
- ✨ **Create** – New projects get a config file (`burger.config.ts`) from
|
|
8
8
|
your answers; the build uses this config when present.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@burger-api/cli",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.4",
|
|
4
4
|
"description": "Simple command-line tool for Burger API projects",
|
|
5
5
|
"module": "src/index.ts",
|
|
6
6
|
"type": "module",
|
|
@@ -20,10 +20,10 @@
|
|
|
20
20
|
"dev": "bun run src/index.ts",
|
|
21
21
|
"test": "bun test --timeout 30000 ./test",
|
|
22
22
|
"test:build": "bun test test/scanner.test.ts test/virtual-entry.test.ts test/entry-options.test.ts test/build-preserve-options.test.ts",
|
|
23
|
-
"build:win": "bun
|
|
24
|
-
"build:linux": "bun
|
|
25
|
-
"build:mac": "bun
|
|
26
|
-
"build:mac-intel": "bun
|
|
23
|
+
"build:win": "bun run scripts/build-executable.ts win",
|
|
24
|
+
"build:linux": "bun run scripts/build-executable.ts linux",
|
|
25
|
+
"build:mac": "bun run scripts/build-executable.ts mac",
|
|
26
|
+
"build:mac-intel": "bun run scripts/build-executable.ts mac-intel",
|
|
27
27
|
"build:all": "bun run build:win && bun run build:linux && bun run build:mac && bun run build:mac-intel"
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
package/src/commands/add.ts
CHANGED
|
@@ -69,7 +69,7 @@ export const addCommand = new Command('add')
|
|
|
69
69
|
for (const name of middlewareNames) {
|
|
70
70
|
try {
|
|
71
71
|
// Check if it exists on GitHub
|
|
72
|
-
|
|
72
|
+
let spin = spinner(`Checking ${name}...`);
|
|
73
73
|
|
|
74
74
|
let exists;
|
|
75
75
|
try {
|
|
@@ -106,6 +106,7 @@ export const addCommand = new Command('add')
|
|
|
106
106
|
results.skipped.push(name);
|
|
107
107
|
continue;
|
|
108
108
|
}
|
|
109
|
+
spin = spinner(`Downloading ${name}...`);
|
|
109
110
|
}
|
|
110
111
|
|
|
111
112
|
// Download the middleware
|
package/src/commands/build.ts
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
import { Command } from 'commander';
|
|
12
12
|
import { existsSync } from 'fs';
|
|
13
13
|
import { dirname, resolve } from 'path';
|
|
14
|
+
import type { Spinner } from '../utils/logger';
|
|
14
15
|
import {
|
|
15
16
|
spinner,
|
|
16
17
|
success,
|
|
@@ -20,9 +21,51 @@ import {
|
|
|
20
21
|
formatSize,
|
|
21
22
|
dim,
|
|
22
23
|
} from '../utils/logger';
|
|
23
|
-
import {
|
|
24
|
+
import {
|
|
25
|
+
runVirtualEntryBuild,
|
|
26
|
+
type VirtualBuildResult,
|
|
27
|
+
} from '../utils/build/pipeline';
|
|
24
28
|
import { getProjectName } from '../utils/build/project';
|
|
25
29
|
|
|
30
|
+
function ensureEntryFileExists(cwd: string, file: string): void {
|
|
31
|
+
const entryPath = resolve(cwd, file);
|
|
32
|
+
if (!existsSync(entryPath)) {
|
|
33
|
+
logError(`Entry file not found: ${file}`);
|
|
34
|
+
info('Make sure you are in the project directory.');
|
|
35
|
+
process.exit(1);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async function runBuildWithSpinner(params: {
|
|
40
|
+
file: string;
|
|
41
|
+
cwd: string;
|
|
42
|
+
spinMessage: string;
|
|
43
|
+
failMessage: string;
|
|
44
|
+
buildOptions: Parameters<typeof runVirtualEntryBuild>[0];
|
|
45
|
+
onBeforeBuild?: (spin: Spinner) => void;
|
|
46
|
+
onSuccess: (result: VirtualBuildResult, spin: Spinner) => void;
|
|
47
|
+
}): Promise<void> {
|
|
48
|
+
ensureEntryFileExists(params.cwd, params.file);
|
|
49
|
+
const spin = spinner(params.spinMessage);
|
|
50
|
+
try {
|
|
51
|
+
info('Build-time route discovery enabled.');
|
|
52
|
+
params.onBeforeBuild?.(spin);
|
|
53
|
+
const result = await runVirtualEntryBuild(params.buildOptions);
|
|
54
|
+
if (!result.success) {
|
|
55
|
+
spin.stop(params.failMessage, true);
|
|
56
|
+
logError(
|
|
57
|
+
'Bun.build failed. Check that api/page directories and route files are valid.'
|
|
58
|
+
);
|
|
59
|
+
process.exit(1);
|
|
60
|
+
}
|
|
61
|
+
params.onSuccess(result, spin);
|
|
62
|
+
} catch (err) {
|
|
63
|
+
spin.stop(params.failMessage, true);
|
|
64
|
+
logError(err instanceof Error ? err.message : 'Unknown error');
|
|
65
|
+
process.exit(1);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
26
69
|
/**
|
|
27
70
|
* Options for the `build` command.
|
|
28
71
|
*/
|
|
@@ -73,58 +116,39 @@ export const buildCommand = new Command('build')
|
|
|
73
116
|
.option('--target <env>', 'Target environment (default: bun)')
|
|
74
117
|
.action(async (file: string, options: BuildCommandOptions) => {
|
|
75
118
|
const cwd = process.cwd();
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
const spin = spinner('Building project...');
|
|
84
|
-
|
|
85
|
-
try {
|
|
86
|
-
info('Build-time route discovery enabled.');
|
|
87
|
-
|
|
88
|
-
const { success: ok, hasPages } = await runVirtualEntryBuild({
|
|
119
|
+
await runBuildWithSpinner({
|
|
120
|
+
file,
|
|
121
|
+
cwd,
|
|
122
|
+
spinMessage: 'Building project...',
|
|
123
|
+
failMessage: 'Build failed',
|
|
124
|
+
buildOptions: {
|
|
89
125
|
cwd,
|
|
90
126
|
entryFile: file,
|
|
91
127
|
outfile: options.outfile,
|
|
92
128
|
target: options.target || 'bun',
|
|
93
129
|
minify: options.minify,
|
|
94
130
|
sourcemap: options.sourcemap,
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
);
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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
|
-
}
|
|
122
|
-
newline();
|
|
123
|
-
} catch (err) {
|
|
124
|
-
spin.stop('Build failed', true);
|
|
125
|
-
logError(err instanceof Error ? err.message : 'Unknown error');
|
|
126
|
-
process.exit(1);
|
|
127
|
-
}
|
|
131
|
+
},
|
|
132
|
+
onSuccess: (result, spin) => {
|
|
133
|
+
const size = Bun.file(options.outfile).size;
|
|
134
|
+
const bundleDir = dirname(options.outfile);
|
|
135
|
+
spin.stop('Build completed successfully!');
|
|
136
|
+
newline();
|
|
137
|
+
success(`Bundle: ${options.outfile} (${formatSize(size)})`);
|
|
138
|
+
newline();
|
|
139
|
+
if (result.hasPages) {
|
|
140
|
+
info(`Pages and assets are in: ${bundleDir}/`);
|
|
141
|
+
dim(
|
|
142
|
+
'Deploy the entire directory — HTML pages depend on their chunks.'
|
|
143
|
+
);
|
|
144
|
+
dim(`Run: bun ${options.outfile}`);
|
|
145
|
+
} else {
|
|
146
|
+
info('API-only bundle — self-contained single file.');
|
|
147
|
+
dim(`Run anywhere: bun ${options.outfile}`);
|
|
148
|
+
}
|
|
149
|
+
newline();
|
|
150
|
+
},
|
|
151
|
+
});
|
|
128
152
|
});
|
|
129
153
|
|
|
130
154
|
/**
|
|
@@ -151,13 +175,6 @@ export const buildExecutableCommand = new Command('build:exec')
|
|
|
151
175
|
.option('--no-bytecode', 'Disable bytecode compilation')
|
|
152
176
|
.action(async (file: string, options: BuildExecutableOptions) => {
|
|
153
177
|
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.');
|
|
158
|
-
process.exit(1);
|
|
159
|
-
}
|
|
160
|
-
|
|
161
178
|
let outfile = options.outfile;
|
|
162
179
|
if (!outfile) {
|
|
163
180
|
const projectName = getProjectName(cwd);
|
|
@@ -168,14 +185,14 @@ export const buildExecutableCommand = new Command('build:exec')
|
|
|
168
185
|
? `.build/executable/${projectName}.exe`
|
|
169
186
|
: `.build/executable/${projectName}`;
|
|
170
187
|
}
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
188
|
+
const outfileFinal = outfile;
|
|
189
|
+
|
|
190
|
+
await runBuildWithSpinner({
|
|
191
|
+
file,
|
|
192
|
+
cwd,
|
|
193
|
+
spinMessage: 'Compiling to executable...',
|
|
194
|
+
failMessage: 'Compilation failed',
|
|
195
|
+
buildOptions: {
|
|
179
196
|
cwd,
|
|
180
197
|
entryFile: file,
|
|
181
198
|
outfile,
|
|
@@ -183,42 +200,32 @@ export const buildExecutableCommand = new Command('build:exec')
|
|
|
183
200
|
minify: options.minify,
|
|
184
201
|
bytecode: options.bytecode !== false,
|
|
185
202
|
compile: true,
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
203
|
+
},
|
|
204
|
+
onBeforeBuild: (spin) =>
|
|
205
|
+
spin.update('Compiling... (this may take a minute)'),
|
|
206
|
+
onSuccess: (result, spin) => {
|
|
207
|
+
const size = existsSync(outfileFinal)
|
|
208
|
+
? Bun.file(outfileFinal).size
|
|
209
|
+
: (result.outputs[0]?.size ?? 0);
|
|
210
|
+
spin.stop('Compilation completed successfully!');
|
|
211
|
+
newline();
|
|
212
|
+
success(`Executable: ${outfileFinal}`);
|
|
213
|
+
if (size > 0) info(`Size: ${formatSize(size)}`);
|
|
214
|
+
newline();
|
|
215
|
+
info(
|
|
216
|
+
'Standalone binary — copy it anywhere, no Bun required on the target.'
|
|
192
217
|
);
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
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
|
-
);
|
|
211
|
-
newline();
|
|
212
|
-
if (process.platform !== 'win32') {
|
|
213
|
-
dim(`Make executable: chmod +x ${outfile}`);
|
|
214
|
-
dim(`Run: ./${outfile}`);
|
|
215
|
-
} else {
|
|
216
|
-
dim(`Run: ${outfile}`);
|
|
217
|
-
}
|
|
218
|
-
newline();
|
|
219
|
-
} catch (err) {
|
|
220
|
-
spin.stop('Compilation failed', true);
|
|
221
|
-
logError(err instanceof Error ? err.message : 'Unknown error');
|
|
222
|
-
process.exit(1);
|
|
223
|
-
}
|
|
218
|
+
dim(
|
|
219
|
+
'All routes, pages, and assets are embedded inside the binary.'
|
|
220
|
+
);
|
|
221
|
+
newline();
|
|
222
|
+
if (process.platform !== 'win32') {
|
|
223
|
+
dim(`Make executable: chmod +x ${outfileFinal}`);
|
|
224
|
+
dim(`Run: ./${outfileFinal}`);
|
|
225
|
+
} else {
|
|
226
|
+
dim(`Run: ${outfileFinal}`);
|
|
227
|
+
}
|
|
228
|
+
newline();
|
|
229
|
+
},
|
|
230
|
+
});
|
|
224
231
|
});
|
package/src/commands/create.ts
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
import { Command } from 'commander';
|
|
11
11
|
import * as clack from '@clack/prompts';
|
|
12
12
|
import { existsSync } from 'fs';
|
|
13
|
-
import { join } from 'path';
|
|
13
|
+
import { isAbsolute, join, relative, resolve } from 'path';
|
|
14
14
|
import type { CreateOptions } from '../types/index';
|
|
15
15
|
import { createProject, installDependencies } from '../utils/templates';
|
|
16
16
|
import {
|
|
@@ -45,6 +45,26 @@ function validateProjectName(name: string): string | undefined {
|
|
|
45
45
|
return undefined;
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
+
/**
|
|
49
|
+
* Ensure directory name (apiDir/pageDir) resolves under targetDir/src to prevent path traversal.
|
|
50
|
+
*/
|
|
51
|
+
function validateDirUnderSrc(
|
|
52
|
+
targetDir: string,
|
|
53
|
+
dirName: string,
|
|
54
|
+
label: string
|
|
55
|
+
): string | undefined {
|
|
56
|
+
if (!dirName || dirName.includes('..')) {
|
|
57
|
+
return `${label} cannot be empty or contain '..'`;
|
|
58
|
+
}
|
|
59
|
+
const srcRoot = resolve(targetDir, 'src');
|
|
60
|
+
const resolved = resolve(targetDir, 'src', dirName);
|
|
61
|
+
const rel = relative(srcRoot, resolved);
|
|
62
|
+
if (rel.startsWith('..') || isAbsolute(rel)) {
|
|
63
|
+
return `${label} must resolve inside the project's src directory`;
|
|
64
|
+
}
|
|
65
|
+
return undefined;
|
|
66
|
+
}
|
|
67
|
+
|
|
48
68
|
export const createCommand = new Command('create')
|
|
49
69
|
.description('Create a new Burger API project')
|
|
50
70
|
.argument('<project-name>', 'Name of your project')
|
|
@@ -78,6 +98,32 @@ export const createCommand = new Command('create')
|
|
|
78
98
|
process.exit(0);
|
|
79
99
|
}
|
|
80
100
|
|
|
101
|
+
// Validate apiDir/pageDir stay under targetDir/src (prevent path traversal)
|
|
102
|
+
if (options.useApi) {
|
|
103
|
+
const apiDirError = validateDirUnderSrc(
|
|
104
|
+
targetDir,
|
|
105
|
+
options.apiDir || 'api',
|
|
106
|
+
'API directory'
|
|
107
|
+
);
|
|
108
|
+
if (apiDirError) {
|
|
109
|
+
clack.outro('Invalid configuration');
|
|
110
|
+
logError(apiDirError);
|
|
111
|
+
process.exit(1);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
if (options.usePages) {
|
|
115
|
+
const pageDirError = validateDirUnderSrc(
|
|
116
|
+
targetDir,
|
|
117
|
+
options.pageDir || 'pages',
|
|
118
|
+
'Page directory'
|
|
119
|
+
);
|
|
120
|
+
if (pageDirError) {
|
|
121
|
+
clack.outro('Invalid configuration');
|
|
122
|
+
logError(pageDirError);
|
|
123
|
+
process.exit(1);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
81
127
|
// Show what we're about to create
|
|
82
128
|
info('Creating project with the following configuration:');
|
|
83
129
|
newline();
|
|
@@ -154,6 +200,8 @@ async function askQuestions(projectName: string): Promise<CreateOptions> {
|
|
|
154
200
|
return 'Please enter a directory name';
|
|
155
201
|
if (value.includes(' '))
|
|
156
202
|
return 'Directory name cannot contain spaces';
|
|
203
|
+
if (value.includes('..'))
|
|
204
|
+
return 'Directory name cannot contain ..';
|
|
157
205
|
},
|
|
158
206
|
})
|
|
159
207
|
: Promise.resolve('api'),
|
|
@@ -196,6 +244,8 @@ async function askQuestions(projectName: string): Promise<CreateOptions> {
|
|
|
196
244
|
return 'Please enter a directory name';
|
|
197
245
|
if (value.includes(' '))
|
|
198
246
|
return 'Directory name cannot contain spaces';
|
|
247
|
+
if (value.includes('..'))
|
|
248
|
+
return 'Directory name cannot contain ..';
|
|
199
249
|
},
|
|
200
250
|
})
|
|
201
251
|
: Promise.resolve('pages'),
|
package/src/commands/list.ts
CHANGED
|
@@ -11,7 +11,7 @@ import { Command } from 'commander';
|
|
|
11
11
|
import { getMiddlewareList, getMiddlewareInfo } from '../utils/github';
|
|
12
12
|
import {
|
|
13
13
|
header,
|
|
14
|
-
|
|
14
|
+
withSpinner,
|
|
15
15
|
error as logError,
|
|
16
16
|
table,
|
|
17
17
|
newline,
|
|
@@ -28,54 +28,49 @@ export const listCommand = new Command('list')
|
|
|
28
28
|
.description('Show available middleware from the ecosystem')
|
|
29
29
|
.alias('ls') // Allow users to type "burger-api ls" too
|
|
30
30
|
.action(async () => {
|
|
31
|
-
// Show a spinner while fetching from GitHub
|
|
32
|
-
const spin = spinner('Fetching middleware list from GitHub...');
|
|
33
|
-
|
|
34
31
|
try {
|
|
35
|
-
|
|
36
|
-
|
|
32
|
+
await withSpinner(
|
|
33
|
+
'Fetching middleware list from GitHub...',
|
|
34
|
+
async (spin) => {
|
|
35
|
+
const middlewareNames = await getMiddlewareList();
|
|
37
36
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
);
|
|
37
|
+
const middlewareDetails = await Promise.all(
|
|
38
|
+
middlewareNames.map((name) =>
|
|
39
|
+
getMiddlewareInfo(name).catch(() => ({
|
|
40
|
+
name,
|
|
41
|
+
description: 'No description available',
|
|
42
|
+
path: '',
|
|
43
|
+
files: [],
|
|
44
|
+
}))
|
|
45
|
+
)
|
|
46
|
+
);
|
|
49
47
|
|
|
50
|
-
|
|
51
|
-
|
|
48
|
+
spin.stop('Found available middleware!');
|
|
49
|
+
newline();
|
|
52
50
|
|
|
53
|
-
|
|
54
|
-
header('Available Middleware');
|
|
51
|
+
header('Available Middleware');
|
|
55
52
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
];
|
|
53
|
+
const tableData: string[][] = [
|
|
54
|
+
['Name', 'Description'],
|
|
55
|
+
...middlewareDetails.map((m) => [
|
|
56
|
+
m.name,
|
|
57
|
+
m.description.length > 60
|
|
58
|
+
? m.description.substring(0, 57) + '...'
|
|
59
|
+
: m.description,
|
|
60
|
+
]),
|
|
61
|
+
];
|
|
66
62
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
newline();
|
|
63
|
+
table(tableData);
|
|
64
|
+
newline();
|
|
70
65
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
66
|
+
info('To add middleware to your project, run:');
|
|
67
|
+
command('burger-api add <middleware-name>');
|
|
68
|
+
newline();
|
|
69
|
+
dim('Example: burger-api add cors logger rate-limiter');
|
|
70
|
+
newline();
|
|
71
|
+
}
|
|
72
|
+
);
|
|
77
73
|
} catch (err) {
|
|
78
|
-
spin.stop('Failed to fetch middleware list', true);
|
|
79
74
|
logError(
|
|
80
75
|
err instanceof Error
|
|
81
76
|
? err.message
|
package/src/index.ts
CHANGED
|
@@ -20,11 +20,18 @@ import { buildCommand, buildExecutableCommand } from './commands/build';
|
|
|
20
20
|
import { serveCommand } from './commands/serve';
|
|
21
21
|
import { showBanner } from './utils/logger';
|
|
22
22
|
|
|
23
|
+
/** Injected at build time when compiling to executable (--define CLI_VERSION). */
|
|
24
|
+
declare const CLI_VERSION: string | undefined;
|
|
25
|
+
|
|
23
26
|
/**
|
|
24
27
|
* Read CLI version from package.json (single source of truth for publish).
|
|
28
|
+
* When running as compiled binary, version is injected at build time via CLI_VERSION.
|
|
25
29
|
* @returns The version of the CLI.
|
|
26
30
|
*/
|
|
27
31
|
function getVersion(): string {
|
|
32
|
+
if (typeof CLI_VERSION !== 'undefined') {
|
|
33
|
+
return CLI_VERSION;
|
|
34
|
+
}
|
|
28
35
|
try {
|
|
29
36
|
const pkgPath = join(import.meta.dir, '..', 'package.json');
|
|
30
37
|
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8')) as {
|
|
@@ -73,5 +80,25 @@ program.configureOutput({
|
|
|
73
80
|
},
|
|
74
81
|
});
|
|
75
82
|
|
|
83
|
+
// Use exit code 2 for usage errors (invalid options/args) per CLI guidelines
|
|
84
|
+
const USAGE_ERROR_CODES = [
|
|
85
|
+
'commander.unknownOption',
|
|
86
|
+
'commander.unknownArgument',
|
|
87
|
+
'commander.missingMandatoryOptionValue',
|
|
88
|
+
'commander.invalidOptionArgument',
|
|
89
|
+
'commander.invalidArgument',
|
|
90
|
+
];
|
|
91
|
+
program.exitOverride((err) => {
|
|
92
|
+
const errorCode = (err as { code?: string }).code;
|
|
93
|
+
const code =
|
|
94
|
+
err.exitCode === 0
|
|
95
|
+
? 0
|
|
96
|
+
: typeof errorCode === 'string' &&
|
|
97
|
+
USAGE_ERROR_CODES.includes(errorCode)
|
|
98
|
+
? 2
|
|
99
|
+
: 1;
|
|
100
|
+
process.exit(code);
|
|
101
|
+
});
|
|
102
|
+
|
|
76
103
|
// Run the CLI - this parses the arguments the user typed
|
|
77
104
|
program.parse();
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { resolve } from 'path';
|
|
2
|
+
|
|
3
|
+
function formatBunBuildLogs(logs: unknown): string {
|
|
4
|
+
if (!Array.isArray(logs) || logs.length === 0) {
|
|
5
|
+
return '';
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const messages: string[] = [];
|
|
9
|
+
for (const item of logs) {
|
|
10
|
+
if (!item || typeof item !== 'object') continue;
|
|
11
|
+
|
|
12
|
+
const log = item as {
|
|
13
|
+
level?: string;
|
|
14
|
+
message?: string;
|
|
15
|
+
text?: string;
|
|
16
|
+
name?: string;
|
|
17
|
+
position?: { file?: string; line?: number; column?: number };
|
|
18
|
+
};
|
|
19
|
+
const text =
|
|
20
|
+
(typeof log.message === 'string' && log.message) ||
|
|
21
|
+
(typeof log.text === 'string' && log.text) ||
|
|
22
|
+
(typeof log.name === 'string' && log.name) ||
|
|
23
|
+
'';
|
|
24
|
+
if (!text) continue;
|
|
25
|
+
|
|
26
|
+
const level =
|
|
27
|
+
typeof log.level === 'string' ? log.level.toUpperCase() : 'ERROR';
|
|
28
|
+
const file = log.position?.file ? `${log.position.file}` : '';
|
|
29
|
+
const line =
|
|
30
|
+
typeof log.position?.line === 'number'
|
|
31
|
+
? `:${log.position.line}`
|
|
32
|
+
: '';
|
|
33
|
+
const column =
|
|
34
|
+
typeof log.position?.column === 'number'
|
|
35
|
+
? `:${log.position.column}`
|
|
36
|
+
: '';
|
|
37
|
+
const location = file ? ` (${file}${line}${column})` : '';
|
|
38
|
+
messages.push(`- [${level}] ${text}${location}`);
|
|
39
|
+
}
|
|
40
|
+
return messages.join('\n');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function extractBunBuildDetails(err: unknown): string {
|
|
44
|
+
if (!err || typeof err !== 'object') {
|
|
45
|
+
return '';
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const anyErr = err as {
|
|
49
|
+
logs?: unknown;
|
|
50
|
+
errors?: unknown;
|
|
51
|
+
cause?: { logs?: unknown; errors?: unknown };
|
|
52
|
+
};
|
|
53
|
+
const candidates = [
|
|
54
|
+
anyErr.logs,
|
|
55
|
+
anyErr.errors,
|
|
56
|
+
anyErr.cause?.logs,
|
|
57
|
+
anyErr.cause?.errors,
|
|
58
|
+
];
|
|
59
|
+
|
|
60
|
+
for (const candidate of candidates) {
|
|
61
|
+
const detail = formatBunBuildLogs(candidate);
|
|
62
|
+
if (detail) return detail;
|
|
63
|
+
}
|
|
64
|
+
return '';
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function createBunBuildOptions(options: {
|
|
68
|
+
entryPath: string;
|
|
69
|
+
outDir: string;
|
|
70
|
+
cwd: string;
|
|
71
|
+
outfile: string;
|
|
72
|
+
target?: string;
|
|
73
|
+
minify?: boolean;
|
|
74
|
+
sourcemap?: string;
|
|
75
|
+
compile?: boolean;
|
|
76
|
+
bytecode?: boolean;
|
|
77
|
+
}): Parameters<typeof Bun.build>[0] {
|
|
78
|
+
const buildOptions: Parameters<typeof Bun.build>[0] = {
|
|
79
|
+
entrypoints: [options.entryPath],
|
|
80
|
+
outdir: options.outDir,
|
|
81
|
+
target: (options.target as 'bun') || 'bun',
|
|
82
|
+
minify: options.minify ?? false,
|
|
83
|
+
splitting: false,
|
|
84
|
+
sourcemap:
|
|
85
|
+
options.sourcemap === undefined
|
|
86
|
+
? undefined
|
|
87
|
+
: (options.sourcemap as 'none' | 'linked' | 'inline' | 'external'),
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const ext = buildOptions as unknown as Record<string, unknown>;
|
|
91
|
+
ext.naming = {
|
|
92
|
+
chunk: '[name]-[hash].[ext]',
|
|
93
|
+
asset: '[name]-[hash].[ext]',
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
if (options.compile) {
|
|
97
|
+
ext.compile = {
|
|
98
|
+
outfile: resolve(options.cwd, options.outfile),
|
|
99
|
+
...(options.target && { target: options.target }),
|
|
100
|
+
};
|
|
101
|
+
if (options.bytecode !== false) {
|
|
102
|
+
ext.bytecode = true;
|
|
103
|
+
}
|
|
104
|
+
delete ext.outdir;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return buildOptions;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export async function runBunBuildOrThrow(
|
|
111
|
+
buildOptions: Parameters<typeof Bun.build>[0]
|
|
112
|
+
): Promise<Awaited<ReturnType<typeof Bun.build>>> {
|
|
113
|
+
let result: Awaited<ReturnType<typeof Bun.build>>;
|
|
114
|
+
try {
|
|
115
|
+
result = await Bun.build(buildOptions);
|
|
116
|
+
} catch (err) {
|
|
117
|
+
const detail = extractBunBuildDetails(err);
|
|
118
|
+
const message = err instanceof Error ? err.message : 'Bun.build failed.';
|
|
119
|
+
if (detail) {
|
|
120
|
+
throw new Error(`${message}\n${detail}`);
|
|
121
|
+
}
|
|
122
|
+
throw new Error(message);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (!result.success) {
|
|
126
|
+
const detail = formatBunBuildLogs((result as { logs?: unknown }).logs);
|
|
127
|
+
if (detail) {
|
|
128
|
+
throw new Error(`Bun.build failed.\n${detail}`);
|
|
129
|
+
}
|
|
130
|
+
throw new Error('Bun.build failed.');
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return result;
|
|
134
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { dirname, resolve } from 'path';
|
|
2
|
+
import { existsSync, mkdirSync, unlinkSync, writeFileSync } from 'fs';
|
|
3
|
+
|
|
4
|
+
export function prepareVirtualEntry(options: {
|
|
5
|
+
cwd: string;
|
|
6
|
+
outfile: string;
|
|
7
|
+
pageDir: string;
|
|
8
|
+
source: string;
|
|
9
|
+
hasPages: boolean;
|
|
10
|
+
}): { outDir: string; virtualSourcePath: string; virtualPath: string } {
|
|
11
|
+
const outDir = resolve(options.cwd, dirname(options.outfile));
|
|
12
|
+
mkdirSync(outDir, { recursive: true });
|
|
13
|
+
|
|
14
|
+
const virtualEntryDir = options.hasPages
|
|
15
|
+
? resolve(options.cwd, options.pageDir)
|
|
16
|
+
: outDir;
|
|
17
|
+
mkdirSync(virtualEntryDir, { recursive: true });
|
|
18
|
+
|
|
19
|
+
const virtualSourcePath = resolve(virtualEntryDir, '__burger_build_entry__.ts');
|
|
20
|
+
const virtualPath = virtualSourcePath.split('\\').join('/');
|
|
21
|
+
writeFileSync(virtualSourcePath, options.source, 'utf-8');
|
|
22
|
+
|
|
23
|
+
return { outDir, virtualSourcePath, virtualPath };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function cleanupVirtualEntry(virtualSourcePath: string): void {
|
|
27
|
+
if (existsSync(virtualSourcePath)) {
|
|
28
|
+
unlinkSync(virtualSourcePath);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export async function finalizeBuildOutputs(options: {
|
|
33
|
+
result: Awaited<ReturnType<typeof Bun.build>>;
|
|
34
|
+
cwd: string;
|
|
35
|
+
outfile: string;
|
|
36
|
+
outDir: string;
|
|
37
|
+
compile?: boolean;
|
|
38
|
+
}): Promise<{ path: string; size: number }[]> {
|
|
39
|
+
const desiredOut = resolve(options.cwd, options.outfile);
|
|
40
|
+
const outputs: { path: string; size: number }[] = [];
|
|
41
|
+
|
|
42
|
+
if (!options.result.outputs?.length) {
|
|
43
|
+
return outputs;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const first = options.result.outputs[0] as Blob & { path?: string };
|
|
47
|
+
const outPath = first.path ? resolve(first.path) : undefined;
|
|
48
|
+
|
|
49
|
+
const entryArtifacts = new Set<string>([
|
|
50
|
+
resolve(options.outDir, '__burger_build_entry__.js'),
|
|
51
|
+
]);
|
|
52
|
+
if (outPath?.endsWith('__burger_build_entry__.js')) {
|
|
53
|
+
entryArtifacts.add(outPath);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (!options.compile && outPath && outPath !== desiredOut) {
|
|
57
|
+
const blob = first as Blob;
|
|
58
|
+
await Bun.write(desiredOut, blob);
|
|
59
|
+
outputs.push({ path: desiredOut, size: blob.size });
|
|
60
|
+
} else {
|
|
61
|
+
outputs.push({
|
|
62
|
+
path: outPath ?? desiredOut,
|
|
63
|
+
size: (first as Blob).size ?? 0,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
for (const entryArtifact of entryArtifacts) {
|
|
68
|
+
if (entryArtifact !== desiredOut && existsSync(entryArtifact)) {
|
|
69
|
+
unlinkSync(entryArtifact);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return outputs;
|
|
74
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { resolveBuildConfig } from '../config';
|
|
2
|
+
import { scanApiRoutes, scanPageRoutes } from '../scanner';
|
|
3
|
+
import { generateVirtualEntrySource } from '../virtual-entry';
|
|
4
|
+
import { createBunBuildOptions, runBunBuildOrThrow } from './bun';
|
|
5
|
+
import {
|
|
6
|
+
cleanupVirtualEntry,
|
|
7
|
+
finalizeBuildOutputs,
|
|
8
|
+
prepareVirtualEntry,
|
|
9
|
+
} from './entry';
|
|
10
|
+
import {
|
|
11
|
+
cleanupEntryOptionsModule,
|
|
12
|
+
prepareEntryOptionsModule,
|
|
13
|
+
} from '../entry-options';
|
|
14
|
+
|
|
15
|
+
export interface VirtualBuildResult {
|
|
16
|
+
success: boolean;
|
|
17
|
+
hasPages: boolean;
|
|
18
|
+
outputs: { path: string; size: number }[];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export async function runVirtualEntryBuild(options: {
|
|
22
|
+
cwd: string;
|
|
23
|
+
entryFile: string;
|
|
24
|
+
outfile: string;
|
|
25
|
+
target?: string;
|
|
26
|
+
minify?: boolean;
|
|
27
|
+
sourcemap?: string;
|
|
28
|
+
compile?: boolean;
|
|
29
|
+
bytecode?: boolean;
|
|
30
|
+
}): Promise<VirtualBuildResult> {
|
|
31
|
+
const config = await resolveBuildConfig(options.cwd);
|
|
32
|
+
const entryOptions = prepareEntryOptionsModule({
|
|
33
|
+
cwd: options.cwd,
|
|
34
|
+
entryFile: options.entryFile,
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
const [apiEntries, pageEntries] = await Promise.all([
|
|
38
|
+
scanApiRoutes(options.cwd, config.apiDir, config.apiPrefix),
|
|
39
|
+
scanPageRoutes(options.cwd, config.pageDir, config.pagePrefix),
|
|
40
|
+
]);
|
|
41
|
+
|
|
42
|
+
if (apiEntries.length === 0 && pageEntries.length === 0) {
|
|
43
|
+
throw new Error(
|
|
44
|
+
`No routes found. Ensure ${config.apiDir} or ${config.pageDir} ` +
|
|
45
|
+
`exist and contain route.ts files or page files.`
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const source = generateVirtualEntrySource(
|
|
50
|
+
config,
|
|
51
|
+
apiEntries,
|
|
52
|
+
pageEntries,
|
|
53
|
+
entryOptions.importPath
|
|
54
|
+
);
|
|
55
|
+
const hasPages = pageEntries.length > 0;
|
|
56
|
+
const { outDir, virtualPath, virtualSourcePath } = prepareVirtualEntry({
|
|
57
|
+
cwd: options.cwd,
|
|
58
|
+
outfile: options.outfile,
|
|
59
|
+
pageDir: config.pageDir,
|
|
60
|
+
source,
|
|
61
|
+
hasPages,
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
const buildOptions = createBunBuildOptions({
|
|
65
|
+
entryPath: virtualPath,
|
|
66
|
+
outDir,
|
|
67
|
+
cwd: options.cwd,
|
|
68
|
+
outfile: options.outfile,
|
|
69
|
+
target: options.target,
|
|
70
|
+
minify: options.minify,
|
|
71
|
+
sourcemap: options.sourcemap,
|
|
72
|
+
compile: options.compile,
|
|
73
|
+
bytecode: options.bytecode,
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
try {
|
|
77
|
+
const result = await runBunBuildOrThrow(buildOptions);
|
|
78
|
+
const outputs = await finalizeBuildOutputs({
|
|
79
|
+
result,
|
|
80
|
+
cwd: options.cwd,
|
|
81
|
+
outfile: options.outfile,
|
|
82
|
+
outDir,
|
|
83
|
+
compile: options.compile,
|
|
84
|
+
});
|
|
85
|
+
return { success: result.success ?? false, hasPages, outputs };
|
|
86
|
+
} finally {
|
|
87
|
+
cleanupVirtualEntry(virtualSourcePath);
|
|
88
|
+
cleanupEntryOptionsModule(entryOptions.tempFilePath);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'fs';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Read the project name from package.json.
|
|
6
|
+
* Falls back to 'app' if not found or on any error.
|
|
7
|
+
*/
|
|
8
|
+
export function getProjectName(cwd: string = process.cwd()): string {
|
|
9
|
+
try {
|
|
10
|
+
const packageJsonPath = join(cwd, 'package.json');
|
|
11
|
+
if (existsSync(packageJsonPath)) {
|
|
12
|
+
const pkg = JSON.parse(readFileSync(packageJsonPath, 'utf-8')) as {
|
|
13
|
+
name?: string;
|
|
14
|
+
};
|
|
15
|
+
return pkg?.name || 'app';
|
|
16
|
+
}
|
|
17
|
+
} catch {
|
|
18
|
+
// Ignore — use fallback
|
|
19
|
+
}
|
|
20
|
+
return 'app';
|
|
21
|
+
}
|
package/src/utils/config.ts
CHANGED
|
@@ -7,7 +7,9 @@
|
|
|
7
7
|
|
|
8
8
|
import { existsSync } from 'fs';
|
|
9
9
|
import { join } from 'path';
|
|
10
|
+
import { pathToFileURL } from 'url';
|
|
10
11
|
import type { BuildConfig } from '../types/index';
|
|
12
|
+
import { warning } from './logger';
|
|
11
13
|
|
|
12
14
|
const CONVENTION_DEFAULTS: BuildConfig = {
|
|
13
15
|
apiDir: './src/api',
|
|
@@ -41,15 +43,16 @@ export async function resolveBuildConfig(cwd: string): Promise<BuildConfig> {
|
|
|
41
43
|
}
|
|
42
44
|
|
|
43
45
|
try {
|
|
44
|
-
const
|
|
46
|
+
const configUrl = pathToFileURL(configPath).href;
|
|
47
|
+
const mod = await import(configUrl);
|
|
45
48
|
const user = mod.default ?? mod;
|
|
46
49
|
if (!user || typeof user !== 'object') {
|
|
47
50
|
return { ...CONVENTION_DEFAULTS };
|
|
48
51
|
}
|
|
49
52
|
return mergeBuildConfig(CONVENTION_DEFAULTS, user);
|
|
50
53
|
} catch (err) {
|
|
51
|
-
|
|
52
|
-
`
|
|
54
|
+
warning(
|
|
55
|
+
`Could not load ${configPath}: ${err instanceof Error ? err.message : String(err)}. Using convention defaults.`
|
|
53
56
|
);
|
|
54
57
|
return { ...CONVENTION_DEFAULTS };
|
|
55
58
|
}
|
|
@@ -60,8 +63,7 @@ function mergeBuildConfig(
|
|
|
60
63
|
user: Record<string, unknown>
|
|
61
64
|
): BuildConfig {
|
|
62
65
|
return {
|
|
63
|
-
apiDir:
|
|
64
|
-
typeof user.apiDir === 'string' ? user.apiDir : defaults.apiDir,
|
|
66
|
+
apiDir: typeof user.apiDir === 'string' ? user.apiDir : defaults.apiDir,
|
|
65
67
|
pageDir:
|
|
66
68
|
typeof user.pageDir === 'string' ? user.pageDir : defaults.pageDir,
|
|
67
69
|
apiPrefix:
|
|
@@ -72,7 +74,6 @@ function mergeBuildConfig(
|
|
|
72
74
|
typeof user.pagePrefix === 'string'
|
|
73
75
|
? user.pagePrefix
|
|
74
76
|
: defaults.pagePrefix,
|
|
75
|
-
debug:
|
|
76
|
-
typeof user.debug === 'boolean' ? user.debug : defaults.debug,
|
|
77
|
+
debug: typeof user.debug === 'boolean' ? user.debug : defaults.debug,
|
|
77
78
|
};
|
|
78
79
|
}
|
package/src/utils/github.ts
CHANGED
|
@@ -14,17 +14,37 @@ import type { GitHubFile, MiddlewareInfo } from '../types/index';
|
|
|
14
14
|
import { unlinkSync } from 'fs';
|
|
15
15
|
|
|
16
16
|
/**
|
|
17
|
-
* Configuration for GitHub repository
|
|
18
|
-
*
|
|
17
|
+
* Configuration for GitHub repository.
|
|
18
|
+
* Override via env: BURGER_API_REPO_OWNER, BURGER_API_REPO_NAME, BURGER_API_BRANCH.
|
|
19
19
|
*/
|
|
20
|
-
const REPO_OWNER = 'isfhan';
|
|
21
|
-
const REPO_NAME = 'burger-api';
|
|
22
|
-
const BRANCH = 'main';
|
|
20
|
+
const REPO_OWNER = process.env.BURGER_API_REPO_OWNER ?? 'isfhan';
|
|
21
|
+
const REPO_NAME = process.env.BURGER_API_REPO_NAME ?? 'burger-api';
|
|
22
|
+
const BRANCH = process.env.BURGER_API_BRANCH ?? 'main';
|
|
23
23
|
|
|
24
24
|
// Build the URLs we'll use to access GitHub
|
|
25
25
|
const RAW_URL = `https://raw.githubusercontent.com/${REPO_OWNER}/${REPO_NAME}/${BRANCH}`;
|
|
26
26
|
const API_URL = `https://api.github.com/repos/${REPO_OWNER}/${REPO_NAME}`;
|
|
27
27
|
|
|
28
|
+
const FETCH_TIMEOUT_MS = 20_000;
|
|
29
|
+
|
|
30
|
+
function createFetchSignal(): AbortSignal {
|
|
31
|
+
const controller = new AbortController();
|
|
32
|
+
const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
|
|
33
|
+
controller.signal.addEventListener('abort', () => clearTimeout(timer), {
|
|
34
|
+
once: true,
|
|
35
|
+
});
|
|
36
|
+
return controller.signal;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function wrapFetchError(err: unknown, fallbackMessage: string): Error {
|
|
40
|
+
if (err instanceof Error && err.name === 'AbortError') {
|
|
41
|
+
return new Error(
|
|
42
|
+
'Request timed out. Please check your internet connection.'
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
return new Error(err instanceof Error ? err.message : fallbackMessage);
|
|
46
|
+
}
|
|
47
|
+
|
|
28
48
|
/**
|
|
29
49
|
* Get list of available middleware from GitHub
|
|
30
50
|
* This scans the ecosystem/middlewares folder and returns what's available
|
|
@@ -41,9 +61,9 @@ export async function getMiddlewareList(): Promise<string[]> {
|
|
|
41
61
|
const response = await fetch(
|
|
42
62
|
`${API_URL}/contents/ecosystem/middlewares`,
|
|
43
63
|
{
|
|
64
|
+
signal: createFetchSignal(),
|
|
44
65
|
headers: {
|
|
45
66
|
Accept: 'application/vnd.github.v3+json',
|
|
46
|
-
// Add User-Agent to be nice to GitHub
|
|
47
67
|
'User-Agent': 'burger-api-cli',
|
|
48
68
|
},
|
|
49
69
|
}
|
|
@@ -64,8 +84,8 @@ export async function getMiddlewareList(): Promise<string[]> {
|
|
|
64
84
|
.map((f) => f.name)
|
|
65
85
|
.sort();
|
|
66
86
|
} catch (err) {
|
|
67
|
-
|
|
68
|
-
|
|
87
|
+
throw wrapFetchError(
|
|
88
|
+
err,
|
|
69
89
|
'Could not get middleware list from GitHub. Please check your internet connection.'
|
|
70
90
|
);
|
|
71
91
|
}
|
|
@@ -87,6 +107,7 @@ export async function getMiddlewareInfo(name: string): Promise<MiddlewareInfo> {
|
|
|
87
107
|
const response = await fetch(
|
|
88
108
|
`${API_URL}/contents/ecosystem/middlewares/${name}`,
|
|
89
109
|
{
|
|
110
|
+
signal: createFetchSignal(),
|
|
90
111
|
headers: {
|
|
91
112
|
Accept: 'application/vnd.github.v3+json',
|
|
92
113
|
'User-Agent': 'burger-api-cli',
|
|
@@ -108,7 +129,9 @@ export async function getMiddlewareInfo(name: string): Promise<MiddlewareInfo> {
|
|
|
108
129
|
|
|
109
130
|
if (readmeFile && readmeFile.download_url) {
|
|
110
131
|
try {
|
|
111
|
-
const readmeResponse = await fetch(readmeFile.download_url
|
|
132
|
+
const readmeResponse = await fetch(readmeFile.download_url, {
|
|
133
|
+
signal: createFetchSignal(),
|
|
134
|
+
});
|
|
112
135
|
const readmeContent = await readmeResponse.text();
|
|
113
136
|
|
|
114
137
|
// Extract first non-empty line after the title as description
|
|
@@ -132,7 +155,10 @@ export async function getMiddlewareInfo(name: string): Promise<MiddlewareInfo> {
|
|
|
132
155
|
files: files.map((f) => f.name),
|
|
133
156
|
};
|
|
134
157
|
} catch (err) {
|
|
135
|
-
throw
|
|
158
|
+
throw wrapFetchError(
|
|
159
|
+
err,
|
|
160
|
+
`Could not get info for middleware "${name}"`
|
|
161
|
+
);
|
|
136
162
|
}
|
|
137
163
|
}
|
|
138
164
|
|
|
@@ -154,8 +180,9 @@ export async function downloadFile(
|
|
|
154
180
|
// Build the URL to the raw file content
|
|
155
181
|
const url = `${RAW_URL}/${path}`;
|
|
156
182
|
|
|
157
|
-
|
|
158
|
-
|
|
183
|
+
const response = await fetch(url, {
|
|
184
|
+
signal: createFetchSignal(),
|
|
185
|
+
});
|
|
159
186
|
|
|
160
187
|
if (!response.ok) {
|
|
161
188
|
throw new Error(`Could not download ${path}`);
|
|
@@ -168,7 +195,8 @@ export async function downloadFile(
|
|
|
168
195
|
// Bun.write is much faster than Node's fs.writeFile!
|
|
169
196
|
await Bun.write(destination, content);
|
|
170
197
|
} catch (err) {
|
|
171
|
-
throw
|
|
198
|
+
throw wrapFetchError(
|
|
199
|
+
err,
|
|
172
200
|
`Failed to download ${path}: ${
|
|
173
201
|
err instanceof Error ? err.message : 'Unknown error'
|
|
174
202
|
}`
|
|
@@ -246,6 +274,7 @@ export async function middlewareExists(name: string): Promise<boolean> {
|
|
|
246
274
|
const response = await fetch(
|
|
247
275
|
`${API_URL}/contents/ecosystem/middlewares/${name}`,
|
|
248
276
|
{
|
|
277
|
+
signal: createFetchSignal(),
|
|
249
278
|
headers: {
|
|
250
279
|
Accept: 'application/vnd.github.v3+json',
|
|
251
280
|
'User-Agent': 'burger-api-cli',
|
package/src/utils/logger.ts
CHANGED
|
@@ -379,6 +379,27 @@ export function spinner(message: string): Spinner {
|
|
|
379
379
|
return new Spinner(message);
|
|
380
380
|
}
|
|
381
381
|
|
|
382
|
+
/**
|
|
383
|
+
* Run an async command with a spinner. On throw, stops the spinner with error state and rethrows.
|
|
384
|
+
* Use so catch blocks do not need to remember to stop the spinner.
|
|
385
|
+
*
|
|
386
|
+
* @param message - Spinner message
|
|
387
|
+
* @param fn - Async callback receiving the spinner
|
|
388
|
+
* @returns Result of fn
|
|
389
|
+
*/
|
|
390
|
+
export async function withSpinner<T>(
|
|
391
|
+
message: string,
|
|
392
|
+
fn: (spin: Spinner) => Promise<T>
|
|
393
|
+
): Promise<T> {
|
|
394
|
+
const spin = new Spinner(message);
|
|
395
|
+
try {
|
|
396
|
+
return await fn(spin);
|
|
397
|
+
} catch (err) {
|
|
398
|
+
spin.stop(message.replace(/\s*\.\.\.\s*$/, '') + ' failed', true);
|
|
399
|
+
throw err;
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
382
403
|
/**
|
|
383
404
|
* Format a file size in a human-readable way
|
|
384
405
|
* Converts bytes to KB, MB, etc.
|
package/src/utils/templates.ts
CHANGED
|
@@ -30,7 +30,7 @@ export function generatePackageJson(projectName: string): string {
|
|
|
30
30
|
build: 'bun build src/index.ts --outdir ./dist',
|
|
31
31
|
},
|
|
32
32
|
dependencies: {
|
|
33
|
-
'burger-api': '^0.9.
|
|
33
|
+
'burger-api': '^0.9.3',
|
|
34
34
|
},
|
|
35
35
|
devDependencies: {
|
|
36
36
|
'@types/bun': 'latest',
|
|
@@ -962,7 +962,7 @@ export function generateIndexPage(projectName: string): string {
|
|
|
962
962
|
|
|
963
963
|
<!-- Footer -->
|
|
964
964
|
<footer class="footer">
|
|
965
|
-
<div class="version">BurgerAPI v0.9.
|
|
965
|
+
<div class="version">BurgerAPI v0.9.3 • Bun v1.3+</div>
|
|
966
966
|
<div class="social-links">
|
|
967
967
|
<a href="https://github.com/isfhan/burger-api" target="_blank">GitHub</a>
|
|
968
968
|
<a href="https://www.npmjs.com/package/burger-api" target="_blank">NPM</a>
|
|
@@ -1142,7 +1142,12 @@ export async function installDependencies(projectDir: string): Promise<void> {
|
|
|
1142
1142
|
const exitCode = await proc.exited;
|
|
1143
1143
|
|
|
1144
1144
|
if (exitCode !== 0) {
|
|
1145
|
-
|
|
1145
|
+
const stderrText = await new Response(proc.stderr).text();
|
|
1146
|
+
const message =
|
|
1147
|
+
stderrText.trim().length > 0
|
|
1148
|
+
? `bun install failed:\n${stderrText.trim()}`
|
|
1149
|
+
: 'bun install failed';
|
|
1150
|
+
throw new Error(message);
|
|
1146
1151
|
}
|
|
1147
1152
|
|
|
1148
1153
|
spin.stop('Dependencies installed!');
|