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

feat: add REPL top level await support #1383

Merged
merged 83 commits into from Aug 8, 2021
Merged
Show file tree
Hide file tree
Changes from 71 commits
Commits
Show all changes
83 commits
Select commit Hold shift + click to select a range
e8dc156
feat: add repl top level await support
ejose19 Jul 3, 2021
cb0b9f0
refactor: add more files as dist-raw, adjust primordial imports
ejose19 Jul 8, 2021
95b3385
refactor: add flag support for experimental repl await
ejose19 Jul 8, 2021
3e2e9ab
refactor: add support for Typescript input in experimental repl await
ejose19 Jul 8, 2021
0335a36
refactor: conditionally await eval result
ejose19 Jul 8, 2021
907a25b
chore: update node-repl-await file
ejose19 Jul 9, 2021
b8fe6aa
refactor: dynamically exclude TLA diagnostics when --experimental-rep…
ejose19 Jul 9, 2021
e3063c4
refactor: exclude sourceMap when transpiling for experimentalReplAwait
ejose19 Jul 9, 2021
ec5611d
refactor: add acorn & acorn-walk as dependencies and remove them from…
ejose19 Jul 9, 2021
d1c667b
refactor: allow setting experimentalReplAwait via env & tsconfig
ejose19 Jul 9, 2021
d2b2ee0
refactor: adjust evalCode signature to avoid being a breaking change
ejose19 Jul 9, 2021
40060c3
refactor: improve top level await mechanism
ejose19 Jul 9, 2021
ce11fec
refactor: adjust ignored diagnostic codes related to top level await/…
ejose19 Jul 9, 2021
f8a89a0
refactor: use evalCodeInternal and revert evalCode to previous signature
ejose19 Jul 9, 2021
4032134
refactor: await evalAndExitOnTsError result during bin `main`
ejose19 Jul 9, 2021
621bd63
refactor: adjust node-primordials
ejose19 Jul 9, 2021
9cba099
chore: remove unused require
ejose19 Jul 9, 2021
22f9b4f
refactor: revert to previous implementation of node-primordials, add …
ejose19 Jul 9, 2021
1e1f793
refactor: adjust node-repl-await to use compatible syntax up to node 12
ejose19 Jul 9, 2021
ff39161
fix: typo in node-repl-await
ejose19 Jul 9, 2021
b8d44ea
refactor: add unhandledRejection listener
ejose19 Jul 10, 2021
a431018
chore: remove node-primordials dts
ejose19 Jul 10, 2021
7f0e177
fix: typo in node version comparison
ejose19 Jul 10, 2021
4bca34c
test: add top level await tests
ejose19 Jul 10, 2021
5ad56a2
test: fix tla test
ejose19 Jul 11, 2021
40d78e1
test: add upstream test suite of tla
ejose19 Jul 11, 2021
784c46e
feat: allow REPL to be configured on start
ejose19 Jul 11, 2021
d24fe23
refactor: return repl server on `start`
ejose19 Jul 11, 2021
69f2977
refactor: use a different context when useGlobal = false
ejose19 Jul 11, 2021
af297a4
test: adjust upstream tests to latest changes
ejose19 Jul 11, 2021
bdfbb91
refactor: adjust new line placement on top level await processing
ejose19 Jul 11, 2021
fd49cca
refactor: adjust ignored codes related to TLA
ejose19 Jul 11, 2021
d50b991
test: adjust TLA tests
ejose19 Jul 11, 2021
19d1589
refactor: override target when experimental repl await is set
ejose19 Jul 11, 2021
cf81617
test: adjust tla test
ejose19 Jul 11, 2021
4e505e2
test: adjust tla tests
ejose19 Jul 11, 2021
6d49e8a
test: adjust tla tests
ejose19 Jul 11, 2021
072064e
test: move tla upstream tests to a separate file
ejose19 Jul 11, 2021
3c11fc9
refactor: lazy load processTopLevelAwait
ejose19 Jul 11, 2021
a3c488e
refactor: correctly handle errors for async eval result
ejose19 Jul 11, 2021
9abb693
refactor: throw error if target is not compatible with experimental r…
ejose19 Jul 11, 2021
f81a5ef
refactor: adjust main call in bin
ejose19 Jul 11, 2021
261e82f
refactor: don't exclude tla diagnostic codes when mode is "entrypoint"
ejose19 Jul 11, 2021
e45d2ed
refactor: move new repl start implementation to startInternal
ejose19 Jul 11, 2021
3a5ed72
fix: typo in config
ejose19 Jul 11, 2021
22efd1b
test: adjust tla tests
ejose19 Jul 12, 2021
64f04d9
test: normalize object usage in commandline
ejose19 Jul 12, 2021
ce3487e
test: move upstream tla deps from testlib to its file
ejose19 Jul 12, 2021
e6c6433
test: fix formatObjectCommandLine
ejose19 Jul 12, 2021
507fcd3
refactor: adjust type assertion
ejose19 Jul 12, 2021
ef26938
refactor: adjust _eval to iterate changes using a for loop
ejose19 Jul 12, 2021
5b0fc39
refactor: adjust execution implementation in nodeEval
ejose19 Jul 12, 2021
81e5e71
test: add tla test
ejose19 Jul 12, 2021
b68ed6f
refactor: fix processTopLevelAwait return type
ejose19 Jul 12, 2021
48778ad
refactor: restore `main` to sync, implement callback mechanism
ejose19 Jul 12, 2021
31424ad
refactor: small adjustments in repl
ejose19 Jul 12, 2021
3ac743b
refactor: remove TLA support from [stdin] & [eval]
ejose19 Jul 13, 2021
e35f1d6
refactor: adjust tla tests
ejose19 Jul 14, 2021
2c09d2d
refactor: improve code reuse in repl tests
ejose19 Jul 14, 2021
2c1e8bf
Add raw/node-repl-await.js for easier diffing in the future
cspotcode Jul 18, 2021
1418bd3
Rename flag to --no-experimental-repl-await to match node; enable by …
cspotcode Jul 18, 2021
629a4cc
Minimize changes to bin, since the only async possibility is in REPL,…
cspotcode Jul 18, 2021
6a102a7
Merge remote-tracking branch 'origin/main' into ej/replTopLevelAwait
cspotcode Jul 22, 2021
c39640e
Integrate with latest `main` branch
cspotcode Jul 22, 2021
05cb7b8
fix test
cspotcode Jul 22, 2021
051f9e1
fix tests
cspotcode Jul 22, 2021
4788cb3
fix
cspotcode Jul 22, 2021
4aaac4c
fix
cspotcode Jul 22, 2021
8c84ef7
fix tests
cspotcode Jul 22, 2021
35bee32
fix test; make test-local fix linting errors instead of blocking on them
cspotcode Jul 22, 2021
f535423
fix
cspotcode Jul 22, 2021
3489501
normalize paths passed to diagnosticFilters to hopefully fix windows …
cspotcode Jul 22, 2021
1fa5bdd
force repl's virtual file to be a module, which is safe as long as no…
cspotcode Jul 22, 2021
8a772ab
remove extraneous prop deletion in show-config output
cspotcode Jul 22, 2021
c44a3d6
remove some todos
cspotcode Jul 22, 2021
781a886
Update src/repl.ts
cspotcode Jul 22, 2021
d50a80e
refactor: move forceToBeModule from createRepl to startRepl
ejose19 Jul 22, 2021
aa31ffc
test: adjust tests
ejose19 Jul 22, 2021
3a6562d
test: set static target for TLA tests
ejose19 Jul 22, 2021
2e276b9
refactor: show hint regarding tla errors when shouldReplAwait=false
ejose19 Jul 22, 2021
6d0b486
test: small adjustments
ejose19 Jul 22, 2021
e985fd0
Final cleanup
cspotcode Aug 8, 2021
fae10d4
increase wait time in repl test to reduce flakiness
cspotcode Aug 8, 2021
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
10 changes: 10 additions & 0 deletions dist-raw/node-primordials.js
@@ -1,24 +1,34 @@
module.exports = {
ArrayFrom: Array.from,
ArrayIsArray: Array.isArray,
ArrayPrototypeJoin: (obj, separator) => Array.prototype.join.call(obj, separator),
ArrayPrototypeShift: (obj) => Array.prototype.shift.call(obj),
ArrayPrototypeForEach: (arr, ...rest) => Array.prototype.forEach.apply(arr, rest),
ArrayPrototypeIncludes: (arr, ...rest) => Array.prototype.includes.apply(arr, rest),
ArrayPrototypeJoin: (arr, ...rest) => Array.prototype.join.apply(arr, rest),
ArrayPrototypePop: (arr, ...rest) => Array.prototype.pop.apply(arr, rest),
ArrayPrototypePush: (arr, ...rest) => Array.prototype.push.apply(arr, rest),
FunctionPrototype: Function.prototype,
JSONParse: JSON.parse,
JSONStringify: JSON.stringify,
ObjectFreeze: Object.freeze,
ObjectKeys: Object.keys,
ObjectGetOwnPropertyNames: Object.getOwnPropertyNames,
ObjectDefineProperty: Object.defineProperty,
ObjectPrototypeHasOwnProperty: (obj, prop) => Object.prototype.hasOwnProperty.call(obj, prop),
RegExpPrototypeTest: (obj, string) => RegExp.prototype.test.call(obj, string),
RegExpPrototypeSymbolReplace: (obj, ...rest) => RegExp.prototype[Symbol.replace].apply(obj, rest),
SafeMap: Map,
SafeSet: Set,
StringPrototypeEndsWith: (str, ...rest) => String.prototype.endsWith.apply(str, rest),
StringPrototypeIncludes: (str, ...rest) => String.prototype.includes.apply(str, rest),
StringPrototypeLastIndexOf: (str, ...rest) => String.prototype.lastIndexOf.apply(str, rest),
StringPrototypeIndexOf: (str, ...rest) => String.prototype.indexOf.apply(str, rest),
StringPrototypeRepeat: (str, ...rest) => String.prototype.repeat.apply(str, rest),
StringPrototypeReplace: (str, ...rest) => String.prototype.replace.apply(str, rest),
StringPrototypeSlice: (str, ...rest) => String.prototype.slice.apply(str, rest),
StringPrototypeSplit: (str, ...rest) => String.prototype.split.apply(str, rest),
StringPrototypeStartsWith: (str, ...rest) => String.prototype.startsWith.apply(str, rest),
StringPrototypeSubstr: (str, ...rest) => String.prototype.substr.apply(str, rest),
SyntaxError: SyntaxError
};
254 changes: 254 additions & 0 deletions dist-raw/node-repl-await.js
@@ -0,0 +1,254 @@
// copied from https://github.com/nodejs/node/blob/88799930794045795e8abac874730f9eba7e2300/lib/internal/repl/await.js
'use strict';

