juxscript 1.0.23 → 1.0.25
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 +44 -44
- package/lib/components/docs-data.json +1 -1
- package/machinery/compiler.js +17 -2
- package/machinery/server.js +61 -51
- package/package.json +1 -1
- package/presets/hey.jux +34 -20
package/bin/cli.js
CHANGED
|
@@ -49,7 +49,6 @@ console.log(` Output: ${PATHS.frontendDist}`);
|
|
|
49
49
|
console.log(` Lib: ${PATHS.juxLib}\n`);
|
|
50
50
|
|
|
51
51
|
const command = process.argv[2];
|
|
52
|
-
const subCommand = process.argv[3]; // For serve <pagename>
|
|
53
52
|
const watchMode = process.argv.includes('--watch');
|
|
54
53
|
const bundleMode = process.argv.includes('--bundle');
|
|
55
54
|
|
|
@@ -234,28 +233,31 @@ async function buildProject(isServe = false) {
|
|
|
234
233
|
// Create structure
|
|
235
234
|
fs.mkdirSync(juxDir, { recursive: true });
|
|
236
235
|
|
|
237
|
-
// Copy hey.jux
|
|
238
|
-
const
|
|
239
|
-
const
|
|
236
|
+
// Copy hey.jux as the starter template
|
|
237
|
+
const heyJuxSrc = path.join(PATHS.packageRoot, 'presets', 'hey.jux');
|
|
238
|
+
const heyJuxDest = path.join(juxDir, 'index.jux');
|
|
240
239
|
|
|
241
|
-
if (fs.existsSync(
|
|
242
|
-
fs.copyFileSync(
|
|
243
|
-
console.log('+ Created jux/hey.jux
|
|
240
|
+
if (fs.existsSync(heyJuxSrc)) {
|
|
241
|
+
fs.copyFileSync(heyJuxSrc, heyJuxDest);
|
|
242
|
+
console.log('+ Created jux/index.jux from hey.jux template');
|
|
244
243
|
} else {
|
|
245
|
-
console.warn('⚠️ hey.jux template not found
|
|
244
|
+
console.warn('⚠️ hey.jux template not found, creating basic file');
|
|
245
|
+
const basicContent = `import { jux } from 'juxscript';\n\njux.heading('welcome').text('Welcome to JUX').render('#app');`;
|
|
246
|
+
fs.writeFileSync(heyJuxDest, basicContent);
|
|
247
|
+
console.log('+ Created jux/index.jux');
|
|
246
248
|
}
|
|
247
249
|
|
|
248
|
-
// Copy
|
|
249
|
-
const
|
|
250
|
+
// Copy preset styles from presets/styles/ to jux/styles/
|
|
251
|
+
const presetsStylesSrc = path.join(PATHS.packageRoot, 'presets', 'styles');
|
|
250
252
|
const stylesDest = path.join(juxDir, 'styles');
|
|
251
253
|
|
|
252
|
-
if (fs.existsSync(
|
|
254
|
+
if (fs.existsSync(presetsStylesSrc)) {
|
|
253
255
|
fs.mkdirSync(stylesDest, { recursive: true });
|
|
254
256
|
|
|
255
|
-
const
|
|
256
|
-
|
|
257
|
+
const styleFiles = fs.readdirSync(presetsStylesSrc);
|
|
258
|
+
styleFiles.forEach(file => {
|
|
257
259
|
if (file.endsWith('.css')) {
|
|
258
|
-
const srcFile = path.join(
|
|
260
|
+
const srcFile = path.join(presetsStylesSrc, file);
|
|
259
261
|
const destFile = path.join(stylesDest, file);
|
|
260
262
|
fs.copyFileSync(srcFile, destFile);
|
|
261
263
|
console.log(`+ Copied preset style: styles/${file}`);
|
|
@@ -279,7 +281,7 @@ async function buildProject(isServe = false) {
|
|
|
279
281
|
}
|
|
280
282
|
};
|
|
281
283
|
fs.writeFileSync(pkgPath, JSON.stringify(pkgContent, null, 2));
|
|
282
|
-
console.log('
|
|
284
|
+
console.log('+ Created package.json');
|
|
283
285
|
}
|
|
284
286
|
|
|
285
287
|
// Create .gitignore
|
|
@@ -294,9 +296,10 @@ node_modules/
|
|
|
294
296
|
console.log('+ Created .gitignore');
|
|
295
297
|
}
|
|
296
298
|
|
|
297
|
-
console.log('
|
|
299
|
+
console.log('\n✅ JUX project initialized!\n');
|
|
298
300
|
console.log('Next steps:');
|
|
299
|
-
console.log('
|
|
301
|
+
console.log(' npm install # Install dependencies');
|
|
302
|
+
console.log(' npx jux serve # Start dev server with hot reload\n');
|
|
300
303
|
console.log('Check out the docs: https://juxscript.com/docs\n');
|
|
301
304
|
|
|
302
305
|
} else if (command === 'build') {
|
|
@@ -305,37 +308,35 @@ node_modules/
|
|
|
305
308
|
console.log(`✅ Build complete: ${PATHS.frontendDist}`);
|
|
306
309
|
|
|
307
310
|
} else if (command === 'serve') {
|
|
308
|
-
// ✅
|
|
309
|
-
const pageName = subCommand; // e.g., "hey" from "npx jux serve hey"
|
|
310
|
-
|
|
311
|
-
if (pageName) {
|
|
312
|
-
console.log(`🎯 Serving specific page: ${pageName}\n`);
|
|
313
|
-
}
|
|
314
|
-
|
|
311
|
+
// ✅ Always serves router bundle
|
|
315
312
|
await buildProject(true);
|
|
316
313
|
|
|
317
|
-
|
|
314
|
+
// Parse port arguments: npx jux serve [httpPort] [wsPort]
|
|
315
|
+
const httpPort = parseInt(process.argv[3]) || 3000;
|
|
316
|
+
const wsPort = parseInt(process.argv[4]) || 3001;
|
|
318
317
|
|
|
319
|
-
|
|
320
|
-
await start(port, pageName);
|
|
318
|
+
await start(httpPort, wsPort);
|
|
321
319
|
|
|
322
320
|
} else {
|
|
323
321
|
console.log(`
|
|
324
322
|
JUX CLI - A JavaScript UX authorship platform
|
|
325
323
|
|
|
326
324
|
Usage:
|
|
327
|
-
npx jux init
|
|
328
|
-
npx jux build
|
|
329
|
-
npx jux serve [
|
|
330
|
-
|
|
325
|
+
npx jux init Initialize a new JUX project
|
|
326
|
+
npx jux build Build router bundle to ./jux-dist/
|
|
327
|
+
npx jux serve [http] [ws] Start dev server with hot reload
|
|
328
|
+
|
|
329
|
+
Arguments:
|
|
330
|
+
[http] HTTP server port (default: 3000)
|
|
331
|
+
[ws] WebSocket port (default: 3001)
|
|
331
332
|
|
|
332
333
|
Project Structure:
|
|
333
334
|
my-project/
|
|
334
335
|
├── jux/ # Your .jux source files
|
|
335
|
-
│ ├──
|
|
336
|
-
│ └── pages/
|
|
337
|
-
├── jux-dist/
|
|
338
|
-
├── server/
|
|
336
|
+
│ ├── index.jux # Entry point
|
|
337
|
+
│ └── pages/ # Additional pages
|
|
338
|
+
├── jux-dist/ # Build output (git-ignore this)
|
|
339
|
+
├── server/ # Your backend
|
|
339
340
|
└── package.json
|
|
340
341
|
|
|
341
342
|
Import Style:
|
|
@@ -344,17 +345,16 @@ Import Style:
|
|
|
344
345
|
import 'juxscript/presets/notion.js';
|
|
345
346
|
|
|
346
347
|
Getting Started:
|
|
347
|
-
1. npx jux init
|
|
348
|
-
2. npm install
|
|
349
|
-
3. npx jux serve
|
|
350
|
-
4.
|
|
351
|
-
5. Serve jux-dist/ from your backend
|
|
348
|
+
1. npx jux init # Create project structure
|
|
349
|
+
2. npm install # Install dependencies
|
|
350
|
+
3. npx jux serve # Dev server with hot reload
|
|
351
|
+
4. Serve jux-dist/ from your backend
|
|
352
352
|
|
|
353
353
|
Examples:
|
|
354
|
-
npx jux build
|
|
355
|
-
npx jux serve
|
|
356
|
-
npx jux serve
|
|
357
|
-
npx jux serve 8080
|
|
354
|
+
npx jux build # Build production bundle
|
|
355
|
+
npx jux serve # Dev server (ports 3000/3001)
|
|
356
|
+
npx jux serve 8080 # HTTP on 8080, WS on 3001
|
|
357
|
+
npx jux serve 8080 8081 # HTTP on 8080, WS on 8081
|
|
358
358
|
`);
|
|
359
359
|
}
|
|
360
360
|
})();
|
package/machinery/compiler.js
CHANGED
|
@@ -580,8 +580,23 @@ ${routeTable}
|
|
|
580
580
|
const app = document.getElementById('app');
|
|
581
581
|
|
|
582
582
|
function render() {
|
|
583
|
-
|
|
584
|
-
|
|
583
|
+
let path = location.pathname;
|
|
584
|
+
|
|
585
|
+
// Try exact match first
|
|
586
|
+
let view = routes[path];
|
|
587
|
+
|
|
588
|
+
// If no match and path ends with /, try appending 'index'
|
|
589
|
+
if (!view && path.endsWith('/')) {
|
|
590
|
+
view = routes[path + 'index'] || routes[path.slice(0, -1) + '/index'];
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
// If still no match and path doesn't end with /, try appending '/index'
|
|
594
|
+
if (!view && !path.endsWith('/')) {
|
|
595
|
+
view = routes[path + '/index'];
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
// Fall back to 404
|
|
599
|
+
view = view || JuxNotFound;
|
|
585
600
|
|
|
586
601
|
app.innerHTML = '';
|
|
587
602
|
app.removeAttribute('data-jux-page');
|
package/machinery/server.js
CHANGED
|
@@ -9,7 +9,45 @@ import { WebSocketServer } from 'ws';
|
|
|
9
9
|
const __filename = fileURLToPath(import.meta.url);
|
|
10
10
|
const __dirname = path.dirname(__filename);
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
/**
|
|
13
|
+
* Try to start a server on a port, with fallback to next port if busy
|
|
14
|
+
* Returns the actual port that was successfully allocated
|
|
15
|
+
*/
|
|
16
|
+
async function tryPort(startPort, maxAttempts = 5, reservedPorts = []) {
|
|
17
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
18
|
+
const port = startPort + attempt;
|
|
19
|
+
|
|
20
|
+
// Skip if this port is already reserved
|
|
21
|
+
if (reservedPorts.includes(port)) {
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
try {
|
|
26
|
+
// Test if port is available
|
|
27
|
+
await new Promise((resolve, reject) => {
|
|
28
|
+
const testServer = http.createServer();
|
|
29
|
+
testServer.once('error', reject);
|
|
30
|
+
testServer.once('listening', () => {
|
|
31
|
+
testServer.close();
|
|
32
|
+
resolve(port);
|
|
33
|
+
});
|
|
34
|
+
testServer.listen(port);
|
|
35
|
+
});
|
|
36
|
+
return port;
|
|
37
|
+
} catch (err) {
|
|
38
|
+
if (err.code === 'EADDRINUSE') {
|
|
39
|
+
if (attempt < maxAttempts - 1) {
|
|
40
|
+
console.log(` Port ${port} in use, trying ${port + 1}...`);
|
|
41
|
+
}
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
throw err;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
throw new Error(`Could not find available port after ${maxAttempts} attempts starting from ${startPort}`);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function serve(httpPort = 3000, wsPort = 3001, distDir = './jux-dist') {
|
|
13
51
|
const app = express();
|
|
14
52
|
const absoluteDistDir = path.resolve(distDir);
|
|
15
53
|
const projectRoot = path.resolve('.');
|
|
@@ -22,50 +60,46 @@ async function serve(port = 3000, distDir = './jux-dist') {
|
|
|
22
60
|
process.exit(1);
|
|
23
61
|
}
|
|
24
62
|
|
|
25
|
-
//
|
|
63
|
+
// Strong cache prevention
|
|
26
64
|
app.use((req, res, next) => {
|
|
27
65
|
res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0');
|
|
28
66
|
res.setHeader('Pragma', 'no-cache');
|
|
29
67
|
res.setHeader('Expires', '0');
|
|
30
|
-
|
|
31
|
-
// ✅ Add ETag prevention
|
|
32
68
|
res.setHeader('ETag', 'W/"' + Date.now() + '"');
|
|
33
|
-
|
|
34
|
-
next();
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
// Disable caching in dev mode
|
|
38
|
-
app.use((req, res, next) => {
|
|
39
|
-
res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate');
|
|
40
|
-
res.setHeader('Pragma', 'no-cache');
|
|
41
|
-
res.setHeader('Expires', '0');
|
|
42
69
|
next();
|
|
43
70
|
});
|
|
44
71
|
|
|
45
|
-
// Serve static files
|
|
72
|
+
// Serve static files
|
|
46
73
|
app.use(express.static(absoluteDistDir));
|
|
47
74
|
|
|
48
|
-
// SPA fallback
|
|
75
|
+
// SPA fallback
|
|
49
76
|
app.get('*', (req, res) => {
|
|
50
|
-
// If request accepts HTML, serve index.html
|
|
51
77
|
if (req.accepts('html')) {
|
|
52
78
|
const indexPath = path.join(absoluteDistDir, 'index.html');
|
|
53
79
|
if (fs.existsSync(indexPath)) {
|
|
54
80
|
res.sendFile(indexPath);
|
|
55
81
|
} else {
|
|
56
|
-
res.status(404).send('index.html not found. Run `npx jux
|
|
82
|
+
res.status(404).send('index.html not found. Run `npx jux build` first.');
|
|
57
83
|
}
|
|
58
84
|
} else {
|
|
59
|
-
// For non-HTML requests (like .js, .css), return 404
|
|
60
85
|
res.status(404).send('Not found');
|
|
61
86
|
}
|
|
62
87
|
});
|
|
63
88
|
|
|
89
|
+
// Find available ports sequentially to avoid collision
|
|
90
|
+
console.log('🔍 Finding available ports...');
|
|
91
|
+
|
|
92
|
+
// First, find HTTP port
|
|
93
|
+
const availableHttpPort = await tryPort(httpPort);
|
|
94
|
+
|
|
95
|
+
// Then find WS port, excluding the HTTP port we just allocated
|
|
96
|
+
const availableWsPort = await tryPort(wsPort, 5, [availableHttpPort]);
|
|
97
|
+
|
|
64
98
|
// Create HTTP server
|
|
65
99
|
const server = http.createServer(app);
|
|
66
100
|
|
|
67
101
|
// WebSocket server for hot reload
|
|
68
|
-
const wss = new WebSocketServer({ port:
|
|
102
|
+
const wss = new WebSocketServer({ port: availableWsPort });
|
|
69
103
|
const clients = [];
|
|
70
104
|
|
|
71
105
|
wss.on('connection', (ws) => {
|
|
@@ -83,16 +117,13 @@ async function serve(port = 3000, distDir = './jux-dist') {
|
|
|
83
117
|
});
|
|
84
118
|
});
|
|
85
119
|
|
|
86
|
-
console.log(
|
|
120
|
+
console.log(`🔌 WebSocket server running at ws://localhost:${availableWsPort}`);
|
|
87
121
|
|
|
88
122
|
// Start HTTP server
|
|
89
|
-
server.listen(
|
|
90
|
-
console.log(
|
|
91
|
-
console.log(`
|
|
92
|
-
|
|
93
|
-
console.log(` (Root redirects to /${pageName})`);
|
|
94
|
-
}
|
|
95
|
-
console.log('');
|
|
123
|
+
server.listen(availableHttpPort, () => {
|
|
124
|
+
console.log(`🚀 JUX dev server running at http://localhost:${availableHttpPort}`);
|
|
125
|
+
console.log(` Serving: ${absoluteDistDir}`);
|
|
126
|
+
console.log(` Press Ctrl+C to stop\n`);
|
|
96
127
|
});
|
|
97
128
|
|
|
98
129
|
// Start file watcher
|
|
@@ -113,30 +144,9 @@ async function serve(port = 3000, distDir = './jux-dist') {
|
|
|
113
144
|
process.on('SIGINT', shutdown);
|
|
114
145
|
process.on('SIGTERM', shutdown);
|
|
115
146
|
|
|
116
|
-
return { server };
|
|
147
|
+
return { server, httpPort: availableHttpPort, wsPort: availableWsPort };
|
|
117
148
|
}
|
|
118
149
|
|
|
119
|
-
export async function start(
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
// ...existing code for static files...
|
|
123
|
-
|
|
124
|
-
// If pageName specified, redirect root to that page
|
|
125
|
-
if (pageName) {
|
|
126
|
-
app.get('/', (req, res) => {
|
|
127
|
-
res.redirect(`/${pageName}`);
|
|
128
|
-
});
|
|
129
|
-
console.log(`🎯 Root (/) redirects to /${pageName}`);
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
// ...existing code...
|
|
133
|
-
|
|
134
|
-
app.listen(port, () => {
|
|
135
|
-
console.log(`\n🚀 JUX dev server running`);
|
|
136
|
-
console.log(` Local: http://localhost:${port}${pageName ? '/' + pageName : ''}`);
|
|
137
|
-
if (pageName) {
|
|
138
|
-
console.log(` (Root redirects to /${pageName})`);
|
|
139
|
-
}
|
|
140
|
-
console.log('');
|
|
141
|
-
});
|
|
150
|
+
export async function start(httpPort = 3000, wsPort = 3001) {
|
|
151
|
+
return serve(httpPort, wsPort, './jux-dist');
|
|
142
152
|
}
|
package/package.json
CHANGED
package/presets/hey.jux
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { jux } from 'juxscript';
|
|
2
2
|
|
|
3
|
-
// Welcome page
|
|
4
|
-
jux.hero('welcome
|
|
3
|
+
// Welcome page with examples
|
|
4
|
+
jux.hero('welcome', {
|
|
5
5
|
title: 'Welcome to JUX',
|
|
6
6
|
subtitle: 'A JavaScript UX authorship platform'
|
|
7
7
|
}).render('#app');
|
|
@@ -12,35 +12,49 @@ jux.container('getting-started')
|
|
|
12
12
|
.style('max-width: 800px; margin: 2rem auto; padding: 2rem;')
|
|
13
13
|
.render('#app');
|
|
14
14
|
|
|
15
|
-
jux.heading('
|
|
15
|
+
jux.heading('start-heading')
|
|
16
16
|
.level(2)
|
|
17
17
|
.text('Getting Started')
|
|
18
18
|
.render('#getting-started');
|
|
19
19
|
|
|
20
|
-
jux.paragraph('
|
|
21
|
-
.text('Edit
|
|
20
|
+
jux.paragraph('start-intro')
|
|
21
|
+
.text('Edit this file to build your app. Here are some quick tips:')
|
|
22
22
|
.style('margin: 1rem 0;')
|
|
23
23
|
.render('#getting-started');
|
|
24
24
|
|
|
25
|
-
jux.list('
|
|
25
|
+
jux.list('quick-tips', {
|
|
26
26
|
items: [
|
|
27
|
-
'Run npx jux
|
|
28
|
-
'Run npx jux
|
|
29
|
-
'
|
|
30
|
-
'
|
|
27
|
+
'Run npx jux serve for dev mode with hot reload',
|
|
28
|
+
'Run npx jux build to compile for production',
|
|
29
|
+
'Serve jux-dist/ from your backend',
|
|
30
|
+
'Check out the docs at juxscript.com/docs'
|
|
31
31
|
]
|
|
32
32
|
}).render('#getting-started');
|
|
33
33
|
|
|
34
34
|
jux.divider().render('#app');
|
|
35
35
|
|
|
36
|
-
jux.
|
|
37
|
-
.
|
|
38
|
-
.variant('primary')
|
|
39
|
-
.bind('click', () => {
|
|
40
|
-
jux.alert('demo-alert')
|
|
41
|
-
.type('success')
|
|
42
|
-
.message('Button clicked! You are ready to build with JUX.')
|
|
43
|
-
.render('#app');
|
|
44
|
-
})
|
|
45
|
-
.style('margin: 2rem auto; display: block;')
|
|
36
|
+
jux.container('example-container')
|
|
37
|
+
.style('max-width: 800px; margin: 2rem auto; padding: 2rem; background: #f9fafb; border-radius: 8px;')
|
|
46
38
|
.render('#app');
|
|
39
|
+
|
|
40
|
+
jux.heading('example-heading')
|
|
41
|
+
.level(3)
|
|
42
|
+
.text('Quick Example')
|
|
43
|
+
.render('#example-container');
|
|
44
|
+
|
|
45
|
+
jux.code('example-code')
|
|
46
|
+
.language('javascript')
|
|
47
|
+
.code(`import { jux, state } from 'juxscript';
|
|
48
|
+
|
|
49
|
+
// Create a reactive counter
|
|
50
|
+
const count = state(0);
|
|
51
|
+
|
|
52
|
+
jux.button('increment')
|
|
53
|
+
.label('Click me!')
|
|
54
|
+
.bind('click', () => count.value++)
|
|
55
|
+
.render('#app');
|
|
56
|
+
|
|
57
|
+
jux.paragraph('counter')
|
|
58
|
+
.sync('text', count, val => \`Count: \${val}\`)
|
|
59
|
+
.render('#app');`)
|
|
60
|
+
.render('#example-container');
|