diff --git a/.changeset/quick-starfishes-taste.md b/.changeset/quick-starfishes-taste.md new file mode 100644 index 00000000..5d792695 --- /dev/null +++ b/.changeset/quick-starfishes-taste.md @@ -0,0 +1,26 @@ +--- +'arui-scripts': minor +--- + +Добавлен кастомный плагин postcss-global-variables, для оптимизации времени обработки глобальных переменных. +@csstools/postcss-global-data *удален* + +Проекты, которые использовали в оверрайдах кастомные настройки для плагина @csstools/postcss-global-data, должны перейти на использование postcss-global-variables следующим образом +``` +postcss: (config) => { + const overrideConfig = config.map((plugin) => { + if (plugin.name === 'postCssGlobalVariables') { + return { + ...plugin, + options: plugin.options.concat([ + // ваши файлы + ]) + } + } + return plugin; + }); + + return overrideConfig; +} +``` +Плагин работает только с глобальными переменными, если вам надо вставить что-то другое, отличное от глобальных переменных, вам нужно будет добавить @csstools/postcss-global-data в свой проект самостоятельно diff --git a/packages/arui-scripts/package.json b/packages/arui-scripts/package.json index 539ec098..d10c4e7a 100644 --- a/packages/arui-scripts/package.json +++ b/packages/arui-scripts/package.json @@ -36,7 +36,6 @@ "@babel/preset-react": "^7.23.3", "@babel/preset-typescript": "^7.23.3", "@babel/runtime": "^7.23.8", - "@csstools/postcss-global-data": "^2.0.1", "@pmmmwh/react-refresh-webpack-plugin": "0.5.11", "@swc/core": "^1.7.35", "@swc/jest": "^0.2.36", diff --git a/packages/arui-scripts/src/configs/postcss.config.ts b/packages/arui-scripts/src/configs/postcss.config.ts index 982d0db1..6fece934 100644 --- a/packages/arui-scripts/src/configs/postcss.config.ts +++ b/packages/arui-scripts/src/configs/postcss.config.ts @@ -1,23 +1,37 @@ import path from 'path'; +import type { PluginCreator } from 'postcss'; + +import { postCssGlobalVariables } from '../plugins/postcss-global-variables/postcss-global-variables'; + import config from './app-configs'; import supportingBrowsers from './supporting-browsers'; + +type PostCssPluginName = string | PluginCreator; +type PostcssPlugin = string | [string, unknown] | {name: string; plugin: PluginCreator; options?: unknown}; + /** * Функция для создания конфигурационного файла postcss - * @param {String[]} plugins список плагинов + * @param {PostCssPluginName[]} plugins список плагинов * @param {Object} options коллекция конфигураций плагинов, где ключ - название плагина, а значение - аргумент для инициализации * @returns {*} */ export function createPostcssConfig( - plugins: string[], + plugins: PostCssPluginName[], options: Record, -): string[] | unknown[] { +): PostcssPlugin[] { return plugins.map((pluginName) => { - if (pluginName in options) { - return [pluginName, options[pluginName]]; + if (typeof pluginName === 'string') { + return pluginName in options + ? [pluginName, options[pluginName]] + : pluginName; } - return pluginName; + return { + name: pluginName.name, + plugin: pluginName, + options: options[pluginName.name] + } }); } @@ -28,7 +42,7 @@ export const postcssPlugins = [ 'postcss-mixins', 'postcss-for', 'postcss-each', - '@csstools/postcss-global-data', + postCssGlobalVariables, 'postcss-custom-media', 'postcss-color-mod-function', !config.keepCssVars && 'postcss-custom-properties', @@ -40,13 +54,13 @@ export const postcssPlugins = [ 'autoprefixer', 'postcss-inherit', 'postcss-discard-comments', -].filter(Boolean) as string[]; +].filter(Boolean) as PostCssPluginName[]; export const postcssPluginsOptions = { 'postcss-import': { path: ['./src'], }, - '@csstools/postcss-global-data': { + 'postCssGlobalVariables': { files: [path.join(__dirname, 'mq.css'), config.componentsTheme].filter(Boolean) as string[], }, 'postcss-url': { diff --git a/packages/arui-scripts/src/configs/postcss.ts b/packages/arui-scripts/src/configs/postcss.ts index 122c706b..82559ea3 100644 --- a/packages/arui-scripts/src/configs/postcss.ts +++ b/packages/arui-scripts/src/configs/postcss.ts @@ -4,6 +4,10 @@ import { createPostcssConfig, postcssPlugins, postcssPluginsOptions } from './po const postcssConfig = applyOverrides( 'postcss', createPostcssConfig(postcssPlugins, postcssPluginsOptions), -); + // тк дается возможность переопределять options для плагинов импортируемых напрямую + // инициализировать их нужно после оверайдов +).map((plugin) => typeof plugin === 'string' || Array.isArray(plugin) + ? plugin + : plugin.plugin(plugin.options)); export default postcssConfig; diff --git a/packages/arui-scripts/src/plugins/postcss-global-variables/postcss-global-variables.ts b/packages/arui-scripts/src/plugins/postcss-global-variables/postcss-global-variables.ts new file mode 100644 index 00000000..0462d94e --- /dev/null +++ b/packages/arui-scripts/src/plugins/postcss-global-variables/postcss-global-variables.ts @@ -0,0 +1,53 @@ +import type { AtRule, Plugin, PluginCreator, Rule } from 'postcss'; + +import { insertParsedCss, parseImport, parseMediaQuery, parseVariables } from './utils/utils'; + +type PluginOptions = { + files?: string[]; +}; + +const postCssGlobalVariables: PluginCreator = (opts?: PluginOptions) => { + const options = { + files: [], + ...opts, + }; + + const parsedVariables: Record = {}; + const parsedCustomMedia: Record = {}; + + let rulesSelectors = new Set(); + + return { + postcssPlugin: '@alfalab/postcss-global-variables', + prepare(): Plugin { + return { + postcssPlugin: '@alfalab/postcss-global-variables', + Once(root, postcssHelpers): void { + if (!Object.keys(parsedVariables).length) { + options.files.forEach((filePath) => { + const importedCss = parseImport(root, postcssHelpers, filePath); + + parseVariables(importedCss, parsedVariables); + parseMediaQuery(importedCss, parsedCustomMedia); + }); + } + + const rootRule = insertParsedCss(root, parsedVariables, parsedCustomMedia); + + root.append(rootRule); + rulesSelectors.add(rootRule) + }, + OnceExit(): void { + rulesSelectors.forEach((rule) => { + rule.remove(); + }); + rulesSelectors = new Set(); + }, + }; + }, + }; +}; + +postCssGlobalVariables.postcss = true; + +export { postCssGlobalVariables }; \ No newline at end of file diff --git a/packages/arui-scripts/src/plugins/postcss-global-variables/utils/__tests__/add-global-variable.tests.ts b/packages/arui-scripts/src/plugins/postcss-global-variables/utils/__tests__/add-global-variable.tests.ts new file mode 100644 index 00000000..402f8bea --- /dev/null +++ b/packages/arui-scripts/src/plugins/postcss-global-variables/utils/__tests__/add-global-variable.tests.ts @@ -0,0 +1,69 @@ +import { Rule } from 'postcss'; + +import { addGlobalVariable } from '../utils'; + +describe('addGlobalVariable', () => { + it('Должен добавлять переменные, найденные в cssValue, в rootSelector', () => { + const mockRootSelector = new Rule({ selector: ':root' }); + const parsedVariables = { + '--color-primary': '#ff0000', + }; + + addGlobalVariable('var(--color-primary)', mockRootSelector, parsedVariables); + + expect(mockRootSelector.nodes).toMatchObject([ + { prop: '--color-primary', value: '#ff0000' } + ]); + }); + + it('Должен рекурсивно добавлять вложенные переменные', () => { + const mockRootSelector = new Rule({ selector: ':root' }); + const mockRootSelectorWithSpace = new Rule({ selector: ':root' }); + const mockRootSelectorWithNewLine = new Rule({ selector: ':root' }); + + const parsedVariables = { + '--color-primary': 'var(--color-secondary)', + '--color-secondary': '#00ff00', + }; + + const parsedVariablesWithSpace = { + '--color-primary': 'var( --color-secondary )', + '--color-secondary': '#00ff00', + }; + + const parsedVariablesWithNewLine = { + '--color-primary': 'var(\n --color-secondary\n )', + '--color-secondary': '#00ff00', + }; + + addGlobalVariable('var(--color-primary)', mockRootSelector, parsedVariables); + addGlobalVariable('var(--color-primary)', mockRootSelectorWithSpace, parsedVariablesWithSpace); + addGlobalVariable('var(--color-primary)', mockRootSelectorWithNewLine, parsedVariablesWithNewLine); + + expect(mockRootSelector.nodes).toMatchObject([ + { prop: '--color-primary', value: 'var(--color-secondary)' }, + { prop: '--color-secondary', value: '#00ff00' }, + ]); + + expect(mockRootSelectorWithSpace.nodes).toMatchObject([ + { prop: '--color-primary', value: 'var( --color-secondary )' }, + { prop: '--color-secondary', value: '#00ff00' }, + ]); + + expect(mockRootSelectorWithNewLine.nodes).toMatchObject([ + { prop: '--color-primary', value: 'var(\n --color-secondary\n )' }, + { prop: '--color-secondary', value: '#00ff00' }, + ]); + }); + + it('Не должен добавлять переменные, если их нет в parsedVariables', () => { + const mockRootSelector = new Rule({ selector: ':root' }); + const parsedVariables = { + 'color-primary': '#ff0000', + }; + + addGlobalVariable('var(--color-secondary)', mockRootSelector, parsedVariables); + + expect(mockRootSelector.nodes).toEqual([]); + }); +}); diff --git a/packages/arui-scripts/src/plugins/postcss-global-variables/utils/__tests__/get-media-query-name.tests.ts b/packages/arui-scripts/src/plugins/postcss-global-variables/utils/__tests__/get-media-query-name.tests.ts new file mode 100644 index 00000000..36bd8d7b --- /dev/null +++ b/packages/arui-scripts/src/plugins/postcss-global-variables/utils/__tests__/get-media-query-name.tests.ts @@ -0,0 +1,17 @@ +import type { AtRule } from 'postcss'; + +import { getMediaQueryName, } from '../utils'; + +describe('getMediaQueryName', () => { + it('Должен возвращать имя медиа-запроса', () => { + const rule: AtRule = { params: 'screen and (min-width: 768px)' } as AtRule; + + expect(getMediaQueryName(rule)).toBe('screen'); + }); + + it('Должен возвращать пустую строку, если params пустой', () => { + const rule: AtRule = { params: '' } as AtRule; + + expect(getMediaQueryName(rule)).toBe(''); + }); +}); \ No newline at end of file diff --git a/packages/arui-scripts/src/plugins/postcss-global-variables/utils/__tests__/parse-variables.tests.ts b/packages/arui-scripts/src/plugins/postcss-global-variables/utils/__tests__/parse-variables.tests.ts new file mode 100644 index 00000000..b52a2f3d --- /dev/null +++ b/packages/arui-scripts/src/plugins/postcss-global-variables/utils/__tests__/parse-variables.tests.ts @@ -0,0 +1,29 @@ +import type { Declaration,Root } from 'postcss'; + +import { parseVariables } from '../utils'; + +describe('parseVariables', () => { + it('Должен корректно заполнять объект переменными на основе импортируемого файла', () => { + const parsedVariables: Record = {}; + + const mockImportedFile = { + walkDecls: (callback: (decl: Declaration, index: number) => false | void) => { + const mockDeclarations: Declaration[] = [ + { prop: '--color-primary', value: '#3498db' } as Declaration, + { prop: '--font-size', value: 'var(--gap-24)' } as Declaration, + ]; + + mockDeclarations.forEach(callback); + + return false; + } + }; + + parseVariables(mockImportedFile as Root, parsedVariables); + + expect(parsedVariables).toEqual({ + '--color-primary': '#3498db', + '--font-size': 'var(--gap-24)', + }); + }); +}); diff --git a/packages/arui-scripts/src/plugins/postcss-global-variables/utils/utils.ts b/packages/arui-scripts/src/plugins/postcss-global-variables/utils/utils.ts new file mode 100644 index 00000000..75e60a9e --- /dev/null +++ b/packages/arui-scripts/src/plugins/postcss-global-variables/utils/utils.ts @@ -0,0 +1,83 @@ +import fs from 'fs'; +import path from 'path'; + +import { AtRule, Declaration, Helpers, Root, Rule } from 'postcss'; + +export const getMediaQueryName = (rule: AtRule) => rule.params.split(' ')[0]; + +export function parseImport(root: Root, postcssHelpers: Helpers, filePath: string) { + let resolvedPath = ''; + + try { + resolvedPath = path.resolve(filePath); + } catch (err) { + throw new Error(`Failed to read ${filePath} with error ${(err instanceof Error) ? err.message : err}`); + } + + postcssHelpers.result.messages.push({ + type: 'dependency', + plugin: 'postcss-global-environments', + file: resolvedPath, + parent: root.source?.input?.file, + }); + + const fileContents = fs.readFileSync(resolvedPath, 'utf8'); + + return postcssHelpers.postcss.parse(fileContents, { from: resolvedPath }); +} + +export const parseVariables = (importedFile: Root, parsedVariables: Record) => { + importedFile.walkDecls((decl) => { + // eslint-disable-next-line no-param-reassign + parsedVariables[decl.prop] = decl.value; + }); +}; + +export const parseMediaQuery = (importedFile: Root, parsedCustomMedia: Record) => { + importedFile.walkAtRules('custom-media', (mediaRule) => { + const mediaName = getMediaQueryName(mediaRule); + + // eslint-disable-next-line no-param-reassign + parsedCustomMedia[mediaName] = mediaRule; + }); +}; + +export function addGlobalVariable(cssValue: string, rootSelector: Rule, parsedVariables: Record) { + const variableMatches = cssValue.match(/var\(\s*--([^)]+)\s*\)/g); + + if (variableMatches) { + variableMatches.forEach((match) => { + // var(--gap-24) => --gap-24 + const variableName = match.slice(4, -1).trim(); + + if (parsedVariables[variableName]) { + rootSelector.append(new Declaration({ prop: variableName, value: parsedVariables[variableName] })); + + // Рекурсивно проходимся по значениям css, там тоже могут использоваться переменные + addGlobalVariable(parsedVariables[variableName], rootSelector, parsedVariables); + } + }); + } +} + +export const insertParsedCss = (root: Root, parsedVariables: Record, parsedCustomMedia: Record): Rule => { + const rootRule = new Rule({ selector: ':root' }); + + root.walkDecls((decl) => { + addGlobalVariable(decl.value, rootRule, parsedVariables); + }); + + root.walkAtRules('media', (rule) => { + const mediaFullName = getMediaQueryName(rule); + + if (mediaFullName.startsWith('(--')) { + const mediaName = mediaFullName.slice(1, -1); + + if (parsedCustomMedia[mediaName]) { + root.append(parsedCustomMedia[mediaName]); + } + } + }); + + return rootRule; +}; diff --git a/yarn.lock b/yarn.lock index c313e086..bf71c618 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2741,15 +2741,6 @@ __metadata: languageName: node linkType: hard -"@csstools/postcss-global-data@npm:^2.0.1": - version: 2.0.1 - resolution: "@csstools/postcss-global-data@npm:2.0.1" - peerDependencies: - postcss: ^8.4 - checksum: 21e7057b7f527481c7374810c3b49a2d47414b39f4408c5d182bb6aab62d190da5b6173aa1accacb091994d1158cee2a651fbf2977957bbcf1fc654669886c1a - languageName: node - linkType: hard - "@csstools/postcss-gradients-interpolation-method@npm:^3.0.6": version: 3.0.6 resolution: "@csstools/postcss-gradients-interpolation-method@npm:3.0.6" @@ -6485,7 +6476,6 @@ __metadata: "@babel/preset-react": ^7.23.3 "@babel/preset-typescript": ^7.23.3 "@babel/runtime": ^7.23.8 - "@csstools/postcss-global-data": ^2.0.1 "@pmmmwh/react-refresh-webpack-plugin": 0.5.11 "@swc/core": ^1.7.35 "@swc/jest": ^0.2.36