Skip to content

Commit 09ce636

Browse files
Adds support for the jsr: protocol (#6752)
## What's the problem this PR addresses? I noticed pnpm/pnpm#9358 and figured it'd make sense to support this in Yarn as well. ## How did you fix it? Yarn will natively understand the `jsr` protocol and automatically reroute the dependency to its `@jsr`-prefixed variant. Still need to add tests. ## Checklist <!--- Don't worry if you miss something, chores are automatically tested. --> <!--- This checklist exists to help you remember doing the chores when you submit a PR. --> <!--- Put an `x` in all the boxes that apply. --> - [x] I have read the [Contributing Guide](https://yarnpkg.com/advanced/contributing). <!-- See https://yarnpkg.com/advanced/contributing#preparing-your-pr-to-be-released for more details. --> <!-- Check with `yarn version check` and fix with `yarn version check -i` --> - [x] I have set the packages that need to be released for my changes to be effective. <!-- The "Testing chores" workflow validates that your PR follows our guidelines. --> <!-- If it doesn't pass, click on it to see details as to what your PR might be missing. --> - [x] I will check that all automated PR checks pass before the PR gets reviewed. --------- Co-authored-by: Luca Casonato <[email protected]>
1 parent ab0afaf commit 09ce636

File tree

14 files changed

+359
-3
lines changed

14 files changed

+359
-3
lines changed

.pnp.cjs

+51
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

.yarn/versions/b82fbe80.yml

+35
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
releases:
2+
"@yarnpkg/cli": minor
3+
"@yarnpkg/core": minor
4+
"@yarnpkg/plugin-jsr": minor
5+
"@yarnpkg/plugin-npm": minor
6+
7+
declined:
8+
- "@yarnpkg/plugin-compat"
9+
- "@yarnpkg/plugin-constraints"
10+
- "@yarnpkg/plugin-dlx"
11+
- "@yarnpkg/plugin-essentials"
12+
- "@yarnpkg/plugin-exec"
13+
- "@yarnpkg/plugin-file"
14+
- "@yarnpkg/plugin-git"
15+
- "@yarnpkg/plugin-github"
16+
- "@yarnpkg/plugin-http"
17+
- "@yarnpkg/plugin-init"
18+
- "@yarnpkg/plugin-interactive-tools"
19+
- "@yarnpkg/plugin-link"
20+
- "@yarnpkg/plugin-nm"
21+
- "@yarnpkg/plugin-npm-cli"
22+
- "@yarnpkg/plugin-pack"
23+
- "@yarnpkg/plugin-patch"
24+
- "@yarnpkg/plugin-pnp"
25+
- "@yarnpkg/plugin-pnpm"
26+
- "@yarnpkg/plugin-stage"
27+
- "@yarnpkg/plugin-typescript"
28+
- "@yarnpkg/plugin-version"
29+
- "@yarnpkg/plugin-workspace-tools"
30+
- "@yarnpkg/builder"
31+
- "@yarnpkg/doctor"
32+
- "@yarnpkg/extensions"
33+
- "@yarnpkg/nm"
34+
- "@yarnpkg/pnpify"
35+
- "@yarnpkg/sdks"

packages/acceptance-tests/pkg-tests-core/sources/utils/tests.ts

+10
Original file line numberDiff line numberDiff line change
@@ -423,6 +423,16 @@ export const startPackageServer = ({type}: {type: keyof typeof packageServerUrls
423423
const {scope, localName, version} = parsedRequest;
424424
const name = scope ? `${scope}/${localName}` : localName;
425425

426+
if (parsedRequest.registry === `jsr` && scope !== `jsr`) {
427+
processError(response, 404, `Package not found: ${name}`);
428+
return;
429+
}
430+
431+
if (parsedRequest.registry !== `jsr` && scope === `jsr`) {
432+
processError(response, 404, `Package not found: ${name}`);
433+
return;
434+
}
435+
426436
const packageEntry = await getPackageEntry(name);
427437
if (!packageEntry) {
428438
processError(response, 404, `Package not found: ${name}`);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
module.exports = require(`./package.json`);
2+
3+
for (const key of [`dependencies`, `devDependencies`, `peerDependencies`]) {
4+
for (const dep of Object.keys(module.exports[key] || {})) {
5+
module.exports[key][dep] = require(dep);
6+
}
7+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"name": "@jsr/no-deps-jsr",
3+
"version": "1.0.0"
4+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
import {ppath, xfs} from '@yarnpkg/fslib';
2+
import {fs as fsUtils} from 'pkg-tests-core';
3+
import {tests} from 'pkg-tests-core';
4+
5+
describe(`Protocols`, () => {
6+
describe(`jsr:`, () => {
7+
test(
8+
`it should allow installing a package from a jsr registry`,
9+
makeTemporaryEnv(
10+
{
11+
dependencies: {[`no-deps-jsr`]: `jsr:1.0.0`},
12+
},
13+
async ({path, run, source}) => {
14+
await xfs.writeFilePromise(ppath.join(path, `.yarnrc.yml`), JSON.stringify({
15+
[`npmScopes`]: {
16+
[`jsr`]: {
17+
[`npmRegistryServer`]: `${await tests.startPackageServer()}/registry/jsr`,
18+
},
19+
},
20+
}));
21+
22+
await run(`install`);
23+
24+
await expect(source(`require('no-deps-jsr')`)).resolves.toMatchObject({
25+
// The package name is prefixed with @jsr/ because that's what the registry returns
26+
name: `@jsr/no-deps-jsr`,
27+
version: `1.0.0`,
28+
});
29+
},
30+
),
31+
);
32+
33+
test(
34+
`it should allow renaming packages`,
35+
makeTemporaryEnv(
36+
{
37+
dependencies: {[`foo`]: `jsr:[email protected]`},
38+
},
39+
async ({path, run, source}) => {
40+
await xfs.writeFilePromise(ppath.join(path, `.yarnrc.yml`), JSON.stringify({
41+
[`npmScopes`]: {
42+
[`jsr`]: {
43+
[`npmRegistryServer`]: `${await tests.startPackageServer()}/registry/jsr`,
44+
},
45+
},
46+
}));
47+
48+
await run(`install`);
49+
50+
await expect(source(`require('foo')`)).resolves.toMatchObject({
51+
name: `@jsr/no-deps-jsr`,
52+
version: `1.0.0`,
53+
});
54+
},
55+
),
56+
);
57+
58+
test(
59+
`it should replace the jsr registry with a npm registry during packing`,
60+
makeTemporaryEnv(
61+
{
62+
dependencies: {[`no-deps-jsr`]: `jsr:1.0.0`},
63+
},
64+
async ({path, run, source}) => {
65+
await xfs.writeFilePromise(ppath.join(path, `.yarnrc.yml`), JSON.stringify({
66+
[`npmScopes`]: {
67+
[`jsr`]: {
68+
[`npmRegistryServer`]: `${await tests.startPackageServer()}/registry/jsr`,
69+
},
70+
},
71+
}));
72+
73+
await run(`install`);
74+
await run(`pack`);
75+
76+
const tarballPath = ppath.join(path, `package.tgz`);
77+
const unpackedPath = ppath.join(path, `unpacked`);
78+
79+
await xfs.mkdirPromise(unpackedPath);
80+
await fsUtils.unpackToDirectory(unpackedPath, tarballPath);
81+
82+
const manifest = await xfs.readJsonPromise(ppath.join(unpackedPath, `package`, `package.json`));
83+
84+
expect(manifest).toMatchObject({
85+
dependencies: {
86+
[`no-deps-jsr`]: `npm:@jsr/[email protected]`,
87+
},
88+
});
89+
},
90+
),
91+
);
92+
});
93+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
---
2+
category: protocols
3+
slug: /protocol/jsr
4+
title: "JSR Protocol"
5+
description: How JSR dependencies work in Yarn.
6+
---
7+
8+
The `jsr:` protocol fetches packages from the [JSR registry](https://jsr.io/).
9+
10+
```
11+
yarn add @luca/flag@jsr:2.0.0
12+
```
13+
14+
Note that because the JSR registry is responsible for compiling packages from TypeScript to JavaScript they sometimes re-pack packages. As a result, the Yarn lockfile contains the full tarball URLs.
15+
16+
Quoting the [JSR documentation](https://jsr.io/docs/npm-compatibility):
17+
18+
> The specific tarballs advertised for a given version of a package may change over time, even if the version itself is not changed. This is because the JSR registry may re-generate npm compatible tarballs for a package version to fix compatibility issues with npm or improve the transpile output in the generated tarball. We refer to this as the “revision” of a tarball. The revision of a tarball is not advertised in the npm registry endpoint, but it is included in the URL of the tarball itself and is included in the `package.json` file in the tarball at the `_jsr_revision` field. The revision of a tarball is not considered part of the package version, and does not affect semver resolution.
19+
>
20+
> However, tarball URLs are immutable. Tools that have a reference to a specific tarball URL will always be able to download that exact tarball. When a new revision of a tarball is generated, the old tarball is not deleted and will continue to be available at the same URL. The new tarball will be available at a new URL that includes the new revision.
21+
>
22+
> Because the tarball URL is included in package manager lock files, running `npm i` / `yarn` / `pnpm i` will never accidentally download a new revision of the tarball.

packages/plugin-jsr/README.md

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# `@yarnpkg/plugin-jsr`
2+
3+
This plugin adds support for the `jsr:` protocol.
4+
5+
## Install
6+
7+
This plugin is included by default in Yarn.

packages/plugin-jsr/package.json

+43
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
{
2+
"name": "@yarnpkg/plugin-jsr",
3+
"version": "1.0.0",
4+
"license": "BSD-2-Clause",
5+
"main": "./sources/index.ts",
6+
"exports": {
7+
".": "./sources/index.ts",
8+
"./package.json": "./package.json"
9+
},
10+
"dependencies": {
11+
"@yarnpkg/fslib": "workspace:^",
12+
"tslib": "^2.4.0"
13+
},
14+
"peerDependencies": {
15+
"@yarnpkg/core": "workspace:^"
16+
},
17+
"devDependencies": {
18+
"@yarnpkg/core": "workspace:^"
19+
},
20+
"repository": {
21+
"type": "git",
22+
"url": "git+https://github.com/yarnpkg/berry.git",
23+
"directory": "packages/plugin-jsr"
24+
},
25+
"scripts": {
26+
"postpack": "rm -rf lib",
27+
"prepack": "run build:compile \"$(pwd)\""
28+
},
29+
"publishConfig": {
30+
"main": "./lib/index.js",
31+
"exports": {
32+
".": "./lib/index.js",
33+
"./package.json": "./package.json"
34+
}
35+
},
36+
"files": [
37+
"/lib/**/*"
38+
],
39+
"engines": {
40+
"node": ">=18.12.0"
41+
},
42+
"stableVersion": "1.0.0"
43+
}

packages/plugin-jsr/sources/index.ts

+54
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import {Descriptor, Locator, Plugin, Project, ResolveOptions, Resolver, Workspace} from '@yarnpkg/core';
2+
import {structUtils, semverUtils} from '@yarnpkg/core';
3+
4+
function normalizeJsrDependency(dependency: Descriptor) {
5+
if (semverUtils.validRange(dependency.range.slice(4)))
6+
return structUtils.makeDescriptor(dependency, `npm:${structUtils.wrapIdentIntoScope(dependency, `jsr`)}@${dependency.range.slice(4)}`);
7+
8+
const parsedRange = structUtils.tryParseDescriptor(dependency.range.slice(4), true);
9+
if (parsedRange !== null)
10+
return structUtils.makeDescriptor(dependency, `npm:${structUtils.wrapIdentIntoScope(parsedRange, `jsr`)}@${parsedRange.range}`);
11+
12+
13+
return dependency;
14+
}
15+
16+
function reduceDependency(dependency: Descriptor, project: Project, locator: Locator, initialDependency: Descriptor, {resolver, resolveOptions}: {resolver: Resolver, resolveOptions: ResolveOptions}) {
17+
return dependency.range.startsWith(`jsr:`)
18+
? normalizeJsrDependency(dependency)
19+
: dependency;
20+
}
21+
22+
const DEPENDENCY_TYPES = [`dependencies`, `devDependencies`, `peerDependencies`];
23+
24+
function beforeWorkspacePacking(workspace: Workspace, rawManifest: any) {
25+
for (const dependencyType of DEPENDENCY_TYPES) {
26+
for (const descriptor of workspace.manifest.getForScope(dependencyType).values()) {
27+
if (!descriptor.range.startsWith(`jsr:`))
28+
continue;
29+
30+
const normalizedDescriptor = normalizeJsrDependency(descriptor);
31+
32+
// Ensure optional dependencies are handled as well
33+
const identDescriptor = dependencyType === `dependencies`
34+
? structUtils.makeDescriptor(descriptor, `unknown`)
35+
: null;
36+
37+
const finalDependencyType = identDescriptor !== null && workspace.manifest.ensureDependencyMeta(identDescriptor).optional
38+
? `optionalDependencies`
39+
: dependencyType;
40+
41+
rawManifest[finalDependencyType][structUtils.stringifyIdent(descriptor)] = normalizedDescriptor.range;
42+
}
43+
}
44+
}
45+
46+
const plugin: Plugin = {
47+
hooks: {
48+
reduceDependency,
49+
beforeWorkspacePacking,
50+
},
51+
};
52+
53+
// eslint-disable-next-line arca/no-default-export
54+
export default plugin;

packages/plugin-npm/sources/npmConfigUtils.ts

+10-3
Original file line numberDiff line numberDiff line change
@@ -64,17 +64,24 @@ export function getRegistryConfiguration(registry: string, {configuration}: {con
6464
return null;
6565
}
6666

67+
const JSR_DEFAULT_SCOPE_CONFIGURATION = new Map([
68+
[`npmRegistryServer`, `https://npm.jsr.io/`],
69+
]);
70+
6771
export function getScopeConfiguration(scope: string | null, {configuration}: {configuration: Configuration}): MapLike | null {
6872
if (scope === null)
6973
return null;
7074

7175
const scopeConfigurations = configuration.get(`npmScopes`);
7276

7377
const scopeConfiguration = scopeConfigurations.get(scope);
74-
if (!scopeConfiguration)
75-
return null;
78+
if (scopeConfiguration)
79+
return scopeConfiguration;
7680

77-
return scopeConfiguration;
81+
if (scope === `jsr`)
82+
return JSR_DEFAULT_SCOPE_CONFIGURATION;
83+
84+
return null;
7885
}
7986

8087
export function getAuthConfiguration(registry: string, {configuration, ident}: {configuration: Configuration, ident?: Ident}): MapLike {

packages/yarnpkg-cli/package.json

+2
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
"@yarnpkg/plugin-http": "workspace:^",
2525
"@yarnpkg/plugin-init": "workspace:^",
2626
"@yarnpkg/plugin-interactive-tools": "workspace:^",
27+
"@yarnpkg/plugin-jsr": "workspace:^",
2728
"@yarnpkg/plugin-link": "workspace:^",
2829
"@yarnpkg/plugin-nm": "workspace:^",
2930
"@yarnpkg/plugin-npm": "workspace:^",
@@ -87,6 +88,7 @@
8788
"@yarnpkg/plugin-http",
8889
"@yarnpkg/plugin-init",
8990
"@yarnpkg/plugin-interactive-tools",
91+
"@yarnpkg/plugin-jsr",
9092
"@yarnpkg/plugin-link",
9193
"@yarnpkg/plugin-nm",
9294
"@yarnpkg/plugin-npm",

packages/yarnpkg-core/sources/structUtils.ts

+8
Original file line numberDiff line numberDiff line change
@@ -639,6 +639,14 @@ export function stringifyIdent(ident: Ident) {
639639
}
640640
}
641641

642+
export function wrapIdentIntoScope(ident: Ident, scope: string) {
643+
if (ident.scope) {
644+
return `@${scope}/${ident.scope}__${ident.name}`;
645+
} else {
646+
return `@${scope}/${ident.name}`;
647+
}
648+
}
649+
642650
/**
643651
* Returns a string from a descriptor (eg. `@types/lodash@^1.0.0`).
644652
*/

yarn.lock

+13
Original file line numberDiff line numberDiff line change
@@ -5603,6 +5603,7 @@ __metadata:
56035603
"@yarnpkg/plugin-http": "workspace:^"
56045604
"@yarnpkg/plugin-init": "workspace:^"
56055605
"@yarnpkg/plugin-interactive-tools": "workspace:^"
5606+
"@yarnpkg/plugin-jsr": "workspace:^"
56065607
"@yarnpkg/plugin-link": "workspace:^"
56075608
"@yarnpkg/plugin-nm": "workspace:^"
56085609
"@yarnpkg/plugin-npm": "workspace:^"
@@ -6123,6 +6124,18 @@ __metadata:
61236124
languageName: unknown
61246125
linkType: soft
61256126

6127+
"@yarnpkg/plugin-jsr@workspace:^, @yarnpkg/plugin-jsr@workspace:packages/plugin-jsr":
6128+
version: 0.0.0-use.local
6129+
resolution: "@yarnpkg/plugin-jsr@workspace:packages/plugin-jsr"
6130+
dependencies:
6131+
"@yarnpkg/core": "workspace:^"
6132+
"@yarnpkg/fslib": "workspace:^"
6133+
tslib: "npm:^2.4.0"
6134+
peerDependencies:
6135+
"@yarnpkg/core": "workspace:^"
6136+
languageName: unknown
6137+
linkType: soft
6138+
61266139
"@yarnpkg/plugin-link@workspace:^, @yarnpkg/plugin-link@workspace:packages/plugin-link":
61276140
version: 0.0.0-use.local
61286141
resolution: "@yarnpkg/plugin-link@workspace:packages/plugin-link"

0 commit comments

Comments
 (0)