-
-
Notifications
You must be signed in to change notification settings - Fork 542
feat: add REPL top level await support #1383
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
Merged
Merged
Changes from all 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 cb0b9f0
refactor: add more files as dist-raw, adjust primordial imports
ejose19 95b3385
refactor: add flag support for experimental repl await
ejose19 3e2e9ab
refactor: add support for Typescript input in experimental repl await
ejose19 0335a36
refactor: conditionally await eval result
ejose19 907a25b
chore: update node-repl-await file
ejose19 b8fe6aa
refactor: dynamically exclude TLA diagnostics when --experimental-rep…
ejose19 e3063c4
refactor: exclude sourceMap when transpiling for experimentalReplAwait
ejose19 ec5611d
refactor: add acorn & acorn-walk as dependencies and remove them from…
ejose19 d1c667b
refactor: allow setting experimentalReplAwait via env & tsconfig
ejose19 d2b2ee0
refactor: adjust evalCode signature to avoid being a breaking change
ejose19 40060c3
refactor: improve top level await mechanism
ejose19 ce11fec
refactor: adjust ignored diagnostic codes related to top level await/…
ejose19 f8a89a0
refactor: use evalCodeInternal and revert evalCode to previous signature
ejose19 4032134
refactor: await evalAndExitOnTsError result during bin `main`
ejose19 621bd63
refactor: adjust node-primordials
ejose19 9cba099
chore: remove unused require
ejose19 22f9b4f
refactor: revert to previous implementation of node-primordials, add …
ejose19 1e1f793
refactor: adjust node-repl-await to use compatible syntax up to node 12
ejose19 ff39161
fix: typo in node-repl-await
ejose19 b8d44ea
refactor: add unhandledRejection listener
ejose19 a431018
chore: remove node-primordials dts
ejose19 7f0e177
fix: typo in node version comparison
ejose19 4bca34c
test: add top level await tests
ejose19 5ad56a2
test: fix tla test
ejose19 40d78e1
test: add upstream test suite of tla
ejose19 784c46e
feat: allow REPL to be configured on start
ejose19 d24fe23
refactor: return repl server on `start`
ejose19 69f2977
refactor: use a different context when useGlobal = false
ejose19 af297a4
test: adjust upstream tests to latest changes
ejose19 bdfbb91
refactor: adjust new line placement on top level await processing
ejose19 fd49cca
refactor: adjust ignored codes related to TLA
ejose19 d50b991
test: adjust TLA tests
ejose19 19d1589
refactor: override target when experimental repl await is set
ejose19 cf81617
test: adjust tla test
ejose19 4e505e2
test: adjust tla tests
ejose19 6d49e8a
test: adjust tla tests
ejose19 072064e
test: move tla upstream tests to a separate file
ejose19 3c11fc9
refactor: lazy load processTopLevelAwait
ejose19 a3c488e
refactor: correctly handle errors for async eval result
ejose19 9abb693
refactor: throw error if target is not compatible with experimental r…
ejose19 f81a5ef
refactor: adjust main call in bin
ejose19 261e82f
refactor: don't exclude tla diagnostic codes when mode is "entrypoint"
ejose19 e45d2ed
refactor: move new repl start implementation to startInternal
ejose19 3a5ed72
fix: typo in config
ejose19 22efd1b
test: adjust tla tests
ejose19 64f04d9
test: normalize object usage in commandline
ejose19 ce3487e
test: move upstream tla deps from testlib to its file
ejose19 e6c6433
test: fix formatObjectCommandLine
ejose19 507fcd3
refactor: adjust type assertion
ejose19 ef26938
refactor: adjust _eval to iterate changes using a for loop
ejose19 5b0fc39
refactor: adjust execution implementation in nodeEval
ejose19 81e5e71
test: add tla test
ejose19 b68ed6f
refactor: fix processTopLevelAwait return type
ejose19 48778ad
refactor: restore `main` to sync, implement callback mechanism
ejose19 31424ad
refactor: small adjustments in repl
ejose19 3ac743b
refactor: remove TLA support from [stdin] & [eval]
ejose19 e35f1d6
refactor: adjust tla tests
ejose19 2c09d2d
refactor: improve code reuse in repl tests
ejose19 2c1e8bf
Add raw/node-repl-await.js for easier diffing in the future
cspotcode 1418bd3
Rename flag to --no-experimental-repl-await to match node; enable by …
cspotcode 629a4cc
Minimize changes to bin, since the only async possibility is in REPL,…
cspotcode 6a102a7
Merge remote-tracking branch 'origin/main' into ej/replTopLevelAwait
cspotcode c39640e
Integrate with latest `main` branch
cspotcode 05cb7b8
fix test
cspotcode 051f9e1
fix tests
cspotcode 4788cb3
fix
cspotcode 4aaac4c
fix
cspotcode 8c84ef7
fix tests
cspotcode 35bee32
fix test; make test-local fix linting errors instead of blocking on them
cspotcode f535423
fix
cspotcode 3489501
normalize paths passed to diagnosticFilters to hopefully fix windows …
cspotcode 1fa5bdd
force repl's virtual file to be a module, which is safe as long as no…
cspotcode 8a772ab
remove extraneous prop deletion in show-config output
cspotcode c44a3d6
remove some todos
cspotcode 781a886
Update src/repl.ts
cspotcode d50a80e
refactor: move forceToBeModule from createRepl to startRepl
ejose19 aa31ffc
test: adjust tests
ejose19 3a6562d
test: set static target for TLA tests
ejose19 2e276b9
refactor: show hint regarding tla errors when shouldReplAwait=false
ejose19 6d0b486
test: small adjustments
ejose19 e985fd0
Final cleanup
cspotcode fae10d4
increase wait time in repl test to reduce flakiness
cspotcode File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
}; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
}; |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.