juxscript 1.1.0 → 1.1.3

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.
@@ -0,0 +1,466 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import esbuild from 'esbuild';
4
+ import {
5
+ bundleJuxFilesToRouter,
6
+ generateRouterIndex
7
+ } from './compiler.js';
8
+
9
+ /**
10
+ * Generate import map script tag
11
+ */
12
+ function generateImportMapScript() {
13
+ return `<script type="importmap">
14
+ {
15
+ "imports": {
16
+ "juxscript": "./lib/jux.js",
17
+ "juxscript/": "./lib/",
18
+ "juxscript/reactivity": "./lib/reactivity/state.js",
19
+ "juxscript/presets/": "./presets/",
20
+ "juxscript/components/": "./lib/components/"
21
+ }
22
+ }
23
+ </script>`;
24
+ }
25
+
26
+ /**
27
+ * Compile a .jux file to .js and .html
28
+ *
29
+ * @param {string} juxFilePath - Path to the .jux file
30
+ * @param {Object} options - Compilation options
31
+ * @param {string} options.distDir - Output directory
32
+ * @param {string} options.projectRoot - Project root directory
33
+ * @param {boolean} options.isServe - Whether serving for development
34
+ * @returns {Promise<{jsOutputPath: string, htmlOutputPath: string}>}
35
+ */
36
+ export async function compileJuxFile(juxFilePath, options = {}) {
37
+ const { distDir, projectRoot, isServe = false } = options;
38
+
39
+ const relativePath = path.relative(projectRoot, juxFilePath);
40
+ const parsedPath = path.parse(relativePath);
41
+
42
+ // Output paths
43
+ const outputDir = path.join(distDir, parsedPath.dir);
44
+ const jsOutputPath = path.join(outputDir, `${parsedPath.name}.js`);
45
+ const htmlOutputPath = path.join(outputDir, `${parsedPath.name}.html`);
46
+
47
+ // Ensure output directory exists
48
+ if (!fs.existsSync(outputDir)) {
49
+ fs.mkdirSync(outputDir, { recursive: true });
50
+ }
51
+
52
+ console.log(`šŸ“ Compiling: ${relativePath}`);
53
+
54
+ // Read the .jux file
55
+ const juxContent = fs.readFileSync(juxFilePath, 'utf-8');
56
+
57
+ // Calculate depth for relative paths
58
+ const depth = parsedPath.dir.split(path.sep).filter(p => p).length;
59
+ const libPath = depth === 0 ? './lib/jux.js' : '../'.repeat(depth) + 'lib/jux.js';
60
+
61
+ // Transform imports
62
+ let transformedContent = juxContent;
63
+
64
+ // Replace common import patterns with calculated path
65
+ transformedContent = transformedContent.replace(
66
+ /from\s+['"]\.\.?\/lib\/jux\.js['"]/g,
67
+ `from '${libPath}'`
68
+ );
69
+
70
+ // Only inject import if:
71
+ // 1. File is not empty (ignoring whitespace and comments)
72
+ // 2. File uses 'jux.' but has no import statement
73
+ const contentWithoutComments = transformedContent
74
+ .replace(/\/\*[\s\S]*?\*\//g, '') // Remove block comments
75
+ .replace(/\/\/.*/g, '') // Remove line comments
76
+ .trim();
77
+
78
+ const hasContent = contentWithoutComments.length > 0;
79
+ const usesJux = /\bjux\./g.test(contentWithoutComments);
80
+ const hasImport = /import\s+.*from/.test(transformedContent);
81
+
82
+ if (hasContent && usesJux && !hasImport) {
83
+ transformedContent = `import { jux } from '${libPath}';\n\n${transformedContent}`;
84
+ }
85
+
86
+ // Write the transformed JS
87
+ fs.writeFileSync(jsOutputPath, transformedContent);
88
+
89
+ console.log(` āœ“ JS: ${path.relative(projectRoot, jsOutputPath)}`);
90
+
91
+ // Generate HTML with import map and correct script path
92
+ const scriptPath = `./${parsedPath.name}.js`;
93
+ const importMapScript = generateImportMapScript();
94
+
95
+ const html = `<!DOCTYPE html>
96
+ <html lang="en">
97
+ <head>
98
+ <meta charset="UTF-8">
99
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
100
+ <title>${parsedPath.name}</title>
101
+ </head>
102
+ <body data-theme="">
103
+ <!-- App container -->
104
+ <div id="app" data-jux-page="${parsedPath.name}"></div>
105
+
106
+ ${importMapScript}
107
+ <script type="module" src="${scriptPath}"></script>
108
+ ${isServe ? `
109
+ <!-- Hot reload -->
110
+ <script type="module">
111
+ const ws = new WebSocket('ws://localhost:3001');
112
+ ws.onmessage = (msg) => {
113
+ const data = JSON.parse(msg.data);
114
+ if (data.type === 'reload') {
115
+ console.log('šŸ”„ Reloading page...');
116
+ location.reload();
117
+ }
118
+ };
119
+ ws.onerror = () => console.warn('āš ļø WebSocket connection failed');
120
+ </script>
121
+ ` : ''}
122
+ </body>
123
+ </html>`;
124
+
125
+ fs.writeFileSync(htmlOutputPath, html);
126
+ console.log(` āœ“ HTML: ${path.relative(projectRoot, htmlOutputPath)}`);
127
+
128
+ return { jsOutputPath, htmlOutputPath };
129
+ }
130
+
131
+ /**
132
+ * Copy and build the JUX library from TypeScript to JavaScript
133
+ *
134
+ * @param {string} projectRoot - Root directory containing lib/
135
+ * @param {string} distDir - Destination directory for built files
136
+ */
137
+ export async function copyLibToOutput(projectRoot, distDir) {
138
+ // Simplified lib path resolution
139
+ const libSrc = path.resolve(projectRoot, '../lib');
140
+
141
+ if (!fs.existsSync(libSrc)) {
142
+ throw new Error(`lib/ directory not found at ${libSrc}`);
143
+ }
144
+
145
+ const libDest = path.join(distDir, 'lib');
146
+
147
+ console.log('šŸ“¦ Building TypeScript library...');
148
+ console.log(` From: ${libSrc}`);
149
+ console.log(` To: ${libDest}`);
150
+
151
+ if (fs.existsSync(libDest)) {
152
+ fs.rmSync(libDest, { recursive: true });
153
+ }
154
+
155
+ fs.mkdirSync(libDest, { recursive: true });
156
+
157
+ // Find all TypeScript entry points
158
+ const tsFiles = findFiles(libSrc, '.ts');
159
+
160
+ if (tsFiles.length === 0) {
161
+ console.warn('āš ļø No TypeScript files found in lib/');
162
+ return;
163
+ }
164
+
165
+ console.log(` Found ${tsFiles.length} TypeScript files`);
166
+
167
+ // Build all TypeScript files with esbuild
168
+ try {
169
+ await esbuild.build({
170
+ entryPoints: tsFiles,
171
+ bundle: false,
172
+ format: 'esm',
173
+ outdir: libDest,
174
+ outbase: libSrc,
175
+ platform: 'browser',
176
+ target: 'es2020',
177
+ loader: {
178
+ '.ts': 'ts'
179
+ },
180
+ logLevel: 'warning'
181
+ });
182
+
183
+ console.log(' āœ“ TypeScript compiled to JavaScript');
184
+
185
+ // Copy non-TS files (CSS, HTML, etc.)
186
+ console.log(' Copying lib assets...');
187
+ copyNonTsFiles(libSrc, libDest);
188
+ console.log(' āœ“ Lib assets copied');
189
+
190
+ } catch (err) {
191
+ console.error('āŒ Failed to build TypeScript:', err.message);
192
+ throw err;
193
+ }
194
+
195
+ console.log('āœ… Library ready\n');
196
+ }
197
+
198
+ /**
199
+ * Copy project assets (CSS, JS, images) from jux/ to dist/
200
+ *
201
+ * @param {string} projectRoot - Source directory (jux/)
202
+ * @param {string} distDir - Destination directory (jux-dist/)
203
+ */
204
+ export async function copyProjectAssets(projectRoot, distDir) {
205
+ console.log('šŸ“¦ Copying project assets...');
206
+
207
+ // Find all CSS and JS files in project root (excluding node_modules, dist, .git)
208
+ const allFiles = [];
209
+ findProjectFiles(projectRoot, ['.css', '.js'], allFiles, projectRoot);
210
+
211
+ console.log(` Found ${allFiles.length} asset file(s)`);
212
+
213
+ for (const srcPath of allFiles) {
214
+ const relativePath = path.relative(projectRoot, srcPath);
215
+ const destPath = path.join(distDir, relativePath);
216
+ const destDir = path.dirname(destPath);
217
+
218
+ // Create destination directory if needed
219
+ if (!fs.existsSync(destDir)) {
220
+ fs.mkdirSync(destDir, { recursive: true });
221
+ }
222
+
223
+ // Copy file
224
+ fs.copyFileSync(srcPath, destPath);
225
+ console.log(` āœ“ ${relativePath}`);
226
+ }
227
+
228
+ console.log('āœ… Project assets copied\n');
229
+ }
230
+
231
+ /**
232
+ * Transpile TypeScript files from jux/ to jux-dist/, preserving folder structure
233
+ *
234
+ * @param {string} srcDir - Source directory (jux/)
235
+ * @param {string} destDir - Destination directory (jux-dist/)
236
+ * @example
237
+ * // jux/samples/mypage.ts -> jux-dist/samples/mypage.js
238
+ * await transpileProjectTypeScript('jux/', 'jux-dist/');
239
+ */
240
+ export async function transpileProjectTypeScript(srcDir, destDir) {
241
+ console.log('šŸ”· Transpiling TypeScript files...');
242
+
243
+ // Find all TypeScript files in the project
244
+ const tsFiles = findFiles(srcDir, '.ts');
245
+
246
+ if (tsFiles.length === 0) {
247
+ console.log(' No TypeScript files found in project');
248
+ return;
249
+ }
250
+
251
+ console.log(` Found ${tsFiles.length} TypeScript file(s)`);
252
+
253
+ try {
254
+ // Build all TypeScript files with esbuild
255
+ await esbuild.build({
256
+ entryPoints: tsFiles,
257
+ bundle: false,
258
+ format: 'esm',
259
+ outdir: destDir,
260
+ outbase: srcDir,
261
+ platform: 'browser',
262
+ target: 'es2020',
263
+ loader: {
264
+ '.ts': 'ts'
265
+ },
266
+ logLevel: 'warning'
267
+ });
268
+
269
+ // Log each transpiled file
270
+ tsFiles.forEach(tsFile => {
271
+ const relativePath = path.relative(srcDir, tsFile);
272
+ const jsPath = relativePath.replace(/\.ts$/, '.js');
273
+ console.log(` āœ“ ${relativePath} → ${jsPath}`);
274
+ });
275
+
276
+ console.log('āœ… TypeScript transpiled\n');
277
+
278
+ } catch (err) {
279
+ console.error('āŒ Failed to transpile TypeScript:', err.message);
280
+ throw err;
281
+ }
282
+ }
283
+
284
+ /**
285
+ * Copy presets folder from lib to dist
286
+ *
287
+ * @param {string} packageRoot - Source package root directory
288
+ * @param {string} distDir - Destination directory
289
+ */
290
+ export async function copyPresetsToOutput(packageRoot, distDir) {
291
+ console.log('šŸ“¦ Copying presets...');
292
+
293
+ const presetsSrc = path.join(packageRoot, 'presets');
294
+ const presetsDest = path.join(distDir, 'presets');
295
+
296
+ if (!fs.existsSync(presetsSrc)) {
297
+ console.log(' No presets directory found');
298
+ return;
299
+ }
300
+
301
+ if (fs.existsSync(presetsDest)) {
302
+ fs.rmSync(presetsDest, { recursive: true });
303
+ }
304
+
305
+ fs.mkdirSync(presetsDest, { recursive: true });
306
+
307
+ // Copy all files in presets directory
308
+ const files = fs.readdirSync(presetsSrc);
309
+ let copiedCount = 0;
310
+
311
+ for (const file of files) {
312
+ const srcPath = path.join(presetsSrc, file);
313
+ const destPath = path.join(presetsDest, file);
314
+
315
+ if (fs.statSync(srcPath).isFile()) {
316
+ fs.copyFileSync(srcPath, destPath);
317
+ console.log(` āœ“ ${file}`);
318
+ copiedCount++;
319
+ }
320
+ }
321
+
322
+ console.log(`āœ… Copied ${copiedCount} preset file(s)\n`);
323
+ }
324
+
325
+ /**
326
+ * Recursively find files with a specific extension
327
+ *
328
+ * @param {string} dir - Directory to search
329
+ * @param {string} extension - File extension (e.g., '.ts')
330
+ * @param {string[]} fileList - Accumulator for found files
331
+ * @returns {string[]} Array of file paths
332
+ */
333
+ async function findFiles(dir, extension, fileList = []) {
334
+ const fs = await import('fs');
335
+ const path = await import('path');
336
+ const files = fs.readdirSync(dir);
337
+
338
+ files.forEach(file => {
339
+ const filePath = path.join(dir, file);
340
+ const stat = fs.statSync(filePath);
341
+
342
+ if (stat.isDirectory() && !['node_modules', 'jux-dist', '.git', 'lib'].includes(file)) {
343
+ findFiles(filePath, extension, fileList);
344
+ } else if (file.endsWith(extension)) {
345
+ fileList.push(filePath);
346
+ }
347
+ });
348
+
349
+ return fileList;
350
+ }
351
+
352
+ /**
353
+ * Copy non-TypeScript files (CSS, JSON, JS, SVG, etc.)
354
+ *
355
+ * @param {string} src - Source directory
356
+ * @param {string} dest - Destination directory
357
+ */
358
+ function copyNonTsFiles(src, dest) {
359
+ const entries = fs.readdirSync(src, { withFileTypes: true });
360
+
361
+ for (const entry of entries) {
362
+ const srcPath = path.join(src, entry.name);
363
+ const destPath = path.join(dest, entry.name);
364
+
365
+ if (entry.isDirectory()) {
366
+ if (!fs.existsSync(destPath)) {
367
+ fs.mkdirSync(destPath, { recursive: true });
368
+ }
369
+ copyNonTsFiles(srcPath, destPath);
370
+ } else if (entry.isFile()) {
371
+ const ext = path.extname(entry.name);
372
+ // Copy CSS, JSON, SVG, and JS files (but not .ts files)
373
+ if (['.css', '.json', '.js', '.svg', '.png', '.jpg', '.jpeg', '.gif', '.webp'].includes(ext)) {
374
+ fs.copyFileSync(srcPath, destPath);
375
+ }
376
+ }
377
+ }
378
+ }
379
+
380
+ /**
381
+ * Find project files with specific extensions, excluding certain directories
382
+ *
383
+ * @param {string} dir - Directory to search
384
+ * @param {string[]} extensions - File extensions to find
385
+ * @param {string[]} fileList - Accumulator for found files
386
+ * @param {string} rootDir - Root directory for relative paths
387
+ * @param {string[]} excludeDirs - Directories to exclude
388
+ * @returns {string[]} Array of file paths
389
+ */
390
+ function findProjectFiles(dir, extensions, fileList = [], rootDir = dir, excludeDirs = ['node_modules', 'jux-dist', '.git', 'lib']) {
391
+ if (!fs.existsSync(dir)) return fileList;
392
+
393
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
394
+
395
+ for (const entry of entries) {
396
+ const fullPath = path.join(dir, entry.name);
397
+
398
+ if (entry.isDirectory()) {
399
+ // Skip excluded directories
400
+ if (excludeDirs.includes(entry.name)) {
401
+ continue;
402
+ }
403
+ findProjectFiles(fullPath, extensions, fileList, rootDir, excludeDirs);
404
+ } else {
405
+ // Check if file has one of the desired extensions
406
+ const hasExtension = extensions.some(ext => entry.name.endsWith(ext));
407
+ if (hasExtension) {
408
+ fileList.push(fullPath);
409
+ }
410
+ }
411
+ }
412
+
413
+ return fileList;
414
+ }
415
+
416
+ /**
417
+ * Build a project
418
+ *
419
+ * @param {string} projectRoot - Root directory containing lib/
420
+ * @param {string} distDir - Destination directory for built files
421
+ * @param {Object} options - Compilation options
422
+ * @param {string} options.routePrefix - Route prefix
423
+ * @returns {Promise<{jsOutputPath: string, htmlOutputPath: string}>}
424
+ */
425
+ export async function buildProject(options = {}) {
426
+ const { projectRoot, distDir } = options;
427
+
428
+ // Ensure project root and dist directory exist
429
+ if (!fs.existsSync(projectRoot)) {
430
+ throw new Error(`Project root directory not found: ${projectRoot}`);
431
+ }
432
+ if (!fs.existsSync(distDir)) {
433
+ fs.mkdirSync(distDir, { recursive: true });
434
+ }
435
+
436
+ // Option to use router mode
437
+ if (options.router) {
438
+ console.log('šŸ”€ Building in router mode...\n');
439
+
440
+ await bundleJuxFilesToRouter(projectRoot, distDir, {
441
+ routePrefix: options.routePrefix || ''
442
+ });
443
+
444
+ // Get route information for index generation
445
+ const juxFiles = findFiles(projectRoot, '.jux');
446
+ const routes = juxFiles.map(juxFile => {
447
+ const relativePath = path.relative(projectRoot, juxFile);
448
+ const parsedPath = path.parse(relativePath);
449
+ const functionName = parsedPath.dir
450
+ ? `${parsedPath.dir.replace(/\//g, '_')}_${parsedPath.name}`
451
+ : parsedPath.name;
452
+ const routePath = (options.routePrefix || '') + '/' +
453
+ (parsedPath.dir ? `${parsedPath.dir}/` : '') + parsedPath.name;
454
+ return {
455
+ path: routePath.replace(/\/+/g, '/'),
456
+ functionName
457
+ };
458
+ });
459
+
460
+ generateRouterIndex(distDir, routes);
461
+ } else {
462
+ // ...existing individual page compilation...
463
+ }
464
+
465
+ // ...existing code...
466
+ }
@@ -0,0 +1,159 @@
1
+ import { JuxCompiler } from './compiler3.js';
2
+ import path from 'path';
3
+ import fs from 'fs';
4
+
5
+ const PROJECT_ROOT = process.cwd();
6
+
7
+ // ═══════════════════════════════════════════════════════════════
8
+ // LOAD CONFIG
9
+ // ═══════════════════════════════════════════════════════════════
10
+ const JUX_CONFIG_PATH = path.resolve(PROJECT_ROOT, 'juxconfig.js');
11
+ let rawConfig = {};
12
+
13
+ try {
14
+ rawConfig = (await import(JUX_CONFIG_PATH)).config;
15
+ console.log(`āš™ļø Loaded config: ${JUX_CONFIG_PATH}`);
16
+ } catch (err) {
17
+ console.warn(`āš ļø No juxconfig.js found, using defaults`);
18
+ }
19
+
20
+ // ═══════════════════════════════════════════════════════════════
21
+ // EXPLODE CONFIG INTO NAMED OBJECTS
22
+ // ═══════════════════════════════════════════════════════════════
23
+ const directories = {
24
+ source: rawConfig.directories?.source || './jux',
25
+ distribution: rawConfig.directories?.distribution || './.jux-dist',
26
+ themes: rawConfig.directories?.themes || './themes',
27
+ layouts: rawConfig.directories?.layouts || './themes/layouts',
28
+ assets: rawConfig.directories?.assets || './themes/assets'
29
+ };
30
+
31
+ const defaults = {
32
+ httpPort: rawConfig.defaults?.httpPort || 3000,
33
+ wsPort: rawConfig.defaults?.wsPort || 3001,
34
+ autoRoute: rawConfig.defaults?.autoRoute ?? true,
35
+ layout: rawConfig.defaults?.layout || null,
36
+ theme: rawConfig.defaults?.theme || null
37
+ };
38
+
39
+ // Resolve absolute paths
40
+ const paths = {
41
+ source: path.resolve(PROJECT_ROOT, directories.source),
42
+ distribution: path.resolve(PROJECT_ROOT, directories.distribution),
43
+ themes: path.resolve(PROJECT_ROOT, directories.source, directories.themes),
44
+ layouts: path.resolve(PROJECT_ROOT, directories.source, directories.layouts),
45
+ assets: path.resolve(PROJECT_ROOT, directories.source, directories.assets)
46
+ };
47
+
48
+ // ═══════════════════════════════════════════════════════════════
49
+ // VALIDATE DIRECTORIES
50
+ // ═══════════════════════════════════════════════════════════════
51
+ console.log(`\nšŸ“ Directory Check:`);
52
+
53
+ const dirStatus = {};
54
+ for (const [name, dirPath] of Object.entries(paths)) {
55
+ if (name === 'distribution') continue;
56
+
57
+ const exists = fs.existsSync(dirPath);
58
+ dirStatus[name] = exists;
59
+
60
+ const icon = exists ? 'āœ“' : 'āœ—';
61
+ const suffix = exists ? '' : ' (will be skipped)';
62
+ console.log(` ${icon} ${name}: ${dirPath}${suffix}`);
63
+ }
64
+
65
+ // ═══════════════════════════════════════════════════════════════
66
+ // RESOLVE DEFAULT LAYOUT & THEME
67
+ // ═══════════════════════════════════════════════════════════════
68
+ function resolveFile(filename, ...searchPaths) {
69
+ if (!filename) return null;
70
+
71
+ // First try the filename as-is
72
+ if (fs.existsSync(filename)) {
73
+ return path.resolve(filename);
74
+ }
75
+
76
+ // Try each search path
77
+ for (const searchPath of searchPaths) {
78
+ if (!searchPath || !fs.existsSync(searchPath)) continue;
79
+
80
+ const fullPath = path.resolve(searchPath, filename);
81
+ if (fs.existsSync(fullPath)) {
82
+ return fullPath;
83
+ }
84
+ }
85
+
86
+ return null;
87
+ }
88
+
89
+ console.log(`\nšŸŽØ Defaults Resolution:`);
90
+
91
+ // Resolve layout
92
+ const layoutPath = defaults.layout ? resolveFile(
93
+ defaults.layout,
94
+ paths.layouts,
95
+ paths.source,
96
+ PROJECT_ROOT
97
+ ) : null;
98
+
99
+ if (defaults.layout) {
100
+ if (layoutPath) {
101
+ console.log(` āœ“ layout: ${layoutPath}`);
102
+ } else {
103
+ console.log(` āœ— layout: "${defaults.layout}" not found, will be skipped`);
104
+ }
105
+ } else {
106
+ console.log(` - layout: not configured`);
107
+ }
108
+
109
+ // Resolve theme
110
+ const themePath = defaults.theme ? resolveFile(
111
+ defaults.theme,
112
+ paths.themes,
113
+ paths.source,
114
+ PROJECT_ROOT
115
+ ) : null;
116
+
117
+ if (defaults.theme) {
118
+ if (themePath) {
119
+ console.log(` āœ“ theme: ${themePath}`);
120
+ } else {
121
+ console.log(` āœ— theme: "${defaults.theme}" not found, will be skipped`);
122
+ }
123
+ } else {
124
+ console.log(` - theme: not configured`);
125
+ }
126
+
127
+ // ═══════════════════════════════════════════════════════════════
128
+ // VALIDATE SOURCE DIRECTORY EXISTS
129
+ // ═══════════════════════════════════════════════════════════════
130
+ if (!dirStatus.source) {
131
+ console.error(`\nāŒ Source directory not found: ${paths.source}`);
132
+ console.error(` Create the directory or update juxconfig.js`);
133
+ process.exit(1);
134
+ }
135
+
136
+ // ═══════════════════════════════════════════════════════════════
137
+ // RUN BUILD
138
+ // ═══════════════════════════════════════════════════════════════
139
+ console.log(`\n`);
140
+
141
+ const compiler = new JuxCompiler({
142
+ srcDir: paths.source,
143
+ distDir: paths.distribution,
144
+ layoutPath,
145
+ themePath,
146
+ defaults,
147
+ paths
148
+ });
149
+
150
+ compiler.build()
151
+ .then(result => {
152
+ if (!result.success) {
153
+ process.exit(1);
154
+ }
155
+ })
156
+ .catch(err => {
157
+ console.error('āŒ Build failed:', err.message);
158
+ process.exit(1);
159
+ });
File without changes
File without changes
File without changes