-
Notifications
You must be signed in to change notification settings - Fork 773
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
Monorepo: Switch to hybrid ESM/CJS Build #2685
Conversation
That sounds right. Every import has to be exactly specified, so if we don't re-export a subfolder from the root level |
Codecov Report
Additional details and impacted files
Flags with carried forward coverage won't be shown. Click here to find out more. |
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.
Along with the changes here (which generally look good, see feedback below) you will also need to update all of your code (potentially in a separate PR) to have explicit full file paths. For example, in packages/block/src/index.ts
you should change
export { Block } from './block'
export { BlockHeader } from './header'
to
export { Block } from './block.js'
export { BlockHeader } from './header.js'
Anywhere in the project you refer to a folder (where NodeJS will implicitly load a file named index.js
), you should include the full path to the file being loaded. For example, in packages/evm/src/evm.ts
you should change
import type { OpHandler, OpcodeList } from './opcodes'
to
import type { OpHandler, OpcodeList } from './opcodes/index.js'
I don't know if it applies to this project, but if you want the users of this library to be able to do subpath imports like import { ... } from '@ethereumjs/evm/opcodes'
then you will need to explicitly declare each valid subpath in the package.json
. You can see the NodeJS docs on subpath imports here: https://nodejs.org/api/packages.html#subpath-imports
69836cd
to
f0a7dd1
Compare
@MicahZoltu thanks a lot for the early review and the detailed write-up, appreciate this a lot!! 🙏 Regarding file extension and path references, think three topics here to handle separately:
|
Thanks for your invitation! I'll try to give my experiences for project building.
|
If you want your code to run in a browser natively you MUST have exact path imports. While NodeJS and bundlers can scan files on disk to figure out an exact path form a fuzzy path, (e.g., adding While many projects use bundlers to fix libraries that do don't have full paths, it would be far better if the library wasn't broken out of the box and in need of fixing by users of the library. If the intent is for this to only be used in NodeJS then fuzzy I think will still work, but the best practice is still to be explicit so you can work in any runtime that may refuse to do fuzzy path matching (node, deno, ts-node, browsers, etc.) |
While I haven't used the If you already have some post-compile build tooling this may not be worth changing now, but my general recommendation to projects is to try to avoid becoming heavily dependent on build tooling magic, especially when you could avoid that dependency with a simple behavior change like using file-relative paths rather package-relative paths. By avoiding dependency on build tooling, it gives you more freedom to swap out your build tooling in the future should you desire to, or drop it completely (which is my preference for my personal projects). |
Another note on point 2: If you import a folder rather than a file, you run into the exact same set of problems I mentioned in #2685 (comment), both of these are the same issue ultimately, which is that the browser will interpret import statements literally. |
@MicahZoltu thanks a lot! 🙏 I am not completely getting it (regarding the file extensions) how this interplays with TypeScript, just tested: When I added a When I use Should I use Any side effects to expect here? |
(also, can't TypeScript do this automatically on build? Always add the full file references?) |
@noname0310 I'm not proficient in this, do you have any good advice? If you have time, you can take a look at this. |
@holgerd77 You can use a typescript transformer to use path aliases or add js extensions to import statements. Transformers commonly used to use path aliases such as "@/": There are also Transformers that add .js extensions, but they may need to be modified and used due to defects: Generally speaking, using transformer or bundler may become a dependent form on that build toolchain, but I think it's almost inevitable for hybrid builds. |
Does the file |
I agree that using a transformer has a tendency to create a dependency on it, but I disagree that it is inevitable for hybrid builds! Prior to Node 20, which no longer requires hybrid builds if you don't care about supporting older versions, all of my libraries were hybrid projects with |
The problem in this case seems difficult for library users to use esm without any bundlers(like webpack) in a browser environment. |
It is difficult for users of the library only if the library omits extensions and doesn't require path rewriting by the library user. If the library correctly has complete exact relative file paths everywhere, then the library can be used natively in a browser without any bundler. All this requires is changing |
What I know:
What I will likely do: In the pure usesm version of noble cryptography, subpath imports will be changed from What needs to be decided: Should v7 have a breaking change switching external interface to extensioned paths, or should the switch be delayed to some v8/v9 whenever pure esm would be ready? Advantage of v7 switch is that users will be able to have less breaking changes in the future. Users will need to be educated about the change, which will take time. |
@paulmillr TBH I cannot connect the dots what is meant by this. If we switch to these full path + file extension references internally, how does this effect the external interface? 🤔 |
I would do it all at once if it were me. If you already have a breaking change planned for ESM, getting everything working well for native browser at once will save people from having to go through two separate adjustments. |
@holgerd77 the external interface is not related to the changes. I'm just mentioning it just in case. To summarize, at some point external interfaces will need to add extension, unless we want ESM in-browser users to mess around with import maps. |
in between work update: will remove the k-bucket internalization by rebase and try to re-integrate today, this had some side effects with now the devp2p tests not running through any more. Otherwise I'll continue with the dual build integrations. After all the discussions I think I will skip the folder transition in this PR, eventually do in a second round. @acolytec3 please just ignore this work for the develop-v7 rebase update, will adjust later on to master. |
caf9d48
to
778e13a
Compare
add8071
to
cfe67da
Compare
Ready for review. 🙂 (I will skip any paths adoptions in this round) |
@@ -18,7 +19,8 @@ export class KBucket extends EventEmitter { | |||
constructor(localNodeId: Uint8Array) { | |||
super() | |||
|
|||
this._kbucket = new _KBucket<CustomContact>({ | |||
// new _KBucket<CustomContact>({ |
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.
This was intened to leave this comment in the code to ease a subsequent type integration for the internalized KBucket package (see ./ext/kbucket.ts
).
"./provider": { | ||
"import": "./dist/esm/provider.js", | ||
"require": "./dist/cjs/provider.js" | ||
} |
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.
This was a long battle (2-3+ hours) to get the newly reworked Tx fromProvider() (or similar) tests running which use test-double to replace the real RPC provider.
I tested out lots of alternative test-double invocations, switch tape to use the node ESM loader,... but nothing worked at the end.
So I took this route to open up this provider
path in the package.json
file here (this is now necessary in case of deep imports for ESM).
I would think though that side effects are limited and this should be acceptable.
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.
I'm good with it too, though would you rather me figure out a better way to rewrite the tests? I get the impression that if we're having to explicitly define this provider export just to satisfy a test, it's my test and not our export configuration that should be fixed.
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.
It would be great if we can fix the test, I went really deep down this rabbit hole though and tried a lot. But maybe you've got another approach I didn't think of. Not sure if it's worth another 3 hours of work time though.
This is looking great! Are you intentionally not building an ESM version of the client? Mainly just curious at this point but we'll need the client to be ESM friendly too if we want to fully upgrade the browser version of it (since libp2p is ESM only now and can't be imported easily in CJS land). |
Not sure, just thought wouldn't be necessary for now. But if we want to reactivate browser build we can for sure do (maybe we leave for now and do along the browser work? Just weak preference though). |
packages/util/package.json
Outdated
"./provider": { | ||
"import": "./dist/esm/provider.js", | ||
"require": "./dist/cjs/provider.js" | ||
} |
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.
Perhaps this is what you meant by "paths adoptions"? Switching to ./provider.js
is fairly strongly recommended for browser, and even the NodeJS documentation now recommends it.
"./provider": { | |
"import": "./dist/esm/provider.js", | |
"require": "./dist/cjs/provider.js" | |
} | |
"./provider.js": { | |
"import": "./dist/esm/provider.js", | |
"require": "./dist/cjs/provider.js" | |
} |
"extends": "../../config/tsconfig.prod.cjs.json", | ||
"exclude": ["test", "examples", "scripts", "node_modules", "dist"], | ||
"compilerOptions": { | ||
"baseUrl": "./", |
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.
I don't remember what exactly breaks when doing this, but using non-relative paths can cause you headache/pain down the road:
"baseUrl": "./", |
This change may also require updating non-relative URLs to be relative URLs in files in this project.
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.
I've removed these baseUrl
properties in each of the tsconfig
s in devp2p
and it doesn't seem to have any impact on our build/tests so will push a commit to clean this up. Thanks for calling it out!
"extends": "../../config/tsconfig.prod.esm.json", | ||
"exclude": ["test", "examples", "scripts", "node_modules", "dist"], | ||
"compilerOptions": { | ||
"baseUrl": "./", |
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.
Same comment as above.
"baseUrl": "./", |
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.
I say we merge this!
result: txData, | ||
} | ||
}, | ||
} |
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.
Ah, interesting. I would have never gotten to this solution. 🙂
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.
It means we're no longer testing the Node16 route for fetching data (using https
instead of the fetch
API but there are separate tests in util
for testing that. The goal here is to test the constructor so this seemed like the least impactful route to that.
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.
looks good 👍
I gave the ESM build a try this morning, is this basically how we want to do it?
I took inspiration from the following two repositories, since both have recent builds and a perceived solid and simple pipeline and we used the code recently:
Block has no subfolders, so for the subfolder including packages (so: like
src/consensus
for Blockchain) we then need to adopt import/export paths like being done here e.g. for thejs-sdsl
package, do I get this right?Early feedback appreciated! 😊