bertui 1.2.8 → 2.0.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 (182) hide show
  1. package/README.md +45 -196
  2. package/TYPES_PATCH.md +17 -0
  3. package/bin/bertui.js +2 -7
  4. package/package.json +32 -98
  5. package/src/config.ts +4 -0
  6. package/src/index.ts +32 -0
  7. package/src/optional.ts +49 -0
  8. package/src/router.ts +3 -0
  9. package/tsconfig.json +29 -0
  10. package/LICENSE +0 -21
  11. package/index.js +0 -103
  12. package/src/analyzer/index.js +0 -370
  13. package/src/build/compiler/file-transpiler.js +0 -216
  14. package/src/build/compiler/index.js +0 -31
  15. package/src/build/compiler/route-discoverer.js +0 -49
  16. package/src/build/compiler/router-generator.js +0 -105
  17. package/src/build/css-builder.js +0 -81
  18. package/src/build/generators/html-generator.js +0 -151
  19. package/src/build/generators/robots-generator.js +0 -58
  20. package/src/build/generators/sitemap-generator.js +0 -63
  21. package/src/build/image-optimizer.js +0 -137
  22. package/src/build/processors/asset-processor.js +0 -19
  23. package/src/build/processors/css-builder.js +0 -142
  24. package/src/build/server-island-validator.js +0 -12
  25. package/src/build.js +0 -266
  26. package/src/cli.js +0 -131
  27. package/src/client/compiler.js +0 -522
  28. package/src/client/fast-refresh.js +0 -72
  29. package/src/client/hmr-runtime.js +0 -59
  30. package/src/compiler/index.js +0 -25
  31. package/src/compiler/router-generator-pure.js +0 -104
  32. package/src/compiler/transform.js +0 -149
  33. package/src/config/defaultConfig.js +0 -37
  34. package/src/config/index.js +0 -2
  35. package/src/config/loadConfig.js +0 -64
  36. package/src/config/og-image.png +0 -0
  37. package/src/css/index.js +0 -46
  38. package/src/css/processor.js +0 -172
  39. package/src/dev.js +0 -68
  40. package/src/hydration/index.js +0 -151
  41. package/src/image-optimizer/index.js +0 -103
  42. package/src/images/index.js +0 -102
  43. package/src/images/processor.js +0 -169
  44. package/src/layouts/index.js +0 -165
  45. package/src/loading/index.js +0 -210
  46. package/src/logger/logger.js +0 -320
  47. package/src/logger/notes.md +0 -20
  48. package/src/middleware/index.js +0 -182
  49. package/src/router/Router.js +0 -150
  50. package/src/router/SSRRouter.js +0 -156
  51. package/src/router/index.js +0 -3
  52. package/src/scaffolder/index.js +0 -310
  53. package/src/serve.js +0 -193
  54. package/src/server/dev-handler.js +0 -195
  55. package/src/server/dev-server-utils.js +0 -406
  56. package/src/server/dev-server.js +0 -15
  57. package/src/server/hmr-handler.js +0 -148
  58. package/src/server/index.js +0 -3
  59. package/src/server/notes.md +0 -1
  60. package/src/server/request-handler.js +0 -36
  61. package/src/server-islands/extractor.js +0 -198
  62. package/src/server-islands/index.js +0 -59
  63. package/src/styles/bertui.css +0 -210
  64. package/src/utils/cache.js +0 -297
  65. package/src/utils/env.js +0 -87
  66. package/src/utils/importhow.js +0 -52
  67. package/src/utils/index.js +0 -11
  68. package/src/utils/meta-extractor.js +0 -127
  69. package/types/bin/bertui.d.ts +0 -3
  70. package/types/bin/bertui.d.ts.map +0 -1
  71. package/types/error-overlay.d.ts +0 -2
  72. package/types/error-overlay.d.ts.map +0 -1
  73. package/types/index.d.ts +0 -26
  74. package/types/index.d.ts.map +0 -1
  75. package/types/scripts/fix-wasm-exports.d.ts +0 -2
  76. package/types/scripts/fix-wasm-exports.d.ts.map +0 -1
  77. package/types/src/analyzer/index.d.ts +0 -8
  78. package/types/src/analyzer/index.d.ts.map +0 -1
  79. package/types/src/build/compiler/file-transpiler.d.ts +0 -5
  80. package/types/src/build/compiler/file-transpiler.d.ts.map +0 -1
  81. package/types/src/build/compiler/index.d.ts +0 -12
  82. package/types/src/build/compiler/index.d.ts.map +0 -1
  83. package/types/src/build/compiler/route-discoverer.d.ts +0 -2
  84. package/types/src/build/compiler/route-discoverer.d.ts.map +0 -1
  85. package/types/src/build/compiler/router-generator.d.ts +0 -2
  86. package/types/src/build/compiler/router-generator.d.ts.map +0 -1
  87. package/types/src/build/css-builder.d.ts +0 -18
  88. package/types/src/build/css-builder.d.ts.map +0 -1
  89. package/types/src/build/generators/html-generator.d.ts +0 -2
  90. package/types/src/build/generators/html-generator.d.ts.map +0 -1
  91. package/types/src/build/generators/robots-generator.d.ts +0 -11
  92. package/types/src/build/generators/robots-generator.d.ts.map +0 -1
  93. package/types/src/build/generators/sitemap-generator.d.ts +0 -5
  94. package/types/src/build/generators/sitemap-generator.d.ts.map +0 -1
  95. package/types/src/build/image-optimizer.d.ts +0 -11
  96. package/types/src/build/image-optimizer.d.ts.map +0 -1
  97. package/types/src/build/processors/asset-processor.d.ts +0 -2
  98. package/types/src/build/processors/asset-processor.d.ts.map +0 -1
  99. package/types/src/build/processors/css-builder.d.ts +0 -2
  100. package/types/src/build/processors/css-builder.d.ts.map +0 -1
  101. package/types/src/build/server-island-validator.d.ts +0 -27
  102. package/types/src/build/server-island-validator.d.ts.map +0 -1
  103. package/types/src/build.d.ts +0 -5
  104. package/types/src/build.d.ts.map +0 -1
  105. package/types/src/cli.d.ts +0 -2
  106. package/types/src/cli.d.ts.map +0 -1
  107. package/types/src/client/compiler.d.ts +0 -16
  108. package/types/src/client/compiler.d.ts.map +0 -1
  109. package/types/src/client/fast-refresh.d.ts +0 -3
  110. package/types/src/client/fast-refresh.d.ts.map +0 -1
  111. package/types/src/client/hmr-runtime.d.ts +0 -4
  112. package/types/src/client/hmr-runtime.d.ts.map +0 -1
  113. package/types/src/compiler/index.d.ts +0 -8
  114. package/types/src/compiler/index.d.ts.map +0 -1
  115. package/types/src/compiler/router-generator-pure.d.ts +0 -2
  116. package/types/src/compiler/router-generator-pure.d.ts.map +0 -1
  117. package/types/src/compiler/transform.d.ts +0 -36
  118. package/types/src/compiler/transform.d.ts.map +0 -1
  119. package/types/src/config/defaultConfig.d.ts +0 -26
  120. package/types/src/config/defaultConfig.d.ts.map +0 -1
  121. package/types/src/config/index.d.ts +0 -3
  122. package/types/src/config/index.d.ts.map +0 -1
  123. package/types/src/config/loadConfig.d.ts +0 -2
  124. package/types/src/config/loadConfig.d.ts.map +0 -1
  125. package/types/src/css/index.d.ts +0 -6
  126. package/types/src/css/index.d.ts.map +0 -1
  127. package/types/src/css/processor.d.ts +0 -23
  128. package/types/src/css/processor.d.ts.map +0 -1
  129. package/types/src/dev.d.ts +0 -2
  130. package/types/src/dev.d.ts.map +0 -1
  131. package/types/src/hydration/index.d.ts +0 -33
  132. package/types/src/hydration/index.d.ts.map +0 -1
  133. package/types/src/image-optimizer/index.d.ts +0 -24
  134. package/types/src/image-optimizer/index.d.ts.map +0 -1
  135. package/types/src/images/index.d.ts +0 -12
  136. package/types/src/images/index.d.ts.map +0 -1
  137. package/types/src/images/processor.d.ts +0 -30
  138. package/types/src/images/processor.d.ts.map +0 -1
  139. package/types/src/layouts/index.d.ts +0 -28
  140. package/types/src/layouts/index.d.ts.map +0 -1
  141. package/types/src/loading/index.d.ts +0 -28
  142. package/types/src/loading/index.d.ts.map +0 -1
  143. package/types/src/logger/logger.d.ts +0 -30
  144. package/types/src/logger/logger.d.ts.map +0 -1
  145. package/types/src/middleware/index.d.ts +0 -61
  146. package/types/src/middleware/index.d.ts.map +0 -1
  147. package/types/src/router/Router.d.ts +0 -16
  148. package/types/src/router/Router.d.ts.map +0 -1
  149. package/types/src/router/SSRRouter.d.ts +0 -20
  150. package/types/src/router/SSRRouter.d.ts.map +0 -1
  151. package/types/src/router/index.d.ts +0 -3
  152. package/types/src/router/index.d.ts.map +0 -1
  153. package/types/src/scaffolder/index.d.ts +0 -14
  154. package/types/src/scaffolder/index.d.ts.map +0 -1
  155. package/types/src/serve.d.ts +0 -3
  156. package/types/src/serve.d.ts.map +0 -1
  157. package/types/src/server/dev-handler.d.ts +0 -13
  158. package/types/src/server/dev-handler.d.ts.map +0 -1
  159. package/types/src/server/dev-server-utils.d.ts +0 -6
  160. package/types/src/server/dev-server-utils.d.ts.map +0 -1
  161. package/types/src/server/dev-server.d.ts +0 -18
  162. package/types/src/server/dev-server.d.ts.map +0 -1
  163. package/types/src/server/hmr-handler.d.ts +0 -19
  164. package/types/src/server/hmr-handler.d.ts.map +0 -1
  165. package/types/src/server/index.d.ts +0 -4
  166. package/types/src/server/index.d.ts.map +0 -1
  167. package/types/src/server/request-handler.d.ts +0 -19
  168. package/types/src/server/request-handler.d.ts.map +0 -1
  169. package/types/src/server-islands/extractor.d.ts +0 -16
  170. package/types/src/server-islands/extractor.d.ts.map +0 -1
  171. package/types/src/server-islands/index.d.ts +0 -3
  172. package/types/src/server-islands/index.d.ts.map +0 -1
  173. package/types/src/utils/cache.d.ts +0 -52
  174. package/types/src/utils/cache.d.ts.map +0 -1
  175. package/types/src/utils/env.d.ts +0 -20
  176. package/types/src/utils/env.d.ts.map +0 -1
  177. package/types/src/utils/importhow.d.ts +0 -15
  178. package/types/src/utils/importhow.d.ts.map +0 -1
  179. package/types/src/utils/index.d.ts +0 -3
  180. package/types/src/utils/index.d.ts.map +0 -1
  181. package/types/src/utils/meta-extractor.d.ts +0 -13
  182. package/types/src/utils/meta-extractor.d.ts.map +0 -1
