@v4fire/client
Version:
V4Fire client core library
363 lines (301 loc) • 9.22 kB
JavaScript
;
/*!
* V4Fire Client Core
* https://github.com/V4Fire/Client
*
* Released under the MIT license
* https://github.com/V4Fire/Client/blob/master/LICENSE
*/
const
$C = require('collection.js'),
config = require('@config/config');
const
{verbose} = config.build;
const {
dsNotIncludedRequiredThemes,
dsNotIncludedDarkTheme,
dsNotIncludedLightTheme
} = include('build/stylus/ds/const');
/**
* Returns a name of a CSS variable, created from the specified path with a dot delimiter
*
* @param {!Array<string>} path
* @returns {string}
*
* @example
* ```
* getVariableName(['a', 'b']) // --a-b
* ```
*/
function getVariableName(path) {
return `--${path.join('-')}`;
}
/**
* Saves the specified value as a CSS variable into a dictionary by the specified path
*
* @param {?} value
* @param {!Array<string>} path - path to set the value
* @param {DesignSystemVariables} varStorage - dictionary of CSS variables
* @param {string=} [mapGroup] - name of a group within the `map` property of the variable storage
*
* @example
* ```
* saveVariable('blue', ['a', 'b'], cssVars)
* ```
*/
function saveVariable(value, path, varStorage, mapGroup) {
const
variable = getVariableName(path),
joinedPath = path.join('.'),
mapValue = [variable, value];
$C(varStorage).set(`var(${variable})`, path);
$C(varStorage).set(`var(${variable}-diff)`, `diff.${joinedPath}`);
if (mapGroup === undefined) {
varStorage.map[joinedPath] = mapValue;
} else {
if (!varStorage.map[mapGroup]) {
Object.defineProperty(varStorage.map, mapGroup, {value: {}, enumerable: false});
}
varStorage.map[mapGroup][joinedPath] = mapValue;
}
}
/**
* Creates a project design system from the specified raw object
*
* @param {DesignSystem} raw
* @param {Object=} [stylus]
* @returns {!BuildTimeDesignSystemParams}
*/
function createDesignSystem(raw, stylus = require('stylus')) {
const
base = Object.create(Object.freeze({meta: raw.meta, raw})),
rawCopy = Object.mixin(true, {}, raw);
delete rawCopy.meta;
const {
data,
variables
} = convertDsToBuildTimeUsableObject(rawCopy, stylus);
return {data: Object.mixin({withProto: true, withDescriptors: 'onlyAccessors'}, base, data), variables};
}
/**
* Converts the specified design system object to a Stylus object
* and creates CSS variables to use within `.styl` files
*
* @param {DesignSystem} ds
* @param {Object} stylus - link to a stylus package instance
* @returns {!BuildTimeDesignSystemParams}
*/
function convertDsToBuildTimeUsableObject(ds, stylus) {
const
variables = Object.create({map: {}});
const
builtinFnRgxp = /^[a-z-_]+\(.*\)$/,
colorHEXRgxp = /^#(?=[0-9a-fA-F]*$)(?:.{3,4}|.{6}|.{8})$/,
unitRgxp = /(-?\d+(?:\.\d+)?)(?=(px|em|rem|%)$)/;
const
data = parseRawDS(ds);
return {data, variables};
/**
* @param {!Array<string>} keys
* @param {string} theme
*/
function getVariablePath(keys, theme) {
return keys.filter((field) => !['theme', theme].includes(field));
}
/**
* Creates an array of key chunks from the passed head and tail
*
* @param {?Array} head
* @param {string|number} tail
* @returns {!Array<string>}
*
* @example
* ```js
* createArrayFrom(['deep', 'path', 'to'], 'variable', 'name') // ['deep', 'path', 'to', 'variable', 'name']
* ```
*/
function createArrayFrom(head, ...tail) {
return [...(head || []), ...tail];
}
/**
* @param {Object} obj
* @param {(Object|Array)=} [res]
* @param {Array<string>=} [path]
* @param {(string|boolean)=} [theme]
*/
function parseRawDS(obj, res, path, theme) {
if (!res) {
res = {};
}
$C(obj).forEach((value, key) => {
if (theme === true) {
if (Object.isDictionary(value)) {
parseRawDS(value, res, createArrayFrom(path, key), key);
} else {
throw new Error('Cannot find a theme dictionary');
}
} else if (key === 'theme') {
if (Object.isDictionary(value)) {
parseRawDS(value, res, createArrayFrom(path, key), true);
} else {
throw new Error('Cannot find themes dictionary');
}
} else if (Object.isDictionary(value)) {
parseRawDS(value, res, createArrayFrom(path, key), theme);
} else if (Object.isArray(value)) {
const
array = parseRawDS(value, [], [], theme);
array.forEach((el, i) => {
const
variablePath = getVariablePath(path, theme);
saveVariable(el, createArrayFrom(variablePath, key, i), variables, theme);
});
$C(res).set(array, createArrayFrom(path, key));
} else {
const
keyPath = createArrayFrom(path, key);
let
parsed;
if (builtinFnRgxp.test(value)) {
parsed = new stylus.Parser(value, {cache: false}).function();
} else if (colorHEXRgxp.test(value)) {
parsed = new stylus.Parser(value, {cache: false}).peek().key;
} else if (Object.isString(value)) {
const
unit = value.match(unitRgxp);
parsed = unit != null ? new stylus.nodes.Unit(parseFloat(unit[1]), unit[2]) : new stylus.nodes.String(value);
} else {
parsed = new stylus.nodes.Unit(value);
}
$C(res).set(parsed, keyPath);
if (path && path.length > 0) {
const
variablePath = getVariablePath(keyPath, theme);
saveVariable(parsed, variablePath, variables, theme);
}
}
});
return res;
}
}
/**
* Returns path chunks to get a themed value from the design system
*
* @param {string} field
* @param {string} [theme]
* @param {boolean} [isFieldThemed] - true, if a value of the specified field depends on the theme
* @returns {!Array<string>}
*
* @example
* ```js
* getThemedPathChunks('colors', 'light', true) // ['colors', 'theme', 'light']
* getThemedPathChunks('colors', 'light') // ['colors']
* ```
*/
function getThemedPathChunks(field, theme, isFieldThemed) {
if (isFieldThemed !== true) {
return [field];
}
return theme != null ? [field, 'theme', theme] : [field];
}
/**
* Checks the specified path to a field for obsolescence at the design system
*
* @param {!DesignSystem} ds
* @param {(string|!Array<string>)} path
*/
function checkDeprecated(ds, path) {
if (!Object.isDictionary($C(ds).get('meta.deprecated')) || !verbose) {
return false;
}
const
[field] = Object.isString(path) ? path.match(/[^.]+/) : path,
strPath = Object.isString(path) ? path.replace(/.*?\./, '') : path.slice(1).join('.'),
deprecated = ds.meta.deprecated[strPath];
if (deprecated == null) {
return false;
}
const
message = [];
if (Object.isDictionary(deprecated)) {
if (deprecated.renamedTo != null) {
message.push(
`[stylus] Warning: design system field "${field}" by path "${strPath}" was renamed to "${deprecated.renamedTo}".`,
'Please use the renamed version instead of the current, because it will be removed from the next major release.'
);
} else if (deprecated.alternative != null) {
message.push(
`[stylus] Warning: design system field "${field}" by path "${strPath}" was deprecated and will be removed from the next major release.`
);
message.push(`Please use "${deprecated.alternative}" instead.`);
}
if (deprecated.notice != null) {
message.push(deprecated.notice);
}
} else {
message.push(
`[stylus] Warning: design system field "${field}" by path "${strPath}" was deprecated and will be removed from the next major release.`
);
}
console.warn(...message);
return true;
}
/**
* Checks that the design system provides all required themes
*
* @param {object} opts
* @param {object} opts.detectUserPreferences - an object containing user preferences configuration to check
* @param {string[]} opts.themesList - an array of themes included to the build
*/
function checkRequiredThemes({detectUserPreferences, themesList}) {
Object.forEach(detectUserPreferences, (v, k) => {
switch (k) {
case 'prefersColorScheme':
checkPrefersColorScheme(v, themesList);
break;
default:
throw new Error(`A parameter with the name "${k}" is unknown in the "detectUserPreferences" context`);
}
});
}
/**
* Checks if the design system provides "dark" and "light" themes to use the `prefersColorScheme` parameter
*
* @param {object} prefersColorScheme
* @param {boolean} prefersColorScheme.enabled - a flag indicating whether the detecting of
* the user's preferred color scheme is enabled
*
* @param {string} prefersColorScheme.aliases.dark - an alias for the "dark" theme
* @param {string} prefersColorScheme.aliases.light - an alias for the "light" theme
* @param {string[]} themesList - an array of themes included to the build
*
* @throws {Error}
*/
function checkPrefersColorScheme(
{
enabled,
aliases: {dark, light} = {dark: 'dark', light: 'light'}
},
themesList
) {
if (!enabled) {
return;
}
if (!themesList?.includes(dark) && !themesList?.includes(light)) {
throw new Error(dsNotIncludedRequiredThemes(dark, light));
}
if (!themesList?.includes(dark)) {
throw new Error(dsNotIncludedDarkTheme(dark));
}
if (!themesList?.includes(light)) {
throw new Error(dsNotIncludedLightTheme(light));
}
}
module.exports = {
saveVariable,
checkDeprecated,
getVariableName,
createDesignSystem,
getThemedPathChunks,
checkRequiredThemes
};