webpack
Version:
Packs ECMAScript/CommonJs/AMD modules for the browser. Allows you to split your codebase into multiple bundles, which can be loaded on demand. Supports loaders to preprocess files, i.e. json, jsx, es7, css, less, ... and your custom stuff.
1,098 lines (1,006 loc) • 34.9 kB
JavaScript
/*
MIT License http://www.opensource.org/licenses/mit-license.php
Author Sergey Melyukov @smelukov
*/
"use strict";
const {
ConcatSource,
OriginalSource,
RawSource,
ReplaceSource,
SourceMapSource
} = require("webpack-sources");
const { UsageState } = require("../ExportsInfo");
const Generator = require("../Generator");
const InitFragment = require("../InitFragment");
const ModuleFilenameHelpers = require("../ModuleFilenameHelpers");
const {
CSS_TEXT_TYPE,
CSS_TEXT_TYPES,
CSS_TYPE,
CSS_TYPES,
JAVASCRIPT_AND_CSS_TEXT_TYPES,
JAVASCRIPT_AND_CSS_TYPES,
JAVASCRIPT_TYPE,
JAVASCRIPT_TYPES,
NO_TYPES
} = require("../ModuleSourceTypeConstants");
const RuntimeGlobals = require("../RuntimeGlobals");
const Template = require("../Template");
const { getPresentKinds } = require("../TemplatedPathPlugin");
const CssImportDependency = require("../dependencies/CssImportDependency");
const HarmonyImportSideEffectDependency = require("../dependencies/HarmonyImportSideEffectDependency");
const { encodeMappings } = require("../util/createMappings");
const {
contextifySourceUrl,
makePathsAbsolute
} = require("../util/identifier");
const PropertyName = require("../util/property");
const {
PUBLIC_PATH_FULL_HASH,
walkFullHashPlaceholders
} = require("../util/publicPathPlaceholder");
const CssModulesPlugin = require("./CssModulesPlugin");
// Avoids `type.split("/")[0]` allocation on the `getTypes` hot path.
const CSS_TYPE_PREFIX = `${CSS_TYPE}/`;
const URL_SCHEME_REGEXP = /^(?:data|https?):/;
/** @import { Source, RawSourceMap } from "webpack-sources" */
/**
* @import {
* CssModuleGeneratorOptions
* } from "../../declarations/WebpackOptions"
*/
/** @import { DependencyConstructor } from "../Compilation" */
/** @import CodeGenerationResults from "../CodeGenerationResults" */
/** @import Dependency from "../Dependency" */
/**
* @import {
* CssData,
* DependencyTemplateContext,
* CssDependencyTemplateContext
* } from "../DependencyTemplate"
*/
/** @import { GenerateContext, UpdateHashContext } from "../Generator" */
/**
* @import {
* ConcatenationBailoutReasonContext,
* SourceType,
* SourceTypes
* } from "../Module"
*/
/** @import ModuleGraph from "../ModuleGraph" */
/** @import NormalModule from "../NormalModule" */
/** @import Hash from "../util/Hash" */
/** @import { ModuleFactoryCacheEntry } from "./CssModulesPlugin" */
/**
* @import CssModule, {
* CssModuleBuildInfo,
* CssModuleBuildMeta
* } from "./CssModule"
*/
/** @typedef {{ line: number, column: number }} SourcePosition */
/** @typedef {Map<string, SourcePosition>} ExportLocsMap */
/**
* Build a v3 source map mapping CSS-class export lines back to their selector positions.
* @param {string} generatedJs the generated JS string
* @param {ExportLocsMap} exportLocs map of export names to CSS source location
* @param {string} cssContent original CSS source content
* @param {string} sourceName source identifier to use in the map
* @returns {RawSourceMap} a v3 RawSourceMap
*/
const buildExportsSourceMap = (
generatedJs,
exportLocs,
cssContent,
sourceName
) => {
const lines = generatedJs.split("\n");
// O(L) pass: match `\t"<name>": <value>` lines against export names.
const keyToName = new Map();
for (const [exportName] of exportLocs) {
keyToName.set(JSON.stringify(exportName), exportName);
}
const lineByExport = new Map();
for (let i = 0; i < lines.length && lineByExport.size < keyToName.size; i++) {
const line = lines[i];
if (line.charCodeAt(0) !== 9 || line.charCodeAt(1) !== 34) continue;
let j = 2;
while (j < line.length) {
const c = line.charCodeAt(j);
if (c === 92) {
j += 2;
continue;
}
if (c === 34) break;
j++;
}
if (line.charCodeAt(j + 1) !== 58) continue;
const name = keyToName.get(line.slice(1, j + 1));
if (name !== undefined && !lineByExport.has(name)) {
lineByExport.set(name, i);
}
}
/** @type {(import("../util/createMappings").LineMappings)[]} */
const perLine = lines.map(() => null);
for (const [exportName, genLine] of lineByExport) {
const pos = /** @type {SourcePosition} */ (exportLocs.get(exportName));
// V3 source maps are 0-based; webpack `loc` lines are 1-based.
perLine[genLine] = {
generatedColumn: 0,
sourceIndex: 0,
originalLine: pos.line - 1,
originalColumn: pos.column
};
}
return {
version: 3,
file: "",
sources: [sourceName],
sourcesContent: [cssContent],
names: [],
mappings: encodeMappings(perLine)
};
};
class CssGenerator extends Generator {
/**
* Creates an instance of CssGenerator.
* @param {CssModuleGeneratorOptions} options options
* @param {ModuleGraph} moduleGraph the module graph
*/
constructor(options, moduleGraph) {
super();
/** @type {CssModuleGeneratorOptions} */
this.options = options;
/** @type {boolean | undefined} */
this._exportsOnly = options.exportsOnly;
/** @type {boolean | undefined} */
this._esModule = options.esModule;
/** @type {ModuleGraph} */
this._moduleGraph = moduleGraph;
/** @type {WeakMap<Source, ModuleFactoryCacheEntry>} */
this._moduleFactoryCache = new WeakMap();
const localIdentName = options.localIdentName;
// Detect hash placeholders via the canonical template parser so this stays
// in sync with `interpolate`'s grammar.
const isFn = typeof localIdentName === "function";
const kinds =
typeof localIdentName === "string"
? getPresentKinds(localIdentName)
: undefined;
/** @type {boolean} */
this._localIdentNeedsHash =
isFn ||
(kinds !== undefined && (kinds.has("fullhash") || kinds.has("hash")));
/** @type {boolean} */
this._localIdentNeedsContentHash =
isFn || (kinds !== undefined && kinds.has("contenthash"));
}
/**
* Returns the reason this module cannot be concatenated, when one exists.
* @param {NormalModule} module module for which the bailout reason should be determined
* @param {ConcatenationBailoutReasonContext} context context
* @returns {string | undefined} reason why this module can't be concatenated, undefined when it can be concatenated
*/
getConcatenationBailoutReason(module, context) {
if (!this._esModule) {
return "Module is not an ECMAScript module";
}
return undefined;
}
/**
* Processes the provided module.
* @param {Dependency} dependency the dependency to generate
* @param {ReplaceSource} source the current replace source which can be modified
* @param {DependencyTemplateContext & { cssData: CssData, type: string }} templateContext the template context (shared across all dependencies of the module)
* @returns {void}
*/
sourceDependency(dependency, source, templateContext) {
const constructor =
/** @type {DependencyConstructor} */
(dependency.constructor);
const template = templateContext.dependencyTemplates.get(constructor);
if (!template) {
throw new Error(
`No template for dependency: ${dependency.constructor.name}`
);
}
template.apply(dependency, source, templateContext);
}
/**
* Processes the provided module.
* @param {NormalModule} module the module to generate
* @param {InitFragment<GenerateContext>[]} initFragments mutable list of init fragments
* @param {ReplaceSource} source the current replace source which can be modified
* @param {GenerateContext & { cssData: CssData }} generateContext the generateContext
* @returns {void}
*/
sourceModule(module, initFragments, source, generateContext) {
// Only `dependency` varies across `template.apply` calls, so one context
// (and one lazy `chunkInitFragments` getter) serves the whole module.
/** @type {InitFragment<GenerateContext>[] | undefined} */
let chunkInitFragments;
/** @type {CssDependencyTemplateContext} */
const templateContext = {
runtimeTemplate: generateContext.runtimeTemplate,
dependencyTemplates: generateContext.dependencyTemplates,
moduleGraph: generateContext.moduleGraph,
chunkGraph: generateContext.chunkGraph,
module,
runtime: generateContext.runtime,
runtimeRequirements: generateContext.runtimeRequirements,
concatenationScope: generateContext.concatenationScope,
codeGenerationResults:
/** @type {CodeGenerationResults} */
(generateContext.codeGenerationResults),
initFragments,
cssData: generateContext.cssData,
type: generateContext.type,
get chunkInitFragments() {
if (!chunkInitFragments) {
const data =
/** @type {NonNullable<GenerateContext["getData"]>} */
(generateContext.getData)();
chunkInitFragments = data.get("chunkInitFragments");
if (!chunkInitFragments) {
chunkInitFragments = [];
data.set("chunkInitFragments", chunkInitFragments);
}
}
return chunkInitFragments;
}
};
for (const dependency of module.dependencies) {
this.sourceDependency(dependency, source, templateContext);
}
if (module.presentationalDependencies !== undefined) {
for (const dependency of module.presentationalDependencies) {
this.sourceDependency(dependency, source, templateContext);
}
}
}
/**
* Generates generated code for this runtime module.
* @param {NormalModule} module module for which the code should be generated
* @param {GenerateContext} generateContext context for generate
* @returns {Source | null} generated code
*/
generate(module, generateContext) {
const exportType = /** @type {CssModule} */ (module).exportType || "link";
const source =
generateContext.type === JAVASCRIPT_TYPE && exportType === "link"
? new ReplaceSource(new RawSource(""))
: new ReplaceSource(/** @type {Source} */ (module.originalSource()));
/** @type {InitFragment<GenerateContext>[]} */
const initFragments = [];
/** @type {CssData} */
const cssData = {
esModule: /** @type {boolean} */ (this._esModule),
exports: new Map(),
exportLocs: new Map()
};
this.sourceModule(module, initFragments, source, {
...generateContext,
cssData
});
switch (generateContext.type) {
case JAVASCRIPT_TYPE: {
const compilation = generateContext.runtimeTemplate.compilation;
const devtool = compilation.options.devtool;
const isCssModule = /** @type {CssModuleBuildMeta} */ (module.buildMeta)
.isCssModule;
const generateContentCode = () => {
switch (exportType) {
case "style": {
const rendered = this._renderCss(module, generateContext);
if (!rendered) return "";
const cssSource = this._renderEmbeddedSource(
rendered,
module,
generateContext
);
generateContext.runtimeRequirements.add(
RuntimeGlobals.cssInjectStyle
);
const moduleId = generateContext.chunkGraph.getModuleId(module);
if (generateContext.concatenationScope) {
return new ConcatSource(
`__webpack_css_styles__.push([${JSON.stringify(moduleId)}, `,
this._cssToJsLiteral(cssSource, devtool, generateContext),
"]);"
);
}
return new ConcatSource(
`${RuntimeGlobals.cssInjectStyle}(${JSON.stringify(
moduleId
)}, `,
this._cssToJsLiteral(cssSource, devtool, generateContext),
");"
);
}
default:
return "";
}
};
const generateImportCode = () => {
switch (exportType) {
case "style": {
return this._generateImportExpressions(module, generateContext)
.map((expr) => `${expr};`)
.join("\n");
}
default:
return "";
}
};
const generateExportCode = () => {
/**
* Generates js default export.
* @returns {Source | null} the default export
*/
const generateJSDefaultExport = () => {
switch (exportType) {
case "text": {
return this._generateCssText(module, generateContext, true);
}
case "css-style-sheet": {
const rt = generateContext.runtimeTemplate;
const fnPrefix = rt.supportsArrowFunction()
? "() => {\n"
: "function() {\n";
const constOrVar = rt.renderConst();
const fallback =
"{ cssText: css, replaceSync: function(value) { this.cssText = value; } }";
if (rt.compilation.compiler.platform.node === true) {
return new ConcatSource(
`(${fnPrefix}${constOrVar} css = `,
this._generateCssText(module, generateContext, true),
`;\nreturn ${fallback};\n})()`
);
}
if (rt.isUniversalTarget()) {
return new ConcatSource(
`(${fnPrefix}${constOrVar} css = `,
this._generateCssText(module, generateContext, true),
`;\nif (typeof CSSStyleSheet === 'undefined') return ${fallback};\n${constOrVar} sheet = new CSSStyleSheet();\nsheet.replaceSync(css);\nreturn sheet;\n})()`
);
}
return new ConcatSource(
`(${fnPrefix}${constOrVar} sheet = new CSSStyleSheet();\nsheet.replaceSync(`,
this._generateCssText(module, generateContext, true),
");\nreturn sheet;\n})()"
);
}
default:
return null;
}
};
/** @type {Source | null} */
const defaultExport = generateJSDefaultExport();
/** @type {CssModuleBuildInfo} */
(module.buildInfo).cssData = cssData;
// Required for HMR
if (module.hot) {
generateContext.runtimeRequirements.add(RuntimeGlobals.module);
}
if (!defaultExport && cssData.exports.size === 0 && !isCssModule) {
return new RawSource("");
}
const exportsInfo =
generateContext.moduleGraph.getExportsInfo(module);
/**
* @param {string} name export name
* @returns {string | false} used export name when the export is referenced
*/
const getUsedExportName = (name) =>
/** @type {string | false} */ (
exportsInfo
.getExportInfo(name)
.getUsedName(name, generateContext.runtime)
);
if (generateContext.concatenationScope) {
const source = new ConcatSource();
/** @type {Set<string>} */
const usedIdentifiers = new Set();
const { RESERVED_IDENTIFIER } = PropertyName;
if (defaultExport) {
const usedName = getUsedExportName("default");
if (usedName) {
let identifier = Template.toIdentifier(usedName);
if (RESERVED_IDENTIFIER.has(identifier)) {
identifier = `_${identifier}`;
}
usedIdentifiers.add(identifier);
generateContext.concatenationScope.registerExport(
"default",
identifier
);
source.add(
`${generateContext.runtimeTemplate.renderConst()} ${identifier} = `
);
source.add(defaultExport);
source.add(";\n");
}
}
for (const [name, v] of cssData.exports) {
const usedName = getUsedExportName(name);
let identifier = Template.toIdentifier(usedName || name);
if (RESERVED_IDENTIFIER.has(identifier)) {
identifier = `_${identifier}`;
}
let i = 0;
while (usedIdentifiers.has(identifier)) {
identifier = Template.toIdentifier(name + i);
i += 1;
}
usedIdentifiers.add(identifier);
generateContext.concatenationScope.registerExport(
name,
identifier
);
source.add(
`${generateContext.runtimeTemplate.renderConst()} ${identifier} = ${JSON.stringify(
v
)};\n`
);
}
return source;
}
const needNsObj =
this._esModule &&
exportsInfo.otherExportsInfo.getUsed(generateContext.runtime) !==
UsageState.Unused;
if (needNsObj) {
generateContext.runtimeRequirements.add(
RuntimeGlobals.makeNamespaceObject
);
}
// Should be after `concatenationScope` to allow module inlining
generateContext.runtimeRequirements.add(RuntimeGlobals.module);
const defaultUsedName = defaultExport
? getUsedExportName("default")
: false;
/** @type {string[]} */
const exportEntries = [];
for (const [name, v] of cssData.exports) {
const exportKey = getUsedExportName(name) || name;
exportEntries.push(
`\t${JSON.stringify(exportKey)}: ${JSON.stringify(v)}`
);
}
if (
!isCssModule &&
!needNsObj &&
exportEntries.length === 0 &&
defaultUsedName
) {
return new ConcatSource(
`${module.moduleArgument}.exports = `,
/** @type {Source} */ (defaultExport)
);
}
const result = new ConcatSource();
result.add(
`${needNsObj ? `${RuntimeGlobals.makeNamespaceObject}(` : ""}${
module.moduleArgument
}.exports = {\n`
);
if (defaultUsedName) {
result.add('\t"default": ');
result.add(/** @type {Source} */ (defaultExport));
if (exportEntries.length > 0) {
result.add(",\n");
}
}
if (exportEntries.length > 0) {
result.add(exportEntries.join(",\n"));
}
result.add(`\n}${needNsObj ? ")" : ""};`);
return result;
};
const codeParts = this._exportsOnly
? [generateExportCode()]
: [generateImportCode(), generateContentCode(), generateExportCode()];
const source = new ConcatSource();
for (const part of codeParts) {
if (part) {
source.add(part);
source.add("\n");
}
}
if (
exportType === "link" &&
!isCssModule &&
cssData.exports.size === 0
) {
return source;
}
const generatedJs = /** @type {string} */ (source.source());
// Context-relative identifier, like `NormalModule.createSource` — so
// `SourceMapDevToolPlugin` can match the module and apply the
// devtool filename template instead of leaking this raw name
const sourceName = contextifySourceUrl(
/** @type {string} */ (compilation.options.context),
module.identifier(),
compilation.compiler.root
);
if (
/** @type {ExportLocsMap} */
(cssData.exportLocs).size > 0
) {
const cssOriginal = module.originalSource();
if (cssOriginal) {
const sourceMap = buildExportsSourceMap(
generatedJs,
/** @type {ExportLocsMap} */
(cssData.exportLocs),
/** @type {string} */ (cssOriginal.source()),
sourceName
);
return new SourceMapSource(generatedJs, sourceName, sourceMap);
}
}
return new OriginalSource(generatedJs, sourceName);
}
case CSS_TYPE: {
if (!(this._exportsOnly || exportType !== "link")) {
generateContext.runtimeRequirements.add(RuntimeGlobals.hasCssModules);
}
return InitFragment.addToSource(source, initFragments, generateContext);
}
case CSS_TEXT_TYPE:
return this._generateCssText(module, generateContext, false);
default:
return null;
}
}
/**
* Generates fallback output for the provided error condition.
* @param {Error} error the error
* @param {NormalModule} module module for which the code should be generated
* @param {GenerateContext} generateContext context for generate
* @returns {Source | null} generated code
*/
generateError(error, module, generateContext) {
switch (generateContext.type) {
case JAVASCRIPT_TYPE: {
return new RawSource(Generator.throwBuildErrorCode(error));
}
case CSS_TYPE: {
return new RawSource(`/**\n ${error.message} \n**/`);
}
default:
return null;
}
}
/**
* Returns the source types available for this module.
* @param {NormalModule} module fresh module
* @returns {SourceTypes} available types (do not mutate)
*/
getTypes(module) {
const exportType = /** @type {CssModule} */ (module).exportType || "link";
if (exportType === "style") {
return JAVASCRIPT_TYPES;
}
let hasJs = false;
let hasCssText = false;
// Uncached per call (#20800) — the common link case stops scanning once
// js is seen; `hasCssText` only matters for exports-only / non-link.
const needsCssText = this._exportsOnly || exportType !== "link";
const connections = this._moduleGraph.getIncomingConnections(module);
for (const connection of connections) {
if (hasJs && (hasCssText || !needsCssText)) break;
if (
exportType === "link" &&
connection.dependency instanceof CssImportDependency
) {
continue;
}
if (connection.dependency instanceof HarmonyImportSideEffectDependency) {
continue;
}
// HTML inline styles consume CSS_TEXT_TYPE directly.
if (
connection.dependency &&
(connection.dependency.category === "html-style" ||
connection.dependency.category === "html-style-attribute")
) {
hasCssText = true;
continue;
}
if (!connection.originModule) {
continue;
}
if (!connection.originModule.type.startsWith(CSS_TYPE_PREFIX)) {
hasJs = true;
} else {
const originModule = /** @type {CssModule} */ connection.originModule;
const originExportType =
/** @type {CssModule} */ (originModule).exportType || "link";
if (originExportType !== "link") {
hasJs = true;
}
}
}
if (this._exportsOnly || exportType !== "link") {
if (hasJs && hasCssText) return JAVASCRIPT_AND_CSS_TEXT_TYPES;
if (hasJs) return JAVASCRIPT_TYPES;
if (hasCssText) return CSS_TEXT_TYPES;
return NO_TYPES;
}
if (hasJs) {
return JAVASCRIPT_AND_CSS_TYPES;
}
return CSS_TYPES;
}
/**
* @returns {boolean} whether getTypes() depends on the module's incoming connections
*/
getTypesDependOnIncomingConnections() {
return true;
}
/**
* Returns the estimated size for the requested source type.
* @param {NormalModule} module the module
* @param {SourceType=} type source type
* @returns {number} estimate size of the module
*/
getSize(module, type) {
switch (type) {
case JAVASCRIPT_TYPE: {
const cssData = /** @type {CssModuleBuildInfo} */ (module.buildInfo)
.cssData;
if (!cssData) {
return 42;
}
if (cssData.exports.size === 0) {
if (
/** @type {CssModuleBuildMeta} */ (module.buildMeta).isCssModule
) {
return 42;
}
return 0;
}
// `JSON.stringify({...exports}).length` computed entry-wise, without
// materializing the object or the full string.
let length = 2 + (cssData.exports.size - 1);
for (const [key, value] of cssData.exports) {
length +=
JSON.stringify(key).length + 1 + JSON.stringify(value).length;
}
return length + 42;
}
case CSS_TEXT_TYPE:
case CSS_TYPE: {
const originalSource = module.originalSource();
if (!originalSource) {
return 0;
}
return originalSource.size();
}
default:
return 0;
}
}
/**
* Updates the hash with the data contributed by this instance.
* @param {Hash} hash hash that will be modified
* @param {UpdateHashContext} updateHashContext context for updating hash
*/
updateHash(hash, { module, runtimeTemplate }) {
hash.update(/** @type {boolean} */ (this._esModule).toString());
hash.update(/** @type {boolean} */ (this._exportsOnly).toString());
// Only a module embedded in JS gets an inlined map, which varies on devtool
// and its filename options without changing the source. `String`: templates
// may be functions.
const exportType = /** @type {CssModule} */ (module).exportType || "link";
if (runtimeTemplate && exportType !== "link") {
// What a tap varies on has to reach the key, or a changed option replays.
runtimeTemplate.compilation.hooks.embeddedSourceHash.call(module, hash);
const { compilation } = runtimeTemplate;
const devtool = compilation.options.devtool;
const outputOptions = compilation.outputOptions;
hash.update(
`|${typeof devtool === "string" ? devtool : ""}|${
outputOptions.devtoolNamespace || ""
}|${String(outputOptions.devtoolModuleFilenameTemplate || "")}|${String(
outputOptions.devtoolFallbackModuleFilenameTemplate || ""
)}`
);
}
}
/**
* Resolve the effective `@charset` by walking text imports.
* @param {NormalModule} module the module
* @param {ModuleGraph} moduleGraph module graph
* @param {WeakSet<NormalModule>=} visited cycle guard
* @returns {string | undefined} the effective charset
*/
_getEffectiveCharset(module, moduleGraph, visited = new WeakSet()) {
if (!module || visited.has(module)) return undefined;
const exportType = /** @type {CssModule} */ (module).exportType;
if (exportType !== "text" && exportType !== "css-style-sheet") {
return undefined;
}
visited.add(module);
const own =
module.buildInfo &&
/** @type {CssModuleBuildInfo} */ (module.buildInfo).charset;
if (own !== undefined) return own;
if (exportType !== "text") return undefined;
for (const dep of module.dependencies) {
if (dep instanceof CssImportDependency) {
const depModule = /** @type {NormalModule} */ (
moduleGraph.getModule(dep)
);
const inherited = this._getEffectiveCharset(
depModule,
moduleGraph,
visited
);
if (inherited !== undefined) return inherited;
}
}
return undefined;
}
/**
* Generate JS expressions for `@import` side effects (`style` exportType only).
* @param {NormalModule} module the module to generate CSS text for
* @param {GenerateContext} generateContext the generate context
* @returns {string[]} JS expressions, one per `@import` dependency
*/
_generateImportExpressions(module, generateContext) {
const { moduleGraph, concatenationScope } = generateContext;
const parts = [];
for (const dep of module.dependencies) {
if (!(dep instanceof CssImportDependency)) continue;
const depModule = /** @type {CssModule} */ (moduleGraph.getModule(dep));
if (concatenationScope && concatenationScope.isModuleInScope(depModule)) {
continue;
}
parts.push(
generateContext.runtimeTemplate.moduleExports({
module: depModule,
chunkGraph: generateContext.chunkGraph,
request: depModule.userRequest,
weak: false,
runtimeRequirements: generateContext.runtimeRequirements
})
);
}
return parts;
}
/**
* Recursively merge `@import`'d CSS into a single Source (text/css-style-sheet only).
* @param {NormalModule} module the module to render
* @param {GenerateContext} generateContext the generate context
* @param {Set<NormalModule>} ancestors modules on the current path (cycle guard)
* @returns {Source | null} merged CSS source, or null when the module has no content
*/
_renderMergedCss(module, generateContext, ancestors) {
if (ancestors.has(module)) return null;
ancestors.add(module);
try {
const { moduleGraph } = generateContext;
/** @type {Source[]} */
const parts = [];
for (const dep of module.dependencies) {
if (!(dep instanceof CssImportDependency)) continue;
const depModule = /** @type {CssModule} */ (moduleGraph.getModule(dep));
if (!depModule) continue;
const depExportType = depModule.exportType;
if (depExportType !== "text" && depExportType !== "css-style-sheet") {
continue;
}
const depMerged = this._renderMergedCss(
depModule,
generateContext,
ancestors
);
if (depMerged) parts.push(depMerged);
}
const own = this._renderCss(module, generateContext);
if (own) parts.push(own);
if (parts.length === 0) return null;
if (parts.length === 1) return parts[0];
return new ConcatSource(...parts);
} finally {
ancestors.delete(module);
}
}
/**
* Generate CSS source for the current module
* @param {NormalModule} module the module to generate CSS source for
* @param {GenerateContext} generateContext the generate context
* @returns {Source | null} the CSS source
*/
_renderCss(module, generateContext) {
const moduleSourceContent = /** @type {Source} */ (
this.generate(module, {
...generateContext,
type: CSS_TYPE
})
);
if (!moduleSourceContent) {
return null;
}
const compilation = generateContext.runtimeTemplate.compilation;
const undoPath = "";
const hooks = CssModulesPlugin.getCompilationHooks(compilation);
return CssModulesPlugin.renderModule(
/** @type {CssModule} */ (module),
{
undoPath,
moduleSourceContent,
moduleFactoryCache: this._moduleFactoryCache,
runtimeTemplate: generateContext.runtimeTemplate
},
hooks
);
}
/**
* Applies the devtool filename template to the sources of a map that is inlined
* into the module's own output: `SourceMapDevToolPlugin` only rewrites maps of
* emitted assets, so this one would keep raw module identifiers.
* @param {RawSourceMap} map the map to rename the sources of
* @param {GenerateContext} generateContext the generate context
* @returns {RawSourceMap} the map with devtool source names
*/
_nameMapSources(map, generateContext) {
const { chunkGraph, runtimeTemplate } = generateContext;
const { compilation } = runtimeTemplate;
const outputOptions = compilation.outputOptions;
const absolutify = makePathsAbsolute.bindContextCache(
/** @type {string} */ (compilation.options.context),
compilation.compiler.root
);
const options = {
moduleFilenameTemplate:
outputOptions.devtoolModuleFilenameTemplate ||
ModuleFilenameHelpers.DEFAULT_MODULE_FILENAME_TEMPLATE,
namespace: outputOptions.devtoolNamespace || ""
};
const contextInfo = {
requestShortener: compilation.requestShortener,
chunkGraph,
hashFunction: outputOptions.hashFunction
};
/** @type {Set<string>} */
const usedNames = new Set();
const sources = map.sources.map((source) => {
if (!source.startsWith("webpack://")) return source;
const absoluteSource = absolutify(source.slice(10));
// Sources a loader reported for files that are not modules (e.g. a less
// partial) are templated as a path, like `SourceMapDevToolPlugin` does
const module = compilation.findModule(absoluteSource) || absoluteSource;
if (typeof module === "string" && URL_SCHEME_REGEXP.test(module)) {
return module;
}
let name = ModuleFilenameHelpers.createFilename(
module,
options,
contextInfo
);
// Same disambiguation as `SourceMapDevToolPlugin`: modules sharing a
// resource path (e.g. one file imported under two layers) collide
if (usedNames.has(name)) {
name = ModuleFilenameHelpers.createFilename(
module,
{
moduleFilenameTemplate:
outputOptions.devtoolFallbackModuleFilenameTemplate ||
ModuleFilenameHelpers.DEFAULT_FALLBACK_MODULE_FILENAME_TEMPLATE,
namespace: options.namespace
},
contextInfo
);
while (usedNames.has(name)) name += "*";
}
usedNames.add(name);
return name;
});
return { ...map, sources };
}
/**
* Offer CSS bound for a JS string literal to `renderEmbeddedSource`. No `.css`
* asset carries this text, so an asset-level minimizer never sees it.
* @param {Source} cssSource the CSS source
* @param {NormalModule} module the module
* @param {GenerateContext} generateContext the generate context
* @returns {Source} the source a tap returned, or the original
*/
_renderEmbeddedSource(cssSource, module, generateContext) {
const { compilation } = generateContext.runtimeTemplate;
return compilation._resolveEmbeddedSource(cssSource, {
type: CSS_TYPE,
hostType: JAVASCRIPT_TYPE,
module
});
}
/**
* Convert a CSS Source into a JS string literal, splicing `__webpack_require__.h()` for `[fullhash]` placeholders.
* @param {Source} cssSource the CSS source
* @param {import("../../declarations/WebpackOptions").DevTool | undefined} devtool the devtool option
* @param {GenerateContext} generateContext the generate context
* @returns {Source} a Source representing a JS string literal
*/
_cssToJsLiteral(cssSource, devtool, generateContext) {
const { source, map } = cssSource.sourceAndMap();
let content = /** @type {string} */ (source);
// A `renderEmbeddedSource` tap may hand back a mapped source whatever
// `devtool` says, so asking for no map at all is checked here too — this
// data URI would ship the sources inside the bundle. `hidden` is the same
// answer for the same reason.
const wanted = typeof devtool === "string" && !devtool.includes("hidden");
if (map && wanted) {
const namedMap = this._nameMapSources(map, generateContext);
const inlineMap =
typeof devtool === "string" && devtool.includes("nosources")
? { ...namedMap, sourcesContent: undefined }
: namedMap;
const base64Map = Buffer.from(JSON.stringify(inlineMap), "utf8").toString(
"base64"
);
const trailingNewline = content.endsWith("\n") ? "" : "\n";
content += `${trailingNewline}/*# sourceMappingURL=data:application/json;charset=utf-8;base64,${base64Map}*/`;
}
if (!content.includes(PUBLIC_PATH_FULL_HASH)) {
return new RawSource(JSON.stringify(content));
}
const result = new ConcatSource();
let last = 0;
walkFullHashPlaceholders(content, (start, end, length) => {
result.add(JSON.stringify(content.slice(last, start)));
result.add(
length === 0
? ` + ${RuntimeGlobals.getFullHash}() + `
: ` + ${RuntimeGlobals.getFullHash}().slice(0, ${length}) + `
);
last = end;
});
result.add(JSON.stringify(content.slice(last)));
generateContext.runtimeRequirements.add(RuntimeGlobals.getFullHash);
return result;
}
/**
* Generate the merged CSS text for a module, optionally wrapped as a
* JS string literal for embedding in JavaScript output.
* @param {NormalModule} module the module
* @param {GenerateContext} generateContext the generate context
* @param {boolean} asJsLiteral wrap the result as a JS string literal
* @returns {Source} the CSS text source
*/
_generateCssText(module, generateContext, asJsLiteral) {
const cssSource = this._renderMergedCss(module, generateContext, new Set());
const effectiveCharset = this._getEffectiveCharset(
module,
generateContext.moduleGraph
);
let result;
if (effectiveCharset !== undefined) {
const prefix = ` "${effectiveCharset}";\n`;
result = cssSource
? new ConcatSource(prefix, cssSource)
: new RawSource(prefix);
} else {
result = cssSource;
}
// Only text bound for a JS literal. `CSS_TEXT_TYPE` is an HTML inline
// `<style>` / `style=""`, and whether that is CSS at all depends on the
// element's `type` — a call the HTML layer makes and `htmlMinify` honors,
// which is not visible here. After the `@charset` prefix, so a tap sees a
// whole stylesheet.
if (result && asJsLiteral) {
result = this._renderEmbeddedSource(result, module, generateContext);
}
if (asJsLiteral) {
const devtool =
generateContext.runtimeTemplate.compilation.options.devtool;
return result
? this._cssToJsLiteral(result, devtool, generateContext)
: new RawSource('""');
}
return result || new RawSource("");
}
}
module.exports = CssGenerator;