import path from 'path' import fs from 'fs' import { type RootLog } from '@pnpm/core-loggers' import { depPathToFilename } from '@pnpm/dependency-path' import { prepareEmpty } from '@pnpm/prepare' import { addDependenciesToPackage, install, } from '@pnpm/core' import { fixtures } from '@pnpm/test-fixtures' import { assertProject } from '@pnpm/assert-project' import { sync as rimraf } from '@zkochan/rimraf' import { isCI } from 'ci-info' import nock from 'nock' import { jest } from '@jest/globals' import { testDefaults } from '../utils/index.js' const f = fixtures(import.meta.dirname) const withGitProtocolDepFixture = f.find('with-git-protocol-dep') afterEach(() => { nock.abortPendingRequests() nock.cleanAll() }) test('from a github repo', async () => { const project = prepareEmpty() // Mock the HEAD request that isRepoPublic() in @pnpm/git-resolver makes. // Without this, transient network failures cause fallback to git+https:// resolution. const githubNock = nock('https://github.com', { allowUnmocked: true }) .head('/kevva/is-negative') .reply(200) const { updatedManifest: manifest } = await addDependenciesToPackage({}, ['kevva/is-negative'], testDefaults()) project.has('is-negative') expect(manifest.dependencies).toStrictEqual({ 'is-negative': 'github:kevva/is-negative', }) githubNock.done() }) test('from a github repo through URL', async () => { const project = prepareEmpty() const githubNock = nock('https://github.com', { allowUnmocked: true }) .head('/kevva/is-negative') .reply(200) const { updatedManifest: manifest } = await addDependenciesToPackage({}, ['https://github.com/kevva/is-negative'], testDefaults()) project.has('is-negative') expect(manifest.dependencies).toStrictEqual({ 'is-negative': 'github:kevva/is-negative' }) githubNock.done() }) test('from a github repo with different name via named installation', async () => { const project = prepareEmpty() const githubNock = nock('https://github.com', { allowUnmocked: true }) .head('/zkochan/hi') .reply(200) const reporter = jest.fn() const { updatedManifest: manifest } = await addDependenciesToPackage( {}, ['say-hi@github:zkochan/hi#4cdebec76b7b9d1f6e219e06c42d92a6b8ea60cd'], testDefaults({ fastUnpack: false, reporter }) ) const m = project.requireModule('say-hi') expect(reporter).toHaveBeenCalledWith(expect.objectContaining({ added: expect.objectContaining({ dependencyType: 'prod', name: 'say-hi', realName: 'hi', version: '1.0.0', }), level: 'debug', name: 'pnpm:root', } as RootLog)) expect(m).toBe('Hi') expect(manifest.dependencies).toStrictEqual({ 'say-hi': 'github:zkochan/hi#4cdebec76b7b9d1f6e219e06c42d92a6b8ea60cd' }) const lockfile = project.readLockfile() expect(lockfile.importers['.'].dependencies).toStrictEqual({ 'say-hi': { specifier: 'github:zkochan/hi#4cdebec76b7b9d1f6e219e06c42d92a6b8ea60cd', version: 'hi@https://codeload.github.com/zkochan/hi/tar.gz/4cdebec76b7b9d1f6e219e06c42d92a6b8ea60cd', }, }) project.isExecutable('.bin/hi') project.isExecutable('.bin/szia') githubNock.done() }) // This used to fail. Maybe won't be needed once api/install.ts gets refactored and covered with dedicated unit tests test('from a github repo with different name', async () => { const project = prepareEmpty() const githubNock = nock('https://github.com', { allowUnmocked: true }) .head('/zkochan/hi') .reply(200) const reporter = jest.fn() const { updatedManifest: manifest } = await install({ dependencies: { 'say-hi': 'github:zkochan/hi#4cdebec76b7b9d1f6e219e06c42d92a6b8ea60cd', }, }, testDefaults({ fastUnpack: false, reporter })) const m = project.requireModule('say-hi') expect(reporter).toHaveBeenCalledWith(expect.objectContaining({ added: expect.objectContaining({ dependencyType: 'prod', name: 'say-hi', realName: 'hi', version: '1.0.0', }), level: 'debug', name: 'pnpm:root', } as RootLog)) expect(m).toBe('Hi') expect(manifest.dependencies).toStrictEqual({ 'say-hi': 'github:zkochan/hi#4cdebec76b7b9d1f6e219e06c42d92a6b8ea60cd', }) const lockfile = project.readLockfile() expect(lockfile.importers['.'].dependencies).toStrictEqual({ 'say-hi': { specifier: 'github:zkochan/hi#4cdebec76b7b9d1f6e219e06c42d92a6b8ea60cd', version: 'hi@https://codeload.github.com/zkochan/hi/tar.gz/4cdebec76b7b9d1f6e219e06c42d92a6b8ea60cd', }, }) project.isExecutable('.bin/hi') project.isExecutable('.bin/szia') githubNock.done() }) test('a subdependency is from a github repo with different name', async () => { const project = prepareEmpty() const githubNock = nock('https://github.com', { allowUnmocked: true }) .head('/zkochan/hi') .reply(200) await addDependenciesToPackage({}, ['@pnpm.e2e/has-aliased-git-dependency'], testDefaults({ fastUnpack: false })) const m = project.requireModule('@pnpm.e2e/has-aliased-git-dependency') expect(m).toBe('Hi') const lockfile = project.readLockfile() expect(lockfile.snapshots['@pnpm.e2e/has-aliased-git-dependency@1.0.0'].dependencies).toStrictEqual({ '@pnpm.e2e/has-say-hi-peer': '1.0.0(hi@https://codeload.github.com/zkochan/hi/tar.gz/4cdebec76b7b9d1f6e219e06c42d92a6b8ea60cd)', 'say-hi': 'hi@https://codeload.github.com/zkochan/hi/tar.gz/4cdebec76b7b9d1f6e219e06c42d92a6b8ea60cd', }) project.isExecutable('@pnpm.e2e/has-aliased-git-dependency/node_modules/.bin/hi') project.isExecutable('@pnpm.e2e/has-aliased-git-dependency/node_modules/.bin/szia') expect(fs.existsSync(path.resolve(`node_modules/.pnpm/${depPathToFilename('@pnpm.e2e/has-say-hi-peer@1.0.0(hi@https://codeload.github.com/zkochan/hi/tar.gz/4cdebec76b7b9d1f6e219e06c42d92a6b8ea60cd)', 120)}/node_modules/@pnpm.e2e/has-say-hi-peer`))).toBeTruthy() githubNock.done() }) test('from a git repo', async () => { if (isCI) { console.log('not testing the SSH GIT access via CI') return } const project = prepareEmpty() await addDependenciesToPackage({}, ['git+ssh://git@github.com/kevva/is-negative.git'], testDefaults()) project.has('is-negative') }) // This test is unstable due to dependency on third party registry test.skip('from a non-github git repo', async () => { const project = prepareEmpty() await addDependenciesToPackage({}, ['git+http://ikt.pm2.io/ikt.git#3325a3e39a502418dc2e2e4bf21529cbbde96228'], testDefaults()) const m = project.requireModule('ikt') expect(m).toBeTruthy() const lockfile = project.readLockfile() const pkgId = 'ikt.pm2.io/ikt/3325a3e39a502418dc2e2e4bf21529cbbde96228' expect(lockfile.packages).toHaveProperty([pkgId]) expect(lockfile.packages[pkgId].resolution).toStrictEqual({ commit: '3325a3e39a502418dc2e2e4bf21529cbbde96228', repo: 'http://ikt.pm2.io/ikt.git', type: 'git', }) }) test('from a github repo the has no package.json file', async () => { const project = prepareEmpty() const githubNock = nock('https://github.com', { allowUnmocked: true }) .head('/pnpm/for-testing.no-package-json') .times(2) .reply(200) const { updatedManifest: manifest } = await addDependenciesToPackage({}, ['pnpm/for-testing.no-package-json'], testDefaults()) project.has('for-testing.no-package-json') expect(manifest.dependencies).toStrictEqual({ 'for-testing.no-package-json': 'github:pnpm/for-testing.no-package-json', }) fs.rmSync(path.join(project.dir(), 'node_modules'), { recursive: true, force: true, }) fs.rmSync(path.join(project.dir(), 'pnpm-lock.yaml')) // if there is an unresolved promise, this test will hang until timeout. // e.g. thrown: "Exceeded timeout of 240000 ms for a test. await addDependenciesToPackage({}, ['pnpm/for-testing.no-package-json'], testDefaults()) project.has('for-testing.no-package-json') githubNock.done() }) test.skip('from a github repo that needs to be built. isolated node linker is used', async () => { const project = prepareEmpty() const { updatedManifest: manifest } = await addDependenciesToPackage({}, ['pnpm-e2e/prepare-script-works'], testDefaults({ ignoreScripts: true }, { ignoreScripts: true })) project.hasNot('@pnpm.e2e/prepare-script-works/prepare.txt') rimraf('node_modules') await install(manifest, testDefaults({ preferFrozenLockfile: false })) project.has('@pnpm.e2e/prepare-script-works/prepare.txt') rimraf('node_modules') await install(manifest, testDefaults({ frozenLockfile: true })) project.has('@pnpm.e2e/prepare-script-works/prepare.txt') rimraf('node_modules') await install(manifest, testDefaults({ frozenLockfile: true, ignoreScripts: true }, { ignoreScripts: true })) project.hasNot('@pnpm.e2e/prepare-script-works/prepare.txt') }) test.skip('from a github repo that needs to be built. hoisted node linker is used', async () => { const project = prepareEmpty() const { updatedManifest: manifest } = await addDependenciesToPackage( {}, ['pnpm-e2e/prepare-script-works'], testDefaults({ ignoreScripts: true, nodeLinker: 'hoisted' }, { ignoreScripts: true }) ) project.hasNot('@pnpm.e2e/prepare-script-works/prepare.txt') rimraf('node_modules') await install(manifest, testDefaults({ preferFrozenLockfile: false, nodeLinker: 'hoisted' })) project.has('@pnpm.e2e/prepare-script-works/prepare.txt') rimraf('node_modules') await install(manifest, testDefaults({ frozenLockfile: true, nodeLinker: 'hoisted' })) project.has('@pnpm.e2e/prepare-script-works/prepare.txt') rimraf('node_modules') await install(manifest, testDefaults({ frozenLockfile: true, ignoreScripts: true, nodeLinker: 'hoisted' }, { ignoreScripts: true })) project.hasNot('@pnpm.e2e/prepare-script-works/prepare.txt') }) test('re-adding a git repo with a different tag', async () => { const project = prepareEmpty() const githubNock = nock('https://github.com', { allowUnmocked: true }) .head('/kevva/is-negative') .times(2) .reply(200) let { updatedManifest: manifest } = await addDependenciesToPackage({}, ['kevva/is-negative#1.0.0'], testDefaults()) project.has('is-negative') expect(manifest.dependencies).toStrictEqual({ 'is-negative': 'github:kevva/is-negative#1.0.0', }) expect(JSON.parse(fs.readFileSync('./node_modules/is-negative/package.json', 'utf8')).version).toBe('1.0.0') let lockfile = project.readLockfile() expect(lockfile.importers['.'].dependencies?.['is-negative']).toEqual({ specifier: 'github:kevva/is-negative#1.0.0', version: 'https://codeload.github.com/kevva/is-negative/tar.gz/163360a8d3ae6bee9524541043197ff356f8ed99', }) expect(lockfile.packages).toEqual( { 'is-negative@https://codeload.github.com/kevva/is-negative/tar.gz/163360a8d3ae6bee9524541043197ff356f8ed99': { resolution: { tarball: 'https://codeload.github.com/kevva/is-negative/tar.gz/163360a8d3ae6bee9524541043197ff356f8ed99' }, version: '1.0.0', engines: { node: '>=0.10.0' }, }, } ) manifest = (await addDependenciesToPackage(manifest, ['kevva/is-negative#1.0.1'], testDefaults())).updatedManifest project.has('is-negative') expect(JSON.parse(fs.readFileSync('./node_modules/is-negative/package.json', 'utf8')).version).toBe('1.0.1') lockfile = project.readLockfile() expect(lockfile.importers['.'].dependencies?.['is-negative']).toEqual({ specifier: 'github:kevva/is-negative#1.0.1', version: 'https://codeload.github.com/kevva/is-negative/tar.gz/f7dec4d66a5a56719e49b9f94a24d73f924ddeb3', }) expect(lockfile.packages).toEqual( { 'is-negative@https://codeload.github.com/kevva/is-negative/tar.gz/f7dec4d66a5a56719e49b9f94a24d73f924ddeb3': { resolution: { tarball: 'https://codeload.github.com/kevva/is-negative/tar.gz/f7dec4d66a5a56719e49b9f94a24d73f924ddeb3' }, version: '1.0.1', engines: { node: '>=0.10.0' }, }, } ) githubNock.done() }) test('should not update when adding unrelated dependency', async () => { process.chdir(withGitProtocolDepFixture) fs.rmSync('./node_modules', { recursive: true, force: true, }) let manifest = JSON.parse(fs.readFileSync('./package.json', 'utf8')) await install(manifest, testDefaults({ preferFrozenLockfile: false, dir: withGitProtocolDepFixture, lockfileDir: withGitProtocolDepFixture })) expect(fs.readdirSync('./node_modules/.pnpm')).toContain('is-negative@https+++codeload.github.com+kevva+is-negative+tar.gz+1d7e288222b53a0cab90a331f1865220ec29560c') // cspell:disable-line manifest = (await addDependenciesToPackage(manifest, ['is-number'], testDefaults({ preferFrozenLockfile: false, modulesCacheMaxAge: 0 }))).updatedManifest expect(manifest.dependencies).toHaveProperty('is-number') expect(manifest.dependencies['is-negative']).toBe('github:kevva/is-negative#master') const project = assertProject(withGitProtocolDepFixture) project.has('is-number') expect(fs.existsSync('./node_modules/.pnpm/is-negative@https+++codeload.github.com+kevva+is-negative+tar.gz+1d7e288222b53a0cab90a331f1865220ec29560c')).toBe(true) // cspell:disable-line expect(project.readLockfile().importers['.'].dependencies).toEqual({ 'is-negative': { specifier: 'github:kevva/is-negative#master', version: 'https://codeload.github.com/kevva/is-negative/tar.gz/1d7e288222b53a0cab90a331f1865220ec29560c', }, 'is-number': { specifier: '^7.0.0', version: '7.0.0', }, }) }) test('git-hosted repository is not added to the store if it fails to be built', async () => { prepareEmpty() await expect( addDependenciesToPackage({}, ['pnpm-e2e/prepare-script-fails'], testDefaults()) ).rejects.toThrow() await expect( addDependenciesToPackage({}, ['pnpm-e2e/prepare-script-fails'], testDefaults()) ).rejects.toThrow() }) test('from subdirectories of a git repo', async () => { const project = prepareEmpty() const githubNock = nock('https://github.com', { allowUnmocked: true }) .head('/RexSkz/test-git-subfolder-fetch') .times(2) .reply(200) const { updatedManifest: 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', ], testDefaults()) project.has('@my-namespace/simple-react-app') 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', }) githubNock.done() }) test('no hash character for github subdirectory install', async () => { prepareEmpty() const githubNock = nock('https://github.com', { allowUnmocked: true }) .head('/pnpm/only-allow') .reply(200) await addDependenciesToPackage({}, [ 'github:pnpm/only-allow#path:/&v1.2.1', ], testDefaults()) expect(fs.readdirSync('./node_modules/.pnpm')) .toContain('only-allow@https+++codeload.github.com+pnpm+only-allow+tar.gz+91ab41994c6a1b7319869fa8864163c9954f56ec+path++') githubNock.done() })