From aad7e1470a26d5b684d4015f4464ecdeb83322a0 Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Sat, 2 Mar 2024 10:39:24 -0800 Subject: [PATCH 01/24] jsr: imports --- docs/jsr.md | 11 +++++++++ package.json | 2 ++ src/build.ts | 11 +++++---- src/jsr.ts | 53 +++++++++++++++++++++++++++++++++++++++++ src/preview.ts | 3 +++ src/resolvers.ts | 54 +++++++++++++++++++++++++---------------- yarn.lock | 62 ++++++++++++++++++++++++++++++++++++++++++++++++ 7 files changed, 171 insertions(+), 25 deletions(-) create mode 100644 docs/jsr.md create mode 100644 src/jsr.ts diff --git a/docs/jsr.md b/docs/jsr.md new file mode 100644 index 000000000..54cbaa7c9 --- /dev/null +++ b/docs/jsr.md @@ -0,0 +1,11 @@ +# Look, Ma! JSR imports! + +```js echo +import {printProgress} from "jsr:@luca/flag"; + +printProgress(); +``` + +```js echo +import.meta.resolve("jsr:@luca/flag") +``` diff --git a/package.json b/package.json index 83dcacf90..39053e921 100644 --- a/package.json +++ b/package.json @@ -74,6 +74,7 @@ "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.2.1", "untildify": "^5.0.0", @@ -91,6 +92,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": "^6.7.3", diff --git a/src/build.ts b/src/build.ts index 466f79d4e..25a78f261 100644 --- a/src/build.ts +++ b/src/build.ts @@ -175,10 +175,13 @@ export async function build( // these, too, but it would involve rewriting the files since populateNpmCache // doesn’t let you pass in a resolver. for (const path of globalImports) { - if (!path.startsWith("/_npm/")) continue; // skip _observablehq - effects.output.write(`${faint("copy")} npm:${resolveNpmSpecifier(path)} ${faint("→")} `); - const sourcePath = await populateNpmCache(root, path); // TODO effects - await effects.copyFile(sourcePath, path); + if (path.startsWith("/_npm/")) { + effects.output.write(`${faint("copy")} npm:${resolveNpmSpecifier(path)} ${faint("→")} `); + const sourcePath = await populateNpmCache(root, path); // TODO effects + await effects.copyFile(sourcePath, path); + } else if (path.startsWith("/_jsr/")) { + // TODO jsr: + } } // Copy over imported local modules, overriding import resolution so that diff --git a/src/jsr.ts b/src/jsr.ts new file mode 100644 index 000000000..3a1f34053 --- /dev/null +++ b/src/jsr.ts @@ -0,0 +1,53 @@ +import {mkdir, readFile} from "node:fs/promises"; +import {join} from "node:path/posix"; +import {Readable} from "node:stream"; +import {finished} from "node:stream/promises"; +import {satisfies} from "semver"; +import {x} from "tar"; +import {formatNpmSpecifier, parseNpmSpecifier} from "./npm.js"; +import {faint} from "./tty.js"; + +const jsrRequests = new Map>>(); + +async function getJsrPackage(root: string, specifier: string): Promise> { + let promise = jsrRequests.get(specifier); + if (promise) return promise; + promise = (async function () { + process.stdout.write(`jsr:${specifier} ${faint("→")} `); + const {name, range = "latest"} = parseNpmSpecifier(specifier); + const metaHref = `https://npm.jsr.io/@jsr/${name.replace(/^@/, "").replace(/\//, "__")}`; + const metaResponse = await fetch(metaHref); + if (!metaResponse.ok) throw new Error(`unable to fetch: ${metaHref}`); + const meta = await metaResponse.json(); + let version: {version: string; dist: {tarball: string}} | undefined; + if (meta["dist-tags"][range]) { + version = meta["versions"][meta["dist-tags"][range]]; + } else if (range) { + if (meta.versions[range]) { + version = meta.versions[range]; // exact match; ignore yanked + } else { + for (const key in meta.versions) { + if (satisfies(key, range) && !meta.versions[key].yanked) { + version = meta.versions[key]; + } + } + } + } + if (!version) throw new Error(`unable to resolve version: ${specifier}`); + const tarballResponse = await fetch(version.dist.tarball); + if (!tarballResponse.ok) throw new Error(`unable to fetch: ${version.dist.tarball}`); + const dir = join(root, ".observablehq", "cache", "_jsr", formatNpmSpecifier({name, range: version.version})); + await mkdir(dir, {recursive: true}); + await finished(Readable.fromWeb(tarballResponse.body as any).pipe(x({strip: 1, C: dir}))); + process.stdout.write(`${version.version}\n`); + return JSON.parse(await readFile(join(dir, "package.json"), "utf8")); + })(); + jsrRequests.set(specifier, promise); + return promise; +} + +export async function resolveJsrImport(root: string, specifier: string): Promise { + const version = await getJsrPackage(root, specifier); + const {name, path = version.exports["."]} = parseNpmSpecifier(specifier); + return join("/", "_jsr", `${name}@${version.version}`, path); +} diff --git a/src/preview.ts b/src/preview.ts index a3c517b52..87df98f28 100644 --- a/src/preview.ts +++ b/src/preview.ts @@ -111,6 +111,9 @@ export class PreviewServer { } else if (pathname.startsWith("/_npm/")) { await populateNpmCache(root, pathname); send(req, pathname, {root: join(root, ".observablehq", "cache")}).pipe(res); + } else if (pathname.startsWith("/_jsr/")) { + // TODO await populateJsrCache + send(req, pathname, {root: join(root, ".observablehq", "cache")}).pipe(res); } else if (pathname.startsWith("/_import/")) { const path = pathname.slice("/_import".length); const filepath = join(root, path); diff --git a/src/resolvers.ts b/src/resolvers.ts index ee86861ba..4782b82a0 100644 --- a/src/resolvers.ts +++ b/src/resolvers.ts @@ -9,6 +9,7 @@ import {getImplicitStylesheets} from "./libraries.js"; import type {MarkdownPage} from "./markdown.js"; import {populateNpmCache, resolveNpmImport, resolveNpmImports, resolveNpmSpecifier} from "./npm.js"; import {isPathImport, relativePath, resolvePath} from "./path.js"; +import { resolveJsrImport } from "./jsr.js"; export interface Resolvers { hash: string; @@ -152,39 +153,50 @@ export async function getResolvers(page: MarkdownPage, {root, path}: {root: stri globalImports.add(i); } - // Resolve npm: imports. + // Resolve npm: and jsr: imports. for (const i of globalImports) { - if (i.startsWith("npm:") && !builtins.has(i)) { + if (builtins.has(i)) continue; + if (i.startsWith("npm:")) { resolutions.set(i, await resolveNpmImport(root, i.slice("npm:".length))); + } else if (i.startsWith("jsr:")) { + resolutions.set(i, await resolveJsrImport(root, i.slice("jsr:".length))); } } // Follow transitive imports of npm imports. This has the side-effect of // populating the npm cache. for (const value of resolutions.values()) { - for (const i of await resolveNpmImports(root, value)) { - if (i.type === "local") { - const path = resolvePath(value, i.name); - const specifier = `npm:${resolveNpmSpecifier(path)}`; - globalImports.add(specifier); - resolutions.set(specifier, path); + if (value.startsWith("/_npm/")) { + for (const i of await resolveNpmImports(root, value)) { + if (i.type === "local") { + const path = resolvePath(value, i.name); + const specifier = `npm:${resolveNpmSpecifier(path)}`; + globalImports.add(specifier); + resolutions.set(specifier, path); + } } + } else if (value.startsWith("/_jsr/")) { + // TODO jsr: } } - // Resolve transitive static npm: imports. - const npmStaticResolutions = new Set(); + // Resolve transitive static npm: and jsr: imports. + const globalStaticResolutions = new Set(); for (const i of staticImports) { const r = resolutions.get(i); - if (r) npmStaticResolutions.add(r); + if (r) globalStaticResolutions.add(r); } - for (const value of npmStaticResolutions) { - for (const i of await resolveNpmImports(root, value)) { - if (i.type === "local" && i.method === "static") { - const path = resolvePath(value, i.name); - const specifier = `npm:${resolveNpmSpecifier(path)}`; - staticImports.add(specifier); + for (const value of globalStaticResolutions) { + if (value.startsWith("/_npm/")) { + for (const i of await resolveNpmImports(root, value)) { + if (i.type === "local" && i.method === "static") { + const path = resolvePath(value, i.name); + const specifier = `npm:${resolveNpmSpecifier(path)}`; + staticImports.add(specifier); + } } + } else { + // TODO jsr: } } @@ -195,6 +207,8 @@ export async function getResolvers(page: MarkdownPage, {root, path}: {root: stri const path = await resolveNpmImport(root, specifier.slice("npm:".length)); resolutions.set(specifier, path); await populateNpmCache(root, path); + } else if (specifier.startsWith("jsr:")) { + // TODO jsr: } } @@ -206,6 +220,8 @@ export async function getResolvers(page: MarkdownPage, {root, path}: {root: stri const path = await resolveNpmImport(root, specifier.slice("npm:".length)); resolutions.set(specifier, path); await populateNpmCache(root, path); + } else if (specifier.startsWith("jsr:")) { + // TODO jsr: } } @@ -218,8 +234,6 @@ export async function getResolvers(page: MarkdownPage, {root, path}: {root: stri ? relativePath(path, `/_observablehq/${specifier.slice("observablehq:".length)}${extname(specifier) ? "" : ".js"}`) // prettier-ignore : resolutions.has(specifier) ? relativePath(path, resolutions.get(specifier)!) - : specifier.startsWith("npm:") - ? `https://cdn.jsdelivr.net/npm/${specifier.slice("npm:".length)}` : specifier; } @@ -234,8 +248,6 @@ export async function getResolvers(page: MarkdownPage, {root, path}: {root: stri ? relativePath(path, `/_observablehq/${specifier.slice("observablehq:".length)}`) : resolutions.has(specifier) ? relativePath(path, resolutions.get(specifier)!) - : specifier.startsWith("npm:") - ? `https://cdn.jsdelivr.net/npm/${specifier.slice("npm:".length)}` : specifier; } diff --git a/yarn.lock b/yarn.lock index 18e2e39bf..32bbfa16d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -633,6 +633,14 @@ dependencies: "@types/node" "*" +"@types/tar@^6.1.11": + version "6.1.11" + resolved "https://registry.yarnpkg.com/@types/tar/-/tar-6.1.11.tgz#48de9ccee8db37efb0d5a9f288567fc0378cb734" + integrity sha512-ThA1WD8aDdVU4VLuyq5NEqriwXErF5gEIJeyT6gHBWU7JtSmW2a5qjNv3/vR82O20mW+1vhmeZJfBQPT3HCugg== + dependencies: + "@types/node" "*" + minipass "^4.0.0" + "@types/tough-cookie@*": version "4.0.5" resolved "https://registry.yarnpkg.com/@types/tough-cookie/-/tough-cookie-4.0.5.tgz#cb6e2a691b70cb177c6e3ae9c1d2e8b2ea8cd304" @@ -1072,6 +1080,11 @@ chokidar@3.5.3: optionalDependencies: fsevents "~2.3.2" +chownr@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/chownr/-/chownr-2.0.0.tgz#15bfbe53d2eab4cf70f18a8cd68ebe5b3cb1dece" + integrity sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ== + ci-info@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-4.0.0.tgz#65466f8b280fc019b9f50a5388115d17a63a44f2" @@ -1893,6 +1906,13 @@ fresh@0.5.2: resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" integrity sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q== +fs-minipass@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-2.1.0.tgz#7f5036fdbf12c63c169190cbe4199c852271f9fb" + integrity sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg== + dependencies: + minipass "^3.0.0" + fs.realpath@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" @@ -2776,6 +2796,23 @@ minimist@^1.2.0, minimist@^1.2.6: resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== +minipass@^3.0.0: + version "3.3.6" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-3.3.6.tgz#7bba384db3a1520d18c9c0e5251c3444e95dd94a" + integrity sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw== + dependencies: + yallist "^4.0.0" + +minipass@^4.0.0: + version "4.2.8" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-4.2.8.tgz#f0010f64393ecfc1d1ccb5f582bcaf45f48e1a3a" + integrity sha512-fNzuVyifolSLFL4NzpF+wEF4qrgqaaKX0haXPQEdQ7NKAN+WecoKMHV09YcuL/DHxrUsYQOK3MiuDf7Ip2OXfQ== + +minipass@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-5.0.0.tgz#3e9788ffb90b694a5d0ec94479a45b5d8738133d" + integrity sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ== + "minipass@^5.0.0 || ^6.0.2 || ^7.0.0": version "7.0.4" resolved "https://registry.yarnpkg.com/minipass/-/minipass-7.0.4.tgz#dbce03740f50a4786ba994c1fb908844d27b038c" @@ -2786,6 +2823,19 @@ minisearch@^6.3.0: resolved "https://registry.yarnpkg.com/minisearch/-/minisearch-6.3.0.tgz#985a2f1ca3c73c2d65af94f0616bfe57164b0b6b" integrity sha512-ihFnidEeU8iXzcVHy74dhkxh/dn8Dc08ERl0xwoMMGqp4+LvRSCgicb+zGqWthVokQKvCSxITlh3P08OzdTYCQ== +minizlib@^2.1.1: + version "2.1.2" + resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-2.1.2.tgz#e90d3466ba209b932451508a11ce3d3632145931" + integrity sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg== + dependencies: + minipass "^3.0.0" + yallist "^4.0.0" + +mkdirp@^1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" + integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== + mocha@^10.2.0: version "10.2.0" resolved "https://registry.yarnpkg.com/mocha/-/mocha-10.2.0.tgz#1fd4a7c32ba5ac372e03a17eef435bd00e5c68b8" @@ -3600,6 +3650,18 @@ tar-stream@^3.1.6: fast-fifo "^1.2.0" streamx "^2.15.0" +tar@^6.2.0: + version "6.2.0" + resolved "https://registry.yarnpkg.com/tar/-/tar-6.2.0.tgz#b14ce49a79cb1cd23bc9b016302dea5474493f73" + integrity sha512-/Wo7DcT0u5HUV486xg675HtjNd3BXZ6xDbzsCUZPt5iw8bTQ63bP0Raut3mvro9u+CUyq7YQd8Cx55fsZXxqLQ== + dependencies: + chownr "^2.0.0" + fs-minipass "^2.0.0" + minipass "^5.0.0" + minizlib "^2.1.1" + mkdirp "^1.0.3" + yallist "^4.0.0" + temp-dir@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/temp-dir/-/temp-dir-3.0.0.tgz#7f147b42ee41234cc6ba3138cd8e8aa2302acffa" From c9d6b4f88d201cffc8f7b2f02b8de58511116d93 Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Sat, 2 Mar 2024 10:48:43 -0800 Subject: [PATCH 02/24] comments, prettier --- src/resolvers.ts | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/resolvers.ts b/src/resolvers.ts index 4782b82a0..ee6f0d4e8 100644 --- a/src/resolvers.ts +++ b/src/resolvers.ts @@ -3,13 +3,13 @@ import {extname, join} from "node:path/posix"; import {findAssets} from "./html.js"; import {defaultGlobals} from "./javascript/globals.js"; import {getFileHash, getModuleHash, getModuleInfo} from "./javascript/module.js"; +import {resolveJsrImport} from "./jsr.js"; import {getImplicitDependencies, getImplicitDownloads} from "./libraries.js"; import {getImplicitFileImports, getImplicitInputImports} from "./libraries.js"; import {getImplicitStylesheets} from "./libraries.js"; import type {MarkdownPage} from "./markdown.js"; import {populateNpmCache, resolveNpmImport, resolveNpmImports, resolveNpmSpecifier} from "./npm.js"; import {isPathImport, relativePath, resolvePath} from "./path.js"; -import { resolveJsrImport } from "./jsr.js"; export interface Resolvers { hash: string; @@ -201,27 +201,25 @@ export async function getResolvers(page: MarkdownPage, {root, path}: {root: stri } // Add implicit stylesheets. + // TODO Add jsr: here as needed? for (const specifier of getImplicitStylesheets(staticImports)) { stylesheets.add(specifier); if (specifier.startsWith("npm:")) { const path = await resolveNpmImport(root, specifier.slice("npm:".length)); resolutions.set(specifier, path); await populateNpmCache(root, path); - } else if (specifier.startsWith("jsr:")) { - // TODO jsr: } } // Add implicit downloads. (This should be maybe be stored separately rather // than being tossed into global imports, but it works for now.) + // TODO Add jsr: here as needed? for (const specifier of getImplicitDownloads(globalImports)) { globalImports.add(specifier); if (specifier.startsWith("npm:")) { const path = await resolveNpmImport(root, specifier.slice("npm:".length)); resolutions.set(specifier, path); await populateNpmCache(root, path); - } else if (specifier.startsWith("jsr:")) { - // TODO jsr: } } From addf714c87c1f7c0c987041fb4805820838499cc Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Wed, 11 Sep 2024 16:56:59 -0700 Subject: [PATCH 03/24] read _jsr cache --- src/jsr.ts | 48 +++++++++++++++++++++++++++++++++++++----------- src/npm.ts | 6 +++--- 2 files changed, 40 insertions(+), 14 deletions(-) diff --git a/src/jsr.ts b/src/jsr.ts index 6bfccc3a4..bf1c15d7c 100644 --- a/src/jsr.ts +++ b/src/jsr.ts @@ -4,24 +4,49 @@ import {Readable} from "node:stream"; import {finished} from "node:stream/promises"; import {satisfies} from "semver"; import {x} from "tar"; -import {formatNpmSpecifier, parseNpmSpecifier} from "./npm.js"; +import type {NpmSpecifier} from "./npm.js"; +import {formatNpmSpecifier, initializeNpmVersionCache, parseNpmSpecifier} from "./npm.js"; import {faint} from "./tty.js"; +const jsrVersionCaches = new Map>>(); +const jsrVersionRequests = new Map>(); const jsrRequests = new Map>>(); +function getJsrVersionCache(root: string): Promise> { + let cache = jsrVersionCaches.get(root); + if (!cache) jsrVersionCaches.set(root, (cache = initializeNpmVersionCache(root, "_jsr"))); + return cache; +} + async function getJsrPackage(root: string, specifier: string): Promise> { let promise = jsrRequests.get(specifier); if (promise) return promise; promise = (async function () { - process.stdout.write(`jsr:${specifier} ${faint("→")} `); - const {name, range = "latest"} = parseNpmSpecifier(specifier); - const metaHref = `https://npm.jsr.io/@jsr/${name.replace(/^@/, "").replace(/\//, "__")}`; - const metaResponse = await fetch(metaHref); - if (!metaResponse.ok) throw new Error(`unable to fetch: ${metaHref}`); + const {name, range} = parseNpmSpecifier(specifier); + const version = await resolveJsrVersion(root, {name, range}); + const dir = join(root, ".observablehq", "cache", "_jsr", formatNpmSpecifier({name, range: version})); + return JSON.parse(await readFile(join(dir, "package.json"), "utf8")); + })(); + jsrRequests.set(specifier, promise); + return promise; +} + +async function resolveJsrVersion(root: string, specifier: NpmSpecifier): Promise { + const {name, range} = specifier; + const cache = await getJsrVersionCache(root); + const versions = cache.get(name); + if (versions) for (const version of versions) if (!range || satisfies(version, range)) return version; + const href = `https://npm.jsr.io/@jsr/${name.replace(/^@/, "").replace(/\//, "__")}`; + let promise = jsrVersionRequests.get(href); + if (promise) return promise; // coalesce concurrent requests + promise = (async function () { + process.stdout.write(`jsr:${formatNpmSpecifier(specifier)} ${faint("→")} `); + const metaResponse = await fetch(href); + if (!metaResponse.ok) throw new Error(`unable to fetch: ${href}`); const meta = await metaResponse.json(); let version: {version: string; dist: {tarball: string}} | undefined; - if (meta["dist-tags"][range]) { - version = meta["versions"][meta["dist-tags"][range]]; + if (meta["dist-tags"][range ?? "latest"]) { + version = meta["versions"][meta["dist-tags"][range ?? "latest"]]; } else if (range) { if (meta.versions[range]) { version = meta.versions[range]; // exact match; ignore yanked @@ -33,16 +58,17 @@ async function getJsrPackage(root: string, specifier: string): Promise jsrVersionRequests.delete(href)); + jsrVersionRequests.set(href, promise); return promise; } diff --git a/src/npm.ts b/src/npm.ts index 9245950fc..1f77489da 100644 --- a/src/npm.ts +++ b/src/npm.ts @@ -181,9 +181,9 @@ export async function getDependencyResolver( }; } -async function initializeNpmVersionCache(root: string): Promise> { +export async function initializeNpmVersionCache(root: string, dir = "_npm"): Promise> { const cache = new Map(); - const cacheDir = join(root, ".observablehq", "cache", "_npm"); + const cacheDir = join(root, ".observablehq", "cache", dir); try { for (const entry of await readdir(cacheDir)) { if (entry.startsWith("@")) { @@ -214,7 +214,7 @@ const npmVersionRequests = new Map>(); function getNpmVersionCache(root: string): Promise> { let cache = npmVersionCaches.get(root); - if (!cache) npmVersionCaches.set(root, (cache = initializeNpmVersionCache(root))); + if (!cache) npmVersionCaches.set(root, (cache = initializeNpmVersionCache(root, "_npm"))); return cache; } From 8e75cef78a5a8e29e12b5228bf2b07e39eadd254 Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Wed, 11 Sep 2024 16:59:04 -0700 Subject: [PATCH 04/24] consolidate --- src/preview.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/preview.ts b/src/preview.ts index e5fce30ca..9d52b94ef 100644 --- a/src/preview.ts +++ b/src/preview.ts @@ -139,14 +139,11 @@ export class PreviewServer { } else if (pathname.startsWith("/_observablehq/") && pathname.endsWith(".css")) { const path = getClientPath(pathname.slice("/_observablehq/".length)); end(req, res, await bundleStyles({path}), "text/css"); - } else if (pathname.startsWith("/_node/")) { + } else if (pathname.startsWith("/_node/") || pathname.startsWith("/_jsr/")) { send(req, pathname, {root: join(root, ".observablehq", "cache")}).pipe(res); } else if (pathname.startsWith("/_npm/")) { await populateNpmCache(root, pathname); send(req, pathname, {root: join(root, ".observablehq", "cache")}).pipe(res); - } else if (pathname.startsWith("/_jsr/")) { - // TODO await populateJsrCache - send(req, pathname, {root: join(root, ".observablehq", "cache")}).pipe(res); } else if (pathname.startsWith("/_import/")) { const path = pathname.slice("/_import".length); if (pathname.endsWith(".css")) { From 022a94399f126aedc473959128acf1ede1812835 Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Wed, 11 Sep 2024 17:04:08 -0700 Subject: [PATCH 05/24] test @quentinadam/hex --- docs/jsr.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/jsr.md b/docs/jsr.md index 54cbaa7c9..7ab8568e1 100644 --- a/docs/jsr.md +++ b/docs/jsr.md @@ -9,3 +9,9 @@ printProgress(); ```js echo import.meta.resolve("jsr:@luca/flag") ``` + +```js echo +import {decode} from "jsr:@quentinadam/hex"; + +display(decode("000102")); +``` From 1da29cd888d75a24cbbecdc283892321e408c9d4 Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Wed, 11 Sep 2024 17:20:54 -0700 Subject: [PATCH 06/24] test @oak/oak --- docs/jsr.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/jsr.md b/docs/jsr.md index 7ab8568e1..4cb4dd5de 100644 --- a/docs/jsr.md +++ b/docs/jsr.md @@ -15,3 +15,7 @@ import {decode} from "jsr:@quentinadam/hex"; display(decode("000102")); ``` + +```js echo +import "jsr:@oak/oak"; +``` From fe5bd4121a04c401656950402081dbd9f33a0ca6 Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Wed, 11 Sep 2024 18:08:41 -0700 Subject: [PATCH 07/24] transitive jsr: imports --- docs/jsr.md | 4 --- src/jsr.ts | 78 +++++++++++++++++++++++++++++++++++++---------------- 2 files changed, 55 insertions(+), 27 deletions(-) diff --git a/docs/jsr.md b/docs/jsr.md index 4cb4dd5de..7ab8568e1 100644 --- a/docs/jsr.md +++ b/docs/jsr.md @@ -15,7 +15,3 @@ import {decode} from "jsr:@quentinadam/hex"; display(decode("000102")); ``` - -```js echo -import "jsr:@oak/oak"; -``` diff --git a/src/jsr.ts b/src/jsr.ts index bf1c15d7c..a8cfab5e3 100644 --- a/src/jsr.ts +++ b/src/jsr.ts @@ -1,16 +1,17 @@ -import {mkdir, readFile} from "node:fs/promises"; -import {join} from "node:path/posix"; +import {mkdir, readFile, writeFile} from "node:fs/promises"; +import {extname, join, relative} from "node:path/posix"; import {Readable} from "node:stream"; import {finished} from "node:stream/promises"; import {satisfies} from "semver"; import {x} from "tar"; import type {NpmSpecifier} from "./npm.js"; -import {formatNpmSpecifier, initializeNpmVersionCache, parseNpmSpecifier} from "./npm.js"; +import {formatNpmSpecifier, initializeNpmVersionCache, parseNpmSpecifier, rewriteNpmImports} from "./npm.js"; import {faint} from "./tty.js"; +import {isPathImport, resolvePath} from "./path.js"; const jsrVersionCaches = new Map>>(); const jsrVersionRequests = new Map>(); -const jsrRequests = new Map>>(); +const jsrRequests = new Map>(); function getJsrVersionCache(root: string): Promise> { let cache = jsrVersionCaches.get(root); @@ -18,19 +19,6 @@ function getJsrVersionCache(root: string): Promise> { return cache; } -async function getJsrPackage(root: string, specifier: string): Promise> { - let promise = jsrRequests.get(specifier); - if (promise) return promise; - promise = (async function () { - const {name, range} = parseNpmSpecifier(specifier); - const version = await resolveJsrVersion(root, {name, range}); - const dir = join(root, ".observablehq", "cache", "_jsr", formatNpmSpecifier({name, range: version})); - return JSON.parse(await readFile(join(dir, "package.json"), "utf8")); - })(); - jsrRequests.set(specifier, promise); - return promise; -} - async function resolveJsrVersion(root: string, specifier: NpmSpecifier): Promise { const {name, range} = specifier; const cache = await getJsrVersionCache(root); @@ -73,12 +61,56 @@ async function resolveJsrVersion(root: string, specifier: NpmSpecifier): Promise } export async function resolveJsrImport(root: string, specifier: string): Promise { - const version = await getJsrPackage(root, specifier); - const {name, path = getDefaultEntry(version)} = parseNpmSpecifier(specifier); - return join("/", "_jsr", `${name}@${version.version}`, path); + let promise = jsrRequests.get(specifier); + if (promise) return promise; + promise = (async function () { + const spec = parseNpmSpecifier(specifier); + const {name} = spec; + const version = await resolveJsrVersion(root, spec); + const dir = join(root, ".observablehq", "cache", "_jsr", formatNpmSpecifier({name, range: version})); + const info = JSON.parse(await readFile(join(dir, "package.json"), "utf8")); + let path = spec.path; + try { + path = findEntry(info, path); + await rewriteJsrImports(root, dir, path); + } catch { + path ??= "index.js"; + } + return join("/", "_jsr", `${name}@${version}`, path); + })(); + jsrRequests.set(specifier, promise); + return promise; +} + +// TODO This should probably be done when initially downloading the package, +// rather than when we try to resolve it. Maybe we only do this for files that +// are listed in exports (and their transitive dependencies)… and maybe only for +// .js files? +async function rewriteJsrImports(root: string, dir: string, path: string): Promise { + const input = await readFile(join(dir, path), "utf8"); + const promises = new Map>(); + const transitives: Promise[] = []; + rewriteNpmImports(input, (i) => { + if (i.startsWith("@jsr/")) { + const s = `@${i.slice("@jsr/".length).replace(/__/, "/")}`; + if (!promises.has(s)) promises.set(i, resolveJsrImport(root, s)); + } else if (isPathImport(i)) { + transitives.push(rewriteJsrImports(root, dir, resolvePath(path, i))); + } else if (!/^\0?[\w-]+:/.test(i)) { + // TODO npm import + } + return i; + }); + const resolutions = new Map(); + for (const [key, promise] of promises) resolutions.set(key, await promise); + await Promise.all(transitives); + const output = rewriteNpmImports(input, (i) => resolutions.get(i) ?? i); + await writeFile(join(dir, path), output, "utf8"); } -function getDefaultEntry(version: Record): string { - const entry = version.exports["."]; - return typeof entry === "string" ? entry : entry.default; +function findEntry({exports}: Record, name = "."): string { + const entry = exports[name]; + if (typeof entry === "string") return entry; + if (typeof entry?.default === "string") return entry.default; + throw new Error(`unable to find entry for ${name}`, exports); } From b4cc1794640fc47bb16482aa4cc0da9c447f336b Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Wed, 11 Sep 2024 20:54:54 -0700 Subject: [PATCH 08/24] it works! --- docs/jsr.md | 6 +++ src/jsr.ts | 108 ++++++++++++++++++++++++++++++++-------------------- 2 files changed, 72 insertions(+), 42 deletions(-) diff --git a/docs/jsr.md b/docs/jsr.md index 7ab8568e1..f6d74c264 100644 --- a/docs/jsr.md +++ b/docs/jsr.md @@ -15,3 +15,9 @@ import {decode} from "jsr:@quentinadam/hex"; display(decode("000102")); ``` + +```js echo +import * as oak from "jsr:@oak/oak"; + +display(oak); +``` diff --git a/src/jsr.ts b/src/jsr.ts index a8cfab5e3..e87f34948 100644 --- a/src/jsr.ts +++ b/src/jsr.ts @@ -1,17 +1,19 @@ import {mkdir, readFile, writeFile} from "node:fs/promises"; -import {extname, join, relative} from "node:path/posix"; +import {join, relative} from "node:path/posix"; import {Readable} from "node:stream"; import {finished} from "node:stream/promises"; import {satisfies} from "semver"; import {x} from "tar"; import type {NpmSpecifier} from "./npm.js"; -import {formatNpmSpecifier, initializeNpmVersionCache, parseNpmSpecifier, rewriteNpmImports} from "./npm.js"; -import {faint} from "./tty.js"; +import {formatNpmSpecifier, parseNpmSpecifier} from "./npm.js"; +import {initializeNpmVersionCache, resolveNpmImport, rewriteNpmImports} from "./npm.js"; import {isPathImport, resolvePath} from "./path.js"; +import {faint} from "./tty.js"; const jsrVersionCaches = new Map>>(); const jsrVersionRequests = new Map>(); -const jsrRequests = new Map>(); +const jsrPackageRequests = new Map>(); +const jsrResolveRequests = new Map>(); function getJsrVersionCache(root: string): Promise> { let cache = jsrVersionCaches.get(root); @@ -47,11 +49,7 @@ async function resolveJsrVersion(root: string, specifier: NpmSpecifier): Promise } } if (!version) throw new Error(`unable to resolve version: ${formatNpmSpecifier(specifier)}`); - const tarballResponse = await fetch(version.dist.tarball); - if (!tarballResponse.ok) throw new Error(`unable to fetch: ${version.dist.tarball}`); - const dir = join(root, ".observablehq", "cache", "_jsr", formatNpmSpecifier({name, range: version.version})); - await mkdir(dir, {recursive: true}); - await finished(Readable.fromWeb(tarballResponse.body as any).pipe(x({strip: 1, C: dir}))); + await fetchJsrPackage(root, name, version.version, version.dist.tarball); process.stdout.write(`${version.version}\n`); return version.version; })(); @@ -60,8 +58,23 @@ async function resolveJsrVersion(root: string, specifier: NpmSpecifier): Promise return promise; } +async function fetchJsrPackage(root: string, name: string, version: string, tarball: string): Promise { + const dir = join(root, ".observablehq", "cache", "_jsr", formatNpmSpecifier({name, range: version})); + let promise = jsrPackageRequests.get(dir); + if (promise) return promise; + promise = (async () => { + const tarballResponse = await fetch(tarball); + if (!tarballResponse.ok) throw new Error(`unable to fetch: ${tarball}`); + await mkdir(dir, {recursive: true}); + await finished(Readable.fromWeb(tarballResponse.body as any).pipe(x({strip: 1, C: dir}))); + await rewriteJsrImports(root, dir); + })(); + jsrPackageRequests.set(dir, promise); + return promise; +} + export async function resolveJsrImport(root: string, specifier: string): Promise { - let promise = jsrRequests.get(specifier); + let promise = jsrResolveRequests.get(specifier); if (promise) return promise; promise = (async function () { const spec = parseNpmSpecifier(specifier); @@ -69,48 +82,59 @@ export async function resolveJsrImport(root: string, specifier: string): Promise const version = await resolveJsrVersion(root, spec); const dir = join(root, ".observablehq", "cache", "_jsr", formatNpmSpecifier({name, range: version})); const info = JSON.parse(await readFile(join(dir, "package.json"), "utf8")); - let path = spec.path; - try { - path = findEntry(info, path); - await rewriteJsrImports(root, dir, path); - } catch { - path ??= "index.js"; - } + const path = findEntry(info, spec.path); return join("/", "_jsr", `${name}@${version}`, path); })(); - jsrRequests.set(specifier, promise); + jsrResolveRequests.set(specifier, promise); return promise; } -// TODO This should probably be done when initially downloading the package, -// rather than when we try to resolve it. Maybe we only do this for files that -// are listed in exports (and their transitive dependencies)… and maybe only for -// .js files? -async function rewriteJsrImports(root: string, dir: string, path: string): Promise { - const input = await readFile(join(dir, path), "utf8"); - const promises = new Map>(); - const transitives: Promise[] = []; - rewriteNpmImports(input, (i) => { - if (i.startsWith("@jsr/")) { - const s = `@${i.slice("@jsr/".length).replace(/__/, "/")}`; - if (!promises.has(s)) promises.set(i, resolveJsrImport(root, s)); - } else if (isPathImport(i)) { - transitives.push(rewriteJsrImports(root, dir, resolvePath(path, i))); - } else if (!/^\0?[\w-]+:/.test(i)) { - // TODO npm import +const rewritten = new Set(); + +async function rewriteJsrImports(root: string, dir: string): Promise { + const paths = new Set(); + const normalizePath = (path: string) => relative(dir, join(dir, path)); + let {exports} = JSON.parse(await readFile(join(dir, "package.json"), "utf8")); + if (exports !== undefined) { + if (typeof exports === "string") exports = {".": exports}; + for (const name in exports) { + const value = exports[name]; + if (typeof value === "string") paths.add(normalizePath(value)); + else if (typeof value?.default === "string") paths.add(normalizePath(value.default)); // TODO browser entry? + } + } + for (const path of paths) { + if (rewritten.has(join(dir, path))) throw new Error(`already rewritten: ${join(dir, path)}`); + rewritten.add(join(dir, path)); + const input = await readFile(join(dir, path), "utf8"); + const promises = new Map>(); + try { + rewriteNpmImports(input, (i) => { + if (i.startsWith("@jsr/")) { + const s = `@${i.slice("@jsr/".length).replace(/__/, "/")}`; + if (!promises.has(s)) promises.set(i, resolveJsrImport(root, s)); + } else if (isPathImport(i)) { + paths.add(normalizePath(resolvePath(path, i))); + } else if (!/^[\w-]+:/.test(i)) { + if (!promises.has(i)) promises.set(i, resolveNpmImport(root, i)); + } + return i; + }); + } catch { + continue; // ignore syntax errors } - return i; - }); - const resolutions = new Map(); - for (const [key, promise] of promises) resolutions.set(key, await promise); - await Promise.all(transitives); - const output = rewriteNpmImports(input, (i) => resolutions.get(i) ?? i); - await writeFile(join(dir, path), output, "utf8"); + const resolutions = new Map(); + for (const [key, promise] of promises) resolutions.set(key, await promise); + const output = rewriteNpmImports(input, (i) => resolutions.get(i) ?? i); + await writeFile(join(dir, path), output, "utf8"); + } } +// TODO subpath patterns? import condition? nested conditions? function findEntry({exports}: Record, name = "."): string { + if (name !== "." && !name.startsWith("./")) name = `./${name}`; const entry = exports[name]; if (typeof entry === "string") return entry; if (typeof entry?.default === "string") return entry.default; - throw new Error(`unable to find entry for ${name}`, exports); + throw new Error(`unable to find entry for ${name}`); } From 5e3fcf94d747e86c736c56062e21c7cf60ff884b Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Wed, 11 Sep 2024 21:04:48 -0700 Subject: [PATCH 09/24] jsr preloads --- src/jsr.ts | 7 +++++++ src/resolvers.ts | 26 +++++++++++++++++++++++--- 2 files changed, 30 insertions(+), 3 deletions(-) diff --git a/src/jsr.ts b/src/jsr.ts index e87f34948..3a6e108a1 100644 --- a/src/jsr.ts +++ b/src/jsr.ts @@ -4,6 +4,8 @@ import {Readable} from "node:stream"; import {finished} from "node:stream/promises"; import {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"; @@ -138,3 +140,8 @@ function findEntry({exports}: Record, name = "."): string { if (typeof entry?.default === "string") return entry.default; throw new Error(`unable to find entry for ${name}`); } + +export async function resolveJsrImports(root: string, path: string): Promise { + if (!path.startsWith("/_jsr/")) throw new Error(`invalid jsr path: ${path}`); + return parseImports(join(root, ".observablehq", "cache"), path); +} diff --git a/src/resolvers.ts b/src/resolvers.ts index b43629f2f..6db656476 100644 --- a/src/resolvers.ts +++ b/src/resolvers.ts @@ -3,7 +3,7 @@ import {extname, join} from "node:path/posix"; import {findAssets} from "./html.js"; import {defaultGlobals} from "./javascript/globals.js"; import {getFileHash, getModuleHash, getModuleInfo} from "./javascript/module.js"; -import {resolveJsrImport} from "./jsr.js"; +import {resolveJsrImport, resolveJsrImports} from "./jsr.js"; import {getImplicitDependencies, getImplicitDownloads} from "./libraries.js"; import {getImplicitFileImports, getImplicitInputImports} from "./libraries.js"; import {getImplicitStylesheets} from "./libraries.js"; @@ -272,7 +272,17 @@ async function resolveResolvers( } } } else if (key.startsWith("jsr:")) { - // TODO jsr + for (const i of await resolveJsrImports(root, value)) { + if (i.type === "local") { + const path = resolvePath(value, i.name); + let specifier: string; + if (path.startsWith("/_npm/")) specifier = `npm:${extractNpmSpecifier(path)}`; + else if (path.startsWith("/_jsr/")) specifier = `jsr:${path.slice("/_jsr/".length)}`; + else continue; + globalImports.add(specifier); + resolutions.set(specifier, path); + } + } } else if (!/^\w+:/.test(key)) { for (const i of await resolveNodeImports(root, value)) { if (i.type === "local") { @@ -304,7 +314,17 @@ async function resolveResolvers( } } } else if (key.startsWith("jsr:")) { - // TODO jsr: + for (const i of await resolveJsrImports(root, value)) { + if (i.type === "local" && i.method === "static") { + const path = resolvePath(value, i.name); + let specifier: string; + if (path.startsWith("/_npm/")) specifier = `npm:${extractNpmSpecifier(path)}`; + else if (path.startsWith("/_jsr/")) specifier = `jsr:${path.slice("/_jsr/".length)}`; + else continue; + staticImports.add(specifier); + staticResolutions.set(specifier, path); + } + } } else if (!/^\w+:/.test(key)) { for (const i of await resolveNodeImports(root, value)) { if (i.type === "local" && i.method === "static") { From da2f106dc5f413ae7ea11f5b1f7a44d2be04c801 Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Wed, 11 Sep 2024 21:13:15 -0700 Subject: [PATCH 10/24] it just works --- docs/jsr.md | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/docs/jsr.md b/docs/jsr.md index f6d74c264..01ca1181d 100644 --- a/docs/jsr.md +++ b/docs/jsr.md @@ -1,5 +1,44 @@ # Look, Ma! JSR imports! +```js echo +import {yassify} from "jsr:@kwhinnery/yassify"; + +display(yassify("hello")); +``` + +```ts echo +import {BinarySearchTree} from "jsr:@std/data-structures@1"; + +const values = [3, 10, 13, 4, 6, 7, 1, 14]; +const tree = new BinarySearchTree(); +values.forEach((value) => tree.insert(value)); + +display(tree.min()); +display(tree.max()); +display(tree.find(42)); +display(tree.find(7)); +display(tree.remove(42)); +display(tree.remove(7)); +``` + +```js echo +import * as cases from "jsr:@luca/cases"; + +display(cases.splitPieces("helloWorld")); // ["hello", "world"] +display(cases.camelCase("hello world")); // "helloWorld" +display(cases.snakeCase("helloWorld")); // "hello_world" +display(cases.kebabCase("hello_world")); // "hello-world" +display(cases.titleCase("hello-world")); // "Hello World" +display(cases.pascalCase(["hello", "world"])); // "HelloWorld" +display(cases.constantCase("hello world")); // "HELLO_WORLD" +``` + +```js echo +import {detect} from "jsr:@luca/runtime-detector"; + +display(detect()); // "browser" +``` + ```js echo import {printProgress} from "jsr:@luca/flag"; From cb56630a29dced3fe92dc7bbdc0f12790d19fbe4 Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Wed, 11 Sep 2024 21:36:35 -0700 Subject: [PATCH 11/24] resolve.exports --- docs/jsr.md | 6 ++++++ package.json | 1 + src/jsr.ts | 12 ++---------- yarn.lock | 10 +++++----- 4 files changed, 14 insertions(+), 15 deletions(-) diff --git a/docs/jsr.md b/docs/jsr.md index 01ca1181d..1e417c19b 100644 --- a/docs/jsr.md +++ b/docs/jsr.md @@ -1,5 +1,11 @@ # Look, Ma! JSR imports! +```js echo +import {Hono} from "jsr:@hono/hono"; + +display(new Hono()); +``` + ```js echo import {yassify} from "jsr:@kwhinnery/yassify"; diff --git a/package.json b/package.json index 0213a1068..ac229e8d9 100644 --- a/package.json +++ b/package.json @@ -84,6 +84,7 @@ "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", diff --git a/src/jsr.ts b/src/jsr.ts index 3a6e108a1..60379e3f6 100644 --- a/src/jsr.ts +++ b/src/jsr.ts @@ -2,6 +2,7 @@ import {mkdir, readFile, writeFile} from "node:fs/promises"; import {join, relative} from "node:path/posix"; import {Readable} from "node:stream"; import {finished} from "node:stream/promises"; +import {exports as resolveExports} from "resolve.exports"; import {satisfies} from "semver"; import {x} from "tar"; import type {ImportReference} from "./javascript/imports.js"; @@ -84,7 +85,7 @@ export async function resolveJsrImport(root: string, specifier: string): Promise const version = await resolveJsrVersion(root, spec); const dir = join(root, ".observablehq", "cache", "_jsr", formatNpmSpecifier({name, range: version})); const info = JSON.parse(await readFile(join(dir, "package.json"), "utf8")); - const path = findEntry(info, spec.path); + const [path] = resolveExports(info, spec.path === undefined ? "." : `./${spec.path}`, {browser: true})!; return join("/", "_jsr", `${name}@${version}`, path); })(); jsrResolveRequests.set(specifier, promise); @@ -132,15 +133,6 @@ async function rewriteJsrImports(root: string, dir: string): Promise { } } -// TODO subpath patterns? import condition? nested conditions? -function findEntry({exports}: Record, name = "."): string { - if (name !== "." && !name.startsWith("./")) name = `./${name}`; - const entry = exports[name]; - if (typeof entry === "string") return entry; - if (typeof entry?.default === "string") return entry.default; - throw new Error(`unable to find entry for ${name}`); -} - export async function resolveJsrImports(root: string, path: string): Promise { if (!path.startsWith("/_jsr/")) throw new Error(`invalid jsr path: ${path}`); return parseImports(join(root, ".observablehq", "cache"), path); diff --git a/yarn.lock b/yarn.lock index b34c21d41..2c769cdfc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2567,11 +2567,6 @@ is-unicode-supported@^0.1.0: resolved "https://registry.yarnpkg.com/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz#3f26c76a809593b52bfa2ecb5710ed2779b522a7" integrity sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw== -is-unicode-supported@^1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/is-unicode-supported/-/is-unicode-supported-1.3.0.tgz#d824984b616c292a2e198207d4a609983842f714" - integrity sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ== - is-weakref@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/is-weakref/-/is-weakref-1.0.2.tgz#9529f383a9338205e89765e0392efc2f100f06f2" @@ -3318,6 +3313,11 @@ resolve-pkg-maps@^1.0.0: resolved "https://registry.yarnpkg.com/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz#616b3dc2c57056b5588c31cdf4b3d64db133720f" integrity sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw== +resolve.exports@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/resolve.exports/-/resolve.exports-2.0.2.tgz#f8c934b8e6a13f539e38b7098e2e36134f01e800" + integrity sha512-X2UW6Nw3n/aMgDVy+0rSqgHlv39WZAlZrXCdnbyEiKm17DSqHX4MmQMaST3FbeWR5FTuRcUwYAziZajji0Y7mg== + resolve@^1.22.1, resolve@^1.22.4: version "1.22.8" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.8.tgz#b6c87a9f2aa06dfab52e3d70ac8cde321fa5a48d" From e43a3e6354f8c077958b2b0818877cd2cb5f2699 Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Wed, 11 Sep 2024 21:47:46 -0700 Subject: [PATCH 12/24] rewrite all .js --- src/jsr.ts | 26 +++++--------------------- 1 file changed, 5 insertions(+), 21 deletions(-) diff --git a/src/jsr.ts b/src/jsr.ts index 60379e3f6..a6411d36b 100644 --- a/src/jsr.ts +++ b/src/jsr.ts @@ -1,7 +1,8 @@ import {mkdir, readFile, writeFile} from "node:fs/promises"; -import {join, relative} from "node:path/posix"; +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 {satisfies} from "semver"; import {x} from "tar"; @@ -10,7 +11,7 @@ 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, resolvePath} from "./path.js"; +import {isPathImport} from "./path.js"; import {faint} from "./tty.js"; const jsrVersionCaches = new Map>>(); @@ -92,23 +93,8 @@ export async function resolveJsrImport(root: string, specifier: string): Promise return promise; } -const rewritten = new Set(); - async function rewriteJsrImports(root: string, dir: string): Promise { - const paths = new Set(); - const normalizePath = (path: string) => relative(dir, join(dir, path)); - let {exports} = JSON.parse(await readFile(join(dir, "package.json"), "utf8")); - if (exports !== undefined) { - if (typeof exports === "string") exports = {".": exports}; - for (const name in exports) { - const value = exports[name]; - if (typeof value === "string") paths.add(normalizePath(value)); - else if (typeof value?.default === "string") paths.add(normalizePath(value.default)); // TODO browser entry? - } - } - for (const path of paths) { - if (rewritten.has(join(dir, path))) throw new Error(`already rewritten: ${join(dir, path)}`); - rewritten.add(join(dir, path)); + for (const path of globSync("**/*.js", {cwd: dir, nodir: true})) { const input = await readFile(join(dir, path), "utf8"); const promises = new Map>(); try { @@ -116,9 +102,7 @@ async function rewriteJsrImports(root: string, dir: string): Promise { if (i.startsWith("@jsr/")) { const s = `@${i.slice("@jsr/".length).replace(/__/, "/")}`; if (!promises.has(s)) promises.set(i, resolveJsrImport(root, s)); - } else if (isPathImport(i)) { - paths.add(normalizePath(resolvePath(path, i))); - } else if (!/^[\w-]+:/.test(i)) { + } else if (!isPathImport(i) && !/^[\w-]+:/.test(i)) { if (!promises.has(i)) promises.set(i, resolveNpmImport(root, i)); } return i; From fd146023cedb66efc3cb40054926664c27d28b8e Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Wed, 11 Sep 2024 21:52:08 -0700 Subject: [PATCH 13/24] simpler rewriteNpmImports --- src/jsr.ts | 3 +-- src/npm.ts | 10 ++++------ 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/src/jsr.ts b/src/jsr.ts index a6411d36b..759c78257 100644 --- a/src/jsr.ts +++ b/src/jsr.ts @@ -105,14 +105,13 @@ async function rewriteJsrImports(root: string, dir: string): Promise { } else if (!isPathImport(i) && !/^[\w-]+:/.test(i)) { if (!promises.has(i)) promises.set(i, resolveNpmImport(root, i)); } - return i; }); } catch { continue; // ignore syntax errors } const resolutions = new Map(); for (const [key, promise] of promises) resolutions.set(key, await promise); - const output = rewriteNpmImports(input, (i) => resolutions.get(i) ?? i); + const output = rewriteNpmImports(input, (i) => resolutions.get(i)); await writeFile(join(dir, path), output, "utf8"); } } diff --git a/src/npm.ts b/src/npm.ts index 1f77489da..b5f6badc5 100644 --- a/src/npm.ts +++ b/src/npm.ts @@ -35,11 +35,8 @@ export function formatNpmSpecifier({name, range, path}: NpmSpecifier): string { return `${name}${range ? `@${range}` : ""}${path ? `/${path}` : ""}`; } -/** - * Rewrites /npm/ import specifiers to be relative paths to /_npm/. - * TODO This isn’t specific to npm imports; it’ll resolve any static import? - */ -export function rewriteNpmImports(input: string, resolve: (specifier: string) => string = String): string { +/** Rewrites /npm/ import specifiers to be relative paths to /_npm/. */ +export function rewriteNpmImports(input: string, resolve: (s: string) => string | void = () => undefined): string { const body = parseProgram(input); const output = new Sourcemap(input); @@ -66,7 +63,8 @@ export function rewriteNpmImports(input: string, resolve: (specifier: string) => function rewriteImportSource(source: StringLiteral) { const value = getStringLiteralValue(source); const resolved = resolve(value); - if (value !== resolved) output.replaceLeft(source.start, source.end, JSON.stringify(resolved)); + if (resolved === undefined || value === resolved) return; + output.replaceLeft(source.start, source.end, JSON.stringify(resolved)); } // TODO Preserve the source map, but download it too. From 72add1b7f7b5cf0d59cacf51e6d9a3fae29d0aa4 Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Thu, 12 Sep 2024 07:29:38 -0700 Subject: [PATCH 14/24] jsr docs --- docs/imports.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/docs/imports.md b/docs/imports.md index 0a340b90b..528b9efef 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 the [JSR 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 `jsr:` imports manually.) As an example, here the number three is computed using a seeded [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. From dd210252af6a32cef9df94685b6f946b13badfe7 Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Thu, 12 Sep 2024 08:42:42 -0700 Subject: [PATCH 15/24] remove test page --- docs/jsr.md | 68 ----------------------------------------------------- 1 file changed, 68 deletions(-) delete mode 100644 docs/jsr.md diff --git a/docs/jsr.md b/docs/jsr.md deleted file mode 100644 index 1e417c19b..000000000 --- a/docs/jsr.md +++ /dev/null @@ -1,68 +0,0 @@ -# Look, Ma! JSR imports! - -```js echo -import {Hono} from "jsr:@hono/hono"; - -display(new Hono()); -``` - -```js echo -import {yassify} from "jsr:@kwhinnery/yassify"; - -display(yassify("hello")); -``` - -```ts echo -import {BinarySearchTree} from "jsr:@std/data-structures@1"; - -const values = [3, 10, 13, 4, 6, 7, 1, 14]; -const tree = new BinarySearchTree(); -values.forEach((value) => tree.insert(value)); - -display(tree.min()); -display(tree.max()); -display(tree.find(42)); -display(tree.find(7)); -display(tree.remove(42)); -display(tree.remove(7)); -``` - -```js echo -import * as cases from "jsr:@luca/cases"; - -display(cases.splitPieces("helloWorld")); // ["hello", "world"] -display(cases.camelCase("hello world")); // "helloWorld" -display(cases.snakeCase("helloWorld")); // "hello_world" -display(cases.kebabCase("hello_world")); // "hello-world" -display(cases.titleCase("hello-world")); // "Hello World" -display(cases.pascalCase(["hello", "world"])); // "HelloWorld" -display(cases.constantCase("hello world")); // "HELLO_WORLD" -``` - -```js echo -import {detect} from "jsr:@luca/runtime-detector"; - -display(detect()); // "browser" -``` - -```js echo -import {printProgress} from "jsr:@luca/flag"; - -printProgress(); -``` - -```js echo -import.meta.resolve("jsr:@luca/flag") -``` - -```js echo -import {decode} from "jsr:@quentinadam/hex"; - -display(decode("000102")); -``` - -```js echo -import * as oak from "jsr:@oak/oak"; - -display(oak); -``` From e9ee64b783725898d5d035a7bace42dd8cd34bcc Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Thu, 12 Sep 2024 08:43:02 -0700 Subject: [PATCH 16/24] test initializeNpmVersionCache --- test/npm-test.ts | 33 ++++++++++++++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/test/npm-test.ts b/test/npm-test.ts index aa9067830..ff7414e4a 100644 --- a/test/npm-test.ts +++ b/test/npm-test.ts @@ -1,5 +1,7 @@ import assert from "node:assert"; -import {extractNpmSpecifier, parseNpmSpecifier} from "../src/npm.js"; +import {mkdir} from "node:fs/promises"; +import {join} from "node:path/posix"; +import {extractNpmSpecifier, initializeNpmVersionCache, parseNpmSpecifier} from "../src/npm.js"; import {fromJsDelivrPath, getDependencyResolver, resolveNpmImport, rewriteNpmImports} from "../src/npm.js"; import {relativePath} from "../src/path.js"; import {mockJsDelivr} from "./mocks/jsdelivr.js"; @@ -145,6 +147,35 @@ describe("rewriteNpmImports(input, resolve)", () => { }); }); +describe("initializeNpmVersionCache(root, dir)", () => { + const root = join("test", "input", "npm"); + const dir = join(root, ".observablehq", "cache", "_npm"); + before(async () => { + await mkdir(join(dir, "@observablehq", "plot@0.6.11"), {recursive: true}); + await mkdir(join(dir, "@observablehq", "sample-datasets@1.0.1"), {recursive: true}); + await mkdir(join(dir, "d3-dsv@3.0.0"), {recursive: true}); + await mkdir(join(dir, "d3-dsv@3.0.1"), {recursive: true}); + await mkdir(join(dir, "htl@0.3.1"), {recursive: true}); + await mkdir(join(dir, "leaflet@1.9.4"), {recursive: true}); + }); + it("reads the contents of the specified _npm cache", async () => { + const cache = await initializeNpmVersionCache(root); + assert.deepStrictEqual( + cache, + new Map([ + ["@observablehq/plot", ["0.6.11"]], + ["@observablehq/sample-datasets", ["1.0.1"]], + ["d3-dsv", ["3.0.1", "3.0.0"]], + ["htl", ["0.3.1"]], + ["leaflet", ["1.9.4"]] + ]) + ); + }); + it("dir defaults to _npm", async () => { + assert.deepStrictEqual(await initializeNpmVersionCache(root, "_npm"), await initializeNpmVersionCache(root)); + }); +}); + function resolve(path: string, specifier: string): string { return specifier.startsWith("/npm/") ? relativePath(path, fromJsDelivrPath(specifier)) : specifier; } From 6360637513afe9ef6bb140f1ea4dcc94f26909d6 Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Thu, 12 Sep 2024 09:17:38 -0700 Subject: [PATCH 17/24] resolve dependency version --- src/jsr.ts | 40 +++++++++++++++++++++++++++++++++++++--- src/resolvers.ts | 4 ++-- 2 files changed, 39 insertions(+), 5 deletions(-) diff --git a/src/jsr.ts b/src/jsr.ts index 759c78257..ec48cf61c 100644 --- a/src/jsr.ts +++ b/src/jsr.ts @@ -93,17 +93,29 @@ export async function resolveJsrImport(root: string, specifier: string): Promise return promise; } +/** + * After downloading a package from JSR, this rewrites any transitive JSR and + * Node imports to use relative paths within the import cache. For example, if + * jsr:@std/streams depends on jsr:@std/bytes, this will replace an import of + * @jsr/std__bytes with a relative path to /_jsr/@std/bytes@1.0.2/mod.js. + */ async function rewriteJsrImports(root: string, dir: string): Promise { + const info = JSON.parse(await readFile(join(dir, "package.json"), "utf8")); for (const path of globSync("**/*.js", {cwd: dir, nodir: true})) { const input = await readFile(join(dir, path), "utf8"); const promises = new Map>(); try { rewriteNpmImports(input, (i) => { if (i.startsWith("@jsr/")) { - const s = `@${i.slice("@jsr/".length).replace(/__/, "/")}`; - if (!promises.has(s)) promises.set(i, resolveJsrImport(root, s)); + const {name, path} = parseNpmSpecifier(i); + const range = resolveDependencyVersion(info, name); + const specifier = formatNpmSpecifier({name: `@${name.slice("@jsr/".length).replace(/__/, "/")}`, range, path}); // prettier-ignore + if (!promises.has(i)) promises.set(i, resolveJsrImport(root, specifier)); } else if (!isPathImport(i) && !/^[\w-]+:/.test(i)) { - if (!promises.has(i)) promises.set(i, resolveNpmImport(root, i)); + const {name, path} = parseNpmSpecifier(i); + const range = resolveDependencyVersion(info, i); + const specifier = formatNpmSpecifier({name, range, path}); + if (!promises.has(i)) promises.set(i, resolveNpmImport(root, specifier)); } }); } catch { @@ -116,6 +128,28 @@ async function rewriteJsrImports(root: string, dir: string): Promise { } } +type PackageDependencies = Record; + +interface PackageInfo { + dependencies?: PackageDependencies; + devDependencies?: PackageDependencies; + peerDependencies?: PackageDependencies; + optionalDependencies?: PackageDependencies; + bundleDependencies?: PackageDependencies; + bundledDependencies?: PackageDependencies; +} + +function resolveDependencyVersion(info: PackageInfo, name: string): string | undefined { + return ( + info.dependencies?.[name] ?? + info.devDependencies?.[name] ?? + info.peerDependencies?.[name] ?? + info.optionalDependencies?.[name] ?? + info.bundleDependencies?.[name] ?? + info.bundledDependencies?.[name] + ); +} + export async function resolveJsrImports(root: string, path: string): Promise { if (!path.startsWith("/_jsr/")) throw new Error(`invalid jsr path: ${path}`); return parseImports(join(root, ".observablehq", "cache"), path); diff --git a/src/resolvers.ts b/src/resolvers.ts index 6db656476..28ebd415a 100644 --- a/src/resolvers.ts +++ b/src/resolvers.ts @@ -242,8 +242,8 @@ async function resolveResolvers( } // Resolve npm:, jsr:, and bare imports. This has the side-effect of - // populating the npm (and jsr?) import cache with direct dependencies, and - // the node import cache with all transitive dependencies. + // populating the npm import cache with direct dependencies, and the node + // and jsr import caches with all transitive dependencies. for (const i of globalImports) { if (builtins.has(i)) continue; if (i.startsWith("npm:")) { From 143d74aa5e31db3049e24873fac5024228f94b3e Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Thu, 12 Sep 2024 09:31:38 -0700 Subject: [PATCH 18/24] more comments --- src/jsr.ts | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/src/jsr.ts b/src/jsr.ts index ec48cf61c..88f5ad69b 100644 --- a/src/jsr.ts +++ b/src/jsr.ts @@ -25,8 +25,14 @@ function getJsrVersionCache(root: string): Promise> { return cache; } -async function resolveJsrVersion(root: string, specifier: NpmSpecifier): Promise { - const {name, range} = specifier; +/** + * Resolves the desired version of the specified JSR package, respecting the + * specifier’s range if any. If any satisfying packages already exist in the JSR + * import cache, the greatest satisfying cached version is returned. Otherwise, + * the desired version is resolved via JSR’s API, and then the package and all + * its transitive dependencies are downloaded from JSR (and npm if needed). + */ +async function resolveJsrVersion(root: string, {name, range}: NpmSpecifier): Promise { const cache = await getJsrVersionCache(root); const versions = cache.get(name); if (versions) for (const version of versions) if (!range || satisfies(version, range)) return version; @@ -34,7 +40,7 @@ async function resolveJsrVersion(root: string, specifier: NpmSpecifier): Promise let promise = jsrVersionRequests.get(href); if (promise) return promise; // coalesce concurrent requests promise = (async function () { - process.stdout.write(`jsr:${formatNpmSpecifier(specifier)} ${faint("→")} `); + process.stdout.write(`jsr:${formatNpmSpecifier({name, range})} ${faint("→")} `); const metaResponse = await fetch(href); if (!metaResponse.ok) throw new Error(`unable to fetch: ${href}`); const meta = await metaResponse.json(); @@ -52,7 +58,7 @@ async function resolveJsrVersion(root: string, specifier: NpmSpecifier): Promise } } } - if (!version) throw new Error(`unable to resolve version: ${formatNpmSpecifier(specifier)}`); + if (!version) throw new Error(`unable to resolve version: ${formatNpmSpecifier({name, range})}`); await fetchJsrPackage(root, name, version.version, version.dist.tarball); process.stdout.write(`${version.version}\n`); return version.version; @@ -62,6 +68,11 @@ async function resolveJsrVersion(root: string, specifier: NpmSpecifier): Promise return promise; } +/** + * Fetches a package from the JSR registry, as well as its transitive + * dependencies from JSR and npm, rewriting any dependency imports as relative + * paths within the import cache. + */ async function fetchJsrPackage(root: string, name: string, version: string, tarball: string): Promise { const dir = join(root, ".observablehq", "cache", "_jsr", formatNpmSpecifier({name, range: version})); let promise = jsrPackageRequests.get(dir); @@ -77,6 +88,10 @@ async function fetchJsrPackage(root: string, name: string, version: string, tarb return promise; } +/** + * Resolves the given JSR specifier, such as `@std/bytes@^1.0.0`, returning the + * path to the module such as `/_jsr/@std/bytes@1.0.2/mod.js`. + */ export async function resolveJsrImport(root: string, specifier: string): Promise { let promise = jsrResolveRequests.get(specifier); if (promise) return promise; @@ -139,6 +154,7 @@ interface PackageInfo { bundledDependencies?: PackageDependencies; } +// https://docs.npmjs.com/cli/v10/configuring-npm/package-json function resolveDependencyVersion(info: PackageInfo, name: string): string | undefined { return ( info.dependencies?.[name] ?? From 01e0223e7c7750474a9f8de86297cfcae54b8814 Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Thu, 12 Sep 2024 10:05:56 -0700 Subject: [PATCH 19/24] copy edits --- docs/imports.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/imports.md b/docs/imports.md index 528b9efef..31bfa8a05 100644 --- a/docs/imports.md +++ b/docs/imports.md @@ -99,9 +99,9 @@ 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 +## JSR imports -You can import a package from the [JSR 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 `jsr:` imports manually.) As an example, here the number three is computed using a seeded [pseudorandom number generator](https://jsr.io/@std/random) from the [Deno Standard Library](https://deno.com/blog/std-on-jsr): +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"; From c364226d60edb25b468724389382a04e25cb93f1 Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Sat, 14 Sep 2024 08:41:07 -0700 Subject: [PATCH 20/24] populate cache; handle error --- src/jsr.ts | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/src/jsr.ts b/src/jsr.ts index 88f5ad69b..daa01ac43 100644 --- a/src/jsr.ts +++ b/src/jsr.ts @@ -4,7 +4,7 @@ import {Readable} from "node:stream"; import {finished} from "node:stream/promises"; import {globSync} from "glob"; import {exports as resolveExports} from "resolve.exports"; -import {satisfies} from "semver"; +import {rsort, satisfies} from "semver"; import {x} from "tar"; import type {ImportReference} from "./javascript/imports.js"; import {parseImports} from "./javascript/imports.js"; @@ -44,24 +44,26 @@ async function resolveJsrVersion(root: string, {name, range}: NpmSpecifier): Pro const metaResponse = await fetch(href); if (!metaResponse.ok) throw new Error(`unable to fetch: ${href}`); const meta = await metaResponse.json(); - let version: {version: string; dist: {tarball: string}} | undefined; + let info: {version: string; dist: {tarball: string}} | undefined; if (meta["dist-tags"][range ?? "latest"]) { - version = meta["versions"][meta["dist-tags"][range ?? "latest"]]; + info = meta["versions"][meta["dist-tags"][range ?? "latest"]]; } else if (range) { if (meta.versions[range]) { - version = meta.versions[range]; // exact match; ignore yanked + info = meta.versions[range]; // exact match; ignore yanked } else { for (const key in meta.versions) { if (satisfies(key, range) && !meta.versions[key].yanked) { - version = meta.versions[key]; + info = meta.versions[key]; } } } } - if (!version) throw new Error(`unable to resolve version: ${formatNpmSpecifier({name, range})}`); - await fetchJsrPackage(root, name, version.version, version.dist.tarball); - process.stdout.write(`${version.version}\n`); - return version.version; + if (!info) throw new Error(`unable to resolve version: ${formatNpmSpecifier({name, range})}`); + const {version, dist} = info; + await fetchJsrPackage(root, name, version, dist.tarball); + cache.set(name, versions ? rsort(versions.concat(version)) : [version]); + process.stdout.write(`${version}\n`); + return version; })(); promise.catch(console.error).then(() => jsrVersionRequests.delete(href)); jsrVersionRequests.set(href, promise); @@ -84,6 +86,7 @@ async function fetchJsrPackage(root: string, name: string, version: string, tarb await finished(Readable.fromWeb(tarballResponse.body as any).pipe(x({strip: 1, C: dir}))); await rewriteJsrImports(root, dir); })(); + promise.catch(console.error).then(() => jsrPackageRequests.delete(dir)); jsrPackageRequests.set(dir, promise); return promise; } @@ -104,6 +107,7 @@ export async function resolveJsrImport(root: string, specifier: string): Promise const [path] = resolveExports(info, spec.path === undefined ? "." : `./${spec.path}`, {browser: true})!; return join("/", "_jsr", `${name}@${version}`, path); })(); + // TODO delete request promise? jsrResolveRequests.set(specifier, promise); return promise; } From a98196dad9c70a78e544f5c53d5e9159d461904a Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Sat, 14 Sep 2024 08:49:32 -0700 Subject: [PATCH 21/24] fail on unhandled implicit import --- src/resolvers.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/resolvers.ts b/src/resolvers.ts index 28ebd415a..98106a6a9 100644 --- a/src/resolvers.ts +++ b/src/resolvers.ts @@ -344,8 +344,8 @@ async function resolveResolvers( const path = await resolveNpmImport(root, specifier.slice("npm:".length)); resolutions.set(specifier, path); await populateNpmCache(root, path); - } else if (specifier.startsWith("jsr:")) { - // TODO jsr: + } else if (!specifier.startsWith("observablehq:")) { + throw new Error(`unhandled implicit stylesheet: ${specifier}`); } } @@ -357,8 +357,8 @@ async function resolveResolvers( const path = await resolveNpmImport(root, specifier.slice("npm:".length)); resolutions.set(specifier, path); await populateNpmCache(root, path); - } else if (specifier.startsWith("jsr:")) { - // TODO jsr: + } else if (!specifier.startsWith("observablehq:")) { + throw new Error(`unhandled implicit download: ${specifier}`); } } From 3de640685183337d671f6038e05a386453cd72ba Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Sat, 14 Sep 2024 09:16:19 -0700 Subject: [PATCH 22/24] fix getModuleStaticImports for jsr: --- src/jsr.ts | 17 ++++++++++++++++- src/resolvers.ts | 11 +++++++---- 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/src/jsr.ts b/src/jsr.ts index daa01ac43..e41d57297 100644 --- a/src/jsr.ts +++ b/src/jsr.ts @@ -93,9 +93,12 @@ async function fetchJsrPackage(root: string, name: string, version: string, tarb /** * Resolves the given JSR specifier, such as `@std/bytes@^1.0.0`, returning the - * path to the module such as `/_jsr/@std/bytes@1.0.2/mod.js`. + * path to the module such as `/_jsr/@std/bytes@1.0.2/mod.js`. This function + * also allows JSR specifiers with a leading slash indicating an already- + * resolved path such as /@std/bytes@1.0.2/mod.js. */ export async function resolveJsrImport(root: string, specifier: string): Promise { + if (specifier.startsWith("/")) return `/_jsr/${specifier.slice("/".length)}`; let promise = jsrResolveRequests.get(specifier); if (promise) return promise; promise = (async function () { @@ -174,3 +177,15 @@ export async function resolveJsrImports(root: string, path: string): Promise Date: Tue, 17 Sep 2024 17:56:42 -0700 Subject: [PATCH 23/24] test jsr: imports --- test/build-test.ts | 2 + test/input/build/jsr/index.md | 9 ++ test/input/jsr/std__random/0.1.0.tgz | Bin 0 -> 13661 bytes test/mocks/jsr.ts | 44 ++++++++++ test/mocks/undici.ts | 6 +- .../_jsr/@std/random@0.1.0/_pcg32.b2f4838f.js | 78 ++++++++++++++++++ .../@std/random@0.1.0/between.2ce008c6.js | 36 ++++++++ .../random@0.1.0/integer_between.4528767d.js | 26 ++++++ .../_jsr/@std/random@0.1.0/mod.e50195f4.js | 23 ++++++ .../_jsr/@std/random@0.1.0/sample.6e7cf133.js | 64 ++++++++++++++ .../_jsr/@std/random@0.1.0/seeded.4e59f274.js | 36 ++++++++ .../@std/random@0.1.0/shuffle.0ef8dd95.js | 43 ++++++++++ .../jsr/_observablehq/client.00000001.js | 0 .../jsr/_observablehq/runtime.00000002.js | 0 .../jsr/_observablehq/stdlib.00000003.js | 0 .../theme-air,near-midnight.00000004.css | 0 test/output/build/jsr/index.html | 53 ++++++++++++ 17 files changed, 416 insertions(+), 4 deletions(-) create mode 100644 test/input/build/jsr/index.md create mode 100644 test/input/jsr/std__random/0.1.0.tgz create mode 100644 test/mocks/jsr.ts create mode 100644 test/output/build/jsr/_jsr/@std/random@0.1.0/_pcg32.b2f4838f.js create mode 100644 test/output/build/jsr/_jsr/@std/random@0.1.0/between.2ce008c6.js create mode 100644 test/output/build/jsr/_jsr/@std/random@0.1.0/integer_between.4528767d.js create mode 100644 test/output/build/jsr/_jsr/@std/random@0.1.0/mod.e50195f4.js create mode 100644 test/output/build/jsr/_jsr/@std/random@0.1.0/sample.6e7cf133.js create mode 100644 test/output/build/jsr/_jsr/@std/random@0.1.0/seeded.4e59f274.js create mode 100644 test/output/build/jsr/_jsr/@std/random@0.1.0/shuffle.0ef8dd95.js create mode 100644 test/output/build/jsr/_observablehq/client.00000001.js create mode 100644 test/output/build/jsr/_observablehq/runtime.00000002.js create mode 100644 test/output/build/jsr/_observablehq/stdlib.00000003.js create mode 100644 test/output/build/jsr/_observablehq/theme-air,near-midnight.00000004.css create mode 100644 test/output/build/jsr/index.html diff --git a/test/build-test.ts b/test/build-test.ts index 2614ec21d..65a56a300 100644 --- a/test/build-test.ts +++ b/test/build-test.ts @@ -9,6 +9,7 @@ import type {BuildManifest} from "../src/build.js"; import {FileBuildEffects, build} from "../src/build.js"; import {normalizeConfig, readConfig, setCurrentDate} from "../src/config.js"; import {mockJsDelivr} from "./mocks/jsdelivr.js"; +import {mockJsr} from "./mocks/jsr.js"; const silentEffects = { logger: {log() {}, warn() {}, error() {}}, @@ -31,6 +32,7 @@ const failureTests = ["missing-file", "missing-import"]; describe("build", () => { before(() => setCurrentDate(new Date("2024-01-10T16:00:00"))); mockJsDelivr(); + mockJsr(); // Each sub-directory of test/input/build is a test case. const inputRoot = "test/input/build"; diff --git a/test/input/build/jsr/index.md b/test/input/build/jsr/index.md new file mode 100644 index 000000000..fc5f23396 --- /dev/null +++ b/test/input/build/jsr/index.md @@ -0,0 +1,9 @@ +# Hello JSR + +```js echo +import {randomIntegerBetween, randomSeeded} from "jsr:@std/random"; + +const prng = randomSeeded(1n); + +display(randomIntegerBetween(1, 10, {prng})); +``` diff --git a/test/input/jsr/std__random/0.1.0.tgz b/test/input/jsr/std__random/0.1.0.tgz new file mode 100644 index 0000000000000000000000000000000000000000..62e4888201c591755a5c40c0a874260c9fd0d1a5 GIT binary patch literal 13661 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 literal 0 HcmV?d00001 diff --git a/test/mocks/jsr.ts b/test/mocks/jsr.ts new file mode 100644 index 000000000..c9fe86e0d --- /dev/null +++ b/test/mocks/jsr.ts @@ -0,0 +1,44 @@ +import {readFile} from "node:fs/promises"; +import {join} from "node:path/posix"; +import {getCurrentAgent, mockAgent} from "./undici.js"; + +interface JsrPackageInfo { + "dist-tags": Record; + versions: Record; +} + +const packages: [name: string, JsrPackageInfo][] = [ + [ + "@std/random", + { + "dist-tags": { + latest: "0.1.0" + }, + versions: { + "0.1.0": {version: "0.1.0", dist: {tarball: "https://npm.jsr.io/~/11/@jsr/std__random/0.1.0.tgz"}} + } + } + ] +]; + +export function mockJsr() { + mockAgent(); + before(async () => { + const agent = getCurrentAgent(); + const npmClient = agent.get("https://npm.jsr.io"); + for (const [name, pkg] of packages) { + console.log("mockJsr", name); + npmClient + .intercept({path: `/@jsr/${name.replace(/^@/, "").replace(/\//, "__")}`, method: "GET"}) + .reply(200, pkg, {headers: {"content-type": "application/json; charset=utf-8"}}) + .persist(); // prettier-ignore + for (const version of Object.values(pkg.versions)) { + if (!version.dist.tarball.startsWith("https://npm.jsr.io")) continue; + npmClient + .intercept({path: version.dist.tarball.slice("https://npm.jsr.io".length), method: "GET"}) + .reply(200, async () => readFile(join("test/input/jsr", name.replace(/^@/, "").replace(/\//, "__"), version.version + ".tgz")), {headers: {"content-type": "application/octet-stream"}}) + .persist(); // prettier-ignore + } + } + }); +} diff --git a/test/mocks/undici.ts b/test/mocks/undici.ts index e18cdc719..4441ee78c 100644 --- a/test/mocks/undici.ts +++ b/test/mocks/undici.ts @@ -2,11 +2,10 @@ import type {Dispatcher} from "undici"; import {MockAgent, getGlobalDispatcher, setGlobalDispatcher} from "undici"; let currentAgent: MockAgent | null = null; +let globalDispatcher: Dispatcher; +let refCount = 0; export function mockAgent() { - let globalDispatcher: Dispatcher; - let refCount = 0; - before(async () => { if (refCount++ !== 0) return; globalDispatcher = getGlobalDispatcher(); @@ -14,7 +13,6 @@ export function mockAgent() { currentAgent.disableNetConnect(); setGlobalDispatcher(currentAgent); }); - after(async () => { if (--refCount !== 0) return; currentAgent = null; diff --git a/test/output/build/jsr/_jsr/@std/random@0.1.0/_pcg32.b2f4838f.js b/test/output/build/jsr/_jsr/@std/random@0.1.0/_pcg32.b2f4838f.js new file mode 100644 index 000000000..6c8d9cf96 --- /dev/null +++ b/test/output/build/jsr/_jsr/@std/random@0.1.0/_pcg32.b2f4838f.js @@ -0,0 +1,78 @@ +// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. +// Based on Rust `rand` crate (https://github.com/rust-random/rand). Apache-2.0 + MIT license. +/** Multiplier for the PCG32 algorithm. */ const MUL = 6364136223846793005n; +/** Initial increment for the PCG32 algorithm. Only used during seeding. */ const INC = 11634580027462260723n; +// Constants are for 64-bit state, 32-bit output +const ROTATE = 59n; // 64 - 5 +const XSHIFT = 18n; // (5 + 32) / 2 +const SPARE = 27n; // 64 - 32 - 5 +/** + * Modified from https://github.com/rust-random/rand/blob/f7bbccaedf6c63b02855b90b003c9b1a4d1fd1cb/rand_pcg/src/pcg64.rs#L129-L135 + */ export function fromSeed(seed) { + const d = new DataView(seed.buffer); + return fromStateIncr(d.getBigUint64(0, true), d.getBigUint64(8, true) | 1n); +} +/** + * Mutates `pcg` by advancing `pcg.state`. + */ function step(pgc) { + pgc.state = BigInt.asUintN(64, pgc.state * MUL + (pgc.inc | 1n)); +} +/** + * Modified from https://github.com/rust-random/rand/blob/f7bbccaedf6c63b02855b90b003c9b1a4d1fd1cb/rand_pcg/src/pcg64.rs#L99-L105 + */ function fromStateIncr(state, inc) { + const pcg = { + state, + inc + }; + // Move away from initial value + pcg.state = BigInt.asUintN(64, state + inc); + step(pcg); + return pcg; +} +/** + * Internal PCG32 implementation, used by both the public seeded random + * function and the seed generation algorithm. + * + * Modified from https://github.com/rust-random/rand/blob/f7bbccaedf6c63b02855b90b003c9b1a4d1fd1cb/rand_pcg/src/pcg64.rs#L140-L153 + * + * `pcg.state` is internally advanced by the function. + * + * @param pcg The state and increment values to use for the PCG32 algorithm. + * @returns The next pseudo-random 32-bit integer. + */ export function nextU32(pcg) { + const state = pcg.state; + step(pcg); + // Output function XSH RR: xorshift high (bits), followed by a random rotate + const rot = state >> ROTATE; + const xsh = BigInt.asUintN(32, (state >> XSHIFT ^ state) >> SPARE); + return Number(rotateRightU32(xsh, rot)); +} +// `n`, `rot`, and return val are all u32 +function rotateRightU32(n, rot) { + const left = BigInt.asUintN(32, n << (-rot & 31n)); + const right = n >> rot; + return left | right; +} +/** + * Convert a scalar bigint seed to a Uint8Array of the specified length. + * Modified from https://github.com/rust-random/rand/blob/f7bbccaedf6c63b02855b90b003c9b1a4d1fd1cb/rand_core/src/lib.rs#L359-L388 + */ export function seedFromU64(state, numBytes) { + const seed = new Uint8Array(numBytes); + const pgc = { + state: BigInt.asUintN(64, state), + inc: INC + }; + // We advance the state first (to get away from the input value, + // in case it has low Hamming Weight). + step(pgc); + for(let i = 0; i < Math.floor(numBytes / 4); ++i){ + new DataView(seed.buffer).setUint32(i * 4, nextU32(pgc), true); + } + const rem = numBytes % 4; + if (rem) { + const bytes = new Uint8Array(4); + new DataView(bytes.buffer).setUint32(0, nextU32(pgc), true); + seed.set(bytes.subarray(0, rem), numBytes - rem); + } + return seed; +} diff --git a/test/output/build/jsr/_jsr/@std/random@0.1.0/between.2ce008c6.js b/test/output/build/jsr/_jsr/@std/random@0.1.0/between.2ce008c6.js new file mode 100644 index 000000000..e6abbdb5a --- /dev/null +++ b/test/output/build/jsr/_jsr/@std/random@0.1.0/between.2ce008c6.js @@ -0,0 +1,36 @@ +// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. +// This module is browser compatible. +/** + * Generates a random number between the provided minimum and maximum values. + * + * The number is in the range `[min, max)`, i.e. `min` is included but `max` is excluded. + * + * @experimental **UNSTABLE**: New API, yet to be vetted. + * + * @param min The minimum value (inclusive) + * @param max The maximum value (exclusive) + * @param options The options for the random number generator + * @returns A random number between the provided minimum and maximum values + * + * @example Usage + * ```ts no-assert + * import { randomBetween } from "@std/random"; + * + * randomBetween(1, 10); // 6.688009464410508 + * randomBetween(1, 10); // 3.6267118101712006 + * randomBetween(1, 10); // 7.853320239013774 + * ``` + */ export function randomBetween(min, max, options) { + if (!Number.isFinite(min)) { + throw new RangeError(`Cannot generate a random number: min cannot be ${min}`); + } + if (!Number.isFinite(max)) { + throw new RangeError(`Cannot generate a random number: max cannot be ${max}`); + } + if (max < min) { + throw new RangeError(`Cannot generate a random number as max must be greater than or equal to min: max=${max}, min=${min}`); + } + const x = (options?.prng ?? Math.random)(); + const y = min * (1 - x) + max * x; + return y >= min && y < max ? y : min; +} diff --git a/test/output/build/jsr/_jsr/@std/random@0.1.0/integer_between.4528767d.js b/test/output/build/jsr/_jsr/@std/random@0.1.0/integer_between.4528767d.js new file mode 100644 index 000000000..3f00f1eb9 --- /dev/null +++ b/test/output/build/jsr/_jsr/@std/random@0.1.0/integer_between.4528767d.js @@ -0,0 +1,26 @@ +// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. +// This module is browser compatible. +import { randomBetween } from "./between.2ce008c6.js"; +/** + * Generates a random integer between the provided minimum and maximum values. + * + * The number is in the range `[min, max]`, i.e. both `min` and `max` are included. + * + * @experimental **UNSTABLE**: New API, yet to be vetted. + * + * @param min The minimum value (inclusive) + * @param max The maximum value (inclusive) + * @param options The options for the random number generator + * @returns A random integer between the provided minimum and maximum values + * + * @example Usage + * ```ts no-assert + * import { randomIntegerBetween } from "@std/random"; + * + * randomIntegerBetween(1, 10); // 7 + * randomIntegerBetween(1, 10); // 9 + * randomIntegerBetween(1, 10); // 2 + * ``` + */ export function randomIntegerBetween(min, max, options) { + return Math.floor(randomBetween(Math.ceil(min), Math.floor(max) + 1, options)); +} diff --git a/test/output/build/jsr/_jsr/@std/random@0.1.0/mod.e50195f4.js b/test/output/build/jsr/_jsr/@std/random@0.1.0/mod.e50195f4.js new file mode 100644 index 000000000..a36b4b4b1 --- /dev/null +++ b/test/output/build/jsr/_jsr/@std/random@0.1.0/mod.e50195f4.js @@ -0,0 +1,23 @@ +// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. +// This module is browser compatible. +/** + * Utilities for generating random numbers. + * + * ```ts + * import { randomIntegerBetween } from "@std/random"; + * import { randomSeeded } from "@std/random"; + * import { assertEquals } from "@std/assert"; + * + * const prng = randomSeeded(1n); + * + * assertEquals(randomIntegerBetween(1, 10, { prng }), 3); + * ``` + * + * @experimental **UNSTABLE**: New API, yet to be vetted. + * + * @module + */ export * from "./between.2ce008c6.js"; +export * from "./integer_between.4528767d.js"; +export * from "./sample.6e7cf133.js"; +export * from "./seeded.4e59f274.js"; +export * from "./shuffle.0ef8dd95.js"; diff --git a/test/output/build/jsr/_jsr/@std/random@0.1.0/sample.6e7cf133.js b/test/output/build/jsr/_jsr/@std/random@0.1.0/sample.6e7cf133.js new file mode 100644 index 000000000..c0e30e7d1 --- /dev/null +++ b/test/output/build/jsr/_jsr/@std/random@0.1.0/sample.6e7cf133.js @@ -0,0 +1,64 @@ +// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. +// This module is browser compatible. +import { randomIntegerBetween } from "./integer_between.4528767d.js"; +/** + * Returns a random element from the given array. + * + * @experimental **UNSTABLE**: New API, yet to be vetted. + * + * @typeParam T The type of the elements in the array. + * @typeParam O The type of the accumulator. + * + * @param array The array to sample from. + * @param options Options modifying the sampling behavior. + * + * @returns A random element from the given array, or `undefined` if the array + * is empty. + * + * @example Basic usage + * ```ts + * import { sample } from "@std/random/sample"; + * import { assertArrayIncludes } from "@std/assert"; + * + * const numbers = [1, 2, 3, 4]; + * const sampled = sample(numbers); + * + * assertArrayIncludes(numbers, [sampled]); + * ``` + * + * @example Using `weights` option + * ```ts no-assert + * import { sample } from "@std/random/sample"; + * + * const values = ["a", "b", "c"]; + * const weights = [5, 3, 2]; + * const result = sample(values, { weights }); + * // gives "a" 50% of the time, "b" 30% of the time, and "c" 20% of the time + * ``` + */ export function sample(array, options) { + const { weights } = { + ...options + }; + if (weights) { + if (weights.length !== array.length) { + throw new RangeError("Cannot sample an item: The length of the weights array must match the length of the input array"); + } + if (!array.length) return undefined; + const total = Object.values(weights).reduce((sum, n)=>sum + n, 0); + if (total <= 0) { + throw new RangeError("Cannot sample an item: Total weight must be greater than 0"); + } + const rand = (options?.prng ?? Math.random)() * total; + let current = 0; + for(let i = 0; i < array.length; ++i){ + current += weights[i]; + if (rand < current) { + return array[i]; + } + } + // this line should never be hit, but in case of rounding errors etc. + return array[0]; + } + const length = array.length; + return length ? array[randomIntegerBetween(0, length - 1, options)] : undefined; +} diff --git a/test/output/build/jsr/_jsr/@std/random@0.1.0/seeded.4e59f274.js b/test/output/build/jsr/_jsr/@std/random@0.1.0/seeded.4e59f274.js new file mode 100644 index 000000000..68112d183 --- /dev/null +++ b/test/output/build/jsr/_jsr/@std/random@0.1.0/seeded.4e59f274.js @@ -0,0 +1,36 @@ +// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. +// This module is browser compatible. +import { fromSeed, nextU32, seedFromU64 } from "./_pcg32.b2f4838f.js"; +/** + * Creates a pseudo-random number generator that generates random numbers in + * the range `[0, 1)`, based on the given seed. The algorithm used for + * generation is {@link https://www.pcg-random.org/download.html | PCG32}. + * + * @experimental **UNSTABLE**: New API, yet to be vetted. + * + * @param seed The seed used to initialize the random number generator's state. + * @returns A pseudo-random number generator function, which will generate + * different random numbers on each call. + * + * @example Usage + * ```ts + * import { randomSeeded } from "@std/random"; + * import { assertEquals } from "@std/assert"; + * + * const prng = randomSeeded(1n); + * + * assertEquals(prng(), 0.20176767697557807); + * assertEquals(prng(), 0.4911644416861236); + * assertEquals(prng(), 0.7924694607499987); + * ``` + */ export function randomSeeded(seed) { + const pcg = fromSeed(seedFromU64(seed, 16)); + return ()=>uint32ToFloat64(nextU32(pcg)); +} +/** + * Convert a 32-bit unsigned integer to a float64 in the range `[0, 1)`. + * This operation is lossless, i.e. it's always possible to get the original + * value back by multiplying by 2 ** 32. + */ function uint32ToFloat64(u32) { + return u32 / 2 ** 32; +} diff --git a/test/output/build/jsr/_jsr/@std/random@0.1.0/shuffle.0ef8dd95.js b/test/output/build/jsr/_jsr/@std/random@0.1.0/shuffle.0ef8dd95.js new file mode 100644 index 000000000..07667ead1 --- /dev/null +++ b/test/output/build/jsr/_jsr/@std/random@0.1.0/shuffle.0ef8dd95.js @@ -0,0 +1,43 @@ +// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. +// This module is browser compatible. +import { randomIntegerBetween } from "./integer_between.4528767d.js"; +/** + * Shuffles the provided array, returning a copy and without modifying the original array. + * + * @experimental **UNSTABLE**: New API, yet to be vetted. + * + * @typeParam T The type of the items in the array + * @param items The items to shuffle + * @param options The options for the random number generator + * @returns A shuffled copy of the provided items + * + * @example Usage + * ```ts no-assert + * import { shuffle } from "@std/random"; + * + * const items = [1, 2, 3, 4, 5]; + * + * shuffle(items); // [2, 5, 1, 4, 3] + * shuffle(items); // [3, 4, 5, 1, 2] + * shuffle(items); // [5, 2, 4, 3, 1] + * + * items; // [1, 2, 3, 4, 5] (original array is unchanged) + * ``` + */ export function shuffle(items, options) { + const result = [ + ...items + ]; + // https://en.wikipedia.org/wiki/Fisher–Yates_shuffle#The_modern_algorithm + // -- To shuffle an array a of n elements (indices 0..n-1): + // for i from n−1 down to 1 do + for(let i = result.length - 1; i >= 1; --i){ + // j ← random integer such that 0 ≤ j ≤ i + const j = randomIntegerBetween(0, i, options); + // exchange a[j] and a[i] + [result[i], result[j]] = [ + result[j], + result[i] + ]; + } + return result; +} diff --git a/test/output/build/jsr/_observablehq/client.00000001.js b/test/output/build/jsr/_observablehq/client.00000001.js new file mode 100644 index 000000000..e69de29bb diff --git a/test/output/build/jsr/_observablehq/runtime.00000002.js b/test/output/build/jsr/_observablehq/runtime.00000002.js new file mode 100644 index 000000000..e69de29bb diff --git a/test/output/build/jsr/_observablehq/stdlib.00000003.js b/test/output/build/jsr/_observablehq/stdlib.00000003.js new file mode 100644 index 000000000..e69de29bb diff --git a/test/output/build/jsr/_observablehq/theme-air,near-midnight.00000004.css b/test/output/build/jsr/_observablehq/theme-air,near-midnight.00000004.css new file mode 100644 index 000000000..e69de29bb diff --git a/test/output/build/jsr/index.html b/test/output/build/jsr/index.html new file mode 100644 index 000000000..38d52ed02 --- /dev/null +++ b/test/output/build/jsr/index.html @@ -0,0 +1,53 @@ + + + + +Hello JSR + + + + + + + + + + + + + + + + + +
+
+

Hello JSR

+
+
import {randomIntegerBetween, randomSeeded} from "jsr:@std/random";
+
+const prng = randomSeeded(1n);
+
+display(randomIntegerBetween(1, 10, {prng}));
+
+
+ +
From 0139cc17d13f419a1f5843f4b0419183a6b57b91 Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Tue, 17 Sep 2024 18:02:07 -0700 Subject: [PATCH 24/24] remove spurious log --- test/mocks/jsr.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/test/mocks/jsr.ts b/test/mocks/jsr.ts index c9fe86e0d..51fac2083 100644 --- a/test/mocks/jsr.ts +++ b/test/mocks/jsr.ts @@ -27,7 +27,6 @@ export function mockJsr() { const agent = getCurrentAgent(); const npmClient = agent.get("https://npm.jsr.io"); for (const [name, pkg] of packages) { - console.log("mockJsr", name); npmClient .intercept({path: `/@jsr/${name.replace(/^@/, "").replace(/\//, "__")}`, method: "GET"}) .reply(200, pkg, {headers: {"content-type": "application/json; charset=utf-8"}})