Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/services/extensions/common/extensionsProposedApi.ts
3296 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import { isNonEmptyArray } from '../../../../base/common/arrays.js';
7
import { localize } from '../../../../nls.js';
8
import { Disposable } from '../../../../base/common/lifecycle.js';
9
import { ExtensionIdentifier, IExtensionDescription, IExtensionManifest } from '../../../../platform/extensions/common/extensions.js';
10
import { allApiProposals, ApiProposalName } from '../../../../platform/extensions/common/extensionsApiProposals.js';
11
import { SyncDescriptor } from '../../../../platform/instantiation/common/descriptors.js';
12
import { ILogService } from '../../../../platform/log/common/log.js';
13
import { IProductService } from '../../../../platform/product/common/productService.js';
14
import { Registry } from '../../../../platform/registry/common/platform.js';
15
import { IWorkbenchEnvironmentService } from '../../environment/common/environmentService.js';
16
import { Extensions, IExtensionFeatureMarkdownRenderer, IExtensionFeaturesRegistry, IRenderedData } from '../../extensionManagement/common/extensionFeatures.js';
17
import { IMarkdownString, MarkdownString } from '../../../../base/common/htmlContent.js';
18
import { Mutable } from '../../../../base/common/types.js';
19
20
export class ExtensionsProposedApi {
21
22
private readonly _envEnablesProposedApiForAll: boolean;
23
private readonly _envEnabledExtensions: Set<string>;
24
private readonly _productEnabledExtensions: Map<string, string[]>;
25
26
constructor(
27
@ILogService private readonly _logService: ILogService,
28
@IWorkbenchEnvironmentService private readonly _environmentService: IWorkbenchEnvironmentService,
29
@IProductService productService: IProductService
30
) {
31
32
this._envEnabledExtensions = new Set((_environmentService.extensionEnabledProposedApi ?? []).map(id => ExtensionIdentifier.toKey(id)));
33
34
this._envEnablesProposedApiForAll =
35
!_environmentService.isBuilt || // always allow proposed API when running out of sources
36
(_environmentService.isExtensionDevelopment && productService.quality !== 'stable') || // do not allow proposed API against stable builds when developing an extension
37
(this._envEnabledExtensions.size === 0 && Array.isArray(_environmentService.extensionEnabledProposedApi)); // always allow proposed API if --enable-proposed-api is provided without extension ID
38
39
this._productEnabledExtensions = new Map<string, ApiProposalName[]>();
40
41
42
// NEW world - product.json spells out what proposals each extension can use
43
if (productService.extensionEnabledApiProposals) {
44
for (const [k, value] of Object.entries(productService.extensionEnabledApiProposals)) {
45
const key = ExtensionIdentifier.toKey(k);
46
const proposalNames = value.filter(name => {
47
if (!allApiProposals[<ApiProposalName>name]) {
48
_logService.warn(`Via 'product.json#extensionEnabledApiProposals' extension '${key}' wants API proposal '${name}' but that proposal DOES NOT EXIST. Likely, the proposal has been finalized (check 'vscode.d.ts') or was abandoned.`);
49
return false;
50
}
51
return true;
52
});
53
this._productEnabledExtensions.set(key, proposalNames);
54
}
55
}
56
}
57
58
updateEnabledApiProposals(extensions: IExtensionDescription[]): void {
59
for (const extension of extensions) {
60
this.doUpdateEnabledApiProposals(extension);
61
}
62
}
63
64
private doUpdateEnabledApiProposals(extension: Mutable<IExtensionDescription>): void {
65
66
const key = ExtensionIdentifier.toKey(extension.identifier);
67
68
// warn about invalid proposal and remove them from the list
69
if (isNonEmptyArray(extension.enabledApiProposals)) {
70
extension.enabledApiProposals = extension.enabledApiProposals.filter(name => {
71
const result = Boolean(allApiProposals[<ApiProposalName>name]);
72
if (!result) {
73
this._logService.error(`Extension '${key}' wants API proposal '${name}' but that proposal DOES NOT EXIST. Likely, the proposal has been finalized (check 'vscode.d.ts') or was abandoned.`);
74
}
75
return result;
76
});
77
}
78
79
80
if (this._productEnabledExtensions.has(key)) {
81
// NOTE that proposals that are listed in product.json override whatever is declared in the extension
82
// itself. This is needed for us to know what proposals are used "in the wild". Merging product.json-proposals
83
// and extension-proposals would break that.
84
85
const productEnabledProposals = this._productEnabledExtensions.get(key)!;
86
87
// check for difference between product.json-declaration and package.json-declaration
88
const productSet = new Set(productEnabledProposals);
89
const extensionSet = new Set(extension.enabledApiProposals);
90
const diff = new Set([...extensionSet].filter(a => !productSet.has(a)));
91
if (diff.size > 0) {
92
this._logService.error(`Extension '${key}' appears in product.json but enables LESS API proposals than the extension wants.\npackage.json (LOSES): ${[...extensionSet].join(', ')}\nproduct.json (WINS): ${[...productSet].join(', ')}`);
93
94
if (this._environmentService.isExtensionDevelopment) {
95
this._logService.error(`Proceeding with EXTRA proposals (${[...diff].join(', ')}) because extension is in development mode. Still, this EXTENSION WILL BE BROKEN unless product.json is updated.`);
96
productEnabledProposals.push(...diff);
97
}
98
}
99
100
extension.enabledApiProposals = productEnabledProposals;
101
return;
102
}
103
104
if (this._envEnablesProposedApiForAll || this._envEnabledExtensions.has(key)) {
105
// proposed API usage is not restricted and allowed just like the extension
106
// has declared it
107
return;
108
}
109
110
if (!extension.isBuiltin && isNonEmptyArray(extension.enabledApiProposals)) {
111
// restrictive: extension cannot use proposed API in this context and its declaration is nulled
112
this._logService.error(`Extension '${extension.identifier.value} CANNOT USE these API proposals '${extension.enabledApiProposals?.join(', ') || '*'}'. You MUST start in extension development mode or use the --enable-proposed-api command line flag`);
113
extension.enabledApiProposals = [];
114
}
115
}
116
}
117
118
class ApiProposalsMarkdowneRenderer extends Disposable implements IExtensionFeatureMarkdownRenderer {
119
120
readonly type = 'markdown';
121
122
shouldRender(manifest: IExtensionManifest): boolean {
123
return !!manifest.originalEnabledApiProposals?.length || !!manifest.enabledApiProposals?.length;
124
}
125
126
render(manifest: IExtensionManifest): IRenderedData<IMarkdownString> {
127
const enabledApiProposals = manifest.originalEnabledApiProposals ?? manifest.enabledApiProposals ?? [];
128
const data = new MarkdownString();
129
if (enabledApiProposals.length) {
130
for (const proposal of enabledApiProposals) {
131
data.appendMarkdown(`- \`${proposal}\`\n`);
132
}
133
}
134
return {
135
data,
136
dispose: () => { }
137
};
138
}
139
}
140
141
Registry.as<IExtensionFeaturesRegistry>(Extensions.ExtensionFeaturesRegistry).registerExtensionFeature({
142
id: 'enabledApiProposals',
143
label: localize('enabledProposedAPIs', "API Proposals"),
144
access: {
145
canToggle: false
146
},
147
renderer: new SyncDescriptor(ApiProposalsMarkdowneRenderer),
148
});
149
150