summaryrefslogtreecommitdiff
path: root/node_modules/doiuse/lib
diff options
context:
space:
mode:
authorlittleblack111 <littleblack11111@gmail.com>2023-10-14 15:50:21 +0800
committerlittleblack111 <littleblack11111@gmail.com>2023-10-14 15:50:21 +0800
commit0379526e20e1ff212df23e6326741aa9c06df204 (patch)
tree340143b0c4dc4280c88ba1591d137b53477005b3 /node_modules/doiuse/lib
parent7aba14fb7c970c6e6af82c805db2aef6d70bfe2b (diff)
added doiuse plugin for postcss
Diffstat (limited to 'node_modules/doiuse/lib')
-rw-r--r--node_modules/doiuse/lib/BrowserSelection.js181
-rw-r--r--node_modules/doiuse/lib/Detector.js157
-rw-r--r--node_modules/doiuse/lib/DoIUse.js130
-rw-r--r--node_modules/doiuse/lib/stream/CssUsageDuplex.js36
-rw-r--r--node_modules/doiuse/lib/stream/RuleUsageTransform.js80
-rw-r--r--node_modules/doiuse/lib/stream/SourceMapTransform.js74
6 files changed, 658 insertions, 0 deletions
diff --git a/node_modules/doiuse/lib/BrowserSelection.js b/node_modules/doiuse/lib/BrowserSelection.js
new file mode 100644
index 00000000..17467548
--- /dev/null
+++ b/node_modules/doiuse/lib/BrowserSelection.js
@@ -0,0 +1,181 @@
+import browserslist from 'browserslist';
+import * as caniuse from 'caniuse-lite';
+
+import FEATURES from '../data/features.js';
+import { formatBrowserName } from '../utils/util.js';
+
+/**
+ * @template T
+ * @typedef {{missing: T, partial: T}} Filter
+ */
+
+/**
+ * @typedef {Object} MissingSupportResultStats
+ * @prop {string} title
+ * @prop {caniuse.Feature} caniuseData
+ * @prop {string} [missing]
+ * @prop {Record<string, Record<string, string>>} [missingData]
+ * @prop {string} [partial]
+ * @prop {Record<string, Record<string, string>>} [partialData]
+ */
+
+/**
+ * @typedef {Partial<Record<keyof FEATURES, MissingSupportResultStats>>} BrowserSupportStats
+ */
+
+/**
+ * @typedef {Object} MissingSupportResult
+ * @prop {string[][]} browsers
+ * @prop {Partial<Record<keyof FEATURES, MissingSupportResultStats>>} features
+ */
+
+export default class BrowserSelection {
+ #list;
+
+ /**
+ * @param {string | string[]} [query]
+ * @param {string | false} [from]
+ */
+ constructor(query, from) {
+ this.browsersRequest = query;
+ const options = from ? { path: from } : {};
+ this.#list = browserslist(this.browsersRequest, options).map((browser) => browser.split(' '));
+ }
+
+ /**
+ * @param {string} browser
+ * @param {string} version
+ * @return {string[]|undefined}
+ */
+ test(browser, version) {
+ const versions = version.split('-');
+
+ if (versions.length === 1) {
+ versions.push(versions[0]);
+ }
+
+ return this.#list.find(([b, v]) => b === browser && v >= versions[0] && v <= versions[1]);
+ }
+
+ list() {
+ return [...this.#list];
+ }
+
+ /**
+ * @param {Record<string, Record<string, string>>} browserStats
+ * @return {string}
+ */
+ static lackingBrowsers(browserStats) {
+ const result = [];
+ for (const browser of Object.keys(browserStats)) {
+ result.push(formatBrowserName(browser, Object.keys(browserStats[browser])));
+ }
+ return result.join(', ');
+ }
+
+ /**
+ * @param {import('caniuse-lite').StatsByAgentID} stats
+ * @return {Filter<Record<string,Record<string, string>>>}
+ */
+ filterStats(stats) {
+ /** @type {Filter<Record<string,Record<string, string>>>} */
+ const result = { missing: {}, partial: {} };
+ for (const [browser, versions] of Object.entries(stats)) {
+ /** @type {Filter<Record<string,string>>} */
+ const feature = { missing: {}, partial: {} };
+ for (const [version, support] of Object.entries(versions)) {
+ const selected = this.test(browser, version);
+
+ // check if browser is NOT fully (i.e., don't have 'y' in their stats) supported
+ if (!selected) continue;
+ if ((/(^|\s)y($|\s)/.test(support))) continue;
+ // when it's not partially supported ('a'), it's missing
+ const type = (/(^|\s)a($|\s)/.test(support) ? 'partial' : 'missing');
+
+ if (!feature[type]) {
+ feature[type] = {};
+ }
+
+ feature[type][selected[1]] = support;
+ }
+ if (Object.keys(feature.missing).length > 0) {
+ result.missing[browser] = feature.missing;
+ }
+
+ if (Object.keys(feature.partial).length > 0) {
+ result.partial[browser] = feature.partial;
+ }
+ }
+ return result;
+ }
+
+ /**
+ * Get data on CSS features not supported by the given autoprefixer-like
+ * browser selection.
+ * @return {BrowserSupportStats} `features` is an array of:
+ * ```
+ * {
+ * 'feature-name': {
+ * title: 'Title of feature'
+ * missing: "IE (8), Chrome (31)"
+ * missingData: {
+ * // map of browser -> version -> (lack of)support code
+ * ie: { '8': 'n' },
+ * chrome: { '31': 'n' }
+ * }
+ * partialData: {
+ * // map of browser -> version -> (partial)support code
+ * ie: { '7': 'a' },
+ * ff: { '29': 'a #1' }
+ * }
+ * caniuseData: {
+ * // caniuse-db json data for this feature
+ * }
+ * },
+ * 'feature-name-2': {} // etc.
+ * }
+ * ```
+ *
+ * `feature-name` is a caniuse-db slug.
+ */
+ compileBrowserSupport() {
+ /** @type {Partial<Record<keyof FEATURES, MissingSupportResultStats>>} */
+ const result = {};
+
+ for (const key of Object.keys(FEATURES)) {
+ const feature = /** @type {keyof FEATURES} */ (key);
+ const packedFeature = caniuse.features[feature];
+ const featureData = caniuse.feature(packedFeature);
+ const lackData = this.filterStats(featureData.stats);
+ const missingData = lackData.missing;
+ const partialData = lackData.partial;
+ // browsers with missing or partial support for this feature
+ const missing = BrowserSelection.lackingBrowsers(missingData);
+ const partial = BrowserSelection.lackingBrowsers(partialData);
+
+ if (missing.length > 0 || partial.length > 0) {
+ result[feature] = {
+ title: featureData.title,
+ caniuseData: featureData,
+ ...(missing.length > 0 ? { missingData, missing } : null),
+ ...(partial.length > 0 ? { partialData, partial } : null),
+ };
+ }
+ }
+
+ return result;
+ }
+
+ /**
+ * @see BrowserSelection.compileBrowserSupport
+ * @param {ConstructorParameters<typeof this>} constructorParameters
+ * @return {MissingSupportResult} `{browsers, features}`
+ */
+ static missingSupport(...constructorParameters) {
+ const selection = new BrowserSelection(...constructorParameters);
+ return {
+ browsers: selection.list(),
+ features: selection.compileBrowserSupport(),
+ };
+ }
+}
diff --git a/node_modules/doiuse/lib/Detector.js b/node_modules/doiuse/lib/Detector.js
new file mode 100644
index 00000000..2858bca9
--- /dev/null
+++ b/node_modules/doiuse/lib/Detector.js
@@ -0,0 +1,157 @@
+import FEATURES from '../data/features.js';
+import { performFeatureCheck } from '../utils/util.js';
+
+/**
+ * @typedef DetectorCallbackArgument
+ * @prop {!import('postcss').ChildNode} usage
+ * @prop {keyof FEATURES} feature
+ * @prop {(keyof FEATURES & string)[]} ignore
+ */
+
+/**
+ * @callback DetectorCallback
+ * @param {DetectorCallbackArgument} result
+ * @return {any}
+ */
+
+const PLUGIN_OPTION_COMMENT = 'doiuse-';
+const DISABLE_FEATURE_COMMENT = `${PLUGIN_OPTION_COMMENT}disable`;
+const ENABLE_FEATURE_COMMENT = `${PLUGIN_OPTION_COMMENT}enable`;
+
+/**
+ * Strip the contents of url literals so they aren't matched
+ * by our naive substring matching.
+ * @param {string} input
+ * @return {string}
+ */
+function stripUrls(input) {
+ return input.replaceAll(/url\([^)]*\)/g, 'url()');
+}
+
+/**
+ * Detect the use of any of a given list of CSS features.
+ * ```
+ * var detector = new Detector(featureList)
+ * detector.process(css, cb)
+ * ```
+ *
+ * `featureList`: an array of feature slugs (see caniuse-db)
+ * `cb`: a callback that gets called for each usage of one of the given features,
+ * called with an argument like:
+ * ```
+ * {
+ * usage: {} // postcss node where usage was found
+ * feature: {} // caniuse-db feature slug
+ * ignore: {} // caniuse-db feature to ignore in current file
+ * }
+ * ```
+ */
+export default class Detector {
+ /**
+ * @param {(keyof FEATURES & string)[]} featureList an array of feature slugs (see caniuse-db)
+ */
+ constructor(featureList) {
+ /** @type {Partial<FEATURES>} */
+ this.features = {};
+ for (const feature of featureList) {
+ if (FEATURES[feature]) {
+ this.features[feature] = FEATURES[feature];
+ }
+ }
+ /** @type {(keyof FEATURES & string)[]} */
+ this.ignore = [];
+ }
+
+ /**
+ * @param {import('postcss').Comment} comment
+ * @return {void}
+ */
+ comment(comment) {
+ const text = comment.text.toLowerCase();
+
+ if (!text.startsWith(PLUGIN_OPTION_COMMENT)) return;
+ const option = text.split(' ', 1)[0];
+ const value = text.replace(option, '').trim();
+
+ switch (option) {
+ case DISABLE_FEATURE_COMMENT: {
+ if (value === '') {
+ // @ts-expect-error Skip cast
+ this.ignore = Object.keys(this.features);
+ } else {
+ for (const feat of value.split(',')) {
+ /** @type {any} */
+ const f = feat.trim();
+ if (!this.ignore.includes(f)) {
+ this.ignore.push(f);
+ }
+ }
+ }
+ break;
+ }
+ case ENABLE_FEATURE_COMMENT: {
+ if (value === '') {
+ this.ignore = [];
+ } else {
+ const without = new Set(value.split(',').map((feat) => feat.trim()));
+ this.ignore = this.ignore.filter((index) => !without.has(index));
+ }
+ break;
+ }
+ default:
+ }
+ }
+
+ /**
+ * @param {import('postcss').Container} node
+ * @param {DetectorCallback} callback
+ * @return {void}
+ */
+ node(node, callback) {
+ node.each((child) => {
+ if (child.type === 'comment') {
+ this.comment(child);
+ return;
+ }
+
+ for (const [feat] of Object.entries(this.features).filter(([, featValue]) => {
+ if (!featValue) return false;
+ if (typeof featValue === 'function') {
+ return featValue(child);
+ }
+ if (Array.isArray(featValue)) {
+ return featValue.some((function_) => function_(child));
+ }
+ if (child.type !== 'decl') {
+ return false;
+ }
+
+ return Object.entries(featValue).some(([property, value]) => {
+ if (property !== '' && property !== child.prop) return false;
+ if (value === true) return true;
+ if (value === false) return false;
+ return performFeatureCheck(value, stripUrls(child.value));
+ });
+ })) {
+ const feature = /** @type {keyof FEATURES} */ (feat);
+ callback({ usage: child, feature, ignore: this.ignore });
+ }
+ if (child.type !== 'decl') {
+ this.node(child, callback);
+ }
+ });
+ }
+
+ /**
+ * @param {import('postcss').Root} node
+ * @param {DetectorCallback} callback
+ * @return {void}
+ */
+ process(node, callback) {
+ // Reset ignoring rules specified by inline comments per each file
+ this.ignore = [];
+
+ // Recursively walk nodes in file
+ this.node(node, callback);
+ }
+}
diff --git a/node_modules/doiuse/lib/DoIUse.js b/node_modules/doiuse/lib/DoIUse.js
new file mode 100644
index 00000000..78db53c8
--- /dev/null
+++ b/node_modules/doiuse/lib/DoIUse.js
@@ -0,0 +1,130 @@
+import multimatch from 'multimatch';
+
+import BrowserSelection from './BrowserSelection.js';
+import Detector from './Detector.js';
+
+/** @typedef {import('../data/features.js').FeatureKeys} FeatureKeys */
+
+/**
+ * @typedef {Object} OnFeatureUsageArguments
+ * @prop {FeatureKeys} feature
+ * @prop {import('./BrowserSelection.js').MissingSupportResultStats|undefined} featureData
+ * @prop {import('postcss').Node} usage
+ * @prop {string} message
+ */
+
+/**
+ * @callback OnFeatureUsage
+ * @param {OnFeatureUsageArguments} result
+ * @return {any}
+ */
+
+/**
+ * @typedef {Object} DoIUseOptions
+ * @prop {ConstructorParameters<typeof BrowserSelection>[0]} [browsers]
+ * @prop {FeatureKeys[]} [ignore]
+ * @prop {OnFeatureUsage} [onFeatureUsage]
+ * @prop {string[]} [ignoreFiles]
+ */
+
+export default class DoIUse {
+ static default = null;
+
+ /**
+ * @param {DoIUseOptions} [optionsOrBrowserQuery]
+ */
+ constructor(optionsOrBrowserQuery) {
+ const options = (typeof optionsOrBrowserQuery === 'string')
+ ? { browsers: optionsOrBrowserQuery }
+ : { ...optionsOrBrowserQuery };
+ this.browserQuery = options.browsers;
+ this.onFeatureUsage = options.onFeatureUsage;
+ this.ignoreOptions = options.ignore;
+ this.ignoreFiles = options.ignoreFiles;
+ this.info = this.info.bind(this);
+ this.postcss = this.postcss.bind(this);
+ }
+
+ /**
+ * @param {Object} [options]
+ * @param {ConstructorParameters<typeof BrowserSelection>[1]} [options.from]
+ */
+ info(options = {}) {
+ const { browsers, features } = BrowserSelection.missingSupport(this.browserQuery, options.from);
+
+ return {
+ browsers,
+ features,
+ };
+ }
+
+ /** @type {import('postcss').TransformCallback} */
+ postcss(css, result) {
+ let from;
+ if (css.source && css.source.input) {
+ from = css.source.input.file;
+ }
+ const { features } = BrowserSelection.missingSupport(this.browserQuery, from);
+ // @ts-expect-error Needs cast
+ const detector = new Detector(Object.keys(features));
+
+ return detector.process(css, ({ feature, usage, ignore }) => {
+ if (ignore && ignore.includes(feature)) {
+ return;
+ }
+
+ if (this.ignoreOptions && this.ignoreOptions.includes(feature)) {
+ return;
+ }
+
+ if (!usage.source) {
+ throw new Error('No source?');
+ }
+ if (this.ignoreFiles && multimatch(usage.source.input.from, this.ignoreFiles).length > 0) {
+ return;
+ }
+
+ const data = features[feature];
+ if (!data) {
+ throw new Error('No feature data?');
+ }
+ const messages = [];
+ if (data.missing) {
+ messages.push(`not supported by: ${data.missing}`);
+ }
+ if (data.partial) {
+ messages.push(`only partially supported by: ${data.partial}`);
+ }
+
+ let message = `${data.title} ${messages.join(' and ')} (${feature})`;
+
+ result.warn(message, { node: usage, plugin: 'doiuse' });
+
+ if (this.onFeatureUsage) {
+ if (!usage.source) {
+ throw new Error('No usage source?');
+ }
+ const { start, input } = usage.source;
+ if (!start) {
+ throw new Error('No usage source start?');
+ }
+
+ const map = css.source && css.source.input.map;
+ const mappedStart = map && map.consumer().originalPositionFor(start);
+
+ const file = (mappedStart && mappedStart.source) || input.file || input.from;
+ const line = (mappedStart && mappedStart.line) || start.line;
+ const column = (mappedStart && mappedStart.column) || start.column;
+
+ message = `${file}:${line}:${column}: ${message}`;
+
+ this.onFeatureUsage({
+ feature,
+ featureData: features[feature],
+ usage,
+ message,
+ });
+ }
+ });
+ }
+}
diff --git a/node_modules/doiuse/lib/stream/CssUsageDuplex.js b/node_modules/doiuse/lib/stream/CssUsageDuplex.js
new file mode 100644
index 00000000..063ed1dc
--- /dev/null
+++ b/node_modules/doiuse/lib/stream/CssUsageDuplex.js
@@ -0,0 +1,36 @@
+/* eslint-disable no-underscore-dangle */
+
+import { pipeline } from 'node:stream';
+
+import Tokenize from 'css-tokenize';
+import Duplexify from 'duplexify';
+
+import RuleUsageTransform from './RuleUsageTransform.js';
+import SourceMapTransform from './SourceMapTransform.js';
+
+/** @typedef {import('../../data/features.js').FeatureKeys} FeatureKeys */
+
+export default class CssUsageDuplex extends Duplexify {
+ /**
+ * @param {Object} options
+ * @param {string} options.browsers
+ * @param {FeatureKeys[]} [options.ignore]
+ * @param {boolean} [options.skipErrors]
+ * @param {string} [filename] Filename for outputting source code locations.
+ */
+ constructor(options, filename) {
+ const streams = [
+ new Tokenize(),
+ new SourceMapTransform(),
+ new RuleUsageTransform(options, filename),
+ ];
+ pipeline(
+ // @ts-expect-error Bad typings
+ ...streams,
+ () => {
+ // noop (Error handled by Duplexify)
+ },
+ );
+ super(streams[0], streams.at(-1), { objectMode: true });
+ }
+}
diff --git a/node_modules/doiuse/lib/stream/RuleUsageTransform.js b/node_modules/doiuse/lib/stream/RuleUsageTransform.js
new file mode 100644
index 00000000..a2a79d4d
--- /dev/null
+++ b/node_modules/doiuse/lib/stream/RuleUsageTransform.js
@@ -0,0 +1,80 @@
+/* eslint-disable no-underscore-dangle */
+import { Transform } from 'node:stream';
+
+import postcss from 'postcss';
+import sourcemap from 'source-map';
+
+import DoIUse from '../DoIUse.js';
+
+/** @typedef {import('../../data/features.js').FeatureKeys} FeatureKeys */
+/** @typedef {import('./SourceMapTransform.js').SourceMapTransformation} SourceMapTransformation */
+
+export default class RuleUsageTransform extends Transform {
+ /** @type {import("postcss").Processor} */
+ #processor;
+
+ #skipErrors;
+
+ #filename;
+
+ /**
+ * @param {Object} options
+ * @param {string} options.browsers
+ * @param {FeatureKeys[]} [options.ignore]
+ * @param {boolean} [options.skipErrors]
+ * @param {string} [filename] Filename for outputting source code locations.
+ */
+ constructor(options, filename = '<streaming css input>') {
+ super({
+ objectMode: true,
+ });
+ this.#processor = postcss([new DoIUse({
+ browsers: options.browsers,
+ ignore: options.ignore,
+ onFeatureUsage: (usage) => {
+ this.push(usage);
+ },
+ })]);
+ this.#filename = filename;
+ this.#skipErrors = options.skipErrors;
+ }
+
+ /**
+ * @param {SourceMapTransformation} rule
+ * @param {any} encoding
+ * @param {Function} callback
+ */
+ async _transform(rule, encoding, callback) {
+ let caughtError;
+ try {
+ const mapper = new sourcemap.SourceMapGenerator();
+ const lines = rule.content.split('\n');
+ let oline = rule.line;
+ let ocol = rule.column;
+ for (const [lineNumber, content] of lines.entries()) {
+ mapper.addMapping({
+ generated: { line: lineNumber + 1, column: 1 },
+ original: { line: oline, column: ocol },
+ source: this.#filename,
+ });
+ mapper.addMapping({
+ generated: { line: lineNumber + 1, column: content.length },
+ original: { line: oline, column: ocol + content.length },
+ source: this.#filename,
+ });
+ oline += 1;
+ ocol = 1;
+ }
+ await this.#processor.process(rule.content, {
+ from: this.#filename, map: { prev: mapper.toString() },
+ });
+ } catch (error) {
+ if (this.#skipErrors) {
+ this.emit('warning', error);
+ } else {
+ caughtError = error;
+ }
+ }
+ callback(caughtError);
+ }
+}
diff --git a/node_modules/doiuse/lib/stream/SourceMapTransform.js b/node_modules/doiuse/lib/stream/SourceMapTransform.js
new file mode 100644
index 00000000..6295b1d3
--- /dev/null
+++ b/node_modules/doiuse/lib/stream/SourceMapTransform.js
@@ -0,0 +1,74 @@
+/* eslint-disable no-underscore-dangle */
+import { Transform } from 'node:stream';
+
+const NEW_LINE_CHAR = '\n'.codePointAt(0);
+
+/**
+ * @typedef {Object} SourceMapTransformation
+ * @prop {number} line
+ * @prop {number} column
+ * @prop {string} content
+ */
+
+export default class SourceMapTransform extends Transform {
+ constructor() {
+ super({ objectMode: true });
+ }
+
+ /**
+ * @type {?{location:[number,number], buffers: Buffer[]}}
+ * buffer for the current incoming rule.
+ */
+ #current = null;
+
+ /** track depth to handle rules nested in at-rules. */
+ #depth = 0;
+
+ /** track this and pass it downstream for source mapping. */
+ #line = 1;
+
+ /** track this and pass it downstream for source mapping. */
+ #column = 1;
+
+ /**
+ * @param {[string,Buffer]} chunk
+ * @param {any} encoding
+ * @param {Function} callback
+ */
+ _transform([type, buf], encoding, callback) {
+ if ((type === 'rule_start' || type === 'atrule_start')) {
+ this.#depth += 1;
+ }
+ if (this.#depth > 0 && !this.#current) {
+ this.#current = { location: [this.#line, this.#column], buffers: [] };
+ }
+ if (type === 'rule_end' || type === 'atrule_end') {
+ this.#depth -= 1;
+ }
+
+ if (this.#current) {
+ this.#current.buffers.push(buf);
+ if (this.#depth === 0) {
+ // Push rule
+ this.push({
+ line: this.#current.location[0],
+ column: this.#current.location[1],
+ content: Buffer.concat(this.#current.buffers).toString(),
+ });
+ this.#current = null;
+ }
+ }
+
+ // Update position
+ for (const char of buf) {
+ if (char === NEW_LINE_CHAR) {
+ this.#line += 1;
+ this.#column = 1;
+ } else {
+ this.#column += 1;
+ }
+ }
+
+ callback();
+ }
+}