@buenojs/bueno 0.8.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.
- package/.env.example +109 -0
- package/.github/workflows/ci.yml +31 -0
- package/LICENSE +21 -0
- package/README.md +892 -0
- package/architecture.md +652 -0
- package/bun.lock +70 -0
- package/dist/cli/index.js +3233 -0
- package/dist/index.js +9014 -0
- package/package.json +77 -0
- package/src/cache/index.ts +795 -0
- package/src/cli/ARCHITECTURE.md +837 -0
- package/src/cli/bin.ts +10 -0
- package/src/cli/commands/build.ts +425 -0
- package/src/cli/commands/dev.ts +248 -0
- package/src/cli/commands/generate.ts +541 -0
- package/src/cli/commands/help.ts +55 -0
- package/src/cli/commands/index.ts +112 -0
- package/src/cli/commands/migration.ts +355 -0
- package/src/cli/commands/new.ts +804 -0
- package/src/cli/commands/start.ts +208 -0
- package/src/cli/core/args.ts +283 -0
- package/src/cli/core/console.ts +349 -0
- package/src/cli/core/index.ts +60 -0
- package/src/cli/core/prompt.ts +424 -0
- package/src/cli/core/spinner.ts +265 -0
- package/src/cli/index.ts +135 -0
- package/src/cli/templates/deploy.ts +295 -0
- package/src/cli/templates/docker.ts +307 -0
- package/src/cli/templates/index.ts +24 -0
- package/src/cli/utils/fs.ts +428 -0
- package/src/cli/utils/index.ts +8 -0
- package/src/cli/utils/strings.ts +197 -0
- package/src/config/env.ts +408 -0
- package/src/config/index.ts +506 -0
- package/src/config/loader.ts +329 -0
- package/src/config/merge.ts +285 -0
- package/src/config/types.ts +320 -0
- package/src/config/validation.ts +441 -0
- package/src/container/forward-ref.ts +143 -0
- package/src/container/index.ts +386 -0
- package/src/context/index.ts +360 -0
- package/src/database/index.ts +1142 -0
- package/src/database/migrations/index.ts +371 -0
- package/src/database/schema/index.ts +619 -0
- package/src/frontend/api-routes.ts +640 -0
- package/src/frontend/bundler.ts +643 -0
- package/src/frontend/console-client.ts +419 -0
- package/src/frontend/console-stream.ts +587 -0
- package/src/frontend/dev-server.ts +846 -0
- package/src/frontend/file-router.ts +611 -0
- package/src/frontend/frameworks/index.ts +106 -0
- package/src/frontend/frameworks/react.ts +85 -0
- package/src/frontend/frameworks/solid.ts +104 -0
- package/src/frontend/frameworks/svelte.ts +110 -0
- package/src/frontend/frameworks/vue.ts +92 -0
- package/src/frontend/hmr-client.ts +663 -0
- package/src/frontend/hmr.ts +728 -0
- package/src/frontend/index.ts +342 -0
- package/src/frontend/islands.ts +552 -0
- package/src/frontend/isr.ts +555 -0
- package/src/frontend/layout.ts +475 -0
- package/src/frontend/ssr/react.ts +446 -0
- package/src/frontend/ssr/solid.ts +523 -0
- package/src/frontend/ssr/svelte.ts +546 -0
- package/src/frontend/ssr/vue.ts +504 -0
- package/src/frontend/ssr.ts +699 -0
- package/src/frontend/types.ts +2274 -0
- package/src/health/index.ts +604 -0
- package/src/index.ts +410 -0
- package/src/lock/index.ts +587 -0
- package/src/logger/index.ts +444 -0
- package/src/logger/transports/index.ts +969 -0
- package/src/metrics/index.ts +494 -0
- package/src/middleware/built-in.ts +360 -0
- package/src/middleware/index.ts +94 -0
- package/src/modules/filters.ts +458 -0
- package/src/modules/guards.ts +405 -0
- package/src/modules/index.ts +1256 -0
- package/src/modules/interceptors.ts +574 -0
- package/src/modules/lazy.ts +418 -0
- package/src/modules/lifecycle.ts +478 -0
- package/src/modules/metadata.ts +90 -0
- package/src/modules/pipes.ts +626 -0
- package/src/router/index.ts +339 -0
- package/src/router/linear.ts +371 -0
- package/src/router/regex.ts +292 -0
- package/src/router/tree.ts +562 -0
- package/src/rpc/index.ts +1263 -0
- package/src/security/index.ts +436 -0
- package/src/ssg/index.ts +631 -0
- package/src/storage/index.ts +456 -0
- package/src/telemetry/index.ts +1097 -0
- package/src/testing/index.ts +1586 -0
- package/src/types/index.ts +236 -0
- package/src/types/optional-deps.d.ts +219 -0
- package/src/validation/index.ts +276 -0
- package/src/websocket/index.ts +1004 -0
- package/tests/integration/cli.test.ts +1016 -0
- package/tests/integration/fullstack.test.ts +234 -0
- package/tests/unit/cache.test.ts +174 -0
- package/tests/unit/cli-commands.test.ts +892 -0
- package/tests/unit/cli.test.ts +1258 -0
- package/tests/unit/container.test.ts +279 -0
- package/tests/unit/context.test.ts +221 -0
- package/tests/unit/database.test.ts +183 -0
- package/tests/unit/linear-router.test.ts +280 -0
- package/tests/unit/lock.test.ts +336 -0
- package/tests/unit/middleware.test.ts +184 -0
- package/tests/unit/modules.test.ts +142 -0
- package/tests/unit/pubsub.test.ts +257 -0
- package/tests/unit/regex-router.test.ts +265 -0
- package/tests/unit/router.test.ts +373 -0
- package/tests/unit/rpc.test.ts +1248 -0
- package/tests/unit/security.test.ts +174 -0
- package/tests/unit/telemetry.test.ts +371 -0
- package/tests/unit/test-cache.test.ts +110 -0
- package/tests/unit/test-database.test.ts +282 -0
- package/tests/unit/tree-router.test.ts +325 -0
- package/tests/unit/validation.test.ts +794 -0
- package/tsconfig.json +27 -0
|
@@ -0,0 +1,428 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File System Utilities for Bueno CLI
|
|
3
|
+
*
|
|
4
|
+
* Provides file system operations using Bun's native APIs
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import * as fs from 'fs';
|
|
8
|
+
import * as path from 'path';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Check if a file exists
|
|
12
|
+
*/
|
|
13
|
+
export async function fileExists(filePath: string): Promise<boolean> {
|
|
14
|
+
try {
|
|
15
|
+
return await Bun.file(filePath).exists();
|
|
16
|
+
} catch {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Check if a file exists (sync)
|
|
23
|
+
*/
|
|
24
|
+
export function fileExistsSync(filePath: string): boolean {
|
|
25
|
+
return fs.existsSync(filePath);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Check if a path is a directory
|
|
30
|
+
*/
|
|
31
|
+
export async function isDirectory(dirPath: string): Promise<boolean> {
|
|
32
|
+
try {
|
|
33
|
+
const stat = await fs.promises.stat(dirPath);
|
|
34
|
+
return stat.isDirectory();
|
|
35
|
+
} catch {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Check if a path is a directory (sync)
|
|
42
|
+
*/
|
|
43
|
+
export function isDirectorySync(dirPath: string): boolean {
|
|
44
|
+
try {
|
|
45
|
+
return fs.statSync(dirPath).isDirectory();
|
|
46
|
+
} catch {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Create a directory recursively
|
|
53
|
+
*/
|
|
54
|
+
export async function createDirectory(dirPath: string): Promise<void> {
|
|
55
|
+
await fs.promises.mkdir(dirPath, { recursive: true });
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Create a directory recursively (sync)
|
|
60
|
+
*/
|
|
61
|
+
export function createDirectorySync(dirPath: string): void {
|
|
62
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Read a file as string
|
|
67
|
+
*/
|
|
68
|
+
export async function readFile(filePath: string): Promise<string> {
|
|
69
|
+
return await Bun.file(filePath).text();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Read a file as string (sync)
|
|
74
|
+
*/
|
|
75
|
+
export function readFileSync(filePath: string): string {
|
|
76
|
+
return fs.readFileSync(filePath, 'utf-8');
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Write a file
|
|
81
|
+
*/
|
|
82
|
+
export async function writeFile(
|
|
83
|
+
filePath: string,
|
|
84
|
+
content: string,
|
|
85
|
+
): Promise<void> {
|
|
86
|
+
// Ensure directory exists
|
|
87
|
+
const dir = path.dirname(filePath);
|
|
88
|
+
await createDirectory(dir);
|
|
89
|
+
|
|
90
|
+
await Bun.write(filePath, content);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Write a file (sync)
|
|
95
|
+
*/
|
|
96
|
+
export function writeFileSync(filePath: string, content: string): void {
|
|
97
|
+
// Ensure directory exists
|
|
98
|
+
const dir = path.dirname(filePath);
|
|
99
|
+
createDirectorySync(dir);
|
|
100
|
+
|
|
101
|
+
fs.writeFileSync(filePath, content, 'utf-8');
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Delete a file
|
|
106
|
+
*/
|
|
107
|
+
export async function deleteFile(filePath: string): Promise<void> {
|
|
108
|
+
await fs.promises.unlink(filePath);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Delete a file (sync)
|
|
113
|
+
*/
|
|
114
|
+
export function deleteFileSync(filePath: string): void {
|
|
115
|
+
fs.unlinkSync(filePath);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Delete a directory recursively
|
|
120
|
+
*/
|
|
121
|
+
export async function deleteDirectory(dirPath: string): Promise<void> {
|
|
122
|
+
await fs.promises.rm(dirPath, { recursive: true, force: true });
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Delete a directory recursively (sync)
|
|
127
|
+
*/
|
|
128
|
+
export function deleteDirectorySync(dirPath: string): void {
|
|
129
|
+
fs.rmSync(dirPath, { recursive: true, force: true });
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Copy a file
|
|
134
|
+
*/
|
|
135
|
+
export async function copyFile(
|
|
136
|
+
src: string,
|
|
137
|
+
dest: string,
|
|
138
|
+
): Promise<void> {
|
|
139
|
+
// Ensure destination directory exists
|
|
140
|
+
const dir = path.dirname(dest);
|
|
141
|
+
await createDirectory(dir);
|
|
142
|
+
|
|
143
|
+
await fs.promises.copyFile(src, dest);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Copy a directory recursively
|
|
148
|
+
*/
|
|
149
|
+
export async function copyDirectory(
|
|
150
|
+
src: string,
|
|
151
|
+
dest: string,
|
|
152
|
+
options: { exclude?: string[] } = {},
|
|
153
|
+
): Promise<void> {
|
|
154
|
+
const exclude = options.exclude ?? [];
|
|
155
|
+
|
|
156
|
+
await createDirectory(dest);
|
|
157
|
+
|
|
158
|
+
const entries = await fs.promises.readdir(src, { withFileTypes: true });
|
|
159
|
+
|
|
160
|
+
for (const entry of entries) {
|
|
161
|
+
const srcPath = path.join(src, entry.name);
|
|
162
|
+
const destPath = path.join(dest, entry.name);
|
|
163
|
+
|
|
164
|
+
if (exclude.includes(entry.name)) {
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (entry.isDirectory()) {
|
|
169
|
+
await copyDirectory(srcPath, destPath, options);
|
|
170
|
+
} else if (entry.isFile()) {
|
|
171
|
+
await copyFile(srcPath, destPath);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* List files in a directory
|
|
178
|
+
*/
|
|
179
|
+
export async function listFiles(
|
|
180
|
+
dirPath: string,
|
|
181
|
+
options: { recursive?: boolean; pattern?: RegExp } = {},
|
|
182
|
+
): Promise<string[]> {
|
|
183
|
+
const files: string[] = [];
|
|
184
|
+
|
|
185
|
+
async function walk(dir: string): Promise<void> {
|
|
186
|
+
const entries = await fs.promises.readdir(dir, { withFileTypes: true });
|
|
187
|
+
|
|
188
|
+
for (const entry of entries) {
|
|
189
|
+
const fullPath = path.join(dir, entry.name);
|
|
190
|
+
|
|
191
|
+
if (entry.isDirectory() && options.recursive) {
|
|
192
|
+
await walk(fullPath);
|
|
193
|
+
} else if (entry.isFile()) {
|
|
194
|
+
if (!options.pattern || options.pattern.test(entry.name)) {
|
|
195
|
+
files.push(fullPath);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
await walk(dirPath);
|
|
202
|
+
return files;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Find a file by name in parent directories
|
|
207
|
+
*/
|
|
208
|
+
export async function findFileUp(
|
|
209
|
+
startDir: string,
|
|
210
|
+
fileName: string,
|
|
211
|
+
options: { stopAt?: string } = {},
|
|
212
|
+
): Promise<string | null> {
|
|
213
|
+
let currentDir = startDir;
|
|
214
|
+
const stopAt = options.stopAt ?? '/';
|
|
215
|
+
|
|
216
|
+
while (currentDir !== stopAt && currentDir !== '/') {
|
|
217
|
+
const filePath = path.join(currentDir, fileName);
|
|
218
|
+
if (await fileExists(filePath)) {
|
|
219
|
+
return filePath;
|
|
220
|
+
}
|
|
221
|
+
currentDir = path.dirname(currentDir);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return null;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Get the project root directory
|
|
229
|
+
*/
|
|
230
|
+
export async function getProjectRoot(
|
|
231
|
+
startDir: string = process.cwd(),
|
|
232
|
+
): Promise<string | null> {
|
|
233
|
+
// Look for package.json as indicator
|
|
234
|
+
const packageJsonPath = await findFileUp(startDir, 'package.json');
|
|
235
|
+
if (packageJsonPath) {
|
|
236
|
+
return path.dirname(packageJsonPath);
|
|
237
|
+
}
|
|
238
|
+
return null;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Check if path is inside a Bueno project
|
|
243
|
+
*/
|
|
244
|
+
export async function isBuenoProject(
|
|
245
|
+
dir: string = process.cwd(),
|
|
246
|
+
): Promise<boolean> {
|
|
247
|
+
const root = await getProjectRoot(dir);
|
|
248
|
+
if (!root) return false;
|
|
249
|
+
|
|
250
|
+
// Check for bueno.config.ts or package.json with bueno dependency
|
|
251
|
+
const configPath = path.join(root, 'bueno.config.ts');
|
|
252
|
+
if (await fileExists(configPath)) return true;
|
|
253
|
+
|
|
254
|
+
const packageJsonPath = path.join(root, 'package.json');
|
|
255
|
+
if (await fileExists(packageJsonPath)) {
|
|
256
|
+
const content = await readFile(packageJsonPath);
|
|
257
|
+
try {
|
|
258
|
+
const pkg = JSON.parse(content);
|
|
259
|
+
return !!(pkg.dependencies?.bueno || pkg.devDependencies?.bueno);
|
|
260
|
+
} catch {
|
|
261
|
+
return false;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
return false;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Read JSON file
|
|
270
|
+
*/
|
|
271
|
+
export async function readJson<T = unknown>(filePath: string): Promise<T> {
|
|
272
|
+
const content = await readFile(filePath);
|
|
273
|
+
return JSON.parse(content);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Write JSON file
|
|
278
|
+
*/
|
|
279
|
+
export async function writeJson(
|
|
280
|
+
filePath: string,
|
|
281
|
+
data: unknown,
|
|
282
|
+
options: { pretty?: boolean } = {},
|
|
283
|
+
): Promise<void> {
|
|
284
|
+
const content = options.pretty !== false
|
|
285
|
+
? JSON.stringify(data, null, 2)
|
|
286
|
+
: JSON.stringify(data);
|
|
287
|
+
await writeFile(filePath, content);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Get relative path
|
|
292
|
+
*/
|
|
293
|
+
export function relativePath(from: string, to: string): string {
|
|
294
|
+
return path.relative(from, to);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Join paths
|
|
299
|
+
*/
|
|
300
|
+
export function joinPaths(...paths: string[]): string {
|
|
301
|
+
return path.join(...paths);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Get file name without extension
|
|
306
|
+
*/
|
|
307
|
+
export function getFileName(filePath: string): string {
|
|
308
|
+
return path.basename(filePath, path.extname(filePath));
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Get directory name
|
|
313
|
+
*/
|
|
314
|
+
export function getDirName(filePath: string): string {
|
|
315
|
+
return path.dirname(filePath);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Get file extension
|
|
320
|
+
*/
|
|
321
|
+
export function getExtName(filePath: string): string {
|
|
322
|
+
return path.extname(filePath);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Normalize path separators
|
|
327
|
+
*/
|
|
328
|
+
export function normalizePath(filePath: string): string {
|
|
329
|
+
return filePath.replace(/\\/g, '/');
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Template file processing
|
|
334
|
+
*/
|
|
335
|
+
export interface TemplateData {
|
|
336
|
+
[key: string]: string | number | boolean | TemplateData | TemplateData[];
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Process a template string
|
|
341
|
+
*/
|
|
342
|
+
export function processTemplate(
|
|
343
|
+
template: string,
|
|
344
|
+
data: TemplateData,
|
|
345
|
+
): string {
|
|
346
|
+
let result = template;
|
|
347
|
+
|
|
348
|
+
// Process conditionals: {{#if key}}...{{/if}}
|
|
349
|
+
result = result.replace(
|
|
350
|
+
/\{\{#if\s+(\w+)\}\}([\s\S]*?)\{\{\/if\}\}/g,
|
|
351
|
+
(_, key: string, content: string) => {
|
|
352
|
+
const value = data[key];
|
|
353
|
+
return value ? content : '';
|
|
354
|
+
},
|
|
355
|
+
);
|
|
356
|
+
|
|
357
|
+
// Process each loops: {{#each items}}...{{/each}}
|
|
358
|
+
result = result.replace(
|
|
359
|
+
/\{\{#each\s+(\w+)\}\}([\s\S]*?)\{\{\/each\}\}/g,
|
|
360
|
+
(_, key: string, content: string) => {
|
|
361
|
+
const items = data[key];
|
|
362
|
+
if (!Array.isArray(items)) return '';
|
|
363
|
+
|
|
364
|
+
return items
|
|
365
|
+
.map((item) => {
|
|
366
|
+
let itemContent = content;
|
|
367
|
+
if (typeof item === 'object' && item !== null) {
|
|
368
|
+
// Replace nested properties
|
|
369
|
+
for (const [k, v] of Object.entries(item)) {
|
|
370
|
+
itemContent = itemContent.replace(
|
|
371
|
+
new RegExp(`\\{\\{${k}\\}\\}`, 'g'),
|
|
372
|
+
String(v),
|
|
373
|
+
);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
return itemContent;
|
|
377
|
+
})
|
|
378
|
+
.join('');
|
|
379
|
+
},
|
|
380
|
+
);
|
|
381
|
+
|
|
382
|
+
// Process simple variables with helpers: {{helperName key}}
|
|
383
|
+
const helpers: Record<string, (v: string) => string> = {
|
|
384
|
+
camelCase: (v) =>
|
|
385
|
+
v.replace(/[-_\s]+(.)?/g, (_, c) => (c ? c.toUpperCase() : '')).replace(/^(.)/, (c) => c.toLowerCase()),
|
|
386
|
+
pascalCase: (v) =>
|
|
387
|
+
v.replace(/[-_\s]+(.)?/g, (_, c) => (c ? c.toUpperCase() : '')).replace(/^(.)/, (c) => c.toUpperCase()),
|
|
388
|
+
kebabCase: (v) =>
|
|
389
|
+
v.replace(/([a-z])([A-Z])/g, '$1-$2').replace(/[-_\s]+/g, '-').toLowerCase(),
|
|
390
|
+
snakeCase: (v) =>
|
|
391
|
+
v.replace(/([a-z])([A-Z])/g, '$1_$2').replace(/[-\s]+/g, '_').toLowerCase(),
|
|
392
|
+
upperCase: (v) => v.toUpperCase(),
|
|
393
|
+
lowerCase: (v) => v.toLowerCase(),
|
|
394
|
+
capitalize: (v) => v.charAt(0).toUpperCase() + v.slice(1),
|
|
395
|
+
pluralize: (v) => {
|
|
396
|
+
if (v.endsWith('y') && !['ay', 'ey', 'iy', 'oy', 'uy'].some((e) => v.endsWith(e))) {
|
|
397
|
+
return v.slice(0, -1) + 'ies';
|
|
398
|
+
}
|
|
399
|
+
if (v.endsWith('s') || v.endsWith('x') || v.endsWith('z') || v.endsWith('ch') || v.endsWith('sh')) {
|
|
400
|
+
return v + 'es';
|
|
401
|
+
}
|
|
402
|
+
return v + 's';
|
|
403
|
+
},
|
|
404
|
+
};
|
|
405
|
+
|
|
406
|
+
for (const [helperName, helperFn] of Object.entries(helpers)) {
|
|
407
|
+
const regex = new RegExp(`\\{\\{${helperName}\\s+(\\w+)\\}\\}`, 'g');
|
|
408
|
+
result = result.replace(regex, (_, key: string) => {
|
|
409
|
+
const value = data[key];
|
|
410
|
+
if (typeof value === 'string') {
|
|
411
|
+
return helperFn(value);
|
|
412
|
+
}
|
|
413
|
+
return String(value);
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// Process simple variables: {{key}}
|
|
418
|
+
for (const [key, value] of Object.entries(data)) {
|
|
419
|
+
const regex = new RegExp(`\\{\\{${key}\\}\\}`, 'g');
|
|
420
|
+
result = result.replace(regex, String(value));
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// Clean up empty lines left by conditionals
|
|
424
|
+
result = result.replace(/^\s*\n/gm, '\n');
|
|
425
|
+
result = result.replace(/\n{3,}/g, '\n\n');
|
|
426
|
+
|
|
427
|
+
return result.trim();
|
|
428
|
+
}
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* String Utility Functions for Bueno CLI
|
|
3
|
+
*
|
|
4
|
+
* Provides string transformation helpers for code generation
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Convert string to camelCase
|
|
9
|
+
*/
|
|
10
|
+
export function camelCase(str: string): string {
|
|
11
|
+
return str
|
|
12
|
+
.replace(/[-_\s]+(.)?/g, (_, c) => (c ? c.toUpperCase() : ''))
|
|
13
|
+
.replace(/^(.)/, (c) => c.toLowerCase());
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Convert string to PascalCase
|
|
18
|
+
*/
|
|
19
|
+
export function pascalCase(str: string): string {
|
|
20
|
+
return str
|
|
21
|
+
.replace(/[-_\s]+(.)?/g, (_, c) => (c ? c.toUpperCase() : ''))
|
|
22
|
+
.replace(/^(.)/, (c) => c.toUpperCase());
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Convert string to kebab-case
|
|
27
|
+
*/
|
|
28
|
+
export function kebabCase(str: string): string {
|
|
29
|
+
return str
|
|
30
|
+
.replace(/([a-z])([A-Z])/g, '$1-$2')
|
|
31
|
+
.replace(/[-_\s]+/g, '-')
|
|
32
|
+
.toLowerCase();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Convert string to snake_case
|
|
37
|
+
*/
|
|
38
|
+
export function snakeCase(str: string): string {
|
|
39
|
+
return str
|
|
40
|
+
.replace(/([a-z])([A-Z])/g, '$1_$2')
|
|
41
|
+
.replace(/[-\s]+/g, '_')
|
|
42
|
+
.toLowerCase();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Convert string to UPPER_CASE
|
|
47
|
+
*/
|
|
48
|
+
export function upperCase(str: string): string {
|
|
49
|
+
return snakeCase(str).toUpperCase();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Convert string to lower_case
|
|
54
|
+
*/
|
|
55
|
+
export function lowerCase(str: string): string {
|
|
56
|
+
return snakeCase(str).toLowerCase();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Capitalize first letter
|
|
61
|
+
*/
|
|
62
|
+
export function capitalize(str: string): string {
|
|
63
|
+
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Pluralize a word (simple implementation)
|
|
68
|
+
*/
|
|
69
|
+
export function pluralize(word: string): string {
|
|
70
|
+
if (word.endsWith('y') && !['ay', 'ey', 'iy', 'oy', 'uy'].some((e) => word.endsWith(e))) {
|
|
71
|
+
return word.slice(0, -1) + 'ies';
|
|
72
|
+
}
|
|
73
|
+
if (word.endsWith('s') || word.endsWith('x') || word.endsWith('z') || word.endsWith('ch') || word.endsWith('sh')) {
|
|
74
|
+
return word + 'es';
|
|
75
|
+
}
|
|
76
|
+
return word + 's';
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Singularize a word (simple implementation)
|
|
81
|
+
*/
|
|
82
|
+
export function singularize(word: string): string {
|
|
83
|
+
if (word.endsWith('ies')) {
|
|
84
|
+
return word.slice(0, -3) + 'y';
|
|
85
|
+
}
|
|
86
|
+
if (word.endsWith('es')) {
|
|
87
|
+
// Check for s, x, z, ch, sh endings
|
|
88
|
+
const withoutEs = word.slice(0, -2);
|
|
89
|
+
if (withoutEs.endsWith('s') || withoutEs.endsWith('x') || withoutEs.endsWith('z') ||
|
|
90
|
+
withoutEs.endsWith('ch') || withoutEs.endsWith('sh')) {
|
|
91
|
+
return withoutEs;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
if (word.endsWith('s') && !word.endsWith('ss')) {
|
|
95
|
+
return word.slice(0, -1);
|
|
96
|
+
}
|
|
97
|
+
return word;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Check if string is a valid identifier
|
|
102
|
+
*/
|
|
103
|
+
export function isValidIdentifier(str: string): boolean {
|
|
104
|
+
return /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(str);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Check if string is a valid file name
|
|
109
|
+
*/
|
|
110
|
+
export function isValidFileName(str: string): boolean {
|
|
111
|
+
return !/[<>:"/\\|?*\x00-\x1f]/.test(str);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Truncate string with ellipsis
|
|
116
|
+
*/
|
|
117
|
+
export function truncate(str: string, maxLength: number): string {
|
|
118
|
+
if (str.length <= maxLength) return str;
|
|
119
|
+
return str.slice(0, maxLength - 3) + '...';
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Pad string to center
|
|
124
|
+
*/
|
|
125
|
+
export function padCenter(str: string, length: number, char = ' '): string {
|
|
126
|
+
const padding = length - str.length;
|
|
127
|
+
if (padding <= 0) return str;
|
|
128
|
+
const left = Math.floor(padding / 2);
|
|
129
|
+
const right = padding - left;
|
|
130
|
+
return char.repeat(left) + str + char.repeat(right);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Remove file extension
|
|
135
|
+
*/
|
|
136
|
+
export function removeExtension(filename: string): string {
|
|
137
|
+
const lastDot = filename.lastIndexOf('.');
|
|
138
|
+
if (lastDot === -1 || lastDot === 0) return filename;
|
|
139
|
+
return filename.slice(0, lastDot);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Get file extension
|
|
144
|
+
*/
|
|
145
|
+
export function getExtension(filename: string): string {
|
|
146
|
+
const lastDot = filename.lastIndexOf('.');
|
|
147
|
+
if (lastDot === -1 || lastDot === 0) return '';
|
|
148
|
+
return filename.slice(lastDot + 1);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Generate a unique ID
|
|
153
|
+
*/
|
|
154
|
+
export function generateId(length = 8): string {
|
|
155
|
+
const chars = 'abcdefghijklmnopqrstuvwxyz0123456789';
|
|
156
|
+
let result = '';
|
|
157
|
+
for (let i = 0; i < length; i++) {
|
|
158
|
+
result += chars.charAt(Math.floor(Math.random() * chars.length));
|
|
159
|
+
}
|
|
160
|
+
return result;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Escape string for use in template literals
|
|
165
|
+
*/
|
|
166
|
+
export function escapeTemplateString(str: string): string {
|
|
167
|
+
return str.replace(/[`\\$]/g, '\\$&');
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Escape string for use in regular expressions
|
|
172
|
+
*/
|
|
173
|
+
export function escapeRegExp(str: string): string {
|
|
174
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Indent a multiline string
|
|
179
|
+
*/
|
|
180
|
+
export function indent(str: string, spaces = 2): string {
|
|
181
|
+
const indentation = ' '.repeat(spaces);
|
|
182
|
+
return str
|
|
183
|
+
.split('\n')
|
|
184
|
+
.map((line) => (line.trim() ? indentation + line : line))
|
|
185
|
+
.join('\n');
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Strip leading/trailing whitespace from each line
|
|
190
|
+
*/
|
|
191
|
+
export function stripLines(str: string): string {
|
|
192
|
+
return str
|
|
193
|
+
.split('\n')
|
|
194
|
+
.map((line) => line.trim())
|
|
195
|
+
.join('\n')
|
|
196
|
+
.replace(/\n{3,}/g, '\n\n');
|
|
197
|
+
}
|