juxscript 1.0.105 → 1.0.107
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/lib/componentsv2/grid/component.js +0 -4
- package/machinery/build3.js +141 -3
- package/machinery/compiler3.js +168 -166
- package/package.json +1 -1
|
@@ -19,10 +19,6 @@ export function Grid(id, options = {}) {
|
|
|
19
19
|
};
|
|
20
20
|
// @ts-ignore
|
|
21
21
|
engine.injectCSS = (id, css) => skin.injectCSS(id, css);
|
|
22
|
-
// --- Structural API ---
|
|
23
|
-
// --- Tools ---
|
|
24
|
-
// @ts-ignore
|
|
25
|
-
engine.gridder = (active) => { engine.toggleGridder(active); return engine; };
|
|
26
22
|
// --- Helpers ---
|
|
27
23
|
// @ts-ignore
|
|
28
24
|
engine.getCellId = (row, col) => `${id}-${row}-${col}`;
|
package/machinery/build3.js
CHANGED
|
@@ -1,12 +1,150 @@
|
|
|
1
1
|
import { JuxCompiler } from './compiler3.js';
|
|
2
2
|
import path from 'path';
|
|
3
|
+
import fs from 'fs';
|
|
3
4
|
|
|
4
|
-
// Resolve paths relative to CWD (user's project)
|
|
5
5
|
const PROJECT_ROOT = process.cwd();
|
|
6
6
|
|
|
7
|
+
// ═══════════════════════════════════════════════════════════════
|
|
8
|
+
// LOAD CONFIG
|
|
9
|
+
// ═══════════════════════════════════════════════════════════════
|
|
10
|
+
const JUX_CONFIG_PATH = path.resolve(PROJECT_ROOT, 'juxconfig.js');
|
|
11
|
+
let rawConfig = {};
|
|
12
|
+
|
|
13
|
+
try {
|
|
14
|
+
rawConfig = (await import(JUX_CONFIG_PATH)).config;
|
|
15
|
+
console.log(`⚙️ Loaded config: ${JUX_CONFIG_PATH}`);
|
|
16
|
+
} catch (err) {
|
|
17
|
+
console.warn(`⚠️ No juxconfig.js found, using defaults`);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// ═══════════════════════════════════════════════════════════════
|
|
21
|
+
// EXPLODE CONFIG INTO NAMED OBJECTS
|
|
22
|
+
// ═══════════════════════════════════════════════════════════════
|
|
23
|
+
const directories = {
|
|
24
|
+
source: rawConfig.directories?.source || './jux',
|
|
25
|
+
distribution: rawConfig.directories?.distribution || './.jux-dist',
|
|
26
|
+
themes: rawConfig.directories?.themes || './themes',
|
|
27
|
+
layouts: rawConfig.directories?.layouts || './themes/layouts',
|
|
28
|
+
assets: rawConfig.directories?.assets || './themes/assets'
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const defaults = {
|
|
32
|
+
httpPort: rawConfig.defaults?.httpPort || 3000,
|
|
33
|
+
wsPort: rawConfig.defaults?.wsPort || 3001,
|
|
34
|
+
autoRoute: rawConfig.defaults?.autoRoute ?? true,
|
|
35
|
+
layout: rawConfig.defaults?.layout || null,
|
|
36
|
+
theme: rawConfig.defaults?.theme || null
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
// Resolve absolute paths
|
|
40
|
+
const paths = {
|
|
41
|
+
source: path.resolve(PROJECT_ROOT, directories.source),
|
|
42
|
+
distribution: path.resolve(PROJECT_ROOT, directories.distribution),
|
|
43
|
+
themes: path.resolve(PROJECT_ROOT, directories.source, directories.themes),
|
|
44
|
+
layouts: path.resolve(PROJECT_ROOT, directories.source, directories.layouts),
|
|
45
|
+
assets: path.resolve(PROJECT_ROOT, directories.source, directories.assets)
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
// ═══════════════════════════════════════════════════════════════
|
|
49
|
+
// VALIDATE DIRECTORIES
|
|
50
|
+
// ═══════════════════════════════════════════════════════════════
|
|
51
|
+
console.log(`\n📁 Directory Check:`);
|
|
52
|
+
|
|
53
|
+
const dirStatus = {};
|
|
54
|
+
for (const [name, dirPath] of Object.entries(paths)) {
|
|
55
|
+
if (name === 'distribution') continue;
|
|
56
|
+
|
|
57
|
+
const exists = fs.existsSync(dirPath);
|
|
58
|
+
dirStatus[name] = exists;
|
|
59
|
+
|
|
60
|
+
const icon = exists ? '✓' : '✗';
|
|
61
|
+
const suffix = exists ? '' : ' (will be skipped)';
|
|
62
|
+
console.log(` ${icon} ${name}: ${dirPath}${suffix}`);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// ═══════════════════════════════════════════════════════════════
|
|
66
|
+
// RESOLVE DEFAULT LAYOUT & THEME
|
|
67
|
+
// ═══════════════════════════════════════════════════════════════
|
|
68
|
+
function resolveFile(filename, ...searchPaths) {
|
|
69
|
+
if (!filename) return null;
|
|
70
|
+
|
|
71
|
+
// First try the filename as-is
|
|
72
|
+
if (fs.existsSync(filename)) {
|
|
73
|
+
return path.resolve(filename);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Try each search path
|
|
77
|
+
for (const searchPath of searchPaths) {
|
|
78
|
+
if (!searchPath || !fs.existsSync(searchPath)) continue;
|
|
79
|
+
|
|
80
|
+
const fullPath = path.resolve(searchPath, filename);
|
|
81
|
+
if (fs.existsSync(fullPath)) {
|
|
82
|
+
return fullPath;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
console.log(`\n🎨 Defaults Resolution:`);
|
|
90
|
+
|
|
91
|
+
// Resolve layout
|
|
92
|
+
const layoutPath = defaults.layout ? resolveFile(
|
|
93
|
+
defaults.layout,
|
|
94
|
+
paths.layouts,
|
|
95
|
+
paths.source,
|
|
96
|
+
PROJECT_ROOT
|
|
97
|
+
) : null;
|
|
98
|
+
|
|
99
|
+
if (defaults.layout) {
|
|
100
|
+
if (layoutPath) {
|
|
101
|
+
console.log(` ✓ layout: ${layoutPath}`);
|
|
102
|
+
} else {
|
|
103
|
+
console.log(` ✗ layout: "${defaults.layout}" not found, will be skipped`);
|
|
104
|
+
}
|
|
105
|
+
} else {
|
|
106
|
+
console.log(` - layout: not configured`);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Resolve theme
|
|
110
|
+
const themePath = defaults.theme ? resolveFile(
|
|
111
|
+
defaults.theme,
|
|
112
|
+
paths.themes,
|
|
113
|
+
paths.source,
|
|
114
|
+
PROJECT_ROOT
|
|
115
|
+
) : null;
|
|
116
|
+
|
|
117
|
+
if (defaults.theme) {
|
|
118
|
+
if (themePath) {
|
|
119
|
+
console.log(` ✓ theme: ${themePath}`);
|
|
120
|
+
} else {
|
|
121
|
+
console.log(` ✗ theme: "${defaults.theme}" not found, will be skipped`);
|
|
122
|
+
}
|
|
123
|
+
} else {
|
|
124
|
+
console.log(` - theme: not configured`);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// ═══════════════════════════════════════════════════════════════
|
|
128
|
+
// VALIDATE SOURCE DIRECTORY EXISTS
|
|
129
|
+
// ═══════════════════════════════════════════════════════════════
|
|
130
|
+
if (!dirStatus.source) {
|
|
131
|
+
console.error(`\n❌ Source directory not found: ${paths.source}`);
|
|
132
|
+
console.error(` Create the directory or update juxconfig.js`);
|
|
133
|
+
process.exit(1);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// ═══════════════════════════════════════════════════════════════
|
|
137
|
+
// RUN BUILD
|
|
138
|
+
// ═══════════════════════════════════════════════════════════════
|
|
139
|
+
console.log(`\n`);
|
|
140
|
+
|
|
7
141
|
const compiler = new JuxCompiler({
|
|
8
|
-
srcDir:
|
|
9
|
-
distDir:
|
|
142
|
+
srcDir: paths.source,
|
|
143
|
+
distDir: paths.distribution,
|
|
144
|
+
layoutPath,
|
|
145
|
+
themePath,
|
|
146
|
+
defaults,
|
|
147
|
+
paths
|
|
10
148
|
});
|
|
11
149
|
|
|
12
150
|
compiler.build()
|
package/machinery/compiler3.js
CHANGED
|
@@ -13,6 +13,10 @@ export class JuxCompiler {
|
|
|
13
13
|
this.config = config;
|
|
14
14
|
this.srcDir = config.srcDir || './jux';
|
|
15
15
|
this.distDir = config.distDir || './.jux-dist';
|
|
16
|
+
this.layoutPath = config.layoutPath || null;
|
|
17
|
+
this.themePath = config.themePath || null;
|
|
18
|
+
this.defaults = config.defaults || {};
|
|
19
|
+
this.paths = config.paths || {};
|
|
16
20
|
this._juxscriptExports = null;
|
|
17
21
|
this._juxscriptPath = null;
|
|
18
22
|
}
|
|
@@ -61,7 +65,7 @@ export class JuxCompiler {
|
|
|
61
65
|
|
|
62
66
|
scanFiles() {
|
|
63
67
|
const files = fs.readdirSync(this.srcDir)
|
|
64
|
-
.filter(f => f.endsWith('.jux') || f.endsWith('.js'));
|
|
68
|
+
.filter(f => (f.endsWith('.jux') || f.endsWith('.js')) && !this.isAssetFile(f));
|
|
65
69
|
|
|
66
70
|
const views = [], dataModules = [], sharedModules = [];
|
|
67
71
|
|
|
@@ -81,6 +85,11 @@ export class JuxCompiler {
|
|
|
81
85
|
return { views, dataModules, sharedModules };
|
|
82
86
|
}
|
|
83
87
|
|
|
88
|
+
isAssetFile(filename) {
|
|
89
|
+
const assetExtensions = ['.css', '.png', '.jpg', '.jpeg', '.gif', '.svg', '.woff', '.woff2', '.ttf', '.eot'];
|
|
90
|
+
return assetExtensions.some(ext => filename.endsWith(ext));
|
|
91
|
+
}
|
|
92
|
+
|
|
84
93
|
removeImports(code) {
|
|
85
94
|
return code
|
|
86
95
|
.replace(/^\s*import\s+.*?from\s+['"][^'"]+['"][\s;]*$/gm, '')
|
|
@@ -93,44 +102,81 @@ export class JuxCompiler {
|
|
|
93
102
|
return name.replace(/[^a-zA-Z0-9]/g, '_');
|
|
94
103
|
}
|
|
95
104
|
|
|
105
|
+
/**
|
|
106
|
+
* Copy all source assets (themes, layouts, assets folders) to dist
|
|
107
|
+
*/
|
|
108
|
+
copySourceAssets() {
|
|
109
|
+
const copied = { themes: 0, layouts: 0, assets: 0, other: 0 };
|
|
110
|
+
|
|
111
|
+
const copyDir = (srcDir, destDir, category) => {
|
|
112
|
+
if (!fs.existsSync(srcDir)) return 0;
|
|
113
|
+
|
|
114
|
+
let count = 0;
|
|
115
|
+
fs.mkdirSync(destDir, { recursive: true });
|
|
116
|
+
|
|
117
|
+
const entries = fs.readdirSync(srcDir, { withFileTypes: true });
|
|
118
|
+
for (const entry of entries) {
|
|
119
|
+
const srcPath = path.join(srcDir, entry.name);
|
|
120
|
+
const destPath = path.join(destDir, entry.name);
|
|
121
|
+
|
|
122
|
+
if (entry.isDirectory()) {
|
|
123
|
+
count += copyDir(srcPath, destPath, category);
|
|
124
|
+
} else {
|
|
125
|
+
fs.copyFileSync(srcPath, destPath);
|
|
126
|
+
count++;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return count;
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
// Copy themes folder
|
|
133
|
+
if (this.paths.themes && fs.existsSync(this.paths.themes)) {
|
|
134
|
+
copied.themes = copyDir(this.paths.themes, path.join(this.distDir, 'themes'), 'themes');
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Copy layouts folder (if separate from themes)
|
|
138
|
+
if (this.paths.layouts && fs.existsSync(this.paths.layouts) && this.paths.layouts !== this.paths.themes) {
|
|
139
|
+
copied.layouts = copyDir(this.paths.layouts, path.join(this.distDir, 'layouts'), 'layouts');
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Copy assets folder
|
|
143
|
+
if (this.paths.assets && fs.existsSync(this.paths.assets)) {
|
|
144
|
+
copied.assets = copyDir(this.paths.assets, path.join(this.distDir, 'assets'), 'assets');
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const total = copied.themes + copied.layouts + copied.assets;
|
|
148
|
+
if (total > 0) {
|
|
149
|
+
console.log(`📂 Copied source assets: ${copied.themes} themes, ${copied.layouts} layouts, ${copied.assets} assets`);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return copied;
|
|
153
|
+
}
|
|
154
|
+
|
|
96
155
|
/**
|
|
97
156
|
* Collect and bundle all component CSS into a single file
|
|
98
157
|
*/
|
|
99
158
|
copyJuxscriptCss() {
|
|
100
159
|
const componentsDir = this.getComponentsDir();
|
|
101
|
-
if (!componentsDir) {
|
|
102
|
-
console.log('⚠️ Components directory not found, skipping CSS bundling');
|
|
103
|
-
return { bundlePath: null, components: [] };
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
console.log(`📂 Scanning for CSS in: ${componentsDir}`);
|
|
160
|
+
if (!componentsDir) return { bundlePath: null, components: [] };
|
|
107
161
|
|
|
108
162
|
const cssContents = [];
|
|
109
163
|
const components = [];
|
|
110
164
|
|
|
111
165
|
const walkDir = (dir, relativePath = '') => {
|
|
112
166
|
if (!fs.existsSync(dir)) return;
|
|
113
|
-
|
|
114
167
|
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
115
168
|
for (const entry of entries) {
|
|
116
169
|
const fullPath = path.join(dir, entry.name);
|
|
117
|
-
const relPath =
|
|
118
|
-
|
|
170
|
+
const relPath = path.join(relativePath, entry.name);
|
|
119
171
|
if (entry.isDirectory()) {
|
|
120
172
|
walkDir(fullPath, relPath);
|
|
121
173
|
} else if (entry.name === 'structure.css') {
|
|
122
174
|
const componentName = relativePath || 'base';
|
|
123
175
|
const content = fs.readFileSync(fullPath, 'utf8');
|
|
124
|
-
|
|
125
|
-
// Add comment header for each component's CSS
|
|
126
|
-
cssContents.push(`/* ═══════════════════════════════════════════════════════════════
|
|
127
|
-
* Component: ${componentName}
|
|
128
|
-
* ═══════════════════════════════════════════════════════════════ */`);
|
|
176
|
+
cssContents.push(`/* Component: ${componentName} */\n`);
|
|
129
177
|
cssContents.push(content);
|
|
130
|
-
cssContents.push('');
|
|
131
|
-
|
|
178
|
+
cssContents.push('\n');
|
|
132
179
|
components.push(componentName);
|
|
133
|
-
console.log(` 📄 Found: ${componentName}/structure.css`);
|
|
134
180
|
}
|
|
135
181
|
}
|
|
136
182
|
};
|
|
@@ -138,43 +184,38 @@ export class JuxCompiler {
|
|
|
138
184
|
walkDir(componentsDir);
|
|
139
185
|
|
|
140
186
|
if (cssContents.length === 0) {
|
|
141
|
-
console.log('⚠️ No CSS files found to bundle');
|
|
142
187
|
return { bundlePath: null, components: [] };
|
|
143
188
|
}
|
|
144
189
|
|
|
145
|
-
// Create bundled CSS file
|
|
146
190
|
const cssDistDir = path.join(this.distDir, 'css');
|
|
147
191
|
fs.mkdirSync(cssDistDir, { recursive: true });
|
|
148
192
|
|
|
149
|
-
const
|
|
150
|
-
|
|
151
|
-
* Auto-generated - Do not edit directly
|
|
152
|
-
*
|
|
153
|
-
* Components included: ${components.join(', ')}
|
|
154
|
-
* Generated: ${new Date().toISOString()}
|
|
155
|
-
*/
|
|
156
|
-
|
|
157
|
-
`;
|
|
158
|
-
|
|
159
|
-
const bundledCss = bundleHeader + cssContents.join('\n\n');
|
|
160
|
-
const bundlePath = path.join(cssDistDir, 'jux-components.css');
|
|
193
|
+
const bundledCss = `/* JUX Components CSS Bundle */\n\n` + cssContents.join('\n');
|
|
194
|
+
fs.writeFileSync(path.join(cssDistDir, 'jux-components.css'), bundledCss);
|
|
161
195
|
|
|
162
|
-
|
|
196
|
+
console.log(`📄 Bundled ${components.length} component CSS files`);
|
|
163
197
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
return {
|
|
167
|
-
bundlePath: './css/jux-components.css',
|
|
168
|
-
components
|
|
169
|
-
};
|
|
198
|
+
return { bundlePath: './css/jux-components.css', components };
|
|
170
199
|
}
|
|
171
200
|
|
|
172
201
|
/**
|
|
173
|
-
* Generate
|
|
202
|
+
* Generate CSS links including theme if configured
|
|
174
203
|
*/
|
|
175
204
|
generateCssLinks(cssBundle) {
|
|
176
|
-
|
|
177
|
-
|
|
205
|
+
const links = [];
|
|
206
|
+
|
|
207
|
+
// JUX components CSS
|
|
208
|
+
if (cssBundle.bundlePath) {
|
|
209
|
+
links.push(` <link rel="stylesheet" href="${cssBundle.bundlePath}">`);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// User theme CSS
|
|
213
|
+
if (this.themePath) {
|
|
214
|
+
const themeName = path.basename(this.themePath);
|
|
215
|
+
links.push(` <link rel="stylesheet" href="./themes/${themeName}" id="jux-theme">`);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return links.join('\n');
|
|
178
219
|
}
|
|
179
220
|
|
|
180
221
|
async loadJuxscriptExports() {
|
|
@@ -186,7 +227,6 @@ export class JuxCompiler {
|
|
|
186
227
|
const indexContent = fs.readFileSync(juxscriptPath, 'utf8');
|
|
187
228
|
const exports = new Set();
|
|
188
229
|
|
|
189
|
-
// Parse export statements
|
|
190
230
|
for (const match of indexContent.matchAll(/export\s*\{\s*([^}]+)\s*\}/g)) {
|
|
191
231
|
match[1].split(',').forEach(exp => {
|
|
192
232
|
const name = exp.trim().split(/\s+as\s+/)[0].trim();
|
|
@@ -223,16 +263,12 @@ export class JuxCompiler {
|
|
|
223
263
|
return issues;
|
|
224
264
|
}
|
|
225
265
|
|
|
226
|
-
const juxscriptImports = new Set();
|
|
227
266
|
const allImports = new Set();
|
|
228
267
|
|
|
229
268
|
walk(ast, {
|
|
230
269
|
ImportDeclaration(node) {
|
|
231
270
|
node.specifiers.forEach(spec => {
|
|
232
271
|
allImports.add(spec.local.name);
|
|
233
|
-
if (node.source.value === 'juxscript') {
|
|
234
|
-
juxscriptImports.add(spec.local.name);
|
|
235
|
-
}
|
|
236
272
|
});
|
|
237
273
|
}
|
|
238
274
|
});
|
|
@@ -259,6 +295,17 @@ export class JuxCompiler {
|
|
|
259
295
|
return issues;
|
|
260
296
|
}
|
|
261
297
|
|
|
298
|
+
/**
|
|
299
|
+
* Extract layout function name from layout file
|
|
300
|
+
*/
|
|
301
|
+
getLayoutFunctionName() {
|
|
302
|
+
if (!this.layoutPath || !fs.existsSync(this.layoutPath)) return null;
|
|
303
|
+
|
|
304
|
+
const content = fs.readFileSync(this.layoutPath, 'utf8');
|
|
305
|
+
const match = content.match(/export\s+function\s+(\w+)/);
|
|
306
|
+
return match ? match[1] : null;
|
|
307
|
+
}
|
|
308
|
+
|
|
262
309
|
generateEntryPoint(views, dataModules, sharedModules) {
|
|
263
310
|
let entry = `// Auto-generated JUX entry point\n\n`;
|
|
264
311
|
const allIssues = [];
|
|
@@ -273,6 +320,16 @@ export class JuxCompiler {
|
|
|
273
320
|
}
|
|
274
321
|
});
|
|
275
322
|
|
|
323
|
+
// Check layout file for juxscript imports too
|
|
324
|
+
if (this.layoutPath && fs.existsSync(this.layoutPath)) {
|
|
325
|
+
const layoutContent = fs.readFileSync(this.layoutPath, 'utf8');
|
|
326
|
+
for (const match of layoutContent.matchAll(/import\s*\{\s*([^}]+)\s*\}\s*from\s*['"]juxscript['"]/g)) {
|
|
327
|
+
match[1].split(',').map(s => s.trim()).forEach(imp => {
|
|
328
|
+
if (imp) juxImports.add(imp);
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
276
333
|
if (juxImports.size > 0) {
|
|
277
334
|
entry += `import { ${[...juxImports].sort().join(', ')} } from 'juxscript';\n\n`;
|
|
278
335
|
}
|
|
@@ -292,6 +349,25 @@ export class JuxCompiler {
|
|
|
292
349
|
entry += `\nObject.assign(window, { ${[...juxImports].join(', ')} });\n`;
|
|
293
350
|
}
|
|
294
351
|
|
|
352
|
+
// Add layout function if configured
|
|
353
|
+
const layoutFnName = this.getLayoutFunctionName();
|
|
354
|
+
if (layoutFnName && this.layoutPath) {
|
|
355
|
+
const layoutContent = fs.readFileSync(this.layoutPath, 'utf8');
|
|
356
|
+
const layoutCode = this.removeImports(layoutContent);
|
|
357
|
+
entry += `\n// --- DEFAULT LAYOUT ---\n`;
|
|
358
|
+
entry += layoutCode;
|
|
359
|
+
entry += `\n`;
|
|
360
|
+
|
|
361
|
+
// Store in source snapshot
|
|
362
|
+
const layoutFile = path.basename(this.layoutPath);
|
|
363
|
+
sourceSnapshot[layoutFile] = {
|
|
364
|
+
name: 'layout',
|
|
365
|
+
file: layoutFile,
|
|
366
|
+
content: layoutContent,
|
|
367
|
+
lines: layoutContent.split('\n')
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
|
|
295
371
|
entry += `\n// --- VIEW FUNCTIONS ---\n`;
|
|
296
372
|
|
|
297
373
|
views.forEach(v => {
|
|
@@ -320,6 +396,7 @@ export class JuxCompiler {
|
|
|
320
396
|
|
|
321
397
|
this._sourceSnapshot = sourceSnapshot;
|
|
322
398
|
this._validationIssues = allIssues;
|
|
399
|
+
this._layoutFnName = layoutFnName;
|
|
323
400
|
entry += this._generateRouter(views);
|
|
324
401
|
return entry;
|
|
325
402
|
}
|
|
@@ -349,6 +426,11 @@ export class JuxCompiler {
|
|
|
349
426
|
routeMap += ` '/${v.name.toLowerCase()}': render${cap},\n`;
|
|
350
427
|
});
|
|
351
428
|
|
|
429
|
+
// Initialize layout call if configured
|
|
430
|
+
const layoutInit = this._layoutFnName
|
|
431
|
+
? `\n// Initialize default layout\nif (typeof ${this._layoutFnName} === 'function') {\n ${this._layoutFnName}();\n}\n`
|
|
432
|
+
: '';
|
|
433
|
+
|
|
352
434
|
return `
|
|
353
435
|
// --- JUX SOURCE LOADER ---
|
|
354
436
|
var __juxSources = null;
|
|
@@ -363,7 +445,6 @@ async function __juxLoadSources() {
|
|
|
363
445
|
return __juxSources;
|
|
364
446
|
}
|
|
365
447
|
|
|
366
|
-
// Find source file from error stack
|
|
367
448
|
function __juxFindSource(stack) {
|
|
368
449
|
var match = stack.match(/render(\\w+)/);
|
|
369
450
|
if (match) {
|
|
@@ -374,6 +455,14 @@ function __juxFindSource(stack) {
|
|
|
374
455
|
}
|
|
375
456
|
}
|
|
376
457
|
}
|
|
458
|
+
// Also check for layout
|
|
459
|
+
if (stack.indexOf('Layout') > -1) {
|
|
460
|
+
for (var file in __juxSources || {}) {
|
|
461
|
+
if (file.indexOf('layout') > -1) {
|
|
462
|
+
return { file: file, source: __juxSources[file] };
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
}
|
|
377
466
|
return null;
|
|
378
467
|
}
|
|
379
468
|
|
|
@@ -385,39 +474,21 @@ var __juxErrorOverlay = {
|
|
|
385
474
|
background: rgba(0, 0, 0, 0.4);
|
|
386
475
|
display: flex; align-items: center; justify-content: center;
|
|
387
476
|
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, monospace;
|
|
388
|
-
opacity: 0;
|
|
389
|
-
transition: opacity 0.2s ease-out;
|
|
390
|
-
backdrop-filter: blur(2px);
|
|
477
|
+
opacity: 0; transition: opacity 0.2s ease-out; backdrop-filter: blur(2px);
|
|
391
478
|
}
|
|
392
479
|
#__jux-error-overlay.visible { opacity: 1; }
|
|
393
480
|
#__jux-error-overlay * { box-sizing: border-box; }
|
|
394
481
|
.__jux-modal {
|
|
395
|
-
background: #f8f9fa;
|
|
396
|
-
border-radius: 4px;
|
|
482
|
+
background: #f8f9fa; border-radius: 4px;
|
|
397
483
|
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.4);
|
|
398
|
-
max-width: 80vw;
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
overflow: hidden;
|
|
402
|
-
display: flex;
|
|
403
|
-
flex-direction: column;
|
|
404
|
-
transform: translateY(10px);
|
|
405
|
-
transition: transform 0.2s ease-out;
|
|
484
|
+
max-width: 80vw; width: 90%; max-height: 90vh;
|
|
485
|
+
overflow: hidden; display: flex; flex-direction: column;
|
|
486
|
+
transform: translateY(10px); transition: transform 0.2s ease-out;
|
|
406
487
|
}
|
|
407
488
|
#__jux-error-overlay.visible .__jux-modal { transform: translateY(0); }
|
|
408
|
-
.__jux-header {
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
border-bottom: 1px solid #e5e7eb;
|
|
412
|
-
}
|
|
413
|
-
.__jux-header h3 {
|
|
414
|
-
margin: 0 0 6px; font-weight: 600; font-size: 11px;
|
|
415
|
-
color: #9ca3af; text-transform: uppercase; letter-spacing: 0.5px;
|
|
416
|
-
}
|
|
417
|
-
.__jux-header h1 {
|
|
418
|
-
margin: 0 0 8px; font-size: 18px; font-weight: 600; color: #dc2626;
|
|
419
|
-
line-height: 1.3;
|
|
420
|
-
}
|
|
489
|
+
.__jux-header { background: #fff; padding: 20px 24px; border-bottom: 1px solid #e5e7eb; }
|
|
490
|
+
.__jux-header h3 { margin: 0 0 6px; font-weight: 600; font-size: 11px; color: #9ca3af; text-transform: uppercase; }
|
|
491
|
+
.__jux-header h1 { margin: 0 0 8px; font-size: 18px; font-weight: 600; color: #dc2626; line-height: 1.3; }
|
|
421
492
|
.__jux-header .file-info { color: #6b7280; font-size: 13px; }
|
|
422
493
|
.__jux-header .file-info strong { color: #dc2626; font-weight: 600; }
|
|
423
494
|
.__jux-source { padding: 16px 24px; overflow: auto; flex: 1; background: #f8f9fa; }
|
|
@@ -426,138 +497,73 @@ var __juxErrorOverlay = {
|
|
|
426
497
|
.__jux-code-line.error { background: #fef2f2; }
|
|
427
498
|
.__jux-code-line.error .__jux-line-code { color: #dc2626; font-weight: 500; }
|
|
428
499
|
.__jux-code-line.context { background: #fefce8; }
|
|
429
|
-
.__jux-line-num {
|
|
430
|
-
min-width: 44px; padding: 4px 12px; text-align: right;
|
|
431
|
-
color: #9ca3af; background: #f9fafb; user-select: none;
|
|
432
|
-
border-right: 1px solid #e5e7eb; font-size: 12px;
|
|
433
|
-
}
|
|
500
|
+
.__jux-line-num { min-width: 44px; padding: 4px 12px; text-align: right; color: #9ca3af; background: #f9fafb; border-right: 1px solid #e5e7eb; font-size: 12px; }
|
|
434
501
|
.__jux-code-line.error .__jux-line-num { background: #fef2f2; color: #dc2626; }
|
|
435
502
|
.__jux-line-code { flex: 1; padding: 4px 16px; color: #374151; white-space: pre; overflow-x: auto; }
|
|
436
|
-
.__jux-footer {
|
|
437
|
-
padding: 16px 24px; background: #fff; border-top: 1px solid #e5e7eb;
|
|
438
|
-
display: flex; justify-content: space-between; align-items: center;
|
|
439
|
-
}
|
|
503
|
+
.__jux-footer { padding: 16px 24px; background: #fff; border-top: 1px solid #e5e7eb; display: flex; justify-content: space-between; align-items: center; }
|
|
440
504
|
.__jux-tip { color: #6b7280; font-size: 12px; }
|
|
441
|
-
.__jux-dismiss {
|
|
442
|
-
background: #f3f4f6; color: #374151; border: 1px solid #d1d5db;
|
|
443
|
-
padding: 8px 16px; border-radius: 6px; cursor: pointer;
|
|
444
|
-
font-size: 13px; font-weight: 500; transition: background 0.15s;
|
|
445
|
-
}
|
|
505
|
+
.__jux-dismiss { background: #f3f4f6; color: #374151; border: 1px solid #d1d5db; padding: 8px 16px; border-radius: 6px; cursor: pointer; font-size: 13px; font-weight: 500; }
|
|
446
506
|
.__jux-dismiss:hover { background: #e5e7eb; }
|
|
447
507
|
.__jux-no-source { color: #6b7280; padding: 24px; text-align: center; }
|
|
448
|
-
.__jux-no-source pre {
|
|
449
|
-
background: #fff; padding: 16px; border-radius: 8px; margin-top: 16px;
|
|
450
|
-
font-size: 11px; color: #6b7280; overflow-x: auto; text-align: left;
|
|
451
|
-
border: 1px solid #e5e7eb;
|
|
452
|
-
}
|
|
508
|
+
.__jux-no-source pre { background: #fff; padding: 16px; border-radius: 8px; margin-top: 16px; font-size: 11px; color: #6b7280; overflow-x: auto; text-align: left; border: 1px solid #e5e7eb; }
|
|
453
509
|
\`,
|
|
454
510
|
|
|
455
511
|
show: async function(error, title) {
|
|
456
512
|
title = title || 'Runtime Error';
|
|
457
|
-
|
|
458
513
|
var existing = document.getElementById('__jux-error-overlay');
|
|
459
514
|
if (existing) existing.remove();
|
|
460
|
-
|
|
461
515
|
await __juxLoadSources();
|
|
462
516
|
|
|
463
517
|
var overlay = document.createElement('div');
|
|
464
518
|
overlay.id = '__jux-error-overlay';
|
|
465
|
-
|
|
466
519
|
var stack = error && error.stack ? error.stack : '';
|
|
467
520
|
var msg = error && error.message ? error.message : String(error);
|
|
468
|
-
|
|
469
521
|
var found = __juxFindSource(stack);
|
|
470
|
-
var sourceHtml = '';
|
|
471
|
-
var fileInfo = '';
|
|
522
|
+
var sourceHtml = '', fileInfo = '';
|
|
472
523
|
|
|
473
524
|
if (found && found.source && found.source.lines) {
|
|
474
525
|
var lines = found.source.lines;
|
|
475
526
|
fileInfo = '<span class="file-info">in <strong>' + found.file + '</strong></span>';
|
|
476
|
-
|
|
477
527
|
var errorLineIndex = -1;
|
|
478
528
|
var errorMethod = msg.match(/\\.([a-zA-Z]+)\\s*is not a function/);
|
|
479
529
|
if (errorMethod) {
|
|
480
530
|
for (var i = 0; i < lines.length; i++) {
|
|
481
|
-
if (lines[i].indexOf('.' + errorMethod[1]) > -1) {
|
|
482
|
-
errorLineIndex = i;
|
|
483
|
-
break;
|
|
484
|
-
}
|
|
531
|
+
if (lines[i].indexOf('.' + errorMethod[1]) > -1) { errorLineIndex = i; break; }
|
|
485
532
|
}
|
|
486
533
|
}
|
|
487
|
-
|
|
488
534
|
if (errorLineIndex === -1) {
|
|
489
535
|
for (var i = 0; i < lines.length; i++) {
|
|
490
|
-
if (lines[i].indexOf('throw ') > -1
|
|
491
|
-
errorLineIndex = i;
|
|
492
|
-
break;
|
|
493
|
-
}
|
|
536
|
+
if (lines[i].indexOf('throw ') > -1) { errorLineIndex = i; break; }
|
|
494
537
|
}
|
|
495
538
|
}
|
|
496
|
-
|
|
497
539
|
var contextStart = Math.max(0, errorLineIndex - 3);
|
|
498
540
|
var contextEnd = Math.min(lines.length - 1, errorLineIndex + 5);
|
|
499
|
-
|
|
500
|
-
if (errorLineIndex === -1) {
|
|
501
|
-
contextStart = 0;
|
|
502
|
-
contextEnd = Math.min(14, lines.length - 1);
|
|
503
|
-
}
|
|
541
|
+
if (errorLineIndex === -1) { contextStart = 0; contextEnd = Math.min(14, lines.length - 1); }
|
|
504
542
|
|
|
505
543
|
for (var i = contextStart; i <= contextEnd; i++) {
|
|
506
544
|
var isError = (i === errorLineIndex);
|
|
507
545
|
var isContext = !isError && errorLineIndex > -1 && Math.abs(i - errorLineIndex) <= 2;
|
|
508
546
|
var lineClass = isError ? ' error' : (isContext ? ' context' : '');
|
|
509
547
|
var lineCode = lines[i].replace(/</g, '<').replace(/>/g, '>') || ' ';
|
|
510
|
-
sourceHtml += '<div class="__jux-code-line' + lineClass + '">' +
|
|
511
|
-
'<span class="__jux-line-num">' + (i + 1) + '</span>' +
|
|
512
|
-
'<span class="__jux-line-code">' + lineCode + '</span>' +
|
|
513
|
-
'</div>';
|
|
548
|
+
sourceHtml += '<div class="__jux-code-line' + lineClass + '"><span class="__jux-line-num">' + (i + 1) + '</span><span class="__jux-line-code">' + lineCode + '</span></div>';
|
|
514
549
|
}
|
|
515
550
|
} else {
|
|
516
|
-
sourceHtml = '<div class="__jux-no-source">' +
|
|
517
|
-
'<p>Could not locate source file for this error.</p>' +
|
|
518
|
-
'<pre>' + stack + '</pre>' +
|
|
519
|
-
'</div>';
|
|
551
|
+
sourceHtml = '<div class="__jux-no-source"><p>Could not locate source file.</p><pre>' + stack + '</pre></div>';
|
|
520
552
|
}
|
|
521
553
|
|
|
522
|
-
overlay.innerHTML = '<style>' + this.styles + '</style>' +
|
|
523
|
-
'<div class="__jux-modal">' +
|
|
524
|
-
'<div class="__jux-header">' +
|
|
525
|
-
'<h3>' + title + '</h3>' +
|
|
526
|
-
'<h1>' + msg + '</h1>' +
|
|
527
|
-
fileInfo +
|
|
528
|
-
'</div>' +
|
|
529
|
-
'<div class="__jux-source">' +
|
|
530
|
-
'<div class="__jux-code">' + sourceHtml + '</div>' +
|
|
531
|
-
'</div>' +
|
|
532
|
-
'<div class="__jux-footer">' +
|
|
533
|
-
'<span class="__jux-tip">💡 Fix the error and save to reload</span>' +
|
|
534
|
-
'<button class="__jux-dismiss" onclick="document.getElementById(\\'__jux-error-overlay\\').remove()">Dismiss</button>' +
|
|
535
|
-
'</div>' +
|
|
536
|
-
'</div>';
|
|
537
|
-
|
|
554
|
+
overlay.innerHTML = '<style>' + this.styles + '</style><div class="__jux-modal"><div class="__jux-header"><h3>' + title + '</h3><h1>' + msg + '</h1>' + fileInfo + '</div><div class="__jux-source"><div class="__jux-code">' + sourceHtml + '</div></div><div class="__jux-footer"><span class="__jux-tip">💡 Fix the error and save to reload</span><button class="__jux-dismiss" onclick="document.getElementById(\\'__jux-error-overlay\\').remove()">Dismiss</button></div></div>';
|
|
538
555
|
document.body.appendChild(overlay);
|
|
539
|
-
|
|
540
|
-
// Trigger transition
|
|
541
|
-
requestAnimationFrame(function() {
|
|
542
|
-
overlay.classList.add('visible');
|
|
543
|
-
});
|
|
544
|
-
|
|
556
|
+
requestAnimationFrame(function() { overlay.classList.add('visible'); });
|
|
545
557
|
console.error(title + ':', error);
|
|
546
558
|
}
|
|
547
559
|
};
|
|
548
560
|
|
|
549
|
-
|
|
550
|
-
window.addEventListener('
|
|
551
|
-
__juxErrorOverlay.show(e.error || new Error(e.message), 'Uncaught Error');
|
|
552
|
-
}, true);
|
|
553
|
-
|
|
554
|
-
window.addEventListener('unhandledrejection', function(e) {
|
|
555
|
-
__juxErrorOverlay.show(e.reason || new Error('Promise rejected'), 'Unhandled Promise Rejection');
|
|
556
|
-
}, true);
|
|
561
|
+
window.addEventListener('error', function(e) { __juxErrorOverlay.show(e.error || new Error(e.message), 'Uncaught Error'); }, true);
|
|
562
|
+
window.addEventListener('unhandledrejection', function(e) { __juxErrorOverlay.show(e.reason || new Error('Promise rejected'), 'Unhandled Promise Rejection'); }, true);
|
|
557
563
|
|
|
558
564
|
// --- JUX ROUTER ---
|
|
559
565
|
const routes = {\n${routeMap}};
|
|
560
|
-
|
|
566
|
+
${layoutInit}
|
|
561
567
|
async function navigate(path) {
|
|
562
568
|
const view = routes[path];
|
|
563
569
|
if (!view) {
|
|
@@ -565,15 +571,9 @@ async function navigate(path) {
|
|
|
565
571
|
return;
|
|
566
572
|
}
|
|
567
573
|
document.getElementById('app').innerHTML = '';
|
|
568
|
-
|
|
569
574
|
var overlay = document.getElementById('__jux-error-overlay');
|
|
570
575
|
if (overlay) overlay.remove();
|
|
571
|
-
|
|
572
|
-
try {
|
|
573
|
-
await view();
|
|
574
|
-
} catch (err) {
|
|
575
|
-
__juxErrorOverlay.show(err, 'Jux Render Error');
|
|
576
|
-
}
|
|
576
|
+
try { await view(); } catch (err) { __juxErrorOverlay.show(err, 'Jux Render Error'); }
|
|
577
577
|
}
|
|
578
578
|
|
|
579
579
|
document.addEventListener('click', e => {
|
|
@@ -608,9 +608,12 @@ navigate(location.pathname);
|
|
|
608
608
|
fs.mkdirSync(this.distDir, { recursive: true });
|
|
609
609
|
|
|
610
610
|
const { views, dataModules, sharedModules } = this.scanFiles();
|
|
611
|
-
console.log(`📁 Found ${views.length} views, ${sharedModules.length} shared, ${dataModules.length} data
|
|
611
|
+
console.log(`📁 Found ${views.length} views, ${sharedModules.length} shared, ${dataModules.length} data`);
|
|
612
|
+
|
|
613
|
+
// Copy source assets (themes, layouts, assets)
|
|
614
|
+
this.copySourceAssets();
|
|
612
615
|
|
|
613
|
-
//
|
|
616
|
+
// Bundle juxscript component CSS
|
|
614
617
|
const cssBundle = this.copyJuxscriptCss();
|
|
615
618
|
|
|
616
619
|
// Copy data/shared modules to dist
|
|
@@ -624,7 +627,6 @@ navigate(location.pathname);
|
|
|
624
627
|
const entryPath = path.join(this.distDir, 'entry.js');
|
|
625
628
|
fs.writeFileSync(entryPath, entryContent);
|
|
626
629
|
|
|
627
|
-
// Write source snapshot for dev tools
|
|
628
630
|
const snapshotPath = path.join(this.distDir, '__jux_sources.json');
|
|
629
631
|
fs.writeFileSync(snapshotPath, JSON.stringify(this._sourceSnapshot, null, 2));
|
|
630
632
|
console.log(`📸 Source snapshot written`);
|
|
@@ -658,7 +660,7 @@ navigate(location.pathname);
|
|
|
658
660
|
return { success: false, errors: [{ message: err.message }], warnings: [] };
|
|
659
661
|
}
|
|
660
662
|
|
|
661
|
-
// Generate
|
|
663
|
+
// Generate HTML with layout container sibling to app
|
|
662
664
|
const html = `<!DOCTYPE html>
|
|
663
665
|
<html lang="en">
|
|
664
666
|
<head>
|
|
@@ -669,12 +671,12 @@ ${this.generateCssLinks(cssBundle)}
|
|
|
669
671
|
<script type="module" src="./bundle.js"></script>
|
|
670
672
|
</head>
|
|
671
673
|
<body>
|
|
674
|
+
<div id="jux-layout"></div>
|
|
672
675
|
<div id="app"></div>
|
|
673
676
|
</body>
|
|
674
677
|
</html>`;
|
|
675
678
|
fs.writeFileSync(path.join(this.distDir, 'index.html'), html);
|
|
676
679
|
|
|
677
|
-
// Cleanup temp files
|
|
678
680
|
fs.unlinkSync(entryPath);
|
|
679
681
|
fs.rmSync(juxDistDir, { recursive: true, force: true });
|
|
680
682
|
|