/
githubmirror
/
webpack
Обзор
Документация
Войти
/
githubmirror
/
webpack
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
v5.107.2
lib/css/CssParser.js
3 085 строк
86 KB
Xiao
fix: fix HMR for concatenated CSS modules with style exportType (#20911)
18 май 2026, 20:21
Не верифицирован
18 май 2026, 20:21
a5521a0
Код
Авторство
О чём код?
/* 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, start, end) => { // No need to handle `:` (COLON), because it's always a function const name = input .slice(start, end - 1) .replace(/\\/g, "") .toLowerCase(); const type = name === "local" ? 1 : name === "global" ? 2 : undefined; if (!found && type) { found = true; if (type === 1 && pureMode) currentSelectorHasLocal = true; return processLocalOrGlobalFunction(input, type, start, end); } if ( this.options.dashedIdents && isLocalMode() && (name === "var" || name === "style") ) { return processDashedIdent(input, end, end); } return end; } }, { onlyTopLevel: true, atRulePrelude: true } ); }; /** * Process dashed ident. * @param {string} input input * @param {number} start start position * @param {number} end end position * @returns {number} position after handling */ const processDashedIdent = (input, start, end) => { const customIdent = walkCssTokens.eatIdentSequence(input, start); if (!customIdent) return end; const identifier = unescapeIdentifierCached( input.slice(customIdent[0] + 2, customIdent[1]) ); const afterCustomIdent = walkCssTokens.eatWhitespaceAndComments( input, customIdent[1] )[0]; if ( input.charCodeAt(afterCustomIdent) === CC_LOWER_F || input.charCodeAt(afterCustomIdent) === CC_UPPER_F ) { const fromWord = walkCssTokens.eatIdentSequence( input, afterCustomIdent ); if ( !fromWord || input.slice(fromWord[0], fromWord[1]).toLowerCase() !== "from" ) { return end; } const from = walkCssTokens.eatIdentSequenceOrString( input, walkCssTokens.eatWhitespaceAndComments(input, fromWord[1])[0] ); if (!from) { return end; } const path = input.slice(from[0], from[1]); if (from[2] === true && path === "global") { const dep = new ConstDependency("", [customIdent[1], from[1]]); module.addPresentationalDependency(dep); return end; } else if (from[2] === false) { const { line: sl, column: sc } = locConverter.get(customIdent[0]); const { line: el, column: ec } = locConverter.get(from[1] - 1); const localName = `__ICSS_IMPORT_${counter++}__`; { const dep = new CssIcssImportDependency( path.slice(1, -1), [customIdent[0], from[1] - 1], /** @type {"local" | "global"} */ (mode), identifier, localName ); dep.setLoc(sl, sc, el, ec); module.addDependency(dep); } { const dep = new CssIcssExportDependency( identifier, getReexport(identifier, localName, true), [customIdent[0], from[1] - 1], true, CssIcssExportDependency.EXPORT_MODE.ONCE, CssIcssExportDependency.EXPORT_TYPE.CUSTOM_VARIABLE ); dep.setLoc(sl, sc, el, ec); module.addDependency(dep); } { const dep = new ConstDependency("", [fromWord[0], from[1]]); module.addPresentationalDependency(dep); return end; } } } else { const { line: sl, column: sc } = locConverter.get(customIdent[0]); const { line: el, column: ec } = locConverter.get(customIdent[1]); const dep = new CssIcssExportDependency( identifier, getReexport(identifier, undefined, true), [customIdent[0], customIdent[1]], true, CssIcssExportDependency.EXPORT_MODE.ONCE, CssIcssExportDependency.EXPORT_TYPE.CUSTOM_VARIABLE ); dep.setLoc(sl, sc, el, ec); module.addDependency(dep); return end; } return end; }; /** * Process local declaration. * @param {string} input input * @param {number} pos name start position * @param {number} end name end position * @returns {number} position after handling */ const processLocalDeclaration = (input, pos, end) => { pos = walkCssTokens.eatWhitespaceAndComments(input, pos)[0]; const identifier = walkCssTokens.eatIdentSequence(input, pos); if (!identifier) { return end; } const propertyNameStart = identifier[0]; pos = walkCssTokens.eatWhitespaceAndComments(input, identifier[1])[0]; if (input.charCodeAt(pos) !== CC_COLON) { return end; } pos += 1; // Remove prefix and lowercase const propertyName = input .slice(identifier[0], identifier[1]) .replace(/^(-\w+-)/, "") .toLowerCase(); if (isLocalMode() && knownProperties.has(propertyName)) { /** @type {[number, number, boolean?][]} */ const values = []; /** @type {Record<string, number>} */ let parsedKeywords = Object.create(null); const isGridProperty = Boolean(propertyName.startsWith("grid")); const isGridTemplate = isGridProperty ? Boolean( propertyName === "grid" || propertyName === "grid-template" || propertyName === "grid-template-columns" || propertyName === "grid-template-rows" ) : false; let afterExclamation = false; const end = walkCssTokens.consumeUntil( input, pos, { delim(input, start, end) { afterExclamation = input.charCodeAt(start) === CC_EXCLAMATION; return end; }, leftSquareBracket(input, start, end) { let i = end; while (true) { i = walkCssTokens.eatWhitespaceAndComments(input, i)[0]; const name = walkCssTokens.eatIdentSequence(input, i); if (!name) { break; } values.push(name); i = name[1]; } return end; }, string(_input, start, end) { if ( propertyName === "animation" || propertyName === "animation-name" ) { values.push([start, end, true]); } if ( propertyName === "grid" || propertyName === "grid-template" || propertyName === "grid-template-areas" ) { const areas = unescapeIdentifierCached( input.slice(start + 1, end - 1) ); const matches = matchAll(/\b\w+\b/g, areas); for (const match of matches) { const areaStart = start + 1 + match.index; values.push([areaStart, areaStart + match[0].length, false]); } } return end; }, identifier(input, start, end) { if (isGridTemplate) { return end; } if (afterExclamation) { afterExclamation = false; return end; } const identifier = input.slice(start, end); const keyword = identifier.toLowerCase(); parsedKeywords[keyword] = typeof parsedKeywords[keyword] !== "undefined" ? parsedKeywords[keyword] + 1 : 0; const keywords = /** @type {Record<string, number>} */ (knownProperties.get(propertyName)); if ( keywords[keyword] && parsedKeywords[keyword] < keywords[keyword] ) { return end; } values.push([start, end]); return end; }, comma(_input, _start, end) { parsedKeywords = {}; return end; } }, { function: (input, start, end) => { const name = input .slice(start, end - 1) .replace(/\\/g, "") .toLowerCase(); const type = name === "local" ? 1 : name === "global" ? 2 : undefined; if (type) { return processLocalOrGlobalFunction(input, type, start, end); } if ( this.options.dashedIdents && isLocalMode() && name === "var" ) { return processDashedIdent(input, end, end); } if (this.options.url) { if (name === "src" || name === "url") { return processURLFunction(input, end, name); } else if (IMAGE_SET_FUNCTION.test(name)) { return processImageSetFunction(input, start, end); } } return end; } }, { onlyTopLevel: !isGridTemplate, declarationValue: true } ); if (values.length > 0) { for (const value of values) { const { line: sl, column: sc } = locConverter.get(value[0]); const { line: el, column: ec } = locConverter.get(value[1]); const [start, end, isString] = value; const name = unescapeIdentifierCached( isString ? input.slice(start + 1, end - 1) : input.slice(start, end) ); const dep = new CssIcssExportDependency( name, getReexport(name), [start, end], true, CssIcssExportDependency.EXPORT_MODE.ONCE, isGridProperty ? CssIcssExportDependency.EXPORT_TYPE.GRID_CUSTOM_IDENTIFIER : CssIcssExportDependency.EXPORT_TYPE.NORMAL ); dep.setLoc(sl, sc, el, ec); module.addDependency(dep); } } return end; } else if (COMPOSES_PROPERTY.test(propertyName)) { if (lastLocalIdentifiers.length > 1) { const end = eatUntilSemi(input, pos); this._emitWarning( state, `Composition is only allowed when selector is single local class name not in "${lastLocalIdentifiers.join('", "')}"`, locConverter, pos, end ); return end; } if (lastLocalIdentifiers.length !== 1) return pos; const lastLocalIdentifier = lastLocalIdentifiers[0]; let end = pos; /** @type {Set<[number, number, boolean]>} */ const classNames = new Set(); while (true) { pos = walkCssTokens.eatWhitespaceAndComments(input, pos)[0]; let className = walkCssTokens.eatIdentSequence(input, pos); const ifFunction = className && input.charCodeAt(className[1]) === CC_LEFT_PARENTHESIS; let isGlobalFunction = false; if (className && ifFunction) { const name = input .slice(className[0], className[1]) .replace(/\\/g, "") .toLowerCase(); isGlobalFunction = name === "global"; pos = walkCssTokens.eatWhitespaceAndComments( input, className[1] + 1 )[0]; className = walkCssTokens.eatIdentSequence(input, pos); if (className) { pos = walkCssTokens.eatWhitespaceAndComments( input, className[1] )[0]; pos += 1; } } else if (className) { pos = walkCssTokens.eatWhitespaceAndComments( input, className[1] )[0]; pos = className[1]; } // True when we have multiple values const isComma = input.charCodeAt(pos) === CC_COMMA; const isSemicolon = input.charCodeAt(pos) === CC_SEMICOLON; const isRightCurly = input.charCodeAt(pos) === CC_RIGHT_CURLY; if (isComma || isSemicolon || isRightCurly) { if (className) { classNames.add([className[0], className[1], isGlobalFunction]); } for (const entry of classNames) { const [start, end, isGlobal] = entry; const identifier = unescapeIdentifierCached( input.slice(start, end) ); const dep = new CssIcssExportDependency( lastLocalIdentifier, getReexport(identifier), [start, end], !isGlobal, isGlobal ? CssIcssExportDependency.EXPORT_MODE.APPEND : CssIcssExportDependency.EXPORT_MODE.SELF_REFERENCE, CssIcssExportDependency.EXPORT_TYPE.COMPOSES ); 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); } classNames.clear(); if (isSemicolon || isRightCurly) { end = isSemicolon ? walkCssTokens.eatWhitespace(input, pos + 1) : pos; break; } pos += 1; } else if ( classNames.size > 0 && className && input.slice(className[0], className[1]).toLowerCase() === "from" ) { let from = walkCssTokens.eatString(input, pos); if (from) { const request = input.slice(from[0] + 1, from[1] - 1); const selfReference = isSelfReferenceRequest(request); if (!selfReference && !currentRuleComposesFiles.has(request)) { currentRuleComposesFiles.add(request); if ( currentRulePrevComposesFile !== undefined && currentRulePrevComposesFile !== request ) { let successors = composesGraph.get( currentRulePrevComposesFile ); if (!successors) { successors = new Set(); composesGraph.set(currentRulePrevComposesFile, successors); } successors.add(request); } currentRulePrevComposesFile = request; } for (const entry of classNames) { const [start, end] = entry; const identifier = unescapeIdentifierCached( input.slice(start, end) ); const { line: sl, column: sc } = locConverter.get(start); const { line: el, column: ec } = locConverter.get(end); if (selfReference) { // `composes: foo from "./self.module.css"` from inside // `self.module.css` — collapse to a self-reference, like // `composes: foo` without `from`. When the composed name // equals the local class name, it's a true no-op. if (identifier === lastLocalIdentifier) continue; const dep = new CssIcssExportDependency( lastLocalIdentifier, getReexport(identifier), [start, end], true, CssIcssExportDependency.EXPORT_MODE.SELF_REFERENCE, CssIcssExportDependency.EXPORT_TYPE.COMPOSES ); dep.setLoc(sl, sc, el, ec); module.addDependency(dep); continue; } const localName = `__ICSS_IMPORT_${counter++}__`; { const dep = new CssIcssImportDependency( request, [start, end], /** @type {"local" | "global"} */ (mode), identifier, localName ); dep.setLoc(sl, sc, el, ec); module.addDependency(dep); if (!composesFirstFileImport.has(request)) { composesFirstFileImport.set(request, dep); } } { const dep = new CssIcssExportDependency( lastLocalIdentifier, getReexport(identifier, localName), [start, end], true, CssIcssExportDependency.EXPORT_MODE.APPEND, CssIcssExportDependency.EXPORT_TYPE.COMPOSES ); dep.setLoc(sl, sc, el, ec); module.addDependency(dep); } } classNames.clear(); pos = from[1]; } else { from = walkCssTokens.eatIdentSequence(input, pos); if (from && input.slice(from[0], from[1]) === "global") { for (const entry of classNames) { const [start, end] = entry; const identifier = unescapeIdentifierCached( input.slice(start, end) ); const dep = new CssIcssExportDependency( /** @type {string} */ (lastLocalIdentifier), getReexport(identifier), [start, end], false, CssIcssExportDependency.EXPORT_MODE.APPEND, CssIcssExportDependency.EXPORT_TYPE.COMPOSES ); 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); } classNames.clear(); pos = from[1]; } else { const end = eatUntilSemi(input, pos); this._emitWarning( state, "Incorrect composition, expected global keyword or string value", locConverter, pos, end ); return end; } } } else if (className) { classNames.add([className[0], className[1], isGlobalFunction]); } else { const end = eatUntilSemi(input, pos); this._emitWarning( state, "Incorrect composition, expected class named", locConverter, pos, end ); return end; } } // Remove `composes` from source code const dep = new ConstDependency("", [propertyNameStart, end]); module.addPresentationalDependency(dep); } return pos; }; /** * Process id selector. * @param {string} input input * @param {number} start start position * @param {number} end end position * @returns {number} position after handling */ const processIdSelector = (input, start, end) => { const valueStart = start + 1; const name = unescapeIdentifierCached(input.slice(valueStart, end)); const dep = new CssIcssExportDependency( name, getReexport(name), [valueStart, end], true, CssIcssExportDependency.EXPORT_MODE.ONCE ); 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); if (pureMode) currentSelectorHasLocal = true; return end; }; /** * Process class selector. * @param {string} input input * @param {number} start start position * @param {number} end end position * @returns {number} position after handling */ const processClassSelector = (input, start, end) => { const ident = walkCssTokens.skipCommentsAndEatIdentSequence(input, end); if (!ident) return end; const name = unescapeIdentifierCached(input.slice(ident[0], ident[1])); lastLocalIdentifiers.push(name); const dep = new CssIcssExportDependency( name, getReexport(name), [ident[0], ident[1]], true, CssIcssExportDependency.EXPORT_MODE.ONCE ); const { line: sl, column: sc } = locConverter.get(ident[0]); const { line: el, column: ec } = locConverter.get(ident[1]); dep.setLoc(sl, sc, el, ec); module.addDependency(dep); if (pureMode) currentSelectorHasLocal = true; return ident[1]; }; /** * Process attribute selector. * @param {string} input input * @param {number} start start position * @param {number} end end position * @returns {number} position after handling */ const processAttributeSelector = (input, start, end) => { end = walkCssTokens.eatWhitespaceAndComments(input, end)[0]; const identifier = walkCssTokens.eatIdentSequence(input, end); if (!identifier) return end; const name = unescapeIdentifierCached( input.slice(identifier[0], identifier[1]) ); if (name.toLowerCase() !== "class") { return end; } end = walkCssTokens.eatWhitespaceAndComments(input, identifier[1])[0]; const isTilde = input.charCodeAt(end) === CC_TILDE; if ( input.charCodeAt(end) !== CC_EQUAL && input.charCodeAt(end) !== CC_TILDE ) { return end; } end += 1; if (isTilde) { if (input.charCodeAt(end) !== CC_EQUAL) { return end; } end += 1; } end = walkCssTokens.eatWhitespaceAndComments(input, end)[0]; const value = walkCssTokens.eatIdentSequenceOrString(input, end); if (!value) { return end; } const classNameStart = value[2] ? value[0] : value[0] + 1; const classNameEnd = value[2] ? value[1] : value[1] - 1; const className = unescapeIdentifierCached( input.slice(classNameStart, classNameEnd) ); const dep = new CssIcssExportDependency( className, getReexport(className), [classNameStart, classNameEnd], true, CssIcssExportDependency.EXPORT_MODE.NONE ); const { line: sl, column: sc } = locConverter.get(classNameStart); const { line: el, column: ec } = locConverter.get(classNameEnd); dep.setLoc(sl, sc, el, ec); module.addDependency(dep); return value[2] ? classNameEnd : classNameEnd + 1; }; walkCssTokens(source, 0, { comment, leftCurlyBracket: (input, start, end) => { const wasTopLevel = scope === CSS_MODE_TOP_LEVEL; if (wasTopLevel) { allowImportAtRule = false; scope = CSS_MODE_IN_BLOCK; } else if (scope !== CSS_MODE_IN_BLOCK) { return end; } if (!isModules) return end; if (pureMode) { inAtRulePrelude = false; if (wasTopLevel) seenTopLevelRule = true; const isRulePrelude = isNextRulePrelude; if (isRulePrelude) finalizeSelector(); const top = pureTop(); if (top) top.hasNestedBlock = true; const inheritedSkip = top ? top.skipChildren : false; pureBlockStack.push({ ignored: pureIgnorePending, skipOwn: inheritedSkip, skipChildren: nextBlockChildrenSkip || inheritedSkip, treatAsLeaf: nextBlockTreatAsLeaf, // "this rule is fully pure" (no impure comma-segment) OR any // ancestor pure. Matches PCSL's `[isPureSelectorSymbol]`. ancestorHadLocal: parentEffectivePure() || (isRulePrelude && !currentRuleHasImpureSelector), impure: isRulePrelude && currentRuleHasImpureSelector, hasDirectDecl: false, hasNestedBlock: false, isRulePrelude, preludeStart: currentRulePreludeStart, preludeEnd: start }); pureIgnorePending = false; nextBlockChildrenSkip = false; nextBlockTreatAsLeaf = false; currentRuleHasImpureSelector = false; currentSelectorHasLocal = false; currentRulePreludeStart = end; } blockNestingLevel = wasTopLevel ? 1 : blockNestingLevel + 1; isNextRulePrelude = isNextNestedSyntax(input, end); return end; }, rightCurlyBracket: (input, start, end) => { if (scope !== CSS_MODE_IN_BLOCK) return end; const closing = blockNestingLevel === 1; if (closing) { scope = CSS_MODE_TOP_LEVEL; blockNestingLevel = 0; if (!isModules) return end; isNextRulePrelude = true; modeData = undefined; lastLocalIdentifiers = []; currentRulePrevComposesFile = undefined; currentRuleComposesFiles.clear(); } else { blockNestingLevel--; if (!isModules) return end; isNextRulePrelude = isNextNestedSyntax(input, end); } if (pureMode) { const frame = pureBlockStack.pop(); if (frame) { // PCSL throws on impure rules whose body has any non-rule // content (declaration, empty body). Rules whose body is // only nested rules are skipped — child rules carry the // check themselves. if ( !pureNoCheck && !frame.ignored && !frame.skipOwn && frame.isRulePrelude && frame.impure && (frame.hasDirectDecl || !frame.hasNestedBlock || frame.treatAsLeaf) ) { reportPureRule(frame.preludeStart, frame.preludeEnd); } // Propagate "has direct declaration" through at-rule frames // so a parent rule containing only e.g. `@media { decl }` is // still treated as "rule with declarations". if (!frame.isRulePrelude && frame.hasDirectDecl) { const parent = pureTop(); if (parent) parent.hasDirectDecl = true; } } currentRuleHasImpureSelector = false; currentSelectorHasLocal = false; currentRulePreludeStart = end; } return end; }, url: (input, start, end, contentStart, contentEnd) => { if (!this.options.url) { return end; } return processOldURLFunction( input, start, end, contentStart, contentEnd ); }, atKeyword: (input, start, end) => { const name = input.slice(start, end).toLowerCase(); const wasTopLevel = scope === CSS_MODE_TOP_LEVEL; if (pureMode) { inAtRulePrelude = true; // Match PCSL's `isPureCheckDisabled`: any non-comment top-level // node (including `;`-terminated at-rules like `@import`) seals // the leading-comments window. if (wasTopLevel) seenTopLevelRule = true; } let pos = end; switch (name) { case "@namespace": { this._emitWarning( state, "'@namespace' is not supported in bundled CSS", locConverter, start, end ); pos = eatUntilSemi(input, start); break; } case "@charset": { const atRuleEnd = eatUntilSemi(input, start); if (/** @type {CssModule} */ (module).exportType === "style") { pos = atRuleEnd; break; } const dep = new ConstDependency("", [start, atRuleEnd + 1]); module.addPresentationalDependency(dep); const value = walkCssTokens.eatString(input, end); if (!value) { pos = atRuleEnd; break; } /** @type {BuildInfo} */ (module.buildInfo).charset = input .slice(value[0] + 1, value[1] - 1) .toUpperCase(); pos = atRuleEnd; break; } case "@import": { if (!this.options.import) { pos = eatUntilSemi(input, end); break; } if (!allowImportAtRule) { this._emitWarning( state, "Any '@import' rules must precede all other rules", locConverter, start, end ); pos = eatUntilSemi(input, end); break; } pos = processAtImport(input, start, end); break; } default: { if (isModules) { if (name === "@value") { pos = processAtValue(input, start, end); break; } else if ( this.options.animation && OPTIONALLY_VENDOR_PREFIXED_KEYFRAMES_AT_RULE.test(name) && isLocalMode() ) { if (pureMode) { nextBlockChildrenSkip = true; nextBlockTreatAsLeaf = true; } pos = processLocalAtRule(input, end, { string: true, identifier: true }); break; } else if ( this.options.customIdents && name === "@counter-style" && isLocalMode() ) { if (pureMode) { nextBlockChildrenSkip = true; nextBlockTreatAsLeaf = true; } pos = processLocalAtRule(input, end, { identifier: true }); break; } else if ( this.options.container && name === "@container" && isLocalMode() ) { pos = processLocalAtRule(input, end, { identifier: /^(none|and|or|not)$/ }); break; } else if (name === "@scope") { isNextRulePrelude = true; break; } isNextRulePrelude = false; } } } // If the at-rule consumed its own `;` (for `@import`/`@value`/ // `@charset`/`@namespace`), advance the prelude pointer so a // later impure rule's reported selector doesn't include this // at-rule's text. Body-bearing at-rules return at `{` — let // `leftCurlyBracket` handle those. if (pureMode && wasTopLevel && pos > end) { let probe = pos - 1; while ( probe > end && walkCssTokens.isWhiteSpace(input.charCodeAt(probe)) ) { probe--; } if (input.charCodeAt(probe) === CC_SEMICOLON) { currentRulePreludeStart = pos; } } return pos; }, semicolon: (input, start, end) => { if (isModules && scope === CSS_MODE_IN_BLOCK) { isNextRulePrelude = isNextNestedSyntax(input, end); } if (pureMode) { if (scope === CSS_MODE_IN_BLOCK) { if ( balanced.length === 0 && !isNextRulePrelude && !inAtRulePrelude ) { const top = pureTop(); if (top) top.hasDirectDecl = true; } } else if (scope === CSS_MODE_TOP_LEVEL && balanced.length === 0) { // Top-level `;` ends a statement (e.g. `@import "x";`). // Advance the prelude pointer so a later impure rule's // reported selector doesn't include the preceding text. currentRulePreludeStart = end; } inAtRulePrelude = false; } return end; }, identifier: (input, start, end) => { if (isModules) { const identifier = input.slice(start, end); if ( this.options.dashedIdents && isLocalMode() && isDashedIdentifier(identifier) ) { return processDashedIdent(input, start, end); } if (icssDefinitions.has(identifier)) { return processICSSSymbol(identifier, start, end); } switch (scope) { case CSS_MODE_IN_BLOCK: { if (isModules && !isNextRulePrelude) { if (balanced.length === 0 && !inAtRulePrelude) { const top = pureTop(); if (top) top.hasDirectDecl = true; } // Handle only top level values and not inside functions return processLocalDeclaration(input, start, end); } break; } } } return end; }, delim: (input, start, end) => { const ch = input.charCodeAt(start); if (ch === CC_FULL_STOP && isNextRulePrelude && isLocalMode()) { return processClassSelector(input, start, end); } if ( ch === CC_AMPERSAND && isNextRulePrelude && parentEffectivePure() && pureMode ) { currentSelectorHasLocal = true; } return end; }, hash: (input, start, end, isID) => { if (isNextRulePrelude && isLocalMode() && isID) { return processIdSelector(input, start, end); } return end; }, colon: (input, start, end) => { if (isModules) { const ident = walkCssTokens.skipCommentsAndEatIdentSequence( input, end ); if (!ident) return end; const name = input.slice(ident[0], ident[1]).toLowerCase(); switch (scope) { case CSS_MODE_TOP_LEVEL: { if (name === "import") { const pos = processImportOrExport(0, input, ident[1]); const dep = new ConstDependency("", [start, pos]); module.addPresentationalDependency(dep); return pos; } else if (name === "export") { const pos = processImportOrExport(1, input, ident[1]); const dep = new ConstDependency("", [start, pos]); module.addPresentationalDependency(dep); return pos; } } // falls through default: { if (isNextRulePrelude) { const isFn = input.charCodeAt(ident[1]) === CC_LEFT_PARENTHESIS; if (isFn && name === "local") { // Eat extra whitespace const end = walkCssTokens.eatWhitespaceAndComments( input, ident[1] + 1 )[0]; modeData = LOCAL_MODE; const dep = new ConstDependency("", [start, end]); module.addPresentationalDependency(dep); balanced.push([":local", start, end, true]); return end; } else if (name === "local") { modeData = LOCAL_MODE; const found = walkCssTokens.eatWhitespaceAndComments( input, ident[1] ); if (!found[1]) { this._emitWarning( state, `Missing whitespace after ':local' in '${input.slice( start, eatUntilLeftCurly(input, end) + 1 )}'`, locConverter, start, end ); } end = walkCssTokens.eatWhitespace(input, ident[1]); const dep = new ConstDependency("", [start, end]); module.addPresentationalDependency(dep); return end; } else if (isFn && name === "global") { // Eat extra whitespace const end = walkCssTokens.eatWhitespaceAndComments( input, ident[1] + 1 )[0]; modeData = GLOBAL_MODE; const dep = new ConstDependency("", [start, end]); module.addPresentationalDependency(dep); balanced.push([":global", start, end, true]); return end; } else if (name === "global") { modeData = GLOBAL_MODE; // Eat extra whitespace const found = walkCssTokens.eatWhitespaceAndComments( input, ident[1] ); if (!found[1]) { this._emitWarning( state, `Missing whitespace after ':global' in '${input.slice( start, eatUntilLeftCurly(input, end) + 1 )}'`, locConverter, start, end ); } end = walkCssTokens.eatWhitespace(input, ident[1]); const dep = new ConstDependency("", [start, end]); module.addPresentationalDependency(dep); return end; } } } } } lastTokenEndForComments = end; return end; }, function: (input, start, end) => { const name = input .slice(start, end - 1) .replace(/\\/g, "") .toLowerCase(); balanced.push([name, start, end]); switch (name) { case "src": case "url": { if (!this.options.url) { return end; } return processURLFunction(input, end, name); } default: { if (this.options.url && IMAGE_SET_FUNCTION.test(name)) { return processImageSetFunction(input, start, end); } if (isModules) { if ( this.options.function && isLocalMode() && isDashedIdentifier(name) ) { return processDashedIdent(input, start, end); } const type = name === "local" ? 1 : name === "global" ? 2 : undefined; if (type && !isNextRulePrelude) { return processLocalOrGlobalFunction(input, type, start, end); } } } } return end; }, leftSquareBracket: (input, start, end) => { if (isNextRulePrelude && isLocalMode()) { return processAttributeSelector(input, start, end); } return end; }, leftParenthesis: (input, start, end) => { balanced.push(["(", start, end]); return end; }, rightParenthesis: (input, start, end) => { const popped = balanced.pop(); if (isModules && popped) { const isLocal = popped[0] === ":local"; const isGlobal = popped[0] === ":global"; if (isLocal || isGlobal) { modeData = balanced[balanced.length - 1] ? balanced[balanced.length - 1][0] === ":local" ? LOCAL_MODE : balanced[balanced.length - 1][0] === ":global" ? GLOBAL_MODE : undefined : undefined; if (popped[3] && isLocal) { while (walkCssTokens.isWhiteSpace(input.charCodeAt(start - 1))) { start -= 1; } } const dep = new ConstDependency("", [start, end]); module.addPresentationalDependency(dep); } else if (isNextRulePrelude) { modeData = undefined; } } return end; }, comma: (input, start, end) => { if (isModules && balanced.length === 0) { // Reset stack for `:global .class :local .class-other` selector after modeData = undefined; if (pureMode && isNextRulePrelude) finalizeSelector(); } lastTokenEndForComments = start; return end; } }); /** @type {BuildInfo} */ (module.buildInfo).strict = true; // Topologically sort the files referenced by `composes ... from` // declarations and tag each file's first import dep with the // resulting `sourceOrder`. `NormalModule#build` then reorders the // deps via `sortWithSourceOrder` so the bundle loads them in // cascade-correct order. Files stuck in a cycle are not visited // and keep their natural loc-based position. if (composesFirstFileImport.size > 1) { topologicalSort( composesGraph, [...composesFirstFileImport.keys()], (file, i) => { /** @type {CssIcssImportDependency} */ (composesFirstFileImport.get(file)).sourceOrder = i; } ); } const buildMeta = /** @type {BuildMeta} */ (state.module.buildMeta); buildMeta.exportsType = this.options.namedExports ? "namespace" : "default"; buildMeta.defaultObject = this.options.namedExports ? false : "redirect-warn"; if ( /** @type {CssModule} */ (module).exportType === "text" || /** @type {CssModule} */ (module).exportType === "css-style-sheet" ) { module.addDependency(new StaticExportsDependency(["default"], true)); } else { module.addDependency(new StaticExportsDependency([], true)); } return state; } /** * Returns comments in the range. * @param {Range} range range * @returns {Comment[]} comments in the range */ getComments(range) { if (!this.comments) return []; const [rangeStart, rangeEnd] = range; /** * Returns compared. * @param {Comment} comment comment * @param {number} needle needle * @returns {number} compared */ const compare = (comment, needle) => /** @type {Range} */ (comment.range)[0] - needle; const comments = /** @type {Comment[]} */ (this.comments); let idx = binarySearchBounds.ge(comments, rangeStart, compare); /** @type {Comment[]} */ const commentsInRange = []; while ( comments[idx] && /** @type {Range} */ (comments[idx].range)[1] <= rangeEnd ) { commentsInRange.push(comments[idx]); idx++; } return commentsInRange; } /** * Parses comment options. * @param {Range} range range of the comment * @returns {{ options: Record<string, EXPECTED_ANY> | null, errors: (Error & { comment: Comment })[] | null }} result */ parseCommentOptions(range) { const comments = this.getComments(range); if (comments.length === 0) { return EMPTY_COMMENT_OPTIONS; } /** @type {Record<string, EXPECTED_ANY>} */ const options = {}; /** @type {(Error & { comment: Comment })[]} */ const errors = []; for (const comment of comments) { const { value } = comment; if (value && webpackCommentRegExp.test(value)) { // try compile only if webpack options comment is present try { for (let [key, val] of Object.entries( vm.runInContext( `(function(){return {${value}};})()`, this.magicCommentContext ) )) { if (typeof val === "object" && val !== null) { val = val.constructor.name === "RegExp" ? new RegExp(val) : JSON.parse(JSON.stringify(val)); } options[key] = val; } } catch (err) { const newErr = new Error(String(/** @type {Error} */ (err).message)); newErr.stack = String(/** @type {Error} */ (err).stack); Object.assign(newErr, { comment }); errors.push(/** @type {(Error & { comment: Comment })} */ (newErr)); } } } return { options, errors }; } } module.exports = CssParser; module.exports.escapeIdentifier = escapeIdentifier; module.exports.unescapeIdentifier = unescapeIdentifier;