const {
ArrayFrom,
ArrayPrototypeForEach,
ArrayPrototypeIncludes,
ArrayPrototypeJoin,
ArrayPrototypePop,
ArrayPrototypePush,
FunctionPrototype,
ObjectKeys,
RegExpPrototypeSymbolReplace,
StringPrototypeEndsWith,
StringPrototypeIncludes,
StringPrototypeIndexOf,
StringPrototypeRepeat,
StringPrototypeSplit,
StringPrototypeStartsWith,
SyntaxError,
} = require('./node-primordials');

const parser = require('acorn').Parser;
const walk = require('acorn-walk');
const { Recoverable } = require('repl');

function isTopLevelDeclaration(state) {
return state.ancestors[state.ancestors.length - 2] === state.body;
}

const noop = FunctionPrototype;
const visitorsWithoutAncestors = {
ClassDeclaration(node, state, c) {
if (isTopLevelDeclaration(state)) {
state.prepend(node, `${node.id.name}=`);
ArrayPrototypePush(
state.hoistedDeclarationStatements,
`let ${node.id.name}; `
);
}

walk.base.ClassDeclaration(node, state, c);
},
ForOfStatement(node, state, c) {
if (node.await === true) {
state.containsAwait = true;
}
walk.base.ForOfStatement(node, state, c);
},
FunctionDeclaration(node, state, c) {
state.prepend(node, `${node.id.name}=`);
ArrayPrototypePush(
state.hoistedDeclarationStatements,
`var ${node.id.name}; `
);
},
FunctionExpression: noop,
ArrowFunctionExpression: noop,
MethodDefinition: noop,
AwaitExpression(node, state, c) {
state.containsAwait = true;
walk.base.AwaitExpression(node, state, c);
},
ReturnStatement(node, state, c) {
state.containsReturn = true;
walk.base.ReturnStatement(node, state, c);
},
VariableDeclaration(node, state, c) {
const variableKind = node.kind;
const isIterableForDeclaration = ArrayPrototypeIncludes(
['ForOfStatement', 'ForInStatement'],
state.ancestors[state.ancestors.length - 2].type
);

if (variableKind === 'var' || isTopLevelDeclaration(state)) {
state.replace(
node.start,
node.start + variableKind.length + (isIterableForDeclaration ? 1 : 0),
variableKind === 'var' && isIterableForDeclaration ?
'' :
'void' + (node.declarations.length === 1 ? '' : ' (')
);

if (!isIterableForDeclaration) {
ArrayPrototypeForEach(node.declarations, (decl) => {
state.prepend(decl, '(');
state.append(decl, decl.init ? ')' : '=undefined)');
});

if (node.declarations.length !== 1) {
state.append(node.declarations[node.declarations.length - 1], ')');
}
}

const variableIdentifiersToHoist = [
['var', []],
['let', []],
];
function registerVariableDeclarationIdentifiers(node) {
switch (node.type) {
case 'Identifier':
ArrayPrototypePush(
variableIdentifiersToHoist[variableKind === 'var' ? 0 : 1][1],
node.name
);
break;
case 'ObjectPattern':
ArrayPrototypeForEach(node.properties, (property) => {
registerVariableDeclarationIdentifiers(property.value);
});
break;
case 'ArrayPattern':
ArrayPrototypeForEach(node.elements, (element) => {
registerVariableDeclarationIdentifiers(element);
});
break;
}
}

ArrayPrototypeForEach(node.declarations, (decl) => {
registerVariableDeclarationIdentifiers(decl.id);
});

ArrayPrototypeForEach(
variableIdentifiersToHoist,
({ 0: kind, 1: identifiers }) => {
if (identifiers.length > 0) {
ArrayPrototypePush(
state.hoistedDeclarationStatements,
`${kind} ${ArrayPrototypeJoin(identifiers, ', ')}; `
);
}
}
);
}

walk.base.VariableDeclaration(node, state, c);
}
};

