diff --git a/lib/ModuleInfoHeaderPlugin.js b/lib/ModuleInfoHeaderPlugin.js index f75dd42ef..994bfed88 100644 --- a/lib/ModuleInfoHeaderPlugin.js +++ b/lib/ModuleInfoHeaderPlugin.js @@ -8,8 +8,8 @@ const { ConcatSource, RawSource, CachedSource } = require("webpack-sources"); const { UsageState } = require("./ExportsInfo"); const Template = require("./Template"); -const JavascriptModulesPlugin = require("./javascript/JavascriptModulesPlugin"); const CssModulesPlugin = require("./css/CssModulesPlugin"); +const JavascriptModulesPlugin = require("./javascript/JavascriptModulesPlugin"); /** @typedef {import("webpack-sources").Source} Source */ /** @typedef {import("./Compiler")} Compiler */ @@ -164,8 +164,9 @@ class ModuleInfoHeaderPlugin { apply(compiler) { const { _verbose: verbose } = this; compiler.hooks.compilation.tap("ModuleInfoHeaderPlugin", compilation => { - const hooks = JavascriptModulesPlugin.getCompilationHooks(compilation); - hooks.renderModulePackage.tap( + const javascriptHooks = + JavascriptModulesPlugin.getCompilationHooks(compilation); + javascriptHooks.renderModulePackage.tap( "ModuleInfoHeaderPlugin", ( moduleSource, @@ -245,10 +246,13 @@ class ModuleInfoHeaderPlugin { return cachedSource; } ); - hooks.chunkHash.tap("ModuleInfoHeaderPlugin", (chunk, hash) => { - hash.update("ModuleInfoHeaderPlugin"); - hash.update("1"); - }); + javascriptHooks.chunkHash.tap( + "ModuleInfoHeaderPlugin", + (_chunk, hash) => { + hash.update("ModuleInfoHeaderPlugin"); + hash.update("1"); + } + ); const cssHooks = CssModulesPlugin.getCompilationHooks(compilation); cssHooks.renderModulePackage.tap( "ModuleInfoHeaderPlugin", @@ -287,13 +291,17 @@ class ModuleInfoHeaderPlugin { return cachedSource; } ); + cssHooks.chunkHash.tap("ModuleInfoHeaderPlugin", (_chunk, hash) => { + hash.update("ModuleInfoHeaderPlugin"); + hash.update("1"); + }); }); } /** * @param {Module} module the module * @param {RequestShortener} requestShortener request shortener - * @returns {Source} the header + * @returns {RawSource} the header */ generateHeader(module, requestShortener) { const req = module.readableIdentifier(requestShortener); diff --git a/lib/css/CssModulesPlugin.js b/lib/css/CssModulesPlugin.js index 6ed2e61c2..1a9e7167f 100644 --- a/lib/css/CssModulesPlugin.js +++ b/lib/css/CssModulesPlugin.js @@ -5,13 +5,16 @@ "use strict"; +const { SyncWaterfallHook, SyncHook } = require("tapable"); const { ConcatSource, PrefixSource, ReplaceSource, CachedSource } = require("webpack-sources"); +const Compilation = require("../Compilation"); const CssModule = require("../CssModule"); +const { tryRunOrWebpackError } = require("../HookWebpackError"); const HotUpdateChunk = require("../HotUpdateChunk"); const { CSS_MODULE_TYPE, @@ -37,21 +40,20 @@ const nonNumericOnlyHash = require("../util/nonNumericOnlyHash"); const CssExportsGenerator = require("./CssExportsGenerator"); const CssGenerator = require("./CssGenerator"); const CssParser = require("./CssParser"); -const { SyncWaterfallHook } = require("tapable"); -const Compilation = require("../Compilation"); -const { tryRunOrWebpackError } = require("../HookWebpackError"); /** @typedef {import("webpack-sources").Source} Source */ /** @typedef {import("../../declarations/WebpackOptions").Output} OutputOptions */ /** @typedef {import("../Chunk")} Chunk */ /** @typedef {import("../ChunkGraph")} ChunkGraph */ /** @typedef {import("../CodeGenerationResults")} CodeGenerationResults */ +/** @typedef {import("../Compilation").ChunkHashContext} ChunkHashContext */ /** @typedef {import("../Compiler")} Compiler */ /** @typedef {import("../CssModule").Inheritance} Inheritance */ /** @typedef {import("../DependencyTemplate").CssExportsData} CssExportsData */ /** @typedef {import("../Module")} Module */ -/** @typedef {import("../util/memoize")} Memoize */ /** @typedef {import("../Template").RuntimeTemplate} RuntimeTemplate */ +/** @typedef {import("../util/Hash")} Hash */ +/** @typedef {import("../util/memoize")} Memoize */ /** * @typedef {object} ChunkRenderContext @@ -61,6 +63,7 @@ const { tryRunOrWebpackError } = require("../HookWebpackError"); /** * @typedef {object} CompilationHooks * @property {SyncWaterfallHook<[Source, Module, ChunkRenderContext]>} renderModulePackage + * @property {SyncHook<[Chunk, Hash, ChunkHashContext]>} chunkHash */ const getCssLoadingRuntimeModule = memoize(() => @@ -181,7 +184,7 @@ const lzwEncode = str => { return encoded; }; -const plugin = "CssModulesPlugin"; +const PLUGIN_NAME = "CssModulesPlugin"; class CssModulesPlugin { /** @@ -201,7 +204,8 @@ class CssModulesPlugin { "source", "module", "renderContext" - ]) + ]), + chunkHash: new SyncHook(["chunk", "hash", "context"]) }; compilationHooksMap.set(compilation, hooks); } @@ -220,7 +224,7 @@ class CssModulesPlugin { */ apply(compiler) { compiler.hooks.compilation.tap( - plugin, + PLUGIN_NAME, (compilation, { normalModuleFactory }) => { const hooks = CssModulesPlugin.getCompilationHooks(compilation); const selfFactory = new SelfModuleFactory(compilation.moduleGraph); @@ -268,7 +272,7 @@ class CssModulesPlugin { ]) { normalModuleFactory.hooks.createParser .for(type) - .tap(plugin, parserOptions => { + .tap(PLUGIN_NAME, parserOptions => { validateParserOptions[type](parserOptions); const { namedExports } = parserOptions; @@ -292,7 +296,7 @@ class CssModulesPlugin { }); normalModuleFactory.hooks.createGenerator .for(type) - .tap(plugin, generatorOptions => { + .tap(PLUGIN_NAME, generatorOptions => { validateGeneratorOptions[type](generatorOptions); return generatorOptions.exportsOnly @@ -309,7 +313,7 @@ class CssModulesPlugin { }); normalModuleFactory.hooks.createModuleClass .for(type) - .tap(plugin, (createData, resolveData) => { + .tap(PLUGIN_NAME, (createData, resolveData) => { if (resolveData.dependencies.length > 0) { // When CSS is imported from CSS there is only one dependency const dependency = resolveData.dependencies[0]; @@ -381,9 +385,18 @@ class CssModulesPlugin { } } }); + compilation.hooks.chunkHash.tap( + "CssModulesPlugin", + (chunk, hash, context) => { + hooks.chunkHash.call(chunk, hash, context); + } + ); compilation.hooks.contentHash.tap("CssModulesPlugin", chunk => { const { chunkGraph, + codeGenerationResults, + moduleGraph, + runtimeTemplate, outputOptions: { hashSalt, hashDigest, @@ -391,17 +404,24 @@ class CssModulesPlugin { hashFunction } } = compilation; - const modules = orderedCssModulesPerChunk.get(chunk); - if (modules === undefined) return; const hash = createHash(hashFunction); if (hashSalt) hash.update(hashSalt); - for (const module of modules) { - hash.update(chunkGraph.getModuleHash(module, chunk.runtime)); + hooks.chunkHash.call(chunk, hash, { + chunkGraph, + codeGenerationResults, + moduleGraph, + runtimeTemplate + }); + const modules = orderedCssModulesPerChunk.get(chunk); + if (modules) { + for (const module of modules) { + hash.update(chunkGraph.getModuleHash(module, chunk.runtime)); + } } const digest = /** @type {string} */ (hash.digest(hashDigest)); chunk.contentHash.css = nonNumericOnlyHash(digest, hashDigestLength); }); - compilation.hooks.renderManifest.tap(plugin, (result, options) => { + compilation.hooks.renderManifest.tap(PLUGIN_NAME, (result, options) => { const { chunkGraph } = compilation; const { hash, chunk, codeGenerationResults, runtimeTemplate } = options; @@ -484,13 +504,13 @@ class CssModulesPlugin { }; compilation.hooks.runtimeRequirementInTree .for(RuntimeGlobals.hasCssModules) - .tap(plugin, handler); + .tap(PLUGIN_NAME, handler); compilation.hooks.runtimeRequirementInTree .for(RuntimeGlobals.ensureChunkHandlers) - .tap(plugin, handler); + .tap(PLUGIN_NAME, handler); compilation.hooks.runtimeRequirementInTree .for(RuntimeGlobals.hmrDownloadUpdateHandlers) - .tap(plugin, handler); + .tap(PLUGIN_NAME, handler); } ); } @@ -831,6 +851,7 @@ class CssModulesPlugin { true )}:${cssHeadDataCompression ? lzwEncode(metaDataStr) : metaDataStr};}` ); + chunk.rendered = true; return source; } diff --git a/test/configCases/css/pathinfo/index.js b/test/configCases/css/pathinfo/index.js new file mode 100644 index 000000000..c15078254 --- /dev/null +++ b/test/configCases/css/pathinfo/index.js @@ -0,0 +1,14 @@ +import * as style from "./style.css"; + +it("should compile and load style on demand", done => { + expect(style).toEqual(nsObj({})); + import("./style2.css").then(x => { + expect(x).toEqual(nsObj({})); + const style = getComputedStyle(document.body); + expect(style.getPropertyValue("background")).toBe(" red"); + expect(style.getPropertyValue("margin")).toBe(" 10px"); + expect(style.getPropertyValue("color")).toBe(" green"); + expect(style.getPropertyValue("padding")).toBe(" 20px 10px"); + done(); + }, done); +}); diff --git a/test/configCases/css/pathinfo/style-imported.css b/test/configCases/css/pathinfo/style-imported.css new file mode 100644 index 000000000..eb0ae4514 --- /dev/null +++ b/test/configCases/css/pathinfo/style-imported.css @@ -0,0 +1,3 @@ +body { + margin: 10px; +} diff --git a/test/configCases/css/pathinfo/style.css b/test/configCases/css/pathinfo/style.css new file mode 100644 index 000000000..ba0cfaf65 --- /dev/null +++ b/test/configCases/css/pathinfo/style.css @@ -0,0 +1,4 @@ +@import "style-imported.css"; +body { + background: red; +} diff --git a/test/configCases/css/pathinfo/style2-imported.css b/test/configCases/css/pathinfo/style2-imported.css new file mode 100644 index 000000000..ff9387e5d --- /dev/null +++ b/test/configCases/css/pathinfo/style2-imported.css @@ -0,0 +1,3 @@ +body { + padding: 20px 10px; +} diff --git a/test/configCases/css/pathinfo/style2.css b/test/configCases/css/pathinfo/style2.css new file mode 100644 index 000000000..d80cbcd05 --- /dev/null +++ b/test/configCases/css/pathinfo/style2.css @@ -0,0 +1,4 @@ +@import "./style2-imported.css"; +body { + color: green; +} diff --git a/test/configCases/css/pathinfo/test.config.js b/test/configCases/css/pathinfo/test.config.js new file mode 100644 index 000000000..61818ebf3 --- /dev/null +++ b/test/configCases/css/pathinfo/test.config.js @@ -0,0 +1,30 @@ +const fs = require("fs"); +const path = require("path"); + +module.exports = { + moduleScope(scope) { + const link = scope.window.document.createElement("link"); + link.rel = "stylesheet"; + link.href = "bundle0.css"; + scope.window.document.head.appendChild(link); + }, + findBundle: function (i, options) { + const source = fs.readFileSync( + path.resolve(options.output.path, "bundle0.css"), + "utf-8" + ); + + if ( + !source.includes(`/*!********************************!*\\ + !*** css ./style-imported.css ***! + \\********************************/`) && + !source.includes(`/*!***********************!*\\ + !*** css ./style.css ***! + \\***********************/`) + ) { + throw new Error("The `pathinfo` option doesn't work."); + } + + return "./bundle0.js"; + } +}; diff --git a/test/configCases/css/pathinfo/webpack.config.js b/test/configCases/css/pathinfo/webpack.config.js new file mode 100644 index 000000000..e2848b6a9 --- /dev/null +++ b/test/configCases/css/pathinfo/webpack.config.js @@ -0,0 +1,13 @@ +/** @type {import("../../../../").Configuration} */ +module.exports = { + target: "web", + mode: "development", + devtool: false, + output: { + pathinfo: true, + cssChunkFilename: "[name].[chunkhash].css" + }, + experiments: { + css: true + } +}; diff --git a/test/helpers/FakeDocument.js b/test/helpers/FakeDocument.js index 920e436ff..0bcab25f8 100644 --- a/test/helpers/FakeDocument.js +++ b/test/helpers/FakeDocument.js @@ -207,23 +207,26 @@ class FakeSheet { .replace(/^https:\/\/example\.com\//, "") ); let css = fs.readFileSync(filepath, "utf-8"); - css = css.replace(/@import url\("([^"]+)"\);/g, (match, url) => { - if (!/^https:\/\/test\.cases\/path\//.test(url)) { - return url; - } + css = css + // Remove comments + .replace(/\/\*.*?\*\//gms, "") + .replace(/@import url\("([^"]+)"\);/g, (match, url) => { + if (!/^https:\/\/test\.cases\/path\//.test(url)) { + return url; + } - if (url.startsWith("#")) { - return url; - } + if (url.startsWith("#")) { + return url; + } - return fs.readFileSync( - path.resolve( - this._basePath, - url.replace(/^https:\/\/test\.cases\/path\//, "") - ), - "utf-8" - ); - }); + return fs.readFileSync( + path.resolve( + this._basePath, + url.replace(/^https:\/\/test\.cases\/path\//, "") + ), + "utf-8" + ); + }); walkCssTokens(css, { isSelector() { return selector === undefined;