postcss-tape
Version:
Quickly test PostCSS plugins
419 lines (360 loc) • 13.3 kB
JavaScript
;
var fs = require('fs');
var path = require('path');
var readline = require('readline');
function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }
function _interopNamespace(e) {
if (e && e.__esModule) return e;
var n = Object.create(null);
if (e) {
Object.keys(e).forEach(function (k) {
if (k !== 'default') {
var d = Object.getOwnPropertyDescriptor(e, k);
Object.defineProperty(n, k, d.get ? d : {
enumerable: true,
get: function () {
return e[k];
}
});
}
});
}
n['default'] = e;
return Object.freeze(n);
}
var fs__default = /*#__PURE__*/_interopDefaultLegacy(fs);
var path__default = /*#__PURE__*/_interopDefaultLegacy(path);
var readline__default = /*#__PURE__*/_interopDefaultLegacy(readline);
/** Exit the process logging an error and with a failing exit code. */
const fail$1 = error => {
console.log(error);
process.exit(1);
};
/** Exit the process with a passing exit code. */
const pass$1 = () => {
process.exit(0);
};
/** Reads a file and returns its string content */
const readFile =
/** @type {string} */
pathname =>
/** @type {Promise<string>} */
new Promise((resolve, reject) => {
fs__default['default'].readFile(pathname, 'utf8', (error, data) => {
if (error) {
reject(error);
} else {
resolve(data);
}
});
});
/** Returns a value from a JSON file. */
const readJSON = (
/** @type {string} */
pathname,
/** @type {string[]} */
...keys) => readFile(pathname).then(JSON.parse).then(opts => keys.length ? opts[Object.keys(opts).find(key => keys.includes(key))] : opts);
/** Returns the string content of a file if it exists, and otherwise creates the file and returns an empty string. */
const readOrWriteFile = (
/** @type {string} */
pathname,
/** @type {string} */
data) => readFile(pathname).catch(() => writeFile(pathname, data || '').then(() => ''));
/** Reads a file without throwing for any reason. */
const safelyReadFile =
/** @type {string} */
pathname => readFile(pathname).catch(() => '');
/** Writes a file. */
const writeFile = (
/** @type {string} */
pathname,
/** @type {string} */
data) => new Promise((resolve, reject) => {
fs__default['default'].writeFile(pathname, data, error => {
if (error) {
reject(error);
} else {
resolve();
}
});
});
/** Return the error message. */
const getErrorMessage =
/** @type {Error | string} */
error => String(Object(error).message || error);
const argRegExp = /^--([\w-]+)$/;
const primativeRegExp = /^(false|null|true|undefined|(\d+\.)?\d+|\{.*\}|\[.*\])$/;
const relaxedJsonPropRegExp = /(['"])?([a-z0-9A-Z_]+)\1:/g;
const relaxedJsonValueRegExp = /("[a-z0-9A-Z_]+":\s*)(?!true|false|null|\d+)'?([A-z0-9]+)'?([,}])/g;
/** Return an object of options from a CLI array of arguments. */
const getOptionsFromArguments =
/** @type {object} */
defaultOptions => process.argv.slice(2).reduce((
/** @type {object} */
args,
/** @type {string} */
arg,
/** @type {number} */
index,
/** @type {object} */
argv) => {
const nextIndex = index + 1;
const nextArg = argv[nextIndex];
const argMatch = arg.match(argRegExp);
if (argMatch) {
const [, name] = argMatch;
if (!nextArg || argRegExp.test(nextArg)) {
args[name] = true;
} else {
args[name] = primativeRegExp.test(nextArg) ? JSON.parse(nextArg.replace(relaxedJsonPropRegExp, '"$2": ').replace(relaxedJsonValueRegExp, '$1"$2"$3')) : nextArg;
}
}
return args;
}, Object.assign({}, defaultOptions));
/** Asynchronously return the options from the project. */
const getOptions = async () => {
const cwd = process.cwd(); // default options
const defaultOptions = {
plugin: cwd,
config: cwd,
fixtures: path__default['default'].resolve(cwd, 'test')
};
const options = await readJSON('package.json', 'postcss', 'postcssConfig').then(packageOptions => getOptionsFromArguments(Object.assign(defaultOptions, packageOptions)));
const importedPluginFile = path__default['default'].resolve(options.plugin);
const importedPlugin = await Promise.resolve().then(function () { return /*#__PURE__*/_interopNamespace(require(importedPluginFile)); });
options.plugin = importedPlugin;
if (path__default['default'].extname(options.config)) {
const importedConfig = await Promise.resolve().then(function () { return /*#__PURE__*/_interopNamespace(require(path__default['default'].resolve(options.config))); });
options.config = importedConfig.default || importedConfig;
return options;
} else {
const postcssTapeConfigFiles = ['postcss-tape.config.js', 'postcss-tape.config.mjs', 'postcss-tape.config.cjs', '.tape.js', '.tape.mjs', '.tape.cjs'];
let returnError;
while (postcssTapeConfigFiles.length) {
const postcssTapeConfigFile = path__default['default'].resolve(options.config, postcssTapeConfigFiles.shift());
try {
const importedConfig = await Promise.resolve().then(function () { return /*#__PURE__*/_interopNamespace(require(postcssTapeConfigFile)); });
options.config = importedConfig.default || importedConfig;
return options;
} catch (error) {
if (!returnError) returnError = error;
continue;
}
}
throw returnError;
}
};
/** Color keys. */
const colors = {
reset: '\x1b[0m',
bold: '\x1b[1m',
dim: '\x1b[2m',
underline: '\x1b[4m',
blink: '\x1b[5m',
reverse: '\x1b[7m',
hidden: '\x1b[8m',
black: '\x1b[30m',
red: '\x1b[31m',
green: '\x1b[32m',
yellow: '\x1b[33m',
blue: '\x1b[34m',
magenta: '\x1b[35m',
cyan: '\x1b[36m',
white: '\x1b[37m',
bgBlack: '\x1b[40m',
bgRed: '\x1b[41m',
bgGreen: '\x1b[42m',
bgYellow: '\x1b[43m',
bgBlue: '\x1b[44m',
bgMagenta: '\x1b[45m',
bgCyan: '\x1b[46m',
bgWhite: '\x1b[47m'
};
/** Return a string wrapped in a CLI color. */
const color = (
/** @type {keyof colors} */
name,
/** @type {string} */
string) => colors[name] + string.replace(colors.reset, colors.reset + colors[name]) + colors.reset;
const isWin32 = process.platform === 'win32';
const tick = isWin32 ? '√' : '✔';
const cross = isWin32 ? '×' : '✖';
const stdout = process.stdout;
let interval;
/** Log as a passing state. */
const pass = (
/** @type {string} */
name,
/** @type {string} */
message,
/** @type {boolean} */
ci) => {
clearInterval(interval);
if (ci) {
stdout.write(` ${color('green', tick)}\n`);
} else {
// reset current stream line
readline__default['default'].clearLine(stdout, 0);
readline__default['default'].cursorTo(stdout, 0);
stdout.write(`${color('green', tick)} ${name} ${color('dim', message)}\n`);
}
};
/** Log as a failing state. */
const fail = (
/** @type {string} */
name,
/** @type {string} */
message,
/** @type {string} */
details,
/** @type {boolean} */
ci) => {
clearInterval(interval);
if (ci) {
stdout.write(` ${color('red', cross)}\n${details}\n`);
} else {
// reset current stream line
readline__default['default'].clearLine(stdout, 0);
readline__default['default'].cursorTo(stdout, 0);
stdout.write(`${color('red', cross)} ${name} ${color('dim', message)}\n${details}\n`);
}
};
/** Log as a waiting state. */
const wait = (
/** @type {string} */
name,
/** @type {string} */
message,
/** @type {boolean} */
ci) => {
if (ci) {
stdout.write(`${name} ${color('dim', message)}`);
} else {
const spinner = isWin32 ? '-–—–-' : '⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏';
let index = 0;
clearInterval(interval); // reset current stream line
readline__default['default'].clearLine(stdout, 0);
readline__default['default'].cursorTo(stdout, 0);
stdout.write(`${color('yellow', spinner[index])} ${name} ${color('dim', message)}`);
interval = setInterval(() => {
index = (index + 1) % spinner.length;
readline__default['default'].cursorTo(stdout, 0);
stdout.write(`${color('yellow', spinner[index])} ${name} ${color('dim', message)}`);
}, 60);
}
};
const postcss8 = async plugins => {
const pkg = await Promise.resolve().then(function () { return /*#__PURE__*/_interopNamespace(require('postcss/package.json')); });
if (pkg.version[0] === '8') {
const m = await Promise.resolve().then(function () { return /*#__PURE__*/_interopNamespace(require('postcss')); });
return m.default(plugins);
} else {
throw new Error(`postcss@8 must be installed, found ${pkg.version}`);
}
};
const isPostcss8Plugin = plugin => typeof plugin === 'function' && Object(plugin).postcss === true;
getOptions().then(async options => {
let hadError = false; // runner
for (const name in options.config) {
const test = options.config[name];
const testBase = name.split(':')[0];
const testFull = name.split(':').join('.'); // test paths
const sourcePath = path__default['default'].resolve(options.fixtures, test.source || `${testBase}.css`);
const expectPath = path__default['default'].resolve(options.fixtures, test.expect || `${testFull}.expect.css`);
const resultPath = path__default['default'].resolve(options.fixtures, test.result || `${testFull}.result.css`);
const processOptions = Object.assign({
from: sourcePath,
to: resultPath
}, test.processOptions);
const pluginOptions = test.options;
let rawPlugin = test.plugin || options.plugin;
if (rawPlugin.default) {
rawPlugin = rawPlugin.default;
}
const plugin = isPostcss8Plugin(rawPlugin) ? rawPlugin(pluginOptions) : typeof Object(rawPlugin).process === 'function' ? rawPlugin : typeof rawPlugin === 'function' ? {
process: rawPlugin
} : Object(rawPlugin).postcssPlugin;
const pluginName = plugin.postcssPlugin || Object(rawPlugin.postcss).postcssPlugin || 'postcss';
wait(pluginName, test.message, options.ci);
try {
if (Object(test.before) instanceof Function) {
await test.before();
}
const expectCSS = await safelyReadFile(expectPath);
const sourceCSS = await readOrWriteFile(sourcePath, expectCSS);
let result;
if (isPostcss8Plugin(rawPlugin)) {
const postcss = await postcss8([plugin]);
result = await postcss.process(sourceCSS, processOptions);
} else {
result = await plugin.process(sourceCSS, processOptions, pluginOptions);
}
const resultCSS = result.css;
if (options.fix) {
await writeFile(expectPath, resultCSS);
await writeFile(resultPath, resultCSS);
} else {
await writeFile(resultPath, resultCSS);
if (expectCSS !== resultCSS) {
throw new Error([`Expected: ${JSON.stringify(expectCSS).slice(1, -1)}`, `Received: ${JSON.stringify(resultCSS).slice(1, -1)}`].join('\n'));
}
}
const warnings = result.warnings();
if (typeof test.warnings === 'number') {
if (test.warnings !== warnings.length) {
const s = warnings.length !== 1 ? 's' : '';
throw new Error(`Expected: ${test.warnings} warning${s}\nReceived: ${warnings.length} warnings`);
}
} else if (warnings.length) {
const areExpectedWarnings = warnings.every(warning => test.warnings === Object(test.warnings) && Object.keys(test.warnings).every(key => test.warnings[key] instanceof RegExp ? test.warnings[key].test(warning[key]) : test.warnings[key] === warning[key]));
if (!areExpectedWarnings) {
const s = warnings.length !== 1 ? 's' : '';
throw new Error(`Unexpected warning${s}:\n${warnings.join('\n')}`);
}
} else if (test.warnings) {
throw new Error(`Expected a warning`);
} else if (test.errors) {
throw new Error(`Expected an error`);
}
if (Object(test.after) instanceof Function) {
await test.after();
}
pass(pluginName, test.message, options.ci);
} catch (error) {
if ('error' in test) {
const isObjectError = test.error === Object(test.error);
if (isObjectError) {
const isExpectedError = Object.keys(test.error).every(key => test.error[key] instanceof RegExp ? test.error[key].test(Object(error)[key]) : test.error[key] === Object(error)[key]);
if (isExpectedError) {
pass(pluginName, test.message, options.ci);
} else {
const reportedError = Object.keys(test.error).reduce((reportedError, key) => Object.assign(reportedError, {
[key]: Object(error)[key]
}), {});
hadError = error;
fail(pluginName, test.message, ` Expected Error: ${JSON.stringify(test.error)}\n Received Error: ${JSON.stringify(reportedError)}`, options.ci);
}
} else {
const isExpectedError = typeof test.error === 'boolean' && test.error;
if (isExpectedError) {
pass(pluginName, test.message, options.ci);
} else {
hadError = error;
fail(pluginName, test.message, ` Expected Error`, options.ci);
}
if (options.ci) {
break;
}
}
} else {
hadError = error;
fail(pluginName, test.message, getErrorMessage(error), options.ci);
}
}
}
if (hadError) {
throw hadError;
}
}).then(pass$1, fail$1);
//# sourceMappingURL=index.js.map