juxscript 1.0.0
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/README.md +292 -0
- package/bin/cli.js +149 -0
- package/lib/adapters/base-adapter.js +35 -0
- package/lib/adapters/index.js +33 -0
- package/lib/adapters/mysql-adapter.js +65 -0
- package/lib/adapters/postgres-adapter.js +70 -0
- package/lib/adapters/sqlite-adapter.js +56 -0
- package/lib/components/app.ts +124 -0
- package/lib/components/button.ts +136 -0
- package/lib/components/card.ts +205 -0
- package/lib/components/chart.ts +125 -0
- package/lib/components/code.ts +242 -0
- package/lib/components/container.ts +282 -0
- package/lib/components/data.ts +105 -0
- package/lib/components/docs-data.json +1211 -0
- package/lib/components/error-handler.ts +285 -0
- package/lib/components/footer.ts +146 -0
- package/lib/components/header.ts +167 -0
- package/lib/components/hero.ts +170 -0
- package/lib/components/import.ts +430 -0
- package/lib/components/input.ts +175 -0
- package/lib/components/layout.ts +113 -0
- package/lib/components/list.ts +392 -0
- package/lib/components/main.ts +111 -0
- package/lib/components/menu.ts +170 -0
- package/lib/components/modal.ts +216 -0
- package/lib/components/nav.ts +136 -0
- package/lib/components/node.ts +200 -0
- package/lib/components/reactivity.js +104 -0
- package/lib/components/script.ts +152 -0
- package/lib/components/sidebar.ts +168 -0
- package/lib/components/style.ts +129 -0
- package/lib/components/table.ts +279 -0
- package/lib/components/tabs.ts +191 -0
- package/lib/components/theme.ts +97 -0
- package/lib/components/view.ts +174 -0
- package/lib/jux.ts +203 -0
- package/lib/layouts/default.css +260 -0
- package/lib/layouts/default.jux +8 -0
- package/lib/layouts/figma.css +334 -0
- package/lib/layouts/figma.jux +0 -0
- package/lib/layouts/notion.css +258 -0
- package/lib/styles/base-theme.css +186 -0
- package/lib/styles/dark-theme.css +144 -0
- package/lib/styles/global.css +1131 -0
- package/lib/styles/light-theme.css +144 -0
- package/lib/styles/tokens/dark.css +86 -0
- package/lib/styles/tokens/light.css +86 -0
- package/lib/themes/dark.css +86 -0
- package/lib/themes/light.css +86 -0
- package/lib/utils/path-resolver.js +23 -0
- package/machinery/compiler.js +262 -0
- package/machinery/doc-generator.js +160 -0
- package/machinery/generators/css.js +128 -0
- package/machinery/generators/html.js +108 -0
- package/machinery/imports.js +155 -0
- package/machinery/server.js +185 -0
- package/machinery/validators/file-validator.js +123 -0
- package/machinery/watcher.js +148 -0
- package/package.json +58 -0
- package/types/globals.d.ts +16 -0
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
4
|
+
|
|
5
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
6
|
+
const __dirname = path.dirname(__filename);
|
|
7
|
+
const REGISTRY_PATH = path.join(__dirname, '../lib/registry/packages.json');
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Load package registry
|
|
11
|
+
*/
|
|
12
|
+
function loadRegistry() {
|
|
13
|
+
if (!fs.existsSync(REGISTRY_PATH)) {
|
|
14
|
+
return {};
|
|
15
|
+
}
|
|
16
|
+
return JSON.parse(fs.readFileSync(REGISTRY_PATH, 'utf-8'));
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Parse @import directives from Jux code
|
|
21
|
+
*/
|
|
22
|
+
export function parseImports(juxCode) {
|
|
23
|
+
const imports = [];
|
|
24
|
+
const lines = juxCode.split('\n');
|
|
25
|
+
|
|
26
|
+
for (let i = 0; i < lines.length; i++) {
|
|
27
|
+
const line = lines[i].trim();
|
|
28
|
+
|
|
29
|
+
// Stop parsing when we hit actual code (non-directive, non-comment)
|
|
30
|
+
if (line && !line.startsWith('@') && !line.startsWith('//') && !line.startsWith('/*')) {
|
|
31
|
+
break;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Match @import directive
|
|
35
|
+
const importMatch = line.match(/^@import\s+(.+)$/);
|
|
36
|
+
if (importMatch) {
|
|
37
|
+
const importSpec = importMatch[1].trim();
|
|
38
|
+
imports.push({
|
|
39
|
+
line: i + 1,
|
|
40
|
+
raw: importSpec,
|
|
41
|
+
resolved: resolveImport(importSpec)
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
if (process.env.JUX_VERBOSE) {
|
|
45
|
+
console.log(` [Line ${i + 1}] Found @import: ${importSpec}`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return imports;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Resolve import specification to URL/path
|
|
55
|
+
*/
|
|
56
|
+
export function resolveImport(importSpec) {
|
|
57
|
+
// Remove quotes if present
|
|
58
|
+
const cleaned = importSpec.replace(/^['"]|['"]$/g, '');
|
|
59
|
+
|
|
60
|
+
// Direct URL (starts with http:// or https://)
|
|
61
|
+
if (cleaned.startsWith('http://') || cleaned.startsWith('https://')) {
|
|
62
|
+
return {
|
|
63
|
+
type: 'cdn',
|
|
64
|
+
url: cleaned,
|
|
65
|
+
source: 'direct'
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Local file (starts with ./ or ../ or /)
|
|
70
|
+
if (cleaned.startsWith('./') || cleaned.startsWith('../') || cleaned.startsWith('/')) {
|
|
71
|
+
return {
|
|
72
|
+
type: 'local',
|
|
73
|
+
path: cleaned,
|
|
74
|
+
source: 'file'
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Unknown/unsupported
|
|
79
|
+
return {
|
|
80
|
+
type: 'unknown',
|
|
81
|
+
name: cleaned,
|
|
82
|
+
error: `Import "${cleaned}" must be a URL (https://...) or relative path (./...)`
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Auto-detect component dependencies
|
|
88
|
+
*/
|
|
89
|
+
function detectComponentDependencies(usedComponents) {
|
|
90
|
+
const componentDeps = {
|
|
91
|
+
'chart': 'chart.js'
|
|
92
|
+
// Add more mappings as needed
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
const deps = new Set();
|
|
96
|
+
|
|
97
|
+
usedComponents.forEach(comp => {
|
|
98
|
+
if (componentDeps[comp]) {
|
|
99
|
+
deps.add(componentDeps[comp]);
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
return Array.from(deps).map(pkg => ({
|
|
104
|
+
auto: true,
|
|
105
|
+
resolved: resolveImport(pkg)
|
|
106
|
+
}));
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Generate HTML script tags from resolved imports
|
|
111
|
+
*/
|
|
112
|
+
export function generateImportTags(imports) {
|
|
113
|
+
const tags = [];
|
|
114
|
+
const seen = new Set();
|
|
115
|
+
|
|
116
|
+
for (const imp of imports) {
|
|
117
|
+
const resolved = imp.resolved;
|
|
118
|
+
|
|
119
|
+
if (resolved.type === 'unknown') {
|
|
120
|
+
tags.push(`<!-- ERROR: ${resolved.error} -->`);
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const key = resolved.url || resolved.path;
|
|
125
|
+
if (seen.has(key)) continue;
|
|
126
|
+
seen.add(key);
|
|
127
|
+
|
|
128
|
+
if (resolved.type === 'cdn') {
|
|
129
|
+
tags.push(` <script src="${resolved.url}"></script>`);
|
|
130
|
+
} else if (resolved.type === 'local') {
|
|
131
|
+
tags.push(` <script type="module" src="${resolved.path}"></script>`);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return tags.join('\n');
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Validate all imports are resolvable
|
|
140
|
+
*/
|
|
141
|
+
export function validateImports(imports) {
|
|
142
|
+
const errors = [];
|
|
143
|
+
|
|
144
|
+
imports.forEach(imp => {
|
|
145
|
+
if (imp.resolved.type === 'unknown') {
|
|
146
|
+
errors.push({
|
|
147
|
+
line: imp.line,
|
|
148
|
+
message: imp.resolved.error,
|
|
149
|
+
raw: imp.raw
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
return errors;
|
|
155
|
+
}
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import express from 'express';
|
|
2
|
+
import http from 'http';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import fs from 'fs';
|
|
5
|
+
import { fileURLToPath } from 'url';
|
|
6
|
+
import initSqlJs from 'sql.js';
|
|
7
|
+
import { startWatcher } from './watcher.js';
|
|
8
|
+
import { WebSocketServer } from 'ws';
|
|
9
|
+
|
|
10
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
11
|
+
const __dirname = path.dirname(__filename);
|
|
12
|
+
|
|
13
|
+
let db = null;
|
|
14
|
+
|
|
15
|
+
async function serve(port = 3000, distDir = './dist') {
|
|
16
|
+
const app = express();
|
|
17
|
+
const absoluteDistDir = path.resolve(distDir);
|
|
18
|
+
const projectRoot = path.resolve('.');
|
|
19
|
+
|
|
20
|
+
app.use(express.json());
|
|
21
|
+
|
|
22
|
+
if (!fs.existsSync(absoluteDistDir)) {
|
|
23
|
+
console.error(`Error: dist directory not found at ${absoluteDistDir}`);
|
|
24
|
+
console.log('Building project first...\n');
|
|
25
|
+
process.exit(1);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// API endpoint for SQL queries
|
|
29
|
+
app.post('/api/query', async (req, res) => {
|
|
30
|
+
try {
|
|
31
|
+
const { sql, params = [] } = req.body;
|
|
32
|
+
|
|
33
|
+
if (!db) {
|
|
34
|
+
return res.status(500).json({ error: 'Database not initialized' });
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const stmt = db.prepare(sql);
|
|
38
|
+
stmt.bind(params);
|
|
39
|
+
|
|
40
|
+
const rows = [];
|
|
41
|
+
while (stmt.step()) {
|
|
42
|
+
rows.push(stmt.getAsObject());
|
|
43
|
+
}
|
|
44
|
+
stmt.free();
|
|
45
|
+
|
|
46
|
+
res.json({ data: rows, rowCount: rows.length });
|
|
47
|
+
} catch (error) {
|
|
48
|
+
console.error('Query error:', error.message);
|
|
49
|
+
res.status(400).json({ error: error.message });
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// Disable caching in dev mode
|
|
54
|
+
app.use((req, res, next) => {
|
|
55
|
+
res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate');
|
|
56
|
+
res.setHeader('Pragma', 'no-cache');
|
|
57
|
+
res.setHeader('Expires', '0');
|
|
58
|
+
next();
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
// Serve HTML files with clean URLs
|
|
62
|
+
const heyPath = path.join(absoluteDistDir, 'hey.html');
|
|
63
|
+
const indexPath = path.join(absoluteDistDir, 'index.html');
|
|
64
|
+
|
|
65
|
+
app.use((req, res, next) => {
|
|
66
|
+
let requestPath = req.path.endsWith('/') && req.path.length > 1
|
|
67
|
+
? req.path.slice(0, -1)
|
|
68
|
+
: req.path;
|
|
69
|
+
|
|
70
|
+
// Root path - serve hey.html or index.html
|
|
71
|
+
if (requestPath === '/') {
|
|
72
|
+
if (fs.existsSync(heyPath)) return res.sendFile(heyPath);
|
|
73
|
+
if (fs.existsSync(indexPath)) return res.sendFile(indexPath);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Try to serve as HTML file
|
|
77
|
+
const htmlPath = path.join(absoluteDistDir, requestPath + '.html');
|
|
78
|
+
if (fs.existsSync(htmlPath) && fs.statSync(htmlPath).isFile()) {
|
|
79
|
+
return res.sendFile(htmlPath);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Try to serve index.html in directory
|
|
83
|
+
const indexInDirPath = path.join(absoluteDistDir, requestPath, 'index.html');
|
|
84
|
+
if (fs.existsSync(indexInDirPath)) {
|
|
85
|
+
return res.sendFile(indexInDirPath);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
next();
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
// Serve static files (CSS, JS, images, etc.)
|
|
92
|
+
app.use(express.static(absoluteDistDir));
|
|
93
|
+
|
|
94
|
+
// Enhanced 404 handler with debug info
|
|
95
|
+
app.use((req, res) => {
|
|
96
|
+
const notFoundPath = path.join(absoluteDistDir, '404.html');
|
|
97
|
+
const requestedPath = path.join(absoluteDistDir, req.path);
|
|
98
|
+
const fileType = path.extname(req.path) || 'directory';
|
|
99
|
+
|
|
100
|
+
// Log to console for debugging
|
|
101
|
+
console.log(`ā 404: ${req.path}`);
|
|
102
|
+
console.log(` Looked for: ${requestedPath}`);
|
|
103
|
+
console.log(` Type: ${fileType}`);
|
|
104
|
+
console.log(` Referer: ${req.get('referer') || 'direct'}`);
|
|
105
|
+
|
|
106
|
+
// If custom 404.html exists and this isn't already /404
|
|
107
|
+
if (fs.existsSync(notFoundPath) && req.path !== '/404') {
|
|
108
|
+
// Add debug info as query params
|
|
109
|
+
const debugUrl = `/404?path=${encodeURIComponent(req.path)}&type=${fileType}&from=${encodeURIComponent(req.get('referer') || 'direct')}`;
|
|
110
|
+
return res.redirect(debugUrl);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Serve custom 404 page
|
|
114
|
+
if (fs.existsSync(notFoundPath)) {
|
|
115
|
+
return res.status(404).sendFile(notFoundPath);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Fallback: minimal 404 response
|
|
119
|
+
res.status(404).send('<h1>404 - Not Found</h1>');
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
// Create HTTP server (wrap Express app)
|
|
123
|
+
const server = http.createServer(app);
|
|
124
|
+
|
|
125
|
+
// WebSocket server on separate port 3001
|
|
126
|
+
const wss = new WebSocketServer({ port: 3001 });
|
|
127
|
+
const clients = [];
|
|
128
|
+
|
|
129
|
+
wss.on('connection', (ws) => {
|
|
130
|
+
clients.push(ws);
|
|
131
|
+
console.log('š WebSocket client connected');
|
|
132
|
+
|
|
133
|
+
ws.on('close', () => {
|
|
134
|
+
console.log('š WebSocket client disconnected');
|
|
135
|
+
const index = clients.indexOf(ws);
|
|
136
|
+
if (index > -1) clients.splice(index, 1);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
ws.on('error', (error) => {
|
|
140
|
+
console.error('WebSocket error:', error);
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
console.log('š WebSocket server running at ws://localhost:3001');
|
|
145
|
+
|
|
146
|
+
// Start HTTP server
|
|
147
|
+
server.listen(port, () => {
|
|
148
|
+
console.log(`š JUX dev server running at http://localhost:${port}`);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
// Start file watcher
|
|
152
|
+
startWatcher(projectRoot, absoluteDistDir, clients);
|
|
153
|
+
|
|
154
|
+
// Graceful shutdown
|
|
155
|
+
const shutdown = async () => {
|
|
156
|
+
console.log('\n\nš Shutting down server...');
|
|
157
|
+
wss.close();
|
|
158
|
+
server.close();
|
|
159
|
+
process.exit(0);
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
process.on('SIGINT', shutdown);
|
|
163
|
+
process.on('SIGTERM', shutdown);
|
|
164
|
+
|
|
165
|
+
return { server };
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
async function initDatabase() {
|
|
169
|
+
const SQL = await initSqlJs();
|
|
170
|
+
const dbPath = path.join(__dirname, '../db/jux.db');
|
|
171
|
+
|
|
172
|
+
if (fs.existsSync(dbPath)) {
|
|
173
|
+
const buffer = fs.readFileSync(dbPath);
|
|
174
|
+
db = new SQL.Database(buffer);
|
|
175
|
+
console.log('š Database loaded:', dbPath);
|
|
176
|
+
} else {
|
|
177
|
+
db = new SQL.Database();
|
|
178
|
+
console.log('š Using in-memory database');
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export async function start(port = 3000, config = {}) {
|
|
183
|
+
await initDatabase();
|
|
184
|
+
return serve(port, config.distDir || './dist');
|
|
185
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File validation utilities for Jux compiler
|
|
3
|
+
* Validates file types and content security
|
|
4
|
+
*/
|
|
5
|
+
export class FileValidator {
|
|
6
|
+
constructor() {
|
|
7
|
+
this.cssExtensions = /\.(css|scss|sass|less)$/i;
|
|
8
|
+
this.jsExtensions = /\.(js|mjs|ts|tsx|jsx)$/i;
|
|
9
|
+
this.imageExtensions = /\.(png|jpg|jpeg|gif|svg|webp|ico|bmp)$/i;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Check if file is a CSS file
|
|
14
|
+
*/
|
|
15
|
+
isCSSFile(filepath) {
|
|
16
|
+
return this.cssExtensions.test(filepath);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Check if file is a JavaScript file
|
|
21
|
+
*/
|
|
22
|
+
isJavaScriptFile(filepath) {
|
|
23
|
+
return this.jsExtensions.test(filepath);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Check if file is an image file
|
|
28
|
+
*/
|
|
29
|
+
isImageFile(filepath) {
|
|
30
|
+
return this.imageExtensions.test(filepath);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Validate CSS content for security issues
|
|
35
|
+
* Detects <script> tags that could be injected
|
|
36
|
+
*/
|
|
37
|
+
validateStyleContent(content, source = 'unknown') {
|
|
38
|
+
if (/<script/i.test(content)) {
|
|
39
|
+
throw new Error(
|
|
40
|
+
`šØ Security Error: <script> tag detected in ${source}\n` +
|
|
41
|
+
`CSS content must not contain script tags.`
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (/<\/script/i.test(content)) {
|
|
46
|
+
throw new Error(
|
|
47
|
+
`šØ Security Error: </script> tag detected in ${source}\n` +
|
|
48
|
+
`CSS content must not contain script tags.`
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return content;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Determine import type based on file extension
|
|
57
|
+
* Returns: 'css' | 'js' | 'image' | 'unknown'
|
|
58
|
+
*/
|
|
59
|
+
validateImportPath(importPath) {
|
|
60
|
+
// Handle URLs
|
|
61
|
+
if (importPath.startsWith('http://') || importPath.startsWith('https://')) {
|
|
62
|
+
// Try to infer from URL
|
|
63
|
+
if (this.isCSSFile(importPath)) return { type: 'css', path: importPath };
|
|
64
|
+
if (this.isJavaScriptFile(importPath)) return { type: 'js', path: importPath };
|
|
65
|
+
if (this.isImageFile(importPath)) return { type: 'image', path: importPath };
|
|
66
|
+
|
|
67
|
+
// Default to unknown for CDN URLs without clear extensions
|
|
68
|
+
return { type: 'unknown', path: importPath };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Local files
|
|
72
|
+
if (this.isCSSFile(importPath)) {
|
|
73
|
+
return { type: 'css', path: importPath };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (this.isJavaScriptFile(importPath)) {
|
|
77
|
+
return { type: 'js', path: importPath };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (this.isImageFile(importPath)) {
|
|
81
|
+
return { type: 'image', path: importPath };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return { type: 'unknown', path: importPath };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Validate array of imports and categorize them
|
|
89
|
+
* Returns categorized imports with warnings
|
|
90
|
+
*/
|
|
91
|
+
categorizeImports(imports) {
|
|
92
|
+
const categorized = {
|
|
93
|
+
css: [],
|
|
94
|
+
js: [],
|
|
95
|
+
images: [],
|
|
96
|
+
unknown: []
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const warnings = [];
|
|
100
|
+
|
|
101
|
+
for (const importPath of imports) {
|
|
102
|
+
const result = this.validateImportPath(importPath);
|
|
103
|
+
|
|
104
|
+
if (result.type === 'unknown') {
|
|
105
|
+
warnings.push(
|
|
106
|
+
`ā ļø Unknown import type: ${importPath}\n` +
|
|
107
|
+
` Supported: .css/.scss/.sass, .js/.ts, .png/.jpg/.svg`
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
categorized[result.type === 'unknown' ? 'unknown' : result.type].push(result.path);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return { categorized, warnings };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Check if inline style content is empty or whitespace-only
|
|
119
|
+
*/
|
|
120
|
+
isEmptyStyle(styleContent) {
|
|
121
|
+
return !styleContent || styleContent.trim().length === 0;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import chokidar from 'chokidar';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { compileJuxFile, copyLibToOutput } from './compiler.js';
|
|
4
|
+
import fs from 'fs';
|
|
5
|
+
import { glob } from 'glob';
|
|
6
|
+
|
|
7
|
+
export function startWatcher(projectRoot, distDir, clients = []) {
|
|
8
|
+
console.log('š Watcher Configuration:');
|
|
9
|
+
console.log(' Project Root:', projectRoot);
|
|
10
|
+
console.log(' Dist Dir:', distDir);
|
|
11
|
+
|
|
12
|
+
// Manually find all files to watch
|
|
13
|
+
console.log('\nš Finding files to watch...');
|
|
14
|
+
|
|
15
|
+
// Project .jux files
|
|
16
|
+
const juxFiles = glob.sync('**/*.jux', {
|
|
17
|
+
cwd: projectRoot,
|
|
18
|
+
ignore: ['node_modules/**', 'dist/**', '.git/**'],
|
|
19
|
+
absolute: true
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
// Vendor layout .jux files
|
|
23
|
+
const libRoot = path.resolve(projectRoot, '../lib');
|
|
24
|
+
const vendorJuxFiles = glob.sync('layouts/**/*.jux', {
|
|
25
|
+
cwd: libRoot,
|
|
26
|
+
absolute: true
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
const cssFiles = glob.sync('**/*.css', {
|
|
30
|
+
cwd: projectRoot,
|
|
31
|
+
ignore: ['node_modules/**', 'dist/**', '.git/**'],
|
|
32
|
+
absolute: true
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
const libFiles = glob.sync('**/*.{ts,js,css}', {
|
|
36
|
+
cwd: libRoot,
|
|
37
|
+
absolute: true
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const machineryFiles = glob.sync('machinery/**/*.js', {
|
|
41
|
+
cwd: path.resolve(projectRoot, '..'),
|
|
42
|
+
absolute: true
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
const allFiles = [...juxFiles, ...vendorJuxFiles, ...cssFiles, ...libFiles, ...machineryFiles];
|
|
46
|
+
|
|
47
|
+
console.log(` Found ${juxFiles.length} project .jux files`);
|
|
48
|
+
console.log(` Found ${vendorJuxFiles.length} vendor .jux files`);
|
|
49
|
+
console.log(` Found ${cssFiles.length} .css files`);
|
|
50
|
+
console.log(` Found ${libFiles.length} lib/ files`);
|
|
51
|
+
console.log(` Found ${machineryFiles.length} machinery/ files`);
|
|
52
|
+
console.log(` Total: ${allFiles.length} files\n`);
|
|
53
|
+
|
|
54
|
+
if (allFiles.length === 0) {
|
|
55
|
+
console.error('ā No files found to watch!');
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Watch the specific files we found
|
|
60
|
+
const watcher = chokidar.watch(allFiles, {
|
|
61
|
+
persistent: true,
|
|
62
|
+
ignoreInitial: true,
|
|
63
|
+
awaitWriteFinish: {
|
|
64
|
+
stabilityThreshold: 100,
|
|
65
|
+
pollInterval: 50
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
watcher.on('ready', () => {
|
|
70
|
+
console.log('ā
Watching for changes...\n');
|
|
71
|
+
allFiles.forEach(file => {
|
|
72
|
+
const rel = path.relative(path.resolve(projectRoot, '..'), file);
|
|
73
|
+
console.log(` šļø ${rel}`);
|
|
74
|
+
});
|
|
75
|
+
console.log('');
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
watcher.on('change', async (filePath) => {
|
|
79
|
+
const relativePath = path.relative(path.resolve(projectRoot, '..'), filePath);
|
|
80
|
+
console.log(`\nš Changed: ${relativePath}`);
|
|
81
|
+
|
|
82
|
+
try {
|
|
83
|
+
if (filePath.endsWith('.jux')) {
|
|
84
|
+
console.log(' ā Compiling .jux file...');
|
|
85
|
+
|
|
86
|
+
// Determine if it's a vendor or project file
|
|
87
|
+
if (filePath.includes('/lib/layouts/')) {
|
|
88
|
+
// Vendor layout file
|
|
89
|
+
await compileJuxFile(filePath, {
|
|
90
|
+
distDir: path.join(distDir, 'lib'),
|
|
91
|
+
projectRoot: libRoot,
|
|
92
|
+
isServe: true
|
|
93
|
+
});
|
|
94
|
+
} else {
|
|
95
|
+
// Project file
|
|
96
|
+
await compileJuxFile(filePath, { distDir, projectRoot, isServe: true });
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
console.log(`ā
Recompiled: ${relativePath}`);
|
|
100
|
+
} else if (filePath.includes('/lib/')) {
|
|
101
|
+
console.log(' ā Rebuilding lib files...');
|
|
102
|
+
await copyLibToOutput(projectRoot, distDir);
|
|
103
|
+
console.log(`ā
Rebuilt lib files`);
|
|
104
|
+
} else if (filePath.endsWith('.css')) {
|
|
105
|
+
console.log(' ā CSS file changed');
|
|
106
|
+
console.log(`ā
CSS change detected`);
|
|
107
|
+
} else if (filePath.includes('/machinery/')) {
|
|
108
|
+
console.log(' ā Machinery file changed');
|
|
109
|
+
console.log(`ā ļø Restart server to apply changes`);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
console.log(` ā Notifying ${clients.length} client(s)`);
|
|
113
|
+
notifyClients(clients);
|
|
114
|
+
} catch (err) {
|
|
115
|
+
console.error(`ā Error processing ${relativePath}:`, err.message);
|
|
116
|
+
console.error(err.stack);
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
watcher.on('add', (filePath) => {
|
|
121
|
+
const relativePath = path.relative(path.resolve(projectRoot, '..'), filePath);
|
|
122
|
+
console.log(`ā New file detected: ${relativePath}`);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
watcher.on('unlink', (filePath) => {
|
|
126
|
+
const relativePath = path.relative(path.resolve(projectRoot, '..'), filePath);
|
|
127
|
+
console.log(`ā File deleted: ${relativePath}`);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
watcher.on('error', (error) => {
|
|
131
|
+
console.error('ā Watcher error:', error);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
return watcher;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function notifyClients(clients) {
|
|
138
|
+
let notified = 0;
|
|
139
|
+
clients.forEach(client => {
|
|
140
|
+
if (client.readyState === 1) {
|
|
141
|
+
client.send(JSON.stringify({ type: 'reload' }));
|
|
142
|
+
notified++;
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
if (notified > 0) {
|
|
146
|
+
console.log(` āļø Reloaded ${notified} client(s)`);
|
|
147
|
+
}
|
|
148
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "juxscript",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "A JavaScript UX authorship platform",
|
|
6
|
+
"main": "lib/jux.js",
|
|
7
|
+
"types": "types/globals.d.ts",
|
|
8
|
+
"bin": {
|
|
9
|
+
"jux": "./bin/cli.js"
|
|
10
|
+
},
|
|
11
|
+
"scripts": {
|
|
12
|
+
"dev": "cd examples && npx jux serve",
|
|
13
|
+
"build:examples": "cd examples && rm -rf dist && npx jux build",
|
|
14
|
+
"test": "node test/run-tests.js"
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"lib",
|
|
18
|
+
"bin",
|
|
19
|
+
"machinery",
|
|
20
|
+
"types",
|
|
21
|
+
"README.md",
|
|
22
|
+
"LICENSE"
|
|
23
|
+
],
|
|
24
|
+
"publishConfig": {
|
|
25
|
+
"access": "public"
|
|
26
|
+
},
|
|
27
|
+
"repository": {
|
|
28
|
+
"type": "git",
|
|
29
|
+
"url": "https://github.com/juxscript/jux.git"
|
|
30
|
+
},
|
|
31
|
+
"keywords": [
|
|
32
|
+
"jux",
|
|
33
|
+
"ui",
|
|
34
|
+
"authoring",
|
|
35
|
+
"javascript"
|
|
36
|
+
],
|
|
37
|
+
"author": "Tim Kerr",
|
|
38
|
+
"license": "MIT",
|
|
39
|
+
"dependencies": {
|
|
40
|
+
"acorn": "^8.15.0",
|
|
41
|
+
"chokidar": "^5.0.0",
|
|
42
|
+
"clean-css": "^5.3.3",
|
|
43
|
+
"esbuild": "^0.27.2",
|
|
44
|
+
"express": "^5.2.1",
|
|
45
|
+
"glob": "^13.0.0",
|
|
46
|
+
"node": "^24.12.0",
|
|
47
|
+
"sql.js": "^1.10.3",
|
|
48
|
+
"terser": "^5.44.1",
|
|
49
|
+
"ws": "^8.19.0"
|
|
50
|
+
},
|
|
51
|
+
"optionalDependencies": {
|
|
52
|
+
"mysql2": "^3.6.5",
|
|
53
|
+
"pg": "^8.11.3"
|
|
54
|
+
},
|
|
55
|
+
"devDependencies": {
|
|
56
|
+
"typescript": "^5.9.3"
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Global type declarations for .jux files
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { jux } from '../lib/jux';
|
|
6
|
+
|
|
7
|
+
declare global {
|
|
8
|
+
const jux: typeof import('../lib/jux').jux;
|
|
9
|
+
|
|
10
|
+
interface Window {
|
|
11
|
+
REACTIVE_DEBUG?: boolean;
|
|
12
|
+
juxContext?: Record<string, any>;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export {};
|