feat: implement build system and development workflow
- Add npm scripts for CSS/JS compilation (build:css, build:js, build) - Create PowerShell build automation script - Document development workflow in README - Add troubleshooting guide for build issues - Specify proper file structure and compilation process Supports Tailwind CSS v3.4.0 and esbuild bundling with source maps.
This commit is contained in:
324
node_modules/tailwindcss/src/util/formatVariantSelector.js
generated
vendored
Normal file
324
node_modules/tailwindcss/src/util/formatVariantSelector.js
generated
vendored
Normal file
@@ -0,0 +1,324 @@
|
||||
import selectorParser from 'postcss-selector-parser'
|
||||
import unescape from 'postcss-selector-parser/dist/util/unesc'
|
||||
import escapeClassName from '../util/escapeClassName'
|
||||
import prefixSelector from '../util/prefixSelector'
|
||||
import { movePseudos } from './pseudoElements'
|
||||
import { splitAtTopLevelOnly } from './splitAtTopLevelOnly'
|
||||
|
||||
/** @typedef {import('postcss-selector-parser').Root} Root */
|
||||
/** @typedef {import('postcss-selector-parser').Selector} Selector */
|
||||
/** @typedef {import('postcss-selector-parser').Pseudo} Pseudo */
|
||||
/** @typedef {import('postcss-selector-parser').Node} Node */
|
||||
|
||||
/** @typedef {{format: string, respectPrefix: boolean}[]} RawFormats */
|
||||
/** @typedef {import('postcss-selector-parser').Root} ParsedFormats */
|
||||
/** @typedef {RawFormats | ParsedFormats} AcceptedFormats */
|
||||
|
||||
let MERGE = ':merge'
|
||||
|
||||
/**
|
||||
* @param {RawFormats} formats
|
||||
* @param {{context: any, candidate: string, base: string | null}} options
|
||||
* @returns {ParsedFormats | null}
|
||||
*/
|
||||
export function formatVariantSelector(formats, { context, candidate }) {
|
||||
let prefix = context?.tailwindConfig.prefix ?? ''
|
||||
|
||||
// Parse the format selector into an AST
|
||||
let parsedFormats = formats.map((format) => {
|
||||
let ast = selectorParser().astSync(format.format)
|
||||
|
||||
return {
|
||||
...format,
|
||||
ast: format.respectPrefix ? prefixSelector(prefix, ast) : ast,
|
||||
}
|
||||
})
|
||||
|
||||
// We start with the candidate selector
|
||||
let formatAst = selectorParser.root({
|
||||
nodes: [
|
||||
selectorParser.selector({
|
||||
nodes: [selectorParser.className({ value: escapeClassName(candidate) })],
|
||||
}),
|
||||
],
|
||||
})
|
||||
|
||||
// And iteratively merge each format selector into the candidate selector
|
||||
for (let { ast } of parsedFormats) {
|
||||
// 1. Handle :merge() special pseudo-class
|
||||
;[formatAst, ast] = handleMergePseudo(formatAst, ast)
|
||||
|
||||
// 2. Merge the format selector into the current selector AST
|
||||
ast.walkNesting((nesting) => nesting.replaceWith(...formatAst.nodes[0].nodes))
|
||||
|
||||
// 3. Keep going!
|
||||
formatAst = ast
|
||||
}
|
||||
|
||||
return formatAst
|
||||
}
|
||||
|
||||
/**
|
||||
* Given any node in a selector this gets the "simple" selector it's a part of
|
||||
* A simple selector is just a list of nodes without any combinators
|
||||
* Technically :is(), :not(), :has(), etc… can have combinators but those are nested
|
||||
* inside the relevant node and won't be picked up so they're fine to ignore
|
||||
*
|
||||
* @param {Node} node
|
||||
* @returns {Node[]}
|
||||
**/
|
||||
function simpleSelectorForNode(node) {
|
||||
/** @type {Node[]} */
|
||||
let nodes = []
|
||||
|
||||
// Walk backwards until we hit a combinator node (or the start)
|
||||
while (node.prev() && node.prev().type !== 'combinator') {
|
||||
node = node.prev()
|
||||
}
|
||||
|
||||
// Now record all non-combinator nodes until we hit one (or the end)
|
||||
while (node && node.type !== 'combinator') {
|
||||
nodes.push(node)
|
||||
node = node.next()
|
||||
}
|
||||
|
||||
return nodes
|
||||
}
|
||||
|
||||
/**
|
||||
* Resorts the nodes in a selector to ensure they're in the correct order
|
||||
* Tags go before classes, and pseudo classes go after classes
|
||||
*
|
||||
* @param {Selector} sel
|
||||
* @returns {Selector}
|
||||
**/
|
||||
function resortSelector(sel) {
|
||||
sel.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 sel.index(a) - sel.index(b)
|
||||
})
|
||||
|
||||
return sel
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove extraneous selectors that do not include the base class/candidate
|
||||
*
|
||||
* Example:
|
||||
* Given the utility `.a, .b { color: red}`
|
||||
* Given the candidate `sm:b`
|
||||
*
|
||||
* The final selector should be `.sm\:b` and not `.a, .sm\:b`
|
||||
*
|
||||
* @param {Selector} ast
|
||||
* @param {string} base
|
||||
*/
|
||||
export function eliminateIrrelevantSelectors(sel, base) {
|
||||
let hasClassesMatchingCandidate = false
|
||||
|
||||
sel.walk((child) => {
|
||||
if (child.type === 'class' && child.value === base) {
|
||||
hasClassesMatchingCandidate = true
|
||||
return false // Stop walking
|
||||
}
|
||||
})
|
||||
|
||||
if (!hasClassesMatchingCandidate) {
|
||||
sel.remove()
|
||||
}
|
||||
|
||||
// We do NOT recursively eliminate sub selectors that don't have the base class
|
||||
// as this is NOT a safe operation. For example, if we have:
|
||||
// `.space-x-2 > :not([hidden]) ~ :not([hidden])`
|
||||
// We cannot remove the [hidden] from the :not() because it would change the
|
||||
// meaning of the selector.
|
||||
|
||||
// TODO: Can we do this for :matches, :is, and :where?
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} current
|
||||
* @param {AcceptedFormats} formats
|
||||
* @param {{context: any, candidate: string, base: string | null}} options
|
||||
* @returns {string}
|
||||
*/
|
||||
export function finalizeSelector(current, formats, { context, candidate, base }) {
|
||||
let separator = context?.tailwindConfig?.separator ?? ':'
|
||||
|
||||
// Split by the separator, but ignore the separator inside square brackets:
|
||||
//
|
||||
// E.g.: dark:lg:hover:[paint-order:markers]
|
||||
// ┬ ┬ ┬ ┬
|
||||
// │ │ │ ╰── We will not split here
|
||||
// ╰──┴─────┴─────────────── We will split here
|
||||
//
|
||||
base = base ?? splitAtTopLevelOnly(candidate, separator).pop()
|
||||
|
||||
// Parse the selector into an AST
|
||||
let selector = selectorParser().astSync(current)
|
||||
|
||||
// Normalize escaped classes, e.g.:
|
||||
//
|
||||
// The idea would be to replace the escaped `base` in the selector with the
|
||||
// `format`. However, in css you can escape the same selector in a few
|
||||
// different ways. This would result in different strings and therefore we
|
||||
// can't replace it properly.
|
||||
//
|
||||
// base: bg-[rgb(255,0,0)]
|
||||
// base in selector: bg-\\[rgb\\(255\\,0\\,0\\)\\]
|
||||
// escaped base: bg-\\[rgb\\(255\\2c 0\\2c 0\\)\\]
|
||||
//
|
||||
selector.walkClasses((node) => {
|
||||
if (node.raws && node.value.includes(base)) {
|
||||
node.raws.value = escapeClassName(unescape(node.raws.value))
|
||||
}
|
||||
})
|
||||
|
||||
// Remove extraneous selectors that do not include the base candidate
|
||||
selector.each((sel) => eliminateIrrelevantSelectors(sel, base))
|
||||
|
||||
// If ffter eliminating irrelevant selectors, we end up with nothing
|
||||
// Then the whole "rule" this is associated with does not need to exist
|
||||
// We use `null` as a marker value for that case
|
||||
if (selector.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
// If there are no formats that means there were no variants added to the candidate
|
||||
// so we can just return the selector as-is
|
||||
let formatAst = Array.isArray(formats)
|
||||
? formatVariantSelector(formats, { context, candidate })
|
||||
: formats
|
||||
|
||||
if (formatAst === null) {
|
||||
return selector.toString()
|
||||
}
|
||||
|
||||
let simpleStart = selectorParser.comment({ value: '/*__simple__*/' })
|
||||
let simpleEnd = selectorParser.comment({ value: '/*__simple__*/' })
|
||||
|
||||
// We can safely replace the escaped base now, since the `base` section is
|
||||
// now in a normalized escaped value.
|
||||
selector.walkClasses((node) => {
|
||||
if (node.value !== base) {
|
||||
return
|
||||
}
|
||||
|
||||
let parent = node.parent
|
||||
let formatNodes = formatAst.nodes[0].nodes
|
||||
|
||||
// Perf optimization: if the parent is a single class we can just replace it and be done
|
||||
if (parent.nodes.length === 1) {
|
||||
node.replaceWith(...formatNodes)
|
||||
return
|
||||
}
|
||||
|
||||
let simpleSelector = simpleSelectorForNode(node)
|
||||
parent.insertBefore(simpleSelector[0], simpleStart)
|
||||
parent.insertAfter(simpleSelector[simpleSelector.length - 1], simpleEnd)
|
||||
|
||||
for (let child of formatNodes) {
|
||||
parent.insertBefore(simpleSelector[0], child.clone())
|
||||
}
|
||||
|
||||
node.remove()
|
||||
|
||||
// Re-sort the simple selector to ensure it's in the correct order
|
||||
simpleSelector = simpleSelectorForNode(simpleStart)
|
||||
let firstNode = parent.index(simpleStart)
|
||||
|
||||
parent.nodes.splice(
|
||||
firstNode,
|
||||
simpleSelector.length,
|
||||
...resortSelector(selectorParser.selector({ nodes: simpleSelector })).nodes
|
||||
)
|
||||
|
||||
simpleStart.remove()
|
||||
simpleEnd.remove()
|
||||
})
|
||||
|
||||
// Remove unnecessary pseudo selectors that we used as placeholders
|
||||
selector.walkPseudos((p) => {
|
||||
if (p.value === MERGE) {
|
||||
p.replaceWith(p.nodes)
|
||||
}
|
||||
})
|
||||
|
||||
// Move pseudo elements to the end of the selector (if necessary)
|
||||
selector.each((sel) => movePseudos(sel))
|
||||
|
||||
return selector.toString()
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {Selector} selector
|
||||
* @param {Selector} format
|
||||
*/
|
||||
export function handleMergePseudo(selector, format) {
|
||||
/** @type {{pseudo: Pseudo, value: string}[]} */
|
||||
let merges = []
|
||||
|
||||
// Find all :merge() pseudo-classes in `selector`
|
||||
selector.walkPseudos((pseudo) => {
|
||||
if (pseudo.value === MERGE) {
|
||||
merges.push({
|
||||
pseudo,
|
||||
value: pseudo.nodes[0].toString(),
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// Find all :merge() "attachments" in `format` and attach them to the matching selector in `selector`
|
||||
format.walkPseudos((pseudo) => {
|
||||
if (pseudo.value !== MERGE) {
|
||||
return
|
||||
}
|
||||
|
||||
let value = pseudo.nodes[0].toString()
|
||||
|
||||
// Does `selector` contain a :merge() pseudo-class with the same value?
|
||||
let existing = merges.find((merge) => merge.value === value)
|
||||
|
||||
// Nope so there's nothing to do
|
||||
if (!existing) {
|
||||
return
|
||||
}
|
||||
|
||||
// Everything after `:merge()` up to the next combinator is what is attached to the merged selector
|
||||
let attachments = []
|
||||
let next = pseudo.next()
|
||||
while (next && next.type !== 'combinator') {
|
||||
attachments.push(next)
|
||||
next = next.next()
|
||||
}
|
||||
|
||||
let combinator = next
|
||||
|
||||
existing.pseudo.parent.insertAfter(
|
||||
existing.pseudo,
|
||||
selectorParser.selector({ nodes: attachments.map((node) => node.clone()) })
|
||||
)
|
||||
|
||||
pseudo.remove()
|
||||
attachments.forEach((node) => node.remove())
|
||||
|
||||
// What about this case:
|
||||
// :merge(.group):focus > &
|
||||
// :merge(.group):hover &
|
||||
if (combinator && combinator.type === 'combinator') {
|
||||
combinator.remove()
|
||||
}
|
||||
})
|
||||
|
||||
return [selector, format]
|
||||
}
|
||||
Reference in New Issue
Block a user