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.
@@ -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
- function isSetupOnly(stmt) {
50
- // Statements that should stay outside __watch
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
- // Check if there are any unwrapped pageState references at all
82
- let hasUnwrappedPageState = false;
83
- let firstUnwrappedIdx = -1;
84
- let lastUnwrappedIdx = -1;
131
+ const needsWatch = [];
132
+ let i = 0;
85
133
 
86
- for (let i = 0; i < ast.body.length; i++) {
134
+ while (i < ast.body.length) {
87
135
  const stmt = ast.body[i];
88
- if (isAlreadyWrapped(stmt)) continue;
89
- if (isSetupOnly(stmt)) continue;
90
- if (containsPageStateRef(stmt)) {
91
- hasUnwrappedPageState = true;
92
- if (firstUnwrappedIdx === -1) firstUnwrappedIdx = i;
93
- lastUnwrappedIdx = i;
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
- if (!hasUnwrappedPageState) {
98
- return { code: source, wrappedCount: 0, details: [] };
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
- // Find the contiguous range: from first unwrapped pageState ref
102
- // to end of file (or last unwrapped ref), including any non-pageState
103
- // statements in between (they may depend on reactive variables).
104
- // But exclude leading setup-only statements (imports, function decls).
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
- // Walk backwards from firstUnwrappedIdx to include any immediately
107
- // preceding non-setup statements that declare variables used later
108
- let rangeStart = firstUnwrappedIdx;
109
- let rangeEnd = lastUnwrappedIdx;
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
- // Collect all statements in the range
112
- const lines = source.split('\n');
113
- const stmtsInRange = ast.body.slice(rangeStart, rangeEnd + 1);
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
- // Check if ALL pageState refs are already wrapped (nothing to do)
116
- const unwrappedInRange = stmtsInRange.filter(s => !isAlreadyWrapped(s) && containsPageStateRef(s));
117
- if (unwrappedInRange.length === 0) {
199
+ i++;
200
+ }
201
+
202
+ if (needsWatch.length === 0) {
118
203
  return { code: source, wrappedCount: 0, details: [] };
119
204
  }
120
205
 
121
- const startLine = getLineNumber(source, stmtsInRange[0].start);
122
- const endLine = getLineNumber(source, stmtsInRange[stmtsInRange.length - 1].end);
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
- const startIdx = startLine - 1;
125
- const endIdx = endLine - 1;
126
- const blockLines = lines.slice(startIdx, endIdx + 1);
127
- const indent = blockLines[0].match(/^(\s*)/)[1];
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
- const wrapped = [
130
- `${indent}pageState.__watch(() => {`,
131
- ...blockLines.map(l => `${indent} ${l.trimEnd()}`),
132
- `${indent}});`,
133
- ];
232
+ const wrapped = [
233
+ `${indent}pageState.__watch(() => {`,
234
+ ...blockLines.map(l => `${indent} ${l.trim()}`),
235
+ `${indent}});`,
236
+ ];
134
237
 
135
- lines.splice(startIdx, endIdx - startIdx + 1, ...wrapped);
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: 1,
140
- details: [`L${startLine}-${endLine} (single watch block)`],
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
+ }
@@ -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.282",
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",