Skip to content

Commit 27e95ff

Browse files
authored
Merge pull request webpack#17229 from webpack/css-empty-import
fix: CSS `@import` parsing edge cases
2 parents d9f164b + 7961665 commit 27e95ff

File tree

9 files changed

+526
-174
lines changed

9 files changed

+526
-174
lines changed

lib/css/CssParser.js

+166-73
Original file line numberDiff line numberDiff line change
@@ -123,10 +123,9 @@ class LocConverter {
123123

124124
const CSS_MODE_TOP_LEVEL = 0;
125125
const CSS_MODE_IN_BLOCK = 1;
126-
const CSS_MODE_AT_IMPORT_EXPECT_URL = 2;
127-
const CSS_MODE_AT_IMPORT_EXPECT_LAYER_OR_SUPPORTS_OR_MEDIA = 3;
128-
const CSS_MODE_AT_IMPORT_INVALID = 4;
129-
const CSS_MODE_AT_NAMESPACE_INVALID = 5;
126+
const CSS_MODE_IN_AT_IMPORT = 2;
127+
const CSS_MODE_AT_IMPORT_INVALID = 3;
128+
const CSS_MODE_AT_NAMESPACE_INVALID = 4;
130129

131130
class CssParser extends Parser {
132131
constructor({ allowModeSwitch = true, defaultMode = "global" } = {}) {
@@ -185,7 +184,7 @@ class CssParser extends Parser {
185184
let lastIdentifier = undefined;
186185
/** @type [string, number, number][] */
187186
let balanced = [];
188-
/** @type {undefined | { start: number, end: number, url?: string, media?: string, supports?: string, layer?: string }} */
187+
/** @type {undefined | { start: number, url?: string, urlStart?: number, urlEnd?: number, layer?: string, layerStart?: number, layerEnd?: number, supports?: string, supportsStart?: number, supportsEnd?: number, inSupports?:boolean, media?: string }} */
189188
let importData = undefined;
190189
/** @type {boolean} */
191190
let inAnimationProperty = false;
@@ -407,15 +406,32 @@ class CssParser extends Parser {
407406
},
408407
url: (input, start, end, contentStart, contentEnd) => {
409408
let value = normalizeUrl(input.slice(contentStart, contentEnd), false);
409+
410410
switch (scope) {
411-
case CSS_MODE_AT_IMPORT_EXPECT_URL: {
411+
case CSS_MODE_IN_AT_IMPORT: {
412+
// Do not parse URLs in `supports(...)`
413+
if (importData.inSupports) {
414+
break;
415+
}
416+
417+
if (importData.url) {
418+
this._emitWarning(
419+
state,
420+
`Duplicate of 'url(...)' in '${input.slice(
421+
importData.start,
422+
end
423+
)}'`,
424+
locConverter,
425+
start,
426+
end
427+
);
428+
429+
break;
430+
}
431+
412432
importData.url = value;
413-
importData.end = end;
414-
scope = CSS_MODE_AT_IMPORT_EXPECT_LAYER_OR_SUPPORTS_OR_MEDIA;
415-
break;
416-
}
417-
// Do not parse URLs in `supports(...)`
418-
case CSS_MODE_AT_IMPORT_EXPECT_LAYER_OR_SUPPORTS_OR_MEDIA: {
433+
importData.urlStart = start;
434+
importData.urlEnd = end;
419435
break;
420436
}
421437
// Do not parse URLs in import between rules
@@ -442,23 +458,44 @@ class CssParser extends Parser {
442458
},
443459
string: (input, start, end) => {
444460
switch (scope) {
445-
case CSS_MODE_AT_IMPORT_EXPECT_URL: {
461+
case CSS_MODE_IN_AT_IMPORT: {
462+
const insideURLFunction =
463+
balanced[balanced.length - 1] &&
464+
balanced[balanced.length - 1][0] === "url";
465+
466+
// Do not parse URLs in `supports(...)` and other strings if we already have a URL
467+
if (
468+
importData.inSupports ||
469+
(!insideURLFunction && importData.url)
470+
) {
471+
break;
472+
}
473+
474+
if (insideURLFunction && importData.url) {
475+
this._emitWarning(
476+
state,
477+
`Duplicate of 'url(...)' in '${input.slice(
478+
importData.start,
479+
end
480+
)}'`,
481+
locConverter,
482+
start,
483+
end
484+
);
485+
486+
break;
487+
}
488+
446489
importData.url = normalizeUrl(
447490
input.slice(start + 1, end - 1),
448491
true
449492
);
450-
importData.end = end;
451-
const insideURLFunction =
452-
balanced[balanced.length - 1] &&
453-
balanced[balanced.length - 1][0] === "url";
454493

455494
if (!insideURLFunction) {
456-
scope = CSS_MODE_AT_IMPORT_EXPECT_LAYER_OR_SUPPORTS_OR_MEDIA;
495+
importData.urlStart = start;
496+
importData.urlEnd = end;
457497
}
458-
break;
459-
}
460-
// Do not parse URLs in `supports(...)`
461-
case CSS_MODE_AT_IMPORT_EXPECT_LAYER_OR_SUPPORTS_OR_MEDIA: {
498+
462499
break;
463500
}
464501
case CSS_MODE_IN_BLOCK: {
@@ -499,7 +536,7 @@ class CssParser extends Parser {
499536
scope = CSS_MODE_AT_NAMESPACE_INVALID;
500537
this._emitWarning(
501538
state,
502-
"@namespace is not supported in bundled CSS",
539+
"'@namespace' is not supported in bundled CSS",
503540
locConverter,
504541
start,
505542
end
@@ -510,16 +547,16 @@ class CssParser extends Parser {
510547
scope = CSS_MODE_AT_IMPORT_INVALID;
511548
this._emitWarning(
512549
state,
513-
"Any @import rules must precede all other rules",
550+
"Any '@import' rules must precede all other rules",
514551
locConverter,
515552
start,
516553
end
517554
);
518555
return end;
519556
}
520557

521-
scope = CSS_MODE_AT_IMPORT_EXPECT_URL;
522-
importData = { start, end };
558+
scope = CSS_MODE_IN_AT_IMPORT;
559+
importData = { start };
523560
} else if (
524561
this.allowModeSwitch &&
525562
OPTIONALLY_VENDOR_PREFIXED_KEYFRAMES_AT_RULE.test(name)
@@ -600,54 +637,99 @@ class CssParser extends Parser {
600637
},
601638
semicolon: (input, start, end) => {
602639
switch (scope) {
603-
case CSS_MODE_AT_IMPORT_EXPECT_URL: {
604-
this._emitWarning(
605-
state,
606-
`Expected URL for @import at ${start}`,
607-
locConverter,
608-
start,
609-
end
610-
);
611-
return end;
612-
}
613-
case CSS_MODE_AT_IMPORT_EXPECT_LAYER_OR_SUPPORTS_OR_MEDIA: {
614-
if (!importData.url === undefined) {
640+
case CSS_MODE_IN_AT_IMPORT: {
641+
const { start } = importData;
642+
643+
if (importData.url === undefined) {
615644
this._emitWarning(
616645
state,
617-
`Expected URL for @import at ${importData.start}`,
646+
`Expected URL in '${input.slice(start, end)}'`,
618647
locConverter,
619-
importData.start,
620-
importData.end
648+
start,
649+
end
621650
);
651+
importData = undefined;
652+
scope = CSS_MODE_TOP_LEVEL;
653+
return end;
654+
}
655+
if (
656+
importData.urlStart > importData.layerStart ||
657+
importData.urlStart > importData.supportsStart
658+
) {
659+
this._emitWarning(
660+
state,
661+
`An URL in '${input.slice(
662+
start,
663+
end
664+
)}' should be before 'layer(...)' or 'supports(...)'`,
665+
locConverter,
666+
start,
667+
end
668+
);
669+
importData = undefined;
670+
scope = CSS_MODE_TOP_LEVEL;
622671
return end;
623672
}
673+
if (importData.layerStart > importData.supportsStart) {
674+
this._emitWarning(
675+
state,
676+
`The 'layer(...)' in '${input.slice(
677+
start,
678+
end
679+
)}' should be before 'supports(...)'`,
680+
locConverter,
681+
start,
682+
end
683+
);
684+
importData = undefined;
685+
scope = CSS_MODE_TOP_LEVEL;
686+
return end;
687+
}
688+
624689
const semicolonPos = end;
625690
end = walkCssTokens.eatWhiteLine(input, end + 1);
626-
const { line: sl, column: sc } = locConverter.get(importData.start);
691+
const { line: sl, column: sc } = locConverter.get(start);
627692
const { line: el, column: ec } = locConverter.get(end);
628-
const pos = walkCssTokens.eatWhitespaceAndComments(
629-
input,
630-
importData.end
631-
);
693+
const lastEnd =
694+
importData.supportsEnd ||
695+
importData.layerEnd ||
696+
importData.urlEnd ||
697+
start;
698+
const pos = walkCssTokens.eatWhitespaceAndComments(input, lastEnd);
632699
// Prevent to consider comments as a part of media query
633700
if (pos !== semicolonPos - 1) {
634-
importData.media = input
635-
.slice(importData.end, semicolonPos - 1)
636-
.trim();
701+
importData.media = input.slice(lastEnd, semicolonPos - 1).trim();
637702
}
638-
const dep = new CssImportDependency(
639-
importData.url.trim(),
640-
[importData.start, end],
641-
importData.layer,
642-
importData.supports,
643-
importData.media && importData.media.length > 0
644-
? importData.media
645-
: undefined
646-
);
647-
dep.setLoc(sl, sc, el, ec);
648-
module.addDependency(dep);
703+
704+
const url = importData.url.trim();
705+
706+
if (url.length === 0) {
707+
const dep = new ConstDependency("", [start, end]);
708+
module.addPresentationalDependency(dep);
709+
dep.setLoc(sl, sc, el, ec);
710+
} else {
711+
const dep = new CssImportDependency(
712+
url,
713+
[start, end],
714+
importData.layer,
715+
importData.supports,
716+
importData.media && importData.media.length > 0
717+
? importData.media
718+
: undefined
719+
);
720+
dep.setLoc(sl, sc, el, ec);
721+
module.addDependency(dep);
722+
}
723+
649724
importData = undefined;
650725
scope = CSS_MODE_TOP_LEVEL;
726+
727+
break;
728+
}
729+
case CSS_MODE_AT_IMPORT_INVALID:
730+
case CSS_MODE_AT_NAMESPACE_INVALID: {
731+
scope = CSS_MODE_TOP_LEVEL;
732+
651733
break;
652734
}
653735
case CSS_MODE_IN_BLOCK: {
@@ -720,10 +802,11 @@ class CssParser extends Parser {
720802
}
721803
break;
722804
}
723-
case CSS_MODE_AT_IMPORT_EXPECT_LAYER_OR_SUPPORTS_OR_MEDIA: {
805+
case CSS_MODE_IN_AT_IMPORT: {
724806
if (input.slice(start, end).toLowerCase() === "layer") {
725807
importData.layer = "";
726-
importData.end = end;
808+
importData.layerStart = start;
809+
importData.layerEnd = end;
727810
}
728811
break;
729812
}
@@ -758,6 +841,13 @@ class CssParser extends Parser {
758841

759842
balanced.push([name, start, end]);
760843

844+
if (
845+
scope === CSS_MODE_IN_AT_IMPORT &&
846+
name.toLowerCase() === "supports"
847+
) {
848+
importData.inSupports = true;
849+
}
850+
761851
if (isLocalMode()) {
762852
name = name.toLowerCase();
763853

@@ -812,20 +902,23 @@ class CssParser extends Parser {
812902
}
813903

814904
switch (scope) {
815-
case CSS_MODE_AT_IMPORT_EXPECT_URL: {
816-
if (last && last[0] === "url") {
817-
importData.end = end;
818-
scope = CSS_MODE_AT_IMPORT_EXPECT_LAYER_OR_SUPPORTS_OR_MEDIA;
819-
}
820-
break;
821-
}
822-
case CSS_MODE_AT_IMPORT_EXPECT_LAYER_OR_SUPPORTS_OR_MEDIA: {
823-
if (last && last[0].toLowerCase() === "layer") {
905+
case CSS_MODE_IN_AT_IMPORT: {
906+
if (last && last[0] === "url" && !importData.inSupports) {
907+
importData.urlStart = last[1];
908+
importData.urlEnd = end;
909+
} else if (
910+
last &&
911+
last[0].toLowerCase() === "layer" &&
912+
!importData.inSupports
913+
) {
824914
importData.layer = input.slice(last[2], end - 1).trim();
825-
importData.end = end;
915+
importData.layerStart = last[1];
916+
importData.layerEnd = end;
826917
} else if (last && last[0].toLowerCase() === "supports") {
827918
importData.supports = input.slice(last[2], end - 1).trim();
828-
importData.end = end;
919+
importData.supportsStart = last[1];
920+
importData.supportsEnd = end;
921+
importData.inSupports = false;
829922
}
830923
break;
831924
}

0 commit comments

Comments
 (0)