From 9378b577d226f48b1a2a4d0e0f03135b511f32cf Mon Sep 17 00:00:00 2001 From: Tony Ross Date: Thu, 12 Sep 2019 23:01:45 -0500 Subject: [PATCH 1/2] New: Add helpers to get browser names and support details Ref #2896 --- packages/utils/README.md | 3 + .../utils/scripts/mdn-browser-compat-data.js | 6 +- packages/utils/src/compat/browsers.ts | 104 ++++++++++++++---- packages/utils/src/compat/cache.ts | 2 +- packages/utils/src/compat/css.ts | 32 ++++-- packages/utils/src/compat/html.ts | 4 +- packages/utils/src/compat/support.ts | 38 +++++-- packages/utils/tests/compat/browsers.ts | 72 +++++++++--- 8 files changed, 194 insertions(+), 67 deletions(-) diff --git a/packages/utils/README.md b/packages/utils/README.md index 86b8e2c070f..c406959717d 100644 --- a/packages/utils/README.md +++ b/packages/utils/README.md @@ -33,7 +33,10 @@ hints inside the extended configurations. ### compat +* `getFriendlyName`: Get the friendly name of a browser from an id. * `getUnsupported`: Get browsers without support for CSS or HTML features. +* `getUnsupportedDetails`: Get browsers without support with details on + when support was added or removed. * `isSupported`: Query MDN for support of CSS or HTML features. ### configStore diff --git a/packages/utils/scripts/mdn-browser-compat-data.js b/packages/utils/scripts/mdn-browser-compat-data.js index db1ace65954..8f365413bc4 100644 --- a/packages/utils/scripts/mdn-browser-compat-data.js +++ b/packages/utils/scripts/mdn-browser-compat-data.js @@ -233,7 +233,7 @@ const removeFeatures = (data) => { */ const removeBrowserDetails = (browsers) => { for (const browserName of Object.keys(browsers)) { - browsers[browserName] = /** @type {any} */({}); + browsers[browserName] = /** @type {any} */({ name: browsers[browserName].name }); } }; @@ -251,10 +251,10 @@ removeFeatures(data.css); removeFeatures(data.html); const code = `/* eslint-disable */ -import { PrimaryIdentifier } from 'mdn-browser-compat-data/types'; +import { Browsers, PrimaryIdentifier } from 'mdn-browser-compat-data/types'; type Data = { - browsers: PrimaryIdentifier; + browsers: Browsers; css: PrimaryIdentifier; html: PrimaryIdentifier; } diff --git a/packages/utils/src/compat/browsers.ts b/packages/utils/src/compat/browsers.ts index 79232fc3ce6..12c203c72da 100644 --- a/packages/utils/src/compat/browsers.ts +++ b/packages/utils/src/compat/browsers.ts @@ -1,7 +1,17 @@ import { Identifier, SimpleSupportStatement, SupportStatement } from 'mdn-browser-compat-data/types'; import * as semver from 'semver'; -export type UnsupportedBrowsers = string[] | null; +import { mdn } from './browser-compat-data'; + +export type SupportDetails = { + versionAdded?: string; + versionRemoved?: string; +}; + +export type UnsupportedBrowsers = { + browsers: string[]; + details: Map; +}; const enum Support { No, @@ -9,6 +19,10 @@ const enum Support { Unknown } +type SupportStatus = SupportDetails & { + support: Support; +}; + // Map `browserslist` browser names to MDN ones. const browserToMDN = new Map([ ['and_chr', 'chrome_android'], @@ -32,30 +46,34 @@ const coerce = (version: string): string | semver.SemVer => { return semver.coerce(version) || version; }; +const normalizeBrowserName = (name: string) => { + return browserToMDN.get(name) || name; +}; + /** * Intepret if the provided statement indicates support for the given browser version. */ -const isSupported = (support: SimpleSupportStatement, prefix: string, rawVersion: string): Support => { +const isSupported = (support: SimpleSupportStatement, prefix: string, rawVersion: string): SupportStatus => { const version = coerce(rawVersion); // Ignore support that requires users to enable a flag. if (support.flags) { - return Support.Unknown; + return { support: Support.Unknown }; } // If feature doesn't match the same prefix, then it's not supported. if (prefix !== (support.prefix || '')) { - return Support.No; + return { support: Support.No }; } // If feature was never added, then it's not supported. if (support.version_added === false) { - return Support.No; + return { support: Support.No }; } // If a feature was removed before the target version, it's not supported. if (typeof support.version_removed === 'string' && semver.lte(coerce(support.version_removed), version)) { - return Support.No; + return { support: Support.No, versionRemoved: support.version_removed }; } /* @@ -67,33 +85,41 @@ const isSupported = (support: SimpleSupportStatement, prefix: string, rawVersion * https://github.com/mdn/browser-compat-data/issues/3021 */ if (support.version_added === true) { - return Support.Yes; + return { support: Support.Yes }; } // If feature was added by the target version, it's supported; if after it's not. if (typeof support.version_added === 'string') { - return semver.lte(coerce(support.version_added), version) ? Support.Yes : Support.No; + if (semver.lte(coerce(support.version_added), version)) { + return { support: Support.Yes }; + } + + return { support: Support.No, versionAdded: support.version_added }; } // Ignore all other cases (e.g. if a feature was removed but we don't know when). - return Support.Unknown; + return { support: Support.Unknown }; }; /** - * Interpret if the provided support statements indicate the given browser version in supported. + * Interpret if the provided support statements indicate the given browser version is supported. */ -const isBrowserUnsupported = (support: SupportStatement, prefix: string, version: string): boolean => { +const isBrowserSupported = (support: SupportStatement, prefix: string, version: string): SupportStatus => { // Convert single entries to an array for consistent handling. const browserSupport = Array.isArray(support) ? support : [support]; - let status = Support.Unknown; + const details: SupportStatus = { support: Support.Unknown }; // Items are listed from newest to oldest. The first clear rule wins. for (const simpleSupport of browserSupport) { - switch (isSupported(simpleSupport, prefix, version)) { + const status = isSupported(simpleSupport, prefix, version); + + switch (status.support) { case Support.Yes: - return false; + return { support: Support.Yes }; case Support.No: - status = Support.No; + details.support = Support.No; + details.versionAdded = status.versionAdded || details.versionAdded; + details.versionRemoved = status.versionRemoved || details.versionRemoved; break; // Keep looking in case a feature was temporarily removed or is prefixed. case Support.Unknown: default: @@ -101,7 +127,22 @@ const isBrowserUnsupported = (support: SupportStatement, prefix: string, version } } - return status === Support.No; + if (details.support === Support.Unknown) { + details.support = Support.Yes; + } + + return details; +}; + +/** + * Retrieve the friendly name of the provided browser + * (e.g. "Internet Explorer" for "ie"). + */ +export const getFriendlyName = (browser: string): string => { + const [name] = browser.split(' '); + const data = mdn.browsers[normalizeBrowserName(name)]; + + return data.name; }; /** @@ -112,30 +153,45 @@ const isBrowserUnsupported = (support: SupportStatement, prefix: string, version * @param feature An MDN feature `Identifier` with `__compat` data. * @param browsers A list of target browsers (e.g. `['chrome 74', 'ie 11']`). */ -export const getUnsupportedBrowsers = (feature: Identifier | undefined, prefix: string, browsers: string[]): UnsupportedBrowsers => { +export const getUnsupportedBrowsers = (feature: Identifier | undefined, prefix: string, browsers: string[]): UnsupportedBrowsers | null => { if (!feature || !feature.__compat) { return null; // Assume support if no matching feature was provided. } const support = feature.__compat.support; const unsupported: string[] = []; + const details = new Map(); for (const browser of browsers) { const [name, versionStr] = browser.split(' '); - const mdnBrowser = browserToMDN.get(name)!; + const mdnBrowser = normalizeBrowserName(name); const browserSupport = support[mdnBrowser]; const versions = versionStr.split('-'); // Handle 'android 4.4.3-4.4.4'. - if (browserSupport) { - const isUnsupported = versions.some((version) => { - return isBrowserUnsupported(browserSupport, prefix, version); - }); + if (!browserSupport) { + continue; + } + + for (const version of versions) { + const status = isBrowserSupported(browserSupport, prefix, version); + + if (status.support === Support.No) { + const supportDetails: SupportDetails = {}; - if (isUnsupported) { + if (status.versionAdded) { + supportDetails.versionAdded = status.versionAdded; + } + + if (status.versionRemoved) { + supportDetails.versionRemoved = status.versionRemoved; + } + + details.set(browser, supportDetails); unsupported.push(browser); + break; } } } - return unsupported.length ? unsupported : null; + return unsupported.length ? { browsers: unsupported, details } : null; }; diff --git a/packages/utils/src/compat/cache.ts b/packages/utils/src/compat/cache.ts index eda1666668b..8056f27791d 100644 --- a/packages/utils/src/compat/cache.ts +++ b/packages/utils/src/compat/cache.ts @@ -1,4 +1,4 @@ -type Value = string[] | null; +type Value = any | null; const cache = new Map>(); diff --git a/packages/utils/src/compat/css.ts b/packages/utils/src/compat/css.ts index f7dda4e0c48..a17768ded1a 100644 --- a/packages/utils/src/compat/css.ts +++ b/packages/utils/src/compat/css.ts @@ -53,7 +53,7 @@ const getTokens = (nodes: any[]): [string, string][] => { * sub-features in the provided context, using `matches` data to test * each tokenized string from the value. */ -const getPartialValueUnsupported = (context: Identifier, value: string, browsers: string[]): UnsupportedBrowsers => { +const getPartialValueUnsupported = (context: Identifier, value: string, browsers: string[]): UnsupportedBrowsers | null => { const prefix = vendor.prefix(value); const unprefixedValue = vendor.unprefixed(value); const tokens = getTokens(valueParser(value).nodes); @@ -96,7 +96,7 @@ const getPartialValueUnsupported = (context: Identifier, value: string, browsers * Determine if the provided CSS value is supported, first by looking for an * exact match for the full value, falling back to search for a partial match. */ -const getValueUnsupported = (context: Identifier, value: string, browsers: string[]): UnsupportedBrowsers => { +const getValueUnsupported = (context: Identifier, value: string, browsers: string[]): UnsupportedBrowsers | null => { const [data, prefix] = getFeatureData(context, value); if (data) { @@ -110,7 +110,7 @@ const getValueUnsupported = (context: Identifier, value: string, browsers: strin * Determine if the provided CSS declaration consisting of a property * and optionally a value is supported (e.g. `border-radius` or `display: grid`). */ -export const getDeclarationUnsupported = (feature: DeclarationQuery, browsers: string[]): UnsupportedBrowsers => { +export const getDeclarationUnsupported = (feature: DeclarationQuery, browsers: string[]): UnsupportedBrowsers | null => { const key = `css-declaration:${feature.property}|${feature.value || ''}`; return getCachedValue(key, browsers, () => { @@ -127,7 +127,7 @@ export const getDeclarationUnsupported = (feature: DeclarationQuery, browsers: s /** * Determine if the provided CSS at-rule is supported (e.g. `keyframes`). */ -export const getRuleUnsupported = (feature: RuleQuery, browsers: string[]): UnsupportedBrowsers => { +export const getRuleUnsupported = (feature: RuleQuery, browsers: string[]): UnsupportedBrowsers | null => { return getCachedValue(`css-rule:${feature.rule}`, browsers, () => { const [data, prefix] = getFeatureData(mdn.css['at-rules'], feature.rule); @@ -135,7 +135,7 @@ export const getRuleUnsupported = (feature: RuleQuery, browsers: string[]): Unsu }); }; -const getPseudoSelectorUnsupported = (value: string, browsers: string[]): UnsupportedBrowsers => { +const getPseudoSelectorUnsupported = (value: string, browsers: string[]): UnsupportedBrowsers | null => { const name = value.replace(/^::?/, ''); // Strip leading `:` or `::`. return getCachedValue(`css-pseudo-selector:${name}`, browsers, () => { @@ -153,20 +153,34 @@ const getPseudoSelectorUnsupported = (value: string, browsers: string[]): Unsupp * special cases (e.g. newer attribute and combinator selectors) need special * handling to map to the MDN data (which hasn't been done yet). */ -export const getSelectorUnsupported = (feature: SelectorQuery, browsers: string[]): UnsupportedBrowsers => { +export const getSelectorUnsupported = (feature: SelectorQuery, browsers: string[]): UnsupportedBrowsers | null => { const parser = selectorParser(); const root = parser.astSync(feature.selector); // eslint-disable-line no-sync - let unsupported: string[] = []; + const unsupported: UnsupportedBrowsers = { + browsers: [], + details: new Map() + }; // https://github.com/postcss/postcss-selector-parser/blob/master/API.md#containerwalk-proxies root.walkPseudos((node: { value: string }) => { const result = getPseudoSelectorUnsupported(node.value, browsers); if (result) { - unsupported = [...unsupported, ...result]; + unsupported.browsers = [...unsupported.browsers, ...result.browsers]; + + /* + * Note: Details can be incorrect if multiple parts of a selector + * are unsupported. Currently details will be set based on the + * last part of the selector which was unsupported. + * + * TODO: Fix by requiring callers to parse the selector instead. + */ + for (const [browser, details] of result.details) { + unsupported.details.set(browser, details); + } } }); - return unsupported.length ? unsupported : null; + return unsupported.browsers.length ? unsupported : null; }; diff --git a/packages/utils/src/compat/html.ts b/packages/utils/src/compat/html.ts index 640b59df9b4..87c36d8aff0 100644 --- a/packages/utils/src/compat/html.ts +++ b/packages/utils/src/compat/html.ts @@ -20,7 +20,7 @@ export type ElementQuery = { * a context element and/or value is supported (e.g. `{ attribute: 'hidden' }` * or `{ attribute: 'rel', element: 'link', value: 'stylesheet' }`). */ -export const getAttributeUnsupported = (feature: AttributeQuery, browsers: string[]): UnsupportedBrowsers => { +export const getAttributeUnsupported = (feature: AttributeQuery, browsers: string[]): UnsupportedBrowsers | null => { const key = `html-attribute:${feature.element || ''}|${feature.attribute}|${feature.value || ''}`; return getCachedValue(key, browsers, () => { @@ -56,7 +56,7 @@ export const getAttributeUnsupported = (feature: AttributeQuery, browsers: strin /** * Determine if the provided HTML element is supported (e.g. `details`). */ -export const getElementUnsupported = (feature: ElementQuery, browsers: string[]): UnsupportedBrowsers => { +export const getElementUnsupported = (feature: ElementQuery, browsers: string[]): UnsupportedBrowsers | null => { return getCachedValue(`html-element:${feature.element}`, browsers, () => { const [data, prefix] = getFeatureData(mdn.html.elements, feature.element); diff --git a/packages/utils/src/compat/support.ts b/packages/utils/src/compat/support.ts index 01af1be2174..022d003990d 100644 --- a/packages/utils/src/compat/support.ts +++ b/packages/utils/src/compat/support.ts @@ -16,21 +16,19 @@ import { ElementQuery } from './html'; +export { + getFriendlyName, + UnsupportedBrowsers +} from './browsers'; + export type FeatureQuery = AttributeQuery | DeclarationQuery | ElementQuery | RuleQuery | SelectorQuery; /** - * ```js - * getUnsupported({ element: 'details' }, ['chrome 74', 'ie 11']); // ['ie 11'] - * getUnsupported({ attribute: 'hidden' }, browsers); - * getUnsupported({ attribute: 'rel', element: 'link', value: 'noopener' }, ['edge 12', 'firefox 63']); // ['edge 12'] - * getUnsupported({ property: 'border-radius' }, browsers); - * getUnsupported({ property: 'color', value: '#00FF00FF' }, browsers); - * getUnsupported({ property: 'transform', value: 'translate3d(0, 0, 10px)' }, browsers); - * getUnsupported({ rule: '@supports' }, browsers); - * getUnsupported({ selector: 'input:invalid' }, browsers); - * ``` + * Similar to `getUnsupported`, but returns an object with both a list of + * `browsers` which were unsupported and a map of browsers to `browserDetails` + * to get additional information (e.g. what version the feature is added in). */ -export const getUnsupported = (feature: FeatureQuery, browsers: string[]): UnsupportedBrowsers => { +export const getUnsupportedDetails = (feature: FeatureQuery, browsers: string[]): UnsupportedBrowsers | null => { if ('attribute' in feature) { return getAttributeUnsupported(feature, browsers); } else if ('element' in feature) { @@ -44,6 +42,24 @@ export const getUnsupported = (feature: FeatureQuery, browsers: string[]): Unsup return getSelectorUnsupported(feature, browsers); }; +/** + * ```js + * getUnsupported({ element: 'details' }, ['chrome 74', 'ie 11']); // ['ie 11'] + * getUnsupported({ attribute: 'hidden' }, browsers); + * getUnsupported({ attribute: 'rel', element: 'link', value: 'noopener' }, ['edge 12', 'firefox 63']); // ['edge 12'] + * getUnsupported({ property: 'border-radius' }, browsers); + * getUnsupported({ property: 'color', value: '#00FF00FF' }, browsers); + * getUnsupported({ property: 'transform', value: 'translate3d(0, 0, 10px)' }, browsers); + * getUnsupported({ rule: '@supports' }, browsers); + * getUnsupported({ selector: 'input:invalid' }, browsers); + * ``` + */ +export const getUnsupported = (feature: FeatureQuery, browsers: string[]): string[] | null => { + const data = getUnsupportedDetails(feature, browsers); + + return data && data.browsers; +}; + export const getSupported = (feature: FeatureQuery, browsers: string[]): string[] | null => { const unsupported = getUnsupported(feature, browsers); diff --git a/packages/utils/tests/compat/browsers.ts b/packages/utils/tests/compat/browsers.ts index 89c5c2966e5..5da0e93a5ee 100644 --- a/packages/utils/tests/compat/browsers.ts +++ b/packages/utils/tests/compat/browsers.ts @@ -31,20 +31,20 @@ test('Handles complex support', (t) => { } as any; /* eslint-enable */ - t.deepEqual(getUnsupportedBrowsers(keyframes, '', ['opera 12']), ['opera 12'], 'Before first unprefixed support'); + t.deepEqual(getUnsupportedBrowsers(keyframes, '', ['opera 12'])!.browsers, ['opera 12'], 'Before first unprefixed support'); t.deepEqual(getUnsupportedBrowsers(keyframes, '', ['opera 12.1']), null, 'At first unprefixed support'); t.deepEqual(getUnsupportedBrowsers(keyframes, '', ['opera 13']), null, 'During first unprefixed support'); - t.deepEqual(getUnsupportedBrowsers(keyframes, '', ['opera 15']), ['opera 15'], 'After first unprefixed support'); - t.deepEqual(getUnsupportedBrowsers(keyframes, '', ['opera 29']), ['opera 29'], 'Before second unprefixed support'); + t.deepEqual(getUnsupportedBrowsers(keyframes, '', ['opera 15'])!.browsers, ['opera 15'], 'After first unprefixed support'); + t.deepEqual(getUnsupportedBrowsers(keyframes, '', ['opera 29'])!.browsers, ['opera 29'], 'Before second unprefixed support'); t.deepEqual(getUnsupportedBrowsers(keyframes, '', ['opera 30']), null, 'At second unprefixed support'); t.deepEqual(getUnsupportedBrowsers(keyframes, '', ['opera 31']), null, 'After second unprefixed support'); - t.deepEqual(getUnsupportedBrowsers(keyframes, '-o-', ['opera 11']), ['opera 11'], 'Before -o- support'); + t.deepEqual(getUnsupportedBrowsers(keyframes, '-o-', ['opera 11'])!.browsers, ['opera 11'], 'Before -o- support'); t.deepEqual(getUnsupportedBrowsers(keyframes, '-o-', ['opera 12']), null, 'At -o- support'); t.deepEqual(getUnsupportedBrowsers(keyframes, '-o-', ['opera 12']), null, 'During -o- support'); - t.deepEqual(getUnsupportedBrowsers(keyframes, '-o-', ['opera 15']), ['opera 15'], 'After -o- support'); + t.deepEqual(getUnsupportedBrowsers(keyframes, '-o-', ['opera 15'])!.browsers, ['opera 15'], 'After -o- support'); - t.deepEqual(getUnsupportedBrowsers(keyframes, '-webkit-', ['opera 14']), ['opera 14'], 'Before -webkit- support'); + t.deepEqual(getUnsupportedBrowsers(keyframes, '-webkit-', ['opera 14'])!.browsers, ['opera 14'], 'Before -webkit- support'); t.deepEqual(getUnsupportedBrowsers(keyframes, '-webkit-', ['opera 15']), null, 'At -webkit- support'); t.deepEqual(getUnsupportedBrowsers(keyframes, '-webkit-', ['opera 16']), null, 'During -webkit- support'); }); @@ -67,14 +67,14 @@ test('Handles supported prefix', (t) => { /* eslint-enable */ t.deepEqual(getUnsupportedBrowsers(maxContent, '', ['firefox 66']), null); - t.deepEqual(getUnsupportedBrowsers(maxContent, '', ['firefox 65']), ['firefox 65']); - t.deepEqual(getUnsupportedBrowsers(maxContent, '', ['firefox 41']), ['firefox 41']); - t.deepEqual(getUnsupportedBrowsers(maxContent, '', ['firefox 40']), ['firefox 40']); + t.deepEqual(getUnsupportedBrowsers(maxContent, '', ['firefox 65'])!.browsers, ['firefox 65']); + t.deepEqual(getUnsupportedBrowsers(maxContent, '', ['firefox 41'])!.browsers, ['firefox 41']); + t.deepEqual(getUnsupportedBrowsers(maxContent, '', ['firefox 40'])!.browsers, ['firefox 40']); t.deepEqual(getUnsupportedBrowsers(maxContent, '-moz-', ['firefox 66']), null); t.deepEqual(getUnsupportedBrowsers(maxContent, '-moz-', ['firefox 65']), null); t.deepEqual(getUnsupportedBrowsers(maxContent, '-moz-', ['firefox 41']), null); - t.deepEqual(getUnsupportedBrowsers(maxContent, '-moz-', ['firefox 40']), ['firefox 40']); + t.deepEqual(getUnsupportedBrowsers(maxContent, '-moz-', ['firefox 40'])!.browsers, ['firefox 40']); }); test('Handles unsupported prefix', (t) => { @@ -91,8 +91,8 @@ test('Handles unsupported prefix', (t) => { } as any; /* eslint-enable */ - t.deepEqual(getUnsupportedBrowsers(appearance, '', ['firefox 1']), ['firefox 1']); - t.deepEqual(getUnsupportedBrowsers(appearance, '-webkit-', ['firefox 1']), ['firefox 1']); + t.deepEqual(getUnsupportedBrowsers(appearance, '', ['firefox 1'])!.browsers, ['firefox 1']); + t.deepEqual(getUnsupportedBrowsers(appearance, '-webkit-', ['firefox 1'])!.browsers, ['firefox 1']); t.deepEqual(getUnsupportedBrowsers(appearance, '-moz-', ['firefox 1']), null); }); @@ -116,11 +116,11 @@ test('Handles multiple supported prefixes', (t) => { } as any; /* eslint-enable*/ - t.deepEqual(getUnsupportedBrowsers(boxFlex, '', ['firefox 48']), ['firefox 48']); + t.deepEqual(getUnsupportedBrowsers(boxFlex, '', ['firefox 48'])!.browsers, ['firefox 48']); t.deepEqual(getUnsupportedBrowsers(boxFlex, '-moz-', ['firefox 48']), null); - t.deepEqual(getUnsupportedBrowsers(boxFlex, '-webkit-', ['firefox 48']), ['firefox 48']); + t.deepEqual(getUnsupportedBrowsers(boxFlex, '-webkit-', ['firefox 48'])!.browsers, ['firefox 48']); - t.deepEqual(getUnsupportedBrowsers(boxFlex, '', ['firefox 49']), ['firefox 49']); + t.deepEqual(getUnsupportedBrowsers(boxFlex, '', ['firefox 49'])!.browsers, ['firefox 49']); t.deepEqual(getUnsupportedBrowsers(boxFlex, '-moz-', ['firefox 49']), null); t.deepEqual(getUnsupportedBrowsers(boxFlex, '-webkit-', ['firefox 49']), null); }); @@ -141,6 +141,44 @@ test('Handles removed features', (t) => { /* eslint-enable */ t.deepEqual(getUnsupportedBrowsers(boxLines, '-webkit-', ['chrome 66']), null); - t.deepEqual(getUnsupportedBrowsers(boxLines, '-webkit-', ['chrome 67']), ['chrome 67']); - t.deepEqual(getUnsupportedBrowsers(boxLines, '-webkit-', ['chrome 66', 'chrome 67']), ['chrome 67']); + t.deepEqual(getUnsupportedBrowsers(boxLines, '-webkit-', ['chrome 67'])!.browsers, ['chrome 67']); + t.deepEqual(getUnsupportedBrowsers(boxLines, '-webkit-', ['chrome 66', 'chrome 67'])!.browsers, ['chrome 67']); +}); + +test('Includes accurate details', (t) => { + /* eslint-disable */ + const keyframes: Identifier = { + __compat: { + support: { + opera: [ + { + version_added: "30" + }, + { + prefix: "-webkit-", + version_added: "15" + }, + { + version_added: "12.1", + version_removed: "15" + }, + { + prefix: "-o-", + version_added: "12", + version_removed: "15" + } + ] + } + } + } as any; + /* eslint-enable */ + + t.deepEqual(getUnsupportedBrowsers(keyframes, '', ['opera 12'])!.details.get('opera 12'), { versionAdded: '12.1' }, 'Before first unprefixed support'); + t.deepEqual(getUnsupportedBrowsers(keyframes, '', ['opera 15'])!.details.get('opera 15'), { versionAdded: '30', versionRemoved: '15' }, 'After first unprefixed support'); + t.deepEqual(getUnsupportedBrowsers(keyframes, '', ['opera 29'])!.details.get('opera 29'), { versionAdded: '30', versionRemoved: '15' }, 'Before second unprefixed support'); + + t.deepEqual(getUnsupportedBrowsers(keyframes, '-o-', ['opera 11'])!.details.get('opera 11'), { versionAdded: '12' }, 'Before -o- support'); + t.deepEqual(getUnsupportedBrowsers(keyframes, '-o-', ['opera 15'])!.details.get('opera 15'), { versionRemoved: '15' }, 'After -o- support'); + + t.deepEqual(getUnsupportedBrowsers(keyframes, '-webkit-', ['opera 14'])!.details.get('opera 14'), { versionAdded: '15' }, 'Before -webkit- support'); }); From 27176a93969e446c4b67b3ceab61e7865e33775d Mon Sep 17 00:00:00 2001 From: Tony Ross Date: Thu, 12 Sep 2019 23:10:53 -0500 Subject: [PATCH 2/2] New: Use user-friendly browser names/versions in reports Names are pulled from mdn-browser-compat-data. Versions now focus on when a feature was added/removed. - - - - - - - - - - Fix #2896 --- packages/hint-compat-api/package.json | 1 - packages/hint-compat-api/src/css.ts | 21 +++--- packages/hint-compat-api/src/html.ts | 10 +-- .../hint-compat-api/src/utils/browsers.ts | 66 ++++++------------- packages/hint-compat-api/tests/css.ts | 28 ++++---- packages/hint-compat-api/tests/html.ts | 12 ++-- .../hint-compat-api/tests/utils/browsers.ts | 43 +++++++++--- 7 files changed, 91 insertions(+), 90 deletions(-) diff --git a/packages/hint-compat-api/package.json b/packages/hint-compat-api/package.json index ff43bd8c09d..3d15ba9f69a 100644 --- a/packages/hint-compat-api/package.json +++ b/packages/hint-compat-api/package.json @@ -29,7 +29,6 @@ "nyc": "^14.1.0", "postcss": "^7.0.17", "rimraf": "^3.0.0", - "semver": "^6.3.0", "typescript": "^3.6.2" }, "engines": { diff --git a/packages/hint-compat-api/src/css.ts b/packages/hint-compat-api/src/css.ts index 702cc2ddf2b..fa00ca6d836 100644 --- a/packages/hint-compat-api/src/css.ts +++ b/packages/hint-compat-api/src/css.ts @@ -8,7 +8,7 @@ import { vendor, AtRule, Rule, Declaration, ChildNode, ContainerBase } from 'pos import { HintContext } from 'hint/dist/src/lib/hint-context'; import { IHint, ProblemLocation } from 'hint/dist/src/lib/types'; import { StyleEvents } from '@hint/parser-css/dist/src/types'; -import { getUnsupported } from '@hint/utils/dist/src/compat'; +import { getUnsupportedDetails, UnsupportedBrowsers } from '@hint/utils/dist/src/compat'; import { getCSSCodeSnippet } from '@hint/utils/dist/src/report'; import { filterBrowsers, joinBrowsers } from './utils/browsers'; @@ -21,7 +21,7 @@ import { getMessage } from './i18n.import'; type ReportData = { feature: string; node: ChildNode; - unsupported: string[]; + unsupported: UnsupportedBrowsers; }; type ReportMap = Map; @@ -57,7 +57,7 @@ const validateAtRule = (node: AtRule, context: Context): ReportData | null => { return null; } - const unsupported = getUnsupported({ rule: node.name }, context.browsers); + const unsupported = getUnsupportedDetails({ rule: node.name }, context.browsers); if (unsupported) { return { feature: `@${node.name}`, node, unsupported }; @@ -69,7 +69,7 @@ const validateAtRule = (node: AtRule, context: Context): ReportData | null => { }; const validateDeclValue = (node: Declaration, context: Context): ReportData | null => { - const unsupported = getUnsupported({ property: node.prop, value: node.value }, context.browsers); + const unsupported = getUnsupportedDetails({ property: node.prop, value: node.value }, context.browsers); if (unsupported) { return { feature: `${node.prop}: ${node.value}`, node, unsupported }; @@ -85,7 +85,7 @@ const validateDecl = (node: Declaration, context: Context): ReportData | null => return null; } - const unsupported = getUnsupported({ property }, context.browsers); + const unsupported = getUnsupportedDetails({ property }, context.browsers); if (unsupported) { return { feature: `${property}`, node, unsupported }; @@ -132,12 +132,12 @@ const reportUnsupported = (reportsMap: ReportMap, context: Context): void => { } // Remove browsers not included in ALL reports for this property. - const unsupported = intersection(...reports.map((report) => { - return report.unsupported; + const browsers = intersection(...reports.map((report) => { + return report.unsupported.browsers; })); // Ignore if every browser passed at least one report for this property. - if (!unsupported.length) { + if (!browsers.length) { continue; } @@ -156,6 +156,11 @@ const reportUnsupported = (reportsMap: ReportMap, context: Context): void => { const finalReports = unprefixedReports.length ? unprefixedReports : reports; for (const report of finalReports) { + const unsupported: UnsupportedBrowsers = { + browsers, + details: report.unsupported.details + }; + context.report({ ...report, unsupported }); } } diff --git a/packages/hint-compat-api/src/html.ts b/packages/hint-compat-api/src/html.ts index 42433a13d3f..836e2a4e2d8 100644 --- a/packages/hint-compat-api/src/html.ts +++ b/packages/hint-compat-api/src/html.ts @@ -5,7 +5,7 @@ import { HintContext } from 'hint/dist/src/lib/hint-context'; import { IHint } from 'hint/dist/src/lib/types'; import { HTMLAttribute, HTMLElement } from '@hint/utils'; -import { getUnsupported } from '@hint/utils/dist/src/compat'; +import { getUnsupportedDetails, UnsupportedBrowsers } from '@hint/utils/dist/src/compat'; import { filterBrowsers, joinBrowsers } from './utils/browsers'; import { resolveIgnore } from './utils/ignore'; @@ -15,7 +15,7 @@ import { getMessage } from './i18n.import'; type ReportData = { feature: string; - unsupported: string[]; + unsupported: UnsupportedBrowsers; }; type Context = { @@ -29,7 +29,7 @@ const validateAttributeValue = (element: string, attr: HTMLAttribute, context: C return; } - const unsupported = getUnsupported({ attribute: attr.name, element, value: attr.value }, context.browsers); + const unsupported = getUnsupportedDetails({ attribute: attr.name, element, value: attr.value }, context.browsers); if (unsupported) { context.report({ feature: `${element}[${attr.name}=${attr.value}]`, unsupported }); @@ -41,7 +41,7 @@ const validateAttribute = (element: string, attr: HTMLAttribute, context: Contex return; } - const unsupported = getUnsupported({ attribute: attr.name, element }, context.browsers); + const unsupported = getUnsupportedDetails({ attribute: attr.name, element }, context.browsers); if (unsupported) { context.report({ feature: `${element}[${attr.name}]`, unsupported }); @@ -57,7 +57,7 @@ const validateElement = (node: HTMLElement, context: Context) => { return; } - const unsupported = getUnsupported({ element }, context.browsers); + const unsupported = getUnsupportedDetails({ element }, context.browsers); if (unsupported) { context.report({ feature: element, unsupported }); diff --git a/packages/hint-compat-api/src/utils/browsers.ts b/packages/hint-compat-api/src/utils/browsers.ts index a3707fa9bbe..9cd7e956876 100644 --- a/packages/hint-compat-api/src/utils/browsers.ts +++ b/packages/hint-compat-api/src/utils/browsers.ts @@ -1,8 +1,5 @@ -import * as semver from 'semver'; - -const coerce = (version: string): semver.SemVer | string => { - return semver.coerce(version) || /* istanbul ignore next */ version; -}; +import { UnsupportedBrowsers } from '@hint/utils/dist/src/compat'; +import { getFriendlyName } from '@hint/utils/dist/src/compat/browsers'; /** * Apply temporary filters to the list of target browsers to reduce @@ -32,55 +29,32 @@ export const filterBrowsers = (browsers: string[]): string[] => { }; /** - * Serialize condensed version ranges for provided browsers. + * Serialize summarized support ranges for provided browsers. * * ```js - * joinBrowsers(['chrome 74', 'chrome 75', 'chrome 76', 'edge 15', 'edge 16', 'firefox 67']); - * // returns 'chrome 74-76, edge 15-16, firefox 67'; + * joinBrowsers({ browsers: ['edge 15'], browserDetails: new Map([['edge 15', { versionAdded: '18' }]])); + * // returns 'Edge < 18'; * ``` */ -export const joinBrowsers = (browsers: string[]): string => { - const versionsByName = new Map(); - - // Group browser versions by browser name. - for (const browser of browsers) { - const [name, version] = browser.split(' '); +export const joinBrowsers = (unsupported: UnsupportedBrowsers): string => { + const summaries = unsupported.browsers.map((browser) => { + const name = getFriendlyName(browser); + const details = unsupported.details.get(browser); - if (!versionsByName.has(name)) { - versionsByName.set(name, []); + if (!details) { + throw new Error(`No details provided for browser: ${name}`); } - versionsByName.get(name)!.push(version); - } - - const results: string[] = []; - - // Sort and serialize version ranges for each browser name. - for (const [name, versions] of versionsByName) { - versions.sort((v1, v2) => { - return semver.compare(coerce(v1), coerce(v2)); - }); - - const ranges: string[] = []; - - for (let i = 0, start = versions[0]; i < versions.length; i++) { - if (parseInt(versions[i + 1]) - parseInt(versions[i]) <= 1) { - continue; // Continue until the end of a range. - } - - // Format current range as either `start` or `start-end`. - if (start === versions[i]) { - ranges.push(start); - } else { - ranges.push(`${start}-${versions[i]}`); - } - - // Remember the start of the next range. - start = versions[i + 1]; + if (details.versionAdded && details.versionRemoved) { + return `${name} ${details.versionRemoved}-${details.versionAdded}`; + } else if (details.versionAdded) { + return `${name} < ${details.versionAdded}`; + } else if (details.versionRemoved) { + return `${name} ${details.versionRemoved}+`; } - results.push(`${name} ${ranges.join(', ')}`); - } + return name; + }); - return results.join(', '); + return [...new Set(summaries)].sort().join(', '); }; diff --git a/packages/hint-compat-api/tests/css.ts b/packages/hint-compat-api/tests/css.ts index 2584ff177e1..ae7486894e2 100644 --- a/packages/hint-compat-api/tests/css.ts +++ b/packages/hint-compat-api/tests/css.ts @@ -27,7 +27,7 @@ testHint(hintPath, name: 'Reports unsupported CSS at-rules', reports: [ { - message: '@keyframes is not supported by ie 9.', + message: '@keyframes is not supported by Internet Explorer < 10.', position: { match: '@keyframes' } } ], @@ -41,27 +41,27 @@ testHint(hintPath, name: 'Reports unsupported properties, respecting prefixes and fallback', reports: [ { - message: 'appearance is not supported by ie 9-11.', + message: 'appearance is not supported by Internet Explorer.', position: { match: 'appearance: button; /* Report 1 */' } }, { - message: 'appearance is not supported by ie 9-11.', + message: 'appearance is not supported by Internet Explorer.', position: { match: 'appearance: button; /* Report 2 */' } }, { - message: '-webkit-appearance is not supported by ie 9-11.', + message: '-webkit-appearance is not supported by Internet Explorer.', position: { match: '-webkit-appearance: button; /* Report 3 */' } }, { - message: '-moz-appearance is not supported by ie 9-11.', + message: '-moz-appearance is not supported by Internet Explorer.', position: { match: '-moz-appearance: button; /* Report 4 */' } }, { - message: '-webkit-appearance is not supported by firefox 65-66, ie 9-11.', + message: '-webkit-appearance is not supported by Firefox, Internet Explorer.', position: { match: '-webkit-appearance: button; /* Report 5 */' } }, { - message: 'appearance is not supported by chrome 73-74, edge 15-16, firefox 65-66, ie 9-11.', + message: 'appearance is not supported by Chrome, Edge, Firefox, Internet Explorer.', position: { match: 'appearance: button; /* Report 6 */' } } ], @@ -74,7 +74,7 @@ testHint(hintPath, * name: 'Reports unsupported CSS selectors', * reports: [ * { - * message: ':valid is not supported by ie 9.', + * message: ':valid is not supported by Internet Explorer < 10.', * position: { match: ':valid' } * } * ], @@ -85,7 +85,7 @@ testHint(hintPath, name: 'Respects CSS @supports rules when generating reports', reports: [ { - message: 'display: grid is not supported by edge 15.', + message: 'display: grid is not supported by Edge < 16.', position: { match: 'display: grid; /* Report */' } } ], @@ -95,19 +95,19 @@ testHint(hintPath, name: 'Reports unsupported CSS property values, respecting prefixes and fallback', reports: [ { - message: 'display: grid is not supported by ie 9.', + message: 'display: grid is not supported by Internet Explorer.', position: { match: 'display: grid; /* Report 1 */' } }, { - message: 'display: grid is not supported by ie 9.', + message: 'display: grid is not supported by Internet Explorer.', position: { match: 'display: grid; /* Report 2 */' } }, { - message: 'display: -ms-grid is not supported by chrome 73-74, firefox 65-66, ie 9.', + message: 'display: -ms-grid is not supported by Chrome, Firefox, Internet Explorer < 10.', position: { match: 'display: -ms-grid; /* Report 3 */' } }, { - message: 'display: grid is not supported by edge 15, ie 9-11.', + message: 'display: grid is not supported by Edge < 16, Internet Explorer.', position: { match: 'display: grid; /* Report 4 */' } } ], @@ -139,7 +139,7 @@ testHint(hintPath, name: 'Reports overridden ignored CSS features', reports: [ { - message: 'appearance is not supported by ie 9-11.', + message: 'appearance is not supported by Internet Explorer.', position: { match: 'appearance: none; /* unprefixed */' } } ], diff --git a/packages/hint-compat-api/tests/html.ts b/packages/hint-compat-api/tests/html.ts index 20593e6205f..ba1a80c58e2 100644 --- a/packages/hint-compat-api/tests/html.ts +++ b/packages/hint-compat-api/tests/html.ts @@ -21,11 +21,11 @@ testHint(hintPath, name: 'Reports unsupported HTML attributes', reports: [ { - message: 'img[srcset] is not supported by ie 10-11.', + message: 'img[srcset] is not supported by Internet Explorer.', position: { match: 'img srcset=' } }, { - message: 'div[hidden] is not supported by ie 10.', + message: 'div[hidden] is not supported by Internet Explorer < 11.', position: { match: 'div hidden' } } ], @@ -35,11 +35,11 @@ testHint(hintPath, name: 'Reports unsupported HTML elements', reports: [ { - message: 'blink is not supported by chrome 73-74, edge 17-18, firefox 65-66, ie 10-11.', + message: 'blink is not supported by Chrome, Edge, Firefox 22+, Internet Explorer.', position: { match: 'blink' } }, { - message: 'details is not supported by edge 17-18, ie 10-11.', + message: 'details is not supported by Edge, Internet Explorer.', position: { match: 'details' } } ], @@ -54,7 +54,7 @@ testHint(hintPath, reports: [ // TODO: Include
or similar once MDN data is available { - message: 'input[type=color] is not supported by ie 10-11.', + message: 'input[type=color] is not supported by Internet Explorer.', position: { match: 'input type="color"' } } ], @@ -70,7 +70,7 @@ testHint(hintPath, name: 'Reports overridden ignored HTML features', reports: [ { - message: 'script[integrity] is not supported by ie 10-11.', + message: 'script[integrity] is not supported by Internet Explorer.', position: { match: 'script integrity' } } ], diff --git a/packages/hint-compat-api/tests/utils/browsers.ts b/packages/hint-compat-api/tests/utils/browsers.ts index 842e308ee87..6d3b1858ab1 100644 --- a/packages/hint-compat-api/tests/utils/browsers.ts +++ b/packages/hint-compat-api/tests/utils/browsers.ts @@ -4,28 +4,51 @@ import { joinBrowsers } from '../../src/utils/browsers'; test('disjoint', (t) => { t.deepEqual( - joinBrowsers(['chrome 74', 'chrome 76']), - 'chrome 74, 76' + joinBrowsers({ + browsers: ['chrome 74', 'chrome 76'], + details: new Map([ + ['chrome 74', { versionAdded: '77' }], + ['chrome 76', { versionAdded: '77' }] + ]) + }), + 'Chrome < 77' ); }); test('range', (t) => { t.deepEqual( - joinBrowsers(['firefox 65', 'firefox 66', 'firefox 67']), - 'firefox 65-67' + joinBrowsers({ + browsers: ['firefox 65', 'firefox 66', 'and_ff 66'], + details: new Map([ + ['firefox 65', { versionAdded: '67' }], + ['firefox 66', { versionAdded: '67' }], + ['and_ff 66', { versionAdded: '67' }] + ]) + }), + 'Firefox < 67, Firefox Android < 67' ); }); -test('disjoint + range', (t) => { +test('removed then re-added', (t) => { t.deepEqual( - joinBrowsers(['chrome 73', 'chrome 75', 'chrome 76', 'chrome 78']), - 'chrome 73, 75-76, 78' + joinBrowsers({ + browsers: ['opera 16'], + details: new Map([ + ['opera 16', { versionAdded: '30', versionRemoved: '15'}] + ]) + }), + 'Opera 15-30' ); }); -test('multiple browsers', (t) => { +test('removed', (t) => { t.deepEqual( - joinBrowsers(['chrome 74', 'chrome 75', 'edge 15', 'edge 16']), - 'chrome 74-75, edge 15-16' + joinBrowsers({ + browsers: ['opera 15'], + details: new Map([ + ['opera 15', { versionRemoved: '15' }] + ]) + }), + 'Opera 15+' ); });