Skip to content

Commit

Permalink
Breaking: Replace caniuse util with a compat util based on MDN data
Browse files Browse the repository at this point in the history
  • Loading branch information
antross committed Apr 29, 2019
1 parent e545325 commit 74f0b67
Show file tree
Hide file tree
Showing 19 changed files with 694 additions and 59 deletions.
2 changes: 0 additions & 2 deletions packages/extension-browser/package.json
Expand Up @@ -67,7 +67,6 @@
"is-ci": "^2.0.0",
"jsdom": "^15.0.0",
"lodash": "^4.17.11",
"mdn-browser-compat-data": "^0.0.72",
"npm-run-all": "^4.1.5",
"nyc": "^14.0.0",
"proxyquire": "2.0.0",
Expand Down Expand Up @@ -127,7 +126,6 @@
"prebuild:css": "tcm src",
"prebuild:hints": "node ./scripts/import-hints.js",
"prebuild:i18n": "node ./scripts/create-i18n.js",
"prebuild:mdn": "node ./scripts/pack-mdn-data.js",
"stats": "webpack --json > stats.json",
"stats-release": "webpack --env.release --json > release-stats.json",
"test": "npm run lint && npm run build && npm run test-only && npm run build-release",
Expand Down
5 changes: 3 additions & 2 deletions packages/utils/README.md
Expand Up @@ -29,9 +29,10 @@ and exception.
* `getClient`: Return the Application Insights client.
* `isConfigured`: Check if Application Insights is configured.

### caniuse
### compat

* `isSupported`: Convinience wrapper to expose the method `isSupported`.
* `getUnsupported`: Get browsers without support for CSS or HTML features.
* `isSupported`: Query MDN for support of CSS or HTML features.

### configStore

Expand Down
7 changes: 6 additions & 1 deletion packages/utils/package.json
Expand Up @@ -8,7 +8,6 @@
},
"dependencies": {
"applicationinsights": "^1.2.0",
"caniuse-api": "^3.0.0",
"configstore": "^4.0.0",
"content-type": "^1.0.4",
"debug": "^4.1.1",
Expand All @@ -18,7 +17,10 @@
"npm-registry-fetch": "^3.9.0",
"parse5": "^5.1.0",
"parse5-htmlparser2-tree-adapter": "^5.1.0",
"postcss-selector-parser": "^6.0.2",
"postcss-value-parser": "^3.3.1",
"request": "^2.88.0",
"semver": "^6.0.0",
"strip-bom": "^4.0.0",
"strip-json-comments": "^2.0.1"
},
Expand All @@ -30,6 +32,7 @@
"@types/parse5": "^5.0.0",
"@types/parse5-htmlparser2-tree-adapter": "^5.0.1",
"@types/request": "^2.48.1",
"@types/semver": "^6.0.0",
"@types/strip-bom": "^3.0.0",
"@types/strip-json-comments": "^0.0.30",
"@typescript-eslint/eslint-plugin": "^1.7.0",
Expand All @@ -40,6 +43,7 @@
"eslint-plugin-import": "^2.17.2",
"eslint-plugin-markdown": "^1.0.0",
"eventemitter2": "^5.0.1",
"mdn-browser-compat-data": "^0.0.76",
"mime-db": "1.35.0",
"npm-run-all": "^4.1.5",
"nyc": "^14.0.0",
Expand Down Expand Up @@ -87,6 +91,7 @@
"lint:js": "eslint . --cache --ext js --ext md --ext ts --ignore-path ../../.eslintignore --report-unused-disable-directives",
"lint:md": "node ../../scripts/lint-markdown.js",
"prebuild": "npm-run-all prebuild:*",
"prebuild:mdn": "node ./scripts/mdn-browser-compat-data.js",
"prebuild:mime-db": "node ./scripts/mime-db.js",
"test": "npm run lint && npm run build && npm run test-only",
"test-only": "nyc ava",
Expand Down
@@ -1,7 +1,7 @@
const fs = require('fs');
const mdn = require('mdn-browser-compat-data');
const path = require('path');
const filename = path.resolve(`${__dirname}/../dist/mdn-browser-compat-data.packed.json`);
const filename = path.resolve(`${__dirname}/../src/compat/browser-compat-data.ts`);

