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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mrmd-js",
3
- "version": "2.0.0",
3
+ "version": "2.0.1",
4
4
  "description": "MRP-compliant browser JavaScript runtime for mrmd notebooks",
5
5
  "type": "module",
6
6
  "main": "dist/index.cjs",
@@ -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
- const rawResult = await context.execute(wrapped, { execId: options.execId });
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
 
@@ -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
- * Check if code contains top-level await
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 template literals (simple version)
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
- const needsAsync = hasTopLevelAwait(code);
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
- // Find the last expression and make it a return value
114
- // This is tricky without AST - we use eval trick instead
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
- ;(${needsAsync ? 'async ' : ''}function() {
266
+ ;(function() {
117
267
  let __result__;
118
268
  try {
119
- __result__ = eval(${JSON.stringify(code)});
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(code)});
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' && isWordBoundary(code, i + 5)) {
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' && isWordBoundary(code, i + 3)) {
130
+ if (code.slice(i, i + 3) === 'let' && isWordBoundaryAfter(code, i + 3)) {
131
131
  result += 'var';
132
132
  i += 3;
133
133
  continue;