bertui 1.1.0 → 1.1.2

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/package.json CHANGED
@@ -1,21 +1,33 @@
1
1
  {
2
2
  "name": "bertui",
3
- "version": "1.1.0",
4
- "description": "Lightning-fast React dev server powered by Bun and Elysia",
3
+ "version": "1.1.2",
4
+ "description": "Lightning-fast React dev server powered by Bun and Elysia - Now with TypeScript support!",
5
5
  "type": "module",
6
6
  "main": "./index.js",
7
+ "types": "./types/index.d.ts",
7
8
  "bin": {
8
9
  "bertui": "./bin/bertui.js"
9
10
  },
10
11
  "exports": {
11
- ".": "./index.js",
12
+ ".": {
13
+ "types": "./types/index.d.ts",
14
+ "default": "./index.js"
15
+ },
12
16
  "./styles": "./src/styles/bertui.css",
13
17
  "./logger": "./src/logger/logger.js",
14
- "./router": "./src/router/Router.jsx"
18
+ "./router": {
19
+ "types": "./types/router.d.ts",
20
+ "default": "./src/router/Router.jsx"
21
+ },
22
+ "./config": {
23
+ "types": "./types/config.d.ts",
24
+ "default": "./src/config/loadConfig.js"
25
+ }
15
26
  },
16
27
  "files": [
17
28
  "bin",
18
29
  "src",
30
+ "types",
19
31
  "index.js",
20
32
  "README.md",
21
33
  "LICENSE"
@@ -35,7 +47,11 @@
35
47
  "bundler",
36
48
  "fast",
37
49
  "hmr",
38
- "file-based-routing"
50
+ "file-based-routing",
51
+ "typescript",
52
+ "seo",
53
+ "sitemap",
54
+ "server-islands"
39
55
  ],
40
56
  "author": "Pease Ernest",
41
57
  "license": "MIT",
@@ -45,7 +61,7 @@
45
61
  },
46
62
  "dependencies": {
47
63
  "elysia": "^1.0.0",
48
- "ernest-logger": "latest",
64
+ "ernest-logger": "^2.0.0",
49
65
  "lightningcss": "^1.30.2"
50
66
  },
51
67
  "peerDependencies": {
@@ -53,6 +69,10 @@
53
69
  "react-dom": "^19.2.3",
54
70
  "bun": ">=1.0.0"
55
71
  },
72
+ "devDependencies": {
73
+ "@types/react": "^18.2.0",
74
+ "@types/react-dom": "^18.2.0"
75
+ },
56
76
  "engines": {
57
77
  "bun": ">=1.0.0"
58
78
  }
