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.js CHANGED
@@ -54,6 +54,15 @@ const DEFAULT_FEATURES = {
54
54
  * @typedef {import('./context/interface.js').LogEntry} LogEntry
55
55
  */
56
56
 
57
+ /**
58
+ * Check if a value is a Promise
59
+ * @param {*} value
60
+ * @returns {boolean}
61
+ */
62
+ function isPromise(value) {
63
+ return value && typeof value === 'object' && typeof value.then === 'function';
64
+ }
65
+
57
66
  /**
58
67
  * Format arguments for logging
59
68
  * @param {Array<*>} args
@@ -76,6 +85,21 @@ function formatArgs(args) {
76
85
  .join(' ');
77
86
  }
78
87
 
88
+ /**
89
+ * Check args for Promises and return warning message if found
90
+ * @param {Array<*>} args
91
+ * @returns {string | null}
92
+ */
93
+ function checkForPromises(args) {
94
+ for (const arg of args) {
95
+ if (isPromise(arg)) {
96
+ return '⚠️ Promise detected - this value needs to be awaited. ' +
97
+ 'Add "await" before the async call, or assign it to get the resolved value.';
98
+ }
99
+ }
100
+ return null;
101
+ }
102
+
79
103
  /**
80
104
  * Create a console capture for a window context
81
105
  */
@@ -118,11 +142,23 @@ class ConsoleCapture {
118
142
  // Intercept methods
119
143
  console.log = (...args) => {
120
144
  this.#queue.push({ type: 'log', args, timestamp: Date.now() });
145
+ // Check for unresolved Promises and warn
146
+ const promiseWarning = checkForPromises(args);
147
+ if (promiseWarning) {
148
+ this.#queue.push({ type: 'warn', args: [promiseWarning], timestamp: Date.now() });
149
+ this.#originalConsole?.warn?.(promiseWarning);
150
+ }
121
151
  this.#originalConsole?.log?.(...args);
122
152
  };
123
153
 
124
154
  console.info = (...args) => {
125
155
  this.#queue.push({ type: 'info', args, timestamp: Date.now() });
156
+ // Check for unresolved Promises and warn
157
+ const promiseWarning = checkForPromises(args);
158
+ if (promiseWarning) {
159
+ this.#queue.push({ type: 'warn', args: [promiseWarning], timestamp: Date.now() });
160
+ this.#originalConsole?.warn?.(promiseWarning);
161
+ }
126
162
  this.#originalConsole?.info?.(...args);
127
163
  };
128
164
 
@@ -1508,12 +1544,12 @@ function transformForPersistence(code) {
1508
1544
 
1509
1545
  // Check for const/let keywords
1510
1546
  if (isWordBoundary(code, i)) {
1511
- if (code.slice(i, i + 5) === 'const' && isWordBoundary(code, i + 5)) {
1547
+ if (code.slice(i, i + 5) === 'const' && isWordBoundaryAfter(code, i + 5)) {
1512
1548
  result += 'var';
1513
1549
  i += 5;
1514
1550
  continue;
1515
1551
  }
1516
- if (code.slice(i, i + 3) === 'let' && isWordBoundary(code, i + 3)) {
1552
+ if (code.slice(i, i + 3) === 'let' && isWordBoundaryAfter(code, i + 3)) {
1517
1553
  result += 'var';
1518
1554
  i += 3;
1519
1555
  continue;
@@ -1550,15 +1586,124 @@ function isWordBoundary(code, pos) {
1550
1586
  return true;
1551
1587
  }
1552
1588
 
1589
+ /**
1590
+ * Check if position after keyword is a word boundary
1591
+ * @param {string} code
1592
+ * @param {number} pos - Position after the keyword
1593
+ * @returns {boolean}
1594
+ */
1595
+ function isWordBoundaryAfter(code, pos) {
1596
+ if (pos >= code.length) return true;
1597
+ return !/[a-zA-Z0-9_$]/.test(code[pos]);
1598
+ }
1599
+
1553
1600
  /**
1554
1601
  * Async Transform
1555
1602
  *
1556
- * Wraps code to support top-level await.
1603
+ * Wraps code to support top-level await and auto-awaits common async patterns.
1604
+ * This makes JavaScript feel more linear like Python/R/Julia.
1557
1605
  * @module transform/async
1558
1606
  */
1559
1607
 
1608
+
1609
+ /**
1610
+ * Auto-insert await before common async function calls
1611
+ * This makes JavaScript feel more linear like Python/R
1612
+ *
1613
+ * @param {string} code - Source code
1614
+ * @returns {string} Code with auto-awaits inserted
1615
+ */
1616
+ function autoInsertAwaits(code) {
1617
+ // Don't process if code already uses await extensively
1618
+ // (user knows what they're doing)
1619
+ const awaitCount = (code.match(/\bawait\b/g) || []).length;
1620
+ const lines = code.split('\n').length;
1621
+ if (awaitCount > lines / 2) {
1622
+ return code;
1623
+ }
1624
+
1625
+ let result = code;
1626
+
1627
+ // Track positions to avoid double-processing
1628
+ // We need to be careful not to add await before already-awaited expressions
1629
+
1630
+ // First, temporarily replace existing awaits to protect them
1631
+ const awaitPlaceholders = [];
1632
+ result = result.replace(/\bawait\s+/g, (match) => {
1633
+ const placeholder = `__AWAIT_PLACEHOLDER_${awaitPlaceholders.length}__`;
1634
+ awaitPlaceholders.push(match);
1635
+ return placeholder;
1636
+ });
1637
+
1638
+ // Also protect strings, comments, and template literals
1639
+ const protectedStrings = [];
1640
+
1641
+ // Protect comments FIRST (before strings) to handle apostrophes in comments like "doesn't"
1642
+ result = result.replace(/\/\/[^\n]*/g, (match) => {
1643
+ const placeholder = `__PROTECTED_${protectedStrings.length}__`;
1644
+ protectedStrings.push(match);
1645
+ return placeholder;
1646
+ });
1647
+ result = result.replace(/\/\*[\s\S]*?\*\//g, (match) => {
1648
+ const placeholder = `__PROTECTED_${protectedStrings.length}__`;
1649
+ protectedStrings.push(match);
1650
+ return placeholder;
1651
+ });
1652
+
1653
+ // Protect template literals
1654
+ result = result.replace(/`(?:[^`\\]|\\.)*`/g, (match) => {
1655
+ const placeholder = `__PROTECTED_${protectedStrings.length}__`;
1656
+ protectedStrings.push(match);
1657
+ return placeholder;
1658
+ });
1659
+
1660
+ // Protect strings (after comments, so apostrophes in comments don't interfere)
1661
+ result = result.replace(/"(?:[^"\\]|\\.)*"/g, (match) => {
1662
+ const placeholder = `__PROTECTED_${protectedStrings.length}__`;
1663
+ protectedStrings.push(match);
1664
+ return placeholder;
1665
+ });
1666
+ result = result.replace(/'(?:[^'\\]|\\.)*'/g, (match) => {
1667
+ const placeholder = `__PROTECTED_${protectedStrings.length}__`;
1668
+ protectedStrings.push(match);
1669
+ return placeholder;
1670
+ });
1671
+
1672
+ // Now auto-insert awaits for common patterns
1673
+ // fetch(...) -> await fetch(...)
1674
+ result = result.replace(/\bfetch\s*\(/g, 'await fetch(');
1675
+
1676
+ // import(...) -> await import(...)
1677
+ // But not "import x from" statements
1678
+ result = result.replace(/(?<![.\w])import\s*\(/g, 'await import(');
1679
+
1680
+ // .json() .text() .blob() etc on response objects
1681
+ // These methods also return Promises, so we need to await both:
1682
+ // response.json() -> await (await response).json()
1683
+ result = result.replace(/([a-zA-Z_$][a-zA-Z0-9_$]*)\s*\.\s*json\s*\(\s*\)/g, 'await (await $1).json()');
1684
+ result = result.replace(/([a-zA-Z_$][a-zA-Z0-9_$]*)\s*\.\s*text\s*\(\s*\)/g, 'await (await $1).text()');
1685
+ result = result.replace(/([a-zA-Z_$][a-zA-Z0-9_$]*)\s*\.\s*blob\s*\(\s*\)/g, 'await (await $1).blob()');
1686
+ result = result.replace(/([a-zA-Z_$][a-zA-Z0-9_$]*)\s*\.\s*arrayBuffer\s*\(\s*\)/g, 'await (await $1).arrayBuffer()');
1687
+ result = result.replace(/([a-zA-Z_$][a-zA-Z0-9_$]*)\s*\.\s*formData\s*\(\s*\)/g, 'await (await $1).formData()');
1688
+
1689
+ // Restore protected content
1690
+ for (let i = protectedStrings.length - 1; i >= 0; i--) {
1691
+ result = result.replace(`__PROTECTED_${i}__`, protectedStrings[i]);
1692
+ }
1693
+
1694
+ // Restore existing awaits
1695
+ for (let i = awaitPlaceholders.length - 1; i >= 0; i--) {
1696
+ result = result.replace(`__AWAIT_PLACEHOLDER_${i}__`, awaitPlaceholders[i]);
1697
+ }
1698
+
1699
+ // Clean up any double awaits we might have introduced
1700
+ result = result.replace(/\bawait\s+await\b/g, 'await');
1701
+
1702
+ return result;
1703
+ }
1704
+
1560
1705
  /**
1561
- * Check if code contains top-level await
1706
+ * Check if code contains top-level await (or will after auto-insertion)
1562
1707
  * @param {string} code
1563
1708
  * @returns {boolean}
1564
1709
  */
@@ -1567,16 +1712,17 @@ function hasTopLevelAwait(code) {
1567
1712
  // This is a heuristic; a proper check would need AST parsing
1568
1713
 
1569
1714
  // Remove strings, comments, and regex to avoid false positives
1715
+ // IMPORTANT: Remove comments FIRST to handle apostrophes in comments like "doesn't"
1570
1716
  const cleaned = code
1571
- // Remove template literals (simple version)
1572
- .replace(/`[^`]*`/g, '')
1573
- // Remove strings
1574
- .replace(/"(?:[^"\\]|\\.)*"/g, '')
1575
- .replace(/'(?:[^'\\]|\\.)*'/g, '')
1576
- // Remove single-line comments
1717
+ // Remove single-line comments FIRST (before strings)
1577
1718
  .replace(/\/\/[^\n]*/g, '')
1578
1719
  // Remove multi-line comments
1579
- .replace(/\/\*[\s\S]*?\*\//g, '');
1720
+ .replace(/\/\*[\s\S]*?\*\//g, '')
1721
+ // Remove template literals
1722
+ .replace(/`[^`]*`/g, '')
1723
+ // Remove strings (after comments, so apostrophes in comments don't interfere)
1724
+ .replace(/"(?:[^"\\]|\\.)*"/g, '')
1725
+ .replace(/'(?:[^'\\]|\\.)*'/g, '');
1580
1726
  let i = 0;
1581
1727
 
1582
1728
  while (i < cleaned.length) {
@@ -1648,19 +1794,50 @@ ${code}
1648
1794
  * @returns {string} Wrapped code that returns last expression
1649
1795
  */
1650
1796
  function wrapWithLastExpression(code) {
1651
- const needsAsync = hasTopLevelAwait(code);
1797
+ // Auto-insert awaits for common async patterns (fetch, import, .json(), etc.)
1798
+ // This makes JavaScript feel more linear like Python/R/Julia
1799
+ const autoAwaitedCode = autoInsertAwaits(code);
1652
1800
 
1653
- // Find the last expression and make it a return value
1654
- // This is tricky without AST - we use eval trick instead
1801
+ // Check if code needs async (either explicit await or auto-inserted)
1802
+ const needsAsync = hasTopLevelAwait(autoAwaitedCode);
1803
+
1804
+ if (needsAsync) {
1805
+ // For code with await, wrap in async IIFE
1806
+ // This allows await to work at the "top level" of the user's code
1807
+ const asyncWrappedCode = `(async () => {\n${autoAwaitedCode}\n})()`;
1808
+
1809
+ // Now wrap to capture the result
1810
+ // Use indirect eval (0, eval)() to run in global scope so var declarations persist
1811
+ const wrapped = `
1812
+ ;(async function() {
1813
+ let __result__;
1814
+ try {
1815
+ __result__ = await (0, eval)(${JSON.stringify(asyncWrappedCode)});
1816
+ } catch (e) {
1817
+ if (e instanceof SyntaxError) {
1818
+ await (0, eval)(${JSON.stringify(asyncWrappedCode)});
1819
+ __result__ = undefined;
1820
+ } else {
1821
+ throw e;
1822
+ }
1823
+ }
1824
+ return __result__;
1825
+ })()`;
1826
+ return wrapped.trim();
1827
+ }
1828
+
1829
+ // No async needed - use simpler synchronous wrapper
1830
+ // Use indirect eval (0, eval)() to run in global scope so var declarations persist
1831
+ // Note: Still use autoAwaitedCode in case auto-awaits were added but hasTopLevelAwait missed them
1655
1832
  const wrapped = `
1656
- ;(${needsAsync ? 'async ' : ''}function() {
1833
+ ;(function() {
1657
1834
  let __result__;
1658
1835
  try {
1659
- __result__ = eval(${JSON.stringify(code)});
1836
+ __result__ = (0, eval)(${JSON.stringify(autoAwaitedCode)});
1660
1837
  } catch (e) {
1661
1838
  if (e instanceof SyntaxError) {
1662
1839
  // Code might be statements, not expression
1663
- eval(${JSON.stringify(code)});
1840
+ (0, eval)(${JSON.stringify(autoAwaitedCode)});
1664
1841
  __result__ = undefined;
1665
1842
  } else {
1666
1843
  throw e;
@@ -1758,7 +1935,19 @@ class JavaScriptExecutor extends BaseExecutor {
1758
1935
 
1759
1936
  try {
1760
1937
  // Execute in context (pass execId for input() support)
1761
- const rawResult = await context.execute(wrapped, { execId: options.execId });
1938
+ let rawResult = await context.execute(wrapped, { execId: options.execId });
1939
+
1940
+ // Auto-await if result is a Promise (catch cases not handled by auto-await transform)
1941
+ if (rawResult.result && typeof rawResult.result === 'object' && typeof rawResult.result.then === 'function') {
1942
+ try {
1943
+ rawResult.result = await rawResult.result;
1944
+ } catch (promiseError) {
1945
+ // Promise rejected - treat as error
1946
+ rawResult.error = promiseError instanceof Error ? promiseError : new Error(String(promiseError));
1947
+ rawResult.result = undefined;
1948
+ }
1949
+ }
1950
+
1762
1951
  const duration = performance.now() - startTime;
1763
1952
 
1764
1953
  // Format result