const visitors = {};
for (const nodeType of ObjectKeys(walk.base)) {
const callback = visitorsWithoutAncestors[nodeType] || walk.base[nodeType];
visitors[nodeType] = (node, state, c) => {
const isNew = node !== state.ancestors[state.ancestors.length - 1];
if (isNew) {
ArrayPrototypePush(state.ancestors, node);
}
callback(node, state, c);
if (isNew) {
ArrayPrototypePop(state.ancestors);
}
};
}

function processTopLevelAwait(src) {
const wrapPrefix = '(async () => { ';
const wrapped = `${wrapPrefix}${src} })()`;
const wrappedArray = ArrayFrom(wrapped);
let root;
try {
root = parser.parse(wrapped, { ecmaVersion: 'latest' });
} catch (e) {
if (StringPrototypeStartsWith(e.message, 'Unterminated '))
throw new Recoverable(e);
// If the parse error is before the first "await", then use the execution
// error. Otherwise we must emit this parse error, making it look like a
// proper syntax error.
const awaitPos = StringPrototypeIndexOf(src, 'await');
const errPos = e.pos - wrapPrefix.length;
if (awaitPos > errPos)
return null;
// Convert keyword parse errors on await into their original errors when
// possible.
if (errPos === awaitPos + 6 &&
StringPrototypeIncludes(e.message, 'Expecting Unicode escape sequence'))
return null;
if (errPos === awaitPos + 7 &&
StringPrototypeIncludes(e.message, 'Unexpected token'))
return null;
const line = e.loc.line;
const column = line === 1 ? e.loc.column - wrapPrefix.length : e.loc.column;
let message = '\n' + StringPrototypeSplit(src, '\n')[line - 1] + '\n' +
StringPrototypeRepeat(' ', column) +
'^\n\n' + RegExpPrototypeSymbolReplace(/ \([^)]+\)/, e.message, '');
// V8 unexpected token errors include the token string.
if (StringPrototypeEndsWith(message, 'Unexpected token'))
message += " '" +
// Wrapper end may cause acorn to report error position after the source
((src.length - 1) >= (e.pos - wrapPrefix.length)
? src[e.pos - wrapPrefix.length]
: src[src.length - 1]) +
"'";
// eslint-disable-next-line no-restricted-syntax
throw new SyntaxError(message);
}
const body = root.body[0].expression.callee.body;
const state = {
body,
ancestors: [],
hoistedDeclarationStatements: [],
replace(from, to, str) {
for (let i = from; i < to; i++) {
wrappedArray[i] = '';
}
if (from === to) str += wrappedArray[from];
wrappedArray[from] = str;
},
prepend(node, str) {
wrappedArray[node.start] = str + wrappedArray[node.start];
},
append(node, str) {
wrappedArray[node.end - 1] += str;
},
containsAwait: false,
containsReturn: false
};