/**
* Determine if a given support statement qualifies as "always supported" by
Expand All @@ -14,10 +14,19 @@ const filename = path.resolve(`${__dirname}/../dist/mdn-browser-compat-data.pack
const isUniversalSupportStatement = (browserName, supportStatement) => {
const version = supportStatement.version_added;

// Flagged or prefixed support isn't "full" support.
if (supportStatement.flags || supportStatement.prefix) {
return false;
}

if (supportStatement.version_removed) {
return false;
}

if (typeof supportStatement.version_removed === 'string') {
return false;
}

if (typeof version === 'boolean') {
return version;
}
Expand Down Expand Up @@ -114,20 +123,22 @@ const isFeatureUniversallyUnsupported = (compat) => {
/**
* Remove any `bcd.SimpleSupportStatement` that requires `flags`.
* Flagged entries are ignored since they must be enabled by end users.
* Unless the flagged entry is the only entry (in which case it's needed
* to infer lack of support).
*
* @param {string} browserName
* @param {bcd.SupportBlock} support
*/
const removeFlaggedSupport = (browserName, support) => {
const supportStatements = getSupportStatements(browserName, support);
const unflaggedSupportStatements = supportStatements.filter((supportStatement) => {
const unflagged = supportStatements.filter((supportStatement) => {
return !supportStatement.flags;
});

if (unflaggedSupportStatements.length) {
support[browserName] = unflaggedSupportStatements;
if (unflagged.length) {
support[browserName] = unflagged.length === 1 ? unflagged[0] : unflagged;
} else {
delete support[browserName];
support[browserName] = { version_added: false }; // eslint-disable-line camelcase
}
};

Expand Down Expand Up @@ -234,11 +245,24 @@ const data = {
html: mdn.html
};

// TODO: drop `browsers` after `hint-compat-api` uses new util methods.
removeBrowserDetails(data.browsers);
removeFeatures(data.css);
removeFeatures(data.html);

fs.writeFile(filename, JSON.stringify(data), (err) => {
const code = `/* eslint-disable */
import { PrimaryIdentifier } from 'mdn-browser-compat-data/types';
type Data = {
browsers: PrimaryIdentifier;
css: PrimaryIdentifier;
html: PrimaryIdentifier;
}
export const mdn: Data = ${JSON.stringify(data, null, 4)} as any;
`;

fs.writeFile(filename, code, (err) => {
if (err) {
throw err;
} else {
Expand Down
3 changes: 0 additions & 3 deletions packages/utils/src/@types/caniuse-api.d.ts

This file was deleted.

7 changes: 0 additions & 7 deletions packages/utils/src/caniuse.ts

This file was deleted.

1 change: 1 addition & 0 deletions packages/utils/src/compat/.gitignore
@@ -0,0 +1 @@
browser-compat-data.ts
134 changes: 134 additions & 0 deletions packages/utils/src/compat/browsers.ts
@@ -0,0 +1,134 @@
import { Identifier, SimpleSupportStatement, SupportStatement } from 'mdn-browser-compat-data/types';
import * as semver from 'semver';

export type UnsupportedBrowsers = string[] | null;

const enum Support {
No,
Yes,
Unknown
}

// Map `browserslist` browser names to MDN ones.
const browserToMDN = new Map([
['and_chr', 'chrome_android'],
['and_ff', 'firefox_android'],
['and_qq', 'qq_android'],
['and_uc', 'uc_android'],
['android', 'webview_android'],
['chrome', 'chrome'],
['edge', 'edge'],
['edge_mob', 'edge_mobile'],
['firefox', 'firefox'],
['ie', 'ie'],
['ios_saf', 'safari_ios'],
['node', 'nodejs'],
['opera', 'opera'],
['safari', 'safari'],
['samsung', 'samsunginternet_android']
]);

const coerce = (version: string): string | semver.SemVer => {
return semver.coerce(version) || version;
};

/**
* Intepret if the provided statement indicates support for the given browser version.
*/
const isSupported = (support: SimpleSupportStatement, prefix: string, rawVersion: string): Support => {
const version = coerce(rawVersion);

// Ignore support that requires users to enable a flag.
if (support.flags) {
return Support.Unknown;
}

// Ignore support that doesn't match the same prefix.
if (prefix !== (support.prefix || '')) {
return Support.Unknown;
}

/*
* If feature was added but we don't know when, assume support.
*
* Note: This is likely to change to describe the version of the browser
* which was tested for support (e.g. `version_added = "<=60"`).
*
* https://github.com/mdn/browser-compat-data/issues/3021
*/
if (support.version_added === true) {
return Support.Yes;
}

// If feature was never added, then it's not supported.
if (support.version_added === false) {
return 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;
}

// 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;
}

// Ignore all other cases (e.g. if a feature was removed but we don't know when).
return Support.Unknown;
};

/**
* Interpret if the provided support statements indicate the given browser version in supported.
*/
const isBrowserUnsupported = (support: SupportStatement, prefix: string, version: string): boolean => {
// Convert single entries to an array for consistent handling.
const browserSupport = Array.isArray(support) ? support : [support];
let status = Support.Unknown;

// Items are listed from newest to oldest. The first clear rule wins.
for (const simpleSupport of browserSupport) {
switch (isSupported(simpleSupport, prefix, version)) {
case Support.Yes:
return false;
case Support.No:
status = Support.No;
break; // Keep looking in case a feature was temporarily removed.
case Support.Unknown:
default:
break;
}
}

return status === Support.No;
};

/**
* Return provided browsers which don't support the specified feature.
* Browsers are an array of strings as generated by `browserslist`:
* https://github.com/browserslist/browserslist
*
* @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 => {
if (!feature || !feature.__compat) {
return null; // Assume support if no matching feature was provided.
}

const support = feature.__compat.support;
const unsupported: string[] = [];

for (const browser of browsers) {
const [name, version] = browser.split(' ');
const mdnBrowser = browserToMDN.get(name)!;
const browserSupport = support[mdnBrowser];

if (browserSupport && isBrowserUnsupported(browserSupport, prefix, version)) {
unsupported.push(browser);
}
}

return unsupported.length ? unsupported : null;
};
34 changes: 34 additions & 0 deletions packages/utils/src/compat/cache.ts
@@ -0,0 +1,34 @@
type Value = string[] | null;

const cache = new Map<string, Map<string[], Value>>();

const _hasCachedValue = (key: string, browsers: string[]): boolean => {
return cache.has(key) && cache.get(key)!.has(browsers);
};

const _getCachedValue = (key: string, browsers: string[]): Value => {
return cache.has(key) && cache.get(key)!.get(browsers) || null;
};

const _setCachedValue = (key: string, browsers: string[], value: Value): Value => {
if (!cache.has(key)) {
cache.set(key, new Map());
}

cache.get(key)!.set(browsers, value);

return value;
};

/**
* Cache a set of unsupported browsers for the provided key and target
* browsers. Speeds up access for repeat queries which are common when
* processing source code.
*/
export const getCachedValue = (key: string, browsers: string[], getValue: () => Value) => {
if (_hasCachedValue(key, browsers)) {
return _getCachedValue(key, browsers);
}

return _setCachedValue(key, browsers, getValue());
};

0 comments on commit 74f0b67

Please sign in to comment.