fix(util): stage npm installs under the cache directory's real path (#46887)

This commit is contained in:
Kit Langton 2026-09-02 16:40:45 -04:00 committed by GitHub
parent 44e0b35303
commit db09cc842e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 32 additions and 1 deletions

View file

@ -319,6 +319,31 @@ describe("Npm.add", () => {
expect(result.pinned).toContain("root: true")
expect(result.current).toBeFalse()
}, 30_000)
// Symlink creation needs elevated privileges on Windows.
test.skipIf(win)("records Git revisions when the cache directory is reached through a symlink", async () => {
await using tmp = await tmpdir()
const fixture = await createGitFixture(tmp.path)
await fs.mkdir(path.join(tmp.path, "cache"))
await fs.symlink(path.join(tmp.path, "cache"), path.join(tmp.path, "link"))
const mutable = `git+${pathToFileURL(fixture.repository).href}#fixture-branch`
const result = await Effect.gen(function* () {
const npm = yield* Npm.Service
const added = yield* npm.add(mutable)
const current = yield* npm.check(mutable)
yield* Effect.promise(async () => {
await Bun.write(path.join(fixture.repository, "index.js"), 'export default { root: "second" }\n')
await Bun.$`git -C ${fixture.repository} add .`
await Bun.$`git -C ${fixture.repository} -c user.name=fixture -c user.email=fixture@example.com commit -qm second`
})
return { added, current, outdated: yield* npm.check(mutable) }
}).pipe(Effect.scoped, Effect.provide(npmLayer(path.join(tmp.path, "link"))), Effect.runPromise)
expect(result.added.version).toBe(fixture.commit)
expect(result.current).toBeFalse()
expect(result.outdated).toBeTrue()
}, 30_000)
})
describe("Npm.resolve", () => {

View file

@ -286,7 +286,13 @@ const layer = Layer.effect(
yield* mkdir(dir)
const startedAt = yield* Clock.currentTimeMillis
const staging = path.join(dir, `.staging-${startedAt}-${randomUUID()}`)
// Arborist keys lockfile entries relative to the root's real path. When the cache
// directory is reached through a symlink (macOS `/var` → `/private/var`, a linked
// XDG cache), the keys become `../../…` paths that installedRevision never finds,
// so Git checks report "not installed" and updates go undetected. Stage under the
// resolved directory so the root path and real path agree.
const root = yield* fs.realPath(dir).pipe(Effect.mapError((cause) => new InstallFailedError({ dir, cause })))
const staging = path.join(root, `.staging-${startedAt}-${randomUUID()}`)
const staged = yield* Effect.gen(function* () {
const tree = yield* reify({ dir: staging, config: dir, add: [pkg], update })
const installed = tree.edgesOut.values().next().value?.to