@@ -0,0 +1,171 @@
1
+ // bertui/src/build/compiler/file-transpiler.js - SINGLE TRANSPILER INSTANCE
2
+ import { join, relative, dirname, extname } from 'path';
3
+ import { readdirSync, statSync, mkdirSync } from 'fs';
4
+ import logger from '../../logger/logger.js';
5
+ import { replaceEnvInCode } from '../../utils/env.js';
6
+
7
+ // ✅ CRITICAL FIX: Create transpilers ONCE at module level
8
+ const jsxTranspiler = new Bun.Transpiler({ loader: 'jsx' });
9
+
10
+ const tsxTranspiler = new Bun.Transpiler({
11
+ loader: 'tsx',
12
+ tsconfig: {
13
+ compilerOptions: {
14
+ jsx: 'react',
15
+ jsxFactory: 'React.createElement',
16
+ jsxFragmentFactory: 'React.Fragment'
17
+ }
18
+ }
19
+ });
20
+
21
+ const tsTranspiler = new Bun.Transpiler({
22
+ loader: 'ts',
23
+ tsconfig: {
24
+ compilerOptions: {
25
+ jsx: 'preserve'
26
+ }
27
+ }
28
+ });
29
+
30
+ function getTranspiler(loader) {
31
+ // Just return the pre-created instances
32
+ return loader === 'tsx' ? tsxTranspiler :
33
+ loader === 'ts' ? tsTranspiler :
34
+ jsxTranspiler;
35
+ }
36
+
37
+ export async function compileBuildDirectory(srcDir, buildDir, root, envVars) {
38
+ const files = readdirSync(srcDir);
39
+ const filesToCompile = [];
40
+
41
+ for (const file of files) {
42
+ const srcPath = join(srcDir, file);
43
+ const stat = statSync(srcPath);
44
+
45
+ if (stat.isDirectory()) {
46
+ const subBuildDir = join(buildDir, file);
47
+ mkdirSync(subBuildDir, { recursive: true });
48
+ await compileBuildDirectory(srcPath, subBuildDir, root, envVars);
49
+ } else {
50
+ const ext = extname(file);
51
+ if (ext === '.css') continue;
52
+
53
+ if (['.jsx', '.tsx', '.ts'].includes(ext)) {
54
+ filesToCompile.push({ path: srcPath, dir: buildDir, name: file, type: 'tsx' });
55
+ } else if (ext === '.js') {
56
+ filesToCompile.push({ path: srcPath, dir: buildDir, name: file, type: 'js' });
57
+ }
58
+ }
59
+ }
60
+
61
+ if (filesToCompile.length === 0) return;
62
+
63
+ // ✅ Process sequentially with progress
64
+ logger.info(`📦 Compiling ${filesToCompile.length} files sequentially...`);
65
+
66
+ for (let i = 0; i < filesToCompile.length; i++) {
67
+ const file = filesToCompile[i];
68
+
69
+ try {
70
+ if (file.type === 'tsx') {
71
+ await compileBuildFile(file.path, file.dir, file.name, root, envVars);
72
+ } else {
73
+ await compileJSFile(file.path, file.dir, file.name, root, envVars);
74
+ }
75
+
76
+ // Progress every 10 files
77
+ if ((i + 1) % 10 === 0 || i === filesToCompile.length - 1) {
78
+ const percent = (((i + 1) / filesToCompile.length) * 100).toFixed(0);
79
+ logger.info(` Progress: ${i + 1}/${filesToCompile.length} (${percent}%)`);
80
+ }
81
+
82
+ } catch (error) {
83
+ logger.error(`Failed to compile ${file.name}: ${error.message}`);
84
+ // Continue with other files
85
+ }
86
+ }
87
+
88
+ logger.success(`✅ Compiled ${filesToCompile.length} files`);
89
+ }
90
+
91
+ async function compileBuildFile(srcPath, buildDir, filename, root, envVars) {
92
+ const ext = extname(filename);
93
+ const loader = ext === '.tsx' ? 'tsx' : ext === '.ts' ? 'ts' : 'jsx';
94
+
95
+ try {
96
+ let code = await Bun.file(srcPath).text();
97
+ code = removeCSSImports(code);
98
+ code = replaceEnvInCode(code, envVars);
99
+
100
+ const outFilename = filename.replace(/\.(jsx|tsx|ts)$/, '.js');
101
+ const outPath = join(buildDir, outFilename);
102
+ code = fixBuildImports(code, srcPath, outPath, root);
103
+
104
+ // ✅ Reuse the same transpiler instance
105
+ const transpiler = getTranspiler(loader);
106
+ let compiled = await transpiler.transform(code);
107
+
108
+ if (usesJSX(compiled) && !compiled.includes('import React')) {
109
+ compiled = `import React from 'react';\n${compiled}`;
110
+ }
111
+
112
+ compiled = fixRelativeImports(compiled);
113
+ await Bun.write(outPath, compiled);
114
+
115
+ // Help GC
116
+ code = null;
117
+ compiled = null;
118
+
119
+ } catch (error) {
120
+ logger.error(`Failed to compile ${filename}: ${error.message}`);
121
+ throw error;
122
+ }
123
+ }
124
+
125
+ async function compileJSFile(srcPath, buildDir, filename, root, envVars) {
126
+ const outPath = join(buildDir, filename);
127
+ let code = await Bun.file(srcPath).text();
128
+ code = removeCSSImports(code);
129
+ code = replaceEnvInCode(code, envVars);
130
+ code = fixBuildImports(code, srcPath, outPath, root);
131
+
132
+ if (usesJSX(code) && !code.includes('import React')) {
133
+ code = `import React from 'react';\n${code}`;
134
+ }
135
+
136
+ await Bun.write(outPath, code);
137
+ code = null;
138
+ }
139
+
140
+ function usesJSX(code) {
141
+ return code.includes('React.createElement') ||
142
+ code.includes('React.Fragment') ||
143
+ /<[A-Z]/.test(code) ||
144
+ code.includes('jsx(') ||
145
+ code.includes('jsxs(');
146
+ }
147
+
148
+ function removeCSSImports(code) {
149
+ code = code.replace(/import\s+['"][^'"]*\.css['"];?\s*/g, '');
150
+ code = code.replace(/import\s+['"]bertui\/styles['"]\s*;?\s*/g, '');
151
+ return code;
152
+ }
153
+
154
+ function fixBuildImports(code, srcPath, outPath, root) {
155
+ const buildDir = join(root, '.bertuibuild');
156
+ const routerPath = join(buildDir, 'router.js');
157
+ const relativeToRouter = relative(dirname(outPath), routerPath).replace(/\\/g, '/');
158
+ const routerImport = relativeToRouter.startsWith('.') ? relativeToRouter : './' + relativeToRouter;
159
+
160
+ code = code.replace(/from\s+['"]bertui\/router['"]/g, `from '${routerImport}'`);
161
+ return code;
162
+ }
163
+
164
+ function fixRelativeImports(code) {
165
+ const importRegex = /from\s+['"](\.\.[\/\\]|\.\/)((?:[^'"]+?)(?<!\.js|\.jsx|\.ts|\.tsx|\.json))['"];?/g;
166
+ code = code.replace(importRegex, (match, prefix, path) => {
167
+ if (path.endsWith('/') || /\.\w+$/.test(path)) return match;
168
+ return `from '${prefix}${path}.js';`;
169
+ });
170
+ return code;
171
+ }
@@ -0,0 +1,45 @@
1
+ // bertui/src/build/compiler/index.js
2
+ import { join } from 'path';
3
+ import { existsSync } from 'fs';
4
+ import logger from '../../logger/logger.js';
5
+ import { discoverRoutes } from './route-discoverer.js';
6
+ import { compileBuildDirectory } from './file-transpiler.js';
7
+ import { generateBuildRouter } from './router-generator.js';
8
+
9
+
10
+ export async function compileForBuild(root, buildDir, envVars) {
11
+ const srcDir = join(root, 'src');
12
+ const pagesDir = join(srcDir, 'pages');
13
+
14
+ if (!existsSync(srcDir)) {
15
+ throw new Error('src/ directory not found!');
16
+ }
17
+
18
+ let routes = [];
19
+ let serverIslands = [];
20
+ let clientRoutes = [];
21
+
22
+ if (existsSync(pagesDir)) {
23
+ routes = await discoverRoutes(pagesDir);
24
+
25
+ for (const route of routes) {
26
+ const sourceCode = await Bun.file(route.path).text();
27
+ const isServerIsland = sourceCode.includes('export const render = "server"');
28
+
29
+ if (isServerIsland) {
30
+ serverIslands.push(route);
31
+ logger.success(`🏝️ Server Island: ${route.route}`);
32
+ } else {
33
+ clientRoutes.push(route);
34
+ }
35
+ }
36
+ }
37
+
38
+ await compileBuildDirectory(srcDir, buildDir, root, envVars);
39
+
40
+ if (routes.length > 0) {
41
+ await generateBuildRouter(routes, buildDir);
42
+ }
43
+
44
+ return { routes, serverIslands, clientRoutes };
45
+ }
@@ -0,0 +1,46 @@
1
+ // bertui/src/build/compiler/route-discoverer.js
2
+ import { join, extname } from 'path';
3
+ import { readdirSync, statSync } from 'fs';
4
+
5
+ export async function discoverRoutes(pagesDir) {
6
+ const routes = [];
7
+
8
+ async function scanDirectory(dir, basePath = '') {
9
+ const entries = readdirSync(dir, { withFileTypes: true });
10
+
11
+ for (const entry of entries) {
12
+ const fullPath = join(dir, entry.name);
13
+ const relativePath = join(basePath, entry.name);
14
+
15
+ if (entry.isDirectory()) {
16
+ await scanDirectory(fullPath, relativePath);
17
+ } else if (entry.isFile()) {
18
+ const ext = extname(entry.name);
19
+ if (ext === '.css') continue;
20
+
21
+ if (['.jsx', '.tsx', '.js', '.ts'].includes(ext)) {
22
+ const fileName = entry.name.replace(ext, '');
23
+ let route = '/' + relativePath.replace(/\\/g, '/').replace(ext, '');
24
+
25
+ if (fileName === 'index') {
26
+ route = route.replace('/index', '') || '/';
27
+ }
28
+
29
+ const isDynamic = fileName.includes('[') && fileName.includes(']');
30
+
31
+ routes.push({
32
+ route: route === '' ? '/' : route,
33
+ file: relativePath.replace(/\\/g, '/'),
34
+ path: fullPath,
35
+ type: isDynamic ? 'dynamic' : 'static'
36
+ });
37
+ }
38
+ }
39
+ }
40
+ }
41
+
42
+ await scanDirectory(pagesDir);
43
+ routes.sort((a, b) => a.type === b.type ? a.route.localeCompare(b.route) : a.type === 'static' ? -1 : 1);
44
+
45
+ return routes;
46
+ }
@@ -0,0 +1,104 @@
1
+ // bertui/src/build/compiler/router-generator.js
2
+ import { join } from 'path';
3
+
4
+ export async function generateBuildRouter(routes, buildDir) {
5
+ const imports = routes.map((route, i) => {
6
+ const componentName = `Page${i}`;
7
+ const importPath = `./pages/${route.file.replace(/\.(jsx|tsx|ts)$/, '.js')}`;
8
+ return `import ${componentName} from '${importPath}';`;
9
+ }).join('\n');
10
+
11
+ const routeConfigs = routes.map((route, i) => {
12
+ const componentName = `Page${i}`;
13
+ return ` { path: '${route.route}', component: ${componentName}, type: '${route.type}' }`;
14
+ }).join(',\n');
15
+
16
+ const routerCode = `import React, { useState, useEffect, createContext, useContext } from 'react';
17
+
18
+ const RouterContext = createContext(null);
19
+
20
+ export function useRouter() {
21
+ const context = useContext(RouterContext);
22
+ if (!context) throw new Error('useRouter must be used within a Router');
23
+ return context;
24
+ }
25
+
26
+ export function Router({ routes }) {
27
+ const [currentRoute, setCurrentRoute] = useState(null);
28
+ const [params, setParams] = useState({});
29
+
30
+ useEffect(() => {
31
+ matchAndSetRoute(window.location.pathname);
32
+ const handlePopState = () => matchAndSetRoute(window.location.pathname);
33
+ window.addEventListener('popstate', handlePopState);
34
+ return () => window.removeEventListener('popstate', handlePopState);
35
+ }, [routes]);
36
+
37
+ function matchAndSetRoute(pathname) {
38
+ for (const route of routes) {
39
+ if (route.type === 'static' && route.path === pathname) {
40
+ setCurrentRoute(route);
41
+ setParams({});
42
+ return;
43
+ }
44
+ }
45
+ for (const route of routes) {
46
+ if (route.type === 'dynamic') {
47
+ const pattern = route.path.replace(/\\[([^\\]]+)\\]/g, '([^/]+)');
48
+ const regex = new RegExp('^' + pattern + '$');
49
+ const match = pathname.match(regex);
50
+ if (match) {
51
+ const paramNames = [...route.path.matchAll(/\\[([^\\]]+)\\]/g)].map(m => m[1]);
52
+ const extractedParams = {};
53
+ paramNames.forEach((name, i) => { extractedParams[name] = match[i + 1]; });
54
+ setCurrentRoute(route);
55
+ setParams(extractedParams);
56
+ return;
57
+ }
58
+ }
59
+ }
60
+ setCurrentRoute(null);
61
+ setParams({});
62
+ }
63
+
64
+ function navigate(path) {
65
+ window.history.pushState({}, '', path);
66
+ matchAndSetRoute(path);
67
+ }
68
+
69
+ const Component = currentRoute?.component;
70
+ return React.createElement(
71
+ RouterContext.Provider,
72
+ { value: { currentRoute, params, navigate, pathname: window.location.pathname } },
73
+ Component ? React.createElement(Component, { params }) : React.createElement(NotFound)
74
+ );
75
+ }
76
+
77
+ export function Link({ to, children, ...props }) {
78
+ const { navigate } = useRouter();
79
+ return React.createElement('a', {
80
+ href: to,
81
+ onClick: (e) => { e.preventDefault(); navigate(to); },
82
+ ...props
83
+ }, children);
84
+ }
85
+
86
+ function NotFound() {
87
+ return React.createElement('div', {
88
+ style: { display: 'flex', flexDirection: 'column', alignItems: 'center',
89
+ justifyContent: 'center', minHeight: '100vh', fontFamily: 'system-ui' }
90
+ },
91
+ React.createElement('h1', { style: { fontSize: '6rem', margin: 0 } }, '404'),
92
+ React.createElement('p', { style: { fontSize: '1.5rem', color: '#666' } }, 'Page not found'),
93
+ React.createElement('a', { href: '/', style: { color: '#10b981', textDecoration: 'none' } }, 'Go home')
94
+ );
95
+ }
96
+
97
+ ${imports}
98
+
99
+ export const routes = [
100
+ ${routeConfigs}
101
+ ];`;
102
+
103
+ await Bun.write(join(buildDir, 'router.js'), routerCode);
104
+ }