UNPKG

4.35 kBJavaScriptView Raw
1#!/usr/bin/env node
2
3import fs from 'node:fs';
4import path from 'node:path';
5import { PassThrough } from 'node:stream';
6
7import browserslist from 'browserslist';
8import ldjson from 'ldjson-stream';
9import yargs from 'yargs';
10
11import DoIUse from '../lib/DoIUse.js';
12import CssUsageDuplex from '../lib/stream/CssUsageDuplex.js';
13import { formatBrowserName } from '../utils/util.js';
14
15const FILE_NOT_FOUND = 'ENOENT';
16const argv = await yargs(process.argv.slice(2))
17 .usage('Lint your CSS for browser support.')
18 .example('cat FILE | $0 -b "ios >= 6"', '')
19 .example('$0 --browsers "ie >= 9, > 1%, last 3 versions" [FILE] [FILE] ...', '')
20 .example('$0 -b "ie >= 8" -b "> 1%" -b "last 3 versions" [FILE] [FILE] ...', '')
21 .options({
22 browsers: {
23 type: 'string',
24 alias: 'b',
25 description: 'Autoprefixer-like browser criteria.',
26 default: /** @type {string} */ (null),
27 string: true,
28 },
29 ignore: {
30 type: 'string',
31 alias: 'i',
32 description: 'List of features to ignore.',
33 default: '',
34 string: true,
35 },
36 l: {
37 type: 'boolean',
38 alias: 'list-only',
39 description: 'Just show the browsers and features that would be tested by'
40 + 'the specified browser criteria, without actually processing any CSS.',
41 },
42 config: {
43 type: 'string',
44 alias: 'c',
45 description: 'Provide options through config file',
46 },
47 verbose: {
48 type: 'number',
49 alias: 'v',
50 description: 'Verbose output. Multiple levels available.',
51 },
52 json: {
53 type: 'boolean',
54 alias: 'j',
55 description: 'Output JSON instead of string linter-like messages.',
56 },
57 })
58 .count('verbose')
59 .help('h', 'Show help message.')
60 .alias('h', 'help')
61 .argv;
62
63// Config file reading
64if (argv.config) {
65 try {
66 const fileData = fs.readFileSync(path.resolve(argv.config));
67 const config = JSON.parse(fileData.toString());
68
69 for (const [key, value] of Object.entries(config)) {
70 argv[key] = (key === 'browsers' && Array.isArray(value))
71 ? value.join(',')
72 : value;
73 }
74 } catch (error) {
75 if (error && error.code === FILE_NOT_FOUND) {
76 process.stderr.write('Config file not found\n');
77 }
78 throw error;
79 }
80}
81
82const browsers = argv.browsers ? argv.browsers.split(',')
83 .map((/** @type {string} */ s) => s.trim()) : null;
84
85const ignore = argv.ignore.split(',').map((s) => s.trim());
86
87// Informational output
88if (argv.l || argv.verbose >= 1) {
89 const formattedBrowsers = browserslist(browsers)
90 .map((b) => {
91 const [name, version] = b.split(' ');
92 /** @type {[string, number]} */
93 const formatted = [formatBrowserName(name), Number.parseInt(version, 10)];
94 return formatted;
95 })
96 .sort((a, b) => b[1] - a[1])
97 .map((b) => b.join(' '))
98 .join(', ');
99 process.stdout.write(`[doiuse] Browsers: ${formattedBrowsers}\n`);
100}
101
102if (argv.verbose >= 2) {
103 const { features } = new DoIUse({ browsers }).info();
104 process.stdout.write('[doiuse] Unsupported features:\n');
105 for (const feature of Object.values(features)) {
106 process.stdout.write(`${feature.caniuseData.title}\n`);
107 if (argv.verbose >= 3) {
108 process.stdout.write(`\n${feature.missing}\n`);
109 }
110 }
111}
112
113if (argv.l) {
114 // eslint-disable-next-line n/no-process-exit
115 process.exit(0);
116}
117
118// Process the CSS
119if (argv.help || (argv._.length === 0 && process.stdin.isTTY)) {
120 yargs.showHelp();
121 // eslint-disable-next-line n/no-process-exit
122 process.exit(0);
123}
124
125/** @type {import("stream").Writable} */
126let outStream;
127if (argv.json) {
128 outStream = ldjson.serialize();
129} else {
130 outStream = new PassThrough({
131 objectMode: true,
132 transform(usage, enc, next) {
133 next(null, `${usage.message}\n`);
134 },
135 });
136}
137outStream.pipe(process.stdout);
138
139/**
140 * @param {import('stream').Stream} stream
141 * @param {string} [file]
142 */
143function processStream(stream, file) {
144 stream.pipe(new CssUsageDuplex({
145 browsers: argv.browsers,
146 // @ts-expect-error Skip cast
147 ignore,
148 }, file))
149 .on('error', (error) => {
150 process.stderr.write('Error parsing file\n');
151 throw error;
152 })
153 .pipe(outStream);
154}
155if (argv._.length > 0) {
156 for (const file of argv._) {
157 processStream(fs.createReadStream(file.toString()), file.toString());
158 }
159} else {
160 processStream(process.stdin);
161}