`third_party/node/node_modules` is generated by `./mach vendor node`. Differential Revision: https://phabricator.services.mozilla.com/D320322
4221 lines
140 KiB
JavaScript
4221 lines
140 KiB
JavaScript
/*
|
|
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 CommentCompilationWarning = require("../errors/CommentCompilationWarning");
|
|
const ModuleDependencyWarning = require("../errors/ModuleDependencyWarning");
|
|
const UnsupportedFeatureWarning = require("../errors/UnsupportedFeatureWarning");
|
|
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 topologicalSort = require("../util/topologicalSort");
|
|
const {
|
|
A,
|
|
NodeType,
|
|
SourceProcessor,
|
|
buildSkipSet,
|
|
equalsLowerCase,
|
|
isDashedIdentifier,
|
|
isWhitespace,
|
|
normalizeUrl,
|
|
rangeEquals,
|
|
rangeEqualsLowerCase,
|
|
toLowerCaseIfNeeded,
|
|
unescapeIdentifier
|
|
} = require("./syntax");
|
|
|
|
// `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 */
|
|
|
|
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);
|
|
|
|
// 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, Record<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, Record<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;
|
|
};
|
|
|
|
/** @type {Record<string, number>} */
|
|
const PREDEFINED_COUNTER_STYLES = {
|
|
decimal: 1,
|
|
"decimal-leading-zero": 1,
|
|
"arabic-indic": 1,
|
|
armenian: 1,
|
|
"upper-armenian": 1,
|
|
"lower-armenian": 1,
|
|
bengali: 1,
|
|
cambodian: 1,
|
|
khmer: 1,
|
|
"cjk-decimal": 1,
|
|
devanagari: 1,
|
|
georgian: 1,
|
|
gujarati: 1,
|
|
/* cspell:disable-next-line */
|
|
gurmukhi: 1,
|
|
hebrew: 1,
|
|
kannada: 1,
|
|
lao: 1,
|
|
malayalam: 1,
|
|
mongolian: 1,
|
|
myanmar: 1,
|
|
oriya: 1,
|
|
persian: 1,
|
|
"lower-roman": 1,
|
|
"upper-roman": 1,
|
|
tamil: 1,
|
|
telugu: 1,
|
|
thai: 1,
|
|
tibetan: 1,
|
|
|
|
"lower-alpha": 1,
|
|
"lower-latin": 1,
|
|
"upper-alpha": 1,
|
|
"upper-latin": 1,
|
|
"lower-greek": 1,
|
|
hiragana: 1,
|
|
/* cspell:disable-next-line */
|
|
"hiragana-iroha": 1,
|
|
katakana: 1,
|
|
/* cspell:disable-next-line */
|
|
"katakana-iroha": 1,
|
|
|
|
disc: 1,
|
|
circle: 1,
|
|
square: 1,
|
|
"disclosure-open": 1,
|
|
"disclosure-closed": 1,
|
|
|
|
"cjk-earthly-branch": 1,
|
|
"cjk-heavenly-stem": 1,
|
|
|
|
"japanese-informal": 1,
|
|
"japanese-formal": 1,
|
|
|
|
"korean-hangul-formal": 1,
|
|
/* cspell:disable-next-line */
|
|
"korean-hanja-informal": 1,
|
|
/* cspell:disable-next-line */
|
|
"korean-hanja-formal": 1,
|
|
|
|
"simp-chinese-informal": 1,
|
|
"simp-chinese-formal": 1,
|
|
"trad-chinese-informal": 1,
|
|
"trad-chinese-formal": 1,
|
|
"cjk-ideographic": 1,
|
|
|
|
"ethiopic-numeric": 1
|
|
};
|
|
|
|
/** @type {Record<string, number>} */
|
|
const GLOBAL_VALUES = {
|
|
// Global values
|
|
initial: Infinity,
|
|
inherit: Infinity,
|
|
unset: Infinity,
|
|
revert: Infinity,
|
|
"revert-layer": Infinity
|
|
};
|
|
|
|
/** @type {Record<string, number>} */
|
|
const GRID_AREA_OR_COLUMN_OR_ROW = {
|
|
auto: Infinity,
|
|
span: Infinity,
|
|
...GLOBAL_VALUES
|
|
};
|
|
|
|
/** @type {Record<string, number>} */
|
|
const GRID_AUTO_COLUMNS_OR_ROW = {
|
|
"min-content": Infinity,
|
|
"max-content": Infinity,
|
|
auto: Infinity,
|
|
...GLOBAL_VALUES
|
|
};
|
|
|
|
/** @type {Record<string, number>} */
|
|
const GRID_AUTO_FLOW = {
|
|
row: 1,
|
|
column: 1,
|
|
dense: 1,
|
|
...GLOBAL_VALUES
|
|
};
|
|
|
|
/** @type {Record<string, number>} */
|
|
const GRID_TEMPLATE_AREAS = {
|
|
// Special
|
|
none: 1,
|
|
...GLOBAL_VALUES
|
|
};
|
|
|
|
/** @type {Record<string, number>} */
|
|
const GRID_TEMPLATE_COLUMNS_OR_ROWS = {
|
|
none: 1,
|
|
subgrid: 1,
|
|
masonry: 1,
|
|
"max-content": Infinity,
|
|
"min-content": Infinity,
|
|
auto: Infinity,
|
|
...GLOBAL_VALUES
|
|
};
|
|
|
|
/** @type {Record<string, number>} */
|
|
const GRID_TEMPLATE = {
|
|
...GRID_TEMPLATE_AREAS,
|
|
...GRID_TEMPLATE_COLUMNS_OR_ROWS
|
|
};
|
|
|
|
/** @type {Record<string, number>} */
|
|
const GRID = {
|
|
"auto-flow": 1,
|
|
dense: 1,
|
|
...GRID_AUTO_COLUMNS_OR_ROW,
|
|
...GRID_AUTO_FLOW,
|
|
...GRID_TEMPLATE_AREAS,
|
|
...GRID_TEMPLATE_COLUMNS_OR_ROWS
|
|
};
|
|
|
|
/**
|
|
* Gets known properties.
|
|
* @param {{ animation?: boolean, container?: boolean, customIdents?: boolean, grid?: boolean }=} options options
|
|
* @returns {Map<string, Record<string, number>>} list of known properties
|
|
*/
|
|
const buildKnownProperties = (options = {}) => {
|
|
/** @type {Map<string, Record<string, number>>} */
|
|
const knownProperties = new Map();
|
|
|
|
if (options.animation) {
|
|
knownProperties.set("animation", {
|
|
// animation-direction
|
|
normal: 1,
|
|
reverse: 1,
|
|
alternate: 1,
|
|
"alternate-reverse": 1,
|
|
// animation-fill-mode
|
|
forwards: 1,
|
|
backwards: 1,
|
|
both: 1,
|
|
// animation-iteration-count
|
|
infinite: 1,
|
|
// animation-play-state
|
|
paused: 1,
|
|
running: 1,
|
|
// animation-timing-function
|
|
ease: 1,
|
|
"ease-in": 1,
|
|
"ease-out": 1,
|
|
"ease-in-out": 1,
|
|
linear: 1,
|
|
"step-end": 1,
|
|
"step-start": 1,
|
|
// Special
|
|
none: Infinity, // No matter how many times you write none, it will never be an animation name
|
|
...GLOBAL_VALUES
|
|
});
|
|
knownProperties.set("animation-name", {
|
|
// Special
|
|
none: Infinity, // No matter how many times you write none, it will never be an animation name
|
|
...GLOBAL_VALUES
|
|
});
|
|
}
|
|
|
|
if (options.container) {
|
|
knownProperties.set("container", {
|
|
// container-type
|
|
normal: 1,
|
|
size: 1,
|
|
"inline-size": 1,
|
|
"scroll-state": 1,
|
|
// Special
|
|
none: Infinity,
|
|
...GLOBAL_VALUES
|
|
});
|
|
knownProperties.set("container-name", {
|
|
// Special
|
|
none: Infinity,
|
|
...GLOBAL_VALUES
|
|
});
|
|
}
|
|
|
|
if (options.customIdents) {
|
|
knownProperties.set("list-style", {
|
|
// list-style-position
|
|
inside: 1,
|
|
outside: 1,
|
|
// list-style-type
|
|
...PREDEFINED_COUNTER_STYLES,
|
|
// Special
|
|
none: Infinity,
|
|
...GLOBAL_VALUES
|
|
});
|
|
knownProperties.set("list-style-type", {
|
|
// list-style-type
|
|
...PREDEFINED_COUNTER_STYLES,
|
|
// Special
|
|
none: Infinity,
|
|
...GLOBAL_VALUES
|
|
});
|
|
knownProperties.set("system", {
|
|
cyclic: 1,
|
|
numeric: 1,
|
|
alphabetic: 1,
|
|
symbolic: 1,
|
|
additive: 1,
|
|
fixed: 1,
|
|
extends: 1,
|
|
...PREDEFINED_COUNTER_STYLES
|
|
});
|
|
knownProperties.set("fallback", {
|
|
...PREDEFINED_COUNTER_STYLES
|
|
});
|
|
knownProperties.set("speak-as", {
|
|
auto: 1,
|
|
bullets: 1,
|
|
numbers: 1,
|
|
words: 1,
|
|
"spell-out": 1,
|
|
...PREDEFINED_COUNTER_STYLES
|
|
});
|
|
knownProperties.set("view-transition-name", {
|
|
none: Infinity,
|
|
auto: Infinity,
|
|
"match-element": Infinity,
|
|
...GLOBAL_VALUES
|
|
});
|
|
knownProperties.set("view-transition-group", {
|
|
normal: Infinity,
|
|
contain: Infinity,
|
|
nearest: Infinity,
|
|
...GLOBAL_VALUES
|
|
});
|
|
knownProperties.set("view-transition-class", {
|
|
none: Infinity,
|
|
...GLOBAL_VALUES
|
|
});
|
|
}
|
|
|
|
if (options.grid) {
|
|
knownProperties.set("grid", GRID);
|
|
knownProperties.set("grid-area", GRID_AREA_OR_COLUMN_OR_ROW);
|
|
knownProperties.set("grid-column", GRID_AREA_OR_COLUMN_OR_ROW);
|
|
knownProperties.set("grid-column-end", GRID_AREA_OR_COLUMN_OR_ROW);
|
|
knownProperties.set("grid-column-start", GRID_AREA_OR_COLUMN_OR_ROW);
|
|
knownProperties.set("grid-row", GRID_AREA_OR_COLUMN_OR_ROW);
|
|
knownProperties.set("grid-row-end", GRID_AREA_OR_COLUMN_OR_ROW);
|
|
knownProperties.set("grid-row-start", GRID_AREA_OR_COLUMN_OR_ROW);
|
|
knownProperties.set("grid-template", GRID_TEMPLATE);
|
|
knownProperties.set("grid-template-areas", GRID_TEMPLATE_AREAS);
|
|
knownProperties.set("grid-template-columns", GRID_TEMPLATE_COLUMNS_OR_ROWS);
|
|
knownProperties.set("grid-template-rows", GRID_TEMPLATE_COLUMNS_OR_ROWS);
|
|
}
|
|
|
|
return knownProperties;
|
|
};
|
|
|
|
/** @type {Map<number, Map<string, Record<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 (and its value objects) per parsed module.
|
|
* @param {{ animation?: boolean, container?: boolean, customIdents?: boolean, grid?: boolean }=} options options
|
|
* @returns {Map<string, Record<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;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* @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 ModuleDependencyWarning(state.module, new WebpackError(message), {
|
|
start: { line: sl, column: sc },
|
|
end: { line: el, column: ec }
|
|
})
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Emits a build error for the provided range.
|
|
* @param {ParserState} state parser state
|
|
* @param {string} message error message
|
|
* @param {LocConverter} locConverter location converter
|
|
* @param {number} start start offset
|
|
* @param {number} end end offset
|
|
*/
|
|
_emitError(state, message, locConverter, start, end) {
|
|
const { line: sl, column: sc } = locConverter.get(start);
|
|
const { line: el, column: ec } = locConverter.get(end);
|
|
|
|
const err = new WebpackError(message);
|
|
err.module = state.module;
|
|
err.loc = {
|
|
start: { line: sl, column: sc },
|
|
end: { line: el, column: ec }
|
|
};
|
|
state.module.addError(err);
|
|
}
|
|
|
|
/**
|
|
* Parses the provided source and updates the parser state.
|
|
* @param {string | Buffer | PreparsedAst} source the source to parse
|
|
* @param {ParserState} state the parser state
|
|
* @returns {ParserState} the parser state
|
|
*/
|
|
parse(source, state) {
|
|
if (Buffer.isBuffer(source)) {
|
|
source = source.toString("utf8");
|
|
} else if (typeof source === "object") {
|
|
throw new Error("webpackAst is unexpected for the CssParser");
|
|
}
|
|
if (source[0] === "\uFEFF") {
|
|
source = source.slice(1);
|
|
}
|
|
|
|
// 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;
|
|
|
|
// 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 CommentCompilationWarning(
|
|
`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 UnsupportedFeatureWarning(
|
|
`\`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, { text: string, kind: ("condition" | "type" | "unsupported") }> | undefined} */
|
|
let customMediaDefs;
|
|
/** @type {{ name: string, start: number, end: number, invalid: boolean, leading: boolean }[] | undefined} */
|
|
let customMediaUses;
|
|
/** @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);
|
|
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
|
|
*/
|
|
const processLocalOrGlobalFunction = (fn, type) => {
|
|
// 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);
|
|
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
|
|
* @returns {number} position after handling
|
|
*/
|
|
const processLocalAtRule = (atRule, options) => {
|
|
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));
|
|
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));
|
|
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);
|
|
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);
|
|
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);
|
|
|
|
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)
|
|
);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* The child list of an AST node (a function/block `value` or a rule `prelude`) for sibling lookahead.
|
|
* @param {AstNode | null} parent parent node
|
|
* @returns {AstNode[] | undefined} the child list, or undefined
|
|
*/
|
|
const childrenOf = (parent) => {
|
|
if (!parent) return undefined;
|
|
switch (A.type(parent)) {
|
|
case NodeType.Function:
|
|
case NodeType.SimpleBlock:
|
|
case NodeType.Declaration:
|
|
return A.children(parent);
|
|
case NodeType.AtRule:
|
|
case NodeType.QualifiedRule:
|
|
return A.prelude(parent);
|
|
default:
|
|
return undefined;
|
|
}
|
|
};
|
|
|
|
// `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,
|
|
/** LIFO save/restore of `active` + `emit` across function nesting, bit-packed (bit 0 = active, bit 1 = emit) 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));
|
|
},
|
|
/** 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;
|
|
}
|
|
};
|
|
// 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;
|
|
|
|
/**
|
|
* Per-at-rule scope frames; `exit` reads `hasBlock` to pick the block-cleanup vs `suppressNextRulePrelude` branch.
|
|
* @type {{ savedAnchor: boolean, savedLocalIdentifiers: string[], 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();
|
|
}
|
|
};
|
|
|
|
/**
|
|
* 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));
|
|
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));
|
|
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, savedLocalIdentifiers: string[], savedPrevComposesFile: string | undefined, savedComposesFiles: Set<string> | null })[]}
|
|
*/
|
|
const qualifiedRuleStateStack = [];
|
|
|
|
/**
|
|
* Whether url() deps are emitted here (from `currentStructural`): false outside `options.url` and inside an `@import` prelude (the import target, unless url recovery is on).
|
|
* @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.
|
|
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} fname the unescaped function name (any case)
|
|
*/
|
|
const emitUrlFunction = (fn, fname) => {
|
|
if (equalsLowerCase(fname, "url") || equalsLowerCase(fname, "src")) {
|
|
// 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 (IMAGE_SET_FUNCTION.test(fname)) {
|
|
// `image-set(…)`: each comma segment's first string is the URL; advance the magic-comment fence per string.
|
|
lastTokenEndForComments = A.nameEnd(fn) + 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);
|
|
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);
|
|
|
|
addCssExport(sl, sc, el, ec, localName, getReexport(value));
|
|
} else {
|
|
icssDefinitions.set(localName, { value });
|
|
|
|
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. Classifies the value: `"condition"` (a media-in-parens, or comma list → `(a or b)`) inlines anywhere; `"type"` (a media type like `screen` / `not all` / `screen and (…)`) inlines only at a query's start; `true` / `false` / nested custom-media values are `"unsupported"` (warned on use — the ecosystem disagrees on these).
|
|
* @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++;
|
|
|
|
/**
|
|
* @param {string} text resolved text
|
|
* @param {"condition" | "type" | "unsupported"} kind value kind
|
|
*/
|
|
const store = (text, kind) => {
|
|
(customMediaDefs || (customMediaDefs = new Map())).set(name, {
|
|
text,
|
|
kind
|
|
});
|
|
};
|
|
const extra = { hasExtra: false };
|
|
|
|
// 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--;
|
|
if (vs >= ve) {
|
|
store("", "unsupported");
|
|
return;
|
|
}
|
|
|
|
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]));
|
|
if (A.type(first) === NodeType.SimpleBlock) {
|
|
// A lone `(--x)` is an unresolved nested custom media; anything else is a media-in-parens.
|
|
const nested =
|
|
loneDashedIdentOfBlock(
|
|
/** @type {SimpleBlock} */ (first),
|
|
extra
|
|
) !== undefined;
|
|
store(nested ? "" : valueText, nested ? "unsupported" : "condition");
|
|
return;
|
|
}
|
|
if (A.type(first) !== NodeType.Ident) {
|
|
store("", "unsupported");
|
|
return;
|
|
}
|
|
if (
|
|
rangeEqualsLowerCase(input, A.start(first), A.end(first), "true") ||
|
|
rangeEqualsLowerCase(input, A.start(first), A.end(first), "false")
|
|
) {
|
|
store("", "unsupported");
|
|
return;
|
|
}
|
|
if (rangeEqualsLowerCase(input, A.start(first), A.end(first), "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) {
|
|
store(`(${valueText})`, "condition");
|
|
} else {
|
|
store(valueText, "type");
|
|
}
|
|
return;
|
|
}
|
|
// `screen`, `only screen`, `screen and (…)`, …
|
|
store(valueText, "type");
|
|
return;
|
|
}
|
|
|
|
// Comma list → `(seg1 or seg2 …)`; supported only when every segment is a media-in-parens.
|
|
/** @type {string[]} */
|
|
const segments = [];
|
|
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 &&
|
|
loneDashedIdentOfBlock(/** @type {SimpleBlock} */ (f), extra) ===
|
|
undefined
|
|
) {
|
|
segments.push(text);
|
|
} else if (
|
|
A.type(f) === NodeType.Ident &&
|
|
rangeEqualsLowerCase(input, A.start(f), A.end(f), "not")
|
|
) {
|
|
segments.push(`(${text})`);
|
|
} else {
|
|
ok = false;
|
|
}
|
|
};
|
|
for (let k = vs; k < ve; k++) {
|
|
if (A.type(prelude[k]) === NodeType.Comma) {
|
|
flush();
|
|
seg = [];
|
|
} else {
|
|
seg.push(prelude[k]);
|
|
}
|
|
}
|
|
flush();
|
|
if (ok && segments.length > 0) {
|
|
store(
|
|
segments.length === 1 ? segments[0] : `(${segments.join(" or ")})`,
|
|
"condition"
|
|
);
|
|
} else {
|
|
store("", "unsupported");
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Record every `(--name)` custom-media reference in a media prelude (recursing into nested groups/functions) for post-walk resolution; `invalid` flags a non-boolean-context use like `(--name: 1px)`, `leading` marks a reference that is the first term of a top-level query (the only spot a media-type value may be inlined).
|
|
* @param {readonly AstNode[]} tokens prelude (or nested block) tokens
|
|
* @param {boolean} topLevel whether `tokens` is the media prelude itself (vs. a nested group)
|
|
*/
|
|
const scanCustomMediaUses = (tokens, topLevel) => {
|
|
const extra = { hasExtra: false };
|
|
let leading = true;
|
|
for (const t of tokens) {
|
|
const tt = A.type(t);
|
|
if (tt === NodeType.Whitespace) continue;
|
|
if (topLevel && tt === NodeType.Comma) {
|
|
leading = true;
|
|
continue;
|
|
}
|
|
if (tt === NodeType.SimpleBlock) {
|
|
const name = loneDashedIdentOfBlock(
|
|
/** @type {SimpleBlock} */ (t),
|
|
extra
|
|
);
|
|
if (name !== undefined) {
|
|
(customMediaUses || (customMediaUses = [])).push({
|
|
name,
|
|
start: A.start(t),
|
|
end: A.end(t),
|
|
invalid: extra.hasExtra,
|
|
leading: Boolean(topLevel && leading)
|
|
});
|
|
} else {
|
|
scanCustomMediaUses(A.children(t), false);
|
|
}
|
|
} else if (tt === NodeType.Function) {
|
|
scanCustomMediaUses(A.children(t), false);
|
|
}
|
|
if (topLevel) leading = false;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* 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 unsupported custom-media uses.
|
|
*/
|
|
const resolveCustomMediaAndSelectors = () => {
|
|
if (customMediaUses) {
|
|
for (const use of customMediaUses) {
|
|
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 def = customMediaDefs && customMediaDefs.get(use.name);
|
|
if (!def) {
|
|
this._emitWarning(
|
|
state,
|
|
`Unknown custom media query '${use.name}'`,
|
|
locConverter,
|
|
use.start,
|
|
use.end
|
|
);
|
|
continue;
|
|
}
|
|
if (def.kind === "unsupported") {
|
|
this._emitWarning(
|
|
state,
|
|
`Custom media query '${use.name}' uses an unsupported value ('true'/'false' or a nested custom media) and was left unresolved`,
|
|
locConverter,
|
|
use.start,
|
|
use.end
|
|
);
|
|
continue;
|
|
}
|
|
if (def.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
|
|
);
|
|
continue;
|
|
}
|
|
module.addPresentationalDependency(
|
|
new ConstDependency(def.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-*` legitimately takes a dashed-ident value; other known properties keep the historical plain-ident handling.
|
|
const isViewTransitionProperty = Boolean(
|
|
declPropertyName.startsWith("view-transition")
|
|
);
|
|
const isGridTemplate = isGridProperty
|
|
? Boolean(
|
|
declPropertyName === "grid" ||
|
|
declPropertyName === "grid-template" ||
|
|
declPropertyName === "grid-template-columns" ||
|
|
declPropertyName === "grid-template-rows"
|
|
)
|
|
: false;
|
|
const keywords =
|
|
/** @type {Record<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
|
|
);
|
|
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` — a dashed name scopes as a custom property under `dashedIdents`, not as a plain ident export.
|
|
if (isViewTransitionProperty && 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;
|
|
if (
|
|
keywords[keyword] &&
|
|
parsedKeywords[keyword] < keywords[keyword]
|
|
) {
|
|
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:
|
|
if (isGridTemplate) {
|
|
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;
|
|
const savedLocalIdentifiers = currentRule.localIdentifiers;
|
|
// Only modules-mode walks mutate `localIdentifiers`; otherwise share
|
|
// the (empty) parent list instead of copying it per rule.
|
|
currentRule.localIdentifiers = isModules
|
|
? [...savedLocalIdentifiers]
|
|
: savedLocalIdentifiers;
|
|
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) scanCustomMediaUses(A.prelude(at), true);
|
|
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"
|
|
});
|
|
} else if (
|
|
this.options.customIdents &&
|
|
name === "@counter-style"
|
|
) {
|
|
processLocalAtRule(at, {
|
|
identifier: mode === "local"
|
|
});
|
|
} else if (this.options.container && name === "@container") {
|
|
processLocalAtRule(at, {
|
|
identifier: mode === "local" ? /^(none|and|or|not)$/ : false
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
// `@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;
|
|
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.
|
|
const atDecls = A.declarations(at);
|
|
const atChildRules = A.childRules(at);
|
|
const atHasBlock = Boolean(atDecls || atChildRules);
|
|
const atBlockStart = A.blockStart(at);
|
|
if (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,
|
|
savedLocalIdentifiers,
|
|
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 = state.savedLocalIdentifiers;
|
|
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));
|
|
// `:--name` custom-selector references (dashed idents never collide with the `:import`/`:export` ICSS preludes below).
|
|
if (mayHaveCustomSelectors) scanCustomSelectorUses(A.prelude(rule));
|
|
// `:import(…) { … }` / `:export { … }` ICSS pseudo-rules are processed inline at top level; nested ones bail out.
|
|
if (isModules) {
|
|
const rulePrelude = A.prelude(rule);
|
|
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, inheriting (copying) the parent's identifier list so nested `composes:` sees both parent and child class names.
|
|
const savedAnchor = currentRule.hasLocalAnchor;
|
|
const savedLocalIdentifiers = currentRule.localIdentifiers;
|
|
currentRule.hasLocalAnchor = false;
|
|
// Only modules-mode walks mutate `localIdentifiers`; otherwise share the (empty) parent list instead of copying it per rule.
|
|
currentRule.localIdentifiers = isModules
|
|
? [...savedLocalIdentifiers]
|
|
: savedLocalIdentifiers;
|
|
// 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,
|
|
savedLocalIdentifiers,
|
|
savedPrevComposesFile,
|
|
savedComposesFiles
|
|
});
|
|
// Selectors are only CSS-Modules-relevant when `isModules` holds.
|
|
const rulePrelude = A.prelude(rule);
|
|
if (isModules) {
|
|
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;
|
|
dashed.active = false;
|
|
dashed.emit = false;
|
|
if (this.options.url && rulePrelude.length > 0) {
|
|
lastTokenEndForComments = A.start(rulePrelude[0]);
|
|
}
|
|
// Dashed-ident scoping for the deprecated `--foo: { … }` custom-property-set syntax (prelude starts with a dashed-ident).
|
|
if (
|
|
this.options.dashedIdents &&
|
|
isModules &&
|
|
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.
|
|
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 = state.savedLocalIdentifiers;
|
|
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;
|
|
// 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;
|
|
if (source.charCodeAt(nameStart) === CC_HYPHEN_MINUS) {
|
|
// Vendor-prefixed or custom property — rare, take the 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;
|
|
// 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 (declName !== undefined && isDashedIdentifier(declName)) {
|
|
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 && isDashedIdentifier(declName)) &&
|
|
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 fnameRaw = A.unescapedName(fn);
|
|
// `equalsLowerCase` keeps this visitor free of a per-function
|
|
// lowercased copy — functions are the densest value nodes.
|
|
const isLocalFn = equalsLowerCase(fnameRaw, "local");
|
|
const isGlobalFn = !isLocalFn && equalsLowerCase(fnameRaw, "global");
|
|
if (urlActive()) emitUrlFunction(fn, fnameRaw);
|
|
if (localGlobalActive() && (isLocalFn || isGlobalFn)) {
|
|
processLocalOrGlobalFunction(fn, isLocalFn ? 1 : 2);
|
|
}
|
|
if (
|
|
icssActive() &&
|
|
!isLocalFn &&
|
|
!isGlobalFn &&
|
|
!(dashed.active && isDashedIdentifier(fnameRaw)) &&
|
|
icssDefinitions.has(fnameRaw)
|
|
) {
|
|
emitICSSSymbol(fnameRaw, A.nameStart(fn), A.nameEnd(fn));
|
|
}
|
|
// Dashed-ident scoping: handle this function, then set the child nesting level's state for the walk.
|
|
dashed.push();
|
|
if (dashed.active) {
|
|
if (isLocalFn || isGlobalFn) {
|
|
// `local()` / `global()` dashed args go through the ICSS path above, not here.
|
|
dashed.active = false;
|
|
} else if (
|
|
equalsLowerCase(fnameRaw, "var") ||
|
|
equalsLowerCase(fnameRaw, "style")
|
|
) {
|
|
// `var(--foo, …)` / `style(--foo, …)`: emit the first ident; the fallback doesn't self-emit.
|
|
processDashedIdentInVarFunction(fn);
|
|
dashed.emit = false;
|
|
} else if (dashed.emit && isDashedIdentifier(A.name(fn))) {
|
|
// Custom-function call `--my-func(args)` — the name is the exported dashed-ident.
|
|
emitDashedIdentExport(A.nameStart(fn), A.nameEnd(fn));
|
|
}
|
|
}
|
|
},
|
|
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.
|
|
const dashedActive = dashed.active;
|
|
const icss = icssActive() && icssDefinitions.size !== 0;
|
|
if (!dashedActive && !icss) return;
|
|
const identValue = A.value(node);
|
|
if (dashedActive && isDashedIdentifier(identValue)) {
|
|
// 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.
|
|
const siblings = childrenOf(path.parent);
|
|
const i = siblings ? siblings.indexOf(node) : -1;
|
|
if (siblings && i !== -1) {
|
|
const j = nextNonWhitespace(siblings, i + 1);
|
|
if (
|
|
j < siblings.length &&
|
|
A.type(siblings[j]) === NodeType.Ident &&
|
|
rangeEqualsLowerCase(
|
|
input,
|
|
A.start(siblings[j]),
|
|
A.end(siblings[j]),
|
|
"from"
|
|
)
|
|
) {
|
|
const fromIdent = siblings[j];
|
|
const sourceNode = siblings[nextNonWhitespace(siblings, j + 1)];
|
|
if (
|
|
sourceNode &&
|
|
A.type(sourceNode) === NodeType.Ident &&
|
|
rangeEquals(
|
|
input,
|
|
A.start(sourceNode),
|
|
A.end(sourceNode),
|
|
"global"
|
|
)
|
|
) {
|
|
emitDashedIdentFromGlobal(A.end(node), A.end(sourceNode));
|
|
return;
|
|
}
|
|
if (sourceNode && A.type(sourceNode) === NodeType.String) {
|
|
emitDashedIdentImport(
|
|
A.start(node),
|
|
A.end(node),
|
|
A.start(fromIdent),
|
|
A.end(sourceNode),
|
|
input.slice(A.start(sourceNode) + 1, A.end(sourceNode) - 1)
|
|
);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
emitDashedIdentExport(A.start(node), A.end(node));
|
|
return;
|
|
}
|
|
if (!icss) return;
|
|
if (icssDefinitions.has(identValue)) {
|
|
emitICSSSymbol(identValue, A.start(node), A.end(node));
|
|
}
|
|
}
|
|
};
|
|
// `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({
|
|
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
|
|
})
|
|
.use(/** @type {VisitorMap} */ (visitors))
|
|
.process(source, { locConverter });
|
|
|
|
if (customMediaUses || 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) {
|
|
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;
|