walk.recursive(body, state, visitors);

// Do not transform if
// 1. False alarm: there isn't actually an await expression.
// 2. There is a top-level return, which is not allowed.
if (!state.containsAwait || state.containsReturn) {
return null;
}

const last = body.body[body.body.length - 1];
if (last.type === 'ExpressionStatement') {
// For an expression statement of the form
// ( expr ) ;
// ^^^^^^^^^^ // last
// ^^^^ // last.expression
//
// We do not want the left parenthesis before the `return` keyword;
// therefore we prepend the `return (` to `last`.
//
// On the other hand, we do not want the right parenthesis after the
// semicolon. Since there can only be more right parentheses between
// last.expression.end and the semicolon, appending one more to
// last.expression should be fine.
state.prepend(last, 'return (');
state.append(last.expression, ')');
}

return (
ArrayPrototypeJoin(state.hoistedDeclarationStatements, '') +
ArrayPrototypeJoin(wrappedArray, '')
);
}

module.exports = {
processTopLevelAwait
};
30 changes: 14 additions & 16 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion package.json
Expand Up @@ -66,7 +66,7 @@
"test-spec": "ava",
"test-cov": "nyc ava",
"test": "npm run build && npm run lint && npm run test-cov --",
"test-local": "npm run lint && npm run build-tsc && npm run build-pack && npm run test-spec --",
"test-local": "npm run lint-fix && npm run build-tsc && npm run build-pack && npm run test-spec --",
"coverage-report": "nyc report --reporter=lcov",
"prepare": "npm run clean && npm run build-nopack",
"api-extractor": "api-extractor run --local --verbose"
Expand Down Expand Up @@ -162,6 +162,8 @@
"@tsconfig/node12": "^1.0.7",
"@tsconfig/node14": "^1.0.0",
"@tsconfig/node16": "^1.0.2",
"acorn": "^8.4.1",
"acorn-walk": "^8.1.1",
ejose19 marked this conversation as resolved.
Show resolved Hide resolved
"arg": "^4.1.0",
"create-require": "^1.1.0",
"diff": "^4.0.1",
Expand Down