Skip to content

Commit 0023a8f

Browse files
authored
Merge pull request #17118 from webpack/fix-do-not-parse-broken-import
fix: any @import rules must precede all other rules
2 parents ddb9627 + 8f374c6 commit 0023a8f

25 files changed

Lines changed: 427 additions & 59 deletions

lib/css/CssParser.js

Lines changed: 121 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@
55

66
"use strict";
77

8+
const ModuleDependencyWarning = require("../ModuleDependencyWarning");
89
const Parser = require("../Parser");
10+
const WebpackError = require("../WebpackError");
911
const ConstDependency = require("../dependencies/ConstDependency");
1012
const CssExportDependency = require("../dependencies/CssExportDependency");
1113
const CssImportDependency = require("../dependencies/CssImportDependency");
@@ -122,30 +124,8 @@ const CSS_MODE_IN_RULE = 1;
122124
const CSS_MODE_IN_LOCAL_RULE = 2;
123125
const CSS_MODE_AT_IMPORT_EXPECT_URL = 3;
124126
const CSS_MODE_AT_IMPORT_EXPECT_LAYER_OR_SUPPORTS_OR_MEDIA = 4;
125-
const CSS_MODE_AT_OTHER = 5;
126-
127-
/**
128-
* @param {number} mode current mode
129-
* @returns {string} description of mode
130-
*/
131-
const explainMode = mode => {
132-
switch (mode) {
133-
case CSS_MODE_TOP_LEVEL:
134-
return "parsing top level css";
135-
case CSS_MODE_IN_RULE:
136-
return "parsing css rule content (global)";
137-
case CSS_MODE_IN_LOCAL_RULE:
138-
return "parsing css rule content (local)";
139-
case CSS_MODE_AT_IMPORT_EXPECT_URL:
140-
return "parsing @import (expecting url)";
141-
case CSS_MODE_AT_IMPORT_EXPECT_LAYER_OR_SUPPORTS_OR_MEDIA:
142-
return "parsing @import (expecting optionally layer, supports or media query)";
143-
case CSS_MODE_AT_OTHER:
144-
return "parsing at-rule";
145-
default:
146-
return "parsing css";
147-
}
148-
};
127+
const CSS_MODE_AT_IMPORT_INVALID = 5;
128+
const CSS_MODE_AT_NAMESPACE_INVALID = 6;
149129

