The jsr: specifier parser accepted scoped names with an empty scope or name and with path separators inside the name; the malformed name was folded into the @jsr/... npm name and flowed into registry URLs and metadata cache file paths. It now throws INVALID_JSR_PACKAGE_NAME for any name rejected by npm's package-name rules. The named-registry specifier parser (e.g. gh:) had the same gap: the scoped branch only rejected a missing '/' or trailing slash, and the unscoped branch validated nothing, while the downstream pick-package validator only rejects '/' in unscoped names. The parser now validates the final package name in every branch (including alias-derived names) and throws INVALID_NAMED_REGISTRY_PACKAGE_NAME for the same shapes. Rather than hand-rolling the shape checks, both parsers delegate to the validator the repo already ships: validate-npm-package-name (validForOldPackages) in TypeScript and its existing pacquet port is_valid_old_npm_package_name. Both fixes land in the TypeScript CLI and pacquet, with matching tests. --------- Co-authored-by: JSap0914 <JSap0914@users.noreply.github.com> Co-authored-by: Zoltan Kochan <z@kochan.io>
74 lines
3.8 KiB
TypeScript
74 lines
3.8 KiB
TypeScript
import { describe, expect, test } from '@jest/globals'
|
|
import { type JsrSpec, parseJsrSpecifier } 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',
|
|
}))
|
|
expect(() => parseJsrSpecifier('jsr:@foo/')).toThrow(expect.objectContaining({
|
|
code: 'ERR_PNPM_INVALID_JSR_PACKAGE_NAME',
|
|
}))
|
|
expect(() => parseJsrSpecifier('jsr:@foo/@^1.0.0')).toThrow(expect.objectContaining({
|
|
code: 'ERR_PNPM_INVALID_JSR_PACKAGE_NAME',
|
|
}))
|
|
})
|
|
|
|
test('errors on jsr specifiers that contain names with empty scopes', () => {
|
|
expect(() => parseJsrSpecifier('jsr:@/bar')).toThrow(expect.objectContaining({
|
|
code: 'ERR_PNPM_INVALID_JSR_PACKAGE_NAME',
|
|
}))
|
|
})
|
|
|
|
test('errors on jsr specifiers that contain path separators in the package name', () => {
|
|
expect(() => parseJsrSpecifier('jsr:@foo/../bar')).toThrow(expect.objectContaining({
|
|
code: 'ERR_PNPM_INVALID_JSR_PACKAGE_NAME',
|
|
}))
|
|
expect(() => parseJsrSpecifier('jsr:@foo/bar/baz')).toThrow(expect.objectContaining({
|
|
code: 'ERR_PNPM_INVALID_JSR_PACKAGE_NAME',
|
|
}))
|
|
expect(() => parseJsrSpecifier('jsr:@foo/bar\\baz')).toThrow(expect.objectContaining({
|
|
code: 'ERR_PNPM_INVALID_JSR_PACKAGE_NAME',
|
|
}))
|
|
expect(() => parseJsrSpecifier('jsr:@sco\\pe/bar')).toThrow(expect.objectContaining({
|
|
code: 'ERR_PNPM_INVALID_JSR_PACKAGE_NAME',
|
|
}))
|
|
})
|
|
})
|