UNPKG

@cyclonedx/cdxgen

Version:

Creates CycloneDX Software Bill of Materials (SBOM) from source or container image

1,275 lines (1,220 loc) 85.7 kB
import { strict as assert } from "node:assert"; import { existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync, } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import esmock from "esmock"; import { describe, it } from "poku"; import sinon from "sinon"; import { cleanupTempDir, collectInstalledExtensions, extractExtensionCapabilities, getIdeExtensionDirs, parseExtensionDependencies, parseExtensionDirName, parseInstalledExtensionDir, parseVsixManifest, parseVsixPackageJson, toComponent, VSCODE_EXTENSION_PURL_TYPE, } from "./vsixutils.js"; const baseTempDir = mkdtempSync(join(tmpdir(), "cdxgen-vsix-poku-")); process.on("exit", () => { try { rmSync(baseTempDir, { recursive: true, force: true }); } catch (_e) { // Ignore cleanup errors } }); describe("VSCODE_EXTENSION_PURL_TYPE", () => { it("should be vscode-extension", () => { assert.strictEqual(VSCODE_EXTENSION_PURL_TYPE, "vscode-extension"); }); }); describe("extractVsixToTempDir()", () => { it("returns undefined when dry-run blocks vsix extraction", async () => { const safeExtractArchive = sinon.stub().resolves(false); const zipClose = sinon.stub().resolves(); const { extractVsixToTempDir } = await esmock("./vsixutils.js", { "node-stream-zip": { default: { async: sinon.stub().returns({ close: zipClose, }), }, }, "./utils.js": { DEBUG_MODE: false, getTmpDir: sinon.stub().returns("/tmp"), isMac: false, isWin: false, safeExistsSync: sinon.stub().returns(false), safeExtractArchive, safeMkdtempSync: sinon.stub().returns("/tmp/vsix-deps-test"), safeRmSync: sinon.stub(), }, }); const extractedDir = await extractVsixToTempDir("/tmp/sample.vsix"); assert.strictEqual(extractedDir, undefined); sinon.assert.calledOnce(safeExtractArchive); sinon.assert.calledOnce(zipClose); }); }); describe("getIdeExtensionDirs", () => { it("should return an array of IDE configurations", () => { const ides = getIdeExtensionDirs(); assert.ok(Array.isArray(ides)); assert.ok(ides.length > 0); for (const ide of ides) { assert.ok(ide.name, "Each IDE should have a name"); assert.ok(Array.isArray(ide.dirs), "Each IDE should have dirs array"); assert.ok(ide.dirs.length > 0, "Each IDE should have at least one dir"); } }); it("should include well-known IDEs", () => { const ides = getIdeExtensionDirs(); const names = ides.map((ide) => ide.name); assert.ok(names.includes("VS Code"), "Should include VS Code"); assert.ok( names.includes("VS Code Insiders"), "Should include VS Code Insiders", ); assert.ok(names.includes("VSCodium"), "Should include VSCodium"); assert.ok(names.includes("Cursor"), "Should include Cursor"); assert.ok(names.includes("Windsurf"), "Should include Windsurf"); assert.ok(names.includes("Positron"), "Should include Positron"); assert.ok(names.includes("Theia"), "Should include Theia"); assert.ok(names.includes("code-server"), "Should include code-server"); assert.ok(names.includes("Trae"), "Should include Trae"); assert.ok(names.includes("Augment Code"), "Should include Augment Code"); assert.ok( names.includes("VS Code Remote"), "Should include VS Code Remote", ); assert.ok( names.includes("OpenVSCode Server"), "Should include OpenVSCode Server", ); }); }); describe("parseVsixManifest", () => { it("should return undefined for empty input", () => { assert.strictEqual(parseVsixManifest(""), undefined); assert.strictEqual(parseVsixManifest(null), undefined); assert.strictEqual(parseVsixManifest(undefined), undefined); }); it("should parse a valid vsixmanifest XML", () => { const xml = `<?xml version="1.0" encoding="utf-8"?> <PackageManifest Version="2.0.0" xmlns="http://schemas.microsoft.com/developer/vsx-schema/2011"> <Metadata> <Identity Id="python" Version="2023.25.0" Publisher="ms-python" TargetPlatform="linux-x64" /> <DisplayName>Python</DisplayName> <Description>Python language support</Description> </Metadata> </PackageManifest>`; const result = parseVsixManifest(xml); assert.ok(result); assert.strictEqual(result.publisher, "ms-python"); assert.strictEqual(result.name, "python"); assert.strictEqual(result.version, "2023.25.0"); assert.strictEqual(result.displayName, "Python"); assert.strictEqual(result.description, "Python language support"); assert.strictEqual(result.platform, "linux-x64"); }); it("should handle manifest without TargetPlatform", () => { const xml = `<?xml version="1.0" encoding="utf-8"?> <PackageManifest Version="2.0.0" xmlns="http://schemas.microsoft.com/developer/vsx-schema/2011"> <Metadata> <Identity Id="csharp" Version="2.15.30" Publisher="muhammad-sammy" /> <DisplayName>C#</DisplayName> <Description>C# language support</Description> </Metadata> </PackageManifest>`; const result = parseVsixManifest(xml); assert.ok(result); assert.strictEqual(result.publisher, "muhammad-sammy"); assert.strictEqual(result.name, "csharp"); assert.strictEqual(result.version, "2.15.30"); assert.strictEqual(result.platform, ""); }); it("should handle manifest without Description or DisplayName", () => { const xml = `<?xml version="1.0" encoding="utf-8"?> <PackageManifest Version="2.0.0" xmlns="http://schemas.microsoft.com/developer/vsx-schema/2011"> <Metadata> <Identity Id="myext" Version="1.0.0" Publisher="testpub" /> </Metadata> </PackageManifest>`; const result = parseVsixManifest(xml); assert.ok(result); assert.strictEqual(result.publisher, "testpub"); assert.strictEqual(result.name, "myext"); assert.strictEqual(result.version, "1.0.0"); assert.strictEqual(result.displayName, ""); assert.strictEqual(result.description, ""); }); it("should handle larger manifest with tags", () => { const xml = `<?xml version="1.0" encoding="utf-8"?> <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"> <Metadata> <Identity Id="MyCompany.MyExtension" Version="1.2.3" Language="en-US" Publisher="My Company" /> <DisplayName>My Awesome Extension</DisplayName> <Description xml:space="preserve">A description of what this extension does.</Description> <MoreInfo>https://github.com/mycompany/myextension</MoreInfo> <License>LICENSE.txt</License> <Icon>Resources\\icon.png</Icon> <PreviewImage>Resources\\preview.png</PreviewImage> <Tags>productivity, coding, tools</Tags> </Metadata> <Installation InstalledByMsi="false" AllUsers="false"> <InstallationTarget Id="Microsoft.VisualStudio.Community" Version="[17.0,)" /> <InstallationTarget Id="Microsoft.VisualStudio.Pro" Version="[17.0,)" /> <InstallationTarget Id="Microsoft.VisualStudio.Enterprise" Version="[17.0,)" /> </Installation> <Dependencies> <Dependency Id="Microsoft.Framework.NDP" DisplayName=".NET Framework" d:Source="Manual" Version="[4.8,)" /> </Dependencies> <Prerequisites> <Prerequisite Id="Microsoft.VisualStudio.Component.CoreEditor" Version="[17.0,)" DisplayName="Visual Studio core editor" /> </Prerequisites> <Assets> <Asset Type="Microsoft.VisualStudio.VsPackage" d:Source="Project" d:ProjectName="%CurrentProject%" Path="|%CurrentProject%;PkgdefProjectOutputGroup|" /> <Asset Type="Microsoft.VisualStudio.MefComponent" d:Source="Project" d:ProjectName="%CurrentProject%" Path="|%CurrentProject%|" /> </Assets> </PackageManifest>`; const result = parseVsixManifest(xml); assert.ok(result); assert.strictEqual(result.publisher, "My Company"); assert.strictEqual(result.name, "MyCompany.MyExtension"); assert.strictEqual(result.version, "1.2.3"); assert.strictEqual(result.displayName, "My Awesome Extension"); assert.strictEqual( result.description, "A description of what this extension does.", ); assert.deepStrictEqual(result.tags, ["productivity", "coding", "tools"]); }); it("should parse a real one with tags", () => { const xml = ` <?xml version="1.0" encoding="utf-8"?> <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"> <Metadata> <Identity Language="en-US" Id="volar" Version="3.2.6" Publisher="Vue" /> <DisplayName>Vue (Official)</DisplayName> <Description xml:space="preserve">Language Support for Vue</Description> <Tags>json,vue,__ext_vue,markdown,html,jade,__web_extension,__sponsor_extension</Tags> <Categories>Programming Languages</Categories> <GalleryFlags>Public</GalleryFlags> <Properties> <Property Id="Microsoft.VisualStudio.Code.Engine" Value="^1.88.0" /> <Property Id="Microsoft.VisualStudio.Code.ExtensionDependencies" Value="" /> <Property Id="Microsoft.VisualStudio.Code.ExtensionPack" Value="" /> <Property Id="Microsoft.VisualStudio.Code.ExtensionKind" Value="workspace,web" /> <Property Id="Microsoft.VisualStudio.Code.LocalizedLanguages" Value="" /> <Property Id="Microsoft.VisualStudio.Code.EnabledApiProposals" Value="" /> <Property Id="Microsoft.VisualStudio.Code.ExecutesCode" Value="true" /> <Property Id="Microsoft.VisualStudio.Code.SponsorLink" Value="https://github.com/sponsors/johnsoncodehk" /> <Property Id="Microsoft.VisualStudio.Services.Links.Source" Value="https://github.com/vuejs/language-tools.git" /> <Property Id="Microsoft.VisualStudio.Services.Links.Getstarted" Value="https://github.com/vuejs/language-tools.git" /> <Property Id="Microsoft.VisualStudio.Services.Links.GitHub" Value="https://github.com/vuejs/language-tools.git" /> <Property Id="Microsoft.VisualStudio.Services.Links.Support" Value="https://github.com/vuejs/language-tools/issues" /> <Property Id="Microsoft.VisualStudio.Services.Links.Learn" Value="https://github.com/vuejs/language-tools#readme" /> <Property Id="Microsoft.VisualStudio.Services.GitHubFlavoredMarkdown" Value="true" /> <Property Id="Microsoft.VisualStudio.Services.Content.Pricing" Value="Free"/> </Properties> <License>extension/LICENSE.txt</License> <Icon>extension/icon.png</Icon> </Metadata> <Installation> <InstallationTarget Id="Microsoft.VisualStudio.Code"/> </Installation> <Dependencies/> <Assets> <Asset Type="Microsoft.VisualStudio.Code.Manifest" Path="extension/package.json" Addressable="true" /> <Asset Type="Microsoft.VisualStudio.Services.Content.Details" Path="extension/readme.md" Addressable="true" /> <Asset Type="Microsoft.VisualStudio.Services.Content.Changelog" Path="extension/changelog.md" Addressable="true" /> <Asset Type="Microsoft.VisualStudio.Services.Content.License" Path="extension/LICENSE.txt" Addressable="true" /> <Asset Type="Microsoft.VisualStudio.Services.Icons.Default" Path="extension/icon.png" Addressable="true" /> </Assets> </PackageManifest> `; const result = parseVsixManifest(xml); assert.ok(result); assert.strictEqual(result.publisher, "Vue"); assert.strictEqual(result.name, "volar"); assert.strictEqual(result.version, "3.2.6"); assert.strictEqual(result.displayName, "Vue (Official)"); assert.strictEqual(result.description, "Language Support for Vue"); assert.deepStrictEqual(result.tags, [ "json", "vue", "__ext_vue", "markdown", "html", "jade", "__web_extension", "__sponsor_extension", ]); }); it("should parse a real one with properties", () => { const xml = `<?xml version="1.0" encoding="utf-8"?> <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"> <Metadata> <Identity Language="en-US" Id="pyrefly" Version="0.61.0" Publisher="meta" TargetPlatform="win32-x64"/> <DisplayName>Pyrefly - Python Language Tooling</DisplayName> <Description xml:space="preserve">Python autocomplete, typechecking, code navigation and more! Powered by Pyrefly, an open-source language server</Description> <Tags>multi-root ready,python,type,typecheck,typehint,completion,lint,Python,__ext_py,__ext_pyi</Tags> <Categories>Programming Languages,Linters,Other</Categories> <GalleryFlags>Public</GalleryFlags> <Properties> <Property Id="Microsoft.VisualStudio.Code.Engine" Value="^1.94.0" /> <Property Id="Microsoft.VisualStudio.Code.ExtensionDependencies" Value="ms-python.python" /> <Property Id="Microsoft.VisualStudio.Code.ExtensionPack" Value="" /> <Property Id="Microsoft.VisualStudio.Code.ExtensionKind" Value="workspace" /> <Property Id="Microsoft.VisualStudio.Code.LocalizedLanguages" Value="" /> <Property Id="Microsoft.VisualStudio.Code.EnabledApiProposals" Value="" /> <Property Id="Microsoft.VisualStudio.Code.ExecutesCode" Value="true" /> <Property Id="Microsoft.VisualStudio.Services.Links.Source" Value="https://github.com/facebook/pyrefly.git" /> <Property Id="Microsoft.VisualStudio.Services.Links.Getstarted" Value="https://github.com/facebook/pyrefly.git" /> <Property Id="Microsoft.VisualStudio.Services.Links.GitHub" Value="https://github.com/facebook/pyrefly.git" /> <Property Id="Microsoft.VisualStudio.Services.Links.Support" Value="https://github.com/facebook/pyrefly/issues" /> <Property Id="Microsoft.VisualStudio.Services.Links.Learn" Value="https://github.com/facebook/pyrefly#readme" /> <Property Id="Microsoft.VisualStudio.Services.GitHubFlavoredMarkdown" Value="true" /> <Property Id="Microsoft.VisualStudio.Services.Content.Pricing" Value="Free"/> </Properties> <License>extension/LICENSE.txt</License> <Icon>extension/images/pyrefly-symbol.png</Icon> </Metadata> <Installation> <InstallationTarget Id="Microsoft.VisualStudio.Code"/> </Installation> <Dependencies/> <Assets> <Asset Type="Microsoft.VisualStudio.Code.Manifest" Path="extension/package.json" Addressable="true" /> <Asset Type="Microsoft.VisualStudio.Services.Content.Details" Path="extension/README.md" Addressable="true" /> <Asset Type="Microsoft.VisualStudio.Services.Content.License" Path="extension/LICENSE.txt" Addressable="true" /> <Asset Type="Microsoft.VisualStudio.Services.Icons.Default" Path="extension/images/pyrefly-symbol.png" Addressable="true" /> </Assets> </PackageManifest>`; const result = parseVsixManifest(xml); assert.ok(result); assert.strictEqual(result.publisher, "meta"); assert.strictEqual(result.name, "pyrefly"); assert.strictEqual(result.version, "0.61.0"); assert.strictEqual(result.platform, "win32-x64"); assert.strictEqual(result.displayName, "Pyrefly - Python Language Tooling"); // Properties tag parsing assert.strictEqual(result.vscodeEngine, "^1.94.0"); assert.deepStrictEqual(result.extensionDependencies, ["ms-python.python"]); assert.deepStrictEqual(result.extensionKind, ["workspace"]); assert.strictEqual(result.executesCode, true); // Links from Properties assert.ok(result.links); assert.strictEqual( result.links.Source, "https://github.com/facebook/pyrefly.git", ); assert.strictEqual( result.links.GitHub, "https://github.com/facebook/pyrefly.git", ); assert.strictEqual( result.links.Support, "https://github.com/facebook/pyrefly/issues", ); assert.strictEqual( result.links.Learn, "https://github.com/facebook/pyrefly#readme", ); // Empty ExtensionPack should not be set assert.strictEqual(result.extensionPack, undefined); }); it("should return undefined for invalid XML", () => { const result = parseVsixManifest("not xml at all"); assert.strictEqual(result, undefined); }); it("should return undefined for XML without PackageManifest", () => { const xml = `<?xml version="1.0" encoding="utf-8"?><root><child /></root>`; const result = parseVsixManifest(xml); assert.strictEqual(result, undefined); }); }); describe("extractExtensionCapabilities", () => { it("should return empty object for null/undefined", () => { assert.deepStrictEqual(extractExtensionCapabilities(null), {}); assert.deepStrictEqual(extractExtensionCapabilities(undefined), {}); }); it("should extract activation events", () => { const pkg = { activationEvents: ["onLanguage:python", "onCommand:python.runLinting"], }; const caps = extractExtensionCapabilities(pkg); assert.deepStrictEqual(caps.activationEvents, [ "onLanguage:python", "onCommand:python.runLinting", ]); }); it("should flag wildcard activation (always-on extension)", () => { const pkg = { activationEvents: ["*"] }; const caps = extractExtensionCapabilities(pkg); assert.deepStrictEqual(caps.activationEvents, ["*"]); }); it("should extract extensionKind", () => { const pkg = { extensionKind: ["workspace"] }; const caps = extractExtensionCapabilities(pkg); assert.deepStrictEqual(caps.extensionKind, ["workspace"]); }); it("should extract extensionDependencies", () => { const pkg = { extensionDependencies: ["ms-python.python", "ms-toolsai.jupyter"], }; const caps = extractExtensionCapabilities(pkg); assert.deepStrictEqual(caps.extensionDependencies, [ "ms-python.python", "ms-toolsai.jupyter", ]); }); it("should extract extensionPack", () => { const pkg = { extensionPack: [ "ms-python.python", "ms-python.vscode-pylance", "ms-toolsai.jupyter", ], }; const caps = extractExtensionCapabilities(pkg); assert.deepStrictEqual(caps.extensionPack, [ "ms-python.python", "ms-python.vscode-pylance", "ms-toolsai.jupyter", ]); }); it("should extract workspace trust configuration", () => { const pkg = { capabilities: { untrustedWorkspaces: { supported: "limited", description: "Only basic", }, virtualWorkspaces: { supported: false }, }, }; const caps = extractExtensionCapabilities(pkg); assert.deepStrictEqual(caps.untrustedWorkspaces, { supported: "limited", description: "Only basic", }); assert.deepStrictEqual(caps.virtualWorkspaces, { supported: false }); }); it("should extract contributed features", () => { const pkg = { contributes: { commands: [{ command: "ext.run", title: "Run" }], debuggers: [{ type: "python", label: "Python" }], terminal: [{ id: "ext.terminal" }], authentication: [{ id: "ext.auth", label: "My Auth" }], }, }; const caps = extractExtensionCapabilities(pkg); assert.ok(caps.contributes.includes("commands:count:1")); assert.ok(caps.contributes.includes("debuggers:count:1")); assert.ok(caps.contributes.includes("terminal-access")); assert.ok(caps.contributes.includes("authentication-provider")); }); it("should extract main and browser entry points", () => { const pkg = { main: "./dist/extension.js", browser: "./dist/web/extension.js", }; const caps = extractExtensionCapabilities(pkg); assert.strictEqual(caps.main, "./dist/extension.js"); assert.strictEqual(caps.browser, "./dist/web/extension.js"); }); it("should detect lifecycle scripts", () => { const pkg = { scripts: { postinstall: "node setup.js", "vscode:prepublish": "npm run build", "vscode:uninstall": "node cleanup.js", test: "jest", }, }; const caps = extractExtensionCapabilities(pkg); assert.ok(caps.lifecycleScripts.includes("postinstall")); assert.ok(caps.lifecycleScripts.includes("vscode:prepublish")); assert.ok(caps.lifecycleScripts.includes("vscode:uninstall")); assert.ok( !caps.lifecycleScripts.includes("test"), "test is not a lifecycle script", ); }); it("should handle extension with taskDefinitions", () => { const pkg = { contributes: { taskDefinitions: [{ type: "npm" }], }, }; const caps = extractExtensionCapabilities(pkg); assert.ok(caps.contributes.includes("terminal-access")); }); it("should handle extension with filesystem providers", () => { const pkg = { contributes: { fileSystemProviders: [{ scheme: "ftp", authority: "ftp" }], }, }; const caps = extractExtensionCapabilities(pkg); assert.ok(caps.contributes.includes("filesystem-provider")); }); it("should return empty for extension with no capabilities", () => { const pkg = { name: "simple-ext", version: "1.0.0" }; const caps = extractExtensionCapabilities(pkg); assert.ok(!caps.activationEvents); assert.ok(!caps.contributes); assert.ok(!caps.lifecycleScripts); assert.ok(!caps.main); }); }); describe("parseVsixPackageJson", () => { it("should return undefined for empty input", () => { assert.strictEqual(parseVsixPackageJson(""), undefined); assert.strictEqual(parseVsixPackageJson("{}"), undefined); assert.strictEqual(parseVsixPackageJson(null), undefined); }); it("should parse a valid package.json string", () => { const json = JSON.stringify({ name: "python", publisher: "ms-python", version: "2023.25.0", displayName: "Python", description: "Python language support with Pylance", }); const result = parseVsixPackageJson(json, "/test/path"); assert.ok(result); assert.strictEqual(result.publisher, "ms-python"); assert.strictEqual(result.name, "python"); assert.strictEqual(result.version, "2023.25.0"); assert.strictEqual(result.displayName, "Python"); assert.strictEqual( result.description, "Python language support with Pylance", ); assert.strictEqual(result.srcPath, "/test/path"); }); it("should parse a pre-parsed object", () => { const obj = { name: "go", publisher: "golang", version: "0.39.1", displayName: "Go", }; const result = parseVsixPackageJson(obj); assert.ok(result); assert.strictEqual(result.publisher, "golang"); assert.strictEqual(result.name, "go"); assert.strictEqual(result.version, "0.39.1"); }); it("should include capabilities from package.json", () => { const obj = { name: "python", publisher: "ms-python", version: "1.0.0", activationEvents: ["onLanguage:python"], main: "./dist/extension.js", contributes: { commands: [{ command: "python.run", title: "Run" }], }, scripts: { postinstall: "node install.js", }, }; const result = parseVsixPackageJson(obj); assert.ok(result); assert.ok(result.capabilities); assert.deepStrictEqual(result.capabilities.activationEvents, [ "onLanguage:python", ]); assert.strictEqual(result.capabilities.main, "./dist/extension.js"); assert.ok(result.capabilities.contributes.includes("commands:count:1")); assert.ok(result.capabilities.lifecycleScripts.includes("postinstall")); }); it("should handle missing optional fields", () => { const obj = { name: "simple-ext" }; const result = parseVsixPackageJson(obj); assert.ok(result); assert.strictEqual(result.name, "simple-ext"); assert.strictEqual(result.publisher, ""); assert.strictEqual(result.version, ""); assert.strictEqual(result.displayName, ""); assert.strictEqual(result.description, ""); }); it("should return undefined for invalid JSON string", () => { const result = parseVsixPackageJson("not json"); assert.strictEqual(result, undefined); }); it("should handle a real one", () => { const result = parseVsixPackageJson(` { "private": true, "name": "volar", "version": "3.2.6", "repository": { "type": "git", "url": "https://github.com/vuejs/language-tools.git", "directory": "extensions/vscode" }, "categories": [ "Programming Languages" ], "sponsor": { "url": "https://github.com/sponsors/johnsoncodehk" }, "icon": "icon.png", "displayName": "Vue (Official)", "description": "Language Support for Vue", "author": "johnsoncodehk", "publisher": "Vue", "engines": { "vscode": "^1.88.0" }, "activationEvents": [ "onLanguage" ], "main": "./main.js", "browser": "./web.js", "capabilities": { "virtualWorkspaces": { "supported": "limited", "description": "Install https://marketplace.visualstudio.com/items?itemName=johnsoncodehk.vscode-typescript-web to have IntelliSense for .vue files in Web IDE." } }, "contributes": { "jsonValidation": [ { "fileMatch": [ "tsconfig.json", "tsconfig.*.json", "tsconfig-*.json", "jsconfig.json", "jsconfig.*.json", "jsconfig-*.json" ], "url": "./schemas/vue-tsconfig.schema.json" } ], "languages": [ { "id": "vue", "extensions": [ ".vue" ], "configuration": "./languages/vue-language-configuration.json" }, { "id": "markdown", "configuration": "./languages/markdown-language-configuration.json" }, { "id": "html", "configuration": "./languages/sfc-template-language-configuration.json" }, { "id": "jade", "configuration": "./languages/sfc-template-language-configuration.json" } ], "grammars": [ { "language": "vue", "scopeName": "text.html.vue", "path": "./syntaxes/vue.tmLanguage.json", "embeddedLanguages": { "text.html.vue": "vue", "text": "plaintext", "text.html.derivative": "html", "text.html.markdown": "markdown", "text.pug": "jade", "source.css": "css", "source.css.scss": "scss", "source.css.less": "less", "source.sass": "sass", "source.stylus": "stylus", "source.postcss": "postcss", "source.js": "javascript", "source.ts": "typescript", "source.js.jsx": "javascriptreact", "source.tsx": "typescriptreact", "source.coffee": "coffeescript", "meta.tag.js": "jsx-tags", "meta.tag.tsx": "jsx-tags", "meta.tag.without-attributes.js": "jsx-tags", "meta.tag.without-attributes.tsx": "jsx-tags", "source.json": "json", "source.json.comments": "jsonc", "source.json5": "json5", "source.yaml": "yaml", "source.toml": "toml", "source.graphql": "graphql" }, "unbalancedBracketScopes": [ "keyword.operator.relational", "storage.type.function.arrow", "keyword.operator.bitwise.shift", "meta.brace.angle", "punctuation.definition.tag" ] }, { "scopeName": "markdown.vue.codeblock", "path": "./syntaxes/markdown-vue.json", "injectTo": [ "text.html.markdown" ], "embeddedLanguages": { "meta.embedded.block.vue": "vue", "text.html.vue": "vue", "text": "plaintext", "text.html.derivative": "html", "text.html.markdown": "markdown", "text.pug": "jade", "source.css": "css", "source.css.scss": "scss", "source.css.less": "less", "source.sass": "sass", "source.stylus": "stylus", "source.postcss": "postcss", "source.js": "javascript", "source.ts": "typescript", "source.js.jsx": "javascriptreact", "source.tsx": "typescriptreact", "source.coffee": "coffeescript", "meta.tag.js": "jsx-tags", "meta.tag.tsx": "jsx-tags", "meta.tag.without-attributes.js": "jsx-tags", "meta.tag.without-attributes.tsx": "jsx-tags", "source.json": "json", "source.json.comments": "jsonc", "source.json5": "json5", "source.yaml": "yaml", "source.toml": "toml", "source.graphql": "graphql" } }, { "scopeName": "mdx.vue.codeblock", "path": "./syntaxes/mdx-vue.json", "injectTo": [ "source.mdx" ], "embeddedLanguages": { "mdx.embedded.vue": "vue", "text.html.vue": "vue", "text": "plaintext", "text.html.derivative": "html", "text.html.markdown": "markdown", "text.pug": "jade", "source.css": "css", "source.css.scss": "scss", "source.css.less": "less", "source.sass": "sass", "source.stylus": "stylus", "source.postcss": "postcss", "source.js": "javascript", "source.ts": "typescript", "source.js.jsx": "javascriptreact", "source.tsx": "typescriptreact", "source.coffee": "coffeescript", "meta.tag.js": "jsx-tags", "meta.tag.tsx": "jsx-tags", "meta.tag.without-attributes.js": "jsx-tags", "meta.tag.without-attributes.tsx": "jsx-tags", "source.json": "json", "source.json.comments": "jsonc", "source.json5": "json5", "source.yaml": "yaml", "source.toml": "toml", "source.graphql": "graphql" } }, { "scopeName": "vue.directives", "path": "./syntaxes/vue-directives.json", "injectTo": [ "text.html.vue", "text.html.markdown", "text.html.derivative", "text.pug" ] }, { "scopeName": "vue.interpolations", "path": "./syntaxes/vue-interpolations.json", "injectTo": [ "text.html.vue", "text.html.markdown", "text.html.derivative", "text.pug" ] }, { "scopeName": "vue.sfc.script.leading-operator-fix", "path": "./syntaxes/vue-sfc-script-leading-operator-fix.json", "injectTo": [ "text.html.vue" ] }, { "scopeName": "vue.sfc.style.variable.injection", "path": "./syntaxes/vue-sfc-style-variable-injection.json", "injectTo": [ "text.html.vue" ] } ], "semanticTokenScopes": [ { "language": "vue", "scopes": { "component": [ "support.class.component.vue", "entity.name.type.class.vue" ] } }, { "language": "markdown", "scopes": { "component": [ "support.class.component.vue", "entity.name.type.class.vue" ] } }, { "language": "html", "scopes": { "component": [ "support.class.component.vue", "entity.name.type.class.vue" ] } } ], "breakpoints": [ { "language": "vue" } ], "configuration": { "type": "object", "title": "Vue", "properties": { "vue.trace.server": { "scope": "window", "type": "string", "enum": [ "off", "messages", "verbose" ], "default": "off", "markdownDescription": "%configuration.trace.server%" }, "vue.editor.focusMode": { "type": "boolean", "default": false, "markdownDescription": "%configuration.editor.focusMode%" }, "vue.editor.reactivityVisualization": { "type": "boolean", "default": true, "markdownDescription": "%configuration.editor.reactivityVisualization%" }, "vue.editor.templateInterpolationDecorators": { "type": "boolean", "default": true, "markdownDescription": "%configuration.editor.templateInterpolationDecorators%" }, "vue.server.path": { "type": "string", "markdownDescription": "%configuration.server.path%" }, "vue.server.includeLanguages": { "type": "array", "items": { "type": "string" }, "default": [ "vue" ], "markdownDescription": "%configuration.server.includeLanguages%" }, "vue.codeActions.askNewComponentName": { "type": "boolean", "default": true, "markdownDescription": "%configuration.codeActions.askNewComponentName%" }, "vue.hover.rich": { "type": "boolean", "default": false, "markdownDescription": "%configuration.hover.rich%" }, "vue.suggest.componentNameCasing": { "type": "string", "enum": [ "preferKebabCase", "preferPascalCase", "alwaysKebabCase", "alwaysPascalCase" ], "enumDescriptions": [ "Prefer kebab-case (lowercase with hyphens, e.g. my-component)", "Prefer PascalCase (UpperCamelCase, e.g. MyComponent)", "Always kebab-case (enforce kebab-case, e.g. my-component)", "Always PascalCase (enforce PascalCase, e.g. MyComponent)" ], "default": "preferPascalCase", "markdownDescription": "%configuration.suggest.componentNameCasing%" }, "vue.suggest.propNameCasing": { "type": "string", "enum": [ "preferKebabCase", "preferCamelCase", "alwaysKebabCase", "alwaysCamelCase" ], "enumDescriptions": [ "Prefer kebab-case (lowercase with hyphens, e.g. my-prop)", "Prefer camelCase (lowerCamelCase, e.g. myProp)", "Always kebab-case (enforce kebab-case, e.g. my-prop)", "Always camelCase (enforce camelCase, e.g. myProp)" ], "default": "preferKebabCase", "markdownDescription": "%configuration.suggest.propNameCasing%" }, "vue.suggest.defineAssignment": { "type": "boolean", "default": true, "markdownDescription": "%configuration.suggest.defineAssignment%" }, "vue.autoInsert.dotValue": { "type": "boolean", "default": false, "markdownDescription": "%configuration.autoInsert.dotValue%" }, "vue.autoInsert.bracketSpacing": { "type": "boolean", "default": true, "markdownDescription": "%configuration.autoInsert.bracketSpacing%" }, "vue.inlayHints.destructuredProps": { "type": "boolean", "default": false, "markdownDescription": "%configuration.inlayHints.destructuredProps%" }, "vue.inlayHints.missingProps": { "type": "boolean", "default": false, "markdownDescription": "%configuration.inlayHints.missingProps%" }, "vue.inlayHints.inlineHandlerLeading": { "type": "boolean", "default": false, "markdownDescription": "%configuration.inlayHints.inlineHandlerLeading%" }, "vue.inlayHints.optionsWrapper": { "type": "boolean", "default": false, "markdownDescription": "%configuration.inlayHints.optionsWrapper%" }, "vue.inlayHints.vBindShorthand": { "type": "boolean", "default": false, "markdownDescription": "%configuration.inlayHints.vBindShorthand%" }, "vue.format.template.initialIndent": { "type": "boolean", "default": true, "markdownDescription": "%configuration.format.template.initialIndent%" }, "vue.format.script.initialIndent": { "type": "boolean", "default": false, "markdownDescription": "%configuration.format.script.initialIndent%" }, "vue.format.style.initialIndent": { "type": "boolean", "default": false, "markdownDescription": "%configuration.format.style.initialIndent%" }, "vue.format.script.enabled": { "type": "boolean", "default": true, "markdownDescription": "%configuration.format.script.enabled%" }, "vue.format.template.enabled": { "type": "boolean", "default": true, "markdownDescription": "%configuration.format.template.enabled%" }, "vue.format.style.enabled": { "type": "boolean", "default": true, "markdownDescription": "%configuration.format.style.enabled%" }, "vue.format.wrapAttributes": { "type": "string", "default": "auto", "enum": [ "auto", "force", "force-aligned", "force-expand-multiline", "aligned-multiple", "preserve", "preserve-aligned" ], "markdownDescription": "%configuration.format.wrapAttributes%" } } }, "commands": [ { "command": "vue.welcome", "title": "%command.welcome%", "category": "Vue" }, { "command": "vue.action.restartServer", "title": "%command.action.restartServer%", "category": "Vue" } ], "menus": { "editor/context": [ { "command": "typescript.goToSourceDefinition", "when": "tsSupportsSourceDefinition && resourceLangId == vue", "group": "navigation@9" } ], "explorer/context": [ { "command": "typescript.findAllFileReferences", "when": "tsSupportsFileReferences && resourceLangId == vue", "group": "4_search" } ], "editor/title/context": [ { "command": "typescript.findAllFileReferences", "when": "tsSupportsFileReferences && resourceLangId == vue" } ], "commandPalette": [ { "command": "typescript.reloadProjects", "when": "editorLangId == vue && typescript.isManagedFile" }, { "command": "typescript.goToProjectConfig", "when": "editorLangId == vue && typescript.isManagedFile" }, { "command": "typescript.sortImports", "when": "supportedCodeAction =~ /(\\\\s|^)source\\\\.sortImports\\\\b/ && editorLangId =~ /^vue$/" }, { "command": "typescript.removeUnusedImports", "when": "supportedCodeAction =~ /(\\\\s|^)source\\\\.removeUnusedImports\\\\b/ && editorLangId =~ /^vue$/" } ] } }, "scripts": { "vscode:prepublish": "rolldown --config", "pack": "npx @vscode/vsce package", "gen-ext-meta": "vscode-ext-gen --scope vue --output src/generated-meta.ts && cd ../.. && npm run format" }, "devDependencies": { "@types/node": "^22.10.4", "@types/vscode": "1.88.0", "@volar/typescript": "2.4.28", "@volar/vscode": "2.4.28", "@vue/language-core": "workspace:*", "@vue/language-server": "workspace:*", "@vue/typescript-plugin": "workspace:*", "laplacenoma": "latest", "reactive-vscode": "^0.4.1", "rolldown": "latest", "vscode-ext-gen": "latest", "vscode-tmlanguage-snapshot": "latest" } } `); assert.ok(result); assert.strictEqual(result.publisher, "Vue"); assert.strictEqual(result.name, "volar"); assert.strictEqual(result.version, "3.2.6"); assert.strictEqual(result.displayName, "Vue (Official)"); assert.strictEqual(result.description, "Language Support for Vue"); assert.strictEqual(result.platform, ""); assert.strictEqual(result.srcPath, undefined); // Should now capture repository as external reference (was a bug before: checked packageJsonData instead of pkg) assert.ok(result.externalReferences); assert.deepStrictEqual(result.externalReferences, [ { type: "vcs", url: "https://github.com/vuejs/language-tools.git" }, ]); assert.deepStrictEqual(result.capabilities, { activationEvents: ["onLanguage"], virtualWorkspaces: { supported: "limited", description: "Install https://marketplace.visualstudio.com/items?itemName=johnsoncodehk.vscode-typescript-web to have IntelliSense for .vue files in Web IDE.", }, contributes: [ "breakpoints:count:1", "commands:count:2", "language-server-plugins", ], main: "./main.js", browser: "./web.js", lifecycleScripts: ["vscode:prepublish"], }); // Should capture devDependencies for later analysis assert.ok(result.devDependencies); assert.strictEqual(result.devDependencies["@types/node"], "^22.10.4"); assert.strictEqual(result.devDependencies["@types/vscode"], "1.88.0"); assert.strictEqual( result.devDependencies["@vue/language-core"], "workspace:*", ); }); it("should handle another real one (incomplete)", () => { const result = parseVsixPackageJson(` { "name": "pyrefly", "displayName": "Pyrefly - Python Language Tooling", "description": "Python autocomplete, typechecking, code navigation and more! Powered by Pyrefly, an open-source language server", "icon": "images/pyrefly-symbol.png", "extensionKind": [ "workspace" ], "author": "Facebook", "license": "Apache2", "version": "0.61.0", "repository": { "type": "git", "url": "https://github.com/facebook/pyrefly" }, "publisher": "meta", "categories": [ "Programming Languages", "Linters", "Other" ], "keywords": [ "multi-root ready", "python", "type", "typecheck", "typehint", "completion", "lint" ], "engines": { "vscode": "^1.94.0" }, "main": "./dist/extension", "activationEvents": [ "onLanguage:python", "onNotebook:jupyter-notebook" ], "capabilities": { "untrustedWorkspaces": { "supported": false, "description": "Pyrefly can be configured to execute binaries. A malicious actor could exploit this to run arbitrary code on your machine." } }, "contributes": { "languages": [ { "id": "python", "aliases": [ "Python" ], "extensions": [ ".py", ".pyi" ] } ], "commands": [ { "title": "Restart Pyrefly Client", "category": "pyrefly", "command": "pyrefly.restartClient" }, { "title": "Fold All Docstrings", "category": "pyrefly", "command": "pyrefly.foldAllDocstrings" }, { "title": "Unfold All Docstrings", "category": "pyrefly", "command": "pyrefly.unfoldAllDocstrings" }, { "title": "Run File", "category": "pyrefly", "command": "pyrefly.runMain" }, { "title": "Run Test", "category": "pyrefly", "command": "pyrefly.runTest" } ], "semanticTokenScopes": [ { "language": "python", "scopes": { "variable.readonly": [ "variable.other.constant.python" ] } } ], "configurationDefaults": { "editor.semanticTokenColorCustomizations": { "rules": { "variable.readonly:python": "#4EC9B0" } } }, "configuration": { "properties": { "pyre