Source code

Revision control

Copy as Markdown

Other Tools

/*
Author Ivan Kopeykin @vankop
*/
"use strict";
const { JAVASCRIPT_TYPE } = require("../ModuleSourceTypeConstants");
const { interpolate } = require("../TemplatedPathPlugin");
const WebpackError = require("../errors/WebpackError");
const { cssExportConvention } = require("../util/conventions");
const createHash = require("../util/createHash");
const { makePathsRelative } = require("../util/identifier");
const makeSerializable = require("../util/makeSerializable");
const memoize = require("../util/memoize");
const { digestNonNumericOnly } = require("../util/nonNumericOnlyHash");
const { updateHashFromSource } = require("../util/source");
const CssIcssImportDependency = require("./CssIcssImportDependency");
const NullDependency = require("./NullDependency");
const getCssTokens = memoize(() => require("../css/syntax"));
/** @typedef {import("webpack-sources").ReplaceSource} ReplaceSource */
/** @typedef {import("../../declarations/WebpackOptions").CssGeneratorExportsConvention} CssGeneratorExportsConvention */
/** @typedef {import("../../declarations/WebpackOptions").CssGeneratorLocalIdentName} CssGeneratorLocalIdentName */
/** @typedef {import("../../declarations/WebpackOptions").HashFunction} HashFunction */
/** @typedef {import("../css/CssGenerator")} CssGenerator */
/** @typedef {import("../css/CssModule")} CssModule */
/** @typedef {import("../ChunkGraph")} ChunkGraph */
/** @typedef {import("../Dependency").ExportsSpec} ExportsSpec */
/** @typedef {import("../Dependency").ReferencedExports} ReferencedExports */
/** @typedef {import("../Dependency").UpdateHashContext} UpdateHashContext */
/** @typedef {import("../Dependency")} Dependency */
/** @typedef {import("../DependencyTemplate").CssDependencyTemplateContext} DependencyTemplateContext */
/** @typedef {import("../ModuleGraph")} ModuleGraph */
/** @typedef {import("../RuntimeTemplate")} RuntimeTemplate */
/** @typedef {import("../css/CssParser").Range} Range */
/** @typedef {import("../Dependency").DependencyLocation} DependencyLocation */
/** @typedef {import("../serialization/ObjectMiddleware").ObjectDeserializerContext<(number | string | Value | Range | boolean | ExportMode | ExportType | DependencyLocation | undefined)[]>} ObjectDeserializerContext */
/** @typedef {import("../serialization/ObjectMiddleware").ObjectSerializerContext<(number | string | Value | Range | boolean | ExportMode | ExportType | DependencyLocation | undefined)[]>} ObjectSerializerContext */
/** @typedef {import("../util/Hash")} Hash */
/** @typedef {import("../util/runtime").RuntimeSpec} RuntimeSpec */
/** @typedef {import("../errors/WebpackError")} WebpackErrorType */
/** @typedef {string | [string, string] | [string, string, string]} Value */
/** @typedef {(name: string) => string | string[]} ExportsConventionFn */
// 0 - none, 1 - replace, 2 - append, 3 - once, 4 - self-reference
/** @typedef {0 | 1 | 2 | 3 | 4} ExportMode */
// 0 - normal, 1 - custom css variable, 2 - grid custom ident, 3 - composes
/** @typedef {0 | 1 | 2 | 3} ExportType */
/** @type {Record<"NONE" | "REPLACE" | "APPEND" | "ONCE" | "SELF_REFERENCE", ExportMode>} */
const EXPORT_MODE = {
NONE: 0,
REPLACE: 1,
APPEND: 2,
ONCE: 3,
SELF_REFERENCE: 4
};
/** @type {Record<"NORMAL" | "CUSTOM_VARIABLE" | "GRID_CUSTOM_IDENTIFIER" | "COMPOSES", ExportType>} */
const EXPORT_TYPE = {
NORMAL: 0,
CUSTOM_VARIABLE: 1,
GRID_CUSTOM_IDENTIFIER: 2,
COMPOSES: 3
};
// Hoisted out of `getLocalIdent`'s `prepareId` so the patterns + the replacer
// aren't recompiled/reallocated per interpolated id.
const IDENT_LEADING_INVALID_REGEXP = /^([.-]|[^a-z0-9_-])+/i;
const IDENT_INVALID_CHARS_REGEXP = /[^a-z0-9@_-]+/gi;
const IDENT_LOCAL_PLACEHOLDER_REGEXP = /\[local\]/g;
const IDENT_LEADING_PROTECT_REGEXP = /^((-?\d)|--)/;
/**
* `interpolate` `prepareId` hook — sanitize an id segment into a valid CSS ident.
* Pure (captures nothing), so it's shared rather than allocated per call.
* @param {string | number} id id segment
* @returns {string | number} sanitized id segment
*/
const prepareLocalIdentId = (id) => {
if (typeof id !== "string") return id;
return (
id
.replace(IDENT_LEADING_INVALID_REGEXP, "")
// We keep the `@` symbol because it can be used in the package name (e.g. `@company/package`), and if we replace it with `_`, a class conflict may occur.
// For example - `@import "@foo/package/style.module.css"` and `@import "foo/package/style.module.css"` (`foo` is a package, `package` is just a directory) will create a class conflict.
.replace(IDENT_INVALID_CHARS_REGEXP, "_")
);
};
/**
* Returns local ident.
* @param {string} local css local
* @param {CssModule} module module
* @param {ChunkGraph} chunkGraph chunk graph
* @param {RuntimeTemplate} runtimeTemplate runtime template
* @returns {string} local ident
*/
const getLocalIdent = (local, module, chunkGraph, runtimeTemplate) => {
const generator = /** @type {CssGenerator} */ (module.generator);
const localIdentName =
/** @type {CssGeneratorLocalIdentName} */
(generator.options.localIdentName);
const relativeResourcePath = makePathsRelative(
/** @type {string} */
(runtimeTemplate.compilation.compiler.context),
/** @type {string} */
(module.getResource()),
runtimeTemplate.compilation.compiler.root
);
const { uniqueName } = runtimeTemplate.outputOptions;
let localIdentHash = "";
let localIdentHashFull = "";
if (generator._localIdentNeedsHash) {
const hashSalt = generator.options.localIdentHashSalt;
const hashDigest =
/** @type {string} */
(generator.options.localIdentHashDigest);
const hashDigestLength = generator.options.localIdentHashDigestLength;
const hashFunction =
/** @type {HashFunction} */
(generator.options.localIdentHashFunction);
const hash = createHash(hashFunction);
if (hashSalt) {
hash.update(hashSalt);
}
if (uniqueName) {
hash.update(uniqueName);
}
hash.update(relativeResourcePath);
hash.update(local);
// Keep the untruncated digest so an inline `[fullhash:<digest>]` re-encodes
// with full entropy (see the `fullHash`/`fullHashDigest` passed below).
localIdentHashFull = hash.digest(hashDigest);
localIdentHash = localIdentHashFull.slice(0, hashDigestLength);
}
let contentHash = "";
if (generator._localIdentNeedsContentHash) {
const hash = createHash(runtimeTemplate.outputOptions.hashFunction);
const source = module.originalSource();
if (source) {
updateHashFromSource(hash, source);
}
if (module.error) {
hash.update(module.error.toString());
}
contentHash = digestNonNumericOnly(
hash,
/** @type {string} */ (runtimeTemplate.outputOptions.hashDigest),
/** @type {number} */ (runtimeTemplate.outputOptions.hashDigestLength)
);
}
let localIdent = interpolate(localIdentName, {
prepareId: prepareLocalIdentId,
filename: relativeResourcePath,
hash: localIdentHash,
// css-loader semantics: `[hash]` is the local ident hash, not the module
// hash (use `[modulehash]` for that).
hashAsFullHash: true,
// `[hash]`/`[contenthash]` are encoded with the output digest, but the
// `[fullhash]` local ident hash uses `localIdentHashDigest`, so re-encode it
// from its own (full) digest.
hashDigest: runtimeTemplate.outputOptions.hashDigest,
fullHash: localIdentHashFull,
fullHashDigest:
/** @type {string} */
(generator.options.localIdentHashDigest),
local,
uniqueName,
contentHash,
chunkGraph,
module
});
// TODO move this into interpolate
// `replace` on a non-matching global regexp returns the string unchanged, so
// the `.test` guard is redundant — one scan instead of two.
localIdent = localIdent.replace(IDENT_LOCAL_PLACEHOLDER_REGEXP, local);
// Protect the first character from unsupported values
return localIdent.replace(IDENT_LEADING_PROTECT_REGEXP, "_$1");
};
/**
* Computes the interpolated identifier for `(module, value, exportType)`.
* Module-level reference so `moduleGraph.cached` can use it as a stable
* computer key — repeated lookups during a build skip `cssExportConvention`,
* `getLocalIdent` (with its content / path hashing) and `escapeIdentifier`.
* @param {ModuleGraph} _moduleGraph module graph (unused, kept for `cached` signature)
* @param {CssModule} module css module the value resolves in
* @param {string} value raw value to interpolate
* @param {ExportType} exportType export type discriminator
* @param {ChunkGraph} chunkGraph chunk graph
* @param {RuntimeTemplate} runtimeTemplate runtime template
* @returns {string} interpolated identifier
*/
const computeInterpolatedIdentifier = (
_moduleGraph,
module,
value,
exportType,
chunkGraph,
runtimeTemplate
) => {
const generator = /** @type {CssGenerator} */ (module.generator);
const local = cssExportConvention(
value,
/** @type {CssGeneratorExportsConvention} */
(generator.options.exportsConvention)
)[0];
const prefix = exportType === EXPORT_TYPE.CUSTOM_VARIABLE ? "--" : "";
return (
prefix +
getCssTokens().escapeIdentifier(
getLocalIdent(local, module, chunkGraph, runtimeTemplate),
runtimeTemplate.compilation.compiler.root
)
);
};
/**
* One `:export { name: value }` style entry — a plain object so a module holds
* a single `Dependency` plus a flat array instead of thousands of `Dependency`
* instances. `_conventionNames`/`_valueConventionNames` memoize the convention
* expansion per entry.
* @typedef {object} CssExportEntry
* @property {string} name export name
* @property {Value} value value or [localName, importName, request?]
* @property {Range=} range source range to replace, when present
* @property {boolean} interpolate whether the value needs interpolation
* @property {ExportMode} exportMode export mode
* @property {ExportType} exportType export type
* @property {number} locStartLine per-export source start line (1-based; flat numbers instead of a nested `DependencyLocation` — entries are per-export hot allocations)
* @property {number} locStartColumn per-export source start column
* @property {number} locEndLine per-export source end line
* @property {number} locEndColumn per-export source end column
* @property {string[]=} _conventionNames memoized convention names for `name`
* @property {string[]=} _valueConventionNames memoized convention names for `value`
*/
/**
* Memoized `cssExportConvention(entry.name, convention)`.
* @param {CssExportEntry} entry entry
* @param {CssGeneratorExportsConvention} convention convention
* @returns {string[]} convention names
*/
const entryConventionNames = (entry, convention) => {
if (entry._conventionNames) return entry._conventionNames;
entry._conventionNames = cssExportConvention(entry.name, convention);
return entry._conventionNames;
};
/**
* Memoized `cssExportConvention(entry.value, convention)`. Caller guarantees
* `typeof entry.value === "string"`.
* @param {CssExportEntry} entry entry
* @param {CssGeneratorExportsConvention} convention convention
* @returns {string[]} convention names
*/
const entryValueConventionNames = (entry, convention) => {
if (entry._valueConventionNames) return entry._valueConventionNames;
entry._valueConventionNames = cssExportConvention(
/** @type {string} */ (entry.value),
convention
);
return entry._valueConventionNames;
};
class CssIcssExportDependency extends NullDependency {
/**
* One consolidated `:export` dependency for a whole CSS module.
* @param {CssExportEntry[]} entries export entries in source order
*/
constructor(entries) {
super();
/** @type {CssExportEntry[]} */
this.entries = entries;
/** @type {undefined | string} */
this._hashUpdate = undefined;
/** @type {undefined | Map<string, CssExportEntry[]>} lazily-built `name -> entries` index, reused across every importer's codegen */
this._entriesByName = undefined;
}
/**
* The `entries` grouped by export name, built once and reused. `entries` is
* immutable after construction/deserialization, so this stays valid for the
* dependency's lifetime (the composes/`@value` target of many importers).
* @returns {Map<string, CssExportEntry[]>} entries grouped by export name
*/
getEntriesByName() {
if (this._entriesByName === undefined) {
/** @type {Map<string, CssExportEntry[]>} */
const map = new Map();
for (const e of this.entries) {
const list = map.get(e.name);
if (list) list.push(e);
else map.set(e.name, [e]);
}
this._entriesByName = map;
}
return this._entriesByName;
}
get type() {
return "css :export";
}
/**
* Returns the exported names
* @param {ModuleGraph} moduleGraph module graph
* @returns {ExportsSpec | undefined} export names
*/
getExports(moduleGraph) {
const module = /** @type {CssModule} */ (moduleGraph.getParentModule(this));
const generator = /** @type {CssGenerator} */ (module.generator);
const convention =
/** @type {CssGeneratorExportsConvention} */
(generator.options.exportsConvention);
/** @type {{ name: string, canMangle: boolean }[]} */
const exports = [];
for (const entry of this.entries) {
if (
entry.exportMode === EXPORT_MODE.NONE ||
entry.exportMode === EXPORT_MODE.SELF_REFERENCE
) {
continue;
}
for (const name of entryConventionNames(entry, convention)) {
exports.push({ name, canMangle: true });
}
}
if (exports.length === 0) return undefined;
return { exports, dependencies: undefined };
}
/**
* Returns list of exports referenced by this dependency
* @param {ModuleGraph} moduleGraph module graph
* @param {RuntimeSpec} runtime the runtime for which the module is analysed
* @returns {ReferencedExports} referenced exports
*/
getReferencedExports(moduleGraph, runtime) {
// `composes: foo;` (SELF_REFERENCE) references the composed class
// (`entry.value`), not the composing class. Every other entry mode
// keeps the whole exports object referenced (legacy behaviour), so the
// presence of any such entry subsumes the self-reference names.
let hasNonSelfReference = false;
/** @type {ReferencedExports} */
const referenced = [];
let generator;
for (const entry of this.entries) {
if (
entry.exportMode === EXPORT_MODE.SELF_REFERENCE &&
typeof entry.value === "string"
) {
const module =
/** @type {CssModule} */
(moduleGraph.getParentModule(this));
generator = generator || /** @type {CssGenerator} */ (module.generator);
const names = entryValueConventionNames(
entry,
/** @type {CssGeneratorExportsConvention} */
(generator.options.exportsConvention)
);
for (const name of names) {
referenced.push({ name: [name], canMangle: true });
}
} else {
hasNonSelfReference = true;
}
}
if (hasNonSelfReference) {
return super.getReferencedExports(moduleGraph, runtime);
}
if (referenced.length === 0) {
return super.getReferencedExports(moduleGraph, runtime);
}
return referenced;
}
/**
* Returns warnings.
* @param {ModuleGraph} moduleGraph module graph
* @returns {WebpackError[] | null | undefined} warnings
*/
getWarnings(moduleGraph) {
/** @type {WebpackErrorType[] | null} */
let warnings = null;
let module;
let exportsInfo;
let convention;
for (const entry of this.entries) {
if (
entry.exportMode !== EXPORT_MODE.SELF_REFERENCE ||
typeof entry.value !== "string"
) {
continue;
}
if (module === undefined) {
module =
/** @type {CssModule | undefined} */
(moduleGraph.getParentModule(this));
if (!module) return warnings;
const generator = /** @type {CssGenerator} */ (module.generator);
convention =
/** @type {CssGeneratorExportsConvention} */
(generator.options.exportsConvention);
exportsInfo = moduleGraph.getExportsInfo(module);
}
const names = entryValueConventionNames(
entry,
/** @type {CssGeneratorExportsConvention} */ (convention)
);
const ei = /** @type {NonNullable<typeof exportsInfo>} */ (exportsInfo);
const isProvided = names.some((name) => ei.isExportProvided(name));
if (!isProvided) {
const error = new WebpackError(
`Self-referencing name "${entry.value}" not found`
);
error.module = module;
// Per-entry loc so Compilation attributes the warning precisely
// (it can't use the consolidated dependency's single loc). Built
// on demand — entries carry flat numbers, not location objects.
if (entry.locStartLine > 0) {
error.loc = {
start: { line: entry.locStartLine, column: entry.locStartColumn },
end: { line: entry.locEndLine, column: entry.locEndColumn }
};
}
if (warnings === null) warnings = [];
warnings.push(error);
}
}
return warnings;
}
/**
* Updates the hash with the data contributed by this instance.
* @param {Hash} hash hash to be updated
* @param {UpdateHashContext} context context
* @returns {void}
*/
updateHash(hash, { chunkGraph }) {
if (this._hashUpdate === undefined) {
const module =
/** @type {CssModule} */
(chunkGraph.moduleGraph.getParentModule(this));
const generator = /** @type {CssGenerator} */ (module.generator);
const convention =
/** @type {CssGeneratorExportsConvention} */
(generator.options.exportsConvention);
const localIdentName = JSON.stringify(generator.options.localIdentName);
// Concatenate each entry's contribution with no separator so the byte
// stream matches the legacy per-export `updateHash` sequence exactly
// (keeps module hashes stable for import-free modules).
let update = "";
for (const entry of this.entries) {
const names = entryConventionNames(entry, convention);
update += `exportsConvention|${JSON.stringify(names)}|localIdentName|${localIdentName}|value|${JSON.stringify(entry.value)}|range|${JSON.stringify(entry.range)}|interpolate|${entry.interpolate}|exportMode|${entry.exportMode}|exportType|${entry.exportType}`;
}
this._hashUpdate = update;
}
hash.update(this._hashUpdate);
}
/**
* Serializes this instance into the provided serializer context.
* @param {ObjectSerializerContext} context context
*/
serialize(context) {
const { write } = context;
write(this.entries.length);
for (const entry of this.entries) {
write(entry.name);
write(entry.value);
write(entry.range);
write(entry.interpolate);
write(entry.exportMode);
write(entry.exportType);
write(entry.locStartLine);
write(entry.locStartColumn);
write(entry.locEndLine);
write(entry.locEndColumn);
}
super.serialize(context);
}
/**
* Restores this instance from the provided deserializer context.
* @param {ObjectDeserializerContext} context context
*/
deserialize(context) {
const { read } = context;
const length = /** @type {number} */ (read());
/** @type {CssExportEntry[]} */
const entries = [];
for (let i = 0; i < length; i++) {
entries.push({
name: /** @type {string} */ (read()),
value: /** @type {Value} */ (read()),
range: /** @type {Range=} */ (read()),
interpolate: /** @type {boolean} */ (read()),
exportMode: /** @type {ExportMode} */ (read()),
exportType: /** @type {ExportType} */ (read()),
locStartLine: /** @type {number} */ (read()),
locStartColumn: /** @type {number} */ (read()),
locEndLine: /** @type {number} */ (read()),
locEndColumn: /** @type {number} */ (read())
});
}
this.entries = entries;
this._hashUpdate = undefined;
this._entriesByName = undefined;
super.deserialize(context);
}
}
/**
* @typedef {object} ResolutionCache
* @property {Map<CssModule, Map<string, CssExportEntry[]>>} exports module -> (export name -> entries) across all its export deps
* @property {Map<CssIcssImportDependency, CssModule | null>} targets import dep -> resolved target module
*/
/**
* Creates the transient per-`apply` cache shared by `resolve` and
* `resolveReferences`. It is GC'd when the codegen call returns and is never
* retained on the dependency.
* @returns {ResolutionCache} empty resolution cache
*/
const createResolutionCache = () => ({
exports: new Map(),
targets: new Map()
});
/**
* Shared read-only index for modules that own no export dep.
* @type {Map<string, CssExportEntry[]>}
*/
const EMPTY_INDEX = new Map();
/**
* Per-module `local name -> import deps` index, keyed by module and validated by
* `buildInfo` identity (a fresh object per build). `apply` runs once per source
* type (CSS and JS), so this survives both passes — the index is built once per
* build instead of once per `apply`. Weakly held, so it never keeps a module
* alive.
* @type {WeakMap<CssModule, { buildInfo: unknown, index: Map<string, CssIcssImportDependency[]> }>}
*/
const moduleImportIndexes = new WeakMap();
/**
* Get (or lazily resolve) the target module of an `@value`/composes import dep.
* The same import dep is followed by many entries within one codegen, so the
* `moduleGraph.getModule` lookup is memoized per dep.
* @param {ResolutionCache} cache resolution cache
* @param {ModuleGraph} moduleGraph module graph
* @param {CssIcssImportDependency} importDep import dependency
* @returns {CssModule | null} resolved target module, or `null` if none
*/
const getTargetModule = (cache, moduleGraph, importDep) => {
let module = cache.targets.get(importDep);
if (module === undefined) {
module = /** @type {CssModule | null} */ (moduleGraph.getModule(importDep));
cache.targets.set(importDep, module);
}
return module;
};
/**
* Get (or lazily build) the `name -> entries` index for `module`. There is one
* consolidated `CssIcssExportDependency` per module, and its own index is cached
* on the dep — so a module composed/imported by many others builds its index
* once per build rather than once per importer, and resolution does a single Map
* lookup instead of re-scanning `module.dependencies` per reference.
* @param {ResolutionCache} cache resolution cache
* @param {CssModule} module css module
* @returns {Map<string, CssExportEntry[]>} entries grouped by export name
*/
const getExportIndex = (cache, module) => {
let map = cache.exports.get(module);
if (map === undefined) {
map = EMPTY_INDEX;
for (const dep of module.dependencies) {
if (dep instanceof CssIcssExportDependency) {
map = dep.getEntriesByName();
break;
}
}
cache.exports.set(module, map);
}
return map;
};
/**
* Get (or lazily build) the `local name -> import deps` index for `module`, so
* `findImportDep` is a Map lookup instead of a linear scan of every dependency.
* Cached per build on the module (validated by `buildInfo` identity) so both the
* CSS and JS codegen passes reuse one scan of `module.dependencies`.
* @param {CssModule} module css module
* @returns {Map<string, CssIcssImportDependency[]>} import deps grouped by local name
*/
const getImportIndex = (module) => {
const buildInfo = module.buildInfo;
const cached = moduleImportIndexes.get(module);
if (cached !== undefined && cached.buildInfo === buildInfo) {
return cached.index;
}
/** @type {Map<string, CssIcssImportDependency[]>} */
const index = new Map();
for (const d of module.dependencies) {
if (!(d instanceof CssIcssImportDependency)) continue;
const list = index.get(d.localName);
if (list) list.push(d);
else index.set(d.localName, [d]);
}
moduleImportIndexes.set(module, { buildInfo, index });
return index;
};
/**
* Finds the active `CssIcssImportDependency` for a given local name. When a
* `request` is provided the lookup also requires the import dependency's
* `request` to match, so references between two `@value foo from "..."`
* declarations resolve through the import in scope at the reference site.
* @param {CssModule} module module owning the dependencies to search
* @param {string} localName local name
* @param {string=} request user request of the `@value` import to match
* @returns {CssIcssImportDependency | undefined} matching import dep, if any
*/
const findImportDep = (module, localName, request) => {
const list = getImportIndex(module).get(localName);
if (list === undefined) return undefined;
// Import deps keep dependency order, so `list[0]` is the first match (the
// legacy `firstMatch` fallback when no request-specific match exists).
if (request === undefined) return list[0];
for (const d of list) if (d.request === request) return d;
return list[0];
};
CssIcssExportDependency.Template = class CssIcssExportDependencyTemplate extends (
NullDependency.Template
) {
/**
* Resolves a `[localName, importName, request]` reference to its final
* value by following `@value` imports into other modules' consolidated
* export dependencies.
* @param {string} localName local name
* @param {string} importName import name
* @param {DependencyTemplateContext} templateContext the context object
* @param {string | undefined} request user request of the `@value` import
* @param {Set<CssExportEntry>=} seen cycle guard
* @param {ResolutionCache=} cache transient per-codegen index (shared with `resolveReferences` within one `apply`)
* @returns {string | undefined} found reference
*/
static resolve(
localName,
importName,
templateContext,
request,
seen = new Set(),
cache = createResolutionCache()
) {
const { moduleGraph } = templateContext;
const importDep = findImportDep(
/** @type {CssModule} */ (templateContext.module),
localName,
request
);
if (!importDep) return undefined;
const module = getTargetModule(cache, moduleGraph, importDep);
if (!module) return undefined;
const matches = getExportIndex(cache, module).get(importName);
const exportEntry = matches && matches[0];
if (!exportEntry) return undefined;
if (seen.has(exportEntry)) return undefined;
seen.add(exportEntry);
const { value, interpolate } = exportEntry;
if (Array.isArray(value)) {
return CssIcssExportDependencyTemplate.resolve(
value[0],
value[1],
{ ...templateContext, module },
value[2],
seen,
cache
);
}
if (interpolate) {
return CssIcssExportDependencyTemplate.getIdentifier(value, exportEntry, {
...templateContext,
module
});
}
return value;
}
/**
* Resolves all references of a `composes` entry, deduplicated.
* @param {CssExportEntry} entry composes entry
* @param {DependencyTemplateContext} templateContext template context
* @param {Set<CssExportEntry>} seen cycle guard
* @param {ResolutionCache} cache transient per-codegen index, discarded after the module's `apply` (so it isn't retained on the dependency)
* @returns {string[]} final names
*/
static resolveReferences(entry, templateContext, seen, cache) {
/** @type {string[]} */
const references = [];
if (seen.has(entry)) return references;
seen.add(entry);
if (Array.isArray(entry.value)) {
const importDep = findImportDep(
/** @type {CssModule} */ (templateContext.module),
entry.value[0],
entry.value[2]
);
if (!importDep) return references;
const module = getTargetModule(
cache,
templateContext.moduleGraph,
importDep
);
if (!module) return references;
// Same overridden context and target name for every matched entry — build once, not per entry.
const subContext = { ...templateContext, module };
const name = /** @type {string[]} */ (entry.value)[1];
const matches = getExportIndex(cache, module).get(name);
if (matches) {
for (const d of matches) {
if (Array.isArray(d.value)) {
references.push(
...CssIcssExportDependencyTemplate.resolveReferences(
d,
subContext,
seen,
cache
)
);
} else {
references.push(
CssIcssExportDependencyTemplate.getIdentifier(
d.value,
d,
subContext
)
);
}
}
}
} else {
references.push(
CssIcssExportDependencyTemplate.getIdentifier(
/** @type {string} */ (entry.value),
entry,
templateContext
)
);
const matches = getExportIndex(
cache,
/** @type {CssModule} */ (templateContext.module)
).get(/** @type {string} */ (entry.value));
if (matches) {
for (const d of matches) {
if (d.exportType !== EXPORT_TYPE.COMPOSES) continue;
if (Array.isArray(d.value)) {
references.push(
...CssIcssExportDependencyTemplate.resolveReferences(
d,
templateContext,
seen,
cache
)
);
} else {
references.push(
CssIcssExportDependencyTemplate.getIdentifier(
d.value,
d,
templateContext
)
);
}
}
}
}
return references.length > 1 ? [...new Set(references)] : references;
}
/**
* Returns the interpolated identifier for an entry value.
* @param {string} value value to identifier
* @param {CssExportEntry} entry the entry
* @param {DependencyTemplateContext} templateContext the context object
* @returns {string} identifier
*/
static getIdentifier(value, entry, templateContext) {
if (!entry.interpolate) return value;
const { moduleGraph, module, chunkGraph, runtimeTemplate } =
templateContext;
return moduleGraph.cached(
computeInterpolatedIdentifier,
/** @type {CssModule} */ (module),
value,
entry.exportType,
chunkGraph,
runtimeTemplate
);
}
/**
* Applies the plugin by registering its hooks on the compiler.
* @param {Dependency} dependency the dependency for which the template should be applied
* @param {ReplaceSource} source the current replace source which can be modified
* @param {DependencyTemplateContext} templateContext the context object
* @returns {void}
*/
apply(dependency, source, templateContext) {
const dep = /** @type {CssIcssExportDependency} */ (dependency);
const { module: m, moduleGraph, runtime, cssData } = templateContext;
const module = /** @type {CssModule} */ (m);
const generator = /** @type {CssGenerator} */ (module.generator);
const isJs = templateContext.type === JAVASCRIPT_TYPE;
// All entries belong to this one module, so resolve its ExportsInfo once
// instead of re-resolving `module -> exportsInfo` per export name.
const exportsInfo = isJs ? moduleGraph.getExportsInfo(module) : undefined;
// Transient per-module indexes for composes resolution within this
// module's codegen; GC'd when apply returns (never retained on the dependency).
const cache = createResolutionCache();
for (const entry of dep.entries) {
if (!entry.range && !isJs) continue;
const isReference = Array.isArray(entry.value);
/** @type {string} */
let value;
if (entry.exportType === EXPORT_TYPE.COMPOSES && isJs) {
value = CssIcssExportDependencyTemplate.resolveReferences(
entry,
templateContext,
new Set(),
cache
).join(" ");
} else if (isReference) {
const resolved = CssIcssExportDependencyTemplate.resolve(
/** @type {string[]} */ (entry.value)[0],
/** @type {string[]} */ (entry.value)[1],
templateContext,
/** @type {string[]} */ (entry.value)[2],
new Set(),
cache
);
value = resolved || /** @type {string[]} */ (entry.value)[0];
} else {
value = CssIcssExportDependencyTemplate.getIdentifier(
/** @type {string} */ (entry.value),
entry,
templateContext
);
}
if (entry.exportType === EXPORT_TYPE.GRID_CUSTOM_IDENTIFIER) {
value += `-${entry.name}`;
}
if (isJs && entry.exportMode !== EXPORT_MODE.NONE) {
const names = entryConventionNames(
entry,
/** @type {CssGeneratorExportsConvention} */
(generator.options.exportsConvention)
);
/** @type {Set<string>} */
const allNames = new Set();
for (const name of names) {
if (
/** @type {NonNullable<typeof exportsInfo>} */ (exportsInfo)
.getExportInfo(name)
.getUsedName(name, runtime)
) {
allNames.add(name);
}
}
const unescaped = getCssTokens().unescapeIdentifier(
value,
templateContext.runtimeTemplate.compilation.compiler.root
);
const depLocLine = entry.locStartLine;
const depLocColumn = entry.locStartColumn;
for (const used of allNames) {
if (entry.exportMode === EXPORT_MODE.ONCE) {
if (cssData.exports.has(used)) continue;
cssData.exports.set(used, unescaped);
if (
depLocLine > 0 &&
cssData.exportLocs &&
!cssData.exportLocs.has(used)
) {
cssData.exportLocs.set(used, {
line: depLocLine,
column: depLocColumn
});
}
} else {
const originalValue =
entry.exportMode === EXPORT_MODE.REPLACE
? undefined
: cssData.exports.get(used);
cssData.exports.set(
used,
`${originalValue ? `${originalValue}${unescaped ? " " : ""}` : ""}${unescaped}`
);
if (
depLocLine > 0 &&
cssData.exportLocs &&
!cssData.exportLocs.has(used)
) {
cssData.exportLocs.set(used, {
line: depLocLine,
column: depLocColumn
});
}
}
}
} else if (
entry.range &&
templateContext.type !== JAVASCRIPT_TYPE &&
entry.exportMode !== EXPORT_MODE.APPEND &&
entry.exportMode !== EXPORT_MODE.SELF_REFERENCE
) {
source.replace(entry.range[0], entry.range[1] - 1, value);
}
}
}
};
/** @type {Record<"NONE" | "REPLACE" | "APPEND" | "ONCE" | "SELF_REFERENCE", ExportMode>} */
CssIcssExportDependency.EXPORT_MODE = EXPORT_MODE;
/** @type {Record<"NORMAL" | "CUSTOM_VARIABLE" | "GRID_CUSTOM_IDENTIFIER" | "COMPOSES", ExportType>} */
CssIcssExportDependency.EXPORT_TYPE = EXPORT_TYPE;
makeSerializable(
CssIcssExportDependency,
"webpack/lib/dependencies/CssIcssExportDependency"
);
module.exports = CssIcssExportDependency;