본문 바로가기
빌드노트 검색

빌드노트

npm pack --dry-run인데 prepack이 생성 파일을 바꾼다

npm 10.9.8과 11.6.0에서 pack --dry-run이 생성 파일을 바꾸고도 tarball은 저장하지 않는 동작을 확인했다. 10.9.8에서는 ignore-scripts에도 prepare가 실행돼, 버전별 hook 확인과 검사 전용 사본의 목록 조회를 구분한다.

npm pack --dry-run으로 배포 파일 목록만 보려 했는데 생성 파일이 바뀌었다면, package.json의 prepack, prepare, postpack부터 읽는다. tarball을 디스크에 저장하지 않는 것과 lifecycle 스크립트를 실행하지 않는 것은 별개다. 직접 비교한 npm 10.9.8과 11.6.0에서는 dry-run이어도 세 hook이 실행됐다.

--ignore-scripts를 추가하는 방법에도 버전 확인이 필요했다. npm 11.6.0에서는 세 hook이 모두 생략됐지만, npm 10.9.8에서는 prepare가 실행됐다. 따라서 작업 폴더를 바꾸지 않으려는 검사라면 옵션 이름만 보고 실행부터 해서는 안 된다.

먼저 어떤 파일을 만드는 스크립트인지 읽는다

Node.js와 npm이 설치된 Windows PowerShell에서, 확인할 패키지의 package.json이 있는 폴더를 현재 위치로 둔다. 다음 조회는 hook을 실행하지 않고 버전과 등록된 명령을 보여 준다.

npm.cmd --version
$package = Get-Content -LiteralPath './package.json' -Raw | ConvertFrom-Json
$package.scripts | Format-List prepack, prepare, postpack

prepack이 npm run build를 호출한다면 build와 그 명령이 읽는 소스까지 따라간다. 생성 파일을 덮어쓰는지, 출력 폴더를 지우는지, 다른 명령을 호출하는지 확인해야 한다. prepare와 postpack도 같은 방식으로 읽는다. 조회 결과에 hook이 없다면 이 글의 재현 조건과 다르므로, 파일을 바꾸는 다른 명령부터 찾는다.

npm의 scripts 문서 (새 탭에서 열림)는 일반 npm pack의 순서를 prepack → prepare → postpack으로 설명한다. 이 순서를 안다고 dry-run에서 무엇이 생략되는지까지 확인한 것은 아니다. 그래서 세 hook에 각각 표식을 남기는 별도 패키지로 실행을 비교했다.

dry-run에서도 생성과 표식 기록은 끝났다

실험 패키지는 외부 의존성이 없는 pack-side-effect-lab@1.0.0이었다. 각 조건을 새 폴더에서 시작했고, 처음에는 generated.txt에 seed와 줄바꿈만 있었다. prepack은 이 파일을 generated by prepack과 줄바꿈으로 덮어썼다. 세 hook은 실행 순서를 events.log에 추가하고 각각 markers/prepack.txt, markers/prepare.txt, markers/postpack.txt를 만들었다. 패키징 대상은 generated.txt와 markers/로 지정했다. 실제 업무에서 발생한 장애가 아닌 합성 재현이다.

자기 패키지에서도 변경 여부를 확인하려면 검토한 스크립트를 실행해도 되는 작업 사본에서 비교한다. 아래 ./generated.txt를 그 스크립트가 덮어쓰는 기존 파일 경로로 바꾼다. 파일이 없다면 해시 비교를 진행할 수 없으므로 생성 전제부터 확인한다. 예제의 Node 호출은 절대 경로로 고정했다.

$generated = './generated.txt'
$before = (Get-FileHash -LiteralPath $generated -Algorithm SHA256 -ErrorAction Stop).Hash
npm.cmd pack --dry-run --foreground-scripts --ignore-scripts=false --offline
if ($LASTEXITCODE -ne 0) { throw 'pack failed' }
$unchanged = $before -eq (Get-FileHash -LiteralPath $generated -Algorithm SHA256 -ErrorAction Stop).Hash
"generated unchanged: $unchanged"

False면 지정한 파일의 내용이 바뀐 것이다. True여도 다른 파일의 변경이나 새 파일 생성까지 없었다는 뜻은 아니다. --foreground-scripts는 hook 출력을 볼 수 있게 한다. --ignore-scripts=false는 이 비교에서 스크립트 실행을 명시한 값이다. --offline은 npm의 온라인 조회를 막기 위해 붙였으며, hook 안의 임의 네트워크 호출을 차단하는 장치는 아니다. 이 예제에는 그런 호출이 없다.

npm 10.9.8에서 위 명령을 실행한 출력 중 판단에 필요한 부분은 다음과 같았다. npm의 나머지 안내는 생략했다.

HOOK prepack
HOOK prepare
HOOK postpack
pack-side-effect-lab-1.0.0.tgz
generated unchanged: False

파일 이름이 출력됐지만 해당 .tgz는 없었다. 새 폴더에 예전 tarball이 없는 상태에서 Test-Path -LiteralPath './pack-side-effect-lab-1.0.0.tgz'를 실행한 결과도 False였다. 반면 generated.txt의 내용은 바뀌었고 세 marker가 모두 생겼다. npm 11.6.0에서도 별도 실행으로 같은 hook 순서, 파일 변경과 tarball 부재를 확인했다.

