Skip to content

Commit

Permalink
Merge pull request #33 from caub/fix-tla
Browse files Browse the repository at this point in the history
fix TLA with esnext syntax
  • Loading branch information
brigand committed Aug 29, 2019
2 parents 8c512f4 + 216bf9b commit 9ecad94
Show file tree
Hide file tree
Showing 6 changed files with 148 additions and 78 deletions.
5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@
},
"dependencies": {
"@babel/core": "^7.5.5",
"@babel/generator": "^7.5.5",
"@babel/parser": "^7.5.5",
"@babel/plugin-proposal-async-generator-functions": "^7.2.0",
"@babel/plugin-proposal-class-properties": "^7.1.0",
"@babel/plugin-proposal-decorators": "^7.4.4",
"@babel/plugin-proposal-do-expressions": "^7.0.0",
Expand All @@ -30,10 +32,12 @@
"@babel/plugin-proposal-partial-application": "^7.4.4",
"@babel/plugin-proposal-pipeline-operator": "^7.0.0",
"@babel/plugin-proposal-throw-expressions": "^7.2.0",
"@babel/plugin-proposal-unicode-property-regex": "^7.4.4",
"@babel/plugin-syntax-bigint": "^7.0.0",
"@babel/plugin-syntax-import-meta": "^7.0.0",
"@babel/plugin-transform-modules-commonjs": "^7.5.0",
"@babel/plugin-transform-typescript": "^7.5.5",
"@babel/traverse": "^7.5.5",
"bluebird": "^3.5.2",
"chalk": "^2.4.1",
"cheerio": "^0.22.0",
Expand All @@ -44,7 +48,6 @@
"json5": "^2.1.0",
"node-fetch": "^2.3.0",
"prettier": "^1.14.3",
"recast": "^0.18.1",
"shell-escape": "^0.2.0",
"superagent": "^3.8.3"
},
Expand Down
28 changes: 21 additions & 7 deletions src/plugins/js-eval/__tests__/jsEvalPlugin-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -90,14 +90,28 @@ describe('jsEvalPlugin', () => {
expect(output).toEqual(`(okay) undefined`);
});

it('handles top-level await', async () => {
const output = await testEval('n> let x=await `wat`; x // test');
expect(output).toEqual(`(okay) 'wat'`);
});
describe('top-level-await', () => {
it('works', async () => {
expect([
await testEval('n> var x = await Promise.resolve(2n); x'),
await testEval('b> var x = await Promise.resolve(2n); x'),
await testEval('n> var x = await Promise.resolve(2n); if (x) {}'),
await testEval('b> var x = await Promise.resolve(2n); if (x) {}'),
]).toEqual([
'(okay) 2n',
'(okay) 2n',
'(okay) undefined',
'(okay) undefined',
])
});

it('handles top-level await with babel', async () => {
const output = await testEval('b> await `wat` // test');
expect(output).toEqual(`(okay) 'wat'`);
it('works with comments', async () => {
const output = await testEval('n> let x=await `wat`; x // test');
expect(output).toEqual(`(okay) 'wat'`);

const output2 = await testEval('b> await `wat` // test');
expect(output2).toEqual(`(okay) 'wat'`);
});
});

it('works with engine262', async () => {
Expand Down
54 changes: 54 additions & 0 deletions src/plugins/js-eval/babelPlugins.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
// all babel proposal plugins https://github.com/babel/babel/tree/master/packages (preset-stage-n packages are depreacted https://babeljs.io/docs/en/next/babel-preset-stage-1)
// if there are new ones, feel free to add them
exports.transformPlugins = [
'@babel/plugin-proposal-async-generator-functions',
'@babel/plugin-transform-typescript',
'@babel/plugin-transform-modules-commonjs', // required by dynamicImport
['@babel/plugin-proposal-decorators', { decoratorsBeforeExport: false }], // must be before class-properties https://babeljs.io/docs/en/babel-plugin-proposal-decorators#note-compatibility-with-babel-plugin-proposal-class-properties
'@babel/plugin-proposal-class-properties',
'@babel/plugin-proposal-do-expressions',
'@babel/plugin-proposal-export-default-from',
'@babel/plugin-proposal-export-namespace-from',
'@babel/plugin-proposal-function-sent',
'@babel/plugin-proposal-function-bind',
'@babel/plugin-proposal-json-strings',
'@babel/plugin-proposal-logical-assignment-operators',
'@babel/plugin-proposal-nullish-coalescing-operator',
'@babel/plugin-proposal-numeric-separator',
'@babel/plugin-proposal-optional-catch-binding',
'@babel/plugin-proposal-optional-chaining',
'@babel/plugin-proposal-partial-application',
['@babel/plugin-proposal-pipeline-operator', { proposal: 'minimal' }],
'@babel/plugin-proposal-throw-expressions',
'@babel/plugin-proposal-dynamic-import',
'@babel/plugin-syntax-bigint',
'@babel/plugin-syntax-import-meta',
'@babel/plugin-proposal-unicode-property-regex'
];


// @babel/parser plugins https://babeljs.io/docs/en/next/babel-parser.html#ecmascript-proposals-https-githubcom-babel-proposals
exports.parserPlugins = [
'asyncGenerators',
'bigInt',
['decorators', { decoratorsBeforeExport: true }],
'classProperties',
'classPrivateProperties',
'classPrivateMethods',
'doExpressions',
'dynamicImport',
'exportDefaultFrom',
'exportNamespaceFrom',
'functionBind',
'functionSent',
'importMeta',
'logicalAssignment',
'nullishCoalescingOperator',
'numericSeparator',
// 'objectRestSpread', // no need, node has it since a long time
'optionalCatchBinding',
'optionalChaining',
'partialApplication',
['pipelineOperator', { proposal: 'minimal' }],
'throwExpressions'
]
51 changes: 19 additions & 32 deletions src/plugins/js-eval/jsEvalPlugin.js
Original file line number Diff line number Diff line change
@@ -1,54 +1,41 @@
const cp = require('child_process');
const babel = require('@babel/core');
const babelGenerator = require('@babel/generator').default;
const jsEval = require('./jsEval');
const processTopLevelAwait = require('./processTopLevelAwait');
const { transformPlugins } = require('./babelPlugins');

const helpMsg = `n> node stable, h> node harmony, b> babel, s> node vm.Script, m> node vm.SourceTextModule, e> engine262`;

const helpMsg = `n> node stable, h> node --harmony, b> babel, s> node vm.Script, m> node vm.SourceTextModule, e> engine262`;

// default jseval run command
const CMD = ['node', '--no-warnings', '/run/run.js'];
const CMD_SHIMS = ['node', '-r', '/run/node_modules/airbnb-js-shims/target/es2019', '--no-warnings', '/run/run.js'];
const CMD_HARMONY = ['node', '--harmony', '--experimental-vm-modules', '--experimental-modules', '--no-warnings', '/run/run.js'];


const jsEvalPlugin = async ({ mentionUser, respond, message, selfConfig = {} }) => {
if (!/^[nhbsme?]>/.test(message)) return;
const mode = message[0];
if (mode === '?') return respond((mentionUser ? `${mentionUser}, ` : '') + helpMsg);
let code = message.slice(2);

if (mode === 'b') {
code = (await babel.transformAsync(code, {
parserOpts: {
allowAwaitOutsideFunction: true,
allowReturnOutsideFunction: true,
},
plugins: [
'@babel/plugin-transform-typescript',
'@babel/plugin-transform-modules-commonjs', // required by dynamicImport
['@babel/plugin-proposal-decorators', { decoratorsBeforeExport: false }], // must be before class-properties https://babeljs.io/docs/en/babel-plugin-proposal-decorators#note-compatibility-with-babel-plugin-proposal-class-properties
'@babel/plugin-proposal-class-properties',
'@babel/plugin-proposal-do-expressions',
'@babel/plugin-proposal-export-default-from',
'@babel/plugin-proposal-export-namespace-from',
'@babel/plugin-proposal-function-sent',
'@babel/plugin-proposal-function-bind',
'@babel/plugin-proposal-json-strings',
'@babel/plugin-proposal-logical-assignment-operators',
'@babel/plugin-proposal-nullish-coalescing-operator',
'@babel/plugin-proposal-numeric-separator',
'@babel/plugin-proposal-optional-catch-binding',
'@babel/plugin-proposal-optional-chaining',
'@babel/plugin-proposal-partial-application',
['@babel/plugin-proposal-pipeline-operator', { proposal: 'minimal' }],
'@babel/plugin-proposal-throw-expressions',
'@babel/plugin-proposal-dynamic-import',
'@babel/plugin-syntax-bigint',
'@babel/plugin-syntax-import-meta',
]
})).code;
const hasMaybeTLA = /\bawait\b/.test(code);

if (mode === 'b' && !hasMaybeTLA) {
code = (await babel.transformAsync(code, { plugins: transformPlugins })).code;
}

code = processTopLevelAwait(code) || code; // it returns null when no TLA is found
if (hasMaybeTLA) { // there's maybe a TLA await
const iiafe = processTopLevelAwait(code);
if (iiafe) { // there's a TLA
if (mode === 'b') {
code = (await babel.transformFromAstAsync(iiafe, code, { plugins: transformPlugins })).code;
} else {
code = babelGenerator(iiafe).code;
}
}
}

try {
const result = await jsEval(
Expand Down
86 changes: 49 additions & 37 deletions src/plugins/js-eval/processTopLevelAwait.js
Original file line number Diff line number Diff line change
@@ -1,18 +1,19 @@
const babelParser = require('@babel/parser');
const recast = require('recast');

const b = recast.types.builders;

const babelTraverse = require('@babel/traverse').default;
const { parserPlugins } = require('./babelPlugins');

/**
*
* @param {string} src
* @return {object} ast
*/
function processTopLevelAwait(src) {
let root;

try {
root = recast.parse(src, {
parser: {
parse(src) {
return babelParser.parse(src, { allowAwaitOutsideFunction: true });
}
}
root = babelParser.parse(src, {
allowAwaitOutsideFunction: true,
plugins: parserPlugins
});
} catch (error) {
return null; // if code is not valid, don't bother
Expand All @@ -21,33 +22,30 @@ function processTopLevelAwait(src) {
let containsAwait = false;
let containsReturn = false;

recast.visit(root, {
visitNode: function (path) {
const node = path.value;

switch (node.type) {
babelTraverse(root, {
enter(path) {
switch (path.type) {
case 'FunctionDeclaration':
case 'FunctionExpression':
case 'ArrowFunctionExpression':
case 'MethodDefinition':
case 'ClassMethod':
// stop when entering a new function scope:
return false;
return path.stop();

case 'ForOfStatement':
if (node.await === true) {
if (path.node.await === true) {
containsAwait = true;
}
return this.traverse(path);
return;

case 'AwaitExpression':
containsAwait = true;
return this.traverse(path);
return;

case 'ReturnStatement':
containsReturn = true;
return this.traverse(path);

default:
return this.traverse(path);
return;
}
}
});
Expand All @@ -60,25 +58,39 @@ function processTopLevelAwait(src) {
}

let last = root.program.body[root.program.body.length - 1];

// replace last node with a returnStatement of this node, if the last node is an expression
if (last.type === 'ExpressionStatement') {
last = last.expression;
root.program.body[root.program.body.length - 1] = {
type: 'ReturnStatement',
argument: last.expression
};
}

// replace last node with a returnStatement of this node
root.program.body[root.program.body.length - 1] = b.returnStatement(last);

const iiafe = b.callExpression(
b.arrowFunctionExpression(
[],
b.blockStatement(root.program.body),
true
),
[]
);

iiafe.callee.async = true;
const iiafe = {
type: 'Program',
sourceType: 'script',
body: [{
type: 'ExpressionStatement',
expression: {
type: 'CallExpression',
callee: {
type: 'ArrowFunctionExpression',
async: true,
params: [],
body: {
type: 'BlockStatement',
body: root.program.body
},
},
arguments: []
}
}],
};
// const iiafe = t.program([t.expressionStatement(t.callExpression(t.arrowFunctionExpression([], t.blockStatement(root.program.body)), []))]) // with @babel/types

return recast.print(iiafe).code;
return iiafe;
}

module.exports = processTopLevelAwait;
2 changes: 1 addition & 1 deletion src/plugins/js-eval/run.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ const inspect = (val) => {
maxArrayLength: 20,
breakLength: Infinity,
colors: false,
compact: 10,
compact: true,
depth: 10,
});
} catch {
Expand Down

0 comments on commit 9ecad94

Please sign in to comment.