-
Notifications
You must be signed in to change notification settings - Fork 43
Open issues with require(esm) #454
Comments
Example 1: Transitive Dependency Uses TLA// This code works perfectly fine - until a transitive dependency of `some.mjs` uses TLA.
// Then suddenly it breaks and it's afaict impossible to fix since there's nothing to wait for.
// myThing suddenly is undefined even though some.mjs didn't change.
const { myThing } = require('./some.mjs');
myThing(); |
Example 2: Temporary Event Loop and ResourcesThe syncify in the existing prototype requires the introduction of a separate event loop. See: weswigham/ecmascript-modules@3e6a768 If any part of the loading pipeline, including custom loaders, creates resources attached to the event loop, they can prevent the event loop from terminating. Reversely, deref'd resources may appear to "hang" with timeouts. Also, if a long-running handle is created, it may still reference the old event loop (e.g. opening a file archive and keeping it open for future loads). |
Loader hooks are async so I expect code which performed |
@coreyfarrell no, loader hooks work fine with sync code. see https://docs.google.com/presentation/d/16XSS7P2RMUtpBA8iolaMV9MpLP_Yot8lAml1trbl38I/edit?usp=sharing for details on how that is achievable. |
@bmeck I think that comes with an important qualifier though, right? "Hopefully upcoming off-thread loader hooks work fine with sync code". Because the current dynamic instantiate hook isn't necessarily safe in this context afaict. |
@jkrems correct, the current loader hooks are not expected to survive in their state and I'd be unlikely to endorse on thread/shared heap for them currently. |
@robpalme 's concern about async-to-execute stuff entering via your dependencies isn't unique to |
Yah, so does |
I think the reverse. Top-level await's spec chicanery very explicitly makes "flattening" promises a thing. All the execution APIs are supposed to be async to handle TLA; but TLA is also specced, very explicitly, to skip all those extra event loop turns and promise resolutions in cases where the module doesn't actually use |
Long story short, you point out something you find distasteful with |
The specific spec invariant with require of TLA is that the event loop is
being forked, with the main event loop effectively stalled while the ESM
event loop runs separately.
When spawning a child process this is being done using the process
boundaries.
But within the same memory and realm this violates a number of spec timing
properties of the language all at once.
JavaScript is not designed to support two event loops running, where which
event loop code is loaded through, when, and when they will be locked /
running is non determinate.
…On Tue, Dec 10, 2019 at 14:37 Wesley Wigham ***@***.***> wrote:
Long story short, you point out something you find distasteful with
require(esm), and I can probably point out how TLA in modules ends up
requiring the runtime do the same thing. If anything, I feel vindicated by
the current TLA implementation. If we could get a createFunction v8 API
that supported setting the Async flag (which doesn't seem hard to make,
but because I'm not familiar with v8, I'm unsure how it should get done), I
totally think we could even support TLA in cjs with the same caveats it has
in esm (sync code is immediately unwrapped, otherwise it's waited on and
can deadlock on contended resources, just like TLA).
—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
<#454?email_source=notifications&email_token=AAESFSW2KBAFN6PHBR6STATQX7VV3A5CNFSM4JZBQMV2YY3PNVWWK3TUL52HS4DFVREXG43VMVBW63LNMVXHJKTDN5WW2ZLOORPWSZGOEGQSQVA#issuecomment-564209748>,
or unsubscribe
<https://github.com/notifications/unsubscribe-auth/AAESFSXSTI3KTOAJVCSEFDTQX7VV3ANCNFSM4JZBQMVQ>
.
|
Are you talking about a previous draft of TLA? I'm fairly certain that the ESM equivalent of Example 1 is perfectly safe and doesn't break if a transitive dependency uses TLA. import { myThing } from './some.mjs';
// We definitely know that myThing is initialized, even if some transitive dep of some.mjs
// is using top level await.
myThing(); Can you be more specific where TLA in a transitive dep is observable in the same way in ESM? |
@jkrems I'm talking about how If I have // @filename: root.js
import {thing} from "./setUpThing.js";
import "some-library";
import {consume} from "./consumeThing.js";
consume(thing);
// @filename: setUpThing.js
export let thing = true;
function removeThing() {
thing = undefined;
}
Promise.resolve().then(removeThing); // schedule `thing` to be immediately removed because reasons - must be used synchronously
// @filename: consumeThing.js
export function consume(thing) {
console.log(thing ? "all sync deps" : "async dep somewhere");
} the log in With respect to const { myThing } = require('./some.mjs');
myThing(); given how TLA works in ESM, I think it's now reasonable to wait on any promises on |
Okay, I'd prefer if we focus on the relatively clear usability example that doesn't require any special code (other than "some dependency uses TLA in any way whatsoever") to break. I don't think it's fair to put your example on the same level here because it requires very specific coding patterns. So, looking at your explanation: It sounds like what you're actually suggesting as the solution is that during the execution of const x = fs.createReadStream('./x');
require('./foo'); // x may start emitting error events
x.on('error', console.error); That's definitely more of an edge case compared to "Example 1" since it requires that |
TLA does the same for esm. That's the tradeoff TLA makes: const x = fs.createReadStream('./x');
await import('./foo'); // x may start emitting error events
x.on('error', console.error); |
The moral here is that using TLA is observable by people who require/import you, as it yields back to the event loop. That's really it. That's true in |
That code snippet isn't the same. Your code snippet has an explicit const x = fs.createReadStream('./x');
someFunctionThatLooksSync(); // x may start emitting error events
x.on('error', console.error); Because |
As I demonstrated |
What you demonstrate with that snippet is that TLA may delay file execution by ticks. That's part of the spec. This will be part of every runtime and - to a degree - everybody "knew" that the timing of file execution wouldn't be always in one tick. That's a different question. What I'm complaining about is that an arbitrary function call may cause execution to suspend and the event loop to run in the middle of statement evaluation, not even just in the middle of a file. That's the same feature as supporting a synchronous |
To clarify on "may cause execution to suspend and the event loop to run in the middle of statement evaluation", here's an example of code that's now suddenly unsafe because it becomes relevant in which order function arguments get evaluated: attachLoggingErrorListener(
fs.createReadStream('./x'),
// calling a function in the following argument isn't safe!
// someFunctionThatLooksSync may cause ticks!
someFunctionThatLooksSync()
); |
|
And the overwhelmingly common case is that no event loop turns happen at all. It's only if they're required (because a module requires asynchronous execution) that it occurs. |
I've opened nodejs/node#30891 so people more familiar with how |
That's like saying " This would effectively add an API for synchronous await to node. I personally don't think node should add an API for synchronous await. There's a reason that await of promises requires a keyword in JavaScript. |
This implements the ability to use require on .mjs files, loaded via the esm loader, using the same tradeoffs that top level await makes in esm itself. What this means: If possible, all execution and evaluation is done synchronously, via immediately unwrapping the execution's component promises. This means that any and all existing code should have no observable change in behavior, as there exist no asynchronous modules as of yet. The catch is that once a module which requires asynchronous execution is used, it must yield to the event loop to perform that execution, which, in turn, can allow other code to execute before the continuation after the async action, which is observable to callers of the now asynchronous module. If this matters to your callers, this means making your module execution asynchronous could be considered a breaking change to your library, however in practice, it will not matter for most callers. Moreover, as the ecosystem exists today, there are zero asynchronously executing modules, and so until there are, there are no downsides to this approach at all, as no execution is changed from what one would expect today (excepting, ofc, that it's no longer an error to require("./foo.mjs"). Ref: nodejs/modules#308 Ref: https://github.com/nodejs/modules/issues/299 Ref: nodejs/modules#454
I have a suggestion. Currently the core problem with missing For example: // x.cjs
const {y} = require('./y.cjs');
console.log("x" + y);
// y.cjs
const {z} = require('./z.cjs');
module.exports.y = "y" + z;
// z.cjs
module.exports.z = "z"; In this example, without So, hypothetically, what if, when using That way, In other words: instead of making ESM be the major break, which would effectively prevent popular libraries from porting from CJS to ESM, make TLA be the major break. |
It's not just TLA. It's also a return to the 100% synchronous and main thread blocking loader. The loader is currently implemented with promises, allowing us to add things like parsing in the background while executing other JS code. If we want to allow synchronous access to loaded ESM, we'd have a few options:
I think this moves us towards ESM as just syntactic sugar for CJS which I'd consider a missed opportunity for an ESM-first future. I think we may be able to still get somewhat similar execution behavior to the browser - maybe? But it smells like something that risks deadlocks and/or weird timing issues. E.g. if a dependency has TLA but was already executed previously - could it be successfully imported synchronously? |
There will be no ESM-first future until the “bottom up” problem is solved. Option #3 sounds to me like Wes’ implementation, and I, for one, approve of it. TLA is the only real problem with it. If I have to give up on |
…ate columns. * Tried to implement Import button, using react-vmessagebox, but hit issue of esmodules not being supported in electron. For esmodules point, I read this thread: electron/electron#21457 Comments at end (eg. electron/electron#21457 (comment)) seemed to indicate you could use dynamic-import calls; however, from my own tests (in lf-desktop), I found that even with latest electron (v12 beta-5), the dynamic-imports only work for the initial file that electron is pointed to. Dynamic-imports from console, event-handlers, etc. always have a "Failed to fetch dynamically imported module" error. There was also the old solution of using the "esm" module, but apparently that approach stopped working in Electron 10+ (electron/electron#21457 (comment)). Comment suggests the "esm" module/transpiler works again, if you remove the "type:module" field (presumably using .mjs extensions instead), but I don't want to do that. Thus it seems that, for my code's pathway (as a nuclear plugin, called through eval), there is no way to use esmodules right now. ---------- By the way, the above is all based around the idea that importing esmodules from commonjs modules, requires the async-function wrapper. This is because esmodules are allowed to contain "top-level awaits". Despite the great majority of esmodules not using this, this feature makes it impossible to reliably have commonjs "require" (synchronously) an esmodule file. See these for background: Overview) https://redfin.engineering/node-modules-at-war-why-commonjs-and-es-modules-cant-get-along-9617135eeca1 Discussion) nodejs/modules#454 My opinion aligns with the overview article's author, that NodeJS (and by extension Electron) should allow commonjs to just "require" esmodule files -- but that if that esmodule tree contains a "top level await", then an error is generated. However, as of now, that idea doesn't seem to have picked up much momentum, so it's unlikely it would get implemented any time soon. Thus, we have to fall back to the only existing cjs->mjs approach, of an async wrapper. Which electron (even latest versions) only supports for the module-tree pointed to at electron launch... Thus, for now (and the foreseeable future), esmodules will not be possible within this nuclear-vplugin package... My options are thus: 1) Avoid use of esmodules, by extracting needed code blocks from those libraries, or using different libraries. (what I'm currently during, through FromJSVE, and using something like electron-prompt instead of react-vmessagebox) 2) Use a bundler like Webpack to convert esmodules into a simple bundle (or a set of commonjs files). (I think the "esm" transpiler thing fits into this category, just doing so seamlessly/magically -- however, the magic appears to not work so well in the latest electron versions, as mentioned above) 3) Avoid use of esmodules, by updating your libraries to not output esmodules -- or to output both esmodules and commonjs versions. This could work, but I don't really like it, as it's forcing me to spend maintenance on the commonjs format, which is "the past", merely for this nuclear plugin pathway. (since my libraries are not used used by others atm) Of the three options, option 2 is probably the one I'll end up going with, as it lets me keep the nuclear-vplugin pathway's fix restricted to the nuclear-vplugin repo itself. Also, it may come in handy for making the install process easier. (ie. users may not need to do "npm install" anymore, as long as I use only pure-javascript libraries) But anyway, atm I'm doing the lazy approach (option 1): just avoiding those esmodule libraries for now.
i am wondering why we need to solve the problem so complex we could maybe simply make the cjs loading a async process it self so CJS it self as it is sync will not care to execute inside a async process we could throw some process.nexttick magic into that to free some execution time for other stuff to execute. as every CJS part is only aware of its CJS loading it will not conflict it is like
we are not forced to start in a CJS context at all what blocks us from simply only offering the ESM Context and force createRequire use??? my vote is clear for breaking any existing cjs codebase without fear as we do break ESM at present without fear updating the CJS packages will be a no brainer with existing tooling. out of my few years 8+ years with the ESM loader spec and trying to integrate existing cjs packages into browsers and diffrent JS Environments, DENO, GraalJS (other java js runtimes), teavm, nodejs (including forks like the chakra core based nodejs-mobile fork that runs on android and ios). I did always transpil my dependencies up via rollup to ESM when that was not possible i did write the code to convert the packages to esm like "lebab" but there is also ongoing google efforts to create tooling to transpil stuff up not down like babel does. i did even manage to create working electron apps with minimum cjs only for the init.cjs that is needed because they are using a hacked nodejs fork with adjustments and they need to algin the both libuv loops for the browser and the nodejs stuff. the so called "ready" event when both libs are fully inited and loaded |
I recently ran into this problem myself after doing a bit of googling I found a few stackoverflow threads about others asking the same question
Some workarounds I found that might work.
JitiEdit - Another workaround (which unbuild uses for loading it's config files) We can call run.cjs directly from node, jiti is just being referenced as a library here // run.cjs
const jiti = require('jiti')
function test1() {
const rootdir = process.cwd()
const _require = jiti(rootdir, { interopDefault: true, esmResolve: true })
const mod = _require('./testmod')
mod.testfunc()
}
test1()
// testmod.mjs
export function testfunc() {
console.log('testmod')
} |
Ever since the ES moduled became officially supported by Node.js, I wondered why there is no synchronous dynamic import, i.e a special function Server side software (services) do not benefit from asynchronous loading of program files, it's an unnecessary complication that shouldn't have been forced on us. |
Did you read the top comment on this thread first?
There's no spec-compliant thing that export const foo = await fetch('./data.json') Or what if you called The thread has been dormant for years; I think there won't be any answers forthcoming to these questions. I think this issue should probably be closed. |
Agreed, I don’t see any solutions for these questions coming anytime soon. |
Sorry to rip up years-old grievances for you. I came across this thread while looking for a way to use a ES module from a CommonJS module, without turning the latter into an artificial asynchronous animal. Not a philosophical dilemma, just a real-world problem.
Feel free to close the issue, of course. |
the issue is already closed but the elephant in the room is that the module you are consuming is not a dual-module (ESM+CJS) because in ESM you can have your own If such module really needs ESM features you are in bad luck here but if it doesn't, it's rather a lack of community support from that module author to not use any easy-and-fast transformer + |
This issue is meant to track concerns raised in the past about
require(esm)
. I'm hoping to preempt having these discussions in January after we (or rather @weswigham) already invested more work into providing an implementation ofrequire(esm)
.Previous discussions:
require
with ESM planned? nodejs/modules#308From the 2019-10-23 meeting notes:
From #308:
The text was updated successfully, but these errors were encountered: