Skip to content

Commit 9d04e3d

Browse files
authored
feat: build workspace context then resolve dependencies (#68)
* refactor: generate workspace context * remove virtual document * remove compatible properties * expose nameRange & specRange directly * move specific logic into extractor * extract package-manager-detect * use more ResolvedDependencyInfo * lazy init * fix: get workspace file correctly * fix: check resolvedProtocol instead of `isSupportedProtocol` * fix: watch files change, more logs * refactor: class WorkspaceContext * rename * update * fix: correctly handle version spec * ci: fix * feat: get extractor by file extension * fix: only watch document text changes, no active editor changes listening * fix: ensure workspace context initial * fix: issues reported by coderabbit * refactor: re-organize * reloadWorkspace when workspace-level files change * don't require name and version
1 parent 26ce22b commit 9d04e3d

66 files changed

Lines changed: 1350 additions & 844 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

CONTRIBUTING.md

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,10 @@ playground/ # Playground for testing
8383
res/ # Assets (e.g. marketplace icon)
8484
src/ # Extension source code
8585
├── commands/ # Command handlers (vscode API only, no reactive-vscode)
86-
├── extractors/ # Extractors (package.json, pnpm-workspace.yaml)
86+
├── composables/ # Composables (reactive-vscode hooks)
87+
├── core/ # Core logic
88+
│ ├── extractors/ # Extractors (JSON, YAML)
89+
│ └── workspace.ts # Workspace context resolution
8790
├── providers/ # Providers
8891
│ ├── code-actions/ # Code action providers (quick fixes)
8992
│ ├── completion-item/ # Completion providers (version autocomplete)
@@ -101,9 +104,17 @@ tests/ # Tests
101104
├── __setup__/ # Test setup and utilities
102105
├── code-actions/ # Code action tests
103106
├── diagnostics/ # Diagnostic tests
107+
├── fixtures/ # Test fixtures (workspace scenarios)
104108
└── utils/ # Utility tests
105109
```
106110

111+
### Key concepts
112+
113+
- **Extractor** – Parses a supported file (`package.json`, `pnpm-workspace.yaml`, `.yarnrc.yml`) and extracts dependency information with AST ranges. Each file format has its own extractor in `src/core/extractors/`.
114+
- **WorkspaceContext** – Holds per-workspace-folder state: detected package manager, resolved catalogs, and memoized dependency info. Created lazily and invalidated when workspace-level files change.
115+
- **ResolvedDependencyInfo** – A dependency with its protocol resolved (e.g., `catalog:` → actual version, `npm:alias@version` → underlying package). Providers consume resolved dependencies instead of raw AST data.
116+
- **Provider** – VS Code language feature (hover, completion, diagnostics, etc.) that operates on resolved dependencies.
117+
107118
## Code style
108119

109120
When committing changes, try to keep an eye out for unintended formatting updates. These can make a pull request look noisier than it really is and slow down the review process. Sometimes IDEs automatically reformat files on save, which can unintentionally introduce extra changes.
@@ -122,7 +133,7 @@ If you want to get ahead of any formatting issues, you can also run `pnpm lint:f
122133
> This will be fixed by eslint.
123134
124135
1. Type imports first (`import type { ... }`)
125-
2. Internal aliases (`#constants`, `#utils/`, `#composables/`, etc.)
136+
2. Internal aliases (`#constants`, `#state`, `#utils/`, `#core/`, `#composables/`, `#types/`, etc.)
126137
3. External packages (including `node:`)
127138
4. Relative imports (`./`, `../`)
128139
5. No blank lines between groups

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727

2828
- **Hover Information** – Quick links to package details and documentation on [npmx.dev](https://npmx.dev), with provenance verification status.
2929
- **Version Completion** – Autocomplete package versions with provenance filtering and prerelease exclusion support.
30+
- **Workspace-Aware Resolution** – Dependencies in `package.json`, `pnpm-workspace.yaml`, and `.yarnrc.yml` are resolved from a shared workspace context, including catalogs and workspace references.
3031
- **Diagnostics**
3132
- Deprecated package warnings with deprecation messages
3233
- Package replacement suggestions (via [module-replacements](https://github.com/es-tooling/module-replacements))

eslint.config.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ export default defineConfig(
99
{
1010
pnpm: true,
1111
typescript: true,
12-
ignores: ['playground'],
12+
ignores: ['playground', 'tests/fixtures'],
1313
},
1414
{
1515
name: 'extensions/all',

package.json

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,7 @@
4141
"vscode": "^1.101.0"
4242
},
4343
"activationEvents": [
44-
"workspaceContains:package.json",
45-
"workspaceContains:pnpm-workspace.yaml",
46-
"workspaceContains:.yarnrc.yml"
44+
"workspaceContains:package.json"
4745
],
4846
"contributes": {
4947
"configuration": {
@@ -231,6 +229,7 @@
231229
"msw": "catalog:test",
232230
"nano-staged": "catalog:dev",
233231
"ofetch": "catalog:inline",
232+
"pathe": "catalog:inline",
234233
"perfect-debounce": "catalog:inline",
235234
"reactive-vscode": "catalog:inline",
236235
"semver": "catalog:inline",

pnpm-lock.yaml

Lines changed: 6 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pnpm-workspace.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ catalogs:
1616
fast-npm-meta: ^1.3.0
1717
jsonc-parser: ^3.3.1
1818
ofetch: ^2.0.0-alpha.3
19+
pathe: ^2.0.3
1920
perfect-debounce: ^2.1.0
2021
reactive-vscode: ^1.0.0-beta.2
2122
semver: ^7.7.4

src/commands/open-file-in-npmx.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,8 @@ export async function openFileInNpmx(fileUri?: Uri) {
4141
// Construct the npmx.dev URL and open it. VSCode uses 0-indexed lines, npmx uses 1-indexed.
4242
const { selection } = textEditor ?? {}
4343
const url = npmxFileUrl(
44-
manifest.name,
45-
manifest.version,
44+
manifest.name!,
45+
manifest.version!,
4646
relativePath,
4747
openingActiveFile && selection ? selection.start.line + 1 : undefined,
4848
openingActiveFile && selection ? selection.end.line + 1 : undefined,
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import type { Uri } from 'vscode'
2+
import { SUPPORTED_DOCUMENT_PATTERN } from '#constants'
3+
import { deleteWorkspaceContextCache, getWorkspaceContext } from '#core/workspace'
4+
import { logger } from '#state'
5+
import { isSupportedDependencyDocument, isWorkspaceLevelFile } from '#utils/file'
6+
import { useDisposable, useFileSystemWatcher } from 'reactive-vscode'
7+
import { window, workspace } from 'vscode'
8+
9+
export function useWorkspaceContext() {
10+
useDisposable(workspace.onDidChangeWorkspaceFolders(({ removed }) => {
11+
removed.forEach((folder) => {
12+
deleteWorkspaceContextCache(folder)
13+
logger.info(`[workspace-context] delete workspace folder cache: ${folder.uri.path}`)
14+
})
15+
}))
16+
17+
async function deleteCacheByUri(uri: Uri, reload = true) {
18+
if (!isSupportedDependencyDocument(uri))
19+
return
20+
21+
const ctx = await getWorkspaceContext(uri)
22+
if (!ctx)
23+
return
24+
25+
ctx.loadPackageManifestInfo.delete(uri)
26+
ctx.loadWorkspaceCatalogInfo.delete(uri)
27+
logger.info(`[workspace-context] delete dependencies cache: ${uri.path}`)
28+
if (reload && isWorkspaceLevelFile(uri)) {
29+
await ctx.loadWorkspace()
30+
}
31+
}
32+
33+
useDisposable(workspace.onDidChangeTextDocument(({ document }) => {
34+
if (document !== window.activeTextEditor?.document)
35+
return
36+
37+
deleteCacheByUri(document.uri, false)
38+
}))
39+
40+
const { onDidCreate, onDidChange, onDidDelete } = useFileSystemWatcher(SUPPORTED_DOCUMENT_PATTERN)
41+
42+
onDidCreate(deleteCacheByUri)
43+
onDidChange(deleteCacheByUri)
44+
onDidDelete(deleteCacheByUri)
45+
}

src/constants.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ export const PACKAGE_JSON_BASENAME = 'package.json'
22
export const PNPM_WORKSPACE_BASENAME = 'pnpm-workspace.yaml'
33
export const YARN_WORKSPACE_BASENAME = '.yarnrc.yml'
44

5+
export const SUPPORTED_DOCUMENT_PATTERN = `**/{${PACKAGE_JSON_BASENAME},${PNPM_WORKSPACE_BASENAME},${YARN_WORKSPACE_BASENAME}}`
6+
57
export const PRERELEASE_PATTERN = /-.+/
68

79
export const CACHE_TTL_ONE_DAY = 1000 * 60 * 60 * 24

src/core/extractors/index.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { extname } from 'pathe'
2+
import { JsonExtractor } from './json'
3+
import { YamlExtractor } from './yaml'
4+
5+
const jsonExtractor = new JsonExtractor()
6+
const yamlExtractor = new YamlExtractor()
7+
8+
const extractorsByExtension = {
9+
'.json': jsonExtractor,
10+
'.yaml': yamlExtractor,
11+
'.yml': yamlExtractor,
12+
} as const satisfies Record<string, JsonExtractor | YamlExtractor>
13+
14+
type ExtractorByExt<T extends string>
15+
= T extends `${string}.json` ? JsonExtractor
16+
: T extends `${string}.yaml` | `${string}.yml` ? YamlExtractor
17+
: JsonExtractor | YamlExtractor | undefined
18+
19+
export function getExtractor<T extends string>(filename: T): ExtractorByExt<T> {
20+
return extractorsByExtension[extname(filename) as keyof typeof extractorsByExtension] as ExtractorByExt<T>
21+
}

0 commit comments

Comments
 (0)