juxscript 1.0.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/README.md +292 -0
- package/bin/cli.js +149 -0
- package/lib/adapters/base-adapter.js +35 -0
- package/lib/adapters/index.js +33 -0
- package/lib/adapters/mysql-adapter.js +65 -0
- package/lib/adapters/postgres-adapter.js +70 -0
- package/lib/adapters/sqlite-adapter.js +56 -0
- package/lib/components/app.ts +124 -0
- package/lib/components/button.ts +136 -0
- package/lib/components/card.ts +205 -0
- package/lib/components/chart.ts +125 -0
- package/lib/components/code.ts +242 -0
- package/lib/components/container.ts +282 -0
- package/lib/components/data.ts +105 -0
- package/lib/components/docs-data.json +1211 -0
- package/lib/components/error-handler.ts +285 -0
- package/lib/components/footer.ts +146 -0
- package/lib/components/header.ts +167 -0
- package/lib/components/hero.ts +170 -0
- package/lib/components/import.ts +430 -0
- package/lib/components/input.ts +175 -0
- package/lib/components/layout.ts +113 -0
- package/lib/components/list.ts +392 -0
- package/lib/components/main.ts +111 -0
- package/lib/components/menu.ts +170 -0
- package/lib/components/modal.ts +216 -0
- package/lib/components/nav.ts +136 -0
- package/lib/components/node.ts +200 -0
- package/lib/components/reactivity.js +104 -0
- package/lib/components/script.ts +152 -0
- package/lib/components/sidebar.ts +168 -0
- package/lib/components/style.ts +129 -0
- package/lib/components/table.ts +279 -0
- package/lib/components/tabs.ts +191 -0
- package/lib/components/theme.ts +97 -0
- package/lib/components/view.ts +174 -0
- package/lib/jux.ts +203 -0
- package/lib/layouts/default.css +260 -0
- package/lib/layouts/default.jux +8 -0
- package/lib/layouts/figma.css +334 -0
- package/lib/layouts/figma.jux +0 -0
- package/lib/layouts/notion.css +258 -0
- package/lib/styles/base-theme.css +186 -0
- package/lib/styles/dark-theme.css +144 -0
- package/lib/styles/global.css +1131 -0
- package/lib/styles/light-theme.css +144 -0
- package/lib/styles/tokens/dark.css +86 -0
- package/lib/styles/tokens/light.css +86 -0
- package/lib/themes/dark.css +86 -0
- package/lib/themes/light.css +86 -0
- package/lib/utils/path-resolver.js +23 -0
- package/machinery/compiler.js +262 -0
- package/machinery/doc-generator.js +160 -0
- package/machinery/generators/css.js +128 -0
- package/machinery/generators/html.js +108 -0
- package/machinery/imports.js +155 -0
- package/machinery/server.js +185 -0
- package/machinery/validators/file-validator.js +123 -0
- package/machinery/watcher.js +148 -0
- package/package.json +58 -0
- package/types/globals.d.ts +16 -0
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import esbuild from 'esbuild';
|
|
4
|
+
import { generateDocs } from './doc-generator.js';
|
|
5
|
+
|
|
6
|
+
export async function compileJuxFile(juxFilePath, options = {}) {
|
|
7
|
+
const { distDir, projectRoot, isServe = false } = options;
|
|
8
|
+
|
|
9
|
+
const relativePath = path.relative(projectRoot, juxFilePath);
|
|
10
|
+
const parsedPath = path.parse(relativePath);
|
|
11
|
+
|
|
12
|
+
// Output paths
|
|
13
|
+
const outputDir = path.join(distDir, parsedPath.dir);
|
|
14
|
+
const jsOutputPath = path.join(outputDir, `${parsedPath.name}.js`);
|
|
15
|
+
const htmlOutputPath = path.join(outputDir, `${parsedPath.name}.html`);
|
|
16
|
+
|
|
17
|
+
// Ensure output directory exists
|
|
18
|
+
if (!fs.existsSync(outputDir)) {
|
|
19
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
console.log(`📝 Compiling: ${relativePath}`);
|
|
23
|
+
|
|
24
|
+
// Read the .jux file
|
|
25
|
+
const juxContent = fs.readFileSync(juxFilePath, 'utf-8');
|
|
26
|
+
|
|
27
|
+
// Calculate depth for relative paths
|
|
28
|
+
const depth = parsedPath.dir.split(path.sep).filter(p => p).length;
|
|
29
|
+
const libPath = depth === 0 ? './lib/jux.js' : '../'.repeat(depth) + 'lib/jux.js';
|
|
30
|
+
const styleBasePath = depth === 0 ? './lib/styles/' : '../'.repeat(depth) + 'lib/styles/';
|
|
31
|
+
|
|
32
|
+
// Transform imports
|
|
33
|
+
let transformedContent = juxContent;
|
|
34
|
+
|
|
35
|
+
// Replace common import patterns with calculated path
|
|
36
|
+
transformedContent = transformedContent.replace(
|
|
37
|
+
/from\s+['"]\.\.?\/lib\/jux\.js['"]/g,
|
|
38
|
+
`from '${libPath}'`
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
// Only inject import if:
|
|
42
|
+
// 1. File is not empty (ignoring whitespace and comments)
|
|
43
|
+
// 2. File uses 'jux.' but has no import statement
|
|
44
|
+
const contentWithoutComments = transformedContent
|
|
45
|
+
.replace(/\/\*[\s\S]*?\*\//g, '') // Remove block comments
|
|
46
|
+
.replace(/\/\/.*/g, '') // Remove line comments
|
|
47
|
+
.trim();
|
|
48
|
+
|
|
49
|
+
const hasContent = contentWithoutComments.length > 0;
|
|
50
|
+
const usesJux = /\bjux\./g.test(contentWithoutComments);
|
|
51
|
+
const hasImport = /import\s+.*from/.test(transformedContent);
|
|
52
|
+
|
|
53
|
+
if (hasContent && usesJux && !hasImport) {
|
|
54
|
+
transformedContent = `import { jux } from '${libPath}';\n\n${transformedContent}`;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Write the transformed JS
|
|
58
|
+
fs.writeFileSync(jsOutputPath, transformedContent);
|
|
59
|
+
|
|
60
|
+
console.log(` ✓ JS: ${path.relative(projectRoot, jsOutputPath)}`);
|
|
61
|
+
|
|
62
|
+
// Generate HTML with correct script path
|
|
63
|
+
const scriptPath = `./${parsedPath.name}.js`;
|
|
64
|
+
|
|
65
|
+
const html = `<!DOCTYPE html>
|
|
66
|
+
<html lang="en">
|
|
67
|
+
<head>
|
|
68
|
+
<meta charset="UTF-8">
|
|
69
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
70
|
+
<title>${parsedPath.name}</title>
|
|
71
|
+
|
|
72
|
+
<!-- JUX Core Styles -->
|
|
73
|
+
<link rel="stylesheet" href="${styleBasePath}tokens/dark.css">
|
|
74
|
+
<link rel="stylesheet" href="${styleBasePath}global.css">
|
|
75
|
+
</head>
|
|
76
|
+
<body data-theme="dark">
|
|
77
|
+
<!-- App container -->
|
|
78
|
+
<div id="app" data-jux-page="${parsedPath.name}"></div>
|
|
79
|
+
|
|
80
|
+
<!-- Main content -->
|
|
81
|
+
<div id="appmain"></div>
|
|
82
|
+
|
|
83
|
+
<!-- Modal overlay -->
|
|
84
|
+
<div id="appmodal" aria-hidden="true" role="dialog"></div>
|
|
85
|
+
|
|
86
|
+
<script type="module" src="${scriptPath}"></script>
|
|
87
|
+
${isServe ? `
|
|
88
|
+
<!-- Hot reload -->
|
|
89
|
+
<script type="module">
|
|
90
|
+
const ws = new WebSocket('ws://localhost:3001');
|
|
91
|
+
ws.onmessage = (msg) => {
|
|
92
|
+
const data = JSON.parse(msg.data);
|
|
93
|
+
if (data.type === 'reload') {
|
|
94
|
+
console.log('🔄 Reloading page...');
|
|
95
|
+
location.reload();
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
ws.onerror = () => console.warn('⚠️ WebSocket connection failed');
|
|
99
|
+
</script>
|
|
100
|
+
` : ''}
|
|
101
|
+
</body>
|
|
102
|
+
</html>`;
|
|
103
|
+
|
|
104
|
+
fs.writeFileSync(htmlOutputPath, html);
|
|
105
|
+
console.log(` ✓ HTML: ${path.relative(projectRoot, htmlOutputPath)}`);
|
|
106
|
+
|
|
107
|
+
return { jsOutputPath, htmlOutputPath };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export async function copyLibToOutput(projectRoot, distDir) {
|
|
111
|
+
// Simplified lib path resolution
|
|
112
|
+
const libSrc = path.resolve(projectRoot, '../lib');
|
|
113
|
+
|
|
114
|
+
if (!fs.existsSync(libSrc)) {
|
|
115
|
+
throw new Error(`lib/ directory not found at ${libSrc}`);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const libDest = path.join(distDir, 'lib');
|
|
119
|
+
|
|
120
|
+
console.log('📦 Building TypeScript library...');
|
|
121
|
+
console.log(` From: ${libSrc}`);
|
|
122
|
+
console.log(` To: ${libDest}`);
|
|
123
|
+
|
|
124
|
+
if (fs.existsSync(libDest)) {
|
|
125
|
+
fs.rmSync(libDest, { recursive: true });
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
fs.mkdirSync(libDest, { recursive: true });
|
|
129
|
+
|
|
130
|
+
// Find all TypeScript entry points
|
|
131
|
+
const tsFiles = findFiles(libSrc, '.ts');
|
|
132
|
+
|
|
133
|
+
if (tsFiles.length === 0) {
|
|
134
|
+
console.warn('⚠️ No TypeScript files found in lib/');
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
console.log(` Found ${tsFiles.length} TypeScript files`);
|
|
139
|
+
|
|
140
|
+
// Build all TypeScript files with esbuild
|
|
141
|
+
try {
|
|
142
|
+
await esbuild.build({
|
|
143
|
+
entryPoints: tsFiles,
|
|
144
|
+
bundle: false,
|
|
145
|
+
format: 'esm',
|
|
146
|
+
outdir: libDest,
|
|
147
|
+
outbase: libSrc,
|
|
148
|
+
platform: 'browser',
|
|
149
|
+
target: 'es2020',
|
|
150
|
+
loader: {
|
|
151
|
+
'.ts': 'ts'
|
|
152
|
+
},
|
|
153
|
+
logLevel: 'warning'
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
console.log(' ✓ TypeScript compiled to JavaScript');
|
|
157
|
+
|
|
158
|
+
// Copy non-TS files (CSS, HTML, etc.)
|
|
159
|
+
console.log(' Copying lib assets...');
|
|
160
|
+
copyNonTsFiles(libSrc, libDest);
|
|
161
|
+
console.log(' ✓ Lib assets copied');
|
|
162
|
+
|
|
163
|
+
} catch (err) {
|
|
164
|
+
console.error('❌ Failed to build TypeScript:', err.message);
|
|
165
|
+
throw err;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
console.log('✅ Library ready\n');
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export async function copyProjectAssets(projectRoot, distDir) {
|
|
172
|
+
console.log('📦 Copying project assets...');
|
|
173
|
+
|
|
174
|
+
// Find all CSS and JS files in project root (excluding node_modules, dist, .git)
|
|
175
|
+
const allFiles = [];
|
|
176
|
+
findProjectFiles(projectRoot, ['.css', '.js'], allFiles, projectRoot);
|
|
177
|
+
|
|
178
|
+
console.log(` Found ${allFiles.length} asset file(s)`);
|
|
179
|
+
|
|
180
|
+
for (const srcPath of allFiles) {
|
|
181
|
+
const relativePath = path.relative(projectRoot, srcPath);
|
|
182
|
+
const destPath = path.join(distDir, relativePath);
|
|
183
|
+
const destDir = path.dirname(destPath);
|
|
184
|
+
|
|
185
|
+
// Create destination directory if needed
|
|
186
|
+
if (!fs.existsSync(destDir)) {
|
|
187
|
+
fs.mkdirSync(destDir, { recursive: true });
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Copy file
|
|
191
|
+
fs.copyFileSync(srcPath, destPath);
|
|
192
|
+
console.log(` ✓ ${relativePath}`);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
console.log('✅ Project assets copied\n');
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function findFiles(dir, extension, fileList = []) {
|
|
199
|
+
const files = fs.readdirSync(dir);
|
|
200
|
+
|
|
201
|
+
files.forEach(file => {
|
|
202
|
+
const filePath = path.join(dir, file);
|
|
203
|
+
const stat = fs.statSync(filePath);
|
|
204
|
+
|
|
205
|
+
if (stat.isDirectory()) {
|
|
206
|
+
findFiles(filePath, extension, fileList);
|
|
207
|
+
} else if (file.endsWith(extension)) {
|
|
208
|
+
fileList.push(filePath);
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
return fileList;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function copyNonTsFiles(src, dest) {
|
|
216
|
+
const entries = fs.readdirSync(src, { withFileTypes: true });
|
|
217
|
+
|
|
218
|
+
for (const entry of entries) {
|
|
219
|
+
const srcPath = path.join(src, entry.name);
|
|
220
|
+
const destPath = path.join(dest, entry.name);
|
|
221
|
+
|
|
222
|
+
if (entry.isDirectory()) {
|
|
223
|
+
if (!fs.existsSync(destPath)) {
|
|
224
|
+
fs.mkdirSync(destPath, { recursive: true });
|
|
225
|
+
}
|
|
226
|
+
copyNonTsFiles(srcPath, destPath);
|
|
227
|
+
} else if (entry.isFile()) {
|
|
228
|
+
const ext = path.extname(entry.name);
|
|
229
|
+
// Copy CSS, JSON, and JS files (but not .ts files)
|
|
230
|
+
if (ext === '.css' || ext === '.json' || (ext === '.js' && !srcPath.endsWith('.ts'))) {
|
|
231
|
+
fs.copyFileSync(srcPath, destPath);
|
|
232
|
+
console.log(` → Copied: ${path.relative(src, srcPath)}`);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function findProjectFiles(dir, extensions, fileList = [], rootDir = dir, excludeDirs = ['node_modules', 'dist', '.git', 'lib']) {
|
|
239
|
+
if (!fs.existsSync(dir)) return fileList;
|
|
240
|
+
|
|
241
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
242
|
+
|
|
243
|
+
for (const entry of entries) {
|
|
244
|
+
const fullPath = path.join(dir, entry.name);
|
|
245
|
+
|
|
246
|
+
if (entry.isDirectory()) {
|
|
247
|
+
// Skip excluded directories
|
|
248
|
+
if (excludeDirs.includes(entry.name)) {
|
|
249
|
+
continue;
|
|
250
|
+
}
|
|
251
|
+
findProjectFiles(fullPath, extensions, fileList, rootDir, excludeDirs);
|
|
252
|
+
} else {
|
|
253
|
+
// Check if file has one of the desired extensions
|
|
254
|
+
const hasExtension = extensions.some(ext => entry.name.endsWith(ext));
|
|
255
|
+
if (hasExtension) {
|
|
256
|
+
fileList.push(fullPath);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
return fileList;
|
|
262
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { glob } from 'glob';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Generate documentation from TypeScript component files
|
|
7
|
+
*/
|
|
8
|
+
export async function generateDocs(projectRoot) {
|
|
9
|
+
const libRoot = path.resolve(projectRoot, '../lib');
|
|
10
|
+
const componentsDir = path.join(libRoot, 'components');
|
|
11
|
+
|
|
12
|
+
console.log(` Scanning: ${componentsDir}`);
|
|
13
|
+
|
|
14
|
+
// Find all component TypeScript files
|
|
15
|
+
const componentFiles = glob.sync('*.ts', {
|
|
16
|
+
cwd: componentsDir,
|
|
17
|
+
absolute: true,
|
|
18
|
+
ignore: ['reactivity.js', 'error-handler.ts', 'docs.ts']
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
console.log(` Found ${componentFiles.length} component files`);
|
|
22
|
+
|
|
23
|
+
const components = [];
|
|
24
|
+
|
|
25
|
+
for (const filePath of componentFiles) {
|
|
26
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
27
|
+
const componentDoc = parseComponentFile(content, filePath);
|
|
28
|
+
|
|
29
|
+
if (componentDoc) {
|
|
30
|
+
components.push(componentDoc);
|
|
31
|
+
console.log(` ✓ ${componentDoc.name}`);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Sort by category and name
|
|
36
|
+
components.sort((a, b) => {
|
|
37
|
+
const categoryCompare = (a.category || 'ZZZ').localeCompare(b.category || 'ZZZ');
|
|
38
|
+
if (categoryCompare !== 0) return categoryCompare;
|
|
39
|
+
return a.name.localeCompare(b.name);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
// Generate the docs data
|
|
43
|
+
const docsData = {
|
|
44
|
+
components,
|
|
45
|
+
version: '1.0.0',
|
|
46
|
+
lastUpdated: new Date().toISOString()
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
// Write to JSON file
|
|
50
|
+
const outputPath = path.join(componentsDir, 'docs-data.json');
|
|
51
|
+
fs.writeFileSync(outputPath, JSON.stringify(docsData, null, 2));
|
|
52
|
+
|
|
53
|
+
console.log(` Generated: ${outputPath}`);
|
|
54
|
+
|
|
55
|
+
return docsData;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Parse a single component file
|
|
60
|
+
*/
|
|
61
|
+
function parseComponentFile(content, filePath) {
|
|
62
|
+
const fileName = path.basename(filePath, '.ts');
|
|
63
|
+
const className = fileName.charAt(0).toUpperCase() + fileName.slice(1);
|
|
64
|
+
|
|
65
|
+
// Extract category from file content or infer
|
|
66
|
+
const category = inferCategory(className, content);
|
|
67
|
+
|
|
68
|
+
// Extract description from class JSDoc
|
|
69
|
+
const descMatch = content.match(/\/\*\*\s*\n\s*\*\s*([^\n*]+)/);
|
|
70
|
+
const description = descMatch ? descMatch[1].trim() : `${className} component`;
|
|
71
|
+
|
|
72
|
+
// Extract constructor/factory pattern
|
|
73
|
+
const factoryMatch = content.match(/export function\s+\w+\(([^)]*)\)/);
|
|
74
|
+
const constructorSig = factoryMatch
|
|
75
|
+
? `jux.${fileName}(${factoryMatch[1]})`
|
|
76
|
+
: `new ${className}()`;
|
|
77
|
+
|
|
78
|
+
// Extract fluent methods
|
|
79
|
+
const fluentMethods = extractFluentMethods(content, className);
|
|
80
|
+
|
|
81
|
+
// Extract usage example from JSDoc
|
|
82
|
+
const exampleMatch = content.match(/\*\s+Usage:\s*\n\s*\*\s+(.+?)(?:\n|$)/);
|
|
83
|
+
let example = exampleMatch
|
|
84
|
+
? exampleMatch[1].trim()
|
|
85
|
+
: `jux.${fileName}('id').render()`;
|
|
86
|
+
|
|
87
|
+
return {
|
|
88
|
+
name: className,
|
|
89
|
+
category,
|
|
90
|
+
description,
|
|
91
|
+
constructor: constructorSig,
|
|
92
|
+
fluentMethods,
|
|
93
|
+
example
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Extract fluent API methods from class
|
|
99
|
+
*/
|
|
100
|
+
function extractFluentMethods(content, className) {
|
|
101
|
+
const methods = [];
|
|
102
|
+
|
|
103
|
+
// Match method patterns: methodName(params): this
|
|
104
|
+
const methodRegex = /^\s*(\w+)\(([^)]*)\):\s*this\s*\{/gm;
|
|
105
|
+
let match;
|
|
106
|
+
|
|
107
|
+
while ((match = methodRegex.exec(content)) !== null) {
|
|
108
|
+
const methodName = match[1];
|
|
109
|
+
const params = match[2];
|
|
110
|
+
|
|
111
|
+
// Skip private methods and constructor
|
|
112
|
+
if (methodName.startsWith('_') || methodName === 'constructor') continue;
|
|
113
|
+
|
|
114
|
+
// Clean up params
|
|
115
|
+
const cleanParams = params
|
|
116
|
+
.split(',')
|
|
117
|
+
.map(p => {
|
|
118
|
+
const parts = p.trim().split(':');
|
|
119
|
+
return parts[0].trim();
|
|
120
|
+
})
|
|
121
|
+
.filter(p => p)
|
|
122
|
+
.join(', ');
|
|
123
|
+
|
|
124
|
+
methods.push({
|
|
125
|
+
name: methodName,
|
|
126
|
+
params: cleanParams ? `(${cleanParams})` : '()',
|
|
127
|
+
returns: 'this',
|
|
128
|
+
description: `Set ${methodName}`
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Also look for render methods
|
|
133
|
+
const renderMatch = content.match(/^\s*render\(([^)]*)\):\s*(\w+)/m);
|
|
134
|
+
if (renderMatch && !methods.find(m => m.name === 'render')) {
|
|
135
|
+
methods.push({
|
|
136
|
+
name: 'render',
|
|
137
|
+
params: renderMatch[1] ? `(${renderMatch[1]})` : '()',
|
|
138
|
+
returns: renderMatch[2],
|
|
139
|
+
description: 'Render component to DOM'
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return methods;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Infer category from component name or content
|
|
148
|
+
*/
|
|
149
|
+
function inferCategory(name, content) {
|
|
150
|
+
const dataComponents = ['Table', 'List', 'Chart', 'Data'];
|
|
151
|
+
const coreComponents = ['App', 'Layout', 'Theme', 'Style', 'Script', 'Import'];
|
|
152
|
+
|
|
153
|
+
if (dataComponents.some(dc => name.includes(dc))) {
|
|
154
|
+
return 'Data Components';
|
|
155
|
+
}
|
|
156
|
+
if (coreComponents.includes(name)) {
|
|
157
|
+
return 'Core';
|
|
158
|
+
}
|
|
159
|
+
return 'UI Components';
|
|
160
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
4
|
+
import CleanCSS from 'clean-css';
|
|
5
|
+
import { FileValidator } from '../validators/file-validator.js';
|
|
6
|
+
|
|
7
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
8
|
+
const __dirname = path.dirname(__filename);
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Generates CSS content from Jux configuration
|
|
12
|
+
* Handles: global.css, themes, imports, styleImports, and styleInline blocks
|
|
13
|
+
*/
|
|
14
|
+
export function generateCSS(juxConfig, layoutParsed = null) {
|
|
15
|
+
const fileValidator = new FileValidator();
|
|
16
|
+
let cssOutput = '';
|
|
17
|
+
|
|
18
|
+
// 1. Add global.css (always first)
|
|
19
|
+
const globalCssPath = path.join(__dirname, '../../lib/global.css');
|
|
20
|
+
if (fs.existsSync(globalCssPath)) {
|
|
21
|
+
cssOutput += `/* Global Styles */\n`;
|
|
22
|
+
cssOutput += fs.readFileSync(globalCssPath, 'utf-8') + '\n\n';
|
|
23
|
+
console.log(` ✓ Included global.css`);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// 2. Add theme CSS (layout theme first, then page theme if different)
|
|
27
|
+
const themesToLoad = [];
|
|
28
|
+
|
|
29
|
+
if (layoutParsed?.config?.theme) {
|
|
30
|
+
themesToLoad.push({ theme: layoutParsed.config.theme, source: 'layout' });
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (juxConfig.theme && juxConfig.theme !== layoutParsed?.config?.theme) {
|
|
34
|
+
themesToLoad.push({ theme: juxConfig.theme, source: 'page' });
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
for (const { theme, source } of themesToLoad) {
|
|
38
|
+
const themePath = path.join(__dirname, '../../lib/themes', `${theme}.css`);
|
|
39
|
+
if (fs.existsSync(themePath)) {
|
|
40
|
+
cssOutput += `/* Theme: ${theme} (${source}) */\n`;
|
|
41
|
+
cssOutput += fs.readFileSync(themePath, 'utf-8') + '\n\n';
|
|
42
|
+
console.log(` ✓ Included theme: ${theme} (${source})`);
|
|
43
|
+
} else {
|
|
44
|
+
console.warn(` ⚠️ Theme not found: ${theme}`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// 3. Process @import directives (CSS files only from layout, then page)
|
|
49
|
+
const allImports = [
|
|
50
|
+
...(layoutParsed?.config?.import || []),
|
|
51
|
+
...(juxConfig.import || [])
|
|
52
|
+
];
|
|
53
|
+
|
|
54
|
+
if (allImports.length > 0) {
|
|
55
|
+
const { categorized } = fileValidator.categorizeImports(allImports);
|
|
56
|
+
|
|
57
|
+
// Only process CSS files
|
|
58
|
+
for (const cssImport of categorized.css) {
|
|
59
|
+
const resolvedPath = path.join(__dirname, '../../', cssImport);
|
|
60
|
+
if (fs.existsSync(resolvedPath)) {
|
|
61
|
+
cssOutput += `/* Import: ${cssImport} */\n`;
|
|
62
|
+
cssOutput += fs.readFileSync(resolvedPath, 'utf-8') + '\n\n';
|
|
63
|
+
console.log(` ✓ Included import: ${cssImport}`);
|
|
64
|
+
} else {
|
|
65
|
+
console.warn(` ⚠️ Import not found: ${cssImport} (resolved to ${resolvedPath})`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Log skipped JS imports (will be handled in HTML)
|
|
70
|
+
if (categorized.js.length > 0) {
|
|
71
|
+
console.log(` ℹ️ Skipped JS imports (will be added to HTML): ${categorized.js.length} file(s)`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// 4. Process @style imports (CSS file references from layout, then page)
|
|
76
|
+
const allStyleImports = [
|
|
77
|
+
...(layoutParsed?.config?.styleImports || []),
|
|
78
|
+
...(juxConfig.styleImports || [])
|
|
79
|
+
];
|
|
80
|
+
|
|
81
|
+
for (const styleImport of allStyleImports) {
|
|
82
|
+
// Handle URLs (CDN)
|
|
83
|
+
if (styleImport.startsWith('http://') || styleImport.startsWith('https://')) {
|
|
84
|
+
cssOutput += `/* External CSS: ${styleImport} */\n`;
|
|
85
|
+
cssOutput += `@import url('${styleImport}');\n\n`;
|
|
86
|
+
console.log(` ✓ Added CDN import: ${styleImport}`);
|
|
87
|
+
} else {
|
|
88
|
+
// Handle local files
|
|
89
|
+
const resolvedPath = path.join(__dirname, '../../', styleImport);
|
|
90
|
+
if (fs.existsSync(resolvedPath)) {
|
|
91
|
+
cssOutput += `/* Style Import: ${styleImport} */\n`;
|
|
92
|
+
cssOutput += fs.readFileSync(resolvedPath, 'utf-8') + '\n\n';
|
|
93
|
+
console.log(` ✓ Included style import: ${styleImport}`);
|
|
94
|
+
} else {
|
|
95
|
+
console.warn(` ⚠️ Style import not found: ${styleImport} (resolved to ${resolvedPath})`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// 5. Process inline @style blocks (layout first, then page)
|
|
101
|
+
const allInlineStyles = [
|
|
102
|
+
...(layoutParsed?.config?.styleInline || []),
|
|
103
|
+
...(juxConfig.styleInline || [])
|
|
104
|
+
];
|
|
105
|
+
|
|
106
|
+
if (allInlineStyles.length > 0) {
|
|
107
|
+
cssOutput += `/* Inline Styles (${allInlineStyles.length} block(s)) */\n`;
|
|
108
|
+
|
|
109
|
+
allInlineStyles.forEach((styleBlock, index) => {
|
|
110
|
+
const source = index < (layoutParsed?.config?.styleInline?.length || 0) ? 'layout' : 'page';
|
|
111
|
+
|
|
112
|
+
try {
|
|
113
|
+
const validatedStyle = fileValidator.validateStyleContent(styleBlock, `inline block #${index + 1}`);
|
|
114
|
+
|
|
115
|
+
if (!fileValidator.isEmptyStyle(validatedStyle)) {
|
|
116
|
+
cssOutput += `/* Inline Block #${index + 1} (${source}) */\n`;
|
|
117
|
+
cssOutput += validatedStyle + '\n\n';
|
|
118
|
+
}
|
|
119
|
+
} catch (error) {
|
|
120
|
+
console.error(` ❌ ${error.message}`);
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
console.log(` ✓ Included ${allInlineStyles.length} inline style block(s)`);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return cssOutput.trim();
|
|
128
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Generates a simple HTML wrapper for a .jux file
|
|
6
|
+
* No directives, no parsing - just a clean HTML shell
|
|
7
|
+
*/
|
|
8
|
+
export function generateHTML(fileName, options = {}) {
|
|
9
|
+
const { fileDir = '.', pathPrefix = './', isServe = false } = options;
|
|
10
|
+
|
|
11
|
+
const prefix = pathPrefix;
|
|
12
|
+
|
|
13
|
+
// Page script path
|
|
14
|
+
const pageScriptPath = `${prefix}${fileName}.js`;
|
|
15
|
+
|
|
16
|
+
// Build app structure with semantic nodes
|
|
17
|
+
const appStructure = buildAppStructure(fileName);
|
|
18
|
+
|
|
19
|
+
// Hot reload script (only in serve mode)
|
|
20
|
+
const hotReloadScript = isServe ? `
|
|
21
|
+
<!-- Hot Reload -->
|
|
22
|
+
<script>
|
|
23
|
+
(function() {
|
|
24
|
+
const ws = new WebSocket('ws://' + location.host);
|
|
25
|
+
ws.onmessage = (event) => {
|
|
26
|
+
const data = JSON.parse(event.data);
|
|
27
|
+
if (data.type === 'reload') {
|
|
28
|
+
console.log('🔄 Reloading...');
|
|
29
|
+
location.reload();
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
ws.onclose = () => console.log('🔌 Hot reload disconnected');
|
|
33
|
+
})();
|
|
34
|
+
</script>` : '';
|
|
35
|
+
|
|
36
|
+
// Build complete HTML
|
|
37
|
+
const html = `<!DOCTYPE html>
|
|
38
|
+
<html lang="en">
|
|
39
|
+
<head>
|
|
40
|
+
<meta charset="UTF-8">
|
|
41
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
42
|
+
<title>${fileName}</title>
|
|
43
|
+
|
|
44
|
+
<!-- JUX Core Styles -->
|
|
45
|
+
<link rel="stylesheet" href="${prefix}lib/styles/tokens/dark.css">
|
|
46
|
+
<link rel="stylesheet" href="${prefix}lib/styles/global.css">
|
|
47
|
+
</head>
|
|
48
|
+
<body data-theme="dark">
|
|
49
|
+
${appStructure}
|
|
50
|
+
|
|
51
|
+
<!-- Page Script -->
|
|
52
|
+
<script type="module" src="${pageScriptPath}"></script>${hotReloadScript}
|
|
53
|
+
</body>
|
|
54
|
+
</html>`;
|
|
55
|
+
|
|
56
|
+
return html;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Build semantic app structure - Always includes all nodes
|
|
61
|
+
*/
|
|
62
|
+
function buildAppStructure(pageName) {
|
|
63
|
+
const page = pageName || 'index';
|
|
64
|
+
|
|
65
|
+
return ` <!-- App Container -->
|
|
66
|
+
<div id="app" data-jux-page="${page}">
|
|
67
|
+
<!-- Header -->
|
|
68
|
+
<header id="appheader">
|
|
69
|
+
<div id="appheader-logo"></div>
|
|
70
|
+
<nav id="appheader-nav"></nav>
|
|
71
|
+
<div id="appheader-actions"></div>
|
|
72
|
+
</header>
|
|
73
|
+
|
|
74
|
+
<!-- Subheader (breadcrumbs, tabs, etc.) -->
|
|
75
|
+
<div id="appsubheader">
|
|
76
|
+
<div id="appsubheader-breadcrumbs"></div>
|
|
77
|
+
<div id="appsubheader-tabs"></div>
|
|
78
|
+
<div id="appsubheader-actions"></div>
|
|
79
|
+
</div>
|
|
80
|
+
|
|
81
|
+
<!-- Sidebar -->
|
|
82
|
+
<aside id="appsidebar">
|
|
83
|
+
<div id="appsidebar-header"></div>
|
|
84
|
+
<div id="appsidebar-content"></div>
|
|
85
|
+
<div id="appsidebar-footer"></div>
|
|
86
|
+
</aside>
|
|
87
|
+
|
|
88
|
+
<!-- Main content area -->
|
|
89
|
+
<main id="appmain"></main>
|
|
90
|
+
|
|
91
|
+
<!-- Footer -->
|
|
92
|
+
<footer id="appfooter">
|
|
93
|
+
<div id="appfooter-content"></div>
|
|
94
|
+
<div id="appfooter-legal"></div>
|
|
95
|
+
</footer>
|
|
96
|
+
</div>
|
|
97
|
+
|
|
98
|
+
<!-- Modal overlay -->
|
|
99
|
+
<div id="appmodal" aria-hidden="true" role="dialog">
|
|
100
|
+
<div id="appmodal-backdrop"></div>
|
|
101
|
+
<div id="appmodal-container">
|
|
102
|
+
<button id="appmodal-close" aria-label="Close modal">×</button>
|
|
103
|
+
<header id="appmodal-header"></header>
|
|
104
|
+
<div id="appmodal-content"></div>
|
|
105
|
+
<footer id="appmodal-footer"></footer>
|
|
106
|
+
</div>
|
|
107
|
+
</div>`;
|
|
108
|
+
}
|