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,733 lines (1,560 loc) • 88.3 kB
JavaScript
/*
MIT License http://www.opensource.org/licenses/mit-license.php
Author Tobias Koppers @sokra
*/
"use strict";
const path = require("path");
const vm = require("vm");
const { CSS_MODULE_TYPE_AUTO } = require("../ModuleTypeConstants");
const Parser = require("../Parser");
const ConstDependency = require("../dependencies/ConstDependency");
const CssIcssExportDependency = require("../dependencies/CssIcssExportDependency");
const CssIcssImportDependency = require("../dependencies/CssIcssImportDependency");
const CssIcssSymbolDependency = require("../dependencies/CssIcssSymbolDependency");
const CssImportDependency = require("../dependencies/CssImportDependency");
const CssUrlDependency = require("../dependencies/CssUrlDependency");
const StaticExportsDependency = require("../dependencies/StaticExportsDependency");
const CommentCompilationWarning = require("../errors/CommentCompilationWarning");
const ModuleDependencyWarning = require("../errors/ModuleDependencyWarning");
const UnsupportedFeatureWarning = require("../errors/UnsupportedFeatureWarning");
const WebpackError = require("../errors/WebpackError");
const LocConverter = require("../util/LocConverter");
const binarySearchBounds = require("../util/binarySearchBounds");
const { parseResource } = require("../util/identifier");
const {
createMagicCommentContext,
webpackCommentRegExp
} = require("../util/magicComment");
const topologicalSort = require("../util/topologicalSort");
const walkCssTokens = require("./walkCssTokens");
/** @typedef {import("../Module").BuildInfo} BuildInfo */
/** @typedef {import("../Module").BuildMeta} BuildMeta */
/** @typedef {import("../Parser").ParserState} ParserState */
/** @typedef {import("../Parser").PreparsedAst} PreparsedAst */
/** @typedef {import("./walkCssTokens").CssTokenCallbacks} CssTokenCallbacks */
/** @typedef {import("../../declarations/WebpackOptions").CssAutoOrModuleParserOptions} CssAutoOrModuleParserOptions */
/** @typedef {import("../../declarations/WebpackOptions").CssModuleParserOptions} CssModuleParserOptions */
/** @typedef {import("./CssModule")} CssModule */
/** @typedef {[number, number]} Range */
/** @typedef {{ line: number, column: number }} Position */
/** @typedef {{ value: string, range: Range, loc: { start: Position, end: Position } }} Comment */
const CC_COLON = ":".charCodeAt(0);
const CC_SEMICOLON = ";".charCodeAt(0);
const CC_COMMA = ",".charCodeAt(0);
const CC_LEFT_PARENTHESIS = "(".charCodeAt(0);
const CC_RIGHT_PARENTHESIS = ")".charCodeAt(0);
const CC_LOWER_F = "f".charCodeAt(0);
const CC_UPPER_F = "F".charCodeAt(0);
const CC_RIGHT_CURLY = "}".charCodeAt(0);
const CC_HYPHEN_MINUS = "-".charCodeAt(0);
const CC_TILDE = "~".charCodeAt(0);
const CC_EQUAL = "=".charCodeAt(0);
const CC_FULL_STOP = ".".charCodeAt(0);
const CC_EXCLAMATION = "!".charCodeAt(0);
const CC_AMPERSAND = "&".charCodeAt(0);
// https://www.w3.org/TR/css-syntax-3/#newline
// We don't have `preprocessing` stage, so we need specify all of them
const STRING_MULTILINE = /\\[\n\r\f]/g;
// https://www.w3.org/TR/css-syntax-3/#whitespace
const TRIM_WHITE_SPACES = /(^[ \t\n\r\f]*|[ \t\n\r\f]*$)/g;
const UNESCAPE = /\\([0-9a-f]{1,6}[ \t\n\r\f]?|[\s\S])/gi;
const IMAGE_SET_FUNCTION = /^(?:-\w+-)?image-set$/i;
const OPTIONALLY_VENDOR_PREFIXED_KEYFRAMES_AT_RULE = /^@(?:-\w+-)?keyframes$/;
const COMPOSES_PROPERTY = /^(?:composes|compose-with)$/i;
const IS_MODULES = /\.modules?\.[^.]+$/i;
const CSS_COMMENT = /\/\*((?!\*\/)[\s\S]*?)\*\//g;
/**
* Returns matches.
* @param {RegExp} regexp a regexp
* @param {string} str a string
* @returns {RegExpExecArray[]} matches
*/
const matchAll = (regexp, str) => {
/** @type {RegExpExecArray[]} */
const result = [];
/** @type {null | RegExpExecArray} */
let match;
// Use a while loop with exec() to find all matches
while ((match = regexp.exec(str)) !== null) {
result.push(match);
}
// Return an array to be easily iterable (note: a true spec-compliant polyfill
// returns an iterator object, but an array spread often suffices for basic use)
return result;
};
/**
* Returns normalized url.
* @param {string} str url string
* @param {boolean} isString is url wrapped in quotes
* @returns {string} normalized url
*/
const normalizeUrl = (str, isString) => {
// Remove extra spaces and newlines:
// `url("im\
// g.png")`
if (isString) {
str = str.replace(STRING_MULTILINE, "");
}
str = str
// Remove unnecessary spaces from `url(" img.png ")`
.replace(TRIM_WHITE_SPACES, "")
// Unescape
.replace(UNESCAPE, (match) => {
if (match.length > 2) {
return String.fromCharCode(Number.parseInt(match.slice(1).trim(), 16));
}
return match[1];
});
if (/^data:/i.test(str)) {
return str;
}
if (str.includes("%")) {
// Convert `url('%2E/img.png')` -> `url('./img.png')`
try {
str = decodeURIComponent(str);
} catch (_err) {
// Ignore
}
}
return str;
};
const { escapeIdentifier, unescapeIdentifier } = walkCssTokens;
/**
* A custom property is any property whose name starts with two dashes (U+002D HYPHEN-MINUS), like --foo.
* The <custom-property-name> production corresponds to this:
* it’s defined as any <dashed-ident> (a valid identifier that starts with two dashes),
* except -- itself, which is reserved for future use by CSS.
* @param {string} identifier identifier
* @returns {boolean} true when identifier is dashed, otherwise false
*/
const isDashedIdentifier = (identifier) =>
identifier.startsWith("--") && identifier.length >= 3;
/** @type {Record<string, number>} */
const PREDEFINED_COUNTER_STYLES = {
decimal: 1,
"decimal-leading-zero": 1,
"arabic-indic": 1,
armenian: 1,
"upper-armenian": 1,
"lower-armenian": 1,
bengali: 1,
cambodian: 1,
khmer: 1,
"cjk-decimal": 1,
devanagari: 1,
georgian: 1,
gujarati: 1,
/* cspell:disable-next-line */
gurmukhi: 1,
hebrew: 1,
kannada: 1,
lao: 1,
malayalam: 1,
mongolian: 1,
myanmar: 1,
oriya: 1,
persian: 1,
"lower-roman": 1,
"upper-roman": 1,
tamil: 1,
telugu: 1,
thai: 1,
tibetan: 1,
"lower-alpha": 1,
"lower-latin": 1,
"upper-alpha": 1,
"upper-latin": 1,
"lower-greek": 1,
hiragana: 1,
/* cspell:disable-next-line */
"hiragana-iroha": 1,
katakana: 1,
/* cspell:disable-next-line */
"katakana-iroha": 1,
disc: 1,
circle: 1,
square: 1,
"disclosure-open": 1,
"disclosure-closed": 1,
"cjk-earthly-branch": 1,
"cjk-heavenly-stem": 1,
"japanese-informal": 1,
"japanese-formal": 1,
"korean-hangul-formal": 1,
/* cspell:disable-next-line */
"korean-hanja-informal": 1,
/* cspell:disable-next-line */
"korean-hanja-formal": 1,
"simp-chinese-informal": 1,
"simp-chinese-formal": 1,
"trad-chinese-informal": 1,
"trad-chinese-formal": 1,
"cjk-ideographic": 1,
"ethiopic-numeric": 1
};
/** @type {Record<string, number>} */
const GLOBAL_VALUES = {
// Global values
initial: Infinity,
inherit: Infinity,
unset: Infinity,
revert: Infinity,
"revert-layer": Infinity
};
/** @type {Record<string, number>} */
const GRID_AREA_OR_COLUMN_OR_ROW = {
auto: Infinity,
span: Infinity,
...GLOBAL_VALUES
};
/** @type {Record<string, number>} */
const GRID_AUTO_COLUMNS_OR_ROW = {
"min-content": Infinity,
"max-content": Infinity,
auto: Infinity,
...GLOBAL_VALUES
};
/** @type {Record<string, number>} */
const GRID_AUTO_FLOW = {
row: 1,
column: 1,
dense: 1,
...GLOBAL_VALUES
};
/** @type {Record<string, number>} */
const GRID_TEMPLATE_AREAS = {
// Special
none: 1,
...GLOBAL_VALUES
};
/** @type {Record<string, number>} */
const GRID_TEMPLATE_COLUMNS_OR_ROWS = {
none: 1,
subgrid: 1,
masonry: 1,
"max-content": Infinity,
"min-content": Infinity,
auto: Infinity,
...GLOBAL_VALUES
};
/** @type {Record<string, number>} */
const GRID_TEMPLATE = {
...GRID_TEMPLATE_AREAS,
...GRID_TEMPLATE_COLUMNS_OR_ROWS
};
/** @type {Record<string, number>} */
const GRID = {
"auto-flow": 1,
dense: 1,
...GRID_AUTO_COLUMNS_OR_ROW,
...GRID_AUTO_FLOW,
...GRID_TEMPLATE_AREAS,
...GRID_TEMPLATE_COLUMNS_OR_ROWS
};
/**
* Gets known properties.
* @param {{ animation?: boolean, container?: boolean, customIdents?: boolean, grid?: boolean }=} options options
* @returns {Map<string, Record<string, number>>} list of known properties
*/
const getKnownProperties = (options = {}) => {
/** @type {Map<string, Record<string, number>>} */
const knownProperties = new Map();
if (options.animation) {
knownProperties.set("animation", {
// animation-direction
normal: 1,
reverse: 1,
alternate: 1,
"alternate-reverse": 1,
// animation-fill-mode
forwards: 1,
backwards: 1,
both: 1,
// animation-iteration-count
infinite: 1,
// animation-play-state
paused: 1,
running: 1,
// animation-timing-function
ease: 1,
"ease-in": 1,
"ease-out": 1,
"ease-in-out": 1,
linear: 1,
"step-end": 1,
"step-start": 1,
// Special
none: Infinity, // No matter how many times you write none, it will never be an animation name
...GLOBAL_VALUES
});
knownProperties.set("animation-name", {
// Special
none: Infinity, // No matter how many times you write none, it will never be an animation name
...GLOBAL_VALUES
});
}
if (options.container) {
knownProperties.set("container", {
// container-type
normal: 1,
size: 1,
"inline-size": 1,
"scroll-state": 1,
// Special
none: Infinity,
...GLOBAL_VALUES
});
knownProperties.set("container-name", {
// Special
none: Infinity,
...GLOBAL_VALUES
});
}
if (options.customIdents) {
knownProperties.set("list-style", {
// list-style-position
inside: 1,
outside: 1,
// list-style-type
...PREDEFINED_COUNTER_STYLES,
// Special
none: Infinity,
...GLOBAL_VALUES
});
knownProperties.set("list-style-type", {
// list-style-type
...PREDEFINED_COUNTER_STYLES,
// Special
none: Infinity,
...GLOBAL_VALUES
});
knownProperties.set("system", {
cyclic: 1,
numeric: 1,
alphabetic: 1,
symbolic: 1,
additive: 1,
fixed: 1,
extends: 1,
...PREDEFINED_COUNTER_STYLES
});
knownProperties.set("fallback", {
...PREDEFINED_COUNTER_STYLES
});
knownProperties.set("speak-as", {
auto: 1,
bullets: 1,
numbers: 1,
words: 1,
"spell-out": 1,
...PREDEFINED_COUNTER_STYLES
});
}
if (options.grid) {
knownProperties.set("grid", GRID);
knownProperties.set("grid-area", GRID_AREA_OR_COLUMN_OR_ROW);
knownProperties.set("grid-column", GRID_AREA_OR_COLUMN_OR_ROW);
knownProperties.set("grid-column-end", GRID_AREA_OR_COLUMN_OR_ROW);
knownProperties.set("grid-column-start", GRID_AREA_OR_COLUMN_OR_ROW);
knownProperties.set("grid-row", GRID_AREA_OR_COLUMN_OR_ROW);
knownProperties.set("grid-row-end", GRID_AREA_OR_COLUMN_OR_ROW);
knownProperties.set("grid-row-start", GRID_AREA_OR_COLUMN_OR_ROW);
knownProperties.set("grid-template", GRID_TEMPLATE);
knownProperties.set("grid-template-areas", GRID_TEMPLATE_AREAS);
knownProperties.set("grid-template-columns", GRID_TEMPLATE_COLUMNS_OR_ROWS);
knownProperties.set("grid-template-rows", GRID_TEMPLATE_COLUMNS_OR_ROWS);
}
return knownProperties;
};
const EMPTY_COMMENT_OPTIONS = {
options: null,
errors: null
};
const CSS_MODE_TOP_LEVEL = 0;
const CSS_MODE_IN_BLOCK = 1;
const LOCAL_MODE = 0;
const GLOBAL_MODE = 1;
const eatUntilSemi = walkCssTokens.eatUntil(";");
const eatUntilLeftCurly = walkCssTokens.eatUntil("{");
/**
* Defines the css parser own options type used by this module.
* @typedef {object} CssParserOwnOptions
* @property {("pure" | "global" | "local" | "auto")=} defaultMode default mode
*/
/** @typedef {CssAutoOrModuleParserOptions & CssParserOwnOptions} CssParserOptions */
class CssParser extends Parser {
/**
* Creates an instance of CssParser.
* @param {CssParserOptions=} options options
*/
constructor(options = {}) {
super();
this.defaultMode =
typeof options.defaultMode !== "undefined" ? options.defaultMode : "pure";
this.options = {
url: true,
import: true,
namedExports: true,
animation: true,
container: true,
customIdents: true,
dashedIdents: true,
function: true,
grid: true,
...options
};
/** @type {Comment[] | undefined} */
this.comments = undefined;
this.magicCommentContext = createMagicCommentContext();
}
/**
* Processes the provided state.
* @param {ParserState} state parser state
* @param {string} message warning message
* @param {LocConverter} locConverter location converter
* @param {number} start start offset
* @param {number} end end offset
*/
_emitWarning(state, message, locConverter, start, end) {
const { line: sl, column: sc } = locConverter.get(start);
const { line: el, column: ec } = locConverter.get(end);
state.current.addWarning(
new ModuleDependencyWarning(state.module, new WebpackError(message), {
start: { line: sl, column: sc },
end: { line: el, column: ec }
})
);
}
/**
* Emits a build error for the provided range.
* @param {ParserState} state parser state
* @param {string} message error message
* @param {LocConverter} locConverter location converter
* @param {number} start start offset
* @param {number} end end offset
*/
_emitError(state, message, locConverter, start, end) {
const { line: sl, column: sc } = locConverter.get(start);
const { line: el, column: ec } = locConverter.get(end);
const err = new WebpackError(message);
err.module = state.module;
err.loc = {
start: { line: sl, column: sc },
end: { line: el, column: ec }
};
state.module.addError(err);
}
/**
* Parses the provided source and updates the parser state.
* @param {string | Buffer | PreparsedAst} source the source to parse
* @param {ParserState} state the parser state
* @returns {ParserState} the parser state
*/
parse(source, state) {
if (Buffer.isBuffer(source)) {
source = source.toString("utf8");
} else if (typeof source === "object") {
throw new Error("webpackAst is unexpected for the CssParser");
}
if (source[0] === "\uFEFF") {
source = source.slice(1);
}
const unescapeIdentifierCached = unescapeIdentifier.bindCache(
state.compilation.compiler.root
);
let mode = this.defaultMode;
const module = state.module;
if (
mode === "auto" &&
module.type === CSS_MODULE_TYPE_AUTO &&
IS_MODULES.test(
parseResource(/** @type {string} */ (module.getResource())).path
)
) {
mode = "local";
}
const isModules = mode === "global" || mode === "local";
const parsedModuleResource = parseResource(
/** @type {string} */ (module.getResource())
);
/**
* Check whether a request points back to the current module
* (e.g. `composes: foo from "./self.module.css"` inside `self.module.css`).
* Only relative requests are checked — aliases / package / absolute requests
* fall through to the normal import path. Requests with a `?query` or
* `#fragment` are only treated as self when the parent module's resource
* has the same query/fragment, since `NormalModuleFactory` keys modules
* on the full resource string.
* @param {string} request request string from `from "<request>"`
* @returns {boolean} true if request resolves to the current module
*/
const isSelfReferenceRequest = (request) => {
if (!/^\.{1,2}\//.test(request)) return false;
if (!module.context) return false;
const parsedRequest = parseResource(request);
if (parsedRequest.query !== parsedModuleResource.query) return false;
if (parsedRequest.fragment !== parsedModuleResource.fragment) {
return false;
}
try {
return (
path.resolve(module.context, parsedRequest.path) ===
parsedModuleResource.path
);
} catch (_err) {
return false;
}
};
const knownProperties = getKnownProperties({
animation: this.options.animation,
container: this.options.container,
customIdents: this.options.customIdents,
grid: this.options.grid
});
/** @type {BuildMeta} */
(module.buildMeta).isCssModule = isModules;
if (/** @type {CssModule} */ (module).exportType === "style") {
/** @type {BuildMeta} */
(module.buildMeta).needIdInConcatenation = true;
}
const locConverter = new LocConverter(source);
/** @type {number} */
let scope = CSS_MODE_TOP_LEVEL;
/** @type {boolean} */
let allowImportAtRule = true;
/** @type {[string, number, number, boolean?][]} */
const balanced = [];
let lastTokenEndForComments = 0;
/** @type {boolean} */
let isNextRulePrelude = isModules;
/** @type {number} */
let blockNestingLevel = 0;
/** @type {0 | 1 | undefined} */
let modeData;
/** @type {number} */
let counter = 0;
/** @type {string[]} */
let lastLocalIdentifiers = [];
const pureMode = isModules && Boolean(this.options.pure);
/** @type {boolean} */
let currentSelectorHasLocal = false;
/** Whether any comma-separated selector in the current rule's prelude was impure. */
let currentRuleHasImpureSelector = false;
/** Offset just after the previous `}` (or 0) — used as the prelude start. */
let currentRulePreludeStart = 0;
/** Pure-mode flags (only meaningful when `pureMode` is true). */
let pureNoCheck = false;
let pureIgnorePending = false;
let nextBlockChildrenSkip = false;
let nextBlockTreatAsLeaf = false;
let seenTopLevelRule = false;
// True after an at-rule keyword and before the next `{` or `;`. Used so
// identifiers inside the at-rule prelude (e.g. `min-width` inside
// `@media (min-width: 768px)`) don't get counted as declarations.
let inAtRulePrelude = false;
/**
* One entry per open block. `skipOwn` skips this rule's own check (set
* when the parent passed down `skipChildren`, e.g. `from`/`to` inside
* `@keyframes`). `skipChildren` is propagated to descendants. `ignored`
* is per-rule only (PCSL semantics for `cssmodules-pure-ignore`).
* `ancestorHadLocal` lets nested rules inherit purity from a
* local-bearing ancestor.
* @type {{
* ignored: boolean,
* skipOwn: boolean,
* skipChildren: boolean,
* treatAsLeaf: boolean,
* ancestorHadLocal: boolean,
* impure: boolean,
* hasDirectDecl: boolean,
* hasNestedBlock: boolean,
* isRulePrelude: boolean,
* preludeStart: number,
* preludeEnd: number,
* }[]}
*/
const pureBlockStack = [];
const PURE_IGNORE_RE = /^\s*cssmodules-pure-ignore(?:\s|$)/;
const PURE_NO_CHECK_RE = /^\s*cssmodules-pure-no-check(?:\s|$)/;
/**
* @returns {(typeof pureBlockStack)[number] | undefined} top of stack
*/
const pureTop = () => pureBlockStack[pureBlockStack.length - 1];
/**
* Was the parent rule pure overall (its own selectors pure or any
* ancestor pure)? Used both for ancestor-inheritance and `&`-resolution.
* @returns {boolean} true if any ancestor (self inclusive) provided a local
*/
const parentEffectivePure = () => {
const top = pureTop();
return top ? top.ancestorHadLocal : false;
};
/**
* Marks the just-finished comma-separated selector (or whole prelude
* at `{`) as impure if it lacks a local and no ancestor compensates.
*/
const finalizeSelector = () => {
if (!currentSelectorHasLocal && !parentEffectivePure()) {
currentRuleHasImpureSelector = true;
}
currentSelectorHasLocal = false;
};
/**
* Reports a pure-mode violation covering the entire rule prelude.
* @param {number} start prelude start offset
* @param {number} end prelude end offset (`{` position)
*/
const reportPureRule = (start, end) => {
const slice = source.slice(start, end);
const lead = /** @type {RegExpExecArray} */ (
/^(?:\s|\/\*[\s\S]*?\*\/)*/.exec(slice)
)[0].length;
const trail = /** @type {RegExpExecArray} */ (/\s*$/.exec(slice))[0]
.length;
const from = start + lead;
const to = end - trail;
if (to <= from) return;
this._emitError(
state,
`Selector "${source.slice(from, to)}" is not pure (pure selectors must contain at least one local class or id)`,
locConverter,
from,
to
);
};
/** @typedef {{ value?: string, importName?: string, localName?: string, request?: string }} IcssDefinition */
/** @type {Map<string, IcssDefinition>} */
const icssDefinitions = new Map();
// Tracks `composes: <name> from "<file>"` declarations to enforce a
// predictable file load order across rules (port of
// postcss-modules-extract-imports#138). Each rule's composes order
// is a partial ordering: if `.x` composes `b from "./b"` before
// `c from "./c"`, then `b.css` must load before `c.css` so `c` can
// override `b` in the cascade. Edges are added inline as the rule
// is parsed; at end-of-parse the first composes-import dep of each
// file is tagged with `sourceOrder` according to a topological
// sort (`NormalModule#build` reorders by `sourceOrder` for us).
/** @type {Map<string, Set<string>>} */
const composesGraph = new Map();
/** @type {Map<string, CssIcssImportDependency>} */
const composesFirstFileImport = new Map();
/** @type {string | undefined} */
let currentRulePrevComposesFile;
/** @type {Set<string>} */
const currentRuleComposesFiles = new Set();
/**
* Checks whether this css parser is next nested syntax.
* @param {string} input input
* @param {number} pos position
* @returns {boolean} true, when next is nested syntax
*/
const isNextNestedSyntax = (input, pos) => {
pos = walkCssTokens.eatWhitespaceAndComments(input, pos)[0];
if (
input.charCodeAt(pos) === CC_RIGHT_CURLY ||
(input.charCodeAt(pos) === CC_HYPHEN_MINUS &&
input.charCodeAt(pos + 1) === CC_HYPHEN_MINUS)
) {
return false;
}
const identifier = walkCssTokens.eatIdentSequence(input, pos);
if (!identifier) {
return true;
}
const leftCurly = eatUntilLeftCurly(input, pos);
const content = input.slice(identifier[0], leftCurly);
if (content.includes(";") || content.includes("}")) {
return false;
}
return true;
};
/**
* Checks whether this css parser is local mode.
* @returns {boolean} true, when in local scope
*/
const isLocalMode = () =>
modeData === LOCAL_MODE || (mode === "local" && modeData === undefined);
/**
* Returns end.
* @param {string} input input
* @param {number} start start
* @param {number} end end
* @returns {number} end
*/
const comment = (input, start, end) => {
if (!this.comments) this.comments = [];
const { line: sl, column: sc } = locConverter.get(start);
const { line: el, column: ec } = locConverter.get(end);
const value = input.slice(start + 2, end - 2);
/** @type {Comment} */
const comment = {
value,
range: [start, end],
loc: {
start: { line: sl, column: sc },
end: { line: el, column: ec }
}
};
this.comments.push(comment);
if (pureMode) {
if (PURE_IGNORE_RE.test(value)) {
pureIgnorePending = true;
} else if (
PURE_NO_CHECK_RE.test(value) &&
scope === CSS_MODE_TOP_LEVEL &&
!seenTopLevelRule
) {
pureNoCheck = true;
}
}
return end;
};
// Vanilla CSS stuff
/**
* Processes the provided input.
* @param {string} input input
* @param {number} start name start position
* @param {number} end name end position
* @returns {number} position after handling
*/
const processAtImport = (input, start, end) => {
const tokens = walkCssTokens.eatImportTokens(input, end, {
comment
});
if (!tokens[3]) return end;
const semi = tokens[3][1];
if (!tokens[0] || (tokens[0][4] && !isModules)) {
this._emitWarning(
state,
`Expected URL in '${input.slice(start, semi)}'`,
locConverter,
start,
semi
);
return end;
}
const urlToken = tokens[0];
/** @type {string} */
let url;
if (urlToken[4]) {
// URL given as identifier — resolve via CSS Modules @value.
const name = input.slice(urlToken[2], urlToken[3]);
const def = icssDefinitions.get(name);
if (!def) {
this._emitWarning(
state,
`Unknown '@value' identifier '${name}' in '${input.slice(start, semi)}'`,
locConverter,
start,
semi
);
// Consume the whole at-rule so the unresolved identifier
// doesn't get re-tokenized and accidentally substituted
// into a malformed `@import` in the output.
const dep = new ConstDependency("", [start, semi]);
module.addPresentationalDependency(dep);
return semi;
}
if (def.value === undefined) {
this._emitWarning(
state,
`'@value' identifier '${name}' was imported from another module and cannot be used as the URL of '@import' — only locally defined values are supported here`,
locConverter,
start,
semi
);
const dep = new ConstDependency("", [start, semi]);
module.addPresentationalDependency(dep);
return semi;
}
const raw = def.value.trim();
url =
(raw.startsWith('"') && raw.endsWith('"')) ||
(raw.startsWith("'") && raw.endsWith("'"))
? normalizeUrl(raw.slice(1, -1), true)
: normalizeUrl(raw, false);
} else {
url = normalizeUrl(input.slice(urlToken[2], urlToken[3]), true);
}
const newline = walkCssTokens.eatWhiteLine(input, semi);
const { options, errors: commentErrors } = this.parseCommentOptions([
end,
urlToken[1]
]);
if (commentErrors) {
for (const e of commentErrors) {
const { comment } = e;
state.module.addWarning(
new CommentCompilationWarning(
`Compilation error while processing magic comment(-s): /*${comment.value}*/: ${e.message}`,
comment.loc
)
);
}
}
if (options && options.webpackIgnore !== undefined) {
if (typeof options.webpackIgnore !== "boolean") {
const { line: sl, column: sc } = locConverter.get(start);
const { line: el, column: ec } = locConverter.get(newline);
state.module.addWarning(
new UnsupportedFeatureWarning(
`\`webpackIgnore\` expected a boolean, but received: ${options.webpackIgnore}.`,
{
start: { line: sl, column: sc },
end: { line: el, column: ec }
}
)
);
} else if (options.webpackIgnore) {
return newline;
}
}
if (url.length === 0) {
const { line: sl, column: sc } = locConverter.get(start);
const { line: el, column: ec } = locConverter.get(newline);
const dep = new ConstDependency("", [start, newline]);
module.addPresentationalDependency(dep);
dep.setLoc(sl, sc, el, ec);
return newline;
}
/** @type {undefined | string} */
let layer;
if (tokens[1]) {
layer = input.slice(tokens[1][0] + 6, tokens[1][1] - 1).trim();
}
/** @type {undefined | string} */
let supports;
if (tokens[2]) {
supports = input.slice(tokens[2][0] + 9, tokens[2][1] - 1).trim();
}
const last = tokens[2] || tokens[1] || tokens[0];
const mediaStart = walkCssTokens.eatWhitespaceAndComments(
input,
last[1]
)[0];
/** @type {undefined | string} */
let media;
if (mediaStart !== semi - 1) {
media = input.slice(mediaStart, semi - 1).trim();
}
const { line: sl, column: sc } = locConverter.get(start);
const { line: el, column: ec } = locConverter.get(newline);
const dep = new CssImportDependency(
url,
[start, newline],
mode === "local" || mode === "global" ? mode : undefined,
layer,
supports && supports.length > 0 ? supports : undefined,
media && media.length > 0 ? media : undefined
);
dep.setLoc(sl, sc, el, ec);
module.addDependency(dep);
// `text` and `css-style-sheet` parents inline the imported
// module's rendered CSS at build time, which means we read the
// imported module's `codeGenerationResults` (and through it the
// results of any assets the import references). Registering this
// as a code-generation dependency tells the compilation scheduler
// to generate the imported subtree before us.
const exportType = /** @type {import("./CssModule")} */ (module)
.exportType;
if (exportType === "text" || exportType === "css-style-sheet") {
module.addCodeGenerationDependency(dep);
}
return newline;
};
/**
* Process url function.
* @param {string} input input
* @param {number} end end position
* @param {string} name the name of function
* @returns {number} position after handling
*/
const processURLFunction = (input, end, name) => {
const string = walkCssTokens.eatString(input, end);
if (!string) return end;
const { options, errors: commentErrors } = this.parseCommentOptions([
lastTokenEndForComments,
end
]);
if (commentErrors) {
for (const e of commentErrors) {
const { comment } = e;
state.module.addWarning(
new CommentCompilationWarning(
`Compilation error while processing magic comment(-s): /*${comment.value}*/: ${e.message}`,
comment.loc
)
);
}
}
if (options && options.webpackIgnore !== undefined) {
if (typeof options.webpackIgnore !== "boolean") {
const { line: sl, column: sc } = locConverter.get(string[0]);
const { line: el, column: ec } = locConverter.get(string[1]);
state.module.addWarning(
new UnsupportedFeatureWarning(
`\`webpackIgnore\` expected a boolean, but received: ${options.webpackIgnore}.`,
{
start: { line: sl, column: sc },
end: { line: el, column: ec }
}
)
);
} else if (options.webpackIgnore) {
return end;
}
}
const value = normalizeUrl(
input.slice(string[0] + 1, string[1] - 1),
true
);
// Ignore `url()`, `url('')` and `url("")`, they are valid by spec
if (value.length === 0) return end;
const isUrl = name === "url" || name === "src";
const dep = new CssUrlDependency(
value,
[string[0], string[1]],
isUrl ? "string" : "url"
);
const { line: sl, column: sc } = locConverter.get(string[0]);
const { line: el, column: ec } = locConverter.get(string[1]);
dep.setLoc(sl, sc, el, ec);
module.addDependency(dep);
module.addCodeGenerationDependency(dep);
return string[1];
};
/**
* Process old url function.
* @param {string} input input
* @param {number} start start position
* @param {number} end end position
* @param {number} contentStart start position
* @param {number} contentEnd end position
* @returns {number} position after handling
*/
const processOldURLFunction = (
input,
start,
end,
contentStart,
contentEnd
) => {
const { options, errors: commentErrors } = this.parseCommentOptions([
lastTokenEndForComments,
end
]);
if (commentErrors) {
for (const e of commentErrors) {
const { comment } = e;
state.module.addWarning(
new CommentCompilationWarning(
`Compilation error while processing magic comment(-s): /*${comment.value}*/: ${e.message}`,
comment.loc
)
);
}
}
if (options && options.webpackIgnore !== undefined) {
if (typeof options.webpackIgnore !== "boolean") {
const { line: sl, column: sc } = locConverter.get(
lastTokenEndForComments
);
const { line: el, column: ec } = locConverter.get(end);
state.module.addWarning(
new UnsupportedFeatureWarning(
`\`webpackIgnore\` expected a boolean, but received: ${options.webpackIgnore}.`,
{
start: { line: sl, column: sc },
end: { line: el, column: ec }
}
)
);
} else if (options.webpackIgnore) {
return end;
}
}
let value = normalizeUrl(input.slice(contentStart, contentEnd), false);
// Ignore `url()`, `url('')` and `url("")`, they are valid by spec
if (value.length === 0) return end;
if (isModules) {
const def = icssDefinitions.get(value);
if (def) {
if (def.value !== undefined) {
const raw = def.value.trim();
value =
(raw.startsWith('"') && raw.endsWith('"')) ||
(raw.startsWith("'") && raw.endsWith("'"))
? normalizeUrl(raw.slice(1, -1), true)
: normalizeUrl(raw, false);
if (value.length === 0) return end;
} else {
this._emitWarning(
state,
`'@value' identifier '${value}' was imported from another module and cannot be used inside 'url()' — only locally defined values are supported here`,
locConverter,
start,
end
);
return end;
}
}
}
const dep = new CssUrlDependency(value, [start, end], "url");
const { line: sl, column: sc } = locConverter.get(start);
const { line: el, column: ec } = locConverter.get(end);
dep.setLoc(sl, sc, el, ec);
module.addDependency(dep);
module.addCodeGenerationDependency(dep);
return end;
};
/**
* Process image set function.
* @param {string} input input
* @param {number} start start position
* @param {number} end end position
* @returns {number} position after handling
*/
const processImageSetFunction = (input, start, end) => {
lastTokenEndForComments = end;
const values = walkCssTokens.eatImageSetStrings(input, end, {
comment
});
if (values.length === 0) return end;
for (const [index, string] of values.entries()) {
const value = normalizeUrl(
input.slice(string[0] + 1, string[1] - 1),
true
);
if (value.length === 0) return end;
const { options, errors: commentErrors } = this.parseCommentOptions([
index === 0 ? start : values[index - 1][1],
string[1]
]);
if (commentErrors) {
for (const e of commentErrors) {
const { comment } = e;
state.module.addWarning(
new CommentCompilationWarning(
`Compilation error while processing magic comment(-s): /*${comment.value}*/: ${e.message}`,
comment.loc
)
);
}
}
if (options && options.webpackIgnore !== undefined) {
if (typeof options.webpackIgnore !== "boolean") {
const { line: sl, column: sc } = locConverter.get(string[0]);
const { line: el, column: ec } = locConverter.get(string[1]);
state.module.addWarning(
new UnsupportedFeatureWarning(
`\`webpackIgnore\` expected a boolean, but received: ${options.webpackIgnore}.`,
{
start: { line: sl, column: sc },
end: { line: el, column: ec }
}
)
);
} else if (options.webpackIgnore) {
continue;
}
}
const dep = new CssUrlDependency(value, [string[0], string[1]], "url");
const { line: sl, column: sc } = locConverter.get(string[0]);
const { line: el, column: ec } = locConverter.get(string[1]);
dep.setLoc(sl, sc, el, ec);
module.addDependency(dep);
module.addCodeGenerationDependency(dep);
}
// Can contain `url()` inside, so let's return end to allow parse them
return end;
};
// CSS modules stuff
/**
* Returns resolved reexport (localName and importName).
* @param {string} value value to resolve
* @param {string=} localName override local name
* @param {boolean=} isCustomProperty true when it is custom property, otherwise false
* @returns {string | [string, string] | [string, string, string]} resolved reexport (`localName`, `importName` and optional `request` of the active `@value` import)
*/
const getReexport = (value, localName, isCustomProperty) => {
const reexport = icssDefinitions.get(
isCustomProperty ? `--${value}` : value
);
if (reexport) {
if (reexport.importName) {
const resolvedLocalName =
reexport.localName || (isCustomProperty ? `--${value}` : value);
return reexport.request
? [resolvedLocalName, reexport.importName, reexport.request]
: [resolvedLocalName, reexport.importName];
}
if (isCustomProperty) {
return /** @type {string} */ (reexport.value).slice(2);
}
return /** @type {string} */ (reexport.value);
}
if (localName) {
return [localName, value];
}
return value;
};
/**
* Process import or export.
* @param {0 | 1} type import or export
* @param {string} input input
* @param {number} pos start position
* @returns {number} position after parse
*/
const processImportOrExport = (type, input, pos) => {
pos = walkCssTokens.eatWhitespaceAndComments(input, pos)[0];
/** @type {string | undefined} */
let request;
if (type === 0) {
let cc = input.charCodeAt(pos);
if (cc !== CC_LEFT_PARENTHESIS) {
this._emitWarning(
state,
`Unexpected '${input[pos]}' at ${pos} during parsing of ':import' (expected '(')`,
locConverter,
pos,
pos
);
return pos;
}
pos++;
const stringStart = pos;
const str = walkCssTokens.eatString(input, pos);
if (!str) {
this._emitWarning(
state,
`Unexpected '${input[pos]}' at ${pos} during parsing of '${type === 0 ? ":import" : ":export"}' (expected string)`,
locConverter,
stringStart,
pos
);
return pos;
}
request = input.slice(str[0] + 1, str[1] - 1);
pos = str[1];
pos = walkCssTokens.eatWhitespaceAndComments(input, pos)[0];
cc = input.charCodeAt(pos);
if (cc !== CC_RIGHT_PARENTHESIS) {
this._emitWarning(
state,
`Unexpected '${input[pos]}' at ${pos} during parsing of ':import' (expected ')')`,
locConverter,
pos,
pos
);
return pos;
}
pos++;
pos = walkCssTokens.eatWhitespaceAndComments(input, pos)[0];
}
/**
* Creates a dep from the provided name.
* @param {string} name name
* @param {string} value value
* @param {number} start start of position
* @param {number} end end of position
*/
const createDep = (name, value, start, end) => {
if (type === 0) {
const dep = new CssIcssImportDependency(
/** @type {string} */
(request),
[0, 0],
/** @type {"local" | "global"} */
(mode),
value,
name
);
const { line: sl, column: sc } = locConverter.get(start);
const { line: el, column: ec } = locConverter.get(end);
dep.setLoc(sl, sc, el, ec);
module.addDependency(dep);
icssDefinitions.set(name, {
importName: value,
request: /** @type {string} */ (request)
});
} else if (type === 1) {
const dep = new CssIcssExportDependency(name, getReexport(value));
const { line: sl, column: sc } = locConverter.get(start);
const { line: el, column: ec } = locConverter.get(end);
dep.setLoc(sl, sc, el, ec);
module.addDependency(dep);
}
};
let needTerminate = false;
let balanced = 0;
/** @type {undefined | 0 | 1 | 2} */
let scope;
/** @typedef {[number, number]} Name */
/** @type {Name | undefined} */
let name;
/** @type {number | undefined} */
let value;
/** @type {CssTokenCallbacks} */
const callbacks = {
leftCurlyBracket: (_input, _start, end) => {
balanced++;
if (scope === undefined) {
scope = 0;
}
return end;
},
rightCurlyBracket: (_input, _start, end) => {
balanced--;
if (scope === 2) {
const [nameStart, nameEnd] = /** @type {Name} */ (name);
createDep(
input.slice(nameStart, nameEnd),
input.slice(value, end - 1).trim(),
nameEnd,
end - 1
);
scope = 0;
}
if (balanced === 0 && scope === 0) {
needTerminate = true;
}
return end;
},
identifier: (_input, start, end) => {
if (scope === 0) {
name = [start, end];
scope = 1;
}
return end;
},
colon: (_input, _start, end) => {
if (scope === 1) {
scope = 2;
value = walkCssTokens.eatWhitespace(input, end);
return value;
}
return end;
},
semicolon: (input, _start, end) => {
if (scope === 2) {
const [nameStart, nameEnd] = /** @type {Name} */ (name);
createDep(
input.slice(nameStart, nameEnd),
input.slice(value, end - 1),
nameEnd,
end - 1
);
scope = 0;
}
return end;
},
needTerminate: () => needTerminate
};
pos = walkCssTokens(input, pos, callbacks);
pos = walkCssTokens.eatWhiteLine(input, pos);
return pos;
};
/** @typedef {{ from: string, items: ({ localName: string, importName: string })[] }} ValueAtRuleImport */
/** @typedef {{ localName: string, value: string }} ValueAtRuleValue */
/**
* Parses value at rule params.
* @param {string} str value at-rule params
* @returns {ValueAtRuleImport | ValueAtRuleValue} parsed result
*/
const parseValueAtRuleParams = (str) => {
if (/from(\/\*|\s)(?:[\s\S]+)$/i.test(str)) {
str = str.replace(CSS_COMMENT, " ").trim().replace(/;$/, "");
const fromIdx = str.lastIndexOf("from");
const path = str
.slice(fromIdx + 5)
.trim()
.replace(/['"]/g, "");
let content = str.slice(0, fromIdx).trim();
if (content.startsWith("(") && content.endsWith(")")) {
content = content.slice(1, -1);
}
return {
from: path,
items: content.split(",").map((item) => {
item = item.trim();
if (item.includes(":")) {
const [local, remote] = item.split(":");
return { localName: local.trim(), importName: remote.trim() };
}
const asParts = item.split(/\s+as\s+/);
if (asParts.length === 2) {
return {
localName: asParts[1].trim(),
importName: asParts[0].trim()
};
}
return { localName: item, importName: item };
})
};
}
/** @type {string} */
let localName;
/** @type {string} */
let value;
const idx = str.indexOf(":");
if (idx !== -1) {
localName = str.slice(0, idx).replace(CSS_COMMENT, "").trim();
value = str.slice(idx + 1);
} else {
const mask = str.replace(CSS_COMMENT, (m) => " ".repeat(m.length));
const idx = mask.search(/\S\s/) + 1;
localName = str.slice(0, idx).replace(CSS_COMMENT, "").trim();
value = str.slice(idx + (str[idx] === " " ? 1 : 0));
}
if (value.length > 0 && !/^\s+$/.test(value.replace(CSS_COMMENT, ""))) {
value = value.trim();
}
return { localName, value };
};
/**
* Processes the provided input.
* @param {string} input input
* @param {number} start name start position
* @param {number} end name end position
* @returns {number} position after handling
*/
const processAtValue = (input, start, end) => {
const semi = eatUntilSemi(input, end);
const atRuleEnd = semi + 1;
const params = input.slice(end, semi);
const parsed = parseValueAtRuleParams(params);
if (
typeof (/** @type {ValueAtRuleImport} */ (parsed).from) !== "undefined"
) {
if (/** @type {ValueAtRuleImport} */ (parsed).from.length === 0) {
this._emitWarning(
state,
`Broken '@value' at-rule: ${input.slice(start, atRuleEnd)}'`,
locConverter,
start,
atRuleEnd
);
const dep = new ConstDependency("", [start, atRuleEnd]);
module.addPresentationalDependency(dep);
return atRuleEnd;
}
let { from, items } = /** @type {ValueAtRuleImport} */ (parsed);
for (const { importName, localName } of items) {
{
const reexport = icssDefinitions.get(from);
if (reexport && reexport.value) {
from = reexport.value.slice(1, -1);
}
const dep = new CssIcssImportDependency(
from,
[0, 0],
/** @type {"local" | "global"} */
(mode),
importName,
localName
);
const { line: sl, column: sc } = locConverter.get(start);
const { line: el, column: ec } = locConverter.get(end);
dep.setLoc(sl, sc, el, ec);
module.addDependency(dep);
icssDefinitions.set(localName, { importName, request: from });
}
{
const dep = new CssIcssExportDependency(
localName,
getReexport(localName),
undefined,
false,
CssIcssExportDependency.EXPORT_MODE.REPLACE
);
const { line: sl, column: sc } = locConverter.get(start);
const { line: el, column: ec } = locConverter.get(end);
dep.setLoc(sl, sc, el, ec);
module.addDependency(dep);
}
}
} else {
if (/** @type {ValueAtRuleValue} */ (parsed).localName.length === 0) {
this._emitWarning(
state,
`Broken '@value' at-rule: ${input.slice(start, atRuleEnd)}'`,
locConverter,
start,
atRuleEnd
);
const dep = new ConstDependency("", [start, atRuleEnd]);
module.addPresentationalDependency(dep);
return atRuleEnd;
}
const { localName, value } = /** @type {ValueAtRuleValue} */ (parsed);
const { line: sl, column: sc } = locConverter.get(start);
const { line: el, column: ec } = locConverter.get(end);
if (icssDefinitions.has(value)) {
const def =
/** @type {IcssDefinition} */
(icssDefinitions.get(value));
def.localName = value;
icssDefinitions.set(localName, def);
const dep = new CssIcssExportDependency(
localName,
getReexport(value)
);
dep.setLoc(sl, sc, el, ec);
module.addDependency(dep);
} else {
icssDefinitions.set(localName, { value });
const dep = new CssIcssExportDependency(localName, value);
dep.setLoc(sl, sc, el, ec);
module.addDependency(dep);
}
}
const dep = new ConstDependency("", [start, atRuleEnd]);
module.addPresentationalDependency(dep);
return atRuleEnd;
};
/**
* Process icss symbol.
* @param {string} name ICSS symbol name
* @param {number} start start position
* @param {number} end end position
* @returns {number} position after handling
*/
const processICSSSymbol = (name, start, end) => {
const def =
/** @type {IcssDefinition} */
(icssDefinitions.get(name));
const { line: sl, column: sc } = locConverter.get(start);
const { line: el, column: ec } = locConverter.get(end);
const dep = new CssIcssSymbolDependency(
def.localName || name,
[start, end],
def.value,
def.importName,
def.request
);
dep.setLoc(sl, sc, el, ec);
module.addDependency(dep);
return end;
};
/**
* Process local or global function.
* @param {string} input input
* @param {1 | 2} type type of function
* @param {number} start start position
* @param {number} end end position
* @returns {number} position after handling
*/
const processLocalOrGlobalFunction = (input, type, start, end) => {
// Replace `local(`/` or `global(` (handle legacy `:local(` or `:global(` too)
{
const isColon = input.charCodeAt(start - 1) === CC_COLON;
const dep = new ConstDependency("", [isColon ? start - 1 : start, end]);
module.addPresentationalDependency(dep);
}
end = walkCssTokens.consumeUntil(
input,
start,
{
identifier(input, start, end) {
if (type === 1) {
let identifier = unescapeIdentifierCached(
input.slice(start, end)
);
const { line: sl, column: sc } = locConverter.get(start);
const { line: el, column: ec } = locConverter.get(end);
const isDashedIdent = isDashedIdentifier(identifier);
if (isDashedIdent) {
identifier = identifier.slice(2);
}
const dep = new CssIcssExportDependency(
identifier,
getReexport(identifier),
[start, end],
true,
CssIcssExportDependency.EXPORT_MODE.ONCE,
isDashedIdent
? CssIcssExportDependency.EXPORT_TYPE.CUSTOM_VARIABLE
: CssIcssExportDependency.EXPORT_TYPE.NORMAL
);
dep.setLoc(sl, sc, el, ec);
module.addDependency(dep);
}
return end;
}
},
{},
{ onlyTopLevel: true, functionValue: true }
);
{
// Replace the last `)`
const dep = new ConstDependency("", [end, end + 1]);
module.addPresentationalDependency(dep);
}
return end;
};
/**
* Process local at rule.
* @param {string} input input
* @param {number} end name end position
* @param {{ string?: boolean, identifier?: boolean | RegExp }} options types which allowed to handle
* @returns {number} position after handling
*/
const processLocalAtRule = (input, end, options) => {
let found = false;
return walkCssTokens.consumeUntil(
input,
end,
{
string(_input, start, end) {
if (!found && options.string) {
const value = unescapeIdentifierCached(
input.slice(start + 1, end - 1)
);
const { line: sl, column: sc } = locConverter.get(start);
const { line: el, column: ec } = locConverter.get(end);
const dep = new CssIcssExportDependency(
value,
value,
[start, end],
true,
CssIcssExportDependency.EXPORT_MODE.ONCE
);
dep.setLoc(sl, sc, el, ec);
module.addDependency(dep);
found = true;
if (pureMode) currentSelectorHasLocal = true;
}
return end;
},
identifier(input, start, end) {
if (!found) {
const value = input.slice(start, end);
if (options.identifier) {
const identifier = unescapeIdentifierCached(value);
if (
options.identifier instanceof RegExp &&
options.identifier.test(identifier)
) {
return end;
}
const { line: sl, column: sc } = locConverter.get(start);
const { line: el, column: ec } = locConverter.get(end);
const dep = new CssIcssExportDependency(
identifier,
getReexport(identifier),
[start, end],
true,
CssIcssExportDependency.EXPORT_MODE.ONCE,
CssIcssExportDependency.EXPORT_TYPE.NORMAL
);
dep.setLoc(sl, sc, el, ec);
module.addDependency(dep);
found = true;
if (pureMode) currentSelectorHasLocal = true;
}
}
return end;
}
},
{
function: (input, s