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