UNPKG

postcss-tape

Version:
419 lines (360 loc) 13.3 kB
#!/usr/bin/env node 'use strict'; 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