juxscript 1.0.131 → 1.1.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.
Files changed (129) hide show
  1. package/README.md +1 -32
  2. package/bin/cli.js +4 -2
  3. package/index.d.ts +200 -0
  4. package/index.js +96 -22
  5. package/juxconfig.example.js +58 -63
  6. package/lib/components/alert.ts +200 -0
  7. package/lib/components/app.ts +247 -0
  8. package/lib/components/badge.ts +101 -0
  9. package/lib/components/base/BaseComponent.ts +421 -0
  10. package/lib/components/base/FormInput.ts +227 -0
  11. package/lib/components/button.ts +178 -0
  12. package/lib/components/card.ts +173 -0
  13. package/lib/components/chart.ts +231 -0
  14. package/lib/components/checkbox.ts +242 -0
  15. package/lib/components/code.ts +123 -0
  16. package/lib/components/container.ts +140 -0
  17. package/lib/components/data.ts +135 -0
  18. package/lib/components/datepicker.ts +234 -0
  19. package/lib/components/dialog.ts +172 -0
  20. package/lib/components/divider.ts +100 -0
  21. package/lib/components/dropdown.ts +186 -0
  22. package/lib/components/element.ts +267 -0
  23. package/lib/components/fileupload.ts +309 -0
  24. package/lib/components/grid.ts +291 -0
  25. package/lib/components/guard.ts +92 -0
  26. package/lib/components/heading.ts +96 -0
  27. package/lib/components/helpers.ts +41 -0
  28. package/lib/components/hero.ts +224 -0
  29. package/lib/components/icon.ts +178 -0
  30. package/lib/components/icons.ts +464 -0
  31. package/lib/components/include.ts +410 -0
  32. package/lib/components/input.ts +457 -0
  33. package/lib/components/list.ts +419 -0
  34. package/lib/components/loading.ts +100 -0
  35. package/lib/components/menu.ts +275 -0
  36. package/lib/components/modal.ts +284 -0
  37. package/lib/components/nav.ts +257 -0
  38. package/lib/components/paragraph.ts +97 -0
  39. package/lib/components/progress.ts +159 -0
  40. package/lib/components/radio.ts +278 -0
  41. package/lib/components/req.ts +303 -0
  42. package/lib/components/script.ts +41 -0
  43. package/lib/components/select.ts +252 -0
  44. package/lib/components/sidebar.ts +275 -0
  45. package/lib/components/style.ts +41 -0
  46. package/lib/components/switch.ts +246 -0
  47. package/lib/components/table.ts +1249 -0
  48. package/lib/components/tabs.ts +250 -0
  49. package/lib/components/theme-toggle.ts +293 -0
  50. package/lib/components/tooltip.ts +144 -0
  51. package/lib/components/view.ts +190 -0
  52. package/lib/components/write.ts +272 -0
  53. package/lib/globals.d.ts +19 -5
  54. package/lib/layouts/default.css +260 -0
  55. package/lib/layouts/figma.css +334 -0
  56. package/lib/reactivity/state.ts +78 -0
  57. package/lib/utils/{fetch.js → fetch.ts} +206 -81
  58. package/package.json +9 -31
  59. package/create/index.jux +0 -77
  60. package/create/layout.jux +0 -18
  61. package/create/style.css +0 -57
  62. package/create/themes/assets/jux.svg +0 -34
  63. package/create/themes/base.css +0 -197
  64. package/create/themes/base2.css +0 -54
  65. package/create/themes/layouts/base.jux +0 -16
  66. package/create/themes/layouts/base_blog.jux +0 -0
  67. package/create/themes/layouts/base_docs.jux +0 -0
  68. package/create/themes/layouts/base_login.jux +0 -0
  69. package/create/themes/layouts/base_marketing.jux +0 -0
  70. package/create/themes/layouts/base_saas.jux +0 -0
  71. package/lib/componentsv2/base/BaseEngine.d.ts +0 -112
  72. package/lib/componentsv2/base/BaseEngine.js +0 -279
  73. package/lib/componentsv2/base/BaseSkin.d.ts +0 -74
  74. package/lib/componentsv2/base/BaseSkin.js +0 -130
  75. package/lib/componentsv2/base/Neighborhood.d.ts +0 -22
  76. package/lib/componentsv2/base/Neighborhood.js +0 -56
  77. package/lib/componentsv2/base/OptionsContract.d.ts +0 -20
  78. package/lib/componentsv2/base/OptionsContract.js +0 -107
  79. package/lib/componentsv2/base/State.d.ts +0 -18
  80. package/lib/componentsv2/base/State.js +0 -68
  81. package/lib/componentsv2/element/Element.d.ts +0 -29
  82. package/lib/componentsv2/element/Element.js +0 -49
  83. package/lib/componentsv2/element/ElementEngine.d.ts +0 -58
  84. package/lib/componentsv2/element/ElementEngine.js +0 -112
  85. package/lib/componentsv2/element/ElementSkin.d.ts +0 -10
  86. package/lib/componentsv2/element/ElementSkin.js +0 -56
  87. package/lib/componentsv2/element/structure.css +0 -261
  88. package/lib/componentsv2/grid/Grid.d.ts +0 -13
  89. package/lib/componentsv2/grid/Grid.js +0 -27
  90. package/lib/componentsv2/grid/GridEngine.d.ts +0 -77
  91. package/lib/componentsv2/grid/GridEngine.js +0 -153
  92. package/lib/componentsv2/grid/GridSkin.d.ts +0 -11
  93. package/lib/componentsv2/grid/GridSkin.js +0 -84
  94. package/lib/componentsv2/grid/structure.css +0 -27
  95. package/lib/componentsv2/input/Input.d.ts +0 -6
  96. package/lib/componentsv2/input/Input.js +0 -21
  97. package/lib/componentsv2/input/InputEngine.d.ts +0 -70
  98. package/lib/componentsv2/input/InputEngine.js +0 -143
  99. package/lib/componentsv2/input/InputSkin.d.ts +0 -11
  100. package/lib/componentsv2/input/InputSkin.js +0 -89
  101. package/lib/componentsv2/input/structure.css +0 -47
  102. package/lib/componentsv2/list/List.d.ts +0 -49
  103. package/lib/componentsv2/list/List.js +0 -105
  104. package/lib/componentsv2/list/ListEngine.d.ts +0 -121
  105. package/lib/componentsv2/list/ListEngine.js +0 -322
  106. package/lib/componentsv2/list/ListSkin.d.ts +0 -20
  107. package/lib/componentsv2/list/ListSkin.js +0 -345
  108. package/lib/componentsv2/list/structure.css +0 -359
  109. package/lib/componentsv2/plugins/ClientSQLitePlugin.d.ts +0 -21
  110. package/lib/componentsv2/plugins/ClientSQLitePlugin.js +0 -130
  111. package/lib/componentsv2/plugins/IndexedDBPlugin.d.ts +0 -18
  112. package/lib/componentsv2/plugins/IndexedDBPlugin.js +0 -75
  113. package/lib/componentsv2/plugins/LocalStoragePlugin.d.ts +0 -20
  114. package/lib/componentsv2/plugins/LocalStoragePlugin.js +0 -65
  115. package/lib/componentsv2/plugins/ServerSQLitePlugin.d.ts +0 -25
  116. package/lib/componentsv2/plugins/ServerSQLitePlugin.js +0 -70
  117. package/lib/componentsv2/stubs/ComponentComposition.ts.stub +0 -32
  118. package/lib/componentsv2/stubs/ComponentEngine.ts.stub +0 -36
  119. package/lib/componentsv2/stubs/ComponentSkin.ts.stub +0 -35
  120. package/lib/componentsv2/stubs/ComponentStructure.css.d.ts.stub +0 -2
  121. package/lib/componentsv2/stubs/ComponentStructure.css.stub +0 -13
  122. package/lib/utils/fetch.d.ts +0 -176
  123. package/machinery/build3.js +0 -159
  124. package/machinery/compiler3.js +0 -688
  125. package/machinery/config.js +0 -155
  126. package/machinery/serve.js +0 -255
  127. package/machinery/validators/file-validator.js +0 -123
  128. package/machinery/watcher.js +0 -59
  129. package/types/css.d.ts +0 -10
@@ -1,688 +0,0 @@
1
- import * as esbuild from 'esbuild';
2
- import * as acorn from 'acorn';
3
- import { walk } from 'astray';
4
- import fs from 'fs';
5
- import path from 'path';
6
- import { fileURLToPath } from 'url';
7
-
8
- const __filename = fileURLToPath(import.meta.url);
9
- const __dirname = path.dirname(__filename);
10
-
11
- export class JuxCompiler {
12
- constructor(config = {}) {
13
- this.config = config;
14
- this.srcDir = config.srcDir || './jux';
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 || {};
20
- this._juxscriptExports = null;
21
- this._juxscriptPath = null;
22
- }
23
-
24
- /**
25
- * Locate juxscript package - simplified resolution
26
- */
27
- findJuxscriptPath() {
28
- if (this._juxscriptPath) return this._juxscriptPath;
29
-
30
- const projectRoot = process.cwd();
31
-
32
- // Priority 1: User's node_modules (when used as dependency)
33
- const userPath = path.resolve(projectRoot, 'node_modules/juxscript/index.js');
34
- if (fs.existsSync(userPath)) {
35
- this._juxscriptPath = userPath;
36
- return userPath;
37
- }
38
-
39
- // Priority 2: Package root (when developing juxscript itself)
40
- const packageRoot = path.resolve(__dirname, '..');
41
- const devPath = path.resolve(packageRoot, 'index.js');
42
- if (fs.existsSync(devPath)) {
43
- this._juxscriptPath = devPath;
44
- return devPath;
45
- }
46
-
47
- // Priority 3: Sibling in monorepo
48
- const monoPath = path.resolve(projectRoot, '../jux/index.js');
49
- if (fs.existsSync(monoPath)) {
50
- this._juxscriptPath = monoPath;
51
- return monoPath;
52
- }
53
-
54
- return null;
55
- }
56
-
57
- /**
58
- * Get the components directory for CSS copying
59
- */
60
- getComponentsDir() {
61
- const juxPath = this.findJuxscriptPath();
62
- if (!juxPath) return null;
63
- return path.resolve(path.dirname(juxPath), 'lib/componentsv2');
64
- }
65
-
66
- scanFiles() {
67
- const files = fs.readdirSync(this.srcDir)
68
- .filter(f => (f.endsWith('.jux') || f.endsWith('.js')) && !this.isAssetFile(f));
69
-
70
- const views = [], dataModules = [], sharedModules = [];
71
-
72
- files.forEach(file => {
73
- const content = fs.readFileSync(path.join(this.srcDir, file), 'utf8');
74
- const name = file.replace(/\.[^/.]+$/, '');
75
-
76
- if (file.includes('data')) {
77
- dataModules.push({ name, file, content });
78
- } else if (/export\s+(function|const|let|var|class)\s+/.test(content)) {
79
- sharedModules.push({ name, file, content });
80
- } else {
81
- views.push({ name, file, content });
82
- }
83
- });
84
-
85
- return { views, dataModules, sharedModules };
86
- }
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
-
93
- removeImports(code) {
94
- return code
95
- .replace(/^\s*import\s+.*?from\s+['"][^'"]+['"][\s;]*$/gm, '')
96
- .replace(/^\s*import\s*\{[\s\S]*?\}\s*from\s*['"][^'"]+['"][\s;]*/gm, '')
97
- .replace(/^\s*import\s+\w+\s+from\s+['"][^'"]+['"][\s;]*$/gm, '')
98
- .replace(/^\s*import\s*;?\s*$/gm, '');
99
- }
100
-
101
- sanitizeName(name) {
102
- return name.replace(/[^a-zA-Z0-9]/g, '_');
103
- }
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
-
155
- /**
156
- * Collect and bundle all component CSS into a single file
157
- */
158
- copyJuxscriptCss() {
159
- const componentsDir = this.getComponentsDir();
160
- if (!componentsDir) return { bundlePath: null, components: [] };
161
-
162
- const cssContents = [];
163
- const components = [];
164
-
165
- const walkDir = (dir, relativePath = '') => {
166
- if (!fs.existsSync(dir)) return;
167
- const entries = fs.readdirSync(dir, { withFileTypes: true });
168
- for (const entry of entries) {
169
- const fullPath = path.join(dir, entry.name);
170
- const relPath = path.join(relativePath, entry.name);
171
- if (entry.isDirectory()) {
172
- walkDir(fullPath, relPath);
173
- } else if (entry.name === 'structure.css') {
174
- const componentName = relativePath || 'base';
175
- const content = fs.readFileSync(fullPath, 'utf8');
176
- cssContents.push(`/* Component: ${componentName} */\n`);
177
- cssContents.push(content);
178
- cssContents.push('\n');
179
- components.push(componentName);
180
- }
181
- }
182
- };
183
-
184
- walkDir(componentsDir);
185
-
186
- if (cssContents.length === 0) {
187
- return { bundlePath: null, components: [] };
188
- }
189
-
190
- const cssDistDir = path.join(this.distDir, 'css');
191
- fs.mkdirSync(cssDistDir, { recursive: true });
192
-
193
- const bundledCss = `/* JUX Components CSS Bundle */\n\n` + cssContents.join('\n');
194
- fs.writeFileSync(path.join(cssDistDir, 'jux-components.css'), bundledCss);
195
-
196
- console.log(`šŸ“„ Bundled ${components.length} component CSS files`);
197
-
198
- return { bundlePath: './css/jux-components.css', components };
199
- }
200
-
201
- /**
202
- * Generate CSS links including theme if configured
203
- */
204
- generateCssLinks(cssBundle) {
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
- console.log(`šŸŽØ šŸŽØ šŸŽØ šŸŽØ Included theme: ${themeName} šŸŽØ šŸŽØ šŸŽØ šŸŽØ `);
217
- }
218
-
219
- return links.join('\n');
220
- }
221
-
222
- async loadJuxscriptExports() {
223
- if (this._juxscriptExports) return this._juxscriptExports;
224
-
225
- try {
226
- const juxscriptPath = this.findJuxscriptPath();
227
- if (juxscriptPath) {
228
- const indexContent = fs.readFileSync(juxscriptPath, 'utf8');
229
- const exports = new Set();
230
-
231
- for (const match of indexContent.matchAll(/export\s*\{\s*([^}]+)\s*\}/g)) {
232
- match[1].split(',').forEach(exp => {
233
- const name = exp.trim().split(/\s+as\s+/)[0].trim();
234
- if (name) exports.add(name);
235
- });
236
- }
237
-
238
- this._juxscriptExports = [...exports];
239
- if (this._juxscriptExports.length > 0) {
240
- console.log(`šŸ“¦ juxscript exports: ${this._juxscriptExports.join(', ')}`);
241
- }
242
- }
243
- } catch (err) {
244
- this._juxscriptExports = [];
245
- }
246
-
247
- return this._juxscriptExports;
248
- }
249
-
250
- validateViewCode(viewName, code) {
251
- const issues = [];
252
-
253
- let ast;
254
- try {
255
- ast = acorn.parse(code, { ecmaVersion: 'latest', sourceType: 'module', locations: true });
256
- } catch (parseError) {
257
- issues.push({
258
- type: 'error',
259
- view: viewName,
260
- line: parseError.loc?.line || 0,
261
- message: `Syntax error: ${parseError.message}`,
262
- code: ''
263
- });
264
- return issues;
265
- }
266
-
267
- const allImports = new Set();
268
-
269
- walk(ast, {
270
- ImportDeclaration(node) {
271
- node.specifiers.forEach(spec => {
272
- allImports.add(spec.local.name);
273
- });
274
- }
275
- });
276
-
277
- const knownComponents = this._juxscriptExports || ['Element', 'Grid', 'Input', 'List'];
278
-
279
- walk(ast, {
280
- Identifier(node, parent) {
281
- if (parent?.type === 'CallExpression' && parent.callee === node) {
282
- const name = node.name;
283
- if (/^[A-Z]/.test(name) && !allImports.has(name) && knownComponents.includes(name)) {
284
- issues.push({
285
- type: 'warning',
286
- view: viewName,
287
- line: node.loc?.start?.line || 0,
288
- message: `"${name}" is used but not imported from juxscript`,
289
- code: ''
290
- });
291
- }
292
- }
293
- }
294
- });
295
-
296
- return issues;
297
- }
298
-
299
- /**
300
- * Extract layout function name from layout file
301
- */
302
- getLayoutFunctionName() {
303
- if (!this.layoutPath || !fs.existsSync(this.layoutPath)) return null;
304
-
305
- const content = fs.readFileSync(this.layoutPath, 'utf8');
306
- const match = content.match(/export\s+function\s+(\w+)/);
307
- return match ? match[1] : null;
308
- }
309
-
310
- generateEntryPoint(views, dataModules, sharedModules) {
311
- let entry = `// Auto-generated JUX entry point\n\n`;
312
- const allIssues = [];
313
- const sourceSnapshot = {};
314
-
315
- const juxImports = new Set();
316
- [...views, ...dataModules, ...sharedModules].forEach(m => {
317
- for (const match of m.content.matchAll(/import\s*\{\s*([^}]+)\s*\}\s*from\s*['"]juxscript['"]/g)) {
318
- match[1].split(',').map(s => s.trim()).forEach(imp => {
319
- if (imp) juxImports.add(imp);
320
- });
321
- }
322
- });
323
-
324
- // Check layout file for juxscript imports too
325
- if (this.layoutPath && fs.existsSync(this.layoutPath)) {
326
- const layoutContent = fs.readFileSync(this.layoutPath, 'utf8');
327
- for (const match of layoutContent.matchAll(/import\s*\{\s*([^}]+)\s*\}\s*from\s*['"]juxscript['"]/g)) {
328
- match[1].split(',').map(s => s.trim()).forEach(imp => {
329
- if (imp) juxImports.add(imp);
330
- });
331
- }
332
- }
333
-
334
- if (juxImports.size > 0) {
335
- entry += `import { ${[...juxImports].sort().join(', ')} } from 'juxscript';\n\n`;
336
- }
337
-
338
- dataModules.forEach(m => {
339
- entry += `import * as ${this.sanitizeName(m.name)}Data from './jux/${m.file}';\n`;
340
- });
341
- sharedModules.forEach(m => {
342
- entry += `import * as ${this.sanitizeName(m.name)}Shared from './jux/${m.file}';\n`;
343
- });
344
-
345
- entry += `\n// Expose to window\n`;
346
- dataModules.forEach(m => entry += `Object.assign(window, ${this.sanitizeName(m.name)}Data);\n`);
347
- sharedModules.forEach(m => entry += `Object.assign(window, ${this.sanitizeName(m.name)}Shared);\n`);
348
-
349
- if (juxImports.size > 0) {
350
- entry += `\nObject.assign(window, { ${[...juxImports].join(', ')} });\n`;
351
- }
352
-
353
- // Add layout function if configured
354
- const layoutFnName = this.getLayoutFunctionName();
355
- if (layoutFnName && this.layoutPath) {
356
- const layoutContent = fs.readFileSync(this.layoutPath, 'utf8');
357
- const layoutCode = this.removeImports(layoutContent);
358
- entry += `\n// --- DEFAULT LAYOUT ---\n`;
359
- entry += layoutCode;
360
- entry += `\n`;
361
-
362
- // Store in source snapshot
363
- const layoutFile = path.basename(this.layoutPath);
364
- sourceSnapshot[layoutFile] = {
365
- name: 'layout',
366
- file: layoutFile,
367
- content: layoutContent,
368
- lines: layoutContent.split('\n')
369
- };
370
- }
371
-
372
- entry += `\n// --- VIEW FUNCTIONS ---\n`;
373
-
374
- views.forEach(v => {
375
- const capitalized = v.name.charAt(0).toUpperCase() + v.name.slice(1);
376
- allIssues.push(...this.validateViewCode(v.name, v.content));
377
-
378
- sourceSnapshot[v.file] = {
379
- name: v.name,
380
- file: v.file,
381
- content: v.content,
382
- lines: v.content.split('\n')
383
- };
384
-
385
- let viewCode = this.removeImports(v.content).replace(/^\s*export\s+default\s+.*$/gm, '');
386
- const asyncPrefix = viewCode.includes('await ') ? 'async ' : '';
387
-
388
- entry += `\n${asyncPrefix}function render${capitalized}() {\n${viewCode}\n}\n`;
389
- });
390
-
391
- dataModules.forEach(m => {
392
- sourceSnapshot[m.file] = { name: m.name, file: m.file, content: m.content, lines: m.content.split('\n') };
393
- });
394
- sharedModules.forEach(m => {
395
- sourceSnapshot[m.file] = { name: m.name, file: m.file, content: m.content, lines: m.content.split('\n') };
396
- });
397
-
398
- this._sourceSnapshot = sourceSnapshot;
399
- this._validationIssues = allIssues;
400
- this._layoutFnName = layoutFnName;
401
- entry += this._generateRouter(views);
402
- return entry;
403
- }
404
-
405
- reportValidationIssues() {
406
- const issues = this._validationIssues || [];
407
- const errors = issues.filter(i => i.type === 'error');
408
- const warnings = issues.filter(i => i.type === 'warning');
409
-
410
- if (issues.length > 0) {
411
- console.log('\nāš ļø Validation Issues:\n');
412
- issues.forEach(issue => {
413
- const icon = issue.type === 'error' ? 'āŒ' : 'āš ļø';
414
- console.log(`${icon} [${issue.view}:${issue.line}] ${issue.message}`);
415
- });
416
- console.log('');
417
- }
418
-
419
- return { isValid: errors.length === 0, errors, warnings };
420
- }
421
-
422
- _generateRouter(views) {
423
- let routeMap = '';
424
- views.forEach(v => {
425
- const cap = v.name.charAt(0).toUpperCase() + v.name.slice(1);
426
- if (v.name.toLowerCase() === 'index') routeMap += ` '/': render${cap},\n`;
427
- routeMap += ` '/${v.name.toLowerCase()}': render${cap},\n`;
428
- });
429
-
430
- // Initialize layout call if configured
431
- const layoutInit = this._layoutFnName
432
- ? `\n// Initialize default layout\nif (typeof ${this._layoutFnName} === 'function') {\n ${this._layoutFnName}();\n}\n`
433
- : '';
434
-
435
- return `
436
- // --- JUX SOURCE LOADER ---
437
- var __juxSources = null;
438
- async function __juxLoadSources() {
439
- if (__juxSources) return __juxSources;
440
- try {
441
- var res = await fetch('/__jux_sources.json');
442
- __juxSources = await res.json();
443
- } catch (e) {
444
- __juxSources = {};
445
- }
446
- return __juxSources;
447
- }
448
-
449
- function __juxFindSource(stack) {
450
- var match = stack.match(/render(\\w+)/);
451
- if (match) {
452
- var viewName = match[1].toLowerCase();
453
- for (var file in __juxSources || {}) {
454
- if (__juxSources[file].name.toLowerCase() === viewName) {
455
- return { file: file, source: __juxSources[file], viewName: match[1] };
456
- }
457
- }
458
- }
459
- // Also check for layout
460
- if (stack.indexOf('Layout') > -1) {
461
- for (var file in __juxSources || {}) {
462
- if (file.indexOf('layout') > -1) {
463
- return { file: file, source: __juxSources[file] };
464
- }
465
- }
466
- }
467
- return null;
468
- }
469
-
470
- // --- JUX RUNTIME ERROR OVERLAY ---
471
- var __juxErrorOverlay = {
472
- styles: \`
473
- #__jux-error-overlay {
474
- position: fixed; inset: 0; z-index: 99999;
475
- background: rgba(0, 0, 0, 0.4);
476
- display: flex; align-items: center; justify-content: center;
477
- font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, monospace;
478
- opacity: 0; transition: opacity 0.2s ease-out; backdrop-filter: blur(2px);
479
- }
480
- #__jux-error-overlay.visible { opacity: 1; }
481
- #__jux-error-overlay * { box-sizing: border-box; }
482
- .__jux-modal {
483
- background: #f8f9fa; border-radius: 4px;
484
- box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.4);
485
- max-width: 80vw; width: 90%; max-height: 90vh;
486
- overflow: hidden; display: flex; flex-direction: column;
487
- transform: translateY(10px); transition: transform 0.2s ease-out;
488
- }
489
- #__jux-error-overlay.visible .__jux-modal { transform: translateY(0); }
490
- .__jux-header { background: #fff; padding: 20px 24px; border-bottom: 1px solid #e5e7eb; }
491
- .__jux-header h3 { margin: 0 0 6px; font-weight: 600; font-size: 11px; color: #9ca3af; text-transform: uppercase; }
492
- .__jux-header h1 { margin: 0 0 8px; font-size: 18px; font-weight: 600; color: #dc2626; line-height: 1.3; }
493
- .__jux-header .file-info { color: #6b7280; font-size: 13px; }
494
- .__jux-header .file-info strong { color: #dc2626; font-weight: 600; }
495
- .__jux-source { padding: 16px 24px; overflow: auto; flex: 1; background: #f8f9fa; }
496
- .__jux-code { background: #fff; border-radius: 8px; overflow: hidden; border: 1px solid #e5e7eb; }
497
- .__jux-code-line { display: flex; font-size: 13px; line-height: 1.7; }
498
- .__jux-code-line.error { background: #fef2f2; }
499
- .__jux-code-line.error .__jux-line-code { color: #dc2626; font-weight: 500; }
500
- .__jux-code-line.context { background: #fefce8; }
501
- .__jux-line-num { min-width: 44px; padding: 4px 12px; text-align: right; color: #9ca3af; background: #f9fafb; border-right: 1px solid #e5e7eb; font-size: 12px; }
502
- .__jux-code-line.error .__jux-line-num { background: #fef2f2; color: #dc2626; }
503
- .__jux-line-code { flex: 1; padding: 4px 16px; color: #374151; white-space: pre; overflow-x: auto; }
504
- .__jux-footer { padding: 16px 24px; background: #fff; border-top: 1px solid #e5e7eb; display: flex; justify-content: space-between; align-items: center; }
505
- .__jux-tip { color: #6b7280; font-size: 12px; }
506
- .__jux-dismiss { background: #f3f4f6; color: #374151; border: 1px solid #d1d5db; padding: 8px 16px; border-radius: 6px; cursor: pointer; font-size: 13px; font-weight: 500; }
507
- .__jux-dismiss:hover { background: #e5e7eb; }
508
- .__jux-no-source { color: #6b7280; padding: 24px; text-align: center; }
509
- .__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; }
510
- \`,
511
-
512
- show: async function(error, title) {
513
- title = title || 'Runtime Error';
514
- var existing = document.getElementById('__jux-error-overlay');
515
- if (existing) existing.remove();
516
- await __juxLoadSources();
517
-
518
- var overlay = document.createElement('div');
519
- overlay.id = '__jux-error-overlay';
520
- var stack = error && error.stack ? error.stack : '';
521
- var msg = error && error.message ? error.message : String(error);
522
- var found = __juxFindSource(stack);
523
- var sourceHtml = '', fileInfo = '';
524
-
525
- if (found && found.source && found.source.lines) {
526
- var lines = found.source.lines;
527
- fileInfo = '<span class="file-info">in <strong>' + found.file + '</strong></span>';
528
- var errorLineIndex = -1;
529
- var errorMethod = msg.match(/\\.([a-zA-Z]+)\\s*is not a function/);
530
- if (errorMethod) {
531
- for (var i = 0; i < lines.length; i++) {
532
- if (lines[i].indexOf('.' + errorMethod[1]) > -1) { errorLineIndex = i; break; }
533
- }
534
- }
535
- if (errorLineIndex === -1) {
536
- for (var i = 0; i < lines.length; i++) {
537
- if (lines[i].indexOf('throw ') > -1) { errorLineIndex = i; break; }
538
- }
539
- }
540
- var contextStart = Math.max(0, errorLineIndex - 3);
541
- var contextEnd = Math.min(lines.length - 1, errorLineIndex + 5);
542
- if (errorLineIndex === -1) { contextStart = 0; contextEnd = Math.min(14, lines.length - 1); }
543
-
544
- for (var i = contextStart; i <= contextEnd; i++) {
545
- var isError = (i === errorLineIndex);
546
- var isContext = !isError && errorLineIndex > -1 && Math.abs(i - errorLineIndex) <= 2;
547
- var lineClass = isError ? ' error' : (isContext ? ' context' : '');
548
- var lineCode = lines[i].replace(/</g, '&lt;').replace(/>/g, '&gt;') || ' ';
549
- sourceHtml += '<div class="__jux-code-line' + lineClass + '"><span class="__jux-line-num">' + (i + 1) + '</span><span class="__jux-line-code">' + lineCode + '</span></div>';
550
- }
551
- } else {
552
- sourceHtml = '<div class="__jux-no-source"><p>Could not locate source file.</p><pre>' + stack + '</pre></div>';
553
- }
554
-
555
- 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>';
556
- document.body.appendChild(overlay);
557
- requestAnimationFrame(function() { overlay.classList.add('visible'); });
558
- console.error(title + ':', error);
559
- }
560
- };
561
-
562
- window.addEventListener('error', function(e) { __juxErrorOverlay.show(e.error || new Error(e.message), 'Uncaught Error'); }, true);
563
- window.addEventListener('unhandledrejection', function(e) { __juxErrorOverlay.show(e.reason || new Error('Promise rejected'), 'Unhandled Promise Rejection'); }, true);
564
-
565
- // --- JUX ROUTER ---
566
- const routes = {\n${routeMap}};
567
- ${layoutInit}
568
- async function navigate(path) {
569
- const view = routes[path];
570
- if (!view) {
571
- document.getElementById('app').innerHTML = '<h1 style="padding:40px;">404 - Not Found</h1>';
572
- return;
573
- }
574
- document.getElementById('app').innerHTML = '';
575
- var overlay = document.getElementById('__jux-error-overlay');
576
- if (overlay) overlay.remove();
577
- try { await view(); } catch (err) { __juxErrorOverlay.show(err, 'Jux Render Error'); }
578
- }
579
-
580
- document.addEventListener('click', e => {
581
- const a = e.target.closest('a');
582
- if (!a || a.dataset.router === 'false') return;
583
- try { if (new URL(a.href, location.origin).origin !== location.origin) return; } catch { return; }
584
- e.preventDefault();
585
- history.pushState({}, '', a.href);
586
- navigate(new URL(a.href, location.origin).pathname);
587
- });
588
-
589
- window.addEventListener('popstate', () => navigate(location.pathname));
590
- navigate(location.pathname);
591
- `;
592
- }
593
-
594
- async build() {
595
- console.log('šŸš€ JUX Build\n');
596
-
597
- const juxscriptPath = this.findJuxscriptPath();
598
- if (!juxscriptPath) {
599
- console.error('āŒ Could not locate juxscript package');
600
- return { success: false, errors: [{ message: 'juxscript not found' }], warnings: [] };
601
- }
602
- console.log(`šŸ“¦ Using: ${juxscriptPath}`);
603
-
604
- await this.loadJuxscriptExports();
605
-
606
- if (fs.existsSync(this.distDir)) {
607
- fs.rmSync(this.distDir, { recursive: true, force: true });
608
- }
609
- fs.mkdirSync(this.distDir, { recursive: true });
610
-
611
- const { views, dataModules, sharedModules } = this.scanFiles();
612
- console.log(`šŸ“ Found ${views.length} views, ${sharedModules.length} shared, ${dataModules.length} data`);
613
-
614
- // Copy source assets (themes, layouts, assets)
615
- this.copySourceAssets();
616
-
617
- // Bundle juxscript component CSS
618
- const cssBundle = this.copyJuxscriptCss();
619
-
620
- // Copy data/shared modules to dist
621
- const juxDistDir = path.join(this.distDir, 'jux');
622
- fs.mkdirSync(juxDistDir, { recursive: true });
623
- [...dataModules, ...sharedModules].forEach(m => {
624
- fs.writeFileSync(path.join(juxDistDir, m.file), m.content);
625
- });
626
-
627
- const entryContent = this.generateEntryPoint(views, dataModules, sharedModules);
628
- const entryPath = path.join(this.distDir, 'entry.js');
629
- fs.writeFileSync(entryPath, entryContent);
630
-
631
- const snapshotPath = path.join(this.distDir, '__jux_sources.json');
632
- fs.writeFileSync(snapshotPath, JSON.stringify(this._sourceSnapshot, null, 2));
633
- console.log(`šŸ“ø Source snapshot written`);
634
-
635
- const validation = this.reportValidationIssues();
636
- if (!validation.isValid) {
637
- console.log('šŸ›‘ BUILD FAILED\n');
638
- return { success: false, errors: validation.errors, warnings: validation.warnings };
639
- }
640
-
641
- try {
642
- await esbuild.build({
643
- entryPoints: [entryPath],
644
- bundle: true,
645
- outfile: path.join(this.distDir, 'bundle.js'),
646
- format: 'esm',
647
- platform: 'browser',
648
- target: 'esnext',
649
- sourcemap: true,
650
- loader: { '.jux': 'js', '.css': 'empty' },
651
- plugins: [{
652
- name: 'juxscript-resolver',
653
- setup: (build) => {
654
- build.onResolve({ filter: /^juxscript$/ }, () => ({ path: juxscriptPath }));
655
- }
656
- }],
657
- });
658
- console.log('āœ… esbuild complete');
659
- } catch (err) {
660
- console.error('āŒ esbuild failed:', err);
661
- return { success: false, errors: [{ message: err.message }], warnings: [] };
662
- }
663
-
664
- // Generate HTML with layout container sibling to app
665
- const html = `<!DOCTYPE html>
666
- <html lang="en">
667
- <head>
668
- <meta charset="UTF-8">
669
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
670
- <title>JUX App</title>
671
- ${this.generateCssLinks(cssBundle)}
672
- <script type="module" src="./bundle.js"></script>
673
- </head>
674
- <body>
675
- <div id="jux-layout">
676
- <div id="app"></div>
677
- </div>
678
- </body>
679
- </html>`;
680
- fs.writeFileSync(path.join(this.distDir, 'index.html'), html);
681
-
682
- fs.unlinkSync(entryPath);
683
- fs.rmSync(juxDistDir, { recursive: true, force: true });
684
-
685
- console.log(`\nāœ… Build Complete!\n`);
686
- return { success: true, errors: [], warnings: validation.warnings };
687
- }
688
- }