@epicdm/flowstate-extension-core 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.mts +1972 -0
- package/dist/index.d.ts +1972 -0
- package/dist/index.js +1042 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +1012 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +48 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1042 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var fflate = require('fflate');
|
|
4
|
+
|
|
5
|
+
// src/manifest-adapter.ts
|
|
6
|
+
var CATEGORY_MAP = {
|
|
7
|
+
"Azure": "azure",
|
|
8
|
+
"Data Science": "data-science",
|
|
9
|
+
"Debuggers": "debuggers",
|
|
10
|
+
"Education": "education",
|
|
11
|
+
"Extension Packs": "extension-packs",
|
|
12
|
+
"Formatters": "formatters",
|
|
13
|
+
"Keymaps": "keymaps",
|
|
14
|
+
"Language Packs": "language-packs",
|
|
15
|
+
"Linters": "linters",
|
|
16
|
+
"Machine Learning": "machine-learning",
|
|
17
|
+
"Notebooks": "notebooks",
|
|
18
|
+
"Other": "other",
|
|
19
|
+
"Programming Languages": "programming-languages",
|
|
20
|
+
"SCM Providers": "scm-providers",
|
|
21
|
+
"Snippets": "snippets",
|
|
22
|
+
"Testing": "testing",
|
|
23
|
+
"Themes": "themes",
|
|
24
|
+
"Visualization": "visualization"
|
|
25
|
+
};
|
|
26
|
+
function mapCategory(vsCodeCategory) {
|
|
27
|
+
if (vsCodeCategory === void 0) return void 0;
|
|
28
|
+
return CATEGORY_MAP[vsCodeCategory];
|
|
29
|
+
}
|
|
30
|
+
function mapActivationEvents(manifest) {
|
|
31
|
+
const events = manifest.activationEvents;
|
|
32
|
+
if (!events || events.length === 0) return [];
|
|
33
|
+
return events.map((event) => {
|
|
34
|
+
if (event === "onStartupFinished") return "onStartup";
|
|
35
|
+
return event;
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
function mapExtensionKind(manifest) {
|
|
39
|
+
const contributes = manifest.contributes;
|
|
40
|
+
if (contributes) {
|
|
41
|
+
const contributionKeys = Object.keys(contributes).filter(
|
|
42
|
+
(key) => contributes[key] !== void 0
|
|
43
|
+
);
|
|
44
|
+
const themeKeys = /* @__PURE__ */ new Set(["themes", "iconThemes", "productIconThemes", "colors"]);
|
|
45
|
+
const isThemeOnly = contributionKeys.length > 0 && contributionKeys.every((key) => themeKeys.has(key));
|
|
46
|
+
if (isThemeOnly) return "theme";
|
|
47
|
+
if (contributes.localizations && contributes.localizations.length > 0) {
|
|
48
|
+
return "pack";
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
if (manifest.extensionKind?.includes("workspace")) {
|
|
52
|
+
return "app";
|
|
53
|
+
}
|
|
54
|
+
return "ui-extension";
|
|
55
|
+
}
|
|
56
|
+
function vsCodeToFlowState(manifest) {
|
|
57
|
+
const displayName = typeof manifest.displayName === "string" ? manifest.displayName : manifest.displayName?.value ?? manifest.name;
|
|
58
|
+
const result = {
|
|
59
|
+
id: `${manifest.publisher}.${manifest.name}`,
|
|
60
|
+
displayName,
|
|
61
|
+
version: manifest.version,
|
|
62
|
+
publisher: manifest.publisher,
|
|
63
|
+
main: manifest.main ?? manifest.browser ?? ""
|
|
64
|
+
};
|
|
65
|
+
if (manifest.browser !== void 0) {
|
|
66
|
+
result.browser = manifest.browser;
|
|
67
|
+
}
|
|
68
|
+
const category = mapCategory(manifest.categories?.[0]);
|
|
69
|
+
if (category !== void 0) {
|
|
70
|
+
result.category = category;
|
|
71
|
+
}
|
|
72
|
+
const extensionType = mapExtensionKind(manifest);
|
|
73
|
+
if (extensionType !== "ui-extension") {
|
|
74
|
+
result.extensionType = extensionType;
|
|
75
|
+
}
|
|
76
|
+
const activationEvents = mapActivationEvents(manifest);
|
|
77
|
+
if (activationEvents.length > 0) {
|
|
78
|
+
result.activationEvents = activationEvents;
|
|
79
|
+
}
|
|
80
|
+
if (manifest.contributes !== void 0) {
|
|
81
|
+
result.contributes = manifest.contributes;
|
|
82
|
+
}
|
|
83
|
+
return result;
|
|
84
|
+
}
|
|
85
|
+
var REVERSE_CATEGORY_MAP = Object.fromEntries(
|
|
86
|
+
Object.entries(CATEGORY_MAP).map(([vsCode, flowstate]) => [flowstate, vsCode])
|
|
87
|
+
);
|
|
88
|
+
function reverseCategory(flowstateCategory) {
|
|
89
|
+
if (flowstateCategory === void 0) return void 0;
|
|
90
|
+
return REVERSE_CATEGORY_MAP[flowstateCategory];
|
|
91
|
+
}
|
|
92
|
+
function reverseActivationEvents(events) {
|
|
93
|
+
if (!events || events.length === 0) return [];
|
|
94
|
+
return events.map((event) => {
|
|
95
|
+
if (event === "onStartup") return "onStartupFinished";
|
|
96
|
+
return event;
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
function reverseExtensionKind(extensionType) {
|
|
100
|
+
switch (extensionType) {
|
|
101
|
+
case "app":
|
|
102
|
+
return ["workspace"];
|
|
103
|
+
case "theme":
|
|
104
|
+
case "pack":
|
|
105
|
+
return ["ui"];
|
|
106
|
+
default:
|
|
107
|
+
return void 0;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
function flowStateToVsCode(manifest) {
|
|
111
|
+
const dotIndex = manifest.id.indexOf(".");
|
|
112
|
+
const name = dotIndex >= 0 ? manifest.id.slice(dotIndex + 1) : manifest.id;
|
|
113
|
+
const publisher = manifest.publisher ?? (dotIndex >= 0 ? manifest.id.slice(0, dotIndex) : "");
|
|
114
|
+
const result = {
|
|
115
|
+
name,
|
|
116
|
+
publisher,
|
|
117
|
+
version: manifest.version,
|
|
118
|
+
displayName: manifest.displayName
|
|
119
|
+
};
|
|
120
|
+
if (manifest.main) {
|
|
121
|
+
result.main = manifest.main;
|
|
122
|
+
}
|
|
123
|
+
if (manifest.browser !== void 0) {
|
|
124
|
+
result.browser = manifest.browser;
|
|
125
|
+
}
|
|
126
|
+
const vsCodeCategory = reverseCategory(manifest.category);
|
|
127
|
+
if (vsCodeCategory !== void 0) {
|
|
128
|
+
result.categories = [vsCodeCategory];
|
|
129
|
+
}
|
|
130
|
+
const extensionKind = reverseExtensionKind(manifest.extensionType);
|
|
131
|
+
if (extensionKind !== void 0) {
|
|
132
|
+
result.extensionKind = extensionKind;
|
|
133
|
+
}
|
|
134
|
+
const activationEvents = reverseActivationEvents(manifest.activationEvents);
|
|
135
|
+
if (activationEvents.length > 0) {
|
|
136
|
+
result.activationEvents = activationEvents;
|
|
137
|
+
}
|
|
138
|
+
if (manifest.contributes !== void 0) {
|
|
139
|
+
result.contributes = manifest.contributes;
|
|
140
|
+
}
|
|
141
|
+
return result;
|
|
142
|
+
}
|
|
143
|
+
var DECLARATIVE_CONTRIBUTION_POINTS = /* @__PURE__ */ new Set([
|
|
144
|
+
"themes",
|
|
145
|
+
"iconThemes",
|
|
146
|
+
"productIconThemes",
|
|
147
|
+
"colors",
|
|
148
|
+
"grammars",
|
|
149
|
+
"snippets",
|
|
150
|
+
"languages",
|
|
151
|
+
"keybindings",
|
|
152
|
+
"configuration",
|
|
153
|
+
"configurationDefaults",
|
|
154
|
+
"jsonValidation",
|
|
155
|
+
"localizations",
|
|
156
|
+
"semanticTokenTypes",
|
|
157
|
+
"semanticTokenModifiers",
|
|
158
|
+
"semanticTokenScopes"
|
|
159
|
+
]);
|
|
160
|
+
var VsixScanError = class _VsixScanError extends Error {
|
|
161
|
+
constructor(code, message) {
|
|
162
|
+
super(message);
|
|
163
|
+
Object.setPrototypeOf(this, _VsixScanError.prototype);
|
|
164
|
+
this.name = "VsixScanError";
|
|
165
|
+
this.code = code;
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
function classifyDeclarative(manifest) {
|
|
169
|
+
if (manifest.main || manifest.browser) return false;
|
|
170
|
+
const contributes = manifest.contributes;
|
|
171
|
+
if (!contributes) return true;
|
|
172
|
+
const keys = Object.keys(contributes).filter(
|
|
173
|
+
(key) => contributes[key] !== void 0
|
|
174
|
+
);
|
|
175
|
+
if (keys.length === 0) return true;
|
|
176
|
+
return keys.every((key) => DECLARATIVE_CONTRIBUTION_POINTS.has(key));
|
|
177
|
+
}
|
|
178
|
+
function enumerateContributionPoints(manifest) {
|
|
179
|
+
const contributes = manifest.contributes;
|
|
180
|
+
if (!contributes) return [];
|
|
181
|
+
return Object.keys(contributes).filter((key) => {
|
|
182
|
+
const value = contributes[key];
|
|
183
|
+
if (value === void 0 || value === null) return false;
|
|
184
|
+
if (Array.isArray(value) && value.length === 0) return false;
|
|
185
|
+
if (typeof value === "object" && !Array.isArray(value) && Object.keys(value).length === 0)
|
|
186
|
+
return false;
|
|
187
|
+
return true;
|
|
188
|
+
}).sort();
|
|
189
|
+
}
|
|
190
|
+
var textDecoder = new TextDecoder();
|
|
191
|
+
var VSCODE_NAMESPACE_PATTERN = /\bvscode\.(\w+)/g;
|
|
192
|
+
function detectApiNamespaces(assets) {
|
|
193
|
+
const namespaces = /* @__PURE__ */ new Set();
|
|
194
|
+
for (const [path, content] of assets) {
|
|
195
|
+
if (!/\.[cm]?js$/.test(path)) continue;
|
|
196
|
+
const text = textDecoder.decode(content);
|
|
197
|
+
let match;
|
|
198
|
+
VSCODE_NAMESPACE_PATTERN.lastIndex = 0;
|
|
199
|
+
while ((match = VSCODE_NAMESPACE_PATTERN.exec(text)) !== null) {
|
|
200
|
+
namespaces.add(`vscode.${match[1]}`);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return Array.from(namespaces).sort();
|
|
204
|
+
}
|
|
205
|
+
function buildCompatibilityReport(manifest, apiNamespaces) {
|
|
206
|
+
const contributionKeys = enumerateContributionPoints(manifest);
|
|
207
|
+
const supported = contributionKeys.filter((key) => DECLARATIVE_CONTRIBUTION_POINTS.has(key));
|
|
208
|
+
const unsupported = contributionKeys.filter((key) => !DECLARATIVE_CONTRIBUTION_POINTS.has(key));
|
|
209
|
+
const warnings = [];
|
|
210
|
+
if (manifest.main || manifest.browser) {
|
|
211
|
+
warnings.push("Extension has a code entry point and requires an extension host to run.");
|
|
212
|
+
}
|
|
213
|
+
if (unsupported.length > 0) {
|
|
214
|
+
warnings.push(
|
|
215
|
+
`Unsupported contribution points: ${unsupported.join(", ")}. These require a VS Code extension host.`
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
if (apiNamespaces.length > 0) {
|
|
219
|
+
warnings.push(`Uses VS Code API namespaces: ${apiNamespaces.join(", ")}.`);
|
|
220
|
+
}
|
|
221
|
+
const hasEntryPoint = Boolean(manifest.main || manifest.browser);
|
|
222
|
+
const needsRuntime = hasEntryPoint || apiNamespaces.length > 0 || unsupported.length > 0;
|
|
223
|
+
let level;
|
|
224
|
+
if (!needsRuntime && supported.length > 0) {
|
|
225
|
+
level = "declarative-only";
|
|
226
|
+
} else if (needsRuntime && supported.length > 0) {
|
|
227
|
+
level = "partial";
|
|
228
|
+
} else {
|
|
229
|
+
level = "unsupported";
|
|
230
|
+
}
|
|
231
|
+
return {
|
|
232
|
+
level,
|
|
233
|
+
supported,
|
|
234
|
+
unsupported,
|
|
235
|
+
apiNamespacesUsed: apiNamespaces,
|
|
236
|
+
warnings
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
var EXTENSION_PREFIX = "extension/";
|
|
240
|
+
function scanVsix(data) {
|
|
241
|
+
let entries;
|
|
242
|
+
try {
|
|
243
|
+
entries = fflate.unzipSync(data);
|
|
244
|
+
} catch {
|
|
245
|
+
throw new VsixScanError("INVALID_ZIP", "Failed to decompress VSIX archive: invalid ZIP data.");
|
|
246
|
+
}
|
|
247
|
+
const assets = /* @__PURE__ */ new Map();
|
|
248
|
+
for (const [path, content] of Object.entries(entries)) {
|
|
249
|
+
if (path.startsWith(EXTENSION_PREFIX)) {
|
|
250
|
+
const relativePath = path.slice(EXTENSION_PREFIX.length);
|
|
251
|
+
if (relativePath.length > 0) {
|
|
252
|
+
assets.set(relativePath, content);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
const manifestData = assets.get("package.json");
|
|
257
|
+
if (!manifestData) {
|
|
258
|
+
throw new VsixScanError(
|
|
259
|
+
"MISSING_MANIFEST",
|
|
260
|
+
"VSIX archive does not contain extension/package.json."
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
let rawManifest;
|
|
264
|
+
try {
|
|
265
|
+
rawManifest = JSON.parse(textDecoder.decode(manifestData));
|
|
266
|
+
} catch {
|
|
267
|
+
throw new VsixScanError(
|
|
268
|
+
"INVALID_MANIFEST",
|
|
269
|
+
"Failed to parse extension/package.json: invalid JSON."
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
const engines = rawManifest["engines"];
|
|
273
|
+
if (typeof rawManifest["name"] !== "string" || typeof rawManifest["publisher"] !== "string" || typeof rawManifest["version"] !== "string" || typeof engines !== "object" || engines === null || typeof engines["vscode"] !== "string") {
|
|
274
|
+
throw new VsixScanError(
|
|
275
|
+
"INVALID_MANIFEST",
|
|
276
|
+
"Extension manifest missing required fields: name, publisher, version, and engines.vscode are required."
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
const vsCodeManifest = rawManifest;
|
|
280
|
+
const flowstateManifest = vsCodeToFlowState(vsCodeManifest);
|
|
281
|
+
const isDeclarative = classifyDeclarative(vsCodeManifest);
|
|
282
|
+
const contributionPoints = enumerateContributionPoints(vsCodeManifest);
|
|
283
|
+
const apiNamespaces = detectApiNamespaces(assets);
|
|
284
|
+
const compatibility = buildCompatibilityReport(vsCodeManifest, apiNamespaces);
|
|
285
|
+
return {
|
|
286
|
+
vsCodeManifest,
|
|
287
|
+
flowstateManifest,
|
|
288
|
+
isDeclarative,
|
|
289
|
+
contributionPoints,
|
|
290
|
+
assets,
|
|
291
|
+
compatibility
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// src/compatibility-scorer.ts
|
|
296
|
+
var SUPPORT_WEIGHTS = {
|
|
297
|
+
native: 1,
|
|
298
|
+
polyfill: 0.8,
|
|
299
|
+
partial: 0.5,
|
|
300
|
+
unsupported: 0
|
|
301
|
+
};
|
|
302
|
+
var PLATFORM_SUPPORT = {
|
|
303
|
+
desktop: {
|
|
304
|
+
themes: "native",
|
|
305
|
+
iconThemes: "native",
|
|
306
|
+
productIconThemes: "native",
|
|
307
|
+
colors: "native",
|
|
308
|
+
grammars: "native",
|
|
309
|
+
snippets: "native",
|
|
310
|
+
languages: "native",
|
|
311
|
+
keybindings: "native",
|
|
312
|
+
configuration: "native",
|
|
313
|
+
configurationDefaults: "native",
|
|
314
|
+
jsonValidation: "native",
|
|
315
|
+
localizations: "native",
|
|
316
|
+
semanticTokenTypes: "native",
|
|
317
|
+
semanticTokenModifiers: "native",
|
|
318
|
+
semanticTokenScopes: "native",
|
|
319
|
+
commands: "polyfill",
|
|
320
|
+
menus: "polyfill",
|
|
321
|
+
views: "partial",
|
|
322
|
+
viewsContainers: "partial"
|
|
323
|
+
},
|
|
324
|
+
web: {
|
|
325
|
+
themes: "native",
|
|
326
|
+
iconThemes: "native",
|
|
327
|
+
productIconThemes: "native",
|
|
328
|
+
colors: "native",
|
|
329
|
+
grammars: "native",
|
|
330
|
+
snippets: "native",
|
|
331
|
+
languages: "native",
|
|
332
|
+
keybindings: "native",
|
|
333
|
+
configuration: "native",
|
|
334
|
+
configurationDefaults: "native",
|
|
335
|
+
jsonValidation: "native",
|
|
336
|
+
localizations: "native",
|
|
337
|
+
semanticTokenTypes: "native",
|
|
338
|
+
semanticTokenModifiers: "native",
|
|
339
|
+
semanticTokenScopes: "native"
|
|
340
|
+
},
|
|
341
|
+
mobile: {
|
|
342
|
+
themes: "native",
|
|
343
|
+
colors: "native",
|
|
344
|
+
snippets: "native",
|
|
345
|
+
languages: "native",
|
|
346
|
+
keybindings: "partial",
|
|
347
|
+
configuration: "native",
|
|
348
|
+
configurationDefaults: "native",
|
|
349
|
+
localizations: "native"
|
|
350
|
+
}
|
|
351
|
+
};
|
|
352
|
+
var PLATFORMS_WITH_EXTENSION_HOST = /* @__PURE__ */ new Set(["desktop"]);
|
|
353
|
+
var ENTRY_POINT_PENALTY = 0.5;
|
|
354
|
+
function getPlatformSupport(platform) {
|
|
355
|
+
return { ...PLATFORM_SUPPORT[platform] };
|
|
356
|
+
}
|
|
357
|
+
function scoreExtension(manifest, platform, apiNamespaces = []) {
|
|
358
|
+
const contributionKeys = enumerateContributionPoints(manifest);
|
|
359
|
+
const platformSupport = PLATFORM_SUPPORT[platform];
|
|
360
|
+
const breakdown = contributionKeys.map((key) => {
|
|
361
|
+
const status = platformSupport[key] ?? "unsupported";
|
|
362
|
+
return {
|
|
363
|
+
key,
|
|
364
|
+
status,
|
|
365
|
+
weight: SUPPORT_WEIGHTS[status]
|
|
366
|
+
};
|
|
367
|
+
});
|
|
368
|
+
let score;
|
|
369
|
+
if (breakdown.length === 0) {
|
|
370
|
+
score = 0;
|
|
371
|
+
} else {
|
|
372
|
+
const totalWeight = breakdown.reduce((sum, point) => sum + point.weight, 0);
|
|
373
|
+
score = Math.round(totalWeight / breakdown.length * 100);
|
|
374
|
+
}
|
|
375
|
+
const hasEntryPoint = Boolean(manifest.main || manifest.browser);
|
|
376
|
+
if (hasEntryPoint && !PLATFORMS_WITH_EXTENSION_HOST.has(platform)) {
|
|
377
|
+
score = Math.round(score * ENTRY_POINT_PENALTY);
|
|
378
|
+
}
|
|
379
|
+
const level = deriveLevel(score);
|
|
380
|
+
const recommendations = generateRecommendations(breakdown, manifest, platform);
|
|
381
|
+
const report = buildCompatibilityReport(manifest, apiNamespaces);
|
|
382
|
+
return {
|
|
383
|
+
score,
|
|
384
|
+
level,
|
|
385
|
+
platform,
|
|
386
|
+
breakdown,
|
|
387
|
+
report,
|
|
388
|
+
recommendations
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
function generateRecommendations(breakdown, manifest, platform) {
|
|
392
|
+
const recommendations = [];
|
|
393
|
+
const hasEntryPoint = Boolean(manifest.main || manifest.browser);
|
|
394
|
+
const unsupportedPoints = breakdown.filter((p) => p.status === "unsupported");
|
|
395
|
+
const partialPoints = breakdown.filter((p) => p.status === "partial");
|
|
396
|
+
const allNative = breakdown.length > 0 && breakdown.every((p) => p.status === "native");
|
|
397
|
+
if (hasEntryPoint && !PLATFORMS_WITH_EXTENSION_HOST.has(platform)) {
|
|
398
|
+
recommendations.push(
|
|
399
|
+
"This extension requires an extension host \u2014 only declarative contributions will work on this platform."
|
|
400
|
+
);
|
|
401
|
+
}
|
|
402
|
+
if (unsupportedPoints.length > 0 && platform !== "desktop") {
|
|
403
|
+
const pointNames = unsupportedPoints.map((p) => p.key).join(", ");
|
|
404
|
+
recommendations.push(
|
|
405
|
+
`Consider using the desktop platform for full support of: ${pointNames}.`
|
|
406
|
+
);
|
|
407
|
+
}
|
|
408
|
+
if (partialPoints.length > 0) {
|
|
409
|
+
const pointNames = partialPoints.map((p) => p.key).join(", ");
|
|
410
|
+
recommendations.push(
|
|
411
|
+
`Partial support for: ${pointNames}. Some features may not work as expected.`
|
|
412
|
+
);
|
|
413
|
+
}
|
|
414
|
+
if (allNative && (!hasEntryPoint || PLATFORMS_WITH_EXTENSION_HOST.has(platform))) {
|
|
415
|
+
recommendations.push("All contribution points are natively supported on this platform.");
|
|
416
|
+
}
|
|
417
|
+
return recommendations;
|
|
418
|
+
}
|
|
419
|
+
function deriveLevel(score) {
|
|
420
|
+
if (score >= 90) return "full";
|
|
421
|
+
if (score >= 50) return "partial";
|
|
422
|
+
if (score > 0) return "declarative-only";
|
|
423
|
+
return "unsupported";
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// src/dependency-resolver.ts
|
|
427
|
+
function normalizeExtensionId(id) {
|
|
428
|
+
return id.toLowerCase();
|
|
429
|
+
}
|
|
430
|
+
function extractDependencyNode(manifest) {
|
|
431
|
+
const id = normalizeExtensionId(`${manifest.publisher}.${manifest.name}`);
|
|
432
|
+
const extensionDependencies = (manifest.extensionDependencies ?? []).map(normalizeExtensionId);
|
|
433
|
+
const extensionPack = (manifest.extensionPack ?? []).map(normalizeExtensionId);
|
|
434
|
+
return { id, extensionDependencies, extensionPack };
|
|
435
|
+
}
|
|
436
|
+
function buildDependencyGraph(nodes) {
|
|
437
|
+
const nodeMap = /* @__PURE__ */ new Map();
|
|
438
|
+
const edges = [];
|
|
439
|
+
const adjacency = /* @__PURE__ */ new Map();
|
|
440
|
+
for (const node of nodes) {
|
|
441
|
+
nodeMap.set(node.id, node);
|
|
442
|
+
if (!adjacency.has(node.id)) {
|
|
443
|
+
adjacency.set(node.id, /* @__PURE__ */ new Set());
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
for (const node of nodes) {
|
|
447
|
+
for (const dep of node.extensionDependencies) {
|
|
448
|
+
edges.push({ from: node.id, to: dep, type: "dependency" });
|
|
449
|
+
adjacency.get(node.id).add(dep);
|
|
450
|
+
if (!nodeMap.has(dep)) {
|
|
451
|
+
nodeMap.set(dep, { id: dep, extensionDependencies: [], extensionPack: [] });
|
|
452
|
+
adjacency.set(dep, /* @__PURE__ */ new Set());
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
for (const member of node.extensionPack) {
|
|
456
|
+
edges.push({ from: node.id, to: member, type: "pack" });
|
|
457
|
+
adjacency.get(node.id).add(member);
|
|
458
|
+
if (!nodeMap.has(member)) {
|
|
459
|
+
nodeMap.set(member, { id: member, extensionDependencies: [], extensionPack: [] });
|
|
460
|
+
adjacency.set(member, /* @__PURE__ */ new Set());
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
return { nodes: nodeMap, edges, adjacency };
|
|
465
|
+
}
|
|
466
|
+
function resolveDependencies(nodes) {
|
|
467
|
+
const graph = buildDependencyGraph(nodes);
|
|
468
|
+
if (graph.nodes.size === 0) {
|
|
469
|
+
return { ok: true, order: [], graph };
|
|
470
|
+
}
|
|
471
|
+
const reverseAdj = /* @__PURE__ */ new Map();
|
|
472
|
+
for (const id of graph.nodes.keys()) {
|
|
473
|
+
reverseAdj.set(id, /* @__PURE__ */ new Set());
|
|
474
|
+
}
|
|
475
|
+
for (const [id, deps] of graph.adjacency) {
|
|
476
|
+
for (const dep of deps) {
|
|
477
|
+
reverseAdj.get(dep).add(id);
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
const inDegree = /* @__PURE__ */ new Map();
|
|
481
|
+
for (const [id, deps] of graph.adjacency) {
|
|
482
|
+
inDegree.set(id, deps.size);
|
|
483
|
+
}
|
|
484
|
+
const queue = [];
|
|
485
|
+
for (const [id, degree] of inDegree) {
|
|
486
|
+
if (degree === 0) {
|
|
487
|
+
queue.push(id);
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
queue.sort();
|
|
491
|
+
const order = [];
|
|
492
|
+
let queueIdx = 0;
|
|
493
|
+
while (queueIdx < queue.length) {
|
|
494
|
+
const current = queue[queueIdx++];
|
|
495
|
+
order.push(current);
|
|
496
|
+
const freed = [];
|
|
497
|
+
for (const dependent of reverseAdj.get(current)) {
|
|
498
|
+
const newDegree = inDegree.get(dependent) - 1;
|
|
499
|
+
inDegree.set(dependent, newDegree);
|
|
500
|
+
if (newDegree === 0) {
|
|
501
|
+
freed.push(dependent);
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
if (freed.length > 0) {
|
|
505
|
+
freed.sort();
|
|
506
|
+
queue.push(...freed);
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
if (order.length < graph.nodes.size) {
|
|
510
|
+
const visited = new Set(order);
|
|
511
|
+
const participants = Array.from(graph.nodes.keys()).filter((id) => !visited.has(id)).sort();
|
|
512
|
+
return { ok: false, error: "circular-dependency", participants, graph };
|
|
513
|
+
}
|
|
514
|
+
return { ok: true, order, graph };
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
// src/openvsx-client.ts
|
|
518
|
+
var DEFAULT_BASE_URL = "https://open-vsx.org/api";
|
|
519
|
+
var OpenVSXClient = class {
|
|
520
|
+
constructor(options) {
|
|
521
|
+
const raw = options?.baseUrl ?? DEFAULT_BASE_URL;
|
|
522
|
+
this.baseUrl = raw.endsWith("/") ? raw.slice(0, -1) : raw;
|
|
523
|
+
this.fetch = options?.fetch ?? globalThis.fetch;
|
|
524
|
+
}
|
|
525
|
+
// -------------------------------------------------------------------------
|
|
526
|
+
// Search & query
|
|
527
|
+
// -------------------------------------------------------------------------
|
|
528
|
+
/**
|
|
529
|
+
* Search extensions by text query.
|
|
530
|
+
*
|
|
531
|
+
* @param params - Search parameters (query, category, sort, pagination)
|
|
532
|
+
* @returns Paginated search results
|
|
533
|
+
*/
|
|
534
|
+
async search(params) {
|
|
535
|
+
const queryParams = {};
|
|
536
|
+
if (params) {
|
|
537
|
+
if (params.query !== void 0) queryParams["query"] = params.query;
|
|
538
|
+
if (params.category !== void 0) queryParams["category"] = params.category;
|
|
539
|
+
if (params.targetPlatform !== void 0) queryParams["targetPlatform"] = params.targetPlatform;
|
|
540
|
+
if (params.size !== void 0) queryParams["size"] = String(params.size);
|
|
541
|
+
if (params.offset !== void 0) queryParams["offset"] = String(params.offset);
|
|
542
|
+
if (params.sortOrder !== void 0) queryParams["sortOrder"] = params.sortOrder;
|
|
543
|
+
if (params.sortBy !== void 0) queryParams["sortBy"] = params.sortBy;
|
|
544
|
+
if (params.includeAllVersions !== void 0)
|
|
545
|
+
queryParams["includeAllVersions"] = String(params.includeAllVersions);
|
|
546
|
+
}
|
|
547
|
+
return this._get("/-/search", queryParams);
|
|
548
|
+
}
|
|
549
|
+
/**
|
|
550
|
+
* Query extensions with structured field matching.
|
|
551
|
+
*
|
|
552
|
+
* @param params - Query parameters (namespace, extension name, ID, UUID, etc.)
|
|
553
|
+
* @returns Paginated query results with full extension metadata
|
|
554
|
+
*/
|
|
555
|
+
async query(params) {
|
|
556
|
+
const queryParams = {};
|
|
557
|
+
if (params) {
|
|
558
|
+
if (params.namespaceName !== void 0) queryParams["namespaceName"] = params.namespaceName;
|
|
559
|
+
if (params.extensionName !== void 0) queryParams["extensionName"] = params.extensionName;
|
|
560
|
+
if (params.extensionVersion !== void 0)
|
|
561
|
+
queryParams["extensionVersion"] = params.extensionVersion;
|
|
562
|
+
if (params.extensionId !== void 0) queryParams["extensionId"] = params.extensionId;
|
|
563
|
+
if (params.extensionUuid !== void 0) queryParams["extensionUuid"] = params.extensionUuid;
|
|
564
|
+
if (params.namespaceUuid !== void 0) queryParams["namespaceUuid"] = params.namespaceUuid;
|
|
565
|
+
if (params.includeAllVersions !== void 0)
|
|
566
|
+
queryParams["includeAllVersions"] = String(params.includeAllVersions);
|
|
567
|
+
if (params.targetPlatform !== void 0) queryParams["targetPlatform"] = params.targetPlatform;
|
|
568
|
+
if (params.size !== void 0) queryParams["size"] = String(params.size);
|
|
569
|
+
if (params.offset !== void 0) queryParams["offset"] = String(params.offset);
|
|
570
|
+
}
|
|
571
|
+
return this._get("/-/query", queryParams);
|
|
572
|
+
}
|
|
573
|
+
// -------------------------------------------------------------------------
|
|
574
|
+
// Extension metadata
|
|
575
|
+
// -------------------------------------------------------------------------
|
|
576
|
+
/**
|
|
577
|
+
* Get extension metadata (latest version or specific version).
|
|
578
|
+
*
|
|
579
|
+
* @param namespace - Publisher namespace (e.g., `'ms-python'`)
|
|
580
|
+
* @param name - Extension name (e.g., `'python'`)
|
|
581
|
+
* @param version - Specific version string (omit for latest)
|
|
582
|
+
* @param targetPlatform - Target platform filter
|
|
583
|
+
* @returns Full extension metadata
|
|
584
|
+
*/
|
|
585
|
+
async getExtension(namespace, name, version, targetPlatform) {
|
|
586
|
+
let path = `/${encodeURIComponent(namespace)}/${encodeURIComponent(name)}`;
|
|
587
|
+
if (targetPlatform) {
|
|
588
|
+
path += `/${encodeURIComponent(targetPlatform)}`;
|
|
589
|
+
}
|
|
590
|
+
if (version) {
|
|
591
|
+
path += `/${encodeURIComponent(version)}`;
|
|
592
|
+
}
|
|
593
|
+
return this._get(path);
|
|
594
|
+
}
|
|
595
|
+
// -------------------------------------------------------------------------
|
|
596
|
+
// Namespace
|
|
597
|
+
// -------------------------------------------------------------------------
|
|
598
|
+
/**
|
|
599
|
+
* Get namespace information with extension listing.
|
|
600
|
+
*
|
|
601
|
+
* @param namespace - Namespace name (e.g., `'ms-python'`)
|
|
602
|
+
* @returns Namespace with map of extension name → API URL
|
|
603
|
+
*/
|
|
604
|
+
async getNamespace(namespace) {
|
|
605
|
+
return this._get(`/${encodeURIComponent(namespace)}`);
|
|
606
|
+
}
|
|
607
|
+
/**
|
|
608
|
+
* Get namespace details including display name, social links, and extensions.
|
|
609
|
+
*
|
|
610
|
+
* @param namespace - Namespace name
|
|
611
|
+
* @returns Namespace details with social links and extension summaries
|
|
612
|
+
*/
|
|
613
|
+
async getNamespaceDetails(namespace) {
|
|
614
|
+
return this._get(`/${encodeURIComponent(namespace)}/details`);
|
|
615
|
+
}
|
|
616
|
+
// -------------------------------------------------------------------------
|
|
617
|
+
// Versions
|
|
618
|
+
// -------------------------------------------------------------------------
|
|
619
|
+
/**
|
|
620
|
+
* List extension versions with pagination.
|
|
621
|
+
*
|
|
622
|
+
* @param namespace - Publisher namespace
|
|
623
|
+
* @param name - Extension name
|
|
624
|
+
* @param params - Pagination and platform filter options
|
|
625
|
+
* @returns Paginated version listing (version string → API URL)
|
|
626
|
+
*/
|
|
627
|
+
async getVersions(namespace, name, params) {
|
|
628
|
+
let path = `/${encodeURIComponent(namespace)}/${encodeURIComponent(name)}`;
|
|
629
|
+
if (params?.targetPlatform) {
|
|
630
|
+
path += `/${encodeURIComponent(params.targetPlatform)}`;
|
|
631
|
+
}
|
|
632
|
+
path += "/versions";
|
|
633
|
+
const queryParams = {};
|
|
634
|
+
if (params?.size !== void 0) queryParams["size"] = String(params.size);
|
|
635
|
+
if (params?.offset !== void 0) queryParams["offset"] = String(params.offset);
|
|
636
|
+
return this._get(path, queryParams);
|
|
637
|
+
}
|
|
638
|
+
/**
|
|
639
|
+
* List version references with engine compatibility and file URLs.
|
|
640
|
+
*
|
|
641
|
+
* @param namespace - Publisher namespace
|
|
642
|
+
* @param name - Extension name
|
|
643
|
+
* @param params - Pagination and platform filter options
|
|
644
|
+
* @returns Paginated version references
|
|
645
|
+
*/
|
|
646
|
+
async getVersionReferences(namespace, name, params) {
|
|
647
|
+
let path = `/${encodeURIComponent(namespace)}/${encodeURIComponent(name)}`;
|
|
648
|
+
if (params?.targetPlatform) {
|
|
649
|
+
path += `/${encodeURIComponent(params.targetPlatform)}`;
|
|
650
|
+
}
|
|
651
|
+
path += "/version-references";
|
|
652
|
+
const queryParams = {};
|
|
653
|
+
if (params?.size !== void 0) queryParams["size"] = String(params.size);
|
|
654
|
+
if (params?.offset !== void 0) queryParams["offset"] = String(params.offset);
|
|
655
|
+
return this._get(path, queryParams);
|
|
656
|
+
}
|
|
657
|
+
// -------------------------------------------------------------------------
|
|
658
|
+
// Reviews
|
|
659
|
+
// -------------------------------------------------------------------------
|
|
660
|
+
/**
|
|
661
|
+
* Get all reviews for an extension.
|
|
662
|
+
*
|
|
663
|
+
* @param namespace - Publisher namespace
|
|
664
|
+
* @param name - Extension name
|
|
665
|
+
* @returns Review list with all reviews (not paginated)
|
|
666
|
+
*/
|
|
667
|
+
async getReviews(namespace, name) {
|
|
668
|
+
return this._get(
|
|
669
|
+
`/${encodeURIComponent(namespace)}/${encodeURIComponent(name)}/reviews`
|
|
670
|
+
);
|
|
671
|
+
}
|
|
672
|
+
// -------------------------------------------------------------------------
|
|
673
|
+
// URL builders (pure, no fetch)
|
|
674
|
+
// -------------------------------------------------------------------------
|
|
675
|
+
/**
|
|
676
|
+
* Build a VSIX download URL for a specific extension version.
|
|
677
|
+
*
|
|
678
|
+
* This is a pure URL builder — no network request is made.
|
|
679
|
+
*
|
|
680
|
+
* @param namespace - Publisher namespace
|
|
681
|
+
* @param name - Extension name
|
|
682
|
+
* @param version - Version string
|
|
683
|
+
* @param targetPlatform - Target platform (omit for universal)
|
|
684
|
+
* @returns Full download URL
|
|
685
|
+
*/
|
|
686
|
+
getDownloadUrl(namespace, name, version, targetPlatform) {
|
|
687
|
+
let path = `/${encodeURIComponent(namespace)}/${encodeURIComponent(name)}`;
|
|
688
|
+
if (targetPlatform) {
|
|
689
|
+
path += `/${encodeURIComponent(targetPlatform)}`;
|
|
690
|
+
}
|
|
691
|
+
path += `/${encodeURIComponent(version)}/file/${encodeURIComponent(namespace)}.${encodeURIComponent(name)}-${encodeURIComponent(version)}.vsix`;
|
|
692
|
+
return `${this.baseUrl}${path}`;
|
|
693
|
+
}
|
|
694
|
+
/**
|
|
695
|
+
* Build a URL for accessing an extension file (README, icon, etc.).
|
|
696
|
+
*
|
|
697
|
+
* This is a pure URL builder — no network request is made.
|
|
698
|
+
*
|
|
699
|
+
* @param namespace - Publisher namespace
|
|
700
|
+
* @param name - Extension name
|
|
701
|
+
* @param version - Version string
|
|
702
|
+
* @param filePath - Relative file path within the extension
|
|
703
|
+
* @param targetPlatform - Target platform (omit for universal)
|
|
704
|
+
* @returns Full file URL
|
|
705
|
+
*/
|
|
706
|
+
resolveFileUrl(namespace, name, version, filePath, targetPlatform) {
|
|
707
|
+
let path = `/${encodeURIComponent(namespace)}/${encodeURIComponent(name)}`;
|
|
708
|
+
if (targetPlatform) {
|
|
709
|
+
path += `/${encodeURIComponent(targetPlatform)}`;
|
|
710
|
+
}
|
|
711
|
+
const encodedFilePath = filePath.split("/").map(encodeURIComponent).join("/");
|
|
712
|
+
path += `/${encodeURIComponent(version)}/file/${encodedFilePath}`;
|
|
713
|
+
return `${this.baseUrl}${path}`;
|
|
714
|
+
}
|
|
715
|
+
// -------------------------------------------------------------------------
|
|
716
|
+
// Private helpers
|
|
717
|
+
// -------------------------------------------------------------------------
|
|
718
|
+
/** Build a full URL with optional query parameters. */
|
|
719
|
+
_buildUrl(path, params) {
|
|
720
|
+
const url = `${this.baseUrl}${path}`;
|
|
721
|
+
if (!params || Object.keys(params).length === 0) {
|
|
722
|
+
return url;
|
|
723
|
+
}
|
|
724
|
+
const searchParams = new URLSearchParams(params);
|
|
725
|
+
return `${url}?${searchParams.toString()}`;
|
|
726
|
+
}
|
|
727
|
+
/** Execute a GET request and return a typed result. */
|
|
728
|
+
async _get(path, params) {
|
|
729
|
+
const url = this._buildUrl(path, params);
|
|
730
|
+
let response;
|
|
731
|
+
try {
|
|
732
|
+
response = await this.fetch(url);
|
|
733
|
+
} catch (err) {
|
|
734
|
+
return {
|
|
735
|
+
ok: false,
|
|
736
|
+
statusCode: 0,
|
|
737
|
+
error: err instanceof Error ? err.message : "Network error"
|
|
738
|
+
};
|
|
739
|
+
}
|
|
740
|
+
if (!response.ok) {
|
|
741
|
+
let errorMessage = `HTTP ${response.status}`;
|
|
742
|
+
try {
|
|
743
|
+
const body = await response.json();
|
|
744
|
+
if (typeof body["error"] === "string") {
|
|
745
|
+
errorMessage = body["error"];
|
|
746
|
+
}
|
|
747
|
+
} catch {
|
|
748
|
+
}
|
|
749
|
+
return { ok: false, statusCode: response.status, error: errorMessage };
|
|
750
|
+
}
|
|
751
|
+
let data;
|
|
752
|
+
try {
|
|
753
|
+
data = await response.json();
|
|
754
|
+
} catch {
|
|
755
|
+
return { ok: false, statusCode: response.status, error: "Invalid JSON response" };
|
|
756
|
+
}
|
|
757
|
+
return { ok: true, data };
|
|
758
|
+
}
|
|
759
|
+
};
|
|
760
|
+
|
|
761
|
+
// src/federation-client.ts
|
|
762
|
+
var DEFAULT_CACHE_TTL_SECONDS = 3600;
|
|
763
|
+
function generateCacheKey(endpoint, params = {}) {
|
|
764
|
+
const sortedEntries = Object.entries(params).sort(([a], [b]) => a.localeCompare(b));
|
|
765
|
+
const paramString = sortedEntries.map(([k, v]) => `${k}=${v}`).join("&");
|
|
766
|
+
return `${endpoint}:${paramString}`;
|
|
767
|
+
}
|
|
768
|
+
function matchesFilter(extension, filter, contributionPoints) {
|
|
769
|
+
if (filter.blockedPublishers && filter.blockedPublishers.length > 0 && extension.namespace) {
|
|
770
|
+
const ns = extension.namespace.toLowerCase();
|
|
771
|
+
if (filter.blockedPublishers.some((bp) => bp.toLowerCase() === ns)) {
|
|
772
|
+
return { allowed: false, reason: `Publisher '${extension.namespace}' is blocked` };
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
if (filter.allowedCategories && filter.allowedCategories.length > 0) {
|
|
776
|
+
const categories = extension.categories ?? [];
|
|
777
|
+
const allowedLower = filter.allowedCategories.map((c) => c.toLowerCase());
|
|
778
|
+
const hasAllowedCategory = categories.some((cat) => allowedLower.includes(cat.toLowerCase()));
|
|
779
|
+
if (!hasAllowedCategory) {
|
|
780
|
+
return {
|
|
781
|
+
allowed: false,
|
|
782
|
+
reason: `No matching category (has: ${categories.join(", ")}; allowed: ${filter.allowedCategories.join(", ")})`
|
|
783
|
+
};
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
if (filter.declarativeOnly && contributionPoints) {
|
|
787
|
+
if (!isDeclarativeExtension(contributionPoints)) {
|
|
788
|
+
return {
|
|
789
|
+
allowed: false,
|
|
790
|
+
reason: "Extension uses non-declarative contribution points"
|
|
791
|
+
};
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
return { allowed: true };
|
|
795
|
+
}
|
|
796
|
+
function isDeclarativeExtension(contributionPoints) {
|
|
797
|
+
return contributionPoints.every((cp) => DECLARATIVE_CONTRIBUTION_POINTS.has(cp));
|
|
798
|
+
}
|
|
799
|
+
var FederationClient = class {
|
|
800
|
+
/**
|
|
801
|
+
* Create a new federation client.
|
|
802
|
+
*
|
|
803
|
+
* @param config - Federation configuration
|
|
804
|
+
* @param upstreamClient - Optional pre-configured OpenVSXClient (created from config if not provided)
|
|
805
|
+
*/
|
|
806
|
+
constructor(config, upstreamClient) {
|
|
807
|
+
this.config = config;
|
|
808
|
+
this.cacheTtl = config.cacheTtlSeconds ?? DEFAULT_CACHE_TTL_SECONDS;
|
|
809
|
+
if (config.mode === "standalone") {
|
|
810
|
+
this.upstream = null;
|
|
811
|
+
} else if (upstreamClient) {
|
|
812
|
+
this.upstream = upstreamClient;
|
|
813
|
+
} else {
|
|
814
|
+
this.upstream = new OpenVSXClient(
|
|
815
|
+
config.upstream ? { baseUrl: config.upstream } : void 0
|
|
816
|
+
);
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
/**
|
|
820
|
+
* Search extensions with filters applied.
|
|
821
|
+
*
|
|
822
|
+
* In standalone mode, returns empty results. In proxy/mirror mode,
|
|
823
|
+
* delegates to upstream and applies filters.
|
|
824
|
+
*/
|
|
825
|
+
async search(params) {
|
|
826
|
+
const cacheKey = this.getCacheKey("search", params ? this._searchParamsToRecord(params) : void 0);
|
|
827
|
+
if (!this.upstream) {
|
|
828
|
+
return {
|
|
829
|
+
ok: true,
|
|
830
|
+
data: {
|
|
831
|
+
extensions: [],
|
|
832
|
+
totalSize: 0,
|
|
833
|
+
filtered: 0,
|
|
834
|
+
source: "local",
|
|
835
|
+
cacheKey
|
|
836
|
+
}
|
|
837
|
+
};
|
|
838
|
+
}
|
|
839
|
+
const result = await this.upstream.search(params);
|
|
840
|
+
if (!result.ok) {
|
|
841
|
+
return result;
|
|
842
|
+
}
|
|
843
|
+
const { extensions, totalSize } = result.data;
|
|
844
|
+
const filter = this.config.filter;
|
|
845
|
+
if (filter) {
|
|
846
|
+
const passed = [];
|
|
847
|
+
let filteredCount = 0;
|
|
848
|
+
for (const ext of extensions) {
|
|
849
|
+
const extWithCategories = ext;
|
|
850
|
+
const categories = extWithCategories.categories;
|
|
851
|
+
const check = matchesFilter(
|
|
852
|
+
categories ? { categories, namespace: ext.namespace, name: ext.name } : { namespace: ext.namespace, name: ext.name },
|
|
853
|
+
filter
|
|
854
|
+
);
|
|
855
|
+
if (check.allowed) {
|
|
856
|
+
passed.push(ext);
|
|
857
|
+
} else {
|
|
858
|
+
filteredCount++;
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
return {
|
|
862
|
+
ok: true,
|
|
863
|
+
data: {
|
|
864
|
+
extensions: passed,
|
|
865
|
+
totalSize,
|
|
866
|
+
filtered: filteredCount,
|
|
867
|
+
source: "upstream",
|
|
868
|
+
cacheKey
|
|
869
|
+
}
|
|
870
|
+
};
|
|
871
|
+
}
|
|
872
|
+
return {
|
|
873
|
+
ok: true,
|
|
874
|
+
data: {
|
|
875
|
+
extensions,
|
|
876
|
+
totalSize,
|
|
877
|
+
filtered: 0,
|
|
878
|
+
source: "upstream",
|
|
879
|
+
cacheKey
|
|
880
|
+
}
|
|
881
|
+
};
|
|
882
|
+
}
|
|
883
|
+
/**
|
|
884
|
+
* Get a single extension with filter check.
|
|
885
|
+
*
|
|
886
|
+
* In standalone mode, returns null. In proxy/mirror mode,
|
|
887
|
+
* fetches from upstream and checks against filters.
|
|
888
|
+
*/
|
|
889
|
+
async getExtension(namespace, name, version) {
|
|
890
|
+
const params = { namespace, name };
|
|
891
|
+
if (version) {
|
|
892
|
+
params["version"] = version;
|
|
893
|
+
}
|
|
894
|
+
const cacheKey = this.getCacheKey("extension", params);
|
|
895
|
+
if (!this.upstream) {
|
|
896
|
+
return {
|
|
897
|
+
ok: true,
|
|
898
|
+
data: {
|
|
899
|
+
extension: null,
|
|
900
|
+
filtered: false,
|
|
901
|
+
source: "local",
|
|
902
|
+
cacheKey
|
|
903
|
+
}
|
|
904
|
+
};
|
|
905
|
+
}
|
|
906
|
+
const result = await this.upstream.getExtension(namespace, name, version);
|
|
907
|
+
if (!result.ok) {
|
|
908
|
+
return result;
|
|
909
|
+
}
|
|
910
|
+
const ext = result.data;
|
|
911
|
+
const filter = this.config.filter;
|
|
912
|
+
if (filter) {
|
|
913
|
+
const check = matchesFilter(
|
|
914
|
+
{
|
|
915
|
+
categories: ext.categories,
|
|
916
|
+
namespace: ext.namespace,
|
|
917
|
+
name: ext.name
|
|
918
|
+
},
|
|
919
|
+
filter
|
|
920
|
+
);
|
|
921
|
+
if (!check.allowed) {
|
|
922
|
+
const data = {
|
|
923
|
+
extension: null,
|
|
924
|
+
filtered: true,
|
|
925
|
+
source: "upstream",
|
|
926
|
+
cacheKey
|
|
927
|
+
};
|
|
928
|
+
if (check.reason) {
|
|
929
|
+
data.filterReason = check.reason;
|
|
930
|
+
}
|
|
931
|
+
return { ok: true, data };
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
return {
|
|
935
|
+
ok: true,
|
|
936
|
+
data: {
|
|
937
|
+
extension: ext,
|
|
938
|
+
filtered: false,
|
|
939
|
+
source: "upstream",
|
|
940
|
+
cacheKey
|
|
941
|
+
}
|
|
942
|
+
};
|
|
943
|
+
}
|
|
944
|
+
/**
|
|
945
|
+
* Get a VSIX download URL for a specific extension version.
|
|
946
|
+
*
|
|
947
|
+
* Returns null in standalone mode (no upstream available).
|
|
948
|
+
*/
|
|
949
|
+
getDownloadUrl(namespace, name, version, targetPlatform) {
|
|
950
|
+
if (!this.upstream) {
|
|
951
|
+
return null;
|
|
952
|
+
}
|
|
953
|
+
return this.upstream.getDownloadUrl(namespace, name, version, targetPlatform);
|
|
954
|
+
}
|
|
955
|
+
/**
|
|
956
|
+
* Build a cache key for a given endpoint and parameters.
|
|
957
|
+
*/
|
|
958
|
+
getCacheKey(endpoint, params) {
|
|
959
|
+
const raw = generateCacheKey(endpoint, params);
|
|
960
|
+
let key = `federation:${this.config.mode}:${raw}`;
|
|
961
|
+
if (this.config.filter) {
|
|
962
|
+
const f = this.config.filter;
|
|
963
|
+
const parts = [];
|
|
964
|
+
if (f.declarativeOnly) parts.push("d=1");
|
|
965
|
+
if (f.allowedCategories && f.allowedCategories.length > 0) {
|
|
966
|
+
parts.push(`ac=${[...f.allowedCategories].sort().join(",")}`);
|
|
967
|
+
}
|
|
968
|
+
if (f.blockedPublishers && f.blockedPublishers.length > 0) {
|
|
969
|
+
parts.push(`bp=${[...f.blockedPublishers].sort().join(",")}`);
|
|
970
|
+
}
|
|
971
|
+
if (parts.length > 0) {
|
|
972
|
+
key += `:f(${parts.join(";")})`;
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
return { key, ttlSeconds: this.cacheTtl };
|
|
976
|
+
}
|
|
977
|
+
/**
|
|
978
|
+
* Get a readonly copy of the current federation configuration.
|
|
979
|
+
*/
|
|
980
|
+
getConfig() {
|
|
981
|
+
const copy = { ...this.config };
|
|
982
|
+
if (this.config.filter) {
|
|
983
|
+
copy.filter = { ...this.config.filter };
|
|
984
|
+
if (this.config.filter.allowedCategories) {
|
|
985
|
+
copy.filter.allowedCategories = [...this.config.filter.allowedCategories];
|
|
986
|
+
}
|
|
987
|
+
if (this.config.filter.blockedPublishers) {
|
|
988
|
+
copy.filter.blockedPublishers = [...this.config.filter.blockedPublishers];
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
return copy;
|
|
992
|
+
}
|
|
993
|
+
// -------------------------------------------------------------------------
|
|
994
|
+
// Private helpers
|
|
995
|
+
// -------------------------------------------------------------------------
|
|
996
|
+
/** Convert search params to a plain Record for cache key generation. */
|
|
997
|
+
_searchParamsToRecord(params) {
|
|
998
|
+
const record = {};
|
|
999
|
+
if (params.query !== void 0) record["query"] = params.query;
|
|
1000
|
+
if (params.category !== void 0) record["category"] = params.category;
|
|
1001
|
+
if (params.targetPlatform !== void 0) record["targetPlatform"] = params.targetPlatform;
|
|
1002
|
+
if (params.size !== void 0) record["size"] = String(params.size);
|
|
1003
|
+
if (params.offset !== void 0) record["offset"] = String(params.offset);
|
|
1004
|
+
if (params.sortOrder !== void 0) record["sortOrder"] = params.sortOrder;
|
|
1005
|
+
if (params.sortBy !== void 0) record["sortBy"] = params.sortBy;
|
|
1006
|
+
if (params.includeAllVersions !== void 0)
|
|
1007
|
+
record["includeAllVersions"] = String(params.includeAllVersions);
|
|
1008
|
+
return record;
|
|
1009
|
+
}
|
|
1010
|
+
};
|
|
1011
|
+
|
|
1012
|
+
exports.DECLARATIVE_CONTRIBUTION_POINTS = DECLARATIVE_CONTRIBUTION_POINTS;
|
|
1013
|
+
exports.FederationClient = FederationClient;
|
|
1014
|
+
exports.OpenVSXClient = OpenVSXClient;
|
|
1015
|
+
exports.PLATFORM_SUPPORT = PLATFORM_SUPPORT;
|
|
1016
|
+
exports.SUPPORT_WEIGHTS = SUPPORT_WEIGHTS;
|
|
1017
|
+
exports.VsixScanError = VsixScanError;
|
|
1018
|
+
exports.buildCompatibilityReport = buildCompatibilityReport;
|
|
1019
|
+
exports.buildDependencyGraph = buildDependencyGraph;
|
|
1020
|
+
exports.classifyDeclarative = classifyDeclarative;
|
|
1021
|
+
exports.detectApiNamespaces = detectApiNamespaces;
|
|
1022
|
+
exports.enumerateContributionPoints = enumerateContributionPoints;
|
|
1023
|
+
exports.extractDependencyNode = extractDependencyNode;
|
|
1024
|
+
exports.flowStateToVsCode = flowStateToVsCode;
|
|
1025
|
+
exports.generateCacheKey = generateCacheKey;
|
|
1026
|
+
exports.generateRecommendations = generateRecommendations;
|
|
1027
|
+
exports.getPlatformSupport = getPlatformSupport;
|
|
1028
|
+
exports.isDeclarativeExtension = isDeclarativeExtension;
|
|
1029
|
+
exports.mapActivationEvents = mapActivationEvents;
|
|
1030
|
+
exports.mapCategory = mapCategory;
|
|
1031
|
+
exports.mapExtensionKind = mapExtensionKind;
|
|
1032
|
+
exports.matchesFilter = matchesFilter;
|
|
1033
|
+
exports.normalizeExtensionId = normalizeExtensionId;
|
|
1034
|
+
exports.resolveDependencies = resolveDependencies;
|
|
1035
|
+
exports.reverseActivationEvents = reverseActivationEvents;
|
|
1036
|
+
exports.reverseCategory = reverseCategory;
|
|
1037
|
+
exports.reverseExtensionKind = reverseExtensionKind;
|
|
1038
|
+
exports.scanVsix = scanVsix;
|
|
1039
|
+
exports.scoreExtension = scoreExtension;
|
|
1040
|
+
exports.vsCodeToFlowState = vsCodeToFlowState;
|
|
1041
|
+
//# sourceMappingURL=index.js.map
|
|
1042
|
+
//# sourceMappingURL=index.js.map
|