feat: add support for the jsr: protocol (#9358)

close #8941

---------

Co-authored-by: Zoltan Kochan <z@kochan.io>
This commit is contained in:
Khải
2025-04-20 16:51:51 +07:00
committed by GitHub
parent 5d7ba81f77
commit 9c3dd03710
38 changed files with 1133 additions and 80 deletions

View File

@@ -0,0 +1,5 @@
---
"@pnpm/resolving.jsr-specifier-parser": major
---
Initial release.

View File

@@ -0,0 +1,54 @@
---
"@pnpm/config": minor
"@pnpm/normalize-registries": minor
"@pnpm/npm-resolver": minor
"@pnpm/package-requester": minor
"@pnpm/resolve-dependencies": minor
"@pnpm/exportable-manifest": minor
"@pnpm/default-resolver": minor
"@pnpm/resolver-base": minor
"@pnpm/store-controller-types": minor
"pnpm": minor
---
**Added support for installing JSR packages.** You can now install JSR packages using the following syntax:
```
pnpm add jsr:<pkg_name>
```
or with a version range:
```
pnpm add jsr:<pkg_name>@<range>
```
For example, running:
```
pnpm add jsr:@foo/bar
```
will add the following entry to your `package.json`:
```json
{
"dependencies": {
"@foo/bar": "jsr:^0.1.2"
}
}
```
When publishing, this entry will be transformed into a format compatible with npm, older versions of Yarn, and previous pnpm versions:
```json
{
"dependencies": {
"@foo/bar": "npm:@jsr/foo__bar@^0.1.2"
}
}
```
Related issue: [#8941](https://github.com/pnpm/pnpm/issues/8941).
Note: The `@jsr` scope defaults to <https://npm.jsr.io/> if the `@jsr:registry` setting is not defined.

View File

@@ -242,7 +242,10 @@ export async function getConfig (opts: {
? pnpmConfig.rawLocalConfig['user-agent']
: `${packageManager.name}/${packageManager.version} npm/? node/${process.version} ${process.platform} ${process.arch}`
pnpmConfig.rawConfig = Object.assign.apply(Object, [
{ registry: 'https://registry.npmjs.org/' },
{
registry: 'https://registry.npmjs.org/',
'@jsr:registry': 'https://npm.jsr.io/',
},
...[...npmConfig.list].reverse(),
cliOptions,
{ 'user-agent': pnpmConfig.userAgent },

View File

@@ -204,6 +204,7 @@ test('registries of scoped packages are read and normalized', async () => {
expect(config.registries).toStrictEqual({
default: 'https://default.com/',
'@jsr': 'https://npm.jsr.io/',
'@foo': 'https://foo.com/',
'@bar': 'https://bar.com/',
'@qar': 'https://qar.com/qar',
@@ -227,6 +228,7 @@ test('registries in current directory\'s .npmrc have bigger priority then global
expect(config.registries).toStrictEqual({
default: 'https://pnpm.io/',
'@jsr': 'https://npm.jsr.io/',
'@foo': 'https://foo.com/',
'@bar': 'https://bar.com/',
'@qar': 'https://qar.com/qar',

View File

@@ -2,8 +2,9 @@ import { type Registries } from '@pnpm/types'
import normalizeRegistryUrl from 'normalize-registry-url'
import mapValues from 'ramda/src/map'
export const DEFAULT_REGISTRIES = {
export const DEFAULT_REGISTRIES: Registries = {
default: 'https://registry.npmjs.org/',
'@jsr': 'https://npm.jsr.io/',
}
export function normalizeRegistries (registries?: Record<string, string>): Registries {

View File

@@ -126,6 +126,7 @@
"logstream",
"longlink",
"longpaths",
"luca",
"martensson",
"maxtimeout",
"mdast",

View File

@@ -162,6 +162,7 @@ async function resolveAndFetch (
let latest: string | undefined
let manifest: DependencyManifest | undefined
let specifier: string | undefined
let alias: string | undefined
let resolution = options.currentPkg?.resolution as Resolution
let pkgId = options.currentPkg?.id
const skipResolution = resolution && !options.update
@@ -209,6 +210,7 @@ async function resolveAndFetch (
resolution = resolveResult.resolution
pkgId = resolveResult.id
specifier = resolveResult.specifier
alias = resolveResult.alias
}
const id = pkgId!
@@ -226,6 +228,7 @@ async function resolveAndFetch (
resolvedVia,
updated,
specifier,
alias,
},
}
}
@@ -259,6 +262,7 @@ async function resolveAndFetch (
resolvedVia,
updated,
publishedAt,
alias,
},
}
}
@@ -295,6 +299,7 @@ async function resolveAndFetch (
resolvedVia,
updated,
publishedAt,
alias,
},
fetching: fetchResult.fetching,
filesIndexFile: fetchResult.filesIndexFile,

View File

@@ -0,0 +1,218 @@
import path from 'path'
import { type LockfileFile } from '@pnpm/lockfile.types'
import { add } from '@pnpm/plugin-commands-installation'
import { prepare } from '@pnpm/prepare'
import { type ProjectManifest } from '@pnpm/types'
import { sync as loadJsonFile } from 'load-json-file'
import { DEFAULT_OPTS } from './utils'
// This must be a function because some of its values depend on CWD
const createOptions = (jsr: string = 'https://npm.jsr.io/') => ({
...DEFAULT_OPTS,
rawConfig: {
...DEFAULT_OPTS.rawConfig,
'@jsr:registry': jsr,
},
registries: {
...DEFAULT_OPTS.registries,
'@jsr': jsr,
},
dir: process.cwd(),
cacheDir: path.resolve('cache'),
storeDir: path.resolve('store'),
})
test('pnpm add jsr:@<scope>/<name>', async () => {
const project = prepare({
name: 'test-add-jsr',
version: '0.0.0',
private: true,
})
await add.handler(createOptions(), ['jsr:@pnpm-e2e/foo'])
expect(loadJsonFile('package.json')).toMatchObject({
dependencies: {
'@pnpm-e2e/foo': 'jsr:^0.1.0',
},
} as ProjectManifest)
expect(project.readLockfile()).toMatchObject({
importers: {
'.': {
dependencies: {
'@pnpm-e2e/foo': {
specifier: 'jsr:^0.1.0',
version: '@jsr/pnpm-e2e__foo@0.1.0',
},
},
},
},
packages: {
'@jsr/pnpm-e2e__foo@0.1.0': {
resolution: {
integrity: expect.any(String),
},
},
},
snapshots: {
'@jsr/pnpm-e2e__foo@0.1.0': expect.any(Object),
},
} as Partial<LockfileFile>)
})
test('pnpm add jsr:@<scope>/<name>@latest', async () => {
const project = prepare({
name: 'test-add-jsr',
version: '0.0.0',
private: true,
})
await add.handler(createOptions(), ['jsr:@pnpm-e2e/foo@latest'])
expect(loadJsonFile('package.json')).toMatchObject({
dependencies: {
'@pnpm-e2e/foo': 'jsr:^0.1.0',
},
} as ProjectManifest)
expect(project.readLockfile()).toMatchObject({
importers: {
'.': {
dependencies: {
'@pnpm-e2e/foo': {
specifier: 'jsr:^0.1.0',
version: '@jsr/pnpm-e2e__foo@0.1.0',
},
},
},
},
packages: {
'@jsr/pnpm-e2e__foo@0.1.0': {
resolution: {
integrity: expect.any(String),
},
},
},
snapshots: {
'@jsr/pnpm-e2e__foo@0.1.0': expect.any(Object),
},
} as Partial<LockfileFile>)
})
test('pnpm add jsr:@<scope>/<name>@<version_selector>', async () => {
const project = prepare({
name: 'test-add-jsr',
version: '0.0.0',
private: true,
})
await add.handler(createOptions(), ['jsr:@pnpm-e2e/foo@0.1'])
expect(loadJsonFile('package.json')).toMatchObject({
dependencies: {
'@pnpm-e2e/foo': 'jsr:~0.1.0',
},
} as ProjectManifest)
expect(project.readLockfile()).toMatchObject({
importers: {
'.': {
dependencies: {
'@pnpm-e2e/foo': {
specifier: 'jsr:~0.1.0',
version: '@jsr/pnpm-e2e__foo@0.1.0',
},
},
},
},
packages: {
'@jsr/pnpm-e2e__foo@0.1.0': {
resolution: {
integrity: expect.any(String),
},
},
},
snapshots: {
'@jsr/pnpm-e2e__foo@0.1.0': expect.any(Object),
},
} as Partial<LockfileFile>)
})
test('pnpm add <alias>@jsr:@<scope>/<name>', async () => {
const project = prepare({
name: 'test-add-jsr',
version: '0.0.0',
private: true,
})
await add.handler(createOptions(), ['foo-from-jsr@jsr:@pnpm-e2e/foo'])
expect(loadJsonFile('package.json')).toMatchObject({
dependencies: {
'foo-from-jsr': 'jsr:@pnpm-e2e/foo@^0.1.0',
},
} as ProjectManifest)
expect(project.readLockfile()).toMatchObject({
importers: {
'.': {
dependencies: {
'foo-from-jsr': {
specifier: 'jsr:@pnpm-e2e/foo@^0.1.0',
version: '@jsr/pnpm-e2e__foo@0.1.0',
},
},
},
},
packages: {
'@jsr/pnpm-e2e__foo@0.1.0': {
resolution: {
integrity: expect.any(String),
},
},
},
snapshots: {
'@jsr/pnpm-e2e__foo@0.1.0': expect.any(Object),
},
} as Partial<LockfileFile>)
})
test('pnpm add <alias>@jsr:@<scope>/<name>@<version_selector>', async () => {
const project = prepare({
name: 'test-add-jsr',
version: '0.0.0',
private: true,
})
await add.handler(createOptions(), ['foo-from-jsr@jsr:@pnpm-e2e/foo@0.1'])
expect(loadJsonFile('package.json')).toMatchObject({
dependencies: {
'foo-from-jsr': 'jsr:@pnpm-e2e/foo@~0.1.0',
},
} as ProjectManifest)
expect(project.readLockfile()).toMatchObject({
importers: {
'.': {
dependencies: {
'foo-from-jsr': {
specifier: 'jsr:@pnpm-e2e/foo@~0.1.0',
version: '@jsr/pnpm-e2e__foo@0.1.0',
},
},
},
},
packages: {
'@jsr/pnpm-e2e__foo@0.1.0': {
resolution: {
integrity: expect.any(String),
},
},
},
snapshots: {
'@jsr/pnpm-e2e__foo@0.1.0': expect.any(Object),
},
} as Partial<LockfileFile>)
})

View File

@@ -0,0 +1,156 @@
import path from 'path'
import { type LockfileFile } from '@pnpm/lockfile.types'
import { install, update } from '@pnpm/plugin-commands-installation'
import { prepare } from '@pnpm/prepare'
import { addDistTag } from '@pnpm/registry-mock'
import { type ProjectManifest } from '@pnpm/types'
import { sync as loadJsonFile } from 'load-json-file'
import { DEFAULT_OPTS } from '../utils'
// This must be a function because some of its values depend on CWD
const createOptions = (jsr: string = DEFAULT_OPTS.registry) => ({
...DEFAULT_OPTS,
rawConfig: {
...DEFAULT_OPTS.rawConfig,
'@jsr:registry': jsr,
},
registries: {
...DEFAULT_OPTS.registries,
'@jsr': jsr,
},
dir: process.cwd(),
cacheDir: path.resolve('cache'),
storeDir: path.resolve('store'),
})
test('jsr without alias', async () => {
await addDistTag({ package: '@jsr/pnpm-e2e__bar', version: '2.0.0', distTag: 'latest' })
const project = prepare({
dependencies: {
'@pnpm-e2e/bar': 'jsr:1.0.0',
},
})
await install.handler(createOptions())
expect(project.readLockfile()).toMatchObject({
importers: {
'.': {
dependencies: {
'@pnpm-e2e/bar': {
specifier: 'jsr:1.0.0',
version: '@jsr/pnpm-e2e__bar@1.0.0',
},
},
},
},
packages: {
'@jsr/pnpm-e2e__bar@1.0.0': {
resolution: {
integrity: expect.any(String),
},
},
},
snapshots: {
'@jsr/pnpm-e2e__bar@1.0.0': expect.any(Object),
},
} as Partial<LockfileFile>)
await update.handler({
...createOptions(),
latest: true,
})
expect(loadJsonFile('package.json')).toMatchObject({
dependencies: {
'@pnpm-e2e/bar': 'jsr:2.0.0',
},
} as Partial<ProjectManifest>)
expect(project.readLockfile()).toMatchObject({
importers: {
'.': {
dependencies: {
'@pnpm-e2e/bar': {
specifier: 'jsr:2.0.0',
version: '@jsr/pnpm-e2e__bar@2.0.0',
},
},
},
},
packages: {
'@jsr/pnpm-e2e__bar@2.0.0': {
resolution: {
integrity: expect.any(String),
},
},
},
snapshots: {
'@jsr/pnpm-e2e__bar@2.0.0': expect.any(Object),
},
} as Partial<LockfileFile>)
})
test('jsr with alias', async () => {
await addDistTag({ package: '@jsr/pnpm-e2e__bar', version: '2.0.0', distTag: 'latest' })
const project = prepare({
dependencies: {
'bar-from-jsr': 'jsr:@pnpm-e2e/bar@1.0.0',
},
})
await install.handler(createOptions())
expect(project.readLockfile()).toMatchObject({
importers: {
'.': {
dependencies: {
'bar-from-jsr': {
specifier: 'jsr:@pnpm-e2e/bar@1.0.0',
version: '@jsr/pnpm-e2e__bar@1.0.0',
},
},
},
},
packages: {
'@jsr/pnpm-e2e__bar@1.0.0': {
resolution: {
integrity: expect.any(String),
},
},
},
snapshots: {
'@jsr/pnpm-e2e__bar@1.0.0': expect.any(Object),
},
} as Partial<LockfileFile>)
await update.handler({
...createOptions(),
latest: true,
})
expect(loadJsonFile('package.json')).toMatchObject({
dependencies: {
'bar-from-jsr': 'jsr:@pnpm-e2e/bar@2.0.0',
},
} as Partial<ProjectManifest>)
expect(project.readLockfile()).toMatchObject({
importers: {
'.': {
dependencies: {
'bar-from-jsr': {
specifier: 'jsr:@pnpm-e2e/bar@2.0.0',
version: '@jsr/pnpm-e2e__bar@2.0.0',
},
},
},
},
packages: {
'@jsr/pnpm-e2e__bar@2.0.0': {
resolution: {
integrity: expect.any(String),
},
},
},
snapshots: {
'@jsr/pnpm-e2e__bar@2.0.0': expect.any(Object),
},
} as Partial<LockfileFile>)
})

View File

@@ -1351,7 +1351,7 @@ async function resolveDependency (
throw new PnpmError('MISSING_PACKAGE_JSON', `Can't install ${wantedDependency.pref}: Missing package.json file`)
}
return {
alias: wantedDependency.alias || pkgResponse.body.manifest.name || path.basename(pkgResponse.body.resolution.directory),
alias: wantedDependency.alias ?? pkgResponse.body.alias ?? pkgResponse.body.manifest.name ?? path.basename(pkgResponse.body.resolution.directory),
dev: wantedDependency.dev,
isLinkedDependency: true,
name: pkgResponse.body.manifest.name,
@@ -1527,7 +1527,7 @@ async function resolveDependency (
ctx.dependenciesTree.get(nodeId)!.depth = Math.min(ctx.dependenciesTree.get(nodeId)!.depth, options.currentDepth)
} else {
ctx.pendingNodes.push({
alias: wantedDependency.alias || pkg.name,
alias: wantedDependency.alias ?? pkgResponse.body.alias ?? pkg.name,
depth: options.currentDepth,
parentIds: options.parentIds,
installable,
@@ -1566,7 +1566,7 @@ async function resolveDependency (
}
}
return {
alias: wantedDependency.alias || pkg.name,
alias: wantedDependency.alias ?? pkgResponse.body.alias ?? pkg.name,
depIsLinked,
resolvedVia: pkgResponse.body.resolvedVia,
isNew,

View File

@@ -33,6 +33,7 @@
"@pnpm/catalogs.resolver": "workspace:*",
"@pnpm/error": "workspace:*",
"@pnpm/read-project-manifest": "workspace:*",
"@pnpm/resolving.jsr-specifier-parser": "workspace:*",
"@pnpm/types": "workspace:*",
"p-map-values": "catalog:",
"ramda": "catalog:"

View File

@@ -2,6 +2,7 @@ import path from 'path'
import { type CatalogResolver, resolveFromCatalog } from '@pnpm/catalogs.resolver'
import { type Catalogs } from '@pnpm/catalogs.types'
import { PnpmError } from '@pnpm/error'
import { parseJsrSpecifier } from '@pnpm/resolving.jsr-specifier-parser'
import { tryReadProjectManifest } from '@pnpm/read-project-manifest'
import { type Dependencies, type ProjectManifest } from '@pnpm/types'
import omit from 'ramda/src/omit'
@@ -36,7 +37,7 @@ export async function createExportableManifest (
const catalogResolver = resolveFromCatalog.bind(null, opts.catalogs)
const replaceCatalogProtocol = resolveCatalogProtocol.bind(null, catalogResolver)
const convertDependencyForPublish = combineConverters(replaceWorkspaceProtocol, replaceCatalogProtocol)
const convertDependencyForPublish = combineConverters(replaceWorkspaceProtocol, replaceCatalogProtocol, replaceJsrProtocol)
await Promise.all((['dependencies', 'devDependencies', 'optionalDependencies'] as const).map(async (depsField) => {
const deps = await makePublishDependencies(dir, originalManifest[depsField], {
modulesDir: opts?.modulesDir,
@@ -49,7 +50,7 @@ export async function createExportableManifest (
const peerDependencies = originalManifest.peerDependencies
if (peerDependencies) {
const convertPeersForPublish = combineConverters(replaceWorkspaceProtocolPeerDependency, replaceCatalogProtocol)
const convertPeersForPublish = combineConverters(replaceWorkspaceProtocolPeerDependency, replaceCatalogProtocol, replaceJsrProtocol)
publishManifest.peerDependencies = await makePublishDependencies(dir, peerDependencies, {
modulesDir: opts?.modulesDir,
convertDependencyForPublish: convertPeersForPublish,
@@ -178,3 +179,19 @@ async function replaceWorkspaceProtocolPeerDependency (depName: string, depSpec:
return depSpec.replace('workspace:', '')
}
async function replaceJsrProtocol (depName: string, depSpec: string): Promise<string> {
const spec = parseJsrSpecifier(depSpec, depName)
if (spec == null) {
return depSpec
}
return createNpmAliasedSpecifier(spec.npmPkgName, spec.versionSelector)
}
function createNpmAliasedSpecifier (npmPkgName: string, versionSelector?: string): string {
const npmPkgSpecifier = `npm:${npmPkgName}`
if (!versionSelector) {
return npmPkgSpecifier
}
return `${npmPkgSpecifier}@${versionSelector}`
}

View File

@@ -165,7 +165,7 @@ test('workspace deps are replaced', async () => {
})
})
test('catalog deps are replace', async () => {
test('catalog deps are replaced', async () => {
const catalogProtocolPackageManifest: ProjectManifest = {
name: 'catalog-protocol-package',
version: '1.0.0',
@@ -218,3 +218,37 @@ test('catalog deps are replace', async () => {
},
})
})
test('jsr deps are replaced', async () => {
const jsrProtocolPackageManifest = {
name: 'jsr-protocol-manifest',
version: '0.0.0',
dependencies: {
'@foo/bar': 'jsr:^1.0.0',
},
optionalDependencies: {
baz: 'jsr:@foo/baz@3.0',
},
peerDependencies: {
qux: 'jsr:@foo/qux',
},
} satisfies ProjectManifest
preparePackages([jsrProtocolPackageManifest])
process.chdir(jsrProtocolPackageManifest.name)
expect(await createExportableManifest(process.cwd(), jsrProtocolPackageManifest, { catalogs: {} })).toStrictEqual({
name: 'jsr-protocol-manifest',
version: '0.0.0',
dependencies: {
'@foo/bar': 'npm:@jsr/foo__bar@^1.0.0',
},
optionalDependencies: {
baz: 'npm:@jsr/foo__baz@3.0',
},
peerDependencies: {
qux: 'npm:@jsr/foo__qux',
},
} as Partial<typeof jsrProtocolPackageManifest>)
})

View File

@@ -27,6 +27,9 @@
{
"path": "../../packages/types"
},
{
"path": "../../resolving/jsr-specifier-parser"
},
{
"path": "../read-project-manifest"
}

69
pnpm-lock.yaml generated
View File

@@ -55,8 +55,8 @@ catalogs:
specifier: 0.0.1
version: 0.0.1
'@pnpm/registry-mock':
specifier: 4.3.0
version: 4.3.0
specifier: 4.4.0
version: 4.4.0
'@pnpm/semver-diff':
specifier: ^1.1.0
version: 1.1.0
@@ -865,7 +865,7 @@ importers:
version: link:../../pkg-manager/modules-yaml
'@pnpm/registry-mock':
specifier: 'catalog:'
version: 4.3.0(encoding@0.1.13)(typanion@3.14.0)
version: 4.4.0(encoding@0.1.13)(typanion@3.14.0)
'@pnpm/types':
specifier: workspace:*
version: link:../../packages/types
@@ -899,7 +899,7 @@ importers:
dependencies:
'@pnpm/registry-mock':
specifier: 'catalog:'
version: 4.3.0(encoding@0.1.13)(typanion@3.14.0)
version: 4.4.0(encoding@0.1.13)(typanion@3.14.0)
'@pnpm/store.cafs':
specifier: workspace:*
version: link:../../store/cafs
@@ -974,7 +974,7 @@ importers:
dependencies:
'@pnpm/registry-mock':
specifier: 'catalog:'
version: 4.3.0(encoding@0.1.13)(typanion@3.14.0)
version: 4.4.0(encoding@0.1.13)(typanion@3.14.0)
'@pnpm/worker':
specifier: workspace:*
version: link:../../worker
@@ -1160,7 +1160,7 @@ importers:
version: link:../../__utils__/prepare
'@pnpm/registry-mock':
specifier: 'catalog:'
version: 4.3.0(encoding@0.1.13)(typanion@3.14.0)
version: 4.4.0(encoding@0.1.13)(typanion@3.14.0)
'@types/ramda':
specifier: 'catalog:'
version: 0.29.12
@@ -1645,7 +1645,7 @@ importers:
version: link:../../__utils__/prepare
'@pnpm/registry-mock':
specifier: 'catalog:'
version: 4.3.0(encoding@0.1.13)(typanion@3.14.0)
version: 4.4.0(encoding@0.1.13)(typanion@3.14.0)
'@pnpm/testing.temp-store':
specifier: workspace:*
version: link:../../testing/temp-store
@@ -2281,7 +2281,7 @@ importers:
version: link:../../__utils__/prepare
'@pnpm/registry-mock':
specifier: 'catalog:'
version: 4.3.0(encoding@0.1.13)(typanion@3.14.0)
version: 4.4.0(encoding@0.1.13)(typanion@3.14.0)
'@pnpm/types':
specifier: workspace:*
version: link:../../packages/types
@@ -2567,7 +2567,7 @@ importers:
version: link:../../__utils__/prepare
'@pnpm/registry-mock':
specifier: 'catalog:'
version: 4.3.0(encoding@0.1.13)(typanion@3.14.0)
version: 4.4.0(encoding@0.1.13)(typanion@3.14.0)
'@pnpm/test-fixtures':
specifier: workspace:*
version: link:../../__utils__/test-fixtures
@@ -2715,7 +2715,7 @@ importers:
version: link:../../__utils__/prepare
'@pnpm/registry-mock':
specifier: 'catalog:'
version: 4.3.0(encoding@0.1.13)(typanion@3.14.0)
version: 4.4.0(encoding@0.1.13)(typanion@3.14.0)
'@pnpm/test-ipc-server':
specifier: workspace:*
version: link:../../__utils__/test-ipc-server
@@ -4410,7 +4410,7 @@ importers:
version: link:../../__utils__/prepare
'@pnpm/registry-mock':
specifier: 'catalog:'
version: 4.3.0(encoding@0.1.13)(typanion@3.14.0)
version: 4.4.0(encoding@0.1.13)(typanion@3.14.0)
'@pnpm/test-fixtures':
specifier: workspace:*
version: link:../../__utils__/test-fixtures
@@ -4697,7 +4697,7 @@ importers:
version: link:../../pkg-manifest/read-package-json
'@pnpm/registry-mock':
specifier: 'catalog:'
version: 4.3.0(encoding@0.1.13)(typanion@3.14.0)
version: 4.4.0(encoding@0.1.13)(typanion@3.14.0)
'@pnpm/store-path':
specifier: workspace:*
version: link:../../store/store-path
@@ -4970,7 +4970,7 @@ importers:
version: link:../read-projects-context
'@pnpm/registry-mock':
specifier: 'catalog:'
version: 4.3.0(encoding@0.1.13)(typanion@3.14.0)
version: 4.4.0(encoding@0.1.13)(typanion@3.14.0)
'@pnpm/store-path':
specifier: workspace:*
version: link:../../store/store-path
@@ -5333,7 +5333,7 @@ importers:
version: 'link:'
'@pnpm/registry-mock':
specifier: 'catalog:'
version: 4.3.0(encoding@0.1.13)(typanion@3.14.0)
version: 4.4.0(encoding@0.1.13)(typanion@3.14.0)
'@pnpm/test-fixtures':
specifier: workspace:*
version: link:../../__utils__/test-fixtures
@@ -5556,7 +5556,7 @@ importers:
version: link:../../__utils__/prepare
'@pnpm/registry-mock':
specifier: 'catalog:'
version: 4.3.0(encoding@0.1.13)(typanion@3.14.0)
version: 4.4.0(encoding@0.1.13)(typanion@3.14.0)
'@pnpm/test-fixtures':
specifier: workspace:*
version: link:../../__utils__/test-fixtures
@@ -5852,6 +5852,9 @@ importers:
'@pnpm/read-project-manifest':
specifier: workspace:*
version: link:../read-project-manifest
'@pnpm/resolving.jsr-specifier-parser':
specifier: workspace:*
version: link:../../resolving/jsr-specifier-parser
'@pnpm/types':
specifier: workspace:*
version: link:../../packages/types
@@ -6151,7 +6154,7 @@ importers:
version: link:../pkg-manifest/read-project-manifest
'@pnpm/registry-mock':
specifier: 'catalog:'
version: 4.3.0(encoding@0.1.13)(typanion@3.14.0)
version: 4.4.0(encoding@0.1.13)(typanion@3.14.0)
'@pnpm/run-npm':
specifier: workspace:*
version: link:../exec/run-npm
@@ -6464,7 +6467,7 @@ importers:
version: link:../../__utils__/prepare
'@pnpm/registry-mock':
specifier: 'catalog:'
version: 4.3.0(encoding@0.1.13)(typanion@3.14.0)
version: 4.4.0(encoding@0.1.13)(typanion@3.14.0)
'@pnpm/test-fixtures':
specifier: workspace:*
version: link:../../__utils__/test-fixtures
@@ -6588,7 +6591,7 @@ importers:
version: link:../../__utils__/prepare
'@pnpm/registry-mock':
specifier: 'catalog:'
version: 4.3.0(encoding@0.1.13)(typanion@3.14.0)
version: 4.4.0(encoding@0.1.13)(typanion@3.14.0)
'@pnpm/test-ipc-server':
specifier: workspace:*
version: link:../../__utils__/test-ipc-server
@@ -6706,6 +6709,16 @@ importers:
specifier: 'catalog:'
version: 1.0.2
resolving/jsr-specifier-parser:
dependencies:
'@pnpm/error':
specifier: workspace:*
version: link:../../packages/error
devDependencies:
'@pnpm/resolving.jsr-specifier-parser':
specifier: workspace:*
version: 'link:'
resolving/local-resolver:
dependencies:
'@pnpm/crypto.hash':
@@ -6766,6 +6779,9 @@ importers:
'@pnpm/resolver-base':
specifier: workspace:*
version: link:../resolver-base
'@pnpm/resolving.jsr-specifier-parser':
specifier: workspace:*
version: link:../jsr-specifier-parser
'@pnpm/types':
specifier: workspace:*
version: link:../../packages/types
@@ -7187,7 +7203,7 @@ importers:
version: link:../../pkg-manifest/read-package-json
'@pnpm/registry-mock':
specifier: 'catalog:'
version: 4.3.0(encoding@0.1.13)(typanion@3.14.0)
version: 4.4.0(encoding@0.1.13)(typanion@3.14.0)
'@pnpm/test-fixtures':
specifier: workspace:*
version: link:../../__utils__/test-fixtures
@@ -7251,7 +7267,7 @@ importers:
version: link:../../__utils__/prepare
'@pnpm/registry-mock':
specifier: 'catalog:'
version: 4.3.0(encoding@0.1.13)(typanion@3.14.0)
version: 4.4.0(encoding@0.1.13)(typanion@3.14.0)
'@pnpm/workspace.filter-packages-from-dir':
specifier: workspace:*
version: link:../../workspace/filter-packages-from-dir
@@ -7336,7 +7352,7 @@ importers:
version: link:../../__utils__/prepare
'@pnpm/registry-mock':
specifier: 'catalog:'
version: 4.3.0(encoding@0.1.13)(typanion@3.14.0)
version: 4.4.0(encoding@0.1.13)(typanion@3.14.0)
'@pnpm/test-fixtures':
specifier: workspace:*
version: link:../../__utils__/test-fixtures
@@ -7690,7 +7706,7 @@ importers:
version: link:../../__utils__/prepare
'@pnpm/registry-mock':
specifier: 'catalog:'
version: 4.3.0(encoding@0.1.13)(typanion@3.14.0)
version: 4.4.0(encoding@0.1.13)(typanion@3.14.0)
'@types/archy':
specifier: 'catalog:'
version: 0.0.33
@@ -7944,7 +7960,7 @@ importers:
version: link:../../store/package-store
'@pnpm/registry-mock':
specifier: 'catalog:'
version: 4.3.0(encoding@0.1.13)(typanion@3.14.0)
version: 4.4.0(encoding@0.1.13)(typanion@3.14.0)
'@pnpm/store-controller-types':
specifier: workspace:*
version: link:../../store/store-controller-types
@@ -9539,8 +9555,8 @@ packages:
resolution: {integrity: sha512-UY5ZFl8jTgWpPMp3qwVt1z455gDLGh4aAna7ufqsJP9qhI6lr9scFpnEamjpA51Y3MJMBtnML8KATmH6RY+NHQ==}
engines: {node: '>=18.12'}
'@pnpm/registry-mock@4.3.0':
resolution: {integrity: sha512-u2SG2b/yVgeA+Nt+Wvzl6dOFBUx/l6/zCq3vX2VbWOgibTqiJwMY7ks9aXxldyE3Hk8ZZLX5FYcYIKVrC6r1/g==}
'@pnpm/registry-mock@4.4.0':
resolution: {integrity: sha512-OOHciXVfvqt4U70C0w+GyYnT3KrIvLG/1O1OE9fZQRji+/XdhiJd68kfhLhnTvlgi++r3mO1URlKldvi2zV/Yw==}
engines: {node: '>=18.12'}
hasBin: true
@@ -14788,7 +14804,6 @@ packages:
verdaccio@5.20.1:
resolution: {integrity: sha512-zKQXYubQOfl2w09gO9BR7U9ZZkFPPby8tvV+na86/2vGZnY79kNSVnSbK8CM1bpJHTCQ80AGsmIGovg2FgXhdQ==}
engines: {node: '>=12.18'}
deprecated: this version is deprecated, please migrate to 6.x versions
hasBin: true
verror@1.10.0:
@@ -16547,7 +16562,7 @@ snapshots:
read-yaml-file: 2.1.0
strip-bom: 4.0.0
'@pnpm/registry-mock@4.3.0(encoding@0.1.13)(typanion@3.14.0)':
'@pnpm/registry-mock@4.4.0(encoding@0.1.13)(typanion@3.14.0)':
dependencies:
anonymous-npm-registry-client: 0.3.2
execa: 5.1.1

View File

@@ -74,7 +74,7 @@ catalog:
"@pnpm/npm-package-arg": ^1.0.0
"@pnpm/os.env.path-extender": ^2.0.2
"@pnpm/patch-package": 0.0.1
"@pnpm/registry-mock": 4.3.0
"@pnpm/registry-mock": 4.4.0
"@pnpm/semver-diff": ^1.1.0
"@pnpm/tabtab": ^0.5.4
"@pnpm/util.lex-comparator": 3.0.1

View File

@@ -646,7 +646,7 @@ test('preResolution hook', async () => {
expect(ctx.existsCurrentLockfile).toBe(false)
expect(ctx.existsNonEmptyWantedLockfile).toBe(false)
expect(ctx.registries).toEqual({
expect(ctx.registries).toMatchObject({
default: `http://localhost:${REGISTRY_MOCK_PORT}/`,
'@foo': 'https://foo.com/',
})

View File

@@ -24,11 +24,12 @@ export function createResolver (
getAuthHeader: GetAuthHeader,
pnpmOpts: ResolverFactoryOptions
): { resolve: ResolveFunction, clearCache: () => void } {
const { resolveFromNpm, clearCache } = createNpmResolver(fetchFromRegistry, getAuthHeader, pnpmOpts)
const { resolveFromNpm, resolveFromJsr, clearCache } = createNpmResolver(fetchFromRegistry, getAuthHeader, pnpmOpts)
const resolveFromGit = createGitResolver(pnpmOpts)
return {
resolve: async (wantedDependency, opts) => {
const resolution = await resolveFromNpm(wantedDependency, opts as ResolveFromNpmOptions) ??
await resolveFromJsr(wantedDependency, opts as ResolveFromNpmOptions) ??
(wantedDependency.pref && (
await resolveFromTarball(fetchFromRegistry, wantedDependency as { pref: string }) ??
await resolveFromGit(wantedDependency as { pref: string }) ??

View File

@@ -0,0 +1,17 @@
# @pnpm/resolving.jsr-specifier-parser
> Parser of jsr specifiers
<!--@shields('npm')-->
[![npm version](https://img.shields.io/npm/v/@pnpm/resolving.jsr-specifier-parser.svg)](https://www.npmjs.com/package/@pnpm/resolving.jsr-specifier-parser)
<!--/@-->
## Installation
```sh
pnpm add @pnpm/resolving.jsr-specifier-parser
```
## License
MIT

View File

@@ -0,0 +1,45 @@
{
"name": "@pnpm/resolving.jsr-specifier-parser",
"version": "1000.0.0-0",
"description": "Parser of jsr specifiers",
"keywords": [
"pnpm",
"pnpm10",
"jsr"
],
"license": "MIT",
"funding": "https://opencollective.com/pnpm",
"repository": "https://github.com/pnpm/pnpm/blob/main/resolving/jsr-specifier-parser",
"homepage": "https://github.com/pnpm/pnpm/blob/main/resolving/jsr-specifier-parser#readme",
"bugs": {
"url": "https://github.com/pnpm/pnpm/issues"
},
"main": "lib/index.js",
"types": "lib/index.d.ts",
"exports": {
".": "./lib/index.js"
},
"files": [
"lib",
"!*.map"
],
"scripts": {
"lint": "eslint \"src/**/*.ts\" \"test/**/*.ts\"",
"_test": "jest",
"test": "pnpm run compile && pnpm run _test",
"prepublishOnly": "pnpm run compile",
"compile": "tsc --build && pnpm run lint --fix"
},
"dependencies": {
"@pnpm/error": "workspace:*"
},
"devDependencies": {
"@pnpm/resolving.jsr-specifier-parser": "workspace:*"
},
"engines": {
"node": ">=18.12"
},
"jest": {
"preset": "@pnpm/jest-config"
}
}

View File

@@ -0,0 +1,64 @@
import { PnpmError } from '@pnpm/error'
export interface JsrSpec {
jsrPkgName: string
npmPkgName: string
// A versionSelector may be a semver range (e.g. ^1.0.0), exact version (e.g. 2.3.4), or a dist-tag (e.g. "latest").
versionSelector?: string
}
export function parseJsrSpecifier (rawSpecifier: string, alias?: string): JsrSpec | null {
if (!rawSpecifier.startsWith('jsr:')) return null
rawSpecifier = rawSpecifier.substring('jsr:'.length)
// syntax: jsr:@<scope>/<name>[@<version_selector>]
if (rawSpecifier.startsWith('@')) {
const index = rawSpecifier.lastIndexOf('@')
// syntax: jsr:@<scope>/<name>
if (index === 0) {
return {
jsrPkgName: rawSpecifier,
npmPkgName: jsrToNpmPackageName(rawSpecifier),
}
}
// syntax: jsr:@<scope>/<name>@<version_selector>
const jsrPkgName = rawSpecifier.substring(0, index)
return {
jsrPkgName,
npmPkgName: jsrToNpmPackageName(jsrPkgName),
versionSelector: rawSpecifier.substring(index + '@'.length),
}
}
// syntax: jsr:<name>@<version_selector> (invalid)
if (rawSpecifier.includes('@')) {
throw new PnpmError('MISSING_JSR_PACKAGE_SCOPE', 'Package names from JSR must have a scope')
}
if (!alias) {
throw new PnpmError('INVALID_JSR_SPECIFIER', `JSR specifier '${rawSpecifier}' is missing a package name`)
}
// syntax: jsr:<spec>
return {
versionSelector: rawSpecifier,
jsrPkgName: alias,
npmPkgName: jsrToNpmPackageName(alias),
}
}
function jsrToNpmPackageName (jsrPkgName: string): string {
if (!jsrPkgName.startsWith('@')) {
throw new PnpmError('MISSING_JSR_PACKAGE_SCOPE', 'Package names from JSR must have a scope')
}
const sepIndex = jsrPkgName.indexOf('/')
if (sepIndex === -1) {
throw new PnpmError('INVALID_JSR_PACKAGE_NAME', `The package name '${jsrPkgName}' is invalid`)
}
const scope = jsrPkgName.substring(0, sepIndex)
const name = jsrPkgName.substring(sepIndex + '/'.length)
return `@jsr/${scope.substring(1)}__${name}`
}

View File

@@ -0,0 +1,45 @@
import { parseJsrSpecifier, type JsrSpec } from '@pnpm/resolving.jsr-specifier-parser'
describe('parseJsrSpecifier', () => {
test('skips on non-jsr specifiers', () => {
expect(parseJsrSpecifier('^1.0.0')).toBeNull()
expect(parseJsrSpecifier('1.0.0')).toBeNull()
expect(parseJsrSpecifier('latest')).toBeNull()
expect(parseJsrSpecifier('npm:foo')).toBeNull()
expect(parseJsrSpecifier('npm:@foo/bar')).toBeNull()
expect(parseJsrSpecifier('npm:@jsr/foo__bar')).toBeNull()
expect(parseJsrSpecifier('catalog:')).toBeNull()
expect(parseJsrSpecifier('workspace:*')).toBeNull()
})
test('succeeds on jsr specifiers that only specify versions/ranges/tags (jsr:<version_selector>)', () => {
expect(parseJsrSpecifier('jsr:^1.0.0', '@foo/bar')).toStrictEqual({ versionSelector: '^1.0.0', jsrPkgName: '@foo/bar', npmPkgName: '@jsr/foo__bar' } as JsrSpec)
expect(parseJsrSpecifier('jsr:1.0.0', '@foo/bar')).toStrictEqual({ versionSelector: '1.0.0', jsrPkgName: '@foo/bar', npmPkgName: '@jsr/foo__bar' } as JsrSpec)
expect(parseJsrSpecifier('jsr:latest', '@foo/bar')).toStrictEqual({ versionSelector: 'latest', jsrPkgName: '@foo/bar', npmPkgName: '@jsr/foo__bar' } as JsrSpec)
})
test('succeeds on jsr specifiers that only specify scope and name (jsr:@<scope>/<name>)', () => {
expect(parseJsrSpecifier('jsr:@foo/bar')).toStrictEqual({ jsrPkgName: '@foo/bar', npmPkgName: '@jsr/foo__bar' } as JsrSpec)
})
test('succeeds on jsr specifiers that specify scopes, names, and versions/ranges/tags (jsr:@<scope>/<name>@<version_selector>)', () => {
expect(parseJsrSpecifier('jsr:@foo/bar@^1.0.0')).toStrictEqual({ jsrPkgName: '@foo/bar', npmPkgName: '@jsr/foo__bar', versionSelector: '^1.0.0' } as JsrSpec)
expect(parseJsrSpecifier('jsr:@foo/bar@1.0.0')).toStrictEqual({ jsrPkgName: '@foo/bar', npmPkgName: '@jsr/foo__bar', versionSelector: '1.0.0' } as JsrSpec)
expect(parseJsrSpecifier('jsr:@foo/bar@latest')).toStrictEqual({ jsrPkgName: '@foo/bar', npmPkgName: '@jsr/foo__bar', versionSelector: 'latest' } as JsrSpec)
})
test('errors on jsr specifiers that contain names without scopes', () => {
expect(() => parseJsrSpecifier('jsr:foo@^1.0.0')).toThrow(expect.objectContaining({
code: 'ERR_PNPM_MISSING_JSR_PACKAGE_SCOPE',
}))
})
test('errors on jsr specifiers that contain scopes without names', () => {
expect(() => parseJsrSpecifier('jsr:@foo@^1.0.0')).toThrow(expect.objectContaining({
code: 'ERR_PNPM_INVALID_JSR_PACKAGE_NAME',
}))
expect(() => parseJsrSpecifier('jsr:@foo')).toThrow(expect.objectContaining({
code: 'ERR_PNPM_INVALID_JSR_PACKAGE_NAME',
}))
})
})

View File

@@ -0,0 +1,17 @@
{
"extends": "../tsconfig.json",
"compilerOptions": {
"noEmit": false,
"outDir": "../test.lib",
"rootDir": "."
},
"include": [
"**/*.ts",
"../../../__typings__/**/*.d.ts"
],
"references": [
{
"path": ".."
}
]
}

View File

@@ -0,0 +1,16 @@
{
"extends": "@pnpm/tsconfig",
"compilerOptions": {
"outDir": "lib",
"rootDir": "src"
},
"include": [
"src/**/*.ts",
"../../__typings__/**/*.d.ts"
],
"references": [
{
"path": "../../packages/error"
}
]
}

View File

@@ -0,0 +1,8 @@
{
"extends": "./tsconfig.json",
"include": [
"src/**/*.ts",
"test/**/*.ts",
"../../__typings__/**/*.d.ts"
]
}

View File

@@ -41,6 +41,7 @@
"@pnpm/pick-registry-for-package": "workspace:*",
"@pnpm/resolve-workspace-range": "workspace:*",
"@pnpm/resolver-base": "workspace:*",
"@pnpm/resolving.jsr-specifier-parser": "workspace:*",
"@pnpm/types": "workspace:*",
"@pnpm/workspace.spec-parser": "workspace:*",
"@zkochan/retry": "catalog:",

View File

@@ -34,7 +34,9 @@ import {
pickPackage,
} from './pickPackage'
import {
parseJsrSpecifierToRegistryPackageSpec,
parsePref,
type JsrRegistryPackageSpec,
type RegistryPackageSpec,
} from './parsePref'
import { fromRegistry, RegistryResponseError } from './fetch'
@@ -79,7 +81,7 @@ export function createNpmResolver (
fetchFromRegistry: FetchFromRegistry,
getAuthHeader: GetAuthHeader,
opts: ResolverFactoryOptions
): { resolveFromNpm: NpmResolver, clearCache: () => void } {
): { resolveFromNpm: NpmResolver, resolveFromJsr: NpmResolver, clearCache: () => void } {
if (typeof opts.cacheDir !== 'string') {
throw new TypeError('`opts.cacheDir` is required and needs to be a string')
}
@@ -95,27 +97,36 @@ export function createNpmResolver (
max: 10000,
ttl: 120 * 1000, // 2 minutes
})
return {
resolveFromNpm: resolveNpm.bind(null, {
getAuthHeaderValueByURI: getAuthHeader,
pickPackage: pickPackage.bind(null, {
fetch,
filterMetadata: opts.filterMetadata,
metaCache,
metaDir: opts.fullMetadata ? (opts.filterMetadata ? FULL_FILTERED_META_DIR : FULL_META_DIR) : ABBREVIATED_META_DIR,
offline: opts.offline,
preferOffline: opts.preferOffline,
cacheDir: opts.cacheDir,
}),
registries: opts.registries,
saveWorkspaceProtocol: opts.saveWorkspaceProtocol,
const ctx = {
getAuthHeaderValueByURI: getAuthHeader,
pickPackage: pickPackage.bind(null, {
fetch,
filterMetadata: opts.filterMetadata,
metaCache,
metaDir: opts.fullMetadata ? (opts.filterMetadata ? FULL_FILTERED_META_DIR : FULL_META_DIR) : ABBREVIATED_META_DIR,
offline: opts.offline,
preferOffline: opts.preferOffline,
cacheDir: opts.cacheDir,
}),
registries: opts.registries,
saveWorkspaceProtocol: opts.saveWorkspaceProtocol,
}
return {
resolveFromNpm: resolveNpm.bind(null, ctx),
resolveFromJsr: resolveJsr.bind(null, ctx),
clearCache: () => {
metaCache.clear()
},
}
}
export interface ResolveFromNpmContext {
pickPackage: (spec: RegistryPackageSpec, opts: PickPackageOptions) => ReturnType<typeof pickPackage>
getAuthHeaderValueByURI: (registry: string) => string | undefined
registries: Registries
saveWorkspaceProtocol?: boolean | 'rolling'
}
export type ResolveFromNpmOptions = {
alwaysTryWorkspacePackages?: boolean
defaultTag?: string
@@ -138,12 +149,7 @@ export type ResolveFromNpmOptions = {
})
async function resolveNpm (
ctx: {
pickPackage: (spec: RegistryPackageSpec, opts: PickPackageOptions) => ReturnType<typeof pickPackage>
getAuthHeaderValueByURI: (registry: string) => string | undefined
registries: Registries
saveWorkspaceProtocol?: boolean | 'rolling'
},
ctx: ResolveFromNpmContext,
wantedDependency: WantedDependency,
opts: ResolveFromNpmOptions
): Promise<ResolveResult | null> {
@@ -287,6 +293,73 @@ async function resolveNpm (
}
}
async function resolveJsr (
ctx: ResolveFromNpmContext,
wantedDependency: WantedDependency,
opts: Omit<ResolveFromNpmOptions, 'registry'>
): Promise<ResolveResult | null> {
if (!wantedDependency.pref) return null
const defaultTag = opts.defaultTag ?? 'latest'
const registry = ctx.registries['@jsr']! // '@jsr' is always defined
const spec = parseJsrSpecifierToRegistryPackageSpec(wantedDependency.pref, wantedDependency.alias, defaultTag)
if (spec == null) return null
const authHeaderValue = ctx.getAuthHeaderValueByURI(registry)
const { meta, pickedPackage } = await ctx.pickPackage(spec, {
pickLowestVersion: opts.pickLowestVersion,
publishedBy: opts.publishedBy,
authHeaderValue,
dryRun: opts.dryRun === true,
preferredVersionSelectors: opts.preferredVersions?.[spec.name],
registry,
updateToLatest: opts.update === 'latest',
})
if (pickedPackage == null) {
throw new NoMatchingVersionError({ wantedDependency, packageMeta: meta, registry })
}
const id = `${pickedPackage.name}@${pickedPackage.version}` as PkgResolutionId
const resolution = {
integrity: getIntegrity(pickedPackage.dist),
tarball: pickedPackage.dist.tarball,
}
return {
id,
latest: meta['dist-tags'].latest,
manifest: pickedPackage,
specifier: opts.calcSpecifier
? calcJsrSpecifier({
wantedDependency,
spec,
version: pickedPackage.version,
defaultPinnedVersion: opts.pinnedVersion,
})
: undefined,
resolution,
resolvedVia: 'jsr-registry',
publishedAt: meta.time?.[pickedPackage.version],
alias: spec.jsrPkgName,
}
}
function calcJsrSpecifier ({
wantedDependency,
spec,
version,
defaultPinnedVersion,
}: {
wantedDependency: WantedDependency
spec: JsrRegistryPackageSpec
version: string
defaultPinnedVersion?: PinnedVersion
}): string {
const range = calcRange(version, wantedDependency, defaultPinnedVersion)
if (!wantedDependency.alias || spec.jsrPkgName === wantedDependency.alias) return `jsr:${range}`
return `jsr:${spec.jsrPkgName}@${range}`
}
function calcSpecifier ({
wantedDependency,
spec,

View File

@@ -1,3 +1,4 @@
import { parseJsrSpecifier } from '@pnpm/resolving.jsr-specifier-parser'
import parseNpmTarballUrl from 'parse-npm-tarball-url'
import getVersionSelectorType from 'version-selector-type'
@@ -49,3 +50,26 @@ export function parsePref (
}
return null
}
export interface JsrRegistryPackageSpec extends RegistryPackageSpec {
jsrPkgName: string
}
export function parseJsrSpecifierToRegistryPackageSpec (
rawSpecifier: string,
alias: string | undefined,
defaultTag: string
): JsrRegistryPackageSpec | null {
const spec = parseJsrSpecifier(rawSpecifier, alias)
if (!spec?.npmPkgName) return null
const selector = getVersionSelectorType(spec.versionSelector ?? defaultTag)
if (selector == null) return null
return {
fetchSpec: selector.normalized,
name: spec.npmPkgName,
type: selector.type,
jsrPkgName: spec.jsrPkgName,
}
}

View File

@@ -0,0 +1,25 @@
{
"name": "@jsr/luca__cases",
"description": "A collection of functions for converting strings between different cases.",
"dist-tags": {
"latest": "1.0.0"
},
"versions": {
"1.0.0": {
"name": "@jsr/luca__cases",
"version": "1.0.0",
"description": "A collection of functions for converting strings between different cases.",
"dist": {
"tarball": "https://npm.jsr.io/~/11/@jsr/luca__cases/1.0.0.tgz",
"shasum": "2D36BC80F4D7FA0E2F00111925746808D4AF2DB7",
"integrity": "sha512-h3nOT3+JVCXBsLd+st9cytZO+iqJYTTsm3rOrtZZXBgJ0nzBA5mDR9dI3k8wBDQ5AAqqJjD3UkhLxoRCEKwzgg=="
},
"dependencies": {}
}
},
"time": {
"created": "2024-03-06T12:28:36.287Z",
"modified": "2024-03-06T12:30:42.072Z",
"1.0.0": "2024-03-06T12:30:12.551Z"
}
}

View File

@@ -0,0 +1,55 @@
{
"name": "@jsr/rus__greet",
"description": "Example library that greets you!",
"dist-tags": {
"latest": "0.0.3"
},
"versions": {
"0.0.3": {
"name": "@jsr/rus__greet",
"version": "0.0.3",
"description": "Example library that greets you!",
"dist": {
"tarball": "https://npm.jsr.io/~/11/@jsr/rus__greet/0.0.3.tgz",
"shasum": "99158A6BE97ECFDDB52E236A574588897321CD32",
"integrity": "sha512-hOYkQsOF5aMKmsjzXXuttQM/gTN57Ocjwm3UYFF+siCXKjxGusqUQBsajGdvZ/zHyTr7cZg5phXi43I3KDTd5w=="
},
"dependencies": {
"@jsr/luca__cases": "1.0.0"
}
},
"0.0.2": {
"name": "@jsr/rus__greet",
"version": "0.0.2",
"description": "Example library that greets you!",
"dist": {
"tarball": "https://npm.jsr.io/~/11/@jsr/rus__greet/0.0.2.tgz",
"shasum": "D7CD221AC674E964CCF1D8FC238D5E88760B8479",
"integrity": "sha512-+5Z534D4erkvqWjJVnQDvb1Do/rdK4sf3kuCYt2jc1TmCSuEr9tcUD1Llyl6oFpeRxYzi3f2nt8c1z/etFD0jA=="
},
"dependencies": {
"@jsr/luca__cases": "1.0.0"
}
},
"0.0.1": {
"name": "@jsr/rus__greet",
"version": "0.0.1",
"description": "Example library that greets you!",
"dist": {
"tarball": "https://npm.jsr.io/~/11/@jsr/rus__greet/0.0.1.tgz",
"shasum": "3DF04DE78584958FF55F369496BD09CEBAA4A10F",
"integrity": "sha512-er2E93G7KqTOE9Q9gTcCgoBT2OPlqAkGE8JaH8H1+xdVZd6DBrNRJi6TMMtEVZ4YuGIyLU2N3zwhyo29AP8R+A=="
},
"dependencies": {
"@jsr/luca__cases": "1.0.0"
}
}
},
"time": {
"created": "2024-11-16T15:31:57.112Z",
"modified": "2024-11-16T16:24:49.810Z",
"0.0.3": "2024-11-16T16:31:27.117Z",
"0.0.2": "2024-11-16T16:13:24.566Z",
"0.0.1": "2024-11-16T16:08:48.829Z"
}
}

View File

@@ -16,6 +16,7 @@ import loadJsonFile from 'load-json-file'
import nock from 'nock'
import omit from 'ramda/src/omit'
import tempy from 'tempy'
import { delay, retryLoadJsonFile } from './utils'
const f = fixtures(__dirname)
/* eslint-disable @typescript-eslint/no-explicit-any */
@@ -28,33 +29,15 @@ const jsonMeta = loadJsonFile.sync<any>(f.find('JSON.json'))
const brokenIntegrity = loadJsonFile.sync<any>(f.find('broken-integrity.json'))
/* eslint-enable @typescript-eslint/no-explicit-any */
const registries: Registries = {
const registries = {
default: 'https://registry.npmjs.org/',
}
const delay = async (time: number) => new Promise<void>((resolve) => setTimeout(() => {
resolve()
}, time))
'@jsr': 'https://npm.jsr.io/',
} satisfies Registries
const fetch = createFetchFromRegistry({})
const getAuthHeader = () => undefined
const createResolveFromNpm = createNpmResolver.bind(null, fetch, getAuthHeader)
async function retryLoadJsonFile<T> (filePath: string) {
let retry = 0
/* eslint-disable no-await-in-loop */
while (true) {
await delay(500)
try {
return await loadJsonFile<T>(filePath)
} catch (err: any) { // eslint-disable-line
if (retry > 2) throw err
retry++
}
}
/* eslint-enable no-await-in-loop */
}
afterEach(() => {
nock.cleanAll()
nock.disableNetConnect()

View File

@@ -0,0 +1,134 @@
import path from 'path'
import { ABBREVIATED_META_DIR } from '@pnpm/constants'
import { createFetchFromRegistry } from '@pnpm/fetch'
import { createNpmResolver } from '@pnpm/npm-resolver'
import { fixtures } from '@pnpm/test-fixtures'
import { type Registries } from '@pnpm/types'
import loadJsonFile from 'load-json-file'
import nock from 'nock'
import tempy from 'tempy'
import { retryLoadJsonFile } from './utils'
const f = fixtures(__dirname)
/* eslint-disable @typescript-eslint/no-explicit-any */
const jsrRusGreetMeta = loadJsonFile.sync<any>(f.find('jsr-rus-greet.json'))
const jsrLucaCasesMeta = loadJsonFile.sync<any>(f.find('jsr-luca-cases.json'))
/* eslint-enable @typescript-eslint/no-explicit-any */
const registries = {
default: 'https://registry.npmjs.org/',
'@jsr': 'https://npm.jsr.io/',
} satisfies Registries
const fetch = createFetchFromRegistry({})
const getAuthHeader = () => undefined
const createResolveFromNpm = createNpmResolver.bind(null, fetch, getAuthHeader)
afterEach(() => {
nock.cleanAll()
nock.disableNetConnect()
})
beforeEach(() => {
nock.enableNetConnect()
})
test('resolveFromJsr() on jsr', async () => {
const slash = '%2F'
nock(registries.default)
.get(`/@jsr${slash}rus__greet`)
.reply(404)
.get(`/@jsr${slash}luca__cases`)
.reply(404)
nock(registries['@jsr'])
.get(`/@jsr${slash}rus__greet`)
.reply(200, jsrRusGreetMeta)
.get(`/@jsr${slash}luca__cases`)
.reply(200, jsrLucaCasesMeta)
const cacheDir = tempy.directory()
const { resolveFromJsr } = createResolveFromNpm({
cacheDir,
registries,
})
const resolveResult = await resolveFromJsr({ alias: '@rus/greet', pref: 'jsr:0.0.3' }, { calcSpecifier: true })
expect(resolveResult).toMatchObject({
resolvedVia: 'jsr-registry',
id: '@jsr/rus__greet@0.0.3',
latest: '0.0.3',
manifest: {
name: '@jsr/rus__greet',
version: '0.0.3',
},
resolution: {
integrity: expect.any(String),
tarball: 'https://npm.jsr.io/~/11/@jsr/rus__greet/0.0.3.tgz',
},
specifier: 'jsr:0.0.3',
})
// The resolve function does not wait for the package meta cache file to be saved
// so we must delay for a bit in order to read it
const meta = await retryLoadJsonFile<any>(path.join(cacheDir, ABBREVIATED_META_DIR, 'npm.jsr.io/@jsr/rus__greet.json')) // eslint-disable-line @typescript-eslint/no-explicit-any
expect(meta).toMatchObject({
name: expect.any(String),
versions: expect.any(Object),
'dist-tags': expect.any(Object),
})
})
test('resolveFromJsr() on jsr with alias renaming', async () => {
const slash = '%2F'
nock(registries.default)
.get(`/@jsr${slash}rus__greet`)
.reply(404)
.get(`/@jsr${slash}luca__cases`)
.reply(404)
nock(registries['@jsr'])
.get(`/@jsr${slash}rus__greet`)
.reply(200, jsrRusGreetMeta)
.get(`/@jsr${slash}luca__cases`)
.reply(200, jsrLucaCasesMeta)
const cacheDir = tempy.directory()
const { resolveFromJsr } = createResolveFromNpm({
cacheDir,
registries,
})
const resolveResult = await resolveFromJsr({ alias: 'greet', pref: 'jsr:@rus/greet@0.0.3' }, {})
expect(resolveResult).toMatchObject({
resolvedVia: 'jsr-registry',
id: '@jsr/rus__greet@0.0.3',
latest: '0.0.3',
manifest: {
name: '@jsr/rus__greet',
version: '0.0.3',
},
resolution: {
integrity: expect.any(String),
tarball: 'https://npm.jsr.io/~/11/@jsr/rus__greet/0.0.3.tgz',
},
})
// The resolve function does not wait for the package meta cache file to be saved
// so we must delay for a bit in order to read it
const meta = await retryLoadJsonFile<any>(path.join(cacheDir, ABBREVIATED_META_DIR, 'npm.jsr.io/@jsr/rus__greet.json')) // eslint-disable-line @typescript-eslint/no-explicit-any
expect(meta).toMatchObject({
name: expect.any(String),
versions: expect.any(Object),
'dist-tags': expect.any(Object),
})
})
test('resolveFromJsr() on jsr with packages without scope', async () => {
const cacheDir = tempy.directory()
const { resolveFromJsr } = createResolveFromNpm({
cacheDir,
registries,
})
await expect(resolveFromJsr({ alias: 'greet', pref: 'jsr:0.0.3' }, {})).rejects.toMatchObject({
code: 'ERR_PNPM_MISSING_JSR_PACKAGE_SCOPE',
})
})

View File

@@ -0,0 +1,22 @@
import loadJsonFile from 'load-json-file'
export async function retryLoadJsonFile<T> (filePath: string): Promise<T> {
let retry = 0
/* eslint-disable no-await-in-loop */
while (true) {
await delay(500)
try {
return await loadJsonFile<T>(filePath)
} catch (err: any) { // eslint-disable-line
if (retry > 2) throw err
retry++
}
}
/* eslint-enable no-await-in-loop */
}
export async function delay (time: number): Promise<void> {
return new Promise<void>((resolve) => setTimeout(() => {
resolve()
}, time))
}

View File

@@ -11,6 +11,8 @@ test.each([
['npm:foo@^1.0.0', 'major'],
['npm:@foo/foo@^1.0.0', 'major'],
['npm:@pnpm.e2e/qar@100.0.0', 'patch'],
['jsr:@foo/foo@1.0.0', 'patch'],
['jsr:foo@^1.0.0', 'major'],
])('whichVersionIsPinned()', (spec, expectedResult) => {
expect(whichVersionIsPinned(spec)).toEqual(expectedResult)
})

View File

@@ -48,6 +48,9 @@
{
"path": "../../workspace/spec-parser"
},
{
"path": "../jsr-specifier-parser"
},
{
"path": "../resolver-base"
}

View File

@@ -46,6 +46,7 @@ export interface ResolveResult {
resolution: Resolution
resolvedVia: 'npm-registry' | 'git-repository' | 'local-filesystem' | 'workspace' | 'url' | string
specifier?: string
alias?: string
}
/**

View File

@@ -3,6 +3,7 @@ import { getPkgInfo } from '../lib/getPkgInfo'
export const DEFAULT_REGISTRIES = {
default: 'https://registry.npmjs.org/',
'@jsr': 'https://npm.jsr.io/',
}
describe('licences', () => {

View File

@@ -155,6 +155,7 @@ export interface PackageResponse {
// If latest does not equal the version of the
// resolved package, it is out-of-date.
latest?: string
alias?: string
} & (
{
isLocal: true