Skip to content

Commit 556b258

Browse files
edvilmeCopilot
andauthored
refactor: separate API facade from public contracts (#1787)
This pull request clarifies the distinction between the Python Environments runtime API and the public TypeScript contracts used by the extension and npm package. The API is the runtime mechanism that extensions and automation use to execute operations, while the types define the public contracts and shared data shapes used throughout the codebase and exposed through `@vscode/python-environments`. ## New file structure - `src/types.ts` - Canonical public contracts and interfaces, including `PythonEnvironmentApi`, environment and package shapes, manager contracts, and event/option types. - This file is intentionally contract-focused and does not contain extension lookup helpers or concrete implementations. - `src/api.ts` - Public runtime facade for consumers. - Re-exports the public contracts and public errors, keeps `EXTENSION_ID`, and provides `PythonEnvironments.api()` for resolving the VS Code extension API at runtime. - `src/publicErrors.ts` - Public runtime error definitions and guards that package consumers may need to handle explicitly. - `src/extensionApi.ts` - The extension-side implementation of `PythonEnvironmentApi` that is returned from activation. - This keeps the runtime implementation separate from the public facade and type declarations. - `src/managers/common/models.ts` - Concrete `PythonEnvironmentImpl` and `PythonPackageImpl` model implementations. - `src/managers/common/registeredManagers.ts` - Internal manager wrappers, preserving existing names such as `InternalEnvironmentManager` and `InternalPackageManager` to minimize PR impact. - Owning feature modules now define their own internal contracts instead of using the old `src/internal.api.ts` grab bag: - `src/features/envManagers.ts` owns environment-manager registry contracts and events. - `src/features/projectManager.ts` owns project-manager contracts and implementation. - `src/features/creators/projectCreators.ts` owns project-creator contracts and implementation. ## Package and consumer behavior - The npm package continues to expose the existing public surface; this PR does not intentionally remove or rename package exports. - The extension does not consume `@vscode/python-environments` internally. Shared contracts come from local source files. - The package build copies the source-of-truth files from `src/` into the package staging area before compiling. - CommonJS and ES module package entry points are covered by package smoke tests. ## Other cleanup - Removed the obsolete `examples/` sample extension. External extension authors should use the npm package/public contracts instead of copying a local sample that duplicated API files. - Updated documentation, packaging scripts, and release-gate checks to account for the split public API files. - Added webpack resolution support for `.js` import specifiers used by the package-compatible TypeScript sources. ## Compatibility No breaking changes are intended. Public names are preserved, internal wrapper names are preserved where possible, and the npm package remains the consumer-facing entry point for external extensions. --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Copilot-Session: de4cb63f-4a85-432d-a722-697b33e9d49a
1 parent 796282b commit 556b258

103 files changed

Lines changed: 834 additions & 7312 deletions

File tree

Some content is hidden

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

‎.github/workflows/pr-file-check.yml‎

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,15 +45,21 @@ jobs:
4545
- name: 'Public API changes require a version bump'
4646
uses: brettcannon/check-for-changed-files@d85c64d17b3c1d0ac57c9cc46ea39399b0d6fa71 # v1.2.2
4747
with:
48-
prereq-pattern: 'src/api.ts'
48+
prereq-pattern: |
49+
src/api.ts
50+
src/types.ts
51+
src/publicErrors.ts
4952
file-pattern: 'api/package.json'
5053
skip-label: 'skip api version'
5154
failure-message: 'The public API (${prereq-pattern}) was changed without bumping the package version in ${file-pattern} (the ${skip-label} label can be used to pass this check)'
5255

5356
- name: 'Public API changes require a changelog entry'
5457
uses: brettcannon/check-for-changed-files@d85c64d17b3c1d0ac57c9cc46ea39399b0d6fa71 # v1.2.2
5558
with:
56-
prereq-pattern: 'src/api.ts'
59+
prereq-pattern: |
60+
src/api.ts
61+
src/types.ts
62+
src/publicErrors.ts
5763
file-pattern: 'api/CHANGELOG.md'
5864
skip-label: 'skip api changelog'
5965
failure-message: 'The public API (${prereq-pattern}) was changed without a changelog entry in ${file-pattern} (the ${skip-label} label can be used to pass this check)'

‎CONTRIBUTING.md‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -91,12 +91,12 @@ This project has adopted the [Microsoft Open Source Code of Conduct](https://ope
9191

9292
## Public API package (`@vscode/python-environments`)
9393

94-
The npm package under [`api/`](./api) is the public API facade other extensions consume. Its entry point, `api/src/main.ts`, is a **copy** of [`src/api.ts`](./src/api.ts) — the single source of truth — and is **not committed** (see [`api/.gitignore`](./api/.gitignore)).
94+
The npm package under [`api/`](./api) is the public API facade other extensions consume. Its sources — `api/src/main.ts`, `api/src/types.ts`, and `api/src/publicErrors.ts` — are **copies** of [`src/api.ts`](./src/api.ts), [`src/types.ts`](./src/types.ts), and [`src/publicErrors.ts`](./src/publicErrors.ts) respectively — the single sources of truth — and are **not committed** (see [`api/.gitignore`](./api/.gitignore)).
9595

96-
- Edit the API only in `src/api.ts`. This file contains the full public surface, including the runtime `PythonEnvironments.api()` helper and `EXTENSION_ID`. `api/src/main.ts` is a build artifact — never edit or commit it.
97-
- `api/src/main.ts` is produced by the publish pipeline ([`build/azure-pipeline.npm.yml`](./build/azure-pipeline.npm.yml)), which copies `src/api.ts` to `api/src/main.ts` before compiling. The api package is therefore built in CI only; to build it locally, copy the file first (e.g. `cp src/api.ts api/src/main.ts`).
98-
- `src/api.ts` itself is validated on every PR by the extension's own lint and TypeScript compile.
99-
- **Versioning:** the published package version in [`api/package.json`](./api/package.json) must always match the extension version in [`package.json`](./package.json). CI enforces this via [`scripts/compare_package_versions.py`](./scripts/compare_package_versions.py). Additionally, any PR that edits `src/api.ts` must bump `api/package.json` (use the `skip api version` label to bypass) and add an entry to [`api/CHANGELOG.md`](./api/CHANGELOG.md) (use the `skip api changelog` label to bypass). When bumping, update both `package.json` files so they stay in sync.
96+
- Edit the public API only in `src/api.ts` (the runtime facade: `PythonEnvironments.api()` helper and `EXTENSION_ID`), `src/types.ts` (public contracts: interfaces, types, enums), and `src/publicErrors.ts` (concrete public error classes and type guards). `api/src/*.ts` files are build artifacts — never edit or commit them.
97+
- `api/src/main.ts`, `api/src/types.ts`, and `api/src/publicErrors.ts` are produced by the publish pipeline ([`build/azure-pipeline.npm.yml`](./build/azure-pipeline.npm.yml)), which copies `src/api.ts` to `api/src/main.ts`, `src/types.ts` to `api/src/types.ts`, and `src/publicErrors.ts` to `api/src/publicErrors.ts` before compiling. The api package is therefore built in CI only; to build it locally, copy the files first (e.g. `cp src/api.ts api/src/main.ts && cp src/types.ts api/src/types.ts && cp src/publicErrors.ts api/src/publicErrors.ts`).
98+
- `src/api.ts`, `src/types.ts`, and `src/publicErrors.ts` are validated on every PR by the extension's own lint and TypeScript compile.
99+
- **Versioning and compatibility:** the published package version in [`api/package.json`](./api/package.json) is maintained independently of the extension version in [`package.json`](./package.json) — the two do not need to match. Compatibility is based on the API shape exported by the installed Python Environments extension at runtime. Package updates must preserve backwards-compatible contracts unless the API package version intentionally communicates a breaking change; consumers should treat newly added members as optional when they may run against older installed extension versions. Any PR that edits `src/api.ts`, `src/types.ts`, or `src/publicErrors.ts` must bump `api/package.json` (use the `skip api version` label to bypass) and add an entry to [`api/CHANGELOG.md`](./api/CHANGELOG.md) (use the `skip api changelog` label to bypass).
100100

101101
## Questions or Issues?
102102

‎README.md‎

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -155,9 +155,7 @@ The Python Environments extension was built to provide a cohesive and user frien
155155

156156
### API Reference (proposed)
157157

158-
See [api.ts](https://github.com/microsoft/vscode-python-environments/blob/main/src/api.ts) for the full list of Extension APIs.
159-
160-
To consume these APIs you can look at the example here: [API Consumption Examples](https://github.com/microsoft/vscode-python-environments/blob/main/examples/README.md)
158+
See [api.ts](https://github.com/microsoft/vscode-python-environments/blob/main/src/api.ts) for the runtime API facade and [types.ts](https://github.com/microsoft/vscode-python-environments/blob/main/src/types.ts) for the public API contracts. Extension authors can consume these contracts from the `@vscode/python-environments` npm package.
161159

162160
### Callable Commands
163161

‎api/.gitignore‎

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1-
# Copied from ../src/api.ts by the publish pipeline (build/azure-pipeline.npm.yml).
2-
# This is the published package entry point; it is produced at publish time and
3-
# intentionally NOT committed. src/api.ts is the single source of truth.
1+
# Copied from ../src/api.ts, ../src/types.ts, ../src/publicErrors.ts by the publish pipeline
2+
# (build/azure-pipeline.npm.yml). These are the published package sources; they are produced
3+
# at publish time and intentionally NOT committed. src/api.ts, src/types.ts, and
4+
# src/publicErrors.ts are the single sources of truth.
45
src/main.ts
6+
src/types.ts
7+
src/publicErrors.ts

‎api/CHANGELOG.md‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,12 @@ 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.4.0]
9+
10+
### Changed
11+
12+
- Reorganized the package source into separate API facade, public contract, and public error modules without changing the root package exports.
13+
814
## [1.3.0]
915

1016
### Added

‎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: 4 additions & 3 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.3.0",
4+
"version": "1.4.0",
55
"author": {
66
"name": "Microsoft Corporation"
77
},
@@ -39,9 +39,10 @@
3939
"scripts": {
4040
"prepublishOnly": "echo \"⛔ Can only publish from a secure pipeline ⛔\" && node -e \"process.exitCode = 1\"",
4141
"prepack": "npm run all:publish",
42-
"all:publish": "git clean -xfd . && npm install && npm run compile",
42+
"all:publish": "git clean -xfd . && npm install && npm run copy:sources && npm run compile",
43+
"copy:sources": "node ./scripts/copy-sources.cjs",
4344
"compile": "npm run compile:esm && npm run compile:cjs",
44-
"compile:esm": "tsc -b ./tsconfig.esm.json && mve out/esm/main.js out/esm/main.mjs",
45+
"compile:esm": "tsc -b ./tsconfig.esm.json && mve out/esm/main.js out/esm/main.mjs && node -e \"require('fs').writeFileSync('out/esm/package.json', '{\\\"type\\\":\\\"module\\\"}')\"",
4546
"compile:cjs": "tsc -b ./tsconfig.cjs.json && mve out/cjs/main.js out/cjs/main.cjs",
4647
"clean": "node -e \"const fs = require('fs'); fs.rmSync('./out', { recursive: true, force: true });\"",
4748
"test:package": "node ./scripts/test-package.cjs"

‎api/scripts/copy-sources.cjs‎

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
// Repopulates the package's generated `src` sources from the single sources of
5+
// truth in `../src`. These files are gitignored and are removed by the
6+
// `git clean -xfd .` step in `all:publish`, so they must be copied back before
7+
// TypeScript can compile. Mirrors the copy step in build/azure-pipeline.npm.yml.
8+
9+
const fs = require('node:fs');
10+
const path = require('node:path');
11+
12+
const packageRoot = path.resolve(__dirname, '..');
13+
const repoRoot = path.resolve(packageRoot, '..');
14+
const srcDir = path.join(packageRoot, 'src');
15+
16+
const sources = [
17+
{ from: path.join(repoRoot, 'src', 'api.ts'), to: path.join(srcDir, 'main.ts') },
18+
{ from: path.join(repoRoot, 'src', 'types.ts'), to: path.join(srcDir, 'types.ts') },
19+
{ from: path.join(repoRoot, 'src', 'publicErrors.ts'), to: path.join(srcDir, 'publicErrors.ts') },
20+
];
21+
22+
fs.mkdirSync(srcDir, { recursive: true });
23+
for (const { from, to } of sources) {
24+
fs.copyFileSync(from, to);
25+
}

‎api/scripts/test-package.cjs‎

Lines changed: 49 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,23 @@ try {
7878
}
7979

8080
const installedPackageRoot = path.join(testRoot, 'node_modules', '@vscode', 'python-environments');
81+
const vscodeStubRoot = path.join(testRoot, 'node_modules', 'vscode');
82+
fs.mkdirSync(vscodeStubRoot, { recursive: true });
83+
fs.writeFileSync(path.join(vscodeStubRoot, 'package.json'), JSON.stringify({ main: 'index.js' }));
84+
fs.writeFileSync(
85+
path.join(vscodeStubRoot, 'index.js'),
86+
[
87+
"const runtimeApi = { getEnvironments: async () => [] };",
88+
"const extension = {",
89+
" isActive: false,",
90+
" exports: undefined,",
91+
" packageJSON: { version: '1.37.0' },",
92+
" activate: async () => { extension.isActive = true; extension.exports = runtimeApi; return runtimeApi; },",
93+
"};",
94+
'exports.__runtimeApi = runtimeApi;',
95+
'exports.extensions = { getExtension: () => extension };',
96+
].join('\n'),
97+
);
8198
const installedPackageJson = JSON.parse(fs.readFileSync(path.join(installedPackageRoot, 'package.json'), 'utf8'));
8299
assert.strictEqual(installedPackageJson.main, './out/cjs/main.cjs');
83100
assert.strictEqual(installedPackageJson.types, './out/cjs/main.d.ts');
@@ -104,24 +121,52 @@ try {
104121
}
105122

106123
const requireFromConsumer = createRequire(path.join(testRoot, 'legacy', 'consumer.cjs'));
124+
const commonJsModule = requireFromConsumer('@vscode/python-environments');
125+
assert.strictEqual(
126+
typeof commonJsModule.PythonEnvironments.api,
127+
'function',
128+
'CommonJS consumers should load the package runtime facade',
129+
);
130+
execFileSync(
131+
process.execPath,
132+
[
133+
'--eval',
134+
[
135+
"const packageModule = require('@vscode/python-environments');",
136+
"const vscode = require('vscode');",
137+
"(async () => {",
138+
' const api = await packageModule.PythonEnvironments.api();',
139+
' if (api !== vscode.__runtimeApi) process.exit(1);',
140+
'})().catch(() => process.exit(1));',
141+
].join('\n'),
142+
],
143+
{
144+
cwd: path.join(testRoot, 'legacy'),
145+
encoding: 'utf8',
146+
},
147+
);
107148
assert.strictEqual(
108149
canonicalPath(requireFromConsumer.resolve('@vscode/python-environments')),
109150
canonicalPath(path.join(installedPackageRoot, installedPackageJson.exports.require.default)),
110151
'CommonJS consumers should resolve the packaged CommonJS entry point',
111152
);
112153

113-
const esmEntryPoint = execFileSync(
154+
const esmModuleCheck = execFileSync(
114155
process.execPath,
115-
['--input-type=module', '--eval', "console.log(import.meta.resolve('@vscode/python-environments'))"],
156+
[
157+
'--input-type=module',
158+
'--eval',
159+
"const packageModule = await import('@vscode/python-environments'); const vscode = await import('vscode'); if (typeof packageModule.PythonEnvironments.api !== 'function') process.exit(1); const api = await packageModule.PythonEnvironments.api(); if (api !== vscode.default.__runtimeApi) process.exit(1); console.log(import.meta.resolve('@vscode/python-environments'));",
160+
],
116161
{
117162
cwd: path.join(testRoot, 'modern'),
118163
encoding: 'utf8',
119164
},
120165
).trim();
121166
assert.strictEqual(
122-
canonicalPath(fileURLToPath(esmEntryPoint)),
167+
canonicalPath(fileURLToPath(esmModuleCheck)),
123168
canonicalPath(path.join(installedPackageRoot, installedPackageJson.exports.import.default)),
124-
'ES module consumers should resolve the packaged ES module entry point',
169+
'ES module consumers should load the packaged runtime facade',
125170
);
126171
} finally {
127172
fs.rmSync(testRoot, { recursive: true, force: true });

‎api/test/consumer.ts‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,13 @@ import type {
22
PackageManager,
33
Pep440Version,
44
PythonEnvironment,
5+
PythonEnvironmentApi,
56
PythonPackageGetterApi,
67
} from '@vscode/python-environments';
78
import {
89
isPackageVersionLookupNotSupportedError,
910
PackageVersionLookupNotSupportedError,
11+
PythonEnvironments,
1012
} from '@vscode/python-environments';
1113

1214
type Equal<Left, Right> =
@@ -32,6 +34,7 @@ const explicitLegacyAvailableVersions: Promise<Pep440Version[] | undefined> = ap
3234
const throwingAvailableVersions: Promise<Pep440Version[]> = api.getPackageAvailableVersions(environment, 'example', {
3335
errorMode: 'throw',
3436
});
37+
const runtimeApi: Promise<PythonEnvironmentApi> = PythonEnvironments.api();
3538

3639
// The unsupported-capability error is part of the public contract: it is constructible, extends
3740
// Error, and exposes a stable string-literal `code` discriminator.
@@ -50,6 +53,7 @@ void refreshReturnIsExact;
5053
void legacyAvailableVersions;
5154
void explicitLegacyAvailableVersions;
5255
void throwingAvailableVersions;
56+
void runtimeApi;
5357
void lookupErrorIsError;
5458
void lookupErrorCodeIsExact;
5559
void guardNarrows;

0 commit comments

Comments
 (0)