pnpm 기여

pnpmgitopen-sourceturbopack

외부 라이브러리를 패치해보려고 pnpm의 patch, patch-commit을 사용하게 되었고, 패치를 저장하는 단계에서 pnpm이 깨졌다.

pnpm patch / patch commit

수정하려는 버전을 지정하고 patch 명령어를 사용하면

pnpm patch is-positive@1.0.0

pnpm이 패키지를 수정 가능한 임시 디렉터리에 풀고 경로를 알려준다

You can now edit the following folder:
node_modules/.pnpm_patches/is-positive@1.0.0

파일을 수정한 뒤에 pnpm patch-commit으로 차이를 확정한다

pnpm patch-commit node_modules/.pnpm_patches/is-positive@1.0.0

그럼 pnpm에서 원본 패키지와 수정본의 차이를 계산해서 patches에 등록하고 patchedDependencies에 패치를 등록한다

# pnpm-workspace.yaml
patchedDependencies:
  is-positive@1.0.0: patches/is-positive@1.0.0.patch
root
├── patches
│   └── is-positive@1.0.0.patch
├── pnpm-workspace.yaml
└── pnpm-lock.yaml

그럼 이후부터는 해당 버전을 설치할 때 같은 패치를 적용하게 된다.

아, 그리고 patch-commit은 수정 디렉토리를 pnpm 패치로 확정할 뿐 실제 git commit을 해주는건 아니기 때문에 commit은 따로 해줘야한다.

patch-commit이 안되는뎁쇼 ..?

patch-commit을 하려고 보니 다음과 같은 오류가 발생했다.

[ERR_PNPM_INVALID_PATCH]
Bad diff line: diff --git a/index.js "b/tmp/pnpm-patch-\355\225\234\352\270\200/index.js"

일단 patch-commit이 동작하는 순서는 다음과 같다.

  1. 수정 디렉토리 읽기
  2. pnpm patch가 만든 디렉토리인지 검증
  3. 원본 패키지 준비
  4. 원본과 수정본의 diff 생성
  5. patches/*.patch 저장
  6. patchedDependencies 갱신
  7. install을 다시 실행해 패치 적용

여기서 문제가 되는 부분은 4번 diff를 하는 diffFolders 부분이었는데 논의에 필요한 부분만 가져오면 다음과 같다.

async function diffFolders(folderA: string, folderB: string): Promise<string> {
  // ...
  let stdout!: string

  try {
    const result = await execa("git", [
      "-c", "core.safecrlf=false",
      "diff",
      "--src-prefix=a/",
      "--dst-prefix=b/",
      "--ignore-cr-at-eol",
      "--irreversible-delete",
      "--full-index",
      "--no-index",
      "--text",
      "--no-ext-diff",
      "--no-color",
      folderAN,
      folderBN,
    ])
    stdout = result.stdout as string
  } catch (err: any) {
    stdout = err.stdout as string
    // ...
  }
  // ...
  return stdout
    .replace(new RegExp(`(a|b)(${escapeStringRegexp(`/${removeTrailingAndLeadingSlash(folderAN)}/`)})`, 'g'), '$1/')
    .replace(new RegExp(`(a|b)${escapeStringRegexp(`/${removeTrailingAndLeadingSlash(folderBN)}/`)}`, 'g'), '$1/')
    .replace(new RegExp(escapeStringRegexp(`${folderAN}/`), 'g'), '')
    .replace(new RegExp(escapeStringRegexp(`${folderBN}/`), 'g'), '')
    .replace(/\n\\ No newline at end of file\n$/, '\n')
    .replace(/^diff --git a\/.*\.DS_Store b\/.*\.DS_Store[\s\S]+?(?=^diff --git)/gm, '')
    .replace(/^diff --git a\/.*\.DS_Store b\/.*\.DS_Store[\s\S]*$/gm, '')
}

pnpm이 알고 있는 값: /tmp/pnpm-patch-한글

git이 출력한 값: /tmp/pnpm-patch-\355\225\234\352\270\200

그래서 replace 정규식에서 제대로 걸리지 않은채로 diff --git a/index.js "b/tmp/pnpm-patch-\355\225\234\352\270\200/index.js" 이런식으로 반환되게 되고 해당 내용이 패치를 적용하는 파서에 읽히면서 ERR_PNPM_INVALID_PATCH 오류가 발생하는 것.

git의 출력을 pnpm과 맞추자

git은 기본적으로 한글 같은 비ASCII 경로를 이스케이프해서 출력하기 때문에 git이 비 ASCII 경로를 이스케이프하지 않도록 하는것이 해결방법이었다.

"-c", "core.quotePath=false"
// 일회성 옵션 지정
-c

// core.quotePath (default: true)

// true: 0x80보다 큰 바이트를 8진수 형태로 표시
core.quotePath=true
/tmp/pnpm-patch-\355\225\234\352\270\200

// false: 비ASCII 바이트를 그대로 출력
core.quotePath=false
/tmp/pnpm-patch-한글

정확히는 core.quotePath=false가 0x80보다 큰 바이트만 “특이한 문자” 취급에서 제외하며, 따옴표·백슬래시·제어 문자는 여전히 이스케이프된다.

한 가지 더, 기존 명령에 있는 core.safecrlf=false도 무슨 옵션인지 궁금했다.

If true, makes Git check if converting CRLF is reversible when end-of-line conversion is active.

텍스트 파일에서 줄바꿈을 표현하는 방식은 주로 2가지로 LF (\n, 주로 Linux, macOS), CRLF (\r\n, 주로 Windows)가 있다. 예를 들어서 windows(CRLF)에서 아래와 같은 변환이 가능한데,

// 원본
A\r\n
B\n

// git에 삽입 (LF로 바뀜)
A\n
B\n

// checkout (CRLF로 돌아옴)
A\r\n
B\r\n

이렇게 되면 원본과 달라지게 된다. 그리고 safecrlf=false는 줄바꿈 변환 자체를 끄는 것이 아니라 원본으로 되돌릴 수 없는 비가역 변환 검사를 끄는 값이다. 어차피 pnpm에서는 차이만 계산하기 때문에 이것때문에 중단될 필요가 없기 때문에 false로 해버리는것.

-- 넣어주세요

TypeScript 구현뿐 아니라 Rust 포트에도 같은 설정을 반영했고, 몇가지 테스트도 추가하고 PR (새 창에서 열림)을 올렸다. PR 올렸더니 AI 리뷰로 -custom-edit-dir 이런 경로가 오면 옵션으로 해석될 여지가 있다고 옵션과 경로 인자 사이에 -- 를 넣어달라고 했다.

사실상 핵심 수정은 이게 전부다.

결국 머지까지 완료!

푸핫 축하도 받았다

참고 자료