990 words
5 minutes
Using packages from JSR with esbuild
Anubhav Gain
2024-08-01

Using packages from JSR with esbuild#

JSR is a brand new package repository for “modern JavaScript and TypeScript”, launched on March 1st by the Deno team as a new alternative to npm

My JavaScript ecosystem fluency isn’t great, so it took me a bit of work to figure out how to use packages from JSR in my browser.

Installing yassify with npx jsr add#

@kwhinnery/yassify is the demo package created as part of that introductory blog post. The code itself is a tiny snippet of TypeScript:

/**
* Yassify a string of text by appending emoji
*
* @param str The string of text to yassify.
* @returns a string of text with emoji appended
*/
export function yassify(str: string): string {
return `${str} 💅✨👑`;
}

It’s published to JSR here. That page includes npm instructions for using it that look like this:

Terminal window
npx jsr add @kwhinnery/yassify
import * as mod from "@kwhinnery/yassify";

For someone with limited JavaScript ecosystem fluency like myself, that is not enough information! I want to run this code in a browser.

Part of the problem is that there are a bewildering array of build tool options. I wanted the thing with the least number of steps - eventually I found this pattern using esbuild that seems to work.

I already had npm and npx installed.

First I created myself a directory for my experiment:

Terminal window
mkdir /tmp/site
cd /tmp/site

I used the npx jsr add command from the JSR documentation:

Terminal window
npx jsr add @kwhinnery/yassify

Output:

Setting up .npmrc...ok
Installing @kwhinnery/yassify...
$ npm install @kwhinnery/yassify@npm:@jsr/kwhinnery__yassify
added 1 package in 711ms
Completed in 824ms

This created a bunch of files. Running find . reveals the following:

./node_modules
./node_modules/@kwhinnery
./node_modules/@kwhinnery/yassify
./node_modules/@kwhinnery/yassify/mod.d.ts
./node_modules/@kwhinnery/yassify/package.json
./node_modules/@kwhinnery/yassify/mod.js
./node_modules/.package-lock.json
./.npmrc
./package-lock.json
./package.json

The package.json file contains:

{
"dependencies": {
"@kwhinnery/yassify": "npm:@jsr/kwhinnery__yassify@^1.0.1"
}
}

.npmrc has the following, which is described in the JSR documentation about npm compatibility:

@jsr:registry=https://npm.jsr.io

The actual yassify code lives in that ./node_modules/@kwhinnery/yassify/mod.js file.

OK - so we’ve run a command and got ourselves a node_modules directory with the yassify code in it. How do we use that in a browser?

Building an index.js file with esbuild#

Here’s a tiny JavaScript file that uses yassify:

import { yassify } from "@kwhinnery/yassify";
const h1 = document.querySelector("h1");
h1.innerText = yassify(h1.innerText);

And the incantation to have esbuild resolve that import and bundle all of the code together into a single file:

Terminal window
npx esbuild index.js --bundle

That outputs directly to standard out:

(() => {
// node_modules/@kwhinnery/yassify/mod.js
function yassify(str) {
return `${str} \u{1F485}\u2728\u{1F451}\u{1F984}`;
}
// index.js
var h1 = document.querySelector("h1");
h1.innerText = yassify(h1.innerText);
})();

Or you can add --outfile=bundle.js to write it to a file:

Terminal window
npx esbuild index.js --bundle --outfile=bundle.js

Now here’s an index.html file that makes use of this bundle:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Yassify</title>
</head>
<body>
<h1>Yassify</h1>
<script src="bundle.js"></script>
</body>
</html>

And that works! You can open index.html directly in a browser and it loads and executes the bundled code.

Trying that with my own package - and failing to get it to work#

I have just one package on NPM at the moment: datasette-table, an experimental Web Component for rendering tables from Datasette on a page.

The source code for that is in simonw/datasette-table on GitHub.

I followed the JSR intro post and published it to GitHub.

I signed into JSR using my GitHub account and created a scope called @datasette - all packages on JSR are published within a scope.

I added a jsr.json file te the root of the repository:

{
"name": "@datasette/table",
"version": "0.1.0",
"exports": "./datasette-table.js"
}

Then I ran this command:

Terminal window
npx jsr publish

This opened my browser to authenticate and pushed the package to JSR: jsr.io/@datasette/table. Pretty smooth!

Let’s try using that package via the esbuild method described above:

Terminal window
mkdir /tmp/datasette-demo
cd /tmp/datasette-demo
npx jsr add @datasette/table
echo 'import * as mod from "jsr:@datasette/table";' > index.js
npx esbuild index.js --bundle --outfile=bundle.js

And I got this error:

✘ [ERROR] Could not resolve "npm:lit@^2.2.7"
node_modules/@datasette/table/datasette-table.js:1:36:
1 │ import {LitElement, html, css} from 'npm:lit@^2.2.7';
╵ ~~~~~~~~~~~~~~~~
You can mark the path "npm:lit@^2.2.7" as external to exclude it from the bundle, which will
remove this error and leave the unresolved path in the bundle.
1 error

lit is the only dependency of my component. I checked node_modules and it had all of the lit files in it, so they had been installed correctly - but something wasn’t working.

I tried adding --external:npm:lit to the esbuild command, but that didn’t help.

This failed too:

Terminal window
npx esbuild index.js --bundle --outfile=bundle.js --external:lit

So I started from scratch:

Terminal window
mkdir /tmp/datasette-demo2
cd /tmp/datasette-demo2

This time I used a mechanism I found in the advanced setup section of the npm compatibility documentation:

Terminal window
echo '@jsr:registry=https://npm.jsr.io' > .npmrc
npm install @jsr/datasette__table

find . showed me that it had installed the Lit packages, but I also noticed this:

Terminal window
find . | grep datasette
./node_modules/@jsr/datasette__table
./node_modules/@jsr/datasette__table/datasette-table.js
./node_modules/@jsr/datasette__table/package.json

So the JSR @datasette/table package is in a slightly different shape.

Now I tried the esbuild command again:

Terminal window
echo 'import * as mod from "@jsr/datasette__table";' > index.js
npx esbuild index.js --bundle --outfile=bundle.js

But I got the same error again:

✘ [ERROR] Could not resolve "npm:lit@^2.2.7"
node_modules/@jsr/datasette__table/datasette-table.js:1:36:
1 │ import {LitElement, html, css} from 'npm:lit@^2.2.7';
╵ ~~~~~~~~~~~~~~~~
You can mark the path "npm:lit@^2.2.7" as external to exclude it from the bundle, which will
remove this error and leave the unresolved path in the bundle.
1 error

And at this point… I gave up. I’m still seeking a solution - progress so far:

And now it works!#

Update 5th March 2024: the JSR team shipped this fix and now the following recipe works exactly as I want it to:

Terminal window
mkdir /tmp/datasette-demo3
cd /tmp/datasette-demo3
echo '@jsr:registry=https://npm.jsr.io' > .npmrc
npm install @jsr/datasette__table
echo 'import * as mod from "@jsr/datasette__table";' > index.js
npx esbuild index.js --bundle --outfile=bundle.js
echo '<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Datasette News</title>
</head>
<body>
<datasette-table
url="https://datasette.io/content/news.json"
></datasette-table>
<script src="bundle.js"></script>
</body>
</html>' > index.html

Now open index.html in a browser and:

Screenshot of a page with a table of recent news articles on it, rendered by my Web Component

Using packages from JSR with esbuild
https://mranv.pages.dev/posts/using-packages-from-jsr-with-esbuild/
Author
Anubhav Gain
Published at
2024-08-01
License
CC BY-NC-SA 4.0