webpack 5.108.3 → 5.108.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -32,6 +32,7 @@ const {
32
32
  A,
33
33
  NodeType,
34
34
  SourceProcessor,
35
+ buildSkipSet,
35
36
  equalsLowerCase,
36
37
  unescapeIdentifier
37
38
  } = require("./syntax");
@@ -93,8 +94,32 @@ const IMAGE_SET_FUNCTION = /^(?:-\w+-)?image-set$/i;
93
94
  const MAGIC_COMMENT_FAST_PATH =
94
95
  /^\s*(webpack[A-Z][A-Za-z]+)\s*:\s*(true|false|null|-?\d+(?:\.\d+)?)\s*$/;
95
96
  const OPTIONALLY_VENDOR_PREFIXED_KEYFRAMES_AT_RULE = /^@(?:-\w+-)?keyframes$/;
97
+ const VENDOR_PREFIX = /^-\w+-/;
96
98
  const COMPOSES_PROPERTY = /^(?:composes|compose-with)$/i;
97
99
  const IS_MODULES = /\.modules?\.[^.]+$/i;
100
+
101
+ // Skip options for a non-CSS-Modules parse: drop the selector prelude (never
102
+ // walked without modules) plus value / function-arg leaves nothing reads (the
103
+ // `Ident` visitor no-ops, the `Declaration` visitor returns early, no ICSS).
104
+ // `url` / functions / strings / blocks / commas are kept — they carry url()
105
+ // rewrites and image-set fences. At-rule preludes are kept (`@media` / `@import`
106
+ // are read). CSS-Modules parses skip nothing: selectors are walked and ICSS
107
+ // `:export { k: v }` captures each value's byte range from its first / last node.
108
+ const SKIP_NON_MODULES = {
109
+ types: buildSkipSet([
110
+ NodeType.Number,
111
+ NodeType.Dimension,
112
+ NodeType.Percentage,
113
+ NodeType.Ident,
114
+ NodeType.Hash,
115
+ NodeType.Colon,
116
+ NodeType.Delim,
117
+ // Nothing reads value/arg whitespace either — consumers use
118
+ // `nextNonWhitespace` / type checks that tolerate its absence.
119
+ NodeType.Whitespace
120
+ ]),
121
+ selectorPrelude: true
122
+ };
98
123
  const CSS_COMMENT = /\/\*((?!\*\/)[\s\S]*?)\*\//g;
99
124
  // `@value` recognizers (postcss-modules-values shape): the import form `<names> from <source>`, and the `<importName> as <localName>` alias inside it.
100
125
  const VALUE_IMPORT_FORM = /from(\/\*|\s)(?:[\s\S]+)$/i;
