bertui 0.2.6 → 0.2.8
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 +1 -1
- package/src/build.js +354 -18
- package/src/client/compiler.js +5 -353
- package/src/server/dev-server.js +2 -19
package/package.json
CHANGED
package/src/build.js
CHANGED
|
@@ -1,52 +1,64 @@
|
|
|
1
1
|
// src/build.js
|
|
2
2
|
import { join } from 'path';
|
|
3
|
-
import { existsSync, mkdirSync, rmSync, cpSync } from 'fs';
|
|
3
|
+
import { existsSync, mkdirSync, rmSync, cpSync, readdirSync, statSync } from 'fs';
|
|
4
|
+
import { extname, relative, dirname } from 'path';
|
|
4
5
|
import logger from './logger/logger.js';
|
|
5
6
|
import { buildCSS } from './build/css-builder.js';
|
|
6
7
|
|
|
7
8
|
export async function buildProduction(options = {}) {
|
|
8
9
|
const root = options.root || process.cwd();
|
|
10
|
+
const buildDir = join(root, '.bertuibuild');
|
|
9
11
|
const outDir = join(root, 'dist');
|
|
10
12
|
|
|
11
13
|
logger.bigLog('BUILDING FOR PRODUCTION', { color: 'green' });
|
|
12
14
|
|
|
13
|
-
// Clean
|
|
15
|
+
// Clean folders
|
|
16
|
+
if (existsSync(buildDir)) {
|
|
17
|
+
rmSync(buildDir, { recursive: true });
|
|
18
|
+
}
|
|
14
19
|
if (existsSync(outDir)) {
|
|
15
20
|
rmSync(outDir, { recursive: true });
|
|
16
21
|
logger.info('Cleaned dist/');
|
|
17
22
|
}
|
|
23
|
+
|
|
24
|
+
mkdirSync(buildDir, { recursive: true });
|
|
18
25
|
mkdirSync(outDir, { recursive: true });
|
|
19
26
|
|
|
20
27
|
const startTime = Date.now();
|
|
21
28
|
|
|
22
29
|
try {
|
|
23
|
-
// Step 1:
|
|
24
|
-
logger.info('Step 1:
|
|
30
|
+
// Step 1: Compile for production
|
|
31
|
+
logger.info('Step 1: Compiling for production...');
|
|
32
|
+
await compileForBuild(root, buildDir);
|
|
33
|
+
logger.success('Production compilation complete');
|
|
34
|
+
|
|
35
|
+
// Step 2: Build CSS from BertUI library
|
|
36
|
+
logger.info('Step 2: Building CSS...');
|
|
25
37
|
const bertuiCssSource = join(import.meta.dir, 'styles/bertui.css');
|
|
26
38
|
const bertuiCssDest = join(outDir, 'styles/bertui.min.css');
|
|
27
39
|
await buildCSS(bertuiCssSource, bertuiCssDest);
|
|
28
40
|
|
|
29
|
-
// Step
|
|
41
|
+
// Step 3: Copy public assets if they exist
|
|
30
42
|
const publicDir = join(root, 'public');
|
|
31
43
|
if (existsSync(publicDir)) {
|
|
32
|
-
logger.info('Step
|
|
44
|
+
logger.info('Step 3: Copying public assets...');
|
|
33
45
|
cpSync(publicDir, outDir, { recursive: true });
|
|
34
46
|
logger.success('Public assets copied');
|
|
35
47
|
} else {
|
|
36
|
-
logger.info('Step
|
|
48
|
+
logger.info('Step 3: No public directory found, skipping...');
|
|
37
49
|
}
|
|
38
50
|
|
|
39
|
-
// Step
|
|
40
|
-
logger.info('Step
|
|
41
|
-
const
|
|
51
|
+
// Step 4: Build JavaScript with Bun's bundler
|
|
52
|
+
logger.info('Step 4: Bundling JavaScript...');
|
|
53
|
+
const buildEntry = join(buildDir, 'main.js');
|
|
42
54
|
|
|
43
|
-
if (!existsSync(
|
|
44
|
-
logger.error('
|
|
55
|
+
if (!existsSync(buildEntry)) {
|
|
56
|
+
logger.error('Build entry point not found: .bertuibuild/main.js');
|
|
45
57
|
process.exit(1);
|
|
46
58
|
}
|
|
47
59
|
|
|
48
60
|
const result = await Bun.build({
|
|
49
|
-
entrypoints: [
|
|
61
|
+
entrypoints: [buildEntry],
|
|
50
62
|
outdir: join(outDir, 'assets'),
|
|
51
63
|
target: 'browser',
|
|
52
64
|
minify: true,
|
|
@@ -57,7 +69,7 @@ export async function buildProduction(options = {}) {
|
|
|
57
69
|
chunk: 'chunks/[name]-[hash].js',
|
|
58
70
|
asset: '[name]-[hash].[ext]'
|
|
59
71
|
},
|
|
60
|
-
external: [
|
|
72
|
+
external: ['react', 'react-dom']
|
|
61
73
|
});
|
|
62
74
|
|
|
63
75
|
if (!result.success) {
|
|
@@ -68,10 +80,14 @@ export async function buildProduction(options = {}) {
|
|
|
68
80
|
|
|
69
81
|
logger.success('JavaScript bundled');
|
|
70
82
|
|
|
71
|
-
// Step
|
|
72
|
-
logger.info('Step
|
|
83
|
+
// Step 5: Generate index.html
|
|
84
|
+
logger.info('Step 5: Generating index.html...');
|
|
73
85
|
await generateProductionHTML(root, outDir, result);
|
|
74
86
|
|
|
87
|
+
// Step 6: Clean up build folder
|
|
88
|
+
rmSync(buildDir, { recursive: true });
|
|
89
|
+
logger.info('Cleaned up .bertuibuild/');
|
|
90
|
+
|
|
75
91
|
const duration = Date.now() - startTime;
|
|
76
92
|
logger.success(`✨ Build complete in ${duration}ms`);
|
|
77
93
|
logger.info(`📦 Output: ${outDir}`);
|
|
@@ -87,12 +103,324 @@ export async function buildProduction(options = {}) {
|
|
|
87
103
|
if (error.stack) {
|
|
88
104
|
logger.error(error.stack);
|
|
89
105
|
}
|
|
106
|
+
|
|
107
|
+
// Clean up on error
|
|
108
|
+
if (existsSync(buildDir)) {
|
|
109
|
+
rmSync(buildDir, { recursive: true });
|
|
110
|
+
}
|
|
111
|
+
|
|
90
112
|
process.exit(1);
|
|
91
113
|
}
|
|
92
114
|
}
|
|
93
115
|
|
|
116
|
+
async function compileForBuild(root, buildDir) {
|
|
117
|
+
const srcDir = join(root, 'src');
|
|
118
|
+
const pagesDir = join(srcDir, 'pages');
|
|
119
|
+
|
|
120
|
+
if (!existsSync(srcDir)) {
|
|
121
|
+
throw new Error('src/ directory not found!');
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Discover routes
|
|
125
|
+
let routes = [];
|
|
126
|
+
if (existsSync(pagesDir)) {
|
|
127
|
+
routes = await discoverRoutes(pagesDir);
|
|
128
|
+
logger.info(`Found ${routes.length} routes`);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Compile all source files
|
|
132
|
+
await compileBuildDirectory(srcDir, buildDir, root);
|
|
133
|
+
|
|
134
|
+
// Generate router if we have routes
|
|
135
|
+
if (routes.length > 0) {
|
|
136
|
+
await generateBuildRouter(routes, buildDir);
|
|
137
|
+
logger.info('Generated router for build');
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
async function discoverRoutes(pagesDir) {
|
|
142
|
+
const routes = [];
|
|
143
|
+
|
|
144
|
+
async function scanDirectory(dir, basePath = '') {
|
|
145
|
+
const entries = readdirSync(dir, { withFileTypes: true });
|
|
146
|
+
|
|
147
|
+
for (const entry of entries) {
|
|
148
|
+
const fullPath = join(dir, entry.name);
|
|
149
|
+
const relativePath = join(basePath, entry.name);
|
|
150
|
+
|
|
151
|
+
if (entry.isDirectory()) {
|
|
152
|
+
await scanDirectory(fullPath, relativePath);
|
|
153
|
+
} else if (entry.isFile()) {
|
|
154
|
+
const ext = extname(entry.name);
|
|
155
|
+
if (['.jsx', '.tsx', '.js', '.ts'].includes(ext)) {
|
|
156
|
+
const fileName = entry.name.replace(ext, '');
|
|
157
|
+
|
|
158
|
+
let route = '/' + relativePath.replace(/\\/g, '/').replace(ext, '');
|
|
159
|
+
|
|
160
|
+
if (fileName === 'index') {
|
|
161
|
+
route = route.replace('/index', '') || '/';
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const isDynamic = fileName.includes('[') && fileName.includes(']');
|
|
165
|
+
const type = isDynamic ? 'dynamic' : 'static';
|
|
166
|
+
|
|
167
|
+
routes.push({
|
|
168
|
+
route: route === '' ? '/' : route,
|
|
169
|
+
file: relativePath.replace(/\\/g, '/'),
|
|
170
|
+
path: fullPath,
|
|
171
|
+
type
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
await scanDirectory(pagesDir);
|
|
179
|
+
|
|
180
|
+
routes.sort((a, b) => {
|
|
181
|
+
if (a.type === b.type) {
|
|
182
|
+
return a.route.localeCompare(b.route);
|
|
183
|
+
}
|
|
184
|
+
return a.type === 'static' ? -1 : 1;
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
return routes;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
async function generateBuildRouter(routes, buildDir) {
|
|
191
|
+
const imports = routes.map((route, i) => {
|
|
192
|
+
const componentName = `Page${i}`;
|
|
193
|
+
const importPath = `./pages/${route.file.replace(/\.(jsx|tsx|ts)$/, '.js')}`;
|
|
194
|
+
return `import ${componentName} from '${importPath}';`;
|
|
195
|
+
}).join('\n');
|
|
196
|
+
|
|
197
|
+
const routeConfigs = routes.map((route, i) => {
|
|
198
|
+
const componentName = `Page${i}`;
|
|
199
|
+
return ` { path: '${route.route}', component: ${componentName}, type: '${route.type}' }`;
|
|
200
|
+
}).join(',\n');
|
|
201
|
+
|
|
202
|
+
const routerCode = `import React, { useState, useEffect, createContext, useContext } from 'react';
|
|
203
|
+
|
|
204
|
+
const RouterContext = createContext(null);
|
|
205
|
+
|
|
206
|
+
export function useRouter() {
|
|
207
|
+
const context = useContext(RouterContext);
|
|
208
|
+
if (!context) {
|
|
209
|
+
throw new Error('useRouter must be used within a Router component');
|
|
210
|
+
}
|
|
211
|
+
return context;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export function Router({ routes }) {
|
|
215
|
+
const [currentRoute, setCurrentRoute] = useState(null);
|
|
216
|
+
const [params, setParams] = useState({});
|
|
217
|
+
|
|
218
|
+
useEffect(() => {
|
|
219
|
+
matchAndSetRoute(window.location.pathname);
|
|
220
|
+
|
|
221
|
+
const handlePopState = () => {
|
|
222
|
+
matchAndSetRoute(window.location.pathname);
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
window.addEventListener('popstate', handlePopState);
|
|
226
|
+
return () => window.removeEventListener('popstate', handlePopState);
|
|
227
|
+
}, [routes]);
|
|
228
|
+
|
|
229
|
+
function matchAndSetRoute(pathname) {
|
|
230
|
+
for (const route of routes) {
|
|
231
|
+
if (route.type === 'static' && route.path === pathname) {
|
|
232
|
+
setCurrentRoute(route);
|
|
233
|
+
setParams({});
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
for (const route of routes) {
|
|
239
|
+
if (route.type === 'dynamic') {
|
|
240
|
+
const pattern = route.path.replace(/\\[([^\\]]+)\\]/g, '([^/]+)');
|
|
241
|
+
const regex = new RegExp('^' + pattern + '$');
|
|
242
|
+
const match = pathname.match(regex);
|
|
243
|
+
|
|
244
|
+
if (match) {
|
|
245
|
+
const paramNames = [...route.path.matchAll(/\\[([^\\]]+)\\]/g)].map(m => m[1]);
|
|
246
|
+
const extractedParams = {};
|
|
247
|
+
paramNames.forEach((name, i) => {
|
|
248
|
+
extractedParams[name] = match[i + 1];
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
setCurrentRoute(route);
|
|
252
|
+
setParams(extractedParams);
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
setCurrentRoute(null);
|
|
259
|
+
setParams({});
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function navigate(path) {
|
|
263
|
+
window.history.pushState({}, '', path);
|
|
264
|
+
matchAndSetRoute(path);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const routerValue = {
|
|
268
|
+
currentRoute,
|
|
269
|
+
params,
|
|
270
|
+
navigate,
|
|
271
|
+
pathname: window.location.pathname
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
const Component = currentRoute?.component;
|
|
275
|
+
|
|
276
|
+
return React.createElement(
|
|
277
|
+
RouterContext.Provider,
|
|
278
|
+
{ value: routerValue },
|
|
279
|
+
Component ? React.createElement(Component, { params }) : React.createElement(NotFound, null)
|
|
280
|
+
);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
export function Link({ to, children, ...props }) {
|
|
284
|
+
const { navigate } = useRouter();
|
|
285
|
+
|
|
286
|
+
function handleClick(e) {
|
|
287
|
+
e.preventDefault();
|
|
288
|
+
navigate(to);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
return React.createElement('a', { href: to, onClick: handleClick, ...props }, children);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function NotFound() {
|
|
295
|
+
return React.createElement(
|
|
296
|
+
'div',
|
|
297
|
+
{
|
|
298
|
+
style: {
|
|
299
|
+
display: 'flex',
|
|
300
|
+
flexDirection: 'column',
|
|
301
|
+
alignItems: 'center',
|
|
302
|
+
justifyContent: 'center',
|
|
303
|
+
minHeight: '100vh',
|
|
304
|
+
fontFamily: 'system-ui'
|
|
305
|
+
}
|
|
306
|
+
},
|
|
307
|
+
React.createElement('h1', { style: { fontSize: '6rem', margin: 0 } }, '404'),
|
|
308
|
+
React.createElement('p', { style: { fontSize: '1.5rem', color: '#666' } }, 'Page not found'),
|
|
309
|
+
React.createElement('a', {
|
|
310
|
+
href: '/',
|
|
311
|
+
style: { color: '#10b981', textDecoration: 'none', fontSize: '1.2rem' }
|
|
312
|
+
}, 'Go home')
|
|
313
|
+
);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
${imports}
|
|
317
|
+
|
|
318
|
+
export const routes = [
|
|
319
|
+
${routeConfigs}
|
|
320
|
+
];
|
|
321
|
+
`;
|
|
322
|
+
|
|
323
|
+
await Bun.write(join(buildDir, 'router.js'), routerCode);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
async function compileBuildDirectory(srcDir, buildDir, root) {
|
|
327
|
+
const files = readdirSync(srcDir);
|
|
328
|
+
|
|
329
|
+
for (const file of files) {
|
|
330
|
+
const srcPath = join(srcDir, file);
|
|
331
|
+
const stat = statSync(srcPath);
|
|
332
|
+
|
|
333
|
+
if (stat.isDirectory()) {
|
|
334
|
+
const subBuildDir = join(buildDir, file);
|
|
335
|
+
mkdirSync(subBuildDir, { recursive: true });
|
|
336
|
+
await compileBuildDirectory(srcPath, subBuildDir, root);
|
|
337
|
+
} else {
|
|
338
|
+
const ext = extname(file);
|
|
339
|
+
|
|
340
|
+
if (['.jsx', '.tsx', '.ts'].includes(ext)) {
|
|
341
|
+
await compileBuildFile(srcPath, buildDir, file, root);
|
|
342
|
+
} else if (ext === '.js') {
|
|
343
|
+
const outPath = join(buildDir, file);
|
|
344
|
+
let code = await Bun.file(srcPath).text();
|
|
345
|
+
code = fixBuildImports(code, srcPath, outPath, root);
|
|
346
|
+
await Bun.write(outPath, code);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
async function compileBuildFile(srcPath, buildDir, filename, root) {
|
|
353
|
+
const ext = extname(filename);
|
|
354
|
+
const loader = ext === '.tsx' ? 'tsx' : ext === '.ts' ? 'ts' : 'jsx';
|
|
355
|
+
|
|
356
|
+
try {
|
|
357
|
+
let code = await Bun.file(srcPath).text();
|
|
358
|
+
|
|
359
|
+
const outFilename = filename.replace(/\.(jsx|tsx|ts)$/, '.js');
|
|
360
|
+
const outPath = join(buildDir, outFilename);
|
|
361
|
+
|
|
362
|
+
code = fixBuildImports(code, srcPath, outPath, root);
|
|
363
|
+
|
|
364
|
+
const transpiler = new Bun.Transpiler({
|
|
365
|
+
loader,
|
|
366
|
+
tsconfig: {
|
|
367
|
+
compilerOptions: {
|
|
368
|
+
jsx: 'react',
|
|
369
|
+
jsxFactory: 'React.createElement',
|
|
370
|
+
jsxFragmentFactory: 'React.Fragment'
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
let compiled = await transpiler.transform(code);
|
|
376
|
+
|
|
377
|
+
if (!compiled.includes('import React') && (compiled.includes('React.createElement') || compiled.includes('React.Fragment'))) {
|
|
378
|
+
compiled = `import React from 'react';\n${compiled}`;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
compiled = fixRelativeImports(compiled);
|
|
382
|
+
|
|
383
|
+
await Bun.write(outPath, compiled);
|
|
384
|
+
} catch (error) {
|
|
385
|
+
logger.error(`Failed to compile ${filename}: ${error.message}`);
|
|
386
|
+
throw error;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function fixBuildImports(code, srcPath, outPath, root) {
|
|
391
|
+
// Remove bertui/styles imports
|
|
392
|
+
code = code.replace(/import\s+['"]bertui\/styles['"]\s*;?\s*/g, '');
|
|
393
|
+
|
|
394
|
+
const buildDir = join(root, '.bertuibuild');
|
|
395
|
+
const routerPath = join(buildDir, 'router.js');
|
|
396
|
+
|
|
397
|
+
// Calculate relative path from output file to router.js
|
|
398
|
+
const relativeToRouter = relative(dirname(outPath), routerPath).replace(/\\/g, '/');
|
|
399
|
+
const routerImport = relativeToRouter.startsWith('.') ? relativeToRouter : './' + relativeToRouter;
|
|
400
|
+
|
|
401
|
+
// Replace bertui/router imports
|
|
402
|
+
code = code.replace(
|
|
403
|
+
/from\s+['"]bertui\/router['"]/g,
|
|
404
|
+
`from '${routerImport}'`
|
|
405
|
+
);
|
|
406
|
+
|
|
407
|
+
return code;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
function fixRelativeImports(code) {
|
|
411
|
+
const importRegex = /from\s+['"](\.\.[\/\\]|\.\/)((?:[^'"]+?)(?<!\.js|\.jsx|\.ts|\.tsx|\.json))['"];?/g;
|
|
412
|
+
|
|
413
|
+
code = code.replace(importRegex, (match, prefix, path) => {
|
|
414
|
+
if (path.endsWith('/') || /\.\w+$/.test(path)) {
|
|
415
|
+
return match;
|
|
416
|
+
}
|
|
417
|
+
return `from '${prefix}${path}.js';`;
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
return code;
|
|
421
|
+
}
|
|
422
|
+
|
|
94
423
|
async function generateProductionHTML(root, outDir, buildResult) {
|
|
95
|
-
// Find the main bundle
|
|
96
424
|
const mainBundle = buildResult.outputs.find(o =>
|
|
97
425
|
o.path.includes('main') && o.kind === 'entry-point'
|
|
98
426
|
);
|
|
@@ -111,6 +439,14 @@ async function generateProductionHTML(root, outDir, buildResult) {
|
|
|
111
439
|
<meta name="description" content="Built with BertUI - Lightning fast React development">
|
|
112
440
|
<title>BertUI App</title>
|
|
113
441
|
<link rel="stylesheet" href="/styles/bertui.min.css">
|
|
442
|
+
<script type="importmap">
|
|
443
|
+
{
|
|
444
|
+
"imports": {
|
|
445
|
+
"react": "https://esm.sh/react@18.2.0",
|
|
446
|
+
"react-dom": "https://esm.sh/react-dom@18.2.0"
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
</script>
|
|
114
450
|
</head>
|
|
115
451
|
<body>
|
|
116
452
|
<div id="root"></div>
|
|
@@ -120,4 +456,4 @@ async function generateProductionHTML(root, outDir, buildResult) {
|
|
|
120
456
|
|
|
121
457
|
await Bun.write(join(outDir, 'index.html'), html);
|
|
122
458
|
logger.success('Generated index.html');
|
|
123
|
-
}
|
|
459
|
+
}
|
package/src/client/compiler.js
CHANGED
|
@@ -1,349 +1,3 @@
|
|
|
1
|
-
// import { existsSync, mkdirSync, readdirSync, statSync } from 'fs';
|
|
2
|
-
// import { join, extname, relative } from 'path';
|
|
3
|
-
// import logger from '../logger/logger.js';
|
|
4
|
-
|
|
5
|
-
// export async function compileProject(root) {
|
|
6
|
-
// logger.bigLog('COMPILING PROJECT', { color: 'blue' });
|
|
7
|
-
|
|
8
|
-
// const srcDir = join(root, 'src');
|
|
9
|
-
// const pagesDir = join(srcDir, 'pages');
|
|
10
|
-
// const outDir = join(root, '.bertui', 'compiled');
|
|
11
|
-
|
|
12
|
-
// if (!existsSync(srcDir)) {
|
|
13
|
-
// logger.error('src/ directory not found!');
|
|
14
|
-
// process.exit(1);
|
|
15
|
-
// }
|
|
16
|
-
|
|
17
|
-
// if (!existsSync(outDir)) {
|
|
18
|
-
// mkdirSync(outDir, { recursive: true });
|
|
19
|
-
// logger.info('Created .bertui/compiled/');
|
|
20
|
-
// }
|
|
21
|
-
|
|
22
|
-
// let routes = [];
|
|
23
|
-
// if (existsSync(pagesDir)) {
|
|
24
|
-
// routes = await discoverRoutes(pagesDir);
|
|
25
|
-
// logger.info(`Discovered ${routes.length} routes`);
|
|
26
|
-
|
|
27
|
-
// if (routes.length > 0) {
|
|
28
|
-
// logger.bigLog('ROUTES DISCOVERED', { color: 'blue' });
|
|
29
|
-
// logger.table(routes.map((r, i) => ({
|
|
30
|
-
// '': i,
|
|
31
|
-
// route: r.route,
|
|
32
|
-
// file: r.file,
|
|
33
|
-
// type: r.type
|
|
34
|
-
// })));
|
|
35
|
-
// }
|
|
36
|
-
// }
|
|
37
|
-
|
|
38
|
-
// const startTime = Date.now();
|
|
39
|
-
// const stats = await compileDirectory(srcDir, outDir, root);
|
|
40
|
-
// const duration = Date.now() - startTime;
|
|
41
|
-
|
|
42
|
-
// if (routes.length > 0) {
|
|
43
|
-
// await generateRouter(routes, outDir, root);
|
|
44
|
-
// logger.info('Generated router.js');
|
|
45
|
-
// }
|
|
46
|
-
|
|
47
|
-
// logger.success(`Compiled ${stats.files} files in ${duration}ms`);
|
|
48
|
-
// logger.info(`Output: ${outDir}`);
|
|
49
|
-
|
|
50
|
-
// return { outDir, stats, routes };
|
|
51
|
-
// }
|
|
52
|
-
|
|
53
|
-
// async function discoverRoutes(pagesDir) {
|
|
54
|
-
// const routes = [];
|
|
55
|
-
|
|
56
|
-
// async function scanDirectory(dir, basePath = '') {
|
|
57
|
-
// const entries = readdirSync(dir, { withFileTypes: true });
|
|
58
|
-
|
|
59
|
-
// for (const entry of entries) {
|
|
60
|
-
// const fullPath = join(dir, entry.name);
|
|
61
|
-
// const relativePath = join(basePath, entry.name);
|
|
62
|
-
|
|
63
|
-
// if (entry.isDirectory()) {
|
|
64
|
-
// await scanDirectory(fullPath, relativePath);
|
|
65
|
-
// } else if (entry.isFile()) {
|
|
66
|
-
// const ext = extname(entry.name);
|
|
67
|
-
// if (['.jsx', '.tsx', '.js', '.ts'].includes(ext)) {
|
|
68
|
-
// const fileName = entry.name.replace(ext, '');
|
|
69
|
-
|
|
70
|
-
// let route = '/' + relativePath.replace(/\\/g, '/').replace(ext, '');
|
|
71
|
-
|
|
72
|
-
// if (fileName === 'index') {
|
|
73
|
-
// route = route.replace('/index', '') || '/';
|
|
74
|
-
// }
|
|
75
|
-
|
|
76
|
-
// const isDynamic = fileName.includes('[') && fileName.includes(']');
|
|
77
|
-
// const type = isDynamic ? 'dynamic' : 'static';
|
|
78
|
-
|
|
79
|
-
// routes.push({
|
|
80
|
-
// route: route === '' ? '/' : route,
|
|
81
|
-
// file: relativePath.replace(/\\/g, '/'),
|
|
82
|
-
// path: fullPath,
|
|
83
|
-
// type
|
|
84
|
-
// });
|
|
85
|
-
// }
|
|
86
|
-
// }
|
|
87
|
-
// }
|
|
88
|
-
// }
|
|
89
|
-
|
|
90
|
-
// await scanDirectory(pagesDir);
|
|
91
|
-
|
|
92
|
-
// routes.sort((a, b) => {
|
|
93
|
-
// if (a.type === b.type) {
|
|
94
|
-
// return a.route.localeCompare(b.route);
|
|
95
|
-
// }
|
|
96
|
-
// return a.type === 'static' ? -1 : 1;
|
|
97
|
-
// });
|
|
98
|
-
|
|
99
|
-
// return routes;
|
|
100
|
-
// }
|
|
101
|
-
|
|
102
|
-
// async function generateRouter(routes, outDir, root) {
|
|
103
|
-
// const imports = routes.map((route, i) => {
|
|
104
|
-
// const componentName = `Page${i}`;
|
|
105
|
-
// const importPath = `./pages/${route.file.replace(/\.(jsx|tsx|ts)$/, '.js')}`;
|
|
106
|
-
// return `import ${componentName} from '${importPath}';`;
|
|
107
|
-
// }).join('\n');
|
|
108
|
-
|
|
109
|
-
// const routeConfigs = routes.map((route, i) => {
|
|
110
|
-
// const componentName = `Page${i}`;
|
|
111
|
-
// return ` { path: '${route.route}', component: ${componentName}, type: '${route.type}' }`;
|
|
112
|
-
// }).join(',\n');
|
|
113
|
-
|
|
114
|
-
// const routerComponentCode = `import React, { useState, useEffect, createContext, useContext } from 'react';
|
|
115
|
-
|
|
116
|
-
// const RouterContext = createContext(null);
|
|
117
|
-
|
|
118
|
-
// export function useRouter() {
|
|
119
|
-
// const context = useContext(RouterContext);
|
|
120
|
-
// if (!context) {
|
|
121
|
-
// throw new Error('useRouter must be used within a Router component');
|
|
122
|
-
// }
|
|
123
|
-
// return context;
|
|
124
|
-
// }
|
|
125
|
-
|
|
126
|
-
// export function Router({ routes }) {
|
|
127
|
-
// const [currentRoute, setCurrentRoute] = useState(null);
|
|
128
|
-
// const [params, setParams] = useState({});
|
|
129
|
-
|
|
130
|
-
// useEffect(() => {
|
|
131
|
-
// matchAndSetRoute(window.location.pathname);
|
|
132
|
-
|
|
133
|
-
// const handlePopState = () => {
|
|
134
|
-
// matchAndSetRoute(window.location.pathname);
|
|
135
|
-
// };
|
|
136
|
-
|
|
137
|
-
// window.addEventListener('popstate', handlePopState);
|
|
138
|
-
// return () => window.removeEventListener('popstate', handlePopState);
|
|
139
|
-
// }, [routes]);
|
|
140
|
-
|
|
141
|
-
// function matchAndSetRoute(pathname) {
|
|
142
|
-
// for (const route of routes) {
|
|
143
|
-
// if (route.type === 'static' && route.path === pathname) {
|
|
144
|
-
// setCurrentRoute(route);
|
|
145
|
-
// setParams({});
|
|
146
|
-
// return;
|
|
147
|
-
// }
|
|
148
|
-
// }
|
|
149
|
-
|
|
150
|
-
// for (const route of routes) {
|
|
151
|
-
// if (route.type === 'dynamic') {
|
|
152
|
-
// const pattern = route.path.replace(/\\[([^\\]]+)\\]/g, '([^/]+)');
|
|
153
|
-
// const regex = new RegExp('^' + pattern + '$');
|
|
154
|
-
// const match = pathname.match(regex);
|
|
155
|
-
|
|
156
|
-
// if (match) {
|
|
157
|
-
// const paramNames = [...route.path.matchAll(/\\[([^\\]]+)\\]/g)].map(m => m[1]);
|
|
158
|
-
// const extractedParams = {};
|
|
159
|
-
// paramNames.forEach((name, i) => {
|
|
160
|
-
// extractedParams[name] = match[i + 1];
|
|
161
|
-
// });
|
|
162
|
-
|
|
163
|
-
// setCurrentRoute(route);
|
|
164
|
-
// setParams(extractedParams);
|
|
165
|
-
// return;
|
|
166
|
-
// }
|
|
167
|
-
// }
|
|
168
|
-
// }
|
|
169
|
-
|
|
170
|
-
// setCurrentRoute(null);
|
|
171
|
-
// setParams({});
|
|
172
|
-
// }
|
|
173
|
-
|
|
174
|
-
// function navigate(path) {
|
|
175
|
-
// window.history.pushState({}, '', path);
|
|
176
|
-
// matchAndSetRoute(path);
|
|
177
|
-
// }
|
|
178
|
-
|
|
179
|
-
// const routerValue = {
|
|
180
|
-
// currentRoute,
|
|
181
|
-
// params,
|
|
182
|
-
// navigate,
|
|
183
|
-
// pathname: window.location.pathname
|
|
184
|
-
// };
|
|
185
|
-
|
|
186
|
-
// const Component = currentRoute?.component;
|
|
187
|
-
|
|
188
|
-
// return React.createElement(
|
|
189
|
-
// RouterContext.Provider,
|
|
190
|
-
// { value: routerValue },
|
|
191
|
-
// Component ? React.createElement(Component, { params }) : React.createElement(NotFound, null)
|
|
192
|
-
// );
|
|
193
|
-
// }
|
|
194
|
-
|
|
195
|
-
// export function Link({ to, children, ...props }) {
|
|
196
|
-
// const { navigate } = useRouter();
|
|
197
|
-
|
|
198
|
-
// function handleClick(e) {
|
|
199
|
-
// e.preventDefault();
|
|
200
|
-
// navigate(to);
|
|
201
|
-
// }
|
|
202
|
-
|
|
203
|
-
// return React.createElement('a', { href: to, onClick: handleClick, ...props }, children);
|
|
204
|
-
// }
|
|
205
|
-
|
|
206
|
-
// function NotFound() {
|
|
207
|
-
// return React.createElement(
|
|
208
|
-
// 'div',
|
|
209
|
-
// {
|
|
210
|
-
// style: {
|
|
211
|
-
// display: 'flex',
|
|
212
|
-
// flexDirection: 'column',
|
|
213
|
-
// alignItems: 'center',
|
|
214
|
-
// justifyContent: 'center',
|
|
215
|
-
// minHeight: '100vh',
|
|
216
|
-
// fontFamily: 'system-ui'
|
|
217
|
-
// }
|
|
218
|
-
// },
|
|
219
|
-
// React.createElement('h1', { style: { fontSize: '6rem', margin: 0 } }, '404'),
|
|
220
|
-
// React.createElement('p', { style: { fontSize: '1.5rem', color: '#666' } }, 'Page not found'),
|
|
221
|
-
// React.createElement('a', {
|
|
222
|
-
// href: '/',
|
|
223
|
-
// style: { color: '#10b981', textDecoration: 'none', fontSize: '1.2rem' }
|
|
224
|
-
// }, 'Go home')
|
|
225
|
-
// );
|
|
226
|
-
// }
|
|
227
|
-
|
|
228
|
-
// ${imports}
|
|
229
|
-
|
|
230
|
-
// export const routes = [
|
|
231
|
-
// ${routeConfigs}
|
|
232
|
-
// ];
|
|
233
|
-
// `;
|
|
234
|
-
|
|
235
|
-
// const routerPath = join(outDir, 'router.js');
|
|
236
|
-
// await Bun.write(routerPath, routerComponentCode);
|
|
237
|
-
// }
|
|
238
|
-
|
|
239
|
-
// async function compileDirectory(srcDir, outDir, root) {
|
|
240
|
-
// const stats = { files: 0, skipped: 0 };
|
|
241
|
-
|
|
242
|
-
// const files = readdirSync(srcDir);
|
|
243
|
-
|
|
244
|
-
// for (const file of files) {
|
|
245
|
-
// const srcPath = join(srcDir, file);
|
|
246
|
-
// const stat = statSync(srcPath);
|
|
247
|
-
|
|
248
|
-
// if (stat.isDirectory()) {
|
|
249
|
-
// const subOutDir = join(outDir, file);
|
|
250
|
-
// mkdirSync(subOutDir, { recursive: true });
|
|
251
|
-
// const subStats = await compileDirectory(srcPath, subOutDir, root);
|
|
252
|
-
// stats.files += subStats.files;
|
|
253
|
-
// stats.skipped += subStats.skipped;
|
|
254
|
-
// } else {
|
|
255
|
-
// const ext = extname(file);
|
|
256
|
-
// const relativePath = relative(join(root, 'src'), srcPath);
|
|
257
|
-
|
|
258
|
-
// if (['.jsx', '.tsx', '.ts'].includes(ext)) {
|
|
259
|
-
// await compileFile(srcPath, outDir, file, relativePath);
|
|
260
|
-
// stats.files++;
|
|
261
|
-
// } else if (ext === '.js') {
|
|
262
|
-
// const outPath = join(outDir, file);
|
|
263
|
-
// let code = await Bun.file(srcPath).text();
|
|
264
|
-
|
|
265
|
-
// code = fixImports(code);
|
|
266
|
-
|
|
267
|
-
// await Bun.write(outPath, code);
|
|
268
|
-
// logger.debug(`Copied: ${relativePath}`);
|
|
269
|
-
// stats.files++;
|
|
270
|
-
// } else {
|
|
271
|
-
// logger.debug(`Skipped: ${relativePath}`);
|
|
272
|
-
// stats.skipped++;
|
|
273
|
-
// }
|
|
274
|
-
// }
|
|
275
|
-
// }
|
|
276
|
-
|
|
277
|
-
// return stats;
|
|
278
|
-
// }
|
|
279
|
-
|
|
280
|
-
// async function compileFile(srcPath, outDir, filename, relativePath) {
|
|
281
|
-
// const ext = extname(filename);
|
|
282
|
-
// const loader = ext === '.tsx' ? 'tsx' : ext === '.ts' ? 'ts' : 'jsx';
|
|
283
|
-
|
|
284
|
-
// try {
|
|
285
|
-
// let code = await Bun.file(srcPath).text();
|
|
286
|
-
|
|
287
|
-
// code = fixImports(code);
|
|
288
|
-
|
|
289
|
-
// const transpiler = new Bun.Transpiler({
|
|
290
|
-
// loader,
|
|
291
|
-
// tsconfig: {
|
|
292
|
-
// compilerOptions: {
|
|
293
|
-
// jsx: 'react',
|
|
294
|
-
// jsxFactory: 'React.createElement',
|
|
295
|
-
// jsxFragmentFactory: 'React.Fragment'
|
|
296
|
-
// }
|
|
297
|
-
// }
|
|
298
|
-
// });
|
|
299
|
-
// let compiled = await transpiler.transform(code);
|
|
300
|
-
|
|
301
|
-
// if (!compiled.includes('import React') && (compiled.includes('React.createElement') || compiled.includes('React.Fragment'))) {
|
|
302
|
-
// compiled = `import React from 'react';\n${compiled}`;
|
|
303
|
-
// }
|
|
304
|
-
|
|
305
|
-
// compiled = fixRelativeImports(compiled);
|
|
306
|
-
|
|
307
|
-
// const outFilename = filename.replace(/\.(jsx|tsx|ts)$/, '.js');
|
|
308
|
-
// const outPath = join(outDir, outFilename);
|
|
309
|
-
|
|
310
|
-
// await Bun.write(outPath, compiled);
|
|
311
|
-
// logger.debug(`Compiled: ${relativePath} → ${outFilename}`);
|
|
312
|
-
// } catch (error) {
|
|
313
|
-
// logger.error(`Failed to compile ${relativePath}: ${error.message}`);
|
|
314
|
-
// throw error;
|
|
315
|
-
// }
|
|
316
|
-
// }
|
|
317
|
-
|
|
318
|
-
// function fixImports(code) {
|
|
319
|
-
// code = code.replace(/import\s+['"]bertui\/styles['"]\s*;?\s*/g, '');
|
|
320
|
-
|
|
321
|
-
// code = code.replace(
|
|
322
|
-
// /from\s+['"]bertui\/router['"]/g,
|
|
323
|
-
// "from '/compiled/router.js'"
|
|
324
|
-
// );
|
|
325
|
-
|
|
326
|
-
// code = code.replace(
|
|
327
|
-
// /from\s+['"]\.\.\/\.bertui\/compiled\/([^'"]+)['"]/g,
|
|
328
|
-
// "from '/compiled/$1'"
|
|
329
|
-
// );
|
|
330
|
-
|
|
331
|
-
// return code;
|
|
332
|
-
// }
|
|
333
|
-
|
|
334
|
-
// function fixRelativeImports(code) {
|
|
335
|
-
// const importRegex = /from\s+['"](\.\.[\/\\]|\.\/)((?:[^'"]+?)(?<!\.js|\.jsx|\.ts|\.tsx|\.json))['"];?/g;
|
|
336
|
-
|
|
337
|
-
// code = code.replace(importRegex, (match, prefix, path) => {
|
|
338
|
-
// if (path.endsWith('/') || /\.\w+$/.test(path)) {
|
|
339
|
-
// return match;
|
|
340
|
-
// }
|
|
341
|
-
// return `from '${prefix}${path}.js';`;
|
|
342
|
-
// });
|
|
343
|
-
|
|
344
|
-
// return code;
|
|
345
|
-
// }
|
|
346
|
-
|
|
347
1
|
import { existsSync, mkdirSync, readdirSync, statSync } from 'fs';
|
|
348
2
|
import { join, extname, relative } from 'path';
|
|
349
3
|
import logger from '../logger/logger.js';
|
|
@@ -662,19 +316,16 @@ async function compileFile(srcPath, outDir, filename, relativePath) {
|
|
|
662
316
|
}
|
|
663
317
|
|
|
664
318
|
function fixImports(code) {
|
|
665
|
-
// Remove bertui/styles imports
|
|
666
319
|
code = code.replace(/import\s+['"]bertui\/styles['"]\s*;?\s*/g, '');
|
|
667
320
|
|
|
668
|
-
// Fix bertui/router imports to use relative path to compiled router
|
|
669
321
|
code = code.replace(
|
|
670
322
|
/from\s+['"]bertui\/router['"]/g,
|
|
671
|
-
"from '
|
|
323
|
+
"from '/compiled/router.js'"
|
|
672
324
|
);
|
|
673
325
|
|
|
674
|
-
// Also handle any absolute /compiled/ paths
|
|
675
326
|
code = code.replace(
|
|
676
|
-
/from\s+['"]\/compiled\/
|
|
677
|
-
"from '
|
|
327
|
+
/from\s+['"]\.\.\/\.bertui\/compiled\/([^'"]+)['"]/g,
|
|
328
|
+
"from '/compiled/$1'"
|
|
678
329
|
);
|
|
679
330
|
|
|
680
331
|
return code;
|
|
@@ -691,4 +342,5 @@ function fixRelativeImports(code) {
|
|
|
691
342
|
});
|
|
692
343
|
|
|
693
344
|
return code;
|
|
694
|
-
}
|
|
345
|
+
}
|
|
346
|
+
|
package/src/server/dev-server.js
CHANGED
|
@@ -31,24 +31,7 @@ export async function startDevServer(options = {}) {
|
|
|
31
31
|
const path = params['*'];
|
|
32
32
|
|
|
33
33
|
if (path.includes('.')) {
|
|
34
|
-
// Handle
|
|
35
|
-
if (path.startsWith('.bertui/compiled/')) {
|
|
36
|
-
const filePath = join(root, path);
|
|
37
|
-
const file = Bun.file(filePath);
|
|
38
|
-
|
|
39
|
-
if (await file.exists()) {
|
|
40
|
-
const ext = extname(path);
|
|
41
|
-
const contentType = ext === '.js' ? 'application/javascript' : getContentType(ext);
|
|
42
|
-
|
|
43
|
-
return new Response(await file.text(), {
|
|
44
|
-
headers: {
|
|
45
|
-
'Content-Type': contentType,
|
|
46
|
-
'Cache-Control': 'no-store'
|
|
47
|
-
}
|
|
48
|
-
});
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
|
|
34
|
+
// Handle compiled directory files
|
|
52
35
|
if (path.startsWith('compiled/')) {
|
|
53
36
|
const filePath = join(compiledDir, path.replace('compiled/', ''));
|
|
54
37
|
const file = Bun.file(filePath);
|
|
@@ -225,7 +208,7 @@ function serveHTML(root, hasRouter, config) {
|
|
|
225
208
|
<body>
|
|
226
209
|
<div id="root"></div>
|
|
227
210
|
<script type="module" src="/hmr-client.js"></script>
|
|
228
|
-
<script type="module" src="
|
|
211
|
+
<script type="module" src="/compiled/main.js"></script>
|
|
229
212
|
</body>
|
|
230
213
|
</html>`;
|
|
231
214
|
|