vsix-utils
Version:
utilities for working with VSIX packages
296 lines (290 loc) • 10.3 kB
JavaScript
'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;