Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/pnpm-hoisted-workspace-appdir.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"app-builder-lib": patch
---

fix(pnpm-collector): find nested dependencies under the workspace root when the app is a package in a pnpm hoisted (`nodeLinker: hoisted`) workspace, instead of packaging an out-of-range hoisted copy
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { LogMessageByKey, type Package, readJsonOrNull } from "./moduleManager.j
import { NodeModulesCollector } from "./nodeModulesCollector.js"
import { getPackageManagerCommand, PM } from "./packageManager.js"
import type { PackageJson, PnpmDependency } from "./types.js"
import { isValidKey } from "builder-util"
import { exists, isValidKey } from "builder-util"

export class PnpmNodeModulesCollector extends NodeModulesCollector<PnpmDependency, PnpmDependency> {
public readonly installOptions = {
Expand Down Expand Up @@ -39,7 +39,48 @@ export class PnpmNodeModulesCollector extends NodeModulesCollector<PnpmDependenc
* otherwise mask an isolated store.
*/
protected override isHoisted = new Lazy<boolean>(async () => {
const nmDir = path.join(this.rootDir, "node_modules")
// A workspace package's own `node_modules` is often absent (or holds only its version
// conflicts) in a hoisted workspace, so fall back to the workspace root's `node_modules`.
let sawPackage = false
for (const dir of await this.layoutRoots.value) {
const isolated = await this.scanNodeModulesLayout(path.join(dir, "node_modules"))
if (isolated != null) {
if (isolated) {
return false
}
sawPackage = true
}
}
// Packages exist and none route through `.pnpm` → hoisted. No packages → flat default.
return sawPackage
})

/**
* The pnpm workspace root containing `rootDir` (nearest ancestor with `pnpm-workspace.yaml`, as pnpm
* itself resolves it), or null outside a workspace or when `rootDir` is the workspace root.
*/
private readonly workspaceRoot = new Lazy<string | null>(async () => {
let current = path.resolve(this.rootDir)
while (true) {
if (await exists(path.join(current, "pnpm-workspace.yaml"))) {
return current === path.resolve(this.rootDir) ? null : current
}
const parent = path.dirname(current)
if (parent === current) {
return null
}
current = parent
}
})

/** `rootDir`, then the workspace root when `rootDir` is a workspace package. */
private readonly layoutRoots = new Lazy<string[]>(async () => {
const workspaceRoot = await this.workspaceRoot.value
return workspaceRoot == null ? [this.rootDir] : [this.rootDir, workspaceRoot]
})

/** Returns true if `nmDir` is an isolated (`.pnpm`) store, false if hoisted, null if it holds no packages. */
private async scanNodeModulesLayout(nmDir: string): Promise<boolean | null> {
const entries = await _fsExtra.readdir(nmDir).catch(() => [] as string[])
let sawPackage = false
for (const name of entries) {
Expand All @@ -56,13 +97,12 @@ export class PnpmNodeModulesCollector extends NodeModulesCollector<PnpmDependenc
}
sawPackage = true
if (real.split(path.sep).includes(".pnpm")) {
return false // isolated store: a package routes through the virtual store
return true // isolated store: a package routes through the virtual store
}
}
}
// Packages exist and none route through `.pnpm` → hoisted. No packages → flat default.
return sawPackage
})
return sawPackage ? false : null
}

/**
* Memo for `locateFromDepOrRoot`, keyed by `name@version`. pnpm's content-addressed virtual
Expand Down Expand Up @@ -149,9 +189,15 @@ export class PnpmNodeModulesCollector extends NodeModulesCollector<PnpmDependenc
// `.pnpm` virtual store it stays true (forcing it on there would burn thousands of
// readdir/lstat calls — and on Windows, where pnpm uses junctions that `lstat` reports as
// directories, the BFS walks the entire store and resolves the wrong paths).
const satisfying =
(parentPath ? await this.cache.locatePackageVersion({ pkgName, parentDir: parentPath, requiredRange, skipDownwardSearch, skipOverrideFallback: true }) : null) ??
(await this.cache.locatePackageVersion({ pkgName, parentDir: this.rootDir, requiredRange, skipDownwardSearch, skipOverrideFallback: true }))
//
// When `rootDir` is a workspace package, a hoisted install keeps the tree — including the
// nested `<workspaceRoot>/node_modules/A/node_modules/B` copies — under the workspace root,
// which the downward BFS from `rootDir` never reaches (its `node_modules` is often absent),
// so search the workspace root last.
let satisfying = parentPath ? await this.cache.locatePackageVersion({ pkgName, parentDir: parentPath, requiredRange, skipDownwardSearch, skipOverrideFallback: true }) : null
for (const dir of await this.layoutRoots.value) {
satisfying ??= await this.cache.locatePackageVersion({ pkgName, parentDir: dir, requiredRange, skipDownwardSearch, skipOverrideFallback: true })
}
if (satisfying) {
return satisfying
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
lockfileVersion: '9.0'

settings:
autoInstallPeers: true
excludeLinksFromLockfile: false

importers:

.:
dependencies:
lazystream:
specifier: 1.0.1
version: 1.0.1
readable-stream:
specifier: 3.6.2
version: 3.6.2

packages:

core-util-is@1.0.3:
resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==}

inherits@2.0.4:
resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}

isarray@1.0.0:
resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==}

lazystream@1.0.1:
resolution: {integrity: sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==}
engines: {node: '>= 0.6.3'}

process-nextick-args@2.0.1:
resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==}

readable-stream@2.3.8:
resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==}

readable-stream@3.6.2:
resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==}
engines: {node: '>= 6'}

safe-buffer@5.1.2:
resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==}

safe-buffer@5.2.1:
resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==}

string_decoder@1.1.1:
resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==}

string_decoder@1.3.0:
resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==}

util-deprecate@1.0.2:
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}

snapshots:

core-util-is@1.0.3: {}

inherits@2.0.4: {}

isarray@1.0.0: {}

lazystream@1.0.1:
dependencies:
readable-stream: 2.3.8

process-nextick-args@2.0.1: {}

readable-stream@2.3.8:
dependencies:
core-util-is: 1.0.3
inherits: 2.0.4
isarray: 1.0.0
process-nextick-args: 2.0.1
safe-buffer: 5.1.2
string_decoder: 1.1.1
util-deprecate: 1.0.2

readable-stream@3.6.2:
dependencies:
inherits: 2.0.4
string_decoder: 1.3.0
util-deprecate: 1.0.2

safe-buffer@5.1.2: {}

safe-buffer@5.2.1: {}

string_decoder@1.1.1:
dependencies:
safe-buffer: 5.1.2

string_decoder@1.3.0:
dependencies:
safe-buffer: 5.2.1

util-deprecate@1.0.2: {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
lockfileVersion: '9.0'

settings:
autoInstallPeers: true
excludeLinksFromLockfile: false

importers:

.:
dependencies:
readable-stream:
specifier: 3.6.2
version: 3.6.2

packages/foo:
dependencies:
ms:
specifier: 2.0.0
version: 2.0.0

packages/test-app:
dependencies:
lazystream:
specifier: 1.0.1
version: 1.0.1
readable-stream:
specifier: 3.6.2
version: 3.6.2

packages:

core-util-is@1.0.3:
resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==}

inherits@2.0.4:
resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}

isarray@1.0.0:
resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==}

lazystream@1.0.1:
resolution: {integrity: sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==}
engines: {node: '>= 0.6.3'}

ms@2.0.0:
resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==}

process-nextick-args@2.0.1:
resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==}

readable-stream@2.3.8:
resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==}

readable-stream@3.6.2:
resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==}
engines: {node: '>= 6'}

safe-buffer@5.1.2:
resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==}

safe-buffer@5.2.1:
resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==}

string_decoder@1.1.1:
resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==}

string_decoder@1.3.0:
resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==}

util-deprecate@1.0.2:
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}

snapshots:

core-util-is@1.0.3: {}

inherits@2.0.4: {}

isarray@1.0.0: {}

lazystream@1.0.1:
dependencies:
readable-stream: 2.3.8

ms@2.0.0: {}

process-nextick-args@2.0.1: {}

readable-stream@2.3.8:
dependencies:
core-util-is: 1.0.3
inherits: 2.0.4
isarray: 1.0.0
process-nextick-args: 2.0.1
safe-buffer: 5.1.2
string_decoder: 1.1.1
util-deprecate: 1.0.2

readable-stream@3.6.2:
dependencies:
inherits: 2.0.4
string_decoder: 1.3.0
util-deprecate: 1.0.2

safe-buffer@5.1.2: {}

safe-buffer@5.2.1: {}

string_decoder@1.1.1:
dependencies:
safe-buffer: 5.1.2

string_decoder@1.3.0:
dependencies:
safe-buffer: 5.2.1

util-deprecate@1.0.2: {}
Loading
Loading