Skip to content
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

Dual-build for ESM and CJS #164

Merged
merged 1 commit into from
Jun 11, 2024

Conversation

steveluscher
Copy link
Contributor

@steveluscher steveluscher commented Jun 7, 2024

Summary

In this PR we introduce an ESM build. This should ensure compatibility with ESM build pipelines, including:

  • Create React App
  • Ember Embroider

…and all of the other ESM-based bundlers that folks have been having trouble with since v8.

This should be released as v9 because of how much change there is.

Changes

  • Everything is exported at the top level now, so that devs don't have to rely on the ability to dive into dist/ to pick out one particular module (eg. WebSocketBrowserImpl). More about this, including an example, in API.md. Anything you don't use among the exports should be tree-shaken away by your optimizing compiler (eg. if you only use WebSocket then Client should get tree-shaken away).
  • Each export is built into one file. This eliminates the class of problem where bundlers like Embroider had trouble importing dependencies in dist/.
  • We bundle with Parcel now, and configure the entire build through package.json
  • What gets published to npm now is just dist/, README.md, LICENSE, and package.json

Test plan

Unit tests

npm test

@solana/web3.js

Applied this diff and rebuilt. IIFE bundle worked.

Create React App

  1. Created a brand new CRA app
    npm create react-app test-app
    cd test-app
    npm add rpc-websockets@../rpc-websockets
    npm add @solana/web3.js@../solana-web3.js-git/packages/library-legacy
    
  2. Added this code to App.js
    import { Connection } from "@solana/web3.js";
    import { Client } from "rpc-websockets";
    
    const ws = new Client("wss://echo.websocket.org");
    ws.on("open", function () {
        console.log("WebSocket Client Connected");
    });
    
    const c = new Connection("https://api.devnet.solana.com");
    c.getLatestBlockhash().then(console.log);
    c.onSlotChange(console.log);
    
    image

Next.js with App Router

  1. Created a brand new Next.js app
    npm create next-app test-app
    cd test-app
    npm add rpc-websockets@../rpc-websockets
    npm add @solana/web3.js@../solana-web3.js-git/packages/library-legacy
    
  2. Added this code to page.tsx
    'use client';
    import { Connection } from "@solana/web3.js";
    import { Client } from "rpc-websockets";
    
    export default function Home() {
        useEffect(() => {
            const ws = new Client("wss://echo.websocket.org");
            ws.on("open", function () {
                console.log("WebSocket Client Connected");
            });
    
            const c = new Connection("https://api.devnet.solana.com");
            c.getLatestBlockhash().then(console.log);
            c.onSlotChange(console.log);
        }, []);
        /* ... */
    };
    
  3. Ran npm run dev
    image

Next.js with App Router

  1. Created a brand new Next.js app with Pages router
    npm create next-app test-app
    cd test-app
    npm add rpc-websockets@../rpc-websockets
    npm add @solana/web3.js@../solana-web3.js-git/packages/library-legacy
    
  2. Added this code to _app.tsx
    import { Connection } from "@solana/web3.js";
    import { Client } from "rpc-websockets";
    
    export default function App({ Component, pageProps }: AppProps) {
        useEffect(() => {
            const ws = new Client("wss://echo.websocket.org");
            ws.on("open", function () {
                console.log("WebSocket Client Connected");
            });
    
            const c = new Connection("https://api.devnet.solana.com");
            c.getLatestBlockhash().then(console.log);
            c.onSlotChange(console.log);
        }, []);
        /* ... */
    };
    
  3. Ran npm run dev
    image

package.json Outdated
Comment on lines 5 to 21
"browser": "dist/index.browser.mjs",
"main": "dist/index.cjs",
"module": "dist/index.mjs",
"types": "dist/index.d.ts",
"targets": {
"browser": {
"isLibrary": true,
"outputFormat": "esmodule",
"source": "src/index.browser.ts",
"sourceMap": true
},
"main": {
"isLibrary": true,
"outputFormat": "commonjs",
"source": "src/index.ts",
"sourceMap": true
},
"module": {
"context": "node",
"isLibrary": true,
"outputFormat": "esmodule",
"source": "src/index.ts",
"sourceMap": true
},
"browser-bundle": {
"context": "browser",
"distDir": "dist/",
"outputFormat": "global",
"source": "src/index.browser-bundle.ts"
},
"types": {
"source": "src/index.ts"
}
},
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the complete Parcel configuration. When you run parcel build it reads this data to know how to behave.

Comment on lines +57 to +47
"@types/uuid": "^8.3.4",
"@types/ws": "^8.2.2",
"buffer": "^6.0.3",
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Downstream clients may actually need these dependencies if they're building for the browser, so I moved them out of devDependencies.

@steveluscher steveluscher force-pushed the build-dual-mode-esm-cjs branch 2 times, most recently from 571c5b3 to 79ecfe6 Compare June 7, 2024 01:47
steveluscher referenced this pull request Jun 7, 2024
Signed-off-by: Mario Kozjak <kozjakm1@gmail.com>
@steveluscher steveluscher marked this pull request as draft June 10, 2024 17:41
@steveluscher
Copy link
Contributor Author

Converting to draft for a moment; gotta figure out a way to build a typedef for the browser bundle (ie. it needs to omit Server from the exports)

@steveluscher steveluscher marked this pull request as ready for review June 10, 2024 20:14
@steveluscher
Copy link
Contributor Author

OK, Parcel is not appropriate for this multi-package build (browser, node, iife). I've replaced it with the build pipeline that we use ourselves to build multiple packages, retested it, and added Next.js test plans for both App router and Pages router.

Ready to review.

tsconfig.json Outdated
Comment on lines 4 to 5
"noEmit": true,
"declaration": false,
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Only used for typechecking now, not for building.

Comment on lines +23 to +28
globals: {
buffer: true,
},
polyfills: {
buffer: true,
},
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When building browser-bundle.js, include the buffer polyfill.

@mkozjak
Copy link
Member

mkozjak commented Jun 11, 2024

Amazing work, @steveluscher! Thanks much.
Tests are passing. I will squash-merge this one now.

@mkozjak mkozjak self-requested a review June 11, 2024 07:40
@mkozjak mkozjak merged commit 122b1ff into elpheria:trunk Jun 11, 2024
@steveluscher steveluscher deleted the build-dual-mode-esm-cjs branch June 11, 2024 15:03
@steveluscher
Copy link
Contributor Author

I hope this fixes everyone’s build issues once and for all. Sorry for the wild ride, everyone. Once this works it should let y’all:

  • Import just what you need for smaller bundles
  • Build for the exact target you need for smaller bundles
  • Work in ESM projects

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants