juxscript 1.0.50 → 1.0.52

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/bin/cli.js CHANGED
@@ -147,6 +147,10 @@ import {
147
147
  } from '../machinery/compiler.js';
148
148
  import { generateDocs } from '../machinery/doc-generator.js';
149
149
  import { start } from '../machinery/server.js';
150
+ import { loadConfig, runBootstrap } from '../machinery/config.js';
151
+
152
+ // ✅ Load config BEFORE setting up PATHS
153
+ const config = await loadConfig(process.cwd());
150
154
 
151
155
  // CLEAR PATH CONTRACT - CONVENTIONS
152
156
  const PATHS = {
@@ -158,25 +162,25 @@ const PATHS = {
158
162
 
159
163
  // Where user's .jux source files live (CONVENTION: ./jux/)
160
164
  get juxSource() {
161
- return path.join(this.projectRoot, 'jux');
165
+ return path.join(this.projectRoot, config.sourceDir);
162
166
  },
163
167
 
164
168
  // Where jux lib files are (components, layouts, etc.)
165
169
  get juxLib() {
166
- return path.join(this.packageRoot, 'lib');
170
+ return path.resolve(__dirname, '..', 'lib');
167
171
  },
168
172
 
169
173
  // Where frontend build output goes (CONVENTION: ./jux-dist/)
170
174
  get frontendDist() {
171
- return path.join(this.projectRoot, 'jux-dist');
175
+ return path.join(this.projectRoot, config.distDir);
172
176
  }
173
177
  };
174
178
 
175
179
  console.log('📍 JUX Paths:');
176
180
  console.log(` Package: ${PATHS.packageRoot}`);
177
181
  console.log(` Project: ${PATHS.projectRoot}`);
178
- console.log(` Source: ${PATHS.juxSource}`);
179
- console.log(` Output: ${PATHS.frontendDist}`);
182
+ console.log(` Source: ${PATHS.juxSource} (from config: ${config.sourceDir})`);
183
+ console.log(` Output: ${PATHS.frontendDist} (from config: ${config.distDir})`);
180
184
  console.log(` Lib: ${PATHS.juxLib}\n`);
181
185
 
182
186
  /**
@@ -208,7 +212,7 @@ function findJuxFiles(dir, fileList = []) {
208
212
  *
209
213
  * @param {boolean} isServe - Whether building for dev server
210
214
  */
211
- async function buildProject(isServe = false) {
215
+ async function buildProject(isServe = false, wsPort = 3001) {
212
216
  const buildStartTime = performance.now();
213
217
  console.log('🔨 Building JUX frontend...\n');
214
218
 
@@ -430,9 +434,9 @@ jux.paragraph('counter')
430
434
  console.log('+ Created package.json');
431
435
  }
432
436
 
433
- // Create .gitignore
437
+ // Create .gitignore with config.distDir
434
438
  const gitignorePath = path.join(PATHS.projectRoot, '.gitignore');
435
- const gitignoreContent = `.jux/\nnode_modules/\n.DS_Store\n`; // ✅ Changed
439
+ const gitignoreContent = `${config.distDir}/\nnode_modules/\n.DS_Store\n`;
436
440
 
437
441
  if (!fs.existsSync(gitignorePath)) {
438
442
  fs.writeFileSync(gitignorePath, gitignoreContent);
@@ -458,8 +462,8 @@ jux.paragraph('counter')
458
462
 
459
463
  console.log('\n✅ JUX project initialized!\n');
460
464
  console.log('Project structure:');
461
- console.log(' jux/ # Your source files');
462
- console.log(' .jux/ # Build output (git-ignored)');
465
+ console.log(` ${config.sourceDir}/ # Your source files`);
466
+ console.log(` ${config.distDir}/ # Build output (git-ignored)`);
463
467
  console.log(' juxconfig.js # Configuration');
464
468
  console.log(' package.json # Dependencies\n');
465
469
  console.log('Next steps:');
@@ -467,19 +471,19 @@ jux.paragraph('counter')
467
471
  console.log(' npm run dev # Start dev server\n');
468
472
 
469
473
  } else if (command === 'build') {
470
- // ✅ Always builds router bundle
474
+ // ✅ Run bootstrap before build
475
+ await runBootstrap(config.bootstrap);
471
476
  await buildProject(false);
472
477
  console.log(`✅ Build complete: ${PATHS.frontendDist}`);
473
478
 
474
479
  } else if (command === 'serve') {
475
- // ✅ Always serves router bundle
476
- await buildProject(true);
477
-
478
- // Parse port arguments: npx jux serve [httpPort] [wsPort]
479
- const httpPort = parseInt(process.argv[3]) || 3000;
480
- const wsPort = parseInt(process.argv[4]) || 3001;
480
+ // ✅ Use ports from config
481
+ const httpPort = parseInt(process.argv[3]) || config.ports.http;
482
+ const wsPort = parseInt(process.argv[4]) || config.ports.ws;
481
483
 
482
- await start(httpPort, wsPort);
484
+ await runBootstrap(config.bootstrap);
485
+ await buildProject(true, wsPort);
486
+ await start(httpPort, wsPort, PATHS.frontendDist);
483
487
 
484
488
  } else {
485
489
  console.log(`
@@ -488,16 +492,16 @@ JUX CLI - A JavaScript UX authorship platform
488
492
  Usage:
489
493
  npx jux create [name] Create a new JUX project
490
494
  npx jux init Initialize JUX in current directory
491
- npx jux build Build router bundle to ./.jux/
495
+ npx jux build Build router bundle to ${config.distDir}/
492
496
  npx jux serve [http] [ws] Start dev server with hot reload
493
497
 
494
498
  Project Structure:
495
499
  my-project/
496
- ├── .jux/ # Build output (git-ignored, like .git)
500
+ ├── ${config.distDir}/ # Build output (git-ignored, like .git)
497
501
  │ ├── lib/
498
502
  │ ├── main.js
499
503
  │ └── index.html
500
- ├── jux/ # Your .jux source files
504
+ ├── ${config.sourceDir}/ # Your .jux source files
501
505
  │ ├── index.jux
502
506
  │ ├── layout.css
503
507
  │ └── layout.jux
@@ -1,709 +1,86 @@
1
- import fs from 'fs';
2
1
  import path from 'path';
3
- import esbuild from 'esbuild';
4
-
5
- /**
6
- * Generate import map script tag
7
- */
8
- function generateImportMapScript() {
9
- return `<script type="importmap">
10
- {
11
- "imports": {
12
- "juxscript": "./lib/jux.js",
13
- "juxscript/": "./lib/",
14
- "juxscript/reactivity": "./lib/reactivity/state.js",
15
- "juxscript/presets/": "./presets/",
16
- "juxscript/components/": "./lib/components/"
17
- }
18
- }
19
- </script>`;
20
- }
21
-
22
- /**
23
- * Copy and build the JUX library from TypeScript to JavaScript
24
- *
25
- * @param {string} projectRoot - Root directory containing lib/
26
- * @param {string} distDir - Destination directory for built files
27
- */
28
- export async function copyLibToOutput(projectRoot, distDir) {
29
- // Simplified lib path resolution
30
- const libSrc = path.resolve(projectRoot, '../lib');
31
-
32
- if (!fs.existsSync(libSrc)) {
33
- throw new Error(`lib/ directory not found at ${libSrc}`);
34
- }
35
-
36
- const libDest = path.join(distDir, 'lib');
37
-
38
- console.log('📦 Building TypeScript library...');
39
- console.log(` From: ${libSrc}`);
40
- console.log(` To: ${libDest}`);
41
-
42
- if (fs.existsSync(libDest)) {
43
- fs.rmSync(libDest, { recursive: true });
44
- }
45
-
46
- fs.mkdirSync(libDest, { recursive: true });
47
-
48
- // Find all TypeScript entry points
49
- const tsFiles = findFiles(libSrc, '.ts');
50
-
51
- if (tsFiles.length === 0) {
52
- console.warn('⚠️ No TypeScript files found in lib/');
53
- return;
54
- }
55
-
56
- console.log(` Found ${tsFiles.length} TypeScript files`);
57
-
58
- // Build all TypeScript files with esbuild
59
- try {
60
- await esbuild.build({
61
- entryPoints: tsFiles,
62
- bundle: false,
63
- format: 'esm',
64
- outdir: libDest,
65
- outbase: libSrc,
66
- platform: 'browser',
67
- target: 'es2020',
68
- loader: {
69
- '.ts': 'ts'
70
- },
71
- logLevel: 'warning'
72
- });
73
-
74
- console.log(' ✓ TypeScript compiled to JavaScript');
75
-
76
- // Copy non-TS files (CSS, HTML, etc.)
77
- console.log(' Copying lib assets...');
78
- copyNonTsFiles(libSrc, libDest);
79
- console.log(' ✓ Lib assets copied');
80
-
81
- } catch (err) {
82
- console.error('❌ Failed to build TypeScript:', err.message);
83
- throw err;
84
- }
85
-
86
- console.log('✅ Library ready\n');
87
- }
88
-
89
- /**
90
- * Copy project assets (CSS, JS, images) from jux/ to dist/
91
- *
92
- * @param {string} projectRoot - Source directory (jux/)
93
- * @param {string} distDir - Destination directory (jux-dist/)
94
- */
95
- export async function copyProjectAssets(projectRoot, distDir) {
96
- console.log('📦 Copying project assets...');
97
-
98
- // Find all CSS and JS files in project root (excluding node_modules, dist, .git)
99
- const allFiles = [];
100
- findProjectFiles(projectRoot, ['.css', '.js'], allFiles, projectRoot);
101
-
102
- console.log(` Found ${allFiles.length} asset file(s)`);
103
-
104
- for (const srcPath of allFiles) {
105
- const relativePath = path.relative(projectRoot, srcPath);
106
- const destPath = path.join(distDir, relativePath);
107
- const destDir = path.dirname(destPath);
108
-
109
- // Create destination directory if needed
110
- if (!fs.existsSync(destDir)) {
111
- fs.mkdirSync(destDir, { recursive: true });
112
- }
113
-
114
- // Copy file
115
- fs.copyFileSync(srcPath, destPath);
116
- console.log(` ✓ ${relativePath}`);
117
- }
118
-
119
- console.log('✅ Project assets copied\n');
120
- }
121
-
122
- /**
123
- * Transpile TypeScript files from jux/ to jux-dist/, preserving folder structure
124
- *
125
- * @param {string} srcDir - Source directory (jux/)
126
- * @param {string} destDir - Destination directory (jux-dist/)
127
- * @example
128
- * // jux/samples/mypage.ts -> jux-dist/samples/mypage.js
129
- * await transpileProjectTypeScript('jux/', 'jux-dist/');
130
- */
131
- export async function transpileProjectTypeScript(srcDir, destDir) {
132
- console.log('🔷 Transpiling TypeScript files...');
133
-
134
- // Find all TypeScript files in the project
135
- const tsFiles = findFiles(srcDir, '.ts');
136
-
137
- if (tsFiles.length === 0) {
138
- console.log(' No TypeScript files found in project');
139
- return;
140
- }
141
-
142
- console.log(` Found ${tsFiles.length} TypeScript file(s)`);
143
-
144
- try {
145
- // Build all TypeScript files with esbuild
146
- await esbuild.build({
147
- entryPoints: tsFiles,
148
- bundle: false,
149
- format: 'esm',
150
- outdir: destDir,
151
- outbase: srcDir,
152
- platform: 'browser',
153
- target: 'es2020',
154
- loader: {
155
- '.ts': 'ts'
156
- },
157
- logLevel: 'warning'
158
- });
159
-
160
- // Log each transpiled file
161
- tsFiles.forEach(tsFile => {
162
- const relativePath = path.relative(srcDir, tsFile);
163
- const jsPath = relativePath.replace(/\.ts$/, '.js');
164
- console.log(` ✓ ${relativePath} → ${jsPath}`);
165
- });
166
-
167
- console.log('✅ TypeScript transpiled\n');
168
-
169
- } catch (err) {
170
- console.error('❌ Failed to transpile TypeScript:', err.message);
171
- throw err;
172
- }
173
- }
174
-
175
- /**
176
- * Copy presets folder from lib to dist (maintaining directory structure)
177
- *
178
- * @param {string} packageRoot - Source package root directory
179
- * @param {string} distDir - Destination directory
180
- */
181
- export async function copyPresetsToOutput(packageRoot, distDir) {
182
- console.log('📦 Copying presets...');
183
-
184
- const presetsSrc = path.join(packageRoot, 'presets');
185
- const presetsDest = path.join(distDir, 'presets');
186
-
187
- if (!fs.existsSync(presetsSrc)) {
188
- console.log(' No presets directory found');
189
- return;
190
- }
191
-
192
- if (fs.existsSync(presetsDest)) {
193
- fs.rmSync(presetsDest, { recursive: true });
194
- }
195
-
196
- fs.mkdirSync(presetsDest, { recursive: true });
197
-
198
- // Recursively copy entire presets directory structure
199
- let copiedCount = 0;
200
-
201
- function copyRecursive(src, dest) {
202
- const entries = fs.readdirSync(src, { withFileTypes: true });
203
-
204
- for (const entry of entries) {
205
- const srcPath = path.join(src, entry.name);
206
- const destPath = path.join(dest, entry.name);
207
-
208
- if (entry.isDirectory()) {
209
- fs.mkdirSync(destPath, { recursive: true });
210
- copyRecursive(srcPath, destPath);
211
- } else if (entry.isFile()) {
212
- fs.copyFileSync(srcPath, destPath);
213
- const relativePath = path.relative(presetsSrc, srcPath);
214
- console.log(` ✓ ${relativePath}`);
215
- copiedCount++;
216
- }
217
- }
218
- }
219
-
220
- copyRecursive(presetsSrc, presetsDest);
221
-
222
- console.log(`✅ Copied ${copiedCount} preset file(s)\n`);
223
- }
224
-
225
- /**
226
- * Recursively find files with a specific extension
227
- *
228
- * @param {string} dir - Directory to search
229
- * @param {string} extension - File extension (e.g., '.ts')
230
- * @param {string[]} fileList - Accumulator for found files
231
- * @returns {string[]} Array of file paths
232
- */
233
- function findFiles(dir, extension, fileList = []) {
234
- const files = fs.readdirSync(dir);
235
-
236
- files.forEach(file => {
237
- const filePath = path.join(dir, file);
238
- const stat = fs.statSync(filePath);
239
-
240
- if (stat.isDirectory()) {
241
- findFiles(filePath, extension, fileList);
242
- } else if (file.endsWith(extension)) {
243
- fileList.push(filePath);
244
- }
245
- });
2
+ import fs from 'fs';
3
+ import { fileURLToPath } from 'url';
246
4
 
247
- return fileList;
248
- }
5
+ const __filename = fileURLToPath(import.meta.url);
6
+ const __dirname = path.dirname(__filename);
249
7
 
250
8
  /**
251
- * Copy non-TypeScript files (CSS, JSON, JS, SVG, etc.)
252
- *
253
- * @param {string} src - Source directory
254
- * @param {string} dest - Destination directory
9
+ * Default JUX configuration
255
10
  */
256
- function copyNonTsFiles(src, dest) {
257
- const entries = fs.readdirSync(src, { withFileTypes: true });
258
-
259
- for (const entry of entries) {
260
- const srcPath = path.join(src, entry.name);
261
- const destPath = path.join(dest, entry.name);
262
-
263
- if (entry.isDirectory()) {
264
- if (!fs.existsSync(destPath)) {
265
- fs.mkdirSync(destPath, { recursive: true });
266
- }
267
- copyNonTsFiles(srcPath, destPath);
268
- } else if (entry.isFile()) {
269
- const ext = path.extname(entry.name);
270
- // Copy CSS, JSON, SVG, and JS files (but not .ts files)
271
- if (['.css', '.json', '.js', '.svg', '.png', '.jpg', '.jpeg', '.gif', '.webp'].includes(ext)) {
272
- fs.copyFileSync(srcPath, destPath);
273
- }
274
- }
275
- }
276
- }
11
+ export const defaultConfig = {
12
+ // Source directory for .jux files
13
+ sourceDir: 'jux',
14
+
15
+ // Output directory for built files (hidden like .git)
16
+ distDir: '.jux',
17
+
18
+ // Dev server ports
19
+ ports: {
20
+ http: 3000,
21
+ ws: 3001
22
+ },
23
+
24
+ // Build options
25
+ build: {
26
+ minify: false,
27
+ sourcemap: true
28
+ },
29
+
30
+ // Bootstrap functions (run before app starts)
31
+ bootstrap: []
32
+ };
277
33
 
278
34
  /**
279
- * Find project files with specific extensions, excluding certain directories
280
- *
281
- * @param {string} dir - Directory to search
282
- * @param {string[]} extensions - File extensions to find
283
- * @param {string[]} fileList - Accumulator for found files
284
- * @param {string} rootDir - Root directory for relative paths
285
- * @param {string[]} excludeDirs - Directories to exclude
286
- * @returns {string[]} Array of file paths
35
+ * Load configuration from juxconfig.js or use defaults
36
+ * @param {string} projectRoot - Project root directory
37
+ * @returns {Promise<object>} Configuration object
287
38
  */
288
- function findProjectFiles(dir, extensions, fileList = [], rootDir = dir, excludeDirs = ['node_modules', 'jux-dist', '.git', 'lib']) {
289
- if (!fs.existsSync(dir)) return fileList;
290
-
291
- const entries = fs.readdirSync(dir, { withFileTypes: true });
292
-
293
- for (const entry of entries) {
294
- const fullPath = path.join(dir, entry.name);
295
-
296
- if (entry.isDirectory()) {
297
- // Skip excluded directories
298
- if (excludeDirs.includes(entry.name)) {
299
- continue;
300
- }
301
- findProjectFiles(fullPath, extensions, fileList, rootDir, excludeDirs);
302
- } else {
303
- // Check if file has one of the desired extensions
304
- const hasExtension = extensions.some(ext => entry.name.endsWith(ext));
305
- if (hasExtension) {
306
- fileList.push(fullPath);
307
- }
39
+ export async function loadConfig(projectRoot) {
40
+ const configPath = path.join(projectRoot, 'juxconfig.js');
41
+
42
+ if (fs.existsSync(configPath)) {
43
+ try {
44
+ const configModule = await import(`file://${configPath}`);
45
+ const userConfig = configModule.default || configModule;
46
+
47
+ return {
48
+ ...defaultConfig,
49
+ ...userConfig,
50
+ ports: { ...defaultConfig.ports, ...(userConfig.ports || {}) },
51
+ build: { ...defaultConfig.build, ...(userConfig.build || {}) }
52
+ };
53
+ } catch (err) {
54
+ console.warn(`⚠️ Failed to load juxconfig.js: ${err.message}`);
55
+ console.warn(` Using default configuration\n`);
56
+ return defaultConfig;
308
57
  }
309
58
  }
310
-
311
- return fileList;
59
+
60
+ console.log('ℹ️ No juxconfig.js found, using defaults');
61
+ return defaultConfig;
312
62
  }
313
63
 
314
64
  /**
315
- * Bundle all .jux files into a single router-based main.js
316
- *
317
- * @param {string} projectRoot - Source directory (jux/)
318
- * @param {string} distDir - Destination directory (jux-dist/)
319
- * @param {Object} options - Bundle options
320
- * @param {string} options.routePrefix - Route prefix (e.g., '/experiments')
321
- * @returns {Promise<string>} - Returns the generated filename (e.g., 'main.1234567890.js')
65
+ * Run bootstrap functions from config
66
+ * @param {Function[]} bootstrapFunctions - Array of async functions to run
322
67
  */
323
- export async function bundleJuxFilesToRouter(projectRoot, distDir, options = {}) {
324
- const startTime = performance.now();
325
- const { routePrefix = '' } = options;
326
-
327
- console.log('🔀 Bundling .jux files into router...');
328
-
329
- const juxFiles = findFiles(projectRoot, '.jux');
330
-
331
- if (juxFiles.length === 0) {
332
- console.log(' No .jux files found');
68
+ export async function runBootstrap(bootstrapFunctions = []) {
69
+ if (!Array.isArray(bootstrapFunctions) || bootstrapFunctions.length === 0) {
333
70
  return;
334
71
  }
335
-
336
- console.log(` Found ${juxFiles.length} .jux file(s)`);
337
-
338
- const fileTimings = [];
339
- const views = [];
340
- const routes = [];
341
- const sharedModules = new Map(); // Track shared .jux modules
342
-
343
- for (const juxFile of juxFiles) {
344
- const fileStartTime = performance.now();
345
-
346
- const relativePath = path.relative(projectRoot, juxFile);
347
- const parsedPath = path.parse(relativePath);
348
-
349
- const rawFunctionName = parsedPath.dir
350
- ? `${parsedPath.dir.replace(/\//g, '_')}_${parsedPath.name}`
351
- : parsedPath.name;
352
-
353
- const cleanFunctionName = rawFunctionName
354
- .replace(/[-_]/g, ' ')
355
- .split(' ')
356
- .map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
357
- .join('');
358
-
359
- const routePath = routePrefix + '/' + (parsedPath.dir ? `${parsedPath.dir}/` : '') + parsedPath.name;
360
- const cleanRoutePath = routePath.replace(/\/+/g, '/');
361
-
362
- const juxContent = fs.readFileSync(juxFile, 'utf-8');
363
-
364
- // Check if this file exports components (shared module)
365
- const hasExports = /export\s+(const|let|function|class|{)/.test(juxContent);
366
-
367
- if (hasExports) {
368
- // Extract exports to shared modules section
369
- sharedModules.set(relativePath, extractSharedModule(juxContent, rawFunctionName));
370
- }
371
-
372
- const viewFunction = transformJuxToViewFunction(juxContent, rawFunctionName, parsedPath.name, relativePath, sharedModules);
373
-
374
- views.push(viewFunction);
375
- routes.push({ path: cleanRoutePath, functionName: cleanFunctionName });
376
-
377
- const fileTime = performance.now() - fileStartTime;
378
- fileTimings.push({ file: relativePath, time: fileTime });
379
-
380
- const exportNote = hasExports ? ' [+exports]' : '';
381
- console.log(` ✓ ${relativePath} → ${cleanFunctionName}()${exportNote} (${fileTime.toFixed(1)}ms)`);
382
- }
383
-
384
- // ✅ Show slowest files if any took >50ms
385
- const slowFiles = fileTimings.filter(f => f.time > 50).sort((a, b) => b.time - a.time);
386
- if (slowFiles.length > 0) {
387
- console.log(`\n ⚠️ Slowest files:`);
388
- slowFiles.slice(0, 3).forEach(f => {
389
- console.log(` ${f.file}: ${f.time.toFixed(0)}ms`);
390
- });
391
- }
392
-
393
- const bundleStartTime = performance.now();
394
- const routerCode = generateRouterBundle(views, routes, sharedModules);
395
- const bundleGenTime = performance.now() - bundleStartTime;
396
-
397
- // ✅ Use fixed filename (no timestamp)
398
- const mainJsFilename = 'main.js';
399
- const mainJsPath = path.join(distDir, mainJsFilename);
400
-
401
- // Write to dist/main.js
402
- const writeStartTime = performance.now();
403
- fs.writeFileSync(mainJsPath, routerCode);
404
- const writeTime = performance.now() - writeStartTime;
405
-
406
- const totalTime = performance.now() - startTime;
407
-
408
- console.log(` ✓ Generated: ${path.relative(projectRoot, mainJsPath)}`);
409
- console.log(`\n 📊 Bundle Statistics:`);
410
- console.log(` Files processed: ${juxFiles.length}`);
411
- console.log(` Bundle size: ${(routerCode.length / 1024).toFixed(1)} KB`);
412
- console.log(` Code generation: ${bundleGenTime.toFixed(0)}ms`);
413
- console.log(` File write: ${writeTime.toFixed(0)}ms`);
414
- console.log(` Total bundle time: ${totalTime.toFixed(0)}ms`);
415
- console.log('✅ Router bundle complete\n');
416
-
417
- // ✅ Return the filename so index.html can reference it
418
- return mainJsFilename;
419
- }
420
-
421
- /**
422
- * Extract and replace all string literals with placeholders
423
- * Handles: template literals WITHOUT interpolations, single quotes, double quotes
424
- * SKIPS: Template literals WITH ${} interpolations (those are dynamic code)
425
- */
426
- function extractStrings(code) {
427
- const strings = [];
428
- let result = code;
429
-
430
- // 1. Template literals WITHOUT interpolations (static strings only)
431
- // Match backticks that DON'T contain ${
432
- result = result.replace(/`([^`$\\]|\\[^$])*`/g, (match) => {
433
- // Double-check it doesn't contain ${
434
- if (!match.includes('${')) {
435
- strings.push(match);
436
- return `__STRING_${strings.length - 1}__`;
437
- }
438
- return match; // Leave dynamic templates alone
439
- });
440
-
441
- // 2. Double-quoted strings (with escaped quotes)
442
- result = result.replace(/"(?:[^"\\]|\\.)*"/g, (match) => {
443
- strings.push(match);
444
- return `__STRING_${strings.length - 1}__`;
445
- });
446
-
447
- // 3. Single-quoted strings (with escaped quotes)
448
- result = result.replace(/'(?:[^'\\]|\\.)*'/g, (match) => {
449
- strings.push(match);
450
- return `__STRING_${strings.length - 1}__`;
451
- });
452
-
453
- return { code: result, strings };
454
- }
455
-
456
- /**
457
- * Restore string placeholders back to original strings
458
- */
459
- function restoreStrings(code, strings) {
460
- return code.replace(/__STRING_(\d+)__/g, (match, index) => {
461
- const idx = parseInt(index, 10);
462
- if (idx >= 0 && idx < strings.length) {
463
- return strings[idx];
464
- }
465
- console.warn(`[Compiler] String placeholder ${match} not found (idx: ${idx}, available: ${strings.length})`);
466
- return match; // Leave as-is for debugging
467
- });
468
- }
469
-
470
- /**
471
- * Extract shared module exports from a .jux file
472
- *
473
- * @param {string} juxContent - Original .jux file content
474
- * @param {string} moduleName - Module identifier
475
- * @returns {string} Shared module code
476
- */
477
- function extractSharedModule(juxContent, moduleName) {
478
- const { code, strings } = extractStrings(juxContent);
479
-
480
- // Remove ALL imports
481
- let result = code.replace(/import\s+\{[^}]+\}\s+from\s+__STRING_\d+__;?\s*/g, '');
482
- result = result.replace(/import\s+\*\s+as\s+\w+\s+from\s+__STRING_\d+__;?\s*/g, '');
483
- result = result.replace(/import\s+__STRING_\d+__;?\s*/g, '');
484
-
485
- // Convert exports to declarations
486
- result = result.replace(/export\s+const\s+/g, 'const ');
487
- result = result.replace(/export\s+let\s+/g, 'let ');
488
- result = result.replace(/export\s+function\s+/g, 'function ');
489
- result = result.replace(/export\s+class\s+/g, 'class ');
490
- result = result.replace(/export\s+default\s+/g, '');
491
- result = result.replace(/export\s+\{([^}]+)\}\s*;?\s*/g, '');
492
-
493
- // Restore strings
494
- return restoreStrings(result, strings);
495
- }
496
-
497
- /**
498
- * Transform .jux file content into a view function
499
- *
500
- * @param {string} juxContent - Original .jux file content
501
- * @param {string} functionName - Function name for the view
502
- * @param {string} pageName - Page name for data attribute
503
- * @param {string} relativePath - Relative path of the .jux file
504
- * @param {Map} sharedModules - Map of shared module paths to their code
505
- * @returns {string} View function code
506
- */
507
- function transformJuxToViewFunction(juxContent, functionName, pageName, relativePath, sharedModules) {
508
- const { code, strings } = extractStrings(juxContent);
509
-
510
- // Remove ALL imports
511
- let result = code.replace(/import\s+\{[^}]+\}\s+from\s+__STRING_\d+__;?\s*/g, '');
512
- result = result.replace(/import\s+\*\s+as\s+\w+\s+from\s+__STRING_\d+__;?\s*/g, '');
513
- result = result.replace(/import\s+__STRING_\d+__;?\s*/g, '');
514
-
515
- // Handle exports
516
- result = result.replace(/export\s+const\s+(\w+)\s*=/g, 'const $1 =');
517
- result = result.replace(/export\s+let\s+(\w+)\s*=/g, 'let $1 =');
518
- result = result.replace(/export\s+function\s+(\w+)/g, 'const $1 = function');
519
- result = result.replace(/export\s+class\s+/g, 'class ');
520
- result = result.replace(/export\s+default\s+/g, '');
521
- result = result.replace(/export\s+\{([^}]+)\}\s*;?\s*/g, '');
522
-
523
- // Replace render patterns
524
- result = result.replace(/\.renderTo\(container\)/g, '.render("#app")');
525
- result = result.replace(/\.render\(\s*\)/g, '.render("#app")');
526
-
527
- // Restore strings
528
- result = restoreStrings(result, strings);
529
-
530
- const cleanName = functionName
531
- .replace(/[-_]/g, ' ')
532
- .split(' ')
533
- .map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
534
- .join('');
535
-
536
- return `
537
- // View: ${cleanName}
538
- function ${cleanName}() {
539
- ${result}
540
72
 
541
- return document.getElementById('app');
542
- }`;
543
- }
544
-
545
- /**
546
- * Generate complete router bundle with all views and routing logic
547
- *
548
- * @param {string[]} views - Array of view function code
549
- * @param {Array<{path: string, functionName: string}>} routes - Route definitions
550
- * @param {Map} sharedModules - Map of shared module code
551
- * @returns {string} Complete bundle code
552
- */
553
- function generateRouterBundle(views, routes, sharedModules = new Map()) {
554
- const libImport = './lib/jux.js';
555
-
556
- // Generate shared modules section
557
- const sharedModulesCode = Array.from(sharedModules.values())
558
- .filter(code => code.trim())
559
- .join('\n\n');
560
-
561
- // Filter out empty views (shared modules)
562
- const viewsCode = views.filter(v => v.trim()).join('\n\n');
563
-
564
- const routeTable = routes
565
- .map(r => ` '${r.path}': ${r.functionName}`)
566
- .join(',\n');
567
-
568
- return `// Generated Jux Router Bundle
569
- import { jux, state } from '${libImport}';
570
-
571
- // ═══════════════════════════════════════════════════════════════════
572
- // SHARED MODULES
573
- // ═══════════════════════════════════════════════════════════════════
574
-
575
- ${sharedModulesCode}
576
-
577
- // ═══════════════════════════════════════════════════════════════════
578
- // VIEW FUNCTIONS
579
- // ═══════════════════════════════════════════════════════════════════
580
-
581
- ${viewsCode}
582
-
583
- // ═══════════════════════════════════════════════════════════════════
584
- // 404 VIEW
585
- // ═══════════════════════════════════════════════════════════════════
586
-
587
- function JuxNotFound() {
588
- jux.heading(1, { text: '404 - Page Not Found' }).render('#app');
589
- jux.paragraph('404-msg')
590
- .text('The page you are looking for does not exist.')
591
- .render('#app');
73
+ console.log('🚀 Running bootstrap functions...\n');
592
74
 
593
- return document.getElementById('app');
594
- }
595
-
596
- // ═══════════════════════════════════════════════════════════════════
597
- // 403 VIEW
598
- // ═══════════════════════════════════════════════════════════════════
599
-
600
- function JuxForbidden() {
601
- jux.heading(1, { text: '403 - Forbidden' }).render('#app');
602
- jux.paragraph('403-msg')
603
- .text('You are not authorized to view this page.')
604
- .render('#app');
605
-
606
- return document.getElementById('app');
607
- }
608
-
609
- // ═══════════════════════════════════════════════════════════════════
610
- // ROUTE TABLE
611
- // ═══════════════════════════════════════════════════════════════════
612
-
613
- const routes = {
614
- ${routeTable}
615
- };
616
-
617
- // ═══════════════════════════════════════════════════════════════════
618
- // ROUTER CORE
619
- // ═══════════════════════════════════════════════════════════════════
620
-
621
- const app = document.getElementById('app');
622
-
623
- function render() {
624
- let path = location.pathname;
625
-
626
- // Try exact match first
627
- let view = routes[path];
628
-
629
- // If no match and path ends with /, try appending 'index'
630
- if (!view && path.endsWith('/')) {
631
- view = routes[path + 'index'] || routes[path.slice(0, -1) + '/index'];
632
- }
633
-
634
- // If still no match and path doesn't end with /, try appending '/index'
635
- if (!view && !path.endsWith('/')) {
636
- view = routes[path + '/index'];
75
+ for (const fn of bootstrapFunctions) {
76
+ if (typeof fn === 'function') {
77
+ try {
78
+ await fn();
79
+ } catch (err) {
80
+ console.error(`❌ Bootstrap function failed:`, err.message);
81
+ }
82
+ }
637
83
  }
638
84
 
639
- // Fall back to 404
640
- view = view || JuxNotFound;
641
-
642
- app.innerHTML = '';
643
- app.removeAttribute('data-jux-page');
644
-
645
- view();
646
-
647
- const pageName = Object.entries(routes).find(([p, v]) => v === view)?.[0] || 'not-found';
648
- app.setAttribute('data-jux-page', pageName.replace(/^\\\//, '').replace(/\\\//g, '-'));
649
- }
650
-
651
- document.addEventListener('click', e => {
652
- const a = e.target.closest('a');
653
- if (!a) return;
654
-
655
- if (a.dataset.router === 'false') return;
656
-
657
- const url = new URL(a.href);
658
- if (url.origin !== location.origin) return;
659
-
660
- e.preventDefault();
661
- history.pushState({}, '', url.pathname);
662
- render();
663
- });
664
-
665
- window.addEventListener('popstate', render);
666
-
667
- render();
668
- `;
669
-
670
- }
671
-
672
- /**
673
- * Generate a unified index.html for router bundle
674
- *
675
- * @param {string} distDir - Destination directory
676
- * @param {Array<{path: string, functionName: string}>} routes - Route definitions
677
- * @param {string} mainJsFilename - The generated main.js filename (e.g., 'main.1234567890.js')
678
- */
679
- export function generateIndexHtml(distDir, routes, mainJsFilename = 'main.js') {
680
- console.log('📄 Generating index.html...');
681
-
682
- // Generate navigation links
683
- const navLinks = routes
684
- .map(r => ` <a href="${r.path}">${r.functionName.replace(/_/g, ' ')}</a>`)
685
- .join(' |\n');
686
-
687
- const importMapScript = generateImportMapScript();
688
-
689
- const html = `<!DOCTYPE html>
690
- <html lang="en">
691
- <head>
692
- <meta charset="UTF-8">
693
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
694
- <title>Jux Application</title>
695
- </head>
696
- <body data-theme="">
697
- <!-- App container - router renders here -->
698
- <div id="app"></div>
699
- ${importMapScript}
700
- <script type="module" src="/${mainJsFilename}"></script>
701
- </body>
702
- </html>`;
703
-
704
- const indexPath = path.join(distDir, 'index.html');
705
- fs.writeFileSync(indexPath, html);
706
-
707
- console.log(` ✓ Generated: index.html (references ${mainJsFilename})`);
708
- console.log('✅ Index HTML complete\n');
85
+ console.log('✅ Bootstrap complete\n');
709
86
  }
@@ -47,7 +47,7 @@ async function tryPort(startPort, maxAttempts = 5, reservedPorts = []) {
47
47
  throw new Error(`Could not find available port after ${maxAttempts} attempts starting from ${startPort}`);
48
48
  }
49
49
 
50
- async function serve(httpPort = 3000, wsPort = 3001, distDir = './jux-dist') {
50
+ async function serve(httpPort = 3000, wsPort = 3001, distDir = './.jux') {
51
51
  const app = express();
52
52
  const absoluteDistDir = path.resolve(distDir);
53
53
  const projectRoot = path.resolve('.');
@@ -147,6 +147,6 @@ async function serve(httpPort = 3000, wsPort = 3001, distDir = './jux-dist') {
147
147
  return { server, httpPort: availableHttpPort, wsPort: availableWsPort };
148
148
  }
149
149
 
150
- export async function start(httpPort = 3000, wsPort = 3001) {
151
- return serve(httpPort, wsPort, './jux-dist');
150
+ export async function start(httpPort = 3000, wsPort = 3001, distDir = './.jux') {
151
+ return serve(httpPort, wsPort, distDir);
152
152
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "juxscript",
3
- "version": "1.0.50",
3
+ "version": "1.0.52",
4
4
  "type": "module",
5
5
  "description": "A JavaScript UX authorship platform",
6
6
  "main": "lib/jux.js",
@@ -1,5 +1,5 @@
1
1
  import { jux, state } from 'juxscript';
2
- import { initializeGrid } from '../presets/default/layout.jux';
2
+ import { initializeGrid } from 'layout.jux';
3
3
 
4
4
  // Initialize the grid layout - this executes the rendering
5
5
  const grid = initializeGrid();
@@ -1,7 +1,7 @@
1
1
  import { jux } from 'juxscript';
2
2
 
3
3
  // Import the layout styles - testing link.
4
- jux.include('../presets/default/layout.css');
4
+ jux.include('layout.css');
5
5
 
6
6
  // ═══════════════════════════════════════════════════════════════════
7
7
  // GRID LAYOUT - INITIALIZATION FUNCTION