CssParser.js 167 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486248724882489249024912492249324942495249624972498249925002501250225032504250525062507250825092510251125122513251425152516251725182519252025212522252325242525252625272528252925302531253225332534253525362537253825392540254125422543254425452546254725482549255025512552255325542555255625572558255925602561256225632564256525662567256825692570257125722573257425752576257725782579258025812582258325842585258625872588258925902591259225932594259525962597259825992600260126022603260426052606260726082609261026112612261326142615261626172618261926202621262226232624262526262627262826292630263126322633263426352636263726382639264026412642264326442645264626472648264926502651265226532654265526562657265826592660266126622663266426652666266726682669267026712672267326742675267626772678267926802681268226832684268526862687268826892690269126922693269426952696269726982699270027012702270327042705270627072708270927102711271227132714271527162717271827192720272127222723272427252726272727282729273027312732273327342735273627372738273927402741274227432744274527462747274827492750275127522753275427552756275727582759276027612762276327642765276627672768276927702771277227732774277527762777277827792780278127822783278427852786278727882789279027912792279327942795279627972798279928002801280228032804280528062807280828092810281128122813281428152816281728182819282028212822282328242825282628272828282928302831283228332834283528362837283828392840284128422843284428452846284728482849285028512852285328542855285628572858285928602861286228632864286528662867286828692870287128722873287428752876287728782879288028812882288328842885288628872888288928902891289228932894289528962897289828992900290129022903290429052906290729082909291029112912291329142915291629172918291929202921292229232924292529262927292829292930293129322933293429352936293729382939294029412942294329442945294629472948294929502951295229532954295529562957295829592960296129622963296429652966296729682969297029712972297329742975297629772978297929802981298229832984298529862987298829892990299129922993299429952996299729982999300030013002300330043005300630073008300930103011301230133014301530163017301830193020302130223023302430253026302730283029303030313032303330343035303630373038303930403041304230433044304530463047304830493050305130523053305430553056305730583059306030613062306330643065306630673068306930703071307230733074307530763077307830793080308130823083308430853086308730883089309030913092309330943095309630973098309931003101310231033104310531063107310831093110311131123113311431153116311731183119312031213122312331243125312631273128312931303131313231333134313531363137313831393140314131423143314431453146314731483149315031513152315331543155315631573158315931603161316231633164316531663167316831693170317131723173317431753176317731783179318031813182318331843185318631873188318931903191319231933194319531963197319831993200320132023203320432053206320732083209321032113212321332143215321632173218321932203221322232233224322532263227322832293230323132323233323432353236323732383239324032413242324332443245324632473248324932503251325232533254325532563257325832593260326132623263326432653266326732683269327032713272327332743275327632773278327932803281328232833284328532863287328832893290329132923293329432953296329732983299330033013302330333043305330633073308330933103311331233133314331533163317331833193320332133223323332433253326332733283329333033313332333333343335333633373338333933403341334233433344334533463347334833493350335133523353335433553356335733583359336033613362336333643365336633673368336933703371337233733374337533763377337833793380338133823383338433853386338733883389339033913392339333943395339633973398339934003401340234033404340534063407340834093410341134123413341434153416341734183419342034213422342334243425342634273428342934303431343234333434343534363437343834393440344134423443344434453446344734483449345034513452345334543455345634573458345934603461346234633464346534663467346834693470347134723473347434753476347734783479348034813482348334843485348634873488348934903491349234933494349534963497349834993500350135023503350435053506350735083509351035113512351335143515351635173518351935203521352235233524352535263527352835293530353135323533353435353536353735383539354035413542354335443545354635473548354935503551355235533554355535563557355835593560356135623563356435653566356735683569357035713572357335743575357635773578357935803581358235833584358535863587358835893590359135923593359435953596359735983599360036013602360336043605360636073608360936103611361236133614361536163617361836193620362136223623362436253626362736283629363036313632363336343635363636373638363936403641364236433644364536463647364836493650365136523653365436553656365736583659366036613662366336643665366636673668366936703671367236733674367536763677367836793680368136823683368436853686368736883689369036913692369336943695369636973698369937003701370237033704370537063707370837093710371137123713371437153716371737183719372037213722372337243725372637273728372937303731373237333734373537363737373837393740374137423743374437453746374737483749375037513752375337543755375637573758375937603761376237633764376537663767376837693770377137723773377437753776377737783779378037813782378337843785378637873788378937903791379237933794379537963797379837993800380138023803380438053806380738083809381038113812381338143815381638173818381938203821382238233824382538263827382838293830383138323833383438353836383738383839384038413842384338443845384638473848384938503851385238533854385538563857385838593860386138623863386438653866386738683869387038713872387338743875387638773878387938803881388238833884388538863887388838893890389138923893389438953896389738983899390039013902390339043905390639073908390939103911391239133914391539163917391839193920392139223923392439253926392739283929393039313932393339343935393639373938393939403941394239433944394539463947394839493950395139523953395439553956395739583959396039613962396339643965396639673968396939703971397239733974397539763977397839793980398139823983398439853986398739883989399039913992399339943995399639973998399940004001400240034004400540064007400840094010401140124013401440154016401740184019402040214022402340244025402640274028402940304031403240334034403540364037403840394040404140424043404440454046404740484049405040514052405340544055405640574058405940604061406240634064406540664067406840694070407140724073407440754076407740784079408040814082408340844085408640874088408940904091409240934094409540964097409840994100410141024103410441054106410741084109411041114112411341144115411641174118411941204121412241234124412541264127412841294130413141324133413441354136413741384139414041414142414341444145414641474148414941504151415241534154415541564157415841594160416141624163416441654166416741684169417041714172417341744175417641774178417941804181418241834184418541864187418841894190419141924193419441954196419741984199420042014202420342044205420642074208420942104211421242134214421542164217421842194220422142224223422442254226422742284229423042314232423342344235423642374238423942404241424242434244424542464247424842494250425142524253425442554256425742584259426042614262426342644265426642674268426942704271427242734274427542764277427842794280428142824283428442854286428742884289429042914292429342944295429642974298429943004301430243034304430543064307430843094310431143124313431443154316431743184319432043214322432343244325432643274328432943304331433243334334433543364337433843394340434143424343434443454346434743484349435043514352435343544355435643574358435943604361436243634364436543664367436843694370437143724373437443754376437743784379438043814382438343844385438643874388438943904391439243934394439543964397439843994400440144024403440444054406440744084409441044114412441344144415441644174418441944204421442244234424442544264427442844294430443144324433443444354436443744384439444044414442444344444445444644474448444944504451445244534454445544564457445844594460446144624463446444654466446744684469447044714472447344744475447644774478447944804481448244834484448544864487448844894490449144924493449444954496449744984499450045014502450345044505450645074508450945104511451245134514451545164517451845194520452145224523452445254526452745284529453045314532453345344535453645374538453945404541454245434544454545464547454845494550455145524553455445554556455745584559456045614562456345644565456645674568456945704571457245734574457545764577457845794580458145824583458445854586458745884589459045914592459345944595459645974598459946004601460246034604460546064607460846094610461146124613461446154616461746184619462046214622462346244625462646274628462946304631463246334634463546364637463846394640464146424643464446454646464746484649465046514652465346544655465646574658465946604661466246634664466546664667466846694670467146724673467446754676467746784679468046814682468346844685468646874688468946904691469246934694469546964697469846994700470147024703470447054706470747084709471047114712471347144715471647174718471947204721472247234724472547264727472847294730473147324733473447354736473747384739474047414742474347444745474647474748474947504751475247534754475547564757475847594760476147624763476447654766476747684769477047714772477347744775477647774778477947804781478247834784478547864787478847894790479147924793479447954796479747984799480048014802480348044805480648074808480948104811481248134814481548164817481848194820482148224823482448254826482748284829483048314832483348344835483648374838483948404841484248434844484548464847484848494850
  1. /*
  2. MIT License http://www.opensource.org/licenses/mit-license.php
  3. Author Tobias Koppers @sokra
  4. */
  5. "use strict";
  6. const path = require("path");
  7. const { CSS_MODULE_TYPE_AUTO } = require("../ModuleTypeConstants");
  8. const Parser = require("../Parser");
  9. const ConstDependency = require("../dependencies/ConstDependency");
  10. const CssIcssExportDependency = require("../dependencies/CssIcssExportDependency");
  11. const CssIcssImportDependency = require("../dependencies/CssIcssImportDependency");
  12. const CssIcssSymbolDependency = require("../dependencies/CssIcssSymbolDependency");
  13. const CssImportDependency = require("../dependencies/CssImportDependency");
  14. const CssUrlDependency = require("../dependencies/CssUrlDependency");
  15. const StaticExportsDependency = require("../dependencies/StaticExportsDependency");
  16. const WebpackError = require("../errors/WebpackError");
  17. const ResourceHintPlugin = require("../prefetch/ResourceHintPlugin");
  18. const LocConverter = require("../util/LocConverter");
  19. const { parseResource } = require("../util/identifier");
  20. const {
  21. createMagicCommentContext,
  22. parseCommentOptionsInRange
  23. } = require("../util/magicComment");
  24. const memoize = require("../util/memoize");
  25. const topologicalSort = require("../util/topologicalSort");
  26. const { CSS_MODULES_KEYWORDS, CSS_MODULES_KEYWORD_OPTIONS } = require("./data");
  27. const {
  28. A,
  29. NodeType,
  30. SourceProcessor,
  31. buildSkipSet,
  32. equalsLowerCase,
  33. isDashedIdentifier,
  34. isWhitespace,
  35. normalizeUrl,
  36. rangeEquals,
  37. rangeEqualsLowerCase,
  38. skipEscape,
  39. toLowerCaseIfNeeded,
  40. unescapeIdentifier
  41. } = require("./syntax");
  42. const getUnsupportedFeatureWarning = memoize(() =>
  43. require("../errors/UnsupportedFeatureWarning")
  44. );
  45. const getModuleDependencyWarning = memoize(() =>
  46. require("../errors/ModuleDependencyWarning")
  47. );
  48. const getCommentCompilationWarning = memoize(() =>
  49. require("../errors/CommentCompilationWarning")
  50. );
  51. // `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.
  52. /** @import { BuildInfo, BuildMeta } from "../Module" */
  53. /**
  54. * @import CssModule, {
  55. * CssModuleBuildInfo,
  56. * CssModuleBuildMeta,
  57. * Inheritance
  58. * } from "./CssModule"
  59. */
  60. /** @import { ParserState, PreparsedAst } from "../Parser" */
  61. /**
  62. * @import {
  63. * AtRule,
  64. * Declaration,
  65. * FunctionNode,
  66. * Node as AstNode,
  67. * QualifiedRule,
  68. * Rule,
  69. * SimpleBlock,
  70. * Token,
  71. * UrlToken,
  72. * VisitorMap
  73. * } from "./syntax"
  74. */
  75. /**
  76. * @import {
  77. * CssAutoOrModuleParserOptions
  78. * } from "../../declarations/WebpackOptions"
  79. */
  80. /** @typedef {[number, number]} Range */
  81. /** @typedef {{ line: number, column: number }} Position */
  82. /** @typedef {{ from: string, items: ({ localName: string, importName: string })[] }} ValueAtRuleImport */
  83. /** @typedef {{ localName: string, value: string }} ValueAtRuleValue */
  84. /**
  85. * What a `@custom-media` name resolves to. `condition` substitutes into any
  86. * `<media-in-parens>` slot, `type` only at the start of a query, `boolean` is the
  87. * spec's `true` / `false` (folded into the enclosing query rather than written
  88. * out — no `<media-in-parens>` is unconditionally true on every engine).
  89. * @typedef {{ kind: "condition" | "type", text: string } | { kind: "boolean", value: boolean } | { kind: "unsupported" }} CustomMediaValue
  90. */
  91. /** @typedef {CustomMediaValue | { kind: "alias", name: string } | { kind: "or", parts: ({ text: string } | { alias: string })[] }} CustomMediaDefinition */
  92. /** @typedef {{ name: string, start: number, end: number, invalid: boolean, leading: boolean }} CustomMediaUse */
  93. /**
  94. * A `@media` prelude captured as its boolean shape, so a `true` / `false`
  95. * definition found later can be folded into it. Node ids are recycled per
  96. * top-level rule, so this holds text and offsets rather than AST nodes.
  97. * @typedef {{ kind: "text", text: string } | { kind: "ref", text: string, use: CustomMediaUse } | { kind: "group", operand: MediaNode } | { kind: "not", operand: MediaNode } | { kind: "chain", isOr: boolean, terms: MediaNode[] } | { kind: "typed", text: string, rest: MediaNode | null }} MediaNode
  98. */
  99. /** @type {CustomMediaValue} */
  100. const CUSTOM_MEDIA_UNSUPPORTED = { kind: "unsupported" };
  101. /** @type {CustomMediaValue} */
  102. const CUSTOM_MEDIA_TRUE = { kind: "boolean", value: true };
  103. /** @type {CustomMediaValue} */
  104. const CUSTOM_MEDIA_FALSE = { kind: "boolean", value: false };
  105. const CC_COLON = ":".charCodeAt(0);
  106. const CC_FULL_STOP = ".".charCodeAt(0);
  107. const CC_HYPHEN_MINUS = "-".charCodeAt(0);
  108. const CC_SEMICOLON = ";".charCodeAt(0);
  109. const CC_TAB = "\t".charCodeAt(0);
  110. const CC_SPACE = " ".charCodeAt(0);
  111. const CC_LINE_FEED = "\n".charCodeAt(0);
  112. const CC_CARRIAGE_RETURN = "\r".charCodeAt(0);
  113. const CC_FORM_FEED = "\f".charCodeAt(0);
  114. const CC_LEFT_CURLY = "{".charCodeAt(0);
  115. const CC_LOWER_V = "v".charCodeAt(0);
  116. const CC_UPPER_V = "V".charCodeAt(0);
  117. const CC_REVERSE_SOLIDUS = "\\".charCodeAt(0);
  118. // A parsed CSS comment. `loc` is computed on demand — only magic-comment error
  119. // warnings read it, so comment-heavy CSS skips the per-comment line/col work.
  120. // 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).
  121. /** @typedef {{ value: string, range: Range }} Comment */
  122. // Newlines (CSS Syntax 3 §3.3) — listed explicitly since there's no preprocessing stage.
  123. // https://www.w3.org/TR/css-syntax-3/#whitespace
  124. // 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.
  125. const PURE_IGNORE_RE = /^\s*cssmodules-pure-ignore(?:\s|$)/;
  126. const PURE_NO_CHECK_RE = /^\s*cssmodules-pure-no-check(?:\s|$)/;
  127. const IMAGE_SET_FUNCTION = /^(?:-\w+-)?image-set$/i;
  128. const OPTIONALLY_VENDOR_PREFIXED_KEYFRAMES_AT_RULE = /^@(?:-\w+-)?keyframes$/;
  129. const VENDOR_PREFIX = /^-\w+-/;
  130. const COMPOSES_PROPERTY = /^(?:composes|compose-with)$/i;
  131. // Functional view-transition pseudo-elements whose `(<name> .class…)` argument names are scoped like `view-transition-name`/`-class` values.
  132. const VIEW_TRANSITION_PART_PSEUDO =
  133. /^view-transition-(?:group|image-pair|old|new)$/i;
  134. const IS_MODULES = /\.modules?\.[^.]+$/i;
  135. // Which formats get a preload hint is webpack's call; what each one *is* comes
  136. // from `mime-db`, not a second copy of the mapping.
  137. const FONT_EXTENSIONS = new Set(["woff2", "woff", "ttf", "otf", "eot"]);
  138. // mime-db is heavy — only load it once a font src actually needs a `type`.
  139. const getMimeTypes = memoize(() => require("../util/mimeTypes"));
  140. /**
  141. * @param {string} request font url (may carry a query/hash)
  142. * @returns {string | undefined} preload `type` for a known font extension
  143. */
  144. const fontMimeType = (request) => {
  145. // Off the path only: `font?fallback=.woff2` names no font.
  146. const resourcePath = parseResource(request).path;
  147. const extension = path.extname(resourcePath).slice(1).toLowerCase();
  148. if (!FONT_EXTENSIONS.has(extension)) return undefined;
  149. return getMimeTypes().lookup(resourcePath);
  150. };
  151. // Skip options for a non-CSS-Modules parse: drop the selector prelude (never
  152. // walked without modules) plus value / function-arg leaves nothing reads (the
  153. // `Ident` visitor no-ops, the `Declaration` visitor returns early, no ICSS).
  154. // `url` / functions / strings / blocks / commas are kept — they carry url()
  155. // rewrites and image-set fences. At-rule preludes are kept (`@media` / `@import`
  156. // are read). CSS-Modules parses skip nothing: selectors are walked and ICSS
  157. // `:export { k: v }` captures each value's byte range from its first / last node.
  158. const SKIP_NON_MODULES = {
  159. types: buildSkipSet([
  160. NodeType.Number,
  161. NodeType.Dimension,
  162. NodeType.Percentage,
  163. NodeType.Ident,
  164. NodeType.Hash,
  165. NodeType.Colon,
  166. NodeType.Delim,
  167. // Nothing reads value/arg whitespace either — consumers use
  168. // `nextNonWhitespace` / type checks that tolerate its absence.
  169. NodeType.Whitespace
  170. ]),
  171. selectorPrelude: true
  172. };
  173. // Like SKIP_NON_MODULES but keeps selector preludes and the `Colon` / `Ident`
  174. // tokens inside function-arg lists, so `@custom-selector` `:--name` references
  175. // (including nested ones like `:is(:--name)`) survive a non-modules parse.
  176. const SKIP_NON_MODULES_KEEP_SELECTORS = {
  177. types: buildSkipSet([
  178. NodeType.Number,
  179. NodeType.Dimension,
  180. NodeType.Percentage,
  181. NodeType.Hash,
  182. NodeType.Delim,
  183. NodeType.Whitespace
  184. ]),
  185. selectorPrelude: false
  186. };
  187. const CSS_COMMENT = /\/\*((?!\*\/)[\s\S]*?)\*\//g;
  188. // `@value` recognizers (postcss-modules-values shape): the import form `<names> from <source>`, and the `<importName> as <localName>` alias inside it.
  189. const VALUE_IMPORT_FORM = /from(\/\*|\s)(?:[\s\S]+)$/i;
  190. const VALUE_AS_ALIAS = /\s+as\s+/;
  191. // `@value name value`: end of the name run (first non-space followed by space).
  192. const VALUE_NAME_BOUNDARY = /\S\s/;
  193. const ONLY_WHITESPACE = /^\s+$/;
  194. // Relative request prefix (`./` or `../`) — `isSelfReferenceRequest` per `from`.
  195. const RELATIVE_REQUEST = /^\.{1,2}\//;
  196. /**
  197. * Range-keyed index over a known-properties table: ASCII-case-folded 31-hash
  198. * of the name bytes → canonical key(s). Lets the Declaration visitor answer
  199. * "is this a known property" (and get the canonical lowercase name) without
  200. * slicing the property name out of the source per declaration.
  201. * @type {WeakMap<Map<string, Map<string, number>>, Map<number, string | string[]>>}
  202. */
  203. const KNOWN_PROPERTY_INDEX_CACHE = new WeakMap();
  204. /**
  205. * Gets (or builds) the hash index for a known-properties table.
  206. * @param {Map<string, Map<string, number>>} knownProperties known properties table
  207. * @returns {Map<number, string | string[]>} hash → canonical name(s)
  208. */
  209. const getKnownPropertyIndex = (knownProperties) => {
  210. let index = KNOWN_PROPERTY_INDEX_CACHE.get(knownProperties);
  211. if (index === undefined) {
  212. index = new Map();
  213. for (const name of knownProperties.keys()) {
  214. let h = name.length;
  215. for (let i = 0; i < name.length; i++) {
  216. h = ((h << 5) - h + name.charCodeAt(i)) | 0;
  217. }
  218. const hit = index.get(h);
  219. if (hit === undefined) index.set(h, name);
  220. else if (typeof hit === "string") index.set(h, [hit, name]);
  221. else hit.push(name);
  222. }
  223. KNOWN_PROPERTY_INDEX_CACHE.set(knownProperties, index);
  224. }
  225. return index;
  226. };
  227. /**
  228. * Canonical known-property name for a source range (ASCII case-insensitive), without slicing.
  229. * @param {Map<number, string | string[]>} index hash index from `getKnownPropertyIndex`
  230. * @param {string} input source
  231. * @param {number} start name start
  232. * @param {number} end name end (exclusive)
  233. * @returns {string | undefined} canonical lowercase name, or undefined when unknown
  234. */
  235. const knownPropertyForRange = (index, input, start, end) => {
  236. let h = end - start;
  237. for (let i = start; i < end; i++) {
  238. let c = input.charCodeAt(i);
  239. // The table holds no raw-byte spelling, so re-read the unescaped one.
  240. // Each pass shortens the name, so an escaped `\` cannot recurse forever.
  241. if (c === CC_REVERSE_SOLIDUS) {
  242. const name = unescapeIdentifier(input.slice(start, end));
  243. return knownPropertyForRange(index, name, 0, name.length);
  244. }
  245. if (c >= 65 && c <= 90) c |= 0x20;
  246. h = ((h << 5) - h + c) | 0;
  247. }
  248. const hit = index.get(h);
  249. if (hit === undefined) return undefined;
  250. if (typeof hit === "string") {
  251. return rangeEqualsLowerCase(input, start, end, hit) ? hit : undefined;
  252. }
  253. for (let i = 0; i < hit.length; i++) {
  254. if (rangeEqualsLowerCase(input, start, end, hit[i])) return hit[i];
  255. }
  256. return undefined;
  257. };
  258. /**
  259. * Comma-separated index of the counter-name argument in a counter-reading function, or `-1` when the name isn't one. The name is matched over the raw byte range like the other function-name probes; only an escaped name (rare) pays the unescaped slice.
  260. * @param {string} input source
  261. * @param {number} start name start offset
  262. * @param {number} end name end offset
  263. * @param {string=} escapedName unescaped name, when the raw range carries an escape
  264. * @returns {number} argument index of the counter name, or `-1`
  265. */
  266. const counterFunctionNameIndex = (input, start, end, escapedName) => {
  267. // The `target-counter(url, name, style?)` cross-reference forms (Generated Content for Paged Media) name the counter second; print engines read them.
  268. if (escapedName !== undefined) {
  269. if (
  270. equalsLowerCase(escapedName, "counter") ||
  271. equalsLowerCase(escapedName, "counters")
  272. ) {
  273. return 0;
  274. }
  275. if (
  276. equalsLowerCase(escapedName, "target-counter") ||
  277. equalsLowerCase(escapedName, "target-counters")
  278. ) {
  279. return 1;
  280. }
  281. return -1;
  282. }
  283. switch (end - start) {
  284. case 7:
  285. return rangeEqualsLowerCase(input, start, end, "counter") ? 0 : -1;
  286. case 8:
  287. return rangeEqualsLowerCase(input, start, end, "counters") ? 0 : -1;
  288. case 14:
  289. return rangeEqualsLowerCase(input, start, end, "target-counter") ? 1 : -1;
  290. case 15:
  291. return rangeEqualsLowerCase(input, start, end, "target-counters")
  292. ? 1
  293. : -1;
  294. default:
  295. return -1;
  296. }
  297. };
  298. /**
  299. * Gets the known-properties table for the enabled scoping options: the
  300. * properties a `css/module` reads a scoped name out of, each with the keywords
  301. * of its own grammar (see `tooling/generate-css-data.js`).
  302. * @param {{ animation?: boolean, container?: boolean, customIdents?: boolean, grid?: boolean }=} options options
  303. * @returns {Map<string, Map<string, number>>} list of known properties
  304. */
  305. const buildKnownProperties = (options = {}) => {
  306. /** @type {Map<string, Map<string, number>>} */
  307. const knownProperties = new Map();
  308. for (const [property, option] of CSS_MODULES_KEYWORD_OPTIONS) {
  309. if (
  310. options[
  311. /** @type {"animation" | "container" | "customIdents" | "grid"} */
  312. (option)
  313. ]
  314. ) {
  315. knownProperties.set(
  316. property,
  317. /** @type {Map<string, number>} */
  318. (CSS_MODULES_KEYWORDS.get(property))
  319. );
  320. }
  321. }
  322. return knownProperties;
  323. };
  324. /** @type {Map<number, Map<string, Map<string, number>>>} */
  325. const KNOWN_PROPERTIES_CACHE = new Map();
  326. /**
  327. * Memoized {@link buildKnownProperties}: the table depends only on the four
  328. * boolean options (≤ 16 combinations) and is read-only, while the same parser is
  329. * reused across modules — so build each combination once and share it instead
  330. * of rebuilding the Map per parsed module.
  331. * @param {{ animation?: boolean, container?: boolean, customIdents?: boolean, grid?: boolean }=} options options
  332. * @returns {Map<string, Map<string, number>>} known properties table
  333. */
  334. const getKnownProperties = (options = {}) => {
  335. const key =
  336. (options.animation ? 1 : 0) |
  337. (options.container ? 2 : 0) |
  338. (options.customIdents ? 4 : 0) |
  339. (options.grid ? 8 : 0);
  340. let table = KNOWN_PROPERTIES_CACHE.get(key);
  341. if (table === undefined) {
  342. table = buildKnownProperties(options);
  343. KNOWN_PROPERTIES_CACHE.set(key, table);
  344. }
  345. return table;
  346. };
  347. // Byte-level source-cursor scans for computing replacement / strip ranges on raw source after parsing.
  348. /**
  349. * Skip trailing whitespace + at most one newline (CRLF-aware).
  350. * @param {string} input source
  351. * @param {number} pos position
  352. * @returns {number} position past whitespace + one newline
  353. */
  354. const skipWhiteLine = (input, pos) => {
  355. for (;;) {
  356. const cc = input.charCodeAt(pos);
  357. if (cc === CC_SPACE || cc === CC_TAB) {
  358. pos++;
  359. continue;
  360. }
  361. if (
  362. cc === CC_LINE_FEED ||
  363. cc === CC_CARRIAGE_RETURN ||
  364. cc === CC_FORM_FEED
  365. ) {
  366. pos++;
  367. }
  368. // Treat CRLF as one newline: a CR followed by LF advances past the LF.
  369. if (cc === CC_CARRIAGE_RETURN && input.charCodeAt(pos) === CC_LINE_FEED) {
  370. pos++;
  371. }
  372. break;
  373. }
  374. return pos;
  375. };
  376. /**
  377. * 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.
  378. * @param {string} input source
  379. * @param {number} start start offset
  380. * @param {number} end end offset
  381. * @returns {boolean} true for a container keyword
  382. */
  383. const isContainerKeyword = (input, start, end) => {
  384. switch (end - start) {
  385. case 2:
  386. return input.startsWith("or", start);
  387. case 3:
  388. return input.startsWith("and", start) || input.startsWith("not", start);
  389. case 4:
  390. return input.startsWith("none", start);
  391. default:
  392. return false;
  393. }
  394. };
  395. /**
  396. * Whether the byte range contains a CSS escape (`\`) — function names are short, so this scan replaces a per-name slice.
  397. * @param {string} input source
  398. * @param {number} start start offset
  399. * @param {number} end end offset (exclusive)
  400. * @returns {boolean} true when the range contains a backslash
  401. */
  402. const rangeHasEscape = (input, start, end) => {
  403. for (let i = start; i < end; i++) {
  404. if (input.charCodeAt(i) === CC_REVERSE_SOLIDUS) return true;
  405. }
  406. return false;
  407. };
  408. /**
  409. * Whether a raw ident range names a custom property — `\2d\2d x` is `--x` too,
  410. * so a name opening with a dash or a backslash pays the unescape.
  411. * @param {string} input source
  412. * @param {number} start ident start offset
  413. * @param {number} end ident end offset (exclusive)
  414. * @returns {boolean} true when the ident names a custom property
  415. */
  416. const rangeIsDashedIdentifier = (input, start, end) => {
  417. // `--x` is the shortest spelling; an escaped one is longer still.
  418. if (end - start < 3) return false;
  419. const first = input.charCodeAt(start);
  420. if (first === CC_HYPHEN_MINUS) {
  421. if (input.charCodeAt(start + 1) === CC_HYPHEN_MINUS) return true;
  422. if (input.charCodeAt(start + 1) !== CC_REVERSE_SOLIDUS) return false;
  423. } else if (first !== CC_REVERSE_SOLIDUS) {
  424. return false;
  425. }
  426. return isDashedIdentifier(unescapeIdentifier(input.slice(start, end)));
  427. };
  428. /**
  429. * The named cell tokens of a `grid-template-areas` string, in source offsets.
  430. * A whitespace closing a hex escape does not separate; a `.` run names nothing.
  431. * @param {string} input source
  432. * @param {number} start content start offset (inside the quote)
  433. * @param {number} end content end offset (exclusive)
  434. * @returns {[number, number][]} one `[start, end]` per named cell
  435. */
  436. const gridAreaNames = (input, start, end) => {
  437. /** @type {[number, number][]} */
  438. const names = [];
  439. let i = start;
  440. while (i < end) {
  441. while (i < end && isWhitespace(input.charCodeAt(i))) i++;
  442. if (i >= end) break;
  443. const nameStart = i;
  444. let nullCell = true;
  445. while (i < end) {
  446. const cc = input.charCodeAt(i);
  447. if (isWhitespace(cc)) break;
  448. if (cc === CC_REVERSE_SOLIDUS) {
  449. i = Math.min(skipEscape(input, i), end);
  450. nullCell = false;
  451. continue;
  452. }
  453. if (cc !== CC_FULL_STOP) nullCell = false;
  454. i++;
  455. }
  456. if (!nullCell) names.push([nameStart, i]);
  457. }
  458. return names;
  459. };
  460. /**
  461. * @param {string} input source
  462. * @param {number} pos position
  463. * @returns {number} position of the next `{`, or EOF if none
  464. */
  465. const findLeftCurly = (input, pos) => {
  466. while (pos < input.length) {
  467. if (input.charCodeAt(pos) === CC_LEFT_CURLY) return pos;
  468. pos++;
  469. }
  470. return pos;
  471. };
  472. /**
  473. * Defines the css parser own options type used by this module.
  474. * @typedef {object} CssParserOwnOptions
  475. * @property {("pure" | "global" | "local" | "auto")=} defaultMode default mode
  476. */
  477. /** @typedef {CssAutoOrModuleParserOptions & CssParserOwnOptions} CssParserOptions */
  478. /**
  479. * Pure-mode at-rules whose prelude is selector-checked, so their body is opaque to the enclosing rule's declaration accounting.
  480. * @param {string} name at-rule name including the leading `@`, lower-cased
  481. * @returns {boolean} true for `@keyframes` / `@counter-style` / `@container` / `@scope`
  482. */
  483. const isPureBodyAtRule = (name) =>
  484. OPTIONALLY_VENDOR_PREFIXED_KEYFRAMES_AT_RULE.test(name) ||
  485. name === "@counter-style" ||
  486. name === "@container" ||
  487. name === "@scope";
  488. /**
  489. * 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)?
  490. * @param {Declaration[] | null} declarations rule-body declarations
  491. * @param {Rule[] | null} childRules rule-body child rules
  492. * @returns {{ hasDirectDecl: boolean, hasNestedBlock: boolean }} scan result
  493. */
  494. const scanRuleBody = (declarations, childRules) => {
  495. let hasDirectDecl = Boolean(declarations && declarations.length > 0);
  496. let hasNestedBlock = false;
  497. if (childRules) {
  498. for (const child of childRules) {
  499. const t = A.type(child);
  500. if (t === NodeType.QualifiedRule) {
  501. hasNestedBlock = true;
  502. } else if (t === NodeType.AtRule) {
  503. const atDecls = A.declarations(child);
  504. const atChildRules = A.childRules(child);
  505. if (!atDecls && !atChildRules) continue;
  506. hasNestedBlock = true;
  507. if (
  508. !hasDirectDecl &&
  509. !isPureBodyAtRule(`@${toLowerCaseIfNeeded(A.name(child))}`) &&
  510. scanRuleBody(atDecls, atChildRules).hasDirectDecl
  511. ) {
  512. hasDirectDecl = true;
  513. }
  514. }
  515. }
  516. }
  517. return { hasDirectDecl, hasNestedBlock };
  518. };
  519. /**
  520. * Parses value at rule params.
  521. * @param {string} str value at-rule params
  522. * @returns {ValueAtRuleImport | ValueAtRuleValue} parsed result
  523. */
  524. const parseValueAtRuleParams = (str) => {
  525. if (VALUE_IMPORT_FORM.test(str)) {
  526. str = str.replace(CSS_COMMENT, " ").trim().replace(/;$/, "");
  527. const fromIdx = str.lastIndexOf("from");
  528. const path = str
  529. .slice(fromIdx + 5)
  530. .trim()
  531. .replace(/['"]/g, "");
  532. let content = str.slice(0, fromIdx).trim();
  533. if (content.startsWith("(") && content.endsWith(")")) {
  534. content = content.slice(1, -1);
  535. }
  536. return {
  537. from: path,
  538. items: content.split(",").map((item) => {
  539. item = item.trim();
  540. if (item.includes(":")) {
  541. const [local, remote] = item.split(":");
  542. return { localName: local.trim(), importName: remote.trim() };
  543. }
  544. const asParts = item.split(VALUE_AS_ALIAS);
  545. if (asParts.length === 2) {
  546. return {
  547. localName: asParts[1].trim(),
  548. importName: asParts[0].trim()
  549. };
  550. }
  551. return { localName: item, importName: item };
  552. })
  553. };
  554. }
  555. /** @type {string} */
  556. let localName;
  557. /** @type {string} */
  558. let value;
  559. const idx = str.indexOf(":");
  560. if (idx !== -1) {
  561. localName = str.slice(0, idx).replace(CSS_COMMENT, "").trim();
  562. value = str.slice(idx + 1);
  563. } else {
  564. const mask = str.replace(CSS_COMMENT, (m) => " ".repeat(m.length));
  565. const idx = mask.search(VALUE_NAME_BOUNDARY) + 1;
  566. localName = str.slice(0, idx).replace(CSS_COMMENT, "").trim();
  567. value = str.slice(idx + (str[idx] === " " ? 1 : 0));
  568. }
  569. if (
  570. value.length > 0 &&
  571. !ONLY_WHITESPACE.test(value.replace(CSS_COMMENT, ""))
  572. ) {
  573. value = value.trim();
  574. }
  575. return { localName, value };
  576. };
  577. /**
  578. * Index of the next non-whitespace child at or after `from`, or `nodes.length`.
  579. * @param {AstNode[]} nodes node list
  580. * @param {number} from start index (inclusive)
  581. * @returns {number} index of the next non-whitespace node
  582. */
  583. const nextNonWhitespace = (nodes, from) => {
  584. let i = from;
  585. while (i < nodes.length && A.type(nodes[i]) === NodeType.Whitespace) i++;
  586. return i;
  587. };
  588. /** @typedef {{ urlNode: (AstNode | undefined), layerNode: (AstNode | undefined), supportsNode: (FunctionNode | undefined) }} ImportPrelude */
  589. /**
  590. * 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).
  591. * @param {AstNode[]} prelude the at-rule prelude nodes
  592. * @returns {ImportPrelude} the recognized prefix parts (any may be undefined)
  593. */
  594. const parseImportPrelude = (prelude) => {
  595. /** @type {AstNode | undefined} */
  596. let urlNode;
  597. /** @type {AstNode | undefined} */
  598. let layerNode;
  599. /** @type {FunctionNode | undefined} */
  600. let supportsNode;
  601. for (const cv of prelude) {
  602. const t = A.type(cv);
  603. if (t === NodeType.Whitespace) continue;
  604. if (!urlNode) {
  605. if (t === NodeType.Url || t === NodeType.String) {
  606. urlNode = cv;
  607. continue;
  608. }
  609. if (
  610. t === NodeType.Function &&
  611. equalsLowerCase(A.unescapedName(cv), "url")
  612. ) {
  613. urlNode = cv;
  614. continue;
  615. }
  616. if (t === NodeType.Ident) {
  617. // CSS Modules: bare ident is a `@value` reference.
  618. urlNode = cv;
  619. continue;
  620. }
  621. break;
  622. }
  623. if (!layerNode && !supportsNode) {
  624. if (t === NodeType.Ident) {
  625. if (equalsLowerCase(A.unescaped(cv), "layer")) {
  626. layerNode = cv;
  627. continue;
  628. }
  629. } else if (
  630. t === NodeType.Function &&
  631. equalsLowerCase(A.unescapedName(cv), "layer")
  632. ) {
  633. layerNode = cv;
  634. continue;
  635. }
  636. }
  637. if (
  638. !supportsNode &&
  639. t === NodeType.Function &&
  640. equalsLowerCase(A.unescapedName(cv), "supports")
  641. ) {
  642. supportsNode = /** @type {FunctionNode} */ (cv);
  643. continue;
  644. }
  645. // First media-query token — stop scanning for the prefix.
  646. break;
  647. }
  648. return { urlNode, layerNode, supportsNode };
  649. };
  650. /**
  651. * 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`.
  652. * @param {AstNode} second the `import(…)` function / first prelude node after the `:`
  653. * @param {QualifiedRule} rule the `:import` rule
  654. * @param {string} source full CSS source, for the path slice
  655. * @returns {{ request: string } | { errorPos: number }} the unquoted request, or the position for the parse warning
  656. */
  657. const parseIcssImportRequest = (second, rule, source) => {
  658. /** @type {AstNode[] | undefined} */
  659. let args;
  660. if (A.type(second) === NodeType.Function) {
  661. args = A.children(second);
  662. } else {
  663. for (const p of A.prelude(rule)) {
  664. if (A.type(p) === NodeType.SimpleBlock && A.blockToken(p) === "(") {
  665. args = A.children(p);
  666. break;
  667. }
  668. }
  669. }
  670. // The first non-whitespace value inside `(...)` must be a string.
  671. const innerStrToken =
  672. args && args.find((v) => A.type(v) !== NodeType.Whitespace);
  673. if (!innerStrToken || A.type(innerStrToken) !== NodeType.String) {
  674. const errorPos =
  675. A.type(second) === NodeType.Function
  676. ? A.nameEnd(second) + 1
  677. : A.end(second);
  678. return { errorPos };
  679. }
  680. return {
  681. request: source.slice(A.start(innerStrToken) + 1, A.end(innerStrToken) - 1)
  682. };
  683. };
  684. class CssParser extends Parser {
  685. /**
  686. * Creates an instance of CssParser.
  687. * @param {CssParserOptions=} options options
  688. */
  689. constructor(options = {}) {
  690. super();
  691. this.defaultMode =
  692. typeof options.defaultMode !== "undefined" ? options.defaultMode : "pure";
  693. this.options = {
  694. as: "stylesheet",
  695. url: true,
  696. import: true,
  697. namedExports: true,
  698. animation: true,
  699. container: true,
  700. customIdents: true,
  701. customMedia: true,
  702. customSelectors: true,
  703. dashedIdents: true,
  704. function: true,
  705. grid: true,
  706. ...options
  707. };
  708. this.magicCommentContext = createMagicCommentContext();
  709. }
  710. /**
  711. * Processes the provided state.
  712. * @param {ParserState} state parser state
  713. * @param {string} message warning message
  714. * @param {LocConverter} locConverter location converter
  715. * @param {number} start start offset
  716. * @param {number} end end offset
  717. */
  718. _emitWarning(state, message, locConverter, start, end) {
  719. const { line: sl, column: sc } = locConverter.get(start);
  720. const { line: el, column: ec } = locConverter.get(end);
  721. state.current.addWarning(
  722. new (getModuleDependencyWarning())(
  723. state.module,
  724. new WebpackError(message),
  725. {
  726. start: { line: sl, column: sc },
  727. end: { line: el, column: ec }
  728. }
  729. )
  730. );
  731. }
  732. /**
  733. * Emits a build error for the provided range.
  734. * @param {ParserState} state parser state
  735. * @param {string} message error message
  736. * @param {LocConverter} locConverter location converter
  737. * @param {number} start start offset
  738. * @param {number} end end offset
  739. */
  740. _emitError(state, message, locConverter, start, end) {
  741. const { line: sl, column: sc } = locConverter.get(start);
  742. const { line: el, column: ec } = locConverter.get(end);
  743. const err = new WebpackError(message);
  744. err.module = state.module;
  745. err.loc = {
  746. start: { line: sl, column: sc },
  747. end: { line: el, column: ec }
  748. };
  749. state.module.addError(err);
  750. }
  751. /**
  752. * Arbitrate cross-kind CSS-Modules export collisions collected during
  753. * parsing. A class always wins the shared JS export key over the
  754. * CSS-only kinds (`@keyframes`, `@counter-style`, `@container`, custom
  755. * property, grid identifier) — matching css-loader / lightning css /
  756. * postcss-modules; the loser's `EXPORT_MODE` is demoted to `NONE` so
  757. * the CSS-side rewrite is preserved but the entry no longer contributes
  758. * to `styles.<name>`. Other pairs (`class` ↔ `id`,
  759. * `class` ↔ `:export`, `class` ↔ `@value`, kind-vs-kind without a
  760. * class present, …) surface as a warning so the user resolves the name
  761. * themselves. Same-kind redeclarations are silent.
  762. * @param {ParserState} state parser state
  763. * @param {Map<string, { kind: string, line: number, column: number, entries: number[] }[]>} declaredExports collected declarations grouped by (name, kind)
  764. * @param {import("../dependencies/CssIcssExportDependency").CssExportEntry[]} cssExportEntries all export entries — losers are demoted in place
  765. * @returns {void}
  766. */
  767. _resolveAmbiguousExports(state, declaredExports, cssExportEntries) {
  768. const NONE = CssIcssExportDependency.EXPORT_MODE.NONE;
  769. for (const [name, decls] of declaredExports) {
  770. if (decls.length <= 1) continue;
  771. // Prefer `class` as the winner (matches css-loader / lightning css / postcss-modules); otherwise the first-declared kind wins de facto.
  772. let winner;
  773. for (const d of decls) {
  774. if (d.kind === "class") {
  775. winner = d;
  776. break;
  777. }
  778. }
  779. const reference = winner !== undefined ? winner : decls[0];
  780. for (const loser of decls) {
  781. if (loser === reference) continue;
  782. const anchor = cssExportEntries[loser.entries[0]];
  783. if (
  784. winner !== undefined &&
  785. (loser.kind === "custom property" ||
  786. loser.kind === "@keyframes" ||
  787. loser.kind === "@counter-style" ||
  788. loser.kind === "@container" ||
  789. loser.kind === "grid identifier")
  790. ) {
  791. for (const idx of loser.entries) {
  792. cssExportEntries[idx].exportMode = NONE;
  793. }
  794. state.current.addWarning(
  795. new (getModuleDependencyWarning())(
  796. state.module,
  797. new WebpackError(
  798. `CSS module export "${name}" is shadowed by ${winner.kind} at line ${winner.line}:${winner.column}: the ${loser.kind} "${name}" is still scoped in the emitted CSS but is not accessible from the JavaScript export — rename one of them if both are needed`
  799. ),
  800. {
  801. start: {
  802. line: anchor.locStartLine,
  803. column: anchor.locStartColumn
  804. },
  805. end: {
  806. line: anchor.locEndLine,
  807. column: anchor.locEndColumn
  808. }
  809. }
  810. )
  811. );
  812. } else {
  813. // Other collision — warn without changing behavior.
  814. state.current.addWarning(
  815. new (getModuleDependencyWarning())(
  816. state.module,
  817. new WebpackError(
  818. `Conflicting CSS module export "${name}": already declared as ${reference.kind} at line ${reference.line}:${reference.column}, redeclared as ${loser.kind}`
  819. ),
  820. {
  821. start: {
  822. line: anchor.locStartLine,
  823. column: anchor.locStartColumn
  824. },
  825. end: {
  826. line: anchor.locEndLine,
  827. column: anchor.locEndColumn
  828. }
  829. }
  830. )
  831. );
  832. }
  833. }
  834. }
  835. }
  836. /**
  837. * Parses the provided source and updates the parser state.
  838. * @param {string | Buffer | PreparsedAst} source the source to parse
  839. * @param {ParserState} state the parser state
  840. * @returns {ParserState} the parser state
  841. */
  842. parse(source, state) {
  843. if (Buffer.isBuffer(source)) {
  844. source = source.toString("utf8");
  845. } else if (typeof source === "object") {
  846. throw new Error("webpackAst is unexpected for the CssParser");
  847. }
  848. if (source[0] === "\uFEFF") {
  849. source = source.slice(1);
  850. }
  851. // Per-parse comment side-array — kept local (like HtmlParser) so nothing is retained on the reused parser instance between modules.
  852. /** @type {Comment[]} */
  853. const comments = [];
  854. const urlHints = this.options.urlHints;
  855. const fontPreload = this.options.fontPreload;
  856. /**
  857. * Apply `parser.css.urlHints` defaults + `webpackPrefetch` /
  858. * `webpackPreload` / `webpackFetchPriority` / `webpackAs` /
  859. * `webpackType` / `webpackMedia` magic-comment overrides to a fresh
  860. * `CssUrlDependency`. Magic comments win over the project-wide default.
  861. * @param {CssUrlDependency} dep dep
  862. * @param {string} request asset request
  863. * @param {Record<string, EXPECTED_ANY> | null | undefined} options parsed comment options
  864. * @param {import("../Dependency").DependencyLocation} loc location for warnings
  865. * @returns {void}
  866. */
  867. const applyResourceHintDefaults = (dep, request, options, loc) => {
  868. // `fontPreload` heuristic: seed `preload`/`as`/`type` for the first url
  869. // of each `@font-face` (the nearest enclosing at-rule) as the lowest
  870. // default, so `urlHints` rules and magic comments below still override.
  871. if (fontPreload && atRuleStateStack.length > 0) {
  872. const top = atRuleStateStack[atRuleStateStack.length - 1];
  873. if (top.name === "@font-face" && !top.fontPreloaded) {
  874. top.fontPreloaded = true;
  875. dep.preload = true;
  876. dep.asAttribute = "font";
  877. const type = fontMimeType(request);
  878. if (type) dep.typeAttribute = type;
  879. }
  880. }
  881. ResourceHintPlugin.applyResourceHints(
  882. dep,
  883. urlHints,
  884. request,
  885. options,
  886. state.module,
  887. loc
  888. );
  889. };
  890. const module = state.module;
  891. // Every CSS Modules export declaration is tracked here so `_resolveAmbiguousExports` can arbitrate cross-kind collisions in one pass at end-of-parse: a class silently wins over CSS-scoped kinds (`@keyframes`, `@counter-style`, `@container`, custom property, grid identifier) — matching css-loader / lightning css / postcss-modules — while other pairs (`class` ↔ `id`, `class` ↔ `:export`, `class` ↔ `@value`, …) surface as a warning so the user can resolve them.
  892. /** @typedef {{ kind: string, line: number, column: number, entries: number[] }} DeclarationOfKind */
  893. /** @type {Map<string, DeclarationOfKind[]>} */
  894. const declaredExports = new Map();
  895. /**
  896. * Track a declaration site for later ambiguity resolution. Pointer
  897. * into `cssExportEntries` is captured now because the entry is
  898. * pushed immediately after by the calling `addCssExport(…)`.
  899. * Property-value references (`animation: foo`) are not declarations
  900. * and skip this hook.
  901. * @param {string} name export name
  902. * @param {string} kind human-readable declaration kind
  903. * @param {number} line start line (1-based) of the declaration
  904. * @param {number} column start column (0-based) of the declaration
  905. * @returns {void}
  906. */
  907. const recordDeclaration = (name, kind, line, column) => {
  908. let decls = declaredExports.get(name);
  909. if (decls === undefined) {
  910. decls = [];
  911. declaredExports.set(name, decls);
  912. }
  913. let d;
  914. for (const existing of decls) {
  915. if (existing.kind === kind) {
  916. d = existing;
  917. break;
  918. }
  919. }
  920. if (d === undefined) {
  921. d = { kind, line, column, entries: [] };
  922. decls.push(d);
  923. }
  924. d.entries.push(cssExportEntries.length);
  925. };
  926. // All `:export`-style exports are collected into a single
  927. // `CssIcssExportDependency` per module (emitted at parse end) instead of one
  928. // `Dependency` per export — far fewer retained objects on CSS-heavy builds.
  929. /** @type {import("../dependencies/CssIcssExportDependency").CssExportEntry[]} */
  930. const cssExportEntries = [];
  931. /**
  932. * @param {number} sl start line
  933. * @param {number} sc start column
  934. * @param {number} el end line
  935. * @param {number} ec end column
  936. * @param {string} name export name
  937. * @param {import("../dependencies/CssIcssExportDependency").Value} value value or [localName, importName, request?]
  938. * @param {Range=} range source range to replace, when present
  939. * @param {boolean=} interpolate whether the value needs interpolation
  940. * @param {import("../dependencies/CssIcssExportDependency").ExportMode=} exportMode export mode
  941. * @param {import("../dependencies/CssIcssExportDependency").ExportType=} exportType export type
  942. * @returns {void}
  943. */
  944. const addCssExport = (
  945. sl,
  946. sc,
  947. el,
  948. ec,
  949. name,
  950. value,
  951. range,
  952. interpolate = false,
  953. exportMode = CssIcssExportDependency.EXPORT_MODE.REPLACE,
  954. exportType = CssIcssExportDependency.EXPORT_TYPE.NORMAL
  955. ) => {
  956. // Flat location numbers — the nested loc objects were the parser's
  957. // hottest allocation (3 objects per exported name).
  958. cssExportEntries.push({
  959. name,
  960. value,
  961. range,
  962. interpolate,
  963. exportMode,
  964. exportType,
  965. locStartLine: sl,
  966. locStartColumn: sc,
  967. locEndLine: el,
  968. locEndColumn: ec
  969. });
  970. };
  971. const parsedModuleResource = parseResource(
  972. /** @type {string} */ (module.getResource())
  973. );
  974. const mode =
  975. this.defaultMode === "auto" &&
  976. module.type === CSS_MODULE_TYPE_AUTO &&
  977. IS_MODULES.test(parsedModuleResource.path)
  978. ? "local"
  979. : this.defaultMode;
  980. const isModules = mode === "global" || mode === "local";
  981. /** @type {Map<string, boolean>} */
  982. const selfReferenceCache = new Map();
  983. /**
  984. * Whether a relative `from "<request>"` resolves back to the current module (matching query/fragment too).
  985. * Memoized per parse — `composes … from "./x.css"` repeats the same request many times per file.
  986. * @param {string} request request string from `from "<request>"`
  987. * @returns {boolean} true if request resolves to the current module
  988. */
  989. const isSelfReferenceRequest = (request) => {
  990. const cached = selfReferenceCache.get(request);
  991. if (cached !== undefined) return cached;
  992. const result = isSelfReferenceRequestUncached(request);
  993. selfReferenceCache.set(request, result);
  994. return result;
  995. };
  996. /**
  997. * Uncached `isSelfReferenceRequest`.
  998. * @param {string} request request string from `from "<request>"`
  999. * @returns {boolean} true if request resolves to the current module
  1000. */
  1001. const isSelfReferenceRequestUncached = (request) => {
  1002. if (!RELATIVE_REQUEST.test(request)) return false;
  1003. if (!module.context) return false;
  1004. const parsedRequest = parseResource(request);
  1005. if (parsedRequest.query !== parsedModuleResource.query) return false;
  1006. if (parsedRequest.fragment !== parsedModuleResource.fragment) {
  1007. return false;
  1008. }
  1009. try {
  1010. return (
  1011. path.resolve(module.context, parsedRequest.path) ===
  1012. parsedModuleResource.path
  1013. );
  1014. } catch (_err) {
  1015. return false;
  1016. }
  1017. };
  1018. const knownProperties = getKnownProperties({
  1019. animation: this.options.animation,
  1020. container: this.options.container,
  1021. customIdents: this.options.customIdents,
  1022. grid: this.options.grid
  1023. });
  1024. const knownPropertyIndex = getKnownPropertyIndex(knownProperties);
  1025. /** @type {CssModuleBuildMeta} */
  1026. (module.buildMeta).isCssModule = isModules;
  1027. if (/** @type {CssModule} */ (module).exportType === "style") {
  1028. /** @type {CssModuleBuildMeta} */
  1029. (module.buildMeta).needIdInConcatenation = true;
  1030. }
  1031. const locConverter = new LocConverter(source);
  1032. /**
  1033. * Source location of a byte range. `LocConverter#get` mutates and returns itself, so snapshot between the two calls.
  1034. * @param {number} start start offset
  1035. * @param {number} end end offset
  1036. * @returns {{ start: Position, end: Position }} the source location
  1037. */
  1038. const rangeLoc = (start, end) => {
  1039. const s = locConverter.get(start);
  1040. const sl = s.line;
  1041. const sc = s.column;
  1042. const e = locConverter.get(end);
  1043. return {
  1044. start: { line: sl, column: sc },
  1045. end: { line: e.line, column: e.column }
  1046. };
  1047. };
  1048. /**
  1049. * Set a dependency's source location from a byte range.
  1050. * @param {ConstDependency | CssUrlDependency | CssImportDependency | CssIcssImportDependency | CssIcssSymbolDependency} dep dependency with `setLoc`
  1051. * @param {number} start start offset
  1052. * @param {number} end end offset
  1053. */
  1054. const setDepLoc = (dep, start, end) => {
  1055. const s = locConverter.get(start);
  1056. const sl = s.line;
  1057. const sc = s.column;
  1058. const e = locConverter.get(end);
  1059. dep.setLoc(sl, sc, e.line, e.column);
  1060. };
  1061. /**
  1062. * 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.
  1063. * @param {Range} range byte range to scan for magic comments
  1064. * @param {number} warnStart start offset of the loc for an invalid-`webpackIgnore` warning (computed lazily)
  1065. * @param {number} warnEnd end offset of that loc
  1066. * @returns {{ ignored: boolean, options: Record<string, EXPECTED_ANY> | null }} parsed options and `webpackIgnore` flag
  1067. */
  1068. const magicCommentsIn = (range, warnStart, warnEnd) => {
  1069. /** @type {{ options: Record<string, EXPECTED_ANY> | null, errors: (Error & { comment: Comment })[] | null }} */
  1070. const { options, errors } = parseCommentOptionsInRange(
  1071. /** @type {(Comment & { range: [number, number], value: string })[]} */ (
  1072. comments
  1073. ),
  1074. range,
  1075. this.magicCommentContext
  1076. );
  1077. if (errors) {
  1078. for (const e of errors) {
  1079. state.module.addWarning(
  1080. new (getCommentCompilationWarning())(
  1081. `Compilation error while processing magic comment(-s): /*${e.comment.value}*/: ${e.message}`,
  1082. rangeLoc(e.comment.range[0], e.comment.range[1])
  1083. )
  1084. );
  1085. }
  1086. }
  1087. let ignored = false;
  1088. if (options && options.webpackIgnore !== undefined) {
  1089. if (typeof options.webpackIgnore !== "boolean") {
  1090. // Loc is computed lazily here — it's only needed for this rare
  1091. // warning, not on every checked `url()` / `@import`.
  1092. state.module.addWarning(
  1093. new (getUnsupportedFeatureWarning())(
  1094. `\`webpackIgnore\` expected a boolean, but received: ${options.webpackIgnore}.`,
  1095. rangeLoc(warnStart, warnEnd)
  1096. )
  1097. );
  1098. } else {
  1099. ignored = options.webpackIgnore;
  1100. }
  1101. }
  1102. return { ignored, options };
  1103. };
  1104. /**
  1105. * Backwards-compatible boolean shortcut of {@link magicCommentsIn} for call sites that only need the ignore flag.
  1106. * @param {Range} range byte range to scan for magic comments
  1107. * @param {number} warnStart start offset of the loc for an invalid-`webpackIgnore` warning
  1108. * @param {number} warnEnd end offset of that loc
  1109. * @returns {boolean} true when `webpackIgnore: true`
  1110. */
  1111. const webpackIgnored = (range, warnStart, warnEnd) =>
  1112. magicCommentsIn(range, warnStart, warnEnd).ignored;
  1113. // Closure-scope alias for `source` used by AST-walking helpers for substring extraction.
  1114. const input = source;
  1115. // `@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.
  1116. const mayHaveCustomMedia =
  1117. this.options.customMedia && input.includes("@custom-media");
  1118. const mayHaveCustomSelectors =
  1119. this.options.customSelectors && input.includes("@custom-selector");
  1120. /** @type {Map<string, CustomMediaDefinition> | undefined} */
  1121. let customMediaDefs;
  1122. /** @type {Map<string, CustomMediaValue> | undefined} */
  1123. let customMediaValues;
  1124. /** @type {{ queries: MediaNode[], start: number, end: number, uses: CustomMediaUse[] }[] | undefined} */
  1125. let customMediaQueries;
  1126. /** @type {Map<string, string> | undefined} */
  1127. let customSelectorDefs;
  1128. /** @type {{ name: string, start: number, end: number }[] | undefined} */
  1129. let customSelectorUses;
  1130. /**
  1131. * Unescape a CSS identifier from a source byte range — for value spans not
  1132. * backed by a single token (string contents, `--` dashed-ident bodies,
  1133. * composed names). Token-backed names use `A.unescaped(node)` instead.
  1134. * @param {number} start start offset
  1135. * @param {number} end end offset
  1136. * @returns {string} the unescaped identifier
  1137. */
  1138. const unescapeRange = (start, end) =>
  1139. unescapeIdentifier(input.slice(start, end));
  1140. let lastTokenEndForComments = 0;
  1141. /** Generates unique `__ICSS_IMPORT_${n}__` placeholders per ICSS import. */
  1142. const nextIcssImportName = (() => {
  1143. let n = 0;
  1144. return () => `__ICSS_IMPORT_${n++}__`;
  1145. })();
  1146. // 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.
  1147. const pure = {
  1148. enabled: isModules && Boolean(this.options.pure),
  1149. /** Whether the current rule's prelude has so far seen any impure comma-separated selector (set by `finalizeSelector`). */
  1150. ruleImpure: false,
  1151. /** Whether the current comma-separated selector has carried a local class / id (cleared by `finalizeSelector`). */
  1152. segmentLocal: false,
  1153. /** File-level kill switch from a top-of-file `cssmodules-pure-no-check` comment. */
  1154. noCheck: false,
  1155. /** Single-shot ignore from a `cssmodules-pure-ignore` comment — consumed by the next rule frame. */
  1156. ignorePending: false,
  1157. /** Has any top-level rule been processed (locks `noCheck`)? */
  1158. seenTopLevelRule: false,
  1159. /**
  1160. * Inherited per open block: `ancestorHadLocal` (nested rules inherit purity from a local-bearing ancestor) and `skipChildren` (a check-suppressing ancestor like `@keyframes`).
  1161. * @type {{ ancestorHadLocal: boolean, skipChildren: boolean }[]}
  1162. */
  1163. stack: [],
  1164. /**
  1165. * Whether any ancestor (self inclusive) was pure — for ancestor-inheritance and `&`-resolution.
  1166. * @returns {boolean} true if any ancestor provided a local
  1167. */
  1168. ancestorHadLocal() {
  1169. const top = this.stack[this.stack.length - 1];
  1170. return top ? top.ancestorHadLocal : false;
  1171. },
  1172. /**
  1173. * Record that the current comma-separated selector carries a local class / id (no-op when pure-mode is off).
  1174. */
  1175. markLocal() {
  1176. if (this.enabled) this.segmentLocal = true;
  1177. },
  1178. /**
  1179. * 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).
  1180. */
  1181. finalizeSelector() {
  1182. if (!this.enabled) return;
  1183. if (!this.segmentLocal && !this.ancestorHadLocal()) {
  1184. this.ruleImpure = true;
  1185. }
  1186. this.segmentLocal = false;
  1187. },
  1188. /**
  1189. * Mark that a top-level rule has been processed; locks `noCheck` (no-op when pure-mode is off).
  1190. */
  1191. markSeenTopLevelRule() {
  1192. if (this.enabled) this.seenTopLevelRule = true;
  1193. },
  1194. /**
  1195. * Report a pure-mode violation covering the entire rule prelude.
  1196. * @param {number} start prelude start offset
  1197. * @param {number} end prelude end offset (`{` position)
  1198. */
  1199. report: (start, end) => {
  1200. const slice = source.slice(start, end);
  1201. const lead = /** @type {RegExpExecArray} */ (
  1202. /^(?:\s|\/\*[\s\S]*?\*\/)*/.exec(slice)
  1203. )[0].length;
  1204. const trail = /** @type {RegExpExecArray} */ (/\s*$/.exec(slice))[0]
  1205. .length;
  1206. const from = start + lead;
  1207. const to = end - trail;
  1208. if (to <= from) return;
  1209. this._emitError(
  1210. state,
  1211. `Selector "${source.slice(
  1212. from,
  1213. to
  1214. )}" is not pure (pure selectors must contain at least one local class or id)`,
  1215. locConverter,
  1216. from,
  1217. to
  1218. );
  1219. },
  1220. /**
  1221. * 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.
  1222. * @param {{ isRulePrelude: boolean, treatAsLeaf: boolean, ownSkip: boolean, declarations: Declaration[] | null, childRules: Rule[] | null, preludeStart: number, preludeEnd: number }} opts entry options
  1223. */
  1224. enterBlock(opts) {
  1225. if (!this.enabled) return;
  1226. const {
  1227. isRulePrelude,
  1228. treatAsLeaf,
  1229. ownSkip,
  1230. declarations,
  1231. childRules,
  1232. preludeStart,
  1233. preludeEnd
  1234. } = opts;
  1235. const top = this.stack[this.stack.length - 1];
  1236. const skipOwn = top ? top.skipChildren : false;
  1237. const reportable =
  1238. !this.noCheck &&
  1239. !this.ignorePending &&
  1240. !skipOwn &&
  1241. isRulePrelude &&
  1242. this.ruleImpure;
  1243. if (reportable) {
  1244. const hasBody = Boolean(declarations || childRules);
  1245. let leaf = treatAsLeaf || !hasBody;
  1246. if (!leaf && hasBody) {
  1247. const { hasDirectDecl, hasNestedBlock } = scanRuleBody(
  1248. declarations,
  1249. childRules
  1250. );
  1251. leaf = hasDirectDecl || !hasNestedBlock;
  1252. }
  1253. if (leaf) this.report(preludeStart, preludeEnd);
  1254. }
  1255. this.stack.push({
  1256. ancestorHadLocal:
  1257. this.ancestorHadLocal() || (isRulePrelude && !this.ruleImpure),
  1258. skipChildren: ownSkip || skipOwn
  1259. });
  1260. this.ignorePending = false;
  1261. this.segmentLocal = false;
  1262. this.ruleImpure = false;
  1263. },
  1264. /**
  1265. * Drop the inherited-context frame (no-op when pure-mode is off).
  1266. */
  1267. exitBlock() {
  1268. if (this.enabled) this.stack.pop();
  1269. },
  1270. /**
  1271. * 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.
  1272. * @param {string} value comment body (without the surrounding delimiters)
  1273. */
  1274. applyComment(value) {
  1275. if (PURE_IGNORE_RE.test(value)) {
  1276. this.ignorePending = true;
  1277. } else if (PURE_NO_CHECK_RE.test(value) && !this.seenTopLevelRule) {
  1278. this.noCheck = true;
  1279. }
  1280. }
  1281. };
  1282. /** @typedef {{ value?: string, importName?: string, localName?: string, request?: string }} IcssDefinition */
  1283. /** @type {Map<string, IcssDefinition>} */
  1284. const icssDefinitions = new Map();
  1285. // `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`.
  1286. /** @type {Map<string, Set<string>>} */
  1287. const composesGraph = new Map();
  1288. /** @type {Map<string, CssIcssImportDependency>} */
  1289. const composesFirstFileImport = new Map();
  1290. // 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).
  1291. const currentRule = {
  1292. /** Did this rule's prelude declare a local-mode anchor selector? */
  1293. hasLocalAnchor: false,
  1294. /** Local class / id names in source order (composes reads `[0]` as the anchor). */
  1295. /** @type {string[]} */
  1296. localIdentifiers: [],
  1297. /** Previous `composes: … from "…"` file in this rule (for the load-order graph edges). */
  1298. /** @type {string | undefined} */
  1299. composesPrevFile: undefined,
  1300. /** 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`. */
  1301. /** @type {Set<string> | null} */
  1302. composesFiles: null
  1303. };
  1304. /**
  1305. * Whether the module's default mode is local (callers here have no `:local`/`:global` wrapper in scope, so it reduces to the default mode).
  1306. * @returns {boolean} true when the module's default mode is `local`
  1307. */
  1308. const isLocalMode = () => mode === "local";
  1309. /**
  1310. * Effective local mode: persistent `:local`/`:global` from `modeData` if any, else the module's default.
  1311. * @returns {boolean} true when the effective mode is local
  1312. */
  1313. const isEffectivelyLocal = () =>
  1314. modeData ? modeData === "local" : mode === "local";
  1315. /**
  1316. * 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).
  1317. * @param {import("./syntax").CssPath} path walk path at the comment node
  1318. */
  1319. const commentVisitor = (path) => {
  1320. const node = path.node;
  1321. const start = A.start(node);
  1322. const end = A.end(node);
  1323. comments.push({
  1324. value: source.slice(start + 2, end - 2),
  1325. range: [start, end]
  1326. });
  1327. };
  1328. /**
  1329. * 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.
  1330. * @returns {(until: number) => void} the cursor-advance function
  1331. */
  1332. const advanceCommentCursor = (() => {
  1333. let cursor = 0;
  1334. /** @param {number} until source position to advance the cursor to */
  1335. return (until) => {
  1336. if (!pure.enabled) return;
  1337. while (cursor < comments.length) {
  1338. const c = comments[cursor];
  1339. if (c.range[1] > until) return;
  1340. pure.applyComment(c.value);
  1341. cursor++;
  1342. }
  1343. };
  1344. })();
  1345. // CSS modules stuff
  1346. /**
  1347. * Returns resolved reexport (localName and importName).
  1348. * @param {string} value value to resolve
  1349. * @param {string=} localName override local name
  1350. * @param {boolean=} isCustomProperty true when it is custom property, otherwise false
  1351. * @returns {string | [string, string] | [string, string, string]} resolved reexport (`localName`, `importName` and optional `request` of the active `@value` import)
  1352. */
  1353. const getReexport = (value, localName, isCustomProperty) => {
  1354. // No `@value` / `:import` / composes definitions: skip the `--` key
  1355. // concat + map probe (the common case for plain CSS-Modules files).
  1356. const reexport =
  1357. icssDefinitions.size === 0
  1358. ? undefined
  1359. : icssDefinitions.get(isCustomProperty ? `--${value}` : value);
  1360. if (reexport) {
  1361. if (reexport.importName) {
  1362. const resolvedLocalName =
  1363. reexport.localName || (isCustomProperty ? `--${value}` : value);
  1364. return reexport.request
  1365. ? [resolvedLocalName, reexport.importName, reexport.request]
  1366. : [resolvedLocalName, reexport.importName];
  1367. }
  1368. if (isCustomProperty) {
  1369. return /** @type {string} */ (reexport.value).slice(2);
  1370. }
  1371. return /** @type {string} */ (reexport.value);
  1372. }
  1373. if (localName) {
  1374. return [localName, value];
  1375. }
  1376. return value;
  1377. };
  1378. /**
  1379. * Process import or export, reusing the already-parsed rule nodes.
  1380. * @param {0 | 1} type import or export
  1381. * @param {AstNode} second the `import(…)` function / `export` ident node from the prelude
  1382. * @param {QualifiedRule} rule the `:import` / `:export` qualified rule
  1383. * @returns {number} position after parse
  1384. */
  1385. const processImportOrExport = (type, second, rule) => {
  1386. /** @type {string | undefined} */
  1387. let request;
  1388. if (type === 0) {
  1389. const parsed = parseIcssImportRequest(second, rule, source);
  1390. if ("errorPos" in parsed) {
  1391. const { errorPos } = parsed;
  1392. this._emitWarning(
  1393. state,
  1394. `Unexpected '${source[errorPos]}' at ${errorPos} during parsing of ':import' (expected string)`,
  1395. locConverter,
  1396. errorPos,
  1397. errorPos
  1398. );
  1399. return errorPos;
  1400. }
  1401. request = parsed.request;
  1402. }
  1403. /**
  1404. * Creates a dep from the provided name.
  1405. * @param {string} name name
  1406. * @param {string} value value
  1407. * @param {number} start start of position
  1408. * @param {number} end end of position
  1409. */
  1410. const createDep = (name, value, start, end) => {
  1411. if (type === 0) {
  1412. const dep = new CssIcssImportDependency(
  1413. /** @type {string} */
  1414. (request),
  1415. [0, 0],
  1416. /** @type {"local" | "global"} */
  1417. (mode),
  1418. value,
  1419. name
  1420. );
  1421. setDepLoc(dep, start, end);
  1422. module.addDependency(dep);
  1423. icssDefinitions.set(name, {
  1424. importName: value,
  1425. request: /** @type {string} */ (request)
  1426. });
  1427. } else if (type === 1) {
  1428. const { line: sl, column: sc } = locConverter.get(start);
  1429. const { line: el, column: ec } = locConverter.get(end);
  1430. recordDeclaration(name, ":export", sl, sc);
  1431. addCssExport(sl, sc, el, ec, name, getReexport(value));
  1432. }
  1433. };
  1434. // Body `{ name: value; … }` is parsed eagerly (§5.4.4) — emit a dep per declaration.
  1435. const ruleDecls = A.declarations(rule);
  1436. if (!ruleDecls || A.blockStart(rule) === -1) return A.end(second);
  1437. for (const decl of ruleDecls) {
  1438. const vals = A.children(decl);
  1439. if (vals.length === 0) continue;
  1440. const rawStart = A.start(vals[0]);
  1441. const rawEnd = A.end(vals[vals.length - 1]);
  1442. createDep(
  1443. source.slice(A.nameStart(decl), A.nameEnd(decl)),
  1444. source.slice(rawStart, rawEnd),
  1445. A.nameEnd(decl),
  1446. rawEnd
  1447. );
  1448. }
  1449. return skipWhiteLine(source, A.blockEnd(rule));
  1450. };
  1451. /**
  1452. * 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).
  1453. * @param {string} name ICSS symbol name
  1454. * @param {number} start start position
  1455. * @param {number} end end position
  1456. * @returns {number} `end` of the rewritten ident range
  1457. */
  1458. const emitICSSSymbol = (name, start, end) => {
  1459. const def =
  1460. /** @type {IcssDefinition} */
  1461. (icssDefinitions.get(name));
  1462. const { line: sl, column: sc } = locConverter.get(start);
  1463. const { line: el, column: ec } = locConverter.get(end);
  1464. const dep = new CssIcssSymbolDependency(
  1465. def.localName || name,
  1466. [start, end],
  1467. def.value,
  1468. def.importName,
  1469. def.request
  1470. );
  1471. dep.setLoc(sl, sc, el, ec);
  1472. module.addDependency(dep);
  1473. return end;
  1474. };
  1475. /**
  1476. * 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.
  1477. * @param {FunctionNode} fn parsed local/global function node
  1478. * @param {1 | 2} type 1 = local, 2 = global
  1479. * @param {string=} declarationKind human-readable export kind to use for conflict tracking when the caller is a declaration site (selector or at-rule prelude). Undefined for property-value references, which are not tracked.
  1480. */
  1481. const processLocalOrGlobalFunction = (fn, type, declarationKind) => {
  1482. // Replace `local(` / `global(` (and a leading `:` for the `:local(`/`:global(` selector form) with empty.
  1483. const fnStart = A.start(fn);
  1484. const isColon = input.charCodeAt(fnStart - 1) === CC_COLON;
  1485. const openEnd = A.nameEnd(fn) + 1;
  1486. module.addPresentationalDependency(
  1487. new ConstDependency("", [isColon ? fnStart - 1 : fnStart, openEnd])
  1488. );
  1489. if (type === 1) {
  1490. for (const cv of A.children(fn)) {
  1491. if (A.type(cv) !== NodeType.Ident) continue;
  1492. let identifier = A.unescaped(cv);
  1493. // Cursor reads instead of `A.loc` — no location objects allocated.
  1494. const { line: sl, column: sc } = locConverter.get(A.start(cv));
  1495. const { line: el, column: ec } = locConverter.get(A.end(cv));
  1496. const isDashedIdent = isDashedIdentifier(identifier);
  1497. if (isDashedIdent) identifier = identifier.slice(2);
  1498. if (declarationKind !== undefined) {
  1499. recordDeclaration(
  1500. identifier,
  1501. isDashedIdent ? "custom property" : declarationKind,
  1502. sl,
  1503. sc
  1504. );
  1505. }
  1506. addCssExport(
  1507. sl,
  1508. sc,
  1509. el,
  1510. ec,
  1511. identifier,
  1512. getReexport(identifier),
  1513. [A.start(cv), A.end(cv)],
  1514. true,
  1515. CssIcssExportDependency.EXPORT_MODE.ONCE,
  1516. isDashedIdent
  1517. ? CssIcssExportDependency.EXPORT_TYPE.CUSTOM_VARIABLE
  1518. : CssIcssExportDependency.EXPORT_TYPE.NORMAL
  1519. );
  1520. }
  1521. }
  1522. // Replace the closing `)`.
  1523. module.addPresentationalDependency(
  1524. new ConstDependency("", [A.end(fn) - 1, A.end(fn)])
  1525. );
  1526. };
  1527. /**
  1528. * 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.
  1529. * @param {AtRule} atRule parsed at-rule
  1530. * @param {{ string?: boolean, identifier?: boolean | RegExp }} options which prelude value kinds count as the local name
  1531. * @param {string} kind human-readable export kind for the surrounding at-rule (`"@keyframes"`, `"@counter-style"`, `"@container"`)
  1532. * @returns {number} position after handling
  1533. */
  1534. const processLocalAtRule = (atRule, options, kind) => {
  1535. let found = false;
  1536. for (const cv of A.prelude(atRule)) {
  1537. const cvType = A.type(cv);
  1538. if (cvType === NodeType.Whitespace) continue;
  1539. if (cvType === NodeType.String) {
  1540. if (!found && options.string) {
  1541. const value = A.unescaped(cv);
  1542. const { line: sl, column: sc } = locConverter.get(A.start(cv));
  1543. const { line: el, column: ec } = locConverter.get(A.end(cv));
  1544. recordDeclaration(value, kind, sl, sc);
  1545. addCssExport(
  1546. sl,
  1547. sc,
  1548. el,
  1549. ec,
  1550. value,
  1551. value,
  1552. [A.start(cv), A.end(cv)],
  1553. true,
  1554. CssIcssExportDependency.EXPORT_MODE.ONCE
  1555. );
  1556. found = true;
  1557. pure.markLocal();
  1558. }
  1559. continue;
  1560. }
  1561. if (cvType === NodeType.Ident) {
  1562. if (!found && options.identifier) {
  1563. const identifier = A.unescaped(cv);
  1564. if (
  1565. options.identifier instanceof RegExp &&
  1566. options.identifier.test(identifier)
  1567. ) {
  1568. continue;
  1569. }
  1570. const { line: sl, column: sc } = locConverter.get(A.start(cv));
  1571. const { line: el, column: ec } = locConverter.get(A.end(cv));
  1572. recordDeclaration(identifier, kind, sl, sc);
  1573. addCssExport(
  1574. sl,
  1575. sc,
  1576. el,
  1577. ec,
  1578. identifier,
  1579. getReexport(identifier),
  1580. [A.start(cv), A.end(cv)],
  1581. true,
  1582. CssIcssExportDependency.EXPORT_MODE.ONCE,
  1583. CssIcssExportDependency.EXPORT_TYPE.NORMAL
  1584. );
  1585. found = true;
  1586. pure.markLocal();
  1587. }
  1588. continue;
  1589. }
  1590. if (cvType === NodeType.Function) {
  1591. const fn = /** @type {FunctionNode} */ (cv);
  1592. const fname = A.unescapedName(fn);
  1593. const type = equalsLowerCase(fname, "local")
  1594. ? 1
  1595. : equalsLowerCase(fname, "global")
  1596. ? 2
  1597. : undefined;
  1598. if (!found && type) {
  1599. found = true;
  1600. if (type === 1) pure.markLocal();
  1601. processLocalOrGlobalFunction(fn, type, kind);
  1602. continue;
  1603. }
  1604. if (
  1605. this.options.dashedIdents &&
  1606. isLocalMode() &&
  1607. (equalsLowerCase(fname, "var") || equalsLowerCase(fname, "style"))
  1608. ) {
  1609. processDashedIdentInVarFunction(fn);
  1610. }
  1611. }
  1612. }
  1613. return A.end(atRule);
  1614. };
  1615. /**
  1616. * The custom property's name, without its `--`. An escaped dash makes the
  1617. * prefix longer than two bytes, so it comes off the unescaped value.
  1618. * @param {number} identStart start of the `--<name>` ident
  1619. * @param {number} identEnd end of the ident
  1620. * @returns {string} the name after `--`
  1621. */
  1622. const dashedIdentName = (identStart, identEnd) =>
  1623. source.charCodeAt(identStart) === CC_HYPHEN_MINUS &&
  1624. source.charCodeAt(identStart + 1) === CC_HYPHEN_MINUS
  1625. ? unescapeRange(identStart + 2, identEnd)
  1626. : unescapeRange(identStart, identEnd).slice(2);
  1627. /**
  1628. * Emit the ICSS export declaring this module exports the given custom property.
  1629. * @param {number} identStart start of the `--<name>` ident
  1630. * @param {number} identEnd end of the ident (exclusive)
  1631. */
  1632. const emitDashedIdentExport = (identStart, identEnd) => {
  1633. const identifier = dashedIdentName(identStart, identEnd);
  1634. const { line: sl, column: sc } = locConverter.get(identStart);
  1635. const { line: el, column: ec } = locConverter.get(identEnd);
  1636. recordDeclaration(identifier, "custom property", sl, sc);
  1637. addCssExport(
  1638. sl,
  1639. sc,
  1640. el,
  1641. ec,
  1642. identifier,
  1643. getReexport(identifier, undefined, true),
  1644. [identStart, identEnd],
  1645. true,
  1646. CssIcssExportDependency.EXPORT_MODE.ONCE,
  1647. CssIcssExportDependency.EXPORT_TYPE.CUSTOM_VARIABLE
  1648. );
  1649. };
  1650. /**
  1651. * 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).
  1652. * @param {number} identStart start of the `--<name>` ident
  1653. * @param {number} identEnd end of the ident
  1654. * @param {number} fromIdentStart start of the `from` keyword (lower bound of the strip)
  1655. * @param {number} sourceEnd position past the closing quote of the source string
  1656. * @param {string} pathContent unquoted path between the source's quotes
  1657. */
  1658. const emitDashedIdentImport = (
  1659. identStart,
  1660. identEnd,
  1661. fromIdentStart,
  1662. sourceEnd,
  1663. pathContent
  1664. ) => {
  1665. const identifier = dashedIdentName(identStart, identEnd);
  1666. const { line: sl, column: sc } = locConverter.get(identStart);
  1667. const { line: el, column: ec } = locConverter.get(sourceEnd - 1);
  1668. const localName = nextIcssImportName();
  1669. const importDep = new CssIcssImportDependency(
  1670. pathContent,
  1671. [identStart, sourceEnd - 1],
  1672. /** @type {"local" | "global"} */ (mode),
  1673. identifier,
  1674. localName
  1675. );
  1676. importDep.setLoc(sl, sc, el, ec);
  1677. module.addDependency(importDep);
  1678. recordDeclaration(identifier, "custom property", sl, sc);
  1679. addCssExport(
  1680. sl,
  1681. sc,
  1682. el,
  1683. ec,
  1684. identifier,
  1685. getReexport(identifier, localName, true),
  1686. [identStart, sourceEnd - 1],
  1687. true,
  1688. CssIcssExportDependency.EXPORT_MODE.ONCE,
  1689. CssIcssExportDependency.EXPORT_TYPE.CUSTOM_VARIABLE
  1690. );
  1691. module.addPresentationalDependency(
  1692. new ConstDependency("", [fromIdentStart, sourceEnd])
  1693. );
  1694. };
  1695. /**
  1696. * Strip ` from global` and emit no ICSS export (an explicitly-global custom property isn't a CSS-Modules name).
  1697. * @param {number} identEnd end of the `--<name>` ident
  1698. * @param {number} sourceEnd position past the `global` ident
  1699. */
  1700. const emitDashedIdentFromGlobal = (identEnd, sourceEnd) => {
  1701. module.addPresentationalDependency(
  1702. new ConstDependency("", [identEnd, sourceEnd])
  1703. );
  1704. };
  1705. /**
  1706. * Scope a dashed-ident inside `var(…)` / `style(…)`: emit the first (dashed) ident and its optional `from <ident|string>` suffix.
  1707. * @param {FunctionNode} fn parsed `var`/`style` function node
  1708. */
  1709. // 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.
  1710. const processDashedIdentInVarFunction = (fn) => {
  1711. /** @type {Token | undefined} */
  1712. let identNode;
  1713. let identIdx = -1;
  1714. const fv = A.children(fn);
  1715. for (let i = 0; i < fv.length; i++) {
  1716. const cv = fv[i];
  1717. if (A.type(cv) === NodeType.Whitespace) continue;
  1718. if (A.type(cv) === NodeType.Ident) {
  1719. identNode = /** @type {Token} */ (cv);
  1720. identIdx = i;
  1721. }
  1722. break;
  1723. }
  1724. if (!identNode) return;
  1725. const identStart = A.start(identNode);
  1726. const identEnd = A.end(identNode);
  1727. if (!rangeIsDashedIdentifier(input, identStart, identEnd)) return;
  1728. let j = identIdx + 1;
  1729. while (j < fv.length && A.type(fv[j]) === NodeType.Whitespace) {
  1730. j++;
  1731. }
  1732. if (
  1733. j >= fv.length ||
  1734. A.type(fv[j]) !== NodeType.Ident ||
  1735. !rangeEqualsLowerCase(input, A.start(fv[j]), A.end(fv[j]), "from")
  1736. ) {
  1737. emitDashedIdentExport(identStart, identEnd);
  1738. return;
  1739. }
  1740. const fromIdent = fv[j];
  1741. j++;
  1742. while (j < fv.length && A.type(fv[j]) === NodeType.Whitespace) {
  1743. j++;
  1744. }
  1745. if (j >= fv.length) return;
  1746. const src = fv[j];
  1747. if (
  1748. A.type(src) === NodeType.Ident &&
  1749. rangeEquals(input, A.start(src), A.end(src), "global")
  1750. ) {
  1751. emitDashedIdentFromGlobal(identEnd, A.end(src));
  1752. return;
  1753. }
  1754. if (A.type(src) === NodeType.String) {
  1755. emitDashedIdentImport(
  1756. identStart,
  1757. identEnd,
  1758. A.start(fromIdent),
  1759. A.end(src),
  1760. input.slice(A.start(src) + 1, A.end(src) - 1)
  1761. );
  1762. }
  1763. };
  1764. // `allowImport` mirrors `allowImportAtRule`: true until the first top-level block-bearing rule.
  1765. let allowImport = true;
  1766. // Persistent CSS-Modules mode for a top-level rule: set by bare `:local` / `:global`, leaks into sibling rules, reset at each top-level `}`.
  1767. /** @type {"local" | "global" | undefined} */
  1768. let modeData;
  1769. // Suppress localizing the next qualified rule's selectors after a `;`-terminated at-rule.
  1770. let suppressNextRulePrelude = false;
  1771. // Dashed-ident (custom-property) scoping state — mutated across function nesting (saved/restored via `dashed.stack`).
  1772. const dashed = {
  1773. /** Is dashed-ident scoping active in the current value context? */
  1774. active: false,
  1775. /** Should top-level dashed-ident exports be emitted at this nesting level? */
  1776. emit: false,
  1777. /** Directly inside a `counter()`-family call whose arguments were already rewritten — suppresses the generic ICSS ident rewrite over the same ranges. */
  1778. counterArgs: false,
  1779. /** LIFO save/restore of `active` + `emit` + `counterArgs` across function nesting, bit-packed (bit 0 = active, bit 1 = emit, bit 2 = counterArgs) to avoid a per-function-token object allocation. */
  1780. /** @type {number[]} */
  1781. stack: [],
  1782. /** Push the current scope; call before descending into a `Function` body. */
  1783. push() {
  1784. this.stack.push(
  1785. (this.active ? 1 : 0) |
  1786. (this.emit ? 2 : 0) |
  1787. (this.counterArgs ? 4 : 0)
  1788. );
  1789. },
  1790. /** Pop the saved scope; call when leaving a `Function` body. */
  1791. pop() {
  1792. const s = /** @type {number} */ (this.stack.pop());
  1793. this.active = (s & 1) !== 0;
  1794. this.emit = (s & 2) !== 0;
  1795. this.counterArgs = (s & 4) !== 0;
  1796. }
  1797. };
  1798. // 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.
  1799. /** @type {AstNode | undefined} */
  1800. let currentStructural;
  1801. // 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.
  1802. let currentAtRuleName = "";
  1803. // 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).
  1804. let currentUrlRecovery = false;
  1805. /** Whether the current Declaration's property is a localizable known property. */
  1806. let currentDeclIsKnownProperty = false;
  1807. /** Whether the current `composes:` declaration owns the whole value (suppresses value rewrites). */
  1808. let currentDeclComposesSkip = false;
  1809. /** Whether `counter()`-family arguments are scoped in the current declaration's value. */
  1810. let currentDeclCounterActive = false;
  1811. /**
  1812. * Per-at-rule scope frames; `exit` reads `hasBlock` to pick the block-cleanup vs `suppressNextRulePrelude` branch.
  1813. * @type {{ savedAnchor: boolean, savedLocalIdentifierCount: number, name: string, hasBlock: boolean, endsWithSemicolon: boolean, fontPreloaded: boolean }[]}
  1814. */
  1815. const atRuleStateStack = [];
  1816. /**
  1817. * 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).
  1818. * @param {AstNode} colon the `:` node
  1819. * @param {AstNode} ident the `local` / `global` ident node
  1820. * @param {AstNode} after the node following the ident (may be `undefined` at runtime)
  1821. * @returns {boolean} whether whitespace follows the marker
  1822. */
  1823. const stripBareMarker = (colon, ident, after) => {
  1824. const afterIsWhitespace = Boolean(
  1825. after && A.type(after) === NodeType.Whitespace
  1826. );
  1827. const identEnd = A.end(ident);
  1828. const stripEnd =
  1829. afterIsWhitespace && A.start(after) === identEnd
  1830. ? A.end(after)
  1831. : identEnd;
  1832. if (isModules) {
  1833. module.addPresentationalDependency(
  1834. new ConstDependency("", [A.start(colon), stripEnd])
  1835. );
  1836. }
  1837. return afterIsWhitespace;
  1838. };
  1839. /**
  1840. * 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).
  1841. * @param {AstNode} colon the `:` node
  1842. * @param {FunctionNode} fn the `local(…)` / `global(…)` function node
  1843. * @param {boolean} isLocal whether the marker is `:local(`
  1844. * @returns {void}
  1845. */
  1846. const stripFunctionMarker = (colon, fn, isLocal) => {
  1847. if (!isModules) return;
  1848. const fnEnd = A.end(fn);
  1849. let stripLeadEnd = fnEnd - 1;
  1850. for (const arg of A.children(fn)) {
  1851. if (A.type(arg) !== NodeType.Whitespace) {
  1852. stripLeadEnd = A.start(arg);
  1853. break;
  1854. }
  1855. }
  1856. module.addPresentationalDependency(
  1857. new ConstDependency("", [A.start(colon), stripLeadEnd])
  1858. );
  1859. let trailStart = fnEnd - 1; // position of `)`
  1860. if (isLocal) {
  1861. while (
  1862. trailStart > 0 &&
  1863. isWhitespace(source.charCodeAt(trailStart - 1))
  1864. ) {
  1865. trailStart--;
  1866. }
  1867. }
  1868. module.addPresentationalDependency(
  1869. new ConstDependency("", [trailStart, fnEnd])
  1870. );
  1871. };
  1872. /**
  1873. * 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.
  1874. * @param {SimpleBlock} block the `[…]` block
  1875. * @returns {void}
  1876. */
  1877. const handleAttributeSelector = (block) => {
  1878. const attrParts = A.children(block);
  1879. let ai = 0;
  1880. while (
  1881. ai < attrParts.length &&
  1882. A.type(attrParts[ai]) === NodeType.Whitespace
  1883. ) {
  1884. ai++;
  1885. }
  1886. const attrNameNode = attrParts[ai];
  1887. if (!attrNameNode || A.type(attrNameNode) !== NodeType.Ident) return;
  1888. const attrName = A.unescaped(attrNameNode);
  1889. if (!equalsLowerCase(attrName, "class")) return;
  1890. ai++;
  1891. while (
  1892. ai < attrParts.length &&
  1893. A.type(attrParts[ai]) === NodeType.Whitespace
  1894. ) {
  1895. ai++;
  1896. }
  1897. // `=` or `~=` (two `Delim` tokens for the latter).
  1898. const op1 = attrParts[ai];
  1899. if (!op1 || A.type(op1) !== NodeType.Delim) return;
  1900. const op1v = A.value(op1);
  1901. if (op1v === "~") {
  1902. ai++;
  1903. const op2 = attrParts[ai];
  1904. if (!op2 || A.type(op2) !== NodeType.Delim || A.value(op2) !== "=") {
  1905. return;
  1906. }
  1907. } else if (op1v !== "=") {
  1908. return;
  1909. }
  1910. ai++;
  1911. while (
  1912. ai < attrParts.length &&
  1913. A.type(attrParts[ai]) === NodeType.Whitespace
  1914. ) {
  1915. ai++;
  1916. }
  1917. const attrValueNode = attrParts[ai];
  1918. if (!attrValueNode) return;
  1919. /** @type {number} */
  1920. let classNameStart;
  1921. /** @type {number} */
  1922. let classNameEnd;
  1923. if (A.type(attrValueNode) === NodeType.String) {
  1924. classNameStart = A.start(attrValueNode) + 1;
  1925. classNameEnd = A.end(attrValueNode) - 1;
  1926. } else if (A.type(attrValueNode) === NodeType.Ident) {
  1927. classNameStart = A.start(attrValueNode);
  1928. classNameEnd = A.end(attrValueNode);
  1929. } else {
  1930. return;
  1931. }
  1932. const className = unescapeRange(classNameStart, classNameEnd);
  1933. const { line: sl, column: sc } = locConverter.get(classNameStart);
  1934. const { line: el, column: ec } = locConverter.get(classNameEnd);
  1935. addCssExport(
  1936. sl,
  1937. sc,
  1938. el,
  1939. ec,
  1940. className,
  1941. getReexport(className),
  1942. [classNameStart, classNameEnd],
  1943. true,
  1944. CssIcssExportDependency.EXPORT_MODE.NONE
  1945. );
  1946. };
  1947. /**
  1948. * 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.
  1949. * @param {AstNode[]} cvs the pseudo-element's argument component values
  1950. * @returns {void}
  1951. */
  1952. const walkViewTransitionPart = (cvs) => {
  1953. // customIdents off: leave the whole `(…)` untouched (both name and classes), consistent with unscoped `view-transition-name`/`-class` declarations.
  1954. if (!this.options.customIdents) return;
  1955. for (const cv of cvs) {
  1956. if (A.type(cv) !== NodeType.Ident) continue;
  1957. const name = A.unescaped(cv);
  1958. if (isDashedIdentifier(name)) continue;
  1959. const start = A.start(cv);
  1960. const end = A.end(cv);
  1961. const { line: sl, column: sc } = locConverter.get(start);
  1962. const { line: el, column: ec } = locConverter.get(end);
  1963. addCssExport(
  1964. sl,
  1965. sc,
  1966. el,
  1967. ec,
  1968. name,
  1969. getReexport(name),
  1970. [start, end],
  1971. true,
  1972. CssIcssExportDependency.EXPORT_MODE.ONCE
  1973. );
  1974. pure.markLocal();
  1975. }
  1976. };
  1977. /**
  1978. * Emit the ICSS export for one ident argument of a `counter()`-family call. Dashed names are left to the blanket dashed-ident scanner, and reserved names (UA counters, predefined counter styles, CSS-wide keywords) stay global.
  1979. * @param {AstNode} node the ident node
  1980. * @param {string} reservedFrom property whose keywords must not be localized here
  1981. * @returns {void}
  1982. */
  1983. const emitCounterIdent = (node, reservedFrom) => {
  1984. const name = A.unescaped(node);
  1985. if (isDashedIdentifier(name)) return;
  1986. const reserved =
  1987. /** @type {Map<string, number>} */
  1988. (CSS_MODULES_KEYWORDS.get(reservedFrom));
  1989. if (reserved.has(toLowerCaseIfNeeded(name))) return;
  1990. const start = A.start(node);
  1991. const end = A.end(node);
  1992. const { line: sl, column: sc } = locConverter.get(start);
  1993. const { line: el, column: ec } = locConverter.get(end);
  1994. addCssExport(
  1995. sl,
  1996. sc,
  1997. el,
  1998. ec,
  1999. name,
  2000. getReexport(name),
  2001. [start, end],
  2002. true,
  2003. CssIcssExportDependency.EXPORT_MODE.ONCE
  2004. );
  2005. };
  2006. /**
  2007. * Scope a `counter()` / `counters()` / `target-counter()` / `target-counters()` call: the counter-name argument localizes like a `counter-reset` value and a trailing `<counter-style>` ident like a `list-style-type` value, so both keep naming the scoped `counter-*` declaration / `@counter-style` rule.
  2008. * @param {AstNode[]} cvs the call's argument component values
  2009. * @param {number} nameIndex comma-separated index of the counter-name argument
  2010. * @returns {void}
  2011. */
  2012. const walkCounterFunction = (cvs, nameIndex) => {
  2013. let group = 0;
  2014. let groupNodes = 0;
  2015. /** Sole ident of the current argument, or undefined when the argument isn't a lone ident. */
  2016. let lastIdent;
  2017. for (const cv of cvs) {
  2018. const type = A.type(cv);
  2019. if (type === NodeType.Whitespace) continue;
  2020. if (type === NodeType.Comma) {
  2021. group++;
  2022. groupNodes = 0;
  2023. lastIdent = undefined;
  2024. continue;
  2025. }
  2026. groupNodes++;
  2027. const loneIdent = groupNodes === 1 && type === NodeType.Ident;
  2028. if (group === nameIndex) {
  2029. if (loneIdent) emitCounterIdent(cv, "counter-reset");
  2030. lastIdent = undefined;
  2031. } else {
  2032. lastIdent = loneIdent ? cv : undefined;
  2033. }
  2034. }
  2035. // The counter style is the last argument when it's a lone ident (`counter(n, style)` / `counters(n, sep, style)`).
  2036. if (lastIdent !== undefined && group > nameIndex) {
  2037. emitCounterIdent(lastIdent, "list-style-type");
  2038. }
  2039. };
  2040. /**
  2041. * 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).
  2042. * @param {AstNode[]} values component values to walk
  2043. * @param {"local" | "global"} localMode CSS-Modules mode applicable to this sub-tree
  2044. * @param {boolean=} topLevel whether commas in this list reset to `localMode` (defaults to `true`)
  2045. * @returns {void}
  2046. */
  2047. const walkSelectorList = (values, localMode, topLevel = true) => {
  2048. // At a rule's top level, inherit persistent `modeData` (or the one-shot `suppressNextRulePrelude` → "global"); recursive calls use the passed `localMode`.
  2049. let segmentMode = localMode;
  2050. if (topLevel) {
  2051. if (suppressNextRulePrelude) {
  2052. segmentMode = "global";
  2053. suppressNextRulePrelude = false;
  2054. } else if (modeData) {
  2055. segmentMode = modeData;
  2056. }
  2057. }
  2058. for (let i = 0; i < values.length; i++) {
  2059. const v = values[i];
  2060. switch (A.type(v)) {
  2061. case NodeType.Whitespace:
  2062. break;
  2063. case NodeType.Comma:
  2064. if (topLevel) {
  2065. // Top-level comma resets the segment + persistent mode and, in pure mode, finalizes the segment's purity.
  2066. segmentMode = localMode;
  2067. modeData = undefined;
  2068. pure.finalizeSelector();
  2069. }
  2070. break;
  2071. case NodeType.Colon: {
  2072. // Look ahead for `:local` / `:global` markers; other pseudos fall through.
  2073. const next = values[i + 1];
  2074. if (!next) break;
  2075. const nextType = A.type(next);
  2076. if (nextType === NodeType.Ident) {
  2077. const raw = A.value(next);
  2078. const isLocal = equalsLowerCase(raw, "local");
  2079. if (isLocal || equalsLowerCase(raw, "global")) {
  2080. const id = isLocal ? "local" : "global";
  2081. // Bare `:local` / `:global`: switch the segment (and top-level persistent) mode and strip the marker.
  2082. const afterIsWhitespace = stripBareMarker(
  2083. v,
  2084. next,
  2085. values[i + 2]
  2086. );
  2087. // Bare `:local` / `:global` needs whitespace before the next selector (else `:local.b` is ambiguous) — warn when none follows.
  2088. if (!afterIsWhitespace) {
  2089. this._emitWarning(
  2090. state,
  2091. `Missing whitespace after ':${id}' in '${source.slice(
  2092. A.start(v),
  2093. findLeftCurly(source, A.end(next)) + 1
  2094. )}'`,
  2095. locConverter,
  2096. A.start(v),
  2097. A.end(next)
  2098. );
  2099. }
  2100. segmentMode = id;
  2101. if (topLevel) modeData = id;
  2102. // Skip past the colon + ident.
  2103. i += 1;
  2104. }
  2105. } else if (nextType === NodeType.Function) {
  2106. const fn = /** @type {FunctionNode} */ (next);
  2107. const rawName = A.unescapedName(fn);
  2108. const isLocal = equalsLowerCase(rawName, "local");
  2109. if (isLocal || equalsLowerCase(rawName, "global")) {
  2110. // `:local(…)` / `:global(…)`: scope mode to the args and strip the `:name(` … `)` wrapper.
  2111. stripFunctionMarker(v, fn, isLocal);
  2112. walkSelectorList(
  2113. A.children(fn),
  2114. isLocal ? "local" : "global",
  2115. false
  2116. );
  2117. // Skip past the colon + function.
  2118. i += 1;
  2119. }
  2120. }
  2121. break;
  2122. }
  2123. case NodeType.Function: {
  2124. // `::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).
  2125. const fnNameStart = A.nameStart(v);
  2126. if (
  2127. segmentMode === "local" &&
  2128. (input.charCodeAt(fnNameStart) === CC_LOWER_V ||
  2129. input.charCodeAt(fnNameStart) === CC_UPPER_V) &&
  2130. VIEW_TRANSITION_PART_PSEUDO.test(A.unescapedName(v))
  2131. ) {
  2132. walkViewTransitionPart(A.children(v));
  2133. break;
  2134. }
  2135. // Any other function (`:not(…)`, `:is(…)`, …): recurse with the segment mode preserved (only `:local(…)` / `:global(…)`, handled above, switch mode).
  2136. walkSelectorList(A.children(v), segmentMode, false);
  2137. break;
  2138. }
  2139. case NodeType.Hash: {
  2140. if (A.typeFlag(v) !== "id" || segmentMode !== "local") {
  2141. break;
  2142. }
  2143. // ID selectors emit the ICSS export but aren't a `composes:` anchor.
  2144. const idValueStart = A.start(v) + 1;
  2145. const idName = A.unescaped(v);
  2146. const { line: idSl, column: idSc } = locConverter.get(A.start(v));
  2147. const { line: idEl, column: idEc } = locConverter.get(A.end(v));
  2148. recordDeclaration(idName, "id", idSl, idSc);
  2149. addCssExport(
  2150. idSl,
  2151. idSc,
  2152. idEl,
  2153. idEc,
  2154. idName,
  2155. getReexport(idName),
  2156. [idValueStart, A.end(v)],
  2157. true,
  2158. CssIcssExportDependency.EXPORT_MODE.ONCE
  2159. );
  2160. pure.markLocal();
  2161. break;
  2162. }
  2163. case NodeType.SimpleBlock: {
  2164. const block = /** @type {SimpleBlock} */ (v);
  2165. const bt = A.blockToken(block);
  2166. if (bt === "[") {
  2167. // Attribute selectors `[class="foo"]` localize only in local mode.
  2168. if (segmentMode === "local") handleAttributeSelector(block);
  2169. } else if (bt === "(") {
  2170. // `@scope (.foo)` and other parenthesised selector wrappers — recurse into the `(…)` block.
  2171. walkSelectorList(A.children(block), segmentMode, false);
  2172. }
  2173. break;
  2174. }
  2175. case NodeType.Delim: {
  2176. const delim = A.value(v);
  2177. if (delim === "&") {
  2178. // Pure-mode: a nesting `&` inherits a pure ancestor's purity.
  2179. if (topLevel && pure.ancestorHadLocal()) pure.markLocal();
  2180. break;
  2181. }
  2182. if (delim !== ".") break;
  2183. const next = values[i + 1];
  2184. if (!next || A.type(next) !== NodeType.Ident) break;
  2185. if (segmentMode === "local") {
  2186. // `.<ident>` in local mode is a class selector (dep covers the ident bytes only).
  2187. const name = A.unescaped(next);
  2188. // Cursor reads instead of `A.loc` — this is the hottest export site.
  2189. const { line: sl, column: sc } = locConverter.get(A.start(next));
  2190. const { line: el, column: ec } = locConverter.get(A.end(next));
  2191. recordDeclaration(name, "class", sl, sc);
  2192. addCssExport(
  2193. sl,
  2194. sc,
  2195. el,
  2196. ec,
  2197. name,
  2198. getReexport(name),
  2199. [A.start(next), A.end(next)],
  2200. true,
  2201. CssIcssExportDependency.EXPORT_MODE.ONCE
  2202. );
  2203. currentRule.hasLocalAnchor = true;
  2204. currentRule.localIdentifiers.push(name);
  2205. pure.markLocal();
  2206. } else if (icssDefinitions.size !== 0) {
  2207. // `.<ident>` in global mode: not localized, but the ident may be `@value`-defined and need ICSS rewrite.
  2208. const ident = A.value(next);
  2209. if (!isDashedIdentifier(ident) && icssDefinitions.has(ident)) {
  2210. emitICSSSymbol(ident, A.start(next), A.end(next));
  2211. }
  2212. }
  2213. // Skip the consumed ident.
  2214. i += 1;
  2215. break;
  2216. }
  2217. case NodeType.Ident: {
  2218. // ICSS rewrite for a bare `@value`-defined ident used as a type-style selector; only worth the slice when definitions exist.
  2219. if (icssDefinitions.size === 0) break;
  2220. const ident = A.value(v);
  2221. if (!isDashedIdentifier(ident) && icssDefinitions.has(ident)) {
  2222. emitICSSSymbol(ident, A.start(v), A.end(v));
  2223. }
  2224. break;
  2225. }
  2226. default:
  2227. break;
  2228. }
  2229. }
  2230. // Pure-mode: finalize the trailing comma-separated segment.
  2231. if (topLevel) pure.finalizeSelector();
  2232. };
  2233. /**
  2234. * Per-qualified-rule scope frames; `{ bailed: true }` for inline-handled `:import` / `:export` pseudo-rules.
  2235. * @type {({ bailed: true } | { bailed: false, savedAnchor: boolean, savedLocalIdentifierCount: number, savedPrevComposesFile: string | undefined, savedComposesFiles: Set<string> | null })[]}
  2236. */
  2237. const qualifiedRuleStateStack = [];
  2238. /**
  2239. * Whether url() deps are emitted here (from `currentStructural`): off in an `@import`
  2240. * prelude (the import target, unless recovering) and in `@namespace` (an opaque identifier).
  2241. * @returns {boolean} true when url() deps should be emitted
  2242. */
  2243. const urlActive = () => {
  2244. if (!this.options.url || !currentStructural) return false;
  2245. if (A.type(currentStructural) === NodeType.AtRule) {
  2246. // `currentAtRuleName` is cached on at-rule enter — no per-value slice.
  2247. if (currentAtRuleName === "@namespace") return false;
  2248. return currentAtRuleName !== "@import" || currentUrlRecovery;
  2249. }
  2250. return true;
  2251. };
  2252. /**
  2253. * At-rules with a dedicated CSS-Modules handler, so the generic `local()`/`global()` value rewrite and dashed-ident scoping are off for them.
  2254. * @param {string} name at-rule name including the leading `@`, lower-cased
  2255. * @returns {boolean} true for `@import` / `@charset` / `@namespace` / `@value` / `@scope` and the option-gated `@keyframes` / `@counter-style` / `@container`
  2256. */
  2257. const isLocalHandledAtRule = (name) =>
  2258. name === "@import" ||
  2259. name === "@charset" ||
  2260. name === "@namespace" ||
  2261. name === "@value" ||
  2262. name === "@scope" ||
  2263. (mayHaveCustomMedia && name === "@custom-media") ||
  2264. (mayHaveCustomSelectors && name === "@custom-selector") ||
  2265. (isModules &&
  2266. ((this.options.animation &&
  2267. OPTIONALLY_VENDOR_PREFIXED_KEYFRAMES_AT_RULE.test(name)) ||
  2268. (this.options.customIdents && name === "@counter-style") ||
  2269. (this.options.container && name === "@container")));
  2270. /**
  2271. * Whether `local()` / `global()` value functions are rewritten to ICSS here (from `currentStructural`).
  2272. * @returns {boolean} true when the rewrite is active
  2273. */
  2274. const localGlobalActive = () => {
  2275. if (!isModules || !currentStructural) return false;
  2276. const t = A.type(currentStructural);
  2277. if (t === NodeType.AtRule) {
  2278. return !isLocalHandledAtRule(currentAtRuleName);
  2279. }
  2280. if (t === NodeType.Declaration) {
  2281. return !currentDeclComposesSkip;
  2282. }
  2283. return false;
  2284. };
  2285. /**
  2286. * Whether `@value`-defined idents / function names are ICSS-rewritten here (from `currentStructural`).
  2287. * @returns {boolean} true when the rewrite is active
  2288. */
  2289. const icssActive = () => {
  2290. if (!isModules || !currentStructural) return false;
  2291. const t = A.type(currentStructural);
  2292. if (t === NodeType.AtRule) {
  2293. return (
  2294. currentAtRuleName !== "@value" &&
  2295. currentAtRuleName !== "@import" &&
  2296. currentAtRuleName !== "@custom-media" &&
  2297. currentAtRuleName !== "@custom-selector"
  2298. );
  2299. }
  2300. if (t === NodeType.Declaration) {
  2301. return !currentDeclComposesSkip && !currentDeclIsKnownProperty;
  2302. }
  2303. return false;
  2304. };
  2305. /**
  2306. * 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.
  2307. * @param {AstNode} node the rule node
  2308. * @param {boolean} blockBearing whether to require a body before locking `allowImport` (true for at-rules, ignored for QRs which always have a body)
  2309. * @returns {void}
  2310. */
  2311. const finishTopLevelRule = (node, blockBearing) => {
  2312. if (!blockBearing || A.declarations(node) || A.childRules(node)) {
  2313. allowImport = false;
  2314. }
  2315. pure.markSeenTopLevelRule();
  2316. modeData = undefined;
  2317. };
  2318. /**
  2319. * 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.
  2320. * @param {{ start: number, end: number, isGlobal: boolean }[]} classNames composed names with source ranges
  2321. * @param {{ kind: "string", path: string } | { kind: "global" } | undefined} fromSource the `from …` clause, if any
  2322. * @param {string} lastLocalIdentifier the rule's single local-class anchor
  2323. * @returns {void}
  2324. */
  2325. const emitComposesGroup = (classNames, fromSource, lastLocalIdentifier) => {
  2326. if (fromSource && fromSource.kind === "string") {
  2327. const request = fromSource.path;
  2328. const selfReference = isSelfReferenceRequest(request);
  2329. if (!selfReference) {
  2330. let files = currentRule.composesFiles;
  2331. if (!files) {
  2332. files = new Set();
  2333. currentRule.composesFiles = files;
  2334. }
  2335. if (!files.has(request)) {
  2336. files.add(request);
  2337. if (
  2338. currentRule.composesPrevFile !== undefined &&
  2339. currentRule.composesPrevFile !== request
  2340. ) {
  2341. let successors = composesGraph.get(currentRule.composesPrevFile);
  2342. if (!successors) {
  2343. successors = new Set();
  2344. composesGraph.set(currentRule.composesPrevFile, successors);
  2345. }
  2346. successors.add(request);
  2347. }
  2348. currentRule.composesPrevFile = request;
  2349. }
  2350. }
  2351. for (const { start, end } of classNames) {
  2352. const identifier = unescapeRange(start, end);
  2353. const { line: sl, column: sc } = locConverter.get(start);
  2354. const { line: el, column: ec } = locConverter.get(end);
  2355. if (selfReference) {
  2356. if (identifier === lastLocalIdentifier) continue;
  2357. addCssExport(
  2358. sl,
  2359. sc,
  2360. el,
  2361. ec,
  2362. lastLocalIdentifier,
  2363. getReexport(identifier),
  2364. [start, end],
  2365. true,
  2366. CssIcssExportDependency.EXPORT_MODE.SELF_REFERENCE,
  2367. CssIcssExportDependency.EXPORT_TYPE.COMPOSES
  2368. );
  2369. continue;
  2370. }
  2371. const localName = nextIcssImportName();
  2372. const importDep = new CssIcssImportDependency(
  2373. request,
  2374. [start, end],
  2375. /** @type {"local" | "global"} */ (mode),
  2376. identifier,
  2377. localName
  2378. );
  2379. importDep.setLoc(sl, sc, el, ec);
  2380. module.addDependency(importDep);
  2381. if (!composesFirstFileImport.has(request)) {
  2382. composesFirstFileImport.set(request, importDep);
  2383. }
  2384. addCssExport(
  2385. sl,
  2386. sc,
  2387. el,
  2388. ec,
  2389. lastLocalIdentifier,
  2390. getReexport(identifier, localName),
  2391. [start, end],
  2392. true,
  2393. CssIcssExportDependency.EXPORT_MODE.APPEND,
  2394. CssIcssExportDependency.EXPORT_TYPE.COMPOSES
  2395. );
  2396. }
  2397. } else if (fromSource && fromSource.kind === "global") {
  2398. for (const { start, end } of classNames) {
  2399. const identifier = unescapeRange(start, end);
  2400. const { line: sl, column: sc } = locConverter.get(start);
  2401. const { line: el, column: ec } = locConverter.get(end);
  2402. addCssExport(
  2403. sl,
  2404. sc,
  2405. el,
  2406. ec,
  2407. lastLocalIdentifier,
  2408. getReexport(identifier),
  2409. [start, end],
  2410. false,
  2411. CssIcssExportDependency.EXPORT_MODE.APPEND,
  2412. CssIcssExportDependency.EXPORT_TYPE.COMPOSES
  2413. );
  2414. }
  2415. } else {
  2416. for (const { start, end, isGlobal } of classNames) {
  2417. const identifier = unescapeRange(start, end);
  2418. const { line: sl, column: sc } = locConverter.get(start);
  2419. const { line: el, column: ec } = locConverter.get(end);
  2420. addCssExport(
  2421. sl,
  2422. sc,
  2423. el,
  2424. ec,
  2425. lastLocalIdentifier,
  2426. getReexport(identifier),
  2427. [start, end],
  2428. !isGlobal,
  2429. isGlobal
  2430. ? CssIcssExportDependency.EXPORT_MODE.APPEND
  2431. : CssIcssExportDependency.EXPORT_MODE.SELF_REFERENCE,
  2432. CssIcssExportDependency.EXPORT_TYPE.COMPOSES
  2433. );
  2434. }
  2435. }
  2436. };
  2437. /**
  2438. * 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).
  2439. * @param {Declaration} decl the `composes` declaration
  2440. */
  2441. const emitComposesWithAnchor = (decl) => {
  2442. if (currentRule.localIdentifiers.length > 1) {
  2443. this._emitWarning(
  2444. state,
  2445. `Composition is only allowed when selector is single local class name not in "${currentRule.localIdentifiers.join(
  2446. '", "'
  2447. )}"`,
  2448. locConverter,
  2449. A.start(decl),
  2450. A.end(decl)
  2451. );
  2452. return;
  2453. }
  2454. const lastLocalIdentifier = currentRule.localIdentifiers[0];
  2455. // Split the value at top-level commas — each segment is one `<name>+ [from <source>]` group.
  2456. /** @type {AstNode[][]} */
  2457. const groups = [];
  2458. /** @type {AstNode[]} */
  2459. let currentGroup = [];
  2460. for (const cv of A.children(decl)) {
  2461. if (A.type(cv) === NodeType.Comma) {
  2462. groups.push(currentGroup);
  2463. currentGroup = [];
  2464. } else {
  2465. currentGroup.push(cv);
  2466. }
  2467. }
  2468. groups.push(currentGroup);
  2469. // Inline scan + dispatch per group — warm path (composes-heavy modules), so no per-group result object is allocated.
  2470. for (const group of groups) {
  2471. /** @type {{ start: number, end: number, isGlobal: boolean }[]} */
  2472. const classNames = [];
  2473. /** @type {"names" | "expecting-source" | "done"} */
  2474. let phase = "names";
  2475. /** @type {{ kind: "string", path: string } | { kind: "global" } | undefined} */
  2476. let fromSource;
  2477. /** @type {AstNode | undefined} */
  2478. let errorToken;
  2479. let errorMessage = "";
  2480. for (let i = 0; i < group.length; i++) {
  2481. const cv = group[i];
  2482. if (A.type(cv) === NodeType.Whitespace) continue;
  2483. if (phase === "expecting-source") {
  2484. if (A.type(cv) === NodeType.String) {
  2485. fromSource = {
  2486. kind: "string",
  2487. path: source.slice(A.start(cv) + 1, A.end(cv) - 1)
  2488. };
  2489. phase = "done";
  2490. continue;
  2491. }
  2492. if (
  2493. A.type(cv) === NodeType.Ident &&
  2494. equalsLowerCase(A.value(cv), "global")
  2495. ) {
  2496. fromSource = { kind: "global" };
  2497. phase = "done";
  2498. continue;
  2499. }
  2500. errorToken = cv;
  2501. errorMessage =
  2502. "Incorrect composition, expected global keyword or string value";
  2503. break;
  2504. }
  2505. if (phase === "done") {
  2506. continue;
  2507. }
  2508. if (A.type(cv) === NodeType.Ident) {
  2509. const identValue = A.value(cv);
  2510. if (
  2511. equalsLowerCase(identValue, "from") &&
  2512. classNames.length > 0 &&
  2513. nextNonWhitespace(group, i + 1) < group.length
  2514. ) {
  2515. phase = "expecting-source";
  2516. continue;
  2517. }
  2518. classNames.push({
  2519. start: A.start(cv),
  2520. end: A.end(cv),
  2521. isGlobal: false
  2522. });
  2523. continue;
  2524. }
  2525. if (A.type(cv) === NodeType.Function) {
  2526. const fn = /** @type {FunctionNode} */ (cv);
  2527. const isGlobal = equalsLowerCase(A.unescapedName(fn), "global");
  2528. for (const inner of A.children(fn)) {
  2529. if (A.type(inner) === NodeType.Ident) {
  2530. classNames.push({
  2531. start: A.start(inner),
  2532. end: A.end(inner),
  2533. isGlobal
  2534. });
  2535. break;
  2536. }
  2537. }
  2538. continue;
  2539. }
  2540. errorToken = cv;
  2541. errorMessage = "Incorrect composition, expected class named";
  2542. break;
  2543. }
  2544. if (!errorToken && phase === "expecting-source") {
  2545. errorMessage =
  2546. "Incorrect composition, expected global keyword or string value";
  2547. errorToken = /** @type {AstNode | undefined} */ (
  2548. group[group.length - 1]
  2549. );
  2550. }
  2551. if (errorToken) {
  2552. this._emitWarning(
  2553. state,
  2554. errorMessage,
  2555. locConverter,
  2556. A.start(errorToken),
  2557. A.end(errorToken)
  2558. );
  2559. return;
  2560. }
  2561. if (classNames.length === 0) continue;
  2562. emitComposesGroup(classNames, fromSource, lastLocalIdentifier);
  2563. }
  2564. // 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.
  2565. let resumeAt = A.end(decl);
  2566. if (source.charCodeAt(A.end(decl)) === CC_SEMICOLON) {
  2567. resumeAt = A.end(decl) + 1;
  2568. while (isWhitespace(source.charCodeAt(resumeAt))) resumeAt++;
  2569. }
  2570. module.addPresentationalDependency(
  2571. new ConstDependency("", [A.nameStart(decl), resumeAt])
  2572. );
  2573. };
  2574. /**
  2575. * Emit url() deps for a `url(...)` / `src(...)` / `image-set(...)` value function.
  2576. * @param {FunctionNode} fn the function node
  2577. * @param {string | undefined} escapedName the unescaped function name when it carries an escape, undefined for the raw byte-range path
  2578. */
  2579. const emitUrlFunction = (fn, escapedName) => {
  2580. const fnNameStart = A.nameStart(fn);
  2581. const fnNameEnd = A.nameEnd(fn);
  2582. let isUrlOrSrc;
  2583. let isImageSet = false;
  2584. if (escapedName !== undefined) {
  2585. isUrlOrSrc =
  2586. equalsLowerCase(escapedName, "url") ||
  2587. equalsLowerCase(escapedName, "src");
  2588. if (!isUrlOrSrc) isImageSet = IMAGE_SET_FUNCTION.test(escapedName);
  2589. } else {
  2590. const nameLength = fnNameEnd - fnNameStart;
  2591. isUrlOrSrc =
  2592. nameLength === 3 &&
  2593. (rangeEqualsLowerCase(input, fnNameStart, fnNameEnd, "url") ||
  2594. rangeEqualsLowerCase(input, fnNameStart, fnNameEnd, "src"));
  2595. if (!isUrlOrSrc) {
  2596. // Suffix probe first, so only a vendor-prefixed `…-image-set` pays the slice + regex.
  2597. isImageSet =
  2598. nameLength >= 9 &&
  2599. rangeEqualsLowerCase(
  2600. input,
  2601. fnNameEnd - 9,
  2602. fnNameEnd,
  2603. "image-set"
  2604. ) &&
  2605. (nameLength === 9 ||
  2606. IMAGE_SET_FUNCTION.test(input.slice(fnNameStart, fnNameEnd)));
  2607. }
  2608. }
  2609. if (isUrlOrSrc) {
  2610. // Quoted `url("…")` / `src("…")`: first non-whitespace value must be the string token.
  2611. const first = A.children(fn)[nextNonWhitespace(A.children(fn), 0)];
  2612. if (!first || A.type(first) !== NodeType.String) return;
  2613. const string = /** @type {Token} */ (first);
  2614. const { ignored, options } = magicCommentsIn(
  2615. [lastTokenEndForComments, A.start(fn)],
  2616. A.start(string),
  2617. A.end(string)
  2618. );
  2619. if (ignored) return;
  2620. const value = normalizeUrl(
  2621. input.slice(A.start(string) + 1, A.end(string) - 1),
  2622. true
  2623. );
  2624. // Ignore `url()`, `url('')` and `url("")`, they are valid by spec
  2625. if (value.length === 0) return;
  2626. const dep = new CssUrlDependency(
  2627. value,
  2628. [A.start(string), A.end(string)],
  2629. "string"
  2630. );
  2631. setDepLoc(dep, A.start(string), A.end(string));
  2632. applyResourceHintDefaults(
  2633. dep,
  2634. value,
  2635. options,
  2636. rangeLoc(A.start(string), A.end(string))
  2637. );
  2638. module.addDependency(dep);
  2639. module.addCodeGenerationDependency(dep);
  2640. } else if (isImageSet) {
  2641. // `image-set(…)`: each comma segment's first string is the URL; advance the magic-comment fence per string.
  2642. lastTokenEndForComments = fnNameEnd + 1;
  2643. let prevStringEnd = A.start(fn);
  2644. let firstInSegment = true;
  2645. for (const cv of A.children(fn)) {
  2646. if (A.type(cv) === NodeType.Comma) {
  2647. firstInSegment = true;
  2648. continue;
  2649. }
  2650. if (A.type(cv) === NodeType.Whitespace) continue;
  2651. const wasFirst = firstInSegment;
  2652. firstInSegment = false;
  2653. if (!wasFirst || A.type(cv) !== NodeType.String) continue;
  2654. const string = /** @type {Token} */ (cv);
  2655. const start = prevStringEnd;
  2656. prevStringEnd = A.end(string);
  2657. const value = normalizeUrl(
  2658. input.slice(A.start(string) + 1, A.end(string) - 1),
  2659. true
  2660. );
  2661. if (value.length === 0) continue;
  2662. const { ignored, options } = magicCommentsIn(
  2663. [start, A.end(string)],
  2664. A.start(string),
  2665. A.end(string)
  2666. );
  2667. if (ignored) continue;
  2668. const dep = new CssUrlDependency(
  2669. value,
  2670. [A.start(string), A.end(string)],
  2671. "url"
  2672. );
  2673. setDepLoc(dep, A.start(string), A.end(string));
  2674. applyResourceHintDefaults(
  2675. dep,
  2676. value,
  2677. options,
  2678. rangeLoc(A.start(string), A.end(string))
  2679. );
  2680. module.addDependency(dep);
  2681. module.addCodeGenerationDependency(dep);
  2682. }
  2683. }
  2684. };
  2685. /**
  2686. * Handle an `@import` at-rule: parse its prelude (url, layer, supports, media), emit the `CssImportDependency`, and warn on malformed forms.
  2687. * @param {AtRule} at the `@import` at-rule
  2688. * @param {boolean} topLevel whether the rule is at the stylesheet top level
  2689. */
  2690. const handleImportAtRule = (at, topLevel) => {
  2691. if (!this.options.import) return;
  2692. if (!topLevel || !allowImport) {
  2693. this._emitWarning(
  2694. state,
  2695. "Any '@import' rules must precede all other rules",
  2696. locConverter,
  2697. A.start(at),
  2698. A.nameEnd(at)
  2699. );
  2700. return;
  2701. }
  2702. const importStart = A.start(at);
  2703. const importNameEnd = A.nameEnd(at);
  2704. // We only accept `;`-terminated @import; block / EOF / `}` ends are silent bails.
  2705. if (source.charCodeAt(A.end(at)) !== CC_SEMICOLON) return;
  2706. // Walk the prelude in spec order (URL → layer? → supports? → media query); anything else joins the media query.
  2707. const { urlNode, layerNode, supportsNode } = parseImportPrelude(
  2708. A.prelude(at)
  2709. );
  2710. const semi = A.end(at) + 1; // position past `;`
  2711. if (!urlNode || (A.type(urlNode) === NodeType.Ident && !isModules)) {
  2712. this._emitWarning(
  2713. state,
  2714. `Expected URL in '${input.slice(importStart, semi)}'`,
  2715. locConverter,
  2716. importStart,
  2717. semi
  2718. );
  2719. // A malformed `@import` still emits orphan url() deps from its prelude — flag it so the value visitors enable url().
  2720. currentUrlRecovery = true;
  2721. return;
  2722. }
  2723. /** @type {string} */
  2724. let url;
  2725. if (A.type(urlNode) === NodeType.Ident) {
  2726. // URL given as identifier — resolve via CSS Modules `@value`.
  2727. const identName = A.value(urlNode);
  2728. const def = icssDefinitions.get(identName);
  2729. if (!def) {
  2730. this._emitWarning(
  2731. state,
  2732. `Unknown '@value' identifier '${identName}' in '${input.slice(
  2733. importStart,
  2734. semi
  2735. )}'`,
  2736. locConverter,
  2737. importStart,
  2738. semi
  2739. );
  2740. // Drop the whole at-rule so the unresolved identifier isn't substituted into the output.
  2741. const dep = new ConstDependency("", [importStart, semi]);
  2742. module.addPresentationalDependency(dep);
  2743. return;
  2744. }
  2745. if (def.value === undefined) {
  2746. this._emitWarning(
  2747. state,
  2748. `'@value' identifier '${identName}' was imported from another module and cannot be used as the URL of '@import' — only locally defined values are supported here`,
  2749. locConverter,
  2750. importStart,
  2751. semi
  2752. );
  2753. const dep = new ConstDependency("", [importStart, semi]);
  2754. module.addPresentationalDependency(dep);
  2755. return;
  2756. }
  2757. const raw = def.value.trim();
  2758. url =
  2759. (raw.startsWith('"') && raw.endsWith('"')) ||
  2760. (raw.startsWith("'") && raw.endsWith("'"))
  2761. ? normalizeUrl(raw.slice(1, -1), true)
  2762. : normalizeUrl(raw, false);
  2763. } else if (A.type(urlNode) === NodeType.Url) {
  2764. const ut = /** @type {UrlToken} */ (urlNode);
  2765. url = normalizeUrl(
  2766. input.slice(A.contentStart(ut), A.contentEnd(ut)),
  2767. false
  2768. );
  2769. } else if (A.type(urlNode) === NodeType.String) {
  2770. url = normalizeUrl(
  2771. input.slice(A.start(urlNode) + 1, A.end(urlNode) - 1),
  2772. true
  2773. );
  2774. } else {
  2775. // url(...) function — first non-whitespace child is the string.
  2776. /** @type {Token | undefined} */
  2777. let string;
  2778. for (const inner of A.children(urlNode)) {
  2779. if (A.type(inner) === NodeType.Whitespace) continue;
  2780. if (A.type(inner) === NodeType.String) {
  2781. string = /** @type {Token} */ (inner);
  2782. }
  2783. break;
  2784. }
  2785. if (!string) {
  2786. this._emitWarning(
  2787. state,
  2788. `Expected URL in '${input.slice(importStart, semi)}'`,
  2789. locConverter,
  2790. importStart,
  2791. semi
  2792. );
  2793. return;
  2794. }
  2795. url = normalizeUrl(
  2796. input.slice(A.start(string) + 1, A.end(string) - 1),
  2797. true
  2798. );
  2799. }
  2800. const newline = skipWhiteLine(input, semi);
  2801. if (
  2802. webpackIgnored([importNameEnd, A.end(urlNode)], importStart, newline)
  2803. ) {
  2804. return;
  2805. }
  2806. if (url.length === 0) {
  2807. const dep = new ConstDependency("", [importStart, newline]);
  2808. module.addPresentationalDependency(dep);
  2809. setDepLoc(dep, importStart, newline);
  2810. return;
  2811. }
  2812. /** @type {undefined | string} */
  2813. let layer;
  2814. if (layerNode) {
  2815. if (A.type(layerNode) === NodeType.Function) {
  2816. // `layer(<ident>)` — extract content between `(` and `)`.
  2817. const fn = /** @type {FunctionNode} */ (layerNode);
  2818. layer = input.slice(A.nameEnd(fn) + 1, A.end(fn) - 1).trim();
  2819. } else {
  2820. // Bare `layer` ident — anonymous layer.
  2821. layer = "";
  2822. }
  2823. }
  2824. /** @type {undefined | string} */
  2825. let supports;
  2826. if (supportsNode) {
  2827. supports = input
  2828. .slice(A.nameEnd(supportsNode) + 1, A.end(supportsNode) - 1)
  2829. .trim();
  2830. }
  2831. // 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).
  2832. const lastPrefixPart = supportsNode || layerNode || urlNode;
  2833. const afterIdx =
  2834. /** @type {AstNode[]} */ (A.prelude(at)).indexOf(lastPrefixPart) + 1;
  2835. const nextIdx = nextNonWhitespace(A.prelude(at), afterIdx);
  2836. const mediaStart =
  2837. nextIdx < A.prelude(at).length
  2838. ? A.start(A.prelude(at)[nextIdx])
  2839. : A.end(at);
  2840. /** @type {undefined | string} */
  2841. let media;
  2842. if (mediaStart !== A.end(at)) {
  2843. media = input.slice(mediaStart, A.end(at)).trim();
  2844. }
  2845. const { line: sl, column: sc } = locConverter.get(importStart);
  2846. const { line: el, column: ec } = locConverter.get(newline);
  2847. const parent = /** @type {CssModule} */ (module);
  2848. // Carry the importing module's layer/supports/media chain onto the dep so a nested `@import` inherits it.
  2849. /** @type {Inheritance | undefined} */
  2850. let inheritance;
  2851. if (parent.cssLayer !== undefined || parent.supports || parent.media) {
  2852. inheritance = [[parent.cssLayer, parent.supports, parent.media]];
  2853. }
  2854. if (parent.inheritance) {
  2855. if (!inheritance) inheritance = [];
  2856. inheritance.push(...parent.inheritance);
  2857. }
  2858. const dep = new CssImportDependency(
  2859. url,
  2860. [importStart, newline],
  2861. mode === "local" || mode === "global" ? mode : undefined,
  2862. layer,
  2863. supports && supports.length > 0 ? supports : undefined,
  2864. media && media.length > 0 ? media : undefined,
  2865. inheritance,
  2866. parent.exportType
  2867. );
  2868. dep.setLoc(sl, sc, el, ec);
  2869. module.addDependency(dep);
  2870. // `text` / `css-style-sheet` parents inline the import at build time, so order it via a code-generation dependency.
  2871. if (
  2872. parent.exportType === "text" ||
  2873. parent.exportType === "css-style-sheet"
  2874. ) {
  2875. module.addCodeGenerationDependency(dep);
  2876. }
  2877. };
  2878. /**
  2879. * Handle a CSS-Modules `@value` at-rule: register the local / imported value(s) in `icssDefinitions`, emit the import + export deps, and strip the rule.
  2880. * @param {AtRule} at the `@value` at-rule
  2881. */
  2882. const handleValueAtRule = (at) => {
  2883. const start = A.start(at);
  2884. const nameEnd = A.nameEnd(at);
  2885. const semi = A.end(at);
  2886. const atRuleEnd =
  2887. source.charCodeAt(semi) === CC_SEMICOLON ? semi + 1 : semi;
  2888. const params = input.slice(nameEnd, semi);
  2889. const parsed = parseValueAtRuleParams(params);
  2890. if (
  2891. typeof (/** @type {ValueAtRuleImport} */ (parsed).from) !== "undefined"
  2892. ) {
  2893. if (/** @type {ValueAtRuleImport} */ (parsed).from.length === 0) {
  2894. this._emitWarning(
  2895. state,
  2896. `Broken '@value' at-rule: ${input.slice(start, atRuleEnd)}'`,
  2897. locConverter,
  2898. start,
  2899. atRuleEnd
  2900. );
  2901. const dep = new ConstDependency("", [start, atRuleEnd]);
  2902. module.addPresentationalDependency(dep);
  2903. return;
  2904. }
  2905. let { from, items } = /** @type {ValueAtRuleImport} */ (parsed);
  2906. for (const { importName, localName } of items) {
  2907. {
  2908. const reexport = icssDefinitions.get(from);
  2909. if (reexport && reexport.value) {
  2910. from = reexport.value.slice(1, -1);
  2911. }
  2912. const dep = new CssIcssImportDependency(
  2913. from,
  2914. [0, 0],
  2915. /** @type {"local" | "global"} */
  2916. (mode),
  2917. importName,
  2918. localName
  2919. );
  2920. setDepLoc(dep, start, nameEnd);
  2921. module.addDependency(dep);
  2922. icssDefinitions.set(localName, {
  2923. importName,
  2924. request: from
  2925. });
  2926. }
  2927. {
  2928. const { line: sl, column: sc } = locConverter.get(start);
  2929. const { line: el, column: ec } = locConverter.get(nameEnd);
  2930. recordDeclaration(localName, "@value", sl, sc);
  2931. addCssExport(
  2932. sl,
  2933. sc,
  2934. el,
  2935. ec,
  2936. localName,
  2937. getReexport(localName),
  2938. undefined,
  2939. false,
  2940. CssIcssExportDependency.EXPORT_MODE.REPLACE
  2941. );
  2942. }
  2943. }
  2944. } else {
  2945. if (/** @type {ValueAtRuleValue} */ (parsed).localName.length === 0) {
  2946. this._emitWarning(
  2947. state,
  2948. `Broken '@value' at-rule: ${input.slice(start, atRuleEnd)}'`,
  2949. locConverter,
  2950. start,
  2951. atRuleEnd
  2952. );
  2953. const dep = new ConstDependency("", [start, atRuleEnd]);
  2954. module.addPresentationalDependency(dep);
  2955. return;
  2956. }
  2957. const { localName, value } = /** @type {ValueAtRuleValue} */ (parsed);
  2958. const { line: sl, column: sc } = locConverter.get(start);
  2959. const { line: el, column: ec } = locConverter.get(nameEnd);
  2960. if (icssDefinitions.has(value)) {
  2961. const def =
  2962. /** @type {IcssDefinition} */
  2963. (icssDefinitions.get(value));
  2964. def.localName = value;
  2965. icssDefinitions.set(localName, def);
  2966. recordDeclaration(localName, "@value", sl, sc);
  2967. addCssExport(sl, sc, el, ec, localName, getReexport(value));
  2968. } else {
  2969. icssDefinitions.set(localName, { value });
  2970. recordDeclaration(localName, "@value", sl, sc);
  2971. addCssExport(sl, sc, el, ec, localName, value);
  2972. }
  2973. }
  2974. const dep = new ConstDependency("", [start, atRuleEnd]);
  2975. module.addPresentationalDependency(dep);
  2976. };
  2977. /**
  2978. * 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).
  2979. * @param {SimpleBlock} block parenthesised block node
  2980. * @param {{ hasExtra: boolean }} out extra-token flag output
  2981. * @returns {string | undefined} the dashed-ident name, or undefined
  2982. */
  2983. const loneDashedIdentOfBlock = (block, out) => {
  2984. out.hasExtra = false;
  2985. let ident;
  2986. for (const k of A.children(block)) {
  2987. if (A.type(k) === NodeType.Whitespace) continue;
  2988. if (ident !== undefined) {
  2989. out.hasExtra = true;
  2990. return ident;
  2991. }
  2992. if (A.type(k) !== NodeType.Ident) return undefined;
  2993. const v = A.value(k);
  2994. if (!isDashedIdentifier(v)) return undefined;
  2995. ident = v;
  2996. }
  2997. return ident;
  2998. };
  2999. /**
  3000. * Collect one `@custom-media --name <value>` definition (last-wins) and drop
  3001. * the rule. A value naming another custom media is kept as an `alias` — the
  3002. * name it points at may be defined further down the file, and node ids are
  3003. * recycled per top-level rule, so nothing AST-shaped can outlive the walk.
  3004. * @param {AtRule} at the `@custom-media` at-rule
  3005. */
  3006. const collectCustomMedia = (at) => {
  3007. const start = A.start(at);
  3008. const end = A.end(at);
  3009. const ruleEnd = input.charCodeAt(end) === CC_SEMICOLON ? end + 1 : end;
  3010. module.addPresentationalDependency(
  3011. new ConstDependency("", [start, ruleEnd])
  3012. );
  3013. const prelude = A.prelude(at);
  3014. let i = 0;
  3015. while (i < prelude.length && A.type(prelude[i]) === NodeType.Whitespace) {
  3016. i++;
  3017. }
  3018. if (i >= prelude.length || A.type(prelude[i]) !== NodeType.Ident) return;
  3019. const name = A.value(prelude[i]);
  3020. if (!isDashedIdentifier(name)) return;
  3021. i++;
  3022. // Value span after the name, trimmed.
  3023. let vs = i;
  3024. let ve = prelude.length;
  3025. while (vs < ve && A.type(prelude[vs]) === NodeType.Whitespace) vs++;
  3026. while (ve > vs && A.type(prelude[ve - 1]) === NodeType.Whitespace) ve--;
  3027. (customMediaDefs || (customMediaDefs = new Map())).set(
  3028. name,
  3029. classifyCustomMedia(prelude, vs, ve)
  3030. );
  3031. };
  3032. /**
  3033. * Classify one `@custom-media` value against the live AST.
  3034. * @param {readonly AstNode[]} prelude the defining at-rule's prelude
  3035. * @param {number} vs first value token
  3036. * @param {number} ve one past the last value token
  3037. * @returns {CustomMediaDefinition} what the value is, aliases unresolved
  3038. */
  3039. const classifyCustomMedia = (prelude, vs, ve) => {
  3040. if (vs >= ve) return CUSTOM_MEDIA_UNSUPPORTED;
  3041. const extra = { hasExtra: false };
  3042. let hasComma = false;
  3043. for (let k = vs; k < ve; k++) {
  3044. if (A.type(prelude[k]) === NodeType.Comma) {
  3045. hasComma = true;
  3046. break;
  3047. }
  3048. }
  3049. if (!hasComma) {
  3050. const first = prelude[vs];
  3051. const valueText = input.slice(A.start(first), A.end(prelude[ve - 1]));
  3052. // A `(--x)` standing alone is this value spelled as another name.
  3053. if (ve - vs === 1 && A.type(first) === NodeType.SimpleBlock) {
  3054. const nested = loneDashedIdentOfBlock(
  3055. /** @type {SimpleBlock} */ (first),
  3056. extra
  3057. );
  3058. if (nested !== undefined && !extra.hasExtra) {
  3059. return { kind: "alias", name: nested };
  3060. }
  3061. }
  3062. // A reference anywhere else in the value has no resolution step of its
  3063. // own — only a `@media` prelude's uses are scanned — so writing this
  3064. // value out would emit the reference unresolved.
  3065. if (hasCustomMediaRef(prelude, vs, ve)) return CUSTOM_MEDIA_UNSUPPORTED;
  3066. if (A.type(first) === NodeType.SimpleBlock) {
  3067. return { kind: "condition", text: valueText };
  3068. }
  3069. if (A.type(first) !== NodeType.Ident) return CUSTOM_MEDIA_UNSUPPORTED;
  3070. const s = A.start(first);
  3071. const e = A.end(first);
  3072. if (ve - vs === 1) {
  3073. if (rangeEqualsLowerCase(input, s, e, "true")) {
  3074. return CUSTOM_MEDIA_TRUE;
  3075. }
  3076. if (rangeEqualsLowerCase(input, s, e, "false")) {
  3077. return CUSTOM_MEDIA_FALSE;
  3078. }
  3079. }
  3080. if (rangeEqualsLowerCase(input, s, e, "not")) {
  3081. // `not (…)` is a media condition (wrap so it stays a media-in-parens);
  3082. // `not <type>` is a media type.
  3083. let j = vs + 1;
  3084. while (j < ve && A.type(prelude[j]) === NodeType.Whitespace) j++;
  3085. if (j < ve && A.type(prelude[j]) === NodeType.SimpleBlock) {
  3086. return { kind: "condition", text: `(${valueText})` };
  3087. }
  3088. return { kind: "type", text: valueText };
  3089. }
  3090. // `screen`, `only screen`, `screen and (…)`, …
  3091. return { kind: "type", text: valueText };
  3092. }
  3093. // Comma list → `(seg1 or seg2 …)`; supported only when every segment is a
  3094. // media-in-parens (or a name standing for one).
  3095. /** @type {({ text: string } | { alias: string })[]} */
  3096. const parts = [];
  3097. let ok = true;
  3098. /** @type {AstNode[]} */
  3099. let seg = [];
  3100. const flush = () => {
  3101. let a = 0;
  3102. let b = seg.length;
  3103. while (a < b && A.type(seg[a]) === NodeType.Whitespace) a++;
  3104. while (b > a && A.type(seg[b - 1]) === NodeType.Whitespace) b--;
  3105. if (a >= b) {
  3106. ok = false;
  3107. return;
  3108. }
  3109. const f = seg[a];
  3110. const text = input.slice(A.start(f), A.end(seg[b - 1]));
  3111. if (b - a === 1 && A.type(f) === NodeType.SimpleBlock) {
  3112. const nested = loneDashedIdentOfBlock(
  3113. /** @type {SimpleBlock} */ (f),
  3114. extra
  3115. );
  3116. if (extra.hasExtra) ok = false;
  3117. else if (nested === undefined) parts.push({ text });
  3118. else parts.push({ alias: nested });
  3119. return;
  3120. }
  3121. if (
  3122. A.type(f) === NodeType.Ident &&
  3123. rangeEqualsLowerCase(input, A.start(f), A.end(f), "not") &&
  3124. !hasCustomMediaRef(seg, a, b)
  3125. ) {
  3126. parts.push({ text: `(${text})` });
  3127. return;
  3128. }
  3129. ok = false;
  3130. };
  3131. for (let k = vs; k < ve; k++) {
  3132. if (A.type(prelude[k]) === NodeType.Comma) {
  3133. flush();
  3134. seg = [];
  3135. } else {
  3136. seg.push(prelude[k]);
  3137. }
  3138. }
  3139. flush();
  3140. return ok ? { kind: "or", parts } : CUSTOM_MEDIA_UNSUPPORTED;
  3141. };
  3142. /**
  3143. * What a `@custom-media` name resolves to, memoized. An undefined name and a
  3144. * definition cycle both resolve to unsupported (the use site warns).
  3145. * @param {string} name the dashed ident
  3146. * @param {Set<string>} resolving names currently being resolved
  3147. * @returns {CustomMediaValue} what the name resolves to
  3148. */
  3149. const resolveCustomMediaValue = (name, resolving) => {
  3150. const cached = customMediaValues && customMediaValues.get(name);
  3151. if (cached !== undefined) return cached;
  3152. const def = customMediaDefs && customMediaDefs.get(name);
  3153. if (def === undefined || resolving.has(name)) {
  3154. return CUSTOM_MEDIA_UNSUPPORTED;
  3155. }
  3156. resolving.add(name);
  3157. /** @type {CustomMediaValue} */
  3158. let value;
  3159. if (def.kind === "alias") {
  3160. const inner = resolveCustomMediaValue(def.name, resolving);
  3161. // A media type cannot sit in parens, so a name standing for one is no value.
  3162. value = inner.kind === "type" ? CUSTOM_MEDIA_UNSUPPORTED : inner;
  3163. } else if (def.kind === "or") {
  3164. /** @type {string[]} */
  3165. const texts = [];
  3166. let ok = true;
  3167. let anyTrue = false;
  3168. for (const part of def.parts) {
  3169. if (!("alias" in part)) {
  3170. texts.push(part.text);
  3171. continue;
  3172. }
  3173. const inner = resolveCustomMediaValue(part.alias, resolving);
  3174. // A media query list is an `or`, so a constant segment drops out of it.
  3175. if (inner.kind === "boolean") anyTrue = anyTrue || inner.value;
  3176. else if (inner.kind === "condition") texts.push(inner.text);
  3177. else ok = false;
  3178. }
  3179. value = !ok
  3180. ? CUSTOM_MEDIA_UNSUPPORTED
  3181. : anyTrue
  3182. ? CUSTOM_MEDIA_TRUE
  3183. : texts.length === 0
  3184. ? CUSTOM_MEDIA_FALSE
  3185. : {
  3186. kind: "condition",
  3187. text:
  3188. texts.length === 1 ? texts[0] : `(${texts.join(" or ")})`
  3189. };
  3190. } else {
  3191. value = def;
  3192. }
  3193. resolving.delete(name);
  3194. (customMediaValues || (customMediaValues = new Map())).set(name, value);
  3195. return value;
  3196. };
  3197. /**
  3198. * Whether a node is the media keyword `word` (media queries are ASCII
  3199. * case-insensitive).
  3200. * @param {AstNode} node candidate node
  3201. * @param {string} word lower-case keyword
  3202. * @returns {boolean} true on a match
  3203. */
  3204. const isMediaKeyword = (node, word) =>
  3205. A.type(node) === NodeType.Ident &&
  3206. rangeEqualsLowerCase(input, A.start(node), A.end(node), word);
  3207. /**
  3208. * First significant index in `tokens[from…to)`.
  3209. * @param {readonly AstNode[]} tokens token list
  3210. * @param {number} from first index
  3211. * @param {number} to one past the last index
  3212. * @returns {number} the trimmed start
  3213. */
  3214. const mediaTrimStart = (tokens, from, to) => {
  3215. let a = from;
  3216. while (a < to && A.type(tokens[a]) === NodeType.Whitespace) a++;
  3217. return a;
  3218. };
  3219. /**
  3220. * One past the last significant index in `tokens[from…to)`.
  3221. * @param {readonly AstNode[]} tokens token list
  3222. * @param {number} from first index
  3223. * @param {number} to one past the last index
  3224. * @returns {number} the trimmed end
  3225. */
  3226. const mediaTrimEnd = (tokens, from, to) => {
  3227. let b = to;
  3228. while (b > from && A.type(tokens[b - 1]) === NodeType.Whitespace) b--;
  3229. return b;
  3230. };
  3231. /**
  3232. * The source text spanned by `tokens[from…to)`.
  3233. * @param {readonly AstNode[]} tokens token list
  3234. * @param {number} from first index
  3235. * @param {number} to one past the last index
  3236. * @returns {string} the source slice
  3237. */
  3238. const mediaText = (tokens, from, to) =>
  3239. input.slice(A.start(tokens[from]), A.end(tokens[to - 1]));
  3240. // One flag object for the whole parse: `loneDashedIdentOfBlock` writes it and
  3241. // every caller reads it back before the next call.
  3242. const mediaExtra = { hasExtra: false };
  3243. /**
  3244. * Whether any `(--name)` reference is reachable inside `node`. `childAt` walks
  3245. * the children without materializing the list.
  3246. * @param {AstNode} node a block or function node
  3247. * @returns {boolean} true when a reference is present
  3248. */
  3249. const hasCustomMediaRefIn = (node) => {
  3250. const count = A.childCount(node);
  3251. for (let k = 0; k < count; k++) {
  3252. const child = A.childAt(node, k);
  3253. const type = A.type(child);
  3254. if (type !== NodeType.SimpleBlock && type !== NodeType.Function) {
  3255. continue;
  3256. }
  3257. if (
  3258. type === NodeType.SimpleBlock &&
  3259. loneDashedIdentOfBlock(
  3260. /** @type {SimpleBlock} */ (child),
  3261. mediaExtra
  3262. ) !== undefined
  3263. ) {
  3264. return true;
  3265. }
  3266. if (hasCustomMediaRefIn(child)) return true;
  3267. }
  3268. return false;
  3269. };
  3270. /**
  3271. * Whether any `(--name)` reference is reachable in `tokens[from…to)`. Cheap
  3272. * enough to run over every `@media` prelude so one without a reference builds
  3273. * no tree at all.
  3274. * @param {readonly AstNode[]} tokens token list
  3275. * @param {number} from first index
  3276. * @param {number} to one past the last index
  3277. * @returns {boolean} true when a reference is present
  3278. */
  3279. const hasCustomMediaRef = (tokens, from, to) => {
  3280. for (let i = from; i < to; i++) {
  3281. const node = tokens[i];
  3282. const type = A.type(node);
  3283. if (type !== NodeType.SimpleBlock && type !== NodeType.Function) {
  3284. continue;
  3285. }
  3286. if (
  3287. type === NodeType.SimpleBlock &&
  3288. loneDashedIdentOfBlock(
  3289. /** @type {SimpleBlock} */ (node),
  3290. mediaExtra
  3291. ) !== undefined
  3292. ) {
  3293. return true;
  3294. }
  3295. if (hasCustomMediaRefIn(node)) return true;
  3296. }
  3297. return false;
  3298. };
  3299. /**
  3300. * Capture one `<media-in-parens>` — a reference to a custom media name, a
  3301. * parenthesised sub-condition, or an opaque feature test.
  3302. * @param {readonly AstNode[]} tokens token list
  3303. * @param {number} from first index
  3304. * @param {number} to one past the last index
  3305. * @param {boolean} leading whether the term starts a top-level query
  3306. * @param {CustomMediaUse[]} uses the prelude's use list, appended to
  3307. * @returns {MediaNode} the captured term
  3308. */
  3309. const captureMediaInParens = (tokens, from, to, leading, uses) => {
  3310. const a = mediaTrimStart(tokens, from, to);
  3311. const b = mediaTrimEnd(tokens, a, to);
  3312. if (a >= b) return { kind: "text", text: "" };
  3313. const text = mediaText(tokens, a, b);
  3314. if (b - a !== 1 || A.type(tokens[a]) !== NodeType.SimpleBlock) {
  3315. return { kind: "text", text };
  3316. }
  3317. const block = /** @type {SimpleBlock} */ (tokens[a]);
  3318. const name = loneDashedIdentOfBlock(block, mediaExtra);
  3319. if (name !== undefined) {
  3320. const use = {
  3321. name,
  3322. start: A.start(block),
  3323. end: A.end(block),
  3324. invalid: mediaExtra.hasExtra,
  3325. leading
  3326. };
  3327. uses.push(use);
  3328. return { kind: "ref", text, use };
  3329. }
  3330. const children = A.children(block);
  3331. const inner = captureMediaCondition(children, 0, children.length, uses);
  3332. return inner.kind === "text"
  3333. ? { kind: "text", text }
  3334. : { kind: "group", operand: inner };
  3335. };
  3336. /**
  3337. * Capture a `<media-condition>` — `not <in-parens>`, or an `and` / `or` chain
  3338. * of them (CSS never mixes the two at one level).
  3339. * @param {readonly AstNode[]} tokens token list
  3340. * @param {number} from first index
  3341. * @param {number} to one past the last index
  3342. * @param {CustomMediaUse[]} uses the prelude's use list, appended to
  3343. * @returns {MediaNode} the captured condition
  3344. */
  3345. const captureMediaCondition = (tokens, from, to, uses) => {
  3346. const a = mediaTrimStart(tokens, from, to);
  3347. const b = mediaTrimEnd(tokens, a, to);
  3348. if (a >= b) return { kind: "text", text: "" };
  3349. if (isMediaKeyword(tokens[a], "not")) {
  3350. return {
  3351. kind: "not",
  3352. operand: captureMediaInParens(tokens, a + 1, b, false, uses)
  3353. };
  3354. }
  3355. /** @type {MediaNode[]} */
  3356. const terms = [];
  3357. let isOr = false;
  3358. let start = a;
  3359. for (let i = a; i <= b; i++) {
  3360. const atEnd = i === b;
  3361. if (
  3362. !atEnd &&
  3363. !isMediaKeyword(tokens[i], "and") &&
  3364. !isMediaKeyword(tokens[i], "or")
  3365. ) {
  3366. continue;
  3367. }
  3368. if (!atEnd && isMediaKeyword(tokens[i], "or")) isOr = true;
  3369. terms.push(captureMediaInParens(tokens, start, i, start === a, uses));
  3370. start = i + 1;
  3371. }
  3372. if (terms.length === 1) return terms[0];
  3373. return { kind: "chain", isOr, terms };
  3374. };
  3375. /**
  3376. * Capture one `<media-query>` — a bare condition, or a media type with an
  3377. * optional `and` chain after it.
  3378. * @param {readonly AstNode[]} tokens token list
  3379. * @param {number} from first index
  3380. * @param {number} to one past the last index
  3381. * @param {CustomMediaUse[]} uses the prelude's use list, appended to
  3382. * @returns {MediaNode} the captured query
  3383. */
  3384. const captureMediaQuery = (tokens, from, to, uses) => {
  3385. const a = mediaTrimStart(tokens, from, to);
  3386. const b = mediaTrimEnd(tokens, a, to);
  3387. if (a >= b) return { kind: "text", text: "" };
  3388. const rest = mediaTrimStart(tokens, a + 1, b);
  3389. const startsWithType =
  3390. A.type(tokens[a]) === NodeType.Ident &&
  3391. (!isMediaKeyword(tokens[a], "not") ||
  3392. rest >= b ||
  3393. A.type(tokens[rest]) !== NodeType.SimpleBlock);
  3394. if (!startsWithType) return captureMediaCondition(tokens, a, b, uses);
  3395. // `[not | only]? <type> [and <condition-without-or>]?` — the type itself is
  3396. // never constant, so only the `and` chain after it can fold.
  3397. let split = b;
  3398. for (let i = a; i < b; i++) {
  3399. if (isMediaKeyword(tokens[i], "and")) {
  3400. split = i;
  3401. break;
  3402. }
  3403. }
  3404. return {
  3405. kind: "typed",
  3406. text: mediaText(tokens, a, mediaTrimEnd(tokens, a, split)),
  3407. rest:
  3408. split === b ? null : captureMediaCondition(tokens, split + 1, b, uses)
  3409. };
  3410. };
  3411. /**
  3412. * Record one `@media` prelude, the custom-media references inside it, and the
  3413. * boolean shape needed to fold it once the references resolve.
  3414. * @param {AtRule} at the `@media` at-rule
  3415. */
  3416. const collectMediaQuery = (at) => {
  3417. const prelude = A.prelude(at);
  3418. const start = mediaTrimStart(prelude, 0, prelude.length);
  3419. const end = mediaTrimEnd(prelude, start, prelude.length);
  3420. if (start >= end || !hasCustomMediaRef(prelude, start, end)) return;
  3421. /** @type {CustomMediaUse[]} */
  3422. const uses = [];
  3423. /** @type {MediaNode[]} */
  3424. const queries = [];
  3425. let queryStart = start;
  3426. for (let i = start; i <= end; i++) {
  3427. if (i !== end && A.type(prelude[i]) !== NodeType.Comma) continue;
  3428. queries.push(captureMediaQuery(prelude, queryStart, i, uses));
  3429. queryStart = i + 1;
  3430. }
  3431. if (uses.length === 0) return;
  3432. (customMediaQueries || (customMediaQueries = [])).push({
  3433. queries,
  3434. start: A.start(prelude[start]),
  3435. end: A.end(prelude[end - 1]),
  3436. uses
  3437. });
  3438. };
  3439. /**
  3440. * Fold a captured media node against the resolved definitions. A reference the
  3441. * resolver could not classify keeps its source text, so it prints as authored.
  3442. * @param {MediaNode} node the captured node
  3443. * @param {Set<string>} resolving names currently being resolved
  3444. * @returns {boolean | string} the constant, or the node's text
  3445. */
  3446. const foldMediaNode = (node, resolving) => {
  3447. switch (node.kind) {
  3448. case "text":
  3449. return node.text;
  3450. case "ref": {
  3451. const value = resolveCustomMediaValue(node.use.name, resolving);
  3452. if (node.use.invalid) return node.text;
  3453. if (value.kind === "boolean") return value.value;
  3454. if (value.kind === "condition") return value.text;
  3455. if (value.kind === "type" && node.use.leading) return value.text;
  3456. return node.text;
  3457. }
  3458. case "group": {
  3459. const inner = foldMediaNode(node.operand, resolving);
  3460. return typeof inner === "boolean" ? inner : `(${inner})`;
  3461. }
  3462. case "not": {
  3463. const operand = foldMediaNode(node.operand, resolving);
  3464. return typeof operand === "boolean" ? !operand : `not ${operand}`;
  3465. }
  3466. case "chain": {
  3467. /** @type {string[]} */
  3468. const kept = [];
  3469. for (const term of node.terms) {
  3470. const folded = foldMediaNode(term, resolving);
  3471. if (folded === node.isOr) return node.isOr;
  3472. if (typeof folded !== "boolean") kept.push(folded);
  3473. }
  3474. if (kept.length === 0) return !node.isOr;
  3475. if (kept.length === 1) return kept[0];
  3476. return kept.join(node.isOr ? " or " : " and ");
  3477. }
  3478. default: {
  3479. if (node.rest === null) return node.text;
  3480. const rest = foldMediaNode(node.rest, resolving);
  3481. if (rest === false) return false;
  3482. if (rest === true) return node.text;
  3483. return `${node.text} and ${rest}`;
  3484. }
  3485. }
  3486. };
  3487. /**
  3488. * Fold a whole `@media` prelude. A query that is always false leaves the list;
  3489. * one that is always true makes the list match everywhere (`all`), and a list
  3490. * with nothing left matches nowhere (`not all`).
  3491. * @param {MediaNode[]} queries the prelude's captured queries
  3492. * @param {Set<string>} resolving names currently being resolved
  3493. * @returns {string} the folded prelude
  3494. */
  3495. const foldMediaPrelude = (queries, resolving) => {
  3496. /** @type {string[]} */
  3497. const kept = [];
  3498. for (const query of queries) {
  3499. const folded = foldMediaNode(query, resolving);
  3500. if (folded === true) return "all";
  3501. if (folded !== false && folded !== "") kept.push(folded);
  3502. }
  3503. return kept.length === 0 ? "not all" : kept.join(", ");
  3504. };
  3505. /**
  3506. * Collect one `@custom-selector :--name <selector-list>` definition (last-wins) and drop the rule.
  3507. * @param {AtRule} at the `@custom-selector` at-rule
  3508. */
  3509. const collectCustomSelector = (at) => {
  3510. const start = A.start(at);
  3511. const end = A.end(at);
  3512. const ruleEnd = input.charCodeAt(end) === CC_SEMICOLON ? end + 1 : end;
  3513. module.addPresentationalDependency(
  3514. new ConstDependency("", [start, ruleEnd])
  3515. );
  3516. const prelude = A.prelude(at);
  3517. let i = 0;
  3518. while (i < prelude.length && A.type(prelude[i]) === NodeType.Whitespace) {
  3519. i++;
  3520. }
  3521. if (i + 1 >= prelude.length || A.type(prelude[i]) !== NodeType.Colon) {
  3522. return;
  3523. }
  3524. const nameNode = prelude[i + 1];
  3525. if (
  3526. A.type(nameNode) !== NodeType.Ident ||
  3527. A.end(prelude[i]) !== A.start(nameNode)
  3528. ) {
  3529. return;
  3530. }
  3531. const name = A.value(nameNode);
  3532. if (!isDashedIdentifier(name)) return;
  3533. const list = input.slice(A.end(nameNode), end).trim();
  3534. if (list.length === 0) return;
  3535. (customSelectorDefs || (customSelectorDefs = new Map())).set(name, list);
  3536. };
  3537. /**
  3538. * 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.
  3539. * @param {readonly AstNode[]} tokens selector prelude (or nested) tokens
  3540. */
  3541. const scanCustomSelectorUses = (tokens) => {
  3542. for (let k = 0; k < tokens.length; k++) {
  3543. const t = tokens[k];
  3544. const tt = A.type(t);
  3545. if (tt === NodeType.Colon) {
  3546. const next = tokens[k + 1];
  3547. if (
  3548. next &&
  3549. A.type(next) === NodeType.Ident &&
  3550. A.end(t) === A.start(next) &&
  3551. isDashedIdentifier(A.value(next))
  3552. ) {
  3553. (customSelectorUses || (customSelectorUses = [])).push({
  3554. name: A.value(next),
  3555. start: A.start(t),
  3556. end: A.end(next)
  3557. });
  3558. }
  3559. } else if (tt === NodeType.Function || tt === NodeType.SimpleBlock) {
  3560. scanCustomSelectorUses(A.children(t));
  3561. }
  3562. }
  3563. };
  3564. /**
  3565. * Rewrite the collected `@custom-media` / `@custom-selector` uses now that all (possibly later-defined) definitions are known; warns on invalid, unknown, or media-type-in-a-non-leading-position custom-media uses.
  3566. */
  3567. const resolveCustomMediaAndSelectors = () => {
  3568. if (customMediaQueries) {
  3569. /** @type {Set<string>} */
  3570. const resolving = new Set();
  3571. for (const query of customMediaQueries) {
  3572. let hasBoolean = false;
  3573. for (const use of query.uses) {
  3574. if (use.invalid) {
  3575. this._emitWarning(
  3576. state,
  3577. `Custom media query '${use.name}' must be used in a boolean context`,
  3578. locConverter,
  3579. use.start,
  3580. use.end
  3581. );
  3582. continue;
  3583. }
  3584. const value = resolveCustomMediaValue(use.name, resolving);
  3585. if (value.kind === "unsupported") {
  3586. this._emitWarning(
  3587. state,
  3588. customMediaDefs && customMediaDefs.has(use.name)
  3589. ? `Custom media query '${use.name}' has a value that cannot be resolved and was left as written`
  3590. : `Unknown custom media query '${use.name}'`,
  3591. locConverter,
  3592. use.start,
  3593. use.end
  3594. );
  3595. } else if (value.kind === "boolean") {
  3596. hasBoolean = true;
  3597. } else if (value.kind === "type" && !use.leading) {
  3598. this._emitWarning(
  3599. state,
  3600. `Custom media query '${use.name}' resolves to a media type and can only be used at the start of a media query`,
  3601. locConverter,
  3602. use.start,
  3603. use.end
  3604. );
  3605. }
  3606. }
  3607. // `true` / `false` have no `<media-in-parens>` spelling, so a prelude
  3608. // holding one is rewritten whole — every other use in it folds with it.
  3609. if (hasBoolean) {
  3610. module.addPresentationalDependency(
  3611. new ConstDependency(foldMediaPrelude(query.queries, resolving), [
  3612. query.start,
  3613. query.end
  3614. ])
  3615. );
  3616. continue;
  3617. }
  3618. for (const use of query.uses) {
  3619. if (use.invalid) continue;
  3620. const value = resolveCustomMediaValue(use.name, resolving);
  3621. if (
  3622. value.kind === "condition" ||
  3623. (value.kind === "type" && use.leading)
  3624. ) {
  3625. module.addPresentationalDependency(
  3626. new ConstDependency(value.text, [use.start, use.end])
  3627. );
  3628. }
  3629. }
  3630. }
  3631. }
  3632. if (customSelectorUses) {
  3633. for (const use of customSelectorUses) {
  3634. const list = customSelectorDefs && customSelectorDefs.get(use.name);
  3635. if (list === undefined) continue;
  3636. module.addPresentationalDependency(
  3637. new ConstDependency(`:is(${list})`, [use.start, use.end])
  3638. );
  3639. }
  3640. }
  3641. };
  3642. /**
  3643. * 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.
  3644. * @param {Declaration} decl the declaration
  3645. * @param {string} declPropertyName the vendor-stripped, lower-cased property name
  3646. * @returns {void}
  3647. */
  3648. const emitKnownPropertyExports = (decl, declPropertyName) => {
  3649. /** @type {Record<string, number>} */
  3650. let parsedKeywords = Object.create(null);
  3651. const isGridProperty = Boolean(declPropertyName.startsWith("grid"));
  3652. // Only `view-transition-*` and `counter-*` legitimately take a dashed-ident value; other known properties keep the historical plain-ident handling.
  3653. const isViewTransitionProperty = Boolean(
  3654. declPropertyName.startsWith("view-transition")
  3655. );
  3656. const isCounterProperty = Boolean(
  3657. declPropertyName.startsWith("counter-")
  3658. );
  3659. const isGridTemplate = isGridProperty
  3660. ? Boolean(
  3661. declPropertyName === "grid" ||
  3662. declPropertyName === "grid-template" ||
  3663. declPropertyName === "grid-template-columns" ||
  3664. declPropertyName === "grid-template-rows"
  3665. )
  3666. : false;
  3667. const keywords =
  3668. /** @type {Map<string, number>} */
  3669. (knownProperties.get(declPropertyName));
  3670. let afterExclamation = false;
  3671. /**
  3672. * 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.
  3673. * @param {number} start name start offset
  3674. * @param {number} end name end offset
  3675. * @param {boolean=} isString whether the span is a quoted string
  3676. * @returns {void}
  3677. */
  3678. const emit = (start, end, isString) => {
  3679. const { line: sl, column: sc } = locConverter.get(start);
  3680. const { line: el, column: ec } = locConverter.get(end);
  3681. const name = unescapeRange(
  3682. isString ? start + 1 : start,
  3683. isString ? end - 1 : end
  3684. );
  3685. // Only grid-line names are declaration sites here; property-value references (`animation: foo`) are usages.
  3686. if (isGridProperty) recordDeclaration(name, "grid identifier", sl, sc);
  3687. addCssExport(
  3688. sl,
  3689. sc,
  3690. el,
  3691. ec,
  3692. name,
  3693. getReexport(name),
  3694. [start, end],
  3695. true,
  3696. CssIcssExportDependency.EXPORT_MODE.ONCE,
  3697. isGridProperty
  3698. ? CssIcssExportDependency.EXPORT_TYPE.GRID_CUSTOM_IDENTIFIER
  3699. : CssIcssExportDependency.EXPORT_TYPE.NORMAL
  3700. );
  3701. };
  3702. // Collect idents/strings to export — top-level only, except grid-template recurses (`[line-name]` blocks live in `repeat(…)`).
  3703. /** @type {(cvs: AstNode[]) => void} */
  3704. const walkExports = (cvs) => {
  3705. for (const cv of cvs) {
  3706. switch (A.type(cv)) {
  3707. case NodeType.Comma:
  3708. parsedKeywords = Object.create(null);
  3709. break;
  3710. case NodeType.Delim:
  3711. afterExclamation = A.value(cv) === "!";
  3712. break;
  3713. case NodeType.Ident: {
  3714. if (isGridTemplate) break;
  3715. if (afterExclamation) {
  3716. afterExclamation = false;
  3717. break;
  3718. }
  3719. const identifier = A.value(cv);
  3720. // `view-transition-name: --foo` / `counter-reset: --foo` — a dashed name scopes as a custom property under `dashedIdents`, not as a plain ident export.
  3721. if (
  3722. (isViewTransitionProperty || isCounterProperty) &&
  3723. isDashedIdentifier(identifier)
  3724. ) {
  3725. if (this.options.dashedIdents) {
  3726. emitDashedIdentExport(A.start(cv), A.end(cv));
  3727. }
  3728. break;
  3729. }
  3730. // Values are almost always lowercase already — avoid the copy.
  3731. const keyword = toLowerCaseIfNeeded(identifier);
  3732. parsedKeywords[keyword] =
  3733. typeof parsedKeywords[keyword] !== "undefined"
  3734. ? parsedKeywords[keyword] + 1
  3735. : 0;
  3736. const limit = keywords.get(keyword);
  3737. if (limit !== undefined && parsedKeywords[keyword] < limit) {
  3738. break;
  3739. }
  3740. emit(A.start(cv), A.end(cv));
  3741. break;
  3742. }
  3743. case NodeType.String: {
  3744. if (
  3745. declPropertyName === "animation" ||
  3746. declPropertyName === "animation-name"
  3747. ) {
  3748. emit(A.start(cv), A.end(cv), true);
  3749. }
  3750. if (
  3751. declPropertyName === "grid" ||
  3752. declPropertyName === "grid-template" ||
  3753. declPropertyName === "grid-template-areas"
  3754. ) {
  3755. // Raw offsets: a match index into the unescaped text would
  3756. // slide the replaced range past every escape before it.
  3757. const names = gridAreaNames(
  3758. source,
  3759. A.start(cv) + 1,
  3760. A.end(cv) - 1
  3761. );
  3762. for (const [nameStart, nameEnd] of names) {
  3763. emit(nameStart, nameEnd, false);
  3764. }
  3765. }
  3766. break;
  3767. }
  3768. case NodeType.SimpleBlock: {
  3769. const block = /** @type {SimpleBlock} */ (cv);
  3770. if (A.blockToken(block) === "[") {
  3771. // Collect identifiers until the first non-ident token (`<line-names> = '[' <custom-ident>* ']'`).
  3772. for (const inner of A.children(block)) {
  3773. if (A.type(inner) === NodeType.Whitespace) continue;
  3774. if (A.type(inner) !== NodeType.Ident) break;
  3775. emit(A.start(inner), A.end(inner));
  3776. }
  3777. } else if (isGridTemplate) {
  3778. walkExports(A.children(block));
  3779. }
  3780. break;
  3781. }
  3782. case NodeType.Function:
  3783. // `repeat(…)` line names, and the `counter-reset: reversed(name)` wrapper — but not `local()`/`global()`, whose idents the value visitors own.
  3784. if (
  3785. isGridTemplate ||
  3786. (isCounterProperty &&
  3787. equalsLowerCase(A.unescapedName(cv), "reversed"))
  3788. ) {
  3789. walkExports(A.children(cv));
  3790. }
  3791. break;
  3792. // Other types carry no ICSS-export information.
  3793. }
  3794. }
  3795. };
  3796. walkExports(A.children(decl));
  3797. };
  3798. /**
  3799. * 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.
  3800. * @param {AtRule} at the at-rule
  3801. * @param {boolean} isKeyframes whether it's `@keyframes`
  3802. * @param {boolean} isCounterStyle whether it's `@counter-style`
  3803. * @param {boolean} isContainer whether it's `@container`
  3804. * @returns {void}
  3805. */
  3806. const markPureFromAtRulePrelude = (
  3807. at,
  3808. isKeyframes,
  3809. isCounterStyle,
  3810. isContainer
  3811. ) => {
  3812. const acceptIdent = isKeyframes || isCounterStyle || isContainer;
  3813. const acceptString = isKeyframes;
  3814. for (const cv of A.prelude(at)) {
  3815. const cvType = A.type(cv);
  3816. if (cvType === NodeType.Whitespace) continue;
  3817. if (cvType === NodeType.String) {
  3818. if (acceptString) pure.markLocal();
  3819. break;
  3820. }
  3821. if (cvType === NodeType.Ident) {
  3822. if (!acceptIdent) break;
  3823. if (
  3824. isContainer &&
  3825. isContainerKeyword(source, A.start(cv), A.end(cv))
  3826. ) {
  3827. continue;
  3828. }
  3829. pure.markLocal();
  3830. break;
  3831. }
  3832. if (cvType === NodeType.Function) {
  3833. if (equalsLowerCase(A.unescapedName(cv), "local")) {
  3834. pure.markLocal();
  3835. }
  3836. break;
  3837. }
  3838. }
  3839. };
  3840. // Drive the walk through SourceProcessor: structural enter / exit map to the `walkAst…Enter` / `…Exit` halves; value visitors handle url / ICSS / local-global.
  3841. /** @type {VisitorMap} */
  3842. const visitors = {
  3843. [NodeType.Comment]: commentVisitor,
  3844. [NodeType.AtRule]: {
  3845. // At-rule enter: scope save, name dispatch, prelude value context, pure-block push.
  3846. enter: (path) => {
  3847. const node = path.node;
  3848. const at = /** @type {AtRule} */ (node);
  3849. const topLevel = path.parent === null;
  3850. currentUrlRecovery = false;
  3851. advanceCommentCursor(A.start(at));
  3852. const savedAnchor = currentRule.hasLocalAnchor;
  3853. // Rollback point: nested walks append to the shared list; the exit
  3854. // truncates back to this length (no per-rule copy).
  3855. const savedLocalIdentifierCount = currentRule.localIdentifiers.length;
  3856. const name = `@${toLowerCaseIfNeeded(A.unescapedName(at))}`;
  3857. switch (name) {
  3858. case "@namespace": {
  3859. this._emitWarning(
  3860. state,
  3861. "'@namespace' is not supported in bundled CSS",
  3862. locConverter,
  3863. A.start(at),
  3864. A.nameEnd(at)
  3865. );
  3866. break;
  3867. }
  3868. case "@charset": {
  3869. if (/** @type {CssModule} */ (module).exportType !== "style") {
  3870. const atEnd = A.end(at);
  3871. const atRuleEnd =
  3872. source.charCodeAt(atEnd) === CC_SEMICOLON ? atEnd + 1 : atEnd;
  3873. const dep = new ConstDependency("", [A.start(at), atRuleEnd]);
  3874. module.addPresentationalDependency(dep);
  3875. const string = A.prelude(at).find(
  3876. (v) => A.type(v) !== NodeType.Whitespace
  3877. );
  3878. if (string && A.type(string) === NodeType.String) {
  3879. /** @type {CssModuleBuildInfo} */
  3880. (module.buildInfo).charset = source
  3881. .slice(A.start(string) + 1, A.end(string) - 1)
  3882. .toUpperCase();
  3883. }
  3884. }
  3885. break;
  3886. }
  3887. case "@import": {
  3888. handleImportAtRule(at, topLevel);
  3889. break;
  3890. }
  3891. case "@custom-media": {
  3892. if (mayHaveCustomMedia) collectCustomMedia(at);
  3893. break;
  3894. }
  3895. case "@custom-selector": {
  3896. if (mayHaveCustomSelectors) collectCustomSelector(at);
  3897. break;
  3898. }
  3899. case "@media": {
  3900. if (mayHaveCustomMedia) collectMediaQuery(at);
  3901. break;
  3902. }
  3903. default: {
  3904. if (!isModules) break;
  3905. if (name === "@value") {
  3906. handleValueAtRule(at);
  3907. break;
  3908. } else if (
  3909. this.options.animation &&
  3910. OPTIONALLY_VENDOR_PREFIXED_KEYFRAMES_AT_RULE.test(name)
  3911. ) {
  3912. processLocalAtRule(
  3913. at,
  3914. {
  3915. string: mode === "local",
  3916. identifier: mode === "local"
  3917. },
  3918. "@keyframes"
  3919. );
  3920. } else if (
  3921. this.options.customIdents &&
  3922. name === "@counter-style"
  3923. ) {
  3924. processLocalAtRule(
  3925. at,
  3926. {
  3927. identifier: mode === "local"
  3928. },
  3929. "@counter-style"
  3930. );
  3931. } else if (this.options.container && name === "@container") {
  3932. processLocalAtRule(
  3933. at,
  3934. {
  3935. identifier: mode === "local" ? /^(none|and|or|not)$/ : false
  3936. },
  3937. "@container"
  3938. );
  3939. }
  3940. }
  3941. }
  3942. // `@scope (.x) to (.y)` — walk the prelude as a selector list.
  3943. if (
  3944. isModules &&
  3945. equalsLowerCase(A.unescapedName(at), "scope") &&
  3946. A.prelude(at).length > 0
  3947. ) {
  3948. walkSelectorList(
  3949. A.prelude(at),
  3950. /** @type {"local" | "global"} */ (
  3951. mode === "local" ? "local" : "global"
  3952. )
  3953. );
  3954. }
  3955. // Prelude value-visitor context; AST-handled at-rules emit their own deps so they're excluded from the local() / global() / ICSS walks.
  3956. const effectiveLocalMode = isEffectivelyLocal();
  3957. const isProcessedByLocalAtRule = isLocalHandledAtRule(name);
  3958. currentStructural = at;
  3959. currentAtRuleName = name;
  3960. currentDeclCounterActive = false;
  3961. dashed.active = false;
  3962. // `@import` url() is the import target — only walk its prelude for url deps on malformed-import recovery.
  3963. if (this.options.url && (name !== "@import" || currentUrlRecovery)) {
  3964. lastTokenEndForComments = A.nameEnd(at);
  3965. }
  3966. // Dashed-ident scoping over the prelude (the Ident / Function visitors emit).
  3967. dashed.active = Boolean(
  3968. this.options.dashedIdents &&
  3969. isModules &&
  3970. !isProcessedByLocalAtRule &&
  3971. effectiveLocalMode &&
  3972. !(mayHaveCustomMedia && name === "@media")
  3973. );
  3974. dashed.emit = dashed.active;
  3975. // Pure-mode: `@keyframes` / `@counter-style` / `@container` bodies are marked skip / treat-as-leaf.
  3976. let atSkipChildren = false;
  3977. let atTreatAsLeaf = false;
  3978. const isKeyframes =
  3979. OPTIONALLY_VENDOR_PREFIXED_KEYFRAMES_AT_RULE.test(name);
  3980. const isCounterStyle = name === "@counter-style";
  3981. const isContainer = name === "@container";
  3982. if (
  3983. pure.enabled &&
  3984. isModules &&
  3985. isLocalMode() &&
  3986. (isKeyframes || isCounterStyle || isContainer)
  3987. ) {
  3988. if (isKeyframes || isCounterStyle) {
  3989. atSkipChildren = true;
  3990. atTreatAsLeaf = true;
  3991. }
  3992. markPureFromAtRulePrelude(
  3993. at,
  3994. isKeyframes,
  3995. isCounterStyle,
  3996. isContainer
  3997. );
  3998. }
  3999. // pure.stack push for block-bearing at-rules (pure-off builds skip
  4000. // the frame object — every consumer inside is a no-op then).
  4001. const atDecls = A.declarations(at);
  4002. const atChildRules = A.childRules(at);
  4003. const atHasBlock = Boolean(atDecls || atChildRules);
  4004. const atBlockStart = A.blockStart(at);
  4005. if (pure.enabled && atHasBlock && atBlockStart !== -1) {
  4006. const isAtRulePrelude = isPureBodyAtRule(name);
  4007. if (isAtRulePrelude) pure.finalizeSelector();
  4008. pure.enterBlock({
  4009. isRulePrelude: isAtRulePrelude,
  4010. treatAsLeaf: atTreatAsLeaf,
  4011. ownSkip: atSkipChildren,
  4012. declarations: atDecls,
  4013. childRules: atChildRules,
  4014. preludeStart: A.start(at),
  4015. preludeEnd: atBlockStart
  4016. });
  4017. }
  4018. atRuleStateStack.push({
  4019. savedAnchor,
  4020. savedLocalIdentifierCount,
  4021. name,
  4022. hasBlock: atHasBlock,
  4023. endsWithSemicolon: source.charCodeAt(A.end(at)) === CC_SEMICOLON,
  4024. fontPreloaded: false
  4025. });
  4026. },
  4027. // At-rule exit: pure-frame finalization, `suppressNextRulePrelude`, scope restore, top-level reset.
  4028. exit: (path) => {
  4029. const node = path.node;
  4030. const state = atRuleStateStack.pop();
  4031. if (!state) return;
  4032. if (state.hasBlock) {
  4033. pure.exitBlock();
  4034. } else if (
  4035. isModules &&
  4036. state.endsWithSemicolon &&
  4037. !isLocalHandledAtRule(state.name)
  4038. ) {
  4039. // An unrecognized `;`-terminated at-rule: treat the next sibling's selectors as global.
  4040. suppressNextRulePrelude = true;
  4041. }
  4042. currentRule.hasLocalAnchor = state.savedAnchor;
  4043. currentRule.localIdentifiers.length = state.savedLocalIdentifierCount;
  4044. if (path.parent === null) finishTopLevelRule(node, true);
  4045. }
  4046. },
  4047. [NodeType.QualifiedRule]: {
  4048. // Qualified-rule enter: scope setup, selector + prelude context, pure-block push; `:import` / `:export` bail via `path.skipChildren()`.
  4049. enter: (path) => {
  4050. const node = path.node;
  4051. const rule = /** @type {QualifiedRule} */ (node);
  4052. const topLevel = path.parent === null;
  4053. advanceCommentCursor(A.start(rule));
  4054. // One prelude materialization per rule, shared by the scans below
  4055. // (null in plain mode, which never reads the array).
  4056. const rulePrelude =
  4057. isModules || mayHaveCustomSelectors ? A.prelude(rule) : null;
  4058. // `:--name` custom-selector references (dashed idents never collide with the `:import`/`:export` ICSS preludes below).
  4059. if (mayHaveCustomSelectors && rulePrelude !== null) {
  4060. scanCustomSelectorUses(rulePrelude);
  4061. }
  4062. // `:import(…) { … }` / `:export { … }` ICSS pseudo-rules are processed inline at top level; nested ones bail out.
  4063. if (isModules && rulePrelude !== null) {
  4064. const firstIdx = nextNonWhitespace(rulePrelude, 0);
  4065. if (
  4066. firstIdx + 1 < rulePrelude.length &&
  4067. A.type(rulePrelude[firstIdx]) === NodeType.Colon
  4068. ) {
  4069. const second = rulePrelude[firstIdx + 1];
  4070. const secondType = A.type(second);
  4071. const rawName =
  4072. secondType === NodeType.Ident
  4073. ? A.value(second)
  4074. : secondType === NodeType.Function
  4075. ? A.name(second)
  4076. : "";
  4077. const isImport = equalsLowerCase(rawName, "import");
  4078. if (isImport || equalsLowerCase(rawName, "export")) {
  4079. if (topLevel) {
  4080. const startColon = A.start(rulePrelude[firstIdx]);
  4081. const endAfterBody = processImportOrExport(
  4082. isImport ? 0 : 1,
  4083. second,
  4084. rule
  4085. );
  4086. module.addPresentationalDependency(
  4087. new ConstDependency("", [startColon, endAfterBody])
  4088. );
  4089. if (A.blockStart(rule) !== -1) {
  4090. A.setBlockEnd(rule, endAfterBody);
  4091. }
  4092. A.setEnd(rule, endAfterBody);
  4093. } else if (A.blockStart(rule) !== -1) {
  4094. // Nested `:import` / `:export` — leave the body alone.
  4095. A.setEnd(rule, A.blockEnd(rule));
  4096. }
  4097. // Don't recurse into the body — handled inline above.
  4098. path.skipChildren();
  4099. qualifiedRuleStateStack.push({ bailed: true });
  4100. return;
  4101. }
  4102. }
  4103. }
  4104. // Reset the anchor flag for this rule's body; nested `composes:` sees
  4105. // parent + own class names in the shared identifier list, rolled back
  4106. // by length on exit (no per-rule copy).
  4107. const savedAnchor = currentRule.hasLocalAnchor;
  4108. const savedLocalIdentifierCount = currentRule.localIdentifiers.length;
  4109. currentRule.hasLocalAnchor = false;
  4110. // Composes-state reset between rules (saved / restored around this rule); composesFiles is swapped out and re-created lazily only if this rule composes.
  4111. const savedPrevComposesFile = currentRule.composesPrevFile;
  4112. const savedComposesFiles = currentRule.composesFiles;
  4113. currentRule.composesPrevFile = undefined;
  4114. currentRule.composesFiles = null;
  4115. qualifiedRuleStateStack.push({
  4116. bailed: false,
  4117. savedAnchor,
  4118. savedLocalIdentifierCount,
  4119. savedPrevComposesFile,
  4120. savedComposesFiles
  4121. });
  4122. // Selectors are only CSS-Modules-relevant when `isModules` holds.
  4123. if (isModules && rulePrelude !== null) {
  4124. walkSelectorList(
  4125. rulePrelude,
  4126. /** @type {"local" | "global"} */ (
  4127. mode === "local" ? "local" : "global"
  4128. )
  4129. );
  4130. }
  4131. // A malformed declaration can leave orphan `url(...)` in the prelude — let the url visitor pick those up.
  4132. currentStructural = rule;
  4133. currentDeclCounterActive = false;
  4134. dashed.active = false;
  4135. dashed.emit = false;
  4136. if (this.options.url && A.childCount(rule) > 0) {
  4137. lastTokenEndForComments = A.start(A.childAt(rule, 0));
  4138. }
  4139. // Dashed-ident scoping for the deprecated `--foo: { … }` custom-property-set syntax (prelude starts with a dashed-ident).
  4140. if (
  4141. this.options.dashedIdents &&
  4142. isModules &&
  4143. rulePrelude !== null &&
  4144. rulePrelude.length > 0
  4145. ) {
  4146. const first = rulePrelude[nextNonWhitespace(rulePrelude, 0)];
  4147. if (
  4148. first &&
  4149. A.type(first) === NodeType.Ident &&
  4150. rangeIsDashedIdentifier(source, A.start(first), A.end(first))
  4151. ) {
  4152. const effectiveLocalMode = isEffectivelyLocal();
  4153. if (effectiveLocalMode) {
  4154. dashed.active = true;
  4155. dashed.emit = true;
  4156. }
  4157. }
  4158. }
  4159. // Pure-mode: report an impure prelude (if leaf-ish) and push the inherited-context frame before walking the body.
  4160. if (!pure.enabled) return;
  4161. const ruleBlockStart = A.blockStart(rule);
  4162. pure.enterBlock({
  4163. isRulePrelude: true,
  4164. treatAsLeaf: false,
  4165. ownSkip: false,
  4166. declarations: A.declarations(rule),
  4167. childRules: A.childRules(rule),
  4168. preludeStart: A.start(rule),
  4169. preludeEnd: ruleBlockStart !== -1 ? ruleBlockStart : A.end(rule)
  4170. });
  4171. },
  4172. // Qualified-rule exit: pure-frame finalization, scope restore, top-level reset; no-op for bailed ICSS.
  4173. exit: (path) => {
  4174. const node = path.node;
  4175. const state = qualifiedRuleStateStack.pop();
  4176. if (!state || state.bailed) return;
  4177. pure.exitBlock();
  4178. currentRule.hasLocalAnchor = state.savedAnchor;
  4179. currentRule.localIdentifiers.length = state.savedLocalIdentifierCount;
  4180. currentRule.composesPrevFile = state.savedPrevComposesFile;
  4181. currentRule.composesFiles = state.savedComposesFiles;
  4182. if (path.parent === null) finishTopLevelRule(node, false);
  4183. }
  4184. },
  4185. // Top-level declarations are parse errors (dropped by `parseAStylesheet`), so a declaration's parent is always a block.
  4186. [NodeType.Declaration]: (path) => {
  4187. const node = path.node;
  4188. const decl = /** @type {Declaration} */ (node);
  4189. // Reset value-visitor context, read by the value visitors below.
  4190. currentStructural = decl;
  4191. dashed.active = false;
  4192. dashed.emit = false;
  4193. // Position `lastTokenEndForComments` just past the `:` so a magic comment before a url() is found (every mode).
  4194. let colonPos = A.nameEnd(decl);
  4195. while (
  4196. colonPos < source.length &&
  4197. source.charCodeAt(colonPos) !== CC_COLON
  4198. ) {
  4199. colonPos++;
  4200. }
  4201. lastTokenEndForComments = colonPos + 1;
  4202. currentDeclIsKnownProperty = false;
  4203. currentDeclComposesSkip = false;
  4204. currentDeclCounterActive = false;
  4205. dashed.counterArgs = false;
  4206. // Property-name analysis and value exports are CSS-Modules-only; a plain
  4207. // stylesheet's declarations need no per-decl name slice / known-property lookup.
  4208. if (!isModules) return;
  4209. const nameStart = A.nameStart(decl);
  4210. const nameEnd = A.nameEnd(decl);
  4211. // Range-based name analysis — the common declaration never
  4212. // slices its name out of the source.
  4213. /** @type {string | undefined} */
  4214. let declName;
  4215. /** @type {string | undefined} */
  4216. let declPropertyName;
  4217. let declIsDashed = false;
  4218. if (
  4219. source.charCodeAt(nameStart) === CC_HYPHEN_MINUS ||
  4220. source.charCodeAt(nameStart) === CC_REVERSE_SOLIDUS
  4221. ) {
  4222. if (rangeIsDashedIdentifier(source, nameStart, nameEnd)) {
  4223. // Custom property: never vendor-prefixed / known / composes.
  4224. declIsDashed = true;
  4225. } else {
  4226. // Vendor-prefixed, degenerate short, or an escaped name that is
  4227. // not dashed after all — rare, string path.
  4228. declName = A.unescapedName(decl);
  4229. declPropertyName = toLowerCaseIfNeeded(
  4230. declName.replace(VENDOR_PREFIX, "")
  4231. );
  4232. currentDeclIsKnownProperty = knownProperties.has(declPropertyName);
  4233. }
  4234. } else {
  4235. declPropertyName = knownPropertyForRange(
  4236. knownPropertyIndex,
  4237. source,
  4238. nameStart,
  4239. nameEnd
  4240. );
  4241. // Cache the known-property flag so the per-token value visitors don't recompute it.
  4242. currentDeclIsKnownProperty = declPropertyName !== undefined;
  4243. }
  4244. const effectiveLocalMode = isEffectivelyLocal();
  4245. // `composes:` with a local anchor: its strip-dep covers the whole declaration, so suppress the value's local/global/dashed/ICSS rewrites.
  4246. currentDeclComposesSkip =
  4247. currentRule.hasLocalAnchor &&
  4248. (declPropertyName !== undefined
  4249. ? COMPOSES_PROPERTY.test(declPropertyName)
  4250. : rangeEqualsLowerCase(source, nameStart, nameEnd, "composes") ||
  4251. rangeEqualsLowerCase(source, nameStart, nameEnd, "compose-with"));
  4252. if (currentDeclComposesSkip) emitComposesWithAnchor(decl);
  4253. const skipForComposes = currentDeclComposesSkip;
  4254. // `content: counter(name, style)` can name a scoped counter / `@counter-style` from any property, so this isn't driven by the known-property table.
  4255. currentDeclCounterActive =
  4256. Boolean(this.options.customIdents) &&
  4257. effectiveLocalMode &&
  4258. !skipForComposes;
  4259. // Known-property value localization (`animation-name: foo` exports `foo`).
  4260. if (effectiveLocalMode && currentDeclIsKnownProperty) {
  4261. emitKnownPropertyExports(
  4262. decl,
  4263. /** @type {string} */ (declPropertyName)
  4264. );
  4265. }
  4266. // 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).
  4267. if (
  4268. this.options.dashedIdents &&
  4269. effectiveLocalMode &&
  4270. !skipForComposes
  4271. ) {
  4272. // Only the `--`-prefixed path can carry a dashed ident.
  4273. if (declIsDashed) {
  4274. emitDashedIdentExport(nameStart, nameEnd);
  4275. }
  4276. dashed.active = true;
  4277. dashed.emit = !currentDeclIsKnownProperty;
  4278. }
  4279. // 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.
  4280. if (
  4281. !skipForComposes &&
  4282. !currentDeclIsKnownProperty &&
  4283. icssDefinitions.size !== 0
  4284. ) {
  4285. if (declName === undefined) {
  4286. declName = source.slice(nameStart, nameEnd);
  4287. }
  4288. if (
  4289. !(dashed.active && declIsDashed) &&
  4290. icssDefinitions.has(declName)
  4291. ) {
  4292. emitICSSSymbol(declName, nameStart, nameEnd);
  4293. }
  4294. }
  4295. },
  4296. // Value-level visitors decide handling from the enclosing node via `urlActive()` / `localGlobalActive()` / `icssActive()`.
  4297. [NodeType.Url]: (path) => {
  4298. const node = path.node;
  4299. const url = /** @type {UrlToken} */ (node);
  4300. if (!urlActive()) return;
  4301. // Skip bare url-tokens for a known property in CSS-Modules local mode.
  4302. if (
  4303. currentStructural &&
  4304. A.type(currentStructural) === NodeType.Declaration &&
  4305. isModules &&
  4306. currentDeclIsKnownProperty &&
  4307. isEffectivelyLocal()
  4308. ) {
  4309. return;
  4310. }
  4311. const { ignored, options: urlComments } = magicCommentsIn(
  4312. [lastTokenEndForComments, A.end(node)],
  4313. lastTokenEndForComments,
  4314. A.end(node)
  4315. );
  4316. if (ignored) return;
  4317. let value = normalizeUrl(
  4318. input.slice(A.contentStart(url), A.contentEnd(url)),
  4319. false
  4320. );
  4321. // Ignore `url()`, `url('')` and `url("")`, they are valid by spec
  4322. if (value.length === 0) return;
  4323. if (isModules) {
  4324. const def = icssDefinitions.get(value);
  4325. if (def) {
  4326. if (def.value !== undefined) {
  4327. const raw = def.value.trim();
  4328. value =
  4329. (raw.startsWith('"') && raw.endsWith('"')) ||
  4330. (raw.startsWith("'") && raw.endsWith("'"))
  4331. ? normalizeUrl(raw.slice(1, -1), true)
  4332. : normalizeUrl(raw, false);
  4333. if (value.length === 0) return;
  4334. } else {
  4335. this._emitWarning(
  4336. state,
  4337. `'@value' identifier '${value}' was imported from another module and cannot be used inside 'url()' — only locally defined values are supported here`,
  4338. locConverter,
  4339. A.start(node),
  4340. A.end(node)
  4341. );
  4342. return;
  4343. }
  4344. }
  4345. }
  4346. const dep = new CssUrlDependency(
  4347. value,
  4348. [A.start(node), A.end(node)],
  4349. "url"
  4350. );
  4351. setDepLoc(dep, A.start(node), A.end(node));
  4352. applyResourceHintDefaults(
  4353. dep,
  4354. value,
  4355. urlComments,
  4356. rangeLoc(A.start(node), A.end(node))
  4357. );
  4358. module.addDependency(dep);
  4359. module.addCodeGenerationDependency(dep);
  4360. },
  4361. [NodeType.Comma](path) {
  4362. const node = path.node;
  4363. if (urlActive()) lastTokenEndForComments = A.start(node);
  4364. },
  4365. [NodeType.Function]: {
  4366. enter: (path) => {
  4367. const node = path.node;
  4368. const fn = /** @type {FunctionNode} */ (node);
  4369. const fnNameStart = A.nameStart(fn);
  4370. const fnNameEnd = A.nameEnd(fn);
  4371. const fnNameLength = fnNameEnd - fnNameStart;
  4372. // Functions are the densest value nodes, so the name is matched by raw byte range; only a name carrying an escape (rare) pays the unescaped slice.
  4373. /** @type {string | undefined} */
  4374. let escapedName;
  4375. let isLocalFn = false;
  4376. let isGlobalFn = false;
  4377. if (rangeHasEscape(input, fnNameStart, fnNameEnd)) {
  4378. escapedName = A.unescapedName(fn);
  4379. isLocalFn = equalsLowerCase(escapedName, "local");
  4380. isGlobalFn = !isLocalFn && equalsLowerCase(escapedName, "global");
  4381. } else if (fnNameLength === 5) {
  4382. isLocalFn = rangeEqualsLowerCase(
  4383. input,
  4384. fnNameStart,
  4385. fnNameEnd,
  4386. "local"
  4387. );
  4388. } else if (fnNameLength === 6) {
  4389. isGlobalFn = rangeEqualsLowerCase(
  4390. input,
  4391. fnNameStart,
  4392. fnNameEnd,
  4393. "global"
  4394. );
  4395. }
  4396. if (urlActive()) emitUrlFunction(fn, escapedName);
  4397. if (localGlobalActive() && (isLocalFn || isGlobalFn)) {
  4398. processLocalOrGlobalFunction(fn, isLocalFn ? 1 : 2);
  4399. }
  4400. if (
  4401. icssActive() &&
  4402. !isLocalFn &&
  4403. !isGlobalFn &&
  4404. icssDefinitions.size !== 0
  4405. ) {
  4406. // Without an escape the raw name equals the unescaped one — only the `@value`-lookup path needs the string at all.
  4407. const fname = escapedName === undefined ? A.name(fn) : escapedName;
  4408. if (
  4409. !(dashed.active && isDashedIdentifier(fname)) &&
  4410. icssDefinitions.has(fname)
  4411. ) {
  4412. emitICSSSymbol(fname, fnNameStart, fnNameEnd);
  4413. }
  4414. }
  4415. // `counter()` / `counters()` / `target-counter()` / `target-counters()`: rewrite the counter name and any trailing counter-style ident.
  4416. let counterNameIndex = -1;
  4417. if (currentDeclCounterActive && !isLocalFn && !isGlobalFn) {
  4418. counterNameIndex = counterFunctionNameIndex(
  4419. input,
  4420. fnNameStart,
  4421. fnNameEnd,
  4422. escapedName
  4423. );
  4424. if (counterNameIndex !== -1) {
  4425. walkCounterFunction(A.children(fn), counterNameIndex);
  4426. }
  4427. }
  4428. // Dashed-ident scoping: handle this function, then set the child nesting level's state for the walk.
  4429. dashed.push();
  4430. dashed.counterArgs = counterNameIndex !== -1;
  4431. if (dashed.active) {
  4432. if (isLocalFn || isGlobalFn) {
  4433. // `local()` / `global()` dashed args go through the ICSS path above, not here.
  4434. dashed.active = false;
  4435. } else if (
  4436. escapedName === undefined
  4437. ? (fnNameLength === 3 &&
  4438. rangeEqualsLowerCase(
  4439. input,
  4440. fnNameStart,
  4441. fnNameEnd,
  4442. "var"
  4443. )) ||
  4444. (fnNameLength === 5 &&
  4445. rangeEqualsLowerCase(
  4446. input,
  4447. fnNameStart,
  4448. fnNameEnd,
  4449. "style"
  4450. ))
  4451. : equalsLowerCase(escapedName, "var") ||
  4452. equalsLowerCase(escapedName, "style")
  4453. ) {
  4454. // `var(--foo, …)` / `style(--foo, …)`: emit the first ident; the fallback doesn't self-emit.
  4455. processDashedIdentInVarFunction(fn);
  4456. dashed.emit = false;
  4457. } else if (
  4458. dashed.emit &&
  4459. fnNameLength >= 3 &&
  4460. input.charCodeAt(fnNameStart) === CC_HYPHEN_MINUS &&
  4461. input.charCodeAt(fnNameStart + 1) === CC_HYPHEN_MINUS
  4462. ) {
  4463. // Custom-function call `--my-func(args)` — the name is the exported dashed-ident (a literal `--` prefix, like the `A.name` string check it replaces).
  4464. emitDashedIdentExport(fnNameStart, fnNameEnd);
  4465. }
  4466. }
  4467. },
  4468. exit: () => {
  4469. dashed.pop();
  4470. }
  4471. },
  4472. [NodeType.Ident](path) {
  4473. const node = path.node;
  4474. // Fast exit before slicing the ident value: outside dashed-ident scoping
  4475. // and ICSS context (any non-CSS-Modules stylesheet) a bare ident carries
  4476. // no work, and idents are the most common node, so skipping the per-ident
  4477. // `value` slice matters. With no `@value`/`:import` definitions the ICSS
  4478. // probe can never hit, so it doesn't warrant the slice either. The dashed
  4479. // probe reads two char codes instead of slicing (escaped dashes don't
  4480. // match, same as the string form — cf. `processDashedIdentInVarFunction`).
  4481. const dashedActive = dashed.active;
  4482. const icss = icssActive() && icssDefinitions.size !== 0;
  4483. if (!dashedActive && !icss) return;
  4484. const identStart = A.start(node);
  4485. const identEnd = A.end(node);
  4486. if (
  4487. dashedActive &&
  4488. rangeIsDashedIdentifier(input, identStart, identEnd)
  4489. ) {
  4490. // Dashed idents are scoped here, never `@value` ICSS-rewritten.
  4491. if (!dashed.emit) return;
  4492. // Resolve the `--foo from "./x.css"` / `--foo from global` import suffix via sibling lookahead over the parent's child span (`path.index` avoids materializing the sibling list per dashed ident).
  4493. const parent = path.parent;
  4494. if (parent) {
  4495. const count = A.childCount(parent);
  4496. let j = path.index + 1;
  4497. while (
  4498. j < count &&
  4499. A.type(A.childAt(parent, j)) === NodeType.Whitespace
  4500. ) {
  4501. j++;
  4502. }
  4503. const fromIdent = j < count ? A.childAt(parent, j) : undefined;
  4504. if (
  4505. fromIdent &&
  4506. A.type(fromIdent) === NodeType.Ident &&
  4507. rangeEqualsLowerCase(
  4508. input,
  4509. A.start(fromIdent),
  4510. A.end(fromIdent),
  4511. "from"
  4512. )
  4513. ) {
  4514. j++;
  4515. while (
  4516. j < count &&
  4517. A.type(A.childAt(parent, j)) === NodeType.Whitespace
  4518. ) {
  4519. j++;
  4520. }
  4521. const sourceNode = j < count ? A.childAt(parent, j) : undefined;
  4522. if (
  4523. sourceNode &&
  4524. A.type(sourceNode) === NodeType.Ident &&
  4525. rangeEquals(
  4526. input,
  4527. A.start(sourceNode),
  4528. A.end(sourceNode),
  4529. "global"
  4530. )
  4531. ) {
  4532. emitDashedIdentFromGlobal(identEnd, A.end(sourceNode));
  4533. return;
  4534. }
  4535. if (sourceNode && A.type(sourceNode) === NodeType.String) {
  4536. emitDashedIdentImport(
  4537. identStart,
  4538. identEnd,
  4539. A.start(fromIdent),
  4540. A.end(sourceNode),
  4541. input.slice(A.start(sourceNode) + 1, A.end(sourceNode) - 1)
  4542. );
  4543. return;
  4544. }
  4545. }
  4546. }
  4547. emitDashedIdentExport(identStart, identEnd);
  4548. return;
  4549. }
  4550. if (!icss) return;
  4551. // Already rewritten as a counter / counter-style name — a second dep would overlap the same range.
  4552. if (dashed.counterArgs) return;
  4553. // Deferred: only the ICSS probe needs the ident's string value.
  4554. const identValue = A.value(node);
  4555. if (icssDefinitions.has(identValue)) {
  4556. emitICSSSymbol(identValue, identStart, identEnd);
  4557. }
  4558. }
  4559. };
  4560. // `as` selects the top-level production (§5.3): a `style` attribute is a
  4561. // block's contents, everything else a full stylesheet (the default). `as`
  4562. // and `skip` are per-parse config on the processor; only `locConverter`
  4563. // (per-source) is passed to `process`. One skip set: selector prelude +
  4564. // unread value leaves (non-modules only; CSS Modules needs its selectors
  4565. // and ICSS-captured values).
  4566. new SourceProcessor()
  4567. .use(/** @type {VisitorMap} */ (visitors))
  4568. .process(source, {
  4569. as: /** @type {"stylesheet" | "block-contents"} */ (this.options.as),
  4570. // Non-modules parses skip selector preludes, but `@custom-selector` expansion needs them, so keep them when the file may use one.
  4571. skip: isModules
  4572. ? undefined
  4573. : mayHaveCustomSelectors
  4574. ? SKIP_NON_MODULES_KEEP_SELECTORS
  4575. : SKIP_NON_MODULES,
  4576. locConverter
  4577. });
  4578. if (customMediaQueries || customSelectorUses) {
  4579. resolveCustomMediaAndSelectors();
  4580. }
  4581. /** @type {BuildInfo} */
  4582. (module.buildInfo).strict = true;
  4583. // 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).
  4584. if (composesFirstFileImport.size > 1) {
  4585. topologicalSort(
  4586. composesGraph,
  4587. [...composesFirstFileImport.keys()],
  4588. (file, i) => {
  4589. /** @type {CssIcssImportDependency} */
  4590. (composesFirstFileImport.get(file)).sourceOrder = i;
  4591. }
  4592. );
  4593. }
  4594. const buildMeta = /** @type {BuildMeta} */ (state.module.buildMeta);
  4595. buildMeta.exportsType = this.options.namedExports ? "namespace" : "default";
  4596. buildMeta.defaultObject = this.options.namedExports
  4597. ? false
  4598. : "redirect-warn";
  4599. if (cssExportEntries.length > 0) {
  4600. if (isModules && declaredExports.size > 0) {
  4601. this._resolveAmbiguousExports(state, declaredExports, cssExportEntries);
  4602. }
  4603. module.addDependency(new CssIcssExportDependency(cssExportEntries));
  4604. }
  4605. if (
  4606. /** @type {CssModule} */ (module).exportType === "text" ||
  4607. /** @type {CssModule} */ (module).exportType === "css-style-sheet"
  4608. ) {
  4609. module.addDependency(new StaticExportsDependency(["default"], true));
  4610. } else {
  4611. module.addDependency(new StaticExportsDependency([], true));
  4612. }
  4613. return state;
  4614. }
  4615. }
  4616. module.exports = CssParser;