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