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 +25 -21
- package/machinery/config.js +65 -688
- package/machinery/server.js +3 -3
- package/package.json +1 -1
- package/presets/{jux.jux → default/index.jux} +1 -1
- package/presets/default/layout.jux +1 -1
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,
|
|
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.
|
|
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,
|
|
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 =
|
|
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(
|
|
462
|
-
console.log(
|
|
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
|
-
// ✅
|
|
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
|
-
// ✅
|
|
476
|
-
|
|
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
|
|
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
|
|
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
|
-
├── .
|
|
500
|
+
├── ${config.distDir}/ # Build output (git-ignored, like .git)
|
|
497
501
|
│ ├── lib/
|
|
498
502
|
│ ├── main.js
|
|
499
503
|
│ └── index.html
|
|
500
|
-
├──
|
|
504
|
+
├── ${config.sourceDir}/ # Your .jux source files
|
|
501
505
|
│ ├── index.jux
|
|
502
506
|
│ ├── layout.css
|
|
503
507
|
│ └── layout.jux
|
package/machinery/config.js
CHANGED
|
@@ -1,709 +1,86 @@
|
|
|
1
|
-
import fs from 'fs';
|
|
2
1
|
import path from 'path';
|
|
3
|
-
import
|
|
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
|
-
|
|
248
|
-
|
|
5
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
6
|
+
const __dirname = path.dirname(__filename);
|
|
249
7
|
|
|
250
8
|
/**
|
|
251
|
-
*
|
|
252
|
-
*
|
|
253
|
-
* @param {string} src - Source directory
|
|
254
|
-
* @param {string} dest - Destination directory
|
|
9
|
+
* Default JUX configuration
|
|
255
10
|
*/
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
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
|
-
*
|
|
280
|
-
*
|
|
281
|
-
* @
|
|
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
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
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
|
-
|
|
59
|
+
|
|
60
|
+
console.log('ℹ️ No juxconfig.js found, using defaults');
|
|
61
|
+
return defaultConfig;
|
|
312
62
|
}
|
|
313
63
|
|
|
314
64
|
/**
|
|
315
|
-
*
|
|
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
|
|
324
|
-
|
|
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
|
-
|
|
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
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
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
|
-
|
|
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
|
}
|
package/machinery/server.js
CHANGED
|
@@ -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 = '
|
|
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,
|
|
150
|
+
export async function start(httpPort = 3000, wsPort = 3001, distDir = './.jux') {
|
|
151
|
+
return serve(httpPort, wsPort, distDir);
|
|
152
152
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { jux } from 'juxscript';
|
|
2
2
|
|
|
3
3
|
// Import the layout styles - testing link.
|
|
4
|
-
jux.include('
|
|
4
|
+
jux.include('layout.css');
|
|
5
5
|
|
|
6
6
|
// ═══════════════════════════════════════════════════════════════════
|
|
7
7
|
// GRID LAYOUT - INITIALIZATION FUNCTION
|