@cyclonedx/cdxgen 12.1.5 → 12.2.1

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.
Files changed (193) hide show
  1. package/README.md +51 -40
  2. package/bin/cdxgen.js +194 -97
  3. package/bin/evinse.js +4 -4
  4. package/bin/repl.js +1 -1
  5. package/bin/sign.js +102 -0
  6. package/bin/validate.js +233 -0
  7. package/bin/verify.js +69 -28
  8. package/data/queries.json +1 -1
  9. package/data/rules/ci-permissions.yaml +186 -0
  10. package/data/rules/dependency-sources.yaml +123 -0
  11. package/data/rules/package-integrity.yaml +135 -0
  12. package/data/rules/vscode-extensions.yaml +228 -0
  13. package/lib/cli/index.js +449 -429
  14. package/lib/cli/index.poku.js +117 -0
  15. package/lib/evinser/db.js +137 -0
  16. package/lib/{helpers → evinser}/db.poku.js +2 -6
  17. package/lib/evinser/evinser.js +2 -14
  18. package/lib/helpers/analyzer.js +606 -3
  19. package/lib/helpers/analyzer.poku.js +230 -0
  20. package/lib/helpers/bomSigner.js +312 -0
  21. package/lib/helpers/bomSigner.poku.js +156 -0
  22. package/lib/helpers/ciParsers/azurePipelines.js +295 -0
  23. package/lib/helpers/ciParsers/azurePipelines.poku.js +253 -0
  24. package/lib/helpers/ciParsers/circleCi.js +286 -0
  25. package/lib/helpers/ciParsers/circleCi.poku.js +230 -0
  26. package/lib/helpers/ciParsers/common.js +24 -0
  27. package/lib/helpers/ciParsers/githubActions.js +636 -0
  28. package/lib/helpers/ciParsers/githubActions.poku.js +802 -0
  29. package/lib/helpers/ciParsers/gitlabCi.js +213 -0
  30. package/lib/helpers/ciParsers/gitlabCi.poku.js +247 -0
  31. package/lib/helpers/ciParsers/jenkins.js +181 -0
  32. package/lib/helpers/ciParsers/jenkins.poku.js +197 -0
  33. package/lib/helpers/depsUtils.js +219 -0
  34. package/lib/helpers/depsUtils.poku.js +207 -0
  35. package/lib/helpers/display.js +426 -5
  36. package/lib/helpers/envcontext.js +18 -3
  37. package/lib/helpers/formulationParsers.js +351 -0
  38. package/lib/helpers/logger.js +14 -0
  39. package/lib/helpers/protobom.js +9 -9
  40. package/lib/helpers/pythonutils.js +9 -0
  41. package/lib/helpers/remote/dependency-track.js +84 -0
  42. package/lib/helpers/remote/dependency-track.poku.js +119 -0
  43. package/lib/helpers/table.js +384 -0
  44. package/lib/helpers/table.poku.js +186 -0
  45. package/lib/helpers/utils.js +865 -416
  46. package/lib/helpers/utils.poku.js +172 -265
  47. package/lib/helpers/versutils.js +202 -0
  48. package/lib/helpers/versutils.poku.js +315 -0
  49. package/lib/helpers/vsixutils.js +1061 -0
  50. package/lib/helpers/vsixutils.poku.js +2247 -0
  51. package/lib/managers/binary.js +19 -19
  52. package/lib/managers/docker.js +108 -1
  53. package/lib/managers/oci.js +10 -0
  54. package/lib/managers/piptree.js +3 -9
  55. package/lib/parsers/npmrc.js +17 -13
  56. package/lib/parsers/npmrc.poku.js +41 -5
  57. package/lib/server/openapi.yaml +34 -1
  58. package/lib/server/server.js +50 -13
  59. package/lib/server/server.poku.js +332 -144
  60. package/lib/stages/postgen/annotator.js +1 -1
  61. package/lib/stages/postgen/auditBom.js +196 -0
  62. package/lib/stages/postgen/auditBom.poku.js +378 -0
  63. package/lib/stages/postgen/postgen.js +54 -1
  64. package/lib/stages/postgen/postgen.poku.js +90 -1
  65. package/lib/stages/postgen/ruleEngine.js +369 -0
  66. package/lib/stages/pregen/envAudit.js +299 -0
  67. package/lib/stages/pregen/envAudit.poku.js +572 -0
  68. package/lib/stages/pregen/pregen.js +12 -8
  69. package/lib/{helpers/validator.js → validator/bomValidator.js} +107 -47
  70. package/lib/validator/complianceEngine.js +241 -0
  71. package/lib/validator/complianceEngine.poku.js +168 -0
  72. package/lib/validator/complianceRules.js +1610 -0
  73. package/lib/validator/complianceRules.poku.js +328 -0
  74. package/lib/validator/index.js +222 -0
  75. package/lib/validator/index.poku.js +144 -0
  76. package/lib/validator/reporters/annotations.js +121 -0
  77. package/lib/validator/reporters/console.js +149 -0
  78. package/lib/validator/reporters/index.js +41 -0
  79. package/lib/validator/reporters/json.js +37 -0
  80. package/lib/validator/reporters/sarif.js +184 -0
  81. package/lib/validator/reporters.poku.js +150 -0
  82. package/package.json +8 -9
  83. package/types/bin/sign.d.ts +3 -0
  84. package/types/bin/sign.d.ts.map +1 -0
  85. package/types/bin/validate.d.ts +3 -0
  86. package/types/bin/validate.d.ts.map +1 -0
  87. package/types/helpers/utils.d.ts +0 -1
  88. package/types/lib/cli/index.d.ts +49 -52
  89. package/types/lib/cli/index.d.ts.map +1 -1
  90. package/types/lib/evinser/db.d.ts +34 -0
  91. package/types/lib/evinser/db.d.ts.map +1 -0
  92. package/types/lib/evinser/evinser.d.ts +63 -16
  93. package/types/lib/evinser/evinser.d.ts.map +1 -1
  94. package/types/lib/helpers/analyzer.d.ts.map +1 -1
  95. package/types/lib/helpers/bomSigner.d.ts +27 -0
  96. package/types/lib/helpers/bomSigner.d.ts.map +1 -0
  97. package/types/lib/helpers/ciParsers/azurePipelines.d.ts +17 -0
  98. package/types/lib/helpers/ciParsers/azurePipelines.d.ts.map +1 -0
  99. package/types/lib/helpers/ciParsers/circleCi.d.ts +17 -0
  100. package/types/lib/helpers/ciParsers/circleCi.d.ts.map +1 -0
  101. package/types/lib/helpers/ciParsers/common.d.ts +11 -0
  102. package/types/lib/helpers/ciParsers/common.d.ts.map +1 -0
  103. package/types/lib/helpers/ciParsers/githubActions.d.ts +34 -0
  104. package/types/lib/helpers/ciParsers/githubActions.d.ts.map +1 -0
  105. package/types/lib/helpers/ciParsers/gitlabCi.d.ts +17 -0
  106. package/types/lib/helpers/ciParsers/gitlabCi.d.ts.map +1 -0
  107. package/types/lib/helpers/ciParsers/jenkins.d.ts +17 -0
  108. package/types/lib/helpers/ciParsers/jenkins.d.ts.map +1 -0
  109. package/types/lib/helpers/depsUtils.d.ts +21 -0
  110. package/types/lib/helpers/depsUtils.d.ts.map +1 -0
  111. package/types/lib/helpers/display.d.ts +111 -11
  112. package/types/lib/helpers/display.d.ts.map +1 -1
  113. package/types/lib/helpers/envcontext.d.ts +19 -7
  114. package/types/lib/helpers/envcontext.d.ts.map +1 -1
  115. package/types/lib/helpers/formulationParsers.d.ts +50 -0
  116. package/types/lib/helpers/formulationParsers.d.ts.map +1 -0
  117. package/types/lib/helpers/logger.d.ts +15 -1
  118. package/types/lib/helpers/logger.d.ts.map +1 -1
  119. package/types/lib/helpers/protobom.d.ts +2 -2
  120. package/types/lib/helpers/pythonutils.d.ts +10 -1
  121. package/types/lib/helpers/pythonutils.d.ts.map +1 -1
  122. package/types/lib/helpers/remote/dependency-track.d.ts +16 -0
  123. package/types/lib/helpers/remote/dependency-track.d.ts.map +1 -0
  124. package/types/lib/helpers/table.d.ts +6 -0
  125. package/types/lib/helpers/table.d.ts.map +1 -0
  126. package/types/lib/helpers/utils.d.ts +533 -128
  127. package/types/lib/helpers/utils.d.ts.map +1 -1
  128. package/types/lib/helpers/versutils.d.ts +8 -0
  129. package/types/lib/helpers/versutils.d.ts.map +1 -0
  130. package/types/lib/helpers/vsixutils.d.ts +130 -0
  131. package/types/lib/helpers/vsixutils.d.ts.map +1 -0
  132. package/types/lib/managers/docker.d.ts +12 -31
  133. package/types/lib/managers/docker.d.ts.map +1 -1
  134. package/types/lib/managers/oci.d.ts +11 -1
  135. package/types/lib/managers/oci.d.ts.map +1 -1
  136. package/types/lib/managers/piptree.d.ts.map +1 -1
  137. package/types/lib/parsers/npmrc.d.ts +4 -1
  138. package/types/lib/parsers/npmrc.d.ts.map +1 -1
  139. package/types/lib/server/server.d.ts +22 -2
  140. package/types/lib/server/server.d.ts.map +1 -1
  141. package/types/lib/stages/postgen/auditBom.d.ts +20 -0
  142. package/types/lib/stages/postgen/auditBom.d.ts.map +1 -0
  143. package/types/lib/stages/postgen/postgen.d.ts +8 -1
  144. package/types/lib/stages/postgen/postgen.d.ts.map +1 -1
  145. package/types/lib/stages/postgen/ruleEngine.d.ts +18 -0
  146. package/types/lib/stages/postgen/ruleEngine.d.ts.map +1 -0
  147. package/types/lib/stages/pregen/envAudit.d.ts +8 -0
  148. package/types/lib/stages/pregen/envAudit.d.ts.map +1 -0
  149. package/types/lib/stages/pregen/pregen.d.ts.map +1 -1
  150. package/types/lib/{helpers/validator.d.ts → validator/bomValidator.d.ts} +1 -1
  151. package/types/lib/validator/bomValidator.d.ts.map +1 -0
  152. package/types/lib/validator/complianceEngine.d.ts +66 -0
  153. package/types/lib/validator/complianceEngine.d.ts.map +1 -0
  154. package/types/lib/validator/complianceRules.d.ts +70 -0
  155. package/types/lib/validator/complianceRules.d.ts.map +1 -0
  156. package/types/lib/validator/index.d.ts +70 -0
  157. package/types/lib/validator/index.d.ts.map +1 -0
  158. package/types/lib/validator/reporters/annotations.d.ts +31 -0
  159. package/types/lib/validator/reporters/annotations.d.ts.map +1 -0
  160. package/types/lib/validator/reporters/console.d.ts +30 -0
  161. package/types/lib/validator/reporters/console.d.ts.map +1 -0
  162. package/types/lib/validator/reporters/index.d.ts +21 -0
  163. package/types/lib/validator/reporters/index.d.ts.map +1 -0
  164. package/types/lib/validator/reporters/json.d.ts +11 -0
  165. package/types/lib/validator/reporters/json.d.ts.map +1 -0
  166. package/types/lib/validator/reporters/sarif.d.ts +16 -0
  167. package/types/lib/validator/reporters/sarif.d.ts.map +1 -0
  168. package/lib/helpers/db.js +0 -162
  169. package/lib/stages/pregen/env-audit.js +0 -34
  170. package/lib/stages/pregen/env-audit.poku.js +0 -290
  171. package/types/helpers/db.d.ts +0 -35
  172. package/types/helpers/db.d.ts.map +0 -1
  173. package/types/lib/helpers/db.d.ts +0 -35
  174. package/types/lib/helpers/db.d.ts.map +0 -1
  175. package/types/lib/helpers/validator.d.ts.map +0 -1
  176. package/types/lib/stages/pregen/env-audit.d.ts +0 -2
  177. package/types/lib/stages/pregen/env-audit.d.ts.map +0 -1
  178. package/types/managers/binary.d.ts +0 -37
  179. package/types/managers/binary.d.ts.map +0 -1
  180. package/types/managers/docker.d.ts +0 -56
  181. package/types/managers/docker.d.ts.map +0 -1
  182. package/types/managers/oci.d.ts +0 -2
  183. package/types/managers/oci.d.ts.map +0 -1
  184. package/types/managers/piptree.d.ts +0 -2
  185. package/types/managers/piptree.d.ts.map +0 -1
  186. package/types/server/server.d.ts +0 -34
  187. package/types/server/server.d.ts.map +0 -1
  188. package/types/stages/postgen/annotator.d.ts +0 -27
  189. package/types/stages/postgen/annotator.d.ts.map +0 -1
  190. package/types/stages/postgen/postgen.d.ts +0 -51
  191. package/types/stages/postgen/postgen.d.ts.map +0 -1
  192. package/types/stages/pregen/pregen.d.ts +0 -59
  193. package/types/stages/pregen/pregen.d.ts.map +0 -1
