juxscript 1.1.282 → 1.1.283
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/machinery/autowrap.js +212 -45
- package/machinery/compiler4.js +3 -3
- package/machinery/test-autowrap.js +23 -0
- package/package.json +3 -2
package/machinery/autowrap.js
CHANGED
|
@@ -39,6 +39,29 @@ function containsPageStateRef(node) {
|
|
|
39
39
|
return found;
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
+
function containsJuxCall(node) {
|
|
43
|
+
let found = false;
|
|
44
|
+
walkSimple(node, {
|
|
45
|
+
CallExpression(n) { if (n.callee?.object?.name === 'jux') found = true; }
|
|
46
|
+
});
|
|
47
|
+
return found;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function getDeclaredVarNames(stmt) {
|
|
51
|
+
if (stmt.type !== 'VariableDeclaration') return [];
|
|
52
|
+
return stmt.declarations
|
|
53
|
+
.filter(d => d.id.type === 'Identifier')
|
|
54
|
+
.map(d => d.id.name);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function usesIdentifier(node, name) {
|
|
58
|
+
let found = false;
|
|
59
|
+
walkSimple(node, {
|
|
60
|
+
Identifier(n) { if (n.name === name) found = true; }
|
|
61
|
+
});
|
|
62
|
+
return found;
|
|
63
|
+
}
|
|
64
|
+
|
|
42
65
|
function isAlreadyWrapped(stmt) {
|
|
43
66
|
return stmt.type === 'ExpressionStatement' &&
|
|
44
67
|
stmt.expression?.type === 'CallExpression' &&
|
|
@@ -46,10 +69,36 @@ function isAlreadyWrapped(stmt) {
|
|
|
46
69
|
stmt.expression?.callee?.property?.name === '__watch';
|
|
47
70
|
}
|
|
48
71
|
|
|
49
|
-
|
|
50
|
-
|
|
72
|
+
/**
|
|
73
|
+
* Extract the body statements from an existing __watch callback.
|
|
74
|
+
* Returns null if the statement is not a __watch or has no block body.
|
|
75
|
+
*/
|
|
76
|
+
function getWatchBody(stmt) {
|
|
77
|
+
if (!isAlreadyWrapped(stmt)) return null;
|
|
78
|
+
const arg = stmt.expression.arguments?.[0];
|
|
79
|
+
if (!arg) return null;
|
|
80
|
+
if (arg.type === 'ArrowFunctionExpression' || arg.type === 'FunctionExpression') {
|
|
81
|
+
if (arg.body?.type === 'BlockStatement') {
|
|
82
|
+
return arg.body.body; // array of statements
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function isSetupStatement(stmt) {
|
|
89
|
+
// Never treat statements referencing pageState as setup
|
|
90
|
+
if (containsPageStateRef(stmt)) return false;
|
|
91
|
+
|
|
51
92
|
if (stmt.type === 'ImportDeclaration') return true;
|
|
52
93
|
if (stmt.type === 'FunctionDeclaration') return true;
|
|
94
|
+
if (stmt.type === 'ExpressionStatement') {
|
|
95
|
+
const expr = stmt.expression;
|
|
96
|
+
if (expr.type === 'CallExpression' &&
|
|
97
|
+
expr.callee?.object?.name === 'jux') return true;
|
|
98
|
+
if (expr.type === 'AwaitExpression' &&
|
|
99
|
+
expr.argument?.callee?.object?.name === 'jux') return true;
|
|
100
|
+
}
|
|
101
|
+
if (stmt.type === 'VariableDeclaration') return true;
|
|
53
102
|
return false;
|
|
54
103
|
}
|
|
55
104
|
|
|
@@ -75,68 +124,186 @@ export function autowrap(source, filename = '') {
|
|
|
75
124
|
allowAwaitOutsideFunction: true,
|
|
76
125
|
});
|
|
77
126
|
} catch (err) {
|
|
127
|
+
// Can't parse — return as-is
|
|
78
128
|
return { code: source, wrappedCount: 0, details: [`parse error: ${err.message}`] };
|
|
79
129
|
}
|
|
80
130
|
|
|
81
|
-
|
|
82
|
-
let
|
|
83
|
-
let firstUnwrappedIdx = -1;
|
|
84
|
-
let lastUnwrappedIdx = -1;
|
|
131
|
+
const needsWatch = [];
|
|
132
|
+
let i = 0;
|
|
85
133
|
|
|
86
|
-
|
|
134
|
+
while (i < ast.body.length) {
|
|
87
135
|
const stmt = ast.body[i];
|
|
88
|
-
|
|
89
|
-
if (
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
136
|
+
|
|
137
|
+
if (isSetupStatement(stmt)) { i++; continue; }
|
|
138
|
+
|
|
139
|
+
// Check if this is a single __watch that contains multiple independent concerns
|
|
140
|
+
// If so, unwrap it and re-wrap each concern separately
|
|
141
|
+
if (isAlreadyWrapped(stmt)) {
|
|
142
|
+
const bodyStmts = getWatchBody(stmt);
|
|
143
|
+
if (bodyStmts && bodyStmts.length > 1) {
|
|
144
|
+
// Check if the body has multiple independent reactive groups
|
|
145
|
+
const groups = groupBodyStatements(bodyStmts, source);
|
|
146
|
+
if (groups.length > 1) {
|
|
147
|
+
// Replace single watch with multiple watches
|
|
148
|
+
// Record the original watch range for removal
|
|
149
|
+
const watchStart = getLineNumber(source, stmt.start);
|
|
150
|
+
const watchEnd = getLineNumber(source, stmt.end);
|
|
151
|
+
needsWatch.push({
|
|
152
|
+
line: watchStart,
|
|
153
|
+
endLine: watchEnd,
|
|
154
|
+
replace: true,
|
|
155
|
+
groups: groups,
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
i++;
|
|
160
|
+
continue;
|
|
94
161
|
}
|
|
95
|
-
}
|
|
96
162
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
163
|
+
// VariableDeclaration reading pageState — group with subsequent stmts that use declared vars
|
|
164
|
+
if (stmt.type === 'VariableDeclaration' && containsPageStateRef(stmt)) {
|
|
165
|
+
const varNames = getDeclaredVarNames(stmt);
|
|
166
|
+
const groupStmts = [stmt];
|
|
167
|
+
let j = i + 1;
|
|
100
168
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
169
|
+
// Greedily consume following statements that use any of the declared variable names
|
|
170
|
+
while (j < ast.body.length) {
|
|
171
|
+
const next = ast.body[j];
|
|
172
|
+
if (isAlreadyWrapped(next) || isSetupStatement(next)) break;
|
|
173
|
+
if (varNames.some(v => usesIdentifier(next, v))) {
|
|
174
|
+
groupStmts.push(next);
|
|
175
|
+
j++;
|
|
176
|
+
} else {
|
|
177
|
+
break;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
105
180
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
181
|
+
needsWatch.push({
|
|
182
|
+
line: getLineNumber(source, groupStmts[0].start),
|
|
183
|
+
endLine: getLineNumber(source, groupStmts[groupStmts.length - 1].end),
|
|
184
|
+
});
|
|
185
|
+
i = j;
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
110
188
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
189
|
+
// Any other statement referencing pageState
|
|
190
|
+
if (containsPageStateRef(stmt)) {
|
|
191
|
+
needsWatch.push({
|
|
192
|
+
line: getLineNumber(source, stmt.start),
|
|
193
|
+
endLine: getLineNumber(source, stmt.end),
|
|
194
|
+
});
|
|
195
|
+
i++;
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
114
198
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
199
|
+
i++;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (needsWatch.length === 0) {
|
|
118
203
|
return { code: source, wrappedCount: 0, details: [] };
|
|
119
204
|
}
|
|
120
205
|
|
|
121
|
-
|
|
122
|
-
const
|
|
206
|
+
// Apply wraps bottom-up to preserve line numbers
|
|
207
|
+
const lines = source.split('\n');
|
|
208
|
+
const sorted = [...needsWatch].sort((a, b) => b.line - a.line);
|
|
209
|
+
const details = [];
|
|
210
|
+
|
|
211
|
+
for (const item of sorted) {
|
|
212
|
+
const startIdx = item.line - 1;
|
|
213
|
+
const endIdx = item.endLine - 1;
|
|
123
214
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
215
|
+
if (item.replace && item.groups) {
|
|
216
|
+
// Replace a single big __watch with multiple smaller ones
|
|
217
|
+
const indent = lines[startIdx].match(/^(\s*)/)[1];
|
|
218
|
+
const newLines = [];
|
|
219
|
+
for (const group of item.groups) {
|
|
220
|
+
newLines.push(`${indent}pageState.__watch(() => {`);
|
|
221
|
+
for (const gLine of group.lines) {
|
|
222
|
+
newLines.push(`${indent} ${gLine.trim()}`);
|
|
223
|
+
}
|
|
224
|
+
newLines.push(`${indent}});`);
|
|
225
|
+
}
|
|
226
|
+
lines.splice(startIdx, endIdx - startIdx + 1, ...newLines);
|
|
227
|
+
details.push(`L${item.line}-${item.endLine} (split ${item.groups.length})`);
|
|
228
|
+
} else {
|
|
229
|
+
const blockLines = lines.slice(startIdx, endIdx + 1);
|
|
230
|
+
const indent = blockLines[0].match(/^(\s*)/)[1];
|
|
128
231
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
232
|
+
const wrapped = [
|
|
233
|
+
`${indent}pageState.__watch(() => {`,
|
|
234
|
+
...blockLines.map(l => `${indent} ${l.trim()}`),
|
|
235
|
+
`${indent}});`,
|
|
236
|
+
];
|
|
134
237
|
|
|
135
|
-
|
|
238
|
+
lines.splice(startIdx, endIdx - startIdx + 1, ...wrapped);
|
|
239
|
+
details.push(`L${item.line}-${item.endLine}`);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
136
242
|
|
|
137
243
|
return {
|
|
138
244
|
code: lines.join('\n'),
|
|
139
|
-
wrappedCount:
|
|
140
|
-
details
|
|
245
|
+
wrappedCount: needsWatch.length,
|
|
246
|
+
details
|
|
141
247
|
};
|
|
142
248
|
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Groups statements inside a __watch body into independent reactive groups.
|
|
252
|
+
* Uses the same logic as the top-level grouping: VariableDeclarations that
|
|
253
|
+
* read pageState get grouped with following statements that use those vars.
|
|
254
|
+
* Standalone if-statements with pageState become their own group.
|
|
255
|
+
*/
|
|
256
|
+
function groupBodyStatements(bodyStmts, source) {
|
|
257
|
+
const groups = [];
|
|
258
|
+
let i = 0;
|
|
259
|
+
|
|
260
|
+
while (i < bodyStmts.length) {
|
|
261
|
+
const stmt = bodyStmts[i];
|
|
262
|
+
|
|
263
|
+
// VariableDeclaration reading pageState — group with dependents
|
|
264
|
+
if (stmt.type === 'VariableDeclaration' && containsPageStateRef(stmt)) {
|
|
265
|
+
const varNames = getDeclaredVarNames(stmt);
|
|
266
|
+
const groupStmts = [stmt];
|
|
267
|
+
let j = i + 1;
|
|
268
|
+
|
|
269
|
+
while (j < bodyStmts.length) {
|
|
270
|
+
const next = bodyStmts[j];
|
|
271
|
+
if (varNames.some(v => usesIdentifier(next, v))) {
|
|
272
|
+
groupStmts.push(next);
|
|
273
|
+
j++;
|
|
274
|
+
} else {
|
|
275
|
+
break;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
groups.push({
|
|
280
|
+
stmts: groupStmts,
|
|
281
|
+
lines: extractSourceLines(groupStmts, source),
|
|
282
|
+
});
|
|
283
|
+
i = j;
|
|
284
|
+
continue;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// Any statement referencing pageState — standalone group
|
|
288
|
+
if (containsPageStateRef(stmt)) {
|
|
289
|
+
groups.push({
|
|
290
|
+
stmts: [stmt],
|
|
291
|
+
lines: extractSourceLines([stmt], source),
|
|
292
|
+
});
|
|
293
|
+
i++;
|
|
294
|
+
continue;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// Non-reactive statement — attach to next reactive group or skip
|
|
298
|
+
i++;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
return groups;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function extractSourceLines(stmts, source) {
|
|
305
|
+
if (stmts.length === 0) return [];
|
|
306
|
+
const start = stmts[0].start;
|
|
307
|
+
const end = stmts[stmts.length - 1].end;
|
|
308
|
+
return source.slice(start, end).split('\n');
|
|
309
|
+
}
|
package/machinery/compiler4.js
CHANGED
|
@@ -121,7 +121,7 @@ export class JuxCompiler {
|
|
|
121
121
|
wrappedContent = `export default async function() {\n${processedContent}\n}`;
|
|
122
122
|
}
|
|
123
123
|
|
|
124
|
-
views.push({ name, file: relativePath, content: wrappedContent, originalContent: content });
|
|
124
|
+
views.push({ name, file: relativePath, content: wrappedContent, originalContent: content });towrappedContent: processedContent });
|
|
125
125
|
}
|
|
126
126
|
}
|
|
127
127
|
}
|
|
@@ -334,8 +334,8 @@ export class JuxCompiler {
|
|
|
334
334
|
views.forEach((v, index) => {
|
|
335
335
|
const functionName = `renderJux${index}`;
|
|
336
336
|
|
|
337
|
-
let codeBody = v.originalContent || v.content;
|
|
338
|
-
const originalLines = codeBody.split('\n');
|
|
337
|
+
let codeBody = v.autowrappedContent || v.originalContent || v.content;
|
|
338
|
+
const originalLines = (v.originalContent || codeBody).split('\n');
|
|
339
339
|
codeBody = this._stripImportsAndExports(codeBody);
|
|
340
340
|
|
|
341
341
|
sourceSnapshot[v.file] = {
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { readFileSync } from 'fs';
|
|
2
|
+
import { resolve } from 'path';
|
|
3
|
+
import { autowrap } from './autowrap.js';
|
|
4
|
+
|
|
5
|
+
const file = process.argv[2];
|
|
6
|
+
if (!file) {
|
|
7
|
+
console.error('Usage: node test-autowrap.js <path-to-jux-file>');
|
|
8
|
+
process.exit(1);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const filepath = resolve(file);
|
|
12
|
+
const source = readFileSync(filepath, 'utf-8');
|
|
13
|
+
|
|
14
|
+
console.log('═══ INPUT ═══');
|
|
15
|
+
console.log(source);
|
|
16
|
+
console.log('\n═══ AUTOWRAP RESULT ═══');
|
|
17
|
+
|
|
18
|
+
const result = autowrap(source, filepath);
|
|
19
|
+
|
|
20
|
+
console.log(`Wrapped: ${result.wrappedCount}`);
|
|
21
|
+
console.log(`Details: ${result.details.join(', ') || '(none)'}`);
|
|
22
|
+
console.log('\n═══ OUTPUT ═══');
|
|
23
|
+
console.log(result.code);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "juxscript",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.283",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "A JavaScript UX authorship platform",
|
|
6
6
|
"main": "./dist/lib/index.js",
|
|
@@ -45,7 +45,8 @@
|
|
|
45
45
|
"dev": "tsc --watch",
|
|
46
46
|
"prepublishOnly": "npm run scan-css && npm run build",
|
|
47
47
|
"postpublish": "echo '✅ Published successfully. Verify with: npm info juxscript'",
|
|
48
|
-
"scan-css": "node scripts/scan-css-classes.js"
|
|
48
|
+
"scan-css": "node scripts/scan-css-classes.js",
|
|
49
|
+
"test-autowrap": "node machinery/test-autowrap.js"
|
|
49
50
|
},
|
|
50
51
|
"dependencies": {
|
|
51
52
|
"acorn": "^8.15.0",
|