-
Notifications
You must be signed in to change notification settings - Fork 214
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
use Node's ES-module support, stop using -r esm
#527
Comments
This sounds great. I suspect we will have to change the |
It looks like we may also have to use @jfparadis does that seem right? I know you've been using |
Correct. There are 2 things in tests:
It's perfectly fine to use
|
It's starting to look like we have to start from the top of the dependency tree, rather than the bottom. If a "module file" is any .js file under a
So I'm going to try starting from
|
Yes those ideas are the core, and they work in SES-beta/SES-shim. They also work in this POC: In addition to what you listed:
import url from 'url';
import path from 'path';
const __filename = url.fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
Instead, since we have a monorepo, we can use relative paths. This works: // const esend = require.resolve(`@agoric/eventual-send`);
const esend = path.join(__dirname, '../../eventual-send/src/index.js');
// import { rollup as rollup0 } from 'rollup';
import rollupCJS from 'rollup';
const { rollup: rollup0 } = rollupCJS; |
I keep trying to approach this task, and I keep bouncing off problems. It's starting to look like I'll have to change everything all at the same time, which is a drag. Almost all of our code is module-style (uses
Removing Rule 4 means the parent package must stop using But.. Rule 5 means many existing There are a few changes we can apply early to reduce the size of the ultimate delta:
But after that, we must either rewrite a whole bunch of destructuring imports, or we have to change all our package.jsons at the same time, neither of which sounds like a lot of fun. |
Thoughts, @SMotaal? |
and thoughts @bmeck @MylesBorins @guybedford ? |
Ah.. I didn't realize that Node simply does not offer named imports from CommonJS packages: https://github.com/nodejs/node/blob/master/doc/api/esm.md#import-statements
import packageMain from 'commonjs-package'; // Works
import { method } from 'commonjs-package'; // Errors That makes sense to me if I think about scanning the imported package for it's named exports (which can be done without evaluating that package). In the module-imports-module case, it can identify the named exports early, and provide an error (like the one I'm getting, "package X does not export Y") early, then do the actual module loading asynchronously. But in the module-imports-commonjs case, it doesn't have that information until evaluation time. (I was thinking this was a bug, or a problem with the way we export things, but in fact it's deeper than that). I guess the We've gotten in the habit of using named imports everywhere (overreliance on I prefer the clarity of named imports (and I get distracted by trying to choose a name for the temporaries), so I'd love it if our standard style was to use them. But that's just not going to be possible here, at least not when importing external packages. I count roughly 1110 imports in So most of those wouldn't need to be rewritten if all our repo's packages were declared as modules. To avoid the rewrite, we need to convert basically everything at the same time (which I'm trying to avoid but maybe it's the best option). |
Unless there is urgent reason to move to native ESM, I'd stick to compiling until Node's ESM is out of experimental status. Testing/utility frameworks have trouble with real ESM in particular if you do things like: stubbing, mocking, hot module reloading, cache invalidation, etc. There is not real path currently towards fixing that at the spec level so the only alternative is to use a runtime loader for those workflows which is back to a compilation step and Node's loader APIs are not stable. |
@bmeck makes a good point, so maybe I can just elaborate on some subtle considerations.
My recommendation would also be to hold off moving to ESM but to also start exploring in small scale, come up with the timelines and strategy for transitioning once your particular requirements become stable. Starting ahead can help identify potential pitfalls which may be beneficial to bring to the attention in the ongoing efforts. So, yes ESM is one thing, but migration/interop, all that is a completely different bag of tricks! |
Thanks! Ok, I've added tickets for the cleanups we can make which work both under I'll look more carefully at my kernel-on-new-SES branch (#477) and see if I can accomplish it without also switching away from |
@bmeck, unfortunately I wish it was that simple.
On the other hand, the same types of problems do occur with the ESM package or with any transpiration like rollup, and we found that they are much worse with them. Testing With the ESM package, module exports are proxies, and several global object like eval(), Function() and Reflect are replaced with substitutes that introduce distortion to the expected behavior. We have found, by experience, that the issues with mocking to be either non existent for the manual mocks or the one we do with On the other hand, some issues were impossible to resolve with the ESM package. Testing with the package ESM means we need to design and maintain workarounds for the way it changes the intrinsic objects. The only workarounds we have introduced and (that Brian is discussing above) work in both modes native or transpilation. There is nothing else. Standards This Agoric SDK library rely on the SES shim, which is to emulate the specs. Having the ESM package or any other transpiration in the system introduces more complexity and more variations. This means bugs and uncertainty, and we can't emulate the specs because we're not close enough to the native code. Best we can do is have SES emulate what the ESM package is providing. In other words, native ESM have us focus on import/exports while the ESM package introduces evaluators and several other objects to the mix. Security It also means that the ESM package is part of the stack. SES and swingset are very special on that sense, they are security products and have no dependencies because we want to reduce the amount of code to vet for security. Vetting an extra 16K lines of code just for the transpiler is just not an option for our organization. Evolution If node wants to evolve, we need people to start using it and I believe our team is well suited to make conscious decisions about what it uses and contribute to the discussion.
We only interface with other packages in tests, and that where we need the native behavior, not some alternate one! |
@jfparadis So technically the portable route of Okay I am starting to really dig the whole |
@SMotaal, we want SES and Swingset to keep providing exports to the rest of the world via rollup. Here is the overall approach (@warner didn't mention it, as this ticket started with a smaller subset of the discussion):
In the past, we were relying on
With native ESM neither are necessary.
In the end, package Some technical detains: We found that some workaround are necessary, but not hacks. The difference is that the workarounds are following standards, not using accidental behavior. On the opposite, when we need to When evolving SES 2.0, we found issues with the proxies introduced by the esm package. Basically, an object defined in a module behaves differently when it is defined in a different module then imported. That's when we found the current approach and converted all at once. @warner is trying to do a partial convert of this monorepo, which is to change some modules to native ESM and other not. I did all at once, therefore he's finding issues I did not have to deal with. |
esm
-r esm
I was thinking, if we can get #565 and #566 landed first (which involves changing about 161 named imports of third-party packages into two-line import+destructure clauses, and maybe changing test libraries), then would the final "flag day" everything-at-once change be merely to add We got rid of all the bundling steps a few months ago. Some new tests are using It sounds like there's |
We were seeing random test failures in the zoe unit tests (specifically test-offerSafety.js) that looked like: ``` Uncaught exception in test/unitTests/test-offerSafety.js /home/runner/work/agoric-sdk/agoric-sdk/packages/weak-store/src/weakStore.js:5 import { assert, details, q } from '@agoric/assert'; ^^^^^^ SyntaxError: Cannot use import statement outside a module ✖ test/unitTests/test-offerSafety.js exited with a non-zero exit code: 1 ``` or: ``` Uncaught exception in test/unitTests/test-offerSafety.js /home/runner/work/agoric-sdk/agoric-sdk/packages/zoe/test/unitTests/setupBasicMints.js:1 import { makeIssuerKit } from '@agoric/ertp'; SyntaxError: Cannot use import statement outside a module ✖ test/unitTests/test-offerSafety.js exited with a non-zero exit code: 1 ``` under various versions of Node.js. The error suggests that `-r esm` was not active (the error site tried to import an ESM-style module, and failed because the import site was CJS not ESM), however error site itself was in a module, meaning `-r esm` was active up until that moment. This makes no sense. The AVA runner has had some amount of built-in support for ESM, in which it uses Babel to rewrite test files (but perhaps not the code being tested). If that were in effect, it might explain how `test-offerSafety.js` could be loaded, but `weakStore.js` could not. It is less likely to explain how `setupBasicMints.js` could be loaded but `makeIssuerKit` could not, unless AVA somehow believes that `setupBasicMints.js` is a different category of file than the ones pulled from other packages. In any case, I believe we're bypassing that built-in/Babel support, by using their recommended `-r esm` integration recipe (package.json has `ava.require=['esm']`). However the AVA "ESM Plan" (avajs/ava#2293) is worth reading, if only for our future move-to-native-ESM plans (#527). So my hunch here is that the `-r esm` module's cache is not safe against concurrent access, and AVA's parallel test invocation means there are multiple processes reading and writing to that cache in parallel. Zoe has more source files than most other packages, which might increase the opportunity for a cache-corruption bug to show up. This sort of bug might not show up locally because the files are already in the cache, whereas CI may not already have them populated. This patch adds `ESM_DISABLE_CACHE=true` to the environment variables used in all tests, in an attempt to avoid this hypothetical bug. Stale entries in this cache has caused us problems before, so most of us have the same setting in our local shells. Another potential workaround would be to add `--serial` to the `ava` invocation, however the Zoe test suite is large enough that we really to want the parallelism, just to make the tests finish faster. This patch also increases the Zoe test timeout to 10m, just in case. I observed a few tests taking 1m30s or 1m40s to complete, and the previous timeout was 2m, which was too close to the edge.
prompted by discussion in #2988, @kriskowal @warner @michaelfig and i talked about this today. The leading proposal is: RESM+NESM is an non-goal; rather RESM -> RESM+endo -> endo -> endo+NESM. so if a package needs multiple entrypoints, we're ok to lose NESM for that package. any RESM module will not have type: module. @kriskowal notes to get RESM -> RESM+endo -> endo. needs endo to support import.meta.url ; and under some config, import.meta.url + __dirname |
I found a way out of this deadlock, by patching
That's correct.
This is no longer the case. |
All of Agoric SDK is converted to NESM. There is one last vestige: Dapp deployment scripts are still run with RESM. |
Yay! Way to grind it out! I suggest postponing ESM vs deployment scripts as a new issue and declaring victory on this one. Let me know if you'd like me to do it. |
Sounds fantastic to me. Here’s an issue for the dregs #3687 |
Node.js now has enough support for real ES modules, so we can stop using the
esm
module. I've started doing this cleanup for SwingSet, but we should do it across the repo. The requirements are:type: "module"
main:
entry in package.json must point to a module-style file (with anexport
statement), not a CJS-style file (withexports.foo =
ormodule.exports =
)esm
from the package.jsondevDependencies
anddependencies
-r esm
from invocations of node or other tools from package.json and other scriptsimport xx from './foo'
orimport xx from '../bar/blah'
) must be expanded to name the exact file:import xx from './foo.js'
. Node's ESM does not searchx
,x.js
, andx/index.js
the way it does for CommonJS.require.resolve(path)
, for a path likepath.join(__dirname, path)
, need to be replaced withnew URL(path, import.meta.url).pathname
require.resolve(specifier)
, for a specifier like@agoric/zoe/contractFacet.js
, need to be replaced byimportMetaResolve(specifier, import.meta.url)
and use theimport-meta-url
package from npm. In the cases where these are being used to normalize a specifier within a config file, this is an opportunity to fix a bug by replacingimport.meta.url
with the URL of the config file, e.g.,new URL(configPath, import.meta.url).toString()
. Note thatimportMetaResolve
is asynchronous so some workflows may need to be transformed to async functions.import url from 'url'
andurl.filePathToURL(await importMetaResolve(moduleSpecifier, import.meta.url))
to get the path for a module specifier, as with a contract path when passed tobundleSource
. As a matter of style and safety, we only useawait
as a statement at the topmost level of anasync
function, so be sure to expand this example to two lines with an intermediate named variable. Theurl.filePathToURL
function is portable even unto Windows.__dirname
and__filename
must also be migrated, usingnew URL('./', import.meta.url).pathname
andnew URL(import.meta.url).pathname
respectively, if not one of the above patterns. Be sure to remove these from the/* global */
directive and you may also need to remove theimport path from 'path'
statement to appease the linter..jsconfig.json
needs a"module": "esnext"
in addition to"target": "esnext"
to willingly interpretimport.meta.url
.parsers
directive in package.json, if present, may be removed. This exists as a work-around for portability straddling RESM and SESM and is not necessary to straddle SESM and NESM.require('esm')('../src/entrypoint.js')
, since entrypoint modules still had to be CommonJS format.Any package imported by the Node module logic must have
type: "module"
in its package.json, else it won't be recognized as importable, so we basically have to make this change from the bottom of the dependency tree, working our way up.🆕 This now blocks security blocker endojs/endo#850
The text was updated successfully, but these errors were encountered: