Skip to content

Commit af31cf2

Browse files
[fix]: prefer node_modules over stale PnP state (#122)
* prefer node_modules over stale PnP state * docs update * changeset
1 parent ce6d7dd commit af31cf2

6 files changed

Lines changed: 107 additions & 11 deletions

File tree

.changeset/violet-lies-hide.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@tanstack/intent': patch
3+
---
4+
5+
Fix `intent list` in projects with stale Yarn PnP files alongside project `node_modules`, including Bun isolated installs. Intent now prefers project `node_modules` when it exists and only loads Yarn's PnP API for PnP projects without `node_modules`.

docs/cli/intent-list.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,10 +96,10 @@ When both local and global packages are scanned, local packages take precedence.
9696

9797
`packages` are ordered using `intent.requires` when possible.
9898
When the same package exists both locally and globally and global scanning is enabled, `intent list` prefers the local package.
99+
When project `node_modules` exists, `intent list` scans it. In Yarn PnP projects without `node_modules`, `intent list` uses Yarn's PnP API.
99100

100101
## Common errors
101102

102103
- Scanner failures are printed as errors
103104
- Unsupported environments:
104-
- Yarn PnP without `node_modules`
105105
- Deno projects without `node_modules`

docs/overview.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ Intent provides tooling for two workflows:
3030
npx @tanstack/intent@latest list
3131
```
3232

33-
Scans the current project's `node_modules` and workspace dependencies for intent-enabled packages.
33+
Scans the current project's installed dependencies for intent-enabled packages, including `node_modules`, workspace dependencies, and Yarn PnP projects without `node_modules`.
3434
Global package scanning is explicit; pass `--global` to include global packages or `--global-only` to ignore local packages.
3535
When both local and global packages are scanned, local packages take precedence.
3636

packages/intent/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ npx @tanstack/intent@latest setup
104104
| Node.js + pnpm | Supported | Use `pnpm dlx @tanstack/intent@latest <command>` |
105105
| Node.js + Bun | Supported | Use `bunx @tanstack/intent@latest <command>` |
106106
| Deno | Best-effort | Requires `npm:` interop and `node_modules` support |
107-
| Yarn PnP | Unsupported | `@tanstack/intent` scans `node_modules` |
107+
| Yarn PnP | Supported | Uses Yarn's PnP API when `node_modules` is absent |
108108

109109
## Monorepos
110110

packages/intent/src/scanner.ts

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -416,7 +416,6 @@ export function scanForIntents(
416416
const projectRoot = root ?? process.cwd()
417417
const scanScope = getScanScope(options)
418418
const packageManager = detectPackageManager(projectRoot)
419-
const pnpApi = scanScope === 'global' ? null : loadPnpApi(projectRoot)
420419
const nodeModulesDir = join(projectRoot, 'node_modules')
421420
const explicitGlobalNodeModules =
422421
process.env.INTENT_GLOBAL_NODE_MODULES?.trim() || null
@@ -450,6 +449,15 @@ export function scanForIntents(
450449
string,
451450
Map<string, { version: string; packageRoot: string }>
452451
>()
452+
let pnpApi: PnpApi | null | undefined
453+
454+
function getPnpApi(): PnpApi | null {
455+
if (scanScope === 'global') return null
456+
if (pnpApi === undefined) {
457+
pnpApi = loadPnpApi(projectRoot)
458+
}
459+
return pnpApi
460+
}
453461

454462
function rememberVariant(pkg: IntentPackage): void {
455463
let variants = packageVariants.get(pkg.name)
@@ -515,10 +523,7 @@ export function scanForIntents(
515523
warnings,
516524
})
517525

518-
function scanPnpPackages(): void {
519-
if (!pnpApi) return
520-
521-
const api = pnpApi
526+
function scanPnpPackages(api: PnpApi): void {
522527
const visited = new Set<string>()
523528
const workspaceRoot = findWorkspaceRoot(projectRoot)
524529
const projectLocator = api.findPackageLocator?.(
@@ -556,9 +561,12 @@ export function scanForIntents(
556561
}
557562

558563
function scanLocalPackages(): void {
559-
if (pnpApi && !nodeModules.local.exists) {
560-
scanPnpPackages()
561-
return
564+
if (!nodeModules.local.exists) {
565+
const api = getPnpApi()
566+
if (api) {
567+
scanPnpPackages(api)
568+
return
569+
}
562570
}
563571

564572
assertLocalNodeModulesSupported(projectRoot)

packages/intent/tests/scanner.test.ts

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -669,6 +669,89 @@ describe('scanForIntents', () => {
669669
expect(result.packages[0]!.name).toBe('skills-pkg')
670670
})
671671

672+
it('prefers project node_modules over stale PnP state', () => {
673+
const missingPkgJson = join(
674+
root,
675+
'.yarn',
676+
'cache',
677+
'bun-wrapper.zip',
678+
'node_modules',
679+
'bun-wrapper',
680+
'package.json',
681+
)
682+
683+
writeJson(join(root, 'package.json'), {
684+
name: 'app',
685+
private: true,
686+
dependencies: { 'bun-wrapper': '1.0.0' },
687+
})
688+
writeFileSync(
689+
join(root, '.pnp.cjs'),
690+
[
691+
"const Module = require('node:module')",
692+
`const missingPkgJson = ${JSON.stringify(missingPkgJson)}`,
693+
'module.exports = {',
694+
' setup() {',
695+
' const originalResolve = Module._resolveFilename',
696+
' Module._resolveFilename = function(request, parent, isMain, options) {',
697+
" if (request === 'bun-wrapper/package.json') return missingPkgJson",
698+
' return originalResolve.call(this, request, parent, isMain, options)',
699+
' }',
700+
' },',
701+
' getDependencyTreeRoots() { return [] },',
702+
' getPackageInformation() { return null },',
703+
'}',
704+
'',
705+
].join('\n'),
706+
)
707+
708+
const wrapperDir = createDir(
709+
root,
710+
'node_modules',
711+
'.bun',
712+
'bun-wrapper@1.0.0',
713+
'node_modules',
714+
'bun-wrapper',
715+
)
716+
writeJson(join(wrapperDir, 'package.json'), {
717+
name: 'bun-wrapper',
718+
version: '1.0.0',
719+
dependencies: { 'bun-skills-pkg': '1.0.0' },
720+
})
721+
722+
const skillsPkgDir = createDir(
723+
root,
724+
'node_modules',
725+
'.bun',
726+
'bun-skills-pkg@1.0.0',
727+
'node_modules',
728+
'bun-skills-pkg',
729+
)
730+
writeJson(join(skillsPkgDir, 'package.json'), {
731+
name: 'bun-skills-pkg',
732+
version: '1.0.0',
733+
intent: { version: 1, repo: 'test/skills', docs: 'https://example.com' },
734+
})
735+
writeSkillMd(createDir(skillsPkgDir, 'skills', 'core'), {
736+
name: 'core',
737+
description: 'Core skill',
738+
})
739+
740+
createDir(root, 'node_modules')
741+
symlinkSync(wrapperDir, join(root, 'node_modules', 'bun-wrapper'))
742+
createDir(wrapperDir, 'node_modules')
743+
symlinkSync(
744+
skillsPkgDir,
745+
join(wrapperDir, 'node_modules', 'bun-skills-pkg'),
746+
)
747+
748+
const result = scanForIntents(root)
749+
750+
expect(result.packages).toHaveLength(1)
751+
expect(result.packages[0]!.name).toBe('bun-skills-pkg')
752+
expect(result.warnings).toEqual([])
753+
})
754+
672755
it('discovers skills using package.json workspaces', () => {
673756
writeJson(join(root, 'package.json'), {
674757
name: 'monorepo',

0 commit comments

Comments
 (0)