From b1e2c8fd5cb5dfa46bc440a12eafaf56cd844b1c Mon Sep 17 00:00:00 2001 From: Philipp Tanlak Date: Mon, 24 Nov 2025 20:54:57 +0100 Subject: Docs --- .../tailwindcss/src/lib/cacheInvalidation.js | 52 + .../tailwindcss/src/lib/collapseAdjacentRules.js | 58 + .../src/lib/collapseDuplicateDeclarations.js | 93 ++ node_modules/tailwindcss/src/lib/content.js | 208 +++ .../tailwindcss/src/lib/defaultExtractor.js | 216 ++++ node_modules/tailwindcss/src/lib/detectNesting.js | 47 + .../src/lib/evaluateTailwindFunctions.js | 272 ++++ .../tailwindcss/src/lib/expandApplyAtRules.js | 620 +++++++++ .../tailwindcss/src/lib/expandTailwindAtRules.js | 297 +++++ .../tailwindcss/src/lib/findAtConfigPath.js | 48 + node_modules/tailwindcss/src/lib/generateRules.js | 936 ++++++++++++++ .../tailwindcss/src/lib/getModuleDependencies.js | 79 ++ node_modules/tailwindcss/src/lib/load-config.ts | 31 + .../src/lib/normalizeTailwindDirectives.js | 84 ++ node_modules/tailwindcss/src/lib/offsets.js | 373 ++++++ .../tailwindcss/src/lib/partitionApplyAtRules.js | 52 + node_modules/tailwindcss/src/lib/regex.js | 74 ++ node_modules/tailwindcss/src/lib/remap-bitfield.js | 82 ++ .../tailwindcss/src/lib/resolveDefaultsAtRules.js | 163 +++ .../tailwindcss/src/lib/setupContextUtils.js | 1342 ++++++++++++++++++++ .../tailwindcss/src/lib/setupTrackingContext.js | 169 +++ node_modules/tailwindcss/src/lib/sharedState.js | 61 + .../tailwindcss/src/lib/substituteScreenAtRules.js | 19 + 23 files changed, 5376 insertions(+) create mode 100644 node_modules/tailwindcss/src/lib/cacheInvalidation.js create mode 100644 node_modules/tailwindcss/src/lib/collapseAdjacentRules.js create mode 100644 node_modules/tailwindcss/src/lib/collapseDuplicateDeclarations.js create mode 100644 node_modules/tailwindcss/src/lib/content.js create mode 100644 node_modules/tailwindcss/src/lib/defaultExtractor.js create mode 100644 node_modules/tailwindcss/src/lib/detectNesting.js create mode 100644 node_modules/tailwindcss/src/lib/evaluateTailwindFunctions.js create mode 100644 node_modules/tailwindcss/src/lib/expandApplyAtRules.js create mode 100644 node_modules/tailwindcss/src/lib/expandTailwindAtRules.js create mode 100644 node_modules/tailwindcss/src/lib/findAtConfigPath.js create mode 100644 node_modules/tailwindcss/src/lib/generateRules.js create mode 100644 node_modules/tailwindcss/src/lib/getModuleDependencies.js create mode 100644 node_modules/tailwindcss/src/lib/load-config.ts create mode 100644 node_modules/tailwindcss/src/lib/normalizeTailwindDirectives.js create mode 100644 node_modules/tailwindcss/src/lib/offsets.js create mode 100644 node_modules/tailwindcss/src/lib/partitionApplyAtRules.js create mode 100644 node_modules/tailwindcss/src/lib/regex.js create mode 100644 node_modules/tailwindcss/src/lib/remap-bitfield.js create mode 100644 node_modules/tailwindcss/src/lib/resolveDefaultsAtRules.js create mode 100644 node_modules/tailwindcss/src/lib/setupContextUtils.js create mode 100644 node_modules/tailwindcss/src/lib/setupTrackingContext.js create mode 100644 node_modules/tailwindcss/src/lib/sharedState.js create mode 100644 node_modules/tailwindcss/src/lib/substituteScreenAtRules.js (limited to 'node_modules/tailwindcss/src/lib') diff --git a/node_modules/tailwindcss/src/lib/cacheInvalidation.js b/node_modules/tailwindcss/src/lib/cacheInvalidation.js new file mode 100644 index 0000000..fa13702 --- /dev/null +++ b/node_modules/tailwindcss/src/lib/cacheInvalidation.js @@ -0,0 +1,52 @@ +import crypto from 'crypto' +import * as sharedState from './sharedState' + +/** + * Calculate the hash of a string. + * + * This doesn't need to be cryptographically secure or + * anything like that since it's used only to detect + * when the CSS changes to invalidate the context. + * + * This is wrapped in a try/catch because it's really dependent + * on how Node itself is build and the environment and OpenSSL + * version / build that is installed on the user's machine. + * + * Based on the environment this can just outright fail. + * + * See https://github.com/nodejs/node/issues/40455 + * + * @param {string} str + */ +function getHash(str) { + try { + return crypto.createHash('md5').update(str, 'utf-8').digest('binary') + } catch (err) { + return '' + } +} + +/** + * Determine if the CSS tree is different from the + * previous version for the given `sourcePath`. + * + * @param {string} sourcePath + * @param {import('postcss').Node} root + */ +export function hasContentChanged(sourcePath, root) { + let css = root.toString() + + // We only care about files with @tailwind directives + // Other files use an existing context + if (!css.includes('@tailwind')) { + return false + } + + let existingHash = sharedState.sourceHashMap.get(sourcePath) + let rootHash = getHash(css) + let didChange = existingHash !== rootHash + + sharedState.sourceHashMap.set(sourcePath, rootHash) + + return didChange +} diff --git a/node_modules/tailwindcss/src/lib/collapseAdjacentRules.js b/node_modules/tailwindcss/src/lib/collapseAdjacentRules.js new file mode 100644 index 0000000..119f592 --- /dev/null +++ b/node_modules/tailwindcss/src/lib/collapseAdjacentRules.js @@ -0,0 +1,58 @@ +let comparisonMap = { + atrule: ['name', 'params'], + rule: ['selector'], +} +let types = new Set(Object.keys(comparisonMap)) + +export default function collapseAdjacentRules() { + function collapseRulesIn(root) { + let currentRule = null + root.each((node) => { + if (!types.has(node.type)) { + currentRule = null + return + } + + if (currentRule === null) { + currentRule = node + return + } + + let properties = comparisonMap[node.type] + + if (node.type === 'atrule' && node.name === 'font-face') { + currentRule = node + } else if ( + properties.every( + (property) => + (node[property] ?? '').replace(/\s+/g, ' ') === + (currentRule[property] ?? '').replace(/\s+/g, ' ') + ) + ) { + // An AtRule may not have children (for example if we encounter duplicate @import url(…) rules) + if (node.nodes) { + currentRule.append(node.nodes) + } + + node.remove() + } else { + currentRule = node + } + }) + + // After we've collapsed adjacent rules & at-rules, we need to collapse + // adjacent rules & at-rules that are children of at-rules. + // We do not care about nesting rules because Tailwind CSS + // explicitly does not handle rule nesting on its own as + // the user is expected to use a nesting plugin + root.each((node) => { + if (node.type === 'atrule') { + collapseRulesIn(node) + } + }) + } + + return (root) => { + collapseRulesIn(root) + } +} diff --git a/node_modules/tailwindcss/src/lib/collapseDuplicateDeclarations.js b/node_modules/tailwindcss/src/lib/collapseDuplicateDeclarations.js new file mode 100644 index 0000000..d310d58 --- /dev/null +++ b/node_modules/tailwindcss/src/lib/collapseDuplicateDeclarations.js @@ -0,0 +1,93 @@ +export default function collapseDuplicateDeclarations() { + return (root) => { + root.walkRules((node) => { + let seen = new Map() + let droppable = new Set([]) + let byProperty = new Map() + + node.walkDecls((decl) => { + // This could happen if we have nested selectors. In that case the + // parent will loop over all its declarations but also the declarations + // of nested rules. With this we ensure that we are shallowly checking + // declarations. + if (decl.parent !== node) { + return + } + + if (seen.has(decl.prop)) { + // Exact same value as what we have seen so far + if (seen.get(decl.prop).value === decl.value) { + // Keep the last one, drop the one we've seen so far + droppable.add(seen.get(decl.prop)) + // Override the existing one with the new value. This is necessary + // so that if we happen to have more than one declaration with the + // same value, that we keep removing the previous one. Otherwise we + // will only remove the *first* one. + seen.set(decl.prop, decl) + return + } + + // Not the same value, so we need to check if we can merge it so + // let's collect it first. + if (!byProperty.has(decl.prop)) { + byProperty.set(decl.prop, new Set()) + } + + byProperty.get(decl.prop).add(seen.get(decl.prop)) + byProperty.get(decl.prop).add(decl) + } + + seen.set(decl.prop, decl) + }) + + // Drop all the duplicate declarations with the exact same value we've + // already seen so far. + for (let decl of droppable) { + decl.remove() + } + + // Analyze the declarations based on its unit, drop all the declarations + // with the same unit but the last one in the list. + for (let declarations of byProperty.values()) { + let byUnit = new Map() + + for (let decl of declarations) { + let unit = resolveUnit(decl.value) + if (unit === null) { + // We don't have a unit, so should never try and collapse this + // value. This is because we can't know how to do it in a correct + // way (e.g.: overrides for older browsers) + continue + } + + if (!byUnit.has(unit)) { + byUnit.set(unit, new Set()) + } + + byUnit.get(unit).add(decl) + } + + for (let declarations of byUnit.values()) { + // Get all but the last one + let removableDeclarations = Array.from(declarations).slice(0, -1) + + for (let decl of removableDeclarations) { + decl.remove() + } + } + } + }) + } +} + +let UNITLESS_NUMBER = Symbol('unitless-number') + +function resolveUnit(input) { + let result = /^-?\d*.?\d+([\w%]+)?$/g.exec(input) + + if (result) { + return result[1] ?? UNITLESS_NUMBER + } + + return null +} diff --git a/node_modules/tailwindcss/src/lib/content.js b/node_modules/tailwindcss/src/lib/content.js new file mode 100644 index 0000000..e814efe --- /dev/null +++ b/node_modules/tailwindcss/src/lib/content.js @@ -0,0 +1,208 @@ +// @ts-check + +import fs from 'fs' +import path from 'path' +import isGlob from 'is-glob' +import fastGlob from 'fast-glob' +import normalizePath from 'normalize-path' +import { parseGlob } from '../util/parseGlob' +import { env } from './sharedState' + +/** @typedef {import('../../types/config.js').RawFile} RawFile */ +/** @typedef {import('../../types/config.js').FilePath} FilePath */ + +/** + * @typedef {object} ContentPath + * @property {string} original + * @property {string} base + * @property {string | null} glob + * @property {boolean} ignore + * @property {string} pattern + */ + +/** + * Turn a list of content paths (absolute or not; glob or not) into a list of + * absolute file paths that exist on the filesystem + * + * If there are symlinks in the path then multiple paths will be returned + * one for the symlink and one for the actual file + * + * @param {*} context + * @param {import('tailwindcss').Config} tailwindConfig + * @returns {ContentPath[]} + */ +export function parseCandidateFiles(context, tailwindConfig) { + let files = tailwindConfig.content.files + + // Normalize the file globs + files = files.filter((filePath) => typeof filePath === 'string') + files = files.map(normalizePath) + + // Split into included and excluded globs + let tasks = fastGlob.generateTasks(files) + + /** @type {ContentPath[]} */ + let included = [] + + /** @type {ContentPath[]} */ + let excluded = [] + + for (const task of tasks) { + included.push(...task.positive.map((filePath) => parseFilePath(filePath, false))) + excluded.push(...task.negative.map((filePath) => parseFilePath(filePath, true))) + } + + let paths = [...included, ...excluded] + + // Resolve paths relative to the config file or cwd + paths = resolveRelativePaths(context, paths) + + // Resolve symlinks if possible + paths = paths.flatMap(resolvePathSymlinks) + + // Update cached patterns + paths = paths.map(resolveGlobPattern) + + return paths +} + +/** + * + * @param {string} filePath + * @param {boolean} ignore + * @returns {ContentPath} + */ +function parseFilePath(filePath, ignore) { + let contentPath = { + original: filePath, + base: filePath, + ignore, + pattern: filePath, + glob: null, + } + + if (isGlob(filePath)) { + Object.assign(contentPath, parseGlob(filePath)) + } + + return contentPath +} + +/** + * + * @param {ContentPath} contentPath + * @returns {ContentPath} + */ +function resolveGlobPattern(contentPath) { + // This is required for Windows support to properly pick up Glob paths. + // Afaik, this technically shouldn't be needed but there's probably + // some internal, direct path matching with a normalized path in + // a package which can't handle mixed directory separators + let base = normalizePath(contentPath.base) + + // If the user's file path contains any special characters (like parens) for instance fast-glob + // is like "OOOH SHINY" and treats them as such. So we have to escape the base path to fix this + base = fastGlob.escapePath(base) + + contentPath.pattern = contentPath.glob ? `${base}/${contentPath.glob}` : base + contentPath.pattern = contentPath.ignore ? `!${contentPath.pattern}` : contentPath.pattern + + return contentPath +} + +/** + * Resolve each path relative to the config file (when possible) if the experimental flag is enabled + * Otherwise, resolve relative to the current working directory + * + * @param {any} context + * @param {ContentPath[]} contentPaths + * @returns {ContentPath[]} + */ +function resolveRelativePaths(context, contentPaths) { + let resolveFrom = [] + + // Resolve base paths relative to the config file (when possible) if the experimental flag is enabled + if (context.userConfigPath && context.tailwindConfig.content.relative) { + resolveFrom = [path.dirname(context.userConfigPath)] + } + + return contentPaths.map((contentPath) => { + contentPath.base = path.resolve(...resolveFrom, contentPath.base) + + return contentPath + }) +} + +/** + * Resolve the symlink for the base directory / file in each path + * These are added as additional dependencies to watch for changes because + * some tools (like webpack) will only watch the actual file or directory + * but not the symlink itself even in projects that use monorepos. + * + * @param {ContentPath} contentPath + * @returns {ContentPath[]} + */ +function resolvePathSymlinks(contentPath) { + let paths = [contentPath] + + try { + let resolvedPath = fs.realpathSync(contentPath.base) + if (resolvedPath !== contentPath.base) { + paths.push({ + ...contentPath, + base: resolvedPath, + }) + } + } catch { + // TODO: log this? + } + + return paths +} + +/** + * @param {any} context + * @param {ContentPath[]} candidateFiles + * @param {Map} fileModifiedMap + * @returns {[{ content: string, extension: string }[], Map]} + */ +export function resolvedChangedContent(context, candidateFiles, fileModifiedMap) { + let changedContent = context.tailwindConfig.content.files + .filter((item) => typeof item.raw === 'string') + .map(({ raw, extension = 'html' }) => ({ content: raw, extension })) + + let [changedFiles, mTimesToCommit] = resolveChangedFiles(candidateFiles, fileModifiedMap) + + for (let changedFile of changedFiles) { + let extension = path.extname(changedFile).slice(1) + changedContent.push({ file: changedFile, extension }) + } + + return [changedContent, mTimesToCommit] +} + +/** + * + * @param {ContentPath[]} candidateFiles + * @param {Map} fileModifiedMap + * @returns {[Set, Map]} + */ +function resolveChangedFiles(candidateFiles, fileModifiedMap) { + let paths = candidateFiles.map((contentPath) => contentPath.pattern) + let mTimesToCommit = new Map() + + let changedFiles = new Set() + env.DEBUG && console.time('Finding changed files') + let files = fastGlob.sync(paths, { absolute: true }) + for (let file of files) { + let prevModified = fileModifiedMap.get(file) || -Infinity + let modified = fs.statSync(file).mtimeMs + + if (modified > prevModified) { + changedFiles.add(file) + mTimesToCommit.set(file, modified) + } + } + env.DEBUG && console.timeEnd('Finding changed files') + return [changedFiles, mTimesToCommit] +} diff --git a/node_modules/tailwindcss/src/lib/defaultExtractor.js b/node_modules/tailwindcss/src/lib/defaultExtractor.js new file mode 100644 index 0000000..ae546e9 --- /dev/null +++ b/node_modules/tailwindcss/src/lib/defaultExtractor.js @@ -0,0 +1,216 @@ +import { flagEnabled } from '../featureFlags' +import * as regex from './regex' + +export function defaultExtractor(context) { + let patterns = Array.from(buildRegExps(context)) + + /** + * @param {string} content + */ + return (content) => { + /** @type {(string|string)[]} */ + let results = [] + + for (let pattern of patterns) { + for (let result of content.match(pattern) ?? []) { + results.push(clipAtBalancedParens(result)) + } + } + + return results + } +} + +function* buildRegExps(context) { + let separator = context.tailwindConfig.separator + let prefix = + context.tailwindConfig.prefix !== '' + ? regex.optional(regex.pattern([/-?/, regex.escape(context.tailwindConfig.prefix)])) + : '' + + let utility = regex.any([ + // Arbitrary properties (without square brackets) + /\[[^\s:'"`]+:[^\s\[\]]+\]/, + + // Arbitrary properties with balanced square brackets + // This is a targeted fix to continue to allow theme() + // with square brackets to work in arbitrary properties + // while fixing a problem with the regex matching too much + /\[[^\s:'"`\]]+:[^\s]+?\[[^\s]+\][^\s]+?\]/, + + // Utilities + regex.pattern([ + // Utility Name / Group Name + /-?(?:\w+)/, + + // Normal/Arbitrary values + regex.optional( + regex.any([ + regex.pattern([ + // Arbitrary values + /-(?:\w+-)*\[[^\s:]+\]/, + + // Not immediately followed by an `{[(` + /(?![{([]])/, + + // optionally followed by an opacity modifier + /(?:\/[^\s'"`\\><$]*)?/, + ]), + + regex.pattern([ + // Arbitrary values + /-(?:\w+-)*\[[^\s]+\]/, + + // Not immediately followed by an `{[(` + /(?![{([]])/, + + // optionally followed by an opacity modifier + /(?:\/[^\s'"`\\$]*)?/, + ]), + + // Normal values w/o quotes — may include an opacity modifier + /[-\/][^\s'"`\\$={><]*/, + ]) + ), + ]), + ]) + + let variantPatterns = [ + // Without quotes + regex.any([ + // This is here to provide special support for the `@` variant + regex.pattern([/@\[[^\s"'`]+\](\/[^\s"'`]+)?/, separator]), + + // With variant modifier (e.g.: group-[..]/modifier) + regex.pattern([/([^\s"'`\[\\]+-)?\[[^\s"'`]+\]\/\w+/, separator]), + + regex.pattern([/([^\s"'`\[\\]+-)?\[[^\s"'`]+\]/, separator]), + regex.pattern([/[^\s"'`\[\\]+/, separator]), + ]), + + // With quotes allowed + regex.any([ + // With variant modifier (e.g.: group-[..]/modifier) + regex.pattern([/([^\s"'`\[\\]+-)?\[[^\s`]+\]\/\w+/, separator]), + + regex.pattern([/([^\s"'`\[\\]+-)?\[[^\s`]+\]/, separator]), + regex.pattern([/[^\s`\[\\]+/, separator]), + ]), + ] + + for (const variantPattern of variantPatterns) { + yield regex.pattern([ + // Variants + '((?=((', + variantPattern, + ')+))\\2)?', + + // Important (optional) + /!?/, + + prefix, + + utility, + ]) + } + + // 5. Inner matches + yield /[^<>"'`\s.(){}[\]#=%$]*[^<>"'`\s.(){}[\]#=%:$]/g +} + +// We want to capture any "special" characters +// AND the characters immediately following them (if there is one) +let SPECIALS = /([\[\]'"`])([^\[\]'"`])?/g +let ALLOWED_CLASS_CHARACTERS = /[^"'`\s<>\]]+/ + +/** + * Clips a string ensuring that parentheses, quotes, etc… are balanced + * Used for arbitrary values only + * + * We will go past the end of the balanced parens until we find a non-class character + * + * Depth matching behavior: + * w-[calc(100%-theme('spacing[some_key][1.5]'))]'] + * ┬ ┬ ┬┬ ┬ ┬┬ ┬┬┬┬┬┬┬ + * 1 2 3 4 34 3 210 END + * ╰────┴──────────┴────────┴────────┴┴───┴─┴┴┴ + * + * @param {string} input + */ +function clipAtBalancedParens(input) { + // We are care about this for arbitrary values + if (!input.includes('-[')) { + return input + } + + let depth = 0 + let openStringTypes = [] + + // Find all parens, brackets, quotes, etc + // Stop when we end at a balanced pair + // This is naive and will treat mismatched parens as balanced + // This shouldn't be a problem in practice though + let matches = input.matchAll(SPECIALS) + + // We can't use lookbehind assertions because we have to support Safari + // So, instead, we've emulated it using capture groups and we'll re-work the matches to accommodate + matches = Array.from(matches).flatMap((match) => { + const [, ...groups] = match + + return groups.map((group, idx) => + Object.assign([], match, { + index: match.index + idx, + 0: group, + }) + ) + }) + + for (let match of matches) { + let char = match[0] + let inStringType = openStringTypes[openStringTypes.length - 1] + + if (char === inStringType) { + openStringTypes.pop() + } else if (char === "'" || char === '"' || char === '`') { + openStringTypes.push(char) + } + + if (inStringType) { + continue + } else if (char === '[') { + depth++ + continue + } else if (char === ']') { + depth-- + continue + } + + // We've gone one character past the point where we should stop + // This means that there was an extra closing `]` + // We'll clip to just before it + if (depth < 0) { + return input.substring(0, match.index - 1) + } + + // We've finished balancing the brackets but there still may be characters that can be included + // For example in the class `text-[#336699]/[.35]` + // The depth goes to `0` at the closing `]` but goes up again at the `[` + + // If we're at zero and encounter a non-class character then we clip the class there + if (depth === 0 && !ALLOWED_CLASS_CHARACTERS.test(char)) { + return input.substring(0, match.index) + } + } + + return input +} + +// Regular utilities +// {{modifier}:}*{namespace}{-{suffix}}*{/{opacityModifier}}? + +// Arbitrary values +// {{modifier}:}*{namespace}-[{arbitraryValue}]{/{opacityModifier}}? +// arbitraryValue: no whitespace, balanced quotes unless within quotes, balanced brackets unless within quotes + +// Arbitrary properties +// {{modifier}:}*[{validCssPropertyName}:{arbitraryValue}] diff --git a/node_modules/tailwindcss/src/lib/detectNesting.js b/node_modules/tailwindcss/src/lib/detectNesting.js new file mode 100644 index 0000000..03252e2 --- /dev/null +++ b/node_modules/tailwindcss/src/lib/detectNesting.js @@ -0,0 +1,47 @@ +function isRoot(node) { + return node.type === 'root' +} + +function isAtLayer(node) { + return node.type === 'atrule' && node.name === 'layer' +} + +export default function (_context) { + return (root, result) => { + let found = false + + root.walkAtRules('tailwind', (node) => { + if (found) return false + + if (node.parent && !(isRoot(node.parent) || isAtLayer(node.parent))) { + found = true + node.warn( + result, + [ + 'Nested @tailwind rules were detected, but are not supported.', + "Consider using a prefix to scope Tailwind's classes: https://tailwindcss.com/docs/configuration#prefix", + 'Alternatively, use the important selector strategy: https://tailwindcss.com/docs/configuration#selector-strategy', + ].join('\n') + ) + return false + } + }) + + root.walkRules((rule) => { + if (found) return false + + rule.walkRules((nestedRule) => { + found = true + nestedRule.warn( + result, + [ + 'Nested CSS was detected, but CSS nesting has not been configured correctly.', + 'Please enable a CSS nesting plugin *before* Tailwind in your configuration.', + 'See how here: https://tailwindcss.com/docs/using-with-preprocessors#nesting', + ].join('\n') + ) + return false + }) + }) + } +} diff --git a/node_modules/tailwindcss/src/lib/evaluateTailwindFunctions.js b/node_modules/tailwindcss/src/lib/evaluateTailwindFunctions.js new file mode 100644 index 0000000..ff73f46 --- /dev/null +++ b/node_modules/tailwindcss/src/lib/evaluateTailwindFunctions.js @@ -0,0 +1,272 @@ +import dlv from 'dlv' +import didYouMean from 'didyoumean' +import transformThemeValue from '../util/transformThemeValue' +import parseValue from '../value-parser/index' +import { normalizeScreens } from '../util/normalizeScreens' +import buildMediaQuery from '../util/buildMediaQuery' +import { toPath } from '../util/toPath' +import { withAlphaValue } from '../util/withAlphaVariable' +import { parseColorFormat } from '../util/pluginUtils' +import log from '../util/log' + +function isObject(input) { + return typeof input === 'object' && input !== null +} + +function findClosestExistingPath(theme, path) { + let parts = toPath(path) + do { + parts.pop() + + if (dlv(theme, parts) !== undefined) break + } while (parts.length) + + return parts.length ? parts : undefined +} + +function pathToString(path) { + if (typeof path === 'string') return path + return path.reduce((acc, cur, i) => { + if (cur.includes('.')) return `${acc}[${cur}]` + return i === 0 ? cur : `${acc}.${cur}` + }, '') +} + +function list(items) { + return items.map((key) => `'${key}'`).join(', ') +} + +function listKeys(obj) { + return list(Object.keys(obj)) +} + +function validatePath(config, path, defaultValue, themeOpts = {}) { + const pathString = Array.isArray(path) ? pathToString(path) : path.replace(/^['"]+|['"]+$/g, '') + const pathSegments = Array.isArray(path) ? path : toPath(pathString) + const value = dlv(config.theme, pathSegments, defaultValue) + + if (value === undefined) { + let error = `'${pathString}' does not exist in your theme config.` + const parentSegments = pathSegments.slice(0, -1) + const parentValue = dlv(config.theme, parentSegments) + + if (isObject(parentValue)) { + const validKeys = Object.keys(parentValue).filter( + (key) => validatePath(config, [...parentSegments, key]).isValid + ) + const suggestion = didYouMean(pathSegments[pathSegments.length - 1], validKeys) + if (suggestion) { + error += ` Did you mean '${pathToString([...parentSegments, suggestion])}'?` + } else if (validKeys.length > 0) { + error += ` '${pathToString(parentSegments)}' has the following valid keys: ${list( + validKeys + )}` + } + } else { + const closestPath = findClosestExistingPath(config.theme, pathString) + if (closestPath) { + const closestValue = dlv(config.theme, closestPath) + if (isObject(closestValue)) { + error += ` '${pathToString(closestPath)}' has the following keys: ${listKeys( + closestValue + )}` + } else { + error += ` '${pathToString(closestPath)}' is not an object.` + } + } else { + error += ` Your theme has the following top-level keys: ${listKeys(config.theme)}` + } + } + + return { + isValid: false, + error, + } + } + + if ( + !( + typeof value === 'string' || + typeof value === 'number' || + typeof value === 'function' || + value instanceof String || + value instanceof Number || + Array.isArray(value) + ) + ) { + let error = `'${pathString}' was found but does not resolve to a string.` + + if (isObject(value)) { + let validKeys = Object.keys(value).filter( + (key) => validatePath(config, [...pathSegments, key]).isValid + ) + if (validKeys.length) { + error += ` Did you mean something like '${pathToString([...pathSegments, validKeys[0]])}'?` + } + } + + return { + isValid: false, + error, + } + } + + const [themeSection] = pathSegments + + return { + isValid: true, + value: transformThemeValue(themeSection)(value, themeOpts), + } +} + +function extractArgs(node, vNodes, functions) { + vNodes = vNodes.map((vNode) => resolveVNode(node, vNode, functions)) + + let args = [''] + + for (let vNode of vNodes) { + if (vNode.type === 'div' && vNode.value === ',') { + args.push('') + } else { + args[args.length - 1] += parseValue.stringify(vNode) + } + } + + return args +} + +function resolveVNode(node, vNode, functions) { + if (vNode.type === 'function' && functions[vNode.value] !== undefined) { + let args = extractArgs(node, vNode.nodes, functions) + vNode.type = 'word' + vNode.value = functions[vNode.value](node, ...args) + } + + return vNode +} + +function resolveFunctions(node, input, functions) { + let hasAnyFn = Object.keys(functions).some((fn) => input.includes(`${fn}(`)) + if (!hasAnyFn) return input + + return parseValue(input) + .walk((vNode) => { + resolveVNode(node, vNode, functions) + }) + .toString() +} + +let nodeTypePropertyMap = { + atrule: 'params', + decl: 'value', +} + +/** + * @param {string} path + * @returns {Iterable<[path: string, alpha: string|undefined]>} + */ +function* toPaths(path) { + // Strip quotes from beginning and end of string + // This allows the alpha value to be present inside of quotes + path = path.replace(/^['"]+|['"]+$/g, '') + + let matches = path.match(/^([^\s]+)(?![^\[]*\])(?:\s*\/\s*([^\/\s]+))$/) + let alpha = undefined + + yield [path, undefined] + + if (matches) { + path = matches[1] + alpha = matches[2] + + yield [path, alpha] + } +} + +/** + * + * @param {any} config + * @param {string} path + * @param {any} defaultValue + */ +function resolvePath(config, path, defaultValue) { + const results = Array.from(toPaths(path)).map(([path, alpha]) => { + return Object.assign(validatePath(config, path, defaultValue, { opacityValue: alpha }), { + resolvedPath: path, + alpha, + }) + }) + + return results.find((result) => result.isValid) ?? results[0] +} + +export default function (context) { + let config = context.tailwindConfig + + let functions = { + theme: (node, path, ...defaultValue) => { + let { isValid, value, error, alpha } = resolvePath( + config, + path, + defaultValue.length ? defaultValue : undefined + ) + + if (!isValid) { + let parentNode = node.parent + let candidate = parentNode?.raws.tailwind?.candidate + + if (parentNode && candidate !== undefined) { + // Remove this utility from any caches + context.markInvalidUtilityNode(parentNode) + + // Remove the CSS node from the markup + parentNode.remove() + + // Show a warning + log.warn('invalid-theme-key-in-class', [ + `The utility \`${candidate}\` contains an invalid theme value and was not generated.`, + ]) + + return + } + + throw node.error(error) + } + + let maybeColor = parseColorFormat(value) + let isColorFunction = maybeColor !== undefined && typeof maybeColor === 'function' + + if (alpha !== undefined || isColorFunction) { + if (alpha === undefined) { + alpha = 1.0 + } + + value = withAlphaValue(maybeColor, alpha, maybeColor) + } + + return value + }, + screen: (node, screen) => { + screen = screen.replace(/^['"]+/g, '').replace(/['"]+$/g, '') + let screens = normalizeScreens(config.theme.screens) + let screenDefinition = screens.find(({ name }) => name === screen) + + if (!screenDefinition) { + throw node.error(`The '${screen}' screen does not exist in your theme.`) + } + + return buildMediaQuery(screenDefinition) + }, + } + return (root) => { + root.walk((node) => { + let property = nodeTypePropertyMap[node.type] + + if (property === undefined) { + return + } + + node[property] = resolveFunctions(node, node[property], functions) + }) + } +} diff --git a/node_modules/tailwindcss/src/lib/expandApplyAtRules.js b/node_modules/tailwindcss/src/lib/expandApplyAtRules.js new file mode 100644 index 0000000..ed48dbc --- /dev/null +++ b/node_modules/tailwindcss/src/lib/expandApplyAtRules.js @@ -0,0 +1,620 @@ +import postcss from 'postcss' +import parser from 'postcss-selector-parser' + +import { resolveMatches } from './generateRules' +import escapeClassName from '../util/escapeClassName' +import { applyImportantSelector } from '../util/applyImportantSelector' +import { movePseudos } from '../util/pseudoElements' + +/** @typedef {Map} ApplyCache */ + +function extractClasses(node) { + /** @type {Map>} */ + let groups = new Map() + + let container = postcss.root({ nodes: [node.clone()] }) + + container.walkRules((rule) => { + parser((selectors) => { + selectors.walkClasses((classSelector) => { + let parentSelector = classSelector.parent.toString() + + let classes = groups.get(parentSelector) + if (!classes) { + groups.set(parentSelector, (classes = new Set())) + } + + classes.add(classSelector.value) + }) + }).processSync(rule.selector) + }) + + let normalizedGroups = Array.from(groups.values(), (classes) => Array.from(classes)) + let classes = normalizedGroups.flat() + + return Object.assign(classes, { groups: normalizedGroups }) +} + +let selectorExtractor = parser() + +/** + * @param {string} ruleSelectors + */ +function extractSelectors(ruleSelectors) { + return selectorExtractor.astSync(ruleSelectors) +} + +function extractBaseCandidates(candidates, separator) { + let baseClasses = new Set() + + for (let candidate of candidates) { + baseClasses.add(candidate.split(separator).pop()) + } + + return Array.from(baseClasses) +} + +function prefix(context, selector) { + let prefix = context.tailwindConfig.prefix + return typeof prefix === 'function' ? prefix(selector) : prefix + selector +} + +function* pathToRoot(node) { + yield node + while (node.parent) { + yield node.parent + node = node.parent + } +} + +/** + * Only clone the node itself and not its children + * + * @param {*} node + * @param {*} overrides + * @returns + */ +function shallowClone(node, overrides = {}) { + let children = node.nodes + node.nodes = [] + + let tmp = node.clone(overrides) + + node.nodes = children + + return tmp +} + +/** + * Clone just the nodes all the way to the top that are required to represent + * this singular rule in the tree. + * + * For example, if we have CSS like this: + * ```css + * @media (min-width: 768px) { + * @supports (display: grid) { + * .foo { + * display: grid; + * grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + * } + * } + * + * @supports (backdrop-filter: blur(1px)) { + * .bar { + * backdrop-filter: blur(1px); + * } + * } + * + * .baz { + * color: orange; + * } + * } + * ``` + * + * And we're cloning `.bar` it'll return a cloned version of what's required for just that single node: + * + * ```css + * @media (min-width: 768px) { + * @supports (backdrop-filter: blur(1px)) { + * .bar { + * backdrop-filter: blur(1px); + * } + * } + * } + * ``` + * + * @param {import('postcss').Node} node + */ +function nestedClone(node) { + for (let parent of pathToRoot(node)) { + if (node === parent) { + continue + } + + if (parent.type === 'root') { + break + } + + node = shallowClone(parent, { + nodes: [node], + }) + } + + return node +} + +/** + * @param {import('postcss').Root} root + */ +function buildLocalApplyCache(root, context) { + /** @type {ApplyCache} */ + let cache = new Map() + + root.walkRules((rule) => { + // Ignore rules generated by Tailwind + for (let node of pathToRoot(rule)) { + if (node.raws.tailwind?.layer !== undefined) { + return + } + } + + // Clone what's required to represent this singular rule in the tree + let container = nestedClone(rule) + let sort = context.offsets.create('user') + + for (let className of extractClasses(rule)) { + let list = cache.get(className) || [] + cache.set(className, list) + + list.push([ + { + layer: 'user', + sort, + important: false, + }, + container, + ]) + } + }) + + return cache +} + +/** + * @returns {ApplyCache} + */ +function buildApplyCache(applyCandidates, context) { + for (let candidate of applyCandidates) { + if (context.notClassCache.has(candidate) || context.applyClassCache.has(candidate)) { + continue + } + + if (context.classCache.has(candidate)) { + context.applyClassCache.set( + candidate, + context.classCache.get(candidate).map(([meta, rule]) => [meta, rule.clone()]) + ) + continue + } + + let matches = Array.from(resolveMatches(candidate, context)) + + if (matches.length === 0) { + context.notClassCache.add(candidate) + continue + } + + context.applyClassCache.set(candidate, matches) + } + + return context.applyClassCache +} + +/** + * Build a cache only when it's first used + * + * @param {() => ApplyCache} buildCacheFn + * @returns {ApplyCache} + */ +function lazyCache(buildCacheFn) { + let cache = null + + return { + get: (name) => { + cache = cache || buildCacheFn() + + return cache.get(name) + }, + has: (name) => { + cache = cache || buildCacheFn() + + return cache.has(name) + }, + } +} + +/** + * Take a series of multiple caches and merge + * them so they act like one large cache + * + * @param {ApplyCache[]} caches + * @returns {ApplyCache} + */ +function combineCaches(caches) { + return { + get: (name) => caches.flatMap((cache) => cache.get(name) || []), + has: (name) => caches.some((cache) => cache.has(name)), + } +} + +function extractApplyCandidates(params) { + let candidates = params.split(/[\s\t\n]+/g) + + if (candidates[candidates.length - 1] === '!important') { + return [candidates.slice(0, -1), true] + } + + return [candidates, false] +} + +function processApply(root, context, localCache) { + let applyCandidates = new Set() + + // Collect all @apply rules and candidates + let applies = [] + root.walkAtRules('apply', (rule) => { + let [candidates] = extractApplyCandidates(rule.params) + + for (let util of candidates) { + applyCandidates.add(util) + } + + applies.push(rule) + }) + + // Start the @apply process if we have rules with @apply in them + if (applies.length === 0) { + return + } + + // Fill up some caches! + let applyClassCache = combineCaches([localCache, buildApplyCache(applyCandidates, context)]) + + /** + * When we have an apply like this: + * + * .abc { + * @apply hover:font-bold; + * } + * + * What we essentially will do is resolve to this: + * + * .abc { + * @apply .hover\:font-bold:hover { + * font-weight: 500; + * } + * } + * + * Notice that the to-be-applied class is `.hover\:font-bold:hover` and that the utility candidate was `hover:font-bold`. + * What happens in this function is that we prepend a `.` and escape the candidate. + * This will result in `.hover\:font-bold` + * Which means that we can replace `.hover\:font-bold` with `.abc` in `.hover\:font-bold:hover` resulting in `.abc:hover` + * + * @param {string} selector + * @param {string} utilitySelectors + * @param {string} candidate + */ + function replaceSelector(selector, utilitySelectors, candidate) { + let selectorList = extractSelectors(selector) + let utilitySelectorsList = extractSelectors(utilitySelectors) + let candidateList = extractSelectors(`.${escapeClassName(candidate)}`) + let candidateClass = candidateList.nodes[0].nodes[0] + + selectorList.each((sel) => { + /** @type {Set} */ + let replaced = new Set() + + utilitySelectorsList.each((utilitySelector) => { + let hasReplaced = false + utilitySelector = utilitySelector.clone() + + utilitySelector.walkClasses((node) => { + if (node.value !== candidateClass.value) { + return + } + + // Don't replace multiple instances of the same class + // This is theoretically correct but only partially + // We'd need to generate every possible permutation of the replacement + // For example with `.foo + .foo { … }` and `section { @apply foo; }` + // We'd need to generate all of these: + // - `.foo + .foo` + // - `.foo + section` + // - `section + .foo` + // - `section + section` + if (hasReplaced) { + return + } + + // Since you can only `@apply` class names this is sufficient + // We want to replace the matched class name with the selector the user is using + // Ex: Replace `.text-blue-500` with `.foo.bar:is(.something-cool)` + node.replaceWith(...sel.nodes.map((node) => node.clone())) + + // Record that we did something and we want to use this new selector + replaced.add(utilitySelector) + + hasReplaced = true + }) + }) + + // Sort tag names before class names (but only sort each group (separated by a combinator) + // separately and not in total) + // This happens when replacing `.bar` in `.foo.bar` with a tag like `section` + for (let sel of replaced) { + let groups = [[]] + for (let node of sel.nodes) { + if (node.type === 'combinator') { + groups.push(node) + groups.push([]) + } else { + let last = groups[groups.length - 1] + last.push(node) + } + } + + sel.nodes = [] + + for (let group of groups) { + if (Array.isArray(group)) { + group.sort((a, b) => { + if (a.type === 'tag' && b.type === 'class') { + return -1 + } else if (a.type === 'class' && b.type === 'tag') { + return 1 + } else if (a.type === 'class' && b.type === 'pseudo' && b.value.startsWith('::')) { + return -1 + } else if (a.type === 'pseudo' && a.value.startsWith('::') && b.type === 'class') { + return 1 + } + + return 0 + }) + } + + sel.nodes = sel.nodes.concat(group) + } + } + + sel.replaceWith(...replaced) + }) + + return selectorList.toString() + } + + let perParentApplies = new Map() + + // Collect all apply candidates and their rules + for (let apply of applies) { + let [candidates] = perParentApplies.get(apply.parent) || [[], apply.source] + + perParentApplies.set(apply.parent, [candidates, apply.source]) + + let [applyCandidates, important] = extractApplyCandidates(apply.params) + + if (apply.parent.type === 'atrule') { + if (apply.parent.name === 'screen') { + let screenType = apply.parent.params + + throw apply.error( + `@apply is not supported within nested at-rules like @screen. We suggest you write this as @apply ${applyCandidates + .map((c) => `${screenType}:${c}`) + .join(' ')} instead.` + ) + } + + throw apply.error( + `@apply is not supported within nested at-rules like @${apply.parent.name}. You can fix this by un-nesting @${apply.parent.name}.` + ) + } + + for (let applyCandidate of applyCandidates) { + if ([prefix(context, 'group'), prefix(context, 'peer')].includes(applyCandidate)) { + // TODO: Link to specific documentation page with error code. + throw apply.error(`@apply should not be used with the '${applyCandidate}' utility`) + } + + if (!applyClassCache.has(applyCandidate)) { + throw apply.error( + `The \`${applyCandidate}\` class does not exist. If \`${applyCandidate}\` is a custom class, make sure it is defined within a \`@layer\` directive.` + ) + } + + let rules = applyClassCache.get(applyCandidate) + + candidates.push([applyCandidate, important, rules]) + } + } + + for (let [parent, [candidates, atApplySource]] of perParentApplies) { + let siblings = [] + + for (let [applyCandidate, important, rules] of candidates) { + let potentialApplyCandidates = [ + applyCandidate, + ...extractBaseCandidates([applyCandidate], context.tailwindConfig.separator), + ] + + for (let [meta, node] of rules) { + let parentClasses = extractClasses(parent) + let nodeClasses = extractClasses(node) + + // When we encounter a rule like `.dark .a, .b { … }` we only want to be left with `[.dark, .a]` if the base applyCandidate is `.a` or with `[.b]` if the base applyCandidate is `.b` + // So we've split them into groups + nodeClasses = nodeClasses.groups + .filter((classList) => + classList.some((className) => potentialApplyCandidates.includes(className)) + ) + .flat() + + // Add base utility classes from the @apply node to the list of + // classes to check whether it intersects and therefore results in a + // circular dependency or not. + // + // E.g.: + // .foo { + // @apply hover:a; // This applies "a" but with a modifier + // } + // + // We only have to do that with base classes of the `node`, not of the `parent` + // E.g.: + // .hover\:foo { + // @apply bar; + // } + // .bar { + // @apply foo; + // } + // + // This should not result in a circular dependency because we are + // just applying `.foo` and the rule above is `.hover\:foo` which is + // unrelated. However, if we were to apply `hover:foo` then we _did_ + // have to include this one. + nodeClasses = nodeClasses.concat( + extractBaseCandidates(nodeClasses, context.tailwindConfig.separator) + ) + + let intersects = parentClasses.some((selector) => nodeClasses.includes(selector)) + if (intersects) { + throw node.error( + `You cannot \`@apply\` the \`${applyCandidate}\` utility here because it creates a circular dependency.` + ) + } + + let root = postcss.root({ nodes: [node.clone()] }) + + // Make sure every node in the entire tree points back at the @apply rule that generated it + root.walk((node) => { + node.source = atApplySource + }) + + let canRewriteSelector = + node.type !== 'atrule' || (node.type === 'atrule' && node.name !== 'keyframes') + + if (canRewriteSelector) { + root.walkRules((rule) => { + // Let's imagine you have the following structure: + // + // .foo { + // @apply bar; + // } + // + // @supports (a: b) { + // .bar { + // color: blue + // } + // + // .something-unrelated {} + // } + // + // In this case we want to apply `.bar` but it happens to be in + // an atrule node. We clone that node instead of the nested one + // because we still want that @supports rule to be there once we + // applied everything. + // + // However it happens to be that the `.something-unrelated` is + // also in that same shared @supports atrule. This is not good, + // and this should not be there. The good part is that this is + // a clone already and it can be safely removed. The question is + // how do we know we can remove it. Basically what we can do is + // match it against the applyCandidate that you want to apply. If + // it doesn't match the we can safely delete it. + // + // If we didn't do this, then the `replaceSelector` function + // would have replaced this with something that didn't exist and + // therefore it removed the selector altogether. In this specific + // case it would result in `{}` instead of `.something-unrelated {}` + if (!extractClasses(rule).some((candidate) => candidate === applyCandidate)) { + rule.remove() + return + } + + // Strip the important selector from the parent selector if at the beginning + let importantSelector = + typeof context.tailwindConfig.important === 'string' + ? context.tailwindConfig.important + : null + + // We only want to move the "important" selector if this is a Tailwind-generated utility + // We do *not* want to do this for user CSS that happens to be structured the same + let isGenerated = parent.raws.tailwind !== undefined + + let parentSelector = + isGenerated && importantSelector && parent.selector.indexOf(importantSelector) === 0 + ? parent.selector.slice(importantSelector.length) + : parent.selector + + // If the selector becomes empty after replacing the important selector + // This means that it's the same as the parent selector and we don't want to replace it + // Otherwise we'll crash + if (parentSelector === '') { + parentSelector = parent.selector + } + + rule.selector = replaceSelector(parentSelector, rule.selector, applyCandidate) + + // And then re-add it if it was removed + if (importantSelector && parentSelector !== parent.selector) { + rule.selector = applyImportantSelector(rule.selector, importantSelector) + } + + rule.walkDecls((d) => { + d.important = meta.important || important + }) + + // Move pseudo elements to the end of the selector (if necessary) + let selector = parser().astSync(rule.selector) + selector.each((sel) => movePseudos(sel)) + rule.selector = selector.toString() + }) + } + + // It could be that the node we were inserted was removed because the class didn't match + // If that was the *only* rule in the parent, then we have nothing add so we skip it + if (!root.nodes[0]) { + continue + } + + // Insert it + siblings.push([meta.sort, root.nodes[0]]) + } + } + + // Inject the rules, sorted, correctly + let nodes = context.offsets.sort(siblings).map((s) => s[1]) + + // `parent` refers to the node at `.abc` in: .abc { @apply mt-2 } + parent.after(nodes) + } + + for (let apply of applies) { + // If there are left-over declarations, just remove the @apply + if (apply.parent.nodes.length > 1) { + apply.remove() + } else { + // The node is empty, drop the full node + apply.parent.remove() + } + } + + // Do it again, in case we have other `@apply` rules + processApply(root, context, localCache) +} + +export default function expandApplyAtRules(context) { + return (root) => { + // Build a cache of the user's CSS so we can use it to resolve classes used by @apply + let localCache = lazyCache(() => buildLocalApplyCache(root, context)) + + processApply(root, context, localCache) + } +} diff --git a/node_modules/tailwindcss/src/lib/expandTailwindAtRules.js b/node_modules/tailwindcss/src/lib/expandTailwindAtRules.js new file mode 100644 index 0000000..2933d6f --- /dev/null +++ b/node_modules/tailwindcss/src/lib/expandTailwindAtRules.js @@ -0,0 +1,297 @@ +import fs from 'fs' +import LRU from '@alloc/quick-lru' +import * as sharedState from './sharedState' +import { generateRules } from './generateRules' +import log from '../util/log' +import cloneNodes from '../util/cloneNodes' +import { defaultExtractor } from './defaultExtractor' + +let env = sharedState.env + +const builtInExtractors = { + DEFAULT: defaultExtractor, +} + +const builtInTransformers = { + DEFAULT: (content) => content, + svelte: (content) => content.replace(/(?:^|\s)class:/g, ' '), +} + +function getExtractor(context, fileExtension) { + let extractors = context.tailwindConfig.content.extract + + return ( + extractors[fileExtension] || + extractors.DEFAULT || + builtInExtractors[fileExtension] || + builtInExtractors.DEFAULT(context) + ) +} + +function getTransformer(tailwindConfig, fileExtension) { + let transformers = tailwindConfig.content.transform + + return ( + transformers[fileExtension] || + transformers.DEFAULT || + builtInTransformers[fileExtension] || + builtInTransformers.DEFAULT + ) +} + +let extractorCache = new WeakMap() + +// Scans template contents for possible classes. This is a hot path on initial build but +// not too important for subsequent builds. The faster the better though — if we can speed +// up these regexes by 50% that could cut initial build time by like 20%. +function getClassCandidates(content, extractor, candidates, seen) { + if (!extractorCache.has(extractor)) { + extractorCache.set(extractor, new LRU({ maxSize: 25000 })) + } + + for (let line of content.split('\n')) { + line = line.trim() + + if (seen.has(line)) { + continue + } + seen.add(line) + + if (extractorCache.get(extractor).has(line)) { + for (let match of extractorCache.get(extractor).get(line)) { + candidates.add(match) + } + } else { + let extractorMatches = extractor(line).filter((s) => s !== '!*') + let lineMatchesSet = new Set(extractorMatches) + + for (let match of lineMatchesSet) { + candidates.add(match) + } + + extractorCache.get(extractor).set(line, lineMatchesSet) + } + } +} + +/** + * + * @param {[import('./offsets.js').RuleOffset, import('postcss').Node][]} rules + * @param {*} context + */ +function buildStylesheet(rules, context) { + let sortedRules = context.offsets.sort(rules) + + let returnValue = { + base: new Set(), + defaults: new Set(), + components: new Set(), + utilities: new Set(), + variants: new Set(), + } + + for (let [sort, rule] of sortedRules) { + returnValue[sort.layer].add(rule) + } + + return returnValue +} + +export default function expandTailwindAtRules(context) { + return async (root) => { + let layerNodes = { + base: null, + components: null, + utilities: null, + variants: null, + } + + root.walkAtRules((rule) => { + // Make sure this file contains Tailwind directives. If not, we can save + // a lot of work and bail early. Also we don't have to register our touch + // file as a dependency since the output of this CSS does not depend on + // the source of any templates. Think Vue