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.
- package/README.md +45 -196
- package/TYPES_PATCH.md +17 -0
- package/bin/bertui.js +2 -7
- package/package.json +32 -98
- package/src/config.ts +4 -0
- package/src/index.ts +32 -0
- package/src/optional.ts +49 -0
- package/src/router.ts +3 -0
- package/tsconfig.json +29 -0
- package/LICENSE +0 -21
- package/index.js +0 -103
- package/src/analyzer/index.js +0 -370
- package/src/build/compiler/file-transpiler.js +0 -216
- package/src/build/compiler/index.js +0 -31
- package/src/build/compiler/route-discoverer.js +0 -49
- package/src/build/compiler/router-generator.js +0 -105
- package/src/build/css-builder.js +0 -81
- package/src/build/generators/html-generator.js +0 -151
- package/src/build/generators/robots-generator.js +0 -58
- package/src/build/generators/sitemap-generator.js +0 -63
- package/src/build/image-optimizer.js +0 -137
- package/src/build/processors/asset-processor.js +0 -19
- package/src/build/processors/css-builder.js +0 -142
- package/src/build/server-island-validator.js +0 -12
- package/src/build.js +0 -266
- package/src/cli.js +0 -131
- package/src/client/compiler.js +0 -522
- package/src/client/fast-refresh.js +0 -72
- package/src/client/hmr-runtime.js +0 -59
- package/src/compiler/index.js +0 -25
- package/src/compiler/router-generator-pure.js +0 -104
- package/src/compiler/transform.js +0 -149
- package/src/config/defaultConfig.js +0 -37
- package/src/config/index.js +0 -2
- package/src/config/loadConfig.js +0 -64
- package/src/config/og-image.png +0 -0
- package/src/css/index.js +0 -46
- package/src/css/processor.js +0 -172
- package/src/dev.js +0 -68
- package/src/hydration/index.js +0 -151
- package/src/image-optimizer/index.js +0 -103
- package/src/images/index.js +0 -102
- package/src/images/processor.js +0 -169
- package/src/layouts/index.js +0 -165
- package/src/loading/index.js +0 -210
- package/src/logger/logger.js +0 -320
- package/src/logger/notes.md +0 -20
- package/src/middleware/index.js +0 -182
- package/src/router/Router.js +0 -150
- package/src/router/SSRRouter.js +0 -156
- package/src/router/index.js +0 -3
- package/src/scaffolder/index.js +0 -310
- package/src/serve.js +0 -193
- package/src/server/dev-handler.js +0 -195
- package/src/server/dev-server-utils.js +0 -406
- package/src/server/dev-server.js +0 -15
- package/src/server/hmr-handler.js +0 -148
- package/src/server/index.js +0 -3
- package/src/server/notes.md +0 -1
- package/src/server/request-handler.js +0 -36
- package/src/server-islands/extractor.js +0 -198
- package/src/server-islands/index.js +0 -59
- package/src/styles/bertui.css +0 -210
- package/src/utils/cache.js +0 -297
- package/src/utils/env.js +0 -87
- package/src/utils/importhow.js +0 -52
- package/src/utils/index.js +0 -11
- package/src/utils/meta-extractor.js +0 -127
- package/types/bin/bertui.d.ts +0 -3
- package/types/bin/bertui.d.ts.map +0 -1
- package/types/error-overlay.d.ts +0 -2
- package/types/error-overlay.d.ts.map +0 -1
- package/types/index.d.ts +0 -26
- package/types/index.d.ts.map +0 -1
- package/types/scripts/fix-wasm-exports.d.ts +0 -2
- package/types/scripts/fix-wasm-exports.d.ts.map +0 -1
- package/types/src/analyzer/index.d.ts +0 -8
- package/types/src/analyzer/index.d.ts.map +0 -1
- package/types/src/build/compiler/file-transpiler.d.ts +0 -5
- package/types/src/build/compiler/file-transpiler.d.ts.map +0 -1
- package/types/src/build/compiler/index.d.ts +0 -12
- package/types/src/build/compiler/index.d.ts.map +0 -1
- package/types/src/build/compiler/route-discoverer.d.ts +0 -2
- package/types/src/build/compiler/route-discoverer.d.ts.map +0 -1
- package/types/src/build/compiler/router-generator.d.ts +0 -2
- package/types/src/build/compiler/router-generator.d.ts.map +0 -1
- package/types/src/build/css-builder.d.ts +0 -18
- package/types/src/build/css-builder.d.ts.map +0 -1
- package/types/src/build/generators/html-generator.d.ts +0 -2
- package/types/src/build/generators/html-generator.d.ts.map +0 -1
- package/types/src/build/generators/robots-generator.d.ts +0 -11
- package/types/src/build/generators/robots-generator.d.ts.map +0 -1
- package/types/src/build/generators/sitemap-generator.d.ts +0 -5
- package/types/src/build/generators/sitemap-generator.d.ts.map +0 -1
- package/types/src/build/image-optimizer.d.ts +0 -11
- package/types/src/build/image-optimizer.d.ts.map +0 -1
- package/types/src/build/processors/asset-processor.d.ts +0 -2
- package/types/src/build/processors/asset-processor.d.ts.map +0 -1
- package/types/src/build/processors/css-builder.d.ts +0 -2
- package/types/src/build/processors/css-builder.d.ts.map +0 -1
- package/types/src/build/server-island-validator.d.ts +0 -27
- package/types/src/build/server-island-validator.d.ts.map +0 -1
- package/types/src/build.d.ts +0 -5
- package/types/src/build.d.ts.map +0 -1
- package/types/src/cli.d.ts +0 -2
- package/types/src/cli.d.ts.map +0 -1
- package/types/src/client/compiler.d.ts +0 -16
- package/types/src/client/compiler.d.ts.map +0 -1
- package/types/src/client/fast-refresh.d.ts +0 -3
- package/types/src/client/fast-refresh.d.ts.map +0 -1
- package/types/src/client/hmr-runtime.d.ts +0 -4
- package/types/src/client/hmr-runtime.d.ts.map +0 -1
- package/types/src/compiler/index.d.ts +0 -8
- package/types/src/compiler/index.d.ts.map +0 -1
- package/types/src/compiler/router-generator-pure.d.ts +0 -2
- package/types/src/compiler/router-generator-pure.d.ts.map +0 -1
- package/types/src/compiler/transform.d.ts +0 -36
- package/types/src/compiler/transform.d.ts.map +0 -1
- package/types/src/config/defaultConfig.d.ts +0 -26
- package/types/src/config/defaultConfig.d.ts.map +0 -1
- package/types/src/config/index.d.ts +0 -3
- package/types/src/config/index.d.ts.map +0 -1
- package/types/src/config/loadConfig.d.ts +0 -2
- package/types/src/config/loadConfig.d.ts.map +0 -1
- package/types/src/css/index.d.ts +0 -6
- package/types/src/css/index.d.ts.map +0 -1
- package/types/src/css/processor.d.ts +0 -23
- package/types/src/css/processor.d.ts.map +0 -1
- package/types/src/dev.d.ts +0 -2
- package/types/src/dev.d.ts.map +0 -1
- package/types/src/hydration/index.d.ts +0 -33
- package/types/src/hydration/index.d.ts.map +0 -1
- package/types/src/image-optimizer/index.d.ts +0 -24
- package/types/src/image-optimizer/index.d.ts.map +0 -1
- package/types/src/images/index.d.ts +0 -12
- package/types/src/images/index.d.ts.map +0 -1
- package/types/src/images/processor.d.ts +0 -30
- package/types/src/images/processor.d.ts.map +0 -1
- package/types/src/layouts/index.d.ts +0 -28
- package/types/src/layouts/index.d.ts.map +0 -1
- package/types/src/loading/index.d.ts +0 -28
- package/types/src/loading/index.d.ts.map +0 -1
- package/types/src/logger/logger.d.ts +0 -30
- package/types/src/logger/logger.d.ts.map +0 -1
- package/types/src/middleware/index.d.ts +0 -61
- package/types/src/middleware/index.d.ts.map +0 -1
- package/types/src/router/Router.d.ts +0 -16
- package/types/src/router/Router.d.ts.map +0 -1
- package/types/src/router/SSRRouter.d.ts +0 -20
- package/types/src/router/SSRRouter.d.ts.map +0 -1
- package/types/src/router/index.d.ts +0 -3
- package/types/src/router/index.d.ts.map +0 -1
- package/types/src/scaffolder/index.d.ts +0 -14
- package/types/src/scaffolder/index.d.ts.map +0 -1
- package/types/src/serve.d.ts +0 -3
- package/types/src/serve.d.ts.map +0 -1
- package/types/src/server/dev-handler.d.ts +0 -13
- package/types/src/server/dev-handler.d.ts.map +0 -1
- package/types/src/server/dev-server-utils.d.ts +0 -6
- package/types/src/server/dev-server-utils.d.ts.map +0 -1
- package/types/src/server/dev-server.d.ts +0 -18
- package/types/src/server/dev-server.d.ts.map +0 -1
- package/types/src/server/hmr-handler.d.ts +0 -19
- package/types/src/server/hmr-handler.d.ts.map +0 -1
- package/types/src/server/index.d.ts +0 -4
- package/types/src/server/index.d.ts.map +0 -1
- package/types/src/server/request-handler.d.ts +0 -19
- package/types/src/server/request-handler.d.ts.map +0 -1
- package/types/src/server-islands/extractor.d.ts +0 -16
- package/types/src/server-islands/extractor.d.ts.map +0 -1
- package/types/src/server-islands/index.d.ts +0 -3
- package/types/src/server-islands/index.d.ts.map +0 -1
- package/types/src/utils/cache.d.ts +0 -52
- package/types/src/utils/cache.d.ts.map +0 -1
- package/types/src/utils/env.d.ts +0 -20
- package/types/src/utils/env.d.ts.map +0 -1
- package/types/src/utils/importhow.d.ts +0 -15
- package/types/src/utils/importhow.d.ts.map +0 -1
- package/types/src/utils/index.d.ts +0 -3
- package/types/src/utils/index.d.ts.map +0 -1
- package/types/src/utils/meta-extractor.d.ts +0 -13
- package/types/src/utils/meta-extractor.d.ts.map +0 -1
package/src/hydration/index.js
DELETED
|
@@ -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';
|
package/src/images/index.js
DELETED
|
@@ -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';
|
package/src/images/processor.js
DELETED
|
@@ -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
|
-
}
|
package/src/layouts/index.js
DELETED
|
@@ -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
|
-
}
|