diff --git a/src/package.ts b/src/package.ts index 8d7c2d3f..12bed6f3 100644 --- a/src/package.ts +++ b/src/package.ts @@ -14,7 +14,7 @@ import * as cheerio from 'cheerio'; import * as url from 'url'; import { lookup } from 'mime'; import * as urljoin from 'url-join'; -import { validatePublisher, validateExtensionName, validateVersion, validateEngineCompatibility } from './validation'; +import { validatePublisher, validateExtensionName, validateVersion, validateEngineCompatibility, validateVSCodeTypesCompatibility } from './validation'; import { getDependencies } from './npm'; interface IReadFile { @@ -605,6 +605,10 @@ export function validateManifest(manifest: Manifest): Manifest { validateEngineCompatibility(manifest.engines['vscode']); + if (manifest.devDependencies && manifest.devDependencies['@types/vscode']) { + validateVSCodeTypesCompatibility(manifest.engines['vscode'], manifest.devDependencies['@types/vscode']); + } + if (/\.svg$/i.test(manifest.icon || '')) { throw new Error(`SVGs can't be used as icons: ${manifest.icon}`); } diff --git a/src/test/validation.test.ts b/src/test/validation.test.ts index 6084f767..fa4508b2 100644 --- a/src/test/validation.test.ts +++ b/src/test/validation.test.ts @@ -1,5 +1,5 @@ import * as assert from 'assert'; -import { validatePublisher, validateExtensionName, validateVersion, validateEngineCompatibility } from '../validation'; +import { validatePublisher, validateExtensionName, validateVersion, validateEngineCompatibility, validateVSCodeTypesCompatibility } from '../validation'; describe('validatePublisher', () => { it('should throw with empty', () => { @@ -98,4 +98,31 @@ describe('validateEngineCompatibility', () => { assert.throws(() => validateVersion('>=1')); assert.throws(() => validateVersion('>=1.0')); }); +}); + +describe('validateVSCodeTypesCompatibility', () => { + + it('should validate', () => { + validateVSCodeTypesCompatibility('*', '1.30.0'); + validateVSCodeTypesCompatibility('*', '^1.30.0'); + validateVSCodeTypesCompatibility('*', '~1.30.0'); + + validateVSCodeTypesCompatibility('1.30.0', '1.30.0'); + validateVSCodeTypesCompatibility('1.30.0', '1.20.0'); + + assert.throws(() => validateVSCodeTypesCompatibility('1.30.0', '1.40.0')); + assert.throws(() => validateVSCodeTypesCompatibility('1.30.0', '^1.40.0')); + assert.throws(() => validateVSCodeTypesCompatibility('1.30.0', '~1.40.0')); + + assert.throws(() => validateVSCodeTypesCompatibility('1.30.0', '1.40.0')); + assert.throws(() => validateVSCodeTypesCompatibility('^1.30.0', '1.40.0')); + assert.throws(() => validateVSCodeTypesCompatibility('~1.30.0', '1.40.0')); + + assert.throws(() => validateVSCodeTypesCompatibility('1.x.x', '1.30.0')); + assert.throws(() => validateVSCodeTypesCompatibility('1.x.0', '1.30.0')); + + assert.throws(() => validateVSCodeTypesCompatibility('1.5.0', '1.30.0')); + assert.throws(() => validateVSCodeTypesCompatibility('1.5', '1.30.0')); + assert.throws(() => validateVSCodeTypesCompatibility('1.5', '1.30')); + }); }); \ No newline at end of file diff --git a/src/validation.ts b/src/validation.ts index 170c71ab..2d7290e4 100644 --- a/src/validation.ts +++ b/src/validation.ts @@ -1,4 +1,5 @@ import * as semver from 'semver'; +import * as parseSemver from 'parse-semver'; const nameRegex = /^[a-z0-9][a-z0-9\-]*$/i; @@ -40,4 +41,63 @@ export function validateEngineCompatibility(version: string): void { if (!/^\*$|^(\^|>=)?((\d+)|x)\.((\d+)|x)\.((\d+)|x)(\-.*)?$/.test(version)) { throw new Error(`Invalid vscode engine compatibility version '${version}'`); } +} + +/** + * User shouldn't use a newer version of @types/vscode than the one specified in engines.vscode + */ +export function validateVSCodeTypesCompatibility(engineVersion: string, typeVersion: string): void { + if (engineVersion === '*') { + return; + } + + if (!typeVersion) { + throw new Error(`Missing @types/vscode version`); + } + + let plainEngineVersion: string, plainTypeVersion: string; + + try { + const engineSemver = parseSemver(`vscode@${engineVersion}`); + plainEngineVersion = engineSemver.version; + } catch (err) { + throw new Error('Failed to parse semver of engines.vscode'); + } + + try { + const typeSemver = parseSemver(`@types/vscode@${typeVersion}`); + plainTypeVersion = typeSemver.version; + } catch (err) { + throw new Error('Failed to parse semver of @types/vscode'); + } + + // For all `x`, use smallest version for comparison + plainEngineVersion = plainEngineVersion.replace(/x/g, '0'); + + const [typeMajor, typeMinor, typePatch] = plainTypeVersion.split('.').map(x => { + try { + return parseInt(x); + } catch (err) { + return 0; + } + }); + const [engineMajor, engineMinor, enginePatch] = plainEngineVersion.split('.').map(x => { + try { + return parseInt(x); + } catch (err) { + return 0; + } + }); + + const error = new Error(`@types/vscode ${typeVersion} greater than engines.vscode ${engineVersion}. Consider upgrade engines.vscode or use an older @types/vscode version`); + + if (typeof typeMajor === 'number' && typeof engineMajor === 'number' && typeMajor > engineMajor) { + throw error; + } + if (typeof typeMinor === 'number' && typeof engineMinor === 'number' && typeMinor > engineMinor) { + throw error; + } + if (typeof typePatch === 'number' && typeof enginePatch === 'number' && typePatch > enginePatch) { + throw error; + } } \ No newline at end of file