UNPKG

vsix-utils

Version:

utilities for working with VSIX packages

215 lines (204 loc) 10.5 kB
import { readFile } from 'node:fs/promises'; import path from 'node:path'; // src/manifest.ts async function readProjectManifest(projectDir) { try { const manifestPath = path.join(projectDir, "package.json"); const manifest = JSON.parse(await readFile(manifestPath, "utf-8")); return { fileName: manifestPath, manifest }; } catch { return null; } } var escapeChars = /* @__PURE__ */ new Map([ ["'", "&apos;"], ['"', "&quot;"], ["<", "&lt;"], [">", "&gt;"], ["&", "&amp;"] ]); function escapeXml(value) { return String(value).replace(/(['"<>&])/g, (_, char) => escapeChars.get(char)); } var GITHUB_REPOSITORY_REGEX = /^https:\/\/github\.com\/|^git@github\.com:/; function createVsixManifest(manifest, options) { const { assets = [], icon, license, target, tags = [], flags = [], qna = false, preRelease = false } = options; const galleryBanner = manifest.galleryBanner ?? {}; const extensionKind = transformExtensionKind(manifest); const localizedLanguages = manifest.contributes?.localizations ? manifest.contributes.localizations.map((loc) => loc.localizedLanguageName ?? loc.languageName ?? loc.languageId).join(",") : ""; const isQnaEnabled = qna !== false; let repository; if (manifest.repository != null) { if (typeof manifest.repository === "string") { repository = manifest.repository; } else if (typeof manifest.repository === "object" && "url" in manifest.repository && typeof manifest.repository.url === "string") { repository = manifest.repository.url; } } let bugs; if (manifest.bugs != null) { if (typeof manifest.bugs === "string") { bugs = manifest.bugs; } else if (typeof manifest.bugs === "object" && "url" in manifest.bugs && typeof manifest.bugs.url === "string") { bugs = manifest.bugs.url; } else if (typeof manifest.bugs === "object" && "email" in manifest.bugs && typeof manifest.bugs.email === "string") { bugs = `mailto:${manifest.bugs.email}`; } } let homepage; if (manifest.homepage != null && typeof manifest.homepage === "string") { homepage = manifest.homepage; } return ( /* 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="${escapeXml(manifest.name)}" Version="${escapeXml(manifest.version)}" Publisher="${escapeXml(manifest.publisher)}" ${target != null ? `TargetPlatform="${escapeXml(target)}"` : ""} /> <DisplayName>${escapeXml(manifest.displayName ?? manifest.name)}</DisplayName> <Description xml:space="preserve">${escapeXml(manifest.description ?? "")}</Description> <Tags>${escapeXml(tags.join(",") ?? "")}</Tags> <GalleryFlags>${escapeXml(flags.join(" ") ?? "")}</GalleryFlags> ${manifest.badges == null ? "" : ` <Badges> ${manifest.badges.map( (badge) => ` <Badge Link="${escapeXml(badge.href)}" ImgUri="${escapeXml(badge.url)}" Description="${escapeXml(badge.description)}" /> ` ).join("\n")} </Badges> `} <Properties> <Property Id="Microsoft.VisualStudio.Code.Engine" Value="${escapeXml(manifest.engines.vscode)}" /> <Property Id="Microsoft.VisualStudio.Code.ExtensionDependencies" Value="${escapeXml((manifest.extensionDependencies || []).join(","))}" /> <Property Id="Microsoft.VisualStudio.Code.ExtensionPack" Value="${escapeXml((manifest.extensionPack || []).join(","))}" /> <Property Id="Microsoft.VisualStudio.Code.ExtensionKind" Value="${escapeXml(extensionKind.join(","))}" /> <Property Id="Microsoft.VisualStudio.Code.LocalizedLanguages" Value="${escapeXml(localizedLanguages)}" /> <Property Id="Microsoft.VisualStudio.Code.EnabledApiProposals" Value="${escapeXml((manifest.enabledApiProposals || []).join(","))}" /> <Property Id="Microsoft.VisualStudio.Code.PreRelease" Value="${escapeXml(preRelease)}" /> <Property Id="Microsoft.VisualStudio.Code.ExecutesCode" Value="${escapeXml(!!(manifest.main ?? manifest.browser))}" /> ${manifest.sponsor?.url != null ? `<Property Id="Microsoft.VisualStudio.Code.SponsorLink" Value="${escapeXml(manifest.sponsor.url)}" />` : ""} ${repository != null ? `<Property Id="Microsoft.VisualStudio.Services.Links.Source" Value="${escapeXml(repository)}" />` : ""} ${repository != null ? `<Property Id="Microsoft.VisualStudio.Services.Links.Getstarted" Value="${escapeXml(repository)}" />` : ""} ${repository != null ? `<Property Id="Microsoft.VisualStudio.Services.Links.${GITHUB_REPOSITORY_REGEX.test(repository) ? "GitHub" : "Repository"}" Value="${escapeXml(repository)}" />` : ""} ${bugs != null ? `<Property Id="Microsoft.VisualStudio.Services.Links.Support" Value="${escapeXml(bugs)}" />` : ""} ${homepage != null ? `<Property Id="Microsoft.VisualStudio.Services.Links.Learn" Value="${escapeXml(homepage)}" />` : ""} ${galleryBanner.color != null ? `<Property Id="Microsoft.VisualStudio.Services.Branding.Color Value="${escapeXml(galleryBanner.color)}" />` : ""} ${galleryBanner.theme != null ? `<Property Id="Microsoft.VisualStudio.Services.Branding.Theme Value="${escapeXml(galleryBanner.theme)}" />` : ""} <Property Id="Microsoft.VisualStudio.Services.GitHubFlavoredMarkdown" Value="${escapeXml(manifest.markdown !== "standard")}" /> <Property Id="Microsoft.VisualStudio.Services.Content.Pricing" Value="${escapeXml(manifest.pricing ?? "Free")}" /> ${isQnaEnabled ? `<Property Id="Microsoft.VisualStudio.Services.EnableMarketplaceQnA" Value="${escapeXml(qna)}" />` : ""} ${isQnaEnabled && qna !== "marketplace" ? `<Property Id="Microsoft.VisualStudio.Services.CustomerQnALink" Value="${escapeXml(qna)}" />` : ""} </Properties> ${license != null ? `<License>${escapeXml(license)}</License>` : ""} ${icon != null ? `<Icon>${escapeXml(icon)}</Icon>` : ""} </Metadata> <Installation> <InstallationTarget Id="Microsoft.VisualStudio.Code" /> </Installation> <Dependencies /> <Assets> <Asset Type="Microsoft.VisualStudio.Code.Manifest" Path="package.json" Addressable="true" /> ${assets.map((asset) => `<Asset Type="${escapeXml(asset.type)}" Path="${escapeXml(asset.path)}" Addressable="true" />`).join("\n")} </Assets> </PackageManifest>` ); } var CONTRIBUTION_POINTS_FOR_EXTENSION_KIND = /* @__PURE__ */ new Map([ ["jsonValidation", ["workspace", "web"]], ["localizations", ["ui", "workspace"]], ["debuggers", ["workspace"]], ["terminal", ["workspace"]], ["typescriptServerPlugins", ["workspace"]], ["markdown.previewStyles", ["workspace", "web"]], ["markdown.previewScripts", ["workspace", "web"]], ["markdown.markdownItPlugins", ["workspace", "web"]], ["html.customData", ["workspace", "web"]], ["css.customData", ["workspace", "web"]] ]); function transformExtensionKind(manifest) { const isWebSupported = manifest.browser != null; if (manifest.extensionKind != null) { const explicitKinds = Array.isArray(manifest.extensionKind) ? manifest.extensionKind : manifest.extensionKind === "ui" ? ["ui", "workspace"] : [manifest.extensionKind]; if (isWebSupported && !explicitKinds.includes("web")) { explicitKinds.push("web"); } return explicitKinds; } if (manifest.main != null && isWebSupported) { return ["workspace", "web"]; } if (manifest.main != null) { return ["workspace"]; } if (isWebSupported) { return ["web"]; } const isNonEmptyArray = (obj) => Array.isArray(obj) && obj.length > 0; if (isNonEmptyArray(manifest.extensionPack) || isNonEmptyArray(manifest.extensionDependencies)) { return ["workspace", "web"]; } let result = ["ui", "workspace", "web"]; if (manifest.contributes != null) { for (const contribution of Object.keys(manifest.contributes)) { const supportedKinds = CONTRIBUTION_POINTS_FOR_EXTENSION_KIND.get(contribution); if (supportedKinds) { result = result.filter((kind) => supportedKinds.includes(kind)); } } } return result; } function isWebKind(manifest) { return transformExtensionKind(manifest).includes("web"); } function getManifestTags(manifest) { const { contributes = {}, activationEvents = [], keywords = [] } = manifest; const tags = new Set(keywords); const doesContribute = (obj, ...properties) => { return properties.every((property) => obj && (obj = obj[property]) && obj.length > 0); }; if (doesContribute(contributes, "themes")) tags.add("theme").add("color-theme"); if (doesContribute(contributes, "iconThemes")) tags.add("theme").add("icon-theme"); if (doesContribute(contributes, "productIconThemes")) tags.add("theme").add("product-icon-theme"); if (doesContribute(contributes, "snippets")) tags.add("snippet"); if (doesContribute(contributes, "keybindings")) tags.add("keybindings"); if (doesContribute(contributes, "debuggers")) tags.add("debuggers"); if (doesContribute(contributes, "jsonValidation")) tags.add("json"); if (doesContribute(contributes, "menus", "statusBar/remoteIndicator")) tags.add("remote-menu"); if (doesContribute(contributes, "chatParticipants")) tags.add("chat-participant").add("github-copilot"); for (const localization of contributes.localizations ?? []) { tags.add(`lp-${localization.languageId}`); for (const { id } of localization.translations ?? []) { tags.add(`__lp_${id}`).add(`__lp-${localization.languageId}_${id}`); } } for (const language of contributes.languages ?? []) { tags.add(language.id); for (const alias of language.aliases ?? []) { tags.add(alias); } for (const ext of language.extensions ?? []) { const cleanedExt = ext.replace(/\W/g, ""); if (cleanedExt) tags.add(`__ext_${cleanedExt}`); } } for (const event of activationEvents) { if (!event.startsWith("onLanguage:")) continue; tags.add(event.replace("onLanguage:", "")); } for (const grammar of contributes.grammars ?? []) { tags.add(grammar.language); } if (isWebKind(manifest)) { tags.add("__web_extension"); } if (manifest.sponsor != null && manifest.sponsor.url != null) { tags.add("__sponsor_extension"); } return [...tags].filter(Boolean); } export { createVsixManifest, getManifestTags, isWebKind, readProjectManifest, transformExtensionKind };