From 178d6d7699c536868712463c9883cb560f010d55 Mon Sep 17 00:00:00 2001 From: overlookmotel Date: Wed, 30 Aug 2023 13:24:50 +0100 Subject: [PATCH] `with` statements WIP 2 --- README.md | 2 -- TODO.md | 3 --- lib/serialize/blocks.js | 38 ++++++++++++++++++++++++-------------- 3 files changed, 24 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index 1c1f2aa6..bc0b2bd1 100644 --- a/README.md +++ b/README.md @@ -573,8 +573,6 @@ NB Applications can *use* any of these within functions, just that instances of * Unsupported: `export default Promise.resolve();` (Promise instance serialized directly) * Unsupported: `const p = Promise.resolve(); export default function f() { return p; };` (Promise instance in outer scope of exported function) -`with (...) {...}` is also not supported where it alters the scope of a function being serialized. - ### Browser code This works in part. You can, for example, build a simple React app with Livepack. diff --git a/TODO.md b/TODO.md index 29afd424..45e5b75a 100644 --- a/TODO.md +++ b/TODO.md @@ -1,9 +1,6 @@ # TODO -* Create `with () {}` block in output - * Make sure that scope function is sloppy mode * Prevent vars below `with () {}` having their names mangled * Including vars internal to functions * Deal with `with` in `eval()` * Tests -* Remove note in README that `with` is not supported diff --git a/lib/serialize/blocks.js b/lib/serialize/blocks.js index 98c5be1a..7791a2dd 100644 --- a/lib/serialize/blocks.js +++ b/lib/serialize/blocks.js @@ -607,7 +607,7 @@ module.exports = { const paramNodes = []; const {mangle} = this.options; let hasArgumentsOrEvalParam = false, - frozenThisVarName, frozenArgumentsVarName; + frozenThisVarName, frozenArgumentsVarName, withVarName; for (const paramName of paramNames) { let newName; const injectionVarNode = injectionVarNodes[paramName]; @@ -632,7 +632,7 @@ module.exports = { // Rename injection node renameInjectionVarNode(); - } else if (!containsEval) { + } else if (!containsEval || paramName === 'with') { // `with` param is always renamed newName = transformVarName(paramName); if (newName !== paramName) { // Rename all nodes @@ -642,6 +642,9 @@ module.exports = { // Rename injection node renameInjectionVarNode(); + + // Record var name for `with` object + if (paramName === 'with') withVarName = newName; } } else { // Frozen var name (potentially used in `eval()`) @@ -678,10 +681,11 @@ module.exports = { // Handle strict/sloppy mode let isStrict; if (!isRoot) { - if (hasArgumentsOrEvalParam) { + if (hasArgumentsOrEvalParam || withVarName) { // If param named `arguments` or `eval`, scope function must be sloppy mode // or it's a syntax error. // NB Only way param will be called `arguments` or `eval` is if it's frozen by an `eval()`. + // `with (...)` requires sloppy mode too. isStrict = false; } else if (strictFns.length === 0) { // No strict child functions or child blocks. Block is sloppy if any sloppy children, @@ -710,18 +714,18 @@ module.exports = { returnNode = t.sequenceExpression(internalFunctionNodes); } - // If uses frozen `this` or `arguments`, wrap return value in an IIFE - // to inject these values as actual `this` / `arguments`. - // `() => eval(x)` -> `(function() { return () => eval(x); }).apply(this$0, arguments$0)` - // TODO: In sloppy mode, it's possible for `arguments` to be re-defined as a non-iterable object - // which would cause an error when this function is called. - // A better solution when outputting sloppy mode code would be to just use a var called `arguments`, - // rather than injecting. Of course this isn't possible in ESM. - // TODO: Ensure scope function using `this` is strict mode if value of `this` is not an object. - // In sloppy mode literals passed as `this` gets boxed. - // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Strict_mode#securing_javascript - // TODO: Also doesn't work where `this` or `arguments` is circular and is injected late. if (frozenThisVarName || frozenArgumentsVarName) { + // Uses frozen `this` or `arguments`. + // Wrap return value in an IIFE to inject these values as actual `this` / `arguments`. + // `() => eval(x)` -> `(function() { return () => eval(x); }).apply(this$0, arguments$0)` + // TODO: In sloppy mode, it's possible for `arguments` to be re-defined as a non-iterable object + // which would cause an error when this function is called. + // A better solution when outputting sloppy mode code would be to just use a var called + // `arguments`, rather than injecting. Of course this isn't possible in ESM. + // TODO: Ensure scope function using `this` is strict mode if value of `this` is not an object. + // In sloppy mode literals passed as `this` gets boxed. + // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Strict_mode#securing_javascript + // TODO: Also doesn't work where `this` or `arguments` is circular and is injected late. const callArgsNodes = []; let functionNode; if (frozenThisVarName) { @@ -741,6 +745,12 @@ module.exports = { ), callArgsNodes ); + } else if (withVarName) { + // Wrap in `{ with (with$0) return ...; }`. + // NB: It's not possible for the `with` object to be a circular reference. + returnNode = t.blockStatement([ + t.withStatement(t.identifier(withVarName), t.returnStatement(returnNode)) + ]); } const node = t.arrowFunctionExpression(paramNodes, returnNode);