vsix-utils
Version:
utilities for working with VSIX packages
215 lines (204 loc) • 10.5 kB
JavaScript
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([
["'", "'"],
['"', """],
["<", "<"],
[">", ">"],
["&", "&"]
]);
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 };