Merge pull request #17229 from webpack/css-empty-import

fix: CSS `@import` parsing edge cases
This commit is contained in:
Sean Larkin 2023-05-23 14:04:33 -07:00 committed by GitHub
commit 27e95ff3b5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 528 additions and 176 deletions

View File

@ -123,10 +123,9 @@ class LocConverter {
const CSS_MODE_TOP_LEVEL = 0;
const CSS_MODE_IN_BLOCK = 1;
const CSS_MODE_AT_IMPORT_EXPECT_URL = 2;
const CSS_MODE_AT_IMPORT_EXPECT_LAYER_OR_SUPPORTS_OR_MEDIA = 3;
const CSS_MODE_AT_IMPORT_INVALID = 4;
const CSS_MODE_AT_NAMESPACE_INVALID = 5;
const CSS_MODE_IN_AT_IMPORT = 2;
const CSS_MODE_AT_IMPORT_INVALID = 3;
const CSS_MODE_AT_NAMESPACE_INVALID = 4;
class CssParser extends Parser {
constructor({ allowModeSwitch = true, defaultMode = "global" } = {}) {
@ -185,7 +184,7 @@ class CssParser extends Parser {
let lastIdentifier = undefined;
/** @type [string, number, number][] */
let balanced = [];
/** @type {undefined | { start: number, end: number, url?: string, media?: string, supports?: string, layer?: string }} */
/** @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 }} */
let importData = undefined;
/** @type {boolean} */
let inAnimationProperty = false;
@ -407,15 +406,32 @@ class CssParser extends Parser {
},
url: (input, start, end, contentStart, contentEnd) => {
let value = normalizeUrl(input.slice(contentStart, contentEnd), false);
switch (scope) {
case CSS_MODE_AT_IMPORT_EXPECT_URL: {
case CSS_MODE_IN_AT_IMPORT: {
// Do not parse URLs in `supports(...)`
if (importData.inSupports) {
break;
}
if (importData.url) {
this._emitWarning(
state,
`Duplicate of 'url(...)' in '${input.slice(
importData.start,
end
)}'`,
locConverter,
start,
end
);
break;
}
importData.url = value;
importData.end = end;
scope = CSS_MODE_AT_IMPORT_EXPECT_LAYER_OR_SUPPORTS_OR_MEDIA;
break;
}
// Do not parse URLs in `supports(...)`
case CSS_MODE_AT_IMPORT_EXPECT_LAYER_OR_SUPPORTS_OR_MEDIA: {
importData.urlStart = start;
importData.urlEnd = end;
break;
}
// Do not parse URLs in import between rules
@ -442,23 +458,44 @@ class CssParser extends Parser {
},
string: (input, start, end) => {
switch (scope) {
case CSS_MODE_AT_IMPORT_EXPECT_URL: {
importData.url = normalizeUrl(
input.slice(start + 1, end - 1),
true
);
importData.end = end;
case CSS_MODE_IN_AT_IMPORT: {
const insideURLFunction =
balanced[balanced.length - 1] &&
balanced[balanced.length - 1][0] === "url";
if (!insideURLFunction) {
scope = CSS_MODE_AT_IMPORT_EXPECT_LAYER_OR_SUPPORTS_OR_MEDIA;
// Do not parse URLs in `supports(...)` and other strings if we already have a URL
if (
importData.inSupports ||
(!insideURLFunction && importData.url)
) {
break;
}
break;
}
// Do not parse URLs in `supports(...)`
case CSS_MODE_AT_IMPORT_EXPECT_LAYER_OR_SUPPORTS_OR_MEDIA: {
if (insideURLFunction && importData.url) {
this._emitWarning(
state,
`Duplicate of 'url(...)' in '${input.slice(
importData.start,
end
)}'`,
locConverter,
start,
end
);
break;
}
importData.url = normalizeUrl(
input.slice(start + 1, end - 1),
true
);
if (!insideURLFunction) {
importData.urlStart = start;
importData.urlEnd = end;
}
break;
}
case CSS_MODE_IN_BLOCK: {
@ -499,7 +536,7 @@ class CssParser extends Parser {
scope = CSS_MODE_AT_NAMESPACE_INVALID;
this._emitWarning(
state,
"@namespace is not supported in bundled CSS",
"'@namespace' is not supported in bundled CSS",
locConverter,
start,
end
@ -510,7 +547,7 @@ class CssParser extends Parser {
scope = CSS_MODE_AT_IMPORT_INVALID;
this._emitWarning(
state,
"Any @import rules must precede all other rules",
"Any '@import' rules must precede all other rules",
locConverter,
start,
end
@ -518,8 +555,8 @@ class CssParser extends Parser {
return end;
}
scope = CSS_MODE_AT_IMPORT_EXPECT_URL;
importData = { start, end };
scope = CSS_MODE_IN_AT_IMPORT;
importData = { start };
} else if (
this.allowModeSwitch &&
OPTIONALLY_VENDOR_PREFIXED_KEYFRAMES_AT_RULE.test(name)
@ -600,54 +637,99 @@ class CssParser extends Parser {
},
semicolon: (input, start, end) => {
switch (scope) {
case CSS_MODE_AT_IMPORT_EXPECT_URL: {
this._emitWarning(
state,
`Expected URL for @import at ${start}`,
locConverter,
start,
end
);
return end;
}
case CSS_MODE_AT_IMPORT_EXPECT_LAYER_OR_SUPPORTS_OR_MEDIA: {
if (!importData.url === undefined) {
case CSS_MODE_IN_AT_IMPORT: {
const { start } = importData;
if (importData.url === undefined) {
this._emitWarning(
state,
`Expected URL for @import at ${importData.start}`,
`Expected URL in '${input.slice(start, end)}'`,
locConverter,
importData.start,
importData.end
start,
end
);
importData = undefined;
scope = CSS_MODE_TOP_LEVEL;
return end;
}
if (
importData.urlStart > importData.layerStart ||
importData.urlStart > importData.supportsStart
) {
this._emitWarning(
state,
`An URL in '${input.slice(
start,
end
)}' should be before 'layer(...)' or 'supports(...)'`,
locConverter,
start,
end
);
importData = undefined;
scope = CSS_MODE_TOP_LEVEL;
return end;
}
if (importData.layerStart > importData.supportsStart) {
this._emitWarning(
state,
`The 'layer(...)' in '${input.slice(
start,
end
)}' should be before 'supports(...)'`,
locConverter,
start,
end
);
importData = undefined;
scope = CSS_MODE_TOP_LEVEL;
return end;
}
const semicolonPos = end;
end = walkCssTokens.eatWhiteLine(input, end + 1);
const { line: sl, column: sc } = locConverter.get(importData.start);
const { line: sl, column: sc } = locConverter.get(start);
const { line: el, column: ec } = locConverter.get(end);
const pos = walkCssTokens.eatWhitespaceAndComments(
input,
importData.end
);
const lastEnd =
importData.supportsEnd ||
importData.layerEnd ||
importData.urlEnd ||
start;
const pos = walkCssTokens.eatWhitespaceAndComments(input, lastEnd);
// Prevent to consider comments as a part of media query
if (pos !== semicolonPos - 1) {
importData.media = input
.slice(importData.end, semicolonPos - 1)
.trim();
importData.media = input.slice(lastEnd, semicolonPos - 1).trim();
}
const dep = new CssImportDependency(
importData.url.trim(),
[importData.start, end],
importData.layer,
importData.supports,
importData.media && importData.media.length > 0
? importData.media
: undefined
);
dep.setLoc(sl, sc, el, ec);
module.addDependency(dep);
const url = importData.url.trim();
if (url.length === 0) {
const dep = new ConstDependency("", [start, end]);
module.addPresentationalDependency(dep);
dep.setLoc(sl, sc, el, ec);
} else {
const dep = new CssImportDependency(
url,
[start, end],
importData.layer,
importData.supports,
importData.media && importData.media.length > 0
? importData.media
: undefined
);
dep.setLoc(sl, sc, el, ec);
module.addDependency(dep);
}
importData = undefined;
scope = CSS_MODE_TOP_LEVEL;
break;
}
case CSS_MODE_AT_IMPORT_INVALID:
case CSS_MODE_AT_NAMESPACE_INVALID: {
scope = CSS_MODE_TOP_LEVEL;
break;
}
case CSS_MODE_IN_BLOCK: {
@ -720,10 +802,11 @@ class CssParser extends Parser {
}
break;
}
case CSS_MODE_AT_IMPORT_EXPECT_LAYER_OR_SUPPORTS_OR_MEDIA: {
case CSS_MODE_IN_AT_IMPORT: {
if (input.slice(start, end).toLowerCase() === "layer") {
importData.layer = "";
importData.end = end;
importData.layerStart = start;
importData.layerEnd = end;
}
break;
}
@ -758,6 +841,13 @@ class CssParser extends Parser {
balanced.push([name, start, end]);
if (
scope === CSS_MODE_IN_AT_IMPORT &&
name.toLowerCase() === "supports"
) {
importData.inSupports = true;
}
if (isLocalMode()) {
name = name.toLowerCase();
@ -812,20 +902,23 @@ class CssParser extends Parser {
}
switch (scope) {
case CSS_MODE_AT_IMPORT_EXPECT_URL: {
if (last && last[0] === "url") {
importData.end = end;
scope = CSS_MODE_AT_IMPORT_EXPECT_LAYER_OR_SUPPORTS_OR_MEDIA;
}
break;
}
case CSS_MODE_AT_IMPORT_EXPECT_LAYER_OR_SUPPORTS_OR_MEDIA: {
if (last && last[0].toLowerCase() === "layer") {
case CSS_MODE_IN_AT_IMPORT: {
if (last && last[0] === "url" && !importData.inSupports) {
importData.urlStart = last[1];
importData.urlEnd = end;
} else if (
last &&
last[0].toLowerCase() === "layer" &&
!importData.inSupports
) {
importData.layer = input.slice(last[2], end - 1).trim();
importData.end = end;
importData.layerStart = last[1];
importData.layerEnd = end;
} else if (last && last[0].toLowerCase() === "supports") {
importData.supports = input.slice(last[2], end - 1).trim();
importData.end = end;
importData.supportsStart = last[1];
importData.supportsEnd = end;
importData.inSupports = false;
}
break;
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,6 +1,6 @@
module.exports = [
/Any @import rules must precede all other rules/,
/Any @import rules must precede all other rules/,
/Any @import rules must precede all other rules/,
/Any @import rules must precede all other rules/
/Any '@import' rules must precede all other rules/,
/Any '@import' rules must precede all other rules/,
/Any '@import' rules must precede all other rules/,
/Any '@import' rules must precede all other rules/
];

View File

@ -0,0 +1,4 @@
module.exports = function loader(content) {
return content + `.using-loader { color: red; }`;
};

View File

@ -40,7 +40,7 @@
@import url(
style2.css?foo=9
);
/*@import url();
@import url();
@import url('');
@import url("");
@import '';
@ -50,7 +50,9 @@ style2.css?foo=9
";
@import url();
@import url('');
@import url("");*/
@import url("");
@import url("") /* test */;
@import url("") screen and (orientation:landscape);
@import url(style2.css) screen and (orientation:landscape);
@import url(style2.css) SCREEN AND (ORIENTATION: LANDSCAPE);
@import url(style2.css)screen and (orientation:landscape);
@ -58,20 +60,15 @@ style2.css?foo=9
@import url(style2.css) screen and (orientation:landscape);
@import url(style2.css) (min-width: 100px);
@import url(https://test.cases/path/../../../../configCases/css/css-import/external.css);
/*@import url(https://test.cases/path/../../../../configCases/css/css-import/external.css) screen and (orientation:landscape);
@import url(https://test.cases/path/../../../../configCases/css/css-import/external.css) screen and (orientation:landscape);*/
/*@import "//example.com/style.css";*/
/*@import url(~package/test.css);*/
/*@import ;*/
/*@import foo-bar;*/
/*@import-normalize;*/
/*@import url('http://') :root {}*/
/*@import url('query.css?foo=1&bar=1');*/
/*@import url('other-query.css?foo=1&bar=1#hash');*/
/*@import url('other-query.css?foo=1&bar=1#hash') screen and (orientation:landscape);*/
/*@import url('https://fonts.googleapis.com/css?family=Roboto');*/
/*@import url('https://fonts.googleapis.com/css?family=Noto+Sans+TC');*/
/*@import url('https://fonts.googleapis.com/css?family=Noto+Sans+TC|Roboto');*/
@import url(https://test.cases/path/../../../../configCases/css/css-import/external.css) screen and (orientation:landscape);
@import "//example.com/style.css";
@import url('test.css?foo=1&bar=1');
@import url('style2.css?foo=1&bar=1#hash');
@import url('style2.css?foo=1&bar=1#hash') screen and (orientation:landscape);
@import url('https://fonts.googleapis.com/css?family=Roboto');
@import url('https://fonts.googleapis.com/css?family=Noto+Sans+TC');
@import url('https://fonts.googleapis.com/css?family=Noto+Sans+TC|Roboto');
@import url('https://fonts.googleapis.com/css?family=Noto+Sans+TC|Roboto') layer(super.foo) supports(display: flex) screen and (min-width: 400px);
@import './sty\
le3.css?bar=1';
@ -107,12 +104,11 @@ le3.css?=bar4');
@import "./t\65st%20test.css?fpp=10";
@import './t\65st%20test.css?foo=11';
@import url( style6.css?foo=bazz );
/*@import nourl(test.css);
@import '\
\
\
';
@import url('!!../../helpers/string-loader.js?esModule=false!~package/tilde.css');*/
@import url('./string-loader.js?esModule=false!./test.css');
@import url(style4.css?foo=bar);
@import url(style4.css?foo=bar#hash);
@import url(style4.css?#hash);
@ -123,9 +119,9 @@ le3.css?=bar4');
@import url(' ./style4.css?foo=4 ');
@import url( ./style4.css?foo=5 );
/*@import url(' https://fonts.googleapis.com/css?family=Roboto ');*/
/*@import url('!!../../helpers/string-loader.js?esModule=false!');*/
/*@import url(' !!../../helpers/string-loader.js?esModule=false!~package/tilde.css ');*/
@import url(' https://fonts.googleapis.com/css?family=Roboto ');
@import url('./string-loader.js?esModule=false');
@import url(' ./string-loader.js?esModule=false!./test.css ') screen and (orientation: landscape);
@import url(data:text/css;charset=utf-8,a%20%7B%0D%0A%20%20color%3A%20red%3B%0D%0A%7D);
@import url(data:text/css;charset=utf-8,a%20%7B%0D%0A%20%20color%3A%20blue%3B%0D%0A%7D) screen and (orientation:landscape);
@ -145,7 +141,6 @@ le3.css?=bar4');
@import url("./layer.css?foo=5") layer();
@import url("./layer.css?foo=6") layer( foo.bar.baz );
@import url("./layer.css?foo=7") layer( );
/*@import "http://example.com/style.css" supports(display: flex) screen and (min-width: 400px);*/
@import url("./style6.css")layer(default)supports(display: flex)screen and (min-width:400px);
@import "./style6.css?foo=1"layer(default)supports(display: flex)screen and (min-width:400px);
@import "./style6.css?foo=2"supports(display: flex)screen and (min-width:400px);
@ -204,6 +199,46 @@ url(style6.css?foo=14)
@import "./anonymous-nested.css" supports(display: flex) screen and (orientation: portrait);
@import "./all-nested.css" layer(super.foo) supports(display: flex) screen and (min-width: 400px);
/* Inside support */
@import url("/style2.css?warning=6") supports(unknown: layer(super.foo)) screen and (min-width: 400px);
@import url("/style2.css?warning=7") supports(url: url("./unknown.css")) screen and (min-width: 400px);
@import url("/style2.css?warning=8") supports(url: url(./unknown.css)) screen and (min-width: 400px);
/** Possible syntax in future */
@import url("/style2.css?foo=unknown") layer(super.foo) supports(display: flex) unknown("foo") screen and (min-width: 400px);
@import url("/style2.css?foo=unknown1") layer(super.foo) supports(display: url("./unknown.css")) unknown(foo) screen and (min-width: 400px);
@import url("/style2.css?foo=unknown2") layer(super.foo) supports(display: url(./unknown.css)) "foo" screen and (min-width: 400px);
@import "./style2.css?unknown3" "string";
/** Unknown */
@import-normalize;
/** Warnings */
@import nourl(test.css);
@import ;
@import foo-bar;
@import layer(super.foo) "./style2.css?warning=1" supports(display: flex) screen and (min-width: 400px);
@import layer(super.foo) supports(display: flex) "./style2.css?warning=2" screen and (min-width: 400px);
@import layer(super.foo) supports(display: flex) screen and (min-width: 400px) "./style2.css?warning=3";
@import layer(super.foo) url("./style2.css?warning=4") supports(display: flex) screen and (min-width: 400px);
@import layer(super.foo) supports(display: flex) url("./style2.css?warning=5") screen and (min-width: 400px);
@import layer(super.foo) supports(display: flex) screen and (min-width: 400px) url("./style2.css?warning=6");
@import url("/style2.css?warning=6") supports(display: flex) layer(super.foo) screen and (min-width: 400px);
@namespace url(http://www.w3.org/1999/xhtml);
@import url("./style2.css?after-namespace");
@import supports(background: url("./img.png"));
@import supports(background: url("./img.png")) screen and (min-width: 400px);
@import layer(test) supports(background: url("./img.png")) screen and (min-width: 400px);
@import screen and (min-width: 400px);
@import url(./style2.css?multiple=1) url(./style2.css?multiple=2);
@import url("./style2.css?multiple=3") url("./style2.css?multiple=4");
@import "./style2.css?strange=3" url("./style2.css?multiple=4");
body {
background: red;
}

View File

@ -0,0 +1,20 @@
module.exports = [
/Expected URL in '@import nourl\(test.css\);'/,
/Expected URL in '@import ;'/,
/Expected URL in '@import foo-bar;'/,
/An URL in '@import layer\(super\.foo\) "\.\/style2\.css\?warning=1" supports\(display: flex\) screen and \(min-width: 400px\);' should be before 'layer\(\.\.\.\)' or 'supports\(\.\.\.\)'/,
/An URL in '@import layer\(super\.foo\) supports\(display: flex\) "\.\/style2.css\?warning=2" screen and \(min-width: 400px\);' should be before 'layer\(\.\.\.\)' or 'supports\(\.\.\.\)'/,
/An URL in '@import layer\(super\.foo\) supports\(display: flex\) screen and \(min-width: 400px\) "\.\/style2.css\?warning=3";' should be before 'layer\(\.\.\.\)' or 'supports\(\.\.\.\)'/,
/An URL in '@import layer\(super\.foo\) url\("\.\/style2.css\?warning=4"\) supports\(display: flex\) screen and \(min-width: 400px\);' should be before 'layer\(\.\.\.\)' or 'supports\(\.\.\.\)'/,
/An URL in '@import layer\(super\.foo\) supports\(display: flex\) url\("\.\/style2.css\?warning=5"\) screen and \(min-width: 400px\);' should be before 'layer\(\.\.\.\)' or 'supports\(\.\.\.\)'/,
/An URL in '@import layer\(super\.foo\) supports\(display: flex\) screen and \(min-width: 400px\) url\("\.\/style2.css\?warning=6"\);' should be before 'layer\(\.\.\.\)' or 'supports\(\.\.\.\)'/,
/The 'layer\(\.\.\.\)' in '@import url\("\/style2.css\?warning=6"\) supports\(display: flex\) layer\(super.foo\) screen and \(min-width: 400px\);' should be before 'supports\(\.\.\.\)'/,
/'@namespace' is not supported in bundled CSS/,
/Expected URL in '@import supports\(background: url\("\.\/img.png"\)\);'/,
/Expected URL in '@import supports\(background: url\("\.\/img.png"\)\) screen and \(min-width: 400px\);'/,
/Expected URL in '@import layer\(test\) supports\(background: url\("\.\/img.png"\)\) screen and \(min-width: 400px\);'/,
/Expected URL in '@import screen and \(min-width: 400px\);'/,
/Duplicate of 'url\(\.\.\.\)' in '@import url\(\.\/style2.css\?multiple=1\) url\(\.\/style2.css\?multiple=2\)'/,
/Duplicate of 'url\(\.\.\.\)' in '@import url\("\.\/style2.css\?multiple=3"\) url\("\.\/style2.css\?multiple=4"'/,
/Duplicate of 'url\(\.\.\.\)' in '@import "\.\/style2.css\?strange=3" url\("\.\/style2.css\?multiple=4"'/
];

View File

@ -1 +1 @@
module.exports = [/@namespace is not supported in bundled CSS/];
module.exports = [/'@namespace' is not supported in bundled CSS/];

View File

@ -155,6 +155,10 @@ class FakeSheet {
);
css = css.replace(/@import url\("([^"]+)"\);/g, (match, url) => {
if (!/^https:\/\/test\.cases\/path\//.test(url)) {
return `@import url("${url}");`;
}
if (url.startsWith("#")) {
return url;
}
@ -195,6 +199,10 @@ class FakeSheet {
"utf-8"
);
css = css.replace(/@import url\("([^"]+)"\);/g, (match, url) => {
if (!/^https:\/\/test\.cases\/path\//.test(url)) {
return url;
}
if (url.startsWith("#")) {
return url;
}