{"version":3,"file":"narrow-tokens.cjs","names":[],"sources":["../../src/vite/narrow-tokens.ts"],"sourcesContent":["/**\n * Cut the token sheet down to the tokens a set of stylesheets can reach.\n *\n * Once every component loads its own stylesheet, `tokens.css` stops being a rounding\n * error and becomes the biggest single thing an app downloads: measured on a probe\n * app mounting `Button`, `Card` and `Badge`, the sheet is 2516 B brotli of a 4927 B\n * total. The components read 105 of its 223 tokens, so the other 118 are bytes\n * nothing on the page can consult.\n *\n * The cut is a reachability closure, not a filter: a token's value routinely names\n * another token (`--tempest-primary: var(--tempest-primary-500)`), so keeping only\n * the names a stylesheet mentions literally would keep aliases whose targets were\n * dropped — `var()` would resolve to nothing and the component would render with the\n * property unset, which is worse than shipping the whole sheet.\n */\n\n/** A token block: one selector and the custom properties it declares, in order. */\ninterface TokenBlock {\n    /** The block's selector text, e.g. `:root` or `[data-tempest-theme=dark]`. */\n    selector: string;\n    /** Declaration name → value, in source order. */\n    declarations: Map<string, string>;\n}\n\n/**\n * Any custom property being read, not only the `--tempest-*` ones.\n *\n * Deliberately wider than the tokens this cut is about. `tokens.css` also carries\n * the `--lightningcss-light`/`--lightningcss-dark` pair the compiler emits, which\n * nothing reads today — measured: two declarations, zero reads — and which a\n * `--tempest-`-only scan would therefore always drop. That is the right answer right\n * up until one rule uses `light-dark()`, at which point the compiler emits the read\n * and a narrow scan would drop the pair it depends on. Matching every custom\n * property costs nothing (a name the sheet does not declare is discarded) and makes\n * the closure correct for names this file does not know about.\n */\nconst TOKEN_REFERENCE = /var\\(\\s*(--[A-Za-z0-9-]+)/g;\n\n/**\n * A token name being assembled at runtime rather than written out.\n *\n * `style={{ color: `var(--tempest-${tone})` }}` names a token no static scan can\n * know, so a cut made without it would drop the one the app is about to read. The\n * caller falls back to the whole sheet when this matches — the same shape as the\n * plugin's own namespace-import fallback, and for the same reason: serve more than\n * asked for rather than less than needed.\n */\nconst DYNAMIC_TOKEN = /--tempest-[A-Za-z0-9-]*(?:\\$\\{|[\"'`]\\s*\\+|\\s*\\+\\s*[\"'`])/;\n\n/**\n * Parse a token stylesheet into its blocks.\n *\n * Written against the compiler's own output, which is already valid and flat — every\n * block in `tokens.css` is either a bare selector or one nested inside a single\n * at-rule — so a brace-depth reader is enough and a full parser would not be more\n * correct here.\n *\n * @param css - Contents of `tokens.css`.\n * @returns One entry per block that declares anything.\n */\nexport function parseTokenBlocks(css: string): TokenBlock[] {\n    const blocks: TokenBlock[] = [];\n    for (const match of css.matchAll(/([^{}]+)\\{([^{}]*)\\}/g)) {\n        const declarations = new Map<string, string>();\n        for (const declaration of (match[2] ?? \"\").split(\";\")) {\n            const colon = declaration.indexOf(\":\");\n            if (colon < 0) continue;\n            const name = declaration.slice(0, colon).trim();\n            if (name) declarations.set(name, declaration.slice(colon + 1).trim());\n        }\n        if (declarations.size === 0) continue;\n        blocks.push({ selector: (match[1] ?? \"\").trim(), declarations });\n    }\n    return blocks;\n}\n\n/**\n * Every token a body of CSS reaches, following token-to-token references.\n *\n * @param blocks - The parsed token sheet.\n * @param usage - CSS and source text to scan for `var(--tempest-*)`.\n * @returns The reachable token names.\n */\nexport function reachableTokens(blocks: readonly TokenBlock[], usage: string): Set<string> {\n    const declared = new Set<string>();\n    for (const block of blocks) for (const name of block.declarations.keys()) declared.add(name);\n\n    const reached = new Set<string>();\n    const queue = [...usage.matchAll(TOKEN_REFERENCE)].map((match) => match[1] ?? \"\");\n    while (queue.length > 0) {\n        const token = queue.pop() ?? \"\";\n        if (reached.has(token) || !declared.has(token)) continue;\n        reached.add(token);\n        for (const block of blocks) {\n            const value = block.declarations.get(token);\n            if (value === undefined) continue;\n            for (const match of value.matchAll(TOKEN_REFERENCE)) queue.push(match[1] ?? \"\");\n        }\n    }\n    return reached;\n}\n\n/**\n * Rebuild the token sheet with only the tokens a set of stylesheets reaches.\n *\n * Companion declarations that are not custom properties travel with the block that\n * survives — `color-scheme` is the one, and dropping it would leave the browser\n * painting its own surfaces (scrollbar, `<select>` popup, autofill) in the wrong\n * theme while every `.tempest_*` rule was correct.\n *\n * @param tokensCss - Contents of `tokens.css`.\n * @param usage - The CSS the app will load, plus its own source, scanned for reads.\n * @returns The narrowed sheet, or `null` when a token name is built at runtime and\n *   no static answer is safe.\n */\nexport function narrowTokens(tokensCss: string, usage: string): string | null {\n    if (DYNAMIC_TOKEN.test(usage)) return null;\n\n    const blocks = parseTokenBlocks(tokensCss);\n    const reached = reachableTokens(blocks, usage);\n    if (reached.size === 0) return null;\n\n    const out: string[] = [];\n    for (const block of blocks) {\n        const kept = [...block.declarations].filter(\n            ([name]) => name.startsWith(\"--\") === false || reached.has(name),\n        );\n        if (kept.every(([name]) => name.startsWith(\"--\") === false)) continue;\n        out.push(`${block.selector}{${kept.map(([name, value]) => `${name}:${value}`).join(\";\")}}`);\n    }\n    return out.join(\"\\n\");\n}\n"],"mappings":"AAoCA,IAAM,EAAkB,6BAWlB,EAAgB,2DAatB,SAAgB,EAAiB,EAA2B,CACxD,IAAM,EAAuB,CAAC,EAC9B,IAAK,IAAM,KAAS,EAAI,SAAS,uBAAuB,EAAG,CACvD,IAAM,EAAe,IAAI,IACzB,IAAK,IAAM,KAAgB,EAAM,IAAM,GAAA,CAAI,MAAM,GAAG,EAAG,CACnD,IAAM,EAAQ,EAAY,QAAQ,GAAG,EACrC,GAAI,EAAQ,EAAG,SACf,IAAM,EAAO,EAAY,MAAM,EAAG,CAAK,CAAC,CAAC,KAAK,EAC1C,GAAM,EAAa,IAAI,EAAM,EAAY,MAAM,EAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,CACxE,CACI,EAAa,OAAS,GAC1B,EAAO,KAAK,CAAE,UAAW,EAAM,IAAM,GAAA,CAAI,KAAK,EAAG,cAAa,CAAC,CACnE,CACA,OAAO,CACX,CASA,SAAgB,EAAgB,EAA+B,EAA4B,CACvF,IAAM,EAAW,IAAI,IACrB,IAAK,IAAM,KAAS,EAAQ,IAAK,IAAM,KAAQ,EAAM,aAAa,KAAK,EAAG,EAAS,IAAI,CAAI,EAE3F,IAAM,EAAU,IAAI,IACd,EAAQ,CAAC,GAAG,EAAM,SAAS,CAAe,CAAC,CAAC,CAAC,IAAK,GAAU,EAAM,IAAM,EAAE,EAChF,KAAO,EAAM,OAAS,GAAG,CACrB,IAAM,EAAQ,EAAM,IAAI,GAAK,GACzB,MAAQ,IAAI,CAAK,GAAM,EAAS,IAAI,CAAK,EAC7C,GAAQ,IAAI,CAAK,EACjB,IAAK,IAAM,KAAS,EAAQ,CACxB,IAAM,EAAQ,EAAM,aAAa,IAAI,CAAK,EACtC,OAAU,IAAA,GACd,IAAK,IAAM,KAAS,EAAM,SAAS,CAAe,EAAG,EAAM,KAAK,EAAM,IAAM,EAAE,CAClF,CALiB,CAMrB,CACA,OAAO,CACX,CAeA,SAAgB,EAAa,EAAmB,EAA8B,CAC1E,GAAI,EAAc,KAAK,CAAK,EAAG,OAAO,KAEtC,IAAM,EAAS,EAAiB,CAAS,EACnC,EAAU,EAAgB,EAAQ,CAAK,EAC7C,GAAI,EAAQ,OAAS,EAAG,OAAO,KAE/B,IAAM,EAAgB,CAAC,EACvB,IAAK,IAAM,KAAS,EAAQ,CACxB,IAAM,EAAO,CAAC,GAAG,EAAM,YAAY,CAAC,CAAC,QAChC,CAAC,KAAU,EAAK,WAAW,IAAI,IAAM,IAAS,EAAQ,IAAI,CAAI,CACnE,EACI,EAAK,OAAO,CAAC,KAAU,EAAK,WAAW,IAAI,IAAM,EAAK,GAC1D,EAAI,KAAK,GAAG,EAAM,SAAS,GAAG,EAAK,KAAK,CAAC,EAAM,KAAW,GAAG,EAAK,GAAG,GAAO,CAAC,CAAC,KAAK,GAAG,EAAE,EAAE,CAC9F,CACA,OAAO,EAAI,KAAK;CAAI,CACxB"}