diff --git a/packages/config/src/lib/addPrefixToContent.test.ts b/packages/config/src/lib/addPrefixToContent.test.ts new file mode 100644 index 000000000..f4862fb12 --- /dev/null +++ b/packages/config/src/lib/addPrefixToContent.test.ts @@ -0,0 +1,175 @@ +import addPrefixToContent from './addPrefixToContent'; + +expect.extend({ + toEqualIgnoringWhitespace(received, expected) { + const normalize = (str: string) => str.replace(/\s+/g, ''); + const pass = normalize(received) === normalize(expected); + + if (pass) { + return { + message: () => `expected ${received} not to equal (ignoring whitespace) ${expected}`, + pass: true, + }; + } else { + return { + message: () => `expected ${received} to equal (ignoring whitespace) ${expected}`, + pass: false, + }; + } + }, +}); + +describe('shouldnt prefix nested selectors', () => { + test('should add prefix above content', () => { + const content = `.foo { + .bar { + color: blue; + } + }`; + const prefix = '.learningResources, .learning-resources'; + const output = `.learningResources, .learning-resources { + .foo { + .bar { + color: blue; + } + } + }`; + expect(addPrefixToContent(content, prefix)).toEqualIgnoringWhitespace(output); + }); + + test('should handle multiple top-level selectors in one string', () => { + const content = `.learningResources-learningResources{overflow-y:auto}.widget-learning-resources{column-width:300px}`; + const prefix = '.learningResources, .learning-resources'; + const output = `.learningResources, .learning-resources { .learningResources-learningResources {overflow-y:auto}}.learningResources, .learning-resources { .widget-learning-resources {column-width:300px}}`; + expect(addPrefixToContent(content, prefix)).toEqualIgnoringWhitespace(output); + }); + + test('should ignore nested selector containing prefix', () => { + const content = `.foo { + .bar { + .learningResources, .learning-resources { + color: red; + } + }}`; + const prefix = `.learningResources, .learning-resources`; + const output = `.learningResources, .learning-resources { + .foo { + .bar { + .learningResources, .learning-resources { + color: red; + } + } + } + }`; + expect(addPrefixToContent(content, prefix)).toEqualIgnoringWhitespace(output); + }); + + test('should handle deeply nested selectors with various properties and pseudo-classes', () => { + const content = ` + .foo { + .learningResources { + .bar { + color: blue; + .baz:hover { + background-color: red; + &::before { + content: 'before'; + display: block; + } + } + } + } + } + .another-element { + margin: 0; + padding: 0; + .nested-class { + border: 1px solid black; + &:first-child { + border-left: none; + } + .learning-resources { + .wrapper { + background-color: yellow; + .learningResources { + padding: 3px; + } + } + } + } + } + .learning-resources { + .learningResources { + .nested { + .dif { + .container { + .inner { + color: red; + .learningResources .learning-resources { + margin: 0 auto; + } + } + } + } + } + } + } + `; + const prefix = `.learningResources, .learning-resources`; + const output = ` + .learningResources, .learning-resources { + .foo { + .learningResources { + .bar { + color: blue; + .baz:hover { + background-color: red; + &::before { + content: 'before'; + display: block; + } + } + } + } + } + } + .learningResources, .learning-resources { + .another-element { + margin: 0; + padding: 0; + .nested-class { + border: 1px solid black; + &:first-child { + border-left: none; + } + .learning-resources { + .wrapper { + background-color: yellow; + .learningResources { + padding: 3px; + } + } + } + } + } + } + .learning-resources { + .learningResources { + .nested { + .dif { + .container { + .inner { + color: red; + .learningResources .learning-resources { + margin: 0 auto; + } + } + } + } + } + } + } + `; + expect(addPrefixToContent(content, prefix)).toEqualIgnoringWhitespace(output); + }); +}); diff --git a/packages/config/src/lib/addPrefixToContent.ts b/packages/config/src/lib/addPrefixToContent.ts new file mode 100644 index 000000000..dae6dc141 --- /dev/null +++ b/packages/config/src/lib/addPrefixToContent.ts @@ -0,0 +1,37 @@ +const addPrefixToContent = (content: string, sassPrefix: string): string => { + // Stack to store pairs of opening and closing curly brace indexes + const stack: Array<[number, number | undefined]> = []; + const result: string[] = []; + let lastIndex = 0; + const prefixes = sassPrefix.split(',').map((prefix) => prefix.trim()); + + for (let i = 0; i < content.length; i++) { + if (content[i] === '{') { + stack.push([i, undefined]); + } else if (content[i] === '}') { + const startPair = stack.pop(); + if (startPair) { + const [start] = startPair; + if (start !== undefined && stack.length === 0) { + // Top-level block + const blockContent = content.slice(start + 1, i); + const selectors = content.slice(lastIndex, start); + const selectorsArr = selectors.split(',').map((prefix) => prefix.trim()); + + const shouldPrependPrefix = prefixes.some((prefix) => selectorsArr.includes(prefix)); + if (!shouldPrependPrefix) { + // Add prefix to top-level selectors + result.push(`${sassPrefix} { ${selectors} { ${blockContent} } }`); + } else { + result.push(`${selectors} { ${blockContent} }`); + } + lastIndex = i + 1; // Update lastIndex to the next character after closing brace } + } + } + } + } + + return result.join(' '); +}; + +export default addPrefixToContent; diff --git a/packages/config/src/lib/createConfig.ts b/packages/config/src/lib/createConfig.ts index a5620bcfe..f2096a390 100644 --- a/packages/config/src/lib/createConfig.ts +++ b/packages/config/src/lib/createConfig.ts @@ -4,6 +4,7 @@ const MiniCssExtractPlugin = require('mini-css-extract-plugin'); const searchIgnoredStyles = require('@redhat-cloud-services/frontend-components-config-utilities/search-ignored-styles'); import { LogType, ProxyOptions, fecLogger, proxy } from '@redhat-cloud-services/frontend-components-config-utilities'; +import addPrefixToContent from './addPrefixToContent'; type Configuration = import('webpack').Configuration; type CacheOptions = import('webpack').FileCacheOptions | import('webpack').MemoryCacheOptions; type ProxyConfigArrayItem = import('webpack-dev-server').ProxyConfigArrayItem; @@ -196,8 +197,10 @@ export const createConfig = ({ * Add app class context for local style files. * Context class is equal to app name and that class ass added to root element via the chrome-render-loader. */ + if (relativePath.match(/^src/)) { - return `${sassPrefix || `.${appName}`}{\n${content}\n}`; + const transformedContent = addPrefixToContent(content, sassPrefix ?? `.${appName}`); + return transformedContent; } return content;