UNPKG

vsix-utils

Version:

utilities for working with VSIX packages

1,177 lines (1,161 loc) 43 kB
'use strict'; var child_process = require('child_process'); var path2 = require('path'); var process = require('process'); var util = require('util'); var fs = require('fs'); var promises = require('fs/promises'); var ignore = require('ignore'); var mime = require('mime'); var packageManagerDetector = require('package-manager-detector'); var tinyglobby = require('tinyglobby'); var remark = require('remark'); var remarkTransformLinks = require('remark-transform-links'); var semver = require('semver'); var buffer = require('buffer'); var fastXmlParser = require('fast-xml-parser'); var yauzl = require('yauzl'); var yazl = require('yazl'); function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; } var path2__default = /*#__PURE__*/_interopDefault(path2); var process__default = /*#__PURE__*/_interopDefault(process); var ignore__default = /*#__PURE__*/_interopDefault(ignore); var mime__default = /*#__PURE__*/_interopDefault(mime); var remarkTransformLinks__default = /*#__PURE__*/_interopDefault(remarkTransformLinks); var yauzl__default = /*#__PURE__*/_interopDefault(yauzl); var yazl__default = /*#__PURE__*/_interopDefault(yazl); // src/dependencies.ts var execAsync = util.promisify(child_process.exec); async function getExtensionDependencies(manifest, options) { const { packageManager, cwd = process__default.default.cwd() } = options; const dependencies = /* @__PURE__ */ new Set(); if (packageManager === "npm") { const { stdout } = await execAsync("npm list --production --parseable --depth=99999 --loglevel=error", { cwd }); const lines = stdout.split(/[\r\n]/).filter((path4) => path2.isAbsolute(path4)); for (const line of lines) { if (line === path2.resolve(cwd)) { continue; } const dependency = line.split(`${path2__default.default.sep}node_modules${path2__default.default.sep}`)[1]; if (dependency == null) { throw new Error(`could not parse dependency: ${line}`); } dependencies.add({ name: dependency.replace(/\\/g, "/"), version: manifest.dependencies != null ? manifest.dependencies[dependency.replace(/\\/g, "/")] : undefined, path: line }); } } else if (packageManager === "yarn") { let asYarnDependency2 = function(prefix, tree, prune2) { if (prune2 && /@[\^~]/.test(tree.name)) { return null; } let name; let version = ""; try { const tmp = tree.name.split("@"); if (tmp[0] === "") { tmp.shift(); tmp[0] = `@${tmp[0]}`; } name = tmp[0]; version = tmp[1] || ""; } catch { name = tree.name.replace(/^([^@+])@.*$/, "$1"); } const dependencyPath = path2__default.default.join(prefix, name); const children = []; for (const child of tree.children || []) { const dep = asYarnDependency2(path2__default.default.join(prefix, name, "node_modules"), child, prune2); if (dep) { children.push(dep); } } return { name, path: dependencyPath, children, version }; }; const { stdout } = await execAsync("yarn list --prod --json", { cwd }); const match = /^\{"type":"tree".*$/m.exec(stdout); if (!match || match.length !== 1) { throw new Error("Could not parse result of `yarn list --json`"); } const trees = JSON.parse(match[0]).data.trees; if (!Array.isArray(trees) || trees.length === 0) { return []; } const prune = true; const result = trees.map((tree) => asYarnDependency2(path2.join(cwd, "node_modules"), tree, prune)).filter((dep) => dep != null); const internalDeps = /* @__PURE__ */ new Set(); const flatten = (dep) => { if (internalDeps.has(dep.path)) { return; } dependencies.add({ name: dep.name, version: dep.version, path: dep.path }); internalDeps.add(dep.path); if (dep.children) { for (const child of dep.children) { flatten(child); } } }; for (const dep of result) { flatten(dep); } } else if (packageManager === "pnpm") { const { stdout } = await execAsync("pnpm list --production --json --depth=99999 --loglevel=error --ignore-workspace", { cwd }); let entryList = []; try { entryList = JSON.parse(stdout); } catch { return []; } if (!Array.isArray(entryList) || entryList.length === 0) { return []; } const entry = entryList[0]; if (entry == null || typeof entry !== "object" || entry.dependencies == null || typeof entry.dependencies !== "object") { return []; } const internalDeps = /* @__PURE__ */ new Set(); const flatten = (dep) => { if (internalDeps.has(dep.path)) { return; } dependencies.add({ name: dep.from, version: dep.version, path: dep.path }); internalDeps.add(dep.path); if (dep.dependencies) { for (const child of Object.values(dep.dependencies)) { flatten(child); } } }; for (const value of Object.values(entry.dependencies)) { flatten(value); } } else { throw new Error(`unsupported package manager: ${packageManager}`); } return Array.from(dependencies); } async function transformMarkdown(manifest, options) { const { content, rewrite = true, branch = "HEAD" } = options; if (!rewrite) { return content; } let { baseContentUrl, baseImagesUrl } = options; const baseUrls = inferBaseUrls(manifest, branch); if (baseUrls == null) { throw new Error("Couldn't detect the repository where this extension is published."); } if (baseContentUrl == null) { baseContentUrl = baseUrls.contentUrl; } if (baseImagesUrl == null) { baseImagesUrl = baseUrls.imagesUrl; } const detectedSyntax = detectMarkdownSyntax(content); const file = await remark.remark().data("settings", { bullet: detectedSyntax.bullet, bulletOrdered: detectedSyntax.bulletOrdered, emphasis: detectedSyntax.emphasis, listItemIndent: detectedSyntax.listItemIndent, fence: detectedSyntax.fence, strong: detectedSyntax.strong, rule: detectedSyntax.rule }).use(remarkTransformLinks__default.default, { baseUrl(_, type) { if (type === "image" || type === "html_img" || type === "html_video") { return baseImagesUrl; } return baseContentUrl; } }).process(content); return String(file); } function inferBaseUrls(manifest, branch) { let repository = null; if (typeof manifest.repository === "string") { repository = manifest.repository; } else if (manifest.repository && typeof manifest.repository === "object" && "url" in manifest.repository && typeof manifest.repository.url === "string") { repository = manifest.repository.url; } if (!repository) { return null; } if (repository.startsWith("git@")) { repository = repository.replace(":", "/").replace("git@", "https://"); } let url; try { url = new URL(repository); } catch { return null; } const ownerWithRepo = url.pathname.split("/").slice(1, 3).join("/").replace(/\.git$/, ""); const branchName = branch ?? "HEAD"; if (url.hostname === "github.com") { return { contentUrl: `https://github.com/${ownerWithRepo}/blob/${branchName}`, imagesUrl: `https://github.com/${ownerWithRepo}/raw/${branchName}` }; } if (url.hostname === "gitlab.com") { return { contentUrl: `https://gitlab.com/${ownerWithRepo}/-/blob/${branchName}`, imagesUrl: `https://gitlab.com/${ownerWithRepo}/-/raw/${branchName}` }; } if (url.hostname === "gitea.com") { return { contentUrl: `https://gitea.com/${ownerWithRepo}/src/branch/${branchName}`, imagesUrl: `https://gitea.com/${ownerWithRepo}/raw/branch/${branchName}` }; } return null; } function detectMarkdownSyntax(content) { const result = {}; const hyphenBullets = content.match(/^[\t ]*-[\t ][^\n]+/gm); const asteriskBullets = content.match(/^[\t ]*\*[\t ][^\n]+/gm); const plusBullets = content.match(/^[\t ]*\+[\t ][^\n]+/gm); if (hyphenBullets?.length) result.bullet = "-"; else if (asteriskBullets?.length) result.bullet = "*"; else if (plusBullets?.length) result.bullet = "+"; const dotOrdered = content.match(/^\s*\d+\.[\t ][^\n]+/gm); const parenthesisOrdered = content.match(/^\s*\d+\)[\t ][^\n]+/gm); if (dotOrdered?.length) result.bulletOrdered = "."; else if (parenthesisOrdered?.length) result.bulletOrdered = ")"; const asteriskEmphasis = content.match(/(?<!\*)\*[^*\n]+\*(?!\*)/g); const underscoreEmphasis = content.match(/(?<!_)_[^_\n]+_(?!_)/g); if (asteriskEmphasis?.length) result.emphasis = "*"; else if (underscoreEmphasis?.length) result.emphasis = "_"; const asteriskStrong = content.match(/\*\*[^*\n]+\*\*/g); const underscoreStrong = content.match(/__[^_\n]+__/g); if (asteriskStrong?.length) result.strong = "*"; else if (underscoreStrong?.length) result.strong = "_"; const backtickFence = content.match(/^```[^`]*```/gm); const tildeFence = content.match(/^~~~[^~]*~~~/gm); if (backtickFence?.length) result.fence = "`"; else if (tildeFence?.length) result.fence = "~"; const listItems = content.match(/^[\t ]*([-+*]|\d+[.)])\s+[^\n]+/gm); if (listItems?.length) { const indentTypes = new Set( listItems.map((item) => { const leadingSpace = item.match(/^[\t ]*/)?.[0] || ""; if (leadingSpace.includes(" ")) return "tab"; return leadingSpace.length === 1 ? "one" : leadingSpace.length > 1 ? "mixed" : "one"; }) ); if (indentTypes.size === 1) { result.listItemIndent = indentTypes.values().next().value; } else { result.listItemIndent = "mixed"; } } const asteriskRule = content.match(/^[\t ]*(\*[\t ]*){3,}$/m); const hyphenRule = content.match(/^[\t ]*(-[\t ]*){3,}$/m); const underscoreRule = content.match(/^[\t ]*(_[\t ]*){3,}$/m); if (asteriskRule?.length) result.rule = "*"; else if (hyphenRule?.length) result.rule = "-"; else if (underscoreRule?.length) result.rule = "_"; return result; } // src/vsce-constants.ts var VSCE_TRUSTED_SOURCES = [ "api.bintray.com", "api.travis-ci.com", "api.travis-ci.org", "app.fossa.io", "badge.buildkite.com", "badge.fury.io", "badge.waffle.io", "badgen.net", "badges.frapsoft.com", "badges.gitter.im", "badges.greenkeeper.io", "cdn.travis-ci.com", "cdn.travis-ci.org", "ci.appveyor.com", "circleci.com", "cla.opensource.microsoft.com", "codacy.com", "codeclimate.com", "codecov.io", "coveralls.io", "david-dm.org", "deepscan.io", "dev.azure.com", "docs.rs", "flat.badgen.net", "gemnasium.com", "githost.io", "gitlab.com", "godoc.org", "goreportcard.com", "img.shields.io", "isitmaintained.com", "marketplace.visualstudio.com", "nodesecurity.io", "opencollective.com", "snyk.io", "travis-ci.com", "travis-ci.org", "visualstudio.com", "vsmarketplacebadges.dev", "www.bithound.io", "www.versioneye.com" ]; var VSCE_DEFAULT_IGNORE = [ ".vscodeignore", "package-lock.json", "npm-debug.log", "yarn.lock", "yarn-error.log", "npm-shrinkwrap.json", ".editorconfig", ".npmrc", ".yarnrc", ".gitattributes", "*.todo", "tslint.yaml", ".eslintrc*", ".babelrc*", ".prettierrc*", ".cz-config.js", ".commitlintrc*", "webpack.config.js", "ISSUE_TEMPLATE.md", "CONTRIBUTING.md", "PULL_REQUEST_TEMPLATE.md", "CODE_OF_CONDUCT.md", ".github", ".travis.yml", "appveyor.yml", "**/.git", "**/.git/**", "**/*.vsix", "**/.DS_Store", "**/*.vsixmanifest", "**/.vscode-test/**", "**/.vscode-test-web/**" ]; // src/files.ts function isLocalFile(file) { return file.type === "local"; } function isInMemoryFile(file) { return file.type === "in-memory"; } async function collect(manifest, options) { const { cwd = process__default.default.cwd(), ignoreFile = ".vscodeignore", dependencies = [], readme = "README.md" } = options; const gitignorePath = path2__default.default.join(cwd, ".gitignore"); const vscodeIgnorePath = path2__default.default.join(cwd, ignoreFile); const ig = ignore__default.default(); if (fs.existsSync(gitignorePath)) { const ignoreContent = await promises.readFile(gitignorePath, "utf8"); ig.add(ignoreContent); } if (fs.existsSync(vscodeIgnorePath)) { const vsceIgnoreContent = await promises.readFile(vscodeIgnorePath, "utf8"); ig.add(vsceIgnoreContent); } const globbedFiles = await tinyglobby.glob("**", { cwd, followSymbolicLinks: true, expandDirectories: true, ignore: [...VSCE_DEFAULT_IGNORE, "!package.json", `!${readme}`, "node_modules/**"], dot: true, onlyFiles: true }); const filteredFiles = globbedFiles.filter((file) => !ig.ignores(file)); const files = filteredFiles.map((file) => ({ type: "local", localPath: path2__default.default.join(cwd, file), path: path2__default.default.join("extension/", file) })); if (dependencies.length > 0) { for (const dep of dependencies) { files.push({ type: "local", path: path2__default.default.join("extension/node_modules", dep.name), localPath: dep.path }); } } return files; } async function getExtensionPackageManager(cwd) { const result = await packageManagerDetector.detect({ cwd }); if (result == null) { throw new Error("could not detect package manager"); } if (result.name === "deno" || result.name === "bun") { throw new Error(`unsupported package manager: ${result.name}`); } return result.name; } var DEFAULT_MIME_TYPES = /* @__PURE__ */ new Map([ [".json", "application/json"], [".vsixmanifest", "text/xml"], [".md", "text/markdown"], [".png", "image/png"], [".txt", "text/plain"], [".js", "application/javascript"], [".yml", "text/yaml"], [".html", "text/html"], [".markdown", "text/markdown"], [".css", "text/css"] ]); function getContentTypesForFiles(files) { const contentTypes = {}; for (const file of files) { const ext = path2__default.default.extname(file.path).toLowerCase(); if (ext == null) continue; if (!DEFAULT_MIME_TYPES.has(ext)) { const contentType = mime__default.default.getType(ext); if (contentType == null) { throw new Error(`could not determine content type for file: ${file.path}`); } contentTypes[ext] = contentType; } else { contentTypes[ext] = DEFAULT_MIME_TYPES.get(ext); } } const xml = ( /* xml */ `<?xml version="1.0" encoding="utf-8"?> <Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types"> ${Object.entries(contentTypes).map(([ext, contentType]) => `<Default Extension="${ext}" ContentType="${contentType}" /> `).join("")} </Types> ` ); return { xml, contentTypes }; } async function transformFiles(options) { const { manifest, files, readme } = options; const assets = []; let license; let icon; const hasLicenseFile = hasExtensionFile(files, ["LICENSE", "LICENSE.md", "LICENSE.txt", "LICENSE.markdown"]); const hasIconFile = hasExtensionFile(files, [manifest.icon]); const hasReadmeFile = hasExtensionFile(files, [readme, "README.md"]); const hasChangelogFile = hasExtensionFile(files, ["CHANGELOG.md", "CHANGELOG.markdown", "CHANGELOG.txt"]); const hasTranslationsFiles = hasExtensionFile(files, ["package.nls.json"]); if (hasLicenseFile.found) { if (!path2.extname(hasLicenseFile.path)) { const entryIndex = files.findIndex((f) => f.path === hasLicenseFile.path); if (entryIndex === -1) { throw new Error(`could not find license file: ${hasLicenseFile.path}`); } const entry = files[entryIndex]; files[entryIndex] = { ...entry, path: `${hasLicenseFile.path}.md` }; hasLicenseFile.path = files[entryIndex].path; } license = hasLicenseFile.path; assets.push({ type: "Microsoft.VisualStudio.Services.Content.License", path: license }); } if (hasIconFile.found) { icon = hasIconFile.path; assets.push({ type: "Microsoft.VisualStudio.Services.Icons.Default", path: icon }); } if (hasReadmeFile.found) { const entryIndex = files.findIndex((f) => f.path === hasReadmeFile.path); if (entryIndex === -1) { throw new Error("could not find readme file"); } const entry = files[entryIndex]; let contents = entry.type === "in-memory" ? entry.contents : await promises.readFile(entry.localPath, "utf-8"); if (typeof contents !== "string") { contents = String(contents); } files[entryIndex] = { type: "in-memory", contents: await transformMarkdown(manifest, { content: contents, ...options.markdown }), path: hasReadmeFile.path }; assets.push({ type: "Microsoft.VisualStudio.Services.Content.Details", path: hasReadmeFile.path }); } if (hasChangelogFile.found) { assets.push({ type: "Microsoft.VisualStudio.Services.Content.Changelog", path: hasChangelogFile.path }); } if (hasTranslationsFiles.found) { assets.push({ type: "Microsoft.VisualStudio.Code.Translation.en", path: hasTranslationsFiles.path }); } return { assets, icon, license }; } function hasExtensionFile(files, fileNames) { for (const fileName of fileNames) { if (fileName == null) continue; const file = files.find((f) => f.path.endsWith(fileName)); if (file) { return { found: true, path: file.path }; } } return { found: false, path: undefined }; } async function readProjectManifest(projectDir) { try { const manifestPath = path2__default.default.join(projectDir, "package.json"); const manifest = JSON.parse(await promises.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); } async function prepublish(options) { const { cwd, packageManager, manifest, preRelease } = options; if (!manifest.scripts) { return; } const scripts = []; if ("vscode:prepublish" in manifest.scripts) { scripts.push("vscode:prepublish"); } if ("vscode:prepublish:release" in manifest.scripts && !preRelease) { scripts.push("vscode:prepublish:release"); } if ("vscode:prepublish:prerelease" in manifest.scripts && preRelease) { scripts.push("vscode:prepublish:prerelease"); } if (scripts.length === 0) { return; } const results = await Promise.all( scripts.map((script) => runScript({ cwd, packageManager, script })) ); if (results.some((result) => !result)) { throw new Error("failed to run one or more scripts"); } } async function runScript(options) { const { cwd, packageManager, script } = options; try { const { stderr } = await new Promise((resolve2, reject) => { child_process.exec(`${packageManager} run ${script}`, { cwd }, (err, stdout, stderr2) => { if (err) { reject(err); } else { resolve2({ stdout, stderr: stderr2 }); } }); }); if (stderr.trim() !== "") { return false; } return true; } catch { return false; } } function validateVSCodeTypesCompatability(engineVersion, typesVersion) { if (engineVersion === "*") { return; } if (semver.validRange(engineVersion) == null) { throw new Error(`invalid engine version '${engineVersion}'`); } if (semver.validRange(typesVersion) == null) { throw new Error(`invalid types version '${typesVersion}'`); } const [engineMajor, engineMinor] = engineVersion.replace(/^\D+/, "").replace(/x/g, "0").split(".").map((x) => Number.parseInt(x, 10)); const [typesMajor, typesMinor] = typesVersion.replace(/^\D+/, "").replace(/x/g, "0").split(".").map((x) => Number.parseInt(x, 10) || 0); if (typesMajor == null || typesMinor == null || engineMajor == null || engineMinor == null) { throw new Error("invalid engine or types version"); } if (typesMajor > engineMajor || typesMajor === engineMajor && typesMinor > engineMinor) { throw new Error( `@types/vscode version ${typesVersion} is higher than the specified engine version ${engineVersion}` ); } } var ALLOWED_SPONSOR_PROTOCOLS = ["http:", "https:"]; var VALID_EXTENSION_KINDS = ["ui", "workspace"]; var EXTENSION_PRICING = ["Free", "Trial"]; var EXTENSION_NAME_REGEX = /^[a-z0-9][a-z0-9\-]*$/i; var VSCODE_ENGINE_COMPATIBILITY_REGEX = /^\*$|^(\^|>=)?((\d+)|x)\.((\d+)|x)\.((\d+)|x)(-.*)?$/; var GITHUB_BADGE_URL_REGEX = /^https:\/\/github\.com\/[^/]+\/[^/]+\/(actions\/)?workflows\/.*badge\.svg/; async function validateProjectManifest(manifest) { const errors = []; if (manifest.name == null) { errors.push({ field: "name", message: "The `name` field is required.", type: "MISSING_FIELD" }); } if (manifest.version == null) { errors.push({ field: "version", message: "The `version` field is required.", type: "MISSING_FIELD" }); } if (manifest.publisher == null) { errors.push({ field: "publisher", message: "The `publisher` field is required.", type: "MISSING_FIELD" }); } if (manifest.engines == null) { errors.push({ field: "engines", message: "The `engines` field is required.", type: "MISSING_FIELD" }); } if (manifest.engines?.vscode == null) { errors.push({ field: "engines.vscode", message: "The `engines.vscode` field is required.", type: "MISSING_FIELD" }); } const vscodeEngineVersion = manifest.engines?.vscode ?? ""; if (!VSCODE_ENGINE_COMPATIBILITY_REGEX.test(vscodeEngineVersion)) { errors.push({ type: "INVALID_VALUE", field: "engines.vscode", message: "The `engines.vscode` field must be a valid semver version range, or 'x' for any version." }); } const engines = { ...manifest.engines || {}, vscode: vscodeEngineVersion }; const name = manifest.name || ""; if (!EXTENSION_NAME_REGEX.test(name)) { errors.push({ type: "INVALID_VALUE", field: "name", message: "The `name` field should be an identifier and not its human-friendly name." }); } const version = manifest.version || ""; if (semver.valid(version) == null) { errors.push({ type: "INVALID_VALUE", field: "version", message: "The `version` field must be a valid semver version." }); } const publisher = manifest.publisher || ""; if (!EXTENSION_NAME_REGEX.test(publisher)) { errors.push({ type: "INVALID_VALUE", field: "publisher", message: "The `publisher` field should be an identifier and not its human-friendly name." }); } if (manifest.pricing && !EXTENSION_PRICING.includes(manifest.pricing)) { errors.push({ type: "INVALID_PRICING", value: manifest.pricing, message: "The `pricing` field must be either 'Free' or 'Paid'." }); } const hasActivationEvents = !!manifest.activationEvents; const hasImplicitLanguageActivationEvents = manifest.contributes?.languages; const hasOtherImplicitActivationEvents = manifest.contributes?.commands || manifest.contributes?.authentication || manifest.contributes?.customEditors || manifest.contributes?.views; const hasImplicitActivationEvents = hasImplicitLanguageActivationEvents || hasOtherImplicitActivationEvents; const hasMain = !!manifest.main; const hasBrowser = !!manifest.browser; if (hasActivationEvents || (vscodeEngineVersion === "*" || semver.satisfies(vscodeEngineVersion, ">=1.74", { includePrerelease: true })) && hasImplicitActivationEvents) { if (!hasMain && !hasBrowser && (hasActivationEvents || !hasImplicitLanguageActivationEvents)) { errors.push( { type: "MISSING_FIELD", field: "main", message: "The use of `activationEvents` field requires either `browser` or `main` to be set." }, { type: "MISSING_FIELD", field: "browser", message: "The use of `activationEvents` field requires either `browser` or `main` to be set." } ); } } else if (hasMain) { errors.push({ type: "MISSING_FIELD", field: "activationEvents", message: "Manifest needs the 'activationEvents' property, given it has a 'main' property." }); } else if (hasBrowser) { errors.push({ type: "MISSING_FIELD", field: "activationEvents", message: "Manifest needs the 'activationEvents' property, given it has a 'browser' property." }); } if (manifest.devDependencies != null && manifest.devDependencies["@types/vscode"] != null) { try { validateVSCodeTypesCompatability(engines.vscode, manifest.devDependencies["@types/vscode"]); } catch { errors.push({ type: "VSCODE_TYPES_INCOMPATIBILITY", message: "@types/vscode version is either higher than the specified engine version or invalid" }); } } if (manifest.icon?.endsWith(".svg")) { errors.push({ type: "INVALID_ICON", field: "icon", message: "SVG icons are not supported. Use PNG icons instead." }); } if (manifest.badges != null) { for (const badge of manifest.badges) { const decodedUrl = decodeURI(badge.url); let srcURL = null; try { srcURL = new URL(decodedUrl); } catch { errors.push({ type: "INVALID_BADGE_URL", field: "badges", message: `The badge URL '${decodedUrl}' must be a valid URL.` }); } if (!decodedUrl.startsWith("https://")) { errors.push({ type: "INVALID_BADGE_URL", field: "badges", message: "Badge URL must use the 'https' protocol" }); } if (decodedUrl.endsWith(".svg")) { errors.push({ type: "INVALID_BADGE_URL", field: "badges", message: "SVG badges are not supported. Use PNG badges instead" }); } if (srcURL && !(srcURL.host != null && VSCE_TRUSTED_SOURCES.includes(srcURL.host.toLowerCase()) || GITHUB_BADGE_URL_REGEX.test(srcURL.href))) { errors.push({ type: "UNTRUSTED_HOST", field: "badges", message: "Badge URL must use a trusted host" }); } } } if (manifest.dependencies != null && manifest.dependencies.vscode != null) { errors.push({ type: "DEPENDS_ON_VSCODE_IN_DEPENDENCIES", field: "dependencies.vscode", message: `You should not depend on 'vscode' in your 'dependencies'. Did you mean to add it to 'devDependencies'?` }); } if (manifest.extensionKind != null) { const extensionKinds = Array.isArray(manifest.extensionKind) ? manifest.extensionKind : [manifest.extensionKind]; for (const extensionKind of extensionKinds) { if (!VALID_EXTENSION_KINDS.includes(extensionKind)) { errors.push({ type: "INVALID_EXTENSION_KIND", field: "extensionKind", message: `Invalid extension kind '${extensionKind}'. Expected one of: ${VALID_EXTENSION_KINDS.join(", ")}` }); } } } if (manifest.sponsor != null && manifest.sponsor.url != null) { try { const sponsorUrl = new URL(manifest.sponsor.url); if (!ALLOWED_SPONSOR_PROTOCOLS.includes(sponsorUrl.protocol)) { errors.push({ type: "INVALID_SPONSOR_URL", field: "sponsor.url", message: `The protocol '${sponsorUrl.protocol.slice(0, sponsorUrl.protocol.lastIndexOf(":"))}' is not allowed. Use one of: ${ALLOWED_SPONSOR_PROTOCOLS.map((protocol) => protocol.slice(0, protocol.lastIndexOf(":"))).join(", ")}` }); } } catch { errors.push({ type: "INVALID_SPONSOR_URL", field: "sponsor.url", message: "The `sponsor.url` field must be a valid URL." }); } } if (errors.length === 0) { return null; } return errors; } async function writeVsix(options) { const { files, packagePath, force, epoch } = options; if (!files || files.length === 0) { throw new Error("no files specified to package"); } if (!packagePath) { throw new Error("no package path specified"); } if (fs.existsSync(packagePath)) { if (!force) { throw new Error(`package already exists at ${packagePath}`); } await promises.unlink(packagePath); } const zip = new yazl__default.default.ZipFile(); const zipOptions = {}; if (epoch != null) { zipOptions.mtime = new Date(epoch * 1e3); files.sort((a, b) => a.path.localeCompare(b.path)); } try { for (const f of files) { if (isInMemoryFile(f)) { zip.addBuffer( typeof f.contents === "string" ? buffer.Buffer.from(f.contents, "utf8") : f.contents, f.path, { ...zipOptions } ); continue; } zip.addFile(f.localPath, f.path, { ...zipOptions }); } zip.end(); const zipStream = fs.createWriteStream(packagePath, { mode: promises.constants.S_IRWXU | promises.constants.S_IRGRP | promises.constants.S_IXGRP | promises.constants.S_IROTH | promises.constants.S_IXOTH }); zip.outputStream.pipe(zipStream); await new Promise((resolve2, reject) => { zipStream.once("finish", resolve2); zipStream.once("error", reject); zip.once("error", (err) => { zipStream.destroy(); reject(err); }); }); return true; } catch (err) { if (fs.existsSync(packagePath)) { await promises.unlink(packagePath); } if (err instanceof Error) { throw new TypeError(`failed to create package: ${err.message}`); } throw new Error(`failed to create package: ${err}`); } } async function readVsix(options) { const { packagePath } = options; const zip = await new Promise((resolve2, reject) => { yauzl__default.default.open(packagePath, { lazyEntries: true }, (err, zip2) => { if (err) { reject(err); } else { resolve2(zip2); } }); }); const files = await new Promise((resolve2, reject) => { const fileMap = /* @__PURE__ */ new Map(); zip.once("close", () => resolve2(fileMap)); zip.on("entry", async (entry) => { try { const buffer = await new Promise((resolve3, reject2) => { zip.openReadStream(entry, (err, stream) => { if (err) reject2(err); stream.once("error", reject2); stream.once("end", () => { if (stream.readable) { reject2(new Error("Stream ended before all data was read")); } }); return resolve3(bufferStream(stream)); }); }); fileMap.set(entry.fileName, buffer); zip.readEntry(); } catch (err) { zip.close(); reject(err); } }); zip.readEntry(); }); if (!files.has("extension.vsixmanifest")) { throw new Error("extension.vsixmanifest file is missing"); } const rawManifest = files.get("extension.vsixmanifest").toString("utf8"); const parser = new fastXmlParser.XMLParser({ preserveOrder: false, transformTagName(tagName) { return tagName.toLowerCase(); } }); const manifest = parser.parse(rawManifest); return { files, manifest }; } async function bufferStream(stream) { return await new Promise((resolve2, reject) => { const buffers = []; stream.on("data", (buffer) => buffers.push(buffer)); stream.once("error", reject); stream.once("end", () => resolve2(buffer.Buffer.concat(buffers))); }); } exports.ALLOWED_SPONSOR_PROTOCOLS = ALLOWED_SPONSOR_PROTOCOLS; exports.EXTENSION_NAME_REGEX = EXTENSION_NAME_REGEX; exports.EXTENSION_PRICING = EXTENSION_PRICING; exports.GITHUB_BADGE_URL_REGEX = GITHUB_BADGE_URL_REGEX; exports.VALID_EXTENSION_KINDS = VALID_EXTENSION_KINDS; exports.VSCODE_ENGINE_COMPATIBILITY_REGEX = VSCODE_ENGINE_COMPATIBILITY_REGEX; exports.collect = collect; exports.createVsixManifest = createVsixManifest; exports.getContentTypesForFiles = getContentTypesForFiles; exports.getExtensionDependencies = getExtensionDependencies; exports.getExtensionPackageManager = getExtensionPackageManager; exports.getManifestTags = getManifestTags; exports.inferBaseUrls = inferBaseUrls; exports.isInMemoryFile = isInMemoryFile; exports.isLocalFile = isLocalFile; exports.isWebKind = isWebKind; exports.prepublish = prepublish; exports.readProjectManifest = readProjectManifest; exports.readVsix = readVsix; exports.transformExtensionKind = transformExtensionKind; exports.transformFiles = transformFiles; exports.transformMarkdown = transformMarkdown; exports.validateProjectManifest = validateProjectManifest; exports.validateVSCodeTypesCompatability = validateVSCodeTypesCompatability; exports.writeVsix = writeVsix;