feat(CSS): pathinfo support

This commit is contained in:
Alexander Akait 2024-08-13 14:15:07 +03:00 committed by GitHub
commit a5a06140e2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 1596 additions and 61 deletions

View File

@ -8,6 +8,7 @@
const { ConcatSource, RawSource, CachedSource } = require("webpack-sources"); const { ConcatSource, RawSource, CachedSource } = require("webpack-sources");
const { UsageState } = require("./ExportsInfo"); const { UsageState } = require("./ExportsInfo");
const Template = require("./Template"); const Template = require("./Template");
const CssModulesPlugin = require("./css/CssModulesPlugin");
const JavascriptModulesPlugin = require("./javascript/JavascriptModulesPlugin"); const JavascriptModulesPlugin = require("./javascript/JavascriptModulesPlugin");
/** @typedef {import("webpack-sources").Source} Source */ /** @typedef {import("webpack-sources").Source} Source */
@ -163,8 +164,9 @@ class ModuleInfoHeaderPlugin {
apply(compiler) { apply(compiler) {
const { _verbose: verbose } = this; const { _verbose: verbose } = this;
compiler.hooks.compilation.tap("ModuleInfoHeaderPlugin", compilation => { compiler.hooks.compilation.tap("ModuleInfoHeaderPlugin", compilation => {
const hooks = JavascriptModulesPlugin.getCompilationHooks(compilation); const javascriptHooks =
hooks.renderModulePackage.tap( JavascriptModulesPlugin.getCompilationHooks(compilation);
javascriptHooks.renderModulePackage.tap(
"ModuleInfoHeaderPlugin", "ModuleInfoHeaderPlugin",
( (
moduleSource, moduleSource,
@ -195,11 +197,7 @@ class ModuleInfoHeaderPlugin {
const source = new ConcatSource(); const source = new ConcatSource();
let header = cacheEntry.header; let header = cacheEntry.header;
if (header === undefined) { if (header === undefined) {
const req = module.readableIdentifier(requestShortener); header = this.generateHeader(module, requestShortener);
const reqStr = req.replace(/\*\//g, "*_/");
const reqStrStar = "*".repeat(reqStr.length);
const headerStr = `/*!****${reqStrStar}****!*\\\n !*** ${reqStr} ***!\n \\****${reqStrStar}****/\n`;
header = new RawSource(headerStr);
cacheEntry.header = header; cacheEntry.header = header;
} }
source.add(header); source.add(header);
@ -248,11 +246,69 @@ class ModuleInfoHeaderPlugin {
return cachedSource; return cachedSource;
} }
); );
hooks.chunkHash.tap("ModuleInfoHeaderPlugin", (chunk, hash) => { javascriptHooks.chunkHash.tap(
"ModuleInfoHeaderPlugin",
(_chunk, hash) => {
hash.update("ModuleInfoHeaderPlugin");
hash.update("1");
}
);
const cssHooks = CssModulesPlugin.getCompilationHooks(compilation);
cssHooks.renderModulePackage.tap(
"ModuleInfoHeaderPlugin",
(moduleSource, module, { runtimeTemplate }) => {
const { requestShortener } = runtimeTemplate;
let cacheEntry;
let cache = caches.get(requestShortener);
if (cache === undefined) {
caches.set(requestShortener, (cache = new WeakMap()));
cache.set(
module,
(cacheEntry = { header: undefined, full: new WeakMap() })
);
} else {
cacheEntry = cache.get(module);
if (cacheEntry === undefined) {
cache.set(
module,
(cacheEntry = { header: undefined, full: new WeakMap() })
);
} else if (!verbose) {
const cachedSource = cacheEntry.full.get(moduleSource);
if (cachedSource !== undefined) return cachedSource;
}
}
const source = new ConcatSource();
let header = cacheEntry.header;
if (header === undefined) {
header = this.generateHeader(module, requestShortener);
cacheEntry.header = header;
}
source.add(header);
source.add(moduleSource);
const cachedSource = new CachedSource(source);
cacheEntry.full.set(moduleSource, cachedSource);
return cachedSource;
}
);
cssHooks.chunkHash.tap("ModuleInfoHeaderPlugin", (_chunk, hash) => {
hash.update("ModuleInfoHeaderPlugin"); hash.update("ModuleInfoHeaderPlugin");
hash.update("1"); hash.update("1");
}); });
}); });
} }
/**
* @param {Module} module the module
* @param {RequestShortener} requestShortener request shortener
* @returns {RawSource} the header
*/
generateHeader(module, requestShortener) {
const req = module.readableIdentifier(requestShortener);
const reqStr = req.replace(/\*\//g, "*_/");
const reqStrStar = "*".repeat(reqStr.length);
const headerStr = `/*!****${reqStrStar}****!*\\\n !*** ${reqStr} ***!\n \\****${reqStrStar}****/\n`;
return new RawSource(headerStr);
}
} }
module.exports = ModuleInfoHeaderPlugin; module.exports = ModuleInfoHeaderPlugin;

View File

@ -5,13 +5,16 @@
"use strict"; "use strict";
const { SyncWaterfallHook, SyncHook } = require("tapable");
const { const {
ConcatSource, ConcatSource,
PrefixSource, PrefixSource,
ReplaceSource, ReplaceSource,
CachedSource CachedSource
} = require("webpack-sources"); } = require("webpack-sources");
const Compilation = require("../Compilation");
const CssModule = require("../CssModule"); const CssModule = require("../CssModule");
const { tryRunOrWebpackError } = require("../HookWebpackError");
const HotUpdateChunk = require("../HotUpdateChunk"); const HotUpdateChunk = require("../HotUpdateChunk");
const { const {
CSS_MODULE_TYPE, CSS_MODULE_TYPE,
@ -43,14 +46,27 @@ const CssParser = require("./CssParser");
/** @typedef {import("../Chunk")} Chunk */ /** @typedef {import("../Chunk")} Chunk */
/** @typedef {import("../ChunkGraph")} ChunkGraph */ /** @typedef {import("../ChunkGraph")} ChunkGraph */
/** @typedef {import("../CodeGenerationResults")} CodeGenerationResults */ /** @typedef {import("../CodeGenerationResults")} CodeGenerationResults */
/** @typedef {import("../Compilation")} Compilation */ /** @typedef {import("../Compilation").ChunkHashContext} ChunkHashContext */
/** @typedef {import("../Compiler")} Compiler */ /** @typedef {import("../Compiler")} Compiler */
/** @typedef {import("../CssModule").Inheritance} Inheritance */ /** @typedef {import("../CssModule").Inheritance} Inheritance */
/** @typedef {import("../DependencyTemplate").CssExportsData} CssExportsData */ /** @typedef {import("../DependencyTemplate").CssExportsData} CssExportsData */
/** @typedef {import("../Module")} Module */ /** @typedef {import("../Module")} Module */
/** @typedef {import("../Template").RuntimeTemplate} RuntimeTemplate */
/** @typedef {import("../TemplatedPathPlugin").TemplatePath} TemplatePath */ /** @typedef {import("../TemplatedPathPlugin").TemplatePath} TemplatePath */
/** @typedef {import("../util/Hash")} Hash */
/** @typedef {import("../util/memoize")} Memoize */ /** @typedef {import("../util/memoize")} Memoize */
/**
* @typedef {object} ChunkRenderContext
* @property {RuntimeTemplate} runtimeTemplate runtime template
*/
/**
* @typedef {object} CompilationHooks
* @property {SyncWaterfallHook<[Source, Module, ChunkRenderContext]>} renderModulePackage
* @property {SyncHook<[Chunk, Hash, ChunkHashContext]>} chunkHash
*/
const getCssLoadingRuntimeModule = memoize(() => const getCssLoadingRuntimeModule = memoize(() =>
require("./CssLoadingRuntimeModule") require("./CssLoadingRuntimeModule")
); );
@ -121,6 +137,9 @@ const validateParserOptions = {
) )
}; };
/** @type {WeakMap<Compilation, CompilationHooks>} */
const compilationHooksMap = new WeakMap();
/** /**
* @param {string} str string * @param {string} str string
* @param {boolean=} omitOptionalUnderscore if true, optional underscore is not added * @param {boolean=} omitOptionalUnderscore if true, optional underscore is not added
@ -166,9 +185,34 @@ const lzwEncode = str => {
return encoded; return encoded;
}; };
const plugin = "CssModulesPlugin"; const PLUGIN_NAME = "CssModulesPlugin";
class CssModulesPlugin { class CssModulesPlugin {
/**
* @param {Compilation} compilation the compilation
* @returns {CompilationHooks} the attached hooks
*/
static getCompilationHooks(compilation) {
if (!(compilation instanceof Compilation)) {
throw new TypeError(
"The 'compilation' argument must be an instance of Compilation"
);
}
let hooks = compilationHooksMap.get(compilation);
if (hooks === undefined) {
hooks = {
renderModulePackage: new SyncWaterfallHook([
"source",
"module",
"renderContext"
]),
chunkHash: new SyncHook(["chunk", "hash", "context"])
};
compilationHooksMap.set(compilation, hooks);
}
return hooks;
}
constructor() { constructor() {
/** @type {WeakMap<Source, { undoPath: string, inheritance: Inheritance, source: CachedSource }>} */ /** @type {WeakMap<Source, { undoPath: string, inheritance: Inheritance, source: CachedSource }>} */
this._moduleCache = new WeakMap(); this._moduleCache = new WeakMap();
@ -181,8 +225,9 @@ class CssModulesPlugin {
*/ */
apply(compiler) { apply(compiler) {
compiler.hooks.compilation.tap( compiler.hooks.compilation.tap(
plugin, PLUGIN_NAME,
(compilation, { normalModuleFactory }) => { (compilation, { normalModuleFactory }) => {
const hooks = CssModulesPlugin.getCompilationHooks(compilation);
const selfFactory = new SelfModuleFactory(compilation.moduleGraph); const selfFactory = new SelfModuleFactory(compilation.moduleGraph);
compilation.dependencyFactories.set( compilation.dependencyFactories.set(
CssUrlDependency, CssUrlDependency,
@ -228,7 +273,7 @@ class CssModulesPlugin {
]) { ]) {
normalModuleFactory.hooks.createParser normalModuleFactory.hooks.createParser
.for(type) .for(type)
.tap(plugin, parserOptions => { .tap(PLUGIN_NAME, parserOptions => {
validateParserOptions[type](parserOptions); validateParserOptions[type](parserOptions);
const { namedExports } = parserOptions; const { namedExports } = parserOptions;
@ -252,7 +297,7 @@ class CssModulesPlugin {
}); });
normalModuleFactory.hooks.createGenerator normalModuleFactory.hooks.createGenerator
.for(type) .for(type)
.tap(plugin, generatorOptions => { .tap(PLUGIN_NAME, generatorOptions => {
validateGeneratorOptions[type](generatorOptions); validateGeneratorOptions[type](generatorOptions);
return generatorOptions.exportsOnly return generatorOptions.exportsOnly
@ -269,7 +314,7 @@ class CssModulesPlugin {
}); });
normalModuleFactory.hooks.createModuleClass normalModuleFactory.hooks.createModuleClass
.for(type) .for(type)
.tap(plugin, (createData, resolveData) => { .tap(PLUGIN_NAME, (createData, resolveData) => {
if (resolveData.dependencies.length > 0) { if (resolveData.dependencies.length > 0) {
// When CSS is imported from CSS there is only one dependency // When CSS is imported from CSS there is only one dependency
const dependency = resolveData.dependencies[0]; const dependency = resolveData.dependencies[0];
@ -341,9 +386,18 @@ class CssModulesPlugin {
} }
} }
}); });
compilation.hooks.chunkHash.tap(
"CssModulesPlugin",
(chunk, hash, context) => {
hooks.chunkHash.call(chunk, hash, context);
}
);
compilation.hooks.contentHash.tap("CssModulesPlugin", chunk => { compilation.hooks.contentHash.tap("CssModulesPlugin", chunk => {
const { const {
chunkGraph, chunkGraph,
codeGenerationResults,
moduleGraph,
runtimeTemplate,
outputOptions: { outputOptions: {
hashSalt, hashSalt,
hashDigest, hashDigest,
@ -351,19 +405,27 @@ class CssModulesPlugin {
hashFunction hashFunction
} }
} = compilation; } = compilation;
const modules = orderedCssModulesPerChunk.get(chunk);
if (modules === undefined) return;
const hash = createHash(hashFunction); const hash = createHash(hashFunction);
if (hashSalt) hash.update(hashSalt); if (hashSalt) hash.update(hashSalt);
hooks.chunkHash.call(chunk, hash, {
chunkGraph,
codeGenerationResults,
moduleGraph,
runtimeTemplate
});
const modules = orderedCssModulesPerChunk.get(chunk);
if (modules) {
for (const module of modules) { for (const module of modules) {
hash.update(chunkGraph.getModuleHash(module, chunk.runtime)); hash.update(chunkGraph.getModuleHash(module, chunk.runtime));
} }
}
const digest = /** @type {string} */ (hash.digest(hashDigest)); const digest = /** @type {string} */ (hash.digest(hashDigest));
chunk.contentHash.css = nonNumericOnlyHash(digest, hashDigestLength); 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 { chunkGraph } = compilation;
const { hash, chunk, codeGenerationResults } = options; const { hash, chunk, codeGenerationResults, runtimeTemplate } =
options;
if (chunk instanceof HotUpdateChunk) return result; if (chunk instanceof HotUpdateChunk) return result;
@ -397,7 +459,9 @@ class CssModulesPlugin {
cssHeadDataCompression: cssHeadDataCompression:
compilation.outputOptions.cssHeadDataCompression, compilation.outputOptions.cssHeadDataCompression,
undoPath, undoPath,
modules modules,
runtimeTemplate,
hooks
}), }),
filename, filename,
info, info,
@ -441,13 +505,13 @@ class CssModulesPlugin {
}; };
compilation.hooks.runtimeRequirementInTree compilation.hooks.runtimeRequirementInTree
.for(RuntimeGlobals.hasCssModules) .for(RuntimeGlobals.hasCssModules)
.tap(plugin, handler); .tap(PLUGIN_NAME, handler);
compilation.hooks.runtimeRequirementInTree compilation.hooks.runtimeRequirementInTree
.for(RuntimeGlobals.ensureChunkHandlers) .for(RuntimeGlobals.ensureChunkHandlers)
.tap(plugin, handler); .tap(PLUGIN_NAME, handler);
compilation.hooks.runtimeRequirementInTree compilation.hooks.runtimeRequirementInTree
.for(RuntimeGlobals.hmrDownloadUpdateHandlers) .for(RuntimeGlobals.hmrDownloadUpdateHandlers)
.tap(plugin, handler); .tap(PLUGIN_NAME, handler);
} }
); );
} }
@ -600,6 +664,8 @@ class CssModulesPlugin {
* @param {ChunkGraph} options.chunkGraph chunk graph * @param {ChunkGraph} options.chunkGraph chunk graph
* @param {CodeGenerationResults} options.codeGenerationResults code generation results * @param {CodeGenerationResults} options.codeGenerationResults code generation results
* @param {CssModule} options.module css module * @param {CssModule} options.module css module
* @param {RuntimeTemplate} options.runtimeTemplate runtime template
* @param {CompilationHooks} options.hooks hooks
* @returns {Source} css module source * @returns {Source} css module source
*/ */
renderModule({ renderModule({
@ -608,7 +674,9 @@ class CssModulesPlugin {
chunk, chunk,
chunkGraph, chunkGraph,
codeGenerationResults, codeGenerationResults,
module module,
hooks,
runtimeTemplate
}) { }) {
const codeGenResult = codeGenerationResults.get(module, chunk.runtime); const codeGenResult = codeGenerationResults.get(module, chunk.runtime);
const moduleSourceContent = const moduleSourceContent =
@ -722,7 +790,13 @@ class CssModulesPlugin {
: "" : ""
}${esModule ? "&" : ""}${escapeCss(moduleId)}` }${esModule ? "&" : ""}${escapeCss(moduleId)}`
); );
return source; return tryRunOrWebpackError(
() =>
hooks.renderModulePackage.call(source, module, {
runtimeTemplate
}),
"CssModulesPlugin.getCompilationHooks().renderModulePackage"
);
} }
/** /**
@ -734,6 +808,8 @@ class CssModulesPlugin {
* @param {ChunkGraph} options.chunkGraph chunk graph * @param {ChunkGraph} options.chunkGraph chunk graph
* @param {CodeGenerationResults} options.codeGenerationResults code generation results * @param {CodeGenerationResults} options.codeGenerationResults code generation results
* @param {CssModule[]} options.modules ordered css modules * @param {CssModule[]} options.modules ordered css modules
* @param {RuntimeTemplate} options.runtimeTemplate runtime template
* @param {CompilationHooks} options.hooks hooks
* @returns {Source} generated source * @returns {Source} generated source
*/ */
renderChunk({ renderChunk({
@ -743,7 +819,9 @@ class CssModulesPlugin {
chunk, chunk,
chunkGraph, chunkGraph,
codeGenerationResults, codeGenerationResults,
modules modules,
runtimeTemplate,
hooks
}) { }) {
const source = new ConcatSource(); const source = new ConcatSource();
/** @type {string[]} */ /** @type {string[]} */
@ -756,7 +834,9 @@ class CssModulesPlugin {
chunk, chunk,
chunkGraph, chunkGraph,
codeGenerationResults, codeGenerationResults,
module module,
runtimeTemplate,
hooks
}); });
source.add(moduleSource); source.add(moduleSource);
} catch (err) { } catch (err) {
@ -772,6 +852,7 @@ class CssModulesPlugin {
true true
)}:${cssHeadDataCompression ? lzwEncode(metaDataStr) : metaDataStr};}` )}:${cssHeadDataCompression ? lzwEncode(metaDataStr) : metaDataStr};}`
); );
chunk.rendered = true;
return source; return source;
} }

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -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);
});

View File

@ -0,0 +1,3 @@
body {
margin: 10px;
}

View File

@ -0,0 +1,4 @@
@import "style-imported.css";
body {
background: red;
}

View File

@ -0,0 +1,3 @@
body {
padding: 20px 10px;
}

View File

@ -0,0 +1,4 @@
@import "./style2-imported.css";
body {
color: green;
}

View File

@ -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";
}
};

View File

@ -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
}
};

View File

@ -207,7 +207,10 @@ class FakeSheet {
.replace(/^https:\/\/example\.com\//, "") .replace(/^https:\/\/example\.com\//, "")
); );
let css = fs.readFileSync(filepath, "utf-8"); let css = fs.readFileSync(filepath, "utf-8");
css = css.replace(/@import url\("([^"]+)"\);/g, (match, url) => { css = css
// Remove comments
.replace(/\/\*.*?\*\//gms, "")
.replace(/@import url\("([^"]+)"\);/g, (match, url) => {
if (!/^https:\/\/test\.cases\/path\//.test(url)) { if (!/^https:\/\/test\.cases\/path\//.test(url)) {
return url; return url;
} }