150130
class CssParser extends Parser {
151131
constructor({
@@ -159,6 +139,25 @@ class CssParser extends Parser {
159139
this.defaultMode = defaultMode;
160140
}
161141

142+
/**
143+
* @param {ParserState} state parser state
144+
* @param {string} message warning message
145+
* @param {LocConverter} locConverter location converter
146+
* @param {number} start start offset
147+
* @param {number} end end offset
148+
*/
149+
_emitWarning(state, message, locConverter, start, end) {
150+
const { line: sl, column: sc } = locConverter.get(start);
151+
const { line: el, column: ec } = locConverter.get(end);
152+
153+
state.current.addWarning(
154+
new ModuleDependencyWarning(state.module, new WebpackError(message), {
155+
start: { line: sl, column: sc },
156+
end: { line: el, column: ec }
157+
})
158+
);
159+
}
160+
162161
/**
163162
* @param {string | Buffer | PreparsedAst} source the source to parse
164163
* @param {ParserState} state the parser state
@@ -183,6 +182,8 @@ class CssParser extends Parser {
183182
let mode = CSS_MODE_TOP_LEVEL;
184183
/** @type {number} */
185184
let modeNestingLevel = 0;
185+
/** @type {boolean} */
186+
let allowImportAtRule = true;
186187
let modeData = undefined;
187188
/** @type {string | boolean | undefined} */
188189
let singleClassSelector = undefined;
@@ -247,10 +248,16 @@ class CssParser extends Parser {
247248
const parseExports = (input, pos) => {
248249
pos = walkCssTokens.eatWhitespaceAndComments(input, pos);
249250
const cc = input.charCodeAt(pos);
250-
if (cc !== CC_LEFT_CURLY)
251-
throw new Error(
252-
`Unexpected ${input[pos]} at ${pos} during parsing of ':export' (expected '{')`
251+
if (cc !== CC_LEFT_CURLY) {
252+
this._emitWarning(
253+
state,
254+
`Unexpected '${input[pos]}' at ${pos} during parsing of ':export' (expected '{')`,
255+
locConverter,
256+
pos,
257+
pos
253258
);
259+
return pos;
260+
}
254261
pos++;
255262
pos = walkCssTokens.eatWhitespaceAndComments(input, pos);
256263
for (;;) {
@@ -262,9 +269,14 @@ class CssParser extends Parser {
262269
[pos, name] = eatText(input, pos, eatExportName);
263270
if (pos === input.length) return pos;
264271
if (input.charCodeAt(pos) !== CC_COLON) {
265-
throw new Error(
266-
`Unexpected ${input[pos]} at ${pos} during parsing of export name in ':export' (expected ':')`
272+
this._emitWarning(
273+
state,
274+
`Unexpected '${input[pos]}' at ${pos} during parsing of export name in ':export' (expected ':')`,
275+
locConverter,
276+
start,
277+
pos
267278
);
279+
return pos;
268280
}
269281
pos++;
270282
if (pos === input.length) return pos;
@@ -280,9 +292,14 @@ class CssParser extends Parser {
280292
pos = walkCssTokens.eatWhitespaceAndComments(input, pos);
281293
if (pos === input.length) return pos;
282294
} else if (cc !== CC_RIGHT_CURLY) {
283-
throw new Error(
284-
`Unexpected ${input[pos]} at ${pos} during parsing of export value in ':export' (expected ';' or '}')`
295+
this._emitWarning(
296+
state,
297+
`Unexpected '${input[pos]}' at ${pos} during parsing of export value in ':export' (expected ';' or '}')`,
298+
locConverter,
299+
start,
300+
pos
285301
);
302+
return pos;
286303
}
287304
const dep = new CssExportDependency(name, value);
288305
const { line: sl, column: sc } = locConverter.get(start);
@@ -348,14 +365,13 @@ class CssParser extends Parser {
348365
mode !== CSS_MODE_IN_RULE &&
349366
mode !== CSS_MODE_IN_LOCAL_RULE &&
350367
mode !== CSS_MODE_AT_IMPORT_EXPECT_URL &&
351-
mode !== CSS_MODE_AT_IMPORT_EXPECT_LAYER_OR_SUPPORTS_OR_MEDIA
368+
mode !== CSS_MODE_AT_IMPORT_EXPECT_LAYER_OR_SUPPORTS_OR_MEDIA &&
369+
mode !== CSS_MODE_AT_IMPORT_INVALID &&
370+
mode !== CSS_MODE_AT_NAMESPACE_INVALID
352371
);
353372
},
354-
url: (input, start, end, contentStart, contentEnd, isString) => {
355-
let value = normalizeUrl(
356-
input.slice(contentStart, contentEnd),
357-
isString
358-
);
373+
url: (input, start, end, contentStart, contentEnd) => {
374+
let value = normalizeUrl(input.slice(contentStart, contentEnd), false);
359375
switch (mode) {
360376
case CSS_MODE_AT_IMPORT_EXPECT_URL: {
361377
modeData.url = value;
@@ -367,6 +383,11 @@ class CssParser extends Parser {
367383
case CSS_MODE_AT_IMPORT_EXPECT_LAYER_OR_SUPPORTS_OR_MEDIA: {
368384
break;
369385
}
386+
// Do not parse URLs in import between rules
387+
case CSS_MODE_AT_NAMESPACE_INVALID:
388+
case CSS_MODE_AT_IMPORT_INVALID: {
389+
break;
390+
}
370391
default: {
371392
// Ignore `url()`, `url('')` and `url("")`, they are valid by spec
372393
if (value.length === 0) {
@@ -437,14 +458,28 @@ class CssParser extends Parser {
437458
atKeyword: (input, start, end) => {
438459
const name = input.slice(start, end).toLowerCase();
439460
if (name === "@namespace") {
440-
throw new Error("@namespace is not supported in bundled CSS");
441-
}
442-
if (name === "@import") {
443-
if (mode !== CSS_MODE_TOP_LEVEL) {
444-
throw new Error(
445-
`Unexpected @import at ${start} during ${explainMode(mode)}`
461+
mode = CSS_MODE_AT_NAMESPACE_INVALID;
462+
this._emitWarning(
463+
state,
464+
"@namespace is not supported in bundled CSS",
465+
locConverter,
466+
start,
467+
end
468+
);
469+
return end;
470+
} else if (name === "@import") {
471+
if (!allowImportAtRule) {
472+
mode = CSS_MODE_AT_IMPORT_INVALID;
473+
this._emitWarning(
474+
state,
475+
"Any @import rules must precede all other rules",
476+
locConverter,
477+
start,
478+
end
446479
);
480+
return end;
447481
}
482+
448483
mode = CSS_MODE_AT_IMPORT_EXPECT_URL;
449484
modeData = {
450485
atRuleStart: start,
@@ -454,51 +489,77 @@ class CssParser extends Parser {
454489
supports: undefined,
455490
media: undefined
456491
};
457-
}
458-
if (OPTIONALLY_VENDOR_PREFIXED_KEYFRAMES_AT_RULE.test(name)) {
492+
} else if (
493+
isTopLevelLocal() &&
494+
OPTIONALLY_VENDOR_PREFIXED_KEYFRAMES_AT_RULE.test(name)
495+
) {
459496
let pos = end;
460497
pos = walkCssTokens.eatWhitespaceAndComments(input, pos);
461498
if (pos === input.length) return pos;
462499
const [newPos, name] = eatText(input, pos, eatKeyframes);
500+
if (newPos === input.length) return newPos;
501+
if (input.charCodeAt(newPos) !== CC_LEFT_CURLY) {
502+
this._emitWarning(
503+
state,
504+
`Unexpected '${input[newPos]}' at ${newPos} during parsing of @keyframes (expected '{')`,
505+
locConverter,
506+
start,
507+
end
508+
);
509+
510+
return newPos;
511+
}
463512
const { line: sl, column: sc } = locConverter.get(pos);
464513
const { line: el, column: ec } = locConverter.get(newPos);
465514
const dep = new CssLocalIdentifierDependency(name, [pos, newPos]);
466515
dep.setLoc(sl, sc, el, ec);
467516
module.addDependency(dep);
468517
pos = newPos;
469-
if (pos === input.length) return pos;
470-
if (input.charCodeAt(pos) !== CC_LEFT_CURLY) {
471-
throw new Error(
472-
`Unexpected ${input[pos]} at ${pos} during parsing of @keyframes (expected '{')`
473-
);
474-
}
475518
mode = CSS_MODE_IN_LOCAL_RULE;
476519
modeNestingLevel = 1;
477520
return pos + 1;
478-
}
479-
if (name === "@media" || name === "@supports") {
521+
} else if (name === "@media" || name === "@supports") {
522+
// TODO handle nested CSS syntax
480523
let pos = end;
481524
const [newPos] = eatText(input, pos, eatAtRuleNested);
482525
pos = newPos;
483526
if (pos === input.length) return pos;
484527
if (input.charCodeAt(pos) !== CC_LEFT_CURLY) {
485-
throw new Error(
486-
`Unexpected ${input[pos]} at ${pos} during parsing of @media or @supports (expected '{')`
528+
this._emitWarning(
529+
state,
530+
`Unexpected ${input[pos]} at ${pos} during parsing of @media or @supports (expected '{')`,
531+
locConverter,
532+
start,
533+
pos
487534
);
535+
return pos;
488536
}
489537
return pos + 1;
490538
}
491539
return end;
492540
},
493541
semicolon: (input, start, end) => {
494542
switch (mode) {
495-
case CSS_MODE_AT_IMPORT_EXPECT_URL:
496-
throw new Error(`Expected URL for @import at ${start}`);
543+
case CSS_MODE_AT_IMPORT_EXPECT_URL: {
544+
this._emitWarning(
545+
state,
546+
`Expected URL for @import at ${start}`,
547+
locConverter,
548+
start,
549+
end
550+
);
551+
return end;
552+
}
497553
case CSS_MODE_AT_IMPORT_EXPECT_LAYER_OR_SUPPORTS_OR_MEDIA: {
498554
if (modeData.url === undefined) {
499-
throw new Error(
500-
`Expected URL for @import at ${modeData.atRuleStart}`
555+
this._emitWarning(
556+
state,
557+
`Expected URL for @import at ${modeData.atRuleStart}`,
558+
locConverter,
559+
modeData.atRuleStart,
560+
modeData.lastPos
501561
);
562+
return end;
502563
}
503564
const semicolonPos = end;
504565
end = walkCssTokens.eatWhiteLine(input, end + 1);
@@ -547,6 +608,7 @@ class CssParser extends Parser {
547608
leftCurlyBracket: (input, start, end) => {
548609
switch (mode) {
549610
case CSS_MODE_TOP_LEVEL:
611+
allowImportAtRule = false;
550612
mode = isTopLevelLocal()
551613
? CSS_MODE_IN_LOCAL_RULE
552614
: CSS_MODE_IN_RULE;

test/__snapshots__/ConfigCacheTestCases.longtest.js.snap

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1042,6 +1042,46 @@ head{--webpack-main:https\\\\:\\\\/\\\\/test\\\\.cases\\\\/path\\\\/\\\\.\\\\.\\
10421042
]
10431043
`;
10441044

1045+
exports[`ConfigCacheTestCases css pure-css exported tests should compile 1`] = `
1046+
Array [
1047+
".class {
1048+
color: red;
1049+
background: var(--color);
1050+
}
1051+
1052+
@keyframes test {
1053+
0% {
1054+
color: red;
1055+
}
1056+
100% {
1057+
color: blue;
1058+
}
1059+
}
1060+
1061+
:local(.class) {
1062+
color: red;
1063+
}
1064+
1065+
:local .class {
1066+
color: green;
1067+
}
1068+
1069+
:global(.class) {
1070+
color: blue;
1071+
}
1072+
1073+
:global .class {
1074+
color: white;
1075+
}
1076+
1077+
:export {
1078+
foo: bar;
1079+
}
1080+
1081+
head{--webpack-main:\\\\.\\\\/style\\\\.css;}",
1082+
]
1083+
`;
1084+
10451085
exports[`ConfigCacheTestCases css urls exported tests should be able to handle styles in div.css 1`] = `
10461086
Object {
10471087
"--foo": " url(img.09a1a1112c577c279435.png)",

test/__snapshots__/ConfigTestCases.basictest.js.snap

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1042,6 +1042,46 @@ head{--webpack-main:https\\\\:\\\\/\\\\/test\\\\.cases\\\\/path\\\\/\\\\.\\\\.\\
10421042
]
10431043
`;
10441044

1045+
exports[`ConfigTestCases css pure-css exported tests should compile 1`] = `
1046+
Array [
1047+
".class {
1048+
color: red;
1049+
background: var(--color);
1050+
}
1051+
1052+
@keyframes test {
1053+
0% {
1054+
color: red;
1055+
}
1056+
100% {
1057+
color: blue;
1058+
}
1059+
}
1060+
1061+
:local(.class) {
1062+
color: red;
1063+
}
1064+
1065+
:local .class {
1066+
color: green;
1067+
}
1068+
1069+
:global(.class) {
1070+
color: blue;
1071+
}
1072+
1073+
:global .class {
1074+
color: white;
1075+
}
1076+
1077+
:export {
1078+
foo: bar;
1079+
}
1080+
1081+
head{--webpack-main:\\\\.\\\\/style\\\\.css;}",
1082+
]
1083+
`;
1084+
10451085
exports[`ConfigTestCases css urls exported tests should be able to handle styles in div.css 1`] = `
10461086
Object {
10471087
"--foo": " url(img.09a1a1112c577c279435.png)",
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
body {
2+
background: red;
3+
}

0 commit comments

Comments
 (0)