feat: add sub folder support for git url (#7487)

close #4765

---------

Co-authored-by: Zoltan Kochan <z@kochan.io>
This commit is contained in:
Rex Zeng
2024-01-24 09:15:02 +08:00
committed by GitHub
parent 5f05d90d15
commit b13d2dc1ae
20 changed files with 351 additions and 46 deletions

View File

@@ -0,0 +1,17 @@
---
"@pnpm/resolver-base": minor
"@pnpm/git-resolver": minor
"@pnpm/prepare-package": minor
"@pnpm/git-fetcher": minor
"@pnpm/tarball-fetcher": minor
"pnpm": minor
---
It is now possible to install only a subdirectory from a Git repository.
For example, `pnpm add github:user/repo#path:packages/foo` will add a dependency from the `packages/foo` subdirectory.
This new parameter may be combined with other supported parameters separated by `&`. For instance, the next command will install the same package from the `dev` branch: `pnpm add github:user/repo#dev&path:packages/bar`.
Related issue: [#4765](https://github.com/pnpm/pnpm/issues/4765).
Related PR: [#7487](https://github.com/pnpm/pnpm/pull/7487).

View File

@@ -29,6 +29,7 @@
},
"homepage": "https://github.com/pnpm/pnpm/blob/main/exec/prepare-package#readme",
"dependencies": {
"@pnpm/error": "workspace:*",
"@pnpm/lifecycle": "workspace:*",
"@pnpm/read-package-json": "workspace:*",
"@pnpm/types": "workspace:*",

View File

@@ -1,5 +1,6 @@
import fs from 'fs'
import path from 'path'
import { PnpmError } from '@pnpm/error'
import { runLifecycleHook, type RunLifecycleHookOptions } from '@pnpm/lifecycle'
import { safeReadPackageJsonFromDir } from '@pnpm/read-package-json'
import { type PackageManifest } from '@pnpm/types'
@@ -22,11 +23,12 @@ export interface PreparePackageOptions {
unsafePerm?: boolean
}
export async function preparePackage (opts: PreparePackageOptions, pkgDir: string): Promise<boolean> {
export async function preparePackage (opts: PreparePackageOptions, gitRootDir: string, subDir: string): Promise<{ shouldBeBuilt: boolean, pkgDir: string }> {
const pkgDir = safeJoinPath(gitRootDir, subDir)
const manifest = await safeReadPackageJsonFromDir(pkgDir)
if (manifest?.scripts == null || !packageShouldBeBuilt(manifest, pkgDir)) return false
if (opts.ignoreScripts) return true
const pm = (await preferredPM(pkgDir))?.name ?? 'npm'
if (manifest?.scripts == null || !packageShouldBeBuilt(manifest, pkgDir)) return { shouldBeBuilt: false, pkgDir }
if (opts.ignoreScripts) return { shouldBeBuilt: true, pkgDir }
const pm = (await preferredPM(gitRootDir))?.name ?? 'npm'
const execOpts: RunLifecycleHookOptions = {
depPath: `${manifest.name}@${manifest.version}`,
pkgRoot: pkgDir,
@@ -50,7 +52,7 @@ export async function preparePackage (opts: PreparePackageOptions, pkgDir: strin
throw err
}
await rimraf(path.join(pkgDir, 'node_modules'))
return true
return { shouldBeBuilt: true, pkgDir }
}
function packageShouldBeBuilt (manifest: PackageManifest, pkgDir: string): boolean {
@@ -62,3 +64,16 @@ function packageShouldBeBuilt (manifest: PackageManifest, pkgDir: string): boole
const mainFile = manifest.main ?? 'index.js'
return !fs.existsSync(path.join(pkgDir, mainFile))
}
function safeJoinPath (root: string, sub: string) {
const joined = path.join(root, sub)
// prevent the dir traversal attack
const relative = path.relative(root, joined)
if (relative.startsWith('..')) {
throw new PnpmError('INVALID_PATH', `Path "${sub}" should be a sub directory`)
}
if (!fs.existsSync(joined) || !fs.lstatSync(joined).isDirectory()) {
throw new PnpmError('INVALID_PATH', `Path "${sub}" is not a directory`)
}
return joined
}

View File

@@ -0,0 +1,4 @@
{
"name": "has-prepublish-script-in-workspace",
"version": "1.0.0"
}

View File

@@ -0,0 +1,7 @@
{
"name": "foo",
"version": "1.0.0",
"scripts": {
"prepublish": "node -e \"console.log('prepublish')\" | test-ipc-server-client ../../test.sock"
}
}

View File

@@ -0,0 +1,2 @@
packages:
- 'packages/*'

View File

@@ -10,7 +10,7 @@ test('prepare package runs the prepublish script', async () => {
const tmp = tempDir()
await using server = await createTestIpcServer(path.join(tmp, 'test.sock'))
f.copy('has-prepublish-script', tmp)
await preparePackage({ rawConfig: {} }, tmp)
await preparePackage({ rawConfig: {} }, tmp, '')
expect(server.getLines()).toStrictEqual([
'prepublish',
])
@@ -20,7 +20,17 @@ test('prepare package does not run the prepublish script if the main file is pre
const tmp = tempDir()
await using server = await createTestIpcServer(path.join(tmp, 'test.sock'))
f.copy('has-prepublish-script-and-main-file', tmp)
await preparePackage({ rawConfig: {} }, tmp)
await preparePackage({ rawConfig: {} }, tmp, '')
expect(server.getLines()).toStrictEqual([
'prepublish',
])
})
test('prepare package runs the prepublish script in the sub folder if pkgDir is present', async () => {
const tmp = tempDir()
await using server = await createTestIpcServer(path.join(tmp, 'test.sock'))
f.copy('has-prepublish-script-in-workspace', tmp)
await preparePackage({ rawConfig: {} }, tmp, 'packages/foo')
expect(server.getLines()).toStrictEqual([
'prepublish',
])

View File

@@ -18,6 +18,9 @@
{
"path": "../../__utils__/test-ipc-server"
},
{
"path": "../../packages/error"
},
{
"path": "../../packages/types"
},

View File

@@ -33,9 +33,11 @@ export function createGitFetcher (createOpts: CreateGitFetcherOptions) {
await execGit(['clone', resolution.repo, tempLocation])
}
await execGit(['checkout', resolution.commit], { cwd: tempLocation })
let pkgDir: string
try {
const shouldBeBuilt = await preparePkg(tempLocation)
if (ignoreScripts && shouldBeBuilt) {
const prepareResult = await preparePkg(tempLocation, resolution.path ?? '')
pkgDir = prepareResult.pkgDir
if (ignoreScripts && prepareResult.shouldBeBuilt) {
globalWarn(`The git-hosted package fetched from "${resolution.repo}" has to be built but the build scripts were ignored.`)
}
} catch (err: any) { // eslint-disable-line
@@ -49,7 +51,7 @@ export function createGitFetcher (createOpts: CreateGitFetcherOptions) {
// the linking of files to the store is in progress.
return addFilesFromDir({
cafsDir: cafs.cafsDir,
dir: tempLocation,
dir: pkgDir,
filesIndexFile: opts.filesIndexFile,
readManifest: opts.readManifest,
pkg: opts.pkg,

View File

@@ -47,6 +47,66 @@ test('fetch', async () => {
expect(manifest?.name).toEqual('is-positive')
})
test('fetch a package from Git sub folder', async () => {
const cafsDir = tempy.directory()
const fetch = createGitFetcher({ rawConfig: {} }).git
const { filesIndex } = await fetch(
createCafsStore(cafsDir),
{
commit: '2b42a57a945f19f8ffab8ecbd2021fdc2c58ee22',
repo: 'https://github.com/RexSkz/test-git-subfolder-fetch.git',
path: '/packages/simple-react-app',
type: 'git',
},
{
filesIndexFile: path.join(cafsDir, 'index.json'),
}
)
expect(filesIndex[`public${path.sep}index.html`]).toBeTruthy()
})
test('prevent directory traversal attack when using Git sub folder', async () => {
const cafsDir = tempy.directory()
const fetch = createGitFetcher({ rawConfig: {} }).git
const repo = 'https://github.com/RexSkz/test-git-subfolder-fetch.git'
const pkgDir = '../../etc'
await expect(
fetch(
createCafsStore(cafsDir),
{
commit: '2b42a57a945f19f8ffab8ecbd2021fdc2c58ee22',
repo,
path: pkgDir,
type: 'git',
},
{
filesIndexFile: path.join(cafsDir, 'index.json'),
}
)
).rejects.toThrow(`Failed to prepare git-hosted package fetched from "${repo}": Path "${pkgDir}" should be a sub directory`)
})
test('prevent directory traversal attack when using Git sub folder', async () => {
const cafsDir = tempy.directory()
const fetch = createGitFetcher({ rawConfig: {} }).git
const repo = 'https://github.com/RexSkz/test-git-subfolder-fetch.git'
const pkgDir = 'not/exists'
await expect(
fetch(
createCafsStore(cafsDir),
{
commit: '2b42a57a945f19f8ffab8ecbd2021fdc2c58ee22',
repo,
path: pkgDir,
type: 'git',
},
{
filesIndexFile: path.join(cafsDir, 'index.json'),
}
)
).rejects.toThrow(`Failed to prepare git-hosted package fetched from "${repo}": Path "${pkgDir}" is not a directory`)
})
test('fetch a package from Git that has a prepare script', async () => {
const cafsDir = tempy.directory()
const fetch = createGitFetcher({ rawConfig: {} }).git

View File

@@ -3,6 +3,7 @@ import { type FetchFunction, type FetchOptions } from '@pnpm/fetcher-base'
import type { Cafs } from '@pnpm/cafs-types'
import { globalWarn } from '@pnpm/logger'
import { preparePackage } from '@pnpm/prepare-package'
import { type DependencyManifest } from '@pnpm/types'
import { addFilesFromDir } from '@pnpm/worker'
import renameOverwrite from 'rename-overwrite'
import { fastPathTemp as pathTemp } from 'path-temp'
@@ -11,6 +12,7 @@ interface Resolution {
integrity?: string
registry?: string
tarball: string
path?: string
}
export interface CreateGitHostedTarballFetcher {
@@ -21,19 +23,20 @@ export interface CreateGitHostedTarballFetcher {
export function createGitHostedTarballFetcher (fetchRemoteTarball: FetchFunction, fetcherOpts: CreateGitHostedTarballFetcher): FetchFunction {
const fetch = async (cafs: Cafs, resolution: Resolution, opts: FetchOptions) => {
// This solution is not perfect but inside the fetcher we don't currently know the location
// of the built and non-built index files.
const nonBuiltIndexFile = fetcherOpts.ignoreScripts ? opts.filesIndexFile : pathTemp(opts.filesIndexFile)
const tempIndexFile = pathTemp(opts.filesIndexFile)
const { filesIndex, manifest } = await fetchRemoteTarball(cafs, resolution, {
...opts,
filesIndexFile: nonBuiltIndexFile,
filesIndexFile: tempIndexFile,
})
try {
const prepareResult = await prepareGitHostedPkg(filesIndex as Record<string, string>, cafs, nonBuiltIndexFile, opts.filesIndexFile, fetcherOpts, opts)
const prepareResult = await prepareGitHostedPkg(filesIndex as Record<string, string>, cafs, tempIndexFile, opts.filesIndexFile, fetcherOpts, opts, resolution)
if (prepareResult.ignoredBuild) {
globalWarn(`The git-hosted package fetched from "${resolution.tarball}" has to be built but the build scripts were ignored.`)
}
return { filesIndex: prepareResult.filesIndex, manifest }
return {
filesIndex: prepareResult.filesIndex,
manifest: prepareResult.manifest ?? manifest,
}
} catch (err: any) { // eslint-disable-line
err.message = `Failed to prepare git-hosted package fetched from "${resolution.tarball}": ${err.message}`
throw err
@@ -43,14 +46,21 @@ export function createGitHostedTarballFetcher (fetchRemoteTarball: FetchFunction
return fetch as FetchFunction
}
interface PrepareGitHostedPkgResult {
filesIndex: Record<string, string>
manifest?: DependencyManifest
ignoredBuild: boolean
}
async function prepareGitHostedPkg (
filesIndex: Record<string, string>,
cafs: Cafs,
filesIndexFileNonBuilt: string,
filesIndexFile: string,
opts: CreateGitHostedTarballFetcher,
fetcherOpts: FetchOptions
) {
fetcherOpts: FetchOptions,
resolution: Resolution
): Promise<PrepareGitHostedPkgResult> {
const tempLocation = await cafs.tempDir()
cafs.importPackage(tempLocation, {
filesResponse: {
@@ -59,20 +69,22 @@ async function prepareGitHostedPkg (
},
force: true,
})
const shouldBeBuilt = await preparePackage(opts, tempLocation)
if (!shouldBeBuilt) {
if (filesIndexFileNonBuilt !== filesIndexFile) {
await renameOverwrite(filesIndexFileNonBuilt, filesIndexFile)
const { shouldBeBuilt, pkgDir } = await preparePackage(opts, tempLocation, resolution.path ?? '')
if (!resolution.path) {
if (!shouldBeBuilt) {
if (filesIndexFileNonBuilt !== filesIndexFile) {
await renameOverwrite(filesIndexFileNonBuilt, filesIndexFile)
}
return {
filesIndex,
ignoredBuild: false,
}
}
return {
filesIndex,
ignoredBuild: false,
}
}
if (opts.ignoreScripts) {
return {
filesIndex,
ignoredBuild: true,
if (opts.ignoreScripts) {
return {
filesIndex,
ignoredBuild: true,
}
}
}
try {
@@ -85,9 +97,10 @@ async function prepareGitHostedPkg (
return {
...await addFilesFromDir({
cafsDir: cafs.cafsDir,
dir: tempLocation,
dir: pkgDir,
filesIndexFile,
pkg: fetcherOpts.pkg,
readManifest: fetcherOpts.readManifest,
}),
ignoredBuild: false,
}

View File

@@ -513,3 +513,78 @@ test('when extracting files with the same name, pick the last ones', async () =>
expect(pkgJson.name).toBe('pkg2')
expect(manifest?.name).toBe('pkg2')
})
test('use the subfolder when path is present', async () => {
process.chdir(tempy.directory())
const resolution = {
tarball: 'https://codeload.github.com/RexSkz/test-git-subfolder-fetch/tar.gz/2b42a57a945f19f8ffab8ecbd2021fdc2c58ee22',
path: '/packages/simple-react-app',
}
const fetch = createTarballFetcher(fetchFromRegistry, getAuthHeader, {
ignoreScripts: true,
rawConfig: {},
retry: {
maxTimeout: 100,
minTimeout: 0,
retries: 1,
},
})
const { filesIndex } = await fetch.gitHostedTarball(cafs, resolution, {
filesIndexFile,
lockfileDir: process.cwd(),
pkg: {},
})
expect(filesIndex).toHaveProperty(['package.json'])
expect(filesIndex).not.toHaveProperty(['lerna.json'])
})
test('prevent directory traversal attack when path is present', async () => {
process.chdir(tempy.directory())
const tarball = 'https://codeload.github.com/RexSkz/test-git-subfolder-fetch/tar.gz/2b42a57a945f19f8ffab8ecbd2021fdc2c58ee22'
const path = '../../etc'
const resolution = { tarball, path }
const fetch = createTarballFetcher(fetchFromRegistry, getAuthHeader, {
ignoreScripts: true,
rawConfig: {},
retry: {
maxTimeout: 100,
minTimeout: 0,
retries: 1,
},
})
await expect(() => fetch.gitHostedTarball(cafs, resolution, {
filesIndexFile,
lockfileDir: process.cwd(),
pkg: {},
})).rejects.toThrow(`Failed to prepare git-hosted package fetched from "${tarball}": Path "${path}" should be a sub directory`)
})
test('fail when path is not exists', async () => {
process.chdir(tempy.directory())
const tarball = 'https://codeload.github.com/RexSkz/test-git-subfolder-fetch/tar.gz/2b42a57a945f19f8ffab8ecbd2021fdc2c58ee22'
const path = '/not-exists'
const resolution = { tarball, path }
const fetch = createTarballFetcher(fetchFromRegistry, getAuthHeader, {
ignoreScripts: true,
rawConfig: {},
retry: {
maxTimeout: 100,
minTimeout: 0,
retries: 1,
},
})
await expect(() => fetch.gitHostedTarball(cafs, resolution, {
filesIndexFile,
lockfileDir: process.cwd(),
pkg: {},
})).rejects.toThrow(`Failed to prepare git-hosted package fetched from "${tarball}": Path "${path}" is not a directory`)
})

View File

@@ -326,3 +326,20 @@ test('git-hosted repository is not added to the store if it fails to be built',
addDependenciesToPackage({}, ['pnpm-e2e/prepare-script-fails'], await testDefaults())
).rejects.toThrow()
})
test('from subdirectories of a git repo', async () => {
const project = prepareEmpty()
const manifest = await addDependenciesToPackage({}, [
'github:RexSkz/test-git-subfolder-fetch#path:/packages/simple-react-app',
'github:RexSkz/test-git-subfolder-fetch#path:/packages/simple-express-server',
], await testDefaults())
await project.has('@my-namespace/simple-react-app')
await project.has('@my-namespace/simple-express-server')
expect(manifest.dependencies).toStrictEqual({
'@my-namespace/simple-express-server': 'github:RexSkz/test-git-subfolder-fetch#path:/packages/simple-express-server',
'@my-namespace/simple-react-app': 'github:RexSkz/test-git-subfolder-fetch#path:/packages/simple-react-app',
})
})

3
pnpm-lock.yaml generated
View File

@@ -1416,6 +1416,9 @@ importers:
exec/prepare-package:
dependencies:
'@pnpm/error':
specifier: workspace:*
version: link:../../packages/error
'@pnpm/lifecycle':
specifier: workspace:*
version: link:../lifecycle

View File

@@ -1,4 +1,4 @@
import { type ResolveResult } from '@pnpm/resolver-base'
import { type TarballResolution, type GitResolution, type ResolveResult } from '@pnpm/resolver-base'
import git from 'graceful-git'
import semver from 'semver'
import { parsePref, type HostedPackageSpec } from './parsePref'
@@ -29,7 +29,7 @@ export function createGitResolver (
const tarball = hosted.tarball?.()
if (tarball) {
resolution = { tarball }
resolution = { tarball } as TarballResolution
}
}
@@ -38,14 +38,18 @@ export function createGitResolver (
commit,
repo: parsedSpec.fetchSpec,
type: 'git',
} as ({ type: string } & object)
} as GitResolution
}
if (parsedSpec.path) {
resolution.path = parsedSpec.path
}
return {
id: parsedSpec.fetchSpec
.replace(/^.*:\/\/(git@)?/, '')
.replace(/:/g, '+')
.replace(/\.git$/, '') + '/' + commit,
.replace(/\.git$/, '') + '/' + commit + (resolution.path ? `#path:${resolution.path}` : ''),
normalizedPref: parsedSpec.normalizedPref,
resolution,
resolvedVia: 'git-repository',

View File

@@ -17,6 +17,7 @@ export interface HostedPackageSpec {
normalizedPref: string
gitCommittish: string | null
gitRange?: string
path?: string
}
const gitProtocols = new Set([
@@ -43,11 +44,11 @@ export async function parsePref (pref: string): Promise<HostedPackageSpec | null
const url = new URL(correctPref)
if (!url?.protocol) return null
const committish = (url.hash?.length > 1) ? decodeURIComponent(url.hash.slice(1)) : null
const hash = (url.hash?.length > 1) ? decodeURIComponent(url.hash.slice(1)) : null
return {
fetchSpec: urlToFetchSpec(url),
normalizedPref: pref,
...setGitCommittish(committish),
...parseGitParams(hash),
}
}
return null
@@ -87,7 +88,7 @@ async function fromHostedGit (hosted: any): Promise<HostedPackageSpec> { // esli
tarball: undefined,
},
normalizedPref: `git+${httpsUrl}`,
...setGitCommittish(hosted.committish),
...parseGitParams(hosted.committish),
}
} else {
try {
@@ -121,7 +122,7 @@ async function fromHostedGit (hosted: any): Promise<HostedPackageSpec> { // esli
tarball: hosted.tarball,
},
normalizedPref: hosted.shortcut(),
...setGitCommittish(hosted.committish),
...parseGitParams(hosted.committish),
}
}
@@ -143,14 +144,25 @@ async function accessRepository (repository: string) {
}
}
function setGitCommittish (committish: string | null) {
if (committish !== null && committish.length >= 7 && committish.slice(0, 7) === 'semver:') {
return {
gitCommittish: null,
gitRange: committish.slice(7),
type GitParsedParams = Pick<HostedPackageSpec, 'gitCommittish' | 'gitRange' | 'path'>
function parseGitParams (committish: string | null): GitParsedParams {
const result: GitParsedParams = { gitCommittish: null }
if (!committish) {
return result
}
const params = committish.split('&')
for (const param of params) {
if (param.length >= 7 && param.slice(0, 7) === 'semver:') {
result.gitRange = param.slice(7)
} else if (param.slice(0, 5) === 'path:') {
result.path = param.slice(5)
} else {
result.gitCommittish = param
}
}
return { gitCommittish: committish }
return result
}
// handle SCP-like URLs

View File

@@ -165,6 +165,32 @@ test.skip('resolveFromGit() with range semver (v-prefixed tag)', async () => {
})
})
test('resolveFromGit() with sub folder', async () => {
const resolveResult = await resolveFromGit({ pref: 'github:RexSkz/test-git-subfolder-fetch.git#path:/packages/simple-react-app' })
expect(resolveResult).toStrictEqual({
id: 'github.com/RexSkz/test-git-subfolder-fetch/2b42a57a945f19f8ffab8ecbd2021fdc2c58ee22#path:/packages/simple-react-app',
normalizedPref: 'github:RexSkz/test-git-subfolder-fetch#path:/packages/simple-react-app',
resolution: {
tarball: 'https://codeload.github.com/RexSkz/test-git-subfolder-fetch/tar.gz/2b42a57a945f19f8ffab8ecbd2021fdc2c58ee22',
path: '/packages/simple-react-app',
},
resolvedVia: 'git-repository',
})
})
test('resolveFromGit() with both sub folder and branch', async () => {
const resolveResult = await resolveFromGit({ pref: 'github:RexSkz/test-git-subfolder-fetch.git#beta&path:/packages/simple-react-app' })
expect(resolveResult).toStrictEqual({
id: 'github.com/RexSkz/test-git-subfolder-fetch/777e8a3e78cc89bbf41fb3fd9f6cf922d5463313#path:/packages/simple-react-app',
normalizedPref: 'github:RexSkz/test-git-subfolder-fetch#beta&path:/packages/simple-react-app',
resolution: {
tarball: 'https://codeload.github.com/RexSkz/test-git-subfolder-fetch/tar.gz/777e8a3e78cc89bbf41fb3fd9f6cf922d5463313',
path: '/packages/simple-react-app',
},
resolvedVia: 'git-repository',
})
})
test('resolveFromGit() fails when ref not found', async () => {
await expect(
resolveFromGit({ pref: 'zkochan/is-negative#bad-ref' })

View File

@@ -5,10 +5,42 @@ test.each([
['ssh://username:password@example.com:repo/@foo.git', 'ssh://username:password@example.com/repo/@foo.git'],
['ssh://username:password@example.com:22/repo/@foo.git', 'ssh://username:password@example.com:22/repo/@foo.git'],
['ssh://username:password@example.com:22repo/@foo.git', 'ssh://username:password@example.com/22repo/@foo.git'],
['ssh://username:password@example.com:22/repo/@foo.git#path:/a/@b', 'ssh://username:password@example.com:22/repo/@foo.git'],
['ssh://username:password@example.com:22/repo/@foo.git#path:/a/@b&dev', 'ssh://username:password@example.com:22/repo/@foo.git'],
['git+ssh://username:password@example.com:repo.git', 'ssh://username:password@example.com/repo.git'],
['git+ssh://username:password@example.com:repo/@foo.git', 'ssh://username:password@example.com/repo/@foo.git'],
['git+ssh://username:password@example.com:22/repo/@foo.git', 'ssh://username:password@example.com:22/repo/@foo.git'],
['git+ssh://username:password@example.com:22/repo/@foo.git#path:/a/@b', 'ssh://username:password@example.com:22/repo/@foo.git'],
['git+ssh://username:password@example.com:22/repo/@foo.git#path:/a/@b&dev', 'ssh://username:password@example.com:22/repo/@foo.git'],
])('the right colon is escaped in %s', async (input, output) => {
const parsed = await parsePref(input)
expect(parsed?.fetchSpec).toBe(output)
})
test.each([
['ssh://username:password@example.com:repo.git#path:/a/@b', '/a/@b'],
['ssh://username:password@example.com:repo/@foo.git#path:/a/@b', '/a/@b'],
['ssh://username:password@example.com:22/repo/@foo.git#path:/a/@b', '/a/@b'],
['ssh://username:password@example.com:22repo/@foo.git#path:/a/@b', '/a/@b'],
['ssh://username:password@example.com:22/repo/@foo.git#path:/a/@b', '/a/@b'],
['ssh://username:password@example.com:22/repo/@foo.git#path:/a/@b&dev', '/a/@b'],
['git+ssh://username:password@example.com:repo.git#path:/a/@b', '/a/@b'],
['git+ssh://username:password@example.com:repo/@foo.git#path:/a/@b', '/a/@b'],
['git+ssh://username:password@example.com:22/repo/@foo.git#path:/a/@b', '/a/@b'],
['git+ssh://username:password@example.com:22/repo/@foo.git#path:/a/@b', '/a/@b'],
['git+ssh://username:password@example.com:22/repo/@foo.git#path:/a/@b&dev', '/a/@b'],
['ssh://username:password@example.com:repo.git', undefined],
['ssh://username:password@example.com:repo/@foo.git', undefined],
['ssh://username:password@example.com:22/repo/@foo.git', undefined],
['ssh://username:password@example.com:22repo/@foo.git', undefined],
['ssh://username:password@example.com:22/repo/@foo.git', undefined],
['ssh://username:password@example.com:22/repo/@foo.git#dev', undefined],
['git+ssh://username:password@example.com:repo.git', undefined],
['git+ssh://username:password@example.com:repo/@foo.git', undefined],
['git+ssh://username:password@example.com:22/repo/@foo.git', undefined],
['git+ssh://username:password@example.com:22/repo/@foo.git', undefined],
['git+ssh://username:password@example.com:22/repo/@foo.git#dev', undefined],
])('the path of %s should be %s', async (input, output) => {
const parsed = await parsePref(input)
expect(parsed?.path).toBe(output)
})

View File

@@ -7,6 +7,7 @@ export interface TarballResolution {
type?: undefined
tarball: string
integrity?: string
path?: string
}
/**
@@ -20,6 +21,7 @@ export interface DirectoryResolution {
export interface GitResolution {
commit: string
repo: string
path?: string
type: 'git'
}