@lwc/style-compiler 8.24.0 → 8.26.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/css-import/transform.d.ts +2 -1
- package/dist/dir-pseudo-class/transform.d.ts +2 -1
- package/dist/index.cjs.js +259 -172
- package/dist/index.js +260 -173
- package/dist/no-id-selectors/validate.d.ts +2 -1
- package/dist/postcss-lwc-plugin.d.ts +2 -0
- package/dist/scope-at-rules/transform.d.ts +2 -1
- package/dist/selector-scoping/transform.d.ts +2 -1
- package/dist/selector-scoping/validate.d.ts +2 -1
- package/dist/transform.d.ts +3 -0
- package/dist/utils/error-recovery.d.ts +16 -0
- package/package.json +2 -2
|
@@ -1,3 +1,4 @@
|
|
|
1
1
|
import type { Root, Result } from 'postcss';
|
|
2
|
-
|
|
2
|
+
import type { StyleCompilerCtx } from '../utils/error-recovery';
|
|
3
|
+
export default function process(root: Root, result: Result, isScoped: boolean, ctx: StyleCompilerCtx): void;
|
|
3
4
|
//# sourceMappingURL=transform.d.ts.map
|
|
@@ -1,3 +1,4 @@
|
|
|
1
1
|
import type { Root } from 'postcss-selector-parser';
|
|
2
|
-
|
|
2
|
+
import type { StyleCompilerCtx } from '../utils/error-recovery';
|
|
3
|
+
export default function (root: Root, ctx: StyleCompilerCtx): void;
|
|
3
4
|
//# sourceMappingURL=transform.d.ts.map
|
package/dist/index.cjs.js
CHANGED
|
@@ -344,12 +344,14 @@ function tokenizeCss(data) {
|
|
|
344
344
|
return tokens;
|
|
345
345
|
}
|
|
346
346
|
|
|
347
|
-
function validateIdSelectors (root) {
|
|
347
|
+
function validateIdSelectors (root, ctx) {
|
|
348
348
|
root.walkIds((node) => {
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
349
|
+
ctx.withErrorRecovery(() => {
|
|
350
|
+
const message = `Invalid usage of id selector '#${node.value}'. Try using a class selector or some other selector.`;
|
|
351
|
+
throw root.error(message, {
|
|
352
|
+
index: node.sourceIndex,
|
|
353
|
+
word: node.value,
|
|
354
|
+
});
|
|
353
355
|
});
|
|
354
356
|
});
|
|
355
357
|
}
|
|
@@ -360,36 +362,38 @@ function validateIdSelectors (root) {
|
|
|
360
362
|
* SPDX-License-Identifier: MIT
|
|
361
363
|
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT
|
|
362
364
|
*/
|
|
363
|
-
function process$1(root, result, isScoped) {
|
|
365
|
+
function process$1(root, result, isScoped, ctx) {
|
|
364
366
|
root.walkAtRules('import', (node) => {
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
// Ensure @import are at the top of the file
|
|
369
|
-
let prev = node.prev();
|
|
370
|
-
while (prev) {
|
|
371
|
-
if (prev.type === 'comment' || (prev.type === 'atrule' && prev.name === 'import')) {
|
|
372
|
-
prev = prev.prev();
|
|
367
|
+
ctx.withErrorRecovery(() => {
|
|
368
|
+
if (isScoped) {
|
|
369
|
+
throw node.error(`Invalid import statement, imports are not allowed in *.scoped.css files.`);
|
|
373
370
|
}
|
|
374
|
-
|
|
375
|
-
|
|
371
|
+
// Ensure @import are at the top of the file
|
|
372
|
+
let prev = node.prev();
|
|
373
|
+
while (prev) {
|
|
374
|
+
if (prev.type === 'comment' || (prev.type === 'atrule' && prev.name === 'import')) {
|
|
375
|
+
prev = prev.prev();
|
|
376
|
+
}
|
|
377
|
+
else {
|
|
378
|
+
throw prev.error('@import must precede all other statements');
|
|
379
|
+
}
|
|
376
380
|
}
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
381
|
+
const { nodes: params } = valueParser(node.params);
|
|
382
|
+
// Ensure import match the following syntax:
|
|
383
|
+
// @import "foo";
|
|
384
|
+
// @import "./foo.css";
|
|
385
|
+
if (!params.length || params[0].type !== 'string' || !params[0].value) {
|
|
386
|
+
throw node.error(`Invalid import statement, unable to find imported module.`);
|
|
387
|
+
}
|
|
388
|
+
if (params.length > 1) {
|
|
389
|
+
throw node.error(`Invalid import statement, import statement only support a single parameter.`);
|
|
390
|
+
}
|
|
391
|
+
// Add the imported to results messages
|
|
392
|
+
const message = importMessage(params[0].value);
|
|
393
|
+
result.messages.push(message);
|
|
394
|
+
// Remove the import from the generated css
|
|
395
|
+
node.remove();
|
|
396
|
+
});
|
|
393
397
|
});
|
|
394
398
|
}
|
|
395
399
|
|
|
@@ -428,48 +432,52 @@ function trimNodeWhitespaces(node) {
|
|
|
428
432
|
const DEPRECATED_SELECTORS = new Set(['/deep/', '::shadow', '>>>']);
|
|
429
433
|
const UNSUPPORTED_SELECTORS = new Set([':root', ':host-context']);
|
|
430
434
|
const TEMPLATE_DIRECTIVES = [/^key$/, /^lwc:*/, /^if:*/, /^for:*/, /^iterator:*/];
|
|
431
|
-
function validateSelectors(root, native) {
|
|
435
|
+
function validateSelectors(root, native, ctx) {
|
|
432
436
|
root.walk((node) => {
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
437
|
+
ctx.withErrorRecovery(() => {
|
|
438
|
+
const { value, sourceIndex } = node;
|
|
439
|
+
if (value) {
|
|
440
|
+
// Ensure the selector doesn't use a deprecated CSS selector.
|
|
441
|
+
if (DEPRECATED_SELECTORS.has(value)) {
|
|
442
|
+
throw root.error(`Invalid usage of deprecated selector "${value}".`, {
|
|
443
|
+
index: sourceIndex,
|
|
444
|
+
word: value,
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
// Ensure the selector doesn't use an unsupported selector.
|
|
448
|
+
if (!native && UNSUPPORTED_SELECTORS.has(value)) {
|
|
449
|
+
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.`, {
|
|
450
|
+
index: sourceIndex,
|
|
451
|
+
word: value,
|
|
452
|
+
});
|
|
453
|
+
}
|
|
448
454
|
}
|
|
449
|
-
}
|
|
455
|
+
});
|
|
450
456
|
});
|
|
451
457
|
}
|
|
452
|
-
function validateAttribute(root) {
|
|
458
|
+
function validateAttribute(root, ctx) {
|
|
453
459
|
root.walkAttributes((node) => {
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
if (isTemplateDirective) {
|
|
459
|
-
const message = [
|
|
460
|
-
`Invalid usage of attribute selector "${attributeName}". `,
|
|
461
|
-
`"${attributeName}" is a template directive and therefore not supported in css rules.`,
|
|
462
|
-
];
|
|
463
|
-
throw root.error(message.join(''), {
|
|
464
|
-
index: sourceIndex,
|
|
465
|
-
word: attributeName,
|
|
460
|
+
ctx.withErrorRecovery(() => {
|
|
461
|
+
const { attribute: attributeName, sourceIndex } = node;
|
|
462
|
+
const isTemplateDirective = TEMPLATE_DIRECTIVES.some((directive) => {
|
|
463
|
+
return directive.test(attributeName);
|
|
466
464
|
});
|
|
467
|
-
|
|
465
|
+
if (isTemplateDirective) {
|
|
466
|
+
const message = [
|
|
467
|
+
`Invalid usage of attribute selector "${attributeName}". `,
|
|
468
|
+
`"${attributeName}" is a template directive and therefore not supported in css rules.`,
|
|
469
|
+
];
|
|
470
|
+
throw root.error(message.join(''), {
|
|
471
|
+
index: sourceIndex,
|
|
472
|
+
word: attributeName,
|
|
473
|
+
});
|
|
474
|
+
}
|
|
475
|
+
});
|
|
468
476
|
});
|
|
469
477
|
}
|
|
470
|
-
function validate(root, native) {
|
|
471
|
-
validateSelectors(root, native);
|
|
472
|
-
validateAttribute(root);
|
|
478
|
+
function validate(root, native, ctx) {
|
|
479
|
+
validateSelectors(root, native, ctx);
|
|
480
|
+
validateAttribute(root, ctx);
|
|
473
481
|
}
|
|
474
482
|
|
|
475
483
|
/*
|
|
@@ -578,8 +586,8 @@ function transformHost(selector) {
|
|
|
578
586
|
replaceNodeWith(selector, ...contextualSelectors);
|
|
579
587
|
}
|
|
580
588
|
}
|
|
581
|
-
function transformSelector(root, transformConfig) {
|
|
582
|
-
validate(root, transformConfig.disableSyntheticShadowSupport && !transformConfig.scoped);
|
|
589
|
+
function transformSelector(root, transformConfig, ctx) {
|
|
590
|
+
validate(root, transformConfig.disableSyntheticShadowSupport && !transformConfig.scoped, ctx);
|
|
583
591
|
root.each(scopeSelector);
|
|
584
592
|
if (transformConfig.transformHost) {
|
|
585
593
|
root.each(transformHost);
|
|
@@ -595,61 +603,63 @@ function transformSelector(root, transformConfig) {
|
|
|
595
603
|
function isValidDirValue(value) {
|
|
596
604
|
return value === 'ltr' || value === 'rtl';
|
|
597
605
|
}
|
|
598
|
-
function transformDirPseudoClass (root) {
|
|
606
|
+
function transformDirPseudoClass (root, ctx) {
|
|
599
607
|
root.nodes.forEach((selector) => {
|
|
600
608
|
selector.nodes.forEach((node) => {
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
+
ctx.withErrorRecovery(() => {
|
|
610
|
+
if (!isDirPseudoClass(node)) {
|
|
611
|
+
return;
|
|
612
|
+
}
|
|
613
|
+
const value = node.nodes.toString().trim();
|
|
614
|
+
if (!isValidDirValue(value)) {
|
|
615
|
+
throw root.error(`:dir() pseudo class expects "ltr" or "rtl" for value, but received "${value}".`, {
|
|
616
|
+
index: node.sourceIndex,
|
|
617
|
+
word: node.value,
|
|
618
|
+
});
|
|
619
|
+
}
|
|
620
|
+
// Set placeholders for `:dir()` so we can keep it for native shadow and
|
|
621
|
+
// replace it with a polyfill for synthetic shadow.
|
|
622
|
+
//
|
|
623
|
+
// Native: `:dir(ltr)`
|
|
624
|
+
// Synthetic: `[dir="ltr"]`
|
|
625
|
+
//
|
|
626
|
+
// The placeholders look like this: `[__dirAttributeNativeLtr__]`
|
|
627
|
+
// The attribute has no value because it's simpler during serialization, and there
|
|
628
|
+
// are only two valid values: "ltr" and "rtl".
|
|
629
|
+
//
|
|
630
|
+
// Now consider a more complex selector: `.foo:dir(ltr):not(.bar)`.
|
|
631
|
+
// For native shadow, we need to leave it as-is. Whereas for synthetic shadow, we need
|
|
632
|
+
// to convert it to: `[dir="ltr"] .foo:not(.bar)`.
|
|
633
|
+
// I.e. we need to use a descendant selector (' ' combinator) relying on a `dir`
|
|
634
|
+
// attribute added to the host element. So we need two placeholders:
|
|
635
|
+
// `<synthetic_placeholder> .foo<native_placeholder>:not(.bar)`
|
|
636
|
+
const nativeAttribute = postCssSelector.attribute({
|
|
637
|
+
attribute: value === 'ltr' ? DIR_ATTRIBUTE_NATIVE_LTR : DIR_ATTRIBUTE_NATIVE_RTL,
|
|
638
|
+
value: undefined,
|
|
639
|
+
raws: {},
|
|
609
640
|
});
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
});
|
|
632
|
-
const syntheticAttribute = postCssSelector.attribute({
|
|
633
|
-
attribute: value === 'ltr' ? DIR_ATTRIBUTE_SYNTHETIC_LTR : DIR_ATTRIBUTE_SYNTHETIC_RTL,
|
|
634
|
-
value: undefined,
|
|
635
|
-
raws: {},
|
|
641
|
+
const syntheticAttribute = postCssSelector.attribute({
|
|
642
|
+
attribute: value === 'ltr' ? DIR_ATTRIBUTE_SYNTHETIC_LTR : DIR_ATTRIBUTE_SYNTHETIC_RTL,
|
|
643
|
+
value: undefined,
|
|
644
|
+
raws: {},
|
|
645
|
+
});
|
|
646
|
+
node.replaceWith(nativeAttribute);
|
|
647
|
+
// If the selector is not empty and if the first node in the selector is not already a
|
|
648
|
+
// " " combinator, we need to use the descendant selector format
|
|
649
|
+
const shouldAddDescendantCombinator = selector.first && !postCssSelector.isCombinator(selector.first) && selector.first.value !== ' ';
|
|
650
|
+
if (shouldAddDescendantCombinator) {
|
|
651
|
+
selector.insertBefore(selector.first, postCssSelector.combinator({
|
|
652
|
+
value: ' ',
|
|
653
|
+
}));
|
|
654
|
+
// Add the [dir] attribute in front of the " " combinator, i.e. as an ancestor
|
|
655
|
+
selector.insertBefore(selector.first, syntheticAttribute);
|
|
656
|
+
}
|
|
657
|
+
else {
|
|
658
|
+
// Otherwise there's no need for the descendant selector, so we can skip adding the
|
|
659
|
+
// space combinator and just put the synthetic placeholder next to the native one
|
|
660
|
+
selector.insertBefore(nativeAttribute, syntheticAttribute);
|
|
661
|
+
}
|
|
636
662
|
});
|
|
637
|
-
node.replaceWith(nativeAttribute);
|
|
638
|
-
// If the selector is not empty and if the first node in the selector is not already a
|
|
639
|
-
// " " combinator, we need to use the descendant selector format
|
|
640
|
-
const shouldAddDescendantCombinator = selector.first && !postCssSelector.isCombinator(selector.first) && selector.first.value !== ' ';
|
|
641
|
-
if (shouldAddDescendantCombinator) {
|
|
642
|
-
selector.insertBefore(selector.first, postCssSelector.combinator({
|
|
643
|
-
value: ' ',
|
|
644
|
-
}));
|
|
645
|
-
// Add the [dir] attribute in front of the " " combinator, i.e. as an ancestor
|
|
646
|
-
selector.insertBefore(selector.first, syntheticAttribute);
|
|
647
|
-
}
|
|
648
|
-
else {
|
|
649
|
-
// Otherwise there's no need for the descendant selector, so we can skip adding the
|
|
650
|
-
// space combinator and just put the synthetic placeholder next to the native one
|
|
651
|
-
selector.insertBefore(nativeAttribute, syntheticAttribute);
|
|
652
|
-
}
|
|
653
663
|
});
|
|
654
664
|
});
|
|
655
665
|
}
|
|
@@ -676,35 +686,39 @@ function getAllNames(name) {
|
|
|
676
686
|
}
|
|
677
687
|
const ANIMATION = getAllNames('animation');
|
|
678
688
|
const ANIMATION_NAME = getAllNames('animation-name');
|
|
679
|
-
function process(root) {
|
|
689
|
+
function process(root, ctx) {
|
|
680
690
|
const knownNames = new Set();
|
|
681
691
|
root.walkAtRules((atRule) => {
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
692
|
+
ctx.withErrorRecovery(() => {
|
|
693
|
+
// Note that @-webkit-keyframes, @-moz-keyframes, etc. are not actually a thing supported
|
|
694
|
+
// in any browser, even though you'll see it on some StackOverflow answers.
|
|
695
|
+
if (atRule.name === 'keyframes') {
|
|
696
|
+
const { params } = atRule;
|
|
697
|
+
knownNames.add(params);
|
|
698
|
+
atRule.params = `${params}-${SHADOW_ATTRIBUTE}`;
|
|
699
|
+
}
|
|
700
|
+
});
|
|
689
701
|
});
|
|
690
702
|
root.walkRules((rule) => {
|
|
691
703
|
rule.walkDecls((decl) => {
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
.
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
else if (ANIMATION_NAME.has(decl.prop)) {
|
|
704
|
-
if (knownNames.has(decl.value)) {
|
|
705
|
-
decl.value = `${decl.value}-${SHADOW_ATTRIBUTE}`;
|
|
704
|
+
ctx.withErrorRecovery(() => {
|
|
705
|
+
if (ANIMATION.has(decl.prop)) {
|
|
706
|
+
// Use a simple heuristic of breaking up the tokens by whitespace. We could use
|
|
707
|
+
// a dedicated animation prop parser (e.g.
|
|
708
|
+
// https://github.com/hookhookun/parse-animation-shorthand) but it's
|
|
709
|
+
// probably overkill.
|
|
710
|
+
const tokens = decl.value
|
|
711
|
+
.trim()
|
|
712
|
+
.split(/\s+/g)
|
|
713
|
+
.map((token) => knownNames.has(token) ? `${token}-${SHADOW_ATTRIBUTE}` : token);
|
|
714
|
+
decl.value = tokens.join(' ');
|
|
706
715
|
}
|
|
707
|
-
|
|
716
|
+
else if (ANIMATION_NAME.has(decl.prop)) {
|
|
717
|
+
if (knownNames.has(decl.value)) {
|
|
718
|
+
decl.value = `${decl.value}-${SHADOW_ATTRIBUTE}`;
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
});
|
|
708
722
|
});
|
|
709
723
|
});
|
|
710
724
|
}
|
|
@@ -720,14 +734,15 @@ function shouldTransformSelector(rule) {
|
|
|
720
734
|
// scoped like any other rules.
|
|
721
735
|
return rule.parent?.type !== 'atrule' || rule.parent.name !== 'keyframes';
|
|
722
736
|
}
|
|
723
|
-
function selectorProcessorFactory(transformConfig) {
|
|
737
|
+
function selectorProcessorFactory(transformConfig, ctx) {
|
|
724
738
|
return postCssSelector((root) => {
|
|
725
|
-
validateIdSelectors(root);
|
|
726
|
-
transformSelector(root, transformConfig);
|
|
727
|
-
transformDirPseudoClass(root);
|
|
739
|
+
validateIdSelectors(root, ctx);
|
|
740
|
+
transformSelector(root, transformConfig, ctx);
|
|
741
|
+
transformDirPseudoClass(root, ctx);
|
|
728
742
|
});
|
|
729
743
|
}
|
|
730
744
|
function postCssLwcPlugin(options) {
|
|
745
|
+
const { ctx } = options;
|
|
731
746
|
// We need 2 types of selectors processors, since transforming the :host selector make the selector
|
|
732
747
|
// unusable when used in the context of the native shadow and vice-versa.
|
|
733
748
|
// This distinction also applies to light DOM in scoped (synthetic-like) vs unscoped (native-like) mode.
|
|
@@ -735,39 +750,86 @@ function postCssLwcPlugin(options) {
|
|
|
735
750
|
transformHost: false,
|
|
736
751
|
disableSyntheticShadowSupport: options.disableSyntheticShadowSupport,
|
|
737
752
|
scoped: options.scoped,
|
|
738
|
-
});
|
|
753
|
+
}, ctx);
|
|
739
754
|
const syntheticShadowSelectorProcessor = selectorProcessorFactory({
|
|
740
755
|
transformHost: true,
|
|
741
756
|
disableSyntheticShadowSupport: options.disableSyntheticShadowSupport,
|
|
742
757
|
scoped: options.scoped,
|
|
743
|
-
});
|
|
758
|
+
}, ctx);
|
|
744
759
|
return (root, result) => {
|
|
745
|
-
process$1(root, result, options.scoped);
|
|
746
|
-
process(root);
|
|
760
|
+
process$1(root, result, options.scoped, ctx);
|
|
761
|
+
process(root, ctx);
|
|
762
|
+
// Wrap rule processing with error recovery
|
|
747
763
|
root.walkRules((rule) => {
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
764
|
+
ctx.withErrorRecovery(() => {
|
|
765
|
+
if (!shouldTransformSelector(rule)) {
|
|
766
|
+
return;
|
|
767
|
+
}
|
|
768
|
+
// Let transform the selector with the 2 processors.
|
|
769
|
+
const syntheticSelector = syntheticShadowSelectorProcessor.processSync(rule);
|
|
770
|
+
const nativeSelector = nativeShadowSelectorProcessor.processSync(rule);
|
|
771
|
+
rule.selector = syntheticSelector;
|
|
772
|
+
// If the resulting selector are different it means that the selector use the :host selector. In
|
|
773
|
+
// this case we need to duplicate the CSS rule and assign the other selector.
|
|
774
|
+
if (syntheticSelector !== nativeSelector) {
|
|
775
|
+
// The cloned selector is inserted before the currently processed selector to avoid processing
|
|
776
|
+
// again the cloned selector.
|
|
777
|
+
const currentRule = rule;
|
|
778
|
+
const clonedRule = rule.cloneBefore();
|
|
779
|
+
clonedRule.selector = nativeSelector;
|
|
780
|
+
// Safe a reference to each other
|
|
781
|
+
clonedRule._isNativeHost = true;
|
|
782
|
+
currentRule._isSyntheticHost = true;
|
|
783
|
+
}
|
|
784
|
+
});
|
|
767
785
|
});
|
|
768
786
|
};
|
|
769
787
|
}
|
|
770
788
|
|
|
789
|
+
/*
|
|
790
|
+
* Copyright (c) 2025, Salesforce, Inc.
|
|
791
|
+
* All rights reserved.
|
|
792
|
+
* SPDX-License-Identifier: MIT
|
|
793
|
+
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT
|
|
794
|
+
*/
|
|
795
|
+
class StyleCompilerCtx {
|
|
796
|
+
constructor(errorRecoveryMode, filename) {
|
|
797
|
+
this.errors = [];
|
|
798
|
+
this.seenErrorKeys = new Set();
|
|
799
|
+
this.errorRecoveryMode = errorRecoveryMode;
|
|
800
|
+
this.filename = filename;
|
|
801
|
+
}
|
|
802
|
+
/**
|
|
803
|
+
* This method recovers from CSS syntax errors that are encountered when fn is invoked.
|
|
804
|
+
* All other errors are considered compiler errors and can not be recovered from.
|
|
805
|
+
* @param fn method to be invoked.
|
|
806
|
+
*/
|
|
807
|
+
withErrorRecovery(fn) {
|
|
808
|
+
if (!this.errorRecoveryMode) {
|
|
809
|
+
return fn();
|
|
810
|
+
}
|
|
811
|
+
try {
|
|
812
|
+
return fn();
|
|
813
|
+
}
|
|
814
|
+
catch (error) {
|
|
815
|
+
if (error instanceof postcss.CssSyntaxError) {
|
|
816
|
+
if (this.seenErrorKeys.has(error.message)) {
|
|
817
|
+
return;
|
|
818
|
+
}
|
|
819
|
+
this.seenErrorKeys.add(error.message);
|
|
820
|
+
this.errors.push(error);
|
|
821
|
+
}
|
|
822
|
+
else {
|
|
823
|
+
// Non-CSS errors (compiler errors) should still throw
|
|
824
|
+
throw error;
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
hasErrors() {
|
|
829
|
+
return this.errors.length > 0;
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
|
|
771
833
|
/*
|
|
772
834
|
* Copyright (c) 2024, Salesforce, Inc.
|
|
773
835
|
* All rights reserved.
|
|
@@ -798,11 +860,36 @@ function transform(src, id, config = {}) {
|
|
|
798
860
|
const scoped = !!config.scoped;
|
|
799
861
|
shared.getAPIVersionFromNumber(config.apiVersion);
|
|
800
862
|
const disableSyntheticShadowSupport = !!config.disableSyntheticShadowSupport;
|
|
801
|
-
const
|
|
802
|
-
|
|
863
|
+
const errorRecoveryMode = !!config.experimentalErrorRecoveryMode;
|
|
864
|
+
// Create error recovery context
|
|
865
|
+
const ctx = new StyleCompilerCtx(errorRecoveryMode, id);
|
|
866
|
+
const plugins = [
|
|
867
|
+
postCssLwcPlugin({
|
|
868
|
+
scoped,
|
|
869
|
+
disableSyntheticShadowSupport,
|
|
870
|
+
ctx,
|
|
871
|
+
}),
|
|
872
|
+
];
|
|
873
|
+
// Wrap PostCSS processing with error recovery for parsing errors
|
|
874
|
+
let result;
|
|
875
|
+
try {
|
|
876
|
+
result = postcss(plugins).process(src, { from: id }).sync();
|
|
877
|
+
}
|
|
878
|
+
catch (error) {
|
|
879
|
+
if (errorRecoveryMode && error instanceof postcss.CssSyntaxError) {
|
|
880
|
+
ctx.errors.push(error);
|
|
881
|
+
throw AggregateError(ctx.errors);
|
|
882
|
+
}
|
|
883
|
+
else {
|
|
884
|
+
throw error;
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
if (errorRecoveryMode && ctx.hasErrors()) {
|
|
888
|
+
throw AggregateError(ctx.errors);
|
|
889
|
+
}
|
|
803
890
|
return { code: serialize(result, config) };
|
|
804
891
|
}
|
|
805
892
|
|
|
806
893
|
exports.transform = transform;
|
|
807
|
-
/** version: 8.
|
|
894
|
+
/** version: 8.26.0 */
|
|
808
895
|
//# sourceMappingURL=index.cjs.js.map
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Copyright (c) 2025 Salesforce, Inc.
|
|
3
3
|
*/
|
|
4
|
-
import postcss from 'postcss';
|
|
4
|
+
import postcss, { CssSyntaxError } from 'postcss';
|
|
5
5
|
import { LWC_VERSION_COMMENT, KEY__SCOPED_CSS, KEY__NATIVE_ONLY_CSS, getAPIVersionFromNumber } from '@lwc/shared';
|
|
6
6
|
import postCssSelector, { isPseudoClass, isCombinator, isPseudoElement, attribute, combinator } from 'postcss-selector-parser';
|
|
7
7
|
import valueParser from 'postcss-value-parser';
|
|
@@ -340,12 +340,14 @@ function tokenizeCss(data) {
|
|
|
340
340
|
return tokens;
|
|
341
341
|
}
|
|
342
342
|
|
|
343
|
-
function validateIdSelectors (root) {
|
|
343
|
+
function validateIdSelectors (root, ctx) {
|
|
344
344
|
root.walkIds((node) => {
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
345
|
+
ctx.withErrorRecovery(() => {
|
|
346
|
+
const message = `Invalid usage of id selector '#${node.value}'. Try using a class selector or some other selector.`;
|
|
347
|
+
throw root.error(message, {
|
|
348
|
+
index: node.sourceIndex,
|
|
349
|
+
word: node.value,
|
|
350
|
+
});
|
|
349
351
|
});
|
|
350
352
|
});
|
|
351
353
|
}
|
|
@@ -356,36 +358,38 @@ function validateIdSelectors (root) {
|
|
|
356
358
|
* SPDX-License-Identifier: MIT
|
|
357
359
|
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT
|
|
358
360
|
*/
|
|
359
|
-
function process$1(root, result, isScoped) {
|
|
361
|
+
function process$1(root, result, isScoped, ctx) {
|
|
360
362
|
root.walkAtRules('import', (node) => {
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
// Ensure @import are at the top of the file
|
|
365
|
-
let prev = node.prev();
|
|
366
|
-
while (prev) {
|
|
367
|
-
if (prev.type === 'comment' || (prev.type === 'atrule' && prev.name === 'import')) {
|
|
368
|
-
prev = prev.prev();
|
|
363
|
+
ctx.withErrorRecovery(() => {
|
|
364
|
+
if (isScoped) {
|
|
365
|
+
throw node.error(`Invalid import statement, imports are not allowed in *.scoped.css files.`);
|
|
369
366
|
}
|
|
370
|
-
|
|
371
|
-
|
|
367
|
+
// Ensure @import are at the top of the file
|
|
368
|
+
let prev = node.prev();
|
|
369
|
+
while (prev) {
|
|
370
|
+
if (prev.type === 'comment' || (prev.type === 'atrule' && prev.name === 'import')) {
|
|
371
|
+
prev = prev.prev();
|
|
372
|
+
}
|
|
373
|
+
else {
|
|
374
|
+
throw prev.error('@import must precede all other statements');
|
|
375
|
+
}
|
|
372
376
|
}
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
377
|
+
const { nodes: params } = valueParser(node.params);
|
|
378
|
+
// Ensure import match the following syntax:
|
|
379
|
+
// @import "foo";
|
|
380
|
+
// @import "./foo.css";
|
|
381
|
+
if (!params.length || params[0].type !== 'string' || !params[0].value) {
|
|
382
|
+
throw node.error(`Invalid import statement, unable to find imported module.`);
|
|
383
|
+
}
|
|
384
|
+
if (params.length > 1) {
|
|
385
|
+
throw node.error(`Invalid import statement, import statement only support a single parameter.`);
|
|
386
|
+
}
|
|
387
|
+
// Add the imported to results messages
|
|
388
|
+
const message = importMessage(params[0].value);
|
|
389
|
+
result.messages.push(message);
|
|
390
|
+
// Remove the import from the generated css
|
|
391
|
+
node.remove();
|
|
392
|
+
});
|
|
389
393
|
});
|
|
390
394
|
}
|
|
391
395
|
|
|
@@ -424,48 +428,52 @@ function trimNodeWhitespaces(node) {
|
|
|
424
428
|
const DEPRECATED_SELECTORS = new Set(['/deep/', '::shadow', '>>>']);
|
|
425
429
|
const UNSUPPORTED_SELECTORS = new Set([':root', ':host-context']);
|
|
426
430
|
const TEMPLATE_DIRECTIVES = [/^key$/, /^lwc:*/, /^if:*/, /^for:*/, /^iterator:*/];
|
|
427
|
-
function validateSelectors(root, native) {
|
|
431
|
+
function validateSelectors(root, native, ctx) {
|
|
428
432
|
root.walk((node) => {
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
433
|
+
ctx.withErrorRecovery(() => {
|
|
434
|
+
const { value, sourceIndex } = node;
|
|
435
|
+
if (value) {
|
|
436
|
+
// Ensure the selector doesn't use a deprecated CSS selector.
|
|
437
|
+
if (DEPRECATED_SELECTORS.has(value)) {
|
|
438
|
+
throw root.error(`Invalid usage of deprecated selector "${value}".`, {
|
|
439
|
+
index: sourceIndex,
|
|
440
|
+
word: value,
|
|
441
|
+
});
|
|
442
|
+
}
|
|
443
|
+
// Ensure the selector doesn't use an unsupported selector.
|
|
444
|
+
if (!native && UNSUPPORTED_SELECTORS.has(value)) {
|
|
445
|
+
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.`, {
|
|
446
|
+
index: sourceIndex,
|
|
447
|
+
word: value,
|
|
448
|
+
});
|
|
449
|
+
}
|
|
444
450
|
}
|
|
445
|
-
}
|
|
451
|
+
});
|
|
446
452
|
});
|
|
447
453
|
}
|
|
448
|
-
function validateAttribute(root) {
|
|
454
|
+
function validateAttribute(root, ctx) {
|
|
449
455
|
root.walkAttributes((node) => {
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
if (isTemplateDirective) {
|
|
455
|
-
const message = [
|
|
456
|
-
`Invalid usage of attribute selector "${attributeName}". `,
|
|
457
|
-
`"${attributeName}" is a template directive and therefore not supported in css rules.`,
|
|
458
|
-
];
|
|
459
|
-
throw root.error(message.join(''), {
|
|
460
|
-
index: sourceIndex,
|
|
461
|
-
word: attributeName,
|
|
456
|
+
ctx.withErrorRecovery(() => {
|
|
457
|
+
const { attribute: attributeName, sourceIndex } = node;
|
|
458
|
+
const isTemplateDirective = TEMPLATE_DIRECTIVES.some((directive) => {
|
|
459
|
+
return directive.test(attributeName);
|
|
462
460
|
});
|
|
463
|
-
|
|
461
|
+
if (isTemplateDirective) {
|
|
462
|
+
const message = [
|
|
463
|
+
`Invalid usage of attribute selector "${attributeName}". `,
|
|
464
|
+
`"${attributeName}" is a template directive and therefore not supported in css rules.`,
|
|
465
|
+
];
|
|
466
|
+
throw root.error(message.join(''), {
|
|
467
|
+
index: sourceIndex,
|
|
468
|
+
word: attributeName,
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
});
|
|
464
472
|
});
|
|
465
473
|
}
|
|
466
|
-
function validate(root, native) {
|
|
467
|
-
validateSelectors(root, native);
|
|
468
|
-
validateAttribute(root);
|
|
474
|
+
function validate(root, native, ctx) {
|
|
475
|
+
validateSelectors(root, native, ctx);
|
|
476
|
+
validateAttribute(root, ctx);
|
|
469
477
|
}
|
|
470
478
|
|
|
471
479
|
/*
|
|
@@ -574,8 +582,8 @@ function transformHost(selector) {
|
|
|
574
582
|
replaceNodeWith(selector, ...contextualSelectors);
|
|
575
583
|
}
|
|
576
584
|
}
|
|
577
|
-
function transformSelector(root, transformConfig) {
|
|
578
|
-
validate(root, transformConfig.disableSyntheticShadowSupport && !transformConfig.scoped);
|
|
585
|
+
function transformSelector(root, transformConfig, ctx) {
|
|
586
|
+
validate(root, transformConfig.disableSyntheticShadowSupport && !transformConfig.scoped, ctx);
|
|
579
587
|
root.each(scopeSelector);
|
|
580
588
|
if (transformConfig.transformHost) {
|
|
581
589
|
root.each(transformHost);
|
|
@@ -591,61 +599,63 @@ function transformSelector(root, transformConfig) {
|
|
|
591
599
|
function isValidDirValue(value) {
|
|
592
600
|
return value === 'ltr' || value === 'rtl';
|
|
593
601
|
}
|
|
594
|
-
function transformDirPseudoClass (root) {
|
|
602
|
+
function transformDirPseudoClass (root, ctx) {
|
|
595
603
|
root.nodes.forEach((selector) => {
|
|
596
604
|
selector.nodes.forEach((node) => {
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
+
ctx.withErrorRecovery(() => {
|
|
606
|
+
if (!isDirPseudoClass(node)) {
|
|
607
|
+
return;
|
|
608
|
+
}
|
|
609
|
+
const value = node.nodes.toString().trim();
|
|
610
|
+
if (!isValidDirValue(value)) {
|
|
611
|
+
throw root.error(`:dir() pseudo class expects "ltr" or "rtl" for value, but received "${value}".`, {
|
|
612
|
+
index: node.sourceIndex,
|
|
613
|
+
word: node.value,
|
|
614
|
+
});
|
|
615
|
+
}
|
|
616
|
+
// Set placeholders for `:dir()` so we can keep it for native shadow and
|
|
617
|
+
// replace it with a polyfill for synthetic shadow.
|
|
618
|
+
//
|
|
619
|
+
// Native: `:dir(ltr)`
|
|
620
|
+
// Synthetic: `[dir="ltr"]`
|
|
621
|
+
//
|
|
622
|
+
// The placeholders look like this: `[__dirAttributeNativeLtr__]`
|
|
623
|
+
// The attribute has no value because it's simpler during serialization, and there
|
|
624
|
+
// are only two valid values: "ltr" and "rtl".
|
|
625
|
+
//
|
|
626
|
+
// Now consider a more complex selector: `.foo:dir(ltr):not(.bar)`.
|
|
627
|
+
// For native shadow, we need to leave it as-is. Whereas for synthetic shadow, we need
|
|
628
|
+
// to convert it to: `[dir="ltr"] .foo:not(.bar)`.
|
|
629
|
+
// I.e. we need to use a descendant selector (' ' combinator) relying on a `dir`
|
|
630
|
+
// attribute added to the host element. So we need two placeholders:
|
|
631
|
+
// `<synthetic_placeholder> .foo<native_placeholder>:not(.bar)`
|
|
632
|
+
const nativeAttribute = attribute({
|
|
633
|
+
attribute: value === 'ltr' ? DIR_ATTRIBUTE_NATIVE_LTR : DIR_ATTRIBUTE_NATIVE_RTL,
|
|
634
|
+
value: undefined,
|
|
635
|
+
raws: {},
|
|
605
636
|
});
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
});
|
|
628
|
-
const syntheticAttribute = attribute({
|
|
629
|
-
attribute: value === 'ltr' ? DIR_ATTRIBUTE_SYNTHETIC_LTR : DIR_ATTRIBUTE_SYNTHETIC_RTL,
|
|
630
|
-
value: undefined,
|
|
631
|
-
raws: {},
|
|
637
|
+
const syntheticAttribute = attribute({
|
|
638
|
+
attribute: value === 'ltr' ? DIR_ATTRIBUTE_SYNTHETIC_LTR : DIR_ATTRIBUTE_SYNTHETIC_RTL,
|
|
639
|
+
value: undefined,
|
|
640
|
+
raws: {},
|
|
641
|
+
});
|
|
642
|
+
node.replaceWith(nativeAttribute);
|
|
643
|
+
// If the selector is not empty and if the first node in the selector is not already a
|
|
644
|
+
// " " combinator, we need to use the descendant selector format
|
|
645
|
+
const shouldAddDescendantCombinator = selector.first && !isCombinator(selector.first) && selector.first.value !== ' ';
|
|
646
|
+
if (shouldAddDescendantCombinator) {
|
|
647
|
+
selector.insertBefore(selector.first, combinator({
|
|
648
|
+
value: ' ',
|
|
649
|
+
}));
|
|
650
|
+
// Add the [dir] attribute in front of the " " combinator, i.e. as an ancestor
|
|
651
|
+
selector.insertBefore(selector.first, syntheticAttribute);
|
|
652
|
+
}
|
|
653
|
+
else {
|
|
654
|
+
// Otherwise there's no need for the descendant selector, so we can skip adding the
|
|
655
|
+
// space combinator and just put the synthetic placeholder next to the native one
|
|
656
|
+
selector.insertBefore(nativeAttribute, syntheticAttribute);
|
|
657
|
+
}
|
|
632
658
|
});
|
|
633
|
-
node.replaceWith(nativeAttribute);
|
|
634
|
-
// If the selector is not empty and if the first node in the selector is not already a
|
|
635
|
-
// " " combinator, we need to use the descendant selector format
|
|
636
|
-
const shouldAddDescendantCombinator = selector.first && !isCombinator(selector.first) && selector.first.value !== ' ';
|
|
637
|
-
if (shouldAddDescendantCombinator) {
|
|
638
|
-
selector.insertBefore(selector.first, combinator({
|
|
639
|
-
value: ' ',
|
|
640
|
-
}));
|
|
641
|
-
// Add the [dir] attribute in front of the " " combinator, i.e. as an ancestor
|
|
642
|
-
selector.insertBefore(selector.first, syntheticAttribute);
|
|
643
|
-
}
|
|
644
|
-
else {
|
|
645
|
-
// Otherwise there's no need for the descendant selector, so we can skip adding the
|
|
646
|
-
// space combinator and just put the synthetic placeholder next to the native one
|
|
647
|
-
selector.insertBefore(nativeAttribute, syntheticAttribute);
|
|
648
|
-
}
|
|
649
659
|
});
|
|
650
660
|
});
|
|
651
661
|
}
|
|
@@ -672,35 +682,39 @@ function getAllNames(name) {
|
|
|
672
682
|
}
|
|
673
683
|
const ANIMATION = getAllNames('animation');
|
|
674
684
|
const ANIMATION_NAME = getAllNames('animation-name');
|
|
675
|
-
function process(root) {
|
|
685
|
+
function process(root, ctx) {
|
|
676
686
|
const knownNames = new Set();
|
|
677
687
|
root.walkAtRules((atRule) => {
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
688
|
+
ctx.withErrorRecovery(() => {
|
|
689
|
+
// Note that @-webkit-keyframes, @-moz-keyframes, etc. are not actually a thing supported
|
|
690
|
+
// in any browser, even though you'll see it on some StackOverflow answers.
|
|
691
|
+
if (atRule.name === 'keyframes') {
|
|
692
|
+
const { params } = atRule;
|
|
693
|
+
knownNames.add(params);
|
|
694
|
+
atRule.params = `${params}-${SHADOW_ATTRIBUTE}`;
|
|
695
|
+
}
|
|
696
|
+
});
|
|
685
697
|
});
|
|
686
698
|
root.walkRules((rule) => {
|
|
687
699
|
rule.walkDecls((decl) => {
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
.
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
else if (ANIMATION_NAME.has(decl.prop)) {
|
|
700
|
-
if (knownNames.has(decl.value)) {
|
|
701
|
-
decl.value = `${decl.value}-${SHADOW_ATTRIBUTE}`;
|
|
700
|
+
ctx.withErrorRecovery(() => {
|
|
701
|
+
if (ANIMATION.has(decl.prop)) {
|
|
702
|
+
// Use a simple heuristic of breaking up the tokens by whitespace. We could use
|
|
703
|
+
// a dedicated animation prop parser (e.g.
|
|
704
|
+
// https://github.com/hookhookun/parse-animation-shorthand) but it's
|
|
705
|
+
// probably overkill.
|
|
706
|
+
const tokens = decl.value
|
|
707
|
+
.trim()
|
|
708
|
+
.split(/\s+/g)
|
|
709
|
+
.map((token) => knownNames.has(token) ? `${token}-${SHADOW_ATTRIBUTE}` : token);
|
|
710
|
+
decl.value = tokens.join(' ');
|
|
702
711
|
}
|
|
703
|
-
|
|
712
|
+
else if (ANIMATION_NAME.has(decl.prop)) {
|
|
713
|
+
if (knownNames.has(decl.value)) {
|
|
714
|
+
decl.value = `${decl.value}-${SHADOW_ATTRIBUTE}`;
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
});
|
|
704
718
|
});
|
|
705
719
|
});
|
|
706
720
|
}
|
|
@@ -716,14 +730,15 @@ function shouldTransformSelector(rule) {
|
|
|
716
730
|
// scoped like any other rules.
|
|
717
731
|
return rule.parent?.type !== 'atrule' || rule.parent.name !== 'keyframes';
|
|
718
732
|
}
|
|
719
|
-
function selectorProcessorFactory(transformConfig) {
|
|
733
|
+
function selectorProcessorFactory(transformConfig, ctx) {
|
|
720
734
|
return postCssSelector((root) => {
|
|
721
|
-
validateIdSelectors(root);
|
|
722
|
-
transformSelector(root, transformConfig);
|
|
723
|
-
transformDirPseudoClass(root);
|
|
735
|
+
validateIdSelectors(root, ctx);
|
|
736
|
+
transformSelector(root, transformConfig, ctx);
|
|
737
|
+
transformDirPseudoClass(root, ctx);
|
|
724
738
|
});
|
|
725
739
|
}
|
|
726
740
|
function postCssLwcPlugin(options) {
|
|
741
|
+
const { ctx } = options;
|
|
727
742
|
// We need 2 types of selectors processors, since transforming the :host selector make the selector
|
|
728
743
|
// unusable when used in the context of the native shadow and vice-versa.
|
|
729
744
|
// This distinction also applies to light DOM in scoped (synthetic-like) vs unscoped (native-like) mode.
|
|
@@ -731,39 +746,86 @@ function postCssLwcPlugin(options) {
|
|
|
731
746
|
transformHost: false,
|
|
732
747
|
disableSyntheticShadowSupport: options.disableSyntheticShadowSupport,
|
|
733
748
|
scoped: options.scoped,
|
|
734
|
-
});
|
|
749
|
+
}, ctx);
|
|
735
750
|
const syntheticShadowSelectorProcessor = selectorProcessorFactory({
|
|
736
751
|
transformHost: true,
|
|
737
752
|
disableSyntheticShadowSupport: options.disableSyntheticShadowSupport,
|
|
738
753
|
scoped: options.scoped,
|
|
739
|
-
});
|
|
754
|
+
}, ctx);
|
|
740
755
|
return (root, result) => {
|
|
741
|
-
process$1(root, result, options.scoped);
|
|
742
|
-
process(root);
|
|
756
|
+
process$1(root, result, options.scoped, ctx);
|
|
757
|
+
process(root, ctx);
|
|
758
|
+
// Wrap rule processing with error recovery
|
|
743
759
|
root.walkRules((rule) => {
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
760
|
+
ctx.withErrorRecovery(() => {
|
|
761
|
+
if (!shouldTransformSelector(rule)) {
|
|
762
|
+
return;
|
|
763
|
+
}
|
|
764
|
+
// Let transform the selector with the 2 processors.
|
|
765
|
+
const syntheticSelector = syntheticShadowSelectorProcessor.processSync(rule);
|
|
766
|
+
const nativeSelector = nativeShadowSelectorProcessor.processSync(rule);
|
|
767
|
+
rule.selector = syntheticSelector;
|
|
768
|
+
// If the resulting selector are different it means that the selector use the :host selector. In
|
|
769
|
+
// this case we need to duplicate the CSS rule and assign the other selector.
|
|
770
|
+
if (syntheticSelector !== nativeSelector) {
|
|
771
|
+
// The cloned selector is inserted before the currently processed selector to avoid processing
|
|
772
|
+
// again the cloned selector.
|
|
773
|
+
const currentRule = rule;
|
|
774
|
+
const clonedRule = rule.cloneBefore();
|
|
775
|
+
clonedRule.selector = nativeSelector;
|
|
776
|
+
// Safe a reference to each other
|
|
777
|
+
clonedRule._isNativeHost = true;
|
|
778
|
+
currentRule._isSyntheticHost = true;
|
|
779
|
+
}
|
|
780
|
+
});
|
|
763
781
|
});
|
|
764
782
|
};
|
|
765
783
|
}
|
|
766
784
|
|
|
785
|
+
/*
|
|
786
|
+
* Copyright (c) 2025, Salesforce, Inc.
|
|
787
|
+
* All rights reserved.
|
|
788
|
+
* SPDX-License-Identifier: MIT
|
|
789
|
+
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT
|
|
790
|
+
*/
|
|
791
|
+
class StyleCompilerCtx {
|
|
792
|
+
constructor(errorRecoveryMode, filename) {
|
|
793
|
+
this.errors = [];
|
|
794
|
+
this.seenErrorKeys = new Set();
|
|
795
|
+
this.errorRecoveryMode = errorRecoveryMode;
|
|
796
|
+
this.filename = filename;
|
|
797
|
+
}
|
|
798
|
+
/**
|
|
799
|
+
* This method recovers from CSS syntax errors that are encountered when fn is invoked.
|
|
800
|
+
* All other errors are considered compiler errors and can not be recovered from.
|
|
801
|
+
* @param fn method to be invoked.
|
|
802
|
+
*/
|
|
803
|
+
withErrorRecovery(fn) {
|
|
804
|
+
if (!this.errorRecoveryMode) {
|
|
805
|
+
return fn();
|
|
806
|
+
}
|
|
807
|
+
try {
|
|
808
|
+
return fn();
|
|
809
|
+
}
|
|
810
|
+
catch (error) {
|
|
811
|
+
if (error instanceof CssSyntaxError) {
|
|
812
|
+
if (this.seenErrorKeys.has(error.message)) {
|
|
813
|
+
return;
|
|
814
|
+
}
|
|
815
|
+
this.seenErrorKeys.add(error.message);
|
|
816
|
+
this.errors.push(error);
|
|
817
|
+
}
|
|
818
|
+
else {
|
|
819
|
+
// Non-CSS errors (compiler errors) should still throw
|
|
820
|
+
throw error;
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
hasErrors() {
|
|
825
|
+
return this.errors.length > 0;
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
|
|
767
829
|
/*
|
|
768
830
|
* Copyright (c) 2024, Salesforce, Inc.
|
|
769
831
|
* All rights reserved.
|
|
@@ -794,11 +856,36 @@ function transform(src, id, config = {}) {
|
|
|
794
856
|
const scoped = !!config.scoped;
|
|
795
857
|
getAPIVersionFromNumber(config.apiVersion);
|
|
796
858
|
const disableSyntheticShadowSupport = !!config.disableSyntheticShadowSupport;
|
|
797
|
-
const
|
|
798
|
-
|
|
859
|
+
const errorRecoveryMode = !!config.experimentalErrorRecoveryMode;
|
|
860
|
+
// Create error recovery context
|
|
861
|
+
const ctx = new StyleCompilerCtx(errorRecoveryMode, id);
|
|
862
|
+
const plugins = [
|
|
863
|
+
postCssLwcPlugin({
|
|
864
|
+
scoped,
|
|
865
|
+
disableSyntheticShadowSupport,
|
|
866
|
+
ctx,
|
|
867
|
+
}),
|
|
868
|
+
];
|
|
869
|
+
// Wrap PostCSS processing with error recovery for parsing errors
|
|
870
|
+
let result;
|
|
871
|
+
try {
|
|
872
|
+
result = postcss(plugins).process(src, { from: id }).sync();
|
|
873
|
+
}
|
|
874
|
+
catch (error) {
|
|
875
|
+
if (errorRecoveryMode && error instanceof postcss.CssSyntaxError) {
|
|
876
|
+
ctx.errors.push(error);
|
|
877
|
+
throw AggregateError(ctx.errors);
|
|
878
|
+
}
|
|
879
|
+
else {
|
|
880
|
+
throw error;
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
if (errorRecoveryMode && ctx.hasErrors()) {
|
|
884
|
+
throw AggregateError(ctx.errors);
|
|
885
|
+
}
|
|
799
886
|
return { code: serialize(result, config) };
|
|
800
887
|
}
|
|
801
888
|
|
|
802
889
|
export { transform };
|
|
803
|
-
/** version: 8.
|
|
890
|
+
/** version: 8.26.0 */
|
|
804
891
|
//# sourceMappingURL=index.js.map
|
|
@@ -1,3 +1,4 @@
|
|
|
1
1
|
import type { Root } from 'postcss-selector-parser';
|
|
2
|
-
|
|
2
|
+
import type { StyleCompilerCtx } from '../utils/error-recovery';
|
|
3
|
+
export default function (root: Root, ctx: StyleCompilerCtx): void;
|
|
3
4
|
//# sourceMappingURL=validate.d.ts.map
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import type { APIVersion } from '@lwc/shared';
|
|
2
2
|
import type { TransformCallback } from 'postcss';
|
|
3
|
+
import type { StyleCompilerCtx } from './utils/error-recovery';
|
|
3
4
|
export default function postCssLwcPlugin(options: {
|
|
4
5
|
scoped: boolean;
|
|
5
6
|
apiVersion: APIVersion;
|
|
6
7
|
disableSyntheticShadowSupport: boolean;
|
|
8
|
+
ctx: StyleCompilerCtx;
|
|
7
9
|
}): TransformCallback;
|
|
8
10
|
//# sourceMappingURL=postcss-lwc-plugin.d.ts.map
|
|
@@ -1,3 +1,4 @@
|
|
|
1
1
|
import type { Root } from 'postcss';
|
|
2
|
-
|
|
2
|
+
import type { StyleCompilerCtx } from '../utils/error-recovery';
|
|
3
|
+
export default function process(root: Root, ctx: StyleCompilerCtx): void;
|
|
3
4
|
//# sourceMappingURL=transform.d.ts.map
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Root } from 'postcss-selector-parser';
|
|
2
|
+
import type { StyleCompilerCtx } from '../utils/error-recovery';
|
|
2
3
|
export interface SelectorScopingConfig {
|
|
3
4
|
/** When set to true, the :host selector gets replace with the the scoping token. */
|
|
4
5
|
transformHost: boolean;
|
|
@@ -7,5 +8,5 @@ export interface SelectorScopingConfig {
|
|
|
7
8
|
/** When set to true, the selector is scoped. */
|
|
8
9
|
scoped: boolean;
|
|
9
10
|
}
|
|
10
|
-
export default function transformSelector(root: Root, transformConfig: SelectorScopingConfig): void;
|
|
11
|
+
export default function transformSelector(root: Root, transformConfig: SelectorScopingConfig, ctx: StyleCompilerCtx): void;
|
|
11
12
|
//# sourceMappingURL=transform.d.ts.map
|
|
@@ -1,3 +1,4 @@
|
|
|
1
1
|
import type { Root } from 'postcss-selector-parser';
|
|
2
|
-
|
|
2
|
+
import type { StyleCompilerCtx } from '../utils/error-recovery';
|
|
3
|
+
export default function validate(root: Root, native: boolean, ctx: StyleCompilerCtx): void;
|
|
3
4
|
//# sourceMappingURL=validate.d.ts.map
|
package/dist/transform.d.ts
CHANGED
|
@@ -14,6 +14,8 @@ export interface Config {
|
|
|
14
14
|
disableSyntheticShadowSupport?: boolean;
|
|
15
15
|
/** The API version to associate with the compiled stylesheet */
|
|
16
16
|
apiVersion?: number;
|
|
17
|
+
/** When set to true, enables error recovery mode to collect multiple errors */
|
|
18
|
+
experimentalErrorRecoveryMode?: boolean;
|
|
17
19
|
}
|
|
18
20
|
/**
|
|
19
21
|
* Transforms CSS for use with LWC components.
|
|
@@ -34,5 +36,6 @@ export interface Config {
|
|
|
34
36
|
*/
|
|
35
37
|
export declare function transform(src: string, id: string, config?: Config): {
|
|
36
38
|
code: string;
|
|
39
|
+
errors?: Error[];
|
|
37
40
|
};
|
|
38
41
|
//# sourceMappingURL=transform.d.ts.map
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { CssSyntaxError } from 'postcss';
|
|
2
|
+
export declare class StyleCompilerCtx {
|
|
3
|
+
readonly errorRecoveryMode: boolean;
|
|
4
|
+
readonly errors: CssSyntaxError[];
|
|
5
|
+
readonly filename: string;
|
|
6
|
+
private readonly seenErrorKeys;
|
|
7
|
+
constructor(errorRecoveryMode: boolean, filename: string);
|
|
8
|
+
/**
|
|
9
|
+
* This method recovers from CSS syntax errors that are encountered when fn is invoked.
|
|
10
|
+
* All other errors are considered compiler errors and can not be recovered from.
|
|
11
|
+
* @param fn method to be invoked.
|
|
12
|
+
*/
|
|
13
|
+
withErrorRecovery<T>(fn: () => T): T | undefined;
|
|
14
|
+
hasErrors(): boolean;
|
|
15
|
+
}
|
|
16
|
+
//# sourceMappingURL=error-recovery.d.ts.map
|
package/package.json
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
"You can safely modify dependencies, devDependencies, keywords, etc., but other props will be overwritten."
|
|
5
5
|
],
|
|
6
6
|
"name": "@lwc/style-compiler",
|
|
7
|
-
"version": "8.
|
|
7
|
+
"version": "8.26.0",
|
|
8
8
|
"description": "Transform style sheet to be consumed by the LWC engine",
|
|
9
9
|
"keywords": [
|
|
10
10
|
"lwc"
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
}
|
|
47
47
|
},
|
|
48
48
|
"dependencies": {
|
|
49
|
-
"@lwc/shared": "8.
|
|
49
|
+
"@lwc/shared": "8.26.0",
|
|
50
50
|
"postcss": "~8.5.6",
|
|
51
51
|
"postcss-selector-parser": "~7.1.0",
|
|
52
52
|
"postcss-value-parser": "~4.2.0"
|