@@ -184,6 +209,109 @@ const normalizeUrl = (str, isString) => {
184
209
  const isDashedIdentifier = (identifier) =>
185
210
  identifier.startsWith("--") && identifier.length >= 3;
186
211
 
212
+ /**
213
+ * `s.toLowerCase()` that returns `s` itself (no allocation) when it can't
214
+ * change — no ASCII uppercase and no non-ASCII (whose Unicode case mapping is
215
+ * left to the real `toLowerCase`).
216
+ * @param {string} s string
217
+ * @returns {string} lowercased string
218
+ */
219
+ const toLowerCaseIfNeeded = (s) => {
220
+ for (let i = 0; i < s.length; i++) {
221
+ const c = s.charCodeAt(i);
222
+ if ((c >= 65 && c <= 90) || c > 127) return s.toLowerCase();
223
+ }
224
+ return s;
225
+ };
226
+
227
+ /**
228
+ * Case-sensitive equality of a source range against a literal — no slice.
229
+ * @param {string} input source
230
+ * @param {number} start range start
231
+ * @param {number} end range end (exclusive)
232
+ * @param {string} lit literal to match
233
+ * @returns {boolean} true when the range equals `lit`
234
+ */
235
+ const rangeEquals = (input, start, end, lit) =>
236
+ end - start === lit.length && input.startsWith(lit, start);
237
+
238
+ /**
239
+ * ASCII case-insensitive equality of a source range against a lowercase ASCII literal — no slice.
240
+ * @param {string} input source
241
+ * @param {number} start range start
242
+ * @param {number} end range end (exclusive)
243
+ * @param {string} lit lowercase ASCII literal to match
244
+ * @returns {boolean} true when the range equals `lit` ignoring ASCII case
245
+ */
246
+ const rangeEqualsLowerCase = (input, start, end, lit) => {
247
+ if (end - start !== lit.length) return false;
248
+ for (let i = 0; i < lit.length; i++) {
249
+ let c = input.charCodeAt(start + i);
250
+ if (c >= 65 && c <= 90) c |= 0x20;
251
+ if (c !== lit.charCodeAt(i)) return false;
252
+ }
253
+ return true;
254
+ };
255
+
256
+ /**
257
+ * Range-keyed index over a known-properties table: ASCII-case-folded 31-hash
258
+ * of the name bytes → canonical key(s). Lets the Declaration visitor answer
259
+ * "is this a known property" (and get the canonical lowercase name) without
260
+ * slicing the property name out of the source per declaration.
261
+ * @type {WeakMap<Map<string, Record<string, number>>, Map<number, string | string[]>>}
262
+ */
263
+ const KNOWN_PROPERTY_INDEX_CACHE = new WeakMap();
264
+
265
+ /**
266
+ * Gets (or builds) the hash index for a known-properties table.
267
+ * @param {Map<string, Record<string, number>>} knownProperties known properties table
268
+ * @returns {Map<number, string | string[]>} hash → canonical name(s)
269
+ */
270
+ const getKnownPropertyIndex = (knownProperties) => {
271
+ let index = KNOWN_PROPERTY_INDEX_CACHE.get(knownProperties);
272
+ if (index === undefined) {
273
+ index = new Map();
274
+ for (const name of knownProperties.keys()) {
275
+ let h = name.length;
276
+ for (let i = 0; i < name.length; i++) {
277
+ h = ((h << 5) - h + name.charCodeAt(i)) | 0;
278
+ }
279
+ const hit = index.get(h);
280
+ if (hit === undefined) index.set(h, name);
281
+ else if (typeof hit === "string") index.set(h, [hit, name]);
282
+ else hit.push(name);
283
+ }
284
+ KNOWN_PROPERTY_INDEX_CACHE.set(knownProperties, index);
285
+ }
286
+ return index;
287
+ };
288
+
289
+ /**
290
+ * Canonical known-property name for a source range (ASCII case-insensitive), without slicing.
291
+ * @param {Map<number, string | string[]>} index hash index from `getKnownPropertyIndex`
292
+ * @param {string} input source
293
+ * @param {number} start name start
294
+ * @param {number} end name end (exclusive)
295
+ * @returns {string | undefined} canonical lowercase name, or undefined when unknown
296
+ */
297
+ const knownPropertyForRange = (index, input, start, end) => {
298
+ let h = end - start;
299
+ for (let i = start; i < end; i++) {
300
+ let c = input.charCodeAt(i);
301
+ if (c >= 65 && c <= 90) c |= 0x20;
302
+ h = ((h << 5) - h + c) | 0;
303
+ }
304
+ const hit = index.get(h);
305
+ if (hit === undefined) return undefined;
306
+ if (typeof hit === "string") {
307
+ return rangeEqualsLowerCase(input, start, end, hit) ? hit : undefined;
308
+ }
309
+ for (let i = 0; i < hit.length; i++) {
310
+ if (rangeEqualsLowerCase(input, start, end, hit[i])) return hit[i];
311
+ }
312
+ return undefined;
313
+ };
314
+
187
315
  /**
188
316
  * `binarySearchBounds` comparator for `getComments` — hoisted so the lookup
189
317
  * doesn't allocate a fresh closure per call.
@@ -598,7 +726,7 @@ const scanRuleBody = (declarations, childRules) => {
598
726
  hasNestedBlock = true;
599
727
  if (
600
728
  !hasDirectDecl &&
601
- !isPureBodyAtRule(`@${A.name(child).toLowerCase()}`) &&
729
+ !isPureBodyAtRule(`@${toLowerCaseIfNeeded(A.name(child))}`) &&
602
730
  scanRuleBody(atDecls, atChildRules).hasDirectDecl
603
731
  ) {
604
732
  hasDirectDecl = true;
@@ -916,6 +1044,8 @@ class CssParser extends Parser {
916
1044
  exportMode = CssIcssExportDependency.EXPORT_MODE.REPLACE,
917
1045
  exportType = CssIcssExportDependency.EXPORT_TYPE.NORMAL
918
1046
  ) => {
1047
+ // Flat location numbers — the nested loc objects were the parser's
1048
+ // hottest allocation (3 objects per exported name).
919
1049
  cssExportEntries.push({
920
1050
  name,
921
1051
  value,
@@ -923,7 +1053,10 @@ class CssParser extends Parser {
923
1053
  interpolate,
924
1054
  exportMode,
925
1055
  exportType,
926
- loc: { start: { line: sl, column: sc }, end: { line: el, column: ec } }
1056
+ locStartLine: sl,
1057
+ locStartColumn: sc,
1058
+ locEndLine: el,
1059
+ locEndColumn: ec
927
1060
  });
928
1061
  };
929
1062
 
@@ -940,12 +1073,29 @@ class CssParser extends Parser {
940
1073
 
941
1074
  const isModules = mode === "global" || mode === "local";
942
1075
 
1076
+ /** @type {Map<string, boolean>} */
1077
+ const selfReferenceCache = new Map();
1078
+
943
1079
  /**
944
1080
  * Whether a relative `from "<request>"` resolves back to the current module (matching query/fragment too).
1081
+ * Memoized per parse — `composes … from "./x.css"` repeats the same request many times per file.
945
1082
  * @param {string} request request string from `from "<request>"`
946
1083
  * @returns {boolean} true if request resolves to the current module
947
1084
  */
948
1085
  const isSelfReferenceRequest = (request) => {
1086
+ const cached = selfReferenceCache.get(request);
1087
+ if (cached !== undefined) return cached;
1088
+ const result = isSelfReferenceRequestUncached(request);
1089
+ selfReferenceCache.set(request, result);
1090
+ return result;
1091
+ };
1092
+
1093
+ /**
1094
+ * Uncached `isSelfReferenceRequest`.
1095
+ * @param {string} request request string from `from "<request>"`
1096
+ * @returns {boolean} true if request resolves to the current module
1097
+ */
1098
+ const isSelfReferenceRequestUncached = (request) => {
949
1099
  if (!RELATIVE_REQUEST.test(request)) return false;
950
1100
  if (!module.context) return false;
951
1101
  const parsedRequest = parseResource(request);
@@ -969,6 +1119,7 @@ class CssParser extends Parser {
969
1119
  customIdents: this.options.customIdents,
970
1120
  grid: this.options.grid
971
1121
  });
1122
+ const knownPropertyIndex = getKnownPropertyIndex(knownProperties);
972
1123
 
973
1124
  /** @type {CssModuleBuildMeta} */
974
1125
  (module.buildMeta).isCssModule = isModules;
@@ -1242,18 +1393,17 @@ class CssParser extends Parser {
1242
1393
  modeData ? modeData === "local" : mode === "local";
1243
1394
 
1244
1395
  /**
1245
- * Comment callback: push every comment-token (in source order) onto the local `comments`, read back by `advanceCommentCursor` (pure-mode flags) and `parseCommentOptions` (magic comments).
1246
- * @param {string} input input
1247
- * @param {number} start start
1248
- * @param {number} end end
1249
- * @returns {number} end
1396
+ * Comment visitor (`NodeType.Comment`): push every comment (in source order) onto the local `comments`, read back by `advanceCommentCursor` (pure-mode flags) and `parseCommentOptions` (magic comments).
1397
+ * @param {import("./syntax").CssPath} path walk path at the comment node
1250
1398
  */
1251
- const comment = (input, start, end) => {
1399
+ const commentVisitor = (path) => {
1400
+ const node = path.node;
1401
+ const start = A.start(node);
1402
+ const end = A.end(node);
1252
1403
  comments.push({
1253
- value: input.slice(start + 2, end - 2),
1404
+ value: source.slice(start + 2, end - 2),
1254
1405
  range: [start, end]
1255
1406
  });
1256
- return end;
1257
1407
  };
1258
1408
 
1259
1409
  /**
@@ -1516,10 +1666,9 @@ class CssParser extends Parser {
1516
1666
  for (const cv of A.children(fn)) {
1517
1667
  if (A.type(cv) !== NodeType.Ident) continue;
1518
1668
  let identifier = A.unescaped(cv);
1519
- const {
1520
- start: { line: sl, column: sc },
1521
- end: { line: el, column: ec }
1522
- } = A.loc(cv);
1669
+ // Cursor reads instead of `A.loc` — no location objects allocated.
1670
+ const { line: sl, column: sc } = locConverter.get(A.start(cv));
1671
+ const { line: el, column: ec } = locConverter.get(A.end(cv));
1523
1672
  const isDashedIdent = isDashedIdentifier(identifier);
1524
1673
  if (isDashedIdent) identifier = identifier.slice(2);
1525
1674
  addCssExport(
@@ -1560,10 +1709,8 @@ class CssParser extends Parser {
1560
1709
  if (cvType === NodeType.String) {
1561
1710
  if (!found && options.string) {
1562
1711
  const value = A.unescaped(cv);
1563
- const {
1564
- start: { line: sl, column: sc },
1565
- end: { line: el, column: ec }
1566
- } = A.loc(cv);
1712
+ const { line: sl, column: sc } = locConverter.get(A.start(cv));
1713
+ const { line: el, column: ec } = locConverter.get(A.end(cv));
1567
1714
  addCssExport(
1568
1715
  sl,
1569
1716
  sc,
@@ -1590,10 +1737,8 @@ class CssParser extends Parser {
1590
1737
  ) {
1591
1738
  continue;
1592
1739
  }
1593
- const {
1594
- start: { line: sl, column: sc },
1595
- end: { line: el, column: ec }
1596
- } = A.loc(cv);
1740
+ const { line: sl, column: sc } = locConverter.get(A.start(cv));
1741
+ const { line: el, column: ec } = locConverter.get(A.end(cv));
1597
1742
  addCssExport(
1598
1743
  sl,
1599
1744
  sc,
@@ -1614,9 +1759,12 @@ class CssParser extends Parser {
1614
1759
 
1615
1760
  if (cvType === NodeType.Function) {
1616
1761
  const fn = /** @type {FunctionNode} */ (cv);
1617
- const fname = A.unescapedName(fn).toLowerCase();
1618
- const type =
1619
- fname === "local" ? 1 : fname === "global" ? 2 : undefined;
1762
+ const fname = A.unescapedName(fn);
1763
+ const type = equalsLowerCase(fname, "local")
1764
+ ? 1
1765
+ : equalsLowerCase(fname, "global")
1766
+ ? 2
1767
+ : undefined;
1620
1768
  if (!found && type) {
1621
1769
  found = true;
1622
1770
  if (type === 1) pure.markLocal();
@@ -1626,7 +1774,7 @@ class CssParser extends Parser {
1626
1774
  if (
1627
1775
  this.options.dashedIdents &&
1628
1776
  isLocalMode() &&
1629
- (fname === "var" || fname === "style")
1777
+ (equalsLowerCase(fname, "var") || equalsLowerCase(fname, "style"))
1630
1778
  ) {
1631
1779
  processDashedIdentInVarFunction(fn);
1632
1780
  }
@@ -1758,7 +1906,7 @@ class CssParser extends Parser {
1758
1906
  if (
1759
1907
  j >= fv.length ||
1760
1908
  A.type(fv[j]) !== NodeType.Ident ||
1761
- !equalsLowerCase(A.value(fv[j]), "from")
1909
+ !rangeEqualsLowerCase(input, A.start(fv[j]), A.end(fv[j]), "from")
1762
1910
  ) {
1763
1911
  emitDashedIdentExport(identStart, identEnd);
1764
1912
  return;
@@ -1772,7 +1920,10 @@ class CssParser extends Parser {
1772
1920
  if (j >= fv.length) return;
1773
1921
 
1774
1922
  const src = fv[j];
1775
- if (A.type(src) === NodeType.Ident && A.value(src) === "global") {
1923
+ if (
1924
+ A.type(src) === NodeType.Ident &&
1925
+ rangeEquals(input, A.start(src), A.end(src), "global")
1926
+ ) {
1776
1927
  emitDashedIdentFromGlobal(identEnd, A.end(src));
1777
1928
  return;
1778
1929
  }
@@ -2081,10 +2232,8 @@ class CssParser extends Parser {
2081
2232
  // ID selectors emit the ICSS export but aren't a `composes:` anchor.
2082
2233
  const idValueStart = A.start(v) + 1;
2083
2234
  const idName = A.unescaped(v);
2084
- const {
2085
- start: { line: idSl, column: idSc },
2086
- end: { line: idEl, column: idEc }
2087
- } = A.loc(v);
2235
+ const { line: idSl, column: idSc } = locConverter.get(A.start(v));
2236
+ const { line: idEl, column: idEc } = locConverter.get(A.end(v));
2088
2237
  addCssExport(
2089
2238
  idSl,
2090
2239
  idSc,
@@ -2124,10 +2273,9 @@ class CssParser extends Parser {
2124
2273
  if (segmentMode === "local") {
2125
2274
  // `.<ident>` in local mode is a class selector (dep covers the ident bytes only).
2126
2275
  const name = A.unescaped(next);
2127
- const {
2128
- start: { line: sl, column: sc },
2129
- end: { line: el, column: ec }
2130
- } = A.loc(next);
2276
+ // Cursor reads instead of `A.loc` — this is the hottest export site.
2277
+ const { line: sl, column: sc } = locConverter.get(A.start(next));
2278
+ const { line: el, column: ec } = locConverter.get(A.end(next));
2131
2279
  addCssExport(
2132
2280
  sl,
2133
2281
  sc,
@@ -2142,7 +2290,7 @@ class CssParser extends Parser {
2142
2290
  currentRule.hasLocalAnchor = true;
2143
2291
  currentRule.localIdentifiers.push(name);
2144
2292
  pure.markLocal();
2145
- } else {
2293
+ } else if (icssDefinitions.size !== 0) {
2146
2294
  // `.<ident>` in global mode: not localized, but the ident may be `@value`-defined and need ICSS rewrite.
2147
2295
  const ident = A.value(next);
2148
2296
  if (!isDashedIdentifier(ident) && icssDefinitions.has(ident)) {
@@ -2154,7 +2302,8 @@ class CssParser extends Parser {
2154
2302
  break;
2155
2303
  }
2156
2304
  case NodeType.Ident: {
2157
- // ICSS rewrite for a bare `@value`-defined ident used as a type-style selector.
2305
+ // ICSS rewrite for a bare `@value`-defined ident used as a type-style selector; only worth the slice when definitions exist.
2306
+ if (icssDefinitions.size === 0) break;
2158
2307
  const ident = A.value(v);
2159
2308
  if (!isDashedIdentifier(ident) && icssDefinitions.has(ident)) {
2160
2309
  emitICSSSymbol(ident, A.start(v), A.end(v));
@@ -2181,10 +2330,8 @@ class CssParser extends Parser {
2181
2330
  const urlActive = () => {
2182
2331
  if (!this.options.url || !currentStructural) return false;
2183
2332
  if (A.type(currentStructural) === NodeType.AtRule) {
2184
- return (
2185
- !equalsLowerCase(A.name(currentStructural), "import") ||
2186
- currentUrlRecovery
2187
- );
2333
+ // `currentAtRuleName` is cached on at-rule enter — no per-value slice.
2334
+ return currentAtRuleName !== "@import" || currentUrlRecovery;
2188
2335
  }
2189
2336
  return true;
2190
2337
  };
@@ -2534,10 +2681,10 @@ class CssParser extends Parser {
2534
2681
  /**
2535
2682
  * Emit url() deps for a `url(...)` / `src(...)` / `image-set(...)` value function.
2536
2683
  * @param {FunctionNode} fn the function node
2537
- * @param {string} fname the function name, lower-cased
2684
+ * @param {string} fname the unescaped function name (any case)
2538
2685
  */
2539
2686
  const emitUrlFunction = (fn, fname) => {
2540
- if (fname === "url" || fname === "src") {
2687
+ if (equalsLowerCase(fname, "url") || equalsLowerCase(fname, "src")) {
2541
2688
  // Quoted `url("…")` / `src("…")`: first non-whitespace value must be the string token.
2542
2689
  const first = A.children(fn)[nextNonWhitespace(A.children(fn), 0)];
2543
2690
  if (!first || A.type(first) !== NodeType.String) return;
@@ -2994,7 +3141,8 @@ class CssParser extends Parser {
2994
3141
  break;
2995
3142
  }
2996
3143
  const identifier = A.value(cv);
2997
- const keyword = identifier.toLowerCase();
3144
+ // Values are almost always lowercase already — avoid the copy.
3145
+ const keyword = toLowerCaseIfNeeded(identifier);
2998
3146
  parsedKeywords[keyword] =
2999
3147
  typeof parsedKeywords[keyword] !== "undefined"
3000
3148
  ? parsedKeywords[keyword] + 1
@@ -3101,11 +3249,13 @@ class CssParser extends Parser {
3101
3249
  // Drive the walk through SourceProcessor: structural enter / exit map to the `walkAst…Enter` / `…Exit` halves; value visitors handle url / ICSS / local-global.
3102
3250
  /** @type {VisitorMap} */
3103
3251
  const visitors = {
3252
+ [NodeType.Comment]: commentVisitor,
3104
3253
  [NodeType.AtRule]: {
3105
3254
  // At-rule enter: scope save, name dispatch, prelude value context, pure-block push.
3106
- enter: (node, parent) => {
3255
+ enter: (path) => {
3256
+ const node = path.node;
3107
3257
  const at = /** @type {AtRule} */ (node);
3108
- const topLevel = parent === null;
3258
+ const topLevel = path.parent === null;
3109
3259
  currentUrlRecovery = false;
3110
3260
  advanceCommentCursor(A.start(at));
3111
3261
  const savedAnchor = currentRule.hasLocalAnchor;
@@ -3115,7 +3265,7 @@ class CssParser extends Parser {
3115
3265
  currentRule.localIdentifiers = isModules
3116
3266
  ? [...savedLocalIdentifiers]
3117
3267
  : savedLocalIdentifiers;
3118
- const name = `@${A.name(at).toLowerCase()}`;
3268
+ const name = `@${toLowerCaseIfNeeded(A.name(at))}`;
3119
3269
  switch (name) {
3120
3270
  case "@namespace": {
3121
3271
  this._emitWarning(
@@ -3264,7 +3414,8 @@ class CssParser extends Parser {
3264
3414
  });
3265
3415
  },
3266
3416
  // At-rule exit: pure-frame finalization, `suppressNextRulePrelude`, scope restore, top-level reset.
3267
- exit: (node, parent) => {
3417
+ exit: (path) => {
3418
+ const node = path.node;
3268
3419
  const state = atRuleStateStack.pop();
3269
3420
  if (!state) return;
3270
3421
  if (state.hasBlock) {
@@ -3279,14 +3430,15 @@ class CssParser extends Parser {
3279
3430
  }
3280
3431
  currentRule.hasLocalAnchor = state.savedAnchor;
3281
3432
  currentRule.localIdentifiers = state.savedLocalIdentifiers;
3282
- if (parent === null) finishTopLevelRule(node, true);
3433
+ if (path.parent === null) finishTopLevelRule(node, true);
3283
3434
  }
3284
3435
  },
3285
3436
  [NodeType.QualifiedRule]: {
3286
- // Qualified-rule enter: scope setup, selector + prelude context, pure-block push; `:import` / `:export` bail via `ctx.skipChildren()`.
3287
- enter: (node, parent, ctx) => {
3437
+ // Qualified-rule enter: scope setup, selector + prelude context, pure-block push; `:import` / `:export` bail via `path.skipChildren()`.
3438
+ enter: (path) => {
3439
+ const node = path.node;
3288
3440
  const rule = /** @type {QualifiedRule} */ (node);
3289
- const topLevel = parent === null;
3441
+ const topLevel = path.parent === null;
3290
3442
  advanceCommentCursor(A.start(rule));
3291
3443
  // `:import(…) { … }` / `:export { … }` ICSS pseudo-rules are processed inline at top level; nested ones bail out.
3292
3444
  if (isModules) {
@@ -3325,7 +3477,7 @@ class CssParser extends Parser {
3325
3477
  A.setEnd(rule, A.blockEnd(rule));
3326
3478
  }
3327
3479
  // Don't recurse into the body — handled inline above.
3328
- if (ctx) ctx.skipChildren();
3480
+ path.skipChildren();
3329
3481
  qualifiedRuleStateStack.push({ bailed: true });
3330
3482
  return;
3331
3483
  }
@@ -3400,7 +3552,8 @@ class CssParser extends Parser {
3400
3552
  });
3401
3553
  },
3402
3554
  // Qualified-rule exit: pure-frame finalization, scope restore, top-level reset; no-op for bailed ICSS.
3403
- exit: (node, parent) => {
3555
+ exit: (path) => {
3556
+ const node = path.node;
3404
3557
  const state = qualifiedRuleStateStack.pop();
3405
3558
  if (!state || state.bailed) return;
3406
3559
  pure.exitBlock();
@@ -3408,11 +3561,12 @@ class CssParser extends Parser {
3408
3561
  currentRule.localIdentifiers = state.savedLocalIdentifiers;
3409
3562
  currentRule.composesPrevFile = state.savedPrevComposesFile;
3410
3563
  currentRule.composesFiles = state.savedComposesFiles;
3411
- if (parent === null) finishTopLevelRule(node, false);
3564
+ if (path.parent === null) finishTopLevelRule(node, false);
3412
3565
  }
3413
3566
  },
3414
3567
  // Top-level declarations are parse errors (dropped by `parseAStylesheet`), so a declaration's parent is always a block.
3415
- [NodeType.Declaration]: (node) => {
3568
+ [NodeType.Declaration]: (path) => {
3569
+ const node = path.node;
3416
3570
  const decl = /** @type {Declaration} */ (node);
3417
3571
  // Reset value-visitor context, read by the value visitors below.
3418
3572
  currentStructural = decl;
@@ -3432,20 +3586,47 @@ class CssParser extends Parser {
3432
3586
  // Property-name analysis and value exports are CSS-Modules-only; a plain
3433
3587
  // stylesheet's declarations need no per-decl name slice / known-property lookup.
3434
3588
  if (!isModules) return;
3435
- const declName = A.name(decl);
3436
- const declPropertyName = declName.replace(/^(-\w+-)/, "").toLowerCase();
3437
- // Cache the known-property flag so the per-token value visitors don't recompute it.
3438
- currentDeclIsKnownProperty = knownProperties.has(declPropertyName);
3589
+ const nameStart = A.nameStart(decl);
3590
+ const nameEnd = A.nameEnd(decl);
3591
+ // Range-based name analysis the common declaration never
3592
+ // slices its name out of the source.
3593
+ /** @type {string | undefined} */
3594
+ let declName;
3595
+ /** @type {string | undefined} */
3596
+ let declPropertyName;
3597
+ if (source.charCodeAt(nameStart) === CC_HYPHEN_MINUS) {
3598
+ // Vendor-prefixed or custom property — rare, take the string path.
3599
+ declName = A.name(decl);
3600
+ declPropertyName = toLowerCaseIfNeeded(
3601
+ declName.replace(VENDOR_PREFIX, "")
3602
+ );
3603
+ currentDeclIsKnownProperty = knownProperties.has(declPropertyName);
3604
+ } else {
3605
+ declPropertyName = knownPropertyForRange(
3606
+ knownPropertyIndex,
3607
+ source,
3608
+ nameStart,
3609
+ nameEnd
3610
+ );
3611
+ // Cache the known-property flag so the per-token value visitors don't recompute it.
3612
+ currentDeclIsKnownProperty = declPropertyName !== undefined;
3613
+ }
3439
3614
  const effectiveLocalMode = isEffectivelyLocal();
3440
3615
  // `composes:` with a local anchor: its strip-dep covers the whole declaration, so suppress the value's local/global/dashed/ICSS rewrites.
3441
3616
  currentDeclComposesSkip =
3442
3617
  currentRule.hasLocalAnchor &&
3443
- COMPOSES_PROPERTY.test(declPropertyName);
3618
+ (declPropertyName !== undefined
3619
+ ? COMPOSES_PROPERTY.test(declPropertyName)
3620
+ : rangeEqualsLowerCase(source, nameStart, nameEnd, "composes") ||
3621
+ rangeEqualsLowerCase(source, nameStart, nameEnd, "compose-with"));
3444
3622
  if (currentDeclComposesSkip) emitComposesWithAnchor(decl);
3445
3623
  const skipForComposes = currentDeclComposesSkip;
3446
3624
  // Known-property value localization (`animation-name: foo` exports `foo`).
3447
3625
  if (effectiveLocalMode && currentDeclIsKnownProperty) {
3448
- emitKnownPropertyExports(decl, declPropertyName);
3626
+ emitKnownPropertyExports(
3627
+ decl,
3628
+ /** @type {string} */ (declPropertyName)
3629
+ );
3449
3630
  }
3450
3631
  // 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).
3451
3632
  if (
@@ -3453,24 +3634,33 @@ class CssParser extends Parser {
3453
3634
  effectiveLocalMode &&
3454
3635
  !skipForComposes
3455
3636
  ) {
3456
- if (isDashedIdentifier(declName)) {
3457
- emitDashedIdentExport(A.nameStart(decl), A.nameEnd(decl));
3637
+ // Only the `-`-prefixed path can carry a dashed ident.
3638
+ if (declName !== undefined && isDashedIdentifier(declName)) {
3639
+ emitDashedIdentExport(nameStart, nameEnd);
3458
3640
  }
3459
3641
  dashed.active = true;
3460
3642
  dashed.emit = !currentDeclIsKnownProperty;
3461
3643
  }
3462
- // ICSS-symbol rewrite (`color: foo` when `foo` is `@value`-defined), skipping known properties, the composes anchor, and dashed idents (handled above).
3644
+ // 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.
3463
3645
  if (
3464
3646
  !skipForComposes &&
3465
3647
  !currentDeclIsKnownProperty &&
3466
- !(dashed.active && isDashedIdentifier(declName)) &&
3467
- icssDefinitions.has(declName)
3648
+ icssDefinitions.size !== 0
3468
3649
  ) {
3469
- emitICSSSymbol(declName, A.nameStart(decl), A.nameEnd(decl));
3650
+ if (declName === undefined) {
3651
+ declName = source.slice(nameStart, nameEnd);
3652
+ }
3653
+ if (
3654
+ !(dashed.active && isDashedIdentifier(declName)) &&
3655
+ icssDefinitions.has(declName)
3656
+ ) {
3657
+ emitICSSSymbol(declName, nameStart, nameEnd);
3658
+ }
3470
3659
  }
3471
3660
  },
3472
3661
  // Value-level visitors decide handling from the enclosing node via `urlActive()` / `localGlobalActive()` / `icssActive()`.
3473
- [NodeType.Url]: (node) => {
3662
+ [NodeType.Url]: (path) => {
3663
+ const node = path.node;
3474
3664
  const url = /** @type {UrlToken} */ (node);
3475
3665
  if (!urlActive()) return;
3476
3666
  // Skip bare url-tokens for a known property in CSS-Modules local mode.
@@ -3530,25 +3720,27 @@ class CssParser extends Parser {
3530
3720
  module.addDependency(dep);
3531
3721
  module.addCodeGenerationDependency(dep);
3532
3722
  },
3533
- [NodeType.Comma](node) {
3723
+ [NodeType.Comma](path) {
3724
+ const node = path.node;
3534
3725
  if (urlActive()) lastTokenEndForComments = A.start(node);
3535
3726
  },
3536
3727
  [NodeType.Function]: {
3537
- enter: (node) => {
3728
+ enter: (path) => {
3729
+ const node = path.node;
3538
3730
  const fn = /** @type {FunctionNode} */ (node);
3539
3731
  const fnameRaw = A.unescapedName(fn);
3540
- const fname = fnameRaw.toLowerCase();
3541
- if (urlActive()) emitUrlFunction(fn, fname);
3542
- if (
3543
- localGlobalActive() &&
3544
- (fname === "local" || fname === "global")
3545
- ) {
3546
- processLocalOrGlobalFunction(fn, fname === "local" ? 1 : 2);
3732
+ // `equalsLowerCase` keeps this visitor free of a per-function
3733
+ // lowercased copy — functions are the densest value nodes.
3734
+ const isLocalFn = equalsLowerCase(fnameRaw, "local");
3735
+ const isGlobalFn = !isLocalFn && equalsLowerCase(fnameRaw, "global");
3736
+ if (urlActive()) emitUrlFunction(fn, fnameRaw);
3737
+ if (localGlobalActive() && (isLocalFn || isGlobalFn)) {
3738
+ processLocalOrGlobalFunction(fn, isLocalFn ? 1 : 2);
3547
3739
  }
3548
3740
  if (
3549
3741
  icssActive() &&
3550
- fname !== "local" &&
3551
- fname !== "global" &&
3742
+ !isLocalFn &&
3743
+ !isGlobalFn &&
3552
3744
  !(dashed.active && isDashedIdentifier(fnameRaw)) &&
3553
3745
  icssDefinitions.has(fnameRaw)
3554
3746
  ) {
@@ -3557,10 +3749,13 @@ class CssParser extends Parser {
3557
3749
  // Dashed-ident scoping: handle this function, then set the child nesting level's state for the walk.
3558
3750
  dashed.push();
3559
3751
  if (dashed.active) {
3560
- if (fname === "local" || fname === "global") {
3752
+ if (isLocalFn || isGlobalFn) {
3561
3753
  // `local()` / `global()` dashed args go through the ICSS path above, not here.
3562
3754
  dashed.active = false;
3563
- } else if (fname === "var" || fname === "style") {
3755
+ } else if (
3756
+ equalsLowerCase(fnameRaw, "var") ||
3757
+ equalsLowerCase(fnameRaw, "style")
3758
+ ) {
3564
3759
  // `var(--foo, …)` / `style(--foo, …)`: emit the first ident; the fallback doesn't self-emit.
3565
3760
  processDashedIdentInVarFunction(fn);
3566
3761
  dashed.emit = false;
@@ -3574,34 +3769,46 @@ class CssParser extends Parser {
3574
3769
  dashed.pop();
3575
3770
  }
3576
3771
  },
3577
- [NodeType.Ident](node, parent) {
3772
+ [NodeType.Ident](path) {
3773
+ const node = path.node;
3578
3774
  // Fast exit before slicing the ident value: outside dashed-ident scoping
3579
3775
  // and ICSS context (any non-CSS-Modules stylesheet) a bare ident carries
3580
3776
  // no work, and idents are the most common node, so skipping the per-ident
3581
- // `value` slice matters.
3777
+ // `value` slice matters. With no `@value`/`:import` definitions the ICSS
3778
+ // probe can never hit, so it doesn't warrant the slice either.
3582
3779
  const dashedActive = dashed.active;
3583
- const icss = icssActive();
3780
+ const icss = icssActive() && icssDefinitions.size !== 0;
3584
3781
  if (!dashedActive && !icss) return;
3585
3782
  const identValue = A.value(node);
3586
3783
  if (dashedActive && isDashedIdentifier(identValue)) {
3587
3784
  // Dashed idents are scoped here, never `@value` ICSS-rewritten.
3588
3785
  if (!dashed.emit) return;
3589
3786
  // Resolve the `--foo from "./x.css"` / `--foo from global` import suffix via sibling lookahead.
3590
- const siblings = childrenOf(parent);
3787
+ const siblings = childrenOf(path.parent);
3591
3788
  const i = siblings ? siblings.indexOf(node) : -1;
3592
3789
  if (siblings && i !== -1) {
3593
3790
  const j = nextNonWhitespace(siblings, i + 1);
3594
3791
  if (
3595
3792
  j < siblings.length &&
3596
3793
  A.type(siblings[j]) === NodeType.Ident &&
3597
- equalsLowerCase(A.value(siblings[j]), "from")
3794
+ rangeEqualsLowerCase(
3795
+ input,
3796
+ A.start(siblings[j]),
3797
+ A.end(siblings[j]),
3798
+ "from"
3799
+ )
3598
3800
  ) {
3599
3801
  const fromIdent = siblings[j];
3600
3802
  const sourceNode = siblings[nextNonWhitespace(siblings, j + 1)];
3601
3803
  if (
3602
3804
  sourceNode &&
3603
3805
  A.type(sourceNode) === NodeType.Ident &&
3604
- A.value(sourceNode) === "global"
3806
+ rangeEquals(
3807
+ input,
3808
+ A.start(sourceNode),
3809
+ A.end(sourceNode),
3810
+ "global"
3811
+ )
3605
3812
  ) {
3606
3813
  emitDashedIdentFromGlobal(A.end(node), A.end(sourceNode));
3607
3814
  return;
@@ -3628,14 +3835,17 @@ class CssParser extends Parser {
3628
3835
  }
3629
3836
  };
3630
3837
  // `as` selects the top-level production (§5.3): a `style` attribute is a
3631
- // block's contents, everything else a full stylesheet (the default).
3632
- new SourceProcessor()
3838
+ // block's contents, everything else a full stylesheet (the default). `as`
3839
+ // and `skip` are per-parse config on the processor; only `locConverter`
3840
+ // (per-source) is passed to `process`. One skip set: selector prelude +
3841
+ // unread value leaves (non-modules only; CSS Modules needs its selectors
3842
+ // and ICSS-captured values).
3843
+ new SourceProcessor({
3844
+ as: /** @type {"stylesheet" | "block-contents"} */ (this.options.as),
3845
+ skip: isModules ? undefined : SKIP_NON_MODULES
3846
+ })
3633
3847
  .use(/** @type {VisitorMap} */ (visitors))
3634
- .process(source, {
3635
- locConverter,
3636
- comment,
3637
- as: /** @type {"stylesheet" | "block-contents"} */ (this.options.as)
3638
- });
3848
+ .process(source, { locConverter });
3639
3849
 
3640
3850
  /** @type {BuildInfo} */
3641
3851
  (module.buildInfo).strict = true;