Skip to content

Commit 772f34f

Browse files
edvilmeCopilot
andauthored
feat: add getPackageAvailableVersions to public API and bump API to 1.1.0 (#1682)
## Summary - add `PythonPackageGetterApi.getPackageAvailableVersions` to the public API so consumers can query a package's available versions - bump `@vscode/python-environments` from `1.0.0` to `1.1.0` - synchronize both API lockfile version fields - document the public API changes since the latest npm release ## API changes - **add `PythonPackageGetterApi.getPackageAvailableVersions(environment, packageName): Promise<Pep440Version[] | undefined>`** — exposes a package's available versions (newest-first) to API consumers, delegating to the environment's package manager and resolving to `undefined` when version listing is unsupported - re-export `Pep440Version` for package manager version APIs - add `PackageInfo.isTransitive` and `GetPackagesOptions.skipCache` - add optional package manager hooks for watch targets, direct dependencies, tool versions, available package versions, and install-spec formatting - add the optional `GetPackagesOptions` parameter to `PackageManager.getPackages` and `PythonPackageGetterApi.getPackages` - update `PackageManager.refresh` and `PythonPackageGetterApi.refreshPackages` to return the refreshed package list when available ## Validation - `npm run lint` - `npm run compile-tests` - `npm run unittest` (`1494` passing, `5` pending) - `git diff --check` --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3f16397e-0917-4efb-8d75-566c71ebf9ba
1 parent f5fb6c3 commit 772f34f

19 files changed

Lines changed: 254 additions & 27 deletions

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ dist
33
node_modules
44
.vscode-test/
55
*.vsix
6+
*.tgz
67
*.tsbuildinfo
78
.nox/
89
.venv/

api/.npmignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
node_modules/
22
src/
3+
scripts/
4+
test/
35
out/**/*.map
6+
*.tgz
47
*.tsbuildinfo
58
tsconfig*.json

api/CHANGELOG.md

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,20 @@ All notable changes to the `@vscode/python-environments` API package are documen
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8-
## [1.37.0]
8+
## [1.1.0]
99

10-
- Aligned the API package version with the Python Environments extension version.
10+
### Added
11+
12+
- Re-exported the `Pep440Version` type from `@renovatebot/pep440` for use with the new package version APIs.
13+
- Added the optional `PackageInfo.isTransitive?: boolean` property to indicate whether a package is a transitive dependency.
14+
- Added `GetPackagesOptions` with an optional `skipCache?: boolean` property. When `true`, package managers bypass cached data and query the underlying package management tool.
15+
- Added optional `PackageManager.getPackageWatchTargets?(environment: PythonEnvironment): RelativePattern[]` to return manager-specific filesystem patterns to monitor for package installation and removal changes, in addition to the default site-packages metadata locations.
16+
- Added optional `PackageManager.getDirectPackageNames?(environment: PythonEnvironment): Promise<Set<string> | undefined>` to return a best-effort set of direct, non-transitive package names when supported by the package manager.
17+
- Added optional `PackageManager.getVersion?(environment: PythonEnvironment): Promise<Pep440Version | undefined>` to return the version of the underlying package management tool, such as pip, uv, or conda.
18+
- Added optional `PackageManager.getPackageAvailableVersions?(environment: PythonEnvironment, packageName: string): Promise<Pep440Version[] | undefined>` to return the available versions of a package in newest-first order when supported.
19+
- Added optional `PackageManager.formatInstallSpec?(packageName: string, version: string): string` to format a versioned install specification using manager-specific syntax, such as `name==version` for pip or `name=version` for conda.
20+
- Added `PythonPackageGetterApi.getPackageAvailableVersions(environment: PythonEnvironment, packageName: string): Promise<Pep440Version[] | undefined>` so API consumers can query a package's available versions in newest-first order. Resolves to `undefined` when the environment's package manager does not support version listing.
21+
22+
### Changed
23+
24+
- Added the optional `options?: GetPackagesOptions` parameter to `PackageManager.getPackages(environment, options?)` and `PythonPackageGetterApi.getPackages(environment, options?)`. Consumers can set `options.skipCache` to request fresh package data.

api/package-lock.json

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

api/package.json

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "@vscode/python-environments",
33
"description": "An API facade for the Python Environments extension in VS Code",
4-
"version": "1.0.0",
4+
"version": "1.1.0",
55
"author": {
66
"name": "Microsoft Corporation"
77
},
@@ -11,6 +11,8 @@
1111
"API",
1212
"Environments"
1313
],
14+
"main": "./out/cjs/main.cjs",
15+
"types": "./out/cjs/main.d.ts",
1416
"exports": {
1517
"import": {
1618
"types": "./out/esm/main.d.ts",
@@ -41,7 +43,8 @@
4143
"compile": "npm run compile:esm && npm run compile:cjs",
4244
"compile:esm": "tsc -b ./tsconfig.esm.json && mve out/esm/main.js out/esm/main.mjs",
4345
"compile:cjs": "tsc -b ./tsconfig.cjs.json && mve out/cjs/main.js out/cjs/main.cjs",
44-
"clean": "node -e \"const fs = require('fs'); fs.rmSync('./out', { recursive: true, force: true });\""
46+
"clean": "node -e \"const fs = require('fs'); fs.rmSync('./out', { recursive: true, force: true });\"",
47+
"test:package": "node ./scripts/test-package.cjs"
4548
},
4649
"devDependencies": {
4750
"@types/node": "^22.0.0",

api/scripts/test-package.cjs

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
const assert = require('node:assert');
5+
const fs = require('node:fs');
6+
const os = require('node:os');
7+
const path = require('node:path');
8+
const { execFileSync } = require('node:child_process');
9+
const { createRequire } = require('node:module');
10+
const { fileURLToPath } = require('node:url');
11+
12+
const packageRoot = path.resolve(__dirname, '..');
13+
const npmCli = process.env.npm_execpath;
14+
15+
if (!npmCli) {
16+
throw new Error('npm_execpath is unavailable. Run this validation through npm run test:package.');
17+
}
18+
19+
function runNodeScript(script, args, cwd, captureOutput = false) {
20+
return execFileSync(process.execPath, [script, ...args], {
21+
cwd,
22+
encoding: 'utf8',
23+
stdio: captureOutput ? ['ignore', 'pipe', 'inherit'] : 'inherit',
24+
});
25+
}
26+
27+
const packOutput = runNodeScript(npmCli, ['pack', '--ignore-scripts', '--json'], packageRoot, true);
28+
const packResult = JSON.parse(packOutput);
29+
assert.strictEqual(packResult.length, 1, 'Expected npm pack to produce exactly one package');
30+
31+
const tarballPath = path.join(packageRoot, packResult[0].filename);
32+
const testRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'python-environments-api-'));
33+
34+
function canonicalPath(value) {
35+
return fs.realpathSync.native(path.resolve(value));
36+
}
37+
38+
try {
39+
fs.writeFileSync(
40+
path.join(testRoot, 'package.json'),
41+
JSON.stringify({ name: 'python-environments-api-consumer', private: true }),
42+
);
43+
44+
runNodeScript(
45+
npmCli,
46+
[
47+
'install',
48+
'--ignore-scripts',
49+
'--no-package-lock',
50+
'--no-save',
51+
tarballPath,
52+
'@types/node@^22.0.0',
53+
'@types/vscode@^1.99.0',
54+
],
55+
testRoot,
56+
);
57+
58+
const typescriptCli = path.join(packageRoot, 'node_modules', 'typescript', 'bin', 'tsc');
59+
const fixtureRoot = path.join(packageRoot, 'test');
60+
61+
for (const consumer of [
62+
{ name: 'modern', type: 'module' },
63+
{ name: 'legacy', type: 'commonjs' },
64+
]) {
65+
const consumerRoot = path.join(testRoot, consumer.name);
66+
fs.mkdirSync(consumerRoot);
67+
fs.copyFileSync(path.join(fixtureRoot, 'consumer.ts'), path.join(consumerRoot, 'consumer.ts'));
68+
fs.copyFileSync(
69+
path.join(fixtureRoot, `tsconfig.${consumer.name}.json`),
70+
path.join(consumerRoot, 'tsconfig.json'),
71+
);
72+
fs.writeFileSync(
73+
path.join(consumerRoot, 'package.json'),
74+
JSON.stringify({ private: true, type: consumer.type }),
75+
);
76+
77+
runNodeScript(typescriptCli, ['--project', path.join(consumerRoot, 'tsconfig.json')], packageRoot);
78+
}
79+
80+
const installedPackageRoot = path.join(testRoot, 'node_modules', '@vscode', 'python-environments');
81+
const installedPackageJson = JSON.parse(fs.readFileSync(path.join(installedPackageRoot, 'package.json'), 'utf8'));
82+
assert.strictEqual(installedPackageJson.main, './out/cjs/main.cjs');
83+
assert.strictEqual(installedPackageJson.types, './out/cjs/main.d.ts');
84+
assert.deepStrictEqual(installedPackageJson.exports, {
85+
import: {
86+
types: './out/esm/main.d.ts',
87+
default: './out/esm/main.mjs',
88+
},
89+
require: {
90+
types: './out/cjs/main.d.ts',
91+
default: './out/cjs/main.cjs',
92+
},
93+
});
94+
95+
for (const target of [
96+
installedPackageJson.main,
97+
installedPackageJson.types,
98+
installedPackageJson.exports.import.types,
99+
installedPackageJson.exports.import.default,
100+
installedPackageJson.exports.require.types,
101+
installedPackageJson.exports.require.default,
102+
]) {
103+
assert.ok(fs.statSync(path.resolve(installedPackageRoot, target)).isFile(), `${target} must be a file`);
104+
}
105+
106+
const requireFromConsumer = createRequire(path.join(testRoot, 'legacy', 'consumer.cjs'));
107+
assert.strictEqual(
108+
canonicalPath(requireFromConsumer.resolve('@vscode/python-environments')),
109+
canonicalPath(path.join(installedPackageRoot, installedPackageJson.exports.require.default)),
110+
'CommonJS consumers should resolve the packaged CommonJS entry point',
111+
);
112+
113+
const esmEntryPoint = execFileSync(
114+
process.execPath,
115+
['--input-type=module', '--eval', "console.log(import.meta.resolve('@vscode/python-environments'))"],
116+
{
117+
cwd: path.join(testRoot, 'modern'),
118+
encoding: 'utf8',
119+
},
120+
).trim();
121+
assert.strictEqual(
122+
canonicalPath(fileURLToPath(esmEntryPoint)),
123+
canonicalPath(path.join(installedPackageRoot, installedPackageJson.exports.import.default)),
124+
'ES module consumers should resolve the packaged ES module entry point',
125+
);
126+
} finally {
127+
fs.rmSync(testRoot, { recursive: true, force: true });
128+
}

