Skip to content

Commit

Permalink
Merge pull request #17118 from webpack/fix-do-not-parse-broken-import
Browse files Browse the repository at this point in the history
fix: any @import rules must precede all other rules
  • Loading branch information
TheLarkInn committed May 3, 2023
2 parents ddb9627 + 8f374c6 commit 0023a8f
Show file tree
Hide file tree
Showing 25 changed files with 427 additions and 59 deletions.
180 changes: 121 additions & 59 deletions lib/css/CssParser.js
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 @@ -247,10 +248,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 @@ -262,9 +269,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 @@ -280,9 +292,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 @@ -348,14 +365,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 @@ -367,6 +383,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: {
// Ignore `url()`, `url('')` and `url("")`, they are valid by spec
if (value.length === 0) {
Expand Down Expand Up @@ -437,14 +458,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 @@ -454,51 +489,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;
end = walkCssTokens.eatWhiteLine(input, end + 1);
Expand Down Expand Up @@ -547,6 +608,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
Expand Up @@ -1042,6 +1042,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
Expand Up @@ -1042,6 +1042,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
@@ -0,0 +1,3 @@
body {
background: red;
}

0 comments on commit 0023a8f

Please sign in to comment.