SIGN IN SIGN UP
addyosmani / critical UNCLAIMED

Extract & Inline Critical-path CSS in HTML pages

0 0 0 JavaScript
#!/usr/bin/env node
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
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
--inlineImages Inline images
--dimensions Pass dimensions e.g. 1300x900
--ignore RegExp, @type or selector to ignore
--ignore-[OPTION] Pass options to postcss-discard. See https://goo.gl/HGo5YV
--ignoreInlinedStyles Ignore inlined stylesheets
--include RegExp, @type or selector to include
--include-[OPTION] Pass options to inline-critical. See https://goo.gl/w6SHJM
--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
--penthouse-[OPTION] Pass options to penthouse. See https://goo.gl/PQ5HLL
--ua, --userAgent User agent to use when fetching remote src
--strict Throw an error on css parsing errors or if no css is found
`;
2015-03-03 00:43:06 +01:00
2019-08-25 21:46:38 +03:00
const meowOpts = {
importMeta: import.meta,
flags: {
base: {
type: 'string',
2023-09-19 00:20:53 +02:00
shortFlag: 'b',
},
css: {
type: 'string',
2023-09-19 00:20:53 +02:00
shortFlag: 'c',
isMultiple: true,
},
width: {
2023-09-19 00:20:53 +02:00
shortFlag: 'w',
},
height: {
2023-09-19 00:20:53 +02:00
shortFlag: 'h',
},
inline: {
type: 'boolean',
2023-09-19 00:20:53 +02:00
shortFlag: 'i',
},
extract: {
type: 'boolean',
2023-09-19 00:20:53 +02:00
shortFlag: 'e',
2019-01-07 06:28:19 +01:00
default: false,
},
inlineImages: {
type: 'boolean',
},
ignoreInlinedStyles: {
type: 'boolean',
default: false,
},
ignore: {
type: 'string',
},
user: {
type: 'string',
},
strict: {
type: 'boolean',
default: false,
},
pass: {
type: 'string',
},
userAgent: {
type: 'string',
2023-09-19 00:20:53 +02:00
shortFlag: 'ua',
},
dimensions: {
type: 'string',
isMultiple: true,
},
},
};
2019-08-25 21:46:38 +03:00
const cli = meow(help, meowOpts);
2019-04-27 01:11:44 +02:00
const groupKeys = ['ignore', 'inline', 'penthouse', 'target', 'request'];
// Group args for inline-critical and penthouse
const grouped = {
...cli.flags,
...groupArgs(
groupKeys,
{
delimiter: '-',
},
2019-08-25 21:46:38 +03:00
meowOpts
),
};
2015-02-17 00:19:54 +07:00
/**
* Check if key is an alias
* @param {string} key Key to check
* @returns {boolean} True for alias
*/
const isAlias = (key) => {
if (isString(key) && key.length > 1) {
return false;
}
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);
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
*/
const isEmptyObj = (val) => isObject(val) && Object.keys(val).length === 0;
/**
* 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}
*/
const isGroupArgsDefault = (val) => isObject(val) && Object.keys(val).length === 1 && val.default;
/**
* Return regex if value is a string like this: '/.../g'
* @param {mixed} val Value to process
* @returns {mixed} Mapped values
*/
const mapRegExpStr = (val) => {
if (isString(val)) {
const {groups} = val.match(/^\/(?<regex>[^/]+)(?:\/?(?<flags>[igmy]+))?\/$/) || {};
const {regex, flags} = groups || {};
return (groups && new RegExp(escapeRegExp(regex), flags)) || val;
}
if (Array.isArray(val)) {
return val.map((v) => mapRegExpStr(v));
}
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;
}
}
2019-01-07 06:28:19 +01:00
// Cleanup camelized group keys
if (groupKeys.some((k) => key.includes(k)) && !validate(key, val)) {
2019-01-07 06:28:19 +01:00
return res;
}
if (!isAlias(key)) {
res[key] = mapRegExpStr(val);
}
2019-01-16 17:05:45 +01:00
return res;
},
{}
);
function showError(err) {
process.stderr.write(indentString(pico.red('Error: ') + err.message || err, 3));
process.stderr.write(os.EOL);
process.stderr.write(indentString(help, 3));
process.exit(1);
2015-03-03 00:43:06 +01:00
}
function run(data) {
const {_: inputs = [], css, ...opts} = {...normalizedFlags};
// Detect css globbing
const cssBegin = process.argv.findIndex((el) => ['--css', '-c'].includes(el));
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) : [];
const additionalCss = inputs.filter((file) => cssCheck.includes(file));
// 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
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)};
}),
],
[]
);
}
if (Array.isArray(css)) {
opts.css = [...css, ...additionalCss].filter(Boolean);
} else if (css || additionalCss.length > 0) {
opts.css = [css, ...additionalCss].filter(Boolean);
}
if (data) {
opts.html = data;
} else {
opts.src = input;
}
try {
generate(opts, (error, val) => {
if (error) {
showError(error);
} 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);
} else {
process.stdout.write(val.css, process.exit);
}
});
} catch (error) {
showError(error);
}
}
2015-03-03 00:43:06 +01:00
if (cli.input[0]) {
run();
2015-03-03 00:43:06 +01:00
} else {
const data = await stdin();
run(data);
}