doiuse
Version:
Lint CSS for browser support against caniuse database
168 lines (152 loc) • 4.5 kB
JavaScript
import fs from 'node:fs';
import path from 'node:path';
import { PassThrough } from 'node:stream';
import { EOL } from 'node:os';
import browserslist from 'browserslist';
import yargs from 'yargs';
import DoIUse from '../lib/DoIUse.js';
import CssUsageDuplex from '../lib/stream/CssUsageDuplex.js';
import { formatBrowserName } from '../utils/util.js';
const FILE_NOT_FOUND = 'ENOENT';
const yargConfig = yargs(process.argv.slice(2))
.usage('Lint your CSS for browser support.')
.example('cat FILE | $0 -b "ios >= 6"', '')
.example('$0 --browsers "ie >= 9, > 1%, last 3 versions" [FILE] [FILE] ...', '')
.example('$0 -b "ie >= 8" -b "> 1%" -b "last 3 versions" [FILE] [FILE] ...', '')
.options({
browsers: {
type: 'string',
alias: 'b',
description: 'Autoprefixer-like browser criteria.',
default: /** @type {string} */ (null),
string: true,
},
ignore: {
type: 'string',
alias: 'i',
description: 'List of features to ignore.',
default: '',
string: true,
},
l: {
type: 'boolean',
alias: 'list-only',
description: 'Just show the browsers and features that would be tested by'
+ 'the specified browser criteria, without actually processing any CSS.',
},
config: {
type: 'string',
alias: 'c',
description: 'Provide options through config file',
},
verbose: {
type: 'number',
alias: 'v',
description: 'Verbose output. Multiple levels available.',
},
json: {
type: 'boolean',
alias: 'j',
description: 'Output JSON instead of string linter-like messages.',
},
})
.count('verbose')
.help('h', 'Show help message.')
.alias('h', 'help');
const argv = yargConfig.parseSync();
// Config file reading
if (argv.config) {
try {
const fileData = fs.readFileSync(path.resolve(argv.config));
const config = JSON.parse(fileData.toString());
for (const [key, value] of Object.entries(config)) {
argv[key] = (key === 'browsers' && Array.isArray(value))
? value.join(',')
: value;
}
} catch (error) {
if (error && error.code === FILE_NOT_FOUND) {
process.stderr.write('Config file not found\n');
}
throw error;
}
}
const browsers = argv.browsers ? argv.browsers.split(',')
.map((/** @type {string} */ s) => s.trim()) : null;
const ignore = argv.ignore.split(',').map((s) => s.trim());
// Informational output
if (argv.l || argv.verbose >= 1) {
const formattedBrowsers = browserslist(browsers)
.map((b) => {
const [name, version] = b.split(' ');
/** @type {[string, number]} */
const formatted = [formatBrowserName(name), Number.parseInt(version, 10)];
return formatted;
})
.sort((a, b) => b[1] - a[1])
.map((b) => b.join(' '))
.join(', ');
process.stdout.write(`[doiuse] Browsers: ${formattedBrowsers}\n`);
}
if (argv.verbose >= 2) {
const { features } = new DoIUse({ browsers }).info();
process.stdout.write('[doiuse] Unsupported features:\n');
for (const feature of Object.values(features)) {
process.stdout.write(`${feature.caniuseData.title}\n`);
if (argv.verbose >= 3) {
process.stdout.write(`\n${feature.missing}\n`);
}
}
}
if (argv.l) {
// eslint-disable-next-line n/no-process-exit
process.exit(0);
}
// Process the CSS
if (argv.help || (argv._.length === 0 && process.stdin.isTTY)) {
yargConfig.showHelp();
// eslint-disable-next-line n/no-process-exit
process.exit(0);
}
/** @type {import("stream").Writable} */
let outStream;
if (argv.json) {
outStream = new PassThrough({
objectMode: true,
transform(object, encoding, next) {
next(null, JSON.stringify(object) + EOL);
},
});
} else {
outStream = new PassThrough({
objectMode: true,
transform(usage, enc, next) {
next(null, `${usage.message}\n`);
},
});
}
outStream.pipe(process.stdout);
/**
* @param {import('stream').Stream} stream
* @param {string} [file]
*/
function processStream(stream, file) {
stream.pipe(new CssUsageDuplex({
browsers: argv.browsers,
// @ts-expect-error Skip cast
ignore,
}, file))
.on('error', (error) => {
process.stderr.write('Error parsing file\n');
throw error;
})
.pipe(outStream);
}
if (argv._.length > 0) {
for (const file of argv._) {
processStream(fs.createReadStream(file.toString()), file.toString());
}
} else {
processStream(process.stdin);
}