bertui 1.1.8 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.js +65 -45
- package/package.json +5 -2
- package/src/analyzer/index.js +370 -0
- package/src/build/compiler/route-discoverer.js +2 -0
- package/src/build/processors/css-builder.js +116 -80
- package/src/build.js +104 -93
- package/src/cli.js +83 -18
- package/src/client/compiler.js +168 -67
- package/src/css/processor.js +46 -1
- package/src/dev.js +47 -9
- package/src/hydration/index.js +151 -0
- package/src/layouts/index.js +165 -0
- package/src/loading/index.js +210 -0
- package/src/middleware/index.js +182 -0
- package/src/scaffolder/index.js +310 -0
- package/src/serve.js +195 -0
- package/src/server/dev-handler.js +78 -148
- package/src/server/dev-server-utils.js +16 -5
- package/src/server-islands/index.js +1 -1
- package/src/utils/cache.js +297 -0
package/src/client/compiler.js
CHANGED
|
@@ -1,9 +1,6 @@
|
|
|
1
|
-
// ============================================
|
|
2
|
-
// FILE: bertui/src/client/compiler.js (UPDATED - Skip templates/)
|
|
3
|
-
// ============================================
|
|
4
|
-
|
|
5
1
|
import { existsSync, mkdirSync, readdirSync, statSync } from 'fs';
|
|
6
2
|
import { join, extname, relative, dirname } from 'path';
|
|
3
|
+
import { transform } from 'lightningcss';
|
|
7
4
|
import logger from '../logger/logger.js';
|
|
8
5
|
import { loadEnvVariables, generateEnvCode, replaceEnvInCode } from '../utils/env.js';
|
|
9
6
|
|
|
@@ -63,17 +60,18 @@ export async function compileProject(root) {
|
|
|
63
60
|
return { outDir, stats, routes };
|
|
64
61
|
}
|
|
65
62
|
|
|
66
|
-
// NEW EXPORT - Single file compilation for HMR
|
|
67
63
|
export async function compileFile(srcPath, root) {
|
|
68
64
|
const srcDir = join(root, 'src');
|
|
69
65
|
const outDir = join(root, '.bertui', 'compiled');
|
|
70
66
|
const envVars = loadEnvVariables(root);
|
|
71
|
-
|
|
72
67
|
const relativePath = relative(srcDir, srcPath);
|
|
73
68
|
const ext = extname(srcPath);
|
|
74
69
|
|
|
75
|
-
if (!existsSync(outDir)) {
|
|
76
|
-
|
|
70
|
+
if (!existsSync(outDir)) mkdirSync(outDir, { recursive: true });
|
|
71
|
+
|
|
72
|
+
if (srcPath.endsWith('.module.css')) {
|
|
73
|
+
await compileCSSModule(srcPath, root);
|
|
74
|
+
return { success: true };
|
|
77
75
|
}
|
|
78
76
|
|
|
79
77
|
if (['.jsx', '.tsx', '.ts'].includes(ext)) {
|
|
@@ -83,29 +81,30 @@ export async function compileFile(srcPath, root) {
|
|
|
83
81
|
outputPath: relativePath.replace(/\.(jsx|tsx|ts)$/, '.js'),
|
|
84
82
|
success: true
|
|
85
83
|
};
|
|
86
|
-
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (ext === '.js') {
|
|
87
87
|
const fileName = srcPath.split('/').pop();
|
|
88
88
|
const outPath = join(outDir, fileName);
|
|
89
89
|
let code = await Bun.file(srcPath).text();
|
|
90
|
-
|
|
91
|
-
code =
|
|
90
|
+
code = transformCSSModuleImports(code, srcPath, root);
|
|
91
|
+
code = removePlainCSSImports(code);
|
|
92
92
|
code = replaceEnvInCode(code, envVars);
|
|
93
93
|
code = fixRouterImports(code, outPath, root);
|
|
94
|
-
|
|
95
94
|
if (usesJSX(code) && !code.includes('import React')) {
|
|
96
95
|
code = `import React from 'react';\n${code}`;
|
|
97
96
|
}
|
|
98
|
-
|
|
99
97
|
await Bun.write(outPath, code);
|
|
100
|
-
return {
|
|
101
|
-
outputPath: relativePath,
|
|
102
|
-
success: true
|
|
103
|
-
};
|
|
98
|
+
return { outputPath: relativePath, success: true };
|
|
104
99
|
}
|
|
105
100
|
|
|
106
101
|
return { success: false };
|
|
107
102
|
}
|
|
108
103
|
|
|
104
|
+
// ============================================
|
|
105
|
+
// ROUTE DISCOVERY
|
|
106
|
+
// ============================================
|
|
107
|
+
|
|
109
108
|
async function discoverRoutes(pagesDir) {
|
|
110
109
|
const routes = [];
|
|
111
110
|
|
|
@@ -124,20 +123,22 @@ async function discoverRoutes(pagesDir) {
|
|
|
124
123
|
|
|
125
124
|
if (['.jsx', '.tsx', '.js', '.ts'].includes(ext)) {
|
|
126
125
|
const fileName = entry.name.replace(ext, '');
|
|
126
|
+
|
|
127
|
+
// ✅ Only loading is reserved — index is a valid route (renamed to /)
|
|
128
|
+
if (fileName === 'loading') continue;
|
|
129
|
+
|
|
127
130
|
let route = '/' + relativePath.replace(/\\/g, '/').replace(ext, '');
|
|
128
|
-
|
|
129
131
|
if (fileName === 'index') {
|
|
130
132
|
route = route.replace('/index', '') || '/';
|
|
131
133
|
}
|
|
132
134
|
|
|
133
135
|
const isDynamic = fileName.includes('[') && fileName.includes(']');
|
|
134
|
-
const type = isDynamic ? 'dynamic' : 'static';
|
|
135
136
|
|
|
136
137
|
routes.push({
|
|
137
138
|
route: route === '' ? '/' : route,
|
|
138
139
|
file: relativePath.replace(/\\/g, '/'),
|
|
139
140
|
path: fullPath,
|
|
140
|
-
type
|
|
141
|
+
type: isDynamic ? 'dynamic' : 'static'
|
|
141
142
|
});
|
|
142
143
|
}
|
|
143
144
|
}
|
|
@@ -146,15 +147,17 @@ async function discoverRoutes(pagesDir) {
|
|
|
146
147
|
|
|
147
148
|
await scanDirectory(pagesDir);
|
|
148
149
|
routes.sort((a, b) => {
|
|
149
|
-
if (a.type === b.type)
|
|
150
|
-
return a.route.localeCompare(b.route);
|
|
151
|
-
}
|
|
150
|
+
if (a.type === b.type) return a.route.localeCompare(b.route);
|
|
152
151
|
return a.type === 'static' ? -1 : 1;
|
|
153
152
|
});
|
|
154
153
|
|
|
155
154
|
return routes;
|
|
156
155
|
}
|
|
157
156
|
|
|
157
|
+
// ============================================
|
|
158
|
+
// ROUTER GENERATION
|
|
159
|
+
// ============================================
|
|
160
|
+
|
|
158
161
|
async function generateRouter(routes, outDir, root) {
|
|
159
162
|
const imports = routes.map((route, i) => {
|
|
160
163
|
const componentName = `Page${i}`;
|
|
@@ -167,7 +170,7 @@ async function generateRouter(routes, outDir, root) {
|
|
|
167
170
|
return ` { path: '${route.route}', component: ${componentName}, type: '${route.type}' }`;
|
|
168
171
|
}).join(',\n');
|
|
169
172
|
|
|
170
|
-
const
|
|
173
|
+
const routerCode = `import React, { useState, useEffect, createContext, useContext } from 'react';
|
|
171
174
|
|
|
172
175
|
const RouterContext = createContext(null);
|
|
173
176
|
|
|
@@ -230,16 +233,16 @@ export function Router({ routes }) {
|
|
|
230
233
|
|
|
231
234
|
export function Link({ to, children, ...props }) {
|
|
232
235
|
const { navigate } = useRouter();
|
|
233
|
-
return React.createElement('a', {
|
|
234
|
-
href: to,
|
|
235
|
-
onClick: (e) => { e.preventDefault(); navigate(to); },
|
|
236
|
-
...props
|
|
236
|
+
return React.createElement('a', {
|
|
237
|
+
href: to,
|
|
238
|
+
onClick: (e) => { e.preventDefault(); navigate(to); },
|
|
239
|
+
...props
|
|
237
240
|
}, children);
|
|
238
241
|
}
|
|
239
242
|
|
|
240
243
|
function NotFound() {
|
|
241
244
|
return React.createElement('div', {
|
|
242
|
-
style: { display: 'flex', flexDirection: 'column', alignItems: 'center',
|
|
245
|
+
style: { display: 'flex', flexDirection: 'column', alignItems: 'center',
|
|
243
246
|
justifyContent: 'center', minHeight: '100vh', fontFamily: 'system-ui' }
|
|
244
247
|
},
|
|
245
248
|
React.createElement('h1', { style: { fontSize: '6rem', margin: 0 } }, '404'),
|
|
@@ -254,9 +257,13 @@ export const routes = [
|
|
|
254
257
|
${routeConfigs}
|
|
255
258
|
];`;
|
|
256
259
|
|
|
257
|
-
await Bun.write(join(outDir, 'router.js'),
|
|
260
|
+
await Bun.write(join(outDir, 'router.js'), routerCode);
|
|
258
261
|
}
|
|
259
262
|
|
|
263
|
+
// ============================================
|
|
264
|
+
// DIRECTORY COMPILATION
|
|
265
|
+
// ============================================
|
|
266
|
+
|
|
260
267
|
async function compileDirectory(srcDir, outDir, root, envVars) {
|
|
261
268
|
const stats = { files: 0, skipped: 0 };
|
|
262
269
|
const files = readdirSync(srcDir);
|
|
@@ -266,12 +273,10 @@ async function compileDirectory(srcDir, outDir, root, envVars) {
|
|
|
266
273
|
const stat = statSync(srcPath);
|
|
267
274
|
|
|
268
275
|
if (stat.isDirectory()) {
|
|
269
|
-
// ✅ NEW: Skip templates directory
|
|
270
276
|
if (file === 'templates') {
|
|
271
|
-
logger.debug('⏭️ Skipping src/templates/
|
|
277
|
+
logger.debug('⏭️ Skipping src/templates/');
|
|
272
278
|
continue;
|
|
273
279
|
}
|
|
274
|
-
|
|
275
280
|
const subOutDir = join(outDir, file);
|
|
276
281
|
mkdirSync(subOutDir, { recursive: true });
|
|
277
282
|
const subStats = await compileDirectory(srcPath, subOutDir, root, envVars);
|
|
@@ -280,14 +285,16 @@ async function compileDirectory(srcDir, outDir, root, envVars) {
|
|
|
280
285
|
} else {
|
|
281
286
|
const ext = extname(file);
|
|
282
287
|
const relativePath = relative(join(root, 'src'), srcPath);
|
|
283
|
-
|
|
284
|
-
|
|
288
|
+
|
|
289
|
+
// ✅ MUST check .module.css BEFORE plain .css
|
|
290
|
+
if (file.endsWith('.module.css')) {
|
|
291
|
+
await compileCSSModule(srcPath, root);
|
|
292
|
+
stats.files++;
|
|
293
|
+
} else if (ext === '.css') {
|
|
294
|
+
// Plain CSS → copy to .bertui/styles/ for <link> injection
|
|
285
295
|
const stylesOutDir = join(root, '.bertui', 'styles');
|
|
286
|
-
if (!existsSync(stylesOutDir)) {
|
|
287
|
-
|
|
288
|
-
}
|
|
289
|
-
const cssOutPath = join(stylesOutDir, file);
|
|
290
|
-
await Bun.write(cssOutPath, Bun.file(srcPath));
|
|
296
|
+
if (!existsSync(stylesOutDir)) mkdirSync(stylesOutDir, { recursive: true });
|
|
297
|
+
await Bun.write(join(stylesOutDir, file), Bun.file(srcPath));
|
|
291
298
|
logger.debug(`Copied CSS: ${relativePath}`);
|
|
292
299
|
stats.files++;
|
|
293
300
|
} else if (['.jsx', '.tsx', '.ts'].includes(ext)) {
|
|
@@ -296,20 +303,17 @@ async function compileDirectory(srcDir, outDir, root, envVars) {
|
|
|
296
303
|
} else if (ext === '.js') {
|
|
297
304
|
const outPath = join(outDir, file);
|
|
298
305
|
let code = await Bun.file(srcPath).text();
|
|
299
|
-
|
|
300
|
-
code =
|
|
306
|
+
code = transformCSSModuleImports(code, srcPath, root);
|
|
307
|
+
code = removePlainCSSImports(code);
|
|
301
308
|
code = replaceEnvInCode(code, envVars);
|
|
302
309
|
code = fixRouterImports(code, outPath, root);
|
|
303
|
-
|
|
304
310
|
if (usesJSX(code) && !code.includes('import React')) {
|
|
305
311
|
code = `import React from 'react';\n${code}`;
|
|
306
312
|
}
|
|
307
|
-
|
|
308
313
|
await Bun.write(outPath, code);
|
|
309
314
|
logger.debug(`Copied: ${relativePath}`);
|
|
310
315
|
stats.files++;
|
|
311
316
|
} else {
|
|
312
|
-
logger.debug(`Skipped: ${relativePath}`);
|
|
313
317
|
stats.skipped++;
|
|
314
318
|
}
|
|
315
319
|
}
|
|
@@ -318,6 +322,114 @@ async function compileDirectory(srcDir, outDir, root, envVars) {
|
|
|
318
322
|
return stats;
|
|
319
323
|
}
|
|
320
324
|
|
|
325
|
+
// ============================================
|
|
326
|
+
// CSS MODULES
|
|
327
|
+
// ============================================
|
|
328
|
+
|
|
329
|
+
function hashClassName(filename, className) {
|
|
330
|
+
const str = filename + className;
|
|
331
|
+
let hash = 0;
|
|
332
|
+
for (let i = 0; i < str.length; i++) {
|
|
333
|
+
hash = (hash << 5) - hash + str.charCodeAt(i);
|
|
334
|
+
hash |= 0;
|
|
335
|
+
}
|
|
336
|
+
return Math.abs(hash).toString(36).slice(0, 5);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function scopeCSSModule(cssText, filename) {
|
|
340
|
+
const classNames = new Set();
|
|
341
|
+
const classRegex = /\.([a-zA-Z_][a-zA-Z0-9_-]*)\s*[{,\s:]/g;
|
|
342
|
+
let match;
|
|
343
|
+
while ((match = classRegex.exec(cssText)) !== null) {
|
|
344
|
+
classNames.add(match[1]);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
const mapping = {};
|
|
348
|
+
for (const cls of classNames) {
|
|
349
|
+
mapping[cls] = `${cls}_${hashClassName(filename, cls)}`;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
let scopedCSS = cssText;
|
|
353
|
+
for (const [original, scoped] of Object.entries(mapping)) {
|
|
354
|
+
scopedCSS = scopedCSS.replace(
|
|
355
|
+
new RegExp(`\\.${original}(?=[\\s{,:\\[#.>+~)\\]])`, 'g'),
|
|
356
|
+
`.${scoped}`
|
|
357
|
+
);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
return { mapping, scopedCSS };
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
async function compileCSSModule(srcPath, root) {
|
|
364
|
+
const filename = srcPath.split('/').pop(); // e.g. home.module.css
|
|
365
|
+
const cssText = await Bun.file(srcPath).text();
|
|
366
|
+
|
|
367
|
+
const { mapping, scopedCSS } = scopeCSSModule(cssText, filename);
|
|
368
|
+
|
|
369
|
+
// Run through LightningCSS for nesting support
|
|
370
|
+
let finalCSS = scopedCSS;
|
|
371
|
+
try {
|
|
372
|
+
const { code } = transform({
|
|
373
|
+
filename,
|
|
374
|
+
code: Buffer.from(scopedCSS),
|
|
375
|
+
minify: false,
|
|
376
|
+
drafts: { nesting: true },
|
|
377
|
+
targets: { chrome: 90 << 16 }
|
|
378
|
+
});
|
|
379
|
+
finalCSS = code.toString();
|
|
380
|
+
} catch (e) {
|
|
381
|
+
logger.warn(`LightningCSS failed for ${filename}: ${e.message}`);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// ✅ Scoped CSS → .bertui/styles/ (served as <link> tags)
|
|
385
|
+
const stylesOutDir = join(root, '.bertui', 'styles');
|
|
386
|
+
if (!existsSync(stylesOutDir)) mkdirSync(stylesOutDir, { recursive: true });
|
|
387
|
+
await Bun.write(join(stylesOutDir, filename), finalCSS);
|
|
388
|
+
|
|
389
|
+
// ✅ JS mapping → .bertui/compiled/styles/ (flat, imported by pages)
|
|
390
|
+
const compiledStylesDir = join(root, '.bertui', 'compiled', 'styles');
|
|
391
|
+
if (!existsSync(compiledStylesDir)) mkdirSync(compiledStylesDir, { recursive: true });
|
|
392
|
+
const jsContent = `// CSS Module: ${filename} — auto-generated by BertUI\nconst styles = ${JSON.stringify(mapping, null, 2)};\nexport default styles;\n`;
|
|
393
|
+
await Bun.write(join(compiledStylesDir, filename + '.js'), jsContent);
|
|
394
|
+
|
|
395
|
+
logger.debug(`CSS Module: ${filename} → ${Object.keys(mapping).length} classes scoped`);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// Rewrite: import styles from '../styles/home.module.css'
|
|
399
|
+
// → import styles from '../../styles/home.module.css.js' (relative to compiled output)
|
|
400
|
+
function transformCSSModuleImports(code, srcPath, root) {
|
|
401
|
+
const moduleImportRegex = /import\s+(\w+)\s+from\s+['"]([^'"]*\.module\.css)['"]/g;
|
|
402
|
+
|
|
403
|
+
// The compiled output of this file lives in .bertui/compiled/ + relative path from src/
|
|
404
|
+
const srcDir = join(root, 'src');
|
|
405
|
+
const relativeFromSrc = relative(srcDir, srcPath); // e.g. pages/about.jsx
|
|
406
|
+
const compiledFilePath = join(root, '.bertui', 'compiled', relativeFromSrc.replace(/\.(jsx|tsx|ts)$/, '.js'));
|
|
407
|
+
const compiledFileDir = dirname(compiledFilePath); // e.g. .bertui/compiled/pages/
|
|
408
|
+
|
|
409
|
+
const compiledStylesDir = join(root, '.bertui', 'compiled', 'styles');
|
|
410
|
+
|
|
411
|
+
code = code.replace(moduleImportRegex, (match, varName, importPath) => {
|
|
412
|
+
const filename = importPath.split('/').pop(); // e.g. home.module.css
|
|
413
|
+
const jsFile = join(compiledStylesDir, filename + '.js'); // absolute target
|
|
414
|
+
let rel = relative(compiledFileDir, jsFile).replace(/\\/g, '/');
|
|
415
|
+
if (!rel.startsWith('.')) rel = './' + rel;
|
|
416
|
+
return `import ${varName} from '${rel}'`;
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
return code;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// Remove plain CSS imports only — leave .module.css for transformCSSModuleImports
|
|
423
|
+
function removePlainCSSImports(code) {
|
|
424
|
+
code = code.replace(/import\s+['"][^'"]*(?<!\.module)\.css['"];?\s*/g, '');
|
|
425
|
+
code = code.replace(/import\s+['"]bertui\/styles['"]\s*;?\s*/g, '');
|
|
426
|
+
return code;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// ============================================
|
|
430
|
+
// FILE COMPILATION
|
|
431
|
+
// ============================================
|
|
432
|
+
|
|
321
433
|
async function compileFileInternal(srcPath, outDir, filename, relativePath, root, envVars) {
|
|
322
434
|
const ext = extname(filename);
|
|
323
435
|
const loader = ext === '.tsx' ? 'tsx' : ext === '.ts' ? 'ts' : 'jsx';
|
|
@@ -325,14 +437,16 @@ async function compileFileInternal(srcPath, outDir, filename, relativePath, root
|
|
|
325
437
|
try {
|
|
326
438
|
let code = await Bun.file(srcPath).text();
|
|
327
439
|
|
|
328
|
-
|
|
440
|
+
// ✅ Transform module imports BEFORE stripping plain CSS
|
|
441
|
+
code = transformCSSModuleImports(code, srcPath, root);
|
|
442
|
+
code = removePlainCSSImports(code);
|
|
329
443
|
code = removeDotenvImports(code);
|
|
330
444
|
code = replaceEnvInCode(code, envVars);
|
|
331
445
|
|
|
332
446
|
const outPath = join(outDir, filename.replace(/\.(jsx|tsx|ts)$/, '.js'));
|
|
333
447
|
code = fixRouterImports(code, outPath, root);
|
|
334
448
|
|
|
335
|
-
const transpiler = new Bun.Transpiler({
|
|
449
|
+
const transpiler = new Bun.Transpiler({
|
|
336
450
|
loader,
|
|
337
451
|
tsconfig: {
|
|
338
452
|
compilerOptions: {
|
|
@@ -349,7 +463,6 @@ async function compileFileInternal(srcPath, outDir, filename, relativePath, root
|
|
|
349
463
|
}
|
|
350
464
|
|
|
351
465
|
compiled = fixRelativeImports(compiled);
|
|
352
|
-
|
|
353
466
|
await Bun.write(outPath, compiled);
|
|
354
467
|
logger.debug(`Compiled: ${relativePath} → ${filename.replace(/\.(jsx|tsx|ts)$/, '.js')}`);
|
|
355
468
|
} catch (error) {
|
|
@@ -358,20 +471,18 @@ async function compileFileInternal(srcPath, outDir, filename, relativePath, root
|
|
|
358
471
|
}
|
|
359
472
|
}
|
|
360
473
|
|
|
474
|
+
// ============================================
|
|
475
|
+
// HELPERS
|
|
476
|
+
// ============================================
|
|
477
|
+
|
|
361
478
|
function usesJSX(code) {
|
|
362
|
-
return code.includes('React.createElement') ||
|
|
479
|
+
return code.includes('React.createElement') ||
|
|
363
480
|
code.includes('React.Fragment') ||
|
|
364
481
|
/<[A-Z]/.test(code) ||
|
|
365
482
|
code.includes('jsx(') ||
|
|
366
483
|
code.includes('jsxs(');
|
|
367
484
|
}
|
|
368
485
|
|
|
369
|
-
function removeCSSImports(code) {
|
|
370
|
-
code = code.replace(/import\s+['"][^'"]*\.css['"];?\s*/g, '');
|
|
371
|
-
code = code.replace(/import\s+['"]bertui\/styles['"]\s*;?\s*/g, '');
|
|
372
|
-
return code;
|
|
373
|
-
}
|
|
374
|
-
|
|
375
486
|
function removeDotenvImports(code) {
|
|
376
487
|
code = code.replace(/import\s+\w+\s+from\s+['"]dotenv['"]\s*;?\s*/g, '');
|
|
377
488
|
code = code.replace(/import\s+\{[^}]+\}\s+from\s+['"]dotenv['"]\s*;?\s*/g, '');
|
|
@@ -382,27 +493,17 @@ function removeDotenvImports(code) {
|
|
|
382
493
|
function fixRouterImports(code, outPath, root) {
|
|
383
494
|
const buildDir = join(root, '.bertui', 'compiled');
|
|
384
495
|
const routerPath = join(buildDir, 'router.js');
|
|
385
|
-
|
|
386
496
|
const relativeToRouter = relative(dirname(outPath), routerPath).replace(/\\/g, '/');
|
|
387
497
|
const routerImport = relativeToRouter.startsWith('.') ? relativeToRouter : './' + relativeToRouter;
|
|
388
|
-
|
|
389
|
-
code = code.replace(
|
|
390
|
-
/from\s+['"]bertui\/router['"]/g,
|
|
391
|
-
`from '${routerImport}'`
|
|
392
|
-
);
|
|
393
|
-
|
|
498
|
+
code = code.replace(/from\s+['"]bertui\/router['"]/g, `from '${routerImport}'`);
|
|
394
499
|
return code;
|
|
395
500
|
}
|
|
396
501
|
|
|
397
502
|
function fixRelativeImports(code) {
|
|
398
503
|
const importRegex = /from\s+['"](\.\.?\/[^'"]+?)(?<!\.js|\.jsx|\.ts|\.tsx|\.json)['"]/g;
|
|
399
|
-
|
|
400
504
|
code = code.replace(importRegex, (match, path) => {
|
|
401
|
-
if (path.endsWith('/') || /\.\w+$/.test(path))
|
|
402
|
-
return match;
|
|
403
|
-
}
|
|
505
|
+
if (path.endsWith('/') || /\.\w+$/.test(path)) return match;
|
|
404
506
|
return `from '${path}.js'`;
|
|
405
507
|
});
|
|
406
|
-
|
|
407
508
|
return code;
|
|
408
509
|
}
|
package/src/css/processor.js
CHANGED
|
@@ -124,4 +124,49 @@ export function extractCSSImports(code) {
|
|
|
124
124
|
*/
|
|
125
125
|
export function isCSSFile(filename) {
|
|
126
126
|
return filename.toLowerCase().endsWith('.css');
|
|
127
|
-
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// ============================================
|
|
130
|
+
// OPTIONAL SCSS SUPPORT - COMMENTED OUT BY DEFAULT
|
|
131
|
+
// Uncomment only if you install 'sass' package
|
|
132
|
+
// ============================================
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
export async function processSCSS(scssCode, options = {}) {
|
|
136
|
+
try {
|
|
137
|
+
// Dynamic import so it doesn't fail if sass isn't installed
|
|
138
|
+
const sass = await import('sass').catch(() => {
|
|
139
|
+
throw new Error('sass package not installed. Run: bun add sass');
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
const result = sass.compileString(scssCode, {
|
|
143
|
+
style: options.compressed ? 'compressed' : 'expanded',
|
|
144
|
+
sourceMap: false,
|
|
145
|
+
loadPaths: options.loadPaths || []
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
return result.css;
|
|
149
|
+
} catch (error) {
|
|
150
|
+
logger.error(`SCSS compilation failed: ${error.message}`);
|
|
151
|
+
throw error;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export async function compileSCSSFile(filePath, options = {}) {
|
|
156
|
+
try {
|
|
157
|
+
const sass = await import('sass').catch(() => {
|
|
158
|
+
throw new Error('sass package not installed. Run: bun add sass');
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
const result = sass.compile(filePath, {
|
|
162
|
+
style: options.compressed ? 'compressed' : 'expanded',
|
|
163
|
+
sourceMap: false,
|
|
164
|
+
loadPaths: options.loadPaths || [require('path').dirname(filePath)]
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
return result.css;
|
|
168
|
+
} catch (error) {
|
|
169
|
+
logger.error(`SCSS file compilation failed: ${error.message}`);
|
|
170
|
+
throw error;
|
|
171
|
+
}
|
|
172
|
+
}
|
package/src/dev.js
CHANGED
|
@@ -1,26 +1,64 @@
|
|
|
1
|
-
// bertui/src/dev.js -
|
|
1
|
+
// bertui/src/dev.js - WITH MIDDLEWARE + LAYOUTS + LOADING + PARTIAL HYDRATION
|
|
2
2
|
import { compileProject } from './client/compiler.js';
|
|
3
3
|
import { startDevServer } from './server/dev-server.js';
|
|
4
|
+
import { MiddlewareManager } from './middleware/index.js';
|
|
5
|
+
import { compileLayouts, discoverLayouts } from './layouts/index.js';
|
|
6
|
+
import { compileLoadingComponents } from './loading/index.js';
|
|
7
|
+
import { analyzeRoutes, logHydrationReport } from './hydration/index.js';
|
|
4
8
|
import logger from './logger/logger.js';
|
|
5
9
|
import { loadConfig } from './config/loadConfig.js';
|
|
6
10
|
|
|
7
11
|
export async function startDev(options = {}) {
|
|
8
12
|
const root = options.root || process.cwd();
|
|
9
13
|
const port = options.port || 3000;
|
|
10
|
-
|
|
14
|
+
|
|
11
15
|
try {
|
|
12
16
|
const config = await loadConfig(root);
|
|
13
|
-
|
|
17
|
+
|
|
14
18
|
// Step 1: Compile project
|
|
15
19
|
logger.info('Step 1: Compiling project...');
|
|
16
|
-
await compileProject(root);
|
|
17
|
-
|
|
18
|
-
// Step 2:
|
|
19
|
-
logger.info('Step 2:
|
|
20
|
-
await
|
|
21
|
-
|
|
20
|
+
const { routes, outDir } = await compileProject(root);
|
|
21
|
+
|
|
22
|
+
// Step 2: Compile layouts
|
|
23
|
+
logger.info('Step 2: Loading layouts...');
|
|
24
|
+
const layouts = await compileLayouts(root, outDir);
|
|
25
|
+
const layoutCount = Object.keys(layouts).length;
|
|
26
|
+
if (layoutCount > 0) {
|
|
27
|
+
logger.success(`📐 ${layoutCount} layout(s) active`);
|
|
28
|
+
} else {
|
|
29
|
+
logger.info('No layouts found (create src/layouts/default.tsx to wrap all pages)');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Step 3: Compile loading states
|
|
33
|
+
logger.info('Step 3: Loading per-route loading states...');
|
|
34
|
+
const loadingComponents = await compileLoadingComponents(root, outDir);
|
|
35
|
+
|
|
36
|
+
// Step 4: Analyze routes for partial hydration
|
|
37
|
+
if (routes && routes.length > 0) {
|
|
38
|
+
logger.info('Step 4: Analyzing routes for partial hydration...');
|
|
39
|
+
const analyzedRoutes = await analyzeRoutes(routes);
|
|
40
|
+
logHydrationReport(analyzedRoutes);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Step 5: Load middleware
|
|
44
|
+
logger.info('Step 5: Loading middleware...');
|
|
45
|
+
const middlewareManager = new MiddlewareManager(root);
|
|
46
|
+
await middlewareManager.load();
|
|
47
|
+
middlewareManager.watch(); // Hot-reload middleware on change
|
|
48
|
+
|
|
49
|
+
// Step 6: Start dev server with all features
|
|
50
|
+
logger.info('Step 6: Starting dev server...');
|
|
51
|
+
await startDevServer({
|
|
52
|
+
root,
|
|
53
|
+
port,
|
|
54
|
+
middleware: middlewareManager,
|
|
55
|
+
layouts,
|
|
56
|
+
loadingComponents,
|
|
57
|
+
});
|
|
58
|
+
|
|
22
59
|
} catch (error) {
|
|
23
60
|
logger.error(`Dev server failed: ${error.message}`);
|
|
61
|
+
if (error.stack) logger.error(error.stack);
|
|
24
62
|
process.exit(1);
|
|
25
63
|
}
|
|
26
64
|
}
|