tailwindcss-export-config
Version:
Export Tailwindcss config options to SASS, SCSS, LESS and Stylus
545 lines (429 loc) • 14.8 kB
JavaScript
;
var fse = require('fs-extra');
var path = require('path');
var TWResolveConfig = require('tailwindcss/resolveConfig');
function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }
var fse__default = /*#__PURE__*/_interopDefaultLegacy(fse);
var path__default = /*#__PURE__*/_interopDefaultLegacy(path);
var TWResolveConfig__default = /*#__PURE__*/_interopDefaultLegacy(TWResolveConfig);
function _defineProperty(obj, key, value) {
if (key in obj) {
Object.defineProperty(obj, key, {
value: value,
enumerable: true,
configurable: true,
writable: true
});
} else {
obj[key] = value;
}
return obj;
}
function indentWith(value, size) {
return ' '.repeat(size) + value;
}
/**
* Resolves a config.
* If passed a string, imports it first.
* @param {String | Object} config
* @return {Object}
*/
function resolveConfig(config) {
if (typeof config === 'string') {
config = require(config);
}
return TWResolveConfig__default["default"](config);
}
function isObject(value) {
return !Array.isArray(value) && typeof value === 'object';
}
function sanitizeKey(text) {
return text.replace(/%/g, '').replace(/, /g, '-');
}
const INDENT_BY = 2;
/**
* General converter class. To be extended by any specific format converter.
*/
class Converter {
/** @type {string} - the format and file extension */
/** @type {object} - the resolved theme configuration settings */
/** @type {object} - tailwind specific configurations */
/** @type {string} - the symbol that starts a map */
/** @type {string} - the symbol that ends a map */
/** @type {boolean} - should map keys be quoted */
/** @type {number} - should try to flatten deep maps after N level */
/** @type {array} - config keys to preserve */
/**
* @param opts
* @param {Object} opts.config - Tailwind config object
* @param {Boolean} opts.flat - Is flat or not
* @param {String} opts.prefix - If we want a variable prefix
* @param {Boolean} [opts.quotedKeys] - Should map keys be quoted
* @param {Number} [opts.flattenMapsAfter] - Should flatten maps after N level
* @param {Array} [opts.preserveKeys] - config keys to preserve
*/
constructor(opts) {
_defineProperty(this, "format", void 0);
_defineProperty(this, "theme", {});
_defineProperty(this, "configs", {});
_defineProperty(this, "mapOpener", '(\n');
_defineProperty(this, "mapCloser", ')');
_defineProperty(this, "quotedKeys", false);
_defineProperty(this, "flattenMapsAfter", -1);
_defineProperty(this, "preserveKeys", []);
_defineProperty(this, "onlyIncludeKeys", []);
_defineProperty(this, "prefixContent", '');
_defineProperty(this, "suffixContent", '');
const {
theme,
...rest
} = opts.config;
this.theme = theme;
this.configs = rest;
this.flat = opts.flat;
this.prefix = opts.prefix || '';
if (opts.quotedKeys) this.quotedKeys = opts.quotedKeys;
if (typeof opts.flattenMapsAfter !== 'undefined') this.flattenMapsAfter = opts.flattenMapsAfter;
if (typeof opts.preserveKeys !== 'undefined') this.preserveKeys = opts.preserveKeys;
if (typeof opts.onlyIncludeKeys !== 'undefined') this.onlyIncludeKeys = opts.onlyIncludeKeys;
}
/**
* Returns a variable format for the style class
* @param {string} name
* @param {string} value
* @private
*/
_buildVar(name, value) {}
/**
* Converts the supplied data to a list of variables
* @param prop
* @param data
* @private
*/
_convertObjectToVar(prop, data) {
return this._walkFlatRecursively(data, prop).join('');
}
_shouldContinueWalking(value) {
return isObject(value) || Array.isArray(value) && value.some(isObject);
}
_walkFlatRecursively(value, parentPropertyName) {
return Object.entries(value).reduce((all, [propertyName, propertyValue]) => {
const isParentArray = Array.isArray(value); // construct the var name. If parent was an array, we skip the index keys
const property = [parentPropertyName, isParentArray ? null : propertyName].filter(Boolean).join('-');
const val = this._shouldContinueWalking(propertyValue) ? this._walkFlatRecursively(propertyValue, property) : this._buildVar(this._propertyNameSanitizer(property), this._sanitizePropValue(propertyValue));
return all.concat(val);
}, []);
}
/**
* Converts the supplied data to a list of nested map objects
* @private
* @param {string} property
* @param {object} data
* @return {string}
*/
_convertObjectToMap(property, data) {
return this._buildVar(this._propertyNameSanitizer(property), this._buildMap(data));
}
/**
* Builds a map object with indentation
* @param data
* @param indent
* @return {string}
* @private
*/
_buildMap(data, indent = 0) {
// open map
return [`${this.mapOpener}`, // loop over each element
...Object.entries(data).filter(([metric]) => !!metric).map(([metric, value], index) => {
return this._buildMapData(metric, value, indent, index);
}), // close map
indentWith(this.mapCloser, indent)].join('');
}
/**
* Builds the body data of a map
* @param {string} metric - colors, backgroundColor, etc
* @param {object|string} value - the metric value, usually an object
* @param {number} indent - the number of indents to apply
* @param {number} metricIndex - the metric index it is in
* @return {string|*}
* @private
*/
_buildMapData(metric, value, indent, metricIndex) {
if (this._shouldContinueWalking(value)) {
const nestLevel = indent / INDENT_BY;
if (nestLevel <= this.flattenMapsAfter) {
return this._buildObjectEntry(metric, this._buildMap(value, indent + INDENT_BY), indent, metricIndex);
}
return this._walkRecursively(value, metric, indent, metricIndex).join('');
} // not an object so we can directly build an entry
return this._buildObjectEntry(metric, value, indent, metricIndex);
}
_walkRecursively(value, parentPropertyName, indent, metricIndex) {
const isValueArray = Array.isArray(value);
return Object.entries(value).reduce((all, [propertyName, propertyValue], index) => {
const property = [parentPropertyName, isValueArray ? null : propertyName].filter(Boolean).join('-');
const val = isObject(propertyValue) ? this._walkRecursively(propertyValue, property, indent, metricIndex) : this._buildObjectEntry(property, propertyValue, indent, index, metricIndex);
return all.concat(val);
}, []);
}
/**
* Creates a single map entry
* @param {string} key - the key of the entry. Usually concatenated prefixed string
* @param {string | array} value - the value if the entry. Should be either array or a string
* @param {number} indent - the number of indents
* @param {number} index - the current item index
* @param {number} metricIndex - the current metric's index
* @return {string}
* @private
*/
_buildObjectEntry(key, value, indent, index = 0, metricIndex) {
return indentWith(`${this._objectEntryKeySanitizer(key)}: ${this._sanitizePropValue(value)},\n`, indent + INDENT_BY);
}
/**
* Converts the options config to the required format.
* @returns {string}
*/
convert() {
let setting;
let buffer = this.prefixContent;
for (setting in this.theme) {
if (this.theme.hasOwnProperty(setting) && this._isSettingEnabled(setting)) {
const data = this.theme[setting];
const body = this.flat ? this._convertObjectToVar(setting, data) : this._convertObjectToMap(setting, data);
buffer += '\n';
buffer += body;
}
}
buffer = buffer += this.suffixContent;
return buffer;
}
/**
* Checks whether a setting is enabled or not.
* @param {string} key
* @return {boolean}
* @private
*/
_isSettingEnabled(key) {
if (this.onlyIncludeKeys.length) return this.onlyIncludeKeys.includes(key);
const {
corePlugins
} = this.configs;
if (this.preserveKeys.length && this.preserveKeys.includes(key)) return true;
if (!corePlugins) return true;
return Array.isArray(corePlugins) ? corePlugins.includes(key) : corePlugins[key] !== false;
}
/**
* Sanitizes a value, escaping and removing symbols
* @param {*} value
* @return {string|*}
* @private
*/
_sanitizePropValue(value) {
if (Array.isArray(value)) return `(${value})`.replace(/\\"/g, '"');
if ( // if its a string
typeof value === 'string' // and has comma's in it
&& value.includes(',') // but is not a concatenated map
&& !value.startsWith(this.mapOpener)) return `(${value})`;
return value;
}
/**
* Sanitizes a property name by escaping characters
* Adds prefix
* @param {string} property - the property (colors, backgroundColors)
* @return {string}
* @private
*/
_propertyNameSanitizer(property) {
property = sanitizeKey(property.replace(/\//g, '\\/').replace(/\./g, '\\.'));
return [this.prefix, property].filter(v => v).join('-');
}
/**
* Sanitizes object keys
* @param {string} key
* @return {string}
* @private
*/
_objectEntryKeySanitizer(key) {
key = sanitizeKey(key);
return this.quotedKeys ? `"${key}"` : key;
}
}
class LessConverter extends Converter {
constructor(...args) {
super(...args);
_defineProperty(this, "format", 'less');
}
_buildVar(name, value) {
return `@${name}: ${value};\n`;
}
_convertObjectToMap(prop, data) {
return this._convertObjectToVar(prop, data);
}
_sanitizePropValue(value) {
if (Array.isArray(value)) return value.join(', ');
return value;
}
}
class StylusConverter extends Converter {
constructor(...args) {
super(...args);
_defineProperty(this, "format", 'styl');
_defineProperty(this, "mapOpener", '{\n');
_defineProperty(this, "mapCloser", '}');
}
_buildVar(name, value) {
return `$${name} = ${value};\n`;
}
_objectEntryKeySanitizer(prop) {
prop = super._objectEntryKeySanitizer(prop);
if (/\d/.test(prop) && !prop.startsWith("\"")) return `"${prop}"`;
return prop;
}
}
class SassConverter extends Converter {
constructor(...args) {
super(...args);
_defineProperty(this, "format", 'sass');
_defineProperty(this, "mapOpener", '(');
_defineProperty(this, "mapCloser", ')');
}
_buildVar(name, value) {
return `$${name}: ${value}\n`;
}
_buildObjectEntry(key, value, indent, index, metricIndex = 0) {
return indentWith(`${this._objectEntryKeySanitizer(key)}: ${this._sanitizePropValue(value)},`, indent + (!index && !metricIndex ? 0 : 1));
}
}
/**
* @extends Converter
*/
class ScssConverter extends Converter {
constructor(...args) {
super(...args);
_defineProperty(this, "format", 'scss');
_defineProperty(this, "mapOpener", '(\n');
_defineProperty(this, "mapCloser", ')');
}
_buildVar(name, value) {
return `$${name}: ${value};\n`;
}
}
class CssConverter extends Converter {
constructor(...args) {
super(...args);
_defineProperty(this, "format", 'css');
_defineProperty(this, "prefixContent", '\n:root {');
_defineProperty(this, "suffixContent", '}');
}
_buildVar(name, value) {
return `--${name}: ${value};\n`;
}
_convertObjectToMap(prop, data) {
return this._convertObjectToVar(prop, data);
}
_sanitizePropValue(value) {
if (Array.isArray(value)) return value.join(', ');
return value;
}
}
class JSONConverter extends Converter {
constructor(...args) {
super(...args);
_defineProperty(this, "format", 'json');
}
convert() {
const filtered = Object.entries(this.theme).filter(([key]) => {
return this._isSettingEnabled(key);
});
return JSON.stringify(Object.fromEntries(filtered), null, 2);
}
}
var converters = {
Less: LessConverter,
Sass: SassConverter,
Scss: ScssConverter,
Stylus: StylusConverter,
Css: CssConverter,
JSON: JSONConverter
};
const allowedFormatsMap = {
stylus: converters.Stylus,
styl: converters.Stylus,
sass: converters.Sass,
scss: converters.Scss,
less: converters.Less,
json: converters.JSON,
css: converters.Css
};
/**
* Converts tailwind config into desired format
*/
class ConvertTo {
/**
* @param options
* @param {Object | String} options.config - Tailwind config. Could be either the tailwind config object or path to it
* @param {String} [options.prefix] - Variable prefix
* @param {String} [options.destination] - Output destination
* @param {Boolean} [options.flat] - Whether the variables should be nested maps or flat level variables
* @param {String} options.format - The desired format
* @param {Boolean} [options.quotedKeys] - Whether SASS keys should be quoted. Both for Sass and SCSS.
* @param {Number} [options.flattenMapsAfter] - After what nest level, do we want to flatten out nested maps.
*/
constructor(options) {
if (!allowedFormatsMap.hasOwnProperty(options.format)) {
throw new Error(`${options.format} is not supported. Use ${Object.keys(allowedFormatsMap)}`);
}
this.options = options;
const Converter = allowedFormatsMap[options.format];
const config = resolveConfig(options.config);
this.converterInstance = new Converter({ ...options,
config
});
}
/**
* Converts the config and returns a string with in the new format
* @returns {string}
*/
convert() {
let buffer = '';
if (this.options.format !== 'json') {
buffer = `/* Converted Tailwind Config to ${this.options.format} */`;
}
buffer += this.converterInstance.convert();
return buffer;
}
/**
* Write Tailwindcss config to file
* @returns {Promise}
*/
writeToFile() {
let buffer = this.convert();
return this._writeFile(buffer, {
destination: this.options.destination,
format: this.converterInstance.format
});
}
/**
* Internal method to write the supplied data to a tailwind config file with the desired format
* @param {String} data
* @param {String} destination
* @param {String} format
* @private
* @return {Promise}
*/
_writeFile(data, {
destination,
format
}) {
// If destination ends with a slash, we append a name to the file
if (destination.endsWith(path__default["default"].sep)) destination += 'tailwind-config';
const endPath = `${destination}.${format}`;
const file = path__default["default"].join(process.cwd(), endPath);
return fse__default["default"].outputFile(file, data).then(() => {
return {
destination: endPath
};
});
}
}
module.exports = ConvertTo;