@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.
- package/CHANGELOG.md +17 -1
- package/README.md +85 -138
- package/package.json +8 -4
- package/src/commands/build.ts +111 -137
- package/src/commands/create.ts +6 -2
- package/src/index.ts +20 -2
- package/src/types/index.ts +12 -0
- package/src/utils/config.ts +78 -0
- package/src/utils/entry-options.ts +326 -0
- package/src/utils/logger.ts +5 -4
- package/src/utils/route-conventions.ts +142 -0
- package/src/utils/route-methods.ts +91 -0
- package/src/utils/scanner.ts +185 -0
- package/src/utils/templates.ts +41 -4
- package/src/utils/virtual-entry.ts +121 -0
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Build-time route scanner. Discovers route.ts and page files without loading modules.
|
|
3
|
+
* Path conversion rules match the framework (api-router, page-router).
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { readdir } from 'fs/promises';
|
|
7
|
+
import { existsSync } from 'fs';
|
|
8
|
+
import * as path from 'path';
|
|
9
|
+
import { detectExportedMethods } from './route-methods';
|
|
10
|
+
import { ROUTE_CONSTANTS } from './route-conventions';
|
|
11
|
+
import {
|
|
12
|
+
filePathToApiRoutePath,
|
|
13
|
+
filePathToPageRoutePath,
|
|
14
|
+
} from './route-conventions';
|
|
15
|
+
|
|
16
|
+
export interface ApiRouteScanEntry {
|
|
17
|
+
/** Absolute import path used by generated build entry */
|
|
18
|
+
importPath: string;
|
|
19
|
+
/** Route path with prefix (e.g. /api/users/:id) */
|
|
20
|
+
routePath: string;
|
|
21
|
+
isWildcard: boolean;
|
|
22
|
+
/** HTTP methods exported by the route module (set by method detection; omit = emit all) */
|
|
23
|
+
methods?: string[];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface PageRouteScanEntry {
|
|
27
|
+
/** Absolute import path used by generated build entry */
|
|
28
|
+
importPath: string;
|
|
29
|
+
routePath: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Scan apiDir for route.ts files and return entries for codegen.
|
|
34
|
+
* Uses same path/convention rules as framework ApiRouter.
|
|
35
|
+
*/
|
|
36
|
+
export async function scanApiRoutes(
|
|
37
|
+
cwd: string,
|
|
38
|
+
apiDir: string,
|
|
39
|
+
apiPrefix: string
|
|
40
|
+
): Promise<ApiRouteScanEntry[]> {
|
|
41
|
+
const absoluteApiDir = path.resolve(cwd, apiDir);
|
|
42
|
+
if (!existsSync(absoluteApiDir)) {
|
|
43
|
+
return [];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const entries: ApiRouteScanEntry[] = [];
|
|
47
|
+
await scanApiDir(absoluteApiDir, '', apiPrefix, entries);
|
|
48
|
+
return entries;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async function scanApiDir(
|
|
52
|
+
dir: string,
|
|
53
|
+
basePath: string,
|
|
54
|
+
prefix: string,
|
|
55
|
+
out: ApiRouteScanEntry[]
|
|
56
|
+
): Promise<void> {
|
|
57
|
+
let dynamicFolderFound = false;
|
|
58
|
+
let wildcardFolderFound = false;
|
|
59
|
+
|
|
60
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
61
|
+
for (const entry of entries) {
|
|
62
|
+
const entryPath = path.join(dir, entry.name);
|
|
63
|
+
const relativePath = path.join(basePath, entry.name);
|
|
64
|
+
|
|
65
|
+
if (entry.isDirectory()) {
|
|
66
|
+
if (
|
|
67
|
+
entry.name.startsWith(ROUTE_CONSTANTS.WILDCARD_START) &&
|
|
68
|
+
entry.name !== ROUTE_CONSTANTS.WILDCARD_SIMPLE
|
|
69
|
+
) {
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const isDynamic =
|
|
74
|
+
entry.name.startsWith(ROUTE_CONSTANTS.DYNAMIC_FOLDER_START) &&
|
|
75
|
+
entry.name.endsWith(ROUTE_CONSTANTS.DYNAMIC_FOLDER_END) &&
|
|
76
|
+
!entry.name.startsWith(ROUTE_CONSTANTS.WILDCARD_START);
|
|
77
|
+
const isWildcard = entry.name === ROUTE_CONSTANTS.WILDCARD_SIMPLE;
|
|
78
|
+
|
|
79
|
+
if (isDynamic && wildcardFolderFound) {
|
|
80
|
+
throw new Error(
|
|
81
|
+
`Cannot mix dynamic and wildcard route folders. Found dynamic '${entry.name}' but wildcard already exists in '${dir}'.`
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
if (isWildcard && dynamicFolderFound) {
|
|
85
|
+
throw new Error(
|
|
86
|
+
`Cannot mix wildcard and dynamic route folders. Found wildcard '${entry.name}' but dynamic already exists in '${dir}'.`
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
if (isDynamic && dynamicFolderFound) {
|
|
90
|
+
throw new Error(
|
|
91
|
+
`Multiple dynamic route folders in same directory: '${entry.name}' in '${dir}'.`
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
if (isWildcard && wildcardFolderFound) {
|
|
95
|
+
throw new Error(
|
|
96
|
+
`Multiple wildcard route folders in same directory: '${entry.name}' in '${dir}'.`
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
if (isDynamic) dynamicFolderFound = true;
|
|
100
|
+
if (isWildcard) wildcardFolderFound = true;
|
|
101
|
+
|
|
102
|
+
await scanApiDir(entryPath, relativePath, prefix, out);
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (entry.isFile() && entry.name === 'route.ts') {
|
|
107
|
+
const routePath = filePathToApiRoutePath(relativePath, prefix);
|
|
108
|
+
const importPath = entryPath.split(path.sep).join('/');
|
|
109
|
+
const methods = await detectExportedMethods(entryPath);
|
|
110
|
+
const scanEntry: ApiRouteScanEntry = {
|
|
111
|
+
importPath,
|
|
112
|
+
routePath,
|
|
113
|
+
isWildcard: routePath.includes(
|
|
114
|
+
ROUTE_CONSTANTS.WILDCARD_SEGMENT_PREFIX
|
|
115
|
+
),
|
|
116
|
+
};
|
|
117
|
+
if (methods !== undefined) {
|
|
118
|
+
scanEntry.methods = methods;
|
|
119
|
+
}
|
|
120
|
+
out.push(scanEntry);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Scan pageDir for .tsx and .html pages and return entries for codegen.
|
|
127
|
+
*/
|
|
128
|
+
export async function scanPageRoutes(
|
|
129
|
+
cwd: string,
|
|
130
|
+
pageDir: string,
|
|
131
|
+
pagePrefix: string
|
|
132
|
+
): Promise<PageRouteScanEntry[]> {
|
|
133
|
+
const absolutePageDir = path.resolve(cwd, pageDir);
|
|
134
|
+
if (!existsSync(absolutePageDir)) {
|
|
135
|
+
return [];
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const entries: PageRouteScanEntry[] = [];
|
|
139
|
+
await scanPageDir(absolutePageDir, '', pagePrefix, entries);
|
|
140
|
+
return entries;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async function scanPageDir(
|
|
144
|
+
dir: string,
|
|
145
|
+
basePath: string,
|
|
146
|
+
prefix: string,
|
|
147
|
+
out: PageRouteScanEntry[]
|
|
148
|
+
): Promise<void> {
|
|
149
|
+
let dynamicFolderFound = false;
|
|
150
|
+
|
|
151
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
152
|
+
for (const entry of entries) {
|
|
153
|
+
const entryPath = path.join(dir, entry.name);
|
|
154
|
+
const relativePath = path.join(basePath, entry.name);
|
|
155
|
+
|
|
156
|
+
if (entry.isDirectory()) {
|
|
157
|
+
if (entry.name.startsWith(ROUTE_CONSTANTS.WILDCARD_START)) {
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const isDynamic =
|
|
162
|
+
entry.name.startsWith(ROUTE_CONSTANTS.DYNAMIC_FOLDER_START) &&
|
|
163
|
+
entry.name.endsWith(ROUTE_CONSTANTS.DYNAMIC_FOLDER_END);
|
|
164
|
+
if (isDynamic && dynamicFolderFound) {
|
|
165
|
+
throw new Error(
|
|
166
|
+
`Multiple dynamic page folders in same directory: '${entry.name}' in '${dir}'.`
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
if (isDynamic) dynamicFolderFound = true;
|
|
170
|
+
await scanPageDir(entryPath, relativePath, prefix, out);
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (
|
|
175
|
+
entry.isFile() &&
|
|
176
|
+
ROUTE_CONSTANTS.SUPPORTED_PAGE_EXTENSIONS.some((ext) =>
|
|
177
|
+
entry.name.endsWith(ext)
|
|
178
|
+
)
|
|
179
|
+
) {
|
|
180
|
+
const routePath = filePathToPageRoutePath(relativePath, prefix);
|
|
181
|
+
const importPath = entryPath.split(path.sep).join('/');
|
|
182
|
+
out.push({ importPath, routePath });
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
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.
|
|
33
|
+
'burger-api': '^0.9.0',
|
|
34
34
|
},
|
|
35
35
|
devDependencies: {
|
|
36
36
|
'@types/bun': 'latest',
|
|
@@ -181,6 +181,37 @@ export function generateIndexFile(options: CreateOptions): string {
|
|
|
181
181
|
return lines.join('\n');
|
|
182
182
|
}
|
|
183
183
|
|
|
184
|
+
/**
|
|
185
|
+
* Generate burger.config.ts from create command answers.
|
|
186
|
+
* This keeps build/runtime config explicit in scaffolded projects.
|
|
187
|
+
*
|
|
188
|
+
* @param options - Project configuration from user prompts
|
|
189
|
+
* @returns burger.config.ts content as a string
|
|
190
|
+
*/
|
|
191
|
+
export function generateBurgerConfig(options: CreateOptions): string {
|
|
192
|
+
const apiDir = `./src/${options.apiDir || 'api'}`;
|
|
193
|
+
const pageDir = `./src/${options.pageDir || 'pages'}`;
|
|
194
|
+
const apiPrefix = options.apiPrefix || '/api';
|
|
195
|
+
const pagePrefix = options.pagePrefix || '/';
|
|
196
|
+
const debug = Boolean(options.debug);
|
|
197
|
+
|
|
198
|
+
return [
|
|
199
|
+
'/**',
|
|
200
|
+
' * BurgerAPI build and dev config.',
|
|
201
|
+
' * Used by the CLI for build (burger-api build) and by your app if you load it.',
|
|
202
|
+
' * Edit these paths and prefixes to match your project.',
|
|
203
|
+
' */',
|
|
204
|
+
'export default {',
|
|
205
|
+
` apiDir: ${JSON.stringify(apiDir)}, // folder with API route files`,
|
|
206
|
+
` pageDir: ${JSON.stringify(pageDir)}, // folder with HTML pages`,
|
|
207
|
+
` apiPrefix: ${JSON.stringify(apiPrefix)}, // URL prefix for API routes`,
|
|
208
|
+
` pagePrefix: ${JSON.stringify(pagePrefix)}, // URL prefix for pages`,
|
|
209
|
+
` debug: ${debug}, // extra logging when true`,
|
|
210
|
+
'};',
|
|
211
|
+
'',
|
|
212
|
+
].join('\n');
|
|
213
|
+
}
|
|
214
|
+
|
|
184
215
|
/**
|
|
185
216
|
* Generate a comprehensive API route file with full examples
|
|
186
217
|
* Includes: OpenAPI metadata, Zod schemas, all HTTP methods, middleware
|
|
@@ -931,7 +962,7 @@ export function generateIndexPage(projectName: string): string {
|
|
|
931
962
|
|
|
932
963
|
<!-- Footer -->
|
|
933
964
|
<footer class="footer">
|
|
934
|
-
<div class="version">BurgerAPI v0.
|
|
965
|
+
<div class="version">BurgerAPI v0.9.0 • Bun v1.3+</div>
|
|
935
966
|
<div class="social-links">
|
|
936
967
|
<a href="https://github.com/isfhan/burger-api" target="_blank">GitHub</a>
|
|
937
968
|
<a href="https://www.npmjs.com/package/burger-api" target="_blank">NPM</a>
|
|
@@ -962,13 +993,15 @@ export function generateMiddlewareIndex(): string {
|
|
|
962
993
|
* import { cors } from './cors/cors';
|
|
963
994
|
* import { logger } from './logger/logger';
|
|
964
995
|
*
|
|
965
|
-
* export const globalMiddleware = [
|
|
996
|
+
* export const globalMiddleware: Middleware[] = [
|
|
966
997
|
* logger(),
|
|
967
998
|
* cors(),
|
|
968
999
|
* ];
|
|
969
1000
|
*/
|
|
970
1001
|
|
|
971
|
-
|
|
1002
|
+
import type { Middleware } from 'burger-api';
|
|
1003
|
+
|
|
1004
|
+
export const globalMiddleware: Middleware[] = [];
|
|
972
1005
|
`;
|
|
973
1006
|
}
|
|
974
1007
|
|
|
@@ -1026,6 +1059,10 @@ export async function createProject(
|
|
|
1026
1059
|
join(targetDir, '.prettierrc'),
|
|
1027
1060
|
generatePrettierConfig()
|
|
1028
1061
|
);
|
|
1062
|
+
await Bun.write(
|
|
1063
|
+
join(targetDir, 'burger.config.ts'),
|
|
1064
|
+
generateBurgerConfig(options)
|
|
1065
|
+
);
|
|
1029
1066
|
|
|
1030
1067
|
// Create src directory and index file
|
|
1031
1068
|
await Bun.write(
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generate the in-memory virtual entrypoint source for Bun.build().
|
|
3
|
+
* Uses static imports so the bundler traces and embeds all route modules.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { BuildConfig } from '../types/index';
|
|
7
|
+
import type { ApiRouteScanEntry, PageRouteScanEntry } from './scanner';
|
|
8
|
+
|
|
9
|
+
/** Default methods to emit when entry has no methods; excludes OPTIONS so we add 204 only when hasPreflight. */
|
|
10
|
+
const DEFAULT_EMIT_METHODS = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD'];
|
|
11
|
+
|
|
12
|
+
const PREFLIGHT_METHODS = ['POST', 'PUT', 'DELETE', 'PATCH'];
|
|
13
|
+
|
|
14
|
+
function methodsToEmit(entry: ApiRouteScanEntry): string[] {
|
|
15
|
+
if (entry.methods && entry.methods.length > 0) {
|
|
16
|
+
return entry.methods;
|
|
17
|
+
}
|
|
18
|
+
return [...DEFAULT_EMIT_METHODS];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function pushImportLines(
|
|
22
|
+
lines: string[],
|
|
23
|
+
entries: { importPath: string }[],
|
|
24
|
+
variablePrefix: 'r' | 'p'
|
|
25
|
+
): void {
|
|
26
|
+
entries.forEach((entry, index) => {
|
|
27
|
+
lines.push(
|
|
28
|
+
`import * as _${variablePrefix}${index} from '${entry.importPath}';`
|
|
29
|
+
);
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Generate virtual entrypoint source that imports all routes and starts Burger with apiRoutes/pageRoutes.
|
|
35
|
+
*/
|
|
36
|
+
export function generateVirtualEntrySource(
|
|
37
|
+
config: BuildConfig,
|
|
38
|
+
apiEntries: ApiRouteScanEntry[],
|
|
39
|
+
pageEntries: PageRouteScanEntry[],
|
|
40
|
+
optionsImportPath?: string
|
|
41
|
+
): string {
|
|
42
|
+
const lines: string[] = [];
|
|
43
|
+
|
|
44
|
+
lines.push('// Auto-generated by burger-api build. Do not edit.');
|
|
45
|
+
lines.push("import { Burger } from 'burger-api';");
|
|
46
|
+
if (optionsImportPath) {
|
|
47
|
+
lines.push(
|
|
48
|
+
`import { burgerOptions as __burgerOptions } from '${optionsImportPath}';`
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
lines.push('');
|
|
52
|
+
// Ensure CWD matches the bundle directory so HTML chunk paths resolve correctly
|
|
53
|
+
lines.push('process.chdir(import.meta.dir);');
|
|
54
|
+
lines.push('');
|
|
55
|
+
|
|
56
|
+
pushImportLines(lines, apiEntries, 'r');
|
|
57
|
+
if (pageEntries.length) {
|
|
58
|
+
lines.push('');
|
|
59
|
+
pushImportLines(lines, pageEntries, 'p');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
lines.push('');
|
|
63
|
+
lines.push('const apiRoutes = [');
|
|
64
|
+
|
|
65
|
+
apiEntries.forEach((e, i) => {
|
|
66
|
+
const methods = methodsToEmit(e);
|
|
67
|
+
const hasPreflight = PREFLIGHT_METHODS.some((m) => methods.includes(m));
|
|
68
|
+
|
|
69
|
+
lines.push(' {');
|
|
70
|
+
lines.push(` path: ${JSON.stringify(e.routePath)},`);
|
|
71
|
+
lines.push(' handlers: {');
|
|
72
|
+
for (const m of methods) {
|
|
73
|
+
lines.push(` ${m}: _r${i}.${m},`);
|
|
74
|
+
}
|
|
75
|
+
if (!methods.includes('OPTIONS') && hasPreflight) {
|
|
76
|
+
lines.push(
|
|
77
|
+
` OPTIONS: () => new Response(null, { status: 204 }),`
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
lines.push(' },');
|
|
81
|
+
lines.push(` middleware: _r${i}.middleware,`);
|
|
82
|
+
lines.push(` schema: _r${i}.schema,`);
|
|
83
|
+
lines.push(` openapi: _r${i}.openapi,`);
|
|
84
|
+
lines.push(` isWildcard: ${e.isWildcard},`);
|
|
85
|
+
lines.push(' },');
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
lines.push('];');
|
|
89
|
+
lines.push('');
|
|
90
|
+
|
|
91
|
+
lines.push('const pageRoutes = [');
|
|
92
|
+
pageEntries.forEach((p, i) => {
|
|
93
|
+
lines.push(
|
|
94
|
+
` { path: ${JSON.stringify(p.routePath)}, handler: _p${i}.default },`
|
|
95
|
+
);
|
|
96
|
+
if (p.routePath !== '/' && !p.routePath.endsWith('/')) {
|
|
97
|
+
lines.push(
|
|
98
|
+
` { path: ${JSON.stringify(p.routePath + '/')}, handler: _p${i}.default },`
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
lines.push('];');
|
|
103
|
+
lines.push('');
|
|
104
|
+
|
|
105
|
+
lines.push('const app = new Burger({');
|
|
106
|
+
if (optionsImportPath) {
|
|
107
|
+
lines.push(' ...__burgerOptions,');
|
|
108
|
+
} else {
|
|
109
|
+
lines.push(` debug: ${config.debug === true},`);
|
|
110
|
+
}
|
|
111
|
+
lines.push(' apiRoutes,');
|
|
112
|
+
lines.push(' pageRoutes,');
|
|
113
|
+
lines.push('});');
|
|
114
|
+
lines.push('');
|
|
115
|
+
lines.push('const port = Number(process.env.PORT) || 4000;');
|
|
116
|
+
lines.push('app.serve(port, () => {');
|
|
117
|
+
lines.push(' console.log(`Server running on http://localhost:${port}`);');
|
|
118
|
+
lines.push('});');
|
|
119
|
+
|
|
120
|
+
return lines.join('\n');
|
|
121
|
+
}
|