-
Notifications
You must be signed in to change notification settings - Fork 30k
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
repl: add top-level static import #17285
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
'use strict'; | ||
|
||
const Loader = require('internal/loader/Loader'); | ||
|
||
function newLoader(hooks) { | ||
const loader = new Loader(); | ||
Loader.registerImportDynamicallyCallback(loader); | ||
if (hooks) | ||
loader.hook(hooks); | ||
|
||
return loader; | ||
} | ||
|
||
module.exports = { | ||
loaded: false, | ||
get loader() { | ||
delete this.loader; | ||
this.loader = newLoader(); | ||
this.loaded = true; | ||
return this.loader; | ||
}, | ||
replace(hooks) { | ||
this.loader = newLoader(hooks); | ||
}, | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,79 @@ | ||
'use strict'; | ||
|
||
const acorn = require('internal/deps/acorn/dist/acorn'); | ||
const ESLoader = require('internal/loader/singleton'); | ||
|
||
/* | ||
Performs importing from static import statments | ||
|
||
"a(); import { x, y } from 'some_modules'; x(); y" | ||
|
||
would become: | ||
|
||
"a(); x(); y" | ||
|
||
The symbol is a promise from Promise.all on an array of promises | ||
from Loader#import. When a promise finishes it attaches to | ||
`repl.context` with a getter, so that the imported names behave | ||
like variables not values (live binding). | ||
*/ | ||
|
||
function processTopLevelImport(src, repl) { | ||
let root; | ||
try { | ||
root = acorn.parse(src, { ecmaVersion: 8, sourceType: 'module' }); | ||
} catch (err) { | ||
return null; | ||
} | ||
|
||
const importLength = | ||
root.body.filter((n) => n.type === 'ImportDeclaration').length; | ||
|
||
if (importLength === 0) | ||
return null; | ||
|
||
const importPromises = []; | ||
let newBody = ''; | ||
|
||
for (const index in root.body) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Using root.body.forEach((node, index) => {
...
}); There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. i trust acorn to not mess with things like that, but if you think its needed i can make the change There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. using
|
||
const node = root.body[index]; | ||
if (node.type === 'ImportDeclaration') { | ||
const lPromise = ESLoader.loader.import(node.source.value) | ||
.then((exports) => { | ||
const { specifiers } = node; | ||
if (specifiers[0].type === 'ImportNamespaceSpecifier') { | ||
Object.defineProperty(repl.context, specifiers[0].local.name, { | ||
enumerable: true, | ||
configurable: true, | ||
get() { return exports; } | ||
}); | ||
} else { | ||
const properties = {}; | ||
for (const { imported, local } of specifiers) { | ||
const imp = imported ? imported.name : 'default'; | ||
properties[local.name] = { | ||
enumerable: true, | ||
configurable: true, | ||
get() { return exports[imp]; } | ||
}; | ||
} | ||
Object.defineProperties(repl.context, properties); | ||
} | ||
}); | ||
|
||
importPromises.push(lPromise); | ||
} else if (node.expression !== undefined) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I believe this case is unnecessary now. You should be able to evaluate the code with all There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Any thoughts on this? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. i think its faster to concat a new string than it is to remove chunks, but i could be wrong, if anyone has stats on this please let me know. |
||
const { start, end } = node.expression; | ||
newBody += src.substring(start, end) + ';'; | ||
} else { | ||
newBody += src.substring(node.start, node.end); | ||
} | ||
} | ||
|
||
return { | ||
promise: Promise.all(importPromises), | ||
body: newBody, | ||
}; | ||
} | ||
|
||
module.exports = processTopLevelImport; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
export default 'default'; | ||
|
||
export const six = 6; | ||
|
||
export let i = 0; | ||
export const increment = () => { i++; }; | ||
|
||
const _delete = 'delete'; | ||
export { _delete as delete }; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,145 @@ | ||
'use strict'; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit, we need to add tests for the following cases so they do not change over time #17285 (comment) . |
||
|
||
const common = require('../common'); | ||
const assert = require('assert'); | ||
const { stripVTControlCharacters } = require('internal/readline'); | ||
const repl = require('repl'); | ||
|
||
common.crashOnUnhandledRejection(); | ||
|
||
// Flags: --expose-internals --experimental-modules | ||
|
||
const PROMPT = 'import repl > '; | ||
|
||
class REPLStream extends common.ArrayStream { | ||
constructor() { | ||
super(); | ||
this.waitingForResponse = false; | ||
this.lines = ['']; | ||
} | ||
|
||
write(chunk, encoding, callback) { | ||
if (Buffer.isBuffer(chunk)) { | ||
chunk = chunk.toString(encoding); | ||
} | ||
const chunkLines = stripVTControlCharacters(chunk).split('\n'); | ||
this.lines[this.lines.length - 1] += chunkLines[0]; | ||
if (chunkLines.length > 1) { | ||
this.lines.push(...chunkLines.slice(1)); | ||
} | ||
this.emit('line'); | ||
if (callback) callback(); | ||
return true; | ||
} | ||
|
||
wait(lookFor = PROMPT) { | ||
if (this.waitingForResponse) { | ||
throw new Error('Currently waiting for response to another command'); | ||
} | ||
this.lines = ['']; | ||
return common.fires(new Promise((resolve, reject) => { | ||
const onError = (err) => { | ||
this.removeListener('line', onLine); | ||
reject(err); | ||
}; | ||
const onLine = () => { | ||
if (this.lines[this.lines.length - 1].includes(lookFor)) { | ||
this.removeListener('error', onError); | ||
this.removeListener('line', onLine); | ||
resolve(this.lines); | ||
} | ||
}; | ||
this.once('error', onError); | ||
this.on('line', onLine); | ||
}), new Error(), 1000); | ||
} | ||
} | ||
|
||
const putIn = new REPLStream(); | ||
const testMe = repl.start({ | ||
prompt: PROMPT, | ||
stream: putIn, | ||
terminal: true, | ||
useColors: false, | ||
breakEvalOnSigint: true | ||
}); | ||
|
||
function runAndWait(cmds, lookFor) { | ||
const promise = putIn.wait(lookFor); | ||
for (const cmd of cmds) { | ||
if (typeof cmd === 'string') { | ||
putIn.run([cmd]); | ||
} else { | ||
testMe.write('', cmd); | ||
} | ||
} | ||
return promise; | ||
} | ||
|
||
async function runEach(cmds) { | ||
const out = []; | ||
for (const cmd of cmds) { | ||
const ret = await runAndWait([cmd]); | ||
out.push(...ret); | ||
} | ||
return out; | ||
} | ||
|
||
const file = './test/fixtures/esm-with-basic-exports.mjs'; | ||
async function main() { | ||
assert.deepStrictEqual(await runEach([ | ||
`/* comment */ import { six } from "${file}"; six + 1;` | ||
]), [ | ||
`/* comment */ import { six } from "${file}"; six + 1;\r`, | ||
'7', | ||
PROMPT | ||
]); | ||
|
||
assert.deepStrictEqual(await runEach([ | ||
`import def from "${file}"`, | ||
'def', | ||
]), [ | ||
`import def from "${file}"\r`, | ||
'undefined', | ||
PROMPT, | ||
'def\r', | ||
'\'default\'', | ||
PROMPT | ||
]); | ||
|
||
testMe.resetContext(); | ||
|
||
assert.deepStrictEqual(await runEach([ | ||
`import * as test from "${file}"`, | ||
'test.six', | ||
]), [ | ||
`import * as test from "${file}"\r`, | ||
'undefined', | ||
PROMPT, | ||
'test.six\r', | ||
'6', | ||
PROMPT | ||
]); | ||
|
||
assert.deepStrictEqual(await runEach([ | ||
`import { i, increment } from "${file}"`, | ||
'i', | ||
'increment()', | ||
'i', | ||
]), [ | ||
`import { i, increment } from "${file}"\r`, | ||
'undefined', | ||
PROMPT, | ||
'i\r', | ||
'0', | ||
PROMPT, | ||
'increment()\r', | ||
'undefined', | ||
PROMPT, | ||
'i\r', | ||
'1', | ||
PROMPT, | ||
]); | ||
} | ||
|
||
main(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for this @devsnek 👆
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎉🎉🎉