Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: any @import rules must precede all other rules #17118

Merged
merged 7 commits into from
May 3, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
180 changes: 121 additions & 59 deletions lib/css/CssParser.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@

"use strict";

const ModuleDependencyWarning = require("../ModuleDependencyWarning");
const Parser = require("../Parser");
const WebpackError = require("../WebpackError");
const ConstDependency = require("../dependencies/ConstDependency");
const CssExportDependency = require("../dependencies/CssExportDependency");
const CssImportDependency = require("../dependencies/CssImportDependency");
Expand Down Expand Up @@ -122,30 +124,8 @@ const CSS_MODE_IN_RULE = 1;
const CSS_MODE_IN_LOCAL_RULE = 2;
const CSS_MODE_AT_IMPORT_EXPECT_URL = 3;
const CSS_MODE_AT_IMPORT_EXPECT_LAYER_OR_SUPPORTS_OR_MEDIA = 4;
const CSS_MODE_AT_OTHER = 5;

/**
* @param {number} mode current mode
* @returns {string} description of mode
*/
const explainMode = mode => {
switch (mode) {
case CSS_MODE_TOP_LEVEL:
return "parsing top level css";
case CSS_MODE_IN_RULE:
return "parsing css rule content (global)";
case CSS_MODE_IN_LOCAL_RULE:
return "parsing css rule content (local)";
case CSS_MODE_AT_IMPORT_EXPECT_URL:
return "parsing @import (expecting url)";
case CSS_MODE_AT_IMPORT_EXPECT_LAYER_OR_SUPPORTS_OR_MEDIA:
return "parsing @import (expecting optionally layer, supports or media query)";
case CSS_MODE_AT_OTHER:
return "parsing at-rule";
default:
return "parsing css";
}
};
const CSS_MODE_AT_IMPORT_INVALID = 5;
const CSS_MODE_AT_NAMESPACE_INVALID = 6;