api/test/consumer.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import type {
2+
PackageManager,
3+
Pep440Version,
4+
PythonEnvironment,
5+
PythonPackageGetterApi,
6+
} from '@vscode/python-environments';
7+
8+
type Equal<Left, Right> =
9+
(<Value>() => Value extends Left ? 1 : 2) extends <Value>() => Value extends Right ? 1 : 2 ? true : false;
10+
11+
type AvailableVersionsReturn = ReturnType<PythonPackageGetterApi['getPackageAvailableVersions']>;
12+
type RefreshReturn = ReturnType<PackageManager['refresh']>;
13+
14+
const availableVersionsReturnIsExact: Equal<AvailableVersionsReturn, Promise<Pep440Version[] | undefined>> = true;
15+
const refreshReturnIsExact: Equal<RefreshReturn, Promise<void>> = true;
16+
17+
declare const api: PythonPackageGetterApi;
18+
declare const environment: PythonEnvironment;
19+
const availableVersions: Promise<Pep440Version[] | undefined> = api.getPackageAvailableVersions(environment, 'example');
20+
21+
void availableVersionsReturnIsExact;
22+
void refreshReturnIsExact;
23+
void availableVersions;

api/test/tsconfig.legacy.json

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"compilerOptions": {
3+
"target": "ES2020",
4+
"module": "CommonJS",
5+
"moduleResolution": "Node",
6+
"noEmit": true,
7+
"strict": true,
8+
"skipLibCheck": false
9+
},
10+
"include": ["consumer.ts"]
11+
}

api/test/tsconfig.modern.json

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"compilerOptions": {
3+
"target": "ES2020",
4+
"module": "NodeNext",
5+
"moduleResolution": "NodeNext",
6+
"noEmit": true,
7+
"strict": true,
8+
"skipLibCheck": false
9+
},
10+
"include": ["consumer.ts"]
11+
}

api/tsconfig.esm.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
"extends": "./tsconfig.base.json",
33
"compilerOptions": {
44
"module": "esnext",
5+
"moduleResolution": "bundler",
56
"outDir": "./out/esm"
67
}
78
}

0 commit comments

Comments
 (0)