다른 새 폴더에서 dry-run을 끈 일반 pack은 같은 순서로 세 hook을 실행하고 실제 .tgz를 저장했다. 두 버전 모두 그 아카이브 안의 generated.txt는 generated by prepack이었다. 이번 비교에서 dry-run은 생성 파일 변경을 되돌리지 않았고 tarball 저장만 생략했다.

--json으로 받은 목록에는 두 버전 모두 generated.txt, markers/prepack.txt, markers/prepare.txt, package.json이 있었다. 로컬에 생긴 markers/postpack.txt는 없었다. 일반 pack의 실제 아카이브도 같은 네 항목이었다. postpack 표식은 포장이 끝난 뒤 만들어졌으므로 로컬 파일 목록과 포장 목록이 달랐다. hook 로그가 조용했던 기본 dry-run 실행에서도 세 표식은 생겼다. 출력이 없다는 이유로 실행되지 않았다고 판단할 수 없었다.

npm pack 문서 (새 탭에서 열림)의 dry-run 안내만으로 파일 변경이 전혀 없다고 기대하기 쉽다. 실행한 버전의 구현을 보면 이유가 더 분명하다. 10.9.8의 pack 코드 (새 탭에서 열림)와 11.6.0의 같은 코드 (새 탭에서 열림)는 tarball 저장 조건과 prepack·postpack 실행 조건을 따로 검사한다. 두 hook을 건너뛰는 조건은 dry-run이 아니라 ignoreScripts다. 이는 앞의 직접 관찰과 일치한다.

ignore-scripts도 실행 버전에서 확인한다

세 hook이 등록된 새 패키지에 --ignore-scripts를 붙여 다시 비교했다. dry-run과 일반 pack을 각각 실행한 결과는 다음과 같았다.

  • npm 10.9.8: prepack과 postpack은 생략됐지만 prepare는 실행됐다. generated.txt는 seed 그대로였고 markers/prepare.txt와 실행 기록이 생겼다.
  • npm 11.6.0: 세 hook이 모두 생략됐다. generated.txt는 seed 그대로였고 marker와 실행 기록은 없었다.
  • 두 버전 모두 dry-run에서는 .tgz가 없고 일반 pack에서는 있었다. hook 실행 여부와 아카이브 저장 여부는 이 대조에서도 별개였다.

공식 옵션 설명 (새 탭에서 열림)은 ignore-scripts로 package.json 스크립트를 생략한다고 안내한다. 그러나 npm 10.9.8의 이 실행 경로에서는 그 설명만으로 prepare 생략을 보장할 수 없었다. 10.9.8에 포함된 pacote의 디렉터리 포장 코드 (새 탭에서 열림)에는 prepare 호출 전 ignoreScripts 검사가 없고, 11.6.0의 같은 코드 (새 탭에서 열림)에는 있다. 모든 npm 10 또는 11 버전이 각각 같다고 확대할 수는 없다.

현재 파일의 포함 목록만 확인할 때

이미 생성된 파일이 포장 대상인지 보는 목적이라면 생성 작업을 다시 실행할 필요가 없다. npm 11.6.0의 위 조건에서는 pack --dry-run --json --ignore-scripts로 세 hook을 생략할 수 있었다. npm 10.9.8처럼 prepare 생략을 확인하지 못한 환경에서는 검사 전용 사본에서 scripts를 제거하고 목록을 볼 수 있다.

다음은 패키지 밖에 따로 준비한 검사 사본에서만 실행한다. 필요한 생성 파일과 포함·제외 규칙은 복사돼 있어야 하며, 원본 package.json에 적용하지 않는다. 이 사본의 결과물을 배포하지 않는다.

$package = Get-Content -LiteralPath './package.json' -Raw | ConvertFrom-Json
$package.PSObject.Properties.Remove('scripts')
$package | ConvertTo-Json -Depth 100 | Set-Content -LiteralPath './package.json' -Encoding utf8
$pack = npm.cmd pack --dry-run --json --ignore-scripts --offline
if ($LASTEXITCODE -ne 0) { throw 'pack failed' }
($pack | ConvertFrom-Json)[0].files.path

이 경로를 두 버전의 새 사본에서 실행했을 때 출력은 generated.txt, package.json이었다. hook 표식과 .tgz는 없었고 generated.txt는 seed 그대로였다. npm 10.9.8에서는 위 PowerShell 입력도 그대로 실행해 확인했다. 명령이 실패하면 목록 누락으로 판단하지 말고 npm 오류부터 읽는다.

이 검사는 현재 사본에 있는 파일의 포함 여부만 답한다. 스크립트를 제거하면서 package.json 내용도 달라지므로 원본의 최종 배포본을 검증한 결과는 아니다. 원래 prepack이나 prepare가 필수 파일을 만드는 패키지라면, 파일이 없는 사본의 목록만 보고 정상 배포라고 판단해서는 안 된다. 그때는 검토한 생성 작업을 별도 작업 폴더에서 수행하고 일반 pack의 실제 아카이브까지 확인한다.

직접 실행한 범위는 Windows, PowerShell 7.6.6, Node.js 22.23.2, npm 10.9.8·11.6.0의 단일 로컬 패키지다. npm 버전은 전역 설정을 바꾸지 않고 설치된 CLI를 각각 호출했다. 확인 당시 v11 웹 문서는 11.21.1로 표시돼 일반 설명과 실행 버전의 코드를 구분했다. 워크스페이스, Git·원격 패키지, Linux, 다른 npm 버전과 레지스트리 발행은 실행하지 않았다.

← 전체 글 보기