diff --git a/lib/css/CssLoadingRuntimeModule.js b/lib/css/CssLoadingRuntimeModule.js index 1ce053d9f..a06d329d1 100644 --- a/lib/css/CssLoadingRuntimeModule.js +++ b/lib/css/CssLoadingRuntimeModule.js @@ -453,6 +453,9 @@ class CssLoadingRuntimeModule extends RuntimeModule { }.css = ${runtimeTemplate.basicFunction( "chunkIds, removedChunks, removedModules, promises, applyHandlers, updatedModulesList", [ + isNeutralPlatform + ? "if (typeof document === 'undefined') return;" + : "", "applyHandlers.push(applyHandler);", `chunkIds.forEach(${runtimeTemplate.basicFunction("chunkId", [ `var filename = ${RuntimeGlobals.getChunkCssFilename}(chunkId);`, diff --git a/lib/css/CssModulesPlugin.js b/lib/css/CssModulesPlugin.js index 42982e066..fcf451926 100644 --- a/lib/css/CssModulesPlugin.js +++ b/lib/css/CssModulesPlugin.js @@ -378,14 +378,14 @@ class CssModulesPlugin { const hmrCode = Template.asString([ "", - `var exports = ${stringifiedExports};`, + `var __webpack_css_exports__ = ${stringifiedExports};`, "// only invalidate when locals change", - "if (module.hot.data && module.hot.data.exports && module.hot.data.exports != exports) {", + "if (module.hot.data && module.hot.data.__webpack_css_exports__ && module.hot.data.__webpack_css_exports__ != __webpack_css_exports__) {", Template.indent("module.hot.invalidate();"), "} else {", Template.indent("module.hot.accept();"), "}", - "module.hot.dispose(function(data) { data.exports = exports; });" + "module.hot.dispose(function(data) { data.__webpack_css_exports__ = __webpack_css_exports__; });" ]); return new ConcatSource(source, "\n", new RawSource(hmrCode)); diff --git a/test/HotTestCases.template.js b/test/HotTestCases.template.js index 1de7bbee7..5ebad6f68 100644 --- a/test/HotTestCases.template.js +++ b/test/HotTestCases.template.js @@ -97,6 +97,17 @@ const describeCases = config => { new webpack.LoaderOptionsPlugin(fakeUpdateLoaderOptions) ); if (!options.recordsPath) options.recordsPath = recordsPath; + let testConfig = {}; + try { + // try to load a test file + testConfig = Object.assign( + testConfig, + require(path.join(testDirectory, "test.config.js")) + ); + } catch (_err) { + // ignored + } + compiler = webpack(options); compiler.run((err, stats) => { if (err) return done(err); @@ -139,6 +150,7 @@ const describeCases = config => { return `./${url}`; }; const window = { + _elements: [], fetch: async url => { try { const buffer = await new Promise((resolve, reject) => { @@ -169,30 +181,67 @@ const describeCases = config => { createElement(type) { return { _type: type, - _attrs: {}, + sheet: {}, + getAttribute(name) { + return this[name]; + }, setAttribute(name, value) { - this._attrs[name] = value; + this[name] = value; + }, + removeAttribute(name) { + delete this[name]; }, parentNode: { removeChild(node) { - // ok + window._elements = window._elements.filter( + item => item !== node + ); } } }; }, head: { appendChild(element) { + window._elements.push(element); + if (element._type === "script") { // run it Promise.resolve().then(() => { _require(urlToRelativePath(element.src)); }); + } else if (element._type === "link") { + Promise.resolve().then(() => { + if (element.onload) { + // run it + element.onload({ type: "load" }); + } + }); + } + }, + insertBefore(element, before) { + window._elements.push(element); + + if (element._type === "script") { + // run it + Promise.resolve().then(() => { + _require(urlToRelativePath(element.src)); + }); + } else if (element._type === "link") { + // run it + Promise.resolve().then(() => { + element.onload({ type: "load" }); + }); } } }, getElementsByTagName(name) { if (name === "head") return [this.head]; - if (name === "script") return []; + if (name === "script" || name === "link") { + return window._elements.filter( + item => item._type === name + ); + } + throw new Error("Not supported"); } }, @@ -209,6 +258,14 @@ const describeCases = config => { } }; + const moduleScope = { + window + }; + + if (testConfig.moduleScope) { + testConfig.moduleScope(moduleScope, options); + } + function _next(callback) { fakeUpdateLoaderOptions.updateIndex++; compiler.run((err, stats) => { @@ -249,6 +306,9 @@ const describeCases = config => { function _require(module) { if (module.startsWith("./")) { const p = path.join(outputDirectory, module); + if (module.endsWith(".css")) { + return fs.readFileSync(p, "utf-8"); + } if (module.endsWith(".json")) { return JSON.parse(fs.readFileSync(p, "utf-8")); } diff --git a/test/hotCases/css/css-modules/index.js b/test/hotCases/css/css-modules/index.js new file mode 100644 index 000000000..04419adbc --- /dev/null +++ b/test/hotCases/css/css-modules/index.js @@ -0,0 +1,28 @@ +import * as styles from "./style.module.css"; + +it("should work", async function (done) { + expect(styles).toMatchObject({ class: "_style_module_css-class" }); + + const styles2 = await import("./style2.module.css"); + + expect(styles2).toMatchObject({ + foo: "_style2_module_css-foo" + }); + + module.hot.accept(["./style.module.css", "./style2.module.css"], () => { + expect(styles).toMatchObject({ + "class-other": "_style_module_css-class-other" + }); + import("./style2.module.css").then(styles2 => { + expect(styles2).toMatchObject({ + "bar": "_style2_module_css-bar" + }); + + done(); + }); + }); + + NEXT(require("../../update")(done)); +}); + +module.hot.accept(); diff --git a/test/hotCases/css/css-modules/style.module.css b/test/hotCases/css/css-modules/style.module.css new file mode 100644 index 000000000..98c6b2bb5 --- /dev/null +++ b/test/hotCases/css/css-modules/style.module.css @@ -0,0 +1,7 @@ +.class { + color: red; +} +--- +.class-other { + color: blue; +} diff --git a/test/hotCases/css/css-modules/style2.module.css b/test/hotCases/css/css-modules/style2.module.css new file mode 100644 index 000000000..681b83a26 --- /dev/null +++ b/test/hotCases/css/css-modules/style2.module.css @@ -0,0 +1,7 @@ +.foo { + color: red; +} +--- +.bar { + color: blue; +} diff --git a/test/hotCases/css/css-modules/test.config.js b/test/hotCases/css/css-modules/test.config.js new file mode 100644 index 000000000..429d75767 --- /dev/null +++ b/test/hotCases/css/css-modules/test.config.js @@ -0,0 +1,8 @@ +module.exports = { + moduleScope(scope) { + const link = scope.window.document.createElement("link"); + link.rel = "stylesheet"; + link.href = "https://test.cases/path/bundle.css"; + scope.window.document.head.appendChild(link); + } +}; diff --git a/test/hotCases/css/css-modules/webpack.config.js b/test/hotCases/css/css-modules/webpack.config.js new file mode 100644 index 000000000..14df4b565 --- /dev/null +++ b/test/hotCases/css/css-modules/webpack.config.js @@ -0,0 +1,8 @@ +/** @type {import("../../../../").Configuration} */ +module.exports = { + mode: "development", + devtool: false, + experiments: { + css: true + } +}; diff --git a/test/hotCases/css/fetch-priority/index.js b/test/hotCases/css/fetch-priority/index.js new file mode 100644 index 000000000..a5701351c --- /dev/null +++ b/test/hotCases/css/fetch-priority/index.js @@ -0,0 +1,25 @@ +it("should work", async function (done) { + const styles = await import(/* webpackFetchPriority: "high" */ "./style.module.css"); + + expect(styles).toMatchObject({ + class: "_style_module_css-class" + }); + + module.hot.accept("./style.module.css", () => { + import("./style.module.css").then(styles => { + expect(styles).toMatchObject({ + "class-other": "_style_module_css-class-other" + }); + + expect( + window.document.getElementsByTagName('link')[0].getAttribute('fetchpriority') + ).toBe('high') + + done(); + }); + }); + + NEXT(require("../../update")(done)); +}); + +module.hot.accept(); diff --git a/test/hotCases/css/fetch-priority/style.module.css b/test/hotCases/css/fetch-priority/style.module.css new file mode 100644 index 000000000..98c6b2bb5 --- /dev/null +++ b/test/hotCases/css/fetch-priority/style.module.css @@ -0,0 +1,7 @@ +.class { + color: red; +} +--- +.class-other { + color: blue; +} diff --git a/test/hotCases/css/fetch-priority/webpack.config.js b/test/hotCases/css/fetch-priority/webpack.config.js new file mode 100644 index 000000000..14df4b565 --- /dev/null +++ b/test/hotCases/css/fetch-priority/webpack.config.js @@ -0,0 +1,8 @@ +/** @type {import("../../../../").Configuration} */ +module.exports = { + mode: "development", + devtool: false, + experiments: { + css: true + } +}; diff --git a/test/hotCases/css/vanilla/index.js b/test/hotCases/css/vanilla/index.js new file mode 100644 index 000000000..b052608a5 --- /dev/null +++ b/test/hotCases/css/vanilla/index.js @@ -0,0 +1,28 @@ +import "./style.css"; + +const getFile = name => + __non_webpack_require__("fs").readFileSync( + __non_webpack_require__("path").join(__dirname, name), + "utf-8" + ); + +it("should work", async function (done) { + const style = getFile("bundle.css"); + expect(style).toContain("color: red;"); + + await import("./style2.css"); + + const style2 = getFile("style2_css.css"); + expect(style2).toContain("color: red;"); + + NEXT(require("../../update")(done, true, () => { + const style = getFile("bundle.css"); + expect(style).toContain("color: blue;"); + const style2 = getFile("style2_css.css"); + expect(style2).toContain("color: blue;"); + + done(); + })); +}); + +module.hot.accept(); diff --git a/test/hotCases/css/vanilla/style.css b/test/hotCases/css/vanilla/style.css new file mode 100644 index 000000000..98c6b2bb5 --- /dev/null +++ b/test/hotCases/css/vanilla/style.css @@ -0,0 +1,7 @@ +.class { + color: red; +} +--- +.class-other { + color: blue; +} diff --git a/test/hotCases/css/vanilla/style2.css b/test/hotCases/css/vanilla/style2.css new file mode 100644 index 000000000..681b83a26 --- /dev/null +++ b/test/hotCases/css/vanilla/style2.css @@ -0,0 +1,7 @@ +.foo { + color: red; +} +--- +.bar { + color: blue; +} diff --git a/test/hotCases/css/vanilla/test.config.js b/test/hotCases/css/vanilla/test.config.js new file mode 100644 index 000000000..429d75767 --- /dev/null +++ b/test/hotCases/css/vanilla/test.config.js @@ -0,0 +1,8 @@ +module.exports = { + moduleScope(scope) { + const link = scope.window.document.createElement("link"); + link.rel = "stylesheet"; + link.href = "https://test.cases/path/bundle.css"; + scope.window.document.head.appendChild(link); + } +}; diff --git a/test/hotCases/css/vanilla/webpack.config.js b/test/hotCases/css/vanilla/webpack.config.js new file mode 100644 index 000000000..1629277c0 --- /dev/null +++ b/test/hotCases/css/vanilla/webpack.config.js @@ -0,0 +1,14 @@ +/** @type {import("../../../../").Configuration} */ +module.exports = { + mode: "development", + devtool: false, + output: { + cssChunkFilename: "[name].css" + }, + node: { + __dirname: false + }, + experiments: { + css: true + } +};