Recipes

# Renovate 의존성 범프 후 npm workspaces 모노레포에서 qs 모듈 해석 문제 수정 ## 문제 Renovate가 `qs`를 범프한 뒤 npm workspaces 모노레포에서 모듈 해석이 깨졌습니다. 일반적인 증상은 다음과 같습니다: - `Cannot find module 'qs'` 또는 `ERR_MODULE_NOT_FOUND` - 워크스페이스 중 하나만 실패하고 나머지는 정상 동작 - 로컬에서는 통과하지만 CI에서 실패 - `npm ls qs`가 중복되거나 유효하지 않은(extraneous) 항목을 표시 ## 원인 npm workspaces는 의존성을 루트 `node_modules`로 호이스팅합니다. Renovate가 `qs`를 범프할 때 다음과 같은 상황이 발생할 수 있습니다: 1. **버전 불일치**: 서로 다른 워크스페이스가 서로 다른 `qs` 버전을 요구하게 되어, 하나는 루트로 호이스팅되고 다른 하나는 워크스페이스 로컬 `node_modules`에 중첩됩니다. 2. **오래된 lockfile**: `package-lock.json`이 부분적으로만 갱신되어 해석 트리가 실제 `package.json`과 어긋납니다. 3. **중첩된 `node_modules`**: 이전 설치에서 남은 워크스페이스 로컬 `node_modules/qs`가 루트 호이스팅된 버전을 가립니다. 4. **peer/transitive 불일치**: `qs`에 의존하는 패키지(예: `express`, `body-parser`)가 범프된 버전과 호환되지 않는 버전을 고정하고 있을 수 있습니다. ## 해결 방법 ### 1. 현재 상태 진단 ```bash npm ls qs --all ``` 중복, `invalid`, `extraneous` 항목을 확인합니다. 어떤 워크스페이스가 어떤 버전을 해석하는지 파악합니다. ### 2. 클린 재설치 ```bash # 워크스페이스 로컬 node_modules 포함해 모두 제거 rm -rf node_modules package-lock.json find . -name node_modules -type d -prune -exec rm -rf {} + npm install ``` 이렇게 하면 lockfile이 `package.json`과 일치하게 다시 생성되고, 남아 있던 중첩 `qs`가 제거됩니다. ### 3. 버전 정렬 루트 `package.json`에 `overrides`를 추가해 `qs` 버전을 하나로 고정합니다: ```json { "overrides": { "qs": "^6.13.0" } } ``` 그런 다음 다시 설치합니다: ```bash npm install npm ls qs --all ``` 이제 모든 워크스페이스가 동일한 호이스팅된 `qs`를 해석해야 합니다. ### 4. Renovate 설정 조정 재발을 막기 위해 Renovate가 `qs`를 범프할 때 lockfile을 완전히 갱신하도록 설정합니다: ```json { "lockFileMaintenance": { "enabled": true, "schedule": ["before 5am on monday"] }, "packageRules": [ { "matchPackageNames": ["qs"], "groupName": "qs", "rangeStrategy": "replace" } ] } ``` `rangeStrategy: "replace"`는 범프 시 `package.json`의 범위를 갱신해 lockfile과의 불일치를 줄입니다. ### 5. CI 검증 CI에서 클린 설치와 해석 검사를 수행합니다: ```bash npm ci npm ls qs --all ``` `npm ls`가 0이 아닌 종료 코드를 반환하면 빌드가 실패하도록 하여, 해석 문제가 조용히 지나가지 않게 합니다. ## 확인 ```bash npm ls qs --all ``` 출력에 단일 해석된 버전이 표시되고 `invalid`/`extraneous` 항목이 없어야 합니다. 그런 다음 실패했던 워크스페이스에서 테스트를 실행해 모듈이 정상적으로 해석되는지 확인합니다.

README 링크에서 발견 · today

Software & DevOps

Renovate 범프로 인해 깨진 라이브러리 워크스페이스 테스트의 qs 해석을 복원

What they did

