@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.
Files changed (120) hide show
  1. package/.env.example +109 -0
  2. package/.github/workflows/ci.yml +31 -0
  3. package/LICENSE +21 -0
  4. package/README.md +892 -0
  5. package/architecture.md +652 -0
  6. package/bun.lock +70 -0
  7. package/dist/cli/index.js +3233 -0
  8. package/dist/index.js +9014 -0
  9. package/package.json +77 -0
  10. package/src/cache/index.ts +795 -0
  11. package/src/cli/ARCHITECTURE.md +837 -0
  12. package/src/cli/bin.ts +10 -0
  13. package/src/cli/commands/build.ts +425 -0
  14. package/src/cli/commands/dev.ts +248 -0
  15. package/src/cli/commands/generate.ts +541 -0
  16. package/src/cli/commands/help.ts +55 -0
  17. package/src/cli/commands/index.ts +112 -0
  18. package/src/cli/commands/migration.ts +355 -0
  19. package/src/cli/commands/new.ts +804 -0
  20. package/src/cli/commands/start.ts +208 -0
  21. package/src/cli/core/args.ts +283 -0
  22. package/src/cli/core/console.ts +349 -0
  23. package/src/cli/core/index.ts +60 -0
  24. package/src/cli/core/prompt.ts +424 -0
  25. package/src/cli/core/spinner.ts +265 -0
  26. package/src/cli/index.ts +135 -0
  27. package/src/cli/templates/deploy.ts +295 -0
  28. package/src/cli/templates/docker.ts +307 -0
  29. package/src/cli/templates/index.ts +24 -0
  30. package/src/cli/utils/fs.ts +428 -0
  31. package/src/cli/utils/index.ts +8 -0
  32. package/src/cli/utils/strings.ts +197 -0
  33. package/src/config/env.ts +408 -0
  34. package/src/config/index.ts +506 -0
  35. package/src/config/loader.ts +329 -0
  36. package/src/config/merge.ts +285 -0
  37. package/src/config/types.ts +320 -0
  38. package/src/config/validation.ts +441 -0
  39. package/src/container/forward-ref.ts +143 -0
  40. package/src/container/index.ts +386 -0
  41. package/src/context/index.ts +360 -0
  42. package/src/database/index.ts +1142 -0
  43. package/src/database/migrations/index.ts +371 -0
  44. package/src/database/schema/index.ts +619 -0
  45. package/src/frontend/api-routes.ts +640 -0
  46. package/src/frontend/bundler.ts +643 -0
  47. package/src/frontend/console-client.ts +419 -0
  48. package/src/frontend/console-stream.ts +587 -0
  49. package/src/frontend/dev-server.ts +846 -0
  50. package/src/frontend/file-router.ts +611 -0
  51. package/src/frontend/frameworks/index.ts +106 -0
  52. package/src/frontend/frameworks/react.ts +85 -0
  53. package/src/frontend/frameworks/solid.ts +104 -0
  54. package/src/frontend/frameworks/svelte.ts +110 -0
  55. package/src/frontend/frameworks/vue.ts +92 -0
  56. package/src/frontend/hmr-client.ts +663 -0
  57. package/src/frontend/hmr.ts +728 -0
  58. package/src/frontend/index.ts +342 -0
  59. package/src/frontend/islands.ts +552 -0
  60. package/src/frontend/isr.ts +555 -0
  61. package/src/frontend/layout.ts +475 -0
  62. package/src/frontend/ssr/react.ts +446 -0
  63. package/src/frontend/ssr/solid.ts +523 -0
  64. package/src/frontend/ssr/svelte.ts +546 -0
  65. package/src/frontend/ssr/vue.ts +504 -0
  66. package/src/frontend/ssr.ts +699 -0
  67. package/src/frontend/types.ts +2274 -0
  68. package/src/health/index.ts +604 -0
  69. package/src/index.ts +410 -0
  70. package/src/lock/index.ts +587 -0
  71. package/src/logger/index.ts +444 -0
  72. package/src/logger/transports/index.ts +969 -0
  73. package/src/metrics/index.ts +494 -0
  74. package/src/middleware/built-in.ts +360 -0
  75. package/src/middleware/index.ts +94 -0
  76. package/src/modules/filters.ts +458 -0
  77. package/src/modules/guards.ts +405 -0
  78. package/src/modules/index.ts +1256 -0
  79. package/src/modules/interceptors.ts +574 -0
  80. package/src/modules/lazy.ts +418 -0
  81. package/src/modules/lifecycle.ts +478 -0
  82. package/src/modules/metadata.ts +90 -0
  83. package/src/modules/pipes.ts +626 -0
  84. package/src/router/index.ts +339 -0
  85. package/src/router/linear.ts +371 -0
  86. package/src/router/regex.ts +292 -0
  87. package/src/router/tree.ts +562 -0
  88. package/src/rpc/index.ts +1263 -0
  89. package/src/security/index.ts +436 -0
  90. package/src/ssg/index.ts +631 -0
  91. package/src/storage/index.ts +456 -0
  92. package/src/telemetry/index.ts +1097 -0
  93. package/src/testing/index.ts +1586 -0
  94. package/src/types/index.ts +236 -0
  95. package/src/types/optional-deps.d.ts +219 -0
  96. package/src/validation/index.ts +276 -0
  97. package/src/websocket/index.ts +1004 -0
  98. package/tests/integration/cli.test.ts +1016 -0
  99. package/tests/integration/fullstack.test.ts +234 -0
  100. package/tests/unit/cache.test.ts +174 -0
  101. package/tests/unit/cli-commands.test.ts +892 -0
  102. package/tests/unit/cli.test.ts +1258 -0
  103. package/tests/unit/container.test.ts +279 -0
  104. package/tests/unit/context.test.ts +221 -0
  105. package/tests/unit/database.test.ts +183 -0
  106. package/tests/unit/linear-router.test.ts +280 -0
  107. package/tests/unit/lock.test.ts +336 -0
  108. package/tests/unit/middleware.test.ts +184 -0
  109. package/tests/unit/modules.test.ts +142 -0
  110. package/tests/unit/pubsub.test.ts +257 -0
  111. package/tests/unit/regex-router.test.ts +265 -0
  112. package/tests/unit/router.test.ts +373 -0
  113. package/tests/unit/rpc.test.ts +1248 -0
  114. package/tests/unit/security.test.ts +174 -0
  115. package/tests/unit/telemetry.test.ts +371 -0
  116. package/tests/unit/test-cache.test.ts +110 -0
  117. package/tests/unit/test-database.test.ts +282 -0
  118. package/tests/unit/tree-router.test.ts +325 -0
  119. package/tests/unit/validation.test.ts +794 -0
  120. 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,8 @@
1
+ /**
2
+ * CLI Utilities
3
+ *
4
+ * Re-exports all utility functions
5
+ */
6
+
7
+ export * from './strings';
8
+ export * from './fs';
@@ -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
+ }