UNPKG

@lwc/style-compiler

Version:

Transform style sheet to be consumed by the LWC engine

899 lines (880 loc) 38.4 kB
/** * Copyright (c) 2026 Salesforce, Inc. */ 'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); var postcss = require('postcss'); var shared = require('@lwc/shared'); var postCssSelectorParser = require('postcss-selector-parser'); var valueParser = require('postcss-value-parser'); const PLUGIN_NAME = '@lwc/style-compiler'; const IMPORT_TYPE = 'import'; function importMessage(id) { return { plugin: PLUGIN_NAME, type: IMPORT_TYPE, id, }; } function isImportMessage(message) { return message.type === IMPORT_TYPE && message.id; } /* * Copyright (c) 2018, salesforce.com, inc. * All rights reserved. * SPDX-License-Identifier: MIT * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT */ const HOST_ATTRIBUTE = '__hostAttribute__'; const SHADOW_ATTRIBUTE = '__shadowAttribute__'; /* * Copyright (c) 2018, salesforce.com, inc. * All rights reserved. * SPDX-License-Identifier: MIT * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT */ const DIR_ATTRIBUTE_NATIVE_LTR = `__dirAttributeNativeLtr__`; const DIR_ATTRIBUTE_NATIVE_RTL = `__dirAttributeNativeRtl__`; const DIR_ATTRIBUTE_SYNTHETIC_LTR = `__dirAttributeSyntheticLtr__`; const DIR_ATTRIBUTE_SYNTHETIC_RTL = `__dirAttributeSyntheticRtl__`; /* * Copyright (c) 2018, salesforce.com, inc. * All rights reserved. * SPDX-License-Identifier: MIT * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT */ var TokenType; (function (TokenType) { TokenType["text"] = "text"; TokenType["expression"] = "expression"; TokenType["identifier"] = "identifier"; TokenType["divider"] = "divider"; })(TokenType || (TokenType = {})); // "1400 binary expressions are enough to reach Node.js maximum call stack size" // https://github.com/salesforce/lwc/issues/1726 // The vast majority of stylesheet functions are much less than this, so we can set the limit lower // to play it safe. const BINARY_EXPRESSION_LIMIT = 100; // Javascript identifiers used for the generation of the style module const HOST_SELECTOR_IDENTIFIER = 'hostSelector'; const SHADOW_SELECTOR_IDENTIFIER = 'shadowSelector'; const SUFFIX_TOKEN_IDENTIFIER = 'suffixToken'; const USE_ACTUAL_HOST_SELECTOR = 'useActualHostSelector'; const USE_NATIVE_DIR_PSEUDOCLASS = 'useNativeDirPseudoclass'; const TOKEN = 'token'; const STYLESHEET_IDENTIFIER = 'stylesheet'; function serialize(result, config) { const { messages } = result; const importedStylesheets = messages.filter(isImportMessage).map((message) => message.id); const disableSyntheticShadow = Boolean(config.disableSyntheticShadowSupport); const scoped = Boolean(config.scoped); let buffer = ''; for (let i = 0; i < importedStylesheets.length; i++) { buffer += `import ${STYLESHEET_IDENTIFIER + i} from "${importedStylesheets[i]}";\n`; } if (importedStylesheets.length) { buffer += '\n'; } const stylesheetList = importedStylesheets.map((_str, i) => `${STYLESHEET_IDENTIFIER + i}`); const serializedStyle = serializeCss(result).trim(); if (serializedStyle) { // inline function if (disableSyntheticShadow && !scoped) { // If synthetic shadow DOM support is disabled and this is not a scoped stylesheet, then the // function signature will always be: // stylesheet(token = undefined, useActualHostSelector = true, useNativeDirPseudoclass = true) // This means that we can just have a function that takes no arguments and returns a string, // reducing the bundle size when minified. buffer += `function ${STYLESHEET_IDENTIFIER}() {\n`; buffer += ` var ${TOKEN};\n`; // undefined buffer += ` var ${USE_ACTUAL_HOST_SELECTOR} = true;\n`; buffer += ` var ${USE_NATIVE_DIR_PSEUDOCLASS} = true;\n`; } else { buffer += `function ${STYLESHEET_IDENTIFIER}(${TOKEN}, ${USE_ACTUAL_HOST_SELECTOR}, ${USE_NATIVE_DIR_PSEUDOCLASS}) {\n`; } // For scoped stylesheets, we use classes, but for synthetic shadow DOM, we use attributes if (scoped) { buffer += ` var ${SHADOW_SELECTOR_IDENTIFIER} = ${TOKEN} ? ("." + ${TOKEN}) : "";\n`; buffer += ` var ${HOST_SELECTOR_IDENTIFIER} = ${TOKEN} ? ("." + ${TOKEN} + "-host") : "";\n`; } else { buffer += ` var ${SHADOW_SELECTOR_IDENTIFIER} = ${TOKEN} ? ("[" + ${TOKEN} + "]") : "";\n`; buffer += ` var ${HOST_SELECTOR_IDENTIFIER} = ${TOKEN} ? ("[" + ${TOKEN} + "-host]") : "";\n`; } // Used for keyframes buffer += ` var ${SUFFIX_TOKEN_IDENTIFIER} = ${TOKEN} ? ("-" + ${TOKEN}) : "";\n`; buffer += ` return ${serializedStyle};\n`; buffer += ` /*${shared.LWC_VERSION_COMMENT}*/\n`; buffer += `}\n`; if (scoped) { // Mark the stylesheet as scoped so that we can distinguish it later at runtime buffer += `${STYLESHEET_IDENTIFIER}.${shared.KEY__SCOPED_CSS} = true;\n`; } if (disableSyntheticShadow) { // Mark the stylesheet as $nativeOnly$ so it can be ignored in synthetic shadow mode buffer += `${STYLESHEET_IDENTIFIER}.${shared.KEY__NATIVE_ONLY_CSS} = true;\n`; } // add import at the end stylesheetList.push(STYLESHEET_IDENTIFIER); } // exports if (stylesheetList.length) { buffer += `export default [${stylesheetList.join(', ')}];`; } else { buffer += `export default undefined;`; } return buffer; } function reduceTokens(tokens) { return [{ type: TokenType.text, value: '' }, ...tokens, { type: TokenType.text, value: '' }] .reduce((acc, token) => { const prev = acc[acc.length - 1]; if (token.type === TokenType.text && prev && prev.type === TokenType.text) { // clone the previous token to avoid mutating it in-place acc[acc.length - 1] = { type: prev.type, value: prev.value + token.value, }; return acc; } else { return [...acc, token]; } }, []) .filter((t) => t.value !== ''); } function normalizeString(str) { return str.replace(/(\r\n\t|\n|\r\t)/gm, '').trim(); } function generateExpressionFromTokens(tokens) { const serializedTokens = reduceTokens(tokens).map(({ type, value }) => { switch (type) { // Note that we don't expect to get a TokenType.divider here. It should be converted into an // expression elsewhere. case TokenType.text: return JSON.stringify(value); // Expressions may be concatenated with " + ", in which case we must remove ambiguity case TokenType.expression: return `(${value})`; default: return value; } }); if (serializedTokens.length === 0) { return ''; } else if (serializedTokens.length === 1) { return serializedTokens[0]; } else if (serializedTokens.length < BINARY_EXPRESSION_LIMIT) { return serializedTokens.join(' + '); } else { // #1726 Using Array.prototype.join() instead of a standard "+" operator to concatenate the // string to avoid running into a maximum call stack error when the stylesheet is parsed // again by the bundler. return `[${serializedTokens.join(', ')}].join('')`; } } function areTokensEqual(left, right) { return left.type === right.type && left.value === right.value; } function calculateNumDuplicatedTokens(left, right) { // Walk backwards until we find a token that is different between left and right let i = 0; for (; i < left.length && i < right.length; i++) { const currentLeft = left[left.length - 1 - i]; const currentRight = right[right.length - 1 - i]; if (!areTokensEqual(currentLeft, currentRight)) { break; } } return i; } // For `:host` selectors, the token lists for native vs synthetic will be identical at the end of // each list. So as an optimization, we can de-dup these tokens. // See: https://github.com/salesforce/lwc/issues/3224#issuecomment-1353520052 function deduplicateHostTokens(nativeHostTokens, syntheticHostTokens) { const numDuplicatedTokens = calculateNumDuplicatedTokens(nativeHostTokens, syntheticHostTokens); const numUniqueNativeTokens = nativeHostTokens.length - numDuplicatedTokens; const numUniqueSyntheticTokens = syntheticHostTokens.length - numDuplicatedTokens; const uniqueNativeTokens = nativeHostTokens.slice(0, numUniqueNativeTokens); const uniqueSyntheticTokens = syntheticHostTokens.slice(0, numUniqueSyntheticTokens); const nativeExpression = generateExpressionFromTokens(uniqueNativeTokens); const syntheticExpression = generateExpressionFromTokens(uniqueSyntheticTokens); // Generate a conditional ternary to switch between native vs synthetic for the unique tokens const conditionalToken = { type: TokenType.expression, value: `(${USE_ACTUAL_HOST_SELECTOR} ? ${nativeExpression} : ${syntheticExpression})`, }; return [ conditionalToken, // The remaining tokens are the same between native and synthetic ...syntheticHostTokens.slice(numUniqueSyntheticTokens), ]; } function serializeCss(result) { const tokens = []; let currentRuleTokens = []; let nativeHostTokens; // Walk though all nodes in the CSS... postcss.stringify(result.root, (part, node, nodePosition) => { // When consuming the beginning of a rule, first we tokenize the selector if (node && node.type === 'rule' && nodePosition === 'start') { currentRuleTokens.push(...tokenizeCss(normalizeString(part))); // When consuming the end of a rule we normalize it and produce a new one } else if (node && node.type === 'rule' && nodePosition === 'end') { currentRuleTokens.push({ type: TokenType.text, value: part }); // If we are in synthetic shadow or scoped light DOM, we don't want to have native :host selectors // Note that postcss-lwc-plugin should ensure that _isNativeHost appears before _isSyntheticHost if (node._isNativeHost) { // Save native tokens so in the next rule we can apply a conditional ternary nativeHostTokens = [...currentRuleTokens]; } else if (node._isSyntheticHost) { /* istanbul ignore if */ if (!nativeHostTokens) { throw new Error('Unexpected host rules ordering'); } const hostTokens = deduplicateHostTokens(nativeHostTokens, currentRuleTokens); tokens.push(...hostTokens); nativeHostTokens = undefined; } else { /* istanbul ignore if */ if (nativeHostTokens) { throw new Error('Unexpected host rules ordering'); } tokens.push(...currentRuleTokens); } // Reset rule currentRuleTokens = []; // When inside a declaration, tokenize it and push it to the current token list } else if (node && node.type === 'decl') { currentRuleTokens.push(...tokenizeCss(part)); } else if (node && node.type === 'atrule') { // Certain atrules have declaration associated with for example @font-face. We need to add the rules tokens // when it's the case. if (currentRuleTokens.length) { tokens.push(...currentRuleTokens); currentRuleTokens = []; } tokens.push(...tokenizeCss(normalizeString(part))); } else { // When inside anything else but a comment just push it if (!node || node.type !== 'comment') { currentRuleTokens.push({ type: TokenType.text, value: normalizeString(part) }); } } }); return generateExpressionFromTokens(tokens); } // Given any CSS string, replace the scope tokens from the CSS with code to properly // replace it in the stylesheet function. function tokenizeCss(data) { data = data.replace(/( {2,})/gm, ' '); // remove when there are more than two spaces const tokens = []; const attributes = [ SHADOW_ATTRIBUTE, HOST_ATTRIBUTE, DIR_ATTRIBUTE_NATIVE_LTR, DIR_ATTRIBUTE_NATIVE_RTL, DIR_ATTRIBUTE_SYNTHETIC_LTR, DIR_ATTRIBUTE_SYNTHETIC_RTL, ]; const regex = new RegExp(`[[-](${attributes.join('|')})]?`, 'g'); let lastIndex = 0; for (const match of data.matchAll(regex)) { const index = match.index; const [matchString, substring] = match; if (index > lastIndex) { tokens.push({ type: TokenType.text, value: data.substring(lastIndex, index) }); } const identifier = substring === SHADOW_ATTRIBUTE ? SHADOW_SELECTOR_IDENTIFIER : HOST_SELECTOR_IDENTIFIER; if (matchString.startsWith('[')) { if (substring === SHADOW_ATTRIBUTE || substring === HOST_ATTRIBUTE) { // attribute in a selector, e.g. `[__shadowAttribute__]` or `[__hostAttribute__]` tokens.push({ type: TokenType.identifier, value: identifier, }); } else { // :dir pseudoclass placeholder, e.g. `[__dirAttributeNativeLtr__]` or `[__dirAttributeSyntheticRtl__]` const native = substring === DIR_ATTRIBUTE_NATIVE_LTR || substring === DIR_ATTRIBUTE_NATIVE_RTL; const dirValue = substring === DIR_ATTRIBUTE_NATIVE_LTR || substring === DIR_ATTRIBUTE_SYNTHETIC_LTR ? 'ltr' : 'rtl'; tokens.push({ type: TokenType.expression, // use the native :dir() pseudoclass for native shadow, the [dir] attribute otherwise value: native ? `${USE_NATIVE_DIR_PSEUDOCLASS} ? ':dir(${dirValue})' : ''` : `${USE_NATIVE_DIR_PSEUDOCLASS} ? '' : '[dir="${dirValue}"]'`, }); } } else { // suffix for an at-rule, e.g. `@keyframes spin-__shadowAttribute__` tokens.push({ type: TokenType.identifier, // Suffix the keyframe (i.e. "-" plus the token) value: SUFFIX_TOKEN_IDENTIFIER, }); } lastIndex = index + matchString.length; } if (lastIndex < data.length) { tokens.push({ type: TokenType.text, value: data.substring(lastIndex, data.length) }); } return tokens; } function validateIdSelectors (root, ctx) { root.walkIds((node) => { ctx.withErrorRecovery(() => { const message = `Invalid usage of id selector '#${node.value}'. Try using a class selector or some other selector.`; throw root.error(message, { index: node.sourceIndex, word: node.value, }); }); }); } /* * Copyright (c) 2018, salesforce.com, inc. * All rights reserved. * SPDX-License-Identifier: MIT * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT */ function process$1(root, result, isScoped, ctx) { root.walkAtRules('import', (node) => { ctx.withErrorRecovery(() => { if (isScoped) { throw node.error(`Invalid import statement, imports are not allowed in *.scoped.css files.`); } // Ensure @import are at the top of the file let prev = node.prev(); while (prev) { if (prev.type === 'comment' || (prev.type === 'atrule' && prev.name === 'import')) { prev = prev.prev(); } else { throw prev.error('@import must precede all other statements'); } } const { nodes: params } = valueParser(node.params); // Ensure import match the following syntax: // @import "foo"; // @import "./foo.css"; if (!params.length || params[0].type !== 'string' || !params[0].value) { throw node.error(`Invalid import statement, unable to find imported module.`); } if (params.length > 1) { throw node.error(`Invalid import statement, import statement only support a single parameter.`); } // Add the imported to results messages const message = importMessage(params[0].value); result.messages.push(message); // Remove the import from the generated css node.remove(); }); }); } /* * Copyright (c) 2018, salesforce.com, inc. * All rights reserved. * SPDX-License-Identifier: MIT * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT */ function isDirPseudoClass(node) { return postCssSelectorParser.isPseudoClass(node) && node.value === ':dir'; } function findNode(container, predicate) { return container && container.nodes && container.nodes.find(predicate); } function replaceNodeWith(oldNode, ...newNodes) { if (newNodes.length) { const { parent } = oldNode; if (!parent) { throw new Error(`Impossible to replace root node.`); } newNodes.forEach((node) => { parent.insertBefore(oldNode, node); }); oldNode.remove(); } } function trimNodeWhitespaces(node) { if (node && node.spaces) { node.spaces.before = ''; node.spaces.after = ''; } } const DEPRECATED_SELECTORS = new Set(['/deep/', '::shadow', '>>>']); const UNSUPPORTED_SELECTORS = new Set([':root', ':host-context']); const TEMPLATE_DIRECTIVES = [/^key$/, /^lwc:*/, /^if:*/, /^for:*/, /^iterator:*/]; function validateSelectors(root, native, ctx) { root.walk((node) => { ctx.withErrorRecovery(() => { const { value, sourceIndex } = node; if (value) { // Ensure the selector doesn't use a deprecated CSS selector. if (DEPRECATED_SELECTORS.has(value)) { throw root.error(`Invalid usage of deprecated selector "${value}".`, { index: sourceIndex, word: value, }); } // Ensure the selector doesn't use an unsupported selector. if (!native && UNSUPPORTED_SELECTORS.has(value)) { throw root.error(`Invalid usage of unsupported selector "${value}". This selector is only supported in non-scoped CSS where the \`disableSyntheticShadowSupport\` flag is set to true.`, { index: sourceIndex, word: value, }); } } }); }); } function validateAttribute(root, ctx) { root.walkAttributes((node) => { ctx.withErrorRecovery(() => { const { attribute: attributeName, sourceIndex } = node; const isTemplateDirective = TEMPLATE_DIRECTIVES.some((directive) => { return directive.test(attributeName); }); if (isTemplateDirective) { const message = [ `Invalid usage of attribute selector "${attributeName}". `, `"${attributeName}" is a template directive and therefore not supported in css rules.`, ]; throw root.error(message.join(''), { index: sourceIndex, word: attributeName, }); } }); }); } function validate(root, native, ctx) { validateSelectors(root, native, ctx); validateAttribute(root, ctx); } /* * Copyright (c) 2018, salesforce.com, inc. * All rights reserved. * SPDX-License-Identifier: MIT * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT */ function isHostPseudoClass(node) { return postCssSelectorParser.isPseudoClass(node) && node.value === ':host'; } /** * Add scoping attributes to all the matching selectors: * - h1 -> h1[x-foo_tmpl] * - p a -> p[x-foo_tmpl] a[x-foo_tmpl] * @param selector */ function scopeSelector(selector) { const compoundSelectors = [[]]; // Split the selector per compound selector. Compound selectors are interleaved with combinator nodes. // https://drafts.csswg.org/selectors-4/#typedef-complex-selector selector.each((node) => { if (postCssSelectorParser.isCombinator(node)) { compoundSelectors.push([]); } else { const current = compoundSelectors[compoundSelectors.length - 1]; current.push(node); } }); for (const compoundSelector of compoundSelectors) { // Compound selectors with only a single :dir pseudo class should be scoped, the dir pseudo // class transform will take care of transforming it properly. const containsSingleDirSelector = compoundSelector.length === 1 && isDirPseudoClass(compoundSelector[0]); // Compound selectors containing :host have a special treatment and should not be scoped // like the rest of the complex selectors. const containsHost = compoundSelector.some(isHostPseudoClass); if (!containsSingleDirSelector && !containsHost) { let nodeToScope; // In each compound selector we need to locate the last selector to scope. for (const node of compoundSelector) { if (!postCssSelectorParser.isPseudoElement(node)) { nodeToScope = node; } } const shadowAttribute = postCssSelectorParser.attribute({ attribute: SHADOW_ATTRIBUTE, value: undefined, raws: {}, }); if (nodeToScope) { // Add the scoping attribute right after the node scope selector.insertAfter(nodeToScope, shadowAttribute); } else { // Add the scoping token in the first position of the compound selector as a fallback // when there is no node to scope. For example: ::after {} const [firstSelector] = compoundSelector; selector.insertBefore(firstSelector, shadowAttribute); // Move any whitespace before the selector (e.g. " ::after") to before the shadow attribute, // so that the resulting selector is correct (e.g. " [attr]::after", not "[attr] ::after") if (firstSelector && firstSelector.spaces.before) { shadowAttribute.spaces.before = firstSelector.spaces.before; const clonedFirstSelector = firstSelector.clone({}); clonedFirstSelector.spaces.before = ''; firstSelector.replaceWith(clonedFirstSelector); } } } } } /** * Mark the :host selector with a placeholder. If the selector has a list of * contextual selector it will generate a rule for each of them. * - `:host -> [x-foo_tmpl-host]` * - `:host(.foo, .bar) -> [x-foo_tmpl-host].foo, [x-foo_tmpl-host].bar` * @param selector */ function transformHost(selector) { // Locate the first :host pseudo-class const hostNode = findNode(selector, isHostPseudoClass); if (hostNode) { // Store the original location of the :host in the selector const hostIndex = selector.index(hostNode); // Swap the :host pseudo-class with the host scoping token const hostAttribute = postCssSelectorParser.attribute({ attribute: HOST_ATTRIBUTE, value: undefined, raws: {}, }); hostNode.replaceWith(hostAttribute); // Generate a unique contextualized version of the selector for each selector pass as argument // to the :host const contextualSelectors = hostNode.nodes.map((contextSelectors) => { const clonedSelector = selector.clone({}); const clonedHostNode = clonedSelector.at(hostIndex); // Add to the compound selector previously containing the :host pseudo class // the contextual selectors. contextSelectors.each((node) => { trimNodeWhitespaces(node); clonedSelector.insertAfter(clonedHostNode, node); }); return clonedSelector; }); // Replace the current selector with the different variants replaceNodeWith(selector, ...contextualSelectors); } } function transformSelector(root, transformConfig, ctx) { validate(root, transformConfig.disableSyntheticShadowSupport && !transformConfig.scoped, ctx); root.each(scopeSelector); if (transformConfig.transformHost) { root.each(transformHost); } } /* * Copyright (c) 2018, salesforce.com, inc. * All rights reserved. * SPDX-License-Identifier: MIT * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT */ function isValidDirValue(value) { return value === 'ltr' || value === 'rtl'; } function transformDirPseudoClass (root, ctx) { root.nodes.forEach((selector) => { selector.nodes.forEach((node) => { ctx.withErrorRecovery(() => { if (!isDirPseudoClass(node)) { return; } const value = node.nodes.toString().trim(); if (!isValidDirValue(value)) { throw root.error(`:dir() pseudo class expects "ltr" or "rtl" for value, but received "${value}".`, { index: node.sourceIndex, word: node.value, }); } // Set placeholders for `:dir()` so we can keep it for native shadow and // replace it with a polyfill for synthetic shadow. // // Native: `:dir(ltr)` // Synthetic: `[dir="ltr"]` // // The placeholders look like this: `[__dirAttributeNativeLtr__]` // The attribute has no value because it's simpler during serialization, and there // are only two valid values: "ltr" and "rtl". // // Now consider a more complex selector: `.foo:dir(ltr):not(.bar)`. // For native shadow, we need to leave it as-is. Whereas for synthetic shadow, we need // to convert it to: `[dir="ltr"] .foo:not(.bar)`. // I.e. we need to use a descendant selector (' ' combinator) relying on a `dir` // attribute added to the host element. So we need two placeholders: // `<synthetic_placeholder> .foo<native_placeholder>:not(.bar)` const nativeAttribute = postCssSelectorParser.attribute({ attribute: value === 'ltr' ? DIR_ATTRIBUTE_NATIVE_LTR : DIR_ATTRIBUTE_NATIVE_RTL, value: undefined, raws: {}, }); const syntheticAttribute = postCssSelectorParser.attribute({ attribute: value === 'ltr' ? DIR_ATTRIBUTE_SYNTHETIC_LTR : DIR_ATTRIBUTE_SYNTHETIC_RTL, value: undefined, raws: {}, }); node.replaceWith(nativeAttribute); // If the selector is not empty and if the first node in the selector is not already a // " " combinator, we need to use the descendant selector format const shouldAddDescendantCombinator = selector.first && !postCssSelectorParser.isCombinator(selector.first) && selector.first.value !== ' '; if (shouldAddDescendantCombinator) { selector.insertBefore(selector.first, postCssSelectorParser.combinator({ value: ' ', })); // Add the [dir] attribute in front of the " " combinator, i.e. as an ancestor selector.insertBefore(selector.first, syntheticAttribute); } else { // Otherwise there's no need for the descendant selector, so we can skip adding the // space combinator and just put the synthetic placeholder next to the native one selector.insertBefore(nativeAttribute, syntheticAttribute); } }); }); }); } /* * Copyright (c) 2018, salesforce.com, inc. * All rights reserved. * SPDX-License-Identifier: MIT * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT */ // Subset of prefixes for animation-related names that we expect people might be using. // The most important is -webkit, which is actually part of the spec now. All -webkit prefixes // are listed here: https://developer.mozilla.org/en-US/docs/Web/CSS/Webkit_Extensions // -moz is also still supported as of Firefox 95. // We could probably get away with just doing -webkit and -moz (since -ms never seems // to have existed for keyframes/animations, and Opera has used Blink since 2013), but // covering all the popular ones will at least make the compiled code more consistent // for developers who are using all the variants. // List based on a subset from https://github.com/wooorm/vendors/blob/2f489ad/index.js const VENDOR_PREFIXES = ['moz', 'ms', 'o', 'webkit']; // create a list like ['animation', '-webkit-animation', ...] function getAllNames(name) { return new Set([name, ...VENDOR_PREFIXES.map((prefix) => `-${prefix}-${name}`)]); } const ANIMATION = getAllNames('animation'); const ANIMATION_NAME = getAllNames('animation-name'); function process(root, ctx) { const knownNames = new Set(); root.walkAtRules((atRule) => { ctx.withErrorRecovery(() => { // Note that @-webkit-keyframes, @-moz-keyframes, etc. are not actually a thing supported // in any browser, even though you'll see it on some StackOverflow answers. if (atRule.name === 'keyframes') { const { params } = atRule; knownNames.add(params); atRule.params = `${params}-${SHADOW_ATTRIBUTE}`; } }); }); root.walkRules((rule) => { rule.walkDecls((decl) => { ctx.withErrorRecovery(() => { if (ANIMATION.has(decl.prop)) { // Use a simple heuristic of breaking up the tokens by whitespace. We could use // a dedicated animation prop parser (e.g. // https://github.com/hookhookun/parse-animation-shorthand) but it's // probably overkill. const tokens = decl.value .trim() .split(/\s+/g) .map((token) => knownNames.has(token) ? `${token}-${SHADOW_ATTRIBUTE}` : token); decl.value = tokens.join(' '); } else if (ANIMATION_NAME.has(decl.prop)) { if (knownNames.has(decl.value)) { decl.value = `${decl.value}-${SHADOW_ATTRIBUTE}`; } } }); }); }); } /* * Copyright (c) 2018, salesforce.com, inc. * All rights reserved. * SPDX-License-Identifier: MIT * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT */ function shouldTransformSelector(rule) { // @keyframe at-rules are special, rules inside are not standard selectors and should not be // scoped like any other rules. return rule.parent?.type !== 'atrule' || rule.parent.name !== 'keyframes'; } function selectorProcessorFactory(transformConfig, ctx) { return postCssSelectorParser((root) => { validateIdSelectors(root, ctx); transformSelector(root, transformConfig, ctx); transformDirPseudoClass(root, ctx); }); } function postCssLwcPlugin(options) { const { ctx } = options; // We need 2 types of selectors processors, since transforming the :host selector make the selector // unusable when used in the context of the native shadow and vice-versa. // This distinction also applies to light DOM in scoped (synthetic-like) vs unscoped (native-like) mode. const nativeShadowSelectorProcessor = selectorProcessorFactory({ transformHost: false, disableSyntheticShadowSupport: options.disableSyntheticShadowSupport, scoped: options.scoped, }, ctx); const syntheticShadowSelectorProcessor = selectorProcessorFactory({ transformHost: true, disableSyntheticShadowSupport: options.disableSyntheticShadowSupport, scoped: options.scoped, }, ctx); return (root, result) => { process$1(root, result, options.scoped, ctx); process(root, ctx); // Wrap rule processing with error recovery root.walkRules((rule) => { ctx.withErrorRecovery(() => { if (!shouldTransformSelector(rule)) { return; } // Let transform the selector with the 2 processors. const syntheticSelector = syntheticShadowSelectorProcessor.processSync(rule); const nativeSelector = nativeShadowSelectorProcessor.processSync(rule); rule.selector = syntheticSelector; // If the resulting selector are different it means that the selector use the :host selector. In // this case we need to duplicate the CSS rule and assign the other selector. if (syntheticSelector !== nativeSelector) { // The cloned selector is inserted before the currently processed selector to avoid processing // again the cloned selector. const currentRule = rule; const clonedRule = rule.cloneBefore(); clonedRule.selector = nativeSelector; // Safe a reference to each other clonedRule._isNativeHost = true; currentRule._isSyntheticHost = true; } }); }); }; } /* * Copyright (c) 2025, Salesforce, Inc. * All rights reserved. * SPDX-License-Identifier: MIT * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT */ class StyleCompilerCtx { constructor(errorRecoveryMode, filename) { this.errors = []; this.seenErrorKeys = new Set(); this.errorRecoveryMode = errorRecoveryMode; this.filename = filename; } /** * This method recovers from CSS syntax errors that are encountered when fn is invoked. * All other errors are considered compiler errors and can not be recovered from. * @param fn method to be invoked. */ withErrorRecovery(fn) { if (!this.errorRecoveryMode) { return fn(); } try { return fn(); } catch (error) { if (error instanceof postcss.CssSyntaxError) { if (this.seenErrorKeys.has(error.message)) { return; } this.seenErrorKeys.add(error.message); this.errors.push(error); } else { // Non-CSS errors (compiler errors) should still throw throw error; } } } hasErrors() { return this.errors.length > 0; } } /* * Copyright (c) 2024, Salesforce, Inc. * All rights reserved. * SPDX-License-Identifier: MIT * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT */ /** * Transforms CSS for use with LWC components. * @param src Contents of the CSS source file * @param id Filename of the CSS source file * @param config Transformation options * @returns Transformed CSS * @example * import { transform } from '@lwc/style-compiler'; * const source = ` * :host { * opacity: 0.4; * } * span { * text-transform: uppercase; * }`; * const { code } = transform(source, 'example.css'); */ function transform(src, id, config = {}) { if (src === '') { return { code: 'export default undefined' }; } const scoped = !!config.scoped; shared.getAPIVersionFromNumber(config.apiVersion); const disableSyntheticShadowSupport = !!config.disableSyntheticShadowSupport; const errorRecoveryMode = !!config.experimentalErrorRecoveryMode; // Create error recovery context const ctx = new StyleCompilerCtx(errorRecoveryMode, id); const plugins = [ postCssLwcPlugin({ scoped, disableSyntheticShadowSupport, ctx, }), ]; // Wrap PostCSS processing with error recovery for parsing errors let result; try { result = postcss(plugins).process(src, { from: id }).sync(); } catch (error) { if (errorRecoveryMode && error instanceof postcss.CssSyntaxError) { ctx.errors.push(error); // eslint-disable-next-line preserve-caught-error throw AggregateError(ctx.errors); } else { throw error; } } if (errorRecoveryMode && ctx.hasErrors()) { throw AggregateError(ctx.errors); } return { code: serialize(result, config) }; } exports.transform = transform; /** version: 9.1.1 */ //# sourceMappingURL=index.cjs.map