diff options
Diffstat (limited to 'node_modules/tailwindcss/src/lib')
23 files changed, 5376 insertions, 0 deletions
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<string, number>} fileModifiedMap + * @returns {[{ content: string, extension: string }[], Map<string, number>]} + */ +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<string, number>} fileModifiedMap + * @returns {[Set<string>, Map<string, number>]} + */ +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<string, [any, import('postcss').Rule[]]>} ApplyCache */ + +function extractClasses(node) { + /** @type {Map<string, Set<string>>} */ + 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<import('postcss-selector-parser').Selector>} */ + 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 <style> blocks for example. + if (rule.name === 'tailwind') { + if (Object.keys(layerNodes).includes(rule.params)) { + layerNodes[rule.params] = rule + } + } + }) + + if (Object.values(layerNodes).every((n) => n === null)) { + return root + } + + // --- + + // Find potential rules in changed files + let candidates = new Set([...(context.candidates ?? []), sharedState.NOT_ON_DEMAND]) + let seen = new Set() + + env.DEBUG && console.time('Reading changed files') + + if (__OXIDE__) { + // TODO: Pass through or implement `extractor` + for (let candidate of require('@tailwindcss/oxide').parseCandidateStringsFromFiles( + context.changedContent + // Object.assign({}, builtInTransformers, context.tailwindConfig.content.transform) + )) { + candidates.add(candidate) + } + + // for (let { file, content, extension } of context.changedContent) { + // let transformer = getTransformer(context.tailwindConfig, extension) + // let extractor = getExtractor(context, extension) + // getClassCandidatesOxide(file, transformer(content), extractor, candidates, seen) + // } + } else { + /** @type {[item: {file?: string, content?: string}, meta: {transformer: any, extractor: any}][]} */ + let regexParserContent = [] + + for (let item of context.changedContent) { + let transformer = getTransformer(context.tailwindConfig, item.extension) + let extractor = getExtractor(context, item.extension) + regexParserContent.push([item, { transformer, extractor }]) + } + + const BATCH_SIZE = 500 + + for (let i = 0; i < regexParserContent.length; i += BATCH_SIZE) { + let batch = regexParserContent.slice(i, i + BATCH_SIZE) + await Promise.all( + batch.map(async ([{ file, content }, { transformer, extractor }]) => { + content = file ? await fs.promises.readFile(file, 'utf8') : content + getClassCandidates(transformer(content), extractor, candidates, seen) + }) + ) + } + } + + env.DEBUG && console.timeEnd('Reading changed files') + + // --- + + // Generate the actual CSS + let classCacheCount = context.classCache.size + + env.DEBUG && console.time('Generate rules') + env.DEBUG && console.time('Sorting candidates') + let sortedCandidates = __OXIDE__ + ? candidates + : new Set( + [...candidates].sort((a, z) => { + if (a === z) return 0 + if (a < z) return -1 + return 1 + }) + ) + env.DEBUG && console.timeEnd('Sorting candidates') + generateRules(sortedCandidates, context) + env.DEBUG && console.timeEnd('Generate rules') + + // We only ever add to the classCache, so if it didn't grow, there is nothing new. + env.DEBUG && console.time('Build stylesheet') + if (context.stylesheetCache === null || context.classCache.size !== classCacheCount) { + context.stylesheetCache = buildStylesheet([...context.ruleCache], context) + } + env.DEBUG && console.timeEnd('Build stylesheet') + + let { + defaults: defaultNodes, + base: baseNodes, + components: componentNodes, + utilities: utilityNodes, + variants: screenNodes, + } = context.stylesheetCache + + // --- + + // Replace any Tailwind directives with generated CSS + + if (layerNodes.base) { + layerNodes.base.before( + cloneNodes([...baseNodes, ...defaultNodes], layerNodes.base.source, { + layer: 'base', + }) + ) + layerNodes.base.remove() + } + + if (layerNodes.components) { + layerNodes.components.before( + cloneNodes([...componentNodes], layerNodes.components.source, { + layer: 'components', + }) + ) + layerNodes.components.remove() + } + + if (layerNodes.utilities) { + layerNodes.utilities.before( + cloneNodes([...utilityNodes], layerNodes.utilities.source, { + layer: 'utilities', + }) + ) + layerNodes.utilities.remove() + } + + // We do post-filtering to not alter the emitted order of the variants + const variantNodes = Array.from(screenNodes).filter((node) => { + const parentLayer = node.raws.tailwind?.parentLayer + + if (parentLayer === 'components') { + return layerNodes.components !== null + } + + if (parentLayer === 'utilities') { + return layerNodes.utilities !== null + } + + return true + }) + + if (layerNodes.variants) { + layerNodes.variants.before( + cloneNodes(variantNodes, layerNodes.variants.source, { + layer: 'variants', + }) + ) + layerNodes.variants.remove() + } else if (variantNodes.length > 0) { + root.append( + cloneNodes(variantNodes, root.source, { + layer: 'variants', + }) + ) + } + + // If we've got a utility layer and no utilities are generated there's likely something wrong + const hasUtilityVariants = variantNodes.some( + (node) => node.raws.tailwind?.parentLayer === 'utilities' + ) + + if (layerNodes.utilities && utilityNodes.size === 0 && !hasUtilityVariants) { + log.warn('content-problems', [ + 'No utility classes were detected in your source files. If this is unexpected, double-check the `content` option in your Tailwind CSS configuration.', + 'https://tailwindcss.com/docs/content-configuration', + ]) + } + + // --- + + if (env.DEBUG) { + console.log('Potential classes: ', candidates.size) + console.log('Active contexts: ', sharedState.contextSourcesMap.size) + } + + // Clear the cache for the changed files + context.changedContent = [] + + // Cleanup any leftover @layer atrules + root.walkAtRules('layer', (rule) => { + if (Object.keys(layerNodes).includes(rule.params)) { + rule.remove() + } + }) + } +} diff --git a/node_modules/tailwindcss/src/lib/findAtConfigPath.js b/node_modules/tailwindcss/src/lib/findAtConfigPath.js new file mode 100644 index 0000000..ac0adab --- /dev/null +++ b/node_modules/tailwindcss/src/lib/findAtConfigPath.js @@ -0,0 +1,48 @@ +import fs from 'fs' +import path from 'path' + +/** + * Find the @config at-rule in the given CSS AST and return the relative path to the config file + * + * @param {import('postcss').Root} root + * @param {import('postcss').Result} result + */ +export function findAtConfigPath(root, result) { + let configPath = null + let relativeTo = null + + root.walkAtRules('config', (rule) => { + relativeTo = rule.source?.input.file ?? result.opts.from ?? null + + if (relativeTo === null) { + throw rule.error( + 'The `@config` directive cannot be used without setting `from` in your PostCSS config.' + ) + } + + if (configPath) { + throw rule.error('Only one `@config` directive is allowed per file.') + } + + let matches = rule.params.match(/(['"])(.*?)\1/) + if (!matches) { + throw rule.error('A path is required when using the `@config` directive.') + } + + let inputPath = matches[2] + if (path.isAbsolute(inputPath)) { + throw rule.error('The `@config` directive cannot be used with an absolute path.') + } + + configPath = path.resolve(path.dirname(relativeTo), inputPath) + if (!fs.existsSync(configPath)) { + throw rule.error( + `The config file at "${inputPath}" does not exist. Make sure the path is correct and the file exists.` + ) + } + + rule.remove() + }) + + return configPath ? configPath : null +} diff --git a/node_modules/tailwindcss/src/lib/generateRules.js b/node_modules/tailwindcss/src/lib/generateRules.js new file mode 100644 index 0000000..69b6827 --- /dev/null +++ b/node_modules/tailwindcss/src/lib/generateRules.js @@ -0,0 +1,936 @@ +import postcss from 'postcss' +import selectorParser from 'postcss-selector-parser' +import parseObjectStyles from '../util/parseObjectStyles' +import isPlainObject from '../util/isPlainObject' +import prefixSelector from '../util/prefixSelector' +import { updateAllClasses, getMatchingTypes } from '../util/pluginUtils' +import log from '../util/log' +import * as sharedState from './sharedState' +import { + formatVariantSelector, + finalizeSelector, + eliminateIrrelevantSelectors, +} from '../util/formatVariantSelector' +import { asClass } from '../util/nameClass' +import { normalize } from '../util/dataTypes' +import { isValidVariantFormatString, parseVariant, INTERNAL_FEATURES } from './setupContextUtils' +import isValidArbitraryValue from '../util/isSyntacticallyValidPropertyValue' +import { splitAtTopLevelOnly } from '../util/splitAtTopLevelOnly.js' +import { flagEnabled } from '../featureFlags' +import { applyImportantSelector } from '../util/applyImportantSelector' + +let classNameParser = selectorParser((selectors) => { + return selectors.first.filter(({ type }) => type === 'class').pop().value +}) + +export function getClassNameFromSelector(selector) { + return classNameParser.transformSync(selector) +} + +// Generate match permutations for a class candidate, like: +// ['ring-offset-blue', '100'] +// ['ring-offset', 'blue-100'] +// ['ring', 'offset-blue-100'] +// Example with dynamic classes: +// ['grid-cols', '[[linename],1fr,auto]'] +// ['grid', 'cols-[[linename],1fr,auto]'] +function* candidatePermutations(candidate) { + let lastIndex = Infinity + + while (lastIndex >= 0) { + let dashIdx + let wasSlash = false + + if (lastIndex === Infinity && candidate.endsWith(']')) { + let bracketIdx = candidate.indexOf('[') + + // If character before `[` isn't a dash or a slash, this isn't a dynamic class + // eg. string[] + if (candidate[bracketIdx - 1] === '-') { + dashIdx = bracketIdx - 1 + } else if (candidate[bracketIdx - 1] === '/') { + dashIdx = bracketIdx - 1 + wasSlash = true + } else { + dashIdx = -1 + } + } else if (lastIndex === Infinity && candidate.includes('/')) { + dashIdx = candidate.lastIndexOf('/') + wasSlash = true + } else { + dashIdx = candidate.lastIndexOf('-', lastIndex) + } + + if (dashIdx < 0) { + break + } + + let prefix = candidate.slice(0, dashIdx) + let modifier = candidate.slice(wasSlash ? dashIdx : dashIdx + 1) + + lastIndex = dashIdx - 1 + + // TODO: This feels a bit hacky + if (prefix === '' || modifier === '/') { + continue + } + + yield [prefix, modifier] + } +} + +function applyPrefix(matches, context) { + if (matches.length === 0 || context.tailwindConfig.prefix === '') { + return matches + } + + for (let match of matches) { + let [meta] = match + if (meta.options.respectPrefix) { + let container = postcss.root({ nodes: [match[1].clone()] }) + let classCandidate = match[1].raws.tailwind.classCandidate + + container.walkRules((r) => { + // If this is a negative utility with a dash *before* the prefix we + // have to ensure that the generated selector matches the candidate + + // Not doing this will cause `-tw-top-1` to generate the class `.tw--top-1` + // The disconnect between candidate <-> class can cause @apply to hard crash. + let shouldPrependNegative = classCandidate.startsWith('-') + + r.selector = prefixSelector( + context.tailwindConfig.prefix, + r.selector, + shouldPrependNegative + ) + }) + + match[1] = container.nodes[0] + } + } + + return matches +} + +function applyImportant(matches, classCandidate) { + if (matches.length === 0) { + return matches + } + + let result = [] + + for (let [meta, rule] of matches) { + let container = postcss.root({ nodes: [rule.clone()] }) + + container.walkRules((r) => { + let ast = selectorParser().astSync(r.selector) + + // Remove extraneous selectors that do not include the base candidate + ast.each((sel) => eliminateIrrelevantSelectors(sel, classCandidate)) + + // Update all instances of the base candidate to include the important marker + updateAllClasses(ast, (className) => + className === classCandidate ? `!${className}` : className + ) + + r.selector = ast.toString() + + r.walkDecls((d) => (d.important = true)) + }) + + result.push([{ ...meta, important: true }, container.nodes[0]]) + } + + return result +} + +// Takes a list of rule tuples and applies a variant like `hover`, sm`, +// whatever to it. We used to do some extra caching here to avoid generating +// a variant of the same rule more than once, but this was never hit because +// we cache at the entire selector level further up the tree. +// +// Technically you can get a cache hit if you have `hover:focus:text-center` +// and `focus:hover:text-center` in the same project, but it doesn't feel +// worth the complexity for that case. + +function applyVariant(variant, matches, context) { + if (matches.length === 0) { + return matches + } + + /** @type {{modifier: string | null, value: string | null}} */ + let args = { modifier: null, value: sharedState.NONE } + + // Retrieve "modifier" + { + let [baseVariant, ...modifiers] = splitAtTopLevelOnly(variant, '/') + + // This is a hack to support variants with `/` in them, like `ar-1/10/20:text-red-500` + // In this case 1/10 is a value but /20 is a modifier + if (modifiers.length > 1) { + baseVariant = baseVariant + '/' + modifiers.slice(0, -1).join('/') + modifiers = modifiers.slice(-1) + } + + if (modifiers.length && !context.variantMap.has(variant)) { + variant = baseVariant + args.modifier = modifiers[0] + + if (!flagEnabled(context.tailwindConfig, 'generalizedModifiers')) { + return [] + } + } + } + + // Retrieve "arbitrary value" + if (variant.endsWith(']') && !variant.startsWith('[')) { + // We either have: + // @[200px] + // group-[:hover] + // + // But we don't want: + // @-[200px] (`-` is incorrect) + // group[:hover] (`-` is missing) + let match = /(.)(-?)\[(.*)\]/g.exec(variant) + if (match) { + let [, char, separator, value] = match + // @-[200px] case + if (char === '@' && separator === '-') return [] + // group[:hover] case + if (char !== '@' && separator === '') return [] + + variant = variant.replace(`${separator}[${value}]`, '') + args.value = value + } + } + + // Register arbitrary variants + if (isArbitraryValue(variant) && !context.variantMap.has(variant)) { + let sort = context.offsets.recordVariant(variant) + + let selector = normalize(variant.slice(1, -1)) + let selectors = splitAtTopLevelOnly(selector, ',') + + // We do not support multiple selectors for arbitrary variants + if (selectors.length > 1) { + return [] + } + + if (!selectors.every(isValidVariantFormatString)) { + return [] + } + + let records = selectors.map((sel, idx) => [ + context.offsets.applyParallelOffset(sort, idx), + parseVariant(sel.trim()), + ]) + + context.variantMap.set(variant, records) + } + + if (context.variantMap.has(variant)) { + let isArbitraryVariant = isArbitraryValue(variant) + let internalFeatures = context.variantOptions.get(variant)?.[INTERNAL_FEATURES] ?? {} + let variantFunctionTuples = context.variantMap.get(variant).slice() + let result = [] + + let respectPrefix = (() => { + if (isArbitraryVariant) return false + if (internalFeatures.respectPrefix === false) return false + return true + })() + + for (let [meta, rule] of matches) { + // Don't generate variants for user css + if (meta.layer === 'user') { + continue + } + + let container = postcss.root({ nodes: [rule.clone()] }) + + for (let [variantSort, variantFunction, containerFromArray] of variantFunctionTuples) { + let clone = (containerFromArray ?? container).clone() + let collectedFormats = [] + + function prepareBackup() { + // Already prepared, chicken out + if (clone.raws.neededBackup) { + return + } + clone.raws.neededBackup = true + clone.walkRules((rule) => (rule.raws.originalSelector = rule.selector)) + } + + function modifySelectors(modifierFunction) { + prepareBackup() + clone.each((rule) => { + if (rule.type !== 'rule') { + return + } + + rule.selectors = rule.selectors.map((selector) => { + return modifierFunction({ + get className() { + return getClassNameFromSelector(selector) + }, + selector, + }) + }) + }) + + return clone + } + + let ruleWithVariant = variantFunction({ + // Public API + get container() { + prepareBackup() + return clone + }, + separator: context.tailwindConfig.separator, + modifySelectors, + + // Private API for now + wrap(wrapper) { + let nodes = clone.nodes + clone.removeAll() + wrapper.append(nodes) + clone.append(wrapper) + }, + format(selectorFormat) { + collectedFormats.push({ + format: selectorFormat, + respectPrefix, + }) + }, + args, + }) + + // It can happen that a list of format strings is returned from within the function. In that + // case, we have to process them as well. We can use the existing `variantSort`. + if (Array.isArray(ruleWithVariant)) { + for (let [idx, variantFunction] of ruleWithVariant.entries()) { + // This is a little bit scary since we are pushing to an array of items that we are + // currently looping over. However, you can also think of it like a processing queue + // where you keep handling jobs until everything is done and each job can queue more + // jobs if needed. + variantFunctionTuples.push([ + context.offsets.applyParallelOffset(variantSort, idx), + variantFunction, + + // If the clone has been modified we have to pass that back + // though so each rule can use the modified container + clone.clone(), + ]) + } + continue + } + + if (typeof ruleWithVariant === 'string') { + collectedFormats.push({ + format: ruleWithVariant, + respectPrefix, + }) + } + + if (ruleWithVariant === null) { + continue + } + + // We had to backup selectors, therefore we assume that somebody touched + // `container` or `modifySelectors`. Let's see if they did, so that we + // can restore the selectors, and collect the format strings. + if (clone.raws.neededBackup) { + delete clone.raws.neededBackup + clone.walkRules((rule) => { + let before = rule.raws.originalSelector + if (!before) return + delete rule.raws.originalSelector + if (before === rule.selector) return // No mutation happened + + let modified = rule.selector + + // Rebuild the base selector, this is what plugin authors would do + // as well. E.g.: `${variant}${separator}${className}`. + // However, plugin authors probably also prepend or append certain + // classes, pseudos, ids, ... + let rebuiltBase = selectorParser((selectors) => { + selectors.walkClasses((classNode) => { + classNode.value = `${variant}${context.tailwindConfig.separator}${classNode.value}` + }) + }).processSync(before) + + // Now that we know the original selector, the new selector, and + // the rebuild part in between, we can replace the part that plugin + // authors need to rebuild with `&`, and eventually store it in the + // collectedFormats. Similar to what `format('...')` would do. + // + // E.g.: + // variant: foo + // selector: .markdown > p + // modified (by plugin): .foo .foo\\:markdown > p + // rebuiltBase (internal): .foo\\:markdown > p + // format: .foo & + collectedFormats.push({ + format: modified.replace(rebuiltBase, '&'), + respectPrefix, + }) + rule.selector = before + }) + } + + // This tracks the originating layer for the variant + // For example: + // .sm:underline {} is a variant of something in the utilities layer + // .sm:container {} is a variant of the container component + clone.nodes[0].raws.tailwind = { ...clone.nodes[0].raws.tailwind, parentLayer: meta.layer } + + let withOffset = [ + { + ...meta, + sort: context.offsets.applyVariantOffset( + meta.sort, + variantSort, + Object.assign(args, context.variantOptions.get(variant)) + ), + collectedFormats: (meta.collectedFormats ?? []).concat(collectedFormats), + }, + clone.nodes[0], + ] + result.push(withOffset) + } + } + + return result + } + + return [] +} + +function parseRules(rule, cache, options = {}) { + // PostCSS node + if (!isPlainObject(rule) && !Array.isArray(rule)) { + return [[rule], options] + } + + // Tuple + if (Array.isArray(rule)) { + return parseRules(rule[0], cache, rule[1]) + } + + // Simple object + if (!cache.has(rule)) { + cache.set(rule, parseObjectStyles(rule)) + } + + return [cache.get(rule), options] +} + +const IS_VALID_PROPERTY_NAME = /^[a-z_-]/ + +function isValidPropName(name) { + return IS_VALID_PROPERTY_NAME.test(name) +} + +/** + * @param {string} declaration + * @returns {boolean} + */ +function looksLikeUri(declaration) { + // Quick bailout for obvious non-urls + // This doesn't support schemes that don't use a leading // but that's unlikely to be a problem + if (!declaration.includes('://')) { + return false + } + + try { + const url = new URL(declaration) + return url.scheme !== '' && url.host !== '' + } catch (err) { + // Definitely not a valid url + return false + } +} + +function isParsableNode(node) { + let isParsable = true + + node.walkDecls((decl) => { + if (!isParsableCssValue(decl.prop, decl.value)) { + isParsable = false + return false + } + }) + + return isParsable +} + +function isParsableCssValue(property, value) { + // We don't want to to treat [https://example.com] as a custom property + // Even though, according to the CSS grammar, it's a totally valid CSS declaration + // So we short-circuit here by checking if the custom property looks like a url + if (looksLikeUri(`${property}:${value}`)) { + return false + } + + try { + postcss.parse(`a{${property}:${value}}`).toResult() + return true + } catch (err) { + return false + } +} + +function extractArbitraryProperty(classCandidate, context) { + let [, property, value] = classCandidate.match(/^\[([a-zA-Z0-9-_]+):(\S+)\]$/) ?? [] + + if (value === undefined) { + return null + } + + if (!isValidPropName(property)) { + return null + } + + if (!isValidArbitraryValue(value)) { + return null + } + + let normalized = normalize(value, { property }) + + if (!isParsableCssValue(property, normalized)) { + return null + } + + let sort = context.offsets.arbitraryProperty() + + return [ + [ + { sort, layer: 'utilities' }, + () => ({ + [asClass(classCandidate)]: { + [property]: normalized, + }, + }), + ], + ] +} + +function* resolveMatchedPlugins(classCandidate, context) { + if (context.candidateRuleMap.has(classCandidate)) { + yield [context.candidateRuleMap.get(classCandidate), 'DEFAULT'] + } + + yield* (function* (arbitraryPropertyRule) { + if (arbitraryPropertyRule !== null) { + yield [arbitraryPropertyRule, 'DEFAULT'] + } + })(extractArbitraryProperty(classCandidate, context)) + + let candidatePrefix = classCandidate + let negative = false + + const twConfigPrefix = context.tailwindConfig.prefix + + const twConfigPrefixLen = twConfigPrefix.length + + const hasMatchingPrefix = + candidatePrefix.startsWith(twConfigPrefix) || candidatePrefix.startsWith(`-${twConfigPrefix}`) + + if (candidatePrefix[twConfigPrefixLen] === '-' && hasMatchingPrefix) { + negative = true + candidatePrefix = twConfigPrefix + candidatePrefix.slice(twConfigPrefixLen + 1) + } + + if (negative && context.candidateRuleMap.has(candidatePrefix)) { + yield [context.candidateRuleMap.get(candidatePrefix), '-DEFAULT'] + } + + for (let [prefix, modifier] of candidatePermutations(candidatePrefix)) { + if (context.candidateRuleMap.has(prefix)) { + yield [context.candidateRuleMap.get(prefix), negative ? `-${modifier}` : modifier] + } + } +} + +function splitWithSeparator(input, separator) { + if (input === sharedState.NOT_ON_DEMAND) { + return [sharedState.NOT_ON_DEMAND] + } + + return splitAtTopLevelOnly(input, separator) +} + +function* recordCandidates(matches, classCandidate) { + for (const match of matches) { + match[1].raws.tailwind = { + ...match[1].raws.tailwind, + classCandidate, + preserveSource: match[0].options?.preserveSource ?? false, + } + + yield match + } +} + +function* resolveMatches(candidate, context) { + let separator = context.tailwindConfig.separator + let [classCandidate, ...variants] = splitWithSeparator(candidate, separator).reverse() + let important = false + + if (classCandidate.startsWith('!')) { + important = true + classCandidate = classCandidate.slice(1) + } + + // TODO: Reintroduce this in ways that doesn't break on false positives + // function sortAgainst(toSort, against) { + // return toSort.slice().sort((a, z) => { + // return bigSign(against.get(a)[0] - against.get(z)[0]) + // }) + // } + // let sorted = sortAgainst(variants, context.variantMap) + // if (sorted.toString() !== variants.toString()) { + // let corrected = sorted.reverse().concat(classCandidate).join(':') + // throw new Error(`Class ${candidate} should be written as ${corrected}`) + // } + + for (let matchedPlugins of resolveMatchedPlugins(classCandidate, context)) { + let matches = [] + let typesByMatches = new Map() + + let [plugins, modifier] = matchedPlugins + let isOnlyPlugin = plugins.length === 1 + + for (let [sort, plugin] of plugins) { + let matchesPerPlugin = [] + + if (typeof plugin === 'function') { + for (let ruleSet of [].concat(plugin(modifier, { isOnlyPlugin }))) { + let [rules, options] = parseRules(ruleSet, context.postCssNodeCache) + for (let rule of rules) { + matchesPerPlugin.push([{ ...sort, options: { ...sort.options, ...options } }, rule]) + } + } + } + // Only process static plugins on exact matches + else if (modifier === 'DEFAULT' || modifier === '-DEFAULT') { + let ruleSet = plugin + let [rules, options] = parseRules(ruleSet, context.postCssNodeCache) + for (let rule of rules) { + matchesPerPlugin.push([{ ...sort, options: { ...sort.options, ...options } }, rule]) + } + } + + if (matchesPerPlugin.length > 0) { + let matchingTypes = Array.from( + getMatchingTypes( + sort.options?.types ?? [], + modifier, + sort.options ?? {}, + context.tailwindConfig + ) + ).map(([_, type]) => type) + + if (matchingTypes.length > 0) { + typesByMatches.set(matchesPerPlugin, matchingTypes) + } + + matches.push(matchesPerPlugin) + } + } + + if (isArbitraryValue(modifier)) { + if (matches.length > 1) { + // Partition plugins in 2 categories so that we can start searching in the plugins that + // don't have `any` as a type first. + let [withAny, withoutAny] = matches.reduce( + (group, plugin) => { + let hasAnyType = plugin.some(([{ options }]) => + options.types.some(({ type }) => type === 'any') + ) + + if (hasAnyType) { + group[0].push(plugin) + } else { + group[1].push(plugin) + } + return group + }, + [[], []] + ) + + function findFallback(matches) { + // If only a single plugin matches, let's take that one + if (matches.length === 1) { + return matches[0] + } + + // Otherwise, find the plugin that creates a valid rule given the arbitrary value, and + // also has the correct type which preferOnConflicts the plugin in case of clashes. + return matches.find((rules) => { + let matchingTypes = typesByMatches.get(rules) + return rules.some(([{ options }, rule]) => { + if (!isParsableNode(rule)) { + return false + } + + return options.types.some( + ({ type, preferOnConflict }) => matchingTypes.includes(type) && preferOnConflict + ) + }) + }) + } + + // Try to find a fallback plugin, because we already know that multiple plugins matched for + // the given arbitrary value. + let fallback = findFallback(withoutAny) ?? findFallback(withAny) + if (fallback) { + matches = [fallback] + } + + // We couldn't find a fallback plugin which means that there are now multiple plugins that + // generated css for the current candidate. This means that the result is ambiguous and this + // should not happen. We won't generate anything right now, so let's report this to the user + // by logging some options about what they can do. + else { + let typesPerPlugin = matches.map( + (match) => new Set([...(typesByMatches.get(match) ?? [])]) + ) + + // Remove duplicates, so that we can detect proper unique types for each plugin. + for (let pluginTypes of typesPerPlugin) { + for (let type of pluginTypes) { + let removeFromOwnGroup = false + + for (let otherGroup of typesPerPlugin) { + if (pluginTypes === otherGroup) continue + + if (otherGroup.has(type)) { + otherGroup.delete(type) + removeFromOwnGroup = true + } + } + + if (removeFromOwnGroup) pluginTypes.delete(type) + } + } + + let messages = [] + + for (let [idx, group] of typesPerPlugin.entries()) { + for (let type of group) { + let rules = matches[idx] + .map(([, rule]) => rule) + .flat() + .map((rule) => + rule + .toString() + .split('\n') + .slice(1, -1) // Remove selector and closing '}' + .map((line) => line.trim()) + .map((x) => ` ${x}`) // Re-indent + .join('\n') + ) + .join('\n\n') + + messages.push( + ` Use \`${candidate.replace('[', `[${type}:`)}\` for \`${rules.trim()}\`` + ) + break + } + } + + log.warn([ + `The class \`${candidate}\` is ambiguous and matches multiple utilities.`, + ...messages, + `If this is content and not a class, replace it with \`${candidate + .replace('[', '[') + .replace(']', ']')}\` to silence this warning.`, + ]) + continue + } + } + + matches = matches.map((list) => list.filter((match) => isParsableNode(match[1]))) + } + + matches = matches.flat() + matches = Array.from(recordCandidates(matches, classCandidate)) + matches = applyPrefix(matches, context) + + if (important) { + matches = applyImportant(matches, classCandidate) + } + + for (let variant of variants) { + matches = applyVariant(variant, matches, context) + } + + for (let match of matches) { + match[1].raws.tailwind = { ...match[1].raws.tailwind, candidate } + + // Apply final format selector + match = applyFinalFormat(match, { context, candidate }) + + // Skip rules with invalid selectors + // This will cause the candidate to be added to the "not class" + // cache skipping it entirely for future builds + if (match === null) { + continue + } + + yield match + } + } +} + +function applyFinalFormat(match, { context, candidate }) { + if (!match[0].collectedFormats) { + return match + } + + let isValid = true + let finalFormat + + try { + finalFormat = formatVariantSelector(match[0].collectedFormats, { + context, + candidate, + }) + } catch { + // The format selector we produced is invalid + // This could be because: + // - A bug exists + // - A plugin introduced an invalid variant selector (ex: `addVariant('foo', '&;foo')`) + // - The user used an invalid arbitrary variant (ex: `[&;foo]:underline`) + // Either way the build will fail because of this + // We would rather that the build pass "silently" given that this could + // happen because of picking up invalid things when scanning content + // So we'll throw out the candidate instead + + return null + } + + let container = postcss.root({ nodes: [match[1].clone()] }) + + container.walkRules((rule) => { + if (inKeyframes(rule)) { + return + } + + try { + let selector = finalizeSelector(rule.selector, finalFormat, { + candidate, + context, + }) + + // Finalize Selector determined that this candidate is irrelevant + // TODO: This elimination should happen earlier so this never happens + if (selector === null) { + rule.remove() + return + } + + rule.selector = selector + } catch { + // If this selector is invalid we also want to skip it + // But it's likely that being invalid here means there's a bug in a plugin rather than too loosely matching content + isValid = false + return false + } + }) + + if (!isValid) { + return null + } + + match[1] = container.nodes[0] + + return match +} + +function inKeyframes(rule) { + return rule.parent && rule.parent.type === 'atrule' && rule.parent.name === 'keyframes' +} + +function getImportantStrategy(important) { + if (important === true) { + return (rule) => { + if (inKeyframes(rule)) { + return + } + + rule.walkDecls((d) => { + if (d.parent.type === 'rule' && !inKeyframes(d.parent)) { + d.important = true + } + }) + } + } + + if (typeof important === 'string') { + return (rule) => { + if (inKeyframes(rule)) { + return + } + + rule.selectors = rule.selectors.map((selector) => { + return applyImportantSelector(selector, important) + }) + } + } +} + +function generateRules(candidates, context, isSorting = false) { + let allRules = [] + let strategy = getImportantStrategy(context.tailwindConfig.important) + + for (let candidate of candidates) { + if (context.notClassCache.has(candidate)) { + continue + } + + if (context.candidateRuleCache.has(candidate)) { + allRules = allRules.concat(Array.from(context.candidateRuleCache.get(candidate))) + continue + } + + let matches = Array.from(resolveMatches(candidate, context)) + + if (matches.length === 0) { + context.notClassCache.add(candidate) + continue + } + + context.classCache.set(candidate, matches) + + let rules = context.candidateRuleCache.get(candidate) ?? new Set() + context.candidateRuleCache.set(candidate, rules) + + for (const match of matches) { + let [{ sort, options }, rule] = match + + if (options.respectImportant && strategy) { + let container = postcss.root({ nodes: [rule.clone()] }) + container.walkRules(strategy) + rule = container.nodes[0] + } + + // Note: We have to clone rules during sorting + // so we eliminate some shared mutable state + let newEntry = [sort, isSorting ? rule.clone() : rule] + rules.add(newEntry) + context.ruleCache.add(newEntry) + allRules.push(newEntry) + } + } + + return allRules +} + +function isArbitraryValue(input) { + return input.startsWith('[') && input.endsWith(']') +} + +export { resolveMatches, generateRules } diff --git a/node_modules/tailwindcss/src/lib/getModuleDependencies.js b/node_modules/tailwindcss/src/lib/getModuleDependencies.js new file mode 100644 index 0000000..e6a38a8 --- /dev/null +++ b/node_modules/tailwindcss/src/lib/getModuleDependencies.js @@ -0,0 +1,79 @@ +import fs from 'fs' +import path from 'path' + +let jsExtensions = ['.js', '.cjs', '.mjs'] + +// Given the current file `a.ts`, we want to make sure that when importing `b` that we resolve +// `b.ts` before `b.js` +// +// E.g.: +// +// a.ts +// b // .ts +// c // .ts +// a.js +// b // .js or .ts + +let jsResolutionOrder = ['', '.js', '.cjs', '.mjs', '.ts', '.cts', '.mts', '.jsx', '.tsx'] +let tsResolutionOrder = ['', '.ts', '.cts', '.mts', '.tsx', '.js', '.cjs', '.mjs', '.jsx'] + +function resolveWithExtension(file, extensions) { + // Try to find `./a.ts`, `./a.ts`, ... from `./a` + for (let ext of extensions) { + let full = `${file}${ext}` + if (fs.existsSync(full) && fs.statSync(full).isFile()) { + return full + } + } + + // Try to find `./a/index.js` from `./a` + for (let ext of extensions) { + let full = `${file}/index${ext}` + if (fs.existsSync(full)) { + return full + } + } + + return null +} + +function* _getModuleDependencies(filename, base, seen, ext = path.extname(filename)) { + // Try to find the file + let absoluteFile = resolveWithExtension( + path.resolve(base, filename), + jsExtensions.includes(ext) ? jsResolutionOrder : tsResolutionOrder + ) + if (absoluteFile === null) return // File doesn't exist + + // Prevent infinite loops when there are circular dependencies + if (seen.has(absoluteFile)) return // Already seen + seen.add(absoluteFile) + + // Mark the file as a dependency + yield absoluteFile + + // Resolve new base for new imports/requires + base = path.dirname(absoluteFile) + ext = path.extname(absoluteFile) + + let contents = fs.readFileSync(absoluteFile, 'utf-8') + + // Find imports/requires + for (let match of [ + ...contents.matchAll(/import[\s\S]*?['"](.{3,}?)['"]/gi), + ...contents.matchAll(/import[\s\S]*from[\s\S]*?['"](.{3,}?)['"]/gi), + ...contents.matchAll(/require\(['"`](.+)['"`]\)/gi), + ]) { + // Bail out if it's not a relative file + if (!match[1].startsWith('.')) continue + + yield* _getModuleDependencies(match[1], base, seen, ext) + } +} + +export default function getModuleDependencies(absoluteFilePath) { + if (absoluteFilePath === null) return new Set() + return new Set( + _getModuleDependencies(absoluteFilePath, path.dirname(absoluteFilePath), new Set()) + ) +} diff --git a/node_modules/tailwindcss/src/lib/load-config.ts b/node_modules/tailwindcss/src/lib/load-config.ts new file mode 100644 index 0000000..645e8e1 --- /dev/null +++ b/node_modules/tailwindcss/src/lib/load-config.ts @@ -0,0 +1,31 @@ +import jitiFactory from 'jiti' +import { transform } from 'sucrase' + +import { Config } from '../../types/config' + +let jiti: ReturnType<typeof jitiFactory> | null = null +function lazyJiti() { + return ( + jiti ?? + (jiti = jitiFactory(__filename, { + interopDefault: true, + transform: (opts) => { + return transform(opts.source, { + transforms: ['typescript', 'imports'], + }) + }, + })) + ) +} + +export function loadConfig(path: string): Config { + let config = (function () { + try { + return path ? require(path) : {} + } catch { + return lazyJiti()(path) + } + })() + + return config.default ?? config +} diff --git a/node_modules/tailwindcss/src/lib/normalizeTailwindDirectives.js b/node_modules/tailwindcss/src/lib/normalizeTailwindDirectives.js new file mode 100644 index 0000000..3349a7e --- /dev/null +++ b/node_modules/tailwindcss/src/lib/normalizeTailwindDirectives.js @@ -0,0 +1,84 @@ +import log from '../util/log' + +export default function normalizeTailwindDirectives(root) { + let tailwindDirectives = new Set() + let layerDirectives = new Set() + let applyDirectives = new Set() + + root.walkAtRules((atRule) => { + if (atRule.name === 'apply') { + applyDirectives.add(atRule) + } + + if (atRule.name === 'import') { + if (atRule.params === '"tailwindcss/base"' || atRule.params === "'tailwindcss/base'") { + atRule.name = 'tailwind' + atRule.params = 'base' + } else if ( + atRule.params === '"tailwindcss/components"' || + atRule.params === "'tailwindcss/components'" + ) { + atRule.name = 'tailwind' + atRule.params = 'components' + } else if ( + atRule.params === '"tailwindcss/utilities"' || + atRule.params === "'tailwindcss/utilities'" + ) { + atRule.name = 'tailwind' + atRule.params = 'utilities' + } else if ( + atRule.params === '"tailwindcss/screens"' || + atRule.params === "'tailwindcss/screens'" || + atRule.params === '"tailwindcss/variants"' || + atRule.params === "'tailwindcss/variants'" + ) { + atRule.name = 'tailwind' + atRule.params = 'variants' + } + } + + if (atRule.name === 'tailwind') { + if (atRule.params === 'screens') { + atRule.params = 'variants' + } + tailwindDirectives.add(atRule.params) + } + + if (['layer', 'responsive', 'variants'].includes(atRule.name)) { + if (['responsive', 'variants'].includes(atRule.name)) { + log.warn(`${atRule.name}-at-rule-deprecated`, [ + `The \`@${atRule.name}\` directive has been deprecated in Tailwind CSS v3.0.`, + `Use \`@layer utilities\` or \`@layer components\` instead.`, + 'https://tailwindcss.com/docs/upgrade-guide#replace-variants-with-layer', + ]) + } + layerDirectives.add(atRule) + } + }) + + if ( + !tailwindDirectives.has('base') || + !tailwindDirectives.has('components') || + !tailwindDirectives.has('utilities') + ) { + for (let rule of layerDirectives) { + if (rule.name === 'layer' && ['base', 'components', 'utilities'].includes(rule.params)) { + if (!tailwindDirectives.has(rule.params)) { + throw rule.error( + `\`@layer ${rule.params}\` is used but no matching \`@tailwind ${rule.params}\` directive is present.` + ) + } + } else if (rule.name === 'responsive') { + if (!tailwindDirectives.has('utilities')) { + throw rule.error('`@responsive` is used but `@tailwind utilities` is missing.') + } + } else if (rule.name === 'variants') { + if (!tailwindDirectives.has('utilities')) { + throw rule.error('`@variants` is used but `@tailwind utilities` is missing.') + } + } + } + } + + return { tailwindDirectives, applyDirectives } +} diff --git a/node_modules/tailwindcss/src/lib/offsets.js b/node_modules/tailwindcss/src/lib/offsets.js new file mode 100644 index 0000000..a43ebe4 --- /dev/null +++ b/node_modules/tailwindcss/src/lib/offsets.js @@ -0,0 +1,373 @@ +// @ts-check + +import bigSign from '../util/bigSign' +import { remapBitfield } from './remap-bitfield.js' + +/** + * @typedef {'base' | 'defaults' | 'components' | 'utilities' | 'variants' | 'user'} Layer + */ + +/** + * @typedef {object} VariantOption + * @property {number} id An unique identifier to identify `matchVariant` + * @property {function | undefined} sort The sort function + * @property {string|null} value The value we want to compare + * @property {string|null} modifier The modifier that was used (if any) + * @property {bigint} variant The variant bitmask + */ + +/** + * @typedef {object} RuleOffset + * @property {Layer} layer The layer that this rule belongs to + * @property {Layer} parentLayer The layer that this rule originally belonged to. Only different from layer if this is a variant. + * @property {bigint} arbitrary 0n if false, 1n if true + * @property {bigint} variants Dynamic size. 1 bit per registered variant. 0n means no variants + * @property {bigint} parallelIndex Rule index for the parallel variant. 0 if not applicable. + * @property {bigint} index Index of the rule / utility in it's given *parent* layer. Monotonically increasing. + * @property {VariantOption[]} options Some information on how we can sort arbitrary variants + */ + +export class Offsets { + constructor() { + /** + * Offsets for the next rule in a given layer + * + * @type {Record<Layer, bigint>} + */ + this.offsets = { + defaults: 0n, + base: 0n, + components: 0n, + utilities: 0n, + variants: 0n, + user: 0n, + } + + /** + * Positions for a given layer + * + * @type {Record<Layer, bigint>} + */ + this.layerPositions = { + defaults: 0n, + base: 1n, + components: 2n, + utilities: 3n, + + // There isn't technically a "user" layer, but we need to give it a position + // Because it's used for ordering user-css from @apply + user: 4n, + + variants: 5n, + } + + /** + * The total number of functions currently registered across all variants (including arbitrary variants) + * + * @type {bigint} + */ + this.reservedVariantBits = 0n + + /** + * Positions for a given variant + * + * @type {Map<string, bigint>} + */ + this.variantOffsets = new Map() + } + + /** + * @param {Layer} layer + * @returns {RuleOffset} + */ + create(layer) { + return { + layer, + parentLayer: layer, + arbitrary: 0n, + variants: 0n, + parallelIndex: 0n, + index: this.offsets[layer]++, + options: [], + } + } + + /** + * @returns {RuleOffset} + */ + arbitraryProperty() { + return { + ...this.create('utilities'), + arbitrary: 1n, + } + } + + /** + * Get the offset for a variant + * + * @param {string} variant + * @param {number} index + * @returns {RuleOffset} + */ + forVariant(variant, index = 0) { + let offset = this.variantOffsets.get(variant) + if (offset === undefined) { + throw new Error(`Cannot find offset for unknown variant ${variant}`) + } + + return { + ...this.create('variants'), + variants: offset << BigInt(index), + } + } + + /** + * @param {RuleOffset} rule + * @param {RuleOffset} variant + * @param {VariantOption} options + * @returns {RuleOffset} + */ + applyVariantOffset(rule, variant, options) { + options.variant = variant.variants + + return { + ...rule, + layer: 'variants', + parentLayer: rule.layer === 'variants' ? rule.parentLayer : rule.layer, + variants: rule.variants | variant.variants, + options: options.sort ? [].concat(options, rule.options) : rule.options, + + // TODO: Technically this is wrong. We should be handling parallel index on a per variant basis. + // We'll take the max of all the parallel indexes for now. + // @ts-ignore + parallelIndex: max([rule.parallelIndex, variant.parallelIndex]), + } + } + + /** + * @param {RuleOffset} offset + * @param {number} parallelIndex + * @returns {RuleOffset} + */ + applyParallelOffset(offset, parallelIndex) { + return { + ...offset, + parallelIndex: BigInt(parallelIndex), + } + } + + /** + * Each variant gets 1 bit per function / rule registered. + * This is because multiple variants can be applied to a single rule and we need to know which ones are present and which ones are not. + * Additionally, every unique group of variants is grouped together in the stylesheet. + * + * This grouping is order-independent. For instance, we do not differentiate between `hover:focus` and `focus:hover`. + * + * @param {string[]} variants + * @param {(name: string) => number} getLength + */ + recordVariants(variants, getLength) { + for (let variant of variants) { + this.recordVariant(variant, getLength(variant)) + } + } + + /** + * The same as `recordVariants` but for a single arbitrary variant at runtime. + * @param {string} variant + * @param {number} fnCount + * + * @returns {RuleOffset} The highest offset for this variant + */ + recordVariant(variant, fnCount = 1) { + this.variantOffsets.set(variant, 1n << this.reservedVariantBits) + + // Ensure space is reserved for each "function" in the parallel variant + // by offsetting the next variant by the number of parallel variants + // in the one we just added. + + // Single functions that return parallel variants are NOT handled separately here + // They're offset by 1 (or the number of functions) as usual + // And each rule returned is tracked separately since the functions are evaluated lazily. + // @see `RuleOffset.parallelIndex` + this.reservedVariantBits += BigInt(fnCount) + + return { + ...this.create('variants'), + variants: this.variantOffsets.get(variant), + } + } + + /** + * @param {RuleOffset} a + * @param {RuleOffset} b + * @returns {bigint} + */ + compare(a, b) { + // Sort layers together + if (a.layer !== b.layer) { + return this.layerPositions[a.layer] - this.layerPositions[b.layer] + } + + // When sorting the `variants` layer, we need to sort based on the parent layer as well within + // this variants layer. + if (a.parentLayer !== b.parentLayer) { + return this.layerPositions[a.parentLayer] - this.layerPositions[b.parentLayer] + } + + // Sort based on the sorting function + for (let aOptions of a.options) { + for (let bOptions of b.options) { + if (aOptions.id !== bOptions.id) continue + if (!aOptions.sort || !bOptions.sort) continue + + let maxFnVariant = max([aOptions.variant, bOptions.variant]) ?? 0n + + // Create a mask of 0s from bits 1..N where N represents the mask of the Nth bit + let mask = ~(maxFnVariant | (maxFnVariant - 1n)) + let aVariantsAfterFn = a.variants & mask + let bVariantsAfterFn = b.variants & mask + + // If the variants the same, we _can_ sort them + if (aVariantsAfterFn !== bVariantsAfterFn) { + continue + } + + let result = aOptions.sort( + { + value: aOptions.value, + modifier: aOptions.modifier, + }, + { + value: bOptions.value, + modifier: bOptions.modifier, + } + ) + if (result !== 0) return result + } + } + + // Sort variants in the order they were registered + if (a.variants !== b.variants) { + return a.variants - b.variants + } + + // Make sure each rule returned by a parallel variant is sorted in ascending order + if (a.parallelIndex !== b.parallelIndex) { + return a.parallelIndex - b.parallelIndex + } + + // Always sort arbitrary properties after other utilities + if (a.arbitrary !== b.arbitrary) { + return a.arbitrary - b.arbitrary + } + + // Sort utilities, components, etc… in the order they were registered + return a.index - b.index + } + + /** + * Arbitrary variants are recorded in the order they're encountered. + * This means that the order is not stable between environments and sets of content files. + * + * In order to make the order stable, we need to remap the arbitrary variant offsets to + * be in alphabetical order starting from the offset of the first arbitrary variant. + */ + recalculateVariantOffsets() { + // Sort the variants by their name + let variants = Array.from(this.variantOffsets.entries()) + .filter(([v]) => v.startsWith('[')) + .sort(([a], [z]) => fastCompare(a, z)) + + // Sort the list of offsets + // This is not necessarily a discrete range of numbers which is why + // we're using sort instead of creating a range from min/max + let newOffsets = variants.map(([, offset]) => offset).sort((a, z) => bigSign(a - z)) + + // Create a map from the old offsets to the new offsets in the new sort order + /** @type {[bigint, bigint][]} */ + let mapping = variants.map(([, oldOffset], i) => [oldOffset, newOffsets[i]]) + + // Remove any variants that will not move letting us skip + // remapping if everything happens to be in order + return mapping.filter(([a, z]) => a !== z) + } + + /** + * @template T + * @param {[RuleOffset, T][]} list + * @returns {[RuleOffset, T][]} + */ + remapArbitraryVariantOffsets(list) { + let mapping = this.recalculateVariantOffsets() + + // No arbitrary variants? Nothing to do. + // Everyhing already in order? Nothing to do. + if (mapping.length === 0) { + return list + } + + // Remap every variant offset in the list + return list.map((item) => { + let [offset, rule] = item + + offset = { + ...offset, + variants: remapBitfield(offset.variants, mapping), + } + + return [offset, rule] + }) + } + + /** + * @template T + * @param {[RuleOffset, T][]} list + * @returns {[RuleOffset, T][]} + */ + sort(list) { + list = this.remapArbitraryVariantOffsets(list) + + return list.sort(([a], [b]) => bigSign(this.compare(a, b))) + } +} + +/** + * + * @param {bigint[]} nums + * @returns {bigint|null} + */ +function max(nums) { + let max = null + + for (const num of nums) { + max = max ?? num + max = max > num ? max : num + } + + return max +} + +/** + * A fast ASCII order string comparison function. + * + * Using `.sort()` without a custom compare function is faster + * But you can only use that if you're sorting an array of + * only strings. If you're sorting strings inside objects + * or arrays, you need must use a custom compare function. + * + * @param {string} a + * @param {string} b + */ +function fastCompare(a, b) { + let aLen = a.length + let bLen = b.length + let minLen = aLen < bLen ? aLen : bLen + + for (let i = 0; i < minLen; i++) { + let cmp = a.charCodeAt(i) - b.charCodeAt(i) + if (cmp !== 0) return cmp + } + + return aLen - bLen +} diff --git a/node_modules/tailwindcss/src/lib/partitionApplyAtRules.js b/node_modules/tailwindcss/src/lib/partitionApplyAtRules.js new file mode 100644 index 0000000..34813c6 --- /dev/null +++ b/node_modules/tailwindcss/src/lib/partitionApplyAtRules.js @@ -0,0 +1,52 @@ +function partitionRules(root) { + if (!root.walkAtRules) return + + let applyParents = new Set() + + root.walkAtRules('apply', (rule) => { + applyParents.add(rule.parent) + }) + + if (applyParents.size === 0) { + return + } + + for (let rule of applyParents) { + let nodeGroups = [] + let lastGroup = [] + + for (let node of rule.nodes) { + if (node.type === 'atrule' && node.name === 'apply') { + if (lastGroup.length > 0) { + nodeGroups.push(lastGroup) + lastGroup = [] + } + nodeGroups.push([node]) + } else { + lastGroup.push(node) + } + } + + if (lastGroup.length > 0) { + nodeGroups.push(lastGroup) + } + + if (nodeGroups.length === 1) { + continue + } + + for (let group of [...nodeGroups].reverse()) { + let clone = rule.clone({ nodes: [] }) + clone.append(group) + rule.after(clone) + } + + rule.remove() + } +} + +export default function expandApplyAtRules() { + return (root) => { + partitionRules(root) + } +} diff --git a/node_modules/tailwindcss/src/lib/regex.js b/node_modules/tailwindcss/src/lib/regex.js new file mode 100644 index 0000000..5db7657 --- /dev/null +++ b/node_modules/tailwindcss/src/lib/regex.js @@ -0,0 +1,74 @@ +const REGEX_SPECIAL = /[\\^$.*+?()[\]{}|]/g +const REGEX_HAS_SPECIAL = RegExp(REGEX_SPECIAL.source) + +/** + * @param {string|RegExp|Array<string|RegExp>} source + */ +function toSource(source) { + source = Array.isArray(source) ? source : [source] + + source = source.map((item) => (item instanceof RegExp ? item.source : item)) + + return source.join('') +} + +/** + * @param {string|RegExp|Array<string|RegExp>} source + */ +export function pattern(source) { + return new RegExp(toSource(source), 'g') +} + +/** + * @param {string|RegExp|Array<string|RegExp>} source + */ +export function withoutCapturing(source) { + return new RegExp(`(?:${toSource(source)})`, 'g') +} + +/** + * @param {Array<string|RegExp>} sources + */ +export function any(sources) { + return `(?:${sources.map(toSource).join('|')})` +} + +/** + * @param {string|RegExp} source + */ +export function optional(source) { + return `(?:${toSource(source)})?` +} + +/** + * @param {string|RegExp|Array<string|RegExp>} source + */ +export function zeroOrMore(source) { + return `(?:${toSource(source)})*` +} + +/** + * Generate a RegExp that matches balanced brackets for a given depth + * We have to specify a depth because JS doesn't support recursive groups using ?R + * + * Based on https://stackoverflow.com/questions/17759004/how-to-match-string-within-parentheses-nested-in-java/17759264#17759264 + * + * @param {string|RegExp|Array<string|RegExp>} source + */ +export function nestedBrackets(open, close, depth = 1) { + return withoutCapturing([ + escape(open), + /[^\s]*/, + depth === 1 + ? `[^${escape(open)}${escape(close)}\s]*` + : any([`[^${escape(open)}${escape(close)}\s]*`, nestedBrackets(open, close, depth - 1)]), + /[^\s]*/, + escape(close), + ]) +} + +export function escape(string) { + return string && REGEX_HAS_SPECIAL.test(string) + ? string.replace(REGEX_SPECIAL, '\\$&') + : string || '' +} diff --git a/node_modules/tailwindcss/src/lib/remap-bitfield.js b/node_modules/tailwindcss/src/lib/remap-bitfield.js new file mode 100644 index 0000000..3ddaf20 --- /dev/null +++ b/node_modules/tailwindcss/src/lib/remap-bitfield.js @@ -0,0 +1,82 @@ +// @ts-check + +/** + * We must remap all the old bits to new bits for each set variant + * Only arbitrary variants are considered as those are the only + * ones that need to be re-sorted at this time + * + * An iterated process that removes and sets individual bits simultaneously + * will not work because we may have a new bit that is also a later old bit + * This means that we would be removing a previously set bit which we don't + * want to do + * + * For example (assume `bN` = `1<<N`) + * Given the "total" mapping `[[b1, b3], [b2, b4], [b3, b1], [b4, b2]]` + * The mapping is "total" because: + * 1. Every input and output is accounted for + * 2. All combinations are unique + * 3. No one input maps to multiple outputs and vice versa + * And, given an offset with all bits set: + * V = b1 | b2 | b3 | b4 + * + * Let's explore the issue with removing and setting bits simultaneously: + * V & ~b1 | b3 = b2 | b3 | b4 + * V & ~b2 | b4 = b3 | b4 + * V & ~b3 | b1 = b1 | b4 + * V & ~b4 | b2 = b1 | b2 + * + * As you can see, we end up with the wrong result. + * This is because we're removing a bit that was previously set. + * And, thus the final result is missing b3 and b4. + * + * Now, let's explore the issue with removing the bits first: + * V & ~b1 = b2 | b3 | b4 + * V & ~b2 = b3 | b4 + * V & ~b3 = b4 + * V & ~b4 = 0 + * + * And then setting the bits: + * V | b3 = b3 + * V | b4 = b3 | b4 + * V | b1 = b1 | b3 | b4 + * V | b2 = b1 | b2 | b3 | b4 + * + * We get the correct result because we're not removing any bits that were + * previously set thus properly remapping the bits to the new order + * + * To collect this into a single operation that can be done simultaneously + * we must first create a mask for the old bits that are set and a mask for + * the new bits that are set. Then we can remove the old bits and set the new + * bits simultaneously in a "single" operation like so: + * OldMask = b1 | b2 | b3 | b4 + * NewMask = b3 | b4 | b1 | b2 + * + * So this: + * V & ~oldMask | newMask + * + * Expands to this: + * V & ~b1 & ~b2 & ~b3 & ~b4 | b3 | b4 | b1 | b2 + * + * Which becomes this: + * b1 | b2 | b3 | b4 + * + * Which is the correct result! + * + * @param {bigint} num + * @param {[bigint, bigint][]} mapping + */ +export function remapBitfield(num, mapping) { + // Create masks for the old and new bits that are set + let oldMask = 0n + let newMask = 0n + for (let [oldBit, newBit] of mapping) { + if (num & oldBit) { + oldMask = oldMask | oldBit + newMask = newMask | newBit + } + } + + // Remove all old bits + // Set all new bits + return (num & ~oldMask) | newMask +} diff --git a/node_modules/tailwindcss/src/lib/resolveDefaultsAtRules.js b/node_modules/tailwindcss/src/lib/resolveDefaultsAtRules.js new file mode 100644 index 0000000..389ea4b --- /dev/null +++ b/node_modules/tailwindcss/src/lib/resolveDefaultsAtRules.js @@ -0,0 +1,163 @@ +import postcss from 'postcss' +import selectorParser from 'postcss-selector-parser' +import { flagEnabled } from '../featureFlags' + +let getNode = { + id(node) { + return selectorParser.attribute({ + attribute: 'id', + operator: '=', + value: node.value, + quoteMark: '"', + }) + }, +} + +function minimumImpactSelector(nodes) { + let rest = nodes + .filter((node) => { + // Keep non-pseudo nodes + if (node.type !== 'pseudo') return true + + // Keep pseudo nodes that have subnodes + // E.g.: `:not()` contains subnodes inside the parentheses + if (node.nodes.length > 0) return true + + // Keep pseudo `elements` + // This implicitly means that we ignore pseudo `classes` + return ( + node.value.startsWith('::') || + [':before', ':after', ':first-line', ':first-letter'].includes(node.value) + ) + }) + .reverse() + + let searchFor = new Set(['tag', 'class', 'id', 'attribute']) + + let splitPointIdx = rest.findIndex((n) => searchFor.has(n.type)) + if (splitPointIdx === -1) return rest.reverse().join('').trim() + + let node = rest[splitPointIdx] + let bestNode = getNode[node.type] ? getNode[node.type](node) : node + + rest = rest.slice(0, splitPointIdx) + + let combinatorIdx = rest.findIndex((n) => n.type === 'combinator' && n.value === '>') + if (combinatorIdx !== -1) { + rest.splice(0, combinatorIdx) + rest.unshift(selectorParser.universal()) + } + + return [bestNode, ...rest.reverse()].join('').trim() +} + +export let elementSelectorParser = selectorParser((selectors) => { + return selectors.map((s) => { + let nodes = s.split((n) => n.type === 'combinator' && n.value === ' ').pop() + return minimumImpactSelector(nodes) + }) +}) + +let cache = new Map() + +function extractElementSelector(selector) { + if (!cache.has(selector)) { + cache.set(selector, elementSelectorParser.transformSync(selector)) + } + + return cache.get(selector) +} + +export default function resolveDefaultsAtRules({ tailwindConfig }) { + return (root) => { + let variableNodeMap = new Map() + + /** @type {Set<import('postcss').AtRule>} */ + let universals = new Set() + + root.walkAtRules('defaults', (rule) => { + if (rule.nodes && rule.nodes.length > 0) { + universals.add(rule) + return + } + + let variable = rule.params + if (!variableNodeMap.has(variable)) { + variableNodeMap.set(variable, new Set()) + } + + variableNodeMap.get(variable).add(rule.parent) + + rule.remove() + }) + + if (flagEnabled(tailwindConfig, 'optimizeUniversalDefaults')) { + for (let universal of universals) { + /** @type {Map<string, Set<string>>} */ + let selectorGroups = new Map() + + let rules = variableNodeMap.get(universal.params) ?? [] + + for (let rule of rules) { + for (let selector of extractElementSelector(rule.selector)) { + // If selector contains a vendor prefix after a pseudo element or class, + // we consider them separately because merging the declarations into + // a single rule will cause browsers that do not understand the + // vendor prefix to throw out the whole rule + let selectorGroupName = + selector.includes(':-') || selector.includes('::-') ? selector : '__DEFAULT__' + + let selectors = selectorGroups.get(selectorGroupName) ?? new Set() + selectorGroups.set(selectorGroupName, selectors) + + selectors.add(selector) + } + } + + if (flagEnabled(tailwindConfig, 'optimizeUniversalDefaults')) { + if (selectorGroups.size === 0) { + universal.remove() + continue + } + + for (let [, selectors] of selectorGroups) { + let universalRule = postcss.rule({ + source: universal.source, + }) + + universalRule.selectors = [...selectors] + + universalRule.append(universal.nodes.map((node) => node.clone())) + universal.before(universalRule) + } + } + + universal.remove() + } + } else if (universals.size) { + let universalRule = postcss.rule({ + selectors: ['*', '::before', '::after'], + }) + + for (let universal of universals) { + universalRule.append(universal.nodes) + + if (!universalRule.parent) { + universal.before(universalRule) + } + + if (!universalRule.source) { + universalRule.source = universal.source + } + + universal.remove() + } + + let backdropRule = universalRule.clone({ + selectors: ['::backdrop'], + }) + + universalRule.after(backdropRule) + } + } +} diff --git a/node_modules/tailwindcss/src/lib/setupContextUtils.js b/node_modules/tailwindcss/src/lib/setupContextUtils.js new file mode 100644 index 0000000..59c261d --- /dev/null +++ b/node_modules/tailwindcss/src/lib/setupContextUtils.js @@ -0,0 +1,1342 @@ +import fs from 'fs' +import url from 'url' +import postcss from 'postcss' +import dlv from 'dlv' +import selectorParser from 'postcss-selector-parser' + +import transformThemeValue from '../util/transformThemeValue' +import parseObjectStyles from '../util/parseObjectStyles' +import prefixSelector from '../util/prefixSelector' +import isPlainObject from '../util/isPlainObject' +import escapeClassName from '../util/escapeClassName' +import nameClass, { formatClass } from '../util/nameClass' +import { coerceValue } from '../util/pluginUtils' +import { variantPlugins, corePlugins } from '../corePlugins' +import * as sharedState from './sharedState' +import { env } from './sharedState' +import { toPath } from '../util/toPath' +import log from '../util/log' +import negateValue from '../util/negateValue' +import isSyntacticallyValidPropertyValue from '../util/isSyntacticallyValidPropertyValue' +import { generateRules, getClassNameFromSelector } from './generateRules' +import { hasContentChanged } from './cacheInvalidation.js' +import { Offsets } from './offsets.js' +import { flagEnabled } from '../featureFlags.js' +import { finalizeSelector, formatVariantSelector } from '../util/formatVariantSelector' + +export const INTERNAL_FEATURES = Symbol() + +const VARIANT_TYPES = { + AddVariant: Symbol.for('ADD_VARIANT'), + MatchVariant: Symbol.for('MATCH_VARIANT'), +} + +const VARIANT_INFO = { + Base: 1 << 0, + Dynamic: 1 << 1, +} + +function prefix(context, selector) { + let prefix = context.tailwindConfig.prefix + return typeof prefix === 'function' ? prefix(selector) : prefix + selector +} + +function normalizeOptionTypes({ type = 'any', ...options }) { + let types = [].concat(type) + + return { + ...options, + types: types.map((type) => { + if (Array.isArray(type)) { + return { type: type[0], ...type[1] } + } + return { type, preferOnConflict: false } + }), + } +} + +function parseVariantFormatString(input) { + /** @type {string[]} */ + let parts = [] + + // When parsing whitespace around special characters are insignificant + // However, _inside_ of a variant they could be + // Because the selector could look like this + // @media { &[data-name="foo bar"] } + // This is why we do not skip whitespace + + let current = '' + let depth = 0 + + for (let idx = 0; idx < input.length; idx++) { + let char = input[idx] + + if (char === '\\') { + // Escaped characters are not special + current += '\\' + input[++idx] + } else if (char === '{') { + // Nested rule: start + ++depth + parts.push(current.trim()) + current = '' + } else if (char === '}') { + // Nested rule: end + if (--depth < 0) { + throw new Error(`Your { and } are unbalanced.`) + } + + parts.push(current.trim()) + current = '' + } else { + // Normal character + current += char + } + } + + if (current.length > 0) { + parts.push(current.trim()) + } + + parts = parts.filter((part) => part !== '') + + return parts +} + +function insertInto(list, value, { before = [] } = {}) { + before = [].concat(before) + + if (before.length <= 0) { + list.push(value) + return + } + + let idx = list.length - 1 + for (let other of before) { + let iidx = list.indexOf(other) + if (iidx === -1) continue + idx = Math.min(idx, iidx) + } + + list.splice(idx, 0, value) +} + +function parseStyles(styles) { + if (!Array.isArray(styles)) { + return parseStyles([styles]) + } + + return styles.flatMap((style) => { + let isNode = !Array.isArray(style) && !isPlainObject(style) + return isNode ? style : parseObjectStyles(style) + }) +} + +function getClasses(selector, mutate) { + let parser = selectorParser((selectors) => { + let allClasses = [] + + if (mutate) { + mutate(selectors) + } + + selectors.walkClasses((classNode) => { + allClasses.push(classNode.value) + }) + + return allClasses + }) + return parser.transformSync(selector) +} + +/** + * Ignore everything inside a :not(...). This allows you to write code like + * `div:not(.foo)`. If `.foo` is never found in your code, then we used to + * not generated it. But now we will ignore everything inside a `:not`, so + * that it still gets generated. + * + * @param {selectorParser.Root} selectors + */ +function ignoreNot(selectors) { + selectors.walkPseudos((pseudo) => { + if (pseudo.value === ':not') { + pseudo.remove() + } + }) +} + +function extractCandidates(node, state = { containsNonOnDemandable: false }, depth = 0) { + let classes = [] + let selectors = [] + + if (node.type === 'rule') { + // Handle normal rules + selectors.push(...node.selectors) + } else if (node.type === 'atrule') { + // Handle at-rules (which contains nested rules) + node.walkRules((rule) => selectors.push(...rule.selectors)) + } + + for (let selector of selectors) { + let classCandidates = getClasses(selector, ignoreNot) + + // At least one of the selectors contains non-"on-demandable" candidates. + if (classCandidates.length === 0) { + state.containsNonOnDemandable = true + } + + for (let classCandidate of classCandidates) { + classes.push(classCandidate) + } + } + + if (depth === 0) { + return [state.containsNonOnDemandable || classes.length === 0, classes] + } + + return classes +} + +function withIdentifiers(styles) { + return parseStyles(styles).flatMap((node) => { + let nodeMap = new Map() + let [containsNonOnDemandableSelectors, candidates] = extractCandidates(node) + + // If this isn't "on-demandable", assign it a universal candidate to always include it. + if (containsNonOnDemandableSelectors) { + candidates.unshift(sharedState.NOT_ON_DEMAND) + } + + // However, it could be that it also contains "on-demandable" candidates. + // E.g.: `span, .foo {}`, in that case it should still be possible to use + // `@apply foo` for example. + return candidates.map((c) => { + if (!nodeMap.has(node)) { + nodeMap.set(node, node) + } + return [c, nodeMap.get(node)] + }) + }) +} + +export function isValidVariantFormatString(format) { + return format.startsWith('@') || format.includes('&') +} + +export function parseVariant(variant) { + variant = variant + .replace(/\n+/g, '') + .replace(/\s{1,}/g, ' ') + .trim() + + let fns = parseVariantFormatString(variant) + .map((str) => { + if (!str.startsWith('@')) { + return ({ format }) => format(str) + } + + let [, name, params] = /@(\S*)( .+|[({].*)?/g.exec(str) + return ({ wrap }) => wrap(postcss.atRule({ name, params: params?.trim() ?? '' })) + }) + .reverse() + + return (api) => { + for (let fn of fns) { + fn(api) + } + } +} + +/** + * + * @param {any} tailwindConfig + * @param {any} context + * @param {object} param2 + * @param {Offsets} param2.offsets + */ +function buildPluginApi(tailwindConfig, context, { variantList, variantMap, offsets, classList }) { + function getConfigValue(path, defaultValue) { + return path ? dlv(tailwindConfig, path, defaultValue) : tailwindConfig + } + + function applyConfiguredPrefix(selector) { + return prefixSelector(tailwindConfig.prefix, selector) + } + + function prefixIdentifier(identifier, options) { + if (identifier === sharedState.NOT_ON_DEMAND) { + return sharedState.NOT_ON_DEMAND + } + + if (!options.respectPrefix) { + return identifier + } + + return context.tailwindConfig.prefix + identifier + } + + function resolveThemeValue(path, defaultValue, opts = {}) { + let parts = toPath(path) + let value = getConfigValue(['theme', ...parts], defaultValue) + return transformThemeValue(parts[0])(value, opts) + } + + let variantIdentifier = 0 + let api = { + postcss, + prefix: applyConfiguredPrefix, + e: escapeClassName, + config: getConfigValue, + theme: resolveThemeValue, + corePlugins: (path) => { + if (Array.isArray(tailwindConfig.corePlugins)) { + return tailwindConfig.corePlugins.includes(path) + } + + return getConfigValue(['corePlugins', path], true) + }, + variants: () => { + // Preserved for backwards compatibility but not used in v3.0+ + return [] + }, + addBase(base) { + for (let [identifier, rule] of withIdentifiers(base)) { + let prefixedIdentifier = prefixIdentifier(identifier, {}) + let offset = offsets.create('base') + + if (!context.candidateRuleMap.has(prefixedIdentifier)) { + context.candidateRuleMap.set(prefixedIdentifier, []) + } + + context.candidateRuleMap + .get(prefixedIdentifier) + .push([{ sort: offset, layer: 'base' }, rule]) + } + }, + /** + * @param {string} group + * @param {Record<string, string | string[]>} declarations + */ + addDefaults(group, declarations) { + const groups = { + [`@defaults ${group}`]: declarations, + } + + for (let [identifier, rule] of withIdentifiers(groups)) { + let prefixedIdentifier = prefixIdentifier(identifier, {}) + + if (!context.candidateRuleMap.has(prefixedIdentifier)) { + context.candidateRuleMap.set(prefixedIdentifier, []) + } + + context.candidateRuleMap + .get(prefixedIdentifier) + .push([{ sort: offsets.create('defaults'), layer: 'defaults' }, rule]) + } + }, + addComponents(components, options) { + let defaultOptions = { + preserveSource: false, + respectPrefix: true, + respectImportant: false, + } + + options = Object.assign({}, defaultOptions, Array.isArray(options) ? {} : options) + + for (let [identifier, rule] of withIdentifiers(components)) { + let prefixedIdentifier = prefixIdentifier(identifier, options) + + classList.add(prefixedIdentifier) + + if (!context.candidateRuleMap.has(prefixedIdentifier)) { + context.candidateRuleMap.set(prefixedIdentifier, []) + } + + context.candidateRuleMap + .get(prefixedIdentifier) + .push([{ sort: offsets.create('components'), layer: 'components', options }, rule]) + } + }, + addUtilities(utilities, options) { + let defaultOptions = { + preserveSource: false, + respectPrefix: true, + respectImportant: true, + } + + options = Object.assign({}, defaultOptions, Array.isArray(options) ? {} : options) + + for (let [identifier, rule] of withIdentifiers(utilities)) { + let prefixedIdentifier = prefixIdentifier(identifier, options) + + classList.add(prefixedIdentifier) + + if (!context.candidateRuleMap.has(prefixedIdentifier)) { + context.candidateRuleMap.set(prefixedIdentifier, []) + } + + context.candidateRuleMap + .get(prefixedIdentifier) + .push([{ sort: offsets.create('utilities'), layer: 'utilities', options }, rule]) + } + }, + matchUtilities: function (utilities, options) { + let defaultOptions = { + respectPrefix: true, + respectImportant: true, + modifiers: false, + } + + options = normalizeOptionTypes({ ...defaultOptions, ...options }) + + let offset = offsets.create('utilities') + + for (let identifier in utilities) { + let prefixedIdentifier = prefixIdentifier(identifier, options) + let rule = utilities[identifier] + + classList.add([prefixedIdentifier, options]) + + function wrapped(modifier, { isOnlyPlugin }) { + let [value, coercedType, utilityModifier] = coerceValue( + options.types, + modifier, + options, + tailwindConfig + ) + + if (value === undefined) { + return [] + } + + if (!options.types.some(({ type }) => type === coercedType)) { + if (isOnlyPlugin) { + log.warn([ + `Unnecessary typehint \`${coercedType}\` in \`${identifier}-${modifier}\`.`, + `You can safely update it to \`${identifier}-${modifier.replace( + coercedType + ':', + '' + )}\`.`, + ]) + } else { + return [] + } + } + + if (!isSyntacticallyValidPropertyValue(value)) { + return [] + } + + let extras = { + get modifier() { + if (!options.modifiers) { + log.warn(`modifier-used-without-options-for-${identifier}`, [ + 'Your plugin must set `modifiers: true` in its options to support modifiers.', + ]) + } + + return utilityModifier + }, + } + + let modifiersEnabled = flagEnabled(tailwindConfig, 'generalizedModifiers') + + let ruleSets = [] + .concat(modifiersEnabled ? rule(value, extras) : rule(value)) + .filter(Boolean) + .map((declaration) => ({ + [nameClass(identifier, modifier)]: declaration, + })) + + return ruleSets + } + + let withOffsets = [{ sort: offset, layer: 'utilities', options }, wrapped] + + if (!context.candidateRuleMap.has(prefixedIdentifier)) { + context.candidateRuleMap.set(prefixedIdentifier, []) + } + + context.candidateRuleMap.get(prefixedIdentifier).push(withOffsets) + } + }, + matchComponents: function (components, options) { + let defaultOptions = { + respectPrefix: true, + respectImportant: false, + modifiers: false, + } + + options = normalizeOptionTypes({ ...defaultOptions, ...options }) + + let offset = offsets.create('components') + + for (let identifier in components) { + let prefixedIdentifier = prefixIdentifier(identifier, options) + let rule = components[identifier] + + classList.add([prefixedIdentifier, options]) + + function wrapped(modifier, { isOnlyPlugin }) { + let [value, coercedType, utilityModifier] = coerceValue( + options.types, + modifier, + options, + tailwindConfig + ) + + if (value === undefined) { + return [] + } + + if (!options.types.some(({ type }) => type === coercedType)) { + if (isOnlyPlugin) { + log.warn([ + `Unnecessary typehint \`${coercedType}\` in \`${identifier}-${modifier}\`.`, + `You can safely update it to \`${identifier}-${modifier.replace( + coercedType + ':', + '' + )}\`.`, + ]) + } else { + return [] + } + } + + if (!isSyntacticallyValidPropertyValue(value)) { + return [] + } + + let extras = { + get modifier() { + if (!options.modifiers) { + log.warn(`modifier-used-without-options-for-${identifier}`, [ + 'Your plugin must set `modifiers: true` in its options to support modifiers.', + ]) + } + + return utilityModifier + }, + } + + let modifiersEnabled = flagEnabled(tailwindConfig, 'generalizedModifiers') + + let ruleSets = [] + .concat(modifiersEnabled ? rule(value, extras) : rule(value)) + .filter(Boolean) + .map((declaration) => ({ + [nameClass(identifier, modifier)]: declaration, + })) + + return ruleSets + } + + let withOffsets = [{ sort: offset, layer: 'components', options }, wrapped] + + if (!context.candidateRuleMap.has(prefixedIdentifier)) { + context.candidateRuleMap.set(prefixedIdentifier, []) + } + + context.candidateRuleMap.get(prefixedIdentifier).push(withOffsets) + } + }, + addVariant(variantName, variantFunctions, options = {}) { + variantFunctions = [].concat(variantFunctions).map((variantFunction) => { + if (typeof variantFunction !== 'string') { + // Safelist public API functions + return (api = {}) => { + let { args, modifySelectors, container, separator, wrap, format } = api + let result = variantFunction( + Object.assign( + { modifySelectors, container, separator }, + options.type === VARIANT_TYPES.MatchVariant && { args, wrap, format } + ) + ) + + if (typeof result === 'string' && !isValidVariantFormatString(result)) { + throw new Error( + `Your custom variant \`${variantName}\` has an invalid format string. Make sure it's an at-rule or contains a \`&\` placeholder.` + ) + } + + if (Array.isArray(result)) { + return result + .filter((variant) => typeof variant === 'string') + .map((variant) => parseVariant(variant)) + } + + // result may be undefined with legacy variants that use APIs like `modifySelectors` + // result may also be a postcss node if someone was returning the result from `modifySelectors` + return result && typeof result === 'string' && parseVariant(result)(api) + } + } + + if (!isValidVariantFormatString(variantFunction)) { + throw new Error( + `Your custom variant \`${variantName}\` has an invalid format string. Make sure it's an at-rule or contains a \`&\` placeholder.` + ) + } + + return parseVariant(variantFunction) + }) + + insertInto(variantList, variantName, options) + variantMap.set(variantName, variantFunctions) + context.variantOptions.set(variantName, options) + }, + matchVariant(variant, variantFn, options) { + // A unique identifier that "groups" these variants together. + // This is for internal use only which is why it is not present in the types + let id = options?.id ?? ++variantIdentifier + let isSpecial = variant === '@' + + let modifiersEnabled = flagEnabled(tailwindConfig, 'generalizedModifiers') + + for (let [key, value] of Object.entries(options?.values ?? {})) { + if (key === 'DEFAULT') continue + + api.addVariant( + isSpecial ? `${variant}${key}` : `${variant}-${key}`, + ({ args, container }) => { + return variantFn( + value, + modifiersEnabled ? { modifier: args?.modifier, container } : { container } + ) + }, + + { + ...options, + value, + id, + type: VARIANT_TYPES.MatchVariant, + variantInfo: VARIANT_INFO.Base, + } + ) + } + + let hasDefault = 'DEFAULT' in (options?.values ?? {}) + + api.addVariant( + variant, + ({ args, container }) => { + if (args?.value === sharedState.NONE && !hasDefault) { + return null + } + + return variantFn( + args?.value === sharedState.NONE + ? options.values.DEFAULT + : // Falling back to args if it is a string, otherwise '' for older intellisense + // (JetBrains) plugins. + args?.value ?? (typeof args === 'string' ? args : ''), + modifiersEnabled ? { modifier: args?.modifier, container } : { container } + ) + }, + { + ...options, + id, + type: VARIANT_TYPES.MatchVariant, + variantInfo: VARIANT_INFO.Dynamic, + } + ) + }, + } + + return api +} + +let fileModifiedMapCache = new WeakMap() +export function getFileModifiedMap(context) { + if (!fileModifiedMapCache.has(context)) { + fileModifiedMapCache.set(context, new Map()) + } + return fileModifiedMapCache.get(context) +} + +function trackModified(files, fileModifiedMap) { + let changed = false + let mtimesToCommit = new Map() + + for (let file of files) { + if (!file) continue + + let parsed = url.parse(file) + let pathname = parsed.hash ? parsed.href.replace(parsed.hash, '') : parsed.href + pathname = parsed.search ? pathname.replace(parsed.search, '') : pathname + let newModified = fs.statSync(decodeURIComponent(pathname), { throwIfNoEntry: false })?.mtimeMs + if (!newModified) { + // It could happen that a file is passed in that doesn't exist. E.g.: + // postcss-cli will provide you a fake path when reading from stdin. This + // path then looks like /path-to-your-project/stdin In that case we just + // want to ignore it and don't track changes at all. + continue + } + + if (!fileModifiedMap.has(file) || newModified > fileModifiedMap.get(file)) { + changed = true + } + + mtimesToCommit.set(file, newModified) + } + + return [changed, mtimesToCommit] +} + +function extractVariantAtRules(node) { + node.walkAtRules((atRule) => { + if (['responsive', 'variants'].includes(atRule.name)) { + extractVariantAtRules(atRule) + atRule.before(atRule.nodes) + atRule.remove() + } + }) +} + +function collectLayerPlugins(root) { + let layerPlugins = [] + + root.each((node) => { + if (node.type === 'atrule' && ['responsive', 'variants'].includes(node.name)) { + node.name = 'layer' + node.params = 'utilities' + } + }) + + // Walk @layer rules and treat them like plugins + root.walkAtRules('layer', (layerRule) => { + extractVariantAtRules(layerRule) + + if (layerRule.params === 'base') { + for (let node of layerRule.nodes) { + layerPlugins.push(function ({ addBase }) { + addBase(node, { respectPrefix: false }) + }) + } + layerRule.remove() + } else if (layerRule.params === 'components') { + for (let node of layerRule.nodes) { + layerPlugins.push(function ({ addComponents }) { + addComponents(node, { respectPrefix: false, preserveSource: true }) + }) + } + layerRule.remove() + } else if (layerRule.params === 'utilities') { + for (let node of layerRule.nodes) { + layerPlugins.push(function ({ addUtilities }) { + addUtilities(node, { respectPrefix: false, preserveSource: true }) + }) + } + layerRule.remove() + } + }) + + return layerPlugins +} + +function resolvePlugins(context, root) { + let corePluginList = Object.entries({ ...variantPlugins, ...corePlugins }) + .map(([name, plugin]) => { + if (!context.tailwindConfig.corePlugins.includes(name)) { + return null + } + + return plugin + }) + .filter(Boolean) + + let userPlugins = context.tailwindConfig.plugins.map((plugin) => { + if (plugin.__isOptionsFunction) { + plugin = plugin() + } + + return typeof plugin === 'function' ? plugin : plugin.handler + }) + + let layerPlugins = collectLayerPlugins(root) + + // TODO: This is a workaround for backwards compatibility, since custom variants + // were historically sorted before screen/stackable variants. + let beforeVariants = [ + variantPlugins['pseudoElementVariants'], + variantPlugins['pseudoClassVariants'], + variantPlugins['ariaVariants'], + variantPlugins['dataVariants'], + ] + let afterVariants = [ + variantPlugins['supportsVariants'], + variantPlugins['directionVariants'], + variantPlugins['reducedMotionVariants'], + variantPlugins['prefersContrastVariants'], + variantPlugins['darkVariants'], + variantPlugins['printVariant'], + variantPlugins['screenVariants'], + variantPlugins['orientationVariants'], + ] + + return [...corePluginList, ...beforeVariants, ...userPlugins, ...afterVariants, ...layerPlugins] +} + +function registerPlugins(plugins, context) { + let variantList = [] + let variantMap = new Map() + context.variantMap = variantMap + + let offsets = new Offsets() + context.offsets = offsets + + let classList = new Set() + + let pluginApi = buildPluginApi(context.tailwindConfig, context, { + variantList, + variantMap, + offsets, + classList, + }) + + for (let plugin of plugins) { + if (Array.isArray(plugin)) { + for (let pluginItem of plugin) { + pluginItem(pluginApi) + } + } else { + plugin?.(pluginApi) + } + } + + // Make sure to record bit masks for every variant + offsets.recordVariants(variantList, (variant) => variantMap.get(variant).length) + + // Build variantMap + for (let [variantName, variantFunctions] of variantMap.entries()) { + context.variantMap.set( + variantName, + variantFunctions.map((variantFunction, idx) => [ + offsets.forVariant(variantName, idx), + variantFunction, + ]) + ) + } + + let safelist = (context.tailwindConfig.safelist ?? []).filter(Boolean) + if (safelist.length > 0) { + let checks = [] + + for (let value of safelist) { + if (typeof value === 'string') { + context.changedContent.push({ content: value, extension: 'html' }) + continue + } + + if (value instanceof RegExp) { + log.warn('root-regex', [ + 'Regular expressions in `safelist` work differently in Tailwind CSS v3.0.', + 'Update your `safelist` configuration to eliminate this warning.', + 'https://tailwindcss.com/docs/content-configuration#safelisting-classes', + ]) + continue + } + + checks.push(value) + } + + if (checks.length > 0) { + let patternMatchingCount = new Map() + let prefixLength = context.tailwindConfig.prefix.length + let checkImportantUtils = checks.some((check) => check.pattern.source.includes('!')) + + for (let util of classList) { + let utils = Array.isArray(util) + ? (() => { + let [utilName, options] = util + let values = Object.keys(options?.values ?? {}) + let classes = values.map((value) => formatClass(utilName, value)) + + if (options?.supportsNegativeValues) { + // This is the normal negated version + // e.g. `-inset-1` or `-tw-inset-1` + classes = [...classes, ...classes.map((cls) => '-' + cls)] + + // This is the negated version *after* the prefix + // e.g. `tw--inset-1` + // The prefix is already attached to util name + // So we add the negative after the prefix + classes = [ + ...classes, + ...classes.map( + (cls) => cls.slice(0, prefixLength) + '-' + cls.slice(prefixLength) + ), + ] + } + + if (options.types.some(({ type }) => type === 'color')) { + classes = [ + ...classes, + ...classes.flatMap((cls) => + Object.keys(context.tailwindConfig.theme.opacity).map( + (opacity) => `${cls}/${opacity}` + ) + ), + ] + } + + if (checkImportantUtils && options?.respectImportant) { + classes = [...classes, ...classes.map((cls) => '!' + cls)] + } + + return classes + })() + : [util] + + for (let util of utils) { + for (let { pattern, variants = [] } of checks) { + // RegExp with the /g flag are stateful, so let's reset the last + // index pointer to reset the state. + pattern.lastIndex = 0 + + if (!patternMatchingCount.has(pattern)) { + patternMatchingCount.set(pattern, 0) + } + + if (!pattern.test(util)) continue + + patternMatchingCount.set(pattern, patternMatchingCount.get(pattern) + 1) + + context.changedContent.push({ content: util, extension: 'html' }) + for (let variant of variants) { + context.changedContent.push({ + content: variant + context.tailwindConfig.separator + util, + extension: 'html', + }) + } + } + } + } + + for (let [regex, count] of patternMatchingCount.entries()) { + if (count !== 0) continue + + log.warn([ + `The safelist pattern \`${regex}\` doesn't match any Tailwind CSS classes.`, + 'Fix this pattern or remove it from your `safelist` configuration.', + 'https://tailwindcss.com/docs/content-configuration#safelisting-classes', + ]) + } + } + } + + let darkClassName = [].concat(context.tailwindConfig.darkMode ?? 'media')[1] ?? 'dark' + + // A list of utilities that are used by certain Tailwind CSS utilities but + // that don't exist on their own. This will result in them "not existing" and + // sorting could be weird since you still require them in order to make the + // host utilities work properly. (Thanks Biology) + let parasiteUtilities = [ + prefix(context, darkClassName), + prefix(context, 'group'), + prefix(context, 'peer'), + ] + context.getClassOrder = function getClassOrder(classes) { + // Sort classes so they're ordered in a deterministic manner + let sorted = [...classes].sort((a, z) => { + if (a === z) return 0 + if (a < z) return -1 + return 1 + }) + + // Non-util classes won't be generated, so we default them to null + let sortedClassNames = new Map(sorted.map((className) => [className, null])) + + // Sort all classes in order + // Non-tailwind classes won't be generated and will be left as `null` + let rules = generateRules(new Set(sorted), context, true) + rules = context.offsets.sort(rules) + + let idx = BigInt(parasiteUtilities.length) + + for (const [, rule] of rules) { + let candidate = rule.raws.tailwind.candidate + + // When multiple rules match a candidate + // always take the position of the first one + sortedClassNames.set(candidate, sortedClassNames.get(candidate) ?? idx++) + } + + return classes.map((className) => { + let order = sortedClassNames.get(className) ?? null + let parasiteIndex = parasiteUtilities.indexOf(className) + + if (order === null && parasiteIndex !== -1) { + // This will make sure that it is at the very beginning of the + // `components` layer which technically means 'before any + // components'. + order = BigInt(parasiteIndex) + } + + return [className, order] + }) + } + + // Generate a list of strings for autocompletion purposes, e.g. + // ['uppercase', 'lowercase', ...] + context.getClassList = function getClassList(options = {}) { + let output = [] + + for (let util of classList) { + if (Array.isArray(util)) { + let [utilName, utilOptions] = util + let negativeClasses = [] + + let modifiers = Object.keys(utilOptions?.modifiers ?? {}) + + if (utilOptions?.types?.some(({ type }) => type === 'color')) { + modifiers.push(...Object.keys(context.tailwindConfig.theme.opacity ?? {})) + } + + let metadata = { modifiers } + let includeMetadata = options.includeMetadata && modifiers.length > 0 + + for (let [key, value] of Object.entries(utilOptions?.values ?? {})) { + // Ignore undefined and null values + if (value == null) { + continue + } + + let cls = formatClass(utilName, key) + output.push(includeMetadata ? [cls, metadata] : cls) + + if (utilOptions?.supportsNegativeValues && negateValue(value)) { + let cls = formatClass(utilName, `-${key}`) + negativeClasses.push(includeMetadata ? [cls, metadata] : cls) + } + } + + output.push(...negativeClasses) + } else { + output.push(util) + } + } + + return output + } + + // Generate a list of available variants with meta information of the type of variant. + context.getVariants = function getVariants() { + let result = [] + for (let [name, options] of context.variantOptions.entries()) { + if (options.variantInfo === VARIANT_INFO.Base) continue + + result.push({ + name, + isArbitrary: options.type === Symbol.for('MATCH_VARIANT'), + values: Object.keys(options.values ?? {}), + hasDash: name !== '@', + selectors({ modifier, value } = {}) { + let candidate = '__TAILWIND_PLACEHOLDER__' + + let rule = postcss.rule({ selector: `.${candidate}` }) + let container = postcss.root({ nodes: [rule.clone()] }) + + let before = container.toString() + + let fns = (context.variantMap.get(name) ?? []).flatMap(([_, fn]) => fn) + let formatStrings = [] + for (let fn of fns) { + let localFormatStrings = [] + + let api = { + args: { modifier, value: options.values?.[value] ?? value }, + separator: context.tailwindConfig.separator, + modifySelectors(modifierFunction) { + // Run the modifierFunction over each rule + container.each((rule) => { + if (rule.type !== 'rule') { + return + } + + rule.selectors = rule.selectors.map((selector) => { + return modifierFunction({ + get className() { + return getClassNameFromSelector(selector) + }, + selector, + }) + }) + }) + + return container + }, + format(str) { + localFormatStrings.push(str) + }, + wrap(wrapper) { + localFormatStrings.push(`@${wrapper.name} ${wrapper.params} { & }`) + }, + container, + } + + let ruleWithVariant = fn(api) + if (localFormatStrings.length > 0) { + formatStrings.push(localFormatStrings) + } + + if (Array.isArray(ruleWithVariant)) { + for (let variantFunction of ruleWithVariant) { + localFormatStrings = [] + variantFunction(api) + formatStrings.push(localFormatStrings) + } + } + } + + // Reverse engineer the result of the `container` + let manualFormatStrings = [] + let after = container.toString() + + if (before !== after) { + // Figure out all selectors + container.walkRules((rule) => { + let modified = rule.selector + + // Rebuild the base selector, this is what plugin authors would do + // as well. E.g.: `${variant}${separator}${className}`. + // However, plugin authors probably also prepend or append certain + // classes, pseudos, ids, ... + let rebuiltBase = selectorParser((selectors) => { + selectors.walkClasses((classNode) => { + classNode.value = `${name}${context.tailwindConfig.separator}${classNode.value}` + }) + }).processSync(modified) + + // Now that we know the original selector, the new selector, and + // the rebuild part in between, we can replace the part that plugin + // authors need to rebuild with `&`, and eventually store it in the + // collectedFormats. Similar to what `format('...')` would do. + // + // E.g.: + // variant: foo + // selector: .markdown > p + // modified (by plugin): .foo .foo\\:markdown > p + // rebuiltBase (internal): .foo\\:markdown > p + // format: .foo & + manualFormatStrings.push(modified.replace(rebuiltBase, '&').replace(candidate, '&')) + }) + + // Figure out all atrules + container.walkAtRules((atrule) => { + manualFormatStrings.push(`@${atrule.name} (${atrule.params}) { & }`) + }) + } + + let isArbitraryVariant = !(value in (options.values ?? {})) + let internalFeatures = options[INTERNAL_FEATURES] ?? {} + + let respectPrefix = (() => { + if (isArbitraryVariant) return false + if (internalFeatures.respectPrefix === false) return false + return true + })() + + formatStrings = formatStrings.map((format) => + format.map((str) => ({ + format: str, + respectPrefix, + })) + ) + + manualFormatStrings = manualFormatStrings.map((format) => ({ + format, + respectPrefix, + })) + + let opts = { + candidate, + context, + } + + let result = formatStrings.map((formats) => + finalizeSelector(`.${candidate}`, formatVariantSelector(formats, opts), opts) + .replace(`.${candidate}`, '&') + .replace('{ & }', '') + .trim() + ) + + if (manualFormatStrings.length > 0) { + result.push( + formatVariantSelector(manualFormatStrings, opts) + .toString() + .replace(`.${candidate}`, '&') + ) + } + + return result + }, + }) + } + + return result + } +} + +/** + * Mark as class as retroactively invalid + * + * + * @param {string} candidate + */ +function markInvalidUtilityCandidate(context, candidate) { + if (!context.classCache.has(candidate)) { + return + } + + // Mark this as not being a real utility + context.notClassCache.add(candidate) + + // Remove it from any candidate-specific caches + context.classCache.delete(candidate) + context.applyClassCache.delete(candidate) + context.candidateRuleMap.delete(candidate) + context.candidateRuleCache.delete(candidate) + + // Ensure the stylesheet gets rebuilt + context.stylesheetCache = null +} + +/** + * Mark as class as retroactively invalid + * + * @param {import('postcss').Node} node + */ +function markInvalidUtilityNode(context, node) { + let candidate = node.raws.tailwind.candidate + + if (!candidate) { + return + } + + for (const entry of context.ruleCache) { + if (entry[1].raws.tailwind.candidate === candidate) { + context.ruleCache.delete(entry) + // context.postCssNodeCache.delete(node) + } + } + + markInvalidUtilityCandidate(context, candidate) +} + +export function createContext(tailwindConfig, changedContent = [], root = postcss.root()) { + let context = { + disposables: [], + ruleCache: new Set(), + candidateRuleCache: new Map(), + classCache: new Map(), + applyClassCache: new Map(), + // Seed the not class cache with the blocklist (which is only strings) + notClassCache: new Set(tailwindConfig.blocklist ?? []), + postCssNodeCache: new Map(), + candidateRuleMap: new Map(), + tailwindConfig, + changedContent: changedContent, + variantMap: new Map(), + stylesheetCache: null, + variantOptions: new Map(), + + markInvalidUtilityCandidate: (candidate) => markInvalidUtilityCandidate(context, candidate), + markInvalidUtilityNode: (node) => markInvalidUtilityNode(context, node), + } + + let resolvedPlugins = resolvePlugins(context, root) + registerPlugins(resolvedPlugins, context) + + return context +} + +let contextMap = sharedState.contextMap +let configContextMap = sharedState.configContextMap +let contextSourcesMap = sharedState.contextSourcesMap + +export function getContext( + root, + result, + tailwindConfig, + userConfigPath, + tailwindConfigHash, + contextDependencies +) { + let sourcePath = result.opts.from + let isConfigFile = userConfigPath !== null + + env.DEBUG && console.log('Source path:', sourcePath) + + let existingContext + + if (isConfigFile && contextMap.has(sourcePath)) { + existingContext = contextMap.get(sourcePath) + } else if (configContextMap.has(tailwindConfigHash)) { + let context = configContextMap.get(tailwindConfigHash) + contextSourcesMap.get(context).add(sourcePath) + contextMap.set(sourcePath, context) + + existingContext = context + } + + let cssDidChange = hasContentChanged(sourcePath, root) + + // If there's already a context in the cache and we don't need to + // reset the context, return the cached context. + if (existingContext) { + let [contextDependenciesChanged, mtimesToCommit] = trackModified( + [...contextDependencies], + getFileModifiedMap(existingContext) + ) + if (!contextDependenciesChanged && !cssDidChange) { + return [existingContext, false, mtimesToCommit] + } + } + + // If this source is in the context map, get the old context. + // Remove this source from the context sources for the old context, + // and clean up that context if no one else is using it. This can be + // called by many processes in rapid succession, so we check for presence + // first because the first process to run this code will wipe it out first. + if (contextMap.has(sourcePath)) { + let oldContext = contextMap.get(sourcePath) + if (contextSourcesMap.has(oldContext)) { + contextSourcesMap.get(oldContext).delete(sourcePath) + if (contextSourcesMap.get(oldContext).size === 0) { + contextSourcesMap.delete(oldContext) + for (let [tailwindConfigHash, context] of configContextMap) { + if (context === oldContext) { + configContextMap.delete(tailwindConfigHash) + } + } + for (let disposable of oldContext.disposables.splice(0)) { + disposable(oldContext) + } + } + } + } + + env.DEBUG && console.log('Setting up new context...') + + let context = createContext(tailwindConfig, [], root) + + Object.assign(context, { + userConfigPath, + }) + + let [, mtimesToCommit] = trackModified([...contextDependencies], getFileModifiedMap(context)) + + // --- + + // Update all context tracking state + + configContextMap.set(tailwindConfigHash, context) + contextMap.set(sourcePath, context) + + if (!contextSourcesMap.has(context)) { + contextSourcesMap.set(context, new Set()) + } + + contextSourcesMap.get(context).add(sourcePath) + + return [context, true, mtimesToCommit] +} diff --git a/node_modules/tailwindcss/src/lib/setupTrackingContext.js b/node_modules/tailwindcss/src/lib/setupTrackingContext.js new file mode 100644 index 0000000..70e7cb6 --- /dev/null +++ b/node_modules/tailwindcss/src/lib/setupTrackingContext.js @@ -0,0 +1,169 @@ +// @ts-check + +import fs from 'fs' +import LRU from '@alloc/quick-lru' + +import hash from '../util/hashConfig' +import resolveConfig from '../public/resolve-config' +import resolveConfigPath from '../util/resolveConfigPath' +import { getContext, getFileModifiedMap } from './setupContextUtils' +import parseDependency from '../util/parseDependency' +import { validateConfig } from '../util/validateConfig.js' +import { parseCandidateFiles, resolvedChangedContent } from './content.js' +import { loadConfig } from '../lib/load-config' +import getModuleDependencies from './getModuleDependencies' + +let configPathCache = new LRU({ maxSize: 100 }) + +let candidateFilesCache = new WeakMap() + +function getCandidateFiles(context, tailwindConfig) { + if (candidateFilesCache.has(context)) { + return candidateFilesCache.get(context) + } + + let candidateFiles = parseCandidateFiles(context, tailwindConfig) + + return candidateFilesCache.set(context, candidateFiles).get(context) +} + +// Get the config object based on a path +function getTailwindConfig(configOrPath) { + let userConfigPath = resolveConfigPath(configOrPath) + + if (userConfigPath !== null) { + let [prevConfig, prevConfigHash, prevDeps, prevModified] = + configPathCache.get(userConfigPath) || [] + + let newDeps = getModuleDependencies(userConfigPath) + + let modified = false + let newModified = new Map() + for (let file of newDeps) { + let time = fs.statSync(file).mtimeMs + newModified.set(file, time) + if (!prevModified || !prevModified.has(file) || time > prevModified.get(file)) { + modified = true + } + } + + // It hasn't changed (based on timestamps) + if (!modified) { + return [prevConfig, userConfigPath, prevConfigHash, prevDeps] + } + + // It has changed (based on timestamps), or first run + for (let file of newDeps) { + delete require.cache[file] + } + let newConfig = validateConfig(resolveConfig(loadConfig(userConfigPath))) + let newHash = hash(newConfig) + configPathCache.set(userConfigPath, [newConfig, newHash, newDeps, newModified]) + return [newConfig, userConfigPath, newHash, newDeps] + } + + // It's a plain object, not a path + let newConfig = resolveConfig(configOrPath?.config ?? configOrPath ?? {}) + + newConfig = validateConfig(newConfig) + + return [newConfig, null, hash(newConfig), []] +} + +// DISABLE_TOUCH = TRUE + +// Retrieve an existing context from cache if possible (since contexts are unique per +// source path), or set up a new one (including setting up watchers and registering +// plugins) then return it +export default function setupTrackingContext(configOrPath) { + return ({ tailwindDirectives, registerDependency }) => { + return (root, result) => { + let [tailwindConfig, userConfigPath, tailwindConfigHash, configDependencies] = + getTailwindConfig(configOrPath) + + let contextDependencies = new Set(configDependencies) + + // If there are no @tailwind or @apply rules, we don't consider this CSS + // file or its dependencies to be dependencies of the context. Can reuse + // the context even if they change. We may want to think about `@layer` + // being part of this trigger too, but it's tough because it's impossible + // for a layer in one file to end up in the actual @tailwind rule in + // another file since independent sources are effectively isolated. + if (tailwindDirectives.size > 0) { + // Add current css file as a context dependencies. + contextDependencies.add(result.opts.from) + + // Add all css @import dependencies as context dependencies. + for (let message of result.messages) { + if (message.type === 'dependency') { + contextDependencies.add(message.file) + } + } + } + + let [context, , mTimesToCommit] = getContext( + root, + result, + tailwindConfig, + userConfigPath, + tailwindConfigHash, + contextDependencies + ) + + let fileModifiedMap = getFileModifiedMap(context) + + let candidateFiles = getCandidateFiles(context, tailwindConfig) + + // If there are no @tailwind or @apply rules, we don't consider this CSS file or it's + // dependencies to be dependencies of the context. Can reuse the context even if they change. + // We may want to think about `@layer` being part of this trigger too, but it's tough + // because it's impossible for a layer in one file to end up in the actual @tailwind rule + // in another file since independent sources are effectively isolated. + if (tailwindDirectives.size > 0) { + // Add template paths as postcss dependencies. + for (let contentPath of candidateFiles) { + for (let dependency of parseDependency(contentPath)) { + registerDependency(dependency) + } + } + + let [changedContent, contentMTimesToCommit] = resolvedChangedContent( + context, + candidateFiles, + fileModifiedMap + ) + + for (let content of changedContent) { + context.changedContent.push(content) + } + + // Add the mtimes of the content files to the commit list + // We can overwrite the existing values because unconditionally + // This is because: + // 1. Most of the files here won't be in the map yet + // 2. If they are that means it's a context dependency + // and we're reading this after the context. This means + // that the mtime we just read is strictly >= the context + // mtime. Unless the user / os is doing something weird + // in which the mtime would be going backwards. If that + // happens there's already going to be problems. + for (let [path, mtime] of contentMTimesToCommit.entries()) { + mTimesToCommit.set(path, mtime) + } + } + + for (let file of configDependencies) { + registerDependency({ type: 'dependency', file }) + } + + // "commit" the new modified time for all context deps + // We do this here because we want content tracking to + // read the "old" mtime even when it's a context dependency. + for (let [path, mtime] of mTimesToCommit.entries()) { + fileModifiedMap.set(path, mtime) + } + + return context + } + } +} diff --git a/node_modules/tailwindcss/src/lib/sharedState.js b/node_modules/tailwindcss/src/lib/sharedState.js new file mode 100644 index 0000000..97bdf09 --- /dev/null +++ b/node_modules/tailwindcss/src/lib/sharedState.js @@ -0,0 +1,61 @@ +import pkg from '../../package.json' + +export const env = + typeof process !== 'undefined' + ? { + NODE_ENV: process.env.NODE_ENV, + DEBUG: resolveDebug(process.env.DEBUG), + ENGINE: pkg.tailwindcss.engine, + } + : { + NODE_ENV: 'production', + DEBUG: false, + ENGINE: pkg.tailwindcss.engine, + } + +export const contextMap = new Map() +export const configContextMap = new Map() +export const contextSourcesMap = new Map() +export const sourceHashMap = new Map() +export const NOT_ON_DEMAND = new String('*') + +export const NONE = Symbol('__NONE__') + +export function resolveDebug(debug) { + if (debug === undefined) { + return false + } + + // Environment variables are strings, so convert to boolean + if (debug === 'true' || debug === '1') { + return true + } + + if (debug === 'false' || debug === '0') { + return false + } + + // Keep the debug convention into account: + // DEBUG=* -> This enables all debug modes + // DEBUG=projectA,projectB,projectC -> This enables debug for projectA, projectB and projectC + // DEBUG=projectA:* -> This enables all debug modes for projectA (if you have sub-types) + // DEBUG=projectA,-projectB -> This enables debug for projectA and explicitly disables it for projectB + + if (debug === '*') { + return true + } + + let debuggers = debug.split(',').map((d) => d.split(':')[0]) + + // Ignoring tailwindcss + if (debuggers.includes('-tailwindcss')) { + return false + } + + // Including tailwindcss + if (debuggers.includes('tailwindcss')) { + return true + } + + return false +} diff --git a/node_modules/tailwindcss/src/lib/substituteScreenAtRules.js b/node_modules/tailwindcss/src/lib/substituteScreenAtRules.js new file mode 100644 index 0000000..5a45cff --- /dev/null +++ b/node_modules/tailwindcss/src/lib/substituteScreenAtRules.js @@ -0,0 +1,19 @@ +import { normalizeScreens } from '../util/normalizeScreens' +import buildMediaQuery from '../util/buildMediaQuery' + +export default function ({ tailwindConfig: { theme } }) { + return function (css) { + css.walkAtRules('screen', (atRule) => { + let screen = atRule.params + let screens = normalizeScreens(theme.screens) + let screenDefinition = screens.find(({ name }) => name === screen) + + if (!screenDefinition) { + throw atRule.error(`No \`${screen}\` screen found.`) + } + + atRule.name = 'media' + atRule.params = buildMediaQuery(screenDefinition) + }) + } +} |