mrmd-js 2.0.0 → 2.0.1
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/dist/index.cjs +207 -18
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +207 -18
- package/dist/index.js.map +1 -1
- package/dist/mrmd-js.iife.js +207 -18
- package/dist/mrmd-js.iife.js.map +1 -1
- package/package.json +1 -1
- package/src/execute/javascript.js +13 -1
- package/src/session/console-capture.js +36 -0
- package/src/transform/async.js +165 -15
- package/src/transform/persistence.js +2 -2
package/package.json
CHANGED
|
@@ -88,7 +88,19 @@ export class JavaScriptExecutor extends BaseExecutor {
|
|
|
88
88
|
|
|
89
89
|
try {
|
|
90
90
|
// Execute in context (pass execId for input() support)
|
|
91
|
-
|
|
91
|
+
let rawResult = await context.execute(wrapped, { execId: options.execId });
|
|
92
|
+
|
|
93
|
+
// Auto-await if result is a Promise (catch cases not handled by auto-await transform)
|
|
94
|
+
if (rawResult.result && typeof rawResult.result === 'object' && typeof rawResult.result.then === 'function') {
|
|
95
|
+
try {
|
|
96
|
+
rawResult.result = await rawResult.result;
|
|
97
|
+
} catch (promiseError) {
|
|
98
|
+
// Promise rejected - treat as error
|
|
99
|
+
rawResult.error = promiseError instanceof Error ? promiseError : new Error(String(promiseError));
|
|
100
|
+
rawResult.result = undefined;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
92
104
|
const duration = performance.now() - startTime;
|
|
93
105
|
|
|
94
106
|
// Format result
|
|
@@ -9,6 +9,15 @@
|
|
|
9
9
|
* @typedef {import('./context/interface.js').LogEntry} LogEntry
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
+
/**
|
|
13
|
+
* Check if a value is a Promise
|
|
14
|
+
* @param {*} value
|
|
15
|
+
* @returns {boolean}
|
|
16
|
+
*/
|
|
17
|
+
function isPromise(value) {
|
|
18
|
+
return value && typeof value === 'object' && typeof value.then === 'function';
|
|
19
|
+
}
|
|
20
|
+
|
|
12
21
|
/**
|
|
13
22
|
* Format arguments for logging
|
|
14
23
|
* @param {Array<*>} args
|
|
@@ -31,6 +40,21 @@ function formatArgs(args) {
|
|
|
31
40
|
.join(' ');
|
|
32
41
|
}
|
|
33
42
|
|
|
43
|
+
/**
|
|
44
|
+
* Check args for Promises and return warning message if found
|
|
45
|
+
* @param {Array<*>} args
|
|
46
|
+
* @returns {string | null}
|
|
47
|
+
*/
|
|
48
|
+
function checkForPromises(args) {
|
|
49
|
+
for (const arg of args) {
|
|
50
|
+
if (isPromise(arg)) {
|
|
51
|
+
return '⚠️ Promise detected - this value needs to be awaited. ' +
|
|
52
|
+
'Add "await" before the async call, or assign it to get the resolved value.';
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
|
|
34
58
|
/**
|
|
35
59
|
* Create a console capture for a window context
|
|
36
60
|
*/
|
|
@@ -73,11 +97,23 @@ export class ConsoleCapture {
|
|
|
73
97
|
// Intercept methods
|
|
74
98
|
console.log = (...args) => {
|
|
75
99
|
this.#queue.push({ type: 'log', args, timestamp: Date.now() });
|
|
100
|
+
// Check for unresolved Promises and warn
|
|
101
|
+
const promiseWarning = checkForPromises(args);
|
|
102
|
+
if (promiseWarning) {
|
|
103
|
+
this.#queue.push({ type: 'warn', args: [promiseWarning], timestamp: Date.now() });
|
|
104
|
+
this.#originalConsole?.warn?.(promiseWarning);
|
|
105
|
+
}
|
|
76
106
|
this.#originalConsole?.log?.(...args);
|
|
77
107
|
};
|
|
78
108
|
|
|
79
109
|
console.info = (...args) => {
|
|
80
110
|
this.#queue.push({ type: 'info', args, timestamp: Date.now() });
|
|
111
|
+
// Check for unresolved Promises and warn
|
|
112
|
+
const promiseWarning = checkForPromises(args);
|
|
113
|
+
if (promiseWarning) {
|
|
114
|
+
this.#queue.push({ type: 'warn', args: [promiseWarning], timestamp: Date.now() });
|
|
115
|
+
this.#originalConsole?.warn?.(promiseWarning);
|
|
116
|
+
}
|
|
81
117
|
this.#originalConsole?.info?.(...args);
|
|
82
118
|
};
|
|
83
119
|
|
package/src/transform/async.js
CHANGED
|
@@ -1,12 +1,130 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Async Transform
|
|
3
3
|
*
|
|
4
|
-
* Wraps code to support top-level await.
|
|
4
|
+
* Wraps code to support top-level await and auto-awaits common async patterns.
|
|
5
|
+
* This makes JavaScript feel more linear like Python/R/Julia.
|
|
5
6
|
* @module transform/async
|
|
6
7
|
*/
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
|
-
*
|
|
10
|
+
* Patterns that should be auto-awaited
|
|
11
|
+
* These are common async operations that return Promises
|
|
12
|
+
*/
|
|
13
|
+
const AUTO_AWAIT_PATTERNS = [
|
|
14
|
+
// Fetch API
|
|
15
|
+
/\bfetch\s*\(/g,
|
|
16
|
+
// Dynamic import
|
|
17
|
+
/\bimport\s*\(/g,
|
|
18
|
+
// Response methods
|
|
19
|
+
/\.json\s*\(/g,
|
|
20
|
+
/\.text\s*\(/g,
|
|
21
|
+
/\.blob\s*\(/g,
|
|
22
|
+
/\.arrayBuffer\s*\(/g,
|
|
23
|
+
/\.formData\s*\(/g,
|
|
24
|
+
// Common async patterns
|
|
25
|
+
/\.then\s*\(/g,
|
|
26
|
+
/\.catch\s*\(/g,
|
|
27
|
+
/\.finally\s*\(/g,
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Auto-insert await before common async function calls
|
|
32
|
+
* This makes JavaScript feel more linear like Python/R
|
|
33
|
+
*
|
|
34
|
+
* @param {string} code - Source code
|
|
35
|
+
* @returns {string} Code with auto-awaits inserted
|
|
36
|
+
*/
|
|
37
|
+
function autoInsertAwaits(code) {
|
|
38
|
+
// Don't process if code already uses await extensively
|
|
39
|
+
// (user knows what they're doing)
|
|
40
|
+
const awaitCount = (code.match(/\bawait\b/g) || []).length;
|
|
41
|
+
const lines = code.split('\n').length;
|
|
42
|
+
if (awaitCount > lines / 2) {
|
|
43
|
+
return code;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
let result = code;
|
|
47
|
+
|
|
48
|
+
// Track positions to avoid double-processing
|
|
49
|
+
// We need to be careful not to add await before already-awaited expressions
|
|
50
|
+
|
|
51
|
+
// First, temporarily replace existing awaits to protect them
|
|
52
|
+
const awaitPlaceholders = [];
|
|
53
|
+
result = result.replace(/\bawait\s+/g, (match) => {
|
|
54
|
+
const placeholder = `__AWAIT_PLACEHOLDER_${awaitPlaceholders.length}__`;
|
|
55
|
+
awaitPlaceholders.push(match);
|
|
56
|
+
return placeholder;
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
// Also protect strings, comments, and template literals
|
|
60
|
+
const protectedStrings = [];
|
|
61
|
+
|
|
62
|
+
// Protect comments FIRST (before strings) to handle apostrophes in comments like "doesn't"
|
|
63
|
+
result = result.replace(/\/\/[^\n]*/g, (match) => {
|
|
64
|
+
const placeholder = `__PROTECTED_${protectedStrings.length}__`;
|
|
65
|
+
protectedStrings.push(match);
|
|
66
|
+
return placeholder;
|
|
67
|
+
});
|
|
68
|
+
result = result.replace(/\/\*[\s\S]*?\*\//g, (match) => {
|
|
69
|
+
const placeholder = `__PROTECTED_${protectedStrings.length}__`;
|
|
70
|
+
protectedStrings.push(match);
|
|
71
|
+
return placeholder;
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
// Protect template literals
|
|
75
|
+
result = result.replace(/`(?:[^`\\]|\\.)*`/g, (match) => {
|
|
76
|
+
const placeholder = `__PROTECTED_${protectedStrings.length}__`;
|
|
77
|
+
protectedStrings.push(match);
|
|
78
|
+
return placeholder;
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
// Protect strings (after comments, so apostrophes in comments don't interfere)
|
|
82
|
+
result = result.replace(/"(?:[^"\\]|\\.)*"/g, (match) => {
|
|
83
|
+
const placeholder = `__PROTECTED_${protectedStrings.length}__`;
|
|
84
|
+
protectedStrings.push(match);
|
|
85
|
+
return placeholder;
|
|
86
|
+
});
|
|
87
|
+
result = result.replace(/'(?:[^'\\]|\\.)*'/g, (match) => {
|
|
88
|
+
const placeholder = `__PROTECTED_${protectedStrings.length}__`;
|
|
89
|
+
protectedStrings.push(match);
|
|
90
|
+
return placeholder;
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
// Now auto-insert awaits for common patterns
|
|
94
|
+
// fetch(...) -> await fetch(...)
|
|
95
|
+
result = result.replace(/\bfetch\s*\(/g, 'await fetch(');
|
|
96
|
+
|
|
97
|
+
// import(...) -> await import(...)
|
|
98
|
+
// But not "import x from" statements
|
|
99
|
+
result = result.replace(/(?<![.\w])import\s*\(/g, 'await import(');
|
|
100
|
+
|
|
101
|
+
// .json() .text() .blob() etc on response objects
|
|
102
|
+
// These methods also return Promises, so we need to await both:
|
|
103
|
+
// response.json() -> await (await response).json()
|
|
104
|
+
result = result.replace(/([a-zA-Z_$][a-zA-Z0-9_$]*)\s*\.\s*json\s*\(\s*\)/g, 'await (await $1).json()');
|
|
105
|
+
result = result.replace(/([a-zA-Z_$][a-zA-Z0-9_$]*)\s*\.\s*text\s*\(\s*\)/g, 'await (await $1).text()');
|
|
106
|
+
result = result.replace(/([a-zA-Z_$][a-zA-Z0-9_$]*)\s*\.\s*blob\s*\(\s*\)/g, 'await (await $1).blob()');
|
|
107
|
+
result = result.replace(/([a-zA-Z_$][a-zA-Z0-9_$]*)\s*\.\s*arrayBuffer\s*\(\s*\)/g, 'await (await $1).arrayBuffer()');
|
|
108
|
+
result = result.replace(/([a-zA-Z_$][a-zA-Z0-9_$]*)\s*\.\s*formData\s*\(\s*\)/g, 'await (await $1).formData()');
|
|
109
|
+
|
|
110
|
+
// Restore protected content
|
|
111
|
+
for (let i = protectedStrings.length - 1; i >= 0; i--) {
|
|
112
|
+
result = result.replace(`__PROTECTED_${i}__`, protectedStrings[i]);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Restore existing awaits
|
|
116
|
+
for (let i = awaitPlaceholders.length - 1; i >= 0; i--) {
|
|
117
|
+
result = result.replace(`__AWAIT_PLACEHOLDER_${i}__`, awaitPlaceholders[i]);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Clean up any double awaits we might have introduced
|
|
121
|
+
result = result.replace(/\bawait\s+await\b/g, 'await');
|
|
122
|
+
|
|
123
|
+
return result;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Check if code contains top-level await (or will after auto-insertion)
|
|
10
128
|
* @param {string} code
|
|
11
129
|
* @returns {boolean}
|
|
12
130
|
*/
|
|
@@ -15,16 +133,17 @@ function hasTopLevelAwait(code) {
|
|
|
15
133
|
// This is a heuristic; a proper check would need AST parsing
|
|
16
134
|
|
|
17
135
|
// Remove strings, comments, and regex to avoid false positives
|
|
136
|
+
// IMPORTANT: Remove comments FIRST to handle apostrophes in comments like "doesn't"
|
|
18
137
|
const cleaned = code
|
|
19
|
-
// Remove
|
|
20
|
-
.replace(/`[^`]*`/g, '')
|
|
21
|
-
// Remove strings
|
|
22
|
-
.replace(/"(?:[^"\\]|\\.)*"/g, '')
|
|
23
|
-
.replace(/'(?:[^'\\]|\\.)*'/g, '')
|
|
24
|
-
// Remove single-line comments
|
|
138
|
+
// Remove single-line comments FIRST (before strings)
|
|
25
139
|
.replace(/\/\/[^\n]*/g, '')
|
|
26
140
|
// Remove multi-line comments
|
|
27
|
-
.replace(/\/\*[\s\S]*?\*\//g, '')
|
|
141
|
+
.replace(/\/\*[\s\S]*?\*\//g, '')
|
|
142
|
+
// Remove template literals
|
|
143
|
+
.replace(/`[^`]*`/g, '')
|
|
144
|
+
// Remove strings (after comments, so apostrophes in comments don't interfere)
|
|
145
|
+
.replace(/"(?:[^"\\]|\\.)*"/g, '')
|
|
146
|
+
.replace(/'(?:[^'\\]|\\.)*'/g, '');
|
|
28
147
|
|
|
29
148
|
// Track nesting depth of async contexts
|
|
30
149
|
// This is simplified - real implementation would use AST
|
|
@@ -108,19 +227,50 @@ ${code}
|
|
|
108
227
|
* @returns {string} Wrapped code that returns last expression
|
|
109
228
|
*/
|
|
110
229
|
export function wrapWithLastExpression(code) {
|
|
111
|
-
|
|
230
|
+
// Auto-insert awaits for common async patterns (fetch, import, .json(), etc.)
|
|
231
|
+
// This makes JavaScript feel more linear like Python/R/Julia
|
|
232
|
+
const autoAwaitedCode = autoInsertAwaits(code);
|
|
233
|
+
|
|
234
|
+
// Check if code needs async (either explicit await or auto-inserted)
|
|
235
|
+
const needsAsync = hasTopLevelAwait(autoAwaitedCode);
|
|
236
|
+
|
|
237
|
+
if (needsAsync) {
|
|
238
|
+
// For code with await, wrap in async IIFE
|
|
239
|
+
// This allows await to work at the "top level" of the user's code
|
|
240
|
+
const asyncWrappedCode = `(async () => {\n${autoAwaitedCode}\n})()`;
|
|
241
|
+
|
|
242
|
+
// Now wrap to capture the result
|
|
243
|
+
// Use indirect eval (0, eval)() to run in global scope so var declarations persist
|
|
244
|
+
const wrapped = `
|
|
245
|
+
;(async function() {
|
|
246
|
+
let __result__;
|
|
247
|
+
try {
|
|
248
|
+
__result__ = await (0, eval)(${JSON.stringify(asyncWrappedCode)});
|
|
249
|
+
} catch (e) {
|
|
250
|
+
if (e instanceof SyntaxError) {
|
|
251
|
+
await (0, eval)(${JSON.stringify(asyncWrappedCode)});
|
|
252
|
+
__result__ = undefined;
|
|
253
|
+
} else {
|
|
254
|
+
throw e;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
return __result__;
|
|
258
|
+
})()`;
|
|
259
|
+
return wrapped.trim();
|
|
260
|
+
}
|
|
112
261
|
|
|
113
|
-
//
|
|
114
|
-
//
|
|
262
|
+
// No async needed - use simpler synchronous wrapper
|
|
263
|
+
// Use indirect eval (0, eval)() to run in global scope so var declarations persist
|
|
264
|
+
// Note: Still use autoAwaitedCode in case auto-awaits were added but hasTopLevelAwait missed them
|
|
115
265
|
const wrapped = `
|
|
116
|
-
;(
|
|
266
|
+
;(function() {
|
|
117
267
|
let __result__;
|
|
118
268
|
try {
|
|
119
|
-
__result__ = eval(${JSON.stringify(
|
|
269
|
+
__result__ = (0, eval)(${JSON.stringify(autoAwaitedCode)});
|
|
120
270
|
} catch (e) {
|
|
121
271
|
if (e instanceof SyntaxError) {
|
|
122
272
|
// Code might be statements, not expression
|
|
123
|
-
eval(${JSON.stringify(
|
|
273
|
+
(0, eval)(${JSON.stringify(autoAwaitedCode)});
|
|
124
274
|
__result__ = undefined;
|
|
125
275
|
} else {
|
|
126
276
|
throw e;
|
|
@@ -122,12 +122,12 @@ export function transformForPersistence(code) {
|
|
|
122
122
|
|
|
123
123
|
// Check for const/let keywords
|
|
124
124
|
if (isWordBoundary(code, i)) {
|
|
125
|
-
if (code.slice(i, i + 5) === 'const' &&
|
|
125
|
+
if (code.slice(i, i + 5) === 'const' && isWordBoundaryAfter(code, i + 5)) {
|
|
126
126
|
result += 'var';
|
|
127
127
|
i += 5;
|
|
128
128
|
continue;
|
|
129
129
|
}
|
|
130
|
-
if (code.slice(i, i + 3) === 'let' &&
|
|
130
|
+
if (code.slice(i, i + 3) === 'let' && isWordBoundaryAfter(code, i + 3)) {
|
|
131
131
|
result += 'var';
|
|
132
132
|
i += 3;
|
|
133
133
|
continue;
|