1 | #!/usr/bin/env node
|
2 |
|
3 | import fs from 'node:fs';
|
4 | import path from 'node:path';
|
5 | import { PassThrough } from 'node:stream';
|
6 |
|
7 | import browserslist from 'browserslist';
|
8 | import ldjson from 'ldjson-stream';
|
9 | import yargs from 'yargs';
|
10 |
|
11 | import DoIUse from '../lib/DoIUse.js';
|
12 | import CssUsageDuplex from '../lib/stream/CssUsageDuplex.js';
|
13 | import { formatBrowserName } from '../utils/util.js';
|
14 |
|
15 | const FILE_NOT_FOUND = 'ENOENT';
|
16 | const 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: (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 |
|
64 | if (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 |
|
82 | const browsers = argv.browsers ? argv.browsers.split(',')
|
83 | .map((/** @type {string} */ s) => s.trim()) : null;
|
84 |
|
85 | const ignore = argv.ignore.split(',').map((s) => s.trim());
|
86 |
|
87 |
|
88 | if (argv.l || argv.verbose >= 1) {
|
89 | const formattedBrowsers = browserslist(browsers)
|
90 | .map((b) => {
|
91 | const [name, version] = b.split(' ');
|
92 |
|
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 |
|
102 | if (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 |
|
113 | if (argv.l) {
|
114 |
|
115 | process.exit(0);
|
116 | }
|
117 |
|
118 |
|
119 | if (argv.help || (argv._.length === 0 && process.stdin.isTTY)) {
|
120 | yargs.showHelp();
|
121 |
|
122 | process.exit(0);
|
123 | }
|
124 |
|
125 |
|
126 | let outStream;
|
127 | if (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 | }
|
137 | outStream.pipe(process.stdout);
|
138 |
|
139 |
|
140 |
|
141 |
|
142 |
|
143 | function processStream(stream, file) {
|
144 | stream.pipe(new CssUsageDuplex({
|
145 | browsers: argv.browsers,
|
146 |
|
147 | ignore,
|
148 | }, file))
|
149 | .on('error', (error) => {
|
150 | process.stderr.write('Error parsing file\n');
|
151 | throw error;
|
152 | })
|
153 | .pipe(outStream);
|
154 | }
|
155 | if (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 | }
|