class CssParser extends Parser {
constructor({
Expand All @@ -159,6 +139,25 @@ class CssParser extends Parser {
this.defaultMode = defaultMode;
}

/**
* @param {ParserState} state parser state
* @param {string} message warning message
* @param {LocConverter} locConverter location converter
* @param {number} start start offset
* @param {number} end end offset
*/
_emitWarning(state, message, locConverter, start, end) {
const { line: sl, column: sc } = locConverter.get(start);
const { line: el, column: ec } = locConverter.get(end);

state.current.addWarning(
new ModuleDependencyWarning(state.module, new WebpackError(message), {
start: { line: sl, column: sc },
end: { line: el, column: ec }
})
);
}

/**
* @param {string | Buffer | PreparsedAst} source the source to parse
* @param {ParserState} state the parser state
Expand All @@ -183,6 +182,8 @@ class CssParser extends Parser {
let mode = CSS_MODE_TOP_LEVEL;
/** @type {number} */
let modeNestingLevel = 0;
/** @type {boolean} */
let allowImportAtRule = true;
let modeData = undefined;
/** @type {string | boolean | undefined} */
let singleClassSelector = undefined;
Expand Down Expand Up @@ -259,10 +260,16 @@ class CssParser extends Parser {
const parseExports = (input, pos) => {
pos = walkCssTokens.eatWhitespaceAndComments(input, pos);
const cc = input.charCodeAt(pos);
if (cc !== CC_LEFT_CURLY)
throw new Error(
`Unexpected ${input[pos]} at ${pos} during parsing of ':export' (expected '{')`
if (cc !== CC_LEFT_CURLY) {
this._emitWarning(
state,
`Unexpected '${input[pos]}' at ${pos} during parsing of ':export' (expected '{')`,
locConverter,
pos,
pos
);
return pos;
}
pos++;
pos = walkCssTokens.eatWhitespaceAndComments(input, pos);
for (;;) {
Expand All @@ -274,9 +281,14 @@ class CssParser extends Parser {
[pos, name] = eatText(input, pos, eatExportName);
if (pos === input.length) return pos;
if (input.charCodeAt(pos) !== CC_COLON) {
throw new Error(
`Unexpected ${input[pos]} at ${pos} during parsing of export name in ':export' (expected ':')`
this._emitWarning(
state,
`Unexpected '${input[pos]}' at ${pos} during parsing of export name in ':export' (expected ':')`,
locConverter,
start,
pos
);
return pos;
}
pos++;
if (pos === input.length) return pos;
Expand All @@ -292,9 +304,14 @@ class CssParser extends Parser {
pos = walkCssTokens.eatWhitespaceAndComments(input, pos);
if (pos === input.length) return pos;
} else if (cc !== CC_RIGHT_CURLY) {
throw new Error(
`Unexpected ${input[pos]} at ${pos} during parsing of export value in ':export' (expected ';' or '}')`
this._emitWarning(
state,
`Unexpected '${input[pos]}' at ${pos} during parsing of export value in ':export' (expected ';' or '}')`,
locConverter,
start,
pos
);
return pos;
}
const dep = new CssExportDependency(name, value);
const { line: sl, column: sc } = locConverter.get(start);
Expand Down Expand Up @@ -360,14 +377,13 @@ class CssParser extends Parser {
mode !== CSS_MODE_IN_RULE &&
mode !== CSS_MODE_IN_LOCAL_RULE &&
mode !== CSS_MODE_AT_IMPORT_EXPECT_URL &&
mode !== CSS_MODE_AT_IMPORT_EXPECT_LAYER_OR_SUPPORTS_OR_MEDIA
mode !== CSS_MODE_AT_IMPORT_EXPECT_LAYER_OR_SUPPORTS_OR_MEDIA &&
mode !== CSS_MODE_AT_IMPORT_INVALID &&
mode !== CSS_MODE_AT_NAMESPACE_INVALID
);
},
url: (input, start, end, contentStart, contentEnd, isString) => {
let value = normalizeUrl(
input.slice(contentStart, contentEnd),
isString
);
url: (input, start, end, contentStart, contentEnd) => {
let value = normalizeUrl(input.slice(contentStart, contentEnd), false);
switch (mode) {
case CSS_MODE_AT_IMPORT_EXPECT_URL: {
modeData.url = value;
Expand All @@ -379,6 +395,11 @@ class CssParser extends Parser {
case CSS_MODE_AT_IMPORT_EXPECT_LAYER_OR_SUPPORTS_OR_MEDIA: {
break;
}
// Do not parse URLs in import between rules
case CSS_MODE_AT_NAMESPACE_INVALID:
case CSS_MODE_AT_IMPORT_INVALID: {
break;
}
default: {
if (
// Ignore `url(#highlight)` URLs
Expand Down Expand Up @@ -457,14 +478,28 @@ class CssParser extends Parser {
atKeyword: (input, start, end) => {
const name = input.slice(start, end).toLowerCase();
if (name === "@namespace") {
throw new Error("@namespace is not supported in bundled CSS");
}
if (name === "@import") {
if (mode !== CSS_MODE_TOP_LEVEL) {
throw new Error(
`Unexpected @import at ${start} during ${explainMode(mode)}`
mode = CSS_MODE_AT_NAMESPACE_INVALID;
this._emitWarning(
state,
"@namespace is not supported in bundled CSS",
locConverter,
start,
end
);
return end;
} else if (name === "@import") {
if (!allowImportAtRule) {
mode = CSS_MODE_AT_IMPORT_INVALID;
this._emitWarning(
state,
"Any @import rules must precede all other rules",
locConverter,
start,
end
);
return end;
}

mode = CSS_MODE_AT_IMPORT_EXPECT_URL;
modeData = {
atRuleStart: start,
Expand All @@ -474,51 +509,77 @@ class CssParser extends Parser {
supports: undefined,
media: undefined
};
}
if (OPTIONALLY_VENDOR_PREFIXED_KEYFRAMES_AT_RULE.test(name)) {
} else if (
isTopLevelLocal() &&
OPTIONALLY_VENDOR_PREFIXED_KEYFRAMES_AT_RULE.test(name)
) {
let pos = end;
pos = walkCssTokens.eatWhitespaceAndComments(input, pos);
if (pos === input.length) return pos;
const [newPos, name] = eatText(input, pos, eatKeyframes);
if (newPos === input.length) return newPos;
if (input.charCodeAt(newPos) !== CC_LEFT_CURLY) {
this._emitWarning(
state,
`Unexpected '${input[newPos]}' at ${newPos} during parsing of @keyframes (expected '{')`,
locConverter,
start,
end
);

return newPos;
}
const { line: sl, column: sc } = locConverter.get(pos);
const { line: el, column: ec } = locConverter.get(newPos);
const dep = new CssLocalIdentifierDependency(name, [pos, newPos]);
dep.setLoc(sl, sc, el, ec);
module.addDependency(dep);
pos = newPos;
if (pos === input.length) return pos;
if (input.charCodeAt(pos) !== CC_LEFT_CURLY) {
throw new Error(
`Unexpected ${input[pos]} at ${pos} during parsing of @keyframes (expected '{')`
);
}
mode = CSS_MODE_IN_LOCAL_RULE;
modeNestingLevel = 1;
return pos + 1;
}
if (name === "@media" || name === "@supports") {
} else if (name === "@media" || name === "@supports") {
// TODO handle nested CSS syntax
let pos = end;
const [newPos] = eatText(input, pos, eatAtRuleNested);
pos = newPos;
if (pos === input.length) return pos;
if (input.charCodeAt(pos) !== CC_LEFT_CURLY) {
throw new Error(
`Unexpected ${input[pos]} at ${pos} during parsing of @media or @supports (expected '{')`
this._emitWarning(
state,
`Unexpected ${input[pos]} at ${pos} during parsing of @media or @supports (expected '{')`,
locConverter,
start,
pos
);
return pos;
}
return pos + 1;
}
return end;
},
semicolon: (input, start, end) => {
switch (mode) {
case CSS_MODE_AT_IMPORT_EXPECT_URL:
throw new Error(`Expected URL for @import at ${start}`);
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 (modeData.url === undefined) {
throw new Error(
`Expected URL for @import at ${modeData.atRuleStart}`
this._emitWarning(
state,
`Expected URL for @import at ${modeData.atRuleStart}`,
locConverter,
modeData.atRuleStart,
modeData.lastPos
);
return end;
}
const semicolonPos = end;
const { line: sl, column: sc } = locConverter.get(
Expand Down Expand Up @@ -566,6 +627,7 @@ class CssParser extends Parser {
leftCurlyBracket: (input, start, end) => {
switch (mode) {
case CSS_MODE_TOP_LEVEL:
allowImportAtRule = false;
mode = isTopLevelLocal()
? CSS_MODE_IN_LOCAL_RULE
: CSS_MODE_IN_RULE;
Expand Down
40 changes: 40 additions & 0 deletions test/__snapshots__/ConfigCacheTestCases.longtest.js.snap
Original file line number Diff line number Diff line change
Expand Up @@ -632,6 +632,46 @@ head{--webpack-main:https\\\\:\\\\/\\\\/test\\\\.cases\\\\/path\\\\/\\\\.\\\\.\\
]
`;

exports[`ConfigCacheTestCases css pure-css exported tests should compile 1`] = `
Array [
".class {
color: red;
background: var(--color);
}

@keyframes test {
0% {
color: red;
}
100% {
color: blue;
}
}

:local(.class) {
color: red;
}

:local .class {
color: green;
}

:global(.class) {
color: blue;
}

:global .class {
color: white;
}

:export {
foo: bar;
}

head{--webpack-main:\\\\.\\\\/style\\\\.css;}",
]
`;

exports[`ConfigCacheTestCases css urls exported tests should be able to handle styles in div.css 1`] = `
Object {
"--foo": " url(img.09a1a1112c577c279435.png)",
Expand Down
40 changes: 40 additions & 0 deletions test/__snapshots__/ConfigTestCases.basictest.js.snap
Original file line number Diff line number Diff line change
Expand Up @@ -632,6 +632,46 @@ head{--webpack-main:https\\\\:\\\\/\\\\/test\\\\.cases\\\\/path\\\\/\\\\.\\\\.\\
]
`;

exports[`ConfigTestCases css pure-css exported tests should compile 1`] = `
Array [
".class {
color: red;
background: var(--color);
}

@keyframes test {
0% {
color: red;
}
100% {
color: blue;
}
}

:local(.class) {
color: red;
}

:local .class {
color: green;
}

:global(.class) {
color: blue;
}

:global .class {
color: white;
}

:export {
foo: bar;
}

head{--webpack-main:\\\\.\\\\/style\\\\.css;}",
]
`;

exports[`ConfigTestCases css urls exported tests should be able to handle styles in div.css 1`] = `
Object {
"--foo": " url(img.09a1a1112c577c279435.png)",
Expand Down
3 changes: 3 additions & 0 deletions test/configCases/css/css-import-at-middle/a.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
body {
background: red;
}