외부 라이브러리를 패치해보려고 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이 동작하는 순서는 다음과 같다.
- 수정 디렉토리 읽기
- pnpm patch가 만든 디렉토리인지 검증
- 원본 패키지 준비
- 원본과 수정본의 diff 생성
- patches/*.patch 저장
- patchedDependencies 갱신
- 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
CRLFis 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 이런 경로가 오면 옵션으로 해석될 여지가 있다고 옵션과 경로 인자 사이에 -- 를 넣어달라고 했다.
결국 머지까지 완료!