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/dist/mrmd-js.iife.js
CHANGED
|
@@ -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' &&
|
|
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' &&
|
|
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
|
|
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
|
-
|
|
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
|
-
//
|
|
1657
|
-
|
|
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
|
-
;(
|
|
1836
|
+
;(function() {
|
|
1660
1837
|
let __result__;
|
|
1661
1838
|
try {
|
|
1662
|
-
__result__ = eval(${JSON.stringify(
|
|
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(
|
|
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
|
-
|
|
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
|