UNPKG

vsix-utils

Version:

utilities for working with VSIX packages

296 lines (290 loc) 10.3 kB
'use strict'; var semver = require('semver'); // src/validation.ts // 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" ]; // src/validation.ts 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; } 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.validateProjectManifest = validateProjectManifest; exports.validateVSCodeTypesCompatability = validateVSCodeTypesCompatability;