Renovate PR이 qs를 ^6.17.0으로 올렸지만, 라이브러리 워크스페이스 테스트 실행이 "Cannot find module 'qs'"로 실패했습니다. 작성자는 실패 원인을 co-body(@web/test-runner-core를 통해 끌어옴)로 추적했는데, 이 패키지는 런타임에 qs가 필요하지만 lockfile 업데이트가 테스트 의존성 체인이 의존하던 설치 경로를 제거하면서 qs를 더 이상 해석할 수 없게 된 것이었습니다. 테스트를 느슨하게 하거나 건너뛰는 대신, 작성자는 세 가지 조율된 수정으로 원인을 바로잡았습니다: 라이브러리 테스트 툴체인이 우연한 루트 lockfile 상태에 의존하지 않고 명시적인 제공자를 갖도록 packages/realtime-bpm-analyzer devDependencies에 qs ^6.17.0을 추가했고, 저장소 전반의 강제 버전을 Renovate 범프와 정렬시키기 위해 루트 overrides.qs를 ^6.17.0으로 업데이트했으며, node_modules/qs가 6.17.1로 존재하고 해석되도록, 그리고 라이브러리 워크스페이스가 dev 의존성 집합에 qs를 기록하도록 package-lock.json을 수정했습니다. 변경은 가능한 한 작게 유지되었고 원래 Renovate PR을 닫을 수 있도록 같은 PR에서 이루어졌습니다.

How it went

CI는 qs 버전 업그레이드가 적용된 상태에서 통과했습니다. 이 수정은 증상이 아니라 해결 원인을 다루는, 작고 표적화된 세 파일 변경(워크스페이스 package.json, 루트 package.json overrides, lockfile)입니다. 테스트는 완화되거나 건너뛰어지거나 삭제되지 않았으며, 이미 업그레이드 중이던 의존성 외에 새로운 의존성은 추가되지 않았습니다.

Worth knowing

해당 글은 실제 CI 실행 출력을 보여주거나 모든 job이 통과했음을 확인하지 않으며, 모듈 해석 문제가 해결되었다는 점만 밝히고 있다. dropped install path를 초래한 npm workspaces의 정확한 호이스팅 동작은 설명되어 있지만 깊이 있게 풀어내지는 않는다. 해석 자체에 대한 회귀 테스트는 추가되지 않았는데, 이는 라이브러리 동작이 아니라 툴체인 관련 사안이기 때문이다.

Wiring

작업 순서: (1) packages/realtime-bpm-analyzer에서 npm test를 실행하고 Cannot find module 'qs' 스택을 읽어 실패를 재현하여, @web/test-runner-core를 경유한 co-body가 소비자임을 확인한다. (2) 루트 package-lock.json을 검사하여 qs 설치 경로가 누락되었음을 확인한다. (3) music-metadata 및 ts-node와 함께 packages/realtime-bpm-analyzer devDependencies에 "qs": "^6.17.0"을 추가한다. (4) 강제 버전이 Renovate 범프와 일치하도록 루트 package.json overrides.qs를 ^6.17.0으로 설정한다. (5) package-lock.json을 재생성하여 node_modules/qs가 6.17.1로 존재하고 라이브러리 워크스페이스가 dev dependency 집합에 qs를 기록하도록 한다. (6) 라이브러리 워크스페이스 테스트와 전체 CI를 다시 실행하여 통과를 확인한다.

Try it yourself

I can't actually run commands or inspect your repo from here, so I'll give you the exact steps and the reasoning to apply. Follow them in order.

## 1. Locate where `qs` is used

Search the monorepo for direct and transitive usage:

```bash
# direct imports
rg -n "from ['\"]qs['\"]|require\(['\"]qs['\"]\)" --glob '!node_modules'

# transitive via co-body / @web/test-runner-core
rg -n "co-body|@web/test-runner-core" --glob '!node_modules'
npm ls qs --all
```

Typical culprits in a workspaces monorepo:

- A test helper that spins up a dev server using `co-body` (which depends on `qs`).
- `@web/test-runner-core` pulling `qs` transitively for its server middleware.
- A `package.json` in the library workspace that imports `qs` in a test file but never declares it.

## 2. Determine why it stopped resolving

Check these in order:

