juxscript 1.0.67 → 1.0.68
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 +66 -307
- package/create/index.jux +0 -90
- package/lib/componentsv2/element/component.js +23 -0
- package/lib/componentsv2/element/component.js.map +1 -0
- package/lib/componentsv2/element/component.ts +32 -0
- package/lib/componentsv2/element/engine.js +51 -0
- package/lib/componentsv2/element/engine.js.map +1 -0
- package/lib/componentsv2/element/engine.ts +69 -0
- package/lib/componentsv2/element/skin.js +51 -0
- package/lib/componentsv2/element/skin.js.map +1 -0
- package/lib/componentsv2/element/skin.ts +60 -0
- package/lib/componentsv2/element/structure.css +16 -0
- package/lib/componentsv2/index.js +3 -2
- package/lib/componentsv2/index.js.map +1 -1
- package/lib/componentsv2/index.ts +4 -6
- package/lib/componentsv2/tools/Scaffold.js +0 -16
- package/machinery/compiler.js +103 -739
- package/machinery/server.js +16 -112
- package/machinery/watcher.js +16 -106
- package/package.json +1 -1
- package/create/all.jux +0 -343
package/machinery/compiler.js
CHANGED
|
@@ -5,41 +5,22 @@ import * as acorn from 'acorn';
|
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* Copy and build the JUX library from TypeScript to JavaScript
|
|
8
|
-
*
|
|
9
|
-
* @param {string} projectRoot - Root directory containing lib/
|
|
10
|
-
* @param {string} distDir - Destination directory for built files
|
|
11
8
|
*/
|
|
12
9
|
export async function copyLibToOutput(projectRoot, distDir) {
|
|
13
|
-
// Simplified lib path resolution
|
|
14
10
|
const libSrc = path.resolve(projectRoot, '../lib');
|
|
15
|
-
|
|
16
11
|
if (!fs.existsSync(libSrc)) {
|
|
17
12
|
throw new Error(`lib/ directory not found at ${libSrc}`);
|
|
18
13
|
}
|
|
19
|
-
|
|
20
14
|
const libDest = path.join(distDir, 'lib');
|
|
21
15
|
|
|
22
|
-
console.log('📦 Building TypeScript library...');
|
|
23
|
-
console.log(` From: ${libSrc}`);
|
|
24
|
-
console.log(` To: ${libDest}`);
|
|
25
|
-
|
|
26
16
|
if (fs.existsSync(libDest)) {
|
|
27
17
|
fs.rmSync(libDest, { recursive: true });
|
|
28
18
|
}
|
|
29
|
-
|
|
30
19
|
fs.mkdirSync(libDest, { recursive: true });
|
|
31
20
|
|
|
32
|
-
// Find all TypeScript entry points
|
|
33
21
|
const tsFiles = findFiles(libSrc, '.ts');
|
|
22
|
+
if (tsFiles.length === 0) return;
|
|
34
23
|
|
|
35
|
-
if (tsFiles.length === 0) {
|
|
36
|
-
console.warn('⚠️ No TypeScript files found in lib/');
|
|
37
|
-
return;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
console.log(` Found ${tsFiles.length} TypeScript files`);
|
|
41
|
-
|
|
42
|
-
// Build all TypeScript files with esbuild
|
|
43
24
|
try {
|
|
44
25
|
await esbuild.build({
|
|
45
26
|
entryPoints: tsFiles,
|
|
@@ -49,84 +30,40 @@ export async function copyLibToOutput(projectRoot, distDir) {
|
|
|
49
30
|
outbase: libSrc,
|
|
50
31
|
platform: 'browser',
|
|
51
32
|
target: 'es2020',
|
|
52
|
-
loader: {
|
|
53
|
-
|
|
54
|
-
},
|
|
55
|
-
logLevel: 'warning'
|
|
33
|
+
loader: { '.ts': 'ts' },
|
|
34
|
+
logLevel: 'error'
|
|
56
35
|
});
|
|
57
|
-
|
|
58
|
-
console.log(' ✓ TypeScript compiled to JavaScript');
|
|
59
|
-
|
|
60
|
-
// Copy non-TS files (CSS, HTML, etc.)
|
|
61
|
-
console.log(' Copying lib assets...');
|
|
62
36
|
copyNonTsFiles(libSrc, libDest);
|
|
63
|
-
console.log(' ✓ Lib assets copied');
|
|
64
|
-
|
|
65
37
|
} catch (err) {
|
|
66
38
|
console.error('❌ Failed to build TypeScript:', err.message);
|
|
67
39
|
throw err;
|
|
68
40
|
}
|
|
69
|
-
|
|
70
|
-
console.log('✅ Library ready\n');
|
|
71
41
|
}
|
|
72
42
|
|
|
73
43
|
/**
|
|
74
44
|
* Copy project assets (CSS, JS, images) from jux/ to dist/
|
|
75
|
-
*
|
|
76
|
-
* @param {string} projectRoot - Source directory (jux/)
|
|
77
|
-
* @param {string} distDir - Destination directory (jux-dist/)
|
|
78
45
|
*/
|
|
79
46
|
export async function copyProjectAssets(projectRoot, distDir) {
|
|
80
|
-
console.log('📦 Copying project assets...');
|
|
81
|
-
|
|
82
|
-
// Find all CSS and JS files in project root (excluding node_modules, dist, .git)
|
|
83
47
|
const allFiles = [];
|
|
84
48
|
findProjectFiles(projectRoot, ['.css', '.js'], allFiles, projectRoot);
|
|
85
49
|
|
|
86
|
-
console.log(` Found ${allFiles.length} asset file(s)`);
|
|
87
|
-
|
|
88
50
|
for (const srcPath of allFiles) {
|
|
89
51
|
const relativePath = path.relative(projectRoot, srcPath);
|
|
90
52
|
const destPath = path.join(distDir, relativePath);
|
|
91
53
|
const destDir = path.dirname(destPath);
|
|
92
|
-
|
|
93
|
-
// Create destination directory if needed
|
|
94
|
-
if (!fs.existsSync(destDir)) {
|
|
95
|
-
fs.mkdirSync(destDir, { recursive: true });
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
// Copy file
|
|
54
|
+
if (!fs.existsSync(destDir)) fs.mkdirSync(destDir, { recursive: true });
|
|
99
55
|
fs.copyFileSync(srcPath, destPath);
|
|
100
|
-
console.log(` ✓ ${relativePath}`);
|
|
101
56
|
}
|
|
102
|
-
|
|
103
|
-
console.log('✅ Project assets copied\n');
|
|
104
57
|
}
|
|
105
58
|
|
|
106
59
|
/**
|
|
107
|
-
* Transpile TypeScript files from jux/ to jux-dist
|
|
108
|
-
*
|
|
109
|
-
* @param {string} srcDir - Source directory (jux/)
|
|
110
|
-
* @param {string} destDir - Destination directory (jux-dist/)
|
|
111
|
-
* @example
|
|
112
|
-
* // jux/samples/mypage.ts -> jux-dist/samples/mypage.js
|
|
113
|
-
* await transpileProjectTypeScript('jux/', 'jux-dist/');
|
|
60
|
+
* Transpile TypeScript files from jux/ to jux-dist/
|
|
114
61
|
*/
|
|
115
62
|
export async function transpileProjectTypeScript(srcDir, destDir) {
|
|
116
|
-
console.log('🔷 Transpiling TypeScript files...');
|
|
117
|
-
|
|
118
|
-
// Find all TypeScript files in the project
|
|
119
63
|
const tsFiles = findFiles(srcDir, '.ts');
|
|
120
|
-
|
|
121
|
-
if (tsFiles.length === 0) {
|
|
122
|
-
console.log(' No TypeScript files found in project');
|
|
123
|
-
return;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
console.log(` Found ${tsFiles.length} TypeScript file(s)`);
|
|
64
|
+
if (tsFiles.length === 0) return;
|
|
127
65
|
|
|
128
66
|
try {
|
|
129
|
-
// Build all TypeScript files with esbuild
|
|
130
67
|
await esbuild.build({
|
|
131
68
|
entryPoints: tsFiles,
|
|
132
69
|
bundle: false,
|
|
@@ -135,21 +72,9 @@ export async function transpileProjectTypeScript(srcDir, destDir) {
|
|
|
135
72
|
outbase: srcDir,
|
|
136
73
|
platform: 'browser',
|
|
137
74
|
target: 'es2020',
|
|
138
|
-
loader: {
|
|
139
|
-
|
|
140
|
-
},
|
|
141
|
-
logLevel: 'warning'
|
|
142
|
-
});
|
|
143
|
-
|
|
144
|
-
// Log each transpiled file
|
|
145
|
-
tsFiles.forEach(tsFile => {
|
|
146
|
-
const relativePath = path.relative(srcDir, tsFile);
|
|
147
|
-
const jsPath = relativePath.replace(/\.ts$/, '.js');
|
|
148
|
-
console.log(` ✓ ${relativePath} → ${jsPath}`);
|
|
75
|
+
loader: { '.ts': 'ts' },
|
|
76
|
+
logLevel: 'error'
|
|
149
77
|
});
|
|
150
|
-
|
|
151
|
-
console.log('✅ TypeScript transpiled\n');
|
|
152
|
-
|
|
153
78
|
} catch (err) {
|
|
154
79
|
console.error('❌ Failed to transpile TypeScript:', err.message);
|
|
155
80
|
throw err;
|
|
@@ -157,45 +82,26 @@ export async function transpileProjectTypeScript(srcDir, destDir) {
|
|
|
157
82
|
}
|
|
158
83
|
|
|
159
84
|
/**
|
|
160
|
-
* Copy presets
|
|
161
|
-
*
|
|
162
|
-
* @param {string} packageRoot - Source package root directory
|
|
163
|
-
* @param {string} distDir - Destination directory
|
|
85
|
+
* Copy presets (No-op log silenced)
|
|
164
86
|
*/
|
|
165
87
|
export async function copyPresetsToOutput(packageRoot, distDir) {
|
|
166
|
-
//
|
|
167
|
-
console.log('ℹ️ Presets available via import maps (not copied to dist)\n');
|
|
88
|
+
// No-op
|
|
168
89
|
}
|
|
169
90
|
|
|
170
|
-
/**
|
|
171
|
-
* Recursively find files with a specific extension
|
|
172
|
-
*
|
|
173
|
-
* @param {string} dir - Directory to search
|
|
174
|
-
* @param {string} extension - File extension (e.g., '.ts')
|
|
175
|
-
* @param {string[]} fileList - Accumulator for found files
|
|
176
|
-
* @returns {string[]} Array of file paths
|
|
177
|
-
*/
|
|
178
91
|
function findFiles(dir, extension, fileList = []) {
|
|
179
92
|
const files = fs.readdirSync(dir);
|
|
180
|
-
|
|
181
93
|
files.forEach(file => {
|
|
182
94
|
const filePath = path.join(dir, file);
|
|
183
95
|
const stat = fs.statSync(filePath);
|
|
184
|
-
|
|
185
96
|
if (stat.isDirectory()) {
|
|
186
97
|
findFiles(filePath, extension, fileList);
|
|
187
98
|
} else if (file.endsWith(extension)) {
|
|
188
99
|
fileList.push(filePath);
|
|
189
100
|
}
|
|
190
101
|
});
|
|
191
|
-
|
|
192
102
|
return fileList;
|
|
193
103
|
}
|
|
194
104
|
|
|
195
|
-
/**
|
|
196
|
-
* Copy non-TypeScript files (CSS, JSON, JS, SVG, etc.)
|
|
197
|
-
* ✅ Skip layouts folder
|
|
198
|
-
*/
|
|
199
105
|
function copyNonTsFiles(src, dest) {
|
|
200
106
|
const entries = fs.readdirSync(src, { withFileTypes: true });
|
|
201
107
|
|
|
@@ -204,17 +110,17 @@ function copyNonTsFiles(src, dest) {
|
|
|
204
110
|
const destPath = path.join(dest, entry.name);
|
|
205
111
|
|
|
206
112
|
if (entry.isDirectory()) {
|
|
207
|
-
|
|
208
|
-
if (
|
|
209
|
-
continue;
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
if (!fs.existsSync(destPath)) {
|
|
213
|
-
fs.mkdirSync(destPath, { recursive: true });
|
|
214
|
-
}
|
|
113
|
+
if (entry.name === 'layouts') continue;
|
|
114
|
+
if (!fs.existsSync(destPath)) fs.mkdirSync(destPath, { recursive: true });
|
|
215
115
|
copyNonTsFiles(srcPath, destPath);
|
|
216
116
|
} else if (entry.isFile()) {
|
|
217
117
|
const ext = path.extname(entry.name);
|
|
118
|
+
if (ext === '.js' || ext === '.map') {
|
|
119
|
+
let tsSibling = '';
|
|
120
|
+
if (ext === '.js') tsSibling = srcPath.replace(/\.js$/, '.ts');
|
|
121
|
+
else if (ext === '.map') tsSibling = srcPath.replace(/\.js\.map$/, '.ts');
|
|
122
|
+
if (tsSibling && fs.existsSync(tsSibling)) continue;
|
|
123
|
+
}
|
|
218
124
|
if (['.css', '.json', '.js', '.svg', '.png', '.jpg', '.jpeg', '.gif', '.webp'].includes(ext)) {
|
|
219
125
|
fs.copyFileSync(srcPath, destPath);
|
|
220
126
|
}
|
|
@@ -222,127 +128,62 @@ function copyNonTsFiles(src, dest) {
|
|
|
222
128
|
}
|
|
223
129
|
}
|
|
224
130
|
|
|
225
|
-
/**
|
|
226
|
-
* Find project files with specific extensions, excluding certain directories
|
|
227
|
-
*
|
|
228
|
-
* @param {string} dir - Directory to search
|
|
229
|
-
* @param {string[]} extensions - File extensions to find
|
|
230
|
-
* @param {string[]} fileList - Accumulator for found files
|
|
231
|
-
* @param {string} rootDir - Root directory for relative paths
|
|
232
|
-
* @param {string[]} excludeDirs - Directories to exclude
|
|
233
|
-
* @returns {string[]} Array of file paths
|
|
234
|
-
*/
|
|
235
131
|
function findProjectFiles(dir, extensions, fileList = [], rootDir = dir, excludeDirs = ['node_modules', 'jux-dist', '.git', 'lib']) {
|
|
236
132
|
if (!fs.existsSync(dir)) return fileList;
|
|
237
|
-
|
|
238
133
|
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
239
|
-
|
|
240
134
|
for (const entry of entries) {
|
|
241
135
|
const fullPath = path.join(dir, entry.name);
|
|
242
|
-
|
|
243
136
|
if (entry.isDirectory()) {
|
|
244
|
-
|
|
245
|
-
if (excludeDirs.includes(entry.name)) {
|
|
246
|
-
continue;
|
|
247
|
-
}
|
|
137
|
+
if (excludeDirs.includes(entry.name)) continue;
|
|
248
138
|
findProjectFiles(fullPath, extensions, fileList, rootDir, excludeDirs);
|
|
249
139
|
} else {
|
|
250
|
-
// Check if file has one of the desired extensions
|
|
251
140
|
const hasExtension = extensions.some(ext => entry.name.endsWith(ext));
|
|
252
|
-
if (hasExtension)
|
|
253
|
-
fileList.push(fullPath);
|
|
254
|
-
}
|
|
141
|
+
if (hasExtension) fileList.push(fullPath);
|
|
255
142
|
}
|
|
256
143
|
}
|
|
257
|
-
|
|
258
144
|
return fileList;
|
|
259
145
|
}
|
|
260
146
|
|
|
261
147
|
/**
|
|
262
148
|
* Bundle all .jux files into a single router-based main.js
|
|
263
|
-
* ✅ MODIFIED: Vendor dependencies locally + User Routes + Smart Import Merging
|
|
264
149
|
*/
|
|
265
150
|
export async function bundleJuxFilesToRouter(projectRoot, distDir, options = {}) {
|
|
266
|
-
const startTime = performance.now();
|
|
267
151
|
const { routePrefix = '', config } = options;
|
|
268
|
-
|
|
269
|
-
console.log('🔀 Bundling .jux files into router...');
|
|
270
|
-
|
|
271
152
|
const juxFiles = findFiles(projectRoot, '.jux');
|
|
272
153
|
|
|
273
154
|
if (juxFiles.length === 0) {
|
|
274
|
-
|
|
275
|
-
// ✅ FIX: Return empty result instead of undefined
|
|
276
|
-
return {
|
|
277
|
-
mainJsFilename: 'main.js',
|
|
278
|
-
routes: [],
|
|
279
|
-
external: new Set(),
|
|
280
|
-
vendoredPaths: {}
|
|
281
|
-
};
|
|
155
|
+
return { mainJsFilename: 'main.js', routes: [], external: new Set(), vendoredPaths: {} };
|
|
282
156
|
}
|
|
283
157
|
|
|
284
158
|
const pages = config?.pages || {};
|
|
285
|
-
|
|
286
|
-
const fileTimings = [];
|
|
287
159
|
const views = [];
|
|
288
160
|
const routes = [];
|
|
289
|
-
const sharedModules = new Map();
|
|
290
|
-
|
|
291
|
-
// ✅ CHANGE: imports collection now stores context { code, filePath }
|
|
161
|
+
const sharedModules = new Map();
|
|
292
162
|
const allImports = [];
|
|
293
|
-
const externalModules = new Set();
|
|
294
|
-
|
|
295
|
-
// ✅ MAP for user config routing: Key = relative file path, Value = CleanFunctionName
|
|
163
|
+
const externalModules = new Set();
|
|
296
164
|
const fileToFunction = new Map();
|
|
297
165
|
|
|
298
166
|
for (const juxFile of juxFiles) {
|
|
299
|
-
const fileStartTime = performance.now();
|
|
300
|
-
|
|
301
167
|
const relativePath = path.relative(projectRoot, juxFile);
|
|
302
168
|
const parsedPath = path.parse(relativePath);
|
|
169
|
+
const rawFunctionName = parsedPath.dir ? `${parsedPath.dir.replace(/\//g, '_')}_${parsedPath.name}` : parsedPath.name;
|
|
170
|
+
const cleanFunctionName = rawFunctionName.replace(/[-_]/g, ' ').split(' ').map(w => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase()).join('');
|
|
303
171
|
|
|
304
|
-
const rawFunctionName = parsedPath.dir
|
|
305
|
-
? `${parsedPath.dir.replace(/\//g, '_')}_${parsedPath.name}`
|
|
306
|
-
: parsedPath.name;
|
|
307
|
-
|
|
308
|
-
const cleanFunctionName = rawFunctionName
|
|
309
|
-
.replace(/[-_]/g, ' ')
|
|
310
|
-
.split(' ')
|
|
311
|
-
.map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
|
|
312
|
-
.join('');
|
|
313
|
-
|
|
314
|
-
// ✅ Store mapping for Config Route Resolution
|
|
315
|
-
// Store both 'path/file.jux' and normalized versions
|
|
316
172
|
fileToFunction.set(relativePath, cleanFunctionName);
|
|
317
173
|
fileToFunction.set(relativePath.split(path.sep).join('/'), cleanFunctionName);
|
|
318
174
|
|
|
319
175
|
const routePath = routePrefix + '/' + (parsedPath.dir ? `${parsedPath.dir}/` : '') + parsedPath.name;
|
|
320
176
|
const cleanRoutePath = routePath.replace(/\/+/g, '/');
|
|
321
|
-
|
|
322
177
|
const juxContent = fs.readFileSync(juxFile, 'utf-8');
|
|
323
178
|
|
|
324
|
-
// ✅ FIX: Extract imports AND detect external modules in one pass
|
|
325
179
|
try {
|
|
326
|
-
const ast = acorn.parse(juxContent, {
|
|
327
|
-
ecmaVersion: 'latest',
|
|
328
|
-
sourceType: 'module'
|
|
329
|
-
});
|
|
330
|
-
|
|
180
|
+
const ast = acorn.parse(juxContent, { ecmaVersion: 'latest', sourceType: 'module' });
|
|
331
181
|
ast.body.forEach(node => {
|
|
332
182
|
if (node.type === 'ImportDeclaration') {
|
|
333
183
|
const importStatement = juxContent.slice(node.start, node.end);
|
|
334
|
-
|
|
335
|
-
// ✅ Store Object with context instead of raw string
|
|
336
|
-
allImports.push({
|
|
337
|
-
code: importStatement,
|
|
338
|
-
filePath: relativePath // relative to projectRoot
|
|
339
|
-
});
|
|
340
|
-
|
|
184
|
+
allImports.push({ code: importStatement, filePath: relativePath });
|
|
341
185
|
const moduleName = node.source.value;
|
|
342
|
-
if (!moduleName.startsWith('.') &&
|
|
343
|
-
!moduleName.startsWith('/') &&
|
|
344
|
-
!moduleName.startsWith('http') &&
|
|
345
|
-
!moduleName.startsWith('juxscript')) {
|
|
186
|
+
if (!moduleName.startsWith('.') && !moduleName.startsWith('/') && !moduleName.startsWith('http') && !moduleName.startsWith('juxscript')) {
|
|
346
187
|
externalModules.add(moduleName);
|
|
347
188
|
}
|
|
348
189
|
}
|
|
@@ -351,69 +192,37 @@ export async function bundleJuxFilesToRouter(projectRoot, distDir, options = {})
|
|
|
351
192
|
console.warn(` ⚠️ Failed to parse imports in ${relativePath}, skipping`);
|
|
352
193
|
}
|
|
353
194
|
|
|
354
|
-
// Check if file has exports
|
|
355
195
|
const hasExports = /^\s*export\s+(const|let|function|class|{)/m.test(juxContent);
|
|
356
|
-
|
|
357
196
|
if (hasExports) {
|
|
358
|
-
// ✅ Store exports with UNIQUE KEY (file path)
|
|
359
197
|
const exportKey = relativePath;
|
|
360
198
|
const exportCode = extractSharedModule(juxContent, rawFunctionName, relativePath);
|
|
361
|
-
|
|
362
|
-
if (exportCode.trim()) {
|
|
363
|
-
sharedModules.set(exportKey, exportCode);
|
|
364
|
-
}
|
|
199
|
+
if (exportCode.trim()) sharedModules.set(exportKey, exportCode);
|
|
365
200
|
}
|
|
366
201
|
|
|
367
|
-
|
|
368
|
-
const viewFunction = transformJuxToViewFunction(
|
|
369
|
-
juxContent,
|
|
370
|
-
rawFunctionName,
|
|
371
|
-
parsedPath.name,
|
|
372
|
-
relativePath,
|
|
373
|
-
sharedModules
|
|
374
|
-
);
|
|
375
|
-
|
|
202
|
+
const viewFunction = transformJuxToViewFunction(juxContent, rawFunctionName, parsedPath.name, relativePath, sharedModules);
|
|
376
203
|
views.push(viewFunction);
|
|
377
204
|
|
|
378
|
-
// Auto-route based on filesystem
|
|
379
205
|
if (config?.defaults?.autoRoute !== false) {
|
|
380
206
|
routes.push({ path: cleanRoutePath, functionName: cleanFunctionName });
|
|
381
207
|
}
|
|
382
|
-
|
|
383
|
-
const fileTime = performance.now() - fileStartTime;
|
|
384
|
-
fileTimings.push({ file: relativePath, time: fileTime });
|
|
385
|
-
|
|
386
|
-
const exportNote = hasExports ? ' [+exports]' : '';
|
|
387
|
-
// console.log(` ✓ ${relativePath} → ${cleanFunctionName}()${exportNote} (${fileTime.toFixed(1)}ms)`);
|
|
388
208
|
}
|
|
389
209
|
|
|
390
|
-
// ✅ PROCESS USER CONFIGURED ROUTES
|
|
391
210
|
if (pages) {
|
|
392
|
-
console.log(' 🗺️ Mapping user-configured routes...');
|
|
393
|
-
|
|
394
211
|
const resolveAndAddRoute = (urlPath, targetFile) => {
|
|
395
|
-
// Clean up target file path (remove leading ./ and /)
|
|
396
212
|
const cleanTarget = targetFile.replace(/^(\.\/|\/)/, '');
|
|
397
213
|
const funcName = fileToFunction.get(cleanTarget);
|
|
398
|
-
|
|
399
214
|
if (funcName) {
|
|
400
|
-
// User routes take precedence (prepend to list)
|
|
401
215
|
routes.unshift({ path: urlPath, functionName: funcName });
|
|
402
|
-
console.log(` ➕ ${urlPath} → ${funcName}`);
|
|
403
216
|
} else {
|
|
404
|
-
console.warn(` ⚠️ Route target not found: ${targetFile}
|
|
217
|
+
console.warn(` ⚠️ Route target not found: ${targetFile}`);
|
|
405
218
|
}
|
|
406
219
|
};
|
|
407
|
-
|
|
408
220
|
Object.entries(pages).forEach(([key, value]) => {
|
|
409
221
|
if (typeof value === 'string') {
|
|
410
|
-
// Direct mapping: '/' -> './experiments/state.jux'
|
|
411
222
|
resolveAndAddRoute(key, value);
|
|
412
223
|
} else if (typeof value === 'object') {
|
|
413
|
-
// Group mapping
|
|
414
224
|
const prefix = value.prefix || '';
|
|
415
225
|
const groupRoutes = value.routes || {};
|
|
416
|
-
|
|
417
226
|
Object.entries(groupRoutes).forEach(([routePath, targetFile]) => {
|
|
418
227
|
const fullPath = (prefix + routePath).replace(/\/+/g, '/');
|
|
419
228
|
resolveAndAddRoute(fullPath, targetFile);
|
|
@@ -422,564 +231,199 @@ export async function bundleJuxFilesToRouter(projectRoot, distDir, options = {})
|
|
|
422
231
|
});
|
|
423
232
|
}
|
|
424
233
|
|
|
425
|
-
// ✅ Show slowest files if any took >50ms
|
|
426
|
-
const slowFiles = fileTimings.filter(f => f.time > 50).sort((a, b) => b.time - a.time);
|
|
427
|
-
if (slowFiles.length > 0) {
|
|
428
|
-
console.log(`\n ⚠️ Slowest files:`);
|
|
429
|
-
slowFiles.slice(0, 3).forEach(f => {
|
|
430
|
-
console.log(` ${f.file}: ${f.time.toFixed(0)}ms`);
|
|
431
|
-
});
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
const bundleStartTime = performance.now();
|
|
435
|
-
|
|
436
|
-
// ✅ CHANGE: Single summary instead of verbose list
|
|
437
|
-
if (externalModules.size > 0) {
|
|
438
|
-
console.log(`\n 📦 External dependencies: ${Array.from(externalModules).join(', ')}`);
|
|
439
|
-
}
|
|
440
|
-
|
|
441
234
|
const vendoredPaths = await vendorExternalDependencies(externalModules, distDir);
|
|
442
|
-
|
|
443
235
|
const routerCode = generateRouterBundle(views, routes, sharedModules, allImports, projectRoot);
|
|
444
|
-
const bundleGenTime = performance.now() - bundleStartTime;
|
|
445
|
-
|
|
446
236
|
const mainJsFilename = 'main.js';
|
|
447
237
|
const mainJsPath = path.join(distDir, mainJsFilename);
|
|
448
|
-
|
|
449
|
-
const writeStartTime = performance.now();
|
|
450
238
|
fs.writeFileSync(mainJsPath, routerCode);
|
|
451
|
-
const writeTime = performance.now() - writeStartTime;
|
|
452
|
-
|
|
453
|
-
const totalTime = performance.now() - startTime;
|
|
454
|
-
|
|
455
|
-
console.log(` ✓ Generated: ${path.relative(projectRoot, mainJsPath)}`);
|
|
456
|
-
console.log(`\n 📊 Bundle Statistics:`);
|
|
457
|
-
console.log(` Files processed: ${juxFiles.length}`);
|
|
458
|
-
console.log(` External deps: ${externalModules.size}`);
|
|
459
|
-
console.log(` Vendored locally: ${Object.keys(vendoredPaths).length}`);
|
|
460
|
-
console.log(` Bundle size: ${(routerCode.length / 1024).toFixed(1)} KB`);
|
|
461
|
-
console.log(` Total bundle time: ${totalTime.toFixed(0)}ms`);
|
|
462
|
-
console.log('✅ Router bundle complete\n');
|
|
463
239
|
|
|
464
|
-
// ✅ Return bundleResult with vendor info
|
|
465
240
|
return {
|
|
466
241
|
mainJsFilename,
|
|
467
242
|
routes: routes.map(r => ({ path: r.path, functionName: r.functionName })),
|
|
468
|
-
external: externalModules,
|
|
243
|
+
external: externalModules,
|
|
469
244
|
vendoredPaths
|
|
470
245
|
};
|
|
471
246
|
}
|
|
472
247
|
|
|
473
|
-
/**
|
|
474
|
-
* Extract shared module exports from a .jux file using AST parsing
|
|
475
|
-
* ✅ Namespaces exported identifiers to prevent collisions
|
|
476
|
-
*/
|
|
477
248
|
function extractSharedModule(juxContent, moduleName, sourceFilePath) {
|
|
478
249
|
const exportLines = [];
|
|
479
|
-
|
|
480
250
|
try {
|
|
481
|
-
const ast = acorn.parse(juxContent, {
|
|
482
|
-
ecmaVersion: 'latest',
|
|
483
|
-
sourceType: 'module'
|
|
484
|
-
});
|
|
485
|
-
|
|
251
|
+
const ast = acorn.parse(juxContent, { ecmaVersion: 'latest', sourceType: 'module' });
|
|
486
252
|
ast.body.forEach(node => {
|
|
487
|
-
if (node.type === 'ExportNamedDeclaration') {
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
// ✅ Namespace the identifier
|
|
494
|
-
const namespacedCode = namespaceExportedIdentifiers(cleaned, sourceFilePath);
|
|
495
|
-
|
|
496
|
-
exportLines.push(`// From: ${sourceFilePath}\n${namespacedCode}`);
|
|
497
|
-
}
|
|
498
|
-
// Case 2: Named export block (export { foo, bar })
|
|
499
|
-
else if (node.specifiers && node.specifiers.length > 0) {
|
|
500
|
-
console.log(` ℹ️ Skipping named export block in ${sourceFilePath} (declarations already present)`);
|
|
501
|
-
}
|
|
502
|
-
}
|
|
503
|
-
else if (node.type === 'ExportDefaultDeclaration') {
|
|
504
|
-
const exportCode = juxContent.slice(node.start, node.end);
|
|
505
|
-
const cleaned = exportCode.replace(/^export\s+default\s+/, '');
|
|
506
|
-
|
|
507
|
-
// ✅ Namespace default export
|
|
508
|
-
const namespacedCode = namespaceExportedIdentifiers(cleaned, sourceFilePath);
|
|
509
|
-
|
|
510
|
-
exportLines.push(`// From: ${sourceFilePath}\n${namespacedCode}`);
|
|
253
|
+
if (node.type === 'ExportNamedDeclaration' && node.declaration) {
|
|
254
|
+
const exportCode = juxContent.slice(node.start, node.end).replace(/^export\s+/, '');
|
|
255
|
+
exportLines.push(`// From: ${sourceFilePath}\n${namespaceExportedIdentifiers(exportCode, sourceFilePath)}`);
|
|
256
|
+
} else if (node.type === 'ExportDefaultDeclaration') {
|
|
257
|
+
const exportCode = juxContent.slice(node.start, node.end).replace(/^export\s+default\s+/, '');
|
|
258
|
+
exportLines.push(`// From: ${sourceFilePath}\n${namespaceExportedIdentifiers(exportCode, sourceFilePath)}`);
|
|
511
259
|
}
|
|
512
260
|
});
|
|
513
|
-
|
|
514
261
|
} catch (err) {
|
|
515
|
-
|
|
516
|
-
console.error(` Syntax Error: ${err.message}`);
|
|
517
|
-
console.error(` Line: ${err.loc?.line}, Column: ${err.loc?.column}\n`);
|
|
518
|
-
throw new Error(`Invalid JavaScript syntax in ${sourceFilePath}. Please fix and retry.`);
|
|
262
|
+
throw new Error(`Invalid JavaScript syntax in ${sourceFilePath}.`);
|
|
519
263
|
}
|
|
520
|
-
|
|
521
264
|
return exportLines.join('\n\n');
|
|
522
265
|
}
|
|
523
266
|
|
|
524
|
-
/**
|
|
525
|
-
* Namespace exported identifiers by appending source file path
|
|
526
|
-
* @param {string} code - The export code (e.g., "function myFunction() { ... }")
|
|
527
|
-
* @param {string} sourceFilePath - Source file path (e.g., "experiments/test1.jux")
|
|
528
|
-
* @returns {string} - Namespaced code (e.g., "function myFunction$experiments$test1() { ... }")
|
|
529
|
-
*/
|
|
530
267
|
function namespaceExportedIdentifiers(code, sourceFilePath) {
|
|
531
|
-
|
|
532
|
-
const namespace = sourceFilePath
|
|
533
|
-
.replace(/\.jux$/, '')
|
|
534
|
-
.replace(/[\/\\]/g, '$')
|
|
535
|
-
.replace(/[^a-zA-Z0-9$]/g, '_');
|
|
536
|
-
|
|
268
|
+
const namespace = sourceFilePath.replace(/\.jux$/, '').replace(/[\/\\]/g, '$').replace(/[^a-zA-Z0-9$]/g, '_');
|
|
537
269
|
try {
|
|
538
|
-
const ast = acorn.parse(code, {
|
|
539
|
-
ecmaVersion: 'latest',
|
|
540
|
-
sourceType: 'module'
|
|
541
|
-
});
|
|
542
|
-
|
|
543
|
-
// Find all exported identifiers
|
|
270
|
+
const ast = acorn.parse(code, { ecmaVersion: 'latest', sourceType: 'module' });
|
|
544
271
|
const identifiers = [];
|
|
545
|
-
|
|
546
272
|
ast.body.forEach(node => {
|
|
547
|
-
if (node.type === 'FunctionDeclaration' && node.id)
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
start: node.id.start,
|
|
551
|
-
end: node.id.end
|
|
552
|
-
});
|
|
553
|
-
}
|
|
554
|
-
else if (node.type === 'VariableDeclaration') {
|
|
555
|
-
node.declarations.forEach(decl => {
|
|
556
|
-
if (decl.id.type === 'Identifier') {
|
|
557
|
-
identifiers.push({
|
|
558
|
-
name: decl.id.name,
|
|
559
|
-
start: decl.id.start,
|
|
560
|
-
end: decl.id.end
|
|
561
|
-
});
|
|
562
|
-
}
|
|
563
|
-
});
|
|
564
|
-
}
|
|
565
|
-
else if (node.type === 'ClassDeclaration' && node.id) {
|
|
566
|
-
identifiers.push({
|
|
567
|
-
name: node.id.name,
|
|
568
|
-
start: node.id.start,
|
|
569
|
-
end: node.id.end
|
|
570
|
-
});
|
|
571
|
-
}
|
|
273
|
+
if (node.type === 'FunctionDeclaration' && node.id) identifiers.push(node.id);
|
|
274
|
+
else if (node.type === 'VariableDeclaration') node.declarations.forEach(d => { if (d.id.type === 'Identifier') identifiers.push(d.id); });
|
|
275
|
+
else if (node.type === 'ClassDeclaration' && node.id) identifiers.push(node.id);
|
|
572
276
|
});
|
|
573
|
-
|
|
574
|
-
// Replace identifiers in reverse order (to preserve offsets)
|
|
575
277
|
identifiers.sort((a, b) => b.start - a.start);
|
|
576
|
-
|
|
577
278
|
let result = code;
|
|
578
279
|
identifiers.forEach(id => {
|
|
579
|
-
|
|
580
|
-
result = result.slice(0, id.start) + namespacedName + result.slice(id.end);
|
|
280
|
+
result = result.slice(0, id.start) + `${id.name}$${namespace}` + result.slice(id.end);
|
|
581
281
|
});
|
|
582
|
-
|
|
583
282
|
return result;
|
|
584
|
-
|
|
585
283
|
} catch (err) {
|
|
586
|
-
console.warn(`⚠️ Failed to namespace exports in ${sourceFilePath}, returning as-is`);
|
|
587
284
|
return code;
|
|
588
285
|
}
|
|
589
286
|
}
|
|
590
287
|
|
|
591
|
-
/**
|
|
592
|
-
* Transform .jux file content into a view function using AST
|
|
593
|
-
* ✅ Rewrites imports to use namespaced identifiers
|
|
594
|
-
*/
|
|
595
288
|
function transformJuxToViewFunction(juxContent, functionName, pageName, relativePath, sharedModules) {
|
|
596
289
|
let result;
|
|
597
|
-
|
|
598
290
|
try {
|
|
599
|
-
const ast = acorn.parse(juxContent, {
|
|
600
|
-
ecmaVersion: 'latest',
|
|
601
|
-
sourceType: 'module'
|
|
602
|
-
});
|
|
603
|
-
|
|
291
|
+
const ast = acorn.parse(juxContent, { ecmaVersion: 'latest', sourceType: 'module' });
|
|
604
292
|
const nodesToRemove = [];
|
|
605
|
-
const importReplacements = new Map();
|
|
606
|
-
|
|
293
|
+
const importReplacements = new Map();
|
|
607
294
|
ast.body.forEach(node => {
|
|
608
|
-
// Process import declarations
|
|
609
295
|
if (node.type === 'ImportDeclaration') {
|
|
610
296
|
const importPath = node.source.value;
|
|
611
|
-
|
|
612
|
-
// Only process .jux imports
|
|
613
297
|
if (importPath.endsWith('.jux')) {
|
|
614
|
-
// Extract imported identifiers
|
|
615
298
|
node.specifiers.forEach(spec => {
|
|
616
299
|
if (spec.type === 'ImportSpecifier') {
|
|
617
|
-
const localName = spec.local.name;
|
|
618
|
-
const importedName = spec.imported.name;
|
|
619
|
-
|
|
620
|
-
// Resolve import path to namespace
|
|
621
300
|
const resolvedPath = resolveImportPath(importPath, relativePath);
|
|
622
|
-
const namespace = resolvedPath
|
|
623
|
-
|
|
624
|
-
.replace(/[\/\\]/g, '$')
|
|
625
|
-
.replace(/[^a-zA-Z0-9$]/g, '_');
|
|
626
|
-
|
|
627
|
-
const namespacedName = `${importedName}$${namespace}`;
|
|
628
|
-
|
|
629
|
-
// Map local name to namespaced name
|
|
630
|
-
importReplacements.set(localName, namespacedName);
|
|
301
|
+
const namespace = resolvedPath.replace(/\.jux$/, '').replace(/[\/\\]/g, '$').replace(/[^a-zA-Z0-9$]/g, '_');
|
|
302
|
+
importReplacements.set(spec.local.name, `${spec.imported.name}$${namespace}`);
|
|
631
303
|
}
|
|
632
304
|
});
|
|
633
305
|
}
|
|
634
|
-
|
|
635
|
-
// ✅ CRITICAL FIX: Remove ALL imports so they don't break function scope
|
|
636
|
-
// They are hoisted by generateRouterBundle
|
|
637
306
|
nodesToRemove.push({ start: node.start, end: node.end });
|
|
638
307
|
}
|
|
639
|
-
|
|
640
|
-
// Remove export declarations
|
|
641
|
-
if (node.type === 'ExportNamedDeclaration') {
|
|
642
|
-
if (node.declaration) {
|
|
643
|
-
nodesToRemove.push({ start: node.start, end: node.end });
|
|
644
|
-
}
|
|
645
|
-
else if (node.specifiers && node.specifiers.length > 0) {
|
|
646
|
-
nodesToRemove.push({ start: node.start, end: node.end });
|
|
647
|
-
}
|
|
648
|
-
}
|
|
649
|
-
|
|
650
|
-
if (node.type === 'ExportDefaultDeclaration') {
|
|
308
|
+
if (node.type === 'ExportNamedDeclaration' || node.type === 'ExportDefaultDeclaration') {
|
|
651
309
|
nodesToRemove.push({ start: node.start, end: node.end });
|
|
652
310
|
}
|
|
653
311
|
});
|
|
654
|
-
|
|
655
312
|
nodesToRemove.sort((a, b) => b.start - a.start);
|
|
656
|
-
|
|
657
313
|
result = juxContent;
|
|
658
|
-
nodesToRemove.forEach(({ start, end }) => {
|
|
659
|
-
result = result.slice(0, start) + result.slice(end);
|
|
660
|
-
});
|
|
661
|
-
|
|
662
|
-
result = result.trim();
|
|
663
|
-
|
|
664
|
-
// ✅ Replace imported identifiers with namespaced versions
|
|
314
|
+
nodesToRemove.forEach(({ start, end }) => { result = result.slice(0, start) + result.slice(end); });
|
|
665
315
|
importReplacements.forEach((namespacedName, localName) => {
|
|
666
|
-
|
|
667
|
-
// Must use word boundaries to avoid partial matches
|
|
668
|
-
const regex = new RegExp(`\\b${localName}\\b`, 'g');
|
|
669
|
-
result = result.replace(regex, namespacedName);
|
|
316
|
+
result = result.replace(new RegExp(`\\b${localName}\\b`, 'g'), namespacedName);
|
|
670
317
|
});
|
|
671
|
-
|
|
672
318
|
} catch (err) {
|
|
673
|
-
|
|
674
|
-
console.error(` Syntax Error: ${err.message}`);
|
|
675
|
-
console.error(` Line: ${err.loc?.line}, Column: ${err.loc?.column}\n`);
|
|
676
|
-
throw new Error(`Invalid JavaScript syntax in ${relativePath}. Please fix and retry.`);
|
|
319
|
+
throw new Error(`Invalid JavaScript syntax in ${relativePath}`);
|
|
677
320
|
}
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
const cleanName = functionName
|
|
683
|
-
.replace(/[-_]/g, ' ')
|
|
684
|
-
.split(' ')
|
|
685
|
-
.map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
|
|
686
|
-
.join('');
|
|
687
|
-
|
|
688
|
-
return `
|
|
689
|
-
// View: ${cleanName}
|
|
690
|
-
function ${cleanName}() {
|
|
691
|
-
${result}
|
|
692
|
-
|
|
693
|
-
return document.getElementById('app');
|
|
694
|
-
}`;
|
|
321
|
+
result = result.replace(/\.renderTo\(container\)/g, '.render("#app")').replace(/\.render\(\s*\)/g, '.render("#app")');
|
|
322
|
+
const cleanName = functionName.replace(/[-_]/g, ' ').split(' ').map(w => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase()).join('');
|
|
323
|
+
return `\n// View: ${cleanName}\nfunction ${cleanName}() {\n${result}\n \n return document.getElementById('app');\n}`;
|
|
695
324
|
}
|
|
696
325
|
|
|
697
|
-
/**
|
|
698
|
-
* Resolve relative import path to absolute path
|
|
699
|
-
* @param {string} importPath - Import path from import statement (e.g., './test1.jux')
|
|
700
|
-
* @param {string} currentFilePath - Current file path (e.g., 'experiments/test3.jux')
|
|
701
|
-
* @returns {string} - Resolved path (e.g., 'experiments/test1.jux')
|
|
702
|
-
*/
|
|
703
326
|
function resolveImportPath(importPath, currentFilePath) {
|
|
704
327
|
const currentDir = path.dirname(currentFilePath);
|
|
705
|
-
|
|
706
|
-
return resolved.replace(/\\/g, '/'); // Normalize to forward slashes
|
|
328
|
+
return path.join(currentDir, importPath).replace(/\\/g, '/');
|
|
707
329
|
}
|
|
708
330
|
|
|
709
|
-
/**
|
|
710
|
-
* Generate complete router bundle
|
|
711
|
-
* ✅ IMPROVED: Context-Aware Import Resolution (resolves ../paths before dedupe)
|
|
712
|
-
*/
|
|
713
331
|
function generateRouterBundle(views, routes, sharedModules = new Map(), allImports = [], projectRoot = '') {
|
|
714
|
-
const libImport = '/lib/jux.js'; // ✅ Use absolute path for core lib
|
|
715
|
-
|
|
716
|
-
// Map<NormalizedSource, { defaults: Set<string>, named: Set<string>, namespace: string|null }>
|
|
717
332
|
const mergedImports = new Map();
|
|
718
|
-
|
|
719
|
-
// Helper to resolve relative paths to a canonical string
|
|
720
333
|
const getCanonicalSource = (source, contextFilePath) => {
|
|
721
|
-
|
|
722
|
-
if (!source.startsWith('.')) return source;
|
|
723
|
-
if (source.includes('.jux')) return source; // Skipped later anyway
|
|
724
|
-
|
|
725
|
-
// 2. Resolve relative paths against the file they came from
|
|
726
|
-
// contextFilePath is relative to projectRoot (e.g. 'experiments/list.jux')
|
|
334
|
+
if (!source.startsWith('.') || source.includes('.jux')) return source;
|
|
727
335
|
const dir = path.dirname(contextFilePath);
|
|
728
|
-
|
|
729
|
-
// joinedPath is relative to projectRoot (e.g. '../lib/componentsv2')
|
|
730
336
|
const joinedPath = path.join(dir, source);
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
return 'juxscript/
|
|
736
|
-
}
|
|
737
|
-
if (joinedPath.includes('lib/components')) {
|
|
738
|
-
return 'juxscript/components';
|
|
337
|
+
const v2Index = joinedPath.indexOf('lib/componentsv2');
|
|
338
|
+
if (v2Index !== -1) {
|
|
339
|
+
const subPath = joinedPath.substring(v2Index + 'lib/componentsv2'.length);
|
|
340
|
+
if (!subPath || subPath === '/index.js' || subPath === '/index') return 'juxscript';
|
|
341
|
+
return 'juxscript' + subPath.replace(/\\/g, '/');
|
|
739
342
|
}
|
|
740
|
-
|
|
741
|
-
// 4. Fallback: return as-is (but normalized separators)
|
|
343
|
+
if (joinedPath.includes('lib/components')) return 'juxscript/components';
|
|
742
344
|
return joinedPath.replace(/\\/g, '/');
|
|
743
345
|
};
|
|
744
346
|
|
|
745
347
|
const importList = Array.isArray(allImports) ? allImports : Array.from(allImports);
|
|
746
|
-
|
|
747
348
|
for (const item of importList) {
|
|
748
|
-
const code = typeof item === 'string' ? item : item.code;
|
|
749
|
-
const filePath = typeof item === 'string' ? '' : item.filePath; // Context file path
|
|
750
|
-
|
|
751
349
|
try {
|
|
350
|
+
const code = typeof item === 'string' ? item : item.code;
|
|
351
|
+
const filePath = typeof item === 'string' ? '' : item.filePath;
|
|
752
352
|
const ast = acorn.parse(code.trim(), { ecmaVersion: 'latest', sourceType: 'module' });
|
|
753
353
|
const node = ast.body[0];
|
|
754
|
-
|
|
755
354
|
if (node && node.type === 'ImportDeclaration') {
|
|
756
355
|
const rawSource = node.source.value;
|
|
757
|
-
|
|
758
|
-
// Skip base libs handled manually
|
|
759
|
-
if (rawSource === 'juxscript' || rawSource === 'juxscript/reactivity') continue;
|
|
760
356
|
if (rawSource.endsWith('.jux')) continue;
|
|
761
|
-
|
|
762
|
-
// ✅ RESOLVE SOURCE USING CONTEXT (Fixes ./lib/.. vs ../../lib/..)
|
|
763
357
|
const source = getCanonicalSource(rawSource, filePath);
|
|
764
|
-
|
|
765
|
-
if (!mergedImports.has(source)) {
|
|
766
|
-
mergedImports.set(source, { defaults: new Set(), named: new Set(), namespace: null });
|
|
767
|
-
}
|
|
358
|
+
if (!mergedImports.has(source)) mergedImports.set(source, { defaults: new Set(), named: new Set(), namespace: null });
|
|
768
359
|
const storage = mergedImports.get(source);
|
|
769
|
-
|
|
770
360
|
node.specifiers.forEach(spec => {
|
|
771
|
-
if (spec.type === 'ImportDefaultSpecifier')
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
if (spec.imported.name !== spec.local.name) {
|
|
775
|
-
storage.named.add(`${spec.imported.name} as ${spec.local.name}`);
|
|
776
|
-
} else {
|
|
777
|
-
storage.named.add(spec.imported.name);
|
|
778
|
-
}
|
|
779
|
-
} else if (spec.type === 'ImportNamespaceSpecifier') {
|
|
780
|
-
storage.namespace = spec.local.name;
|
|
781
|
-
}
|
|
361
|
+
if (spec.type === 'ImportDefaultSpecifier') storage.defaults.add(spec.local.name);
|
|
362
|
+
else if (spec.type === 'ImportSpecifier') storage.named.add(spec.imported.name !== spec.local.name ? `${spec.imported.name} as ${spec.local.name}` : spec.imported.name);
|
|
363
|
+
else if (spec.type === 'ImportNamespaceSpecifier') storage.namespace = spec.local.name;
|
|
782
364
|
});
|
|
783
365
|
}
|
|
784
|
-
} catch (e) {
|
|
785
|
-
console.warn('⚠️ Failed to parse collected import:', code);
|
|
786
|
-
}
|
|
366
|
+
} catch (e) { }
|
|
787
367
|
}
|
|
788
368
|
|
|
789
|
-
// Generate cleaned export list
|
|
790
369
|
const filteredImports = [];
|
|
791
|
-
|
|
792
370
|
mergedImports.forEach((storage, source) => {
|
|
793
|
-
|
|
794
|
-
if (storage.namespace) {
|
|
795
|
-
filteredImports.push(`import * as ${storage.namespace} from '${source}';`);
|
|
796
|
-
}
|
|
797
|
-
|
|
371
|
+
if (storage.namespace) filteredImports.push(`import * as ${storage.namespace} from '${source}';`);
|
|
798
372
|
const parts = [];
|
|
799
|
-
if (storage.defaults.size > 0)
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
}
|
|
803
|
-
|
|
804
|
-
if (storage.named.size > 0) {
|
|
805
|
-
parts.push(`{ ${Array.from(storage.named).sort().join(', ')} }`);
|
|
806
|
-
}
|
|
807
|
-
|
|
808
|
-
if (storage.defaults.size === 0 && storage.named.size === 0 && !storage.namespace) {
|
|
809
|
-
filteredImports.push(`import '${source}';`);
|
|
810
|
-
} else if (parts.length > 0) {
|
|
811
|
-
filteredImports.push(`import ${parts.join(', ')} from '${source}';`);
|
|
812
|
-
}
|
|
373
|
+
if (storage.defaults.size > 0) parts.push(Array.from(storage.defaults)[0]);
|
|
374
|
+
if (storage.named.size > 0) parts.push(`{ ${Array.from(storage.named).sort().join(', ')} }`);
|
|
375
|
+
if (storage.defaults.size === 0 && storage.named.size === 0 && !storage.namespace) filteredImports.push(`import '${source}';`);
|
|
376
|
+
else if (parts.length > 0) filteredImports.push(`import ${parts.join(', ')} from '${source}';`);
|
|
813
377
|
});
|
|
814
378
|
|
|
815
|
-
// Generate shared modules section (deduplicated by file)
|
|
816
|
-
const sharedModulesCode = Array.from(sharedModules.values())
|
|
817
|
-
.filter(code => code.trim())
|
|
818
|
-
.join('\n\n');
|
|
819
|
-
|
|
820
|
-
// Filter out empty views
|
|
821
|
-
const viewsCode = views.filter(v => v.trim()).join('\n\n');
|
|
822
|
-
|
|
823
|
-
const routeTable = routes
|
|
824
|
-
.map(r => ` '${r.path}': ${r.functionName}`)
|
|
825
|
-
.join(',\n');
|
|
826
|
-
|
|
827
|
-
// Build imports section (now filtered)
|
|
828
|
-
const importsSection = filteredImports.join('\n');
|
|
829
|
-
|
|
830
379
|
return `// Generated Jux Router Bundle
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
834
|
-
// COLLECTED IMPORTS FROM SOURCE FILES
|
|
835
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
836
|
-
|
|
837
|
-
${importsSection}
|
|
838
|
-
|
|
839
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
840
|
-
// SHARED MODULES (Exported Components)
|
|
841
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
380
|
+
${filteredImports.join('\n')}
|
|
842
381
|
|
|
843
|
-
|
|
382
|
+
// SHARED MODULES
|
|
383
|
+
${Array.from(sharedModules.values()).filter(c => c.trim()).join('\n\n')}
|
|
844
384
|
|
|
845
|
-
//
|
|
846
|
-
|
|
847
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
385
|
+
// VIEWS
|
|
386
|
+
${views.filter(v => v.trim()).join('\n\n')}
|
|
848
387
|
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
852
|
-
// 404 VIEW
|
|
853
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
854
|
-
|
|
855
|
-
function JuxNotFound() {
|
|
856
|
-
jux.heading(1, { text: '404 - Page Not Found' }).render('#app');
|
|
857
|
-
jux.paragraph('404-msg')
|
|
858
|
-
.text('The page you are looking for does not exist.')
|
|
859
|
-
.render('#app');
|
|
860
|
-
|
|
861
|
-
return document.getElementById('app');
|
|
862
|
-
}
|
|
863
|
-
|
|
864
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
865
|
-
// 403 VIEW
|
|
866
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
867
|
-
|
|
868
|
-
function JuxForbidden() {
|
|
869
|
-
jux.heading(1, { text: '403 - Forbidden' }).render('#app');
|
|
870
|
-
jux.paragraph('403-msg')
|
|
871
|
-
.text('You are not authorized to view this page.')
|
|
872
|
-
.render('#app');
|
|
873
|
-
|
|
874
|
-
return document.getElementById('app');
|
|
875
|
-
}
|
|
876
|
-
|
|
877
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
878
|
-
// ROUTE TABLE
|
|
879
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
388
|
+
function JuxNotFound() { jux.heading(1, { text: '404 - Page Not Found' }).render('#app'); return document.getElementById('app'); }
|
|
389
|
+
function JuxForbidden() { jux.heading(1, { text: '403 - Forbidden' }).render('#app'); return document.getElementById('app'); }
|
|
880
390
|
|
|
881
391
|
const routes = {
|
|
882
|
-
${
|
|
392
|
+
${routes.map(r => ` '${r.path}': ${r.functionName}`).join(',\n')}
|
|
883
393
|
};
|
|
884
394
|
|
|
885
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
886
|
-
// ROUTER CORE
|
|
887
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
888
|
-
|
|
889
395
|
const app = document.getElementById('app');
|
|
890
|
-
|
|
891
396
|
function render() {
|
|
892
397
|
let path = location.pathname;
|
|
893
|
-
|
|
894
|
-
// Try exact match first
|
|
895
|
-
let view = routes[path];
|
|
896
|
-
|
|
897
|
-
// If no match and path ends with /, try appending 'index'
|
|
898
|
-
if (!view && path.endsWith('/')) {
|
|
899
|
-
view = routes[path + 'index'] || routes[path.slice(0, -1) + '/index'];
|
|
900
|
-
}
|
|
901
|
-
|
|
902
|
-
// If still no match and path doesn't end with /, try appending '/index'
|
|
903
|
-
if (!view && !path.endsWith('/')) {
|
|
904
|
-
view = routes[path + '/index'];
|
|
905
|
-
}
|
|
906
|
-
|
|
907
|
-
// Fall back to 404
|
|
398
|
+
let view = routes[path] || (path.endsWith('/') ? (routes[path + 'index'] || routes[path.slice(0, -1) + '/index']) : routes[path + '/index']);
|
|
908
399
|
view = view || JuxNotFound;
|
|
909
|
-
|
|
910
400
|
app.innerHTML = '';
|
|
911
401
|
app.removeAttribute('data-jux-page');
|
|
912
|
-
|
|
913
402
|
view();
|
|
914
|
-
|
|
915
403
|
const pageName = Object.entries(routes).find(([p, v]) => v === view)?.[0] || 'not-found';
|
|
916
404
|
app.setAttribute('data-jux-page', pageName.replace(/^\\\//, '').replace(/\\\//g, '-'));
|
|
917
405
|
}
|
|
918
|
-
|
|
919
406
|
document.addEventListener('click', e => {
|
|
920
407
|
const a = e.target.closest('a');
|
|
921
|
-
if (!a) return;
|
|
922
|
-
|
|
923
|
-
if (a.dataset.router === 'false') return;
|
|
924
|
-
|
|
925
|
-
const url = new URL(a.href);
|
|
926
|
-
if (url.origin !== location.origin) return;
|
|
927
|
-
|
|
408
|
+
if (!a || a.dataset.router === 'false' || new URL(a.href).origin !== location.origin) return;
|
|
928
409
|
e.preventDefault();
|
|
929
410
|
history.pushState({}, '', url.pathname);
|
|
930
411
|
render();
|
|
931
412
|
});
|
|
932
|
-
|
|
933
413
|
window.addEventListener('popstate', render);
|
|
934
|
-
|
|
935
414
|
render();
|
|
936
415
|
`;
|
|
937
|
-
|
|
938
416
|
}
|
|
939
417
|
|
|
940
|
-
/**
|
|
941
|
-
* Generate a unified index.html for router bundle
|
|
942
|
-
* ✅ MODIFIED: Use vendored paths instead of CDN + Hot Reload Script injection
|
|
943
|
-
*/
|
|
944
418
|
export function generateIndexHtml(distDir, bundleResult, options = {}) {
|
|
945
|
-
|
|
946
|
-
if (!bundleResult) {
|
|
947
|
-
console.warn('⚠️ generateIndexHtml called without bundleResult, using defaults');
|
|
948
|
-
bundleResult = {
|
|
949
|
-
mainJsFilename: 'main.js',
|
|
950
|
-
routes: [],
|
|
951
|
-
vendoredPaths: {}
|
|
952
|
-
};
|
|
953
|
-
}
|
|
954
|
-
|
|
955
|
-
const { mainJsFilename = 'main.js', routes = [], vendoredPaths = {} } = bundleResult;
|
|
419
|
+
const { mainJsFilename = 'main.js', vendoredPaths = {} } = bundleResult || {};
|
|
956
420
|
const { isDev = false, wsPort = 3001 } = options;
|
|
957
|
-
|
|
958
|
-
console.log('📄 Generating index.html...');
|
|
959
|
-
|
|
960
|
-
// ✅ Use vendor import map
|
|
961
421
|
const importMapScript = generateVendorImportMap(vendoredPaths);
|
|
962
|
-
|
|
963
|
-
// ✅ Client-side Hot Reload Script
|
|
964
422
|
const hotReloadScript = isDev ? `
|
|
965
423
|
<script>
|
|
966
424
|
(function() {
|
|
967
|
-
console.log('🔌 JUX Hot Reload: Connecting to port ${wsPort}...');
|
|
968
425
|
let ws = new WebSocket('ws://' + window.location.hostname + ':${wsPort}');
|
|
969
|
-
|
|
970
|
-
ws.onopen = () => console.log('✅ JUX Hot Reload: Connected');
|
|
971
|
-
|
|
972
|
-
ws.onmessage = (msg) => {
|
|
973
|
-
try {
|
|
974
|
-
const data = JSON.parse(msg.data);
|
|
975
|
-
if (data.type === 'reload') {
|
|
976
|
-
console.log('🔄 JUX Hot Reload: Refeshing...');
|
|
977
|
-
window.location.reload();
|
|
978
|
-
}
|
|
979
|
-
} catch(e) { console.error('HMR Error:', e); }
|
|
980
|
-
};
|
|
981
|
-
|
|
982
|
-
ws.onclose = () => console.log('❌ JUX Hot Reload: Disconnected');
|
|
426
|
+
ws.onmessage = (msg) => { if (JSON.parse(msg.data).type === 'reload') window.location.reload(); };
|
|
983
427
|
})();
|
|
984
428
|
</script>` : '';
|
|
985
429
|
|
|
@@ -997,114 +441,34 @@ export function generateIndexHtml(distDir, bundleResult, options = {}) {
|
|
|
997
441
|
${hotReloadScript}
|
|
998
442
|
</body>
|
|
999
443
|
</html>`;
|
|
1000
|
-
|
|
1001
|
-
const indexPath = path.join(distDir, 'index.html');
|
|
1002
|
-
fs.writeFileSync(indexPath, html);
|
|
1003
|
-
|
|
1004
|
-
console.log(` ✓ Generated: index.html (references ${mainJsFilename})`);
|
|
1005
|
-
console.log('✅ Index HTML complete\n');
|
|
444
|
+
fs.writeFileSync(path.join(distDir, 'index.html'), html);
|
|
1006
445
|
}
|
|
1007
446
|
|
|
1008
|
-
/**
|
|
1009
|
-
* ✅ NEW: Download and vendor external dependencies locally
|
|
1010
|
-
* Copies dependencies from node_modules to .jux-dist/vendor/
|
|
1011
|
-
* NO CDN - everything is local, zero hardcoded mappings
|
|
1012
|
-
* BUNDLES dependencies to resolve internal imports
|
|
1013
|
-
*/
|
|
1014
447
|
async function vendorExternalDependencies(externalDeps, distDir) {
|
|
1015
448
|
const vendorDir = path.join(distDir, 'vendor');
|
|
1016
|
-
|
|
1017
|
-
if (!fs.existsSync(vendorDir)) {
|
|
1018
|
-
fs.mkdirSync(vendorDir, { recursive: true });
|
|
1019
|
-
}
|
|
1020
|
-
|
|
449
|
+
if (!fs.existsSync(vendorDir)) fs.mkdirSync(vendorDir, { recursive: true });
|
|
1021
450
|
const vendoredPaths = {};
|
|
1022
|
-
|
|
1023
451
|
for (const dep of externalDeps) {
|
|
1024
452
|
try {
|
|
1025
453
|
const nodeModulePath = path.join(process.cwd(), 'node_modules', dep);
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
const packageJsonPath = path.join(nodeModulePath, 'package.json');
|
|
1033
|
-
|
|
1034
|
-
if (!fs.existsSync(packageJsonPath)) {
|
|
1035
|
-
console.warn(` ⚠️ ${dep}: package.json not found`);
|
|
1036
|
-
continue;
|
|
1037
|
-
}
|
|
1038
|
-
|
|
1039
|
-
const packageJson = JSON.parse(
|
|
1040
|
-
fs.readFileSync(packageJsonPath, 'utf-8')
|
|
1041
|
-
);
|
|
1042
|
-
|
|
1043
|
-
const entryPoints = [
|
|
1044
|
-
packageJson.module,
|
|
1045
|
-
packageJson.browser,
|
|
1046
|
-
packageJson.main,
|
|
1047
|
-
'index.js'
|
|
1048
|
-
].filter(Boolean);
|
|
1049
|
-
|
|
1050
|
-
let entryFile = null;
|
|
1051
|
-
for (const entry of entryPoints) {
|
|
1052
|
-
const candidatePath = path.join(nodeModulePath, entry);
|
|
1053
|
-
if (fs.existsSync(candidatePath)) {
|
|
1054
|
-
entryFile = candidatePath;
|
|
1055
|
-
break;
|
|
1056
|
-
}
|
|
1057
|
-
}
|
|
1058
|
-
|
|
1059
|
-
if (!entryFile) {
|
|
1060
|
-
console.warn(` ⚠️ ${dep}: no valid entry point`);
|
|
1061
|
-
continue;
|
|
1062
|
-
}
|
|
1063
|
-
|
|
1064
|
-
const vendorFile = path.join(vendorDir, `${dep}.js`);
|
|
1065
|
-
|
|
1066
|
-
await esbuild.build({
|
|
1067
|
-
entryPoints: [entryFile],
|
|
1068
|
-
bundle: true,
|
|
1069
|
-
format: 'esm',
|
|
1070
|
-
outfile: vendorFile,
|
|
1071
|
-
platform: 'browser',
|
|
1072
|
-
target: 'es2020',
|
|
1073
|
-
minify: false,
|
|
1074
|
-
logLevel: 'warning'
|
|
1075
|
-
});
|
|
1076
|
-
|
|
454
|
+
if (!fs.existsSync(nodeModulePath)) continue;
|
|
455
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(nodeModulePath, 'package.json'), 'utf-8'));
|
|
456
|
+
const entryPoints = [pkg.module, pkg.browser, pkg.main, 'index.js'].filter(Boolean);
|
|
457
|
+
let entryFile = entryPoints.map(e => path.join(nodeModulePath, e)).find(p => fs.existsSync(p));
|
|
458
|
+
if (!entryFile) continue;
|
|
459
|
+
await esbuild.build({ entryPoints: [entryFile], bundle: true, format: 'esm', outfile: path.join(vendorDir, `${dep}.js`), platform: 'browser', target: 'es2020', logLevel: 'silent' });
|
|
1077
460
|
vendoredPaths[dep] = `/vendor/${dep}.js`;
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
} catch (err) {
|
|
1081
|
-
console.error(` ❌ ${dep}: ${err.message}`);
|
|
1082
|
-
}
|
|
461
|
+
} catch (err) { }
|
|
1083
462
|
}
|
|
1084
|
-
|
|
1085
463
|
return vendoredPaths;
|
|
1086
464
|
}
|
|
1087
465
|
|
|
1088
|
-
/**
|
|
1089
|
-
* ✅ NEW: Generate import map with local vendor paths
|
|
1090
|
-
*/
|
|
1091
466
|
function generateVendorImportMap(vendoredPaths) {
|
|
1092
|
-
// ✅ FIX: Use ABSOLUTE paths (start with /) to support nested routes (e.g. /app/dashboard)
|
|
1093
|
-
// When using ./, paths are resolved relative to the current URL, which breaks on sub-pages.
|
|
1094
467
|
const imports = {
|
|
1095
|
-
"juxscript
|
|
1096
|
-
"juxscript/
|
|
1097
|
-
...vendoredPaths
|
|
468
|
+
"juxscript": "/lib/componentsv2/index.js",
|
|
469
|
+
"juxscript/": "/lib/componentsv2/",
|
|
470
|
+
...vendoredPaths
|
|
1098
471
|
};
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
Object.keys(imports).forEach(key => {
|
|
1102
|
-
if (imports[key].startsWith('./')) {
|
|
1103
|
-
imports[key] = imports[key].substring(1);
|
|
1104
|
-
}
|
|
1105
|
-
});
|
|
1106
|
-
|
|
1107
|
-
return `<script type="importmap">
|
|
1108
|
-
${JSON.stringify({ imports }, null, 2)}
|
|
1109
|
-
</script>`;
|
|
472
|
+
Object.keys(imports).forEach(key => { if (imports[key].startsWith('./')) imports[key] = imports[key].substring(1); });
|
|
473
|
+
return `<script type="importmap">\n${JSON.stringify({ imports }, null, 2)}\n</script>`;
|
|
1110
474
|
}
|