@@ -1,151 +0,0 @@
1
- // bertui/src/hydration/index.js
2
- // Partial hydration - only hydrate interactive components, not whole page
3
-
4
- import logger from '../logger/logger.js';
5
-
6
- /**
7
- * Markers that make a component interactive (needs JS hydration)
8
- */
9
- const INTERACTIVE_MARKERS = [
10
- // React hooks
11
- 'useState', 'useEffect', 'useReducer', 'useCallback',
12
- 'useMemo', 'useRef', 'useContext', 'useLayoutEffect',
13
- 'useTransition', 'useDeferredValue', 'useSyncExternalStore',
14
- // Event handlers
15
- 'onClick', 'onChange', 'onSubmit', 'onInput', 'onFocus',
16
- 'onBlur', 'onMouseEnter', 'onMouseLeave', 'onKeyDown',
17
- 'onKeyUp', 'onScroll', 'onDrop', 'onDrag', 'onTouchStart',
18
- // Browser APIs in component body
19
- 'window.', 'document.', 'localStorage.', 'sessionStorage.',
20
- 'navigator.', 'fetch(', 'WebSocket', 'EventSource',
21
- 'setTimeout(', 'setInterval(', 'requestAnimationFrame(',
22
- ];
23
-
24
- /**
25
- * Scan source code to determine if a component needs hydration
26
- */
27
- export function needsHydration(sourceCode) {
28
- for (const marker of INTERACTIVE_MARKERS) {
29
- if (sourceCode.includes(marker)) {
30
- return true;
31
- }
32
- }
33
- return false;
34
- }
35
-
36
- /**
37
- * Get which specific interactive features a component uses
38
- */
39
- export function getInteractiveFeatures(sourceCode) {
40
- const features = [];
41
-
42
- const hooks = ['useState', 'useEffect', 'useReducer', 'useCallback', 'useMemo', 'useRef'];
43
- for (const hook of hooks) {
44
- if (sourceCode.includes(hook)) features.push({ type: 'hook', name: hook });
45
- }
46
-
47
- const events = ['onClick', 'onChange', 'onSubmit', 'onFocus', 'onBlur', 'onKeyDown'];
48
- for (const event of events) {
49
- if (sourceCode.includes(event)) features.push({ type: 'event', name: event });
50
- }
51
-
52
- const apis = ['fetch(', 'WebSocket', 'localStorage.', 'sessionStorage.'];
53
- for (const api of apis) {
54
- if (sourceCode.includes(api)) features.push({ type: 'api', name: api });
55
- }
56
-
57
- return features;
58
- }
59
-
60
- /**
61
- * Analyze all routes and classify them
62
- * Returns: { static: [], interactive: [], mixed: [] }
63
- */
64
- export async function analyzeRoutes(routes) {
65
- const result = { static: [], interactive: [], mixed: [] };
66
-
67
- for (const route of routes) {
68
- try {
69
- const sourceCode = await Bun.file(route.path).text();
70
- const isServerIsland = sourceCode.includes('export const render = "server"');
71
- const interactive = needsHydration(sourceCode);
72
- const features = getInteractiveFeatures(sourceCode);
73
-
74
- const analyzed = {
75
- ...route,
76
- interactive,
77
- isServerIsland,
78
- features,
79
- hydrationMode: isServerIsland
80
- ? 'none'
81
- : interactive
82
- ? 'full'
83
- : 'none',
84
- };
85
-
86
- if (isServerIsland || !interactive) {
87
- result.static.push(analyzed);
88
- } else {
89
- result.interactive.push(analyzed);
90
- }
91
- } catch (err) {
92
- logger.warn(`Could not analyze ${route.route}: ${err.message}`);
93
- result.interactive.push({ ...route, interactive: true, features: [] });
94
- }
95
- }
96
-
97
- return result;
98
- }
99
-
100
- /**
101
- * Generate hydration-aware router that skips JS for static routes
102
- * Key insight: static routes still render HTML, just skip React.hydrate()
103
- */
104
- export function generatePartialHydrationCode(routes, analyzedRoutes) {
105
- const interactivePaths = new Set(
106
- analyzedRoutes.interactive.map(r => r.route)
107
- );
108
-
109
- const imports = routes.map((route, i) => {
110
- const isInteractive = interactivePaths.has(route.route);
111
- const componentName = `Page${i}`;
112
- const importPath = `./pages/${route.file.replace(/\.(jsx|tsx|ts)$/, '.js')}`;
113
-
114
- // Lazy load static routes (they're just HTML, load fast)
115
- // Eager load interactive routes (need JS ready)
116
- return isInteractive
117
- ? `import ${componentName} from '${importPath}';`
118
- : `const ${componentName} = React.lazy(() => import('${importPath}'));`;
119
- }).join('\n');
120
-
121
- const routeConfigs = routes.map((route, i) => {
122
- const isInteractive = interactivePaths.has(route.route);
123
- return ` {
124
- path: '${route.route}',
125
- component: Page${i},
126
- type: '${route.type}',
127
- hydrate: ${isInteractive},
128
- lazy: ${!isInteractive}
129
- }`;
130
- }).join(',\n');
131
-
132
- return { imports, routeConfigs };
133
- }
134
-
135
- /**
136
- * Log hydration analysis results
137
- */
138
- export function logHydrationReport(analyzedRoutes) {
139
- const { static: staticRoutes, interactive } = analyzedRoutes;
140
-
141
- logger.bigLog('HYDRATION ANALYSIS', { color: 'cyan' });
142
- logger.info(`⚡ Interactive (needs JS): ${interactive.length} routes`);
143
- logger.info(`🏝️ Static (no JS needed): ${staticRoutes.length} routes`);
144
-
145
- if (interactive.length > 0) {
146
- logger.table(interactive.map(r => ({
147
- route: r.route,
148
- features: r.features.map(f => f.name).join(', ').substring(0, 40) || 'unknown',
149
- })));
150
- }
151
- }
@@ -1,103 +0,0 @@
1
- // 3. src/image-optimizer/index.js - USE OXIPNG BINARY
2
- import { exec } from 'child_process';
3
- import { promisify } from 'util';
4
- import { join } from 'path';
5
- import { tmpdir } from 'os';
6
- import { writeFile, readFile, unlink } from 'fs/promises';
7
-
8
- const execAsync = promisify(exec);
9
-
10
- export async function optimizeImage(buffer, options = {}) {
11
- const { format = 'auto', quality = 3 } = options;
12
-
13
- // Detect format
14
- const detectedFormat = format === 'auto' ? detectFormat(buffer) : format;
15
-
16
- // Only PNG supported for now
17
- if (detectedFormat === 'png') {
18
- return await optimizePNG(buffer, quality);
19
- }
20
-
21
- // Fallback to copy
22
- return fallbackOptimize(buffer, detectedFormat);
23
- }
24
-
25
- async function optimizePNG(buffer, level = 3) {
26
- const original_size = buffer.byteLength;
27
-
28
- // Write temp file
29
- const tmpPath = join(tmpdir(), `bertui-${Date.now()}.png`);
30
- const outPath = join(tmpdir(), `bertui-${Date.now()}-opt.png`);
31
-
32
- try {
33
- await writeFile(tmpPath, Buffer.from(buffer));
34
-
35
- // Run oxipng binary
36
- await execAsync(`oxipng -o ${level} -s "${tmpPath}" -o "${outPath}"`);
37
-
38
- // Read optimized file
39
- const optimized = await readFile(outPath);
40
- const optimized_size = optimized.length;
41
- const savings_percent = ((original_size - optimized_size) / original_size * 100).toFixed(1);
42
-
43
- return {
44
- data: optimized.buffer,
45
- original_size,
46
- optimized_size,
47
- format: 'png',
48
- savings_percent: parseFloat(savings_percent)
49
- };
50
- } finally {
51
- // Cleanup
52
- await unlink(tmpPath).catch(() => {});
53
- await unlink(outPath).catch(() => {});
54
- }
55
- }
56
-
57
- function fallbackOptimize(buffer, format) {
58
- return {
59
- data: buffer,
60
- original_size: buffer.byteLength,
61
- optimized_size: buffer.byteLength,
62
- format,
63
- savings_percent: 0
64
- };
65
- }
66
-
67
- function detectFormat(buffer) {
68
- if (buffer.length >= 8) {
69
- const pngSig = [0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A];
70
- if (pngSig.every((b, i) => buffer[i] === b)) return 'png';
71
- }
72
- return 'unknown';
73
- }
74
-
75
- // Batch optimization function
76
- export async function optimizeImagesBatch(images, format = 'auto', options = {}) {
77
- const results = [];
78
-
79
- for (const image of images) {
80
- try {
81
- const result = await optimizeImage(image.buffer || image, {
82
- format,
83
- ...options
84
- });
85
-
86
- results.push({
87
- ...result,
88
- filename: image.filename || image.name || 'unknown'
89
- });
90
- } catch (error) {
91
- results.push({
92
- filename: image.filename || image.name || 'unknown',
93
- error: error.message,
94
- success: false
95
- });
96
- }
97
- }
98
-
99
- return results;
100
- }
101
-
102
- export const hasWasm = () => false;
103
- export const version = '1.1.7';
@@ -1,102 +0,0 @@
1
- // bertui/src/images/index.js
2
- // PURE image handling - NO WASM, NO SHARP, just copy (FALLBACK)
3
-
4
- import { join, extname } from 'path';
5
- import { cpSync, mkdirSync, existsSync, readdirSync } from 'fs';
6
- import logger from '../logger/logger.js';
7
-
8
- export const IMAGE_EXTENSIONS = [
9
- '.png', '.jpg', '.jpeg', '.webp', '.gif', '.svg',
10
- '.avif', '.ico', '.bmp', '.tiff', '.tif'
11
- ];
12
-
13
- export function isImageFile(filename) {
14
- const ext = extname(filename).toLowerCase();
15
- return IMAGE_EXTENSIONS.includes(ext);
16
- }
17
-
18
- export function copyImagesSync(srcDir, destDir) {
19
- let copied = 0;
20
- let skipped = 0;
21
-
22
- if (!existsSync(srcDir)) return { copied, skipped };
23
-
24
- mkdirSync(destDir, { recursive: true });
25
-
26
- function process(dir, targetDir) {
27
- const entries = readdirSync(dir, { withFileTypes: true });
28
-
29
- for (const entry of entries) {
30
- const srcPath = join(dir, entry.name);
31
- const destPath = join(targetDir, entry.name);
32
-
33
- if (entry.isDirectory()) {
34
- const subDest = join(targetDir, entry.name);
35
- mkdirSync(subDest, { recursive: true });
36
- process(srcPath, subDest);
37
- } else if (entry.isFile() && isImageFile(entry.name)) {
38
- try {
39
- cpSync(srcPath, destPath);
40
- copied++;
41
- } catch (error) {
42
- logger.warn(`Failed to copy ${entry.name}: ${error.message}`);
43
- skipped++;
44
- }
45
- } else {
46
- skipped++;
47
- }
48
- }
49
- }
50
-
51
- process(srcDir, destDir);
52
- return { copied, skipped };
53
- }
54
-
55
- export function getImageContentType(ext) {
56
- const types = {
57
- '.jpg': 'image/jpeg',
58
- '.jpeg': 'image/jpeg',
59
- '.png': 'image/png',
60
- '.gif': 'image/gif',
61
- '.svg': 'image/svg+xml',
62
- '.webp': 'image/webp',
63
- '.avif': 'image/avif',
64
- '.ico': 'image/x-icon'
65
- };
66
- return types[ext.toLowerCase()] || 'application/octet-stream';
67
- }
68
-
69
- export function getImageFiles(dir, baseDir = dir) {
70
- const images = [];
71
- if (!existsSync(dir)) return images;
72
-
73
- function scan(directory) {
74
- const entries = readdirSync(directory, { withFileTypes: true });
75
- for (const entry of entries) {
76
- const fullPath = join(directory, entry.name);
77
- const relativePath = fullPath.replace(baseDir, '').replace(/^[\/\\]/, '');
78
- if (entry.isDirectory()) {
79
- scan(fullPath);
80
- } else if (entry.isFile() && isImageFile(entry.name)) {
81
- images.push({ path: fullPath, relativePath, filename: entry.name });
82
- }
83
- }
84
- }
85
- scan(dir);
86
- return images;
87
- }
88
-
89
- export function getTotalImageSize(images) {
90
- return images.reduce((total, img) => total + img.size, 0);
91
- }
92
-
93
- export function formatBytes(bytes) {
94
- if (bytes === 0) return '0 B';
95
- const k = 1024;
96
- const sizes = ['B', 'KB', 'MB', 'GB'];
97
- const i = Math.floor(Math.log(bytes) / Math.log(k));
98
- return `${parseFloat((bytes / Math.pow(k, i)).toFixed(2))} ${sizes[i]}`;
99
- }
100
-
101
- // Legacy exports
102
- export { optimizeImages, copyImages } from '../build/image-optimizer.js';
@@ -1,169 +0,0 @@
1
- // bertui/src/images/processor.js - PURE IMAGE PROCESSING
2
- import { join, extname } from 'path';
3
- import { cpSync, mkdirSync, existsSync, readdirSync, statSync } from 'fs';
4
- import logger from '../logger/logger.js';
5
-
6
- // All supported image formats
7
- export const IMAGE_EXTENSIONS = [
8
- '.png', '.jpg', '.jpeg', '.webp', '.gif', '.svg',
9
- '.avif', '.ico', '.bmp', '.tiff', '.tif'
10
- ];
11
-
12
- /**
13
- * Check if a file is an image based on extension
14
- */
15
- export function isImageFile(filename) {
16
- if (!filename) return false;
17
- const ext = extname(filename).toLowerCase();
18
- return IMAGE_EXTENSIONS.includes(ext);
19
- }
20
-
21
- /**
22
- * Get image content type for HTTP headers
23
- */
24
- export function getImageContentType(ext) {
25
- const types = {
26
- '.jpg': 'image/jpeg',
27
- '.jpeg': 'image/jpeg',
28
- '.png': 'image/png',
29
- '.gif': 'image/gif',
30
- '.svg': 'image/svg+xml',
31
- '.webp': 'image/webp',
32
- '.avif': 'image/avif',
33
- '.ico': 'image/x-icon',
34
- '.bmp': 'image/bmp',
35
- '.tiff': 'image/tiff',
36
- '.tif': 'image/tiff'
37
- };
38
-
39
- return types[ext.toLowerCase()] || 'application/octet-stream';
40
- }
41
-
42
- /**
43
- * Copy images synchronously from source to destination
44
- */
45
- export function copyImagesSync(srcDir, destDir, options = {}) {
46
- const {
47
- verbose = false,
48
- overwrite = true,
49
- filter = null
50
- } = options;
51
-
52
- let copied = 0;
53
- let skipped = 0;
54
- let failed = 0;
55
-
56
- if (!existsSync(srcDir)) {
57
- logger.warn(`Source directory not found: ${srcDir}`);
58
- return { copied, skipped, failed };
59
- }
60
-
61
- // Create destination directory
62
- mkdirSync(destDir, { recursive: true });
63
-
64
- function copyDir(dir, targetDir) {
65
- try {
66
- const entries = readdirSync(dir, { withFileTypes: true });
67
-
68
- for (const entry of entries) {
69
- const srcPath = join(dir, entry.name);
70
- const destPath = join(targetDir, entry.name);
71
-
72
- // Skip if filtered
73
- if (filter && !filter(srcPath)) {
74
- skipped++;
75
- continue;
76
- }
77
-
78
- if (entry.isDirectory()) {
79
- // Recursively copy subdirectories
80
- const subDest = join(targetDir, entry.name);
81
- mkdirSync(subDest, { recursive: true });
82
- copyDir(srcPath, subDest);
83
- }
84
- else if (entry.isFile() && isImageFile(entry.name)) {
85
- try {
86
- // Check if destination exists and we should overwrite
87
- if (!overwrite && existsSync(destPath)) {
88
- skipped++;
89
- if (verbose) logger.debug(`Skipped existing: ${entry.name}`);
90
- continue;
91
- }
92
-
93
- cpSync(srcPath, destPath);
94
- copied++;
95
-
96
- if (verbose) {
97
- const srcSize = statSync(srcPath).size;
98
- logger.debug(`Copied: ${entry.name} (${(srcSize / 1024).toFixed(2)}KB)`);
99
- }
100
- } catch (error) {
101
- failed++;
102
- logger.warn(`Failed to copy ${entry.name}: ${error.message}`);
103
- }
104
- } else {
105
- skipped++;
106
- }
107
- }
108
- } catch (error) {
109
- logger.error(`Error processing ${dir}: ${error.message}`);
110
- }
111
- }
112
-
113
- copyDir(srcDir, destDir);
114
-
115
- return { copied, skipped, failed };
116
- }
117
-
118
- /**
119
- * Get all image files from a directory recursively
120
- */
121
- export function getImageFiles(dir, baseDir = dir) {
122
- const images = [];
123
-
124
- if (!existsSync(dir)) {
125
- return images;
126
- }
127
-
128
- function scan(directory) {
129
- const entries = readdirSync(directory, { withFileTypes: true });
130
-
131
- for (const entry of entries) {
132
- const fullPath = join(directory, entry.name);
133
- const relativePath = fullPath.replace(baseDir, '').replace(/^[\/\\]/, '');
134
-
135
- if (entry.isDirectory()) {
136
- scan(fullPath);
137
- } else if (entry.isFile() && isImageFile(entry.name)) {
138
- images.push({
139
- path: fullPath,
140
- relativePath,
141
- filename: entry.name,
142
- size: statSync(fullPath).size,
143
- ext: extname(entry.name).toLowerCase()
144
- });
145
- }
146
- }
147
- }
148
-
149
- scan(dir);
150
- return images;
151
- }
152
-
153
- /**
154
- * Calculate total size of images
155
- */
156
- export function getTotalImageSize(images) {
157
- return images.reduce((total, img) => total + img.size, 0);
158
- }
159
-
160
- /**
161
- * Format bytes to human readable
162
- */
163
- export function formatBytes(bytes) {
164
- if (bytes === 0) return '0 B';
165
- const k = 1024;
166
- const sizes = ['B', 'KB', 'MB', 'GB'];
167
- const i = Math.floor(Math.log(bytes) / Math.log(k));
168
- return `${parseFloat((bytes / Math.pow(k, i)).toFixed(2))} ${sizes[i]}`;
169
- }
@@ -1,165 +0,0 @@
1
- // bertui/src/layouts/index.js
2
- // Layout system - src/layouts/default.tsx wraps all pages
3
-
4
- import { join, extname, basename } from 'path';
5
- import { existsSync, readdirSync, mkdirSync } from 'fs';;
6
- import logger from '../logger/logger.js';
7
-
8
- /**
9
- * Discover all layouts in src/layouts/
10
- * Layout naming convention:
11
- * default.tsx → wraps all pages (fallback)
12
- * blog.tsx → wraps pages in /blog/*
13
- * [route].tsx → wraps pages matching route prefix
14
- */
15
- export async function discoverLayouts(root) {
16
- const layoutsDir = join(root, 'src', 'layouts');
17
- const layouts = {};
18
-
19
- if (!existsSync(layoutsDir)) {
20
- return layouts;
21
- }
22
-
23
- const entries = readdirSync(layoutsDir, { withFileTypes: true });
24
-
25
- for (const entry of entries) {
26
- if (!entry.isFile()) continue;
27
- const ext = extname(entry.name);
28
- if (!['.jsx', '.tsx', '.js', '.ts'].includes(ext)) continue;
29
-
30
- const name = basename(entry.name, ext);
31
- layouts[name] = {
32
- name,
33
- path: join(layoutsDir, entry.name),
34
- route: name === 'default' ? '*' : `/${name}`,
35
- };
36
-
37
- logger.debug(`📐 Layout found: ${entry.name} → ${layouts[name].route}`);
38
- }
39
-
40
- if (Object.keys(layouts).length > 0) {
41
- logger.success(`✅ ${Object.keys(layouts).length} layout(s) loaded`);
42
- }
43
-
44
- return layouts;
45
- }
46
-
47
- /**
48
- * Match which layout applies to a given route
49
- * Priority: exact name match > default
50
- */
51
- export function matchLayout(route, layouts) {
52
- if (!layouts || Object.keys(layouts).length === 0) return null;
53
-
54
- // Strip leading slash and get first segment
55
- const segment = route.replace(/^\//, '').split('/')[0];
56
-
57
- // Exact match (e.g., /blog → blog.tsx)
58
- if (segment && layouts[segment]) {
59
- return layouts[segment];
60
- }
61
-
62
- // Default layout fallback
63
- if (layouts['default']) {
64
- return layouts['default'];
65
- }
66
-
67
- return null;
68
- }
69
-
70
- /**
71
- * Generate layout wrapper code for the compiler
72
- * Wraps the page component with the layout component
73
- */
74
- export function generateLayoutWrapper(pageImportPath, layoutImportPath, componentName = 'Page') {
75
- return `
76
- import React from 'react';
77
- import ${componentName} from '${pageImportPath}';
78
- import Layout from '${layoutImportPath}';
79
-
80
- export default function LayoutWrapped(props) {
81
- return React.createElement(
82
- Layout,
83
- props,
84
- React.createElement(${componentName}, props)
85
- );
86
- }
87
- `.trim();
88
- }
89
-
90
- /**
91
- * Compile layouts directory - transpiles layout files to .bertui/compiled/layouts/
92
- */
93
- export async function compileLayouts(root, compiledDir) {
94
- const layoutsDir = join(root, 'src', 'layouts');
95
- if (!existsSync(layoutsDir)) return {};
96
-
97
- const outDir = join(compiledDir, 'layouts');
98
-
99
- mkdirSync(outDir, { recursive: true });
100
-
101
- const layouts = await discoverLayouts(root);
102
-
103
- for (const [name, layout] of Object.entries(layouts)) {
104
- const ext = extname(layout.path);
105
- const loader = ext === '.tsx' ? 'tsx' : ext === '.ts' ? 'ts' : 'jsx';
106
-
107
- try {
108
- let code = await Bun.file(layout.path).text();
109
-
110
- // Add React import if missing
111
- if (!code.includes('import React')) {
112
- code = `import React from 'react';\n${code}`;
113
- }
114
-
115
- const transpiler = new Bun.Transpiler({
116
- loader,
117
- target: 'browser',
118
- tsconfig: {
119
- compilerOptions: {
120
- jsx: 'react',
121
- jsxFactory: 'React.createElement',
122
- jsxFragmentFactory: 'React.Fragment',
123
- },
124
- },
125
- });
126
-
127
- let compiled = await transpiler.transform(code);
128
-
129
- // Fix relative imports
130
- compiled = compiled.replace(
131
- /from\s+['"](\.\.?\/[^'"]+?)(?<!\.js|\.jsx|\.ts|\.tsx|\.json)['"]/g,
132
- (match, path) => {
133
- if (path.endsWith('/') || /\.\w+$/.test(path)) return match;
134
- return `from '${path}.js'`;
135
- }
136
- );
137
-
138
- await Bun.write(join(outDir, `${name}.js`), compiled);
139
- logger.debug(`📐 Compiled layout: ${name}`);
140
- } catch (err) {
141
- logger.error(`Failed to compile layout ${name}: ${err.message}`);
142
- }
143
- }
144
-
145
- return layouts;
146
- }
147
-
148
- /**
149
- * Inject layout into router generation
150
- * Called by router-generator to wrap page components with their layouts
151
- */
152
- export function injectLayoutsIntoRouter(routes, layouts, compiledDir) {
153
- if (!layouts || Object.keys(layouts).length === 0) return routes;
154
-
155
- return routes.map(route => {
156
- const layout = matchLayout(route.route, layouts);
157
- if (!layout) return route;
158
-
159
- return {
160
- ...route,
161
- layout: layout.name,
162
- layoutPath: join(compiledDir, 'layouts', `${layout.name}.js`),
163
- };
164
- });
165
- }