diff --git a/lib/css/CssParser.js b/lib/css/CssParser.js index 3734c13db9b..e89aacd870d 100644 --- a/lib/css/CssParser.js +++ b/lib/css/CssParser.js @@ -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"); @@ -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({ @@ -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 @@ -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; @@ -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 (;;) { @@ -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; @@ -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); @@ -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; @@ -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) { @@ -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, @@ -454,37 +489,50 @@ 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; } @@ -492,13 +540,26 @@ class CssParser extends Parser { }, 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); @@ -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; diff --git a/test/__snapshots__/ConfigCacheTestCases.longtest.js.snap b/test/__snapshots__/ConfigCacheTestCases.longtest.js.snap index 3837d0665bf..9092cbc3b61 100644 --- a/test/__snapshots__/ConfigCacheTestCases.longtest.js.snap +++ b/test/__snapshots__/ConfigCacheTestCases.longtest.js.snap @@ -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)", diff --git a/test/__snapshots__/ConfigTestCases.basictest.js.snap b/test/__snapshots__/ConfigTestCases.basictest.js.snap index 312df7be0bb..a36de1cafdc 100644 --- a/test/__snapshots__/ConfigTestCases.basictest.js.snap +++ b/test/__snapshots__/ConfigTestCases.basictest.js.snap @@ -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)", diff --git a/test/configCases/css/css-import-at-middle/a.css b/test/configCases/css/css-import-at-middle/a.css new file mode 100644 index 00000000000..f0d5b13bffd --- /dev/null +++ b/test/configCases/css/css-import-at-middle/a.css @@ -0,0 +1,3 @@ +body { + background: red; +} diff --git a/test/configCases/css/css-import-at-middle/b.css b/test/configCases/css/css-import-at-middle/b.css new file mode 100644 index 00000000000..575be7ba729 --- /dev/null +++ b/test/configCases/css/css-import-at-middle/b.css @@ -0,0 +1,10 @@ +body { + background: blue; +} + +@import "./a.css"; +@import url(./a.css); + +body { + color: green; +} diff --git a/test/configCases/css/css-import-at-middle/c.css b/test/configCases/css/css-import-at-middle/c.css new file mode 100644 index 00000000000..8fc0fb15442 --- /dev/null +++ b/test/configCases/css/css-import-at-middle/c.css @@ -0,0 +1,9 @@ +body { + background: red; +} +@import "./a.css"; +@import "./b.css"; + +body { + color: yellow; +} diff --git a/test/configCases/css/css-import-at-middle/index.js b/test/configCases/css/css-import-at-middle/index.js new file mode 100644 index 00000000000..9b54f968864 --- /dev/null +++ b/test/configCases/css/css-import-at-middle/index.js @@ -0,0 +1,9 @@ +import "./style.css"; + +it("should compile with warnings", done => { + const style = getComputedStyle(document.body); + expect(style.getPropertyValue("background")).toBe(" blue"); + expect(style.getPropertyValue("color")).toBe(" green"); + + done(); +}); diff --git a/test/configCases/css/css-import-at-middle/style.css b/test/configCases/css/css-import-at-middle/style.css new file mode 100644 index 00000000000..1d835e13228 --- /dev/null +++ b/test/configCases/css/css-import-at-middle/style.css @@ -0,0 +1,2 @@ +@import "./c.css"; +@import "./b.css"; diff --git a/test/configCases/css/css-import-at-middle/test.config.js b/test/configCases/css/css-import-at-middle/test.config.js new file mode 100644 index 00000000000..0590757288f --- /dev/null +++ b/test/configCases/css/css-import-at-middle/test.config.js @@ -0,0 +1,8 @@ +module.exports = { + moduleScope(scope) { + const link = scope.window.document.createElement("link"); + link.rel = "stylesheet"; + link.href = "bundle0.css"; + scope.window.document.head.appendChild(link); + } +}; diff --git a/test/configCases/css/css-import-at-middle/warnings.js b/test/configCases/css/css-import-at-middle/warnings.js new file mode 100644 index 00000000000..1182f7d904a --- /dev/null +++ b/test/configCases/css/css-import-at-middle/warnings.js @@ -0,0 +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/ +]; diff --git a/test/configCases/css/css-import-at-middle/webpack.config.js b/test/configCases/css/css-import-at-middle/webpack.config.js new file mode 100644 index 00000000000..cfb8e5c0346 --- /dev/null +++ b/test/configCases/css/css-import-at-middle/webpack.config.js @@ -0,0 +1,8 @@ +/** @type {import("../../../../").Configuration} */ +module.exports = { + target: "web", + mode: "development", + experiments: { + css: true + } +}; diff --git a/test/configCases/css/css-modules-broken-keyframes/index.js b/test/configCases/css/css-modules-broken-keyframes/index.js new file mode 100644 index 00000000000..c75a12a839d --- /dev/null +++ b/test/configCases/css/css-modules-broken-keyframes/index.js @@ -0,0 +1,17 @@ +const prod = process.env.NODE_ENV === "production"; + +it("should allow to create css modules", done => { + prod + ? __non_webpack_require__("./249.bundle0.js") + : __non_webpack_require__("./use-style_js.bundle0.js"); + import("./use-style.js").then(({ default: x }) => { + try { + expect(x).toEqual({ + class: prod ? "my-app-491-S" : "./style.module.css-class", + }); + } catch (e) { + return done(e); + } + done(); + }, done); +}); diff --git a/test/configCases/css/css-modules-broken-keyframes/style.module.css b/test/configCases/css/css-modules-broken-keyframes/style.module.css new file mode 100644 index 00000000000..9b20545cc15 --- /dev/null +++ b/test/configCases/css/css-modules-broken-keyframes/style.module.css @@ -0,0 +1,15 @@ +@keyframes broken; + +.class { + color: red; +} + +@keyframes/*test*/animationName/*test*/{ + 0% { + background: white; + } + 100% { + background: red; + } +} + diff --git a/test/configCases/css/css-modules-broken-keyframes/use-style.js b/test/configCases/css/css-modules-broken-keyframes/use-style.js new file mode 100644 index 00000000000..c2929a40c9c --- /dev/null +++ b/test/configCases/css/css-modules-broken-keyframes/use-style.js @@ -0,0 +1,5 @@ +import * as style from "./style.module.css"; + +export default { + class: style.class, +}; diff --git a/test/configCases/css/css-modules-broken-keyframes/warnings.js b/test/configCases/css/css-modules-broken-keyframes/warnings.js new file mode 100644 index 00000000000..5a2ded6dbc9 --- /dev/null +++ b/test/configCases/css/css-modules-broken-keyframes/warnings.js @@ -0,0 +1,3 @@ +module.exports = [ + /Unexpected ';' at 17 during parsing of @keyframes \(expected '{'\)/ +]; diff --git a/test/configCases/css/css-modules-broken-keyframes/webpack.config.js b/test/configCases/css/css-modules-broken-keyframes/webpack.config.js new file mode 100644 index 00000000000..b952b563cb6 --- /dev/null +++ b/test/configCases/css/css-modules-broken-keyframes/webpack.config.js @@ -0,0 +1,27 @@ +const webpack = require("../../../../"); +const path = require("path"); + +/** @type {function(any, any): import("../../../../").Configuration} */ +module.exports = (env, { testPath }) => ({ + target: "web", + mode: "production", + output: { + uniqueName: "my-app" + }, + experiments: { + css: true + }, + plugins: [ + new webpack.ids.DeterministicModuleIdsPlugin({ + maxLength: 3, + failOnConflict: true, + fixedLength: true, + test: m => m.type.startsWith("css") + }), + new webpack.experiments.ids.SyncModuleIdsPlugin({ + test: m => m.type.startsWith("css"), + path: path.resolve(testPath, "module-ids.json"), + mode: "create" + }) + ] +}); diff --git a/test/configCases/css/namespace/index.js b/test/configCases/css/namespace/index.js new file mode 100644 index 00000000000..78be77a3a32 --- /dev/null +++ b/test/configCases/css/namespace/index.js @@ -0,0 +1,7 @@ +import "./style.css"; + +it("should compile with warning", done => { + const style = getComputedStyle(document.body); + expect(style.getPropertyValue("background")).toBe(" red"); + done(); +}); diff --git a/test/configCases/css/namespace/style.css b/test/configCases/css/namespace/style.css new file mode 100644 index 00000000000..e16ce897e5d --- /dev/null +++ b/test/configCases/css/namespace/style.css @@ -0,0 +1,5 @@ +@namespace svg url('http://www.w3.org/2000/svg'); + +body { + background: red; +} diff --git a/test/configCases/css/namespace/test.config.js b/test/configCases/css/namespace/test.config.js new file mode 100644 index 00000000000..0590757288f --- /dev/null +++ b/test/configCases/css/namespace/test.config.js @@ -0,0 +1,8 @@ +module.exports = { + moduleScope(scope) { + const link = scope.window.document.createElement("link"); + link.rel = "stylesheet"; + link.href = "bundle0.css"; + scope.window.document.head.appendChild(link); + } +}; diff --git a/test/configCases/css/namespace/warnings.js b/test/configCases/css/namespace/warnings.js new file mode 100644 index 00000000000..6befa7e5571 --- /dev/null +++ b/test/configCases/css/namespace/warnings.js @@ -0,0 +1 @@ +module.exports = [/@namespace is not supported in bundled CSS/]; diff --git a/test/configCases/css/namespace/webpack.config.js b/test/configCases/css/namespace/webpack.config.js new file mode 100644 index 00000000000..cfb8e5c0346 --- /dev/null +++ b/test/configCases/css/namespace/webpack.config.js @@ -0,0 +1,8 @@ +/** @type {import("../../../../").Configuration} */ +module.exports = { + target: "web", + mode: "development", + experiments: { + css: true + } +}; diff --git a/test/configCases/css/pure-css/index.js b/test/configCases/css/pure-css/index.js new file mode 100644 index 00000000000..3b26850a1e7 --- /dev/null +++ b/test/configCases/css/pure-css/index.js @@ -0,0 +1,14 @@ +import "./style.css"; + +it("should compile", done => { + const links = document.getElementsByTagName("link"); + const css = []; + + // Skip first because import it by default + for (const link of links.slice(1)) { + css.push(link.sheet.css); + } + + expect(css).toMatchSnapshot(); + done(); +}); diff --git a/test/configCases/css/pure-css/style.css b/test/configCases/css/pure-css/style.css new file mode 100644 index 00000000000..0fdcb919bf4 --- /dev/null +++ b/test/configCases/css/pure-css/style.css @@ -0,0 +1,33 @@ +.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; +} diff --git a/test/configCases/css/pure-css/test.config.js b/test/configCases/css/pure-css/test.config.js new file mode 100644 index 00000000000..0590757288f --- /dev/null +++ b/test/configCases/css/pure-css/test.config.js @@ -0,0 +1,8 @@ +module.exports = { + moduleScope(scope) { + const link = scope.window.document.createElement("link"); + link.rel = "stylesheet"; + link.href = "bundle0.css"; + scope.window.document.head.appendChild(link); + } +}; diff --git a/test/configCases/css/pure-css/webpack.config.js b/test/configCases/css/pure-css/webpack.config.js new file mode 100644 index 00000000000..f3d73b2784e --- /dev/null +++ b/test/configCases/css/pure-css/webpack.config.js @@ -0,0 +1,20 @@ +/** @type {import("../../../../").Configuration} */ +module.exports = { + target: "web", + mode: "development", + module: { + rules: [ + { + test: /\.css$/i, + type: "css/global", + resolve: { + fullySpecified: true, + preferRelative: true + } + } + ] + }, + experiments: { + css: true + } +};