/
githubmirror
/
webpack
Обзор
Документация
Войти
/
githubmirror
/
webpack
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
lib/css/CssParser.js
4 793 строки
165 KB
Alexander Akait
fix(css): leave @namespace URIs alone (#21658)
10 авг 2026, 12:13
Не верифицирован
10 авг 2026, 12:13
829fc32
Код
Авторство
О чём код?
/* MIT License http://www.opensource.org/licenses/mit-license.php Author Tobias Koppers @sokra */ "use strict"; const path = require("path"); 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 WebpackError = require("../errors/WebpackError"); const ResourceHintPlugin = require("../prefetch/ResourceHintPlugin"); const LocConverter = require("../util/LocConverter"); const { parseResource } = require("../util/identifier"); const { createMagicCommentContext, parseCommentOptionsInRange } = require("../util/magicComment"); const memoize = require("../util/memoize"); const topologicalSort = require("../util/topologicalSort"); const { CSS_MODULES_KEYWORDS, CSS_MODULES_KEYWORD_OPTIONS } = require("./data"); const { A, NodeType, SourceProcessor, buildSkipSet, equalsLowerCase, isDashedIdentifier, isWhitespace, normalizeUrl, rangeEquals, rangeEqualsLowerCase, toLowerCaseIfNeeded, unescapeIdentifier } = require("./syntax"); const getUnsupportedFeatureWarning = memoize(() => require("../errors/UnsupportedFeatureWarning") ); const getModuleDependencyWarning = memoize(() => require("../errors/ModuleDependencyWarning") ); const getCommentCompilationWarning = memoize(() => require("../errors/CommentCompilationWarning") ); // `SourceProcessor` drives the parse and hands already-built AST nodes to the visitors; positions are read from those nodes' ranges rather than re-scanning the source. /** @typedef {import("../Module").BuildInfo} BuildInfo */ /** @typedef {import("../Module").BuildMeta} BuildMeta */ /** @typedef {import("./CssModule").CssModuleBuildInfo} CssModuleBuildInfo */ /** @typedef {import("./CssModule").CssModuleBuildMeta} CssModuleBuildMeta */ /** @typedef {import("../Parser").ParserState} ParserState */ /** @typedef {import("../Parser").PreparsedAst} PreparsedAst */ /** @typedef {import("./syntax").AtRule} AtRule */ /** @typedef {import("./syntax").Declaration} Declaration */ /** @typedef {import("./syntax").FunctionNode} FunctionNode */ /** @typedef {import("./syntax").Node} AstNode */ /** @typedef {import("./syntax").QualifiedRule} QualifiedRule */ /** @typedef {import("./syntax").Rule} Rule */ /** @typedef {import("./syntax").SimpleBlock} SimpleBlock */ /** @typedef {import("./syntax").Token} Token */ /** @typedef {import("./syntax").UrlToken} UrlToken */ /** @typedef {import("./syntax").HashToken} HashToken */ /** @typedef {import("./syntax").VisitorMap} VisitorMap */ /** @typedef {import("../../declarations/WebpackOptions").CssAutoOrModuleParserOptions} CssAutoOrModuleParserOptions */ /** @typedef {import("../../declarations/WebpackOptions").CssModuleParserOptions} CssModuleParserOptions */ /** @typedef {import("./CssModule")} CssModule */ /** @typedef {import("./CssModule").Inheritance} Inheritance */ /** @typedef {[number, number]} Range */ /** @typedef {{ line: number, column: number }} Position */ /** @typedef {{ from: string, items: ({ localName: string, importName: string })[] }} ValueAtRuleImport */ /** @typedef {{ localName: string, value: string }} ValueAtRuleValue */ /** * What a `@custom-media` name resolves to. `condition` substitutes into any * `<media-in-parens>` slot, `type` only at the start of a query, `boolean` is the * spec's `true` / `false` (folded into the enclosing query rather than written * out — no `<media-in-parens>` is unconditionally true on every engine). * @typedef {{ kind: "condition" | "type", text: string } | { kind: "boolean", value: boolean } | { kind: "unsupported" }} CustomMediaValue */ /** @typedef {CustomMediaValue | { kind: "alias", name: string } | { kind: "or", parts: ({ text: string } | { alias: string })[] }} CustomMediaDefinition */ /** @typedef {{ name: string, start: number, end: number, invalid: boolean, leading: boolean }} CustomMediaUse */ /** * A `@media` prelude captured as its boolean shape, so a `true` / `false` * definition found later can be folded into it. Node ids are recycled per * top-level rule, so this holds text and offsets rather than AST nodes. * @typedef {{ kind: "text", text: string } | { kind: "ref", text: string, use: CustomMediaUse } | { kind: "group", operand: MediaNode } | { kind: "not", operand: MediaNode } | { kind: "chain", isOr: boolean, terms: MediaNode[] } | { kind: "typed", text: string, rest: MediaNode | null }} MediaNode */ /** @type {CustomMediaValue} */ const CUSTOM_MEDIA_UNSUPPORTED = { kind: "unsupported" }; /** @type {CustomMediaValue} */ const CUSTOM_MEDIA_TRUE = { kind: "boolean", value: true }; /** @type {CustomMediaValue} */ const CUSTOM_MEDIA_FALSE = { kind: "boolean", value: false }; const CC_COLON = ":".charCodeAt(0); const CC_HYPHEN_MINUS = "-".charCodeAt(0); const CC_SEMICOLON = ";".charCodeAt(0); const CC_TAB = "\t".charCodeAt(0); const CC_SPACE = " ".charCodeAt(0); const CC_LINE_FEED = "\n".charCodeAt(0); const CC_CARRIAGE_RETURN = "\r".charCodeAt(0); const CC_FORM_FEED = "\f".charCodeAt(0); const CC_LEFT_CURLY = "{".charCodeAt(0); const CC_LOWER_V = "v".charCodeAt(0); const CC_UPPER_V = "V".charCodeAt(0); const CC_REVERSE_SOLIDUS = "\\".charCodeAt(0); // A parsed CSS comment. `loc` is computed on demand — only magic-comment error // warnings read it, so comment-heavy CSS skips the per-comment line/col work. // Comments are kept in a flat per-parse `comments` side array (not AST nodes); `loc` is derived lazily via `rangeLoc` only where needed (magic-comment errors). /** @typedef {{ value: string, range: Range }} Comment */ // Newlines (CSS Syntax 3 §3.3) — listed explicitly since there's no preprocessing stage. // https://www.w3.org/TR/css-syntax-3/#whitespace // Pure-mode markers: `cssmodules-pure-ignore` opts a single rule out of the purity check, `cssmodules-pure-no-check` (before the first rule) opts the whole file out. const PURE_IGNORE_RE = /^\s*cssmodules-pure-ignore(?:\s|$)/; const PURE_NO_CHECK_RE = /^\s*cssmodules-pure-no-check(?:\s|$)/; const IMAGE_SET_FUNCTION = /^(?:-\w+-)?image-set$/i; const OPTIONALLY_VENDOR_PREFIXED_KEYFRAMES_AT_RULE = /^@(?:-\w+-)?keyframes$/; const VENDOR_PREFIX = /^-\w+-/; const COMPOSES_PROPERTY = /^(?:composes|compose-with)$/i; // Functional view-transition pseudo-elements whose `(<name> .class…)` argument names are scoped like `view-transition-name`/`-class` values. const VIEW_TRANSITION_PART_PSEUDO = /^view-transition-(?:group|image-pair|old|new)$/i; const IS_MODULES = /\.modules?\.[^.]+$/i; // `@font-face` src extension → preload `type`; the emission guesses `as="font"`. const FONT_MIME_TYPES = new Map([ ["woff2", "font/woff2"], ["woff", "font/woff"], ["ttf", "font/ttf"], ["otf", "font/otf"], ["eot", "application/vnd.ms-fontobject"] ]); /** * @param {string} request font url (may carry a query/hash) * @returns {string | undefined} preload `type` for a known font extension */ const fontMimeType = (request) => { const match = /\.([a-z0-9]+)(?:[?#]|$)/i.exec(request); return match ? FONT_MIME_TYPES.get(match[1].toLowerCase()) : undefined; }; // Skip options for a non-CSS-Modules parse: drop the selector prelude (never // walked without modules) plus value / function-arg leaves nothing reads (the // `Ident` visitor no-ops, the `Declaration` visitor returns early, no ICSS). // `url` / functions / strings / blocks / commas are kept — they carry url() // rewrites and image-set fences. At-rule preludes are kept (`@media` / `@import` // are read). CSS-Modules parses skip nothing: selectors are walked and ICSS // `:export { k: v }` captures each value's byte range from its first / last node. const SKIP_NON_MODULES = { types: buildSkipSet([ NodeType.Number, NodeType.Dimension, NodeType.Percentage, NodeType.Ident, NodeType.Hash, NodeType.Colon, NodeType.Delim, // Nothing reads value/arg whitespace either — consumers use // `nextNonWhitespace` / type checks that tolerate its absence. NodeType.Whitespace ]), selectorPrelude: true }; // Like SKIP_NON_MODULES but keeps selector preludes and the `Colon` / `Ident` // tokens inside function-arg lists, so `@custom-selector` `:--name` references // (including nested ones like `:is(:--name)`) survive a non-modules parse. const SKIP_NON_MODULES_KEEP_SELECTORS = { types: buildSkipSet([ NodeType.Number, NodeType.Dimension, NodeType.Percentage, NodeType.Hash, NodeType.Delim, NodeType.Whitespace ]), selectorPrelude: false }; const CSS_COMMENT = /\/\*((?!\*\/)[\s\S]*?)\*\//g; // `@value` recognizers (postcss-modules-values shape): the import form `<names> from <source>`, and the `<importName> as <localName>` alias inside it. const VALUE_IMPORT_FORM = /from(\/\*|\s)(?:[\s\S]+)$/i; const VALUE_AS_ALIAS = /\s+as\s+/; // `@value name value`: end of the name run (first non-space followed by space). const VALUE_NAME_BOUNDARY = /\S\s/; const ONLY_WHITESPACE = /^\s+$/; // Relative request prefix (`./` or `../`) — `isSelfReferenceRequest` per `from`. const RELATIVE_REQUEST = /^\.{1,2}\//; /** * 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; while ((match = regexp.exec(str)) !== null) { result.push(match); } return result; }; /** * Range-keyed index over a known-properties table: ASCII-case-folded 31-hash * of the name bytes → canonical key(s). Lets the Declaration visitor answer * "is this a known property" (and get the canonical lowercase name) without * slicing the property name out of the source per declaration. * @type {WeakMap<Map<string, Map<string, number>>, Map<number, string | string[]>>} */ const KNOWN_PROPERTY_INDEX_CACHE = new WeakMap(); /** * Gets (or builds) the hash index for a known-properties table. * @param {Map<string, Map<string, number>>} knownProperties known properties table * @returns {Map<number, string | string[]>} hash → canonical name(s) */ const getKnownPropertyIndex = (knownProperties) => { let index = KNOWN_PROPERTY_INDEX_CACHE.get(knownProperties); if (index === undefined) { index = new Map(); for (const name of knownProperties.keys()) { let h = name.length; for (let i = 0; i < name.length; i++) { h = ((h << 5) - h + name.charCodeAt(i)) | 0; } const hit = index.get(h); if (hit === undefined) index.set(h, name); else if (typeof hit === "string") index.set(h, [hit, name]); else hit.push(name); } KNOWN_PROPERTY_INDEX_CACHE.set(knownProperties, index); } return index; }; /** * Canonical known-property name for a source range (ASCII case-insensitive), without slicing. * @param {Map<number, string | string[]>} index hash index from `getKnownPropertyIndex` * @param {string} input source * @param {number} start name start * @param {number} end name end (exclusive) * @returns {string | undefined} canonical lowercase name, or undefined when unknown */ const knownPropertyForRange = (index, input, start, end) => { let h = end - start; for (let i = start; i < end; i++) { let c = input.charCodeAt(i); if (c >= 65 && c <= 90) c |= 0x20; h = ((h << 5) - h + c) | 0; } const hit = index.get(h); if (hit === undefined) return undefined; if (typeof hit === "string") { return rangeEqualsLowerCase(input, start, end, hit) ? hit : undefined; } for (let i = 0; i < hit.length; i++) { if (rangeEqualsLowerCase(input, start, end, hit[i])) return hit[i]; } return undefined; }; /** * Comma-separated index of the counter-name argument in a counter-reading function, or `-1` when the name isn't one. The name is matched over the raw byte range like the other function-name probes; only an escaped name (rare) pays the unescaped slice. * @param {string} input source * @param {number} start name start offset * @param {number} end name end offset * @param {string=} escapedName unescaped name, when the raw range carries an escape * @returns {number} argument index of the counter name, or `-1` */ const counterFunctionNameIndex = (input, start, end, escapedName) => { // The `target-counter(url, name, style?)` cross-reference forms (Generated Content for Paged Media) name the counter second; print engines read them. if (escapedName !== undefined) { if ( equalsLowerCase(escapedName, "counter") || equalsLowerCase(escapedName, "counters") ) { return 0; } if ( equalsLowerCase(escapedName, "target-counter") || equalsLowerCase(escapedName, "target-counters") ) { return 1; } return -1; } switch (end - start) { case 7: return rangeEqualsLowerCase(input, start, end, "counter") ? 0 : -1; case 8: return rangeEqualsLowerCase(input, start, end, "counters") ? 0 : -1; case 14: return rangeEqualsLowerCase(input, start, end, "target-counter") ? 1 : -1; case 15: return rangeEqualsLowerCase(input, start, end, "target-counters") ? 1 : -1; default: return -1; } }; /** * Gets the known-properties table for the enabled scoping options: the * properties a `css/module` reads a scoped name out of, each with the keywords * of its own grammar (see `tooling/generate-css-data.js`). * @param {{ animation?: boolean, container?: boolean, customIdents?: boolean, grid?: boolean }=} options options * @returns {Map<string, Map<string, number>>} list of known properties */ const buildKnownProperties = (options = {}) => { /** @type {Map<string, Map<string, number>>} */ const knownProperties = new Map(); for (const [property, option] of CSS_MODULES_KEYWORD_OPTIONS) { if ( options[ /** @type {"animation" | "container" | "customIdents" | "grid"} */ (option) ] ) { knownProperties.set( property, /** @type {Map<string, number>} */ (CSS_MODULES_KEYWORDS.get(property)) ); } } return knownProperties; }; /** @type {Map<number, Map<string, Map<string, number>>>} */ const KNOWN_PROPERTIES_CACHE = new Map(); /** * Memoized {@link buildKnownProperties}: the table depends only on the four * boolean options (≤ 16 combinations) and is read-only, while the same parser is * reused across modules — so build each combination once and share it instead * of rebuilding the Map per parsed module. * @param {{ animation?: boolean, container?: boolean, customIdents?: boolean, grid?: boolean }=} options options * @returns {Map<string, Map<string, number>>} known properties table */ const getKnownProperties = (options = {}) => { const key = (options.animation ? 1 : 0) | (options.container ? 2 : 0) | (options.customIdents ? 4 : 0) | (options.grid ? 8 : 0); let table = KNOWN_PROPERTIES_CACHE.get(key); if (table === undefined) { table = buildKnownProperties(options); KNOWN_PROPERTIES_CACHE.set(key, table); } return table; }; // Byte-level source-cursor scans for computing replacement / strip ranges on raw source after parsing. /** * Skip trailing whitespace + at most one newline (CRLF-aware). * @param {string} input source * @param {number} pos position * @returns {number} position past whitespace + one newline */ const skipWhiteLine = (input, pos) => { for (;;) { const cc = input.charCodeAt(pos); if (cc === CC_SPACE || cc === CC_TAB) { pos++; continue; } if ( cc === CC_LINE_FEED || cc === CC_CARRIAGE_RETURN || cc === CC_FORM_FEED ) { pos++; } // Treat CRLF as one newline: a CR followed by LF advances past the LF. if (cc === CC_CARRIAGE_RETURN && input.charCodeAt(pos) === CC_LINE_FEED) { pos++; } break; } return pos; }; /** * Whether the ident byte-range is a `@container` prelude keyword (`none`/`and`/`or`/`not`, lowercase only) — byte comparison avoids slicing a transient string per prelude ident. * @param {string} input source * @param {number} start start offset * @param {number} end end offset * @returns {boolean} true for a container keyword */ const isContainerKeyword = (input, start, end) => { switch (end - start) { case 2: return input.startsWith("or", start); case 3: return input.startsWith("and", start) || input.startsWith("not", start); case 4: return input.startsWith("none", start); default: return false; } }; /** * Whether the byte range contains a CSS escape (`\`) — function names are short, so this scan replaces a per-name slice. * @param {string} input source * @param {number} start start offset * @param {number} end end offset (exclusive) * @returns {boolean} true when the range contains a backslash */ const rangeHasEscape = (input, start, end) => { for (let i = start; i < end; i++) { if (input.charCodeAt(i) === CC_REVERSE_SOLIDUS) return true; } return false; }; /** * @param {string} input source * @param {number} pos position * @returns {number} position of the next `{`, or EOF if none */ const findLeftCurly = (input, pos) => { while (pos < input.length) { if (input.charCodeAt(pos) === CC_LEFT_CURLY) return pos; pos++; } return pos; }; /** * 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 */ /** * Pure-mode at-rules whose prelude is selector-checked, so their body is opaque to the enclosing rule's declaration accounting. * @param {string} name at-rule name including the leading `@`, lower-cased * @returns {boolean} true for `@keyframes` / `@counter-style` / `@container` / `@scope` */ const isPureBodyAtRule = (name) => OPTIONALLY_VENDOR_PREFIXED_KEYFRAMES_AT_RULE.test(name) || name === "@counter-style" || name === "@container" || name === "@scope"; /** * Scan a rule body once: does it hold a direct declaration counted against the enclosing rule (a declaration, or one in a transparent conditional-group at-rule like `@media`/`@supports`/…) and does it hold a nested block (qualified rule or any block-bearing at-rule)? * @param {Declaration[] | null} declarations rule-body declarations * @param {Rule[] | null} childRules rule-body child rules * @returns {{ hasDirectDecl: boolean, hasNestedBlock: boolean }} scan result */ const scanRuleBody = (declarations, childRules) => { let hasDirectDecl = Boolean(declarations && declarations.length > 0); let hasNestedBlock = false; if (childRules) { for (const child of childRules) { const t = A.type(child); if (t === NodeType.QualifiedRule) { hasNestedBlock = true; } else if (t === NodeType.AtRule) { const atDecls = A.declarations(child); const atChildRules = A.childRules(child); if (!atDecls && !atChildRules) continue; hasNestedBlock = true; if ( !hasDirectDecl && !isPureBodyAtRule(`@${toLowerCaseIfNeeded(A.name(child))}`) && scanRuleBody(atDecls, atChildRules).hasDirectDecl ) { hasDirectDecl = true; } } } } return { hasDirectDecl, hasNestedBlock }; }; /** * Parses value at rule params. * @param {string} str value at-rule params * @returns {ValueAtRuleImport | ValueAtRuleValue} parsed result */ const parseValueAtRuleParams = (str) => { if (VALUE_IMPORT_FORM.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(VALUE_AS_ALIAS); 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(VALUE_NAME_BOUNDARY) + 1; localName = str.slice(0, idx).replace(CSS_COMMENT, "").trim(); value = str.slice(idx + (str[idx] === " " ? 1 : 0)); } if ( value.length > 0 && !ONLY_WHITESPACE.test(value.replace(CSS_COMMENT, "")) ) { value = value.trim(); } return { localName, value }; }; /** * Index of the next non-whitespace child at or after `from`, or `nodes.length`. * @param {AstNode[]} nodes node list * @param {number} from start index (inclusive) * @returns {number} index of the next non-whitespace node */ const nextNonWhitespace = (nodes, from) => { let i = from; while (i < nodes.length && A.type(nodes[i]) === NodeType.Whitespace) i++; return i; }; /** @typedef {{ urlNode: (AstNode | undefined), layerNode: (AstNode | undefined), supportsNode: (FunctionNode | undefined) }} ImportPrelude */ /** * Scan an `@import` prelude in spec order — URL → `layer` / `layer(…)`? → `supports(…)`? — stopping at the first media-query token (the caller slices the media query out separately). * @param {AstNode[]} prelude the at-rule prelude nodes * @returns {ImportPrelude} the recognized prefix parts (any may be undefined) */ const parseImportPrelude = (prelude) => { /** @type {AstNode | undefined} */ let urlNode; /** @type {AstNode | undefined} */ let layerNode; /** @type {FunctionNode | undefined} */ let supportsNode; for (const cv of prelude) { const t = A.type(cv); if (t === NodeType.Whitespace) continue; if (!urlNode) { if (t === NodeType.Url || t === NodeType.String) { urlNode = cv; continue; } if ( t === NodeType.Function && equalsLowerCase(A.unescapedName(cv), "url") ) { urlNode = cv; continue; } if (t === NodeType.Ident) { // CSS Modules: bare ident is a `@value` reference. urlNode = cv; continue; } break; } if (!layerNode && !supportsNode) { if (t === NodeType.Ident) { if (equalsLowerCase(A.unescaped(cv), "layer")) { layerNode = cv; continue; } } else if ( t === NodeType.Function && equalsLowerCase(A.unescapedName(cv), "layer") ) { layerNode = cv; continue; } } if ( !supportsNode && t === NodeType.Function && equalsLowerCase(A.unescapedName(cv), "supports") ) { supportsNode = /** @type {FunctionNode} */ (cv); continue; } // First media-query token — stop scanning for the prefix. break; } return { urlNode, layerNode, supportsNode }; }; /** * Recognize the request of an ICSS `:import("path")` prelude — the `import(…)` function's args, or the spaced `:import (…)` simple block. Pure — the caller emits the "expected string" warning from `errorPos`. * @param {AstNode} second the `import(…)` function / first prelude node after the `:` * @param {QualifiedRule} rule the `:import` rule * @param {string} source full CSS source, for the path slice * @returns {{ request: string } | { errorPos: number }} the unquoted request, or the position for the parse warning */ const parseIcssImportRequest = (second, rule, source) => { /** @type {AstNode[] | undefined} */ let args; if (A.type(second) === NodeType.Function) { args = A.children(second); } else { for (const p of A.prelude(rule)) { if (A.type(p) === NodeType.SimpleBlock && A.blockToken(p) === "(") { args = A.children(p); break; } } } // The first non-whitespace value inside `(...)` must be a string. const innerStrToken = args && args.find((v) => A.type(v) !== NodeType.Whitespace); if (!innerStrToken || A.type(innerStrToken) !== NodeType.String) { const errorPos = A.type(second) === NodeType.Function ? A.nameEnd(second) + 1 : A.end(second); return { errorPos }; } return { request: source.slice(A.start(innerStrToken) + 1, A.end(innerStrToken) - 1) }; }; 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 = { as: "stylesheet", url: true, import: true, namedExports: true, animation: true, container: true, customIdents: true, customMedia: true, customSelectors: true, dashedIdents: true, function: true, grid: true, ...options }; 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 (getModuleDependencyWarning())( 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); } /** * Arbitrate cross-kind CSS-Modules export collisions collected during * parsing. A class always wins the shared JS export key over the * CSS-only kinds (`@keyframes`, `@counter-style`, `@container`, custom * property, grid identifier) — matching css-loader / lightning css / * postcss-modules; the loser's `EXPORT_MODE` is demoted to `NONE` so * the CSS-side rewrite is preserved but the entry no longer contributes * to `styles.<name>`. Other pairs (`class` ↔ `id`, * `class` ↔ `:export`, `class` ↔ `@value`, kind-vs-kind without a * class present, …) surface as a warning so the user resolves the name * themselves. Same-kind redeclarations are silent. * @param {ParserState} state parser state * @param {Map<string, { kind: string, line: number, column: number, entries: number[] }[]>} declaredExports collected declarations grouped by (name, kind) * @param {import("../dependencies/CssIcssExportDependency").CssExportEntry[]} cssExportEntries all export entries — losers are demoted in place * @returns {void} */ _resolveAmbiguousExports(state, declaredExports, cssExportEntries) { const NONE = CssIcssExportDependency.EXPORT_MODE.NONE; for (const [name, decls] of declaredExports) { if (decls.length <= 1) continue; // Prefer `class` as the winner (matches css-loader / lightning css / postcss-modules); otherwise the first-declared kind wins de facto. let winner; for (const d of decls) { if (d.kind === "class") { winner = d; break; } } const reference = winner !== undefined ? winner : decls[0]; for (const loser of decls) { if (loser === reference) continue; const anchor = cssExportEntries[loser.entries[0]]; if ( winner !== undefined && (loser.kind === "custom property" || loser.kind === "@keyframes" || loser.kind === "@counter-style" || loser.kind === "@container" || loser.kind === "grid identifier") ) { for (const idx of loser.entries) { cssExportEntries[idx].exportMode = NONE; } state.current.addWarning( new (getModuleDependencyWarning())( state.module, new WebpackError( `CSS module export "${name}" is shadowed by ${winner.kind} at line ${winner.line}:${winner.column}: the ${loser.kind} "${name}" is still scoped in the emitted CSS but is not accessible from the JavaScript export — rename one of them if both are needed` ), { start: { line: anchor.locStartLine, column: anchor.locStartColumn }, end: { line: anchor.locEndLine, column: anchor.locEndColumn } } ) ); } else { // Other collision — warn without changing behavior. state.current.addWarning( new (getModuleDependencyWarning())( state.module, new WebpackError( `Conflicting CSS module export "${name}": already declared as ${reference.kind} at line ${reference.line}:${reference.column}, redeclared as ${loser.kind}` ), { start: { line: anchor.locStartLine, column: anchor.locStartColumn }, end: { line: anchor.locEndLine, column: anchor.locEndColumn } } ) ); } } } } /** * 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); } // Per-parse comment side-array — kept local (like HtmlParser) so nothing is retained on the reused parser instance between modules. /** @type {Comment[]} */ const comments = []; const urlHints = this.options.urlHints; const fontPreload = this.options.fontPreload; /** * Apply `parser.css.urlHints` defaults + `webpackPrefetch` / * `webpackPreload` / `webpackFetchPriority` / `webpackAs` / * `webpackType` / `webpackMedia` magic-comment overrides to a fresh * `CssUrlDependency`. Magic comments win over the project-wide default. * @param {CssUrlDependency} dep dep * @param {string} request asset request * @param {Record<string, EXPECTED_ANY> | null | undefined} options parsed comment options * @param {import("../Dependency").DependencyLocation} loc location for warnings * @returns {void} */ const applyResourceHintDefaults = (dep, request, options, loc) => { // `fontPreload` heuristic: seed `preload`/`as`/`type` for the first url // of each `@font-face` (the nearest enclosing at-rule) as the lowest // default, so `urlHints` rules and magic comments below still override. if (fontPreload && atRuleStateStack.length > 0) { const top = atRuleStateStack[atRuleStateStack.length - 1]; if (top.name === "@font-face" && !top.fontPreloaded) { top.fontPreloaded = true; dep.preload = true; dep.asAttribute = "font"; const type = fontMimeType(request); if (type) dep.typeAttribute = type; } } ResourceHintPlugin.applyResourceHints( dep, urlHints, request, options, state.module, loc ); }; const module = state.module; // Every CSS Modules export declaration is tracked here so `_resolveAmbiguousExports` can arbitrate cross-kind collisions in one pass at end-of-parse: a class silently wins over CSS-scoped kinds (`@keyframes`, `@counter-style`, `@container`, custom property, grid identifier) — matching css-loader / lightning css / postcss-modules — while other pairs (`class` ↔ `id`, `class` ↔ `:export`, `class` ↔ `@value`, …) surface as a warning so the user can resolve them. /** @typedef {{ kind: string, line: number, column: number, entries: number[] }} DeclarationOfKind */ /** @type {Map<string, DeclarationOfKind[]>} */ const declaredExports = new Map(); /** * Track a declaration site for later ambiguity resolution. Pointer * into `cssExportEntries` is captured now because the entry is * pushed immediately after by the calling `addCssExport(…)`. * Property-value references (`animation: foo`) are not declarations * and skip this hook. * @param {string} name export name * @param {string} kind human-readable declaration kind * @param {number} line start line (1-based) of the declaration * @param {number} column start column (0-based) of the declaration * @returns {void} */ const recordDeclaration = (name, kind, line, column) => { let decls = declaredExports.get(name); if (decls === undefined) { decls = []; declaredExports.set(name, decls); } let d; for (const existing of decls) { if (existing.kind === kind) { d = existing; break; } } if (d === undefined) { d = { kind, line, column, entries: [] }; decls.push(d); } d.entries.push(cssExportEntries.length); }; // All `:export`-style exports are collected into a single // `CssIcssExportDependency` per module (emitted at parse end) instead of one // `Dependency` per export — far fewer retained objects on CSS-heavy builds. /** @type {import("../dependencies/CssIcssExportDependency").CssExportEntry[]} */ const cssExportEntries = []; /** * @param {number} sl start line * @param {number} sc start column * @param {number} el end line * @param {number} ec end column * @param {string} name export name * @param {import("../dependencies/CssIcssExportDependency").Value} value value or [localName, importName, request?] * @param {Range=} range source range to replace, when present * @param {boolean=} interpolate whether the value needs interpolation * @param {import("../dependencies/CssIcssExportDependency").ExportMode=} exportMode export mode * @param {import("../dependencies/CssIcssExportDependency").ExportType=} exportType export type * @returns {void} */ const addCssExport = ( sl, sc, el, ec, name, value, range, interpolate = false, exportMode = CssIcssExportDependency.EXPORT_MODE.REPLACE, exportType = CssIcssExportDependency.EXPORT_TYPE.NORMAL ) => { // Flat location numbers — the nested loc objects were the parser's // hottest allocation (3 objects per exported name). cssExportEntries.push({ name, value, range, interpolate, exportMode, exportType, locStartLine: sl, locStartColumn: sc, locEndLine: el, locEndColumn: ec }); }; const parsedModuleResource = parseResource( /** @type {string} */ (module.getResource()) ); const mode = this.defaultMode === "auto" && module.type === CSS_MODULE_TYPE_AUTO && IS_MODULES.test(parsedModuleResource.path) ? "local" : this.defaultMode; const isModules = mode === "global" || mode === "local"; /** @type {Map<string, boolean>} */ const selfReferenceCache = new Map(); /** * Whether a relative `from "<request>"` resolves back to the current module (matching query/fragment too). * Memoized per parse — `composes … from "./x.css"` repeats the same request many times per file. * @param {string} request request string from `from "<request>"` * @returns {boolean} true if request resolves to the current module */ const isSelfReferenceRequest = (request) => { const cached = selfReferenceCache.get(request); if (cached !== undefined) return cached; const result = isSelfReferenceRequestUncached(request); selfReferenceCache.set(request, result); return result; }; /** * Uncached `isSelfReferenceRequest`. * @param {string} request request string from `from "<request>"` * @returns {boolean} true if request resolves to the current module */ const isSelfReferenceRequestUncached = (request) => { if (!RELATIVE_REQUEST.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 }); const knownPropertyIndex = getKnownPropertyIndex(knownProperties); /** @type {CssModuleBuildMeta} */ (module.buildMeta).isCssModule = isModules; if (/** @type {CssModule} */ (module).exportType === "style") { /** @type {CssModuleBuildMeta} */ (module.buildMeta).needIdInConcatenation = true; } const locConverter = new LocConverter(source); /** * Source location of a byte range. `LocConverter#get` mutates and returns itself, so snapshot between the two calls. * @param {number} start start offset * @param {number} end end offset * @returns {{ start: Position, end: Position }} the source location */ const rangeLoc = (start, end) => { const s = locConverter.get(start); const sl = s.line; const sc = s.column; const e = locConverter.get(end); return { start: { line: sl, column: sc }, end: { line: e.line, column: e.column } }; }; /** * Set a dependency's source location from a byte range. * @param {ConstDependency | CssUrlDependency | CssImportDependency | CssIcssImportDependency | CssIcssSymbolDependency} dep dependency with `setLoc` * @param {number} start start offset * @param {number} end end offset */ const setDepLoc = (dep, start, end) => { const s = locConverter.get(start); const sl = s.line; const sc = s.column; const e = locConverter.get(end); dep.setLoc(sl, sc, e.line, e.column); }; /** * Apply the magic comments in `range`: warn on any compilation error, validate `webpackIgnore`, and return both the parsed options (for resource-hint / other magic-comment consumers) and whether the resource is ignored. * @param {Range} range byte range to scan for magic comments * @param {number} warnStart start offset of the loc for an invalid-`webpackIgnore` warning (computed lazily) * @param {number} warnEnd end offset of that loc * @returns {{ ignored: boolean, options: Record<string, EXPECTED_ANY> | null }} parsed options and `webpackIgnore` flag */ const magicCommentsIn = (range, warnStart, warnEnd) => { /** @type {{ options: Record<string, EXPECTED_ANY> | null, errors: (Error & { comment: Comment })[] | null }} */ const { options, errors } = parseCommentOptionsInRange( /** @type {(Comment & { range: [number, number], value: string })[]} */ ( comments ), range, this.magicCommentContext ); if (errors) { for (const e of errors) { state.module.addWarning( new (getCommentCompilationWarning())( `Compilation error while processing magic comment(-s): /*${e.comment.value}*/: ${e.message}`, rangeLoc(e.comment.range[0], e.comment.range[1]) ) ); } } let ignored = false; if (options && options.webpackIgnore !== undefined) { if (typeof options.webpackIgnore !== "boolean") { // Loc is computed lazily here — it's only needed for this rare // warning, not on every checked `url()` / `@import`. state.module.addWarning( new (getUnsupportedFeatureWarning())( `\`webpackIgnore\` expected a boolean, but received: ${options.webpackIgnore}.`, rangeLoc(warnStart, warnEnd) ) ); } else { ignored = options.webpackIgnore; } } return { ignored, options }; }; /** * Backwards-compatible boolean shortcut of {@link magicCommentsIn} for call sites that only need the ignore flag. * @param {Range} range byte range to scan for magic comments * @param {number} warnStart start offset of the loc for an invalid-`webpackIgnore` warning * @param {number} warnEnd end offset of that loc * @returns {boolean} true when `webpackIgnore: true` */ const webpackIgnored = (range, warnStart, warnEnd) => magicCommentsIn(range, warnStart, warnEnd).ignored; // Closure-scope alias for `source` used by AST-walking helpers for substring extraction. const input = source; // `@custom-media` / `@custom-selector` are build-time only (no engine ships them), so they're resolved by file-local substitution. The `includes` gates keep files without them at zero overhead; definitions may follow their uses (names are stylesheet-global), so uses are collected during the walk and resolved after it. const mayHaveCustomMedia = this.options.customMedia && input.includes("@custom-media"); const mayHaveCustomSelectors = this.options.customSelectors && input.includes("@custom-selector"); /** @type {Map<string, CustomMediaDefinition> | undefined} */ let customMediaDefs; /** @type {Map<string, CustomMediaValue> | undefined} */ let customMediaValues; /** @type {{ queries: MediaNode[], start: number, end: number, uses: CustomMediaUse[] }[] | undefined} */ let customMediaQueries; /** @type {Map<string, string> | undefined} */ let customSelectorDefs; /** @type {{ name: string, start: number, end: number }[] | undefined} */ let customSelectorUses; /** * Unescape a CSS identifier from a source byte range — for value spans not * backed by a single token (string contents, `--` dashed-ident bodies, * composed names). Token-backed names use `A.unescaped(node)` instead. * @param {number} start start offset * @param {number} end end offset * @returns {string} the unescaped identifier */ const unescapeRange = (start, end) => unescapeIdentifier(input.slice(start, end)); let lastTokenEndForComments = 0; /** Generates unique `__ICSS_IMPORT_${n}__` placeholders per ICSS import. */ const nextIcssImportName = (() => { let n = 0; return () => `__ICSS_IMPORT_${n++}__`; })(); // All pure-mode state and helpers live on `pure`. When `pure.enabled` is false, the methods are no-ops, so callers can use them unconditionally. const pure = { enabled: isModules && Boolean(this.options.pure), /** Whether the current rule's prelude has so far seen any impure comma-separated selector (set by `finalizeSelector`). */ ruleImpure: false, /** Whether the current comma-separated selector has carried a local class / id (cleared by `finalizeSelector`). */ segmentLocal: false, /** File-level kill switch from a top-of-file `cssmodules-pure-no-check` comment. */ noCheck: false, /** Single-shot ignore from a `cssmodules-pure-ignore` comment — consumed by the next rule frame. */ ignorePending: false, /** Has any top-level rule been processed (locks `noCheck`)? */ seenTopLevelRule: false, /** * Inherited per open block: `ancestorHadLocal` (nested rules inherit purity from a local-bearing ancestor) and `skipChildren` (a check-suppressing ancestor like `@keyframes`). * @type {{ ancestorHadLocal: boolean, skipChildren: boolean }[]} */ stack: [], /** * Whether any ancestor (self inclusive) was pure — for ancestor-inheritance and `&`-resolution. * @returns {boolean} true if any ancestor provided a local */ ancestorHadLocal() { const top = this.stack[this.stack.length - 1]; return top ? top.ancestorHadLocal : false; }, /** * Record that the current comma-separated selector carries a local class / id (no-op when pure-mode is off). */ markLocal() { if (this.enabled) this.segmentLocal = true; }, /** * Close the current comma-separated selector segment (or whole prelude at `{`): if it had no local and no ancestor compensates, the rule is impure (no-op when pure-mode is off). */ finalizeSelector() { if (!this.enabled) return; if (!this.segmentLocal && !this.ancestorHadLocal()) { this.ruleImpure = true; } this.segmentLocal = false; }, /** * Mark that a top-level rule has been processed; locks `noCheck` (no-op when pure-mode is off). */ markSeenTopLevelRule() { if (this.enabled) this.seenTopLevelRule = true; }, /** * Report a pure-mode violation covering the entire rule prelude. * @param {number} start prelude start offset * @param {number} end prelude end offset (`{` position) */ report: (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 ); }, /** * Rule entry: report an impure leaf-ish rule (prelude purity is known, body already parsed), push the inherited-context frame, reset per-rule selector flags. * @param {{ isRulePrelude: boolean, treatAsLeaf: boolean, ownSkip: boolean, declarations: Declaration[] | null, childRules: Rule[] | null, preludeStart: number, preludeEnd: number }} opts entry options */ enterBlock(opts) { if (!this.enabled) return; const { isRulePrelude, treatAsLeaf, ownSkip, declarations, childRules, preludeStart, preludeEnd } = opts; const top = this.stack[this.stack.length - 1]; const skipOwn = top ? top.skipChildren : false; const reportable = !this.noCheck && !this.ignorePending && !skipOwn && isRulePrelude && this.ruleImpure; if (reportable) { const hasBody = Boolean(declarations || childRules); let leaf = treatAsLeaf || !hasBody; if (!leaf && hasBody) { const { hasDirectDecl, hasNestedBlock } = scanRuleBody( declarations, childRules ); leaf = hasDirectDecl || !hasNestedBlock; } if (leaf) this.report(preludeStart, preludeEnd); } this.stack.push({ ancestorHadLocal: this.ancestorHadLocal() || (isRulePrelude && !this.ruleImpure), skipChildren: ownSkip || skipOwn }); this.ignorePending = false; this.segmentLocal = false; this.ruleImpure = false; }, /** * Drop the inherited-context frame (no-op when pure-mode is off). */ exitBlock() { if (this.enabled) this.stack.pop(); }, /** * Apply a comment's pure-mode side effect: `ignorePending` for `cssmodules-pure-ignore`, or the file-level `noCheck` for `cssmodules-pure-no-check` before the first top-level rule. * @param {string} value comment body (without the surrounding delimiters) */ applyComment(value) { if (PURE_IGNORE_RE.test(value)) { this.ignorePending = true; } else if (PURE_NO_CHECK_RE.test(value) && !this.seenTopLevelRule) { this.noCheck = true; } } }; /** @typedef {{ value?: string, importName?: string, localName?: string, request?: string }} IcssDefinition */ /** @type {Map<string, IcssDefinition>} */ const icssDefinitions = new Map(); // `composes: … from "<file>"` load-order graph (postcss-modules-extract-imports#138); topologically sorted at end-of-parse to tag each file's first composes-import with `sourceOrder`. /** @type {Map<string, Set<string>>} */ const composesGraph = new Map(); /** @type {Map<string, CssIcssImportDependency>} */ const composesFirstFileImport = new Map(); // Per-rule CSS-Modules state, saved on the rule's state stack at enter and restored at exit. `composesPrevFile` / `composesFiles` are only meaningful inside qualified rules (composes can't appear in at-rule preludes). const currentRule = { /** Did this rule's prelude declare a local-mode anchor selector? */ hasLocalAnchor: false, /** Local class / id names in source order (composes reads `[0]` as the anchor). */ /** @type {string[]} */ localIdentifiers: [], /** Previous `composes: … from "…"` file in this rule (for the load-order graph edges). */ /** @type {string | undefined} */ composesPrevFile: undefined, /** All files this rule has composed from (so an edge is added only once per file pair); lazily created — null until the rule's first `composes: … from`. */ /** @type {Set<string> | null} */ composesFiles: null }; /** * Whether the module's default mode is local (callers here have no `:local`/`:global` wrapper in scope, so it reduces to the default mode). * @returns {boolean} true when the module's default mode is `local` */ const isLocalMode = () => mode === "local"; /** * Effective local mode: persistent `:local`/`:global` from `modeData` if any, else the module's default. * @returns {boolean} true when the effective mode is local */ const isEffectivelyLocal = () => modeData ? modeData === "local" : mode === "local"; /** * Comment visitor (`NodeType.Comment`): push every comment (in source order) onto the local `comments`, read back by `advanceCommentCursor` (pure-mode flags) and `parseCommentOptionsInRange` (magic comments). * @param {import("./syntax").CssPath} path walk path at the comment node */ const commentVisitor = (path) => { const node = path.node; const start = A.start(node); const end = A.end(node); comments.push({ value: source.slice(start + 2, end - 2), range: [start, end] }); }; /** * Advance past every comment closing at/before `until` (in source order) and apply its pure-mode side effect: `pure.ignorePending` (next rule) or the file-level `pure.noCheck` (only before the first top-level rule). The cursor is closed over so it isn't visible at parser scope. * @returns {(until: number) => void} the cursor-advance function */ const advanceCommentCursor = (() => { let cursor = 0; /** @param {number} until source position to advance the cursor to */ return (until) => { if (!pure.enabled) return; while (cursor < comments.length) { const c = comments[cursor]; if (c.range[1] > until) return; pure.applyComment(c.value); cursor++; } }; })(); // 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) => { // No `@value` / `:import` / composes definitions: skip the `--` key // concat + map probe (the common case for plain CSS-Modules files). const reexport = icssDefinitions.size === 0 ? undefined : 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, reusing the already-parsed rule nodes. * @param {0 | 1} type import or export * @param {AstNode} second the `import(…)` function / `export` ident node from the prelude * @param {QualifiedRule} rule the `:import` / `:export` qualified rule * @returns {number} position after parse */ const processImportOrExport = (type, second, rule) => { /** @type {string | undefined} */ let request; if (type === 0) { const parsed = parseIcssImportRequest(second, rule, source); if ("errorPos" in parsed) { const { errorPos } = parsed; this._emitWarning( state, `Unexpected '${source[errorPos]}' at ${errorPos} during parsing of ':import' (expected string)`, locConverter, errorPos, errorPos ); return errorPos; } request = parsed.request; } /** * 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 ); setDepLoc(dep, start, end); module.addDependency(dep); icssDefinitions.set(name, { importName: value, request: /** @type {string} */ (request) }); } else if (type === 1) { const { line: sl, column: sc } = locConverter.get(start); const { line: el, column: ec } = locConverter.get(end); recordDeclaration(name, ":export", sl, sc); addCssExport(sl, sc, el, ec, name, getReexport(value)); } }; // Body `{ name: value; … }` is parsed eagerly (§5.4.4) — emit a dep per declaration. const ruleDecls = A.declarations(rule); if (!ruleDecls || A.blockStart(rule) === -1) return A.end(second); for (const decl of ruleDecls) { const vals = A.children(decl); if (vals.length === 0) continue; const rawStart = A.start(vals[0]); const rawEnd = A.end(vals[vals.length - 1]); createDep( source.slice(A.nameStart(decl), A.nameEnd(decl)), source.slice(rawStart, rawEnd), A.nameEnd(decl), rawEnd ); } return skipWhiteLine(source, A.blockEnd(rule)); }; /** * Emit a `CssIcssSymbolDependency` rewrite for an ident resolving to an `@value`-defined ICSS symbol (source-order semantics hold since the walker handles each `@value` before later references). * @param {string} name ICSS symbol name * @param {number} start start position * @param {number} end end position * @returns {number} `end` of the rewritten ident range */ const emitICSSSymbol = (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 a `local(...)` / `global(...)` pseudo-function: strip the call (and a leading legacy `:`) via a presentational dep, then emit `local()`'s inner top-level idents as ICSS exports. * @param {FunctionNode} fn parsed local/global function node * @param {1 | 2} type 1 = local, 2 = global * @param {string=} declarationKind human-readable export kind to use for conflict tracking when the caller is a declaration site (selector or at-rule prelude). Undefined for property-value references, which are not tracked. */ const processLocalOrGlobalFunction = (fn, type, declarationKind) => { // Replace `local(` / `global(` (and a leading `:` for the `:local(`/`:global(` selector form) with empty. const fnStart = A.start(fn); const isColon = input.charCodeAt(fnStart - 1) === CC_COLON; const openEnd = A.nameEnd(fn) + 1; module.addPresentationalDependency( new ConstDependency("", [isColon ? fnStart - 1 : fnStart, openEnd]) ); if (type === 1) { for (const cv of A.children(fn)) { if (A.type(cv) !== NodeType.Ident) continue; let identifier = A.unescaped(cv); // Cursor reads instead of `A.loc` — no location objects allocated. const { line: sl, column: sc } = locConverter.get(A.start(cv)); const { line: el, column: ec } = locConverter.get(A.end(cv)); const isDashedIdent = isDashedIdentifier(identifier); if (isDashedIdent) identifier = identifier.slice(2); if (declarationKind !== undefined) { recordDeclaration( identifier, isDashedIdent ? "custom property" : declarationKind, sl, sc ); } addCssExport( sl, sc, el, ec, identifier, getReexport(identifier), [A.start(cv), A.end(cv)], true, CssIcssExportDependency.EXPORT_MODE.ONCE, isDashedIdent ? CssIcssExportDependency.EXPORT_TYPE.CUSTOM_VARIABLE : CssIcssExportDependency.EXPORT_TYPE.NORMAL ); } } // Replace the closing `)`. module.addPresentationalDependency( new ConstDependency("", [A.end(fn) - 1, A.end(fn)]) ); }; /** * Localize the prelude name of `@keyframes` / `@counter-style` / `@container`: `options.string` takes the first string, `options.identifier` the first ident (a `RegExp` skips matching keywords), `:local()`/`:global()` count as found; top-level `var()`/`style()` dashed idents are always ICSS-processed. * @param {AtRule} atRule parsed at-rule * @param {{ string?: boolean, identifier?: boolean | RegExp }} options which prelude value kinds count as the local name * @param {string} kind human-readable export kind for the surrounding at-rule (`"@keyframes"`, `"@counter-style"`, `"@container"`) * @returns {number} position after handling */ const processLocalAtRule = (atRule, options, kind) => { let found = false; for (const cv of A.prelude(atRule)) { const cvType = A.type(cv); if (cvType === NodeType.Whitespace) continue; if (cvType === NodeType.String) { if (!found && options.string) { const value = A.unescaped(cv); const { line: sl, column: sc } = locConverter.get(A.start(cv)); const { line: el, column: ec } = locConverter.get(A.end(cv)); recordDeclaration(value, kind, sl, sc); addCssExport( sl, sc, el, ec, value, value, [A.start(cv), A.end(cv)], true, CssIcssExportDependency.EXPORT_MODE.ONCE ); found = true; pure.markLocal(); } continue; } if (cvType === NodeType.Ident) { if (!found && options.identifier) { const identifier = A.unescaped(cv); if ( options.identifier instanceof RegExp && options.identifier.test(identifier) ) { continue; } const { line: sl, column: sc } = locConverter.get(A.start(cv)); const { line: el, column: ec } = locConverter.get(A.end(cv)); recordDeclaration(identifier, kind, sl, sc); addCssExport( sl, sc, el, ec, identifier, getReexport(identifier), [A.start(cv), A.end(cv)], true, CssIcssExportDependency.EXPORT_MODE.ONCE, CssIcssExportDependency.EXPORT_TYPE.NORMAL ); found = true; pure.markLocal(); } continue; } if (cvType === NodeType.Function) { const fn = /** @type {FunctionNode} */ (cv); const fname = A.unescapedName(fn); const type = equalsLowerCase(fname, "local") ? 1 : equalsLowerCase(fname, "global") ? 2 : undefined; if (!found && type) { found = true; if (type === 1) pure.markLocal(); processLocalOrGlobalFunction(fn, type, kind); continue; } if ( this.options.dashedIdents && isLocalMode() && (equalsLowerCase(fname, "var") || equalsLowerCase(fname, "style")) ) { processDashedIdentInVarFunction(fn); } } } return A.end(atRule); }; /** * Emit the ICSS export declaring this module exports the given custom property. * @param {number} identStart start of the `--<name>` ident * @param {number} identEnd end of the ident (exclusive) */ const emitDashedIdentExport = (identStart, identEnd) => { const identifier = unescapeRange(identStart + 2, identEnd); const { line: sl, column: sc } = locConverter.get(identStart); const { line: el, column: ec } = locConverter.get(identEnd); recordDeclaration(identifier, "custom property", sl, sc); addCssExport( sl, sc, el, ec, identifier, getReexport(identifier, undefined, true), [identStart, identEnd], true, CssIcssExportDependency.EXPORT_MODE.ONCE, CssIcssExportDependency.EXPORT_TYPE.CUSTOM_VARIABLE ); }; /** * Emit `--<name> from "<path>"` as an ICSS import + export, stripping ` from "<path>"` so the runtime sees just `--<name>` (dep ranges end at `sourceEnd - 1`, the closing quote). * @param {number} identStart start of the `--<name>` ident * @param {number} identEnd end of the ident * @param {number} fromIdentStart start of the `from` keyword (lower bound of the strip) * @param {number} sourceEnd position past the closing quote of the source string * @param {string} pathContent unquoted path between the source's quotes */ const emitDashedIdentImport = ( identStart, identEnd, fromIdentStart, sourceEnd, pathContent ) => { const identifier = unescapeRange(identStart + 2, identEnd); const { line: sl, column: sc } = locConverter.get(identStart); const { line: el, column: ec } = locConverter.get(sourceEnd - 1); const localName = nextIcssImportName(); const importDep = new CssIcssImportDependency( pathContent, [identStart, sourceEnd - 1], /** @type {"local" | "global"} */ (mode), identifier, localName ); importDep.setLoc(sl, sc, el, ec); module.addDependency(importDep); recordDeclaration(identifier, "custom property", sl, sc); addCssExport( sl, sc, el, ec, identifier, getReexport(identifier, localName, true), [identStart, sourceEnd - 1], true, CssIcssExportDependency.EXPORT_MODE.ONCE, CssIcssExportDependency.EXPORT_TYPE.CUSTOM_VARIABLE ); module.addPresentationalDependency( new ConstDependency("", [fromIdentStart, sourceEnd]) ); }; /** * Strip ` from global` and emit no ICSS export (an explicitly-global custom property isn't a CSS-Modules name). * @param {number} identEnd end of the `--<name>` ident * @param {number} sourceEnd position past the `global` ident */ const emitDashedIdentFromGlobal = (identEnd, sourceEnd) => { module.addPresentationalDependency( new ConstDependency("", [identEnd, sourceEnd]) ); }; /** * Scope a dashed-ident inside `var(…)` / `style(…)`: emit the first (dashed) ident and its optional `from <ident|string>` suffix. * @param {FunctionNode} fn parsed `var`/`style` function node */ // Per-`var()`/`style()` dashed-ident scan + dispatch. Warm path (custom-property-heavy CSS has thousands of these), so it dispatches inline rather than allocating a descriptor per call. const processDashedIdentInVarFunction = (fn) => { /** @type {Token | undefined} */ let identNode; let identIdx = -1; const fv = A.children(fn); for (let i = 0; i < fv.length; i++) { const cv = fv[i]; if (A.type(cv) === NodeType.Whitespace) continue; if (A.type(cv) === NodeType.Ident) { identNode = /** @type {Token} */ (cv); identIdx = i; } break; } if (!identNode) return; const identStart = A.start(identNode); const identEnd = A.end(identNode); // `isDashedIdentifier` on the value without slicing it: a literal `--` // prefix of length >= 3 (escaped dashes don't match, same as the string // form, since the raw bytes aren't `--`). if ( identEnd - identStart < 3 || input.charCodeAt(identStart) !== CC_HYPHEN_MINUS || input.charCodeAt(identStart + 1) !== CC_HYPHEN_MINUS ) { return; } let j = identIdx + 1; while (j < fv.length && A.type(fv[j]) === NodeType.Whitespace) { j++; } if ( j >= fv.length || A.type(fv[j]) !== NodeType.Ident || !rangeEqualsLowerCase(input, A.start(fv[j]), A.end(fv[j]), "from") ) { emitDashedIdentExport(identStart, identEnd); return; } const fromIdent = fv[j]; j++; while (j < fv.length && A.type(fv[j]) === NodeType.Whitespace) { j++; } if (j >= fv.length) return; const src = fv[j]; if ( A.type(src) === NodeType.Ident && rangeEquals(input, A.start(src), A.end(src), "global") ) { emitDashedIdentFromGlobal(identEnd, A.end(src)); return; } if (A.type(src) === NodeType.String) { emitDashedIdentImport( identStart, identEnd, A.start(fromIdent), A.end(src), input.slice(A.start(src) + 1, A.end(src) - 1) ); } }; // `allowImport` mirrors `allowImportAtRule`: true until the first top-level block-bearing rule. let allowImport = true; // Persistent CSS-Modules mode for a top-level rule: set by bare `:local` / `:global`, leaks into sibling rules, reset at each top-level `}`. /** @type {"local" | "global" | undefined} */ let modeData; // Suppress localizing the next qualified rule's selectors after a `;`-terminated at-rule. let suppressNextRulePrelude = false; // Dashed-ident (custom-property) scoping state — mutated across function nesting (saved/restored via `dashed.stack`). const dashed = { /** Is dashed-ident scoping active in the current value context? */ active: false, /** Should top-level dashed-ident exports be emitted at this nesting level? */ emit: false, /** Directly inside a `counter()`-family call whose arguments were already rewritten — suppresses the generic ICSS ident rewrite over the same ranges. */ counterArgs: false, /** LIFO save/restore of `active` + `emit` + `counterArgs` across function nesting, bit-packed (bit 0 = active, bit 1 = emit, bit 2 = counterArgs) to avoid a per-function-token object allocation. */ /** @type {number[]} */ stack: [], /** Push the current scope; call before descending into a `Function` body. */ push() { this.stack.push( (this.active ? 1 : 0) | (this.emit ? 2 : 0) | (this.counterArgs ? 4 : 0) ); }, /** Pop the saved scope; call when leaving a `Function` body. */ pop() { const s = /** @type {number} */ (this.stack.pop()); this.active = (s & 1) !== 0; this.emit = (s & 2) !== 0; this.counterArgs = (s & 4) !== 0; } }; // Nearest enclosing declaration / at-rule / qualified-rule, set by each structural enter; the Url / Function / Ident / Comma visitors read it (via `urlActive` / `localGlobalActive` / `icssActive`) to decide value handling from the node hierarchy instead of carrying precomputed flags. /** @type {AstNode | undefined} */ let currentStructural; // Cached on each structural enter and read per value token, so the hot Function / Ident / Url visitors don't re-derive the property / at-rule name on every visit. let currentAtRuleName = ""; // Set by `handleImportAtRule` for a malformed `@import` so its prelude still emits orphan url() deps; read by `urlActive`. Reset per at-rule enter (replaces an ad-hoc property on the node). let currentUrlRecovery = false; /** Whether the current Declaration's property is a localizable known property. */ let currentDeclIsKnownProperty = false; /** Whether the current `composes:` declaration owns the whole value (suppresses value rewrites). */ let currentDeclComposesSkip = false; /** Whether `counter()`-family arguments are scoped in the current declaration's value. */ let currentDeclCounterActive = false; /** * Per-at-rule scope frames; `exit` reads `hasBlock` to pick the block-cleanup vs `suppressNextRulePrelude` branch. * @type {{ savedAnchor: boolean, savedLocalIdentifierCount: number, name: string, hasBlock: boolean, endsWithSemicolon: boolean, fontPreloaded: boolean }[]} */ const atRuleStateStack = []; /** * Strip a bare `:local` / `:global` marker (modules only): the marker plus one adjacent whitespace (a comment between ends the run, since comments aren't AST nodes). * @param {AstNode} colon the `:` node * @param {AstNode} ident the `local` / `global` ident node * @param {AstNode} after the node following the ident (may be `undefined` at runtime) * @returns {boolean} whether whitespace follows the marker */ const stripBareMarker = (colon, ident, after) => { const afterIsWhitespace = Boolean( after && A.type(after) === NodeType.Whitespace ); const identEnd = A.end(ident); const stripEnd = afterIsWhitespace && A.start(after) === identEnd ? A.end(after) : identEnd; if (isModules) { module.addPresentationalDependency( new ConstDependency("", [A.start(colon), stripEnd]) ); } return afterIsWhitespace; }; /** * Strip a `:local(…)` / `:global(…)` wrapper (modules only) with two source-level deps: the leading `:name(` up to the first arg, and the trailing `)` (`:local` also eats whitespace before it). * @param {AstNode} colon the `:` node * @param {FunctionNode} fn the `local(…)` / `global(…)` function node * @param {boolean} isLocal whether the marker is `:local(` * @returns {void} */ const stripFunctionMarker = (colon, fn, isLocal) => { if (!isModules) return; const fnEnd = A.end(fn); let stripLeadEnd = fnEnd - 1; for (const arg of A.children(fn)) { if (A.type(arg) !== NodeType.Whitespace) { stripLeadEnd = A.start(arg); break; } } module.addPresentationalDependency( new ConstDependency("", [A.start(colon), stripLeadEnd]) ); let trailStart = fnEnd - 1; // position of `)` if (isLocal) { while ( trailStart > 0 && isWhitespace(source.charCodeAt(trailStart - 1)) ) { trailStart--; } } module.addPresentationalDependency( new ConstDependency("", [trailStart, fnEnd]) ); }; /** * Emit the ICSS export for an attribute selector `[class="foo"]` / `[class~="foo"]` (not a composes anchor) by walking the `[…]` block's parsed children. No-op for any other attribute shape. * @param {SimpleBlock} block the `[…]` block * @returns {void} */ const handleAttributeSelector = (block) => { const attrParts = A.children(block); let ai = 0; while ( ai < attrParts.length && A.type(attrParts[ai]) === NodeType.Whitespace ) { ai++; } const attrNameNode = attrParts[ai]; if (!attrNameNode || A.type(attrNameNode) !== NodeType.Ident) return; const attrName = A.unescaped(attrNameNode); if (!equalsLowerCase(attrName, "class")) return; ai++; while ( ai < attrParts.length && A.type(attrParts[ai]) === NodeType.Whitespace ) { ai++; } // `=` or `~=` (two `Delim` tokens for the latter). const op1 = attrParts[ai]; if (!op1 || A.type(op1) !== NodeType.Delim) return; const op1v = A.value(op1); if (op1v === "~") { ai++; const op2 = attrParts[ai]; if (!op2 || A.type(op2) !== NodeType.Delim || A.value(op2) !== "=") { return; } } else if (op1v !== "=") { return; } ai++; while ( ai < attrParts.length && A.type(attrParts[ai]) === NodeType.Whitespace ) { ai++; } const attrValueNode = attrParts[ai]; if (!attrValueNode) return; /** @type {number} */ let classNameStart; /** @type {number} */ let classNameEnd; if (A.type(attrValueNode) === NodeType.String) { classNameStart = A.start(attrValueNode) + 1; classNameEnd = A.end(attrValueNode) - 1; } else if (A.type(attrValueNode) === NodeType.Ident) { classNameStart = A.start(attrValueNode); classNameEnd = A.end(attrValueNode); } else { return; } const className = unescapeRange(classNameStart, classNameEnd); const { line: sl, column: sc } = locConverter.get(classNameStart); const { line: el, column: ec } = locConverter.get(classNameEnd); addCssExport( sl, sc, el, ec, className, getReexport(className), [classNameStart, classNameEnd], true, CssIcssExportDependency.EXPORT_MODE.NONE ); }; /** * Scope a `::view-transition-*()` pseudo-element's `(<pt-name> .<pt-class>…)` argument: each non-dashed custom-ident (the part name and its classes) is localized like a `view-transition-name` value; `*` and dashed idents are left untouched. Keeps the pseudo consistent with the scoped `view-transition-name`/`-class` declarations. * @param {AstNode[]} cvs the pseudo-element's argument component values * @returns {void} */ const walkViewTransitionPart = (cvs) => { // customIdents off: leave the whole `(…)` untouched (both name and classes), consistent with unscoped `view-transition-name`/`-class` declarations. if (!this.options.customIdents) return; for (const cv of cvs) { if (A.type(cv) !== NodeType.Ident) continue; const name = A.unescaped(cv); if (isDashedIdentifier(name)) continue; const start = A.start(cv); const end = A.end(cv); const { line: sl, column: sc } = locConverter.get(start); const { line: el, column: ec } = locConverter.get(end); addCssExport( sl, sc, el, ec, name, getReexport(name), [start, end], true, CssIcssExportDependency.EXPORT_MODE.ONCE ); pure.markLocal(); } }; /** * Emit the ICSS export for one ident argument of a `counter()`-family call. Dashed names are left to the blanket dashed-ident scanner, and reserved names (UA counters, predefined counter styles, CSS-wide keywords) stay global. * @param {AstNode} node the ident node * @param {string} reservedFrom property whose keywords must not be localized here * @returns {void} */ const emitCounterIdent = (node, reservedFrom) => { const name = A.unescaped(node); if (isDashedIdentifier(name)) return; const reserved = /** @type {Map<string, number>} */ (CSS_MODULES_KEYWORDS.get(reservedFrom)); if (reserved.has(toLowerCaseIfNeeded(name))) return; const start = A.start(node); const end = A.end(node); const { line: sl, column: sc } = locConverter.get(start); const { line: el, column: ec } = locConverter.get(end); addCssExport( sl, sc, el, ec, name, getReexport(name), [start, end], true, CssIcssExportDependency.EXPORT_MODE.ONCE ); }; /** * Scope a `counter()` / `counters()` / `target-counter()` / `target-counters()` call: the counter-name argument localizes like a `counter-reset` value and a trailing `<counter-style>` ident like a `list-style-type` value, so both keep naming the scoped `counter-*` declaration / `@counter-style` rule. * @param {AstNode[]} cvs the call's argument component values * @param {number} nameIndex comma-separated index of the counter-name argument * @returns {void} */ const walkCounterFunction = (cvs, nameIndex) => { let group = 0; let groupNodes = 0; /** Sole ident of the current argument, or undefined when the argument isn't a lone ident. */ let lastIdent; for (const cv of cvs) { const type = A.type(cv); if (type === NodeType.Whitespace) continue; if (type === NodeType.Comma) { group++; groupNodes = 0; lastIdent = undefined; continue; } groupNodes++; const loneIdent = groupNodes === 1 && type === NodeType.Ident; if (group === nameIndex) { if (loneIdent) emitCounterIdent(cv, "counter-reset"); lastIdent = undefined; } else { lastIdent = loneIdent ? cv : undefined; } } // The counter style is the last argument when it's a lone ident (`counter(n, style)` / `counters(n, sep, style)`). if (lastIdent !== undefined && group > nameIndex) { emitCounterIdent(lastIdent, "list-style-type"); } }; /** * Walk component values as a selector list, emitting ID / attribute deps and recursing into `:not()`/`:is()`/`:local()`/`:global()` wrappers; `localMode` is the sub-tree mode and `topLevel` controls whether commas reset it (only outside parentheses). * @param {AstNode[]} values component values to walk * @param {"local" | "global"} localMode CSS-Modules mode applicable to this sub-tree * @param {boolean=} topLevel whether commas in this list reset to `localMode` (defaults to `true`) * @returns {void} */ const walkSelectorList = (values, localMode, topLevel = true) => { // At a rule's top level, inherit persistent `modeData` (or the one-shot `suppressNextRulePrelude` → "global"); recursive calls use the passed `localMode`. let segmentMode = localMode; if (topLevel) { if (suppressNextRulePrelude) { segmentMode = "global"; suppressNextRulePrelude = false; } else if (modeData) { segmentMode = modeData; } } for (let i = 0; i < values.length; i++) { const v = values[i]; switch (A.type(v)) { case NodeType.Whitespace: break; case NodeType.Comma: if (topLevel) { // Top-level comma resets the segment + persistent mode and, in pure mode, finalizes the segment's purity. segmentMode = localMode; modeData = undefined; pure.finalizeSelector(); } break; case NodeType.Colon: { // Look ahead for `:local` / `:global` markers; other pseudos fall through. const next = values[i + 1]; if (!next) break; const nextType = A.type(next); if (nextType === NodeType.Ident) { const raw = A.value(next); const isLocal = equalsLowerCase(raw, "local"); if (isLocal || equalsLowerCase(raw, "global")) { const id = isLocal ? "local" : "global"; // Bare `:local` / `:global`: switch the segment (and top-level persistent) mode and strip the marker. const afterIsWhitespace = stripBareMarker( v, next, values[i + 2] ); // Bare `:local` / `:global` needs whitespace before the next selector (else `:local.b` is ambiguous) — warn when none follows. if (!afterIsWhitespace) { this._emitWarning( state, `Missing whitespace after ':${id}' in '${source.slice( A.start(v), findLeftCurly(source, A.end(next)) + 1 )}'`, locConverter, A.start(v), A.end(next) ); } segmentMode = id; if (topLevel) modeData = id; // Skip past the colon + ident. i += 1; } } else if (nextType === NodeType.Function) { const fn = /** @type {FunctionNode} */ (next); const rawName = A.unescapedName(fn); const isLocal = equalsLowerCase(rawName, "local"); if (isLocal || equalsLowerCase(rawName, "global")) { // `:local(…)` / `:global(…)`: scope mode to the args and strip the `:name(` … `)` wrapper. stripFunctionMarker(v, fn, isLocal); walkSelectorList( A.children(fn), isLocal ? "local" : "global", false ); // Skip past the colon + function. i += 1; } } break; } case NodeType.Function: { // `::view-transition-group(name .class…)` etc.: scope the part name + classes as custom idents (the `v` char guard skips the slice for the common `:is`/`:not`/… functions). const fnNameStart = A.nameStart(v); if ( segmentMode === "local" && (input.charCodeAt(fnNameStart) === CC_LOWER_V || input.charCodeAt(fnNameStart) === CC_UPPER_V) && VIEW_TRANSITION_PART_PSEUDO.test(A.unescapedName(v)) ) { walkViewTransitionPart(A.children(v)); break; } // Any other function (`:not(…)`, `:is(…)`, …): recurse with the segment mode preserved (only `:local(…)` / `:global(…)`, handled above, switch mode). walkSelectorList(A.children(v), segmentMode, false); break; } case NodeType.Hash: { if (A.typeFlag(v) !== "id" || segmentMode !== "local") { break; } // ID selectors emit the ICSS export but aren't a `composes:` anchor. const idValueStart = A.start(v) + 1; const idName = A.unescaped(v); const { line: idSl, column: idSc } = locConverter.get(A.start(v)); const { line: idEl, column: idEc } = locConverter.get(A.end(v)); recordDeclaration(idName, "id", idSl, idSc); addCssExport( idSl, idSc, idEl, idEc, idName, getReexport(idName), [idValueStart, A.end(v)], true, CssIcssExportDependency.EXPORT_MODE.ONCE ); pure.markLocal(); break; } case NodeType.SimpleBlock: { const block = /** @type {SimpleBlock} */ (v); const bt = A.blockToken(block); if (bt === "[") { // Attribute selectors `[class="foo"]` localize only in local mode. if (segmentMode === "local") handleAttributeSelector(block); } else if (bt === "(") { // `@scope (.foo)` and other parenthesised selector wrappers — recurse into the `(…)` block. walkSelectorList(A.children(block), segmentMode, false); } break; } case NodeType.Delim: { const delim = A.value(v); if (delim === "&") { // Pure-mode: a nesting `&` inherits a pure ancestor's purity. if (topLevel && pure.ancestorHadLocal()) pure.markLocal(); break; } if (delim !== ".") break; const next = values[i + 1]; if (!next || A.type(next) !== NodeType.Ident) break; if (segmentMode === "local") { // `.<ident>` in local mode is a class selector (dep covers the ident bytes only). const name = A.unescaped(next); // Cursor reads instead of `A.loc` — this is the hottest export site. const { line: sl, column: sc } = locConverter.get(A.start(next)); const { line: el, column: ec } = locConverter.get(A.end(next)); recordDeclaration(name, "class", sl, sc); addCssExport( sl, sc, el, ec, name, getReexport(name), [A.start(next), A.end(next)], true, CssIcssExportDependency.EXPORT_MODE.ONCE ); currentRule.hasLocalAnchor = true; currentRule.localIdentifiers.push(name); pure.markLocal(); } else if (icssDefinitions.size !== 0) { // `.<ident>` in global mode: not localized, but the ident may be `@value`-defined and need ICSS rewrite. const ident = A.value(next); if (!isDashedIdentifier(ident) && icssDefinitions.has(ident)) { emitICSSSymbol(ident, A.start(next), A.end(next)); } } // Skip the consumed ident. i += 1; break; } case NodeType.Ident: { // ICSS rewrite for a bare `@value`-defined ident used as a type-style selector; only worth the slice when definitions exist. if (icssDefinitions.size === 0) break; const ident = A.value(v); if (!isDashedIdentifier(ident) && icssDefinitions.has(ident)) { emitICSSSymbol(ident, A.start(v), A.end(v)); } break; } default: break; } } // Pure-mode: finalize the trailing comma-separated segment. if (topLevel) pure.finalizeSelector(); }; /** * Per-qualified-rule scope frames; `{ bailed: true }` for inline-handled `:import` / `:export` pseudo-rules. * @type {({ bailed: true } | { bailed: false, savedAnchor: boolean, savedLocalIdentifierCount: number, savedPrevComposesFile: string | undefined, savedComposesFiles: Set<string> | null })[]} */ const qualifiedRuleStateStack = []; /** * Whether url() deps are emitted here (from `currentStructural`): off in an `@import` * prelude (the import target, unless recovering) and in `@namespace` (an opaque identifier). * @returns {boolean} true when url() deps should be emitted */ const urlActive = () => { if (!this.options.url || !currentStructural) return false; if (A.type(currentStructural) === NodeType.AtRule) { // `currentAtRuleName` is cached on at-rule enter — no per-value slice. if (currentAtRuleName === "@namespace") return false; return currentAtRuleName !== "@import" || currentUrlRecovery; } return true; }; /** * At-rules with a dedicated CSS-Modules handler, so the generic `local()`/`global()` value rewrite and dashed-ident scoping are off for them. * @param {string} name at-rule name including the leading `@`, lower-cased * @returns {boolean} true for `@import` / `@charset` / `@namespace` / `@value` / `@scope` and the option-gated `@keyframes` / `@counter-style` / `@container` */ const isLocalHandledAtRule = (name) => name === "@import" || name === "@charset" || name === "@namespace" || name === "@value" || name === "@scope" || (mayHaveCustomMedia && name === "@custom-media") || (mayHaveCustomSelectors && name === "@custom-selector") || (isModules && ((this.options.animation && OPTIONALLY_VENDOR_PREFIXED_KEYFRAMES_AT_RULE.test(name)) || (this.options.customIdents && name === "@counter-style") || (this.options.container && name === "@container"))); /** * Whether `local()` / `global()` value functions are rewritten to ICSS here (from `currentStructural`). * @returns {boolean} true when the rewrite is active */ const localGlobalActive = () => { if (!isModules || !currentStructural) return false; const t = A.type(currentStructural); if (t === NodeType.AtRule) { return !isLocalHandledAtRule(currentAtRuleName); } if (t === NodeType.Declaration) { return !currentDeclComposesSkip; } return false; }; /** * Whether `@value`-defined idents / function names are ICSS-rewritten here (from `currentStructural`). * @returns {boolean} true when the rewrite is active */ const icssActive = () => { if (!isModules || !currentStructural) return false; const t = A.type(currentStructural); if (t === NodeType.AtRule) { return ( currentAtRuleName !== "@value" && currentAtRuleName !== "@import" && currentAtRuleName !== "@custom-media" && currentAtRuleName !== "@custom-selector" ); } if (t === NodeType.Declaration) { return !currentDeclComposesSkip && !currentDeclIsKnownProperty; } return false; }; /** * Post-rule top-level reset shared by AtRule and QualifiedRule exit: lock `@import` (block-bearing only locks for at-rules), mark seen-top-level for pure-mode, and clear the persistent `:local`/`:global` override. * @param {AstNode} node the rule node * @param {boolean} blockBearing whether to require a body before locking `allowImport` (true for at-rules, ignored for QRs which always have a body) * @returns {void} */ const finishTopLevelRule = (node, blockBearing) => { if (!blockBearing || A.declarations(node) || A.childRules(node)) { allowImport = false; } pure.markSeenTopLevelRule(); modeData = undefined; }; /** * Emit the ICSS export/import deps for one parsed `composes:` group onto `lastLocalIdentifier`: `from "<file>"` imports each name (tracking file load order), `from global` and bare names re-export locally. The caller has already validated and scanned the group. * @param {{ start: number, end: number, isGlobal: boolean }[]} classNames composed names with source ranges * @param {{ kind: "string", path: string } | { kind: "global" } | undefined} fromSource the `from …` clause, if any * @param {string} lastLocalIdentifier the rule's single local-class anchor * @returns {void} */ const emitComposesGroup = (classNames, fromSource, lastLocalIdentifier) => { if (fromSource && fromSource.kind === "string") { const request = fromSource.path; const selfReference = isSelfReferenceRequest(request); if (!selfReference) { let files = currentRule.composesFiles; if (!files) { files = new Set(); currentRule.composesFiles = files; } if (!files.has(request)) { files.add(request); if ( currentRule.composesPrevFile !== undefined && currentRule.composesPrevFile !== request ) { let successors = composesGraph.get(currentRule.composesPrevFile); if (!successors) { successors = new Set(); composesGraph.set(currentRule.composesPrevFile, successors); } successors.add(request); } currentRule.composesPrevFile = request; } } for (const { start, end } of classNames) { const identifier = unescapeRange(start, end); const { line: sl, column: sc } = locConverter.get(start); const { line: el, column: ec } = locConverter.get(end); if (selfReference) { if (identifier === lastLocalIdentifier) continue; addCssExport( sl, sc, el, ec, lastLocalIdentifier, getReexport(identifier), [start, end], true, CssIcssExportDependency.EXPORT_MODE.SELF_REFERENCE, CssIcssExportDependency.EXPORT_TYPE.COMPOSES ); continue; } const localName = nextIcssImportName(); const importDep = new CssIcssImportDependency( request, [start, end], /** @type {"local" | "global"} */ (mode), identifier, localName ); importDep.setLoc(sl, sc, el, ec); module.addDependency(importDep); if (!composesFirstFileImport.has(request)) { composesFirstFileImport.set(request, importDep); } addCssExport( sl, sc, el, ec, lastLocalIdentifier, getReexport(identifier, localName), [start, end], true, CssIcssExportDependency.EXPORT_MODE.APPEND, CssIcssExportDependency.EXPORT_TYPE.COMPOSES ); } } else if (fromSource && fromSource.kind === "global") { for (const { start, end } of classNames) { const identifier = unescapeRange(start, end); const { line: sl, column: sc } = locConverter.get(start); const { line: el, column: ec } = locConverter.get(end); addCssExport( sl, sc, el, ec, lastLocalIdentifier, getReexport(identifier), [start, end], false, CssIcssExportDependency.EXPORT_MODE.APPEND, CssIcssExportDependency.EXPORT_TYPE.COMPOSES ); } } else { for (const { start, end, isGlobal } of classNames) { const identifier = unescapeRange(start, end); const { line: sl, column: sc } = locConverter.get(start); const { line: el, column: ec } = locConverter.get(end); addCssExport( sl, sc, el, ec, lastLocalIdentifier, getReexport(identifier), [start, end], !isGlobal, isGlobal ? CssIcssExportDependency.EXPORT_MODE.APPEND : CssIcssExportDependency.EXPORT_MODE.SELF_REFERENCE, CssIcssExportDependency.EXPORT_TYPE.COMPOSES ); } } }; /** * Emit the ICSS deps + presentational strip for a `composes: …` declaration whose rule has a single local-class anchor (the strip-dep covers the whole declaration). * @param {Declaration} decl the `composes` declaration */ const emitComposesWithAnchor = (decl) => { if (currentRule.localIdentifiers.length > 1) { this._emitWarning( state, `Composition is only allowed when selector is single local class name not in "${currentRule.localIdentifiers.join( '", "' )}"`, locConverter, A.start(decl), A.end(decl) ); return; } const lastLocalIdentifier = currentRule.localIdentifiers[0]; // Split the value at top-level commas — each segment is one `<name>+ [from <source>]` group. /** @type {AstNode[][]} */ const groups = []; /** @type {AstNode[]} */ let currentGroup = []; for (const cv of A.children(decl)) { if (A.type(cv) === NodeType.Comma) { groups.push(currentGroup); currentGroup = []; } else { currentGroup.push(cv); } } groups.push(currentGroup); // Inline scan + dispatch per group — warm path (composes-heavy modules), so no per-group result object is allocated. for (const group of groups) { /** @type {{ start: number, end: number, isGlobal: boolean }[]} */ const classNames = []; /** @type {"names" | "expecting-source" | "done"} */ let phase = "names"; /** @type {{ kind: "string", path: string } | { kind: "global" } | undefined} */ let fromSource; /** @type {AstNode | undefined} */ let errorToken; let errorMessage = ""; for (let i = 0; i < group.length; i++) { const cv = group[i]; if (A.type(cv) === NodeType.Whitespace) continue; if (phase === "expecting-source") { if (A.type(cv) === NodeType.String) { fromSource = { kind: "string", path: source.slice(A.start(cv) + 1, A.end(cv) - 1) }; phase = "done"; continue; } if ( A.type(cv) === NodeType.Ident && equalsLowerCase(A.value(cv), "global") ) { fromSource = { kind: "global" }; phase = "done"; continue; } errorToken = cv; errorMessage = "Incorrect composition, expected global keyword or string value"; break; } if (phase === "done") { continue; } if (A.type(cv) === NodeType.Ident) { const identValue = A.value(cv); if ( equalsLowerCase(identValue, "from") && classNames.length > 0 && nextNonWhitespace(group, i + 1) < group.length ) { phase = "expecting-source"; continue; } classNames.push({ start: A.start(cv), end: A.end(cv), isGlobal: false }); continue; } if (A.type(cv) === NodeType.Function) { const fn = /** @type {FunctionNode} */ (cv); const isGlobal = equalsLowerCase(A.unescapedName(fn), "global"); for (const inner of A.children(fn)) { if (A.type(inner) === NodeType.Ident) { classNames.push({ start: A.start(inner), end: A.end(inner), isGlobal }); break; } } continue; } errorToken = cv; errorMessage = "Incorrect composition, expected class named"; break; } if (!errorToken && phase === "expecting-source") { errorMessage = "Incorrect composition, expected global keyword or string value"; errorToken = /** @type {AstNode | undefined} */ ( group[group.length - 1] ); } if (errorToken) { this._emitWarning( state, errorMessage, locConverter, A.start(errorToken), A.end(errorToken) ); return; } if (classNames.length === 0) continue; emitComposesGroup(classNames, fromSource, lastLocalIdentifier); } // Strip the whole `composes: …;` (property name included) plus trailing same-line whitespace. The `;` and that whitespace aren't AST nodes (a block's contents drop them), so scan the source here. let resumeAt = A.end(decl); if (source.charCodeAt(A.end(decl)) === CC_SEMICOLON) { resumeAt = A.end(decl) + 1; while (isWhitespace(source.charCodeAt(resumeAt))) resumeAt++; } module.addPresentationalDependency( new ConstDependency("", [A.nameStart(decl), resumeAt]) ); }; /** * Emit url() deps for a `url(...)` / `src(...)` / `image-set(...)` value function. * @param {FunctionNode} fn the function node * @param {string | undefined} escapedName the unescaped function name when it carries an escape, undefined for the raw byte-range path */ const emitUrlFunction = (fn, escapedName) => { const fnNameStart = A.nameStart(fn); const fnNameEnd = A.nameEnd(fn); let isUrlOrSrc; let isImageSet = false; if (escapedName !== undefined) { isUrlOrSrc = equalsLowerCase(escapedName, "url") || equalsLowerCase(escapedName, "src"); if (!isUrlOrSrc) isImageSet = IMAGE_SET_FUNCTION.test(escapedName); } else { const nameLength = fnNameEnd - fnNameStart; isUrlOrSrc = nameLength === 3 && (rangeEqualsLowerCase(input, fnNameStart, fnNameEnd, "url") || rangeEqualsLowerCase(input, fnNameStart, fnNameEnd, "src")); if (!isUrlOrSrc) { // Suffix probe first, so only a vendor-prefixed `…-image-set` pays the slice + regex. isImageSet = nameLength >= 9 && rangeEqualsLowerCase( input, fnNameEnd - 9, fnNameEnd, "image-set" ) && (nameLength === 9 || IMAGE_SET_FUNCTION.test(input.slice(fnNameStart, fnNameEnd))); } } if (isUrlOrSrc) { // Quoted `url("…")` / `src("…")`: first non-whitespace value must be the string token. const first = A.children(fn)[nextNonWhitespace(A.children(fn), 0)]; if (!first || A.type(first) !== NodeType.String) return; const string = /** @type {Token} */ (first); const { ignored, options } = magicCommentsIn( [lastTokenEndForComments, A.start(fn)], A.start(string), A.end(string) ); if (ignored) return; const value = normalizeUrl( input.slice(A.start(string) + 1, A.end(string) - 1), true ); // Ignore `url()`, `url('')` and `url("")`, they are valid by spec if (value.length === 0) return; const dep = new CssUrlDependency( value, [A.start(string), A.end(string)], "string" ); setDepLoc(dep, A.start(string), A.end(string)); applyResourceHintDefaults( dep, value, options, rangeLoc(A.start(string), A.end(string)) ); module.addDependency(dep); module.addCodeGenerationDependency(dep); } else if (isImageSet) { // `image-set(…)`: each comma segment's first string is the URL; advance the magic-comment fence per string. lastTokenEndForComments = fnNameEnd + 1; let prevStringEnd = A.start(fn); let firstInSegment = true; for (const cv of A.children(fn)) { if (A.type(cv) === NodeType.Comma) { firstInSegment = true; continue; } if (A.type(cv) === NodeType.Whitespace) continue; const wasFirst = firstInSegment; firstInSegment = false; if (!wasFirst || A.type(cv) !== NodeType.String) continue; const string = /** @type {Token} */ (cv); const start = prevStringEnd; prevStringEnd = A.end(string); const value = normalizeUrl( input.slice(A.start(string) + 1, A.end(string) - 1), true ); if (value.length === 0) continue; const { ignored, options } = magicCommentsIn( [start, A.end(string)], A.start(string), A.end(string) ); if (ignored) continue; const dep = new CssUrlDependency( value, [A.start(string), A.end(string)], "url" ); setDepLoc(dep, A.start(string), A.end(string)); applyResourceHintDefaults( dep, value, options, rangeLoc(A.start(string), A.end(string)) ); module.addDependency(dep); module.addCodeGenerationDependency(dep); } } }; /** * Handle an `@import` at-rule: parse its prelude (url, layer, supports, media), emit the `CssImportDependency`, and warn on malformed forms. * @param {AtRule} at the `@import` at-rule * @param {boolean} topLevel whether the rule is at the stylesheet top level */ const handleImportAtRule = (at, topLevel) => { if (!this.options.import) return; if (!topLevel || !allowImport) { this._emitWarning( state, "Any '@import' rules must precede all other rules", locConverter, A.start(at), A.nameEnd(at) ); return; } const importStart = A.start(at); const importNameEnd = A.nameEnd(at); // We only accept `;`-terminated @import; block / EOF / `}` ends are silent bails. if (source.charCodeAt(A.end(at)) !== CC_SEMICOLON) return; // Walk the prelude in spec order (URL → layer? → supports? → media query); anything else joins the media query. const { urlNode, layerNode, supportsNode } = parseImportPrelude( A.prelude(at) ); const semi = A.end(at) + 1; // position past `;` if (!urlNode || (A.type(urlNode) === NodeType.Ident && !isModules)) { this._emitWarning( state, `Expected URL in '${input.slice(importStart, semi)}'`, locConverter, importStart, semi ); // A malformed `@import` still emits orphan url() deps from its prelude — flag it so the value visitors enable url(). currentUrlRecovery = true; return; } /** @type {string} */ let url; if (A.type(urlNode) === NodeType.Ident) { // URL given as identifier — resolve via CSS Modules `@value`. const identName = A.value(urlNode); const def = icssDefinitions.get(identName); if (!def) { this._emitWarning( state, `Unknown '@value' identifier '${identName}' in '${input.slice( importStart, semi )}'`, locConverter, importStart, semi ); // Drop the whole at-rule so the unresolved identifier isn't substituted into the output. const dep = new ConstDependency("", [importStart, semi]); module.addPresentationalDependency(dep); return; } if (def.value === undefined) { this._emitWarning( state, `'@value' identifier '${identName}' was imported from another module and cannot be used as the URL of '@import' — only locally defined values are supported here`, locConverter, importStart, semi ); const dep = new ConstDependency("", [importStart, semi]); module.addPresentationalDependency(dep); return; } const raw = def.value.trim(); url = (raw.startsWith('"') && raw.endsWith('"')) || (raw.startsWith("'") && raw.endsWith("'")) ? normalizeUrl(raw.slice(1, -1), true) : normalizeUrl(raw, false); } else if (A.type(urlNode) === NodeType.Url) { const ut = /** @type {UrlToken} */ (urlNode); url = normalizeUrl( input.slice(A.contentStart(ut), A.contentEnd(ut)), false ); } else if (A.type(urlNode) === NodeType.String) { url = normalizeUrl( input.slice(A.start(urlNode) + 1, A.end(urlNode) - 1), true ); } else { // url(...) function — first non-whitespace child is the string. /** @type {Token | undefined} */ let string; for (const inner of A.children(urlNode)) { if (A.type(inner) === NodeType.Whitespace) continue; if (A.type(inner) === NodeType.String) { string = /** @type {Token} */ (inner); } break; } if (!string) { this._emitWarning( state, `Expected URL in '${input.slice(importStart, semi)}'`, locConverter, importStart, semi ); return; } url = normalizeUrl( input.slice(A.start(string) + 1, A.end(string) - 1), true ); } const newline = skipWhiteLine(input, semi); if ( webpackIgnored([importNameEnd, A.end(urlNode)], importStart, newline) ) { return; } if (url.length === 0) { const dep = new ConstDependency("", [importStart, newline]); module.addPresentationalDependency(dep); setDepLoc(dep, importStart, newline); return; } /** @type {undefined | string} */ let layer; if (layerNode) { if (A.type(layerNode) === NodeType.Function) { // `layer(<ident>)` — extract content between `(` and `)`. const fn = /** @type {FunctionNode} */ (layerNode); layer = input.slice(A.nameEnd(fn) + 1, A.end(fn) - 1).trim(); } else { // Bare `layer` ident — anonymous layer. layer = ""; } } /** @type {undefined | string} */ let supports; if (supportsNode) { supports = input .slice(A.nameEnd(supportsNode) + 1, A.end(supportsNode) - 1) .trim(); } // Media query = whatever sits between the last url/layer/supports part and the closing `;`, trimmed. Start at the next non-whitespace prelude node (skips the gap, comments included). const lastPrefixPart = supportsNode || layerNode || urlNode; const afterIdx = /** @type {AstNode[]} */ (A.prelude(at)).indexOf(lastPrefixPart) + 1; const nextIdx = nextNonWhitespace(A.prelude(at), afterIdx); const mediaStart = nextIdx < A.prelude(at).length ? A.start(A.prelude(at)[nextIdx]) : A.end(at); /** @type {undefined | string} */ let media; if (mediaStart !== A.end(at)) { media = input.slice(mediaStart, A.end(at)).trim(); } const { line: sl, column: sc } = locConverter.get(importStart); const { line: el, column: ec } = locConverter.get(newline); const parent = /** @type {CssModule} */ (module); // Carry the importing module's layer/supports/media chain onto the dep so a nested `@import` inherits it. /** @type {Inheritance | undefined} */ let inheritance; if (parent.cssLayer !== undefined || parent.supports || parent.media) { inheritance = [[parent.cssLayer, parent.supports, parent.media]]; } if (parent.inheritance) { if (!inheritance) inheritance = []; inheritance.push(...parent.inheritance); } const dep = new CssImportDependency( url, [importStart, newline], mode === "local" || mode === "global" ? mode : undefined, layer, supports && supports.length > 0 ? supports : undefined, media && media.length > 0 ? media : undefined, inheritance, parent.exportType ); dep.setLoc(sl, sc, el, ec); module.addDependency(dep); // `text` / `css-style-sheet` parents inline the import at build time, so order it via a code-generation dependency. if ( parent.exportType === "text" || parent.exportType === "css-style-sheet" ) { module.addCodeGenerationDependency(dep); } }; /** * Handle a CSS-Modules `@value` at-rule: register the local / imported value(s) in `icssDefinitions`, emit the import + export deps, and strip the rule. * @param {AtRule} at the `@value` at-rule */ const handleValueAtRule = (at) => { const start = A.start(at); const nameEnd = A.nameEnd(at); const semi = A.end(at); const atRuleEnd = source.charCodeAt(semi) === CC_SEMICOLON ? semi + 1 : semi; const params = input.slice(nameEnd, 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; } 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 ); setDepLoc(dep, start, nameEnd); module.addDependency(dep); icssDefinitions.set(localName, { importName, request: from }); } { const { line: sl, column: sc } = locConverter.get(start); const { line: el, column: ec } = locConverter.get(nameEnd); recordDeclaration(localName, "@value", sl, sc); addCssExport( sl, sc, el, ec, localName, getReexport(localName), undefined, false, CssIcssExportDependency.EXPORT_MODE.REPLACE ); } } } 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; } const { localName, value } = /** @type {ValueAtRuleValue} */ (parsed); const { line: sl, column: sc } = locConverter.get(start); const { line: el, column: ec } = locConverter.get(nameEnd); if (icssDefinitions.has(value)) { const def = /** @type {IcssDefinition} */ (icssDefinitions.get(value)); def.localName = value; icssDefinitions.set(localName, def); recordDeclaration(localName, "@value", sl, sc); addCssExport(sl, sc, el, ec, localName, getReexport(value)); } else { icssDefinitions.set(localName, { value }); recordDeclaration(localName, "@value", sl, sc); addCssExport(sl, sc, el, ec, localName, value); } } const dep = new ConstDependency("", [start, atRuleEnd]); module.addPresentationalDependency(dep); }; /** * The lone dashed-ident name of a `( … )` block, or `undefined` — `(--foo)` yields `--foo`, `(--foo: 1px)` / `(min-width: 0)` yield `undefined`. `hasExtra` reports whether any non-whitespace token follows the ident (a boolean-context violation). * @param {SimpleBlock} block parenthesised block node * @param {{ hasExtra: boolean }} out extra-token flag output * @returns {string | undefined} the dashed-ident name, or undefined */ const loneDashedIdentOfBlock = (block, out) => { out.hasExtra = false; let ident; for (const k of A.children(block)) { if (A.type(k) === NodeType.Whitespace) continue; if (ident !== undefined) { out.hasExtra = true; return ident; } if (A.type(k) !== NodeType.Ident) return undefined; const v = A.value(k); if (!isDashedIdentifier(v)) return undefined; ident = v; } return ident; }; /** * Collect one `@custom-media --name <value>` definition (last-wins) and drop * the rule. A value naming another custom media is kept as an `alias` — the * name it points at may be defined further down the file, and node ids are * recycled per top-level rule, so nothing AST-shaped can outlive the walk. * @param {AtRule} at the `@custom-media` at-rule */ const collectCustomMedia = (at) => { const start = A.start(at); const end = A.end(at); const ruleEnd = input.charCodeAt(end) === CC_SEMICOLON ? end + 1 : end; module.addPresentationalDependency( new ConstDependency("", [start, ruleEnd]) ); const prelude = A.prelude(at); let i = 0; while (i < prelude.length && A.type(prelude[i]) === NodeType.Whitespace) { i++; } if (i >= prelude.length || A.type(prelude[i]) !== NodeType.Ident) return; const name = A.value(prelude[i]); if (!isDashedIdentifier(name)) return; i++; // Value span after the name, trimmed. let vs = i; let ve = prelude.length; while (vs < ve && A.type(prelude[vs]) === NodeType.Whitespace) vs++; while (ve > vs && A.type(prelude[ve - 1]) === NodeType.Whitespace) ve--; (customMediaDefs || (customMediaDefs = new Map())).set( name, classifyCustomMedia(prelude, vs, ve) ); }; /** * Classify one `@custom-media` value against the live AST. * @param {readonly AstNode[]} prelude the defining at-rule's prelude * @param {number} vs first value token * @param {number} ve one past the last value token * @returns {CustomMediaDefinition} what the value is, aliases unresolved */ const classifyCustomMedia = (prelude, vs, ve) => { if (vs >= ve) return CUSTOM_MEDIA_UNSUPPORTED; const extra = { hasExtra: false }; let hasComma = false; for (let k = vs; k < ve; k++) { if (A.type(prelude[k]) === NodeType.Comma) { hasComma = true; break; } } if (!hasComma) { const first = prelude[vs]; const valueText = input.slice(A.start(first), A.end(prelude[ve - 1])); // A `(--x)` standing alone is this value spelled as another name. if (ve - vs === 1 && A.type(first) === NodeType.SimpleBlock) { const nested = loneDashedIdentOfBlock( /** @type {SimpleBlock} */ (first), extra ); if (nested !== undefined && !extra.hasExtra) { return { kind: "alias", name: nested }; } } // A reference anywhere else in the value has no resolution step of its // own — only a `@media` prelude's uses are scanned — so writing this // value out would emit the reference unresolved. if (hasCustomMediaRef(prelude, vs, ve)) return CUSTOM_MEDIA_UNSUPPORTED; if (A.type(first) === NodeType.SimpleBlock) { return { kind: "condition", text: valueText }; } if (A.type(first) !== NodeType.Ident) return CUSTOM_MEDIA_UNSUPPORTED; const s = A.start(first); const e = A.end(first); if (ve - vs === 1) { if (rangeEqualsLowerCase(input, s, e, "true")) { return CUSTOM_MEDIA_TRUE; } if (rangeEqualsLowerCase(input, s, e, "false")) { return CUSTOM_MEDIA_FALSE; } } if (rangeEqualsLowerCase(input, s, e, "not")) { // `not (…)` is a media condition (wrap so it stays a media-in-parens); // `not <type>` is a media type. let j = vs + 1; while (j < ve && A.type(prelude[j]) === NodeType.Whitespace) j++; if (j < ve && A.type(prelude[j]) === NodeType.SimpleBlock) { return { kind: "condition", text: `(${valueText})` }; } return { kind: "type", text: valueText }; } // `screen`, `only screen`, `screen and (…)`, … return { kind: "type", text: valueText }; } // Comma list → `(seg1 or seg2 …)`; supported only when every segment is a // media-in-parens (or a name standing for one). /** @type {({ text: string } | { alias: string })[]} */ const parts = []; let ok = true; /** @type {AstNode[]} */ let seg = []; const flush = () => { let a = 0; let b = seg.length; while (a < b && A.type(seg[a]) === NodeType.Whitespace) a++; while (b > a && A.type(seg[b - 1]) === NodeType.Whitespace) b--; if (a >= b) { ok = false; return; } const f = seg[a]; const text = input.slice(A.start(f), A.end(seg[b - 1])); if (b - a === 1 && A.type(f) === NodeType.SimpleBlock) { const nested = loneDashedIdentOfBlock( /** @type {SimpleBlock} */ (f), extra ); if (extra.hasExtra) ok = false; else if (nested === undefined) parts.push({ text }); else parts.push({ alias: nested }); return; } if ( A.type(f) === NodeType.Ident && rangeEqualsLowerCase(input, A.start(f), A.end(f), "not") && !hasCustomMediaRef(seg, a, b) ) { parts.push({ text: `(${text})` }); return; } ok = false; }; for (let k = vs; k < ve; k++) { if (A.type(prelude[k]) === NodeType.Comma) { flush(); seg = []; } else { seg.push(prelude[k]); } } flush(); return ok ? { kind: "or", parts } : CUSTOM_MEDIA_UNSUPPORTED; }; /** * What a `@custom-media` name resolves to, memoized. An undefined name and a * definition cycle both resolve to unsupported (the use site warns). * @param {string} name the dashed ident * @param {Set<string>} resolving names currently being resolved * @returns {CustomMediaValue} what the name resolves to */ const resolveCustomMediaValue = (name, resolving) => { const cached = customMediaValues && customMediaValues.get(name); if (cached !== undefined) return cached; const def = customMediaDefs && customMediaDefs.get(name); if (def === undefined || resolving.has(name)) { return CUSTOM_MEDIA_UNSUPPORTED; } resolving.add(name); /** @type {CustomMediaValue} */ let value; if (def.kind === "alias") { const inner = resolveCustomMediaValue(def.name, resolving); // A media type cannot sit in parens, so a name standing for one is no value. value = inner.kind === "type" ? CUSTOM_MEDIA_UNSUPPORTED : inner; } else if (def.kind === "or") { /** @type {string[]} */ const texts = []; let ok = true; let anyTrue = false; for (const part of def.parts) { if (!("alias" in part)) { texts.push(part.text); continue; } const inner = resolveCustomMediaValue(part.alias, resolving); // A media query list is an `or`, so a constant segment drops out of it. if (inner.kind === "boolean") anyTrue = anyTrue || inner.value; else if (inner.kind === "condition") texts.push(inner.text); else ok = false; } value = !ok ? CUSTOM_MEDIA_UNSUPPORTED : anyTrue ? CUSTOM_MEDIA_TRUE : texts.length === 0 ? CUSTOM_MEDIA_FALSE : { kind: "condition", text: texts.length === 1 ? texts[0] : `(${texts.join(" or ")})` }; } else { value = def; } resolving.delete(name); (customMediaValues || (customMediaValues = new Map())).set(name, value); return value; }; /** * Whether a node is the media keyword `word` (media queries are ASCII * case-insensitive). * @param {AstNode} node candidate node * @param {string} word lower-case keyword * @returns {boolean} true on a match */ const isMediaKeyword = (node, word) => A.type(node) === NodeType.Ident && rangeEqualsLowerCase(input, A.start(node), A.end(node), word); /** * First significant index in `tokens[from…to)`. * @param {readonly AstNode[]} tokens token list * @param {number} from first index * @param {number} to one past the last index * @returns {number} the trimmed start */ const mediaTrimStart = (tokens, from, to) => { let a = from; while (a < to && A.type(tokens[a]) === NodeType.Whitespace) a++; return a; }; /** * One past the last significant index in `tokens[from…to)`. * @param {readonly AstNode[]} tokens token list * @param {number} from first index * @param {number} to one past the last index * @returns {number} the trimmed end */ const mediaTrimEnd = (tokens, from, to) => { let b = to; while (b > from && A.type(tokens[b - 1]) === NodeType.Whitespace) b--; return b; }; /** * The source text spanned by `tokens[from…to)`. * @param {readonly AstNode[]} tokens token list * @param {number} from first index * @param {number} to one past the last index * @returns {string} the source slice */ const mediaText = (tokens, from, to) => input.slice(A.start(tokens[from]), A.end(tokens[to - 1])); // One flag object for the whole parse: `loneDashedIdentOfBlock` writes it and // every caller reads it back before the next call. const mediaExtra = { hasExtra: false }; /** * Whether any `(--name)` reference is reachable inside `node`. `childAt` walks * the children without materializing the list. * @param {AstNode} node a block or function node * @returns {boolean} true when a reference is present */ const hasCustomMediaRefIn = (node) => { const count = A.childCount(node); for (let k = 0; k < count; k++) { const child = A.childAt(node, k); const type = A.type(child); if (type !== NodeType.SimpleBlock && type !== NodeType.Function) { continue; } if ( type === NodeType.SimpleBlock && loneDashedIdentOfBlock( /** @type {SimpleBlock} */ (child), mediaExtra ) !== undefined ) { return true; } if (hasCustomMediaRefIn(child)) return true; } return false; }; /** * Whether any `(--name)` reference is reachable in `tokens[from…to)`. Cheap * enough to run over every `@media` prelude so one without a reference builds * no tree at all. * @param {readonly AstNode[]} tokens token list * @param {number} from first index * @param {number} to one past the last index * @returns {boolean} true when a reference is present */ const hasCustomMediaRef = (tokens, from, to) => { for (let i = from; i < to; i++) { const node = tokens[i]; const type = A.type(node); if (type !== NodeType.SimpleBlock && type !== NodeType.Function) { continue; } if ( type === NodeType.SimpleBlock && loneDashedIdentOfBlock( /** @type {SimpleBlock} */ (node), mediaExtra ) !== undefined ) { return true; } if (hasCustomMediaRefIn(node)) return true; } return false; }; /** * Capture one `<media-in-parens>` — a reference to a custom media name, a * parenthesised sub-condition, or an opaque feature test. * @param {readonly AstNode[]} tokens token list * @param {number} from first index * @param {number} to one past the last index * @param {boolean} leading whether the term starts a top-level query * @param {CustomMediaUse[]} uses the prelude's use list, appended to * @returns {MediaNode} the captured term */ const captureMediaInParens = (tokens, from, to, leading, uses) => { const a = mediaTrimStart(tokens, from, to); const b = mediaTrimEnd(tokens, a, to); if (a >= b) return { kind: "text", text: "" }; const text = mediaText(tokens, a, b); if (b - a !== 1 || A.type(tokens[a]) !== NodeType.SimpleBlock) { return { kind: "text", text }; } const block = /** @type {SimpleBlock} */ (tokens[a]); const name = loneDashedIdentOfBlock(block, mediaExtra); if (name !== undefined) { const use = { name, start: A.start(block), end: A.end(block), invalid: mediaExtra.hasExtra, leading }; uses.push(use); return { kind: "ref", text, use }; } const children = A.children(block); const inner = captureMediaCondition(children, 0, children.length, uses); return inner.kind === "text" ? { kind: "text", text } : { kind: "group", operand: inner }; }; /** * Capture a `<media-condition>` — `not <in-parens>`, or an `and` / `or` chain * of them (CSS never mixes the two at one level). * @param {readonly AstNode[]} tokens token list * @param {number} from first index * @param {number} to one past the last index * @param {CustomMediaUse[]} uses the prelude's use list, appended to * @returns {MediaNode} the captured condition */ const captureMediaCondition = (tokens, from, to, uses) => { const a = mediaTrimStart(tokens, from, to); const b = mediaTrimEnd(tokens, a, to); if (a >= b) return { kind: "text", text: "" }; if (isMediaKeyword(tokens[a], "not")) { return { kind: "not", operand: captureMediaInParens(tokens, a + 1, b, false, uses) }; } /** @type {MediaNode[]} */ const terms = []; let isOr = false; let start = a; for (let i = a; i <= b; i++) { const atEnd = i === b; if ( !atEnd && !isMediaKeyword(tokens[i], "and") && !isMediaKeyword(tokens[i], "or") ) { continue; } if (!atEnd && isMediaKeyword(tokens[i], "or")) isOr = true; terms.push(captureMediaInParens(tokens, start, i, start === a, uses)); start = i + 1; } if (terms.length === 1) return terms[0]; return { kind: "chain", isOr, terms }; }; /** * Capture one `<media-query>` — a bare condition, or a media type with an * optional `and` chain after it. * @param {readonly AstNode[]} tokens token list * @param {number} from first index * @param {number} to one past the last index * @param {CustomMediaUse[]} uses the prelude's use list, appended to * @returns {MediaNode} the captured query */ const captureMediaQuery = (tokens, from, to, uses) => { const a = mediaTrimStart(tokens, from, to); const b = mediaTrimEnd(tokens, a, to); if (a >= b) return { kind: "text", text: "" }; const rest = mediaTrimStart(tokens, a + 1, b); const startsWithType = A.type(tokens[a]) === NodeType.Ident && (!isMediaKeyword(tokens[a], "not") || rest >= b || A.type(tokens[rest]) !== NodeType.SimpleBlock); if (!startsWithType) return captureMediaCondition(tokens, a, b, uses); // `[not | only]? <type> [and <condition-without-or>]?` — the type itself is // never constant, so only the `and` chain after it can fold. let split = b; for (let i = a; i < b; i++) { if (isMediaKeyword(tokens[i], "and")) { split = i; break; } } return { kind: "typed", text: mediaText(tokens, a, mediaTrimEnd(tokens, a, split)), rest: split === b ? null : captureMediaCondition(tokens, split + 1, b, uses) }; }; /** * Record one `@media` prelude, the custom-media references inside it, and the * boolean shape needed to fold it once the references resolve. * @param {AtRule} at the `@media` at-rule */ const collectMediaQuery = (at) => { const prelude = A.prelude(at); const start = mediaTrimStart(prelude, 0, prelude.length); const end = mediaTrimEnd(prelude, start, prelude.length); if (start >= end || !hasCustomMediaRef(prelude, start, end)) return; /** @type {CustomMediaUse[]} */ const uses = []; /** @type {MediaNode[]} */ const queries = []; let queryStart = start; for (let i = start; i <= end; i++) { if (i !== end && A.type(prelude[i]) !== NodeType.Comma) continue; queries.push(captureMediaQuery(prelude, queryStart, i, uses)); queryStart = i + 1; } if (uses.length === 0) return; (customMediaQueries || (customMediaQueries = [])).push({ queries, start: A.start(prelude[start]), end: A.end(prelude[end - 1]), uses }); }; /** * Fold a captured media node against the resolved definitions. A reference the * resolver could not classify keeps its source text, so it prints as authored. * @param {MediaNode} node the captured node * @param {Set<string>} resolving names currently being resolved * @returns {boolean | string} the constant, or the node's text */ const foldMediaNode = (node, resolving) => { switch (node.kind) { case "text": return node.text; case "ref": { const value = resolveCustomMediaValue(node.use.name, resolving); if (node.use.invalid) return node.text; if (value.kind === "boolean") return value.value; if (value.kind === "condition") return value.text; if (value.kind === "type" && node.use.leading) return value.text; return node.text; } case "group": { const inner = foldMediaNode(node.operand, resolving); return typeof inner === "boolean" ? inner : `(${inner})`; } case "not": { const operand = foldMediaNode(node.operand, resolving); return typeof operand === "boolean" ? !operand : `not ${operand}`; } case "chain": { /** @type {string[]} */ const kept = []; for (const term of node.terms) { const folded = foldMediaNode(term, resolving); if (folded === node.isOr) return node.isOr; if (typeof folded !== "boolean") kept.push(folded); } if (kept.length === 0) return !node.isOr; if (kept.length === 1) return kept[0]; return kept.join(node.isOr ? " or " : " and "); } default: { if (node.rest === null) return node.text; const rest = foldMediaNode(node.rest, resolving); if (rest === false) return false; if (rest === true) return node.text; return `${node.text} and ${rest}`; } } }; /** * Fold a whole `@media` prelude. A query that is always false leaves the list; * one that is always true makes the list match everywhere (`all`), and a list * with nothing left matches nowhere (`not all`). * @param {MediaNode[]} queries the prelude's captured queries * @param {Set<string>} resolving names currently being resolved * @returns {string} the folded prelude */ const foldMediaPrelude = (queries, resolving) => { /** @type {string[]} */ const kept = []; for (const query of queries) { const folded = foldMediaNode(query, resolving); if (folded === true) return "all"; if (folded !== false && folded !== "") kept.push(folded); } return kept.length === 0 ? "not all" : kept.join(", "); }; /** * Collect one `@custom-selector :--name <selector-list>` definition (last-wins) and drop the rule. * @param {AtRule} at the `@custom-selector` at-rule */ const collectCustomSelector = (at) => { const start = A.start(at); const end = A.end(at); const ruleEnd = input.charCodeAt(end) === CC_SEMICOLON ? end + 1 : end; module.addPresentationalDependency( new ConstDependency("", [start, ruleEnd]) ); const prelude = A.prelude(at); let i = 0; while (i < prelude.length && A.type(prelude[i]) === NodeType.Whitespace) { i++; } if (i + 1 >= prelude.length || A.type(prelude[i]) !== NodeType.Colon) { return; } const nameNode = prelude[i + 1]; if ( A.type(nameNode) !== NodeType.Ident || A.end(prelude[i]) !== A.start(nameNode) ) { return; } const name = A.value(nameNode); if (!isDashedIdentifier(name)) return; const list = input.slice(A.end(nameNode), end).trim(); if (list.length === 0) return; (customSelectorDefs || (customSelectorDefs = new Map())).set(name, list); }; /** * Record every `:--name` custom-selector reference in a selector prelude (recursing into `:is(…)` etc.) for post-walk expansion. A `:--name` is a colon immediately followed by a dashed ident. * @param {readonly AstNode[]} tokens selector prelude (or nested) tokens */ const scanCustomSelectorUses = (tokens) => { for (let k = 0; k < tokens.length; k++) { const t = tokens[k]; const tt = A.type(t); if (tt === NodeType.Colon) { const next = tokens[k + 1]; if ( next && A.type(next) === NodeType.Ident && A.end(t) === A.start(next) && isDashedIdentifier(A.value(next)) ) { (customSelectorUses || (customSelectorUses = [])).push({ name: A.value(next), start: A.start(t), end: A.end(next) }); } } else if (tt === NodeType.Function || tt === NodeType.SimpleBlock) { scanCustomSelectorUses(A.children(t)); } } }; /** * Rewrite the collected `@custom-media` / `@custom-selector` uses now that all (possibly later-defined) definitions are known; warns on invalid, unknown, or media-type-in-a-non-leading-position custom-media uses. */ const resolveCustomMediaAndSelectors = () => { if (customMediaQueries) { /** @type {Set<string>} */ const resolving = new Set(); for (const query of customMediaQueries) { let hasBoolean = false; for (const use of query.uses) { if (use.invalid) { this._emitWarning( state, `Custom media query '${use.name}' must be used in a boolean context`, locConverter, use.start, use.end ); continue; } const value = resolveCustomMediaValue(use.name, resolving); if (value.kind === "unsupported") { this._emitWarning( state, customMediaDefs && customMediaDefs.has(use.name) ? `Custom media query '${use.name}' has a value that cannot be resolved and was left as written` : `Unknown custom media query '${use.name}'`, locConverter, use.start, use.end ); } else if (value.kind === "boolean") { hasBoolean = true; } else if (value.kind === "type" && !use.leading) { this._emitWarning( state, `Custom media query '${use.name}' resolves to a media type and can only be used at the start of a media query`, locConverter, use.start, use.end ); } } // `true` / `false` have no `<media-in-parens>` spelling, so a prelude // holding one is rewritten whole — every other use in it folds with it. if (hasBoolean) { module.addPresentationalDependency( new ConstDependency(foldMediaPrelude(query.queries, resolving), [ query.start, query.end ]) ); continue; } for (const use of query.uses) { if (use.invalid) continue; const value = resolveCustomMediaValue(use.name, resolving); if ( value.kind === "condition" || (value.kind === "type" && use.leading) ) { module.addPresentationalDependency( new ConstDependency(value.text, [use.start, use.end]) ); } } } } if (customSelectorUses) { for (const use of customSelectorUses) { const list = customSelectorDefs && customSelectorDefs.get(use.name); if (list === undefined) continue; module.addPresentationalDependency( new ConstDependency(`:is(${list})`, [use.start, use.end]) ); } } }; /** * Export the localizable idents / strings in a known property's value (`animation-name: foo`, grid line-names / template-areas, …). Top-level only, except `grid-template` recurses into `repeat(…)` and `[line-name]` blocks. * @param {Declaration} decl the declaration * @param {string} declPropertyName the vendor-stripped, lower-cased property name * @returns {void} */ const emitKnownPropertyExports = (decl, declPropertyName) => { /** @type {Record<string, number>} */ let parsedKeywords = Object.create(null); const isGridProperty = Boolean(declPropertyName.startsWith("grid")); // Only `view-transition-*` and `counter-*` legitimately take a dashed-ident value; other known properties keep the historical plain-ident handling. const isViewTransitionProperty = Boolean( declPropertyName.startsWith("view-transition") ); const isCounterProperty = Boolean( declPropertyName.startsWith("counter-") ); const isGridTemplate = isGridProperty ? Boolean( declPropertyName === "grid" || declPropertyName === "grid-template" || declPropertyName === "grid-template-columns" || declPropertyName === "grid-template-rows" ) : false; const keywords = /** @type {Map<string, number>} */ (knownProperties.get(declPropertyName)); let afterExclamation = false; /** * Emit the ICSS export for one collected name span (a quoted string drops its delimiters). Called inline during the walk so no intermediate `values` array / per-name tuples are allocated. * @param {number} start name start offset * @param {number} end name end offset * @param {boolean=} isString whether the span is a quoted string * @returns {void} */ const emit = (start, end, isString) => { const { line: sl, column: sc } = locConverter.get(start); const { line: el, column: ec } = locConverter.get(end); const name = unescapeRange( isString ? start + 1 : start, isString ? end - 1 : end ); // Only grid-line names are declaration sites here; property-value references (`animation: foo`) are usages. if (isGridProperty) recordDeclaration(name, "grid identifier", sl, sc); addCssExport( sl, sc, el, ec, name, getReexport(name), [start, end], true, CssIcssExportDependency.EXPORT_MODE.ONCE, isGridProperty ? CssIcssExportDependency.EXPORT_TYPE.GRID_CUSTOM_IDENTIFIER : CssIcssExportDependency.EXPORT_TYPE.NORMAL ); }; // Collect idents/strings to export — top-level only, except grid-template recurses (`[line-name]` blocks live in `repeat(…)`). /** @type {(cvs: AstNode[]) => void} */ const walkExports = (cvs) => { for (const cv of cvs) { switch (A.type(cv)) { case NodeType.Comma: parsedKeywords = Object.create(null); break; case NodeType.Delim: afterExclamation = A.value(cv) === "!"; break; case NodeType.Ident: { if (isGridTemplate) break; if (afterExclamation) { afterExclamation = false; break; } const identifier = A.value(cv); // `view-transition-name: --foo` / `counter-reset: --foo` — a dashed name scopes as a custom property under `dashedIdents`, not as a plain ident export. if ( (isViewTransitionProperty || isCounterProperty) && isDashedIdentifier(identifier) ) { if (this.options.dashedIdents) { emitDashedIdentExport(A.start(cv), A.end(cv)); } break; } // Values are almost always lowercase already — avoid the copy. const keyword = toLowerCaseIfNeeded(identifier); parsedKeywords[keyword] = typeof parsedKeywords[keyword] !== "undefined" ? parsedKeywords[keyword] + 1 : 0; const limit = keywords.get(keyword); if (limit !== undefined && parsedKeywords[keyword] < limit) { break; } emit(A.start(cv), A.end(cv)); break; } case NodeType.String: { if ( declPropertyName === "animation" || declPropertyName === "animation-name" ) { emit(A.start(cv), A.end(cv), true); } if ( declPropertyName === "grid" || declPropertyName === "grid-template" || declPropertyName === "grid-template-areas" ) { const areas = A.unescaped(cv); const matches = matchAll(/\b\w+\b/g, areas); for (const match of matches) { const areaStart = A.start(cv) + 1 + match.index; emit(areaStart, areaStart + match[0].length, false); } } break; } case NodeType.SimpleBlock: { const block = /** @type {SimpleBlock} */ (cv); if (A.blockToken(block) === "[") { // Collect identifiers until the first non-ident token (`<line-names> = '[' <custom-ident>* ']'`). for (const inner of A.children(block)) { if (A.type(inner) === NodeType.Whitespace) continue; if (A.type(inner) !== NodeType.Ident) break; emit(A.start(inner), A.end(inner)); } } else if (isGridTemplate) { walkExports(A.children(block)); } break; } case NodeType.Function: // `repeat(…)` line names, and the `counter-reset: reversed(name)` wrapper — but not `local()`/`global()`, whose idents the value visitors own. if ( isGridTemplate || (isCounterProperty && equalsLowerCase(A.unescapedName(cv), "reversed")) ) { walkExports(A.children(cv)); } break; // Other types carry no ICSS-export information. } } }; walkExports(A.children(decl)); }; /** * Pure-mode: mark the at-rule local when its prelude names a local `@keyframes` / `@counter-style` / `@container` identifier (or a `:local(…)` function), so the rule isn't flagged impure. `@container` ignores the `none`/`and`/`or`/`not` keywords. * @param {AtRule} at the at-rule * @param {boolean} isKeyframes whether it's `@keyframes` * @param {boolean} isCounterStyle whether it's `@counter-style` * @param {boolean} isContainer whether it's `@container` * @returns {void} */ const markPureFromAtRulePrelude = ( at, isKeyframes, isCounterStyle, isContainer ) => { const acceptIdent = isKeyframes || isCounterStyle || isContainer; const acceptString = isKeyframes; for (const cv of A.prelude(at)) { const cvType = A.type(cv); if (cvType === NodeType.Whitespace) continue; if (cvType === NodeType.String) { if (acceptString) pure.markLocal(); break; } if (cvType === NodeType.Ident) { if (!acceptIdent) break; if ( isContainer && isContainerKeyword(source, A.start(cv), A.end(cv)) ) { continue; } pure.markLocal(); break; } if (cvType === NodeType.Function) { if (equalsLowerCase(A.unescapedName(cv), "local")) { pure.markLocal(); } break; } } }; // Drive the walk through SourceProcessor: structural enter / exit map to the `walkAst…Enter` / `…Exit` halves; value visitors handle url / ICSS / local-global. /** @type {VisitorMap} */ const visitors = { [NodeType.Comment]: commentVisitor, [NodeType.AtRule]: { // At-rule enter: scope save, name dispatch, prelude value context, pure-block push. enter: (path) => { const node = path.node; const at = /** @type {AtRule} */ (node); const topLevel = path.parent === null; currentUrlRecovery = false; advanceCommentCursor(A.start(at)); const savedAnchor = currentRule.hasLocalAnchor; // Rollback point: nested walks append to the shared list; the exit // truncates back to this length (no per-rule copy). const savedLocalIdentifierCount = currentRule.localIdentifiers.length; const name = `@${toLowerCaseIfNeeded(A.name(at))}`; switch (name) { case "@namespace": { this._emitWarning( state, "'@namespace' is not supported in bundled CSS", locConverter, A.start(at), A.nameEnd(at) ); break; } case "@charset": { if (/** @type {CssModule} */ (module).exportType !== "style") { const atEnd = A.end(at); const atRuleEnd = source.charCodeAt(atEnd) === CC_SEMICOLON ? atEnd + 1 : atEnd; const dep = new ConstDependency("", [A.start(at), atRuleEnd]); module.addPresentationalDependency(dep); const string = A.prelude(at).find( (v) => A.type(v) !== NodeType.Whitespace ); if (string && A.type(string) === NodeType.String) { /** @type {CssModuleBuildInfo} */ (module.buildInfo).charset = source .slice(A.start(string) + 1, A.end(string) - 1) .toUpperCase(); } } break; } case "@import": { handleImportAtRule(at, topLevel); break; } case "@custom-media": { if (mayHaveCustomMedia) collectCustomMedia(at); break; } case "@custom-selector": { if (mayHaveCustomSelectors) collectCustomSelector(at); break; } case "@media": { if (mayHaveCustomMedia) collectMediaQuery(at); break; } default: { if (!isModules) break; if (name === "@value") { handleValueAtRule(at); break; } else if ( this.options.animation && OPTIONALLY_VENDOR_PREFIXED_KEYFRAMES_AT_RULE.test(name) ) { processLocalAtRule( at, { string: mode === "local", identifier: mode === "local" }, "@keyframes" ); } else if ( this.options.customIdents && name === "@counter-style" ) { processLocalAtRule( at, { identifier: mode === "local" }, "@counter-style" ); } else if (this.options.container && name === "@container") { processLocalAtRule( at, { identifier: mode === "local" ? /^(none|and|or|not)$/ : false }, "@container" ); } } } // `@scope (.x) to (.y)` — walk the prelude as a selector list. if ( isModules && equalsLowerCase(A.name(at), "scope") && A.prelude(at).length > 0 ) { walkSelectorList( A.prelude(at), /** @type {"local" | "global"} */ ( mode === "local" ? "local" : "global" ) ); } // Prelude value-visitor context; AST-handled at-rules emit their own deps so they're excluded from the local() / global() / ICSS walks. const effectiveLocalMode = isEffectivelyLocal(); const isProcessedByLocalAtRule = isLocalHandledAtRule(name); currentStructural = at; currentAtRuleName = name; currentDeclCounterActive = false; dashed.active = false; // `@import` url() is the import target — only walk its prelude for url deps on malformed-import recovery. if (this.options.url && (name !== "@import" || currentUrlRecovery)) { lastTokenEndForComments = A.nameEnd(at); } // Dashed-ident scoping over the prelude (the Ident / Function visitors emit). dashed.active = Boolean( this.options.dashedIdents && isModules && !isProcessedByLocalAtRule && effectiveLocalMode && !(mayHaveCustomMedia && name === "@media") ); dashed.emit = dashed.active; // Pure-mode: `@keyframes` / `@counter-style` / `@container` bodies are marked skip / treat-as-leaf. let atSkipChildren = false; let atTreatAsLeaf = false; const isKeyframes = OPTIONALLY_VENDOR_PREFIXED_KEYFRAMES_AT_RULE.test(name); const isCounterStyle = name === "@counter-style"; const isContainer = name === "@container"; if ( pure.enabled && isModules && isLocalMode() && (isKeyframes || isCounterStyle || isContainer) ) { if (isKeyframes || isCounterStyle) { atSkipChildren = true; atTreatAsLeaf = true; } markPureFromAtRulePrelude( at, isKeyframes, isCounterStyle, isContainer ); } // pure.stack push for block-bearing at-rules (pure-off builds skip // the frame object — every consumer inside is a no-op then). const atDecls = A.declarations(at); const atChildRules = A.childRules(at); const atHasBlock = Boolean(atDecls || atChildRules); const atBlockStart = A.blockStart(at); if (pure.enabled && atHasBlock && atBlockStart !== -1) { const isAtRulePrelude = isPureBodyAtRule(name); if (isAtRulePrelude) pure.finalizeSelector(); pure.enterBlock({ isRulePrelude: isAtRulePrelude, treatAsLeaf: atTreatAsLeaf, ownSkip: atSkipChildren, declarations: atDecls, childRules: atChildRules, preludeStart: A.start(at), preludeEnd: atBlockStart }); } atRuleStateStack.push({ savedAnchor, savedLocalIdentifierCount, name, hasBlock: atHasBlock, endsWithSemicolon: source.charCodeAt(A.end(at)) === CC_SEMICOLON, fontPreloaded: false }); }, // At-rule exit: pure-frame finalization, `suppressNextRulePrelude`, scope restore, top-level reset. exit: (path) => { const node = path.node; const state = atRuleStateStack.pop(); if (!state) return; if (state.hasBlock) { pure.exitBlock(); } else if ( isModules && state.endsWithSemicolon && !isLocalHandledAtRule(state.name) ) { // An unrecognized `;`-terminated at-rule: treat the next sibling's selectors as global. suppressNextRulePrelude = true; } currentRule.hasLocalAnchor = state.savedAnchor; currentRule.localIdentifiers.length = state.savedLocalIdentifierCount; if (path.parent === null) finishTopLevelRule(node, true); } }, [NodeType.QualifiedRule]: { // Qualified-rule enter: scope setup, selector + prelude context, pure-block push; `:import` / `:export` bail via `path.skipChildren()`. enter: (path) => { const node = path.node; const rule = /** @type {QualifiedRule} */ (node); const topLevel = path.parent === null; advanceCommentCursor(A.start(rule)); // One prelude materialization per rule, shared by the scans below // (null in plain mode, which never reads the array). const rulePrelude = isModules || mayHaveCustomSelectors ? A.prelude(rule) : null; // `:--name` custom-selector references (dashed idents never collide with the `:import`/`:export` ICSS preludes below). if (mayHaveCustomSelectors && rulePrelude !== null) { scanCustomSelectorUses(rulePrelude); } // `:import(…) { … }` / `:export { … }` ICSS pseudo-rules are processed inline at top level; nested ones bail out. if (isModules && rulePrelude !== null) { const firstIdx = nextNonWhitespace(rulePrelude, 0); if ( firstIdx + 1 < rulePrelude.length && A.type(rulePrelude[firstIdx]) === NodeType.Colon ) { const second = rulePrelude[firstIdx + 1]; const secondType = A.type(second); const rawName = secondType === NodeType.Ident ? A.value(second) : secondType === NodeType.Function ? A.name(second) : ""; const isImport = equalsLowerCase(rawName, "import"); if (isImport || equalsLowerCase(rawName, "export")) { if (topLevel) { const startColon = A.start(rulePrelude[firstIdx]); const endAfterBody = processImportOrExport( isImport ? 0 : 1, second, rule ); module.addPresentationalDependency( new ConstDependency("", [startColon, endAfterBody]) ); if (A.blockStart(rule) !== -1) { A.setBlockEnd(rule, endAfterBody); } A.setEnd(rule, endAfterBody); } else if (A.blockStart(rule) !== -1) { // Nested `:import` / `:export` — leave the body alone. A.setEnd(rule, A.blockEnd(rule)); } // Don't recurse into the body — handled inline above. path.skipChildren(); qualifiedRuleStateStack.push({ bailed: true }); return; } } } // Reset the anchor flag for this rule's body; nested `composes:` sees // parent + own class names in the shared identifier list, rolled back // by length on exit (no per-rule copy). const savedAnchor = currentRule.hasLocalAnchor; const savedLocalIdentifierCount = currentRule.localIdentifiers.length; currentRule.hasLocalAnchor = false; // Composes-state reset between rules (saved / restored around this rule); composesFiles is swapped out and re-created lazily only if this rule composes. const savedPrevComposesFile = currentRule.composesPrevFile; const savedComposesFiles = currentRule.composesFiles; currentRule.composesPrevFile = undefined; currentRule.composesFiles = null; qualifiedRuleStateStack.push({ bailed: false, savedAnchor, savedLocalIdentifierCount, savedPrevComposesFile, savedComposesFiles }); // Selectors are only CSS-Modules-relevant when `isModules` holds. if (isModules && rulePrelude !== null) { walkSelectorList( rulePrelude, /** @type {"local" | "global"} */ ( mode === "local" ? "local" : "global" ) ); } // A malformed declaration can leave orphan `url(...)` in the prelude — let the url visitor pick those up. currentStructural = rule; currentDeclCounterActive = false; dashed.active = false; dashed.emit = false; if (this.options.url && A.childCount(rule) > 0) { lastTokenEndForComments = A.start(A.childAt(rule, 0)); } // Dashed-ident scoping for the deprecated `--foo: { … }` custom-property-set syntax (prelude starts with a dashed-ident). if ( this.options.dashedIdents && isModules && rulePrelude !== null && rulePrelude.length > 0 ) { const first = rulePrelude[nextNonWhitespace(rulePrelude, 0)]; if ( first && A.type(first) === NodeType.Ident && isDashedIdentifier(A.value(first)) ) { const effectiveLocalMode = isEffectivelyLocal(); if (effectiveLocalMode) { dashed.active = true; dashed.emit = true; } } } // Pure-mode: report an impure prelude (if leaf-ish) and push the inherited-context frame before walking the body. if (!pure.enabled) return; const ruleBlockStart = A.blockStart(rule); pure.enterBlock({ isRulePrelude: true, treatAsLeaf: false, ownSkip: false, declarations: A.declarations(rule), childRules: A.childRules(rule), preludeStart: A.start(rule), preludeEnd: ruleBlockStart !== -1 ? ruleBlockStart : A.end(rule) }); }, // Qualified-rule exit: pure-frame finalization, scope restore, top-level reset; no-op for bailed ICSS. exit: (path) => { const node = path.node; const state = qualifiedRuleStateStack.pop(); if (!state || state.bailed) return; pure.exitBlock(); currentRule.hasLocalAnchor = state.savedAnchor; currentRule.localIdentifiers.length = state.savedLocalIdentifierCount; currentRule.composesPrevFile = state.savedPrevComposesFile; currentRule.composesFiles = state.savedComposesFiles; if (path.parent === null) finishTopLevelRule(node, false); } }, // Top-level declarations are parse errors (dropped by `parseAStylesheet`), so a declaration's parent is always a block. [NodeType.Declaration]: (path) => { const node = path.node; const decl = /** @type {Declaration} */ (node); // Reset value-visitor context, read by the value visitors below. currentStructural = decl; dashed.active = false; dashed.emit = false; // Position `lastTokenEndForComments` just past the `:` so a magic comment before a url() is found (every mode). let colonPos = A.nameEnd(decl); while ( colonPos < source.length && source.charCodeAt(colonPos) !== CC_COLON ) { colonPos++; } lastTokenEndForComments = colonPos + 1; currentDeclIsKnownProperty = false; currentDeclComposesSkip = false; currentDeclCounterActive = false; dashed.counterArgs = false; // Property-name analysis and value exports are CSS-Modules-only; a plain // stylesheet's declarations need no per-decl name slice / known-property lookup. if (!isModules) return; const nameStart = A.nameStart(decl); const nameEnd = A.nameEnd(decl); // Range-based name analysis — the common declaration never // slices its name out of the source. /** @type {string | undefined} */ let declName; /** @type {string | undefined} */ let declPropertyName; let declIsDashed = false; if (source.charCodeAt(nameStart) === CC_HYPHEN_MINUS) { if ( source.charCodeAt(nameStart + 1) === CC_HYPHEN_MINUS && nameEnd - nameStart >= 3 ) { // Custom property: never vendor-prefixed / known / composes // (escaped dashes aren't raw `--` bytes, same as the string form). declIsDashed = true; } else { // Vendor-prefixed (or degenerate short) — rare, string path. declName = A.name(decl); declPropertyName = toLowerCaseIfNeeded( declName.replace(VENDOR_PREFIX, "") ); currentDeclIsKnownProperty = knownProperties.has(declPropertyName); } } else { declPropertyName = knownPropertyForRange( knownPropertyIndex, source, nameStart, nameEnd ); // Cache the known-property flag so the per-token value visitors don't recompute it. currentDeclIsKnownProperty = declPropertyName !== undefined; } const effectiveLocalMode = isEffectivelyLocal(); // `composes:` with a local anchor: its strip-dep covers the whole declaration, so suppress the value's local/global/dashed/ICSS rewrites. currentDeclComposesSkip = currentRule.hasLocalAnchor && (declPropertyName !== undefined ? COMPOSES_PROPERTY.test(declPropertyName) : rangeEqualsLowerCase(source, nameStart, nameEnd, "composes") || rangeEqualsLowerCase(source, nameStart, nameEnd, "compose-with")); if (currentDeclComposesSkip) emitComposesWithAnchor(decl); const skipForComposes = currentDeclComposesSkip; // `content: counter(name, style)` can name a scoped counter / `@counter-style` from any property, so this isn't driven by the known-property table. currentDeclCounterActive = Boolean(this.options.customIdents) && effectiveLocalMode && !skipForComposes; // Known-property value localization (`animation-name: foo` exports `foo`). if (effectiveLocalMode && currentDeclIsKnownProperty) { emitKnownPropertyExports( decl, /** @type {string} */ (declPropertyName) ); } // Dashed-ident (custom-property) export of the property name; the value's dashed idents are scoped by the Ident / Function visitors (top-level only for unknown properties). if ( this.options.dashedIdents && effectiveLocalMode && !skipForComposes ) { // Only the `--`-prefixed path can carry a dashed ident. if (declIsDashed) { emitDashedIdentExport(nameStart, nameEnd); } dashed.active = true; dashed.emit = !currentDeclIsKnownProperty; } // ICSS-symbol rewrite (`color: foo` when `foo` is `@value`-defined), skipping known properties, the composes anchor, and dashed idents (handled above). Nothing to rewrite without definitions — don't slice the name. if ( !skipForComposes && !currentDeclIsKnownProperty && icssDefinitions.size !== 0 ) { if (declName === undefined) { declName = source.slice(nameStart, nameEnd); } if ( !(dashed.active && declIsDashed) && icssDefinitions.has(declName) ) { emitICSSSymbol(declName, nameStart, nameEnd); } } }, // Value-level visitors decide handling from the enclosing node via `urlActive()` / `localGlobalActive()` / `icssActive()`. [NodeType.Url]: (path) => { const node = path.node; const url = /** @type {UrlToken} */ (node); if (!urlActive()) return; // Skip bare url-tokens for a known property in CSS-Modules local mode. if ( currentStructural && A.type(currentStructural) === NodeType.Declaration && isModules && currentDeclIsKnownProperty && isEffectivelyLocal() ) { return; } const { ignored, options: urlComments } = magicCommentsIn( [lastTokenEndForComments, A.end(node)], lastTokenEndForComments, A.end(node) ); if (ignored) return; let value = normalizeUrl( input.slice(A.contentStart(url), A.contentEnd(url)), false ); // Ignore `url()`, `url('')` and `url("")`, they are valid by spec if (value.length === 0) return; 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; } 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, A.start(node), A.end(node) ); return; } } } const dep = new CssUrlDependency( value, [A.start(node), A.end(node)], "url" ); setDepLoc(dep, A.start(node), A.end(node)); applyResourceHintDefaults( dep, value, urlComments, rangeLoc(A.start(node), A.end(node)) ); module.addDependency(dep); module.addCodeGenerationDependency(dep); }, [NodeType.Comma](path) { const node = path.node; if (urlActive()) lastTokenEndForComments = A.start(node); }, [NodeType.Function]: { enter: (path) => { const node = path.node; const fn = /** @type {FunctionNode} */ (node); const fnNameStart = A.nameStart(fn); const fnNameEnd = A.nameEnd(fn); const fnNameLength = fnNameEnd - fnNameStart; // Functions are the densest value nodes, so the name is matched by raw byte range; only a name carrying an escape (rare) pays the unescaped slice. /** @type {string | undefined} */ let escapedName; let isLocalFn = false; let isGlobalFn = false; if (rangeHasEscape(input, fnNameStart, fnNameEnd)) { escapedName = A.unescapedName(fn); isLocalFn = equalsLowerCase(escapedName, "local"); isGlobalFn = !isLocalFn && equalsLowerCase(escapedName, "global"); } else if (fnNameLength === 5) { isLocalFn = rangeEqualsLowerCase( input, fnNameStart, fnNameEnd, "local" ); } else if (fnNameLength === 6) { isGlobalFn = rangeEqualsLowerCase( input, fnNameStart, fnNameEnd, "global" ); } if (urlActive()) emitUrlFunction(fn, escapedName); if (localGlobalActive() && (isLocalFn || isGlobalFn)) { processLocalOrGlobalFunction(fn, isLocalFn ? 1 : 2); } if ( icssActive() && !isLocalFn && !isGlobalFn && icssDefinitions.size !== 0 ) { // Without an escape the raw name equals the unescaped one — only the `@value`-lookup path needs the string at all. const fname = escapedName === undefined ? A.name(fn) : escapedName; if ( !(dashed.active && isDashedIdentifier(fname)) && icssDefinitions.has(fname) ) { emitICSSSymbol(fname, fnNameStart, fnNameEnd); } } // `counter()` / `counters()` / `target-counter()` / `target-counters()`: rewrite the counter name and any trailing counter-style ident. let counterNameIndex = -1; if (currentDeclCounterActive && !isLocalFn && !isGlobalFn) { counterNameIndex = counterFunctionNameIndex( input, fnNameStart, fnNameEnd, escapedName ); if (counterNameIndex !== -1) { walkCounterFunction(A.children(fn), counterNameIndex); } } // Dashed-ident scoping: handle this function, then set the child nesting level's state for the walk. dashed.push(); dashed.counterArgs = counterNameIndex !== -1; if (dashed.active) { if (isLocalFn || isGlobalFn) { // `local()` / `global()` dashed args go through the ICSS path above, not here. dashed.active = false; } else if ( escapedName === undefined ? (fnNameLength === 3 && rangeEqualsLowerCase( input, fnNameStart, fnNameEnd, "var" )) || (fnNameLength === 5 && rangeEqualsLowerCase( input, fnNameStart, fnNameEnd, "style" )) : equalsLowerCase(escapedName, "var") || equalsLowerCase(escapedName, "style") ) { // `var(--foo, …)` / `style(--foo, …)`: emit the first ident; the fallback doesn't self-emit. processDashedIdentInVarFunction(fn); dashed.emit = false; } else if ( dashed.emit && fnNameLength >= 3 && input.charCodeAt(fnNameStart) === CC_HYPHEN_MINUS && input.charCodeAt(fnNameStart + 1) === CC_HYPHEN_MINUS ) { // Custom-function call `--my-func(args)` — the name is the exported dashed-ident (a literal `--` prefix, like the `A.name` string check it replaces). emitDashedIdentExport(fnNameStart, fnNameEnd); } } }, exit: () => { dashed.pop(); } }, [NodeType.Ident](path) { const node = path.node; // Fast exit before slicing the ident value: outside dashed-ident scoping // and ICSS context (any non-CSS-Modules stylesheet) a bare ident carries // no work, and idents are the most common node, so skipping the per-ident // `value` slice matters. With no `@value`/`:import` definitions the ICSS // probe can never hit, so it doesn't warrant the slice either. The dashed // probe reads two char codes instead of slicing (escaped dashes don't // match, same as the string form — cf. `processDashedIdentInVarFunction`). const dashedActive = dashed.active; const icss = icssActive() && icssDefinitions.size !== 0; if (!dashedActive && !icss) return; const identStart = A.start(node); const identEnd = A.end(node); if ( dashedActive && identEnd - identStart >= 3 && input.charCodeAt(identStart) === CC_HYPHEN_MINUS && input.charCodeAt(identStart + 1) === CC_HYPHEN_MINUS ) { // Dashed idents are scoped here, never `@value` ICSS-rewritten. if (!dashed.emit) return; // Resolve the `--foo from "./x.css"` / `--foo from global` import suffix via sibling lookahead over the parent's child span (`path.index` avoids materializing the sibling list per dashed ident). const parent = path.parent; if (parent) { const count = A.childCount(parent); let j = path.index + 1; while ( j < count && A.type(A.childAt(parent, j)) === NodeType.Whitespace ) { j++; } const fromIdent = j < count ? A.childAt(parent, j) : undefined; if ( fromIdent && A.type(fromIdent) === NodeType.Ident && rangeEqualsLowerCase( input, A.start(fromIdent), A.end(fromIdent), "from" ) ) { j++; while ( j < count && A.type(A.childAt(parent, j)) === NodeType.Whitespace ) { j++; } const sourceNode = j < count ? A.childAt(parent, j) : undefined; if ( sourceNode && A.type(sourceNode) === NodeType.Ident && rangeEquals( input, A.start(sourceNode), A.end(sourceNode), "global" ) ) { emitDashedIdentFromGlobal(identEnd, A.end(sourceNode)); return; } if (sourceNode && A.type(sourceNode) === NodeType.String) { emitDashedIdentImport( identStart, identEnd, A.start(fromIdent), A.end(sourceNode), input.slice(A.start(sourceNode) + 1, A.end(sourceNode) - 1) ); return; } } } emitDashedIdentExport(identStart, identEnd); return; } if (!icss) return; // Already rewritten as a counter / counter-style name — a second dep would overlap the same range. if (dashed.counterArgs) return; // Deferred: only the ICSS probe needs the ident's string value. const identValue = A.value(node); if (icssDefinitions.has(identValue)) { emitICSSSymbol(identValue, identStart, identEnd); } } }; // `as` selects the top-level production (§5.3): a `style` attribute is a // block's contents, everything else a full stylesheet (the default). `as` // and `skip` are per-parse config on the processor; only `locConverter` // (per-source) is passed to `process`. One skip set: selector prelude + // unread value leaves (non-modules only; CSS Modules needs its selectors // and ICSS-captured values). new SourceProcessor() .use(/** @type {VisitorMap} */ (visitors)) .process(source, { as: /** @type {"stylesheet" | "block-contents"} */ (this.options.as), // Non-modules parses skip selector preludes, but `@custom-selector` expansion needs them, so keep them when the file may use one. skip: isModules ? undefined : mayHaveCustomSelectors ? SKIP_NON_MODULES_KEEP_SELECTORS : SKIP_NON_MODULES, locConverter }); if (customMediaQueries || customSelectorUses) { resolveCustomMediaAndSelectors(); } /** @type {BuildInfo} */ (module.buildInfo).strict = true; // Topologically sort the `composes … from` files and tag each file's first import dep with `sourceOrder` for cascade-correct load order (cycles keep their natural 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 (cssExportEntries.length > 0) { if (isModules && declaredExports.size > 0) { this._resolveAmbiguousExports(state, declaredExports, cssExportEntries); } module.addDependency(new CssIcssExportDependency(cssExportEntries)); } 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; } } module.exports = CssParser;