```bash
# Is qs present at the hoisted root?
ls node_modules/qs/package.json && node -p "require('./node_modules/qs/package.json').version"

# Is it nested under the library workspace?
ls packages/<library>/node_modules/qs 2>/dev/null

# What does the lockfile say?
node -e "const l=require('./package-lock.json'); for (const k of Object.keys(l.packages)) if (k.endsWith('node_modules/qs')) console.log(k, l.packages[k].version)"
```

The three failure modes to distinguish:

- **Hoisting gap**: `qs` is only declared as a transitive dep of `co-body`/`@web/test-runner-core`. After the bump, npm's hoisting placed it under a nested `node_modules` (or not at all) because the version range no longer satisfies the hoisted slot. The library workspace's test file imports `qs` directly, so it fails.
- **Lockfile drift**: `package-lock.json` still pins `node_modules/qs` at the old version, or has a stale entry that doesn't match the new `^6.17.0` range, so `npm ci` installs a tree where `qs` isn't resolvable from the library workspace.
- **Wrong package.json**: `qs` is imported in the library workspace's tests but declared in the root `package.json` (or another workspace), which works under hoisting by accident and breaks when the tree changes.

## 3. Fix (smallest change that works)

### a. Add `qs` to the library workspace `devDependencies`

In `packages/<library>/package.json`:

```json
"devDependencies": {
  "qs": "^6.17.0"
}
```

This makes the direct import legitimate and pins the range the tests actually need.

### b. Align the root `overrides.qs`

In the root `package.json`:

```json
"overrides": {
  "qs": "^6.17.0"
}
```

If the override was previously `^6.16.x` or absent, this forces every transitive `qs` (from `co-body`, `@web/test-runner-core`, etc.) to the bumped range so hoisting has a single consistent version to place at `node_modules/qs`.

### c. Correct `package-lock.json`

Regenerate from the repo root so the lockfile reflects both changes:

```bash
npm install
```

Then verify:

```bash
node -e "const l=require('./package-lock.json'); for (const k of Object.keys(l.packages)) if (k.endsWith('node_modules/qs')) console.log(k, l.packages[k].version)"
# expect: node_modules/qs 6.17.x
npm ls qs --all
```

Confirm `node_modules/qs` resolves at `6.17.x` and that no nested `qs` remains under the library workspace unless a genuinely incompatible range requires it.

### d. Verify CI locally

```bash
npm ci
npm test --workspace packages/<library>
```

Do not touch test files, do not add `skip`, do not widen ranges beyond `^6.17.0`.

## 4. Commit messages

Use conventional commits, no `feat:` or breaking markers, no em or en dashes.

```
chore(deps): add qs to library workspace devDependencies

The library tests import qs directly through the co-body and
@web/test-runner-core dev server path. Declaring it explicitly keeps
resolution stable when the hoisted tree changes.
```

```
chore(deps): align root overrides.qs with the bumped range

Forces transitive qs from co-body and @web/test-runner-core onto the
same version so npm hoists a single copy at node_modules/qs.
```

```
chore(deps): refresh package-lock.json for qs 6.17.0

Regenerated from the repo root so node_modules/qs resolves at the
bumped version and npm ci produces a tree the library tests can use.
```

If you prefer a single commit, combine them:

```
chore(deps): fix qs resolution in library workspace after bump

Add qs to the library workspace devDependencies, align the root
overrides.qs range, and refresh package-lock.json so node_modules/qs
resolves at 6.17.0. The library tests import qs through the co-body
and @web/test-runner-core dev server path, and the previous tree only
resolved it by accident of hoisting.
```

## 5. Sanity checks before pushing

- `git diff` shows only: library `package.json` (one line), root `package.json` (override line), `package-lock.json`.
- No test files changed.
- `npm ci && npm test --workspace packages/<library>` passes.
- `npm ls qs --all` shows a single hoisted `6.17.x` (or nested copies only where a range genuinely requires it).
- Commit message has no `feat:`, no `!`, no em or en dashes.

If after step 2 you find `qs` was declared in the wrong `package.json` (for example root instead of the library workspace), remove it from the wrong location as part of the same change so the declaration matches the import site.

Source

English

Tools in this stack