bertui 1.0.3 β 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +551 -132
- package/package.json +27 -7
- package/src/build/compiler/file-transpiler.js +171 -0
- package/src/build/compiler/index.js +45 -0
- package/src/build/compiler/route-discoverer.js +46 -0
- package/src/build/compiler/router-generator.js +104 -0
- package/src/build/generators/html-generator.js +259 -0
- package/src/build/generators/robots-generator.js +58 -0
- package/src/build/generators/sitemap-generator.js +63 -0
- package/src/build/processors/asset-processor.js +19 -0
- package/src/build/processors/css-builder.js +35 -0
- package/src/build/server-island-validator.js +156 -0
- package/src/build.js +96 -632
- package/src/config/defaultConfig.js +26 -6
- package/src/config/loadConfig.js +21 -5
- package/src/router/Router.js +38 -5
- package/src/router/SSRRouter.js +156 -0
- package/src/utils/meta-extractor.js +61 -0
- package/types/config.d.ts +80 -0
- package/types/index.d.ts +116 -0
- package/types/react.d.ts +13 -0
- package/types/router.d.ts +79 -0
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// bertui/src/build/robots-generator.js
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
import logger from '../../logger/logger.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Generate robots.txt from sitemap data
|
|
7
|
+
* @param {Object} config - BertUI config with baseUrl
|
|
8
|
+
* @param {string} outDir - Output directory (dist/)
|
|
9
|
+
* @param {Array} routes - Optional: routes to disallow (e.g., admin pages)
|
|
10
|
+
*/
|
|
11
|
+
export async function generateRobots(config, outDir, routes = []) {
|
|
12
|
+
logger.info('π€ Generating robots.txt...');
|
|
13
|
+
|
|
14
|
+
// β
FIX: Check if baseUrl exists, then remove trailing slash
|
|
15
|
+
if (!config?.baseUrl) {
|
|
16
|
+
logger.error('β baseUrl is required in bertui.config.js for robots.txt generation!');
|
|
17
|
+
logger.error(' Add: baseUrl: "https://your-domain.com" to your config');
|
|
18
|
+
throw new Error('Missing baseUrl in config - robots.txt generation failed');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const baseUrl = config.baseUrl.replace(/\/$/, ''); // Remove trailing slash
|
|
22
|
+
const sitemapUrl = `${baseUrl}/sitemap.xml`;
|
|
23
|
+
|
|
24
|
+
logger.info(` Sitemap URL: ${sitemapUrl}`);
|
|
25
|
+
|
|
26
|
+
// Default robots.txt - Allow all
|
|
27
|
+
let robotsTxt = `# BertUI Generated robots.txt
|
|
28
|
+
User-agent: *
|
|
29
|
+
Allow: /
|
|
30
|
+
|
|
31
|
+
# Sitemap
|
|
32
|
+
Sitemap: ${sitemapUrl}
|
|
33
|
+
`;
|
|
34
|
+
|
|
35
|
+
// Add custom disallow rules if specified in config
|
|
36
|
+
if (config?.robots?.disallow && Array.isArray(config.robots.disallow) && config.robots.disallow.length > 0) {
|
|
37
|
+
robotsTxt += '\n# Custom Disallow Rules\n';
|
|
38
|
+
config.robots.disallow.forEach(path => {
|
|
39
|
+
robotsTxt += `Disallow: ${path}\n`;
|
|
40
|
+
});
|
|
41
|
+
logger.info(` Blocked ${config.robots.disallow.length} path(s)`);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Add crawl delay if specified
|
|
45
|
+
if (config?.robots?.crawlDelay && typeof config.robots.crawlDelay === 'number') {
|
|
46
|
+
robotsTxt += `\nCrawl-delay: ${config.robots.crawlDelay}\n`;
|
|
47
|
+
logger.info(` Crawl delay: ${config.robots.crawlDelay}s`);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Write to dist/robots.txt
|
|
51
|
+
const robotsPath = join(outDir, 'robots.txt');
|
|
52
|
+
await Bun.write(robotsPath, robotsTxt);
|
|
53
|
+
|
|
54
|
+
logger.success('β
robots.txt generated');
|
|
55
|
+
logger.info(` Location: ${robotsPath}`);
|
|
56
|
+
|
|
57
|
+
return { path: robotsPath, content: robotsTxt };
|
|
58
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// bertui/src/build/sitemap-generator.js - SIMPLIFIED
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
import logger from '../../logger/logger';
|
|
4
|
+
|
|
5
|
+
function calculatePriority(route) {
|
|
6
|
+
if (route === '/') return 1.0;
|
|
7
|
+
if (route.includes(':')) return 0.6;
|
|
8
|
+
const depth = route.split('/').filter(Boolean).length;
|
|
9
|
+
if (depth === 1) return 0.8;
|
|
10
|
+
if (depth === 2) return 0.7;
|
|
11
|
+
return 0.6;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function generateSitemapXML(routes, baseUrl) {
|
|
15
|
+
const urls = routes.map(route => {
|
|
16
|
+
const url = `${baseUrl}${route.route}`;
|
|
17
|
+
return ` <url>
|
|
18
|
+
<loc>${url}</loc>
|
|
19
|
+
<lastmod>${route.lastmod}</lastmod>
|
|
20
|
+
<changefreq>weekly</changefreq>
|
|
21
|
+
<priority>${route.priority}</priority>
|
|
22
|
+
</url>`;
|
|
23
|
+
}).join('\n');
|
|
24
|
+
|
|
25
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
26
|
+
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
|
27
|
+
${urls}
|
|
28
|
+
</urlset>`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export async function generateSitemap(routes, config, outDir) {
|
|
32
|
+
logger.info('πΊοΈ Generating sitemap.xml...');
|
|
33
|
+
|
|
34
|
+
if (!config?.baseUrl) {
|
|
35
|
+
logger.error('β baseUrl is required in bertui.config.js for sitemap generation!');
|
|
36
|
+
logger.error(' Add: baseUrl: "https://your-domain.com" to your config');
|
|
37
|
+
throw new Error('Missing baseUrl in config - sitemap generation failed');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const baseUrl = config.baseUrl.replace(/\/$/, '');
|
|
41
|
+
const currentDate = new Date().toISOString().split('T')[0];
|
|
42
|
+
|
|
43
|
+
logger.info(` Base URL: ${baseUrl}`);
|
|
44
|
+
|
|
45
|
+
const staticRoutes = routes.filter(r => r.type === 'static');
|
|
46
|
+
logger.info(` ${staticRoutes.length} static routes will be included in sitemap`);
|
|
47
|
+
|
|
48
|
+
// SIMPLE: No file reading, just route processing
|
|
49
|
+
const sitemapRoutes = staticRoutes.map(route => ({
|
|
50
|
+
route: route.route,
|
|
51
|
+
lastmod: currentDate,
|
|
52
|
+
priority: calculatePriority(route.route)
|
|
53
|
+
}));
|
|
54
|
+
|
|
55
|
+
const xml = generateSitemapXML(sitemapRoutes, baseUrl);
|
|
56
|
+
const sitemapPath = join(outDir, 'sitemap.xml');
|
|
57
|
+
await Bun.write(sitemapPath, xml);
|
|
58
|
+
|
|
59
|
+
logger.success(`β
Sitemap generated: ${sitemapRoutes.length} URLs`);
|
|
60
|
+
logger.info(` Location: ${sitemapPath}`);
|
|
61
|
+
|
|
62
|
+
return { routes: sitemapRoutes, path: sitemapPath };
|
|
63
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// bertui/src/build/processors/asset-processor.js
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
import { existsSync, mkdirSync } from 'fs'; // β
ADD THIS IMPORT
|
|
4
|
+
import { copyImages } from '../image-optimizer.js';
|
|
5
|
+
|
|
6
|
+
export async function copyAllStaticAssets(root, outDir) {
|
|
7
|
+
const publicDir = join(root, 'public');
|
|
8
|
+
const srcImagesDir = join(root, 'src', 'images');
|
|
9
|
+
|
|
10
|
+
if (existsSync(publicDir)) {
|
|
11
|
+
copyImages(publicDir, outDir);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
if (existsSync(srcImagesDir)) {
|
|
15
|
+
const distImagesDir = join(outDir, 'images');
|
|
16
|
+
mkdirSync(distImagesDir, { recursive: true });
|
|
17
|
+
copyImages(srcImagesDir, distImagesDir);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// bertui/src/build/processors/css-builder.js
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
import { existsSync, readdirSync, mkdirSync } from 'fs';
|
|
4
|
+
import logger from '../../logger/logger.js';
|
|
5
|
+
import { buildCSS } from '../css-builder.js';
|
|
6
|
+
|
|
7
|
+
export async function buildAllCSS(root, outDir) {
|
|
8
|
+
const srcStylesDir = join(root, 'src', 'styles');
|
|
9
|
+
const stylesOutDir = join(outDir, 'styles');
|
|
10
|
+
|
|
11
|
+
mkdirSync(stylesOutDir, { recursive: true });
|
|
12
|
+
|
|
13
|
+
if (existsSync(srcStylesDir)) {
|
|
14
|
+
const cssFiles = readdirSync(srcStylesDir).filter(f => f.endsWith('.css'));
|
|
15
|
+
|
|
16
|
+
if (cssFiles.length === 0) {
|
|
17
|
+
await Bun.write(join(stylesOutDir, 'bertui.min.css'), '/* No CSS */');
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
let combinedCSS = '';
|
|
22
|
+
for (const cssFile of cssFiles) {
|
|
23
|
+
const srcPath = join(srcStylesDir, cssFile);
|
|
24
|
+
const file = Bun.file(srcPath);
|
|
25
|
+
const cssContent = await file.text();
|
|
26
|
+
combinedCSS += `/* ${cssFile} */\n${cssContent}\n\n`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const combinedPath = join(stylesOutDir, 'bertui.min.css');
|
|
30
|
+
await Bun.write(combinedPath, combinedCSS);
|
|
31
|
+
await buildCSS(combinedPath, combinedPath);
|
|
32
|
+
|
|
33
|
+
logger.success(`β
Combined ${cssFiles.length} CSS files`);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
// bertui/src/build/server-island-validator.js
|
|
2
|
+
// Fixed validation for Server Islands - no false positives!
|
|
3
|
+
|
|
4
|
+
import logger from '../logger/logger.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Validates that a Server Island component follows all rules
|
|
8
|
+
* @param {string} sourceCode - The component source code
|
|
9
|
+
* @param {string} filePath - Path to the file (for error messages)
|
|
10
|
+
* @returns {{ valid: boolean, errors: string[] }}
|
|
11
|
+
*/
|
|
12
|
+
export function validateServerIsland(sourceCode, filePath) {
|
|
13
|
+
const errors = [];
|
|
14
|
+
|
|
15
|
+
// Rule 1: No React hooks (FIXED: only match actual function calls)
|
|
16
|
+
const hookPatterns = [
|
|
17
|
+
'useState',
|
|
18
|
+
'useEffect',
|
|
19
|
+
'useContext',
|
|
20
|
+
'useReducer',
|
|
21
|
+
'useCallback',
|
|
22
|
+
'useMemo',
|
|
23
|
+
'useRef',
|
|
24
|
+
'useImperativeHandle',
|
|
25
|
+
'useLayoutEffect',
|
|
26
|
+
'useDebugValue',
|
|
27
|
+
'useId',
|
|
28
|
+
'useDeferredValue',
|
|
29
|
+
'useTransition',
|
|
30
|
+
'useSyncExternalStore'
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
for (const hook of hookPatterns) {
|
|
34
|
+
// FIXED: Only match hooks as function calls, not in text/comments
|
|
35
|
+
// Looks for: useState( or const [x] = useState(
|
|
36
|
+
const regex = new RegExp(`\\b${hook}\\s*\\(`, 'g');
|
|
37
|
+
if (regex.test(sourceCode)) {
|
|
38
|
+
errors.push(`β Uses React hook: ${hook}`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Rule 2: No bertui/router imports
|
|
43
|
+
if (sourceCode.includes('from \'bertui/router\'') ||
|
|
44
|
+
sourceCode.includes('from "bertui/router"')) {
|
|
45
|
+
errors.push('β Imports from \'bertui/router\' (use <a> tags instead of Link)');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Rule 3: No browser APIs (FIXED: only match actual usage, not in strings/comments)
|
|
49
|
+
const browserAPIs = [
|
|
50
|
+
{ pattern: 'window\\.', name: 'window' },
|
|
51
|
+
{ pattern: 'document\\.', name: 'document' },
|
|
52
|
+
{ pattern: 'localStorage\\.', name: 'localStorage' },
|
|
53
|
+
{ pattern: 'sessionStorage\\.', name: 'sessionStorage' },
|
|
54
|
+
{ pattern: 'navigator\\.', name: 'navigator' },
|
|
55
|
+
{ pattern: 'location\\.', name: 'location' },
|
|
56
|
+
{ pattern: 'history\\.', name: 'history' },
|
|
57
|
+
{ pattern: '(?<!//.*|/\\*.*|\\*)\\bfetch\\s*\\(', name: 'fetch' },
|
|
58
|
+
{ pattern: '\\.addEventListener\\s*\\(', name: 'addEventListener' },
|
|
59
|
+
{ pattern: '\\.removeEventListener\\s*\\(', name: 'removeEventListener' },
|
|
60
|
+
{ pattern: '\\bsetTimeout\\s*\\(', name: 'setTimeout' },
|
|
61
|
+
{ pattern: '\\bsetInterval\\s*\\(', name: 'setInterval' },
|
|
62
|
+
{ pattern: '\\brequestAnimationFrame\\s*\\(', name: 'requestAnimationFrame' }
|
|
63
|
+
];
|
|
64
|
+
|
|
65
|
+
for (const api of browserAPIs) {
|
|
66
|
+
const regex = new RegExp(api.pattern, 'g');
|
|
67
|
+
if (regex.test(sourceCode)) {
|
|
68
|
+
if (api.name === 'console') {
|
|
69
|
+
logger.warn(`β οΈ ${filePath} uses console.log (will not work in static HTML)`);
|
|
70
|
+
} else {
|
|
71
|
+
errors.push(`β Uses browser API: ${api.name}`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Rule 4: No event handlers (these won't work without JS)
|
|
77
|
+
const eventHandlers = [
|
|
78
|
+
'onClick=',
|
|
79
|
+
'onChange=',
|
|
80
|
+
'onSubmit=',
|
|
81
|
+
'onInput=',
|
|
82
|
+
'onFocus=',
|
|
83
|
+
'onBlur=',
|
|
84
|
+
'onMouseEnter=',
|
|
85
|
+
'onMouseLeave=',
|
|
86
|
+
'onKeyDown=',
|
|
87
|
+
'onKeyUp=',
|
|
88
|
+
'onScroll='
|
|
89
|
+
];
|
|
90
|
+
|
|
91
|
+
for (const handler of eventHandlers) {
|
|
92
|
+
if (sourceCode.includes(handler)) {
|
|
93
|
+
errors.push(`β Uses event handler: ${handler.replace('=', '')} (Server Islands are static HTML)`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Rule 5: Check for dynamic imports
|
|
98
|
+
if (/import\s*\(/.test(sourceCode)) {
|
|
99
|
+
errors.push('β Uses dynamic import() (not supported in Server Islands)');
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Rule 6: Check for async/await (usually indicates API calls)
|
|
103
|
+
if (/async\s+function|async\s*\(|async\s+\w+\s*\(/.test(sourceCode)) {
|
|
104
|
+
errors.push('β Uses async/await (Server Islands must be synchronous)');
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const valid = errors.length === 0;
|
|
108
|
+
|
|
109
|
+
return { valid, errors };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Display validation errors in a clear format
|
|
114
|
+
*/
|
|
115
|
+
export function displayValidationErrors(filePath, errors) {
|
|
116
|
+
logger.error(`\nποΈ Server Island validation failed: ${filePath}`);
|
|
117
|
+
logger.error('\nViolations:');
|
|
118
|
+
errors.forEach(error => logger.error(` ${error}`));
|
|
119
|
+
logger.error('\nπ Server Island Rules:');
|
|
120
|
+
logger.error(' β
Pure static JSX only');
|
|
121
|
+
logger.error(' β No React hooks (useState, useEffect, etc.)');
|
|
122
|
+
logger.error(' β No Link component (use <a> tags)');
|
|
123
|
+
logger.error(' β No browser APIs (window, document, fetch)');
|
|
124
|
+
logger.error(' β No event handlers (onClick, onChange, etc.)');
|
|
125
|
+
logger.error('\nπ‘ Tip: Remove the "export const render = \\"server\\"" line');
|
|
126
|
+
logger.error(' if you need these features (page will be client-only).\n');
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Extract and validate all Server Islands in a project
|
|
131
|
+
*/
|
|
132
|
+
export async function validateAllServerIslands(routes) {
|
|
133
|
+
const serverIslands = [];
|
|
134
|
+
const validationResults = [];
|
|
135
|
+
|
|
136
|
+
for (const route of routes) {
|
|
137
|
+
const sourceCode = await Bun.file(route.path).text();
|
|
138
|
+
const isServerIsland = sourceCode.includes('export const render = "server"');
|
|
139
|
+
|
|
140
|
+
if (isServerIsland) {
|
|
141
|
+
const validation = validateServerIsland(sourceCode, route.path);
|
|
142
|
+
|
|
143
|
+
validationResults.push({
|
|
144
|
+
route: route.route,
|
|
145
|
+
path: route.path,
|
|
146
|
+
...validation
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
if (validation.valid) {
|
|
150
|
+
serverIslands.push(route);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return { serverIslands, validationResults };
|
|
156
|
+
}
|