2014-11-01 00:34:05 +01:00
|
|
|
#!/usr/bin/env node
|
2022-09-04 23:17:08 +02:00
|
|
|
import os from 'node:os';
|
|
|
|
|
import process from 'node:process';
|
|
|
|
|
import stdin from 'get-stdin';
|
|
|
|
|
import groupArgs from 'group-args';
|
|
|
|
|
import indentString from 'indent-string';
|
|
|
|
|
import {escapeRegExp, isObject, isString, reduce} from 'lodash-es';
|
|
|
|
|
import meow from 'meow';
|
|
|
|
|
import pico from 'picocolors';
|
|
|
|
|
import {validate} from './src/config.js';
|
|
|
|
|
import {generate} from './index.js';
|
2016-08-03 04:09:38 +03:00
|
|
|
|
2018-03-15 06:29:17 +01:00
|
|
|
const help = `
|
|
|
|
|
Usage: critical <input> [<option>]
|
|
|
|
|
|
|
|
|
|
Options:
|
|
|
|
|
-b, --base Your base directory
|
|
|
|
|
-c, --css Your CSS Files (optional)
|
|
|
|
|
-w, --width Viewport width
|
|
|
|
|
-h, --height Viewport height
|
|
|
|
|
-i, --inline Generate the HTML with inlined critical-path CSS
|
|
|
|
|
-e, --extract Extract inlined styles from referenced stylesheets
|
2018-11-27 05:31:05 +01:00
|
|
|
|
|
|
|
|
--inlineImages Inline images
|
2021-09-06 21:21:54 +02:00
|
|
|
--dimensions Pass dimensions e.g. 1300x900
|
2018-03-15 06:29:17 +01:00
|
|
|
--ignore RegExp, @type or selector to ignore
|
2018-11-27 05:31:05 +01:00
|
|
|
--ignore-[OPTION] Pass options to postcss-discard. See https://goo.gl/HGo5YV
|
2023-04-10 00:29:36 +02:00
|
|
|
--ignoreInlinedStyles Ignore inlined stylesheets
|
2018-03-15 06:29:17 +01:00
|
|
|
--include RegExp, @type or selector to include
|
2018-11-27 05:31:05 +01:00
|
|
|
--include-[OPTION] Pass options to inline-critical. See https://goo.gl/w6SHJM
|
2023-04-10 00:29:36 +02:00
|
|
|
--assetPaths Directories/Urls where the inliner should start looking for assets
|
2018-05-02 23:51:09 +02:00
|
|
|
--user RFC2617 basic authorization user
|
|
|
|
|
--pass RFC2617 basic authorization password
|
2018-11-27 05:31:05 +01:00
|
|
|
--penthouse-[OPTION] Pass options to penthouse. See https://goo.gl/PQ5HLL
|
|
|
|
|
--ua, --userAgent User agent to use when fetching remote src
|
2023-04-10 00:29:36 +02:00
|
|
|
--strict Throw an error on css parsing errors or if no css is found
|
2018-03-15 06:29:17 +01:00
|
|
|
`;
|
2015-03-03 00:43:06 +01:00
|
|
|
|
2019-08-25 21:46:38 +03:00
|
|
|
const meowOpts = {
|
2022-09-04 23:17:08 +02:00
|
|
|
importMeta: import.meta,
|
2018-11-27 05:31:05 +01:00
|
|
|
flags: {
|
|
|
|
|
base: {
|
|
|
|
|
type: 'string',
|
2023-09-19 00:20:53 +02:00
|
|
|
shortFlag: 'b',
|
2018-11-27 05:31:05 +01:00
|
|
|
},
|
|
|
|
|
css: {
|
|
|
|
|
type: 'string',
|
2023-09-19 00:20:53 +02:00
|
|
|
shortFlag: 'c',
|
2022-10-03 23:28:38 +02:00
|
|
|
isMultiple: true,
|
2018-11-27 05:31:05 +01:00
|
|
|
},
|
|
|
|
|
width: {
|
2023-09-19 00:20:53 +02:00
|
|
|
shortFlag: 'w',
|
2018-11-27 05:31:05 +01:00
|
|
|
},
|
|
|
|
|
height: {
|
2023-09-19 00:20:53 +02:00
|
|
|
shortFlag: 'h',
|
2018-11-27 05:31:05 +01:00
|
|
|
},
|
|
|
|
|
inline: {
|
|
|
|
|
type: 'boolean',
|
2023-09-19 00:20:53 +02:00
|
|
|
shortFlag: 'i',
|
2018-11-27 05:31:05 +01:00
|
|
|
},
|
|
|
|
|
extract: {
|
|
|
|
|
type: 'boolean',
|
2023-09-19 00:20:53 +02:00
|
|
|
shortFlag: 'e',
|
2019-01-07 06:28:19 +01:00
|
|
|
default: false,
|
2018-11-27 05:31:05 +01:00
|
|
|
},
|
|
|
|
|
inlineImages: {
|
|
|
|
|
type: 'boolean',
|
|
|
|
|
},
|
2023-04-10 00:29:36 +02:00
|
|
|
ignoreInlinedStyles: {
|
|
|
|
|
type: 'boolean',
|
|
|
|
|
default: false,
|
|
|
|
|
},
|
2018-11-27 05:31:05 +01:00
|
|
|
ignore: {
|
|
|
|
|
type: 'string',
|
|
|
|
|
},
|
|
|
|
|
user: {
|
|
|
|
|
type: 'string',
|
|
|
|
|
},
|
2023-04-10 00:29:36 +02:00
|
|
|
strict: {
|
|
|
|
|
type: 'boolean',
|
|
|
|
|
default: false,
|
|
|
|
|
},
|
2018-11-27 05:31:05 +01:00
|
|
|
pass: {
|
|
|
|
|
type: 'string',
|
|
|
|
|
},
|
|
|
|
|
userAgent: {
|
|
|
|
|
type: 'string',
|
2023-09-19 00:20:53 +02:00
|
|
|
shortFlag: 'ua',
|
2018-11-27 05:31:05 +01:00
|
|
|
},
|
2020-08-19 01:35:40 +02:00
|
|
|
dimensions: {
|
|
|
|
|
type: 'string',
|
|
|
|
|
isMultiple: true,
|
|
|
|
|
},
|
2018-11-27 05:31:05 +01:00
|
|
|
},
|
2017-07-10 00:42:20 +02:00
|
|
|
};
|
|
|
|
|
|
2019-08-25 21:46:38 +03:00
|
|
|
const cli = meow(help, meowOpts);
|
2017-07-10 00:42:20 +02:00
|
|
|
|
2019-04-27 01:11:44 +02:00
|
|
|
const groupKeys = ['ignore', 'inline', 'penthouse', 'target', 'request'];
|
2017-07-10 00:42:20 +02:00
|
|
|
// Group args for inline-critical and penthouse
|
2018-11-27 05:31:05 +01:00
|
|
|
const grouped = {
|
|
|
|
|
...cli.flags,
|
|
|
|
|
...groupArgs(
|
|
|
|
|
groupKeys,
|
|
|
|
|
{
|
|
|
|
|
delimiter: '-',
|
|
|
|
|
},
|
2019-08-25 21:46:38 +03:00
|
|
|
meowOpts
|
2018-11-27 05:31:05 +01:00
|
|
|
),
|
|
|
|
|
};
|
2015-02-17 00:19:54 +07:00
|
|
|
|
2018-11-27 05:31:05 +01:00
|
|
|
/**
|
|
|
|
|
* Check if key is an alias
|
|
|
|
|
* @param {string} key Key to check
|
|
|
|
|
* @returns {boolean} True for alias
|
|
|
|
|
*/
|
2020-04-08 23:58:14 +02:00
|
|
|
const isAlias = (key) => {
|
2018-11-27 05:31:05 +01:00
|
|
|
if (isString(key) && key.length > 1) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
2020-06-14 00:06:39 +02:00
|
|
|
const aliases = Object.keys(meowOpts.flags)
|
2023-09-19 00:20:53 +02:00
|
|
|
.filter((k) => meowOpts.flags[k].shortFlag)
|
|
|
|
|
.map((k) => meowOpts.flags[k].shortFlag);
|
2018-11-27 05:31:05 +01:00
|
|
|
|
|
|
|
|
return aliases.includes(key);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Check if value is an empty object
|
|
|
|
|
* @param {mixed} val Value to check
|
2019-01-17 10:37:23 +02:00
|
|
|
* @returns {boolean} Whether or not this is an empty object
|
2018-11-27 05:31:05 +01:00
|
|
|
*/
|
2020-04-08 23:58:14 +02:00
|
|
|
const isEmptyObj = (val) => isObject(val) && Object.keys(val).length === 0;
|
2018-11-27 05:31:05 +01:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Check if value is transformed to {default: val}
|
|
|
|
|
* @param {mixed} val Value to check
|
|
|
|
|
* @returns {boolean} True if it's been converted to {default: value}
|
|
|
|
|
*/
|
2020-04-08 23:58:14 +02:00
|
|
|
const isGroupArgsDefault = (val) => isObject(val) && Object.keys(val).length === 1 && val.default;
|
2018-11-27 05:31:05 +01:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Return regex if value is a string like this: '/.../g'
|
|
|
|
|
* @param {mixed} val Value to process
|
|
|
|
|
* @returns {mixed} Mapped values
|
|
|
|
|
*/
|
2020-04-08 23:58:14 +02:00
|
|
|
const mapRegExpStr = (val) => {
|
2018-11-27 05:31:05 +01:00
|
|
|
if (isString(val)) {
|
2020-04-08 23:58:14 +02:00
|
|
|
const {groups} = val.match(/^\/(?<regex>[^/]+)(?:\/?(?<flags>[igmy]+))?\/$/) || {};
|
|
|
|
|
const {regex, flags} = groups || {};
|
|
|
|
|
|
|
|
|
|
return (groups && new RegExp(escapeRegExp(regex), flags)) || val;
|
2018-11-27 05:31:05 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (Array.isArray(val)) {
|
2020-04-08 23:58:14 +02:00
|
|
|
return val.map((v) => mapRegExpStr(v));
|
2018-11-27 05:31:05 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return val;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const normalizedFlags = reduce(
|
|
|
|
|
grouped,
|
|
|
|
|
(res, val, key) => {
|
|
|
|
|
// Cleanup groupArgs mess ;)
|
|
|
|
|
if (groupKeys.includes(key)) {
|
|
|
|
|
// An empty object means param without value, just true
|
|
|
|
|
if (isEmptyObj(val)) {
|
|
|
|
|
val = true;
|
|
|
|
|
} else if (isGroupArgsDefault(val)) {
|
|
|
|
|
val = val.default;
|
|
|
|
|
}
|
2014-12-07 01:20:22 +01:00
|
|
|
}
|
|
|
|
|
|
2019-01-07 06:28:19 +01:00
|
|
|
// Cleanup camelized group keys
|
2021-02-26 00:14:56 +01:00
|
|
|
if (groupKeys.some((k) => key.includes(k)) && !validate(key, val)) {
|
2019-01-07 06:28:19 +01:00
|
|
|
return res;
|
|
|
|
|
}
|
|
|
|
|
|
2018-11-27 05:31:05 +01:00
|
|
|
if (!isAlias(key)) {
|
|
|
|
|
res[key] = mapRegExpStr(val);
|
|
|
|
|
}
|
2019-01-16 17:05:45 +01:00
|
|
|
|
2014-12-07 01:20:22 +01:00
|
|
|
return res;
|
2018-11-27 05:31:05 +01:00
|
|
|
},
|
|
|
|
|
{}
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
function showError(err) {
|
2022-09-04 23:17:08 +02:00
|
|
|
process.stderr.write(indentString(pico.red('Error: ') + err.message || err, 3));
|
2018-11-27 05:31:05 +01:00
|
|
|
process.stderr.write(os.EOL);
|
|
|
|
|
process.stderr.write(indentString(help, 3));
|
|
|
|
|
process.exit(1);
|
2015-03-03 00:43:06 +01:00
|
|
|
}
|
|
|
|
|
|
2014-11-01 00:34:05 +01:00
|
|
|
function run(data) {
|
2018-11-28 06:01:57 +01:00
|
|
|
const {_: inputs = [], css, ...opts} = {...normalizedFlags};
|
|
|
|
|
|
|
|
|
|
// Detect css globbing
|
2020-04-08 23:58:14 +02:00
|
|
|
const cssBegin = process.argv.findIndex((el) => ['--css', '-c'].includes(el));
|
2018-11-28 06:01:57 +01:00
|
|
|
const cssEnd = process.argv.findIndex((el, index) => index > cssBegin && el.startsWith('-'));
|
2019-01-07 06:28:19 +01:00
|
|
|
const cssCheck = cssBegin >= 0 ? process.argv.slice(cssBegin, cssEnd > 0 ? cssEnd : undefined) : [];
|
2020-04-08 23:58:14 +02:00
|
|
|
const additionalCss = inputs.filter((file) => cssCheck.includes(file));
|
2018-11-28 06:01:57 +01:00
|
|
|
// Just take the first html input as we don't support multiple html sources for
|
2020-08-19 00:44:11 +02:00
|
|
|
const [input] = inputs.filter((file) => !additionalCss.includes(file)); // eslint-disable-line unicorn/prefer-array-find
|
2018-11-28 06:01:57 +01:00
|
|
|
|
2020-08-19 01:35:40 +02:00
|
|
|
if (Array.isArray(opts.dimensions)) {
|
|
|
|
|
opts.dimensions = opts.dimensions.reduce(
|
|
|
|
|
(result, data) => [
|
|
|
|
|
...result,
|
|
|
|
|
...data.split(',').map((dimension) => {
|
|
|
|
|
const [width, height] = dimension.split('x');
|
|
|
|
|
return {width: Number.parseInt(width, 10), height: Number.parseInt(height, 10)};
|
|
|
|
|
}),
|
|
|
|
|
],
|
|
|
|
|
[]
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2018-11-28 06:01:57 +01:00
|
|
|
if (Array.isArray(css)) {
|
2022-09-04 23:17:08 +02:00
|
|
|
opts.css = [...css, ...additionalCss].filter(Boolean);
|
2018-11-28 06:01:57 +01:00
|
|
|
} else if (css || additionalCss.length > 0) {
|
2022-09-04 23:17:08 +02:00
|
|
|
opts.css = [css, ...additionalCss].filter(Boolean);
|
2018-11-28 06:01:57 +01:00
|
|
|
}
|
|
|
|
|
|
2018-11-27 05:31:05 +01:00
|
|
|
if (data) {
|
|
|
|
|
opts.html = data;
|
|
|
|
|
} else {
|
|
|
|
|
opts.src = input;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
2022-09-04 23:17:08 +02:00
|
|
|
generate(opts, (error, val) => {
|
2018-11-27 05:31:05 +01:00
|
|
|
if (error) {
|
|
|
|
|
showError(error);
|
2019-01-03 23:41:32 +01:00
|
|
|
} else if (opts.inline) {
|
|
|
|
|
process.stdout.write(val.html, process.exit);
|
2019-01-07 06:28:19 +01:00
|
|
|
} else if (opts.extract) {
|
2019-01-07 23:50:19 +01:00
|
|
|
process.stdout.write(val.uncritical, process.exit);
|
2018-11-27 05:31:05 +01:00
|
|
|
} else {
|
|
|
|
|
process.stdout.write(val.css, process.exit);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
} catch (error) {
|
|
|
|
|
showError(error);
|
|
|
|
|
}
|
2014-11-01 00:34:05 +01:00
|
|
|
}
|
|
|
|
|
|
2015-03-03 00:43:06 +01:00
|
|
|
if (cli.input[0]) {
|
2018-11-27 05:31:05 +01:00
|
|
|
run();
|
2015-03-03 00:43:06 +01:00
|
|
|
} else {
|
2022-12-11 23:42:28 +01:00
|
|
|
const data = await stdin();
|
|
|
|
|
run(data);
|
2014-11-01 00:34:05 +01:00
|
|
|
}
|