@@ -0,0 +1,2247 @@
1
+ import { strict as assert } from "node:assert";
2
+ import {
3
+ existsSync,
4
+ mkdirSync,
5
+ mkdtempSync,
6
+ rmSync,
7
+ writeFileSync,
8
+ } from "node:fs";
9
+ import { tmpdir } from "node:os";
10
+ import { join } from "node:path";
11
+
12
+ import { describe, it } from "poku";
13
+
14
+ import {
15
+ cleanupTempDir,
16
+ collectInstalledExtensions,
17
+ extractExtensionCapabilities,
18
+ getIdeExtensionDirs,
19
+ parseExtensionDependencies,
20
+ parseExtensionDirName,
21
+ parseInstalledExtensionDir,
22
+ parseVsixManifest,
23
+ parseVsixPackageJson,
24
+ toComponent,
25
+ VSCODE_EXTENSION_PURL_TYPE,
26
+ } from "./vsixutils.js";
27
+
28
+ const baseTempDir = mkdtempSync(join(tmpdir(), "cdxgen-vsix-poku-"));
29
+ process.on("exit", () => {
30
+ try {
31
+ rmSync(baseTempDir, { recursive: true, force: true });
32
+ } catch (_e) {
33
+ // Ignore cleanup errors
34
+ }
35
+ });
36
+
37
+ describe("VSCODE_EXTENSION_PURL_TYPE", () => {
38
+ it("should be vscode-extension", () => {
39
+ assert.strictEqual(VSCODE_EXTENSION_PURL_TYPE, "vscode-extension");
40
+ });
41
+ });
42
+
43
+ describe("getIdeExtensionDirs", () => {
44
+ it("should return an array of IDE configurations", () => {
45
+ const ides = getIdeExtensionDirs();
46
+ assert.ok(Array.isArray(ides));
47
+ assert.ok(ides.length > 0);
48
+ for (const ide of ides) {
49
+ assert.ok(ide.name, "Each IDE should have a name");
50
+ assert.ok(Array.isArray(ide.dirs), "Each IDE should have dirs array");
51
+ assert.ok(ide.dirs.length > 0, "Each IDE should have at least one dir");
52
+ }
53
+ });
54
+
55
+ it("should include well-known IDEs", () => {
56
+ const ides = getIdeExtensionDirs();
57
+ const names = ides.map((ide) => ide.name);
58
+ assert.ok(names.includes("VS Code"), "Should include VS Code");
59
+ assert.ok(
60
+ names.includes("VS Code Insiders"),
61
+ "Should include VS Code Insiders",
62
+ );
63
+ assert.ok(names.includes("VSCodium"), "Should include VSCodium");
64
+ assert.ok(names.includes("Cursor"), "Should include Cursor");
65
+ assert.ok(names.includes("Windsurf"), "Should include Windsurf");
66
+ assert.ok(names.includes("Positron"), "Should include Positron");
67
+ assert.ok(names.includes("Theia"), "Should include Theia");
68
+ assert.ok(names.includes("code-server"), "Should include code-server");
69
+ assert.ok(names.includes("Trae"), "Should include Trae");
70
+ assert.ok(names.includes("Augment Code"), "Should include Augment Code");
71
+ assert.ok(
72
+ names.includes("VS Code Remote"),
73
+ "Should include VS Code Remote",
74
+ );
75
+ assert.ok(
76
+ names.includes("OpenVSCode Server"),
77
+ "Should include OpenVSCode Server",
78
+ );
79
+ });
80
+ });
81
+
82
+ describe("parseVsixManifest", () => {
83
+ it("should return undefined for empty input", () => {
84
+ assert.strictEqual(parseVsixManifest(""), undefined);
85
+ assert.strictEqual(parseVsixManifest(null), undefined);
86
+ assert.strictEqual(parseVsixManifest(undefined), undefined);
87
+ });
88
+
89
+ it("should parse a valid vsixmanifest XML", () => {
90
+ const xml = `<?xml version="1.0" encoding="utf-8"?>
91
+ <PackageManifest Version="2.0.0" xmlns="http://schemas.microsoft.com/developer/vsx-schema/2011">
92
+ <Metadata>
93
+ <Identity Id="python" Version="2023.25.0" Publisher="ms-python" TargetPlatform="linux-x64" />
94
+ <DisplayName>Python</DisplayName>
95
+ <Description>Python language support</Description>
96
+ </Metadata>
97
+ </PackageManifest>`;
98
+ const result = parseVsixManifest(xml);
99
+ assert.ok(result);
100
+ assert.strictEqual(result.publisher, "ms-python");
101
+ assert.strictEqual(result.name, "python");
102
+ assert.strictEqual(result.version, "2023.25.0");
103
+ assert.strictEqual(result.displayName, "Python");
104
+ assert.strictEqual(result.description, "Python language support");
105
+ assert.strictEqual(result.platform, "linux-x64");
106
+ });
107
+
108
+ it("should handle manifest without TargetPlatform", () => {
109
+ const xml = `<?xml version="1.0" encoding="utf-8"?>
110
+ <PackageManifest Version="2.0.0" xmlns="http://schemas.microsoft.com/developer/vsx-schema/2011">
111
+ <Metadata>
112
+ <Identity Id="csharp" Version="2.15.30" Publisher="muhammad-sammy" />
113
+ <DisplayName>C#</DisplayName>
114
+ <Description>C# language support</Description>
115
+ </Metadata>
116
+ </PackageManifest>`;
117
+ const result = parseVsixManifest(xml);
118
+ assert.ok(result);
119
+ assert.strictEqual(result.publisher, "muhammad-sammy");
120
+ assert.strictEqual(result.name, "csharp");
121
+ assert.strictEqual(result.version, "2.15.30");
122
+ assert.strictEqual(result.platform, "");
123
+ });
124
+
125
+ it("should handle manifest without Description or DisplayName", () => {
126
+ const xml = `<?xml version="1.0" encoding="utf-8"?>
127
+ <PackageManifest Version="2.0.0" xmlns="http://schemas.microsoft.com/developer/vsx-schema/2011">
128
+ <Metadata>
129
+ <Identity Id="myext" Version="1.0.0" Publisher="testpub" />
130
+ </Metadata>
131
+ </PackageManifest>`;
132
+ const result = parseVsixManifest(xml);
133
+ assert.ok(result);
134
+ assert.strictEqual(result.publisher, "testpub");
135
+ assert.strictEqual(result.name, "myext");
136
+ assert.strictEqual(result.version, "1.0.0");
137
+ assert.strictEqual(result.displayName, "");
138
+ assert.strictEqual(result.description, "");
139
+ });
140
+
141
+ it("should handle larger manifest with tags", () => {
142
+ const xml = `<?xml version="1.0" encoding="utf-8"?>
143
+ <PackageManifest Version="2.0.0"
144
+ xmlns="http://schemas.microsoft.com/developer/vsx-schema/2011"
145
+ xmlns:d="http://schemas.microsoft.com/developer/vsx-schema-design/2011">
146
+
147
+ <Metadata>
148
+ <Identity Id="MyCompany.MyExtension"
149
+ Version="1.2.3"
150
+ Language="en-US"
151
+ Publisher="My Company" />
152
+ <DisplayName>My Awesome Extension</DisplayName>
153
+ <Description xml:space="preserve">A description of what this extension does.</Description>
154
+ <MoreInfo>https://github.com/mycompany/myextension</MoreInfo>
155
+ <License>LICENSE.txt</License>
156
+ <Icon>Resources\\icon.png</Icon>
157
+ <PreviewImage>Resources\\preview.png</PreviewImage>
158
+ <Tags>productivity, coding, tools</Tags>
159
+ </Metadata>
160
+
161
+ <Installation InstalledByMsi="false" AllUsers="false">
162
+ <InstallationTarget Id="Microsoft.VisualStudio.Community" Version="[17.0,)" />
163
+ <InstallationTarget Id="Microsoft.VisualStudio.Pro" Version="[17.0,)" />
164
+ <InstallationTarget Id="Microsoft.VisualStudio.Enterprise" Version="[17.0,)" />
165
+ </Installation>
166
+
167
+ <Dependencies>
168
+ <Dependency Id="Microsoft.Framework.NDP"
169
+ DisplayName=".NET Framework"
170
+ d:Source="Manual"
171
+ Version="[4.8,)" />
172
+ </Dependencies>
173
+
174
+ <Prerequisites>
175
+ <Prerequisite Id="Microsoft.VisualStudio.Component.CoreEditor"
176
+ Version="[17.0,)"
177
+ DisplayName="Visual Studio core editor" />
178
+ </Prerequisites>
179
+
180
+ <Assets>
181
+ <Asset Type="Microsoft.VisualStudio.VsPackage"
182
+ d:Source="Project"
183
+ d:ProjectName="%CurrentProject%"
184
+ Path="|%CurrentProject%;PkgdefProjectOutputGroup|" />
185
+ <Asset Type="Microsoft.VisualStudio.MefComponent"
186
+ d:Source="Project"
187
+ d:ProjectName="%CurrentProject%"
188
+ Path="|%CurrentProject%|" />
189
+ </Assets>
190
+ </PackageManifest>`;
191
+ const result = parseVsixManifest(xml);
192
+ assert.ok(result);
193
+ assert.strictEqual(result.publisher, "My Company");
194
+ assert.strictEqual(result.name, "MyCompany.MyExtension");
195
+ assert.strictEqual(result.version, "1.2.3");
196
+ assert.strictEqual(result.displayName, "My Awesome Extension");
197
+ assert.strictEqual(
198
+ result.description,
199
+ "A description of what this extension does.",
200
+ );
201
+ assert.deepStrictEqual(result.tags, ["productivity", "coding", "tools"]);
202
+ });
203
+
204
+ it("should parse a real one with tags", () => {
205
+ const xml = `
206
+ <?xml version="1.0" encoding="utf-8"?>
207
+ <PackageManifest Version="2.0.0" xmlns="http://schemas.microsoft.com/developer/vsx-schema/2011" xmlns:d="http://schemas.microsoft.com/developer/vsx-schema-design/2011">
208
+ <Metadata>
209
+ <Identity Language="en-US" Id="volar" Version="3.2.6" Publisher="Vue" />
210
+ <DisplayName>Vue (Official)</DisplayName>
211
+ <Description xml:space="preserve">Language Support for Vue</Description>
212
+ <Tags>json,vue,__ext_vue,markdown,html,jade,__web_extension,__sponsor_extension</Tags>
213
+ <Categories>Programming Languages</Categories>
214
+ <GalleryFlags>Public</GalleryFlags>
215
+
216
+ <Properties>
217
+ <Property Id="Microsoft.VisualStudio.Code.Engine" Value="^1.88.0" />
218
+ <Property Id="Microsoft.VisualStudio.Code.ExtensionDependencies" Value="" />
219
+ <Property Id="Microsoft.VisualStudio.Code.ExtensionPack" Value="" />
220
+ <Property Id="Microsoft.VisualStudio.Code.ExtensionKind" Value="workspace,web" />
221
+ <Property Id="Microsoft.VisualStudio.Code.LocalizedLanguages" Value="" />
222
+ <Property Id="Microsoft.VisualStudio.Code.EnabledApiProposals" Value="" />
223
+
224
+ <Property Id="Microsoft.VisualStudio.Code.ExecutesCode" Value="true" />
225
+ <Property Id="Microsoft.VisualStudio.Code.SponsorLink" Value="https://github.com/sponsors/johnsoncodehk" />
226
+ <Property Id="Microsoft.VisualStudio.Services.Links.Source" Value="https://github.com/vuejs/language-tools.git" />
227
+ <Property Id="Microsoft.VisualStudio.Services.Links.Getstarted" Value="https://github.com/vuejs/language-tools.git" />
228
+ <Property Id="Microsoft.VisualStudio.Services.Links.GitHub" Value="https://github.com/vuejs/language-tools.git" />
229
+ <Property Id="Microsoft.VisualStudio.Services.Links.Support" Value="https://github.com/vuejs/language-tools/issues" />
230
+ <Property Id="Microsoft.VisualStudio.Services.Links.Learn" Value="https://github.com/vuejs/language-tools#readme" />
231
+
232
+
233
+ <Property Id="Microsoft.VisualStudio.Services.GitHubFlavoredMarkdown" Value="true" />
234
+ <Property Id="Microsoft.VisualStudio.Services.Content.Pricing" Value="Free"/>
235
+
236
+
237
+
238
+ </Properties>
239
+ <License>extension/LICENSE.txt</License>
240
+ <Icon>extension/icon.png</Icon>
241
+ </Metadata>
242
+ <Installation>
243
+ <InstallationTarget Id="Microsoft.VisualStudio.Code"/>
244
+ </Installation>
245
+ <Dependencies/>
246
+ <Assets>
247
+ <Asset Type="Microsoft.VisualStudio.Code.Manifest" Path="extension/package.json" Addressable="true" />
248
+ <Asset Type="Microsoft.VisualStudio.Services.Content.Details" Path="extension/readme.md" Addressable="true" />
249
+ <Asset Type="Microsoft.VisualStudio.Services.Content.Changelog" Path="extension/changelog.md" Addressable="true" />
250
+ <Asset Type="Microsoft.VisualStudio.Services.Content.License" Path="extension/LICENSE.txt" Addressable="true" />
251
+ <Asset Type="Microsoft.VisualStudio.Services.Icons.Default" Path="extension/icon.png" Addressable="true" />
252
+ </Assets>
253
+ </PackageManifest>
254
+ `;
255
+ const result = parseVsixManifest(xml);
256
+ assert.ok(result);
257
+ assert.strictEqual(result.publisher, "Vue");
258
+ assert.strictEqual(result.name, "volar");
259
+ assert.strictEqual(result.version, "3.2.6");
260
+ assert.strictEqual(result.displayName, "Vue (Official)");
261
+ assert.strictEqual(result.description, "Language Support for Vue");
262
+ assert.deepStrictEqual(result.tags, [
263
+ "json",
264
+ "vue",
265
+ "__ext_vue",
266
+ "markdown",
267
+ "html",
268
+ "jade",
269
+ "__web_extension",
270
+ "__sponsor_extension",
271
+ ]);
272
+ });
273
+ it("should parse a real one with properties", () => {
274
+ const xml = `<?xml version="1.0" encoding="utf-8"?>
275
+ <PackageManifest Version="2.0.0" xmlns="http://schemas.microsoft.com/developer/vsx-schema/2011" xmlns:d="http://schemas.microsoft.com/developer/vsx-schema-design/2011">
276
+ <Metadata>
277
+ <Identity Language="en-US" Id="pyrefly" Version="0.61.0" Publisher="meta" TargetPlatform="win32-x64"/>
278
+ <DisplayName>Pyrefly - Python Language Tooling</DisplayName>
279
+ <Description xml:space="preserve">Python autocomplete, typechecking, code navigation and more! Powered by Pyrefly, an open-source language server</Description>
280
+ <Tags>multi-root ready,python,type,typecheck,typehint,completion,lint,Python,__ext_py,__ext_pyi</Tags>
281
+ <Categories>Programming Languages,Linters,Other</Categories>
282
+ <GalleryFlags>Public</GalleryFlags>
283
+
284
+ <Properties>
285
+ <Property Id="Microsoft.VisualStudio.Code.Engine" Value="^1.94.0" />
286
+ <Property Id="Microsoft.VisualStudio.Code.ExtensionDependencies" Value="ms-python.python" />
287
+ <Property Id="Microsoft.VisualStudio.Code.ExtensionPack" Value="" />
288
+ <Property Id="Microsoft.VisualStudio.Code.ExtensionKind" Value="workspace" />
289
+ <Property Id="Microsoft.VisualStudio.Code.LocalizedLanguages" Value="" />
290
+ <Property Id="Microsoft.VisualStudio.Code.EnabledApiProposals" Value="" />
291
+
292
+ <Property Id="Microsoft.VisualStudio.Code.ExecutesCode" Value="true" />
293
+
294
+ <Property Id="Microsoft.VisualStudio.Services.Links.Source" Value="https://github.com/facebook/pyrefly.git" />
295
+ <Property Id="Microsoft.VisualStudio.Services.Links.Getstarted" Value="https://github.com/facebook/pyrefly.git" />
296
+ <Property Id="Microsoft.VisualStudio.Services.Links.GitHub" Value="https://github.com/facebook/pyrefly.git" />
297
+ <Property Id="Microsoft.VisualStudio.Services.Links.Support" Value="https://github.com/facebook/pyrefly/issues" />
298
+ <Property Id="Microsoft.VisualStudio.Services.Links.Learn" Value="https://github.com/facebook/pyrefly#readme" />
299
+
300
+
301
+ <Property Id="Microsoft.VisualStudio.Services.GitHubFlavoredMarkdown" Value="true" />
302
+ <Property Id="Microsoft.VisualStudio.Services.Content.Pricing" Value="Free"/>
303
+
304
+
305
+
306
+ </Properties>
307
+ <License>extension/LICENSE.txt</License>
308
+ <Icon>extension/images/pyrefly-symbol.png</Icon>
309
+ </Metadata>
310
+ <Installation>
311
+ <InstallationTarget Id="Microsoft.VisualStudio.Code"/>
312
+ </Installation>
313
+ <Dependencies/>
314
+ <Assets>
315
+ <Asset Type="Microsoft.VisualStudio.Code.Manifest" Path="extension/package.json" Addressable="true" />
316
+ <Asset Type="Microsoft.VisualStudio.Services.Content.Details" Path="extension/README.md" Addressable="true" />
317
+ <Asset Type="Microsoft.VisualStudio.Services.Content.License" Path="extension/LICENSE.txt" Addressable="true" />
318
+ <Asset Type="Microsoft.VisualStudio.Services.Icons.Default" Path="extension/images/pyrefly-symbol.png" Addressable="true" />
319
+ </Assets>
320
+ </PackageManifest>`;
321
+ const result = parseVsixManifest(xml);
322
+ assert.ok(result);
323
+ assert.strictEqual(result.publisher, "meta");
324
+ assert.strictEqual(result.name, "pyrefly");
325
+ assert.strictEqual(result.version, "0.61.0");
326
+ assert.strictEqual(result.platform, "win32-x64");
327
+ assert.strictEqual(result.displayName, "Pyrefly - Python Language Tooling");
328
+ // Properties tag parsing
329
+ assert.strictEqual(result.vscodeEngine, "^1.94.0");
330
+ assert.deepStrictEqual(result.extensionDependencies, ["ms-python.python"]);
331
+ assert.deepStrictEqual(result.extensionKind, ["workspace"]);
332
+ assert.strictEqual(result.executesCode, true);
333
+ // Links from Properties
334
+ assert.ok(result.links);
335
+ assert.strictEqual(
336
+ result.links.Source,
337
+ "https://github.com/facebook/pyrefly.git",
338
+ );
339
+ assert.strictEqual(
340
+ result.links.GitHub,
341
+ "https://github.com/facebook/pyrefly.git",
342
+ );
343
+ assert.strictEqual(
344
+ result.links.Support,
345
+ "https://github.com/facebook/pyrefly/issues",
346
+ );
347
+ assert.strictEqual(
348
+ result.links.Learn,
349
+ "https://github.com/facebook/pyrefly#readme",
350
+ );
351
+ // Empty ExtensionPack should not be set
352
+ assert.strictEqual(result.extensionPack, undefined);
353
+ });
354
+ it("should return undefined for invalid XML", () => {
355
+ const result = parseVsixManifest("not xml at all");
356
+ assert.strictEqual(result, undefined);
357
+ });
358
+
359
+ it("should return undefined for XML without PackageManifest", () => {
360
+ const xml = `<?xml version="1.0" encoding="utf-8"?><root><child /></root>`;
361
+ const result = parseVsixManifest(xml);
362
+ assert.strictEqual(result, undefined);
363
+ });
364
+ });
365
+
366
+ describe("extractExtensionCapabilities", () => {
367
+ it("should return empty object for null/undefined", () => {
368
+ assert.deepStrictEqual(extractExtensionCapabilities(null), {});
369
+ assert.deepStrictEqual(extractExtensionCapabilities(undefined), {});
370
+ });
371
+
372
+ it("should extract activation events", () => {
373
+ const pkg = {
374
+ activationEvents: ["onLanguage:python", "onCommand:python.runLinting"],
375
+ };
376
+ const caps = extractExtensionCapabilities(pkg);
377
+ assert.deepStrictEqual(caps.activationEvents, [
378
+ "onLanguage:python",
379
+ "onCommand:python.runLinting",
380
+ ]);
381
+ });
382
+
383
+ it("should flag wildcard activation (always-on extension)", () => {
384
+ const pkg = { activationEvents: ["*"] };
385
+ const caps = extractExtensionCapabilities(pkg);
386
+ assert.deepStrictEqual(caps.activationEvents, ["*"]);
387
+ });
388
+
389
+ it("should extract extensionKind", () => {
390
+ const pkg = { extensionKind: ["workspace"] };
391
+ const caps = extractExtensionCapabilities(pkg);
392
+ assert.deepStrictEqual(caps.extensionKind, ["workspace"]);
393
+ });
394
+
395
+ it("should extract extensionDependencies", () => {
396
+ const pkg = {
397
+ extensionDependencies: ["ms-python.python", "ms-toolsai.jupyter"],
398
+ };
399
+ const caps = extractExtensionCapabilities(pkg);
400
+ assert.deepStrictEqual(caps.extensionDependencies, [
401
+ "ms-python.python",
402
+ "ms-toolsai.jupyter",
403
+ ]);
404
+ });
405
+
406
+ it("should extract extensionPack", () => {
407
+ const pkg = {
408
+ extensionPack: [
409
+ "ms-python.python",
410
+ "ms-python.vscode-pylance",
411
+ "ms-toolsai.jupyter",
412
+ ],
413
+ };
414
+ const caps = extractExtensionCapabilities(pkg);
415
+ assert.deepStrictEqual(caps.extensionPack, [
416
+ "ms-python.python",
417
+ "ms-python.vscode-pylance",
418
+ "ms-toolsai.jupyter",
419
+ ]);
420
+ });
421
+
422
+ it("should extract workspace trust configuration", () => {
423
+ const pkg = {
424
+ capabilities: {
425
+ untrustedWorkspaces: {
426
+ supported: "limited",
427
+ description: "Only basic",
428
+ },
429
+ virtualWorkspaces: { supported: false },
430
+ },
431
+ };
432
+ const caps = extractExtensionCapabilities(pkg);
433
+ assert.deepStrictEqual(caps.untrustedWorkspaces, {
434
+ supported: "limited",
435
+ description: "Only basic",
436
+ });
437
+ assert.deepStrictEqual(caps.virtualWorkspaces, { supported: false });
438
+ });
439
+
440
+ it("should extract contributed features", () => {
441
+ const pkg = {
442
+ contributes: {
443
+ commands: [{ command: "ext.run", title: "Run" }],
444
+ debuggers: [{ type: "python", label: "Python" }],
445
+ terminal: [{ id: "ext.terminal" }],
446
+ authentication: [{ id: "ext.auth", label: "My Auth" }],
447
+ },
448
+ };
449
+ const caps = extractExtensionCapabilities(pkg);
450
+ assert.ok(caps.contributes.includes("commands:count:1"));
451
+ assert.ok(caps.contributes.includes("debuggers:count:1"));
452
+ assert.ok(caps.contributes.includes("terminal-access"));
453
+ assert.ok(caps.contributes.includes("authentication-provider"));
454
+ });
455
+
456
+ it("should extract main and browser entry points", () => {
457
+ const pkg = {
458
+ main: "./dist/extension.js",
459
+ browser: "./dist/web/extension.js",
460
+ };
461
+ const caps = extractExtensionCapabilities(pkg);
462
+ assert.strictEqual(caps.main, "./dist/extension.js");
463
+ assert.strictEqual(caps.browser, "./dist/web/extension.js");
464
+ });
465
+
466
+ it("should detect lifecycle scripts", () => {
467
+ const pkg = {
468
+ scripts: {
469
+ postinstall: "node setup.js",
470
+ "vscode:prepublish": "npm run build",
471
+ "vscode:uninstall": "node cleanup.js",
472
+ test: "jest",
473
+ },
474
+ };
475
+ const caps = extractExtensionCapabilities(pkg);
476
+ assert.ok(caps.lifecycleScripts.includes("postinstall"));
477
+ assert.ok(caps.lifecycleScripts.includes("vscode:prepublish"));
478
+ assert.ok(caps.lifecycleScripts.includes("vscode:uninstall"));
479
+ assert.ok(
480
+ !caps.lifecycleScripts.includes("test"),
481
+ "test is not a lifecycle script",
482
+ );
483
+ });
484
+
485
+ it("should handle extension with taskDefinitions", () => {
486
+ const pkg = {
487
+ contributes: {
488
+ taskDefinitions: [{ type: "npm" }],
489
+ },
490
+ };
491
+ const caps = extractExtensionCapabilities(pkg);
492
+ assert.ok(caps.contributes.includes("terminal-access"));
493
+ });
494
+
495
+ it("should handle extension with filesystem providers", () => {
496
+ const pkg = {
497
+ contributes: {
498
+ fileSystemProviders: [{ scheme: "ftp", authority: "ftp" }],
499
+ },
500
+ };
501
+ const caps = extractExtensionCapabilities(pkg);
502
+ assert.ok(caps.contributes.includes("filesystem-provider"));
503
+ });
504
+
505
+ it("should return empty for extension with no capabilities", () => {
506
+ const pkg = { name: "simple-ext", version: "1.0.0" };
507
+ const caps = extractExtensionCapabilities(pkg);
508
+ assert.ok(!caps.activationEvents);
509
+ assert.ok(!caps.contributes);
510
+ assert.ok(!caps.lifecycleScripts);
511
+ assert.ok(!caps.main);
512
+ });
513
+ });
514
+
515
+ describe("parseVsixPackageJson", () => {
516
+ it("should return undefined for empty input", () => {
517
+ assert.strictEqual(parseVsixPackageJson(""), undefined);
518
+ assert.strictEqual(parseVsixPackageJson("{}"), undefined);
519
+ assert.strictEqual(parseVsixPackageJson(null), undefined);
520
+ });
521
+
522
+ it("should parse a valid package.json string", () => {
523
+ const json = JSON.stringify({
524
+ name: "python",
525
+ publisher: "ms-python",
526
+ version: "2023.25.0",
527
+ displayName: "Python",
528
+ description: "Python language support with Pylance",
529
+ });
530
+ const result = parseVsixPackageJson(json, "/test/path");
531
+ assert.ok(result);
532
+ assert.strictEqual(result.publisher, "ms-python");
533
+ assert.strictEqual(result.name, "python");
534
+ assert.strictEqual(result.version, "2023.25.0");
535
+ assert.strictEqual(result.displayName, "Python");
536
+ assert.strictEqual(
537
+ result.description,
538
+ "Python language support with Pylance",
539
+ );
540
+ assert.strictEqual(result.srcPath, "/test/path");
541
+ });
542
+
543
+ it("should parse a pre-parsed object", () => {
544
+ const obj = {
545
+ name: "go",
546
+ publisher: "golang",
547
+ version: "0.39.1",
548
+ displayName: "Go",
549
+ };
550
+ const result = parseVsixPackageJson(obj);
551
+ assert.ok(result);
552
+ assert.strictEqual(result.publisher, "golang");
553
+ assert.strictEqual(result.name, "go");
554
+ assert.strictEqual(result.version, "0.39.1");
555
+ });
556
+
557
+ it("should include capabilities from package.json", () => {
558
+ const obj = {
559
+ name: "python",
560
+ publisher: "ms-python",
561
+ version: "1.0.0",
562
+ activationEvents: ["onLanguage:python"],
563
+ main: "./dist/extension.js",
564
+ contributes: {
565
+ commands: [{ command: "python.run", title: "Run" }],
566
+ },
567
+ scripts: {
568
+ postinstall: "node install.js",
569
+ },
570
+ };
571
+ const result = parseVsixPackageJson(obj);
572
+ assert.ok(result);
573
+ assert.ok(result.capabilities);
574
+ assert.deepStrictEqual(result.capabilities.activationEvents, [
575
+ "onLanguage:python",
576
+ ]);
577
+ assert.strictEqual(result.capabilities.main, "./dist/extension.js");
578
+ assert.ok(result.capabilities.contributes.includes("commands:count:1"));
579
+ assert.ok(result.capabilities.lifecycleScripts.includes("postinstall"));
580
+ });
581
+
582
+ it("should handle missing optional fields", () => {
583
+ const obj = { name: "simple-ext" };
584
+ const result = parseVsixPackageJson(obj);
585
+ assert.ok(result);
586
+ assert.strictEqual(result.name, "simple-ext");
587
+ assert.strictEqual(result.publisher, "");
588
+ assert.strictEqual(result.version, "");
589
+ assert.strictEqual(result.displayName, "");
590
+ assert.strictEqual(result.description, "");
591
+ });
592
+
593
+ it("should return undefined for invalid JSON string", () => {
594
+ const result = parseVsixPackageJson("not json");
595
+ assert.strictEqual(result, undefined);
596
+ });
597
+ it("should handle a real one", () => {
598
+ const result = parseVsixPackageJson(`
599
+ {
600
+ "private": true,
601
+ "name": "volar",
602
+ "version": "3.2.6",
603
+ "repository": {
604
+ "type": "git",
605
+ "url": "https://github.com/vuejs/language-tools.git",
606
+ "directory": "extensions/vscode"
607
+ },
608
+ "categories": [
609
+ "Programming Languages"
610
+ ],
611
+ "sponsor": {
612
+ "url": "https://github.com/sponsors/johnsoncodehk"
613
+ },
614
+ "icon": "icon.png",
615
+ "displayName": "Vue (Official)",
616
+ "description": "Language Support for Vue",
617
+ "author": "johnsoncodehk",
618
+ "publisher": "Vue",
619
+ "engines": {
620
+ "vscode": "^1.88.0"
621
+ },
622
+ "activationEvents": [
623
+ "onLanguage"
624
+ ],
625
+ "main": "./main.js",
626
+ "browser": "./web.js",
627
+ "capabilities": {
628
+ "virtualWorkspaces": {
629
+ "supported": "limited",
630
+ "description": "Install https://marketplace.visualstudio.com/items?itemName=johnsoncodehk.vscode-typescript-web to have IntelliSense for .vue files in Web IDE."
631
+ }
632
+ },
633
+ "contributes": {
634
+ "jsonValidation": [
635
+ {
636
+ "fileMatch": [
637
+ "tsconfig.json",
638
+ "tsconfig.*.json",
639
+ "tsconfig-*.json",
640
+ "jsconfig.json",
641
+ "jsconfig.*.json",
642
+ "jsconfig-*.json"
643
+ ],
644
+ "url": "./schemas/vue-tsconfig.schema.json"
645
+ }
646
+ ],
647
+ "languages": [
648
+ {
649
+ "id": "vue",
650
+ "extensions": [
651
+ ".vue"
652
+ ],
653
+ "configuration": "./languages/vue-language-configuration.json"
654
+ },
655
+ {
656
+ "id": "markdown",
657
+ "configuration": "./languages/markdown-language-configuration.json"
658
+ },
659
+ {
660
+ "id": "html",
661
+ "configuration": "./languages/sfc-template-language-configuration.json"
662
+ },
663
+ {
664
+ "id": "jade",
665
+ "configuration": "./languages/sfc-template-language-configuration.json"
666
+ }
667
+ ],
668
+ "grammars": [
669
+ {
670
+ "language": "vue",
671
+ "scopeName": "text.html.vue",
672
+ "path": "./syntaxes/vue.tmLanguage.json",
673
+ "embeddedLanguages": {
674
+ "text.html.vue": "vue",
675
+ "text": "plaintext",
676
+ "text.html.derivative": "html",
677
+ "text.html.markdown": "markdown",
678
+ "text.pug": "jade",
679
+ "source.css": "css",
680
+ "source.css.scss": "scss",
681
+ "source.css.less": "less",
682
+ "source.sass": "sass",
683
+ "source.stylus": "stylus",
684
+ "source.postcss": "postcss",
685
+ "source.js": "javascript",
686
+ "source.ts": "typescript",
687
+ "source.js.jsx": "javascriptreact",
688
+ "source.tsx": "typescriptreact",
689
+ "source.coffee": "coffeescript",
690
+ "meta.tag.js": "jsx-tags",
691
+ "meta.tag.tsx": "jsx-tags",
692
+ "meta.tag.without-attributes.js": "jsx-tags",
693
+ "meta.tag.without-attributes.tsx": "jsx-tags",
694
+ "source.json": "json",
695
+ "source.json.comments": "jsonc",
696
+ "source.json5": "json5",
697
+ "source.yaml": "yaml",
698
+ "source.toml": "toml",
699
+ "source.graphql": "graphql"
700
+ },
701
+ "unbalancedBracketScopes": [
702
+ "keyword.operator.relational",
703
+ "storage.type.function.arrow",
704
+ "keyword.operator.bitwise.shift",
705
+ "meta.brace.angle",
706
+ "punctuation.definition.tag"
707
+ ]
708
+ },
709
+ {
710
+ "scopeName": "markdown.vue.codeblock",
711
+ "path": "./syntaxes/markdown-vue.json",
712
+ "injectTo": [
713
+ "text.html.markdown"
714
+ ],
715
+ "embeddedLanguages": {
716
+ "meta.embedded.block.vue": "vue",
717
+ "text.html.vue": "vue",
718
+ "text": "plaintext",
719
+ "text.html.derivative": "html",
720
+ "text.html.markdown": "markdown",
721
+ "text.pug": "jade",
722
+ "source.css": "css",
723
+ "source.css.scss": "scss",
724
+ "source.css.less": "less",
725
+ "source.sass": "sass",
726
+ "source.stylus": "stylus",
727
+ "source.postcss": "postcss",
728
+ "source.js": "javascript",
729
+ "source.ts": "typescript",
730
+ "source.js.jsx": "javascriptreact",
731
+ "source.tsx": "typescriptreact",
732
+ "source.coffee": "coffeescript",
733
+ "meta.tag.js": "jsx-tags",
734
+ "meta.tag.tsx": "jsx-tags",
735
+ "meta.tag.without-attributes.js": "jsx-tags",
736
+ "meta.tag.without-attributes.tsx": "jsx-tags",
737
+ "source.json": "json",
738
+ "source.json.comments": "jsonc",
739
+ "source.json5": "json5",
740
+ "source.yaml": "yaml",
741
+ "source.toml": "toml",
742
+ "source.graphql": "graphql"
743
+ }
744
+ },
745
+ {
746
+ "scopeName": "mdx.vue.codeblock",
747
+ "path": "./syntaxes/mdx-vue.json",
748
+ "injectTo": [
749
+ "source.mdx"
750
+ ],
751
+ "embeddedLanguages": {
752
+ "mdx.embedded.vue": "vue",
753
+ "text.html.vue": "vue",
754
+ "text": "plaintext",
755
+ "text.html.derivative": "html",
756
+ "text.html.markdown": "markdown",
757
+ "text.pug": "jade",
758
+ "source.css": "css",
759
+ "source.css.scss": "scss",
760
+ "source.css.less": "less",
761
+ "source.sass": "sass",
762
+ "source.stylus": "stylus",
763
+ "source.postcss": "postcss",
764
+ "source.js": "javascript",
765
+ "source.ts": "typescript",
766
+ "source.js.jsx": "javascriptreact",
767
+ "source.tsx": "typescriptreact",
768
+ "source.coffee": "coffeescript",
769
+ "meta.tag.js": "jsx-tags",
770
+ "meta.tag.tsx": "jsx-tags",
771
+ "meta.tag.without-attributes.js": "jsx-tags",
772
+ "meta.tag.without-attributes.tsx": "jsx-tags",
773
+ "source.json": "json",
774
+ "source.json.comments": "jsonc",
775
+ "source.json5": "json5",
776
+ "source.yaml": "yaml",
777
+ "source.toml": "toml",
778
+ "source.graphql": "graphql"
779
+ }
780
+ },
781
+ {
782
+ "scopeName": "vue.directives",
783
+ "path": "./syntaxes/vue-directives.json",
784
+ "injectTo": [
785
+ "text.html.vue",
786
+ "text.html.markdown",
787
+ "text.html.derivative",
788
+ "text.pug"
789
+ ]
790
+ },
791
+ {
792
+ "scopeName": "vue.interpolations",
793
+ "path": "./syntaxes/vue-interpolations.json",
794
+ "injectTo": [
795
+ "text.html.vue",
796
+ "text.html.markdown",
797
+ "text.html.derivative",
798
+ "text.pug"
799
+ ]
800
+ },
801
+ {
802
+ "scopeName": "vue.sfc.script.leading-operator-fix",
803
+ "path": "./syntaxes/vue-sfc-script-leading-operator-fix.json",
804
+ "injectTo": [
805
+ "text.html.vue"
806
+ ]
807
+ },
808
+ {
809
+ "scopeName": "vue.sfc.style.variable.injection",
810
+ "path": "./syntaxes/vue-sfc-style-variable-injection.json",
811
+ "injectTo": [
812
+ "text.html.vue"
813
+ ]
814
+ }
815
+ ],
816
+ "semanticTokenScopes": [
817
+ {
818
+ "language": "vue",
819
+ "scopes": {
820
+ "component": [
821
+ "support.class.component.vue",
822
+ "entity.name.type.class.vue"
823
+ ]
824
+ }
825
+ },
826
+ {
827
+ "language": "markdown",
828
+ "scopes": {
829
+ "component": [
830
+ "support.class.component.vue",
831
+ "entity.name.type.class.vue"
832
+ ]
833
+ }
834
+ },
835
+ {
836
+ "language": "html",
837
+ "scopes": {
838
+ "component": [
839
+ "support.class.component.vue",
840
+ "entity.name.type.class.vue"
841
+ ]
842
+ }
843
+ }
844
+ ],
845
+ "breakpoints": [
846
+ {
847
+ "language": "vue"
848
+ }
849
+ ],
850
+ "configuration": {
851
+ "type": "object",
852
+ "title": "Vue",
853
+ "properties": {
854
+ "vue.trace.server": {
855
+ "scope": "window",
856
+ "type": "string",
857
+ "enum": [
858
+ "off",
859
+ "messages",
860
+ "verbose"
861
+ ],
862
+ "default": "off",
863
+ "markdownDescription": "%configuration.trace.server%"
864
+ },
865
+ "vue.editor.focusMode": {
866
+ "type": "boolean",
867
+ "default": false,
868
+ "markdownDescription": "%configuration.editor.focusMode%"
869
+ },
870
+ "vue.editor.reactivityVisualization": {
871
+ "type": "boolean",
872
+ "default": true,
873
+ "markdownDescription": "%configuration.editor.reactivityVisualization%"
874
+ },
875
+ "vue.editor.templateInterpolationDecorators": {
876
+ "type": "boolean",
877
+ "default": true,
878
+ "markdownDescription": "%configuration.editor.templateInterpolationDecorators%"
879
+ },
880
+ "vue.server.path": {
881
+ "type": "string",
882
+ "markdownDescription": "%configuration.server.path%"
883
+ },
884
+ "vue.server.includeLanguages": {
885
+ "type": "array",
886
+ "items": {
887
+ "type": "string"
888
+ },
889
+ "default": [
890
+ "vue"
891
+ ],
892
+ "markdownDescription": "%configuration.server.includeLanguages%"
893
+ },
894
+ "vue.codeActions.askNewComponentName": {
895
+ "type": "boolean",
896
+ "default": true,
897
+ "markdownDescription": "%configuration.codeActions.askNewComponentName%"
898
+ },
899
+ "vue.hover.rich": {
900
+ "type": "boolean",
901
+ "default": false,
902
+ "markdownDescription": "%configuration.hover.rich%"
903
+ },
904
+ "vue.suggest.componentNameCasing": {
905
+ "type": "string",
906
+ "enum": [
907
+ "preferKebabCase",
908
+ "preferPascalCase",
909
+ "alwaysKebabCase",
910
+ "alwaysPascalCase"
911
+ ],
912
+ "enumDescriptions": [
913
+ "Prefer kebab-case (lowercase with hyphens, e.g. my-component)",
914
+ "Prefer PascalCase (UpperCamelCase, e.g. MyComponent)",
915
+ "Always kebab-case (enforce kebab-case, e.g. my-component)",
916
+ "Always PascalCase (enforce PascalCase, e.g. MyComponent)"
917
+ ],
918
+ "default": "preferPascalCase",
919
+ "markdownDescription": "%configuration.suggest.componentNameCasing%"
920
+ },
921
+ "vue.suggest.propNameCasing": {
922
+ "type": "string",
923
+ "enum": [
924
+ "preferKebabCase",
925
+ "preferCamelCase",
926
+ "alwaysKebabCase",
927
+ "alwaysCamelCase"
928
+ ],
929
+ "enumDescriptions": [
930
+ "Prefer kebab-case (lowercase with hyphens, e.g. my-prop)",
931
+ "Prefer camelCase (lowerCamelCase, e.g. myProp)",
932
+ "Always kebab-case (enforce kebab-case, e.g. my-prop)",
933
+ "Always camelCase (enforce camelCase, e.g. myProp)"
934
+ ],
935
+ "default": "preferKebabCase",
936
+ "markdownDescription": "%configuration.suggest.propNameCasing%"
937
+ },
938
+ "vue.suggest.defineAssignment": {
939
+ "type": "boolean",
940
+ "default": true,
941
+ "markdownDescription": "%configuration.suggest.defineAssignment%"
942
+ },
943
+ "vue.autoInsert.dotValue": {
944
+ "type": "boolean",
945
+ "default": false,
946
+ "markdownDescription": "%configuration.autoInsert.dotValue%"
947
+ },
948
+ "vue.autoInsert.bracketSpacing": {
949
+ "type": "boolean",
950
+ "default": true,
951
+ "markdownDescription": "%configuration.autoInsert.bracketSpacing%"
952
+ },
953
+ "vue.inlayHints.destructuredProps": {
954
+ "type": "boolean",
955
+ "default": false,
956
+ "markdownDescription": "%configuration.inlayHints.destructuredProps%"
957
+ },
958
+ "vue.inlayHints.missingProps": {
959
+ "type": "boolean",
960
+ "default": false,
961
+ "markdownDescription": "%configuration.inlayHints.missingProps%"
962
+ },
963
+ "vue.inlayHints.inlineHandlerLeading": {
964
+ "type": "boolean",
965
+ "default": false,
966
+ "markdownDescription": "%configuration.inlayHints.inlineHandlerLeading%"
967
+ },
968
+ "vue.inlayHints.optionsWrapper": {
969
+ "type": "boolean",
970
+ "default": false,
971
+ "markdownDescription": "%configuration.inlayHints.optionsWrapper%"
972
+ },
973
+ "vue.inlayHints.vBindShorthand": {
974
+ "type": "boolean",
975
+ "default": false,
976
+ "markdownDescription": "%configuration.inlayHints.vBindShorthand%"
977
+ },
978
+ "vue.format.template.initialIndent": {
979
+ "type": "boolean",
980
+ "default": true,
981
+ "markdownDescription": "%configuration.format.template.initialIndent%"
982
+ },
983
+ "vue.format.script.initialIndent": {
984
+ "type": "boolean",
985
+ "default": false,
986
+ "markdownDescription": "%configuration.format.script.initialIndent%"
987
+ },
988
+ "vue.format.style.initialIndent": {
989
+ "type": "boolean",
990
+ "default": false,
991
+ "markdownDescription": "%configuration.format.style.initialIndent%"
992
+ },
993
+ "vue.format.script.enabled": {
994
+ "type": "boolean",
995
+ "default": true,
996
+ "markdownDescription": "%configuration.format.script.enabled%"
997
+ },
998
+ "vue.format.template.enabled": {
999
+ "type": "boolean",
1000
+ "default": true,
1001
+ "markdownDescription": "%configuration.format.template.enabled%"
1002
+ },
1003
+ "vue.format.style.enabled": {
1004
+ "type": "boolean",
1005
+ "default": true,
1006
+ "markdownDescription": "%configuration.format.style.enabled%"
1007
+ },
1008
+ "vue.format.wrapAttributes": {
1009
+ "type": "string",
1010
+ "default": "auto",
1011
+ "enum": [
1012
+ "auto",
1013
+ "force",
1014
+ "force-aligned",
1015
+ "force-expand-multiline",
1016
+ "aligned-multiple",
1017
+ "preserve",
1018
+ "preserve-aligned"
1019
+ ],
1020
+ "markdownDescription": "%configuration.format.wrapAttributes%"
1021
+ }
1022
+ }
1023
+ },
1024
+ "commands": [
1025
+ {
1026
+ "command": "vue.welcome",
1027
+ "title": "%command.welcome%",
1028
+ "category": "Vue"
1029
+ },
1030
+ {
1031
+ "command": "vue.action.restartServer",
1032
+ "title": "%command.action.restartServer%",
1033
+ "category": "Vue"
1034
+ }
1035
+ ],
1036
+ "menus": {
1037
+ "editor/context": [
1038
+ {
1039
+ "command": "typescript.goToSourceDefinition",
1040
+ "when": "tsSupportsSourceDefinition && resourceLangId == vue",
1041
+ "group": "navigation@9"
1042
+ }
1043
+ ],
1044
+ "explorer/context": [
1045
+ {
1046
+ "command": "typescript.findAllFileReferences",
1047
+ "when": "tsSupportsFileReferences && resourceLangId == vue",
1048
+ "group": "4_search"
1049
+ }
1050
+ ],
1051
+ "editor/title/context": [
1052
+ {
1053
+ "command": "typescript.findAllFileReferences",
1054
+ "when": "tsSupportsFileReferences && resourceLangId == vue"
1055
+ }
1056
+ ],
1057
+ "commandPalette": [
1058
+ {
1059
+ "command": "typescript.reloadProjects",
1060
+ "when": "editorLangId == vue && typescript.isManagedFile"
1061
+ },
1062
+ {
1063
+ "command": "typescript.goToProjectConfig",
1064
+ "when": "editorLangId == vue && typescript.isManagedFile"
1065
+ },
1066
+ {
1067
+ "command": "typescript.sortImports",
1068
+ "when": "supportedCodeAction =~ /(\\\\s|^)source\\\\.sortImports\\\\b/ && editorLangId =~ /^vue$/"
1069
+ },
1070
+ {
1071
+ "command": "typescript.removeUnusedImports",
1072
+ "when": "supportedCodeAction =~ /(\\\\s|^)source\\\\.removeUnusedImports\\\\b/ && editorLangId =~ /^vue$/"
1073
+ }
1074
+ ]
1075
+ }
1076
+ },
1077
+ "scripts": {
1078
+ "vscode:prepublish": "rolldown --config",
1079
+ "pack": "npx @vscode/vsce package",
1080
+ "gen-ext-meta": "vscode-ext-gen --scope vue --output src/generated-meta.ts && cd ../.. && npm run format"
1081
+ },
1082
+ "devDependencies": {
1083
+ "@types/node": "^22.10.4",
1084
+ "@types/vscode": "1.88.0",
1085
+ "@volar/typescript": "2.4.28",
1086
+ "@volar/vscode": "2.4.28",
1087
+ "@vue/language-core": "workspace:*",
1088
+ "@vue/language-server": "workspace:*",
1089
+ "@vue/typescript-plugin": "workspace:*",
1090
+ "laplacenoma": "latest",
1091
+ "reactive-vscode": "^0.4.1",
1092
+ "rolldown": "latest",
1093
+ "vscode-ext-gen": "latest",
1094
+ "vscode-tmlanguage-snapshot": "latest"
1095
+ }
1096
+ }
1097
+ `);
1098
+ assert.ok(result);
1099
+ assert.strictEqual(result.publisher, "Vue");
1100
+ assert.strictEqual(result.name, "volar");
1101
+ assert.strictEqual(result.version, "3.2.6");
1102
+ assert.strictEqual(result.displayName, "Vue (Official)");
1103
+ assert.strictEqual(result.description, "Language Support for Vue");
1104
+ assert.strictEqual(result.platform, "");
1105
+ assert.strictEqual(result.srcPath, undefined);
1106
+ // Should now capture repository as external reference (was a bug before: checked packageJsonData instead of pkg)
1107
+ assert.ok(result.externalReferences);
1108
+ assert.deepStrictEqual(result.externalReferences, [
1109
+ { type: "vcs", url: "https://github.com/vuejs/language-tools.git" },
1110
+ ]);
1111
+ assert.deepStrictEqual(result.capabilities, {
1112
+ activationEvents: ["onLanguage"],
1113
+ virtualWorkspaces: {
1114
+ supported: "limited",
1115
+ description:
1116
+ "Install https://marketplace.visualstudio.com/items?itemName=johnsoncodehk.vscode-typescript-web to have IntelliSense for .vue files in Web IDE.",
1117
+ },
1118
+ contributes: [
1119
+ "breakpoints:count:1",
1120
+ "commands:count:2",
1121
+ "language-server-plugins",
1122
+ ],
1123
+ main: "./main.js",
1124
+ browser: "./web.js",
1125
+ lifecycleScripts: ["vscode:prepublish"],
1126
+ });
1127
+ // Should capture devDependencies for later analysis
1128
+ assert.ok(result.devDependencies);
1129
+ assert.strictEqual(result.devDependencies["@types/node"], "^22.10.4");
1130
+ assert.strictEqual(result.devDependencies["@types/vscode"], "1.88.0");
1131
+ assert.strictEqual(
1132
+ result.devDependencies["@vue/language-core"],
1133
+ "workspace:*",
1134
+ );
1135
+ });
1136
+ it("should handle another real one (incomplete)", () => {
1137
+ const result = parseVsixPackageJson(`
1138
+ {
1139
+ "name": "pyrefly",
1140
+ "displayName": "Pyrefly - Python Language Tooling",
1141
+ "description": "Python autocomplete, typechecking, code navigation and more! Powered by Pyrefly, an open-source language server",
1142
+ "icon": "images/pyrefly-symbol.png",
1143
+ "extensionKind": [
1144
+ "workspace"
1145
+ ],
1146
+ "author": "Facebook",
1147
+ "license": "Apache2",
1148
+ "version": "0.61.0",
1149
+ "repository": {
1150
+ "type": "git",
1151
+ "url": "https://github.com/facebook/pyrefly"
1152
+ },
1153
+ "publisher": "meta",
1154
+ "categories": [
1155
+ "Programming Languages",
1156
+ "Linters",
1157
+ "Other"
1158
+ ],
1159
+ "keywords": [
1160
+ "multi-root ready",
1161
+ "python",
1162
+ "type",
1163
+ "typecheck",
1164
+ "typehint",
1165
+ "completion",
1166
+ "lint"
1167
+ ],
1168
+ "engines": {
1169
+ "vscode": "^1.94.0"
1170
+ },
1171
+ "main": "./dist/extension",
1172
+ "activationEvents": [
1173
+ "onLanguage:python",
1174
+ "onNotebook:jupyter-notebook"
1175
+ ],
1176
+ "capabilities": {
1177
+ "untrustedWorkspaces": {
1178
+ "supported": false,
1179
+ "description": "Pyrefly can be configured to execute binaries. A malicious actor could exploit this to run arbitrary code on your machine."
1180
+ }
1181
+ },
1182
+ "contributes": {
1183
+ "languages": [
1184
+ {
1185
+ "id": "python",
1186
+ "aliases": [
1187
+ "Python"
1188
+ ],
1189
+ "extensions": [
1190
+ ".py",
1191
+ ".pyi"
1192
+ ]
1193
+ }
1194
+ ],
1195
+ "commands": [
1196
+ {
1197
+ "title": "Restart Pyrefly Client",
1198
+ "category": "pyrefly",
1199
+ "command": "pyrefly.restartClient"
1200
+ },
1201
+ {
1202
+ "title": "Fold All Docstrings",
1203
+ "category": "pyrefly",
1204
+ "command": "pyrefly.foldAllDocstrings"
1205
+ },
1206
+ {
1207
+ "title": "Unfold All Docstrings",
1208
+ "category": "pyrefly",
1209
+ "command": "pyrefly.unfoldAllDocstrings"
1210
+ },
1211
+ {
1212
+ "title": "Run File",
1213
+ "category": "pyrefly",
1214
+ "command": "pyrefly.runMain"
1215
+ },
1216
+ {
1217
+ "title": "Run Test",
1218
+ "category": "pyrefly",
1219
+ "command": "pyrefly.runTest"
1220
+ }
1221
+ ],
1222
+ "semanticTokenScopes": [
1223
+ {
1224
+ "language": "python",
1225
+ "scopes": {
1226
+ "variable.readonly": [
1227
+ "variable.other.constant.python"
1228
+ ]
1229
+ }
1230
+ }
1231
+ ],
1232
+ "configurationDefaults": {
1233
+ "editor.semanticTokenColorCustomizations": {
1234
+ "rules": {
1235
+ "variable.readonly:python": "#4EC9B0"
1236
+ }
1237
+ }
1238
+ },
1239
+ "configuration": {
1240
+ "properties": {
1241
+ "pyrefly.lspPath": {
1242
+ "type": "string",
1243
+ "default": "",
1244
+ "description": "The path to the binary used for the lsp",
1245
+ "scope": "machine-overridable"
1246
+ },
1247
+ "pyrefly.lspArguments": {
1248
+ "type": "array",
1249
+ "items": {
1250
+ "type": "string"
1251
+ },
1252
+ "default": [
1253
+ "lsp"
1254
+ ],
1255
+ "description": "Additional arguments that should be passed to the binary at pyrefly.lspPath",
1256
+ "scope": "machine-overridable"
1257
+ },
1258
+ "python.pyrefly.disableLanguageServices": {
1259
+ "type": "boolean",
1260
+ "default": false,
1261
+ "description": "If true, pyrefly will not provide other IDE services like completions, hover, definition, etc. To control type errors, see \`python.pyrefly.displayTypeErrors\`",
1262
+ "scope": "resource"
1263
+ },
1264
+ "python.pyrefly.displayTypeErrors": {
1265
+ "type": "string",
1266
+ "description": "If 'default', Pyrefly will only provide type check squiggles in the IDE if your file is covered by a Pyrefly configuration. If 'force-off', Pyrefly will never provide type check squiggles in the IDE. If 'force-on', Pyrefly will always provide type check squiggles in the IDE. If 'error-missing-imports', Pyrefly will only show errors for missing imports and missing sources (missing-import, missing-source, and missing-source-for-stubs).",
1267
+ "default": "default",
1268
+ "enum": [
1269
+ "default",
1270
+ "force-on",
1271
+ "force-off",
1272
+ "error-missing-imports"
1273
+ ],
1274
+ "scope": "resource"
1275
+ },
1276
+ "pyrefly.trace.server": {
1277
+ "type": "string",
1278
+ "description": "Set to 'verbose' to enable LSP trace in the console",
1279
+ "default": "off",
1280
+ "enum": [
1281
+ "off",
1282
+ "verbose"
1283
+ ]
1284
+ },
1285
+ "python.pyrefly.disabledLanguageServices": {
1286
+ "type": "object",
1287
+ "default": {},
1288
+ "description": "Disable specific language services. Set individual services to true to disable them.",
1289
+ "scope": "resource",
1290
+ "properties": {
1291
+ "hover": {
1292
+ "type": "boolean",
1293
+ "default": false
1294
+ },
1295
+ "documentSymbol": {
1296
+ "type": "boolean",
1297
+ "default": false
1298
+ },
1299
+ "workspaceSymbol": {
1300
+ "type": "boolean",
1301
+ "default": false
1302
+ },
1303
+ "inlayHint": {
1304
+ "type": "boolean",
1305
+ "default": false
1306
+ },
1307
+ "completion": {
1308
+ "type": "boolean",
1309
+ "default": false
1310
+ },
1311
+ "codeAction": {
1312
+ "type": "boolean",
1313
+ "default": false
1314
+ },
1315
+ "definition": {
1316
+ "type": "boolean",
1317
+ "default": false
1318
+ },
1319
+ "declaration": {
1320
+ "type": "boolean",
1321
+ "default": false
1322
+ },
1323
+ "typeDefinition": {
1324
+ "type": "boolean",
1325
+ "default": false
1326
+ },
1327
+ "references": {
1328
+ "type": "boolean",
1329
+ "default": false
1330
+ },
1331
+ "documentHighlight": {
1332
+ "type": "boolean",
1333
+ "default": false
1334
+ },
1335
+ "rename": {
1336
+ "type": "boolean",
1337
+ "default": false
1338
+ },
1339
+ "codeLens": {
1340
+ "type": "boolean",
1341
+ "default": false
1342
+ },
1343
+ "semanticTokens": {
1344
+ "type": "boolean",
1345
+ "default": false
1346
+ },
1347
+ "signatureHelp": {
1348
+ "type": "boolean",
1349
+ "default": false
1350
+ },
1351
+ "implementation": {
1352
+ "type": "boolean",
1353
+ "default": false
1354
+ },
1355
+ "callHierarchy": {
1356
+ "type": "boolean",
1357
+ "default": false,
1358
+ "description": "Disable call hierarchy feature (Show Incoming/Outgoing Calls)"
1359
+ }
1360
+ }
1361
+ },
1362
+ "python.analysis.showHoverGoToLinks": {
1363
+ "type": "boolean",
1364
+ "default": true,
1365
+ "description": "Controls whether hover tooltips include 'Go to definition' and 'Go to type definition' navigation links.",
1366
+ "scope": "resource"
1367
+ },
1368
+ "python.analysis.completeFunctionParens": {
1369
+ "type": "boolean",
1370
+ "default": false,
1371
+ "description": "Automatically insert parentheses when completing a function or method.",
1372
+ "scope": "resource"
1373
+ },
1374
+ "python.pyrefly.syncNotebooks": {
1375
+ "type": "boolean",
1376
+ "default": true,
1377
+ "description": "If true, Pyrefly will sync notebook documents with the language server. Set to false to disable notebook support."
1378
+ },
1379
+ "python.pyrefly.runnableCodeLens": {
1380
+ "type": "boolean",
1381
+ "default": false,
1382
+ "description": "Enable Pyrefly's Run/Test CodeLens actions for Python files.",
1383
+ "scope": "resource"
1384
+ },
1385
+ "python.pyrefly.streamDiagnostics": {
1386
+ "type": "boolean",
1387
+ "default": true,
1388
+ "description": "If true (default), Pyrefly will stream diagnostics as they become available during recheck, providing incremental feedback. Set to false to only publish diagnostics after the full recheck completes.",
1389
+ "scope": "resource"
1390
+ },
1391
+ "python.pyrefly.diagnosticMode": {
1392
+ "type": "string",
1393
+ "default": "openFilesOnly",
1394
+ "description": "Controls the scope of Pyrefly's diagnostic analysis. When set to 'openFilesOnly', diagnostics are only provided for files that are currently open in the editor. When set to 'workspace', diagnostics are computed and published for all files in the workspace.",
1395
+ "enum": [
1396
+ "openFilesOnly",
1397
+ "workspace"
1398
+ ],
1399
+ "scope": "resource"
1400
+ },
1401
+ "pyrefly.commentFoldingRanges": {
1402
+ "type": "boolean",
1403
+ "default": false,
1404
+ "description": "Controls whether comment section folding ranges are included in the editor. When true, comments following the pattern '# Section Name ----' (with 4+ trailing dashes) create collapsible regions, similar to R's code section convention."
1405
+ },
1406
+ "python.pyrefly.configPath": {
1407
+ "type": "string",
1408
+ "default": "",
1409
+ "description": "Path to a pyrefly.toml or pyproject.toml configuration file. When set, the LSP will use this config for all files in your workspace instead of the default Pyrefly config-finding logic. Prefer to use default logic wherever possible.",
1410
+ "scope": "resource"
1411
+ }
1412
+ }
1413
+ }
1414
+ },
1415
+ "scripts": {
1416
+ "compile": "npm run check-types && node esbuild.js",
1417
+ "check-types": "tsc --noEmit",
1418
+ "watch": "npm-run-all -p watch:*",
1419
+ "watch:esbuild": "node esbuild.js --watch",
1420
+ "watch:tsc": "tsc --noEmit --watch --project tsconfig.json",
1421
+ "vscode:prepublish": "npm run package",
1422
+ "package": "npm run check-types && node esbuild.js --production",
1423
+ "test": "vscode-test"
1424
+ },
1425
+ "devDependencies": {
1426
+ "@types/mocha": "^10.0.10",
1427
+ "@types/node": "^16.11.7",
1428
+ "@types/vscode": "^1.78.1",
1429
+ "@vscode/test-cli": "^0.0.10",
1430
+ "@vscode/test-electron": "^2.5.2",
1431
+ "@vscode/vsce": "^2.9.2",
1432
+ "esbuild": "^0.25.1",
1433
+ "npm-run-all": "^4.1.5",
1434
+ "typescript": "^4.4.3"
1435
+ },
1436
+ "dependencies": {
1437
+ "@vscode/python-extension": "^1.0.5",
1438
+ "serialize-javascript": "^7.0.5",
1439
+ "underscore": "^1.13.8",
1440
+ "vsce": "^2.15.0",
1441
+ "vscode-languageclient": "9.0.1"
1442
+ },
1443
+ "extensionDependencies": [
1444
+ "ms-python.python"
1445
+ ]
1446
+ }
1447
+ `);
1448
+ assert.ok(result);
1449
+ assert.strictEqual(result.publisher, "meta");
1450
+ assert.strictEqual(result.name, "pyrefly");
1451
+ assert.strictEqual(result.version, "0.61.0");
1452
+ assert.strictEqual(result.displayName, "Pyrefly - Python Language Tooling");
1453
+ // Should capture repository as external reference
1454
+ assert.ok(result.externalReferences);
1455
+ assert.deepStrictEqual(result.externalReferences, [
1456
+ { type: "vcs", url: "https://github.com/facebook/pyrefly" },
1457
+ ]);
1458
+ // Capabilities
1459
+ assert.ok(result.capabilities);
1460
+ assert.deepStrictEqual(result.capabilities.extensionKind, ["workspace"]);
1461
+ assert.deepStrictEqual(result.capabilities.activationEvents, [
1462
+ "onLanguage:python",
1463
+ "onNotebook:jupyter-notebook",
1464
+ ]);
1465
+ assert.deepStrictEqual(result.capabilities.extensionDependencies, [
1466
+ "ms-python.python",
1467
+ ]);
1468
+ assert.deepStrictEqual(result.capabilities.untrustedWorkspaces, {
1469
+ supported: false,
1470
+ description:
1471
+ "Pyrefly can be configured to execute binaries. A malicious actor could exploit this to run arbitrary code on your machine.",
1472
+ });
1473
+ assert.strictEqual(result.capabilities.main, "./dist/extension");
1474
+ assert.ok(result.capabilities.contributes.includes("commands:count:5"));
1475
+ assert.ok(
1476
+ result.capabilities.lifecycleScripts.includes("vscode:prepublish"),
1477
+ );
1478
+ // Dependencies
1479
+ assert.ok(result.dependencies);
1480
+ assert.strictEqual(
1481
+ result.dependencies["@vscode/python-extension"],
1482
+ "^1.0.5",
1483
+ );
1484
+ assert.strictEqual(result.dependencies["vscode-languageclient"], "9.0.1");
1485
+ assert.ok(result.devDependencies);
1486
+ assert.strictEqual(result.devDependencies["typescript"], "^4.4.3");
1487
+ assert.strictEqual(result.devDependencies["esbuild"], "^0.25.1");
1488
+ });
1489
+ });
1490
+
1491
+ describe("parseExtensionDependencies", () => {
1492
+ it("should return empty arrays for package with no dependencies", () => {
1493
+ const pkg = { name: "simple-ext" };
1494
+ const result = parseExtensionDependencies(
1495
+ pkg,
1496
+ "pkg:vscode-extension/pub/simple-ext@1.0.0",
1497
+ );
1498
+ assert.deepStrictEqual(result.components, []);
1499
+ assert.deepStrictEqual(result.dependencies, []);
1500
+ });
1501
+
1502
+ it("should parse dependencies with required scope", () => {
1503
+ const pkg = {
1504
+ name: "test-ext",
1505
+ dependencies: {
1506
+ lodash: "^4.17.21",
1507
+ axios: "1.6.0",
1508
+ },
1509
+ };
1510
+ const result = parseExtensionDependencies(
1511
+ pkg,
1512
+ "pkg:vscode-extension/pub/test-ext@1.0.0",
1513
+ );
1514
+ assert.strictEqual(result.components.length, 2);
1515
+ const lodashComp = result.components.find((c) => c.name === "lodash");
1516
+ assert.ok(lodashComp);
1517
+ assert.strictEqual(lodashComp.scope, "required");
1518
+ assert.strictEqual(lodashComp.versionRange, "vers:npm/>=4.17.21|<5.0.0");
1519
+ assert.strictEqual(lodashComp.type, "library");
1520
+ assert.ok(lodashComp.purl.includes("pkg:npm/lodash"));
1521
+ const axiosComp = result.components.find((c) => c.name === "axios");
1522
+ assert.ok(axiosComp);
1523
+ assert.strictEqual(axiosComp.versionRange, "vers:npm/1.6.0");
1524
+ // Should create dependency tree entry
1525
+ assert.strictEqual(result.dependencies.length, 1);
1526
+ assert.strictEqual(
1527
+ result.dependencies[0].ref,
1528
+ "pkg:vscode-extension/pub/test-ext@1.0.0",
1529
+ );
1530
+ assert.ok(result.dependencies[0].dependsOn.length === 2);
1531
+ });
1532
+
1533
+ it("should parse devDependencies with optional scope", () => {
1534
+ const pkg = {
1535
+ name: "test-ext",
1536
+ devDependencies: {
1537
+ typescript: "^5.0.0",
1538
+ esbuild: "^0.19.0",
1539
+ },
1540
+ };
1541
+ const result = parseExtensionDependencies(
1542
+ pkg,
1543
+ "pkg:vscode-extension/pub/test-ext@1.0.0",
1544
+ );
1545
+ assert.strictEqual(result.components.length, 2);
1546
+ for (const comp of result.components) {
1547
+ assert.strictEqual(comp.scope, "optional");
1548
+ }
1549
+ });
1550
+
1551
+ it("should parse scoped npm packages correctly", () => {
1552
+ const pkg = {
1553
+ name: "test-ext",
1554
+ dependencies: {
1555
+ "@vscode/python-extension": "^1.0.5",
1556
+ "@types/node": "^20.0.0",
1557
+ },
1558
+ };
1559
+ const result = parseExtensionDependencies(
1560
+ pkg,
1561
+ "pkg:vscode-extension/pub/test-ext@1.0.0",
1562
+ );
1563
+ assert.strictEqual(result.components.length, 2);
1564
+ const vscodePy = result.components.find(
1565
+ (c) => c.name === "python-extension",
1566
+ );
1567
+ assert.ok(vscodePy);
1568
+ assert.strictEqual(vscodePy.group, "@vscode");
1569
+ assert.ok(vscodePy.purl.includes("pkg:npm/%40vscode/python-extension"));
1570
+ const typesNode = result.components.find((c) => c.name === "node");
1571
+ assert.ok(typesNode);
1572
+ assert.strictEqual(typesNode.group, "@types");
1573
+ });
1574
+
1575
+ it("should handle peerDependencies and optionalDependencies", () => {
1576
+ const pkg = {
1577
+ name: "test-ext",
1578
+ peerDependencies: {
1579
+ react: "^18.0.0",
1580
+ },
1581
+ optionalDependencies: {
1582
+ fsevents: "^2.3.0",
1583
+ },
1584
+ };
1585
+ const result = parseExtensionDependencies(
1586
+ pkg,
1587
+ "pkg:vscode-extension/pub/test-ext@1.0.0",
1588
+ );
1589
+ assert.strictEqual(result.components.length, 2);
1590
+ for (const comp of result.components) {
1591
+ assert.strictEqual(comp.scope, "optional");
1592
+ }
1593
+ });
1594
+
1595
+ it("should deduplicate packages across dependency groups", () => {
1596
+ const pkg = {
1597
+ name: "test-ext",
1598
+ dependencies: {
1599
+ lodash: "^4.17.21",
1600
+ },
1601
+ devDependencies: {
1602
+ lodash: "^4.17.21",
1603
+ },
1604
+ };
1605
+ const result = parseExtensionDependencies(
1606
+ pkg,
1607
+ "pkg:vscode-extension/pub/test-ext@1.0.0",
1608
+ );
1609
+ assert.strictEqual(
1610
+ result.components.length,
1611
+ 1,
1612
+ "Should deduplicate lodash",
1613
+ );
1614
+ });
1615
+
1616
+ it("should skip workspace:* and latest version ranges", () => {
1617
+ const pkg = {
1618
+ name: "test-ext",
1619
+ dependencies: {
1620
+ "real-dep": "^1.0.0",
1621
+ },
1622
+ devDependencies: {
1623
+ "workspace-dep": "workspace:*",
1624
+ "latest-dep": "latest",
1625
+ },
1626
+ };
1627
+ const result = parseExtensionDependencies(
1628
+ pkg,
1629
+ "pkg:vscode-extension/pub/test-ext@1.0.0",
1630
+ );
1631
+ // All three should be created as components
1632
+ assert.strictEqual(result.components.length, 3);
1633
+ // But workspace and latest should have no versionRange
1634
+ const wsDep = result.components.find((c) => c.name === "workspace-dep");
1635
+ assert.ok(wsDep);
1636
+ assert.strictEqual(wsDep.versionRange, undefined);
1637
+ const latestDep = result.components.find((c) => c.name === "latest-dep");
1638
+ assert.ok(latestDep);
1639
+ assert.strictEqual(latestDep.versionRange, undefined);
1640
+ const realDep = result.components.find((c) => c.name === "real-dep");
1641
+ assert.ok(realDep);
1642
+ assert.strictEqual(realDep.versionRange, "vers:npm/>=1.0.0|<2.0.0");
1643
+ });
1644
+
1645
+ it("should handle real-world Pyrefly dependencies", () => {
1646
+ const pkg = {
1647
+ name: "pyrefly",
1648
+ publisher: "meta",
1649
+ version: "0.61.0",
1650
+ dependencies: {
1651
+ "@vscode/python-extension": "^1.0.5",
1652
+ "serialize-javascript": "^7.0.5",
1653
+ underscore: "^1.13.8",
1654
+ vsce: "^2.15.0",
1655
+ "vscode-languageclient": "9.0.1",
1656
+ },
1657
+ devDependencies: {
1658
+ "@types/mocha": "^10.0.10",
1659
+ "@types/node": "^16.11.7",
1660
+ "@types/vscode": "^1.78.1",
1661
+ "@vscode/test-cli": "^0.0.10",
1662
+ "@vscode/test-electron": "^2.5.2",
1663
+ "@vscode/vsce": "^2.9.2",
1664
+ esbuild: "^0.25.1",
1665
+ "npm-run-all": "^4.1.5",
1666
+ typescript: "^4.4.3",
1667
+ },
1668
+ };
1669
+ const result = parseExtensionDependencies(
1670
+ pkg,
1671
+ "pkg:vscode-extension/meta/pyrefly@0.61.0",
1672
+ );
1673
+ // 5 deps + 9 devDeps = 14 total (no overlap)
1674
+ assert.strictEqual(result.components.length, 14);
1675
+ // Verify dependency tree
1676
+ assert.strictEqual(result.dependencies.length, 1);
1677
+ assert.strictEqual(
1678
+ result.dependencies[0].ref,
1679
+ "pkg:vscode-extension/meta/pyrefly@0.61.0",
1680
+ );
1681
+ assert.strictEqual(result.dependencies[0].dependsOn.length, 14);
1682
+ // Check scopes
1683
+ const requiredComps = result.components.filter(
1684
+ (c) => c.scope === "required",
1685
+ );
1686
+ const optionalComps = result.components.filter(
1687
+ (c) => c.scope === "optional",
1688
+ );
1689
+ assert.strictEqual(requiredComps.length, 5);
1690
+ assert.strictEqual(optionalComps.length, 9);
1691
+ // Check specific component
1692
+ const vscLangClient = result.components.find(
1693
+ (c) => c.name === "vscode-languageclient",
1694
+ );
1695
+ assert.ok(vscLangClient);
1696
+ assert.strictEqual(vscLangClient.scope, "required");
1697
+ assert.strictEqual(vscLangClient.versionRange, "vers:npm/9.0.1");
1698
+ });
1699
+ });
1700
+
1701
+ describe("toComponent", () => {
1702
+ it("should return undefined for undefined input", () => {
1703
+ assert.strictEqual(toComponent(undefined), undefined);
1704
+ assert.strictEqual(toComponent(null), undefined);
1705
+ assert.strictEqual(toComponent({}), undefined);
1706
+ });
1707
+
1708
+ it("should create a component with publisher as namespace", () => {
1709
+ const extInfo = {
1710
+ publisher: "ms-python",
1711
+ name: "python",
1712
+ version: "2023.25.0",
1713
+ displayName: "Python",
1714
+ description: "Python language support",
1715
+ platform: "",
1716
+ };
1717
+ const component = toComponent(extInfo);
1718
+ assert.ok(component);
1719
+ assert.strictEqual(component.name, "python");
1720
+ assert.strictEqual(component.group, "ms-python");
1721
+ assert.strictEqual(component.version, "2023.25.0");
1722
+ assert.ok(
1723
+ component.purl.startsWith(
1724
+ "pkg:vscode-extension/ms-python/python@2023.25.0",
1725
+ ),
1726
+ );
1727
+ assert.strictEqual(component.type, "application");
1728
+ });
1729
+
1730
+ it("should include platform qualifier when present", () => {
1731
+ const extInfo = {
1732
+ publisher: "golang",
1733
+ name: "go",
1734
+ version: "0.39.1",
1735
+ displayName: "Go",
1736
+ description: "",
1737
+ platform: "win32-x64",
1738
+ };
1739
+ const component = toComponent(extInfo);
1740
+ assert.ok(component);
1741
+ assert.ok(component.purl.includes("platform=win32-x64"));
1742
+ });
1743
+
1744
+ it("should include IDE name in properties", () => {
1745
+ const extInfo = {
1746
+ publisher: "ms-python",
1747
+ name: "python",
1748
+ version: "1.0.0",
1749
+ displayName: "",
1750
+ description: "",
1751
+ platform: "",
1752
+ };
1753
+ const component = toComponent(extInfo, "Cursor");
1754
+ assert.ok(component);
1755
+ assert.ok(
1756
+ component.properties?.some(
1757
+ (p) => p.name === "cdx:vscode-extension:ide" && p.value === "Cursor",
1758
+ ),
1759
+ );
1760
+ });
1761
+
1762
+ it("should include srcPath in properties", () => {
1763
+ const extInfo = {
1764
+ publisher: "test",
1765
+ name: "myext",
1766
+ version: "1.0.0",
1767
+ displayName: "",
1768
+ description: "",
1769
+ platform: "",
1770
+ srcPath: "/some/path",
1771
+ };
1772
+ const component = toComponent(extInfo);
1773
+ assert.ok(component);
1774
+ assert.ok(
1775
+ component.properties?.some(
1776
+ (p) => p.name === "SrcFile" && p.value === "/some/path",
1777
+ ),
1778
+ );
1779
+ });
1780
+
1781
+ it("should include evidence field", () => {
1782
+ const extInfo = {
1783
+ publisher: "test",
1784
+ name: "myext",
1785
+ version: "1.0.0",
1786
+ displayName: "",
1787
+ description: "",
1788
+ platform: "",
1789
+ };
1790
+ const component = toComponent(extInfo);
1791
+ assert.ok(component);
1792
+ assert.ok(component.evidence);
1793
+ assert.ok(component.evidence.identity);
1794
+ assert.strictEqual(component.evidence.identity.field, "purl");
1795
+ });
1796
+
1797
+ it("should handle extension with no publisher", () => {
1798
+ const extInfo = {
1799
+ publisher: "",
1800
+ name: "standalone-ext",
1801
+ version: "1.0.0",
1802
+ displayName: "",
1803
+ description: "",
1804
+ platform: "",
1805
+ };
1806
+ const component = toComponent(extInfo);
1807
+ assert.ok(component);
1808
+ assert.ok(
1809
+ component.purl.includes("pkg:vscode-extension/standalone-ext@1.0.0"),
1810
+ );
1811
+ });
1812
+
1813
+ it("should include capability properties", () => {
1814
+ const extInfo = {
1815
+ publisher: "ms-python",
1816
+ name: "python",
1817
+ version: "1.0.0",
1818
+ displayName: "Python",
1819
+ description: "",
1820
+ platform: "",
1821
+ capabilities: {
1822
+ activationEvents: ["onLanguage:python", "*"],
1823
+ extensionKind: ["workspace"],
1824
+ extensionDependencies: ["ms-python.vscode-pylance"],
1825
+ contributes: ["commands:5", "debuggers:1", "terminal-access"],
1826
+ main: "./dist/extension.js",
1827
+ lifecycleScripts: ["postinstall", "vscode:prepublish"],
1828
+ untrustedWorkspaces: { supported: "limited" },
1829
+ virtualWorkspaces: false,
1830
+ },
1831
+ };
1832
+ const component = toComponent(extInfo);
1833
+ assert.ok(component);
1834
+ const props = component.properties;
1835
+ assert.ok(props);
1836
+
1837
+ // Check activation events
1838
+ const activationProp = props.find(
1839
+ (p) => p.name === "cdx:vscode-extension:activationEvents",
1840
+ );
1841
+ assert.ok(activationProp);
1842
+ assert.ok(activationProp.value.includes("onLanguage:python"));
1843
+ assert.ok(activationProp.value.includes("*"));
1844
+
1845
+ // Check extension kind
1846
+ const kindProp = props.find(
1847
+ (p) => p.name === "cdx:vscode-extension:extensionKind",
1848
+ );
1849
+ assert.ok(kindProp);
1850
+ assert.strictEqual(kindProp.value, "workspace");
1851
+
1852
+ // Check extension dependencies
1853
+ const depProp = props.find(
1854
+ (p) => p.name === "cdx:vscode-extension:extensionDependencies",
1855
+ );
1856
+ assert.ok(depProp);
1857
+ assert.ok(depProp.value.includes("ms-python.vscode-pylance"));
1858
+
1859
+ // Check contributes
1860
+ const contributesProp = props.find(
1861
+ (p) => p.name === "cdx:vscode-extension:contributes",
1862
+ );
1863
+ assert.ok(contributesProp);
1864
+ assert.ok(contributesProp.value.includes("commands:5"));
1865
+ assert.ok(contributesProp.value.includes("terminal-access"));
1866
+
1867
+ // Check main entry point
1868
+ const mainProp = props.find((p) => p.name === "cdx:vscode-extension:main");
1869
+ assert.ok(mainProp);
1870
+ assert.strictEqual(mainProp.value, "./dist/extension.js");
1871
+
1872
+ // Check lifecycle scripts
1873
+ const scriptsProp = props.find(
1874
+ (p) => p.name === "cdx:vscode-extension:lifecycleScripts",
1875
+ );
1876
+ assert.ok(scriptsProp);
1877
+ assert.ok(scriptsProp.value.includes("postinstall"));
1878
+ assert.ok(scriptsProp.value.includes("vscode:prepublish"));
1879
+
1880
+ // Check untrusted workspaces
1881
+ const trustProp = props.find(
1882
+ (p) => p.name === "cdx:vscode-extension:untrustedWorkspaces",
1883
+ );
1884
+ assert.ok(trustProp);
1885
+ assert.strictEqual(trustProp.value, "limited");
1886
+
1887
+ // Check virtual workspaces
1888
+ const vwsProp = props.find(
1889
+ (p) => p.name === "cdx:vscode-extension:virtualWorkspaces",
1890
+ );
1891
+ assert.ok(vwsProp);
1892
+ assert.strictEqual(vwsProp.value, "false");
1893
+ });
1894
+
1895
+ it("should include manifest Properties fields", () => {
1896
+ const extInfo = {
1897
+ publisher: "meta",
1898
+ name: "pyrefly",
1899
+ version: "0.61.0",
1900
+ displayName: "Pyrefly",
1901
+ description: "Python tooling",
1902
+ platform: "win32-x64",
1903
+ executesCode: true,
1904
+ vscodeEngine: "^1.94.0",
1905
+ extensionDependencies: ["ms-python.python"],
1906
+ extensionKind: ["workspace"],
1907
+ links: {
1908
+ Source: "https://github.com/facebook/pyrefly.git",
1909
+ GitHub: "https://github.com/facebook/pyrefly.git",
1910
+ Support: "https://github.com/facebook/pyrefly/issues",
1911
+ Learn: "https://github.com/facebook/pyrefly#readme",
1912
+ Getstarted: "https://github.com/facebook/pyrefly.git",
1913
+ },
1914
+ };
1915
+ const component = toComponent(extInfo);
1916
+ assert.ok(component);
1917
+ const props = component.properties;
1918
+ assert.ok(props);
1919
+
1920
+ // Check executesCode
1921
+ const execProp = props.find(
1922
+ (p) => p.name === "cdx:vscode-extension:executesCode",
1923
+ );
1924
+ assert.ok(execProp);
1925
+ assert.strictEqual(execProp.value, "true");
1926
+
1927
+ // Check vscodeEngine
1928
+ const engineProp = props.find(
1929
+ (p) => p.name === "cdx:vscode-extension:vscodeEngine",
1930
+ );
1931
+ assert.ok(engineProp);
1932
+ assert.strictEqual(engineProp.value, "^1.94.0");
1933
+
1934
+ // Check extensionDependencies from manifest
1935
+ const depProp = props.find(
1936
+ (p) => p.name === "cdx:vscode-extension:extensionDependencies",
1937
+ );
1938
+ assert.ok(depProp);
1939
+ assert.strictEqual(depProp.value, "ms-python.python");
1940
+
1941
+ // Check extensionKind from manifest
1942
+ const kindProp = props.find(
1943
+ (p) => p.name === "cdx:vscode-extension:extensionKind",
1944
+ );
1945
+ assert.ok(kindProp);
1946
+ assert.strictEqual(kindProp.value, "workspace");
1947
+
1948
+ // Check externalReferences from links
1949
+ assert.ok(component.externalReferences);
1950
+ assert.ok(
1951
+ component.externalReferences.some(
1952
+ (r) =>
1953
+ r.type === "vcs" &&
1954
+ r.url === "https://github.com/facebook/pyrefly.git",
1955
+ ),
1956
+ );
1957
+ assert.ok(
1958
+ component.externalReferences.some(
1959
+ (r) =>
1960
+ r.type === "issue-tracker" &&
1961
+ r.url === "https://github.com/facebook/pyrefly/issues",
1962
+ ),
1963
+ );
1964
+ assert.ok(
1965
+ component.externalReferences.some(
1966
+ (r) =>
1967
+ r.type === "documentation" &&
1968
+ r.url === "https://github.com/facebook/pyrefly#readme",
1969
+ ),
1970
+ );
1971
+ assert.ok(
1972
+ component.externalReferences.some(
1973
+ (r) =>
1974
+ r.type === "website" &&
1975
+ r.url === "https://github.com/facebook/pyrefly.git",
1976
+ ),
1977
+ );
1978
+ });
1979
+ });
1980
+
1981
+ describe("parseExtensionDirName", () => {
1982
+ it("should parse publisher.name-version pattern", () => {
1983
+ const component = parseExtensionDirName(
1984
+ "/home/user/.vscode/extensions/ms-python.python-2023.25.0",
1985
+ );
1986
+ assert.ok(component);
1987
+ assert.strictEqual(component.group, "ms-python");
1988
+ assert.strictEqual(component.name, "python");
1989
+ assert.strictEqual(component.version, "2023.25.0");
1990
+ });
1991
+
1992
+ it("should parse complex extension names", () => {
1993
+ const component = parseExtensionDirName(
1994
+ "/home/user/.vscode/extensions/redhat.vscode-xml-0.27.1",
1995
+ );
1996
+ assert.ok(component);
1997
+ assert.strictEqual(component.group, "redhat");
1998
+ assert.strictEqual(component.name, "vscode-xml");
1999
+ assert.strictEqual(component.version, "0.27.1");
2000
+ });
2001
+
2002
+ it("should return undefined for non-matching names", () => {
2003
+ assert.strictEqual(parseExtensionDirName("/some/random/path"), undefined);
2004
+ assert.strictEqual(parseExtensionDirName(""), undefined);
2005
+ });
2006
+
2007
+ it("should handle Windows paths", () => {
2008
+ const component = parseExtensionDirName(
2009
+ "C:\\Users\\test\\.vscode\\extensions\\golang.go-0.39.1",
2010
+ );
2011
+ assert.ok(component);
2012
+ assert.strictEqual(component.group, "golang");
2013
+ assert.strictEqual(component.name, "go");
2014
+ assert.strictEqual(component.version, "0.39.1");
2015
+ });
2016
+ });
2017
+
2018
+ describe("parseInstalledExtensionDir", () => {
2019
+ const testDir = join(baseTempDir, "test-installed");
2020
+
2021
+ it("should parse extension dir with package.json", () => {
2022
+ const extDir = join(testDir, "ms-python.python-2023.25.0");
2023
+ mkdirSync(extDir, { recursive: true });
2024
+ writeFileSync(
2025
+ join(extDir, "package.json"),
2026
+ JSON.stringify({
2027
+ name: "python",
2028
+ publisher: "ms-python",
2029
+ version: "2023.25.0",
2030
+ displayName: "Python",
2031
+ description: "Python language support",
2032
+ }),
2033
+ );
2034
+ const component = parseInstalledExtensionDir(extDir, "VS Code");
2035
+ assert.ok(component);
2036
+ assert.strictEqual(component.name, "python");
2037
+ assert.strictEqual(component.group, "ms-python");
2038
+ assert.strictEqual(component.version, "2023.25.0");
2039
+ assert.ok(
2040
+ component.purl.startsWith(
2041
+ "pkg:vscode-extension/ms-python/python@2023.25.0",
2042
+ ),
2043
+ );
2044
+ assert.ok(
2045
+ component.properties?.some(
2046
+ (p) => p.name === "cdx:vscode-extension:ide" && p.value === "VS Code",
2047
+ ),
2048
+ );
2049
+ });
2050
+
2051
+ it("should parse extension dir with package.json and extract capabilities", () => {
2052
+ const extDir = join(testDir, "ms-python.python-cap-2023.25.0");
2053
+ mkdirSync(extDir, { recursive: true });
2054
+ writeFileSync(
2055
+ join(extDir, "package.json"),
2056
+ JSON.stringify({
2057
+ name: "python",
2058
+ publisher: "ms-python",
2059
+ version: "2023.25.0",
2060
+ displayName: "Python",
2061
+ main: "./dist/extension.js",
2062
+ activationEvents: ["onLanguage:python"],
2063
+ contributes: {
2064
+ commands: [{ command: "python.run", title: "Run" }],
2065
+ debuggers: [{ type: "python", label: "Python" }],
2066
+ },
2067
+ extensionDependencies: ["ms-python.vscode-pylance"],
2068
+ }),
2069
+ );
2070
+ const component = parseInstalledExtensionDir(extDir, "VS Code");
2071
+ assert.ok(component);
2072
+ // Should have capability properties
2073
+ assert.ok(
2074
+ component.properties?.some(
2075
+ (p) => p.name === "cdx:vscode-extension:activationEvents",
2076
+ ),
2077
+ "Should extract activationEvents",
2078
+ );
2079
+ assert.ok(
2080
+ component.properties?.some((p) => p.name === "cdx:vscode-extension:main"),
2081
+ "Should extract main entry point",
2082
+ );
2083
+ assert.ok(
2084
+ component.properties?.some(
2085
+ (p) => p.name === "cdx:vscode-extension:contributes",
2086
+ ),
2087
+ "Should extract contributed features",
2088
+ );
2089
+ assert.ok(
2090
+ component.properties?.some(
2091
+ (p) => p.name === "cdx:vscode-extension:extensionDependencies",
2092
+ ),
2093
+ "Should extract extension dependencies",
2094
+ );
2095
+ });
2096
+
2097
+ it("should parse extension dir with .vsixmanifest", () => {
2098
+ const extDir = join(testDir, "golang.go-0.39.1");
2099
+ mkdirSync(extDir, { recursive: true });
2100
+ writeFileSync(
2101
+ join(extDir, ".vsixmanifest"),
2102
+ `<?xml version="1.0" encoding="utf-8"?>
2103
+ <PackageManifest Version="2.0.0" xmlns="http://schemas.microsoft.com/developer/vsx-schema/2011">
2104
+ <Metadata>
2105
+ <Identity Id="go" Version="0.39.1" Publisher="golang" />
2106
+ <DisplayName>Go</DisplayName>
2107
+ <Description>Go language support</Description>
2108
+ </Metadata>
2109
+ </PackageManifest>`,
2110
+ );
2111
+ const component = parseInstalledExtensionDir(extDir, "VS Code");
2112
+ assert.ok(component);
2113
+ assert.strictEqual(component.name, "go");
2114
+ assert.strictEqual(component.group, "golang");
2115
+ assert.strictEqual(component.version, "0.39.1");
2116
+ });
2117
+
2118
+ it("should fall back to directory name parsing", () => {
2119
+ const extDir = join(testDir, "redhat.vscode-yaml-1.14.0");
2120
+ mkdirSync(extDir, { recursive: true });
2121
+ // No package.json or .vsixmanifest
2122
+ const component = parseInstalledExtensionDir(extDir);
2123
+ assert.ok(component);
2124
+ assert.strictEqual(component.group, "redhat");
2125
+ assert.strictEqual(component.name, "vscode-yaml");
2126
+ assert.strictEqual(component.version, "1.14.0");
2127
+ });
2128
+ });
2129
+
2130
+ describe("collectInstalledExtensions", () => {
2131
+ const testDir = join(baseTempDir, "test-collect");
2132
+ const extDir = join(testDir, "extensions");
2133
+
2134
+ it("should collect extensions from an extensions directory", () => {
2135
+ // Create mock extension dirs
2136
+ const ext1 = join(extDir, "ms-python.python-2023.25.0");
2137
+ const ext2 = join(extDir, "golang.go-0.39.1");
2138
+ mkdirSync(ext1, { recursive: true });
2139
+ mkdirSync(ext2, { recursive: true });
2140
+ writeFileSync(
2141
+ join(ext1, "package.json"),
2142
+ JSON.stringify({
2143
+ name: "python",
2144
+ publisher: "ms-python",
2145
+ version: "2023.25.0",
2146
+ }),
2147
+ );
2148
+ writeFileSync(
2149
+ join(ext2, "package.json"),
2150
+ JSON.stringify({
2151
+ name: "go",
2152
+ publisher: "golang",
2153
+ version: "0.39.1",
2154
+ }),
2155
+ );
2156
+
2157
+ const components = collectInstalledExtensions([
2158
+ { name: "VS Code", dir: extDir },
2159
+ ]);
2160
+ assert.ok(Array.isArray(components));
2161
+ assert.strictEqual(components.length, 2);
2162
+ const names = components.map((c) => c.name);
2163
+ assert.ok(names.includes("python"));
2164
+ assert.ok(names.includes("go"));
2165
+ });
2166
+
2167
+ it("should skip hidden directories", () => {
2168
+ const hiddenDir = join(extDir, ".obsolete");
2169
+ mkdirSync(hiddenDir, { recursive: true });
2170
+ writeFileSync(
2171
+ join(hiddenDir, "package.json"),
2172
+ JSON.stringify({
2173
+ name: "old-ext",
2174
+ publisher: "test",
2175
+ version: "1.0.0",
2176
+ }),
2177
+ );
2178
+
2179
+ const components = collectInstalledExtensions([
2180
+ { name: "VS Code", dir: extDir },
2181
+ ]);
2182
+ const names = components.map((c) => c.name);
2183
+ assert.ok(!names.includes("old-ext"), "Should not include hidden dirs");
2184
+ });
2185
+
2186
+ it("should deduplicate by purl", () => {
2187
+ // Same extension in two different IDE dirs
2188
+ const ideDir1 = join(testDir, "ide1-ext");
2189
+ const ideDir2 = join(testDir, "ide2-ext");
2190
+ const ext1 = join(ideDir1, "ms-python.python-2023.25.0");
2191
+ const ext2 = join(ideDir2, "ms-python.python-2023.25.0");
2192
+ mkdirSync(ext1, { recursive: true });
2193
+ mkdirSync(ext2, { recursive: true });
2194
+ const pkgJson = JSON.stringify({
2195
+ name: "python",
2196
+ publisher: "ms-python",
2197
+ version: "2023.25.0",
2198
+ });
2199
+ writeFileSync(join(ext1, "package.json"), pkgJson);
2200
+ writeFileSync(join(ext2, "package.json"), pkgJson);
2201
+
2202
+ const components = collectInstalledExtensions([
2203
+ { name: "IDE1", dir: ideDir1 },
2204
+ { name: "IDE2", dir: ideDir2 },
2205
+ ]);
2206
+ const pythonComponents = components.filter((c) => c.name === "python");
2207
+ assert.strictEqual(
2208
+ pythonComponents.length,
2209
+ 1,
2210
+ "Should deduplicate by purl",
2211
+ );
2212
+ });
2213
+
2214
+ it("should handle non-existent directory gracefully", () => {
2215
+ const components = collectInstalledExtensions([
2216
+ { name: "Nonexistent", dir: "/nonexistent/path/that/does/not/exist" },
2217
+ ]);
2218
+ assert.ok(Array.isArray(components));
2219
+ assert.strictEqual(components.length, 0);
2220
+ });
2221
+ });
2222
+
2223
+ describe("cleanupTempDir", () => {
2224
+ it("should not throw for null/undefined", () => {
2225
+ assert.doesNotThrow(() => cleanupTempDir(null));
2226
+ assert.doesNotThrow(() => cleanupTempDir(undefined));
2227
+ assert.doesNotThrow(() => cleanupTempDir(""));
2228
+ });
2229
+
2230
+ it("should clean up a temp dir with vsix-deps- prefix", () => {
2231
+ const tempDir = join(tmpdir(), "vsix-deps-test-cleanup");
2232
+ mkdirSync(tempDir, { recursive: true });
2233
+ assert.ok(existsSync(tempDir));
2234
+ cleanupTempDir(tempDir);
2235
+ assert.ok(!existsSync(tempDir), "Should have been removed");
2236
+ });
2237
+
2238
+ it("should not remove dirs without vsix-deps- prefix", () => {
2239
+ const tempDir = join(tmpdir(), "some-other-dir-test");
2240
+ mkdirSync(tempDir, { recursive: true });
2241
+ assert.ok(existsSync(tempDir));
2242
+ cleanupTempDir(tempDir);
2243
+ assert.ok(existsSync(tempDir), "Should NOT have been removed");
2244
+ // Manual cleanup
2245
+ rmSync(tempDir, { recursive: true, force: true });
2246
+ });
2247
+ });