juxscript 1.0.89 → 1.0.90
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 +243 -92
- package/index.js +21 -0
- package/machinery/build3.js +21 -0
- package/machinery/compiler3.js +638 -0
- package/machinery/serve.js +255 -0
- package/machinery/watcher.js +53 -64
- package/package.json +11 -3
- package/lib/componentsv2/index.d.ts +0 -39
- package/lib/componentsv2/index.d.ts.map +0 -1
- package/lib/componentsv2/index.js +0 -221
- package/lib/componentsv2/index.js.map +0 -1
- package/lib/componentsv2/index.ts +0 -253
- package/machinery/ast.js +0 -347
- package/machinery/compiler.js +0 -706
- package/machinery/diagnose.js +0 -72
- package/machinery/doc-generator.js +0 -136
- package/machinery/imports.js +0 -155
- package/machinery/jux-module-pattern.md +0 -118
- package/machinery/server.js +0 -86
- package/machinery/ts-shim.js +0 -46
- package/machinery/verifier.js +0 -135
- package/tests/dropdown-test.js +0 -25
- package/tests/juxerrors/bad_syntax.jux +0 -8
- package/tests/juxerrors/ghost_dep.jux +0 -10
- package/tests/server_plugin_test.ts +0 -191
package/machinery/compiler.js
DELETED
|
@@ -1,706 +0,0 @@
|
|
|
1
|
-
import fs from 'fs';
|
|
2
|
-
import path, { parse } from 'path';
|
|
3
|
-
import esbuild from 'esbuild';
|
|
4
|
-
import * as acorn from 'acorn';
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Copy and build the JUX library from TypeScript to JavaScript
|
|
8
|
-
*/
|
|
9
|
-
export async function copyLibToOutput(projectRoot, distDir) {
|
|
10
|
-
const libSrc = path.resolve(projectRoot, '../lib');
|
|
11
|
-
if (!fs.existsSync(libSrc)) {
|
|
12
|
-
throw new Error(`lib/ directory not found at ${libSrc}`);
|
|
13
|
-
}
|
|
14
|
-
const libDest = path.join(distDir, 'lib');
|
|
15
|
-
|
|
16
|
-
if (fs.existsSync(libDest)) {
|
|
17
|
-
fs.rmSync(libDest, { recursive: true });
|
|
18
|
-
}
|
|
19
|
-
fs.mkdirSync(libDest, { recursive: true });
|
|
20
|
-
|
|
21
|
-
const tsFiles = findFiles(libSrc, '.ts');
|
|
22
|
-
if (tsFiles.length === 0) return;
|
|
23
|
-
|
|
24
|
-
try {
|
|
25
|
-
await esbuild.build({
|
|
26
|
-
entryPoints: tsFiles,
|
|
27
|
-
bundle: false,
|
|
28
|
-
format: 'esm',
|
|
29
|
-
outdir: libDest,
|
|
30
|
-
outbase: libSrc,
|
|
31
|
-
platform: 'browser',
|
|
32
|
-
target: 'es2020',
|
|
33
|
-
loader: { '.ts': 'ts' },
|
|
34
|
-
logLevel: 'error'
|
|
35
|
-
});
|
|
36
|
-
copyNonTsFiles(libSrc, libDest);
|
|
37
|
-
} catch (err) {
|
|
38
|
-
console.error('❌ Failed to build TypeScript:', err.message);
|
|
39
|
-
throw err;
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* Copy project assets (CSS, JS, images) from jux/ to dist/
|
|
45
|
-
*/
|
|
46
|
-
export async function copyProjectAssets(projectRoot, distDir) {
|
|
47
|
-
const allFiles = [];
|
|
48
|
-
findProjectFiles(projectRoot, ['.css', '.js'], allFiles, projectRoot);
|
|
49
|
-
|
|
50
|
-
for (const srcPath of allFiles) {
|
|
51
|
-
const relativePath = path.relative(projectRoot, srcPath);
|
|
52
|
-
const destPath = path.join(distDir, relativePath);
|
|
53
|
-
const destDir = path.dirname(destPath);
|
|
54
|
-
if (!fs.existsSync(destDir)) fs.mkdirSync(destDir, { recursive: true });
|
|
55
|
-
fs.copyFileSync(srcPath, destPath);
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* Transpile TypeScript files from jux/ to jux-dist/
|
|
61
|
-
*/
|
|
62
|
-
export async function transpileProjectTypeScript(srcDir, destDir) {
|
|
63
|
-
const tsFiles = findFiles(srcDir, '.ts');
|
|
64
|
-
if (tsFiles.length === 0) return;
|
|
65
|
-
|
|
66
|
-
try {
|
|
67
|
-
await esbuild.build({
|
|
68
|
-
entryPoints: tsFiles,
|
|
69
|
-
bundle: false,
|
|
70
|
-
format: 'esm',
|
|
71
|
-
outdir: destDir,
|
|
72
|
-
outbase: srcDir,
|
|
73
|
-
platform: 'browser',
|
|
74
|
-
target: 'es2020',
|
|
75
|
-
loader: { '.ts': 'ts' },
|
|
76
|
-
logLevel: 'error'
|
|
77
|
-
});
|
|
78
|
-
} catch (err) {
|
|
79
|
-
console.error('❌ Failed to transpile TypeScript:', err.message);
|
|
80
|
-
throw err;
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/**
|
|
85
|
-
* Copy presets (No-op log silenced)
|
|
86
|
-
*/
|
|
87
|
-
export async function copyPresetsToOutput(packageRoot, distDir) {
|
|
88
|
-
// No-op
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
function findFiles(dir, extension, fileList = []) {
|
|
92
|
-
if (!fs.existsSync(dir)) return fileList;
|
|
93
|
-
const files = fs.readdirSync(dir);
|
|
94
|
-
files.forEach(file => {
|
|
95
|
-
const filePath = path.join(dir, file);
|
|
96
|
-
const stat = fs.statSync(filePath);
|
|
97
|
-
if (stat.isDirectory()) {
|
|
98
|
-
if (file !== 'node_modules' && file !== '.git' && file !== 'jux-dist') {
|
|
99
|
-
findFiles(filePath, extension, fileList);
|
|
100
|
-
}
|
|
101
|
-
} else if (file.endsWith(extension)) {
|
|
102
|
-
fileList.push(filePath);
|
|
103
|
-
}
|
|
104
|
-
});
|
|
105
|
-
return fileList;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
function copyNonTsFiles(src, dest) {
|
|
109
|
-
const entries = fs.readdirSync(src, { withFileTypes: true });
|
|
110
|
-
|
|
111
|
-
for (const entry of entries) {
|
|
112
|
-
const srcPath = path.join(src, entry.name);
|
|
113
|
-
const destPath = path.join(dest, entry.name);
|
|
114
|
-
|
|
115
|
-
if (entry.isDirectory()) {
|
|
116
|
-
if (entry.name === 'layouts') continue;
|
|
117
|
-
if (!fs.existsSync(destPath)) fs.mkdirSync(destPath, { recursive: true });
|
|
118
|
-
copyNonTsFiles(srcPath, destPath);
|
|
119
|
-
} else if (entry.isFile()) {
|
|
120
|
-
const ext = path.extname(entry.name);
|
|
121
|
-
if (ext === '.js' || ext === '.map') {
|
|
122
|
-
let tsSibling = '';
|
|
123
|
-
if (ext === '.js') tsSibling = srcPath.replace(/\.js$/, '.ts');
|
|
124
|
-
else if (ext === '.map') tsSibling = srcPath.replace(/\.js\.map$/, '.ts');
|
|
125
|
-
if (tsSibling && fs.existsSync(tsSibling)) continue;
|
|
126
|
-
}
|
|
127
|
-
if (['.css', '.json', '.js', '.svg', '.png', '.jpg', '.jpeg', '.gif', '.webp'].includes(ext)) {
|
|
128
|
-
fs.copyFileSync(srcPath, destPath);
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
function findProjectFiles(dir, extensions, fileList = [], rootDir = dir, excludeDirs = ['node_modules', 'jux-dist', '.git', 'lib']) {
|
|
135
|
-
if (!fs.existsSync(dir)) return fileList;
|
|
136
|
-
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
137
|
-
for (const entry of entries) {
|
|
138
|
-
const fullPath = path.join(dir, entry.name);
|
|
139
|
-
if (entry.isDirectory()) {
|
|
140
|
-
if (excludeDirs.includes(entry.name)) continue;
|
|
141
|
-
findProjectFiles(fullPath, extensions, fileList, rootDir, excludeDirs);
|
|
142
|
-
} else {
|
|
143
|
-
const hasExtension = extensions.some(ext => entry.name.endsWith(ext));
|
|
144
|
-
if (hasExtension) fileList.push(fullPath);
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
return fileList;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
/**
|
|
151
|
-
* Bundle all .jux files into a single router-based main.js
|
|
152
|
-
*/
|
|
153
|
-
export async function bundleJuxFilesToRouter(projectRoot, distDir, options = {}) {
|
|
154
|
-
const { routePrefix = '', config } = options;
|
|
155
|
-
|
|
156
|
-
// ✅ 1. Find all processable source files (.jux AND .js)
|
|
157
|
-
// This ensures locally imported helper JS files are bundled and namespaced correctly.
|
|
158
|
-
const juxFiles = findFiles(projectRoot, '.jux');
|
|
159
|
-
const jsFiles = findFiles(projectRoot, '.js');
|
|
160
|
-
|
|
161
|
-
// Combine, filtering duplicates if any
|
|
162
|
-
const allSourceFiles = Array.from(new Set([...juxFiles, ...jsFiles]));
|
|
163
|
-
|
|
164
|
-
if (allSourceFiles.length === 0) {
|
|
165
|
-
return { mainJsFilename: 'main.js', routes: [], external: new Set(), vendoredPaths: {} };
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
// Set of relative paths being bundled (used to decide if an import should be skipped)
|
|
169
|
-
const bundledPaths = new Set(allSourceFiles.map(f => path.relative(projectRoot, f).replace(/\\/g, '/')));
|
|
170
|
-
|
|
171
|
-
const pages = config?.pages || {};
|
|
172
|
-
const views = [];
|
|
173
|
-
const routes = [];
|
|
174
|
-
const sharedModules = new Map();
|
|
175
|
-
const allImports = [];
|
|
176
|
-
const externalModules = new Set();
|
|
177
|
-
const fileToFunction = new Map();
|
|
178
|
-
|
|
179
|
-
for (const sourceFile of allSourceFiles) {
|
|
180
|
-
const isJux = sourceFile.endsWith('.jux');
|
|
181
|
-
const relativePath = path.relative(projectRoot, sourceFile);
|
|
182
|
-
const parsedPath = path.parse(relativePath);
|
|
183
|
-
|
|
184
|
-
// Normalize function name: experiments/my-data -> ExperimentsMyData
|
|
185
|
-
const rawFunctionName = parsedPath.dir ? `${parsedPath.dir.replace(/\//g, '_')}_${parsedPath.name}` : parsedPath.name;
|
|
186
|
-
const cleanFunctionName = rawFunctionName.replace(/[-_]/g, ' ').split(' ').map(w => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase()).join('');
|
|
187
|
-
|
|
188
|
-
// Only .jux files generate Routes/Views
|
|
189
|
-
if (isJux) {
|
|
190
|
-
fileToFunction.set(relativePath, cleanFunctionName);
|
|
191
|
-
fileToFunction.set(relativePath.split(path.sep).join('/'), cleanFunctionName);
|
|
192
|
-
|
|
193
|
-
// ✅ FIX: Register lookup without extension to match juxconfig routes like "/path/to/view"
|
|
194
|
-
const noExt = relativePath.replace(/\.jux$/, '');
|
|
195
|
-
const noExtNormalized = noExt.split(path.sep).join('/');
|
|
196
|
-
fileToFunction.set(noExt, cleanFunctionName);
|
|
197
|
-
fileToFunction.set(noExtNormalized, cleanFunctionName);
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
const routePath = routePrefix + '/' + (parsedPath.dir ? `${parsedPath.dir}/` : '') + parsedPath.name;
|
|
201
|
-
const cleanRoutePath = routePath.replace(/\/+/g, '/');
|
|
202
|
-
|
|
203
|
-
const fileContent = fs.readFileSync(sourceFile, 'utf-8');
|
|
204
|
-
|
|
205
|
-
// Parse Imports
|
|
206
|
-
try {
|
|
207
|
-
const ast = acorn.parse(fileContent, { ecmaVersion: 'latest', sourceType: 'module' });
|
|
208
|
-
ast.body.forEach(node => {
|
|
209
|
-
if (node.type === 'ImportDeclaration') {
|
|
210
|
-
const importStatement = fileContent.slice(node.start, node.end);
|
|
211
|
-
const moduleName = node.source.value;
|
|
212
|
-
|
|
213
|
-
// ✅ CHECK: Is this import pointing to a file we are bundling?
|
|
214
|
-
let isBundledLocal = false;
|
|
215
|
-
// Support relative (./, ../) OR project-root relative (/) imports
|
|
216
|
-
if (moduleName.startsWith('.') || moduleName.startsWith('/')) {
|
|
217
|
-
const resolved = resolveImportPath(moduleName, relativePath);
|
|
218
|
-
|
|
219
|
-
if (bundledPaths.has(resolved)) {
|
|
220
|
-
isBundledLocal = true;
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
// ONLY add to global imports if it is NOT a local bundled file
|
|
224
|
-
if (!isBundledLocal) {
|
|
225
|
-
allImports.push({ code: importStatement, filePath: relativePath });
|
|
226
|
-
|
|
227
|
-
if (!moduleName.startsWith('.') && !moduleName.startsWith('/') && !moduleName.startsWith('http') && !moduleName.startsWith('juxscript')) {
|
|
228
|
-
externalModules.add(moduleName);
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
});
|
|
233
|
-
} catch (parseErr) {
|
|
234
|
-
console.warn(` ⚠️ Failed to parse imports in ${relativePath}, skipping`);
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
// Process Exports -> Shared Modules (Code Hoisting)
|
|
238
|
-
const hasExports = /^\s*export\s+(const|let|function|class|{)/m.test(fileContent);
|
|
239
|
-
if (hasExports) {
|
|
240
|
-
const exportKey = relativePath;
|
|
241
|
-
// ✅ FIX: Pass bundledPaths so imports can be resolved and rewritten in shared code
|
|
242
|
-
const exportCode = extractSharedModule(fileContent, rawFunctionName, relativePath, bundledPaths);
|
|
243
|
-
if (exportCode.trim()) sharedModules.set(exportKey, exportCode);
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
// Process .jux -> View Functions
|
|
247
|
-
if (isJux) {
|
|
248
|
-
// Pass bundledPaths so we know which imports to rewrite
|
|
249
|
-
const viewFunction = transformJuxToViewFunction(fileContent, rawFunctionName, parsedPath.name, relativePath, sharedModules, bundledPaths);
|
|
250
|
-
views.push(viewFunction);
|
|
251
|
-
|
|
252
|
-
if (config?.defaults?.autoRoute !== false) {
|
|
253
|
-
routes.push({ path: cleanRoutePath, functionName: cleanFunctionName });
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
if (pages) {
|
|
259
|
-
const resolveAndAddRoute = (urlPath, targetFile) => {
|
|
260
|
-
// ✅ FIX: Handle ./ and / prefixes in config paths
|
|
261
|
-
const cleanTarget = targetFile.replace(/^(\.\/|\/)/, '');
|
|
262
|
-
let funcName = fileToFunction.get(cleanTarget);
|
|
263
|
-
|
|
264
|
-
// Try appending .jux if not found and no extension provided
|
|
265
|
-
if (!funcName && !cleanTarget.endsWith('.jux')) {
|
|
266
|
-
funcName = fileToFunction.get(cleanTarget + '.jux');
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
if (funcName) {
|
|
270
|
-
console.log(` 📍 Custom Route: ${urlPath.padEnd(25)} -> ${funcName} (${targetFile})`);
|
|
271
|
-
routes.unshift({ path: urlPath, functionName: funcName });
|
|
272
|
-
} else {
|
|
273
|
-
console.warn(` ⚠️ Route target not found: ${targetFile} (Cleaned: ${cleanTarget})`);
|
|
274
|
-
}
|
|
275
|
-
};
|
|
276
|
-
Object.entries(pages).forEach(([key, value]) => {
|
|
277
|
-
if (typeof value === 'string') {
|
|
278
|
-
resolveAndAddRoute(key, value);
|
|
279
|
-
} else if (typeof value === 'object') {
|
|
280
|
-
const prefix = value.prefix || '';
|
|
281
|
-
const groupRoutes = value.routes || {};
|
|
282
|
-
Object.entries(groupRoutes).forEach(([routePath, targetFile]) => {
|
|
283
|
-
const fullPath = (prefix + routePath).replace(/\/+/g, '/');
|
|
284
|
-
resolveAndAddRoute(fullPath, targetFile);
|
|
285
|
-
});
|
|
286
|
-
}
|
|
287
|
-
});
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
const vendoredPaths = await vendorExternalDependencies(externalModules, distDir);
|
|
291
|
-
const routerCode = generateRouterBundle(views, routes, sharedModules, allImports, projectRoot);
|
|
292
|
-
const mainJsFilename = 'main.js';
|
|
293
|
-
const mainJsPath = path.join(distDir, mainJsFilename);
|
|
294
|
-
fs.writeFileSync(mainJsPath, routerCode);
|
|
295
|
-
|
|
296
|
-
return {
|
|
297
|
-
mainJsFilename,
|
|
298
|
-
routes: routes.map(r => ({ path: r.path, functionName: r.functionName })),
|
|
299
|
-
external: externalModules,
|
|
300
|
-
vendoredPaths
|
|
301
|
-
};
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
function extractSharedModule(juxContent, moduleName, sourceFilePath, bundledPaths) {
|
|
305
|
-
// 1. Calculate Namespace
|
|
306
|
-
const namespace = sourceFilePath.replace(/\.(jux|js)$/, '').replace(/[\/\\]/g, '$').replace(/[^a-zA-Z0-9$]/g, '_');
|
|
307
|
-
|
|
308
|
-
let ast;
|
|
309
|
-
try {
|
|
310
|
-
ast = acorn.parse(juxContent, { ecmaVersion: 'latest', sourceType: 'module' });
|
|
311
|
-
} catch (err) {
|
|
312
|
-
throw new Error(`Invalid JavaScript syntax in ${sourceFilePath}: ${err.message}`);
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
// 2. Identify ALL Identifiers to rename (Local defs + Imports)
|
|
316
|
-
// We must rename everything to avoid global collision in the flat bundle
|
|
317
|
-
const identifiersToRename = new Map();
|
|
318
|
-
|
|
319
|
-
// A. Local Declarations (Self-namespacing)
|
|
320
|
-
const addId = (idNode) => {
|
|
321
|
-
if (idNode && idNode.type === 'Identifier') {
|
|
322
|
-
identifiersToRename.set(idNode.name, `${idNode.name}$${namespace}`);
|
|
323
|
-
}
|
|
324
|
-
};
|
|
325
|
-
|
|
326
|
-
ast.body.forEach(node => {
|
|
327
|
-
if (node.type === 'FunctionDeclaration' || node.type === 'ClassDeclaration') {
|
|
328
|
-
addId(node.id);
|
|
329
|
-
} else if (node.type === 'VariableDeclaration') {
|
|
330
|
-
node.declarations.forEach(d => addId(d.id));
|
|
331
|
-
} else if ((node.type === 'ExportNamedDeclaration' || node.type === 'ExportDefaultDeclaration') && node.declaration) {
|
|
332
|
-
const d = node.declaration;
|
|
333
|
-
if (d.type === 'FunctionDeclaration' || d.type === 'ClassDeclaration') addId(d.id);
|
|
334
|
-
else if (d.type === 'VariableDeclaration') d.declarations.forEach(v => addId(v.id));
|
|
335
|
-
} else if (node.type === 'ImportDeclaration') {
|
|
336
|
-
// B. Imports (Rewriting references to other bundled modules)
|
|
337
|
-
const importPath = node.source.value;
|
|
338
|
-
const resolvedPath = resolveImportPath(importPath, sourceFilePath);
|
|
339
|
-
// Check if this import is pointing to a bundled file
|
|
340
|
-
const isBundled = (bundledPaths && bundledPaths.has(resolvedPath)) || importPath.endsWith('.jux');
|
|
341
|
-
|
|
342
|
-
if (isBundled) {
|
|
343
|
-
const targetNamespace = resolvedPath.replace(/\.(jux|js)$/, '').replace(/[\/\\]/g, '$').replace(/[^a-zA-Z0-9$]/g, '_');
|
|
344
|
-
node.specifiers.forEach(spec => {
|
|
345
|
-
if (spec.type === 'ImportSpecifier') {
|
|
346
|
-
identifiersToRename.set(spec.local.name, `${spec.imported.name}$${targetNamespace}`);
|
|
347
|
-
} else if (spec.type === 'ImportDefaultSpecifier') {
|
|
348
|
-
// Best effort for defaults in this flat bundle scheme usually implies named export match or similar convention
|
|
349
|
-
// For now, assuming standard View exports which don't usually default export to shared
|
|
350
|
-
}
|
|
351
|
-
});
|
|
352
|
-
}
|
|
353
|
-
}
|
|
354
|
-
});
|
|
355
|
-
|
|
356
|
-
// 3. Reconstruct Code Line-by-Line (Node-by-Node)
|
|
357
|
-
const outputLines = [`// From: ${sourceFilePath}`];
|
|
358
|
-
|
|
359
|
-
// Sort keys by length desc to avoid partial replacement issues
|
|
360
|
-
const sortedKeys = Array.from(identifiersToRename.keys()).sort((a, b) => b.length - a.length);
|
|
361
|
-
|
|
362
|
-
ast.body.forEach(node => {
|
|
363
|
-
// Skip Imports (handled globally in bundling phase)
|
|
364
|
-
if (node.type === 'ImportDeclaration') return;
|
|
365
|
-
// Skip "export { a, b }" (re-exports) as 'a' and 'b' are already declared/renamed locally
|
|
366
|
-
if (node.type === 'ExportNamedDeclaration' && !node.declaration) return;
|
|
367
|
-
|
|
368
|
-
// Extract relevant code slice
|
|
369
|
-
let code = '';
|
|
370
|
-
if (node.type === 'ExportNamedDeclaration' || node.type === 'ExportDefaultDeclaration') {
|
|
371
|
-
if (node.declaration) {
|
|
372
|
-
code = juxContent.slice(node.declaration.start, node.declaration.end);
|
|
373
|
-
}
|
|
374
|
-
} else {
|
|
375
|
-
code = juxContent.slice(node.start, node.end);
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
if (!code) return;
|
|
379
|
-
|
|
380
|
-
// Replace Identifiers in this block
|
|
381
|
-
// Regex:
|
|
382
|
-
// (?<!\.) -> Negative lookbehind to avoid property access (obj.foo)
|
|
383
|
-
// \bkey\b -> Word boundary
|
|
384
|
-
// (?!\s*:) -> Negative lookahead to avoid object keys ({ foo: 1 })
|
|
385
|
-
for (const key of sortedKeys) {
|
|
386
|
-
const namespaced = identifiersToRename.get(key);
|
|
387
|
-
const regex = new RegExp(`(?<!\\.)\\b${key}\\b(?!\\s*:)`, 'g');
|
|
388
|
-
code = code.replace(regex, namespaced);
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
outputLines.push(code);
|
|
392
|
-
});
|
|
393
|
-
|
|
394
|
-
return outputLines.join('\n\n');
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
function transformJuxToViewFunction(juxContent, functionName, pageName, relativePath, sharedModules, bundledPaths) {
|
|
398
|
-
let result;
|
|
399
|
-
try {
|
|
400
|
-
const ast = acorn.parse(juxContent, { ecmaVersion: 'latest', sourceType: 'module' });
|
|
401
|
-
const nodesToRemove = [];
|
|
402
|
-
const importReplacements = new Map();
|
|
403
|
-
ast.body.forEach(node => {
|
|
404
|
-
if (node.type === 'ImportDeclaration') {
|
|
405
|
-
const importPath = node.source.value;
|
|
406
|
-
const resolvedPath = resolveImportPath(importPath, relativePath);
|
|
407
|
-
|
|
408
|
-
// ✅ Check if this import is pointing to a bundled file
|
|
409
|
-
// Handle BOTH relative and absolute-relative imports (starting with /)
|
|
410
|
-
const isBundled = (bundledPaths && bundledPaths.has(resolvedPath)) || importPath.endsWith('.jux');
|
|
411
|
-
|
|
412
|
-
if (isBundled) {
|
|
413
|
-
node.specifiers.forEach(spec => {
|
|
414
|
-
if (spec.type === 'ImportSpecifier') {
|
|
415
|
-
// ✅ Apply namespace convention to .js imports too
|
|
416
|
-
const namespace = resolvedPath.replace(/\.(jux|js)$/, '').replace(/[\/\\]/g, '$').replace(/[^a-zA-Z0-9$]/g, '_');
|
|
417
|
-
importReplacements.set(spec.local.name, `${spec.imported.name}$${namespace}`);
|
|
418
|
-
}
|
|
419
|
-
});
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
// 🔥 FIX: Always remove ImportDeclaration from the view function contents.
|
|
423
|
-
// Leftover imports inside a function body cause SyntaxError.
|
|
424
|
-
nodesToRemove.push({ start: node.start, end: node.end });
|
|
425
|
-
}
|
|
426
|
-
if (node.type === 'ExportNamedDeclaration' || node.type === 'ExportDefaultDeclaration') {
|
|
427
|
-
nodesToRemove.push({ start: node.start, end: node.end });
|
|
428
|
-
}
|
|
429
|
-
});
|
|
430
|
-
nodesToRemove.sort((a, b) => b.start - a.start);
|
|
431
|
-
result = juxContent;
|
|
432
|
-
nodesToRemove.forEach(({ start, end }) => { result = result.slice(0, start) + result.slice(end); });
|
|
433
|
-
importReplacements.forEach((namespacedName, localName) => {
|
|
434
|
-
result = result.replace(new RegExp(`\\b${localName}\\b`, 'g'), namespacedName);
|
|
435
|
-
});
|
|
436
|
-
} catch (err) {
|
|
437
|
-
throw new Error(`Invalid JavaScript syntax in ${relativePath}`);
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
result = result.replace(/\.renderTo\(container\)/g, '.render("#app")').replace(/\.render\(\s*\)/g, '.render("#app")');
|
|
441
|
-
const cleanName = functionName.replace(/[-_]/g, ' ').split(' ').map(w => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase()).join('');
|
|
442
|
-
|
|
443
|
-
// ✅ Wrap View implementation in underscore function, then export Wrapped version
|
|
444
|
-
return `
|
|
445
|
-
// View: ${cleanName}
|
|
446
|
-
function _${cleanName}() {
|
|
447
|
-
${result}
|
|
448
|
-
|
|
449
|
-
return document.getElementById('app');
|
|
450
|
-
}
|
|
451
|
-
const ${cleanName} = JuxError.wrap('${cleanName}', _${cleanName});
|
|
452
|
-
// Register Source
|
|
453
|
-
JuxError.register('${cleanName}', '${relativePath}');
|
|
454
|
-
`;
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
function resolveImportPath(importPath, currentFilePath) {
|
|
458
|
-
|
|
459
|
-
console.log(`[Resolve] Importing "${importPath}" from "${currentFilePath}"`);
|
|
460
|
-
// ✅ FIX: Handle root-relative imports (start with /)
|
|
461
|
-
if (importPath.startsWith('/')) {
|
|
462
|
-
const resolved = importPath.substring(1).replace(/\\/g, '/');
|
|
463
|
-
// console.log(`[Resolve] Root-relative: "${importPath}" -> "${resolved}"`);
|
|
464
|
-
return resolved;
|
|
465
|
-
}
|
|
466
|
-
const currentDir = path.dirname(currentFilePath);
|
|
467
|
-
// ✅ Ensure normalized separators
|
|
468
|
-
const resolved = path.join(currentDir, importPath).replace(/\\/g, '/');
|
|
469
|
-
// console.log(`[Resolve] Relative: "${importPath}" (from ${currentFilePath}) -> "${resolved}"`);
|
|
470
|
-
console.log(`[Resolve] Importing "${importPath}" from "${currentFilePath}" -> Resolved: "${resolved}"`);
|
|
471
|
-
return resolved;
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
function generateRouterBundle(views, routes, sharedModules = new Map(), allImports = [], projectRoot = '') {
|
|
475
|
-
const mergedImports = new Map();
|
|
476
|
-
const getCanonicalSource = (source, contextFilePath) => {
|
|
477
|
-
if (!source.startsWith('.') || source.includes('.jux')) return source;
|
|
478
|
-
const dir = path.dirname(contextFilePath);
|
|
479
|
-
const joinedPath = path.join(dir, source);
|
|
480
|
-
const v2Index = joinedPath.indexOf('lib/componentsv2');
|
|
481
|
-
if (v2Index !== -1) {
|
|
482
|
-
const subPath = joinedPath.substring(v2Index + 'lib/componentsv2'.length);
|
|
483
|
-
if (!subPath || subPath === '/index.js' || subPath === '/index') return 'juxscript';
|
|
484
|
-
return 'juxscript' + subPath.replace(/\\/g, '/');
|
|
485
|
-
}
|
|
486
|
-
if (joinedPath.includes('lib/components')) return 'juxscript/components';
|
|
487
|
-
|
|
488
|
-
// Fix: Ensure local relative paths ALWAYS start with ./
|
|
489
|
-
let normalized = joinedPath.replace(/\\/g, '/');
|
|
490
|
-
// If it's a bare filename like "file.js", prefix it to "./file.js"
|
|
491
|
-
// Also check for ":" to avoid messing up URLs like http:// or data:
|
|
492
|
-
if (!normalized.startsWith('.') && !normalized.startsWith('/') && !normalized.includes(':')) {
|
|
493
|
-
const old = normalized;
|
|
494
|
-
normalized = './' + normalized;
|
|
495
|
-
console.log(` 🛠️ Canonicalizing local import: "${old}" -> "${normalized}"`);
|
|
496
|
-
}
|
|
497
|
-
return normalized;
|
|
498
|
-
};
|
|
499
|
-
|
|
500
|
-
const importList = Array.isArray(allImports) ? allImports : Array.from(allImports);
|
|
501
|
-
for (const item of importList) {
|
|
502
|
-
try {
|
|
503
|
-
const code = typeof item === 'string' ? item : item.code;
|
|
504
|
-
const filePath = typeof item === 'string' ? '' : item.filePath;
|
|
505
|
-
const ast = acorn.parse(code.trim(), { ecmaVersion: 'latest', sourceType: 'module' });
|
|
506
|
-
const node = ast.body[0];
|
|
507
|
-
if (node && node.type === 'ImportDeclaration') {
|
|
508
|
-
const rawSource = node.source.value;
|
|
509
|
-
const source = getCanonicalSource(rawSource, filePath);
|
|
510
|
-
|
|
511
|
-
// Debug: Log if an import is being rewritten or handled specially
|
|
512
|
-
if (rawSource !== source && !source.startsWith('juxscript')) {
|
|
513
|
-
console.log(` 📦 Processing Import: "${rawSource}" -> "${source}" (in ${filePath})`);
|
|
514
|
-
}
|
|
515
|
-
|
|
516
|
-
if (!mergedImports.has(source)) mergedImports.set(source, { defaults: new Set(), named: new Set(), namespace: null });
|
|
517
|
-
const storage = mergedImports.get(source);
|
|
518
|
-
node.specifiers.forEach(spec => {
|
|
519
|
-
if (spec.type === 'ImportDefaultSpecifier') storage.defaults.add(spec.local.name);
|
|
520
|
-
else if (spec.type === 'ImportSpecifier') storage.named.add(spec.imported.name !== spec.local.name ? `${spec.imported.name} as ${spec.local.name}` : spec.imported.name);
|
|
521
|
-
else if (spec.type === 'ImportNamespaceSpecifier') storage.namespace = spec.local.name;
|
|
522
|
-
});
|
|
523
|
-
}
|
|
524
|
-
} catch (e) {
|
|
525
|
-
console.warn(` ⚠️ Failed to process import in bundle:`, item);
|
|
526
|
-
}
|
|
527
|
-
}
|
|
528
|
-
|
|
529
|
-
const filteredImports = [];
|
|
530
|
-
mergedImports.forEach((storage, source) => {
|
|
531
|
-
if (storage.namespace) filteredImports.push(`import * as ${storage.namespace} from '${source}';`);
|
|
532
|
-
const parts = [];
|
|
533
|
-
if (storage.defaults.size > 0) parts.push(Array.from(storage.defaults)[0]);
|
|
534
|
-
if (storage.named.size > 0) parts.push(`{ ${Array.from(storage.named).sort().join(', ')} }`);
|
|
535
|
-
if (storage.defaults.size === 0 && storage.named.size === 0 && !storage.namespace) filteredImports.push(`import '${source}';`);
|
|
536
|
-
else if (parts.length > 0) filteredImports.push(`import ${parts.join(', ')} from '${source}';`);
|
|
537
|
-
});
|
|
538
|
-
|
|
539
|
-
const juxDebugUtils = `
|
|
540
|
-
// ============================================
|
|
541
|
-
// JUX DEBUG UTILITIES
|
|
542
|
-
// ============================================
|
|
543
|
-
const JUX_DEBUG = true;
|
|
544
|
-
|
|
545
|
-
const JuxError = {
|
|
546
|
-
sourceMap: {},
|
|
547
|
-
|
|
548
|
-
register(viewName, sourceFile) {
|
|
549
|
-
this.sourceMap[viewName] = sourceFile;
|
|
550
|
-
},
|
|
551
|
-
|
|
552
|
-
// Global Error Handler Setup
|
|
553
|
-
init() {
|
|
554
|
-
window.addEventListener('error', (event) => {
|
|
555
|
-
JuxError.displayError('Uncaught Exception', event.error);
|
|
556
|
-
});
|
|
557
|
-
window.addEventListener('unhandledrejection', (event) => {
|
|
558
|
-
JuxError.displayError('Unhandled Promise Rejection', event.reason);
|
|
559
|
-
});
|
|
560
|
-
},
|
|
561
|
-
|
|
562
|
-
displayError(title, error) {
|
|
563
|
-
console.error(\`🚨 JUX \${title}\`, error);
|
|
564
|
-
const app = document.getElementById('app');
|
|
565
|
-
if (app) {
|
|
566
|
-
// Prevent multiple error overlays stacking
|
|
567
|
-
if (document.getElementById('jux-error-overlay')) return;
|
|
568
|
-
|
|
569
|
-
app.innerHTML = \`
|
|
570
|
-
<div id="jux-error-overlay" style="font-family: monospace; background: #1e1e1e; color: #ff6b6b; min-height: 100vh; padding: 2rem; position:fixed; top:0; left:0; width:100%; z-index:99999;">
|
|
571
|
-
<div style="display: flex; align-items: center; gap: 1rem; margin-bottom: 2rem;">
|
|
572
|
-
<div style="font-size: 2rem;">🚨</div>
|
|
573
|
-
<div>
|
|
574
|
-
<h1 style="margin:0; font-size: 1.5rem; color: #ff6b6b;">JUX Runtime Error</h1>
|
|
575
|
-
<div style="color: #888; margin-top: 4px;">\${title}</div>
|
|
576
|
-
</div>
|
|
577
|
-
</div>
|
|
578
|
-
|
|
579
|
-
<pre style="background: #2d2d2d; padding: 20px; border-radius: 8px; overflow-x: auto; color: #e0e0e0; border-left: 4px solid #ff6b6b; font-size: 14px; line-height: 1.5;">\${error?.stack || error?.message || String(error)}</pre>
|
|
580
|
-
|
|
581
|
-
<button onclick="window.location.reload()" style="margin-top:20px; padding: 10px 20px; background: #333; color: white; border: 1px solid #555; cursor: pointer; border-radius: 4px;">Reload Application</button>
|
|
582
|
-
</div>
|
|
583
|
-
\`;
|
|
584
|
-
}
|
|
585
|
-
},
|
|
586
|
-
|
|
587
|
-
wrap(viewName, viewFn) {
|
|
588
|
-
return function() {
|
|
589
|
-
const sourceFile = JuxError.sourceMap[viewName] || 'unknown';
|
|
590
|
-
if (JUX_DEBUG) console.log(\`🚀 [JUX] Rendering: \${viewName} (\${sourceFile})\`);
|
|
591
|
-
|
|
592
|
-
try {
|
|
593
|
-
return viewFn.apply(this, arguments);
|
|
594
|
-
} catch (error) {
|
|
595
|
-
// Enhance error object with view context if possible
|
|
596
|
-
error.message = \`[View: \${viewName}] \${error.message}\`;
|
|
597
|
-
JuxError.displayError('View Rendering Error', error);
|
|
598
|
-
// We do NOT rethrow here to prevent console noise, as we handled it UI-wise.
|
|
599
|
-
// However, stopping execution flow is implicitly handled by not returning valid DOM.
|
|
600
|
-
}
|
|
601
|
-
};
|
|
602
|
-
}
|
|
603
|
-
};
|
|
604
|
-
|
|
605
|
-
// Initialize Global Handlers
|
|
606
|
-
JuxError.init();
|
|
607
|
-
`;
|
|
608
|
-
|
|
609
|
-
return `// Generated Jux Router Bundle
|
|
610
|
-
${filteredImports.join('\n')}
|
|
611
|
-
|
|
612
|
-
${juxDebugUtils}
|
|
613
|
-
|
|
614
|
-
// SHARED MODULES
|
|
615
|
-
${Array.from(sharedModules.values()).filter(c => c.trim()).join('\n\n')}
|
|
616
|
-
|
|
617
|
-
// VIEWS (Wrapped in JuxError)
|
|
618
|
-
${views.filter(v => v.trim()).join('\n\n')}
|
|
619
|
-
|
|
620
|
-
function JuxNotFound() { jux.heading(1, { text: '404 - Page Not Found' }).render('#app'); return document.getElementById('app'); }
|
|
621
|
-
function JuxForbidden() { jux.heading(1, { text: '403 - Forbidden' }).render('#app'); return document.getElementById('app'); }
|
|
622
|
-
|
|
623
|
-
const routes = {
|
|
624
|
-
${routes.map(r => ` '${r.path}': ${r.functionName}`).join(',\n')}
|
|
625
|
-
};
|
|
626
|
-
|
|
627
|
-
const app = document.getElementById('app');
|
|
628
|
-
function render() {
|
|
629
|
-
let path = location.pathname;
|
|
630
|
-
let view = routes[path] || (path.endsWith('/') ? (routes[path + 'index'] || routes[path.slice(0, -1) + '/index']) : routes[path + '/index']);
|
|
631
|
-
view = view || JuxNotFound;
|
|
632
|
-
app.innerHTML = '';
|
|
633
|
-
app.removeAttribute('data-jux-page');
|
|
634
|
-
view();
|
|
635
|
-
const pageName = Object.entries(routes).find(([p, v]) => v === view)?.[0] || 'not-found';
|
|
636
|
-
app.setAttribute('data-jux-page', pageName.replace(/^\\\//, '').replace(/\\\//g, '-'));
|
|
637
|
-
}
|
|
638
|
-
document.addEventListener('click', e => {
|
|
639
|
-
const a = e.target.closest('a');
|
|
640
|
-
if (!a || a.dataset.router === 'false' || new URL(a.href).origin !== location.origin) return;
|
|
641
|
-
e.preventDefault();
|
|
642
|
-
history.pushState({}, '', a.href);
|
|
643
|
-
render();
|
|
644
|
-
});
|
|
645
|
-
window.addEventListener('popstate', render);
|
|
646
|
-
render();
|
|
647
|
-
`;
|
|
648
|
-
}
|
|
649
|
-
|
|
650
|
-
export function generateIndexHtml(distDir, bundleResult, options = {}) {
|
|
651
|
-
const { mainJsFilename = 'main.js', vendoredPaths = {} } = bundleResult || {};
|
|
652
|
-
const { isDev = false, wsPort = 3001 } = options;
|
|
653
|
-
const importMapScript = generateVendorImportMap(vendoredPaths);
|
|
654
|
-
const hotReloadScript = isDev ? `
|
|
655
|
-
<script>
|
|
656
|
-
(function() {
|
|
657
|
-
let ws = new WebSocket('ws://' + window.location.hostname + ':${wsPort}');
|
|
658
|
-
ws.onmessage = (msg) => { if (JSON.parse(msg.data).type === 'reload') window.location.reload(); };
|
|
659
|
-
})();
|
|
660
|
-
</script>` : '';
|
|
661
|
-
|
|
662
|
-
const html = `<!DOCTYPE html>
|
|
663
|
-
<html lang="en">
|
|
664
|
-
<head>
|
|
665
|
-
<meta charset="UTF-8">
|
|
666
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
667
|
-
<title>Jux Application</title>
|
|
668
|
-
</head>
|
|
669
|
-
<body data-theme="">
|
|
670
|
-
<div id="app"></div>
|
|
671
|
-
${importMapScript}
|
|
672
|
-
<script type="module" src="/${mainJsFilename}"></script>
|
|
673
|
-
${hotReloadScript}
|
|
674
|
-
</body>
|
|
675
|
-
</html>`;
|
|
676
|
-
fs.writeFileSync(path.join(distDir, 'index.html'), html);
|
|
677
|
-
}
|
|
678
|
-
|
|
679
|
-
async function vendorExternalDependencies(externalDeps, distDir) {
|
|
680
|
-
const vendorDir = path.join(distDir, 'vendor');
|
|
681
|
-
if (!fs.existsSync(vendorDir)) fs.mkdirSync(vendorDir, { recursive: true });
|
|
682
|
-
const vendoredPaths = {};
|
|
683
|
-
for (const dep of externalDeps) {
|
|
684
|
-
try {
|
|
685
|
-
const nodeModulePath = path.join(process.cwd(), 'node_modules', dep);
|
|
686
|
-
if (!fs.existsSync(nodeModulePath)) continue;
|
|
687
|
-
const pkg = JSON.parse(fs.readFileSync(path.join(nodeModulePath, 'package.json'), 'utf-8'));
|
|
688
|
-
const entryPoints = [pkg.module, pkg.browser, pkg.main, 'index.js'].filter(Boolean);
|
|
689
|
-
let entryFile = entryPoints.map(e => path.join(nodeModulePath, e)).find(p => fs.existsSync(p));
|
|
690
|
-
if (!entryFile) continue;
|
|
691
|
-
await esbuild.build({ entryPoints: [entryFile], bundle: true, format: 'esm', outfile: path.join(vendorDir, `${dep}.js`), platform: 'browser', target: 'es2020', logLevel: 'silent' });
|
|
692
|
-
vendoredPaths[dep] = `/vendor/${dep}.js`;
|
|
693
|
-
} catch (err) { }
|
|
694
|
-
}
|
|
695
|
-
return vendoredPaths;
|
|
696
|
-
}
|
|
697
|
-
|
|
698
|
-
function generateVendorImportMap(vendoredPaths) {
|
|
699
|
-
const imports = {
|
|
700
|
-
"juxscript": "/lib/componentsv2/index.js",
|
|
701
|
-
"juxscript/": "/lib/componentsv2/",
|
|
702
|
-
...vendoredPaths
|
|
703
|
-
};
|
|
704
|
-
Object.keys(imports).forEach(key => { if (imports[key].startsWith('./')) imports[key] = imports[key].substring(1); });
|
|
705
|
-
return `<script type="importmap">\n${JSON.stringify({ imports }, null, 2)}\n</script>`;
|
|
706
|
-
}
|