bertui 0.2.7 ā 0.2.9
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 +361 -17
- package/src/client/compiler.js +15 -376
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}`);
|
|
@@ -82,17 +98,337 @@ export async function buildProduction(options = {}) {
|
|
|
82
98
|
size: `${(o.size / 1024).toFixed(2)} KB`
|
|
83
99
|
})));
|
|
84
100
|
|
|
101
|
+
// Show deployment instructions
|
|
102
|
+
logger.bigLog('READY TO DEPLOY', { color: 'green' });
|
|
103
|
+
console.log('\nš¤ Deploy your app:\n');
|
|
104
|
+
console.log(' Vercel: bunx vercel');
|
|
105
|
+
console.log(' Netlify: bunx netlify deploy');
|
|
106
|
+
console.log('\nš Preview locally:\n');
|
|
107
|
+
console.log(' bun run preview\n');
|
|
108
|
+
|
|
85
109
|
} catch (error) {
|
|
86
110
|
logger.error(`Build failed: ${error.message}`);
|
|
87
111
|
if (error.stack) {
|
|
88
112
|
logger.error(error.stack);
|
|
89
113
|
}
|
|
114
|
+
|
|
115
|
+
// Clean up on error
|
|
116
|
+
if (existsSync(buildDir)) {
|
|
117
|
+
rmSync(buildDir, { recursive: true });
|
|
118
|
+
}
|
|
119
|
+
|
|
90
120
|
process.exit(1);
|
|
91
121
|
}
|
|
92
122
|
}
|
|
93
123
|
|
|
124
|
+
async function compileForBuild(root, buildDir) {
|
|
125
|
+
const srcDir = join(root, 'src');
|
|
126
|
+
const pagesDir = join(srcDir, 'pages');
|
|
127
|
+
|
|
128
|
+
if (!existsSync(srcDir)) {
|
|
129
|
+
throw new Error('src/ directory not found!');
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Discover routes
|
|
133
|
+
let routes = [];
|
|
134
|
+
if (existsSync(pagesDir)) {
|
|
135
|
+
routes = await discoverRoutes(pagesDir);
|
|
136
|
+
logger.info(`Found ${routes.length} routes`);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Compile all source files
|
|
140
|
+
await compileBuildDirectory(srcDir, buildDir, root);
|
|
141
|
+
|
|
142
|
+
// Generate router if we have routes
|
|
143
|
+
if (routes.length > 0) {
|
|
144
|
+
await generateBuildRouter(routes, buildDir);
|
|
145
|
+
logger.info('Generated router for build');
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async function discoverRoutes(pagesDir) {
|
|
150
|
+
const routes = [];
|
|
151
|
+
|
|
152
|
+
async function scanDirectory(dir, basePath = '') {
|
|
153
|
+
const entries = readdirSync(dir, { withFileTypes: true });
|
|
154
|
+
|
|
155
|
+
for (const entry of entries) {
|
|
156
|
+
const fullPath = join(dir, entry.name);
|
|
157
|
+
const relativePath = join(basePath, entry.name);
|
|
158
|
+
|
|
159
|
+
if (entry.isDirectory()) {
|
|
160
|
+
await scanDirectory(fullPath, relativePath);
|
|
161
|
+
} else if (entry.isFile()) {
|
|
162
|
+
const ext = extname(entry.name);
|
|
163
|
+
if (['.jsx', '.tsx', '.js', '.ts'].includes(ext)) {
|
|
164
|
+
const fileName = entry.name.replace(ext, '');
|
|
165
|
+
|
|
166
|
+
let route = '/' + relativePath.replace(/\\/g, '/').replace(ext, '');
|
|
167
|
+
|
|
168
|
+
if (fileName === 'index') {
|
|
169
|
+
route = route.replace('/index', '') || '/';
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const isDynamic = fileName.includes('[') && fileName.includes(']');
|
|
173
|
+
const type = isDynamic ? 'dynamic' : 'static';
|
|
174
|
+
|
|
175
|
+
routes.push({
|
|
176
|
+
route: route === '' ? '/' : route,
|
|
177
|
+
file: relativePath.replace(/\\/g, '/'),
|
|
178
|
+
path: fullPath,
|
|
179
|
+
type
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
await scanDirectory(pagesDir);
|
|
187
|
+
|
|
188
|
+
routes.sort((a, b) => {
|
|
189
|
+
if (a.type === b.type) {
|
|
190
|
+
return a.route.localeCompare(b.route);
|
|
191
|
+
}
|
|
192
|
+
return a.type === 'static' ? -1 : 1;
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
return routes;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
async function generateBuildRouter(routes, buildDir) {
|
|
199
|
+
const imports = routes.map((route, i) => {
|
|
200
|
+
const componentName = `Page${i}`;
|
|
201
|
+
const importPath = `./pages/${route.file.replace(/\.(jsx|tsx|ts)$/, '.js')}`;
|
|
202
|
+
return `import ${componentName} from '${importPath}';`;
|
|
203
|
+
}).join('\n');
|
|
204
|
+
|
|
205
|
+
const routeConfigs = routes.map((route, i) => {
|
|
206
|
+
const componentName = `Page${i}`;
|
|
207
|
+
return ` { path: '${route.route}', component: ${componentName}, type: '${route.type}' }`;
|
|
208
|
+
}).join(',\n');
|
|
209
|
+
|
|
210
|
+
const routerCode = `import React, { useState, useEffect, createContext, useContext } from 'react';
|
|
211
|
+
|
|
212
|
+
const RouterContext = createContext(null);
|
|
213
|
+
|
|
214
|
+
export function useRouter() {
|
|
215
|
+
const context = useContext(RouterContext);
|
|
216
|
+
if (!context) {
|
|
217
|
+
throw new Error('useRouter must be used within a Router component');
|
|
218
|
+
}
|
|
219
|
+
return context;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export function Router({ routes }) {
|
|
223
|
+
const [currentRoute, setCurrentRoute] = useState(null);
|
|
224
|
+
const [params, setParams] = useState({});
|
|
225
|
+
|
|
226
|
+
useEffect(() => {
|
|
227
|
+
matchAndSetRoute(window.location.pathname);
|
|
228
|
+
|
|
229
|
+
const handlePopState = () => {
|
|
230
|
+
matchAndSetRoute(window.location.pathname);
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
window.addEventListener('popstate', handlePopState);
|
|
234
|
+
return () => window.removeEventListener('popstate', handlePopState);
|
|
235
|
+
}, [routes]);
|
|
236
|
+
|
|
237
|
+
function matchAndSetRoute(pathname) {
|
|
238
|
+
for (const route of routes) {
|
|
239
|
+
if (route.type === 'static' && route.path === pathname) {
|
|
240
|
+
setCurrentRoute(route);
|
|
241
|
+
setParams({});
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
for (const route of routes) {
|
|
247
|
+
if (route.type === 'dynamic') {
|
|
248
|
+
const pattern = route.path.replace(/\\[([^\\]]+)\\]/g, '([^/]+)');
|
|
249
|
+
const regex = new RegExp('^' + pattern + '$');
|
|
250
|
+
const match = pathname.match(regex);
|
|
251
|
+
|
|
252
|
+
if (match) {
|
|
253
|
+
const paramNames = [...route.path.matchAll(/\\[([^\\]]+)\\]/g)].map(m => m[1]);
|
|
254
|
+
const extractedParams = {};
|
|
255
|
+
paramNames.forEach((name, i) => {
|
|
256
|
+
extractedParams[name] = match[i + 1];
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
setCurrentRoute(route);
|
|
260
|
+
setParams(extractedParams);
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
setCurrentRoute(null);
|
|
267
|
+
setParams({});
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function navigate(path) {
|
|
271
|
+
window.history.pushState({}, '', path);
|
|
272
|
+
matchAndSetRoute(path);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const routerValue = {
|
|
276
|
+
currentRoute,
|
|
277
|
+
params,
|
|
278
|
+
navigate,
|
|
279
|
+
pathname: window.location.pathname
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
const Component = currentRoute?.component;
|
|
283
|
+
|
|
284
|
+
return React.createElement(
|
|
285
|
+
RouterContext.Provider,
|
|
286
|
+
{ value: routerValue },
|
|
287
|
+
Component ? React.createElement(Component, { params }) : React.createElement(NotFound, null)
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
export function Link({ to, children, ...props }) {
|
|
292
|
+
const { navigate } = useRouter();
|
|
293
|
+
|
|
294
|
+
function handleClick(e) {
|
|
295
|
+
e.preventDefault();
|
|
296
|
+
navigate(to);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
return React.createElement('a', { href: to, onClick: handleClick, ...props }, children);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function NotFound() {
|
|
303
|
+
return React.createElement(
|
|
304
|
+
'div',
|
|
305
|
+
{
|
|
306
|
+
style: {
|
|
307
|
+
display: 'flex',
|
|
308
|
+
flexDirection: 'column',
|
|
309
|
+
alignItems: 'center',
|
|
310
|
+
justifyContent: 'center',
|
|
311
|
+
minHeight: '100vh',
|
|
312
|
+
fontFamily: 'system-ui'
|
|
313
|
+
}
|
|
314
|
+
},
|
|
315
|
+
React.createElement('h1', { style: { fontSize: '6rem', margin: 0 } }, '404'),
|
|
316
|
+
React.createElement('p', { style: { fontSize: '1.5rem', color: '#666' } }, 'Page not found'),
|
|
317
|
+
React.createElement('a', {
|
|
318
|
+
href: '/',
|
|
319
|
+
style: { color: '#10b981', textDecoration: 'none', fontSize: '1.2rem' }
|
|
320
|
+
}, 'Go home')
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
${imports}
|
|
325
|
+
|
|
326
|
+
export const routes = [
|
|
327
|
+
${routeConfigs}
|
|
328
|
+
];
|
|
329
|
+
`;
|
|
330
|
+
|
|
331
|
+
await Bun.write(join(buildDir, 'router.js'), routerCode);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
async function compileBuildDirectory(srcDir, buildDir, root) {
|
|
335
|
+
const files = readdirSync(srcDir);
|
|
336
|
+
|
|
337
|
+
for (const file of files) {
|
|
338
|
+
const srcPath = join(srcDir, file);
|
|
339
|
+
const stat = statSync(srcPath);
|
|
340
|
+
|
|
341
|
+
if (stat.isDirectory()) {
|
|
342
|
+
const subBuildDir = join(buildDir, file);
|
|
343
|
+
mkdirSync(subBuildDir, { recursive: true });
|
|
344
|
+
await compileBuildDirectory(srcPath, subBuildDir, root);
|
|
345
|
+
} else {
|
|
346
|
+
const ext = extname(file);
|
|
347
|
+
|
|
348
|
+
if (['.jsx', '.tsx', '.ts'].includes(ext)) {
|
|
349
|
+
await compileBuildFile(srcPath, buildDir, file, root);
|
|
350
|
+
} else if (ext === '.js') {
|
|
351
|
+
const outPath = join(buildDir, file);
|
|
352
|
+
let code = await Bun.file(srcPath).text();
|
|
353
|
+
code = fixBuildImports(code, srcPath, outPath, root);
|
|
354
|
+
await Bun.write(outPath, code);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
async function compileBuildFile(srcPath, buildDir, filename, root) {
|
|
361
|
+
const ext = extname(filename);
|
|
362
|
+
const loader = ext === '.tsx' ? 'tsx' : ext === '.ts' ? 'ts' : 'jsx';
|
|
363
|
+
|
|
364
|
+
try {
|
|
365
|
+
let code = await Bun.file(srcPath).text();
|
|
366
|
+
|
|
367
|
+
const outFilename = filename.replace(/\.(jsx|tsx|ts)$/, '.js');
|
|
368
|
+
const outPath = join(buildDir, outFilename);
|
|
369
|
+
|
|
370
|
+
code = fixBuildImports(code, srcPath, outPath, root);
|
|
371
|
+
|
|
372
|
+
const transpiler = new Bun.Transpiler({
|
|
373
|
+
loader,
|
|
374
|
+
tsconfig: {
|
|
375
|
+
compilerOptions: {
|
|
376
|
+
jsx: 'react',
|
|
377
|
+
jsxFactory: 'React.createElement',
|
|
378
|
+
jsxFragmentFactory: 'React.Fragment'
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
let compiled = await transpiler.transform(code);
|
|
384
|
+
|
|
385
|
+
if (!compiled.includes('import React') && (compiled.includes('React.createElement') || compiled.includes('React.Fragment'))) {
|
|
386
|
+
compiled = `import React from 'react';\n${compiled}`;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
compiled = fixRelativeImports(compiled);
|
|
390
|
+
|
|
391
|
+
await Bun.write(outPath, compiled);
|
|
392
|
+
} catch (error) {
|
|
393
|
+
logger.error(`Failed to compile ${filename}: ${error.message}`);
|
|
394
|
+
throw error;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
function fixBuildImports(code, srcPath, outPath, root) {
|
|
399
|
+
// Remove bertui/styles imports
|
|
400
|
+
code = code.replace(/import\s+['"]bertui\/styles['"]\s*;?\s*/g, '');
|
|
401
|
+
|
|
402
|
+
const buildDir = join(root, '.bertuibuild');
|
|
403
|
+
const routerPath = join(buildDir, 'router.js');
|
|
404
|
+
|
|
405
|
+
// Calculate relative path from output file to router.js
|
|
406
|
+
const relativeToRouter = relative(dirname(outPath), routerPath).replace(/\\/g, '/');
|
|
407
|
+
const routerImport = relativeToRouter.startsWith('.') ? relativeToRouter : './' + relativeToRouter;
|
|
408
|
+
|
|
409
|
+
// Replace bertui/router imports
|
|
410
|
+
code = code.replace(
|
|
411
|
+
/from\s+['"]bertui\/router['"]/g,
|
|
412
|
+
`from '${routerImport}'`
|
|
413
|
+
);
|
|
414
|
+
|
|
415
|
+
return code;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
function fixRelativeImports(code) {
|
|
419
|
+
const importRegex = /from\s+['"](\.\.[\/\\]|\.\/)((?:[^'"]+?)(?<!\.js|\.jsx|\.ts|\.tsx|\.json))['"];?/g;
|
|
420
|
+
|
|
421
|
+
code = code.replace(importRegex, (match, prefix, path) => {
|
|
422
|
+
if (path.endsWith('/') || /\.\w+$/.test(path)) {
|
|
423
|
+
return match;
|
|
424
|
+
}
|
|
425
|
+
return `from '${prefix}${path}.js';`;
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
return code;
|
|
429
|
+
}
|
|
430
|
+
|
|
94
431
|
async function generateProductionHTML(root, outDir, buildResult) {
|
|
95
|
-
// Find the main bundle
|
|
96
432
|
const mainBundle = buildResult.outputs.find(o =>
|
|
97
433
|
o.path.includes('main') && o.kind === 'entry-point'
|
|
98
434
|
);
|
|
@@ -111,6 +447,14 @@ async function generateProductionHTML(root, outDir, buildResult) {
|
|
|
111
447
|
<meta name="description" content="Built with BertUI - Lightning fast React development">
|
|
112
448
|
<title>BertUI App</title>
|
|
113
449
|
<link rel="stylesheet" href="/styles/bertui.min.css">
|
|
450
|
+
<script type="importmap">
|
|
451
|
+
{
|
|
452
|
+
"imports": {
|
|
453
|
+
"react": "https://esm.sh/react@18.2.0",
|
|
454
|
+
"react-dom": "https://esm.sh/react-dom@18.2.0"
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
</script>
|
|
114
458
|
</head>
|
|
115
459
|
<body>
|
|
116
460
|
<div id="root"></div>
|
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';
|
|
@@ -602,13 +256,13 @@ async function compileDirectory(srcDir, outDir, root) {
|
|
|
602
256
|
const relativePath = relative(join(root, 'src'), srcPath);
|
|
603
257
|
|
|
604
258
|
if (['.jsx', '.tsx', '.ts'].includes(ext)) {
|
|
605
|
-
await compileFile(srcPath, outDir, file, relativePath
|
|
259
|
+
await compileFile(srcPath, outDir, file, relativePath);
|
|
606
260
|
stats.files++;
|
|
607
261
|
} else if (ext === '.js') {
|
|
608
262
|
const outPath = join(outDir, file);
|
|
609
263
|
let code = await Bun.file(srcPath).text();
|
|
610
264
|
|
|
611
|
-
code = fixImports(code
|
|
265
|
+
code = fixImports(code);
|
|
612
266
|
|
|
613
267
|
await Bun.write(outPath, code);
|
|
614
268
|
logger.debug(`Copied: ${relativePath}`);
|
|
@@ -623,14 +277,14 @@ async function compileDirectory(srcDir, outDir, root) {
|
|
|
623
277
|
return stats;
|
|
624
278
|
}
|
|
625
279
|
|
|
626
|
-
async function compileFile(srcPath, outDir, filename, relativePath
|
|
280
|
+
async function compileFile(srcPath, outDir, filename, relativePath) {
|
|
627
281
|
const ext = extname(filename);
|
|
628
282
|
const loader = ext === '.tsx' ? 'tsx' : ext === '.ts' ? 'ts' : 'jsx';
|
|
629
283
|
|
|
630
284
|
try {
|
|
631
285
|
let code = await Bun.file(srcPath).text();
|
|
632
286
|
|
|
633
|
-
code = fixImports(code
|
|
287
|
+
code = fixImports(code);
|
|
634
288
|
|
|
635
289
|
const transpiler = new Bun.Transpiler({
|
|
636
290
|
loader,
|
|
@@ -661,34 +315,18 @@ async function compileFile(srcPath, outDir, filename, relativePath, root) {
|
|
|
661
315
|
}
|
|
662
316
|
}
|
|
663
317
|
|
|
664
|
-
function fixImports(code
|
|
665
|
-
// Remove bertui/styles imports
|
|
318
|
+
function fixImports(code) {
|
|
666
319
|
code = code.replace(/import\s+['"]bertui\/styles['"]\s*;?\s*/g, '');
|
|
667
320
|
|
|
668
|
-
|
|
669
|
-
|
|
321
|
+
code = code.replace(
|
|
322
|
+
/from\s+['"]bertui\/router['"]/g,
|
|
323
|
+
"from '/compiled/router.js'"
|
|
324
|
+
);
|
|
670
325
|
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
"from './router.js'"
|
|
676
|
-
);
|
|
677
|
-
code = code.replace(
|
|
678
|
-
/from\s+['"]\/compiled\/router\.js['"]/g,
|
|
679
|
-
"from './router.js'"
|
|
680
|
-
);
|
|
681
|
-
code = code.replace(
|
|
682
|
-
/from\s+['"]\.\.\/\.bertui\/compiled\/router\.js['"]/g,
|
|
683
|
-
"from './router.js'"
|
|
684
|
-
);
|
|
685
|
-
} else {
|
|
686
|
-
// For page components: router.js is up one level from pages/
|
|
687
|
-
code = code.replace(
|
|
688
|
-
/from\s+['"]bertui\/router['"]/g,
|
|
689
|
-
"from '../router.js'"
|
|
690
|
-
);
|
|
691
|
-
}
|
|
326
|
+
code = code.replace(
|
|
327
|
+
/from\s+['"]\.\.\/\.bertui\/compiled\/([^'"]+)['"]/g,
|
|
328
|
+
"from '/compiled/$1'"
|
|
329
|
+
);
|
|
692
330
|
|
|
693
331
|
return code;
|
|
694
332
|
}
|
|
@@ -704,4 +342,5 @@ function fixRelativeImports(code) {
|
|
|
704
342
|
});
|
|
705
343
|
|
|
706
344
|
return code;
|
|
707
|
-
}
|
|
345
|
+
}
|
|
346
|
+
|