Skip to content

Commit

Permalink
feat(postcss): add custom postcss plugin - postcss-global-data
Browse files Browse the repository at this point in the history
  • Loading branch information
Обмочевский Владислав Вячеславович authored and Обмочевский Владислав Вячеславович committed Nov 24, 2024
1 parent 54300a7 commit fb3b80d
Show file tree
Hide file tree
Showing 11 changed files with 285 additions and 9 deletions.
5 changes: 5 additions & 0 deletions .changeset/quick-starfishes-taste.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'arui-scripts': minor
---

Добавлен кастомный плагин postcss-global-data, для оптимизации времени обработки глобальных переменных
6 changes: 0 additions & 6 deletions packages/arui-scripts/src/configs/postcss.config.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import path from 'path';

import config from './app-configs';
import supportingBrowsers from './supporting-browsers';
/**
Expand Down Expand Up @@ -28,7 +26,6 @@ export const postcssPlugins = [
'postcss-mixins',
'postcss-for',
'postcss-each',
'@csstools/postcss-global-data',
'postcss-custom-media',
'postcss-color-mod-function',
!config.keepCssVars && 'postcss-custom-properties',
Expand All @@ -46,9 +43,6 @@ export const postcssPluginsOptions = {
'postcss-import': {
path: ['./src'],
},
'@csstools/postcss-global-data': {
files: [path.join(__dirname, 'mq.css'), config.componentsTheme].filter(Boolean) as string[],
},
'postcss-url': {
url: 'rebase',
},
Expand Down
12 changes: 10 additions & 2 deletions packages/arui-scripts/src/configs/webpack.client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ import { WebpackDeduplicationPlugin } from 'webpack-deduplication-plugin';
import { WebpackManifestPlugin } from 'webpack-manifest-plugin';

import { AruiRuntimePlugin, getInsertCssRuntimeMethod } from '../plugins/arui-runtime';
import { insertPlugin } from '../plugins/insert-plugin';
import { postCssGlobalData } from '../plugins/postcss-global-data/postcss-global-data';
import { htmlTemplate } from '../templates/html.template';

import { getImageMin } from './config-extras/minimizers';
Expand Down Expand Up @@ -285,7 +287,10 @@ export const createSingleClientWebpackConfig = (
loader: require.resolve('postcss-loader'),
options: {
postcssOptions: {
plugins: postcssConf,
// добавляем postCssGlobalData плагин перед postcss-custom-media
plugins: insertPlugin(postcssConf, 'postcss-custom-media', postCssGlobalData({
files: [path.join(__dirname, 'mq.css'), configs.componentsTheme].filter(Boolean) as string[]
}))
},
},
},
Expand Down Expand Up @@ -313,7 +318,10 @@ export const createSingleClientWebpackConfig = (
loader: require.resolve('postcss-loader'),
options: {
postcssOptions: {
plugins: postcssConf,
// добавляем postCssGlobalData плагин перед postcss-custom-media
plugins: insertPlugin(postcssConf, 'postcss-custom-media', postCssGlobalData({
files: [path.join(__dirname, 'mq.css'), configs.componentsTheme].filter(Boolean) as string[]
}))
},
},
},
Expand Down
8 changes: 7 additions & 1 deletion packages/arui-scripts/src/configs/webpack.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ import TsconfigPathsPlugin from 'tsconfig-paths-webpack-plugin';
import webpack, { Configuration } from 'webpack';
import nodeExternals from 'webpack-node-externals';

import { insertPlugin } from '../plugins/insert-plugin';
import { postCssGlobalData } from '../plugins/postcss-global-data/postcss-global-data';

import getEntry from './util/get-entry';
import { getWebpackCacheDependencies } from './util/get-webpack-cache-dependencies';
import configs from './app-configs';
Expand Down Expand Up @@ -155,7 +158,10 @@ export const createServerConfig = (mode: 'dev' | 'prod'): Configuration => ({
loader: require.resolve('postcss-loader'),
options: {
postcssOptions: {
plugins: postcssConf,
// добавляем postCssGlobalData плагин перед postcss-custom-media
plugins: insertPlugin(postcssConf, 'postcss-custom-media', postCssGlobalData({
files: [path.join(__dirname, 'mq.css'), configs.componentsTheme].filter(Boolean) as string[]
}))
},
},
},
Expand Down
19 changes: 19 additions & 0 deletions packages/arui-scripts/src/plugins/__tests__/insert-plugin.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { insertPlugin } from '../insert-plugin';

describe('insertPlugin', () => {
it('Должен корректно добавить плагин перед искомым плагином', () => {
const plugins = ['plugin1', 'plugin2', 'plugin3', 'plugin4'];

const newPlugins = insertPlugin(plugins, 'plugin4', 'plugin777');

expect(newPlugins).toEqual(['plugin1', 'plugin2', 'plugin3', 'plugin777', 'plugin4']);
});

it('Должен корректно добавить плагин перед искомым плагином, если искомый плагин в массиве', () => {
const plugins = ['plugin1', 'plugin2', ['plugin3', {}], 'plugin4'];

const newPlugins = insertPlugin(plugins, 'plugin3', 'plugin777');

expect(newPlugins).toEqual(['plugin1', 'plugin2', 'plugin777', ['plugin3', {}], 'plugin4']);
});
});
19 changes: 19 additions & 0 deletions packages/arui-scripts/src/plugins/insert-plugin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
export const insertPlugin = (plugins: string[] | unknown[], beforePluginName: string, plugin: string | unknown): string[] | unknown[] => {
let beforePluginIndex = -1;

plugins.forEach((pluginName, index) => {
if(Array.isArray(pluginName) && pluginName[0] === beforePluginName){
beforePluginIndex = index;
}

if(!Array.isArray(pluginName) && pluginName === beforePluginName){
beforePluginIndex = index;
}
});

if(beforePluginIndex === -1) {
return plugins;
}

return [...plugins.slice(0, beforePluginIndex), plugin, ...plugins.slice(beforePluginIndex)]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import type { AtRule, Plugin, PluginCreator, Rule } from 'postcss';

import { insertParsedCss, parseImport, parseMediaQuery, parseVariables } from './utils/utils';

type PluginOptions = {
files?: string[];
};

const postCssGlobalData: PluginCreator<PluginOptions> = (opts?: PluginOptions) => {
const options = {
files: [],
...opts,
};

const parsedVariables: Record<string, string> = {};
const parsedCustomMedia: Record<string, AtRule> = {};

let rulesSelectors = new Set<Rule>();

return {
postcssPlugin: '@alfalab/postcss-global-data',
prepare(): Plugin {
return {
postcssPlugin: '@alfalab/postcss-global-data',
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);
},
OnceExit(): void {
rulesSelectors.forEach((rule) => {
rule.remove();
});
rulesSelectors = new Set<Rule>();
},
};
},
};
};

postCssGlobalData.postcss = true;

export { postCssGlobalData };
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
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 parsedVariables = {
'--color-primary': 'var(--color-secondary)',
'--color-secondary': '#00ff00',
};

addGlobalVariable('var(--color-primary)', mockRootSelector, parsedVariables);

expect(mockRootSelector.nodes).toMatchObject([
{ prop: '--color-primary', value: 'var(--color-secondary)' },
{ 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([]);
});
});
Original file line number Diff line number Diff line change
@@ -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('');
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import type { Declaration,Root } from 'postcss';

import { parseVariables } from '../utils';

describe('parseVariables', () => {
it('Должен корректно заполнять объект переменными на основе импортируемого файла', () => {
const parsedVariables: Record<string, string> = {};

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)',
});
});
});
Original file line number Diff line number Diff line change
@@ -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-data',
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<string, string>) => {
importedFile.walkDecls((decl) => {
// eslint-disable-next-line no-param-reassign
parsedVariables[decl.prop] = decl.value;
});
};

export const parseMediaQuery = (importedFile: Root, parsedCustomMedia: Record<string, AtRule>) => {
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<string, string>) {
const variableMatches = cssValue.match(/var\(--([^)]+)\)/g);

if (variableMatches) {
variableMatches.forEach((match) => {
// var(--gap-24) => --gap-24
const variableName = match.slice(4, -1);

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<string, string>, parsedCustomMedia: Record<string, AtRule>): 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;
};

0 comments on commit fb3b80d

Please sign in to comment.