Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix TLA with esnext syntax #33

Merged
merged 4 commits into from
Aug 29, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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;
}
brigand marked this conversation as resolved.
Show resolved Hide resolved
}
}

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