diff --git a/docs/imports.md b/docs/imports.md
index 0a340b90b..31bfa8a05 100644
--- a/docs/imports.md
+++ b/docs/imports.md
@@ -99,6 +99,18 @@ Unlike `npm:` imports, Node imports do not support semver ranges: the imported v
Imports from `node_modules` are cached in `.observablehq/cache/_node` within your [source root](./config#root) (typically `src`). You shouldn’t need to clear this cache as it is automatically managed, but feel free to clear it you like.
+## JSR imports
+
+You can import a package from [JSR (the JavaScript Registry)](https://jsr.io/) using the `jsr:` protocol. When you import using `jsr:`, Framework automatically downloads and self-hosts the package. (As with `npm:` imports, and unlike Node imports, you don’t have to install from `jsr:` manually.) As an example, here the number three is computed using a [pseudorandom number generator](https://jsr.io/@std/random) from the [Deno Standard Library](https://deno.com/blog/std-on-jsr):
+
+```js echo
+import {randomIntegerBetween, randomSeeded} from "jsr:@std/random";
+
+const prng = randomSeeded(1n);
+
+display(randomIntegerBetween(1, 10, {prng}));
+```
+
## Local imports
You can import [JavaScript](./javascript) and [TypeScript](./javascript#type-script) modules from local files. This is useful for organizing your code into modules that can be imported across multiple pages. You can also unit test your code and share code with other web applications.
diff --git a/package.json b/package.json
index a5a83b516..4d70fbc01 100644
--- a/package.json
+++ b/package.json
@@ -43,6 +43,7 @@
"test/build/src/bin/",
"test/build/src/client/",
"test/build/src/convert.js",
+ "test/build/src/jsr.js",
"test/build/src/observableApiConfig.js",
"test/build/src/preview.js"
],
@@ -83,10 +84,12 @@
"minisearch": "^6.3.0",
"open": "^10.1.0",
"pkg-dir": "^8.0.0",
+ "resolve.exports": "^2.0.2",
"rollup": "^4.6.0",
"rollup-plugin-esbuild": "^6.1.0",
"semver": "^7.5.4",
"send": "^0.18.0",
+ "tar": "^6.2.0",
"tar-stream": "^3.1.6",
"tsx": "^4.7.1",
"untildify": "^5.0.0",
@@ -105,6 +108,7 @@
"@types/node": "^20.7.1",
"@types/prompts": "^2.4.9",
"@types/send": "^0.17.2",
+ "@types/tar": "^6.1.11",
"@types/tar-stream": "^3.1.3",
"@types/ws": "^8.5.6",
"@typescript-eslint/eslint-plugin": "^7.2.0",
diff --git a/src/jsr.ts b/src/jsr.ts
new file mode 100644
index 000000000..e41d57297
--- /dev/null
+++ b/src/jsr.ts
@@ -0,0 +1,191 @@
+import {mkdir, readFile, writeFile} from "node:fs/promises";
+import {join} from "node:path/posix";
+import {Readable} from "node:stream";
+import {finished} from "node:stream/promises";
+import {globSync} from "glob";
+import {exports as resolveExports} from "resolve.exports";
+import {rsort, satisfies} from "semver";
+import {x} from "tar";
+import type {ImportReference} from "./javascript/imports.js";
+import {parseImports} from "./javascript/imports.js";
+import type {NpmSpecifier} from "./npm.js";
+import {formatNpmSpecifier, parseNpmSpecifier} from "./npm.js";
+import {initializeNpmVersionCache, resolveNpmImport, rewriteNpmImports} from "./npm.js";
+import {isPathImport} from "./path.js";
+import {faint} from "./tty.js";
+
+const jsrVersionCaches = new Map>>();
+const jsrVersionRequests = new Map>();
+const jsrPackageRequests = new Map>();
+const jsrResolveRequests = new Map>();
+
+function getJsrVersionCache(root: string): Promise