mirror of
https://github.com/pnpm/pnpm.git
synced 2026-06-27 09:25:24 -04:00
The TypeScript pnpm CLI freezes at v11; pnpm 12 will be the Rust pacquet port. To make that split legible, all TypeScript source, test, and build directories move under a new top-level pnpm11/ directory. The name states the version boundary rather than implying a behavioral fork, since the two stacks are meant to behave identically. Scope is source-only: the shared workspace root stays at the repo root. pnpm-workspace.yaml, package.json, pnpm-lock.yaml, .pnpmfile.cjs, .meta-updater, __patches__, .changeset, .husky, and the lint/spell configs remain in place, so one pnpm workspace and one Cargo workspace still span all three products. pnpr/client and pacquet/tasks/registry-mock stay as cross-product workspace members. Rewiring the move required: - pnpm-workspace.yaml globs prefixed with pnpm11/ - root package.json script paths, eslint.config.mjs, tsconfig.lint.json, .gitignore, and CODEOWNERS updated - .meta-updater/src/index.ts literals repointed (pnpm11/pnpm/package.json, pnpm11/__utils__, pnpm11/__typings__, and the main package directory) - regenerated every moved package's repository/homepage URL via meta-updater - pnpm11/pnpm/bundle-deps.ts and __utils__/scripts/src/typecheck-only.ts climb one more level to reach the repo root .meta-updater stays at the repo root because @pnpm/meta-updater resolves its config at <cwd>/.meta-updater/main.mjs. TS CI (.github/workflows/ci.yml) now only runs when pnpm11/-relevant paths change, via a dorny/paths-filter changes job plus a TS CI / Success aggregate gate; branch protection should require only that gate.
168 lines
6.4 KiB
TypeScript
168 lines
6.4 KiB
TypeScript
import fs from 'node:fs'
|
|
import os from 'node:os'
|
|
import path from 'node:path'
|
|
|
|
import { afterEach, beforeEach, expect, jest, test } from '@jest/globals'
|
|
|
|
import type { PatchRemoveCommandOptions } from '../src/patchRemove.js'
|
|
|
|
jest.unstable_mockModule('@pnpm/installing.commands', () => ({
|
|
install: {
|
|
handler: jest.fn(),
|
|
},
|
|
}))
|
|
|
|
jest.unstable_mockModule('../src/updatePatchedDependencies.js', () => ({
|
|
updatePatchedDependencies: jest.fn(),
|
|
}))
|
|
|
|
const { install } = await import('@pnpm/installing.commands')
|
|
const patchRemove = await import('../src/patchRemove.js')
|
|
const { updatePatchedDependencies } = await import('../src/updatePatchedDependencies.js')
|
|
|
|
const installHandler = jest.mocked(install.handler)
|
|
const updatePatchedDependenciesMock = jest.mocked(updatePatchedDependencies)
|
|
const testOnNonWindows = process.platform === 'win32' ? test.skip : test
|
|
|
|
let tempRoot: string
|
|
|
|
beforeEach(() => {
|
|
tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'pnpm-patch-remove-'))
|
|
installHandler.mockResolvedValue(undefined)
|
|
updatePatchedDependenciesMock.mockResolvedValue(undefined)
|
|
})
|
|
|
|
afterEach(() => {
|
|
installHandler.mockReset()
|
|
updatePatchedDependenciesMock.mockReset()
|
|
fs.rmSync(tempRoot, { force: true, recursive: true })
|
|
})
|
|
|
|
test('patch-remove rejects traversal outside the patches directory before deleting any patch', async () => {
|
|
const projectDir = path.join(tempRoot, 'project')
|
|
const outsideFile = path.join(tempRoot, 'outside.patch')
|
|
const goodPatch = path.join(projectDir, 'patches/good.patch')
|
|
fs.mkdirSync(path.dirname(goodPatch), { recursive: true })
|
|
fs.writeFileSync(goodPatch, 'good patch', 'utf8')
|
|
fs.writeFileSync(outsideFile, 'outside patch', 'utf8')
|
|
|
|
await expect(patchRemove.handler(createOptions(projectDir, {
|
|
good: 'patches/good.patch',
|
|
bad: '../outside.patch',
|
|
}), ['good', 'bad'])).rejects.toMatchObject({
|
|
code: 'ERR_PNPM_PATCH_FILE_OUTSIDE_PATCHES_DIR',
|
|
})
|
|
|
|
expect(fs.existsSync(goodPatch)).toBe(true)
|
|
expect(fs.existsSync(outsideFile)).toBe(true)
|
|
expect(updatePatchedDependenciesMock).not.toHaveBeenCalled()
|
|
expect(installHandler).not.toHaveBeenCalled()
|
|
})
|
|
|
|
test('patch-remove rejects directory entries before deleting any patch', async () => {
|
|
const projectDir = path.join(tempRoot, 'project')
|
|
const goodPatch = path.join(projectDir, 'patches/good.patch')
|
|
const patchDir = path.join(projectDir, 'patches/not-a-file.patch')
|
|
fs.mkdirSync(patchDir, { recursive: true })
|
|
fs.writeFileSync(goodPatch, 'good patch', 'utf8')
|
|
|
|
await expect(patchRemove.handler(createOptions(projectDir, {
|
|
good: 'patches/good.patch',
|
|
bad: 'patches/not-a-file.patch',
|
|
}), ['good', 'bad'])).rejects.toMatchObject({
|
|
code: 'ERR_PNPM_PATCH_FILE_IS_DIRECTORY',
|
|
})
|
|
|
|
expect(fs.existsSync(goodPatch)).toBe(true)
|
|
expect(updatePatchedDependenciesMock).not.toHaveBeenCalled()
|
|
expect(installHandler).not.toHaveBeenCalled()
|
|
})
|
|
|
|
testOnNonWindows('patch-remove rejects a nested parent symlink outside the patches directory before unlinking a dangling target', async () => {
|
|
const projectDir = path.join(tempRoot, 'project')
|
|
const patchesDir = path.join(projectDir, 'patches')
|
|
const outsideDir = path.join(tempRoot, 'outside')
|
|
const outsideDanglingLink = path.join(outsideDir, 'dangling.patch')
|
|
fs.mkdirSync(patchesDir, { recursive: true })
|
|
fs.mkdirSync(outsideDir, { recursive: true })
|
|
fs.symlinkSync(outsideDir, path.join(patchesDir, 'linked-dir'), 'dir')
|
|
fs.symlinkSync(path.join(tempRoot, 'missing-target.patch'), outsideDanglingLink)
|
|
|
|
await expect(patchRemove.handler(createOptions(projectDir, {
|
|
bad: 'patches/linked-dir/dangling.patch',
|
|
}), ['bad'])).rejects.toMatchObject({
|
|
code: 'ERR_PNPM_PATCH_FILE_OUTSIDE_PATCHES_DIR',
|
|
})
|
|
|
|
expect(fs.lstatSync(outsideDanglingLink).isSymbolicLink()).toBe(true)
|
|
expect(updatePatchedDependenciesMock).not.toHaveBeenCalled()
|
|
expect(installHandler).not.toHaveBeenCalled()
|
|
})
|
|
|
|
testOnNonWindows('patch-remove unlinks a final symlink inside the patches directory without touching its target', async () => {
|
|
const projectDir = path.join(tempRoot, 'project')
|
|
const patchesDir = path.join(projectDir, 'patches')
|
|
const outsideTarget = path.join(tempRoot, 'outside-target.patch')
|
|
const patchLink = path.join(patchesDir, 'linked.patch')
|
|
fs.mkdirSync(patchesDir, { recursive: true })
|
|
fs.writeFileSync(outsideTarget, 'outside target', 'utf8')
|
|
fs.symlinkSync(outsideTarget, patchLink)
|
|
|
|
await patchRemove.handler(createOptions(projectDir, {
|
|
pkg: 'patches/linked.patch',
|
|
}), ['pkg'])
|
|
|
|
expect(fs.existsSync(patchLink)).toBe(false)
|
|
expect(fs.readFileSync(outsideTarget, 'utf8')).toBe('outside target')
|
|
expect(updatePatchedDependenciesMock).toHaveBeenCalledWith({}, expect.any(Object))
|
|
expect(installHandler).toHaveBeenCalledWith(expect.objectContaining({
|
|
patchedDependencies: {},
|
|
}))
|
|
})
|
|
|
|
test('patch-remove allows a symlinked patches directory that resolves inside the project', async () => {
|
|
const projectDir = path.join(tempRoot, 'project')
|
|
const realPatchesDir = path.join(projectDir, 'real-patches')
|
|
const patchFile = path.join(realPatchesDir, 'pkg.patch')
|
|
fs.mkdirSync(realPatchesDir, { recursive: true })
|
|
fs.symlinkSync(realPatchesDir, path.join(projectDir, 'patches'), process.platform === 'win32' ? 'junction' : 'dir')
|
|
fs.writeFileSync(patchFile, 'patch', 'utf8')
|
|
|
|
await patchRemove.handler(createOptions(projectDir, {
|
|
pkg: 'patches/pkg.patch',
|
|
}), ['pkg'])
|
|
|
|
expect(fs.existsSync(patchFile)).toBe(false)
|
|
expect(updatePatchedDependenciesMock).toHaveBeenCalledWith({}, expect.any(Object))
|
|
expect(installHandler).toHaveBeenCalledWith(expect.objectContaining({
|
|
patchedDependencies: {},
|
|
}))
|
|
})
|
|
|
|
test('patch-remove keeps missing patch files as no-ops', async () => {
|
|
const projectDir = path.join(tempRoot, 'project')
|
|
fs.mkdirSync(path.join(projectDir, 'patches'), { recursive: true })
|
|
|
|
await patchRemove.handler(createOptions(projectDir, {
|
|
pkg: 'patches/missing.patch',
|
|
}), ['pkg'])
|
|
|
|
expect(updatePatchedDependenciesMock).toHaveBeenCalledWith({}, expect.any(Object))
|
|
expect(installHandler).toHaveBeenCalledWith(expect.objectContaining({
|
|
patchedDependencies: {},
|
|
}))
|
|
})
|
|
|
|
function createOptions (
|
|
projectDir: string,
|
|
patchedDependencies: Record<string, string>
|
|
): PatchRemoveCommandOptions {
|
|
return {
|
|
dir: projectDir,
|
|
lockfileDir: projectDir,
|
|
patchedDependencies,
|
|
rootProjectManifest: {},
|
|
rootProjectManifestDir: projectDir,
|
|
} as PatchRemoveCommandOptions
|
|
}
|