juxscript 1.0.39 ā 1.0.41
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 +149 -73
- package/lib/components/docs-data.json +1 -1
- package/machinery/compiler.js +59 -7
- package/machinery/config.js +94 -0
- package/machinery/server.js +62 -0
- package/machinery/watcher.js +116 -43
- package/package.json +1 -1
- package/presets/default/layout.css +9 -0
- package/presets/default/layout.jux +1 -2
package/bin/cli.js
CHANGED
|
@@ -13,10 +13,14 @@ import { start } from '../machinery/server.js';
|
|
|
13
13
|
import path from 'path';
|
|
14
14
|
import fs from 'fs';
|
|
15
15
|
import { fileURLToPath } from 'url';
|
|
16
|
+
import { loadConfig, runBootstrap } from '../machinery/config.js';
|
|
16
17
|
|
|
17
18
|
const __filename = fileURLToPath(import.meta.url);
|
|
18
19
|
const __dirname = path.dirname(__filename);
|
|
19
20
|
|
|
21
|
+
// Load configuration first (before PATHS)
|
|
22
|
+
const config = await loadConfig(process.cwd());
|
|
23
|
+
|
|
20
24
|
// CLEAR PATH CONTRACT - CONVENTIONS
|
|
21
25
|
const PATHS = {
|
|
22
26
|
// Where jux package is installed (in node_modules/juxscript or local dev)
|
|
@@ -25,20 +29,14 @@ const PATHS = {
|
|
|
25
29
|
// Where the user's project root is (where they run `npx jux`)
|
|
26
30
|
projectRoot: process.cwd(),
|
|
27
31
|
|
|
28
|
-
// Where user's .jux source files live (
|
|
29
|
-
|
|
30
|
-
return path.join(this.projectRoot, 'jux');
|
|
31
|
-
},
|
|
32
|
+
// Where user's .jux source files live (from config or default)
|
|
33
|
+
juxSource: path.join(process.cwd(), config.sourceDir),
|
|
32
34
|
|
|
33
35
|
// Where jux lib files are (components, layouts, etc.)
|
|
34
|
-
|
|
35
|
-
return path.join(this.packageRoot, 'lib');
|
|
36
|
-
},
|
|
36
|
+
juxLib: path.resolve(__dirname, '..', 'lib'),
|
|
37
37
|
|
|
38
|
-
// Where frontend build output goes (
|
|
39
|
-
|
|
40
|
-
return path.join(this.projectRoot, 'jux-dist');
|
|
41
|
-
}
|
|
38
|
+
// Where frontend build output goes (from config or default)
|
|
39
|
+
frontendDist: path.join(process.cwd(), config.distDir)
|
|
42
40
|
};
|
|
43
41
|
|
|
44
42
|
console.log('š JUX Paths:');
|
|
@@ -80,8 +78,9 @@ function findJuxFiles(dir, fileList = []) {
|
|
|
80
78
|
* Build the entire JUX project (ALWAYS uses router bundle)
|
|
81
79
|
*
|
|
82
80
|
* @param {boolean} isServe - Whether building for dev server
|
|
81
|
+
* @param {number} wsPort - WebSocket port for hot reload
|
|
83
82
|
*/
|
|
84
|
-
async function buildProject(isServe = false) {
|
|
83
|
+
async function buildProject(isServe = false, wsPort = 3001) {
|
|
85
84
|
const buildStartTime = performance.now();
|
|
86
85
|
console.log('šØ Building JUX frontend...\n');
|
|
87
86
|
|
|
@@ -101,14 +100,14 @@ async function buildProject(isServe = false) {
|
|
|
101
100
|
|
|
102
101
|
// Step 1: Generate documentation
|
|
103
102
|
const docsStartTime = performance.now();
|
|
104
|
-
let docsTime = 0;
|
|
103
|
+
let docsTime = 0;
|
|
105
104
|
console.log('š Generating documentation...');
|
|
106
105
|
try {
|
|
107
106
|
await generateDocs(PATHS.juxLib);
|
|
108
107
|
docsTime = performance.now() - docsStartTime;
|
|
109
108
|
console.log(`ā
Documentation generated (${docsTime.toFixed(0)}ms)\n`);
|
|
110
109
|
} catch (error) {
|
|
111
|
-
docsTime = performance.now() - docsStartTime;
|
|
110
|
+
docsTime = performance.now() - docsStartTime;
|
|
112
111
|
console.warn(`ā ļø Failed to generate docs (${docsTime.toFixed(0)}ms):`, error.message);
|
|
113
112
|
}
|
|
114
113
|
|
|
@@ -124,7 +123,7 @@ async function buildProject(isServe = false) {
|
|
|
124
123
|
const presetsTime = performance.now() - presetsStartTime;
|
|
125
124
|
console.log(`ā±ļø Presets copy time: ${presetsTime.toFixed(0)}ms\n`);
|
|
126
125
|
|
|
127
|
-
// Step 4: Copy project assets
|
|
126
|
+
// Step 4: Copy project assets
|
|
128
127
|
const assetsStartTime = performance.now();
|
|
129
128
|
await copyProjectAssets(PATHS.juxSource, PATHS.frontendDist);
|
|
130
129
|
const assetsTime = performance.now() - assetsStartTime;
|
|
@@ -145,7 +144,6 @@ async function buildProject(isServe = false) {
|
|
|
145
144
|
process.exit(1);
|
|
146
145
|
}
|
|
147
146
|
|
|
148
|
-
// ā
Bundle and get the generated filename
|
|
149
147
|
const bundleStartTime = performance.now();
|
|
150
148
|
const mainJsFilename = await bundleJuxFilesToRouter(PATHS.juxSource, PATHS.frontendDist, {
|
|
151
149
|
routePrefix: ''
|
|
@@ -175,16 +173,14 @@ async function buildProject(isServe = false) {
|
|
|
175
173
|
};
|
|
176
174
|
});
|
|
177
175
|
|
|
178
|
-
// ā
Generate unified index.html
|
|
179
176
|
const indexStartTime = performance.now();
|
|
180
|
-
generateIndexHtml(PATHS.frontendDist, routes, mainJsFilename);
|
|
177
|
+
generateIndexHtml(PATHS.frontendDist, routes, mainJsFilename, wsPort);
|
|
181
178
|
const indexTime = performance.now() - indexStartTime;
|
|
182
179
|
|
|
183
180
|
const totalBuildTime = performance.now() - buildStartTime;
|
|
184
181
|
|
|
185
182
|
console.log(`\nā
Bundled ${projectJuxFiles.length} page(s) ā ${PATHS.frontendDist}/${mainJsFilename}\n`);
|
|
186
183
|
|
|
187
|
-
// ā
Build summary with timing breakdown
|
|
188
184
|
console.log(`š Build Summary:`);
|
|
189
185
|
console.log(` āāāāāāāāāāāāāāāāāāāāāāāāāāāā`);
|
|
190
186
|
console.log(` Documentation: ${docsTime.toFixed(0)}ms`);
|
|
@@ -197,7 +193,6 @@ async function buildProject(isServe = false) {
|
|
|
197
193
|
console.log(` āāāāāāāāāāāāāāāāāāāāāāāāāāāā`);
|
|
198
194
|
console.log(` Total build time: ${totalBuildTime.toFixed(0)}ms\n`);
|
|
199
195
|
|
|
200
|
-
// Show usage
|
|
201
196
|
if (!isServe) {
|
|
202
197
|
console.log('š¦ Serve from your backend:');
|
|
203
198
|
console.log(` Express: app.use(express.static('jux-dist'))`);
|
|
@@ -220,7 +215,106 @@ async function buildProject(isServe = false) {
|
|
|
220
215
|
}
|
|
221
216
|
|
|
222
217
|
(async () => {
|
|
223
|
-
if (command === '
|
|
218
|
+
if (command === 'create') {
|
|
219
|
+
const projectName = process.argv[3] || 'my-jux-app';
|
|
220
|
+
const projectPath = path.join(PATHS.projectRoot, projectName);
|
|
221
|
+
|
|
222
|
+
console.log(`
|
|
223
|
+
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
224
|
+
ā ā
|
|
225
|
+
ā šØ Welcome to JUX ā
|
|
226
|
+
ā ā
|
|
227
|
+
ā Creating your new JUX project... ā
|
|
228
|
+
ā ā
|
|
229
|
+
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
230
|
+
`);
|
|
231
|
+
|
|
232
|
+
if (fs.existsSync(projectPath)) {
|
|
233
|
+
console.error(`ā Directory "${projectName}" already exists.`);
|
|
234
|
+
console.error(` Please choose a different name or remove the existing directory.\n`);
|
|
235
|
+
process.exit(1);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
try {
|
|
239
|
+
const { execSync } = await import('child_process');
|
|
240
|
+
|
|
241
|
+
console.log(`š Creating directory: ${projectName}`);
|
|
242
|
+
fs.mkdirSync(projectPath, { recursive: true });
|
|
243
|
+
process.chdir(projectPath);
|
|
244
|
+
|
|
245
|
+
console.log(`š¦ Initializing package.json...`);
|
|
246
|
+
const packageJson = {
|
|
247
|
+
name: projectName,
|
|
248
|
+
version: '0.1.0',
|
|
249
|
+
type: 'module',
|
|
250
|
+
scripts: {
|
|
251
|
+
dev: 'jux serve',
|
|
252
|
+
build: 'jux build'
|
|
253
|
+
},
|
|
254
|
+
dependencies: {
|
|
255
|
+
juxscript: '^1.0.8'
|
|
256
|
+
}
|
|
257
|
+
};
|
|
258
|
+
fs.writeFileSync('package.json', JSON.stringify(packageJson, null, 2));
|
|
259
|
+
console.log(` ā package.json created`);
|
|
260
|
+
|
|
261
|
+
console.log(`\nš„ Installing juxscript...\n`);
|
|
262
|
+
try {
|
|
263
|
+
execSync('npm install', { stdio: 'inherit' });
|
|
264
|
+
console.log(`\n ā Dependencies installed`);
|
|
265
|
+
} catch (err) {
|
|
266
|
+
console.error(`\n ā ļø npm install failed, but continuing...`);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
console.log(`\nšØ Initializing JUX project structure...`);
|
|
270
|
+
execSync('npx jux init', { stdio: 'inherit' });
|
|
271
|
+
|
|
272
|
+
console.log(`\nš Creating .gitignore...`);
|
|
273
|
+
fs.writeFileSync('.gitignore', `jux-dist/\nnode_modules/\n.DS_Store\n.env\n*.log\n`);
|
|
274
|
+
console.log(` ā .gitignore created`);
|
|
275
|
+
|
|
276
|
+
console.log(`
|
|
277
|
+
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
278
|
+
ā ā
|
|
279
|
+
ā ā
Project created successfully! ā
|
|
280
|
+
ā ā
|
|
281
|
+
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
282
|
+
|
|
283
|
+
š Your project is ready at: ./${projectName}
|
|
284
|
+
|
|
285
|
+
š Next steps:
|
|
286
|
+
cd ${projectName}
|
|
287
|
+
npm run dev
|
|
288
|
+
|
|
289
|
+
š Resources:
|
|
290
|
+
Documentation: [coming soon]
|
|
291
|
+
GitHub: https://github.com/juxscript/jux
|
|
292
|
+
Examples: https://github.com/juxscript/examples
|
|
293
|
+
|
|
294
|
+
ā If you find JUX useful, please star us on GitHub!
|
|
295
|
+
|
|
296
|
+
š Security: Report issues to security@juxscript.com [placeholder]
|
|
297
|
+
|
|
298
|
+
Happy coding! š
|
|
299
|
+
`);
|
|
300
|
+
|
|
301
|
+
} catch (err) {
|
|
302
|
+
console.error(`\nā Project creation failed:`, err.message);
|
|
303
|
+
|
|
304
|
+
if (fs.existsSync(projectPath)) {
|
|
305
|
+
try {
|
|
306
|
+
process.chdir('..');
|
|
307
|
+
fs.rmSync(projectPath, { recursive: true, force: true });
|
|
308
|
+
console.log(` ā Cleaned up failed project directory\n`);
|
|
309
|
+
} catch (cleanupErr) {
|
|
310
|
+
console.error(` ā ļø Could not clean up directory\n`);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
process.exit(1);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
} else if (command === 'init') {
|
|
224
318
|
console.log('šØ Initializing JUX project...\n');
|
|
225
319
|
|
|
226
320
|
const juxDir = PATHS.juxSource;
|
|
@@ -230,10 +324,8 @@ async function buildProject(isServe = false) {
|
|
|
230
324
|
process.exit(1);
|
|
231
325
|
}
|
|
232
326
|
|
|
233
|
-
// Create structure
|
|
234
327
|
fs.mkdirSync(juxDir, { recursive: true });
|
|
235
328
|
|
|
236
|
-
// Copy jux.jux as the starter index.jux (if it exists)
|
|
237
329
|
const juxJuxSrc = path.join(PATHS.packageRoot, 'presets', 'jux.jux');
|
|
238
330
|
const indexJuxDest = path.join(juxDir, 'index.jux');
|
|
239
331
|
|
|
@@ -241,7 +333,6 @@ async function buildProject(isServe = false) {
|
|
|
241
333
|
fs.copyFileSync(juxJuxSrc, indexJuxDest);
|
|
242
334
|
console.log('+ Created jux/index.jux from jux.jux template');
|
|
243
335
|
} else {
|
|
244
|
-
// Fallback to hey.jux if jux.jux doesn't exist
|
|
245
336
|
const heyJuxSrc = path.join(PATHS.packageRoot, 'presets', 'hey.jux');
|
|
246
337
|
if (fs.existsSync(heyJuxSrc)) {
|
|
247
338
|
fs.copyFileSync(heyJuxSrc, indexJuxDest);
|
|
@@ -254,7 +345,6 @@ async function buildProject(isServe = false) {
|
|
|
254
345
|
}
|
|
255
346
|
}
|
|
256
347
|
|
|
257
|
-
// Copy entire presets folder to jux/presets/ (excluding jux.jux)
|
|
258
348
|
const presetsSrc = path.join(PATHS.packageRoot, 'presets');
|
|
259
349
|
const presetsDest = path.join(juxDir, 'presets');
|
|
260
350
|
|
|
@@ -268,7 +358,6 @@ async function buildProject(isServe = false) {
|
|
|
268
358
|
const srcPath = path.join(src, entry.name);
|
|
269
359
|
const destPath = path.join(dest, entry.name);
|
|
270
360
|
|
|
271
|
-
// Skip jux.jux since we already copied it to index.jux
|
|
272
361
|
if (entry.isFile() && entry.name === 'jux.jux') {
|
|
273
362
|
continue;
|
|
274
363
|
}
|
|
@@ -293,37 +382,51 @@ async function buildProject(isServe = false) {
|
|
|
293
382
|
}
|
|
294
383
|
}
|
|
295
384
|
|
|
296
|
-
// Create package.json if it doesn't exist
|
|
297
385
|
const pkgPath = path.join(PATHS.projectRoot, 'package.json');
|
|
298
386
|
if (!fs.existsSync(pkgPath)) {
|
|
387
|
+
// ā
Get project name from current directory or default
|
|
388
|
+
const projectName = path.basename(PATHS.projectRoot).toLowerCase().replace(/[^a-z0-9-]/g, '-');
|
|
389
|
+
|
|
299
390
|
const pkgContent = {
|
|
300
|
-
"name":
|
|
301
|
-
"version": "1.0
|
|
391
|
+
"name": projectName,
|
|
392
|
+
"version": "0.1.0",
|
|
302
393
|
"type": "module",
|
|
303
394
|
"scripts": {
|
|
304
|
-
"
|
|
305
|
-
"
|
|
395
|
+
"dev": "jux serve",
|
|
396
|
+
"build": "jux build"
|
|
306
397
|
},
|
|
307
398
|
"dependencies": {
|
|
308
|
-
"juxscript": "
|
|
399
|
+
"juxscript": "latest" // ā
Always use latest stable
|
|
309
400
|
}
|
|
310
401
|
};
|
|
311
402
|
fs.writeFileSync(pkgPath, JSON.stringify(pkgContent, null, 2));
|
|
312
403
|
console.log('+ Created package.json');
|
|
313
404
|
}
|
|
314
405
|
|
|
315
|
-
// Create .gitignore
|
|
316
406
|
const gitignorePath = path.join(PATHS.projectRoot, '.gitignore');
|
|
317
|
-
const gitignoreContent = `jux-dist
|
|
318
|
-
node_modules/
|
|
319
|
-
.DS_Store
|
|
320
|
-
`;
|
|
407
|
+
const gitignoreContent = `jux-dist/\nnode_modules/\n.DS_Store\n`;
|
|
321
408
|
|
|
322
409
|
if (!fs.existsSync(gitignorePath)) {
|
|
323
410
|
fs.writeFileSync(gitignorePath, gitignoreContent);
|
|
324
411
|
console.log('+ Created .gitignore');
|
|
325
412
|
}
|
|
326
413
|
|
|
414
|
+
// ā
Create actual juxconfig.js (not just example)
|
|
415
|
+
const configSrc = path.join(PATHS.packageRoot, 'juxconfig.example.js');
|
|
416
|
+
const configDest = path.join(PATHS.projectRoot, 'juxconfig.js');
|
|
417
|
+
|
|
418
|
+
if (fs.existsSync(configSrc) && !fs.existsSync(configDest)) {
|
|
419
|
+
fs.copyFileSync(configSrc, configDest);
|
|
420
|
+
console.log('+ Created juxconfig.js (customize as needed)');
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// Also copy example as reference
|
|
424
|
+
const configExampleDest = path.join(PATHS.projectRoot, 'juxconfig.example.js');
|
|
425
|
+
if (fs.existsSync(configSrc) && !fs.existsSync(configExampleDest)) {
|
|
426
|
+
fs.copyFileSync(configSrc, configExampleDest);
|
|
427
|
+
console.log('+ Created juxconfig.example.js (reference)');
|
|
428
|
+
}
|
|
429
|
+
|
|
327
430
|
console.log('\nā
JUX project initialized!\n');
|
|
328
431
|
console.log('Next steps:');
|
|
329
432
|
console.log(' npm install # Install dependencies');
|
|
@@ -331,18 +434,15 @@ node_modules/
|
|
|
331
434
|
console.log('Check out the docs: https://juxscript.com/docs\n');
|
|
332
435
|
|
|
333
436
|
} else if (command === 'build') {
|
|
334
|
-
// ā
Always builds router bundle
|
|
335
437
|
await buildProject(false);
|
|
336
438
|
console.log(`ā
Build complete: ${PATHS.frontendDist}`);
|
|
337
439
|
|
|
338
440
|
} else if (command === 'serve') {
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
// Parse port arguments: npx jux serve [httpPort] [wsPort]
|
|
343
|
-
const httpPort = parseInt(process.argv[3]) || 3000;
|
|
344
|
-
const wsPort = parseInt(process.argv[4]) || 3001;
|
|
441
|
+
const httpPort = parseInt(process.argv[3]) || config.ports.http;
|
|
442
|
+
const wsPort = parseInt(process.argv[4]) || config.ports.ws;
|
|
345
443
|
|
|
444
|
+
await runBootstrap(config.bootstrap);
|
|
445
|
+
await buildProject(true, wsPort);
|
|
346
446
|
await start(httpPort, wsPort);
|
|
347
447
|
|
|
348
448
|
} else {
|
|
@@ -350,39 +450,15 @@ node_modules/
|
|
|
350
450
|
JUX CLI - A JavaScript UX authorship platform
|
|
351
451
|
|
|
352
452
|
Usage:
|
|
353
|
-
npx jux
|
|
453
|
+
npx jux create [name] Create a new JUX project
|
|
454
|
+
npx jux init Initialize JUX in current directory
|
|
354
455
|
npx jux build Build router bundle to ./jux-dist/
|
|
355
456
|
npx jux serve [http] [ws] Start dev server with hot reload
|
|
356
457
|
|
|
357
|
-
Arguments:
|
|
358
|
-
[http] HTTP server port (default: 3000)
|
|
359
|
-
[ws] WebSocket port (default: 3001)
|
|
360
|
-
|
|
361
|
-
Project Structure:
|
|
362
|
-
my-project/
|
|
363
|
-
āāā jux/ # Your .jux source files
|
|
364
|
-
ā āāā index.jux # Entry point
|
|
365
|
-
ā āāā pages/ # Additional pages
|
|
366
|
-
āāā jux-dist/ # Build output (git-ignore this)
|
|
367
|
-
āāā server/ # Your backend
|
|
368
|
-
āāā package.json
|
|
369
|
-
|
|
370
|
-
Import Style:
|
|
371
|
-
// In your project's .jux files
|
|
372
|
-
import { jux, state } from 'juxscript';
|
|
373
|
-
import 'juxscript/presets/notion.js';
|
|
374
|
-
|
|
375
|
-
Getting Started:
|
|
376
|
-
1. npx jux init # Create project structure
|
|
377
|
-
2. npm install # Install dependencies
|
|
378
|
-
3. npx jux serve # Dev server with hot reload
|
|
379
|
-
4. Serve jux-dist/ from your backend
|
|
380
|
-
|
|
381
458
|
Examples:
|
|
382
|
-
npx jux
|
|
383
|
-
npx jux serve
|
|
384
|
-
npx jux serve 8080
|
|
385
|
-
npx jux serve 8080 8081 # HTTP on 8080, WS on 8081
|
|
459
|
+
npx jux create my-app Create new project
|
|
460
|
+
npx jux serve Dev server (ports 3000/3001)
|
|
461
|
+
npx jux serve 8080 8081 Custom ports
|
|
386
462
|
`);
|
|
387
463
|
}
|
|
388
464
|
})();
|
package/machinery/compiler.js
CHANGED
|
@@ -673,16 +673,12 @@ render();
|
|
|
673
673
|
*
|
|
674
674
|
* @param {string} distDir - Destination directory
|
|
675
675
|
* @param {Array<{path: string, functionName: string}>} routes - Route definitions
|
|
676
|
-
* @param {string} mainJsFilename - The generated main.js filename
|
|
676
|
+
* @param {string} mainJsFilename - The generated main.js filename
|
|
677
|
+
* @param {number} wsPort - WebSocket port for hot reload
|
|
677
678
|
*/
|
|
678
|
-
export function generateIndexHtml(distDir, routes, mainJsFilename = 'main.js') {
|
|
679
|
+
export function generateIndexHtml(distDir, routes, mainJsFilename = 'main.js', wsPort = 3001) {
|
|
679
680
|
console.log('š Generating index.html...');
|
|
680
681
|
|
|
681
|
-
// Generate navigation links
|
|
682
|
-
const navLinks = routes
|
|
683
|
-
.map(r => ` <a href="${r.path}">${r.functionName.replace(/_/g, ' ')}</a>`)
|
|
684
|
-
.join(' |\n');
|
|
685
|
-
|
|
686
682
|
const importMapScript = generateImportMapScript();
|
|
687
683
|
|
|
688
684
|
const html = `<!DOCTYPE html>
|
|
@@ -697,6 +693,62 @@ export function generateIndexHtml(distDir, routes, mainJsFilename = 'main.js') {
|
|
|
697
693
|
<div id="app"></div>
|
|
698
694
|
${importMapScript}
|
|
699
695
|
<script type="module" src="/${mainJsFilename}"></script>
|
|
696
|
+
|
|
697
|
+
<!-- Hot Reload Script -->
|
|
698
|
+
<script>
|
|
699
|
+
(function() {
|
|
700
|
+
const ws = new WebSocket('ws://' + location.hostname + ':${wsPort}');
|
|
701
|
+
|
|
702
|
+
ws.onopen = () => {
|
|
703
|
+
console.log('š Hot reload connected');
|
|
704
|
+
};
|
|
705
|
+
|
|
706
|
+
ws.onmessage = (event) => {
|
|
707
|
+
const data = JSON.parse(event.data);
|
|
708
|
+
|
|
709
|
+
if (data.type === 'reload') {
|
|
710
|
+
console.log('š Hot reload triggered - reloading page...');
|
|
711
|
+
location.reload();
|
|
712
|
+
} else if (data.type === 'css-reload') {
|
|
713
|
+
console.log('šØ CSS hot reload:', data.path);
|
|
714
|
+
|
|
715
|
+
// Find all link tags pointing to this CSS file
|
|
716
|
+
const links = document.querySelectorAll('link[rel="stylesheet"]');
|
|
717
|
+
links.forEach(link => {
|
|
718
|
+
if (link.href.includes(data.path)) {
|
|
719
|
+
const newLink = link.cloneNode();
|
|
720
|
+
newLink.href = data.path + '?t=' + Date.now();
|
|
721
|
+
link.parentNode.insertBefore(newLink, link.nextSibling);
|
|
722
|
+
setTimeout(() => link.remove(), 100);
|
|
723
|
+
}
|
|
724
|
+
});
|
|
725
|
+
|
|
726
|
+
// Also reload any @import in style tags
|
|
727
|
+
const styles = document.querySelectorAll('style');
|
|
728
|
+
styles.forEach(style => {
|
|
729
|
+
if (style.textContent.includes(data.path)) {
|
|
730
|
+
style.textContent = style.textContent.replace(
|
|
731
|
+
new RegExp(data.path + '(\\\\?t=\\\\d+)?', 'g'),
|
|
732
|
+
data.path + '?t=' + Date.now()
|
|
733
|
+
);
|
|
734
|
+
}
|
|
735
|
+
});
|
|
736
|
+
}
|
|
737
|
+
};
|
|
738
|
+
|
|
739
|
+
ws.onclose = () => {
|
|
740
|
+
console.log('š Hot reload disconnected');
|
|
741
|
+
// Try to reconnect after 1 second
|
|
742
|
+
setTimeout(() => {
|
|
743
|
+
location.reload();
|
|
744
|
+
}, 1000);
|
|
745
|
+
};
|
|
746
|
+
|
|
747
|
+
ws.onerror = (error) => {
|
|
748
|
+
console.error('š Hot reload error:', error);
|
|
749
|
+
};
|
|
750
|
+
})();
|
|
751
|
+
</script>
|
|
700
752
|
</body>
|
|
701
753
|
</html>`;
|
|
702
754
|
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Default JUX configuration
|
|
6
|
+
*/
|
|
7
|
+
export const defaultConfig = {
|
|
8
|
+
// Source directory for .jux files
|
|
9
|
+
sourceDir: 'jux',
|
|
10
|
+
|
|
11
|
+
// Output directory for built files
|
|
12
|
+
distDir: 'jux-dist',
|
|
13
|
+
|
|
14
|
+
// Dev server ports
|
|
15
|
+
ports: {
|
|
16
|
+
http: 3000,
|
|
17
|
+
ws: 3001
|
|
18
|
+
},
|
|
19
|
+
|
|
20
|
+
// Build options
|
|
21
|
+
build: {
|
|
22
|
+
minify: false,
|
|
23
|
+
sourcemap: true
|
|
24
|
+
},
|
|
25
|
+
|
|
26
|
+
// Bootstrap functions (run before app starts)
|
|
27
|
+
bootstrap: []
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Load juxconfig.js from project root
|
|
32
|
+
* @param {string} projectRoot - Project root directory
|
|
33
|
+
* @returns {object} Merged configuration
|
|
34
|
+
*/
|
|
35
|
+
export async function loadConfig(projectRoot) {
|
|
36
|
+
const configPath = path.join(projectRoot, 'juxconfig.js');
|
|
37
|
+
|
|
38
|
+
if (!fs.existsSync(configPath)) {
|
|
39
|
+
console.log('ā¹ļø No juxconfig.js found, using defaults');
|
|
40
|
+
return defaultConfig;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
console.log(`š Loading config from: ${configPath}`);
|
|
45
|
+
|
|
46
|
+
// Dynamic import for ES modules
|
|
47
|
+
const configUrl = `file://${configPath}`;
|
|
48
|
+
const { default: userConfig } = await import(configUrl);
|
|
49
|
+
|
|
50
|
+
// Merge with defaults
|
|
51
|
+
const config = {
|
|
52
|
+
...defaultConfig,
|
|
53
|
+
...userConfig,
|
|
54
|
+
ports: {
|
|
55
|
+
...defaultConfig.ports,
|
|
56
|
+
...(userConfig.ports || {})
|
|
57
|
+
},
|
|
58
|
+
build: {
|
|
59
|
+
...defaultConfig.build,
|
|
60
|
+
...(userConfig.build || {})
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
console.log(` ā Config loaded`);
|
|
65
|
+
console.log(` Source: ${config.sourceDir}`);
|
|
66
|
+
console.log(` Output: ${config.distDir}\n`);
|
|
67
|
+
|
|
68
|
+
return config;
|
|
69
|
+
} catch (err) {
|
|
70
|
+
console.warn(`ā ļø Failed to load juxconfig.js:`, err.message);
|
|
71
|
+
console.warn(` Using default configuration\n`);
|
|
72
|
+
return defaultConfig;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Run bootstrap functions from config
|
|
78
|
+
* @param {Array<Function>} bootstrapFns - Array of bootstrap functions
|
|
79
|
+
*/
|
|
80
|
+
export async function runBootstrap(bootstrapFns) {
|
|
81
|
+
if (!bootstrapFns || bootstrapFns.length === 0) return;
|
|
82
|
+
|
|
83
|
+
console.log(`š Running ${bootstrapFns.length} bootstrap function(s)...`);
|
|
84
|
+
|
|
85
|
+
for (const fn of bootstrapFns) {
|
|
86
|
+
try {
|
|
87
|
+
await fn();
|
|
88
|
+
} catch (err) {
|
|
89
|
+
console.error(` ā Bootstrap function failed:`, err.message);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
console.log(` ā Bootstrap complete\n`);
|
|
94
|
+
}
|
package/machinery/server.js
CHANGED
|
@@ -149,4 +149,66 @@ async function serve(httpPort = 3000, wsPort = 3001, distDir = './jux-dist') {
|
|
|
149
149
|
|
|
150
150
|
export async function start(httpPort = 3000, wsPort = 3001) {
|
|
151
151
|
return serve(httpPort, wsPort, './jux-dist');
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function generateIndexHtml(distDir, routes, mainJsFilename = 'main.js') {
|
|
155
|
+
const html = `<!DOCTYPE html>
|
|
156
|
+
<html lang="en">
|
|
157
|
+
<head>
|
|
158
|
+
<meta charset="UTF-8">
|
|
159
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
160
|
+
<title>Jux Application</title>
|
|
161
|
+
</head>
|
|
162
|
+
<body data-theme="">
|
|
163
|
+
<div id="app"></div>
|
|
164
|
+
${importMapScript}
|
|
165
|
+
<script type="module" src="/${mainJsFilename}"></script>
|
|
166
|
+
|
|
167
|
+
<!-- Hot Reload Script -->
|
|
168
|
+
<script>
|
|
169
|
+
(function() {
|
|
170
|
+
const ws = new WebSocket('ws://' + location.hostname + ':${wsPort}');
|
|
171
|
+
|
|
172
|
+
ws.onmessage = (event) => {
|
|
173
|
+
const data = JSON.parse(event.data);
|
|
174
|
+
|
|
175
|
+
if (data.type === 'reload') {
|
|
176
|
+
console.log('š Hot reload triggered');
|
|
177
|
+
location.reload();
|
|
178
|
+
} else if (data.type === 'css-reload') {
|
|
179
|
+
console.log('šØ CSS hot reload:', data.path);
|
|
180
|
+
|
|
181
|
+
// Find all link tags pointing to this CSS file
|
|
182
|
+
const links = document.querySelectorAll('link[rel="stylesheet"]');
|
|
183
|
+
links.forEach(link => {
|
|
184
|
+
if (link.href.includes(data.path)) {
|
|
185
|
+
const newLink = link.cloneNode();
|
|
186
|
+
newLink.href = data.path + '?t=' + Date.now();
|
|
187
|
+
link.parentNode.insertBefore(newLink, link.nextSibling);
|
|
188
|
+
setTimeout(() => link.remove(), 100);
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
// Also reload any @import in style tags
|
|
193
|
+
const styles = document.querySelectorAll('style');
|
|
194
|
+
styles.forEach(style => {
|
|
195
|
+
if (style.textContent.includes(data.path)) {
|
|
196
|
+
style.textContent = style.textContent.replace(
|
|
197
|
+
new RegExp(data.path + '(\\?t=\\d+)?', 'g'),
|
|
198
|
+
data.path + '?t=' + Date.now()
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
ws.onclose = () => {
|
|
206
|
+
console.log('š Hot reload disconnected');
|
|
207
|
+
};
|
|
208
|
+
})();
|
|
209
|
+
</script>
|
|
210
|
+
</body>
|
|
211
|
+
</html>`;
|
|
212
|
+
|
|
213
|
+
return html;
|
|
152
214
|
}
|
package/machinery/watcher.js
CHANGED
|
@@ -1,10 +1,6 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
2
|
import path from 'path';
|
|
3
|
-
import {
|
|
4
|
-
copyLibToOutput,
|
|
5
|
-
bundleJuxFilesToRouter,
|
|
6
|
-
generateIndexHtml
|
|
7
|
-
} from './compiler.js';
|
|
3
|
+
import { bundleJuxFilesToRouter } from './compiler.js';
|
|
8
4
|
|
|
9
5
|
let isRebuilding = false;
|
|
10
6
|
let rebuildQueued = false;
|
|
@@ -119,53 +115,130 @@ async function fullRebuild(juxSource, distDir) {
|
|
|
119
115
|
* Start watching for file changes and rebuild on change
|
|
120
116
|
*/
|
|
121
117
|
export function startWatcher(juxSource, distDir, wsClients) {
|
|
122
|
-
console.log(`š Watching: ${juxSource}`);
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
if (!filename ||
|
|
127
|
-
!filename.endsWith('.jux') ||
|
|
128
|
-
filename.includes('node_modules') ||
|
|
129
|
-
filename.includes('jux-dist') ||
|
|
130
|
-
filename.startsWith('.')) {
|
|
131
|
-
return;
|
|
132
|
-
}
|
|
118
|
+
console.log(`š Watching for changes in: ${juxSource}`);
|
|
119
|
+
|
|
120
|
+
// Debounce map to prevent multiple rapid triggers
|
|
121
|
+
const debounceTimers = new Map();
|
|
133
122
|
|
|
134
|
-
|
|
135
|
-
if (
|
|
136
|
-
|
|
137
|
-
return;
|
|
123
|
+
function debounce(key, fn, delay = 100) {
|
|
124
|
+
if (debounceTimers.has(key)) {
|
|
125
|
+
clearTimeout(debounceTimers.get(key));
|
|
138
126
|
}
|
|
127
|
+
debounceTimers.set(key, setTimeout(() => {
|
|
128
|
+
debounceTimers.delete(key);
|
|
129
|
+
fn();
|
|
130
|
+
}, delay));
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Recursively watch directories
|
|
134
|
+
function watchRecursive(dir) {
|
|
135
|
+
if (!fs.existsSync(dir)) return;
|
|
136
|
+
|
|
137
|
+
try {
|
|
138
|
+
fs.watch(dir, { recursive: true }, (eventType, filename) => {
|
|
139
|
+
if (!filename) return;
|
|
140
|
+
|
|
141
|
+
const filePath = path.join(dir, filename);
|
|
142
|
+
const ext = path.extname(filename);
|
|
143
|
+
|
|
144
|
+
// Skip certain patterns
|
|
145
|
+
if (filename.includes('node_modules') ||
|
|
146
|
+
filename.includes('jux-dist') ||
|
|
147
|
+
filename.includes('.git') ||
|
|
148
|
+
filename.startsWith('.')) {
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Debounce to avoid multiple rapid events
|
|
153
|
+
debounce(filePath, async () => {
|
|
154
|
+
// ā
Handle CSS files
|
|
155
|
+
if (ext === '.css') {
|
|
156
|
+
console.log(`\nšØ CSS changed: ${filename}`);
|
|
157
|
+
|
|
158
|
+
try {
|
|
159
|
+
const relativePath = path.relative(juxSource, filePath);
|
|
160
|
+
const destPath = path.join(distDir, relativePath);
|
|
161
|
+
const destDir = path.dirname(destPath);
|
|
162
|
+
|
|
163
|
+
if (!fs.existsSync(destDir)) {
|
|
164
|
+
fs.mkdirSync(destDir, { recursive: true });
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
fs.copyFileSync(filePath, destPath);
|
|
168
|
+
console.log(` ā Copied to: ${path.relative(process.cwd(), destPath)}`);
|
|
169
|
+
|
|
170
|
+
// Notify browser to reload CSS
|
|
171
|
+
wsClients.forEach(client => {
|
|
172
|
+
if (client.readyState === 1) {
|
|
173
|
+
client.send(JSON.stringify({
|
|
174
|
+
type: 'css-reload',
|
|
175
|
+
path: `/${relativePath}`
|
|
176
|
+
}));
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
console.log(` š Browser CSS reloaded\n`);
|
|
181
|
+
} catch (err) {
|
|
182
|
+
console.error(` ā CSS copy failed:`, err.message);
|
|
183
|
+
}
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// ā
Handle .jux files
|
|
188
|
+
if (ext === '.jux') {
|
|
189
|
+
console.log(`\nš File changed: ${filename}`);
|
|
190
|
+
console.log(' šØ Rebuilding...');
|
|
191
|
+
|
|
192
|
+
try {
|
|
193
|
+
await bundleJuxFilesToRouter(juxSource, distDir, { routePrefix: '' });
|
|
194
|
+
|
|
195
|
+
wsClients.forEach(client => {
|
|
196
|
+
if (client.readyState === 1) {
|
|
197
|
+
client.send(JSON.stringify({ type: 'reload' }));
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
console.log(' ā
Rebuild complete');
|
|
202
|
+
console.log(' š Browser reloaded\n');
|
|
203
|
+
} catch (err) {
|
|
204
|
+
console.error(' ā Rebuild failed:', err.message);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// ā
Handle .js files (non-_dev-imports.js)
|
|
209
|
+
if (ext === '.js' && !filename.includes('_dev-imports.js')) {
|
|
210
|
+
console.log(`\nš¦ JS asset changed: ${filename}`);
|
|
139
211
|
|
|
140
|
-
|
|
141
|
-
|
|
212
|
+
try {
|
|
213
|
+
const relativePath = path.relative(juxSource, filePath);
|
|
214
|
+
const destPath = path.join(distDir, relativePath);
|
|
215
|
+
const destDir = path.dirname(destPath);
|
|
142
216
|
|
|
143
|
-
|
|
144
|
-
|
|
217
|
+
if (!fs.existsSync(destDir)) {
|
|
218
|
+
fs.mkdirSync(destDir, { recursive: true });
|
|
219
|
+
}
|
|
145
220
|
|
|
146
|
-
|
|
221
|
+
fs.copyFileSync(filePath, destPath);
|
|
222
|
+
console.log(` ā Copied to: ${path.relative(process.cwd(), destPath)}`);
|
|
147
223
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
224
|
+
wsClients.forEach(client => {
|
|
225
|
+
if (client.readyState === 1) {
|
|
226
|
+
client.send(JSON.stringify({ type: 'reload' }));
|
|
227
|
+
}
|
|
228
|
+
});
|
|
151
229
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
client.send(JSON.stringify({ type: 'reload' }));
|
|
230
|
+
console.log(` š Browser reloaded\n`);
|
|
231
|
+
} catch (err) {
|
|
232
|
+
console.error(` ā JS copy failed:`, err.message);
|
|
233
|
+
}
|
|
157
234
|
}
|
|
158
235
|
});
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
// Process queued rebuild if needed
|
|
163
|
-
if (rebuildQueued) {
|
|
164
|
-
rebuildQueued = false;
|
|
165
|
-
console.log('š Processing queued rebuild...');
|
|
166
|
-
setTimeout(() => watcher.emit('change', 'change', filename), 500);
|
|
236
|
+
});
|
|
237
|
+
} catch (err) {
|
|
238
|
+
console.error(` ā ļø Failed to watch ${dir}:`, err.message);
|
|
167
239
|
}
|
|
168
|
-
}
|
|
240
|
+
}
|
|
169
241
|
|
|
170
|
-
|
|
242
|
+
// Start watching
|
|
243
|
+
watchRecursive(juxSource);
|
|
171
244
|
}
|
package/package.json
CHANGED
|
@@ -105,6 +105,15 @@ body {
|
|
|
105
105
|
cursor: pointer;
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
+
/* ā
Fix h1 inside logo - reset default heading styles */
|
|
109
|
+
#appheader-logo h1 {
|
|
110
|
+
font-size: 1.25rem;
|
|
111
|
+
font-weight: 700;
|
|
112
|
+
margin: 0;
|
|
113
|
+
line-height: 1;
|
|
114
|
+
letter-spacing: -0.02em;
|
|
115
|
+
}
|
|
116
|
+
|
|
108
117
|
#appheader-nav {
|
|
109
118
|
flex: 1;
|
|
110
119
|
display: flex;
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import { jux } from 'juxscript';
|
|
2
2
|
|
|
3
|
-
// Import the layout styles
|
|
3
|
+
// Import the layout styles - testing link.
|
|
4
4
|
jux.include('../presets/default/layout.css');
|
|
5
5
|
|
|
6
|
-
|
|
7
6
|
// āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
8
7
|
// GRID LAYOUT - INITIALIZATION FUNCTION
|
|
9
8
|
// āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|