first version
This commit is contained in:
+15
@@ -0,0 +1,15 @@
|
||||
export function lazyPostcss() {
|
||||
return require('postcss')
|
||||
}
|
||||
|
||||
export function lazyPostcssImport() {
|
||||
return require('postcss-import')
|
||||
}
|
||||
|
||||
export function lazyAutoprefixer() {
|
||||
return require('autoprefixer')
|
||||
}
|
||||
|
||||
export function lazyCssnano() {
|
||||
return require('cssnano')
|
||||
}
|
||||
+915
@@ -0,0 +1,915 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { lazyPostcss, lazyPostcssImport, lazyCssnano, lazyAutoprefixer } from '../peers/index.js'
|
||||
|
||||
import chokidar from 'chokidar'
|
||||
import path from 'path'
|
||||
import arg from 'arg'
|
||||
import fs from 'fs'
|
||||
import postcssrc from 'postcss-load-config'
|
||||
import { lilconfig } from 'lilconfig'
|
||||
import loadPlugins from 'postcss-load-config/src/plugins' // Little bit scary, looking at private/internal API
|
||||
import loadOptions from 'postcss-load-config/src/options' // Little bit scary, looking at private/internal API
|
||||
import tailwind from './processTailwindFeatures'
|
||||
import resolveConfigInternal from '../resolveConfig'
|
||||
import fastGlob from 'fast-glob'
|
||||
import getModuleDependencies from './lib/getModuleDependencies'
|
||||
import log from './util/log'
|
||||
import packageJson from '../package.json'
|
||||
import normalizePath from 'normalize-path'
|
||||
import { validateConfig } from './util/validateConfig.js'
|
||||
|
||||
let env = {
|
||||
DEBUG: process.env.DEBUG !== undefined && process.env.DEBUG !== '0',
|
||||
}
|
||||
|
||||
function isESM() {
|
||||
const pkgPath = path.resolve('./package.json')
|
||||
|
||||
try {
|
||||
let pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'))
|
||||
return pkg.type && pkg.type === 'module'
|
||||
} catch (err) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
let configs = isESM()
|
||||
? {
|
||||
tailwind: 'tailwind.config.cjs',
|
||||
postcss: 'postcss.config.cjs',
|
||||
}
|
||||
: {
|
||||
tailwind: 'tailwind.config.js',
|
||||
postcss: 'postcss.config.js',
|
||||
}
|
||||
|
||||
// ---
|
||||
|
||||
function indentRecursive(node, indent = 0) {
|
||||
node.each &&
|
||||
node.each((child, i) => {
|
||||
if (!child.raws.before || !child.raws.before.trim() || child.raws.before.includes('\n')) {
|
||||
child.raws.before = `\n${node.type !== 'rule' && i > 0 ? '\n' : ''}${' '.repeat(indent)}`
|
||||
}
|
||||
child.raws.after = `\n${' '.repeat(indent)}`
|
||||
indentRecursive(child, indent + 1)
|
||||
})
|
||||
}
|
||||
|
||||
function formatNodes(root) {
|
||||
indentRecursive(root)
|
||||
if (root.first) {
|
||||
root.first.raws.before = ''
|
||||
}
|
||||
}
|
||||
|
||||
async function outputFile(file, contents) {
|
||||
if (fs.existsSync(file) && (await fs.promises.readFile(file, 'utf8')) === contents) {
|
||||
return // Skip writing the file
|
||||
}
|
||||
|
||||
// Write the file
|
||||
await fs.promises.writeFile(file, contents, 'utf8')
|
||||
}
|
||||
|
||||
function drainStdin() {
|
||||
return new Promise((resolve, reject) => {
|
||||
let result = ''
|
||||
process.stdin.on('data', (chunk) => {
|
||||
result += chunk
|
||||
})
|
||||
process.stdin.on('end', () => resolve(result))
|
||||
process.stdin.on('error', (err) => reject(err))
|
||||
})
|
||||
}
|
||||
|
||||
function help({ message, usage, commands, options }) {
|
||||
let indent = 2
|
||||
|
||||
// Render header
|
||||
console.log()
|
||||
console.log(`${packageJson.name} v${packageJson.version}`)
|
||||
|
||||
// Render message
|
||||
if (message) {
|
||||
console.log()
|
||||
for (let msg of message.split('\n')) {
|
||||
console.log(msg)
|
||||
}
|
||||
}
|
||||
|
||||
// Render usage
|
||||
if (usage && usage.length > 0) {
|
||||
console.log()
|
||||
console.log('Usage:')
|
||||
for (let example of usage) {
|
||||
console.log(' '.repeat(indent), example)
|
||||
}
|
||||
}
|
||||
|
||||
// Render commands
|
||||
if (commands && commands.length > 0) {
|
||||
console.log()
|
||||
console.log('Commands:')
|
||||
for (let command of commands) {
|
||||
console.log(' '.repeat(indent), command)
|
||||
}
|
||||
}
|
||||
|
||||
// Render options
|
||||
if (options) {
|
||||
let groupedOptions = {}
|
||||
for (let [key, value] of Object.entries(options)) {
|
||||
if (typeof value === 'object') {
|
||||
groupedOptions[key] = { ...value, flags: [key] }
|
||||
} else {
|
||||
groupedOptions[value].flags.push(key)
|
||||
}
|
||||
}
|
||||
|
||||
console.log()
|
||||
console.log('Options:')
|
||||
for (let { flags, description, deprecated } of Object.values(groupedOptions)) {
|
||||
if (deprecated) continue
|
||||
|
||||
if (flags.length === 1) {
|
||||
console.log(
|
||||
' '.repeat(indent + 4 /* 4 = "-i, ".length */),
|
||||
flags.slice().reverse().join(', ').padEnd(20, ' '),
|
||||
description
|
||||
)
|
||||
} else {
|
||||
console.log(
|
||||
' '.repeat(indent),
|
||||
flags.slice().reverse().join(', ').padEnd(24, ' '),
|
||||
description
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log()
|
||||
}
|
||||
|
||||
function oneOf(...options) {
|
||||
return Object.assign(
|
||||
(value = true) => {
|
||||
for (let option of options) {
|
||||
let parsed = option(value)
|
||||
if (parsed === value) {
|
||||
return parsed
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error('...')
|
||||
},
|
||||
{ manualParsing: true }
|
||||
)
|
||||
}
|
||||
|
||||
function loadPostcss() {
|
||||
// Try to load a local `postcss` version first
|
||||
try {
|
||||
return require('postcss')
|
||||
} catch {}
|
||||
|
||||
return lazyPostcss()
|
||||
}
|
||||
|
||||
let commands = {
|
||||
init: {
|
||||
run: init,
|
||||
args: {
|
||||
'--full': { type: Boolean, description: `Initialize a full \`${configs.tailwind}\` file` },
|
||||
'--postcss': { type: Boolean, description: `Initialize a \`${configs.postcss}\` file` },
|
||||
'-f': '--full',
|
||||
'-p': '--postcss',
|
||||
},
|
||||
},
|
||||
build: {
|
||||
run: build,
|
||||
args: {
|
||||
'--input': { type: String, description: 'Input file' },
|
||||
'--output': { type: String, description: 'Output file' },
|
||||
'--watch': { type: Boolean, description: 'Watch for changes and rebuild as needed' },
|
||||
'--poll': {
|
||||
type: Boolean,
|
||||
description: 'Use polling instead of filesystem events when watching',
|
||||
},
|
||||
'--content': {
|
||||
type: String,
|
||||
description: 'Content paths to use for removing unused classes',
|
||||
},
|
||||
'--purge': {
|
||||
type: String,
|
||||
deprecated: true,
|
||||
},
|
||||
'--postcss': {
|
||||
type: oneOf(String, Boolean),
|
||||
description: 'Load custom PostCSS configuration',
|
||||
},
|
||||
'--minify': { type: Boolean, description: 'Minify the output' },
|
||||
'--config': {
|
||||
type: String,
|
||||
description: 'Path to a custom config file',
|
||||
},
|
||||
'--no-autoprefixer': {
|
||||
type: Boolean,
|
||||
description: 'Disable autoprefixer',
|
||||
},
|
||||
'-c': '--config',
|
||||
'-i': '--input',
|
||||
'-o': '--output',
|
||||
'-m': '--minify',
|
||||
'-w': '--watch',
|
||||
'-p': '--poll',
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
let sharedFlags = {
|
||||
'--help': { type: Boolean, description: 'Display usage information' },
|
||||
'-h': '--help',
|
||||
}
|
||||
|
||||
if (
|
||||
process.stdout.isTTY /* Detect redirecting output to a file */ &&
|
||||
(process.argv[2] === undefined ||
|
||||
process.argv.slice(2).every((flag) => sharedFlags[flag] !== undefined))
|
||||
) {
|
||||
help({
|
||||
usage: [
|
||||
'tailwindcss [--input input.css] [--output output.css] [--watch] [options...]',
|
||||
'tailwindcss init [--full] [--postcss] [options...]',
|
||||
],
|
||||
commands: Object.keys(commands)
|
||||
.filter((command) => command !== 'build')
|
||||
.map((command) => `${command} [options]`),
|
||||
options: { ...commands.build.args, ...sharedFlags },
|
||||
})
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
let command = ((arg = '') => (arg.startsWith('-') ? undefined : arg))(process.argv[2]) || 'build'
|
||||
|
||||
if (commands[command] === undefined) {
|
||||
if (fs.existsSync(path.resolve(command))) {
|
||||
// TODO: Deprecate this in future versions
|
||||
// Check if non-existing command, might be a file.
|
||||
command = 'build'
|
||||
} else {
|
||||
help({
|
||||
message: `Invalid command: ${command}`,
|
||||
usage: ['tailwindcss <command> [options]'],
|
||||
commands: Object.keys(commands)
|
||||
.filter((command) => command !== 'build')
|
||||
.map((command) => `${command} [options]`),
|
||||
options: sharedFlags,
|
||||
})
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
// Execute command
|
||||
let { args: flags, run } = commands[command]
|
||||
let args = (() => {
|
||||
try {
|
||||
let result = arg(
|
||||
Object.fromEntries(
|
||||
Object.entries({ ...flags, ...sharedFlags })
|
||||
.filter(([_key, value]) => !value?.type?.manualParsing)
|
||||
.map(([key, value]) => [key, typeof value === 'object' ? value.type : value])
|
||||
),
|
||||
{ permissive: true }
|
||||
)
|
||||
|
||||
// Manual parsing of flags to allow for special flags like oneOf(Boolean, String)
|
||||
for (let i = result['_'].length - 1; i >= 0; --i) {
|
||||
let flag = result['_'][i]
|
||||
if (!flag.startsWith('-')) continue
|
||||
|
||||
let flagName = flag
|
||||
let handler = flags[flag]
|
||||
|
||||
// Resolve flagName & handler
|
||||
while (typeof handler === 'string') {
|
||||
flagName = handler
|
||||
handler = flags[handler]
|
||||
}
|
||||
|
||||
if (!handler) continue
|
||||
|
||||
let args = []
|
||||
let offset = i + 1
|
||||
|
||||
// Parse args for current flag
|
||||
while (result['_'][offset] && !result['_'][offset].startsWith('-')) {
|
||||
args.push(result['_'][offset++])
|
||||
}
|
||||
|
||||
// Cleanup manually parsed flags + args
|
||||
result['_'].splice(i, 1 + args.length)
|
||||
|
||||
// Set the resolved value in the `result` object
|
||||
result[flagName] = handler.type(
|
||||
args.length === 0 ? undefined : args.length === 1 ? args[0] : args,
|
||||
flagName
|
||||
)
|
||||
}
|
||||
|
||||
// Ensure that the `command` is always the first argument in the `args`.
|
||||
// This is important so that we don't have to check if a default command
|
||||
// (build) was used or not from within each plugin.
|
||||
//
|
||||
// E.g.: tailwindcss input.css -> _: ['build', 'input.css']
|
||||
// E.g.: tailwindcss build input.css -> _: ['build', 'input.css']
|
||||
if (result['_'][0] !== command) {
|
||||
result['_'].unshift(command)
|
||||
}
|
||||
|
||||
return result
|
||||
} catch (err) {
|
||||
if (err.code === 'ARG_UNKNOWN_OPTION') {
|
||||
help({
|
||||
message: err.message,
|
||||
usage: ['tailwindcss <command> [options]'],
|
||||
options: sharedFlags,
|
||||
})
|
||||
process.exit(1)
|
||||
}
|
||||
throw err
|
||||
}
|
||||
})()
|
||||
|
||||
if (args['--help']) {
|
||||
help({
|
||||
options: { ...flags, ...sharedFlags },
|
||||
usage: [`tailwindcss ${command} [options]`],
|
||||
})
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
run()
|
||||
|
||||
// ---
|
||||
|
||||
function init() {
|
||||
let messages = []
|
||||
|
||||
let tailwindConfigLocation = path.resolve(args['_'][1] ?? `./${configs.tailwind}`)
|
||||
if (fs.existsSync(tailwindConfigLocation)) {
|
||||
messages.push(`${path.basename(tailwindConfigLocation)} already exists.`)
|
||||
} else {
|
||||
let stubFile = fs.readFileSync(
|
||||
args['--full']
|
||||
? path.resolve(__dirname, '../stubs/defaultConfig.stub.js')
|
||||
: path.resolve(__dirname, '../stubs/simpleConfig.stub.js'),
|
||||
'utf8'
|
||||
)
|
||||
|
||||
// Change colors import
|
||||
stubFile = stubFile.replace('../colors', 'tailwindcss/colors')
|
||||
|
||||
fs.writeFileSync(tailwindConfigLocation, stubFile, 'utf8')
|
||||
|
||||
messages.push(`Created Tailwind CSS config file: ${path.basename(tailwindConfigLocation)}`)
|
||||
}
|
||||
|
||||
if (args['--postcss']) {
|
||||
let postcssConfigLocation = path.resolve(`./${configs.postcss}`)
|
||||
if (fs.existsSync(postcssConfigLocation)) {
|
||||
messages.push(`${path.basename(postcssConfigLocation)} already exists.`)
|
||||
} else {
|
||||
let stubFile = fs.readFileSync(
|
||||
path.resolve(__dirname, '../stubs/defaultPostCssConfig.stub.js'),
|
||||
'utf8'
|
||||
)
|
||||
|
||||
fs.writeFileSync(postcssConfigLocation, stubFile, 'utf8')
|
||||
|
||||
messages.push(`Created PostCSS config file: ${path.basename(postcssConfigLocation)}`)
|
||||
}
|
||||
}
|
||||
|
||||
if (messages.length > 0) {
|
||||
console.log()
|
||||
for (let message of messages) {
|
||||
console.log(message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function build() {
|
||||
let input = args['--input']
|
||||
let output = args['--output']
|
||||
let shouldWatch = args['--watch']
|
||||
let shouldPoll = args['--poll']
|
||||
let shouldCoalesceWriteEvents = shouldPoll || process.platform === 'win32'
|
||||
let includePostCss = args['--postcss']
|
||||
|
||||
// Polling interval in milliseconds
|
||||
// Used only when polling or coalescing add/change events on Windows
|
||||
let pollInterval = 10
|
||||
|
||||
// TODO: Deprecate this in future versions
|
||||
if (!input && args['_'][1]) {
|
||||
console.error('[deprecation] Running tailwindcss without -i, please provide an input file.')
|
||||
input = args['--input'] = args['_'][1]
|
||||
}
|
||||
|
||||
if (input && input !== '-' && !fs.existsSync((input = path.resolve(input)))) {
|
||||
console.error(`Specified input file ${args['--input']} does not exist.`)
|
||||
process.exit(9)
|
||||
}
|
||||
|
||||
if (args['--config'] && !fs.existsSync((args['--config'] = path.resolve(args['--config'])))) {
|
||||
console.error(`Specified config file ${args['--config']} does not exist.`)
|
||||
process.exit(9)
|
||||
}
|
||||
|
||||
let configPath = args['--config']
|
||||
? args['--config']
|
||||
: ((defaultPath) => (fs.existsSync(defaultPath) ? defaultPath : null))(
|
||||
path.resolve(`./${configs.tailwind}`)
|
||||
)
|
||||
|
||||
async function loadPostCssPlugins() {
|
||||
let customPostCssPath = typeof args['--postcss'] === 'string' ? args['--postcss'] : undefined
|
||||
let config = customPostCssPath
|
||||
? await (async () => {
|
||||
let file = path.resolve(customPostCssPath)
|
||||
|
||||
// Implementation, see: https://unpkg.com/browse/postcss-load-config@3.1.0/src/index.js
|
||||
let { config = {} } = await lilconfig('postcss').load(file)
|
||||
if (typeof config === 'function') {
|
||||
config = config()
|
||||
} else {
|
||||
config = Object.assign({}, config)
|
||||
}
|
||||
|
||||
if (!config.plugins) {
|
||||
config.plugins = []
|
||||
}
|
||||
|
||||
return {
|
||||
file,
|
||||
plugins: loadPlugins(config, file),
|
||||
options: loadOptions(config, file),
|
||||
}
|
||||
})()
|
||||
: await postcssrc()
|
||||
|
||||
let configPlugins = config.plugins
|
||||
|
||||
let configPluginTailwindIdx = configPlugins.findIndex((plugin) => {
|
||||
if (typeof plugin === 'function' && plugin.name === 'tailwindcss') {
|
||||
return true
|
||||
}
|
||||
|
||||
if (typeof plugin === 'object' && plugin !== null && plugin.postcssPlugin === 'tailwindcss') {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
})
|
||||
|
||||
let beforePlugins =
|
||||
configPluginTailwindIdx === -1 ? [] : configPlugins.slice(0, configPluginTailwindIdx)
|
||||
let afterPlugins =
|
||||
configPluginTailwindIdx === -1
|
||||
? configPlugins
|
||||
: configPlugins.slice(configPluginTailwindIdx + 1)
|
||||
|
||||
return [beforePlugins, afterPlugins, config.options]
|
||||
}
|
||||
|
||||
function loadBuiltinPostcssPlugins() {
|
||||
let postcss = loadPostcss()
|
||||
let IMPORT_COMMENT = '__TAILWIND_RESTORE_IMPORT__: '
|
||||
return [
|
||||
[
|
||||
(root) => {
|
||||
root.walkAtRules('import', (rule) => {
|
||||
if (rule.params.slice(1).startsWith('tailwindcss/')) {
|
||||
rule.after(postcss.comment({ text: IMPORT_COMMENT + rule.params }))
|
||||
rule.remove()
|
||||
}
|
||||
})
|
||||
},
|
||||
(() => {
|
||||
try {
|
||||
return require('postcss-import')
|
||||
} catch {}
|
||||
|
||||
return lazyPostcssImport()
|
||||
})(),
|
||||
(root) => {
|
||||
root.walkComments((rule) => {
|
||||
if (rule.text.startsWith(IMPORT_COMMENT)) {
|
||||
rule.after(
|
||||
postcss.atRule({
|
||||
name: 'import',
|
||||
params: rule.text.replace(IMPORT_COMMENT, ''),
|
||||
})
|
||||
)
|
||||
rule.remove()
|
||||
}
|
||||
})
|
||||
},
|
||||
],
|
||||
[],
|
||||
{},
|
||||
]
|
||||
}
|
||||
|
||||
function resolveConfig() {
|
||||
let config = configPath ? require(configPath) : {}
|
||||
|
||||
if (args['--purge']) {
|
||||
log.warn('purge-flag-deprecated', [
|
||||
'The `--purge` flag has been deprecated.',
|
||||
'Please use `--content` instead.',
|
||||
])
|
||||
if (!args['--content']) {
|
||||
args['--content'] = args['--purge']
|
||||
}
|
||||
}
|
||||
|
||||
if (args['--content']) {
|
||||
let files = args['--content'].split(/(?<!{[^}]+),/)
|
||||
let resolvedConfig = resolveConfigInternal(config, { content: { files } })
|
||||
resolvedConfig.content.files = files
|
||||
resolvedConfig = validateConfig(resolvedConfig)
|
||||
return resolvedConfig
|
||||
}
|
||||
|
||||
let resolvedConfig = resolveConfigInternal(config)
|
||||
resolvedConfig = validateConfig(resolvedConfig)
|
||||
return resolvedConfig
|
||||
}
|
||||
|
||||
function extractFileGlobs(config) {
|
||||
return config.content.files
|
||||
.filter((file) => {
|
||||
// Strings in this case are files / globs. If it is something else,
|
||||
// like an object it's probably a raw content object. But this object
|
||||
// is not watchable, so let's remove it.
|
||||
return typeof file === 'string'
|
||||
})
|
||||
.map((glob) => normalizePath(glob))
|
||||
}
|
||||
|
||||
function extractRawContent(config) {
|
||||
return config.content.files.filter((file) => {
|
||||
return typeof file === 'object' && file !== null
|
||||
})
|
||||
}
|
||||
|
||||
function getChangedContent(config) {
|
||||
let changedContent = []
|
||||
|
||||
// Resolve globs from the content config
|
||||
let globs = extractFileGlobs(config)
|
||||
let files = fastGlob.sync(globs)
|
||||
|
||||
for (let file of files) {
|
||||
changedContent.push({
|
||||
content: fs.readFileSync(path.resolve(file), 'utf8'),
|
||||
extension: path.extname(file).slice(1),
|
||||
})
|
||||
}
|
||||
|
||||
// Resolve raw content in the tailwind config
|
||||
for (let { raw: content, extension = 'html' } of extractRawContent(config)) {
|
||||
changedContent.push({ content, extension })
|
||||
}
|
||||
|
||||
return changedContent
|
||||
}
|
||||
|
||||
async function buildOnce() {
|
||||
let config = resolveConfig()
|
||||
let changedContent = getChangedContent(config)
|
||||
|
||||
let tailwindPlugin = () => {
|
||||
return {
|
||||
postcssPlugin: 'tailwindcss',
|
||||
Once(root, { result }) {
|
||||
tailwind(({ createContext }) => {
|
||||
return () => {
|
||||
return createContext(config, changedContent)
|
||||
}
|
||||
})(root, result)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
tailwindPlugin.postcss = true
|
||||
|
||||
let [beforePlugins, afterPlugins, postcssOptions] = includePostCss
|
||||
? await loadPostCssPlugins()
|
||||
: loadBuiltinPostcssPlugins()
|
||||
|
||||
let plugins = [
|
||||
...beforePlugins,
|
||||
tailwindPlugin,
|
||||
!args['--minify'] && formatNodes,
|
||||
...afterPlugins,
|
||||
!args['--no-autoprefixer'] &&
|
||||
(() => {
|
||||
// Try to load a local `autoprefixer` version first
|
||||
try {
|
||||
return require('autoprefixer')
|
||||
} catch {}
|
||||
|
||||
return lazyAutoprefixer()
|
||||
})(),
|
||||
args['--minify'] &&
|
||||
(() => {
|
||||
let options = { preset: ['default', { cssDeclarationSorter: false }] }
|
||||
|
||||
// Try to load a local `cssnano` version first
|
||||
try {
|
||||
return require('cssnano')
|
||||
} catch {}
|
||||
|
||||
return lazyCssnano()(options)
|
||||
})(),
|
||||
].filter(Boolean)
|
||||
|
||||
let postcss = loadPostcss()
|
||||
let processor = postcss(plugins)
|
||||
|
||||
function processCSS(css) {
|
||||
let start = process.hrtime.bigint()
|
||||
return Promise.resolve()
|
||||
.then(() => (output ? fs.promises.mkdir(path.dirname(output), { recursive: true }) : null))
|
||||
.then(() => processor.process(css, { ...postcssOptions, from: input, to: output }))
|
||||
.then((result) => {
|
||||
if (!output) {
|
||||
return process.stdout.write(result.css)
|
||||
}
|
||||
|
||||
return Promise.all(
|
||||
[
|
||||
outputFile(output, result.css),
|
||||
result.map && outputFile(output + '.map', result.map.toString()),
|
||||
].filter(Boolean)
|
||||
)
|
||||
})
|
||||
.then(() => {
|
||||
let end = process.hrtime.bigint()
|
||||
console.error()
|
||||
console.error('Done in', (end - start) / BigInt(1e6) + 'ms.')
|
||||
})
|
||||
}
|
||||
|
||||
let css = await (() => {
|
||||
// Piping in data, let's drain the stdin
|
||||
if (input === '-') {
|
||||
return drainStdin()
|
||||
}
|
||||
|
||||
// Input file has been provided
|
||||
if (input) {
|
||||
return fs.readFileSync(path.resolve(input), 'utf8')
|
||||
}
|
||||
|
||||
// No input file provided, fallback to default atrules
|
||||
return '@tailwind base; @tailwind components; @tailwind utilities'
|
||||
})()
|
||||
|
||||
return processCSS(css)
|
||||
}
|
||||
|
||||
let context = null
|
||||
|
||||
async function startWatcher() {
|
||||
let changedContent = []
|
||||
let configDependencies = []
|
||||
let contextDependencies = new Set()
|
||||
let watcher = null
|
||||
|
||||
function refreshConfig() {
|
||||
env.DEBUG && console.time('Module dependencies')
|
||||
for (let file of configDependencies) {
|
||||
delete require.cache[require.resolve(file)]
|
||||
}
|
||||
|
||||
if (configPath) {
|
||||
configDependencies = getModuleDependencies(configPath).map(({ file }) => file)
|
||||
|
||||
for (let dependency of configDependencies) {
|
||||
contextDependencies.add(dependency)
|
||||
}
|
||||
}
|
||||
env.DEBUG && console.timeEnd('Module dependencies')
|
||||
|
||||
return resolveConfig()
|
||||
}
|
||||
|
||||
let [beforePlugins, afterPlugins] = includePostCss
|
||||
? await loadPostCssPlugins()
|
||||
: loadBuiltinPostcssPlugins()
|
||||
|
||||
let plugins = [
|
||||
...beforePlugins,
|
||||
'__TAILWIND_PLUGIN_POSITION__',
|
||||
!args['--minify'] && formatNodes,
|
||||
...afterPlugins,
|
||||
!args['--no-autoprefixer'] &&
|
||||
(() => {
|
||||
// Try to load a local `autoprefixer` version first
|
||||
try {
|
||||
return require('autoprefixer')
|
||||
} catch {}
|
||||
|
||||
return lazyAutoprefixer()
|
||||
})(),
|
||||
args['--minify'] &&
|
||||
(() => {
|
||||
let options = { preset: ['default', { cssDeclarationSorter: false }] }
|
||||
|
||||
// Try to load a local `cssnano` version first
|
||||
try {
|
||||
return require('cssnano')
|
||||
} catch {}
|
||||
|
||||
return lazyCssnano()(options)
|
||||
})(),
|
||||
].filter(Boolean)
|
||||
|
||||
async function rebuild(config) {
|
||||
env.DEBUG && console.time('Finished in')
|
||||
|
||||
let tailwindPlugin = () => {
|
||||
return {
|
||||
postcssPlugin: 'tailwindcss',
|
||||
Once(root, { result }) {
|
||||
env.DEBUG && console.time('Compiling CSS')
|
||||
tailwind(({ createContext }) => {
|
||||
console.error()
|
||||
console.error('Rebuilding...')
|
||||
|
||||
return () => {
|
||||
if (context !== null) {
|
||||
context.changedContent = changedContent.splice(0)
|
||||
return context
|
||||
}
|
||||
|
||||
env.DEBUG && console.time('Creating context')
|
||||
context = createContext(config, changedContent.splice(0))
|
||||
env.DEBUG && console.timeEnd('Creating context')
|
||||
return context
|
||||
}
|
||||
})(root, result)
|
||||
env.DEBUG && console.timeEnd('Compiling CSS')
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
tailwindPlugin.postcss = true
|
||||
|
||||
let tailwindPluginIdx = plugins.indexOf('__TAILWIND_PLUGIN_POSITION__')
|
||||
let copy = plugins.slice()
|
||||
copy.splice(tailwindPluginIdx, 1, tailwindPlugin)
|
||||
let postcss = loadPostcss()
|
||||
let processor = postcss(copy)
|
||||
|
||||
function processCSS(css) {
|
||||
let start = process.hrtime.bigint()
|
||||
return Promise.resolve()
|
||||
.then(() =>
|
||||
output ? fs.promises.mkdir(path.dirname(output), { recursive: true }) : null
|
||||
)
|
||||
.then(() => processor.process(css, { from: input, to: output }))
|
||||
.then(async (result) => {
|
||||
for (let message of result.messages) {
|
||||
if (message.type === 'dependency') {
|
||||
contextDependencies.add(message.file)
|
||||
}
|
||||
}
|
||||
watcher.add([...contextDependencies])
|
||||
|
||||
if (!output) {
|
||||
return process.stdout.write(result.css)
|
||||
}
|
||||
|
||||
return Promise.all(
|
||||
[
|
||||
outputFile(output, result.css),
|
||||
result.map && outputFile(output + '.map', result.map.toString()),
|
||||
].filter(Boolean)
|
||||
)
|
||||
})
|
||||
.then(() => {
|
||||
let end = process.hrtime.bigint()
|
||||
console.error('Done in', (end - start) / BigInt(1e6) + 'ms.')
|
||||
})
|
||||
.catch((err) => {
|
||||
if (err.name === 'CssSyntaxError') {
|
||||
console.error(err.toString())
|
||||
} else {
|
||||
console.error(err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
let css = await (() => {
|
||||
// Piping in data, let's drain the stdin
|
||||
if (input === '-') {
|
||||
return drainStdin()
|
||||
}
|
||||
|
||||
// Input file has been provided
|
||||
if (input) {
|
||||
return fs.readFileSync(path.resolve(input), 'utf8')
|
||||
}
|
||||
|
||||
// No input file provided, fallback to default atrules
|
||||
return '@tailwind base; @tailwind components; @tailwind utilities'
|
||||
})()
|
||||
|
||||
let result = await processCSS(css)
|
||||
env.DEBUG && console.timeEnd('Finished in')
|
||||
return result
|
||||
}
|
||||
|
||||
let config = refreshConfig(configPath)
|
||||
|
||||
if (input) {
|
||||
contextDependencies.add(path.resolve(input))
|
||||
}
|
||||
|
||||
watcher = chokidar.watch([...contextDependencies, ...extractFileGlobs(config)], {
|
||||
usePolling: shouldPoll,
|
||||
interval: shouldPoll ? pollInterval : undefined,
|
||||
ignoreInitial: true,
|
||||
awaitWriteFinish: shouldCoalesceWriteEvents
|
||||
? {
|
||||
stabilityThreshold: 50,
|
||||
pollInterval: pollInterval,
|
||||
}
|
||||
: false,
|
||||
})
|
||||
|
||||
let chain = Promise.resolve()
|
||||
|
||||
watcher.on('change', async (file) => {
|
||||
if (contextDependencies.has(file)) {
|
||||
env.DEBUG && console.time('Resolve config')
|
||||
context = null
|
||||
config = refreshConfig(configPath)
|
||||
env.DEBUG && console.timeEnd('Resolve config')
|
||||
|
||||
env.DEBUG && console.time('Watch new files')
|
||||
let globs = extractFileGlobs(config)
|
||||
watcher.add(configDependencies)
|
||||
watcher.add(globs)
|
||||
env.DEBUG && console.timeEnd('Watch new files')
|
||||
|
||||
chain = chain.then(async () => {
|
||||
changedContent.push(...getChangedContent(config))
|
||||
await rebuild(config)
|
||||
})
|
||||
} else {
|
||||
chain = chain.then(async () => {
|
||||
changedContent.push({
|
||||
content: fs.readFileSync(path.resolve(file), 'utf8'),
|
||||
extension: path.extname(file).slice(1),
|
||||
})
|
||||
|
||||
await rebuild(config)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
watcher.on('add', async (file) => {
|
||||
chain = chain.then(async () => {
|
||||
changedContent.push({
|
||||
content: fs.readFileSync(path.resolve(file), 'utf8'),
|
||||
extension: path.extname(file).slice(1),
|
||||
})
|
||||
|
||||
await rebuild(config)
|
||||
})
|
||||
})
|
||||
|
||||
chain = chain.then(() => {
|
||||
changedContent.push(...getChangedContent(config))
|
||||
return rebuild(config)
|
||||
})
|
||||
}
|
||||
|
||||
if (shouldWatch) {
|
||||
/* Abort the watcher if stdin is closed to avoid zombie processes */
|
||||
if (process.stdin.isTTY) {
|
||||
process.stdin.on('end', () => process.exit(0))
|
||||
process.stdin.resume()
|
||||
}
|
||||
startWatcher()
|
||||
} else {
|
||||
buildOnce()
|
||||
}
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
import path from 'path'
|
||||
|
||||
export const cli = 'tailwind'
|
||||
export const defaultConfigFile = './tailwind.config.js'
|
||||
export const defaultPostCssConfigFile = './postcss.config.js'
|
||||
export const cjsConfigFile = './tailwind.config.cjs'
|
||||
export const cjsPostCssConfigFile = './postcss.config.cjs'
|
||||
|
||||
export const supportedConfigFiles = [cjsConfigFile, defaultConfigFile]
|
||||
export const supportedPostCssConfigFile = [cjsPostCssConfigFile, defaultPostCssConfigFile]
|
||||
|
||||
export const defaultConfigStubFile = path.resolve(__dirname, '../stubs/defaultConfig.stub.js')
|
||||
export const simpleConfigStubFile = path.resolve(__dirname, '../stubs/simpleConfig.stub.js')
|
||||
export const defaultPostCssConfigStubFile = path.resolve(
|
||||
__dirname,
|
||||
'../stubs/defaultPostCssConfig.stub.js'
|
||||
)
|
||||
+1
@@ -0,0 +1 @@
|
||||
export default ["preflight","container","accessibility","pointerEvents","visibility","position","inset","isolation","zIndex","order","gridColumn","gridColumnStart","gridColumnEnd","gridRow","gridRowStart","gridRowEnd","float","clear","margin","boxSizing","display","aspectRatio","height","maxHeight","minHeight","width","minWidth","maxWidth","flex","flexShrink","flexGrow","flexBasis","tableLayout","borderCollapse","borderSpacing","transformOrigin","translate","rotate","skew","scale","transform","animation","cursor","touchAction","userSelect","resize","scrollSnapType","scrollSnapAlign","scrollSnapStop","scrollMargin","scrollPadding","listStylePosition","listStyleType","appearance","columns","breakBefore","breakInside","breakAfter","gridAutoColumns","gridAutoFlow","gridAutoRows","gridTemplateColumns","gridTemplateRows","flexDirection","flexWrap","placeContent","placeItems","alignContent","alignItems","justifyContent","justifyItems","gap","space","divideWidth","divideStyle","divideColor","divideOpacity","placeSelf","alignSelf","justifySelf","overflow","overscrollBehavior","scrollBehavior","textOverflow","whitespace","wordBreak","borderRadius","borderWidth","borderStyle","borderColor","borderOpacity","backgroundColor","backgroundOpacity","backgroundImage","gradientColorStops","boxDecorationBreak","backgroundSize","backgroundAttachment","backgroundClip","backgroundPosition","backgroundRepeat","backgroundOrigin","fill","stroke","strokeWidth","objectFit","objectPosition","padding","textAlign","textIndent","verticalAlign","fontFamily","fontSize","fontWeight","textTransform","fontStyle","fontVariantNumeric","lineHeight","letterSpacing","textColor","textOpacity","textDecoration","textDecorationColor","textDecorationStyle","textDecorationThickness","textUnderlineOffset","fontSmoothing","placeholderColor","placeholderOpacity","caretColor","accentColor","opacity","backgroundBlendMode","mixBlendMode","boxShadow","boxShadowColor","outlineStyle","outlineWidth","outlineOffset","outlineColor","ringWidth","ringColor","ringOpacity","ringOffsetWidth","ringOffsetColor","blur","brightness","contrast","dropShadow","grayscale","hueRotate","invert","saturate","sepia","filter","backdropBlur","backdropBrightness","backdropContrast","backdropGrayscale","backdropHueRotate","backdropInvert","backdropOpacity","backdropSaturate","backdropSepia","backdropFilter","transitionProperty","transitionDelay","transitionDuration","transitionTimingFunction","willChange","content"]
|
||||
+2442
File diff suppressed because it is too large
Load Diff
+25
@@ -0,0 +1,25 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) Nicolas Gallagher
|
||||
Copyright (c) Jonathan Neal
|
||||
Copyright (c) Sindre Sorhus <sindresorhus@gmail.com> (sindresorhus.com)
|
||||
Copyright (c) Adam Wathan
|
||||
Copyright (c) Jonathan Reinink
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
+360
@@ -0,0 +1,360 @@
|
||||
/*
|
||||
1. Prevent padding and border from affecting element width. (https://github.com/mozdevs/cssremedy/issues/4)
|
||||
2. Allow adding a border to an element by just adding a border-width. (https://github.com/tailwindcss/tailwindcss/pull/116)
|
||||
*/
|
||||
|
||||
*,
|
||||
::before,
|
||||
::after {
|
||||
box-sizing: border-box; /* 1 */
|
||||
border-width: 0; /* 2 */
|
||||
border-style: solid; /* 2 */
|
||||
border-color: theme('borderColor.DEFAULT', currentColor); /* 2 */
|
||||
}
|
||||
|
||||
::before,
|
||||
::after {
|
||||
--tw-content: '';
|
||||
}
|
||||
|
||||
/*
|
||||
1. Use a consistent sensible line-height in all browsers.
|
||||
2. Prevent adjustments of font size after orientation changes in iOS.
|
||||
3. Use a more readable tab size.
|
||||
4. Use the user's configured `sans` font-family by default.
|
||||
*/
|
||||
|
||||
html {
|
||||
line-height: 1.5; /* 1 */
|
||||
-webkit-text-size-adjust: 100%; /* 2 */
|
||||
-moz-tab-size: 4; /* 3 */
|
||||
tab-size: 4; /* 3 */
|
||||
font-family: theme('fontFamily.sans', ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"); /* 4 */
|
||||
}
|
||||
|
||||
/*
|
||||
1. Remove the margin in all browsers.
|
||||
2. Inherit line-height from `html` so users can set them as a class directly on the `html` element.
|
||||
*/
|
||||
|
||||
body {
|
||||
margin: 0; /* 1 */
|
||||
line-height: inherit; /* 2 */
|
||||
}
|
||||
|
||||
/*
|
||||
1. Add the correct height in Firefox.
|
||||
2. Correct the inheritance of border color in Firefox. (https://bugzilla.mozilla.org/show_bug.cgi?id=190655)
|
||||
3. Ensure horizontal rules are visible by default.
|
||||
*/
|
||||
|
||||
hr {
|
||||
height: 0; /* 1 */
|
||||
color: inherit; /* 2 */
|
||||
border-top-width: 1px; /* 3 */
|
||||
}
|
||||
|
||||
/*
|
||||
Add the correct text decoration in Chrome, Edge, and Safari.
|
||||
*/
|
||||
|
||||
abbr:where([title]) {
|
||||
text-decoration: underline dotted;
|
||||
}
|
||||
|
||||
/*
|
||||
Remove the default font size and weight for headings.
|
||||
*/
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
font-size: inherit;
|
||||
font-weight: inherit;
|
||||
}
|
||||
|
||||
/*
|
||||
Reset links to optimize for opt-in styling instead of opt-out.
|
||||
*/
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
text-decoration: inherit;
|
||||
}
|
||||
|
||||
/*
|
||||
Add the correct font weight in Edge and Safari.
|
||||
*/
|
||||
|
||||
b,
|
||||
strong {
|
||||
font-weight: bolder;
|
||||
}
|
||||
|
||||
/*
|
||||
1. Use the user's configured `mono` font family by default.
|
||||
2. Correct the odd `em` font sizing in all browsers.
|
||||
*/
|
||||
|
||||
code,
|
||||
kbd,
|
||||
samp,
|
||||
pre {
|
||||
font-family: theme('fontFamily.mono', ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace); /* 1 */
|
||||
font-size: 1em; /* 2 */
|
||||
}
|
||||
|
||||
/*
|
||||
Add the correct font size in all browsers.
|
||||
*/
|
||||
|
||||
small {
|
||||
font-size: 80%;
|
||||
}
|
||||
|
||||
/*
|
||||
Prevent `sub` and `sup` elements from affecting the line height in all browsers.
|
||||
*/
|
||||
|
||||
sub,
|
||||
sup {
|
||||
font-size: 75%;
|
||||
line-height: 0;
|
||||
position: relative;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
sub {
|
||||
bottom: -0.25em;
|
||||
}
|
||||
|
||||
sup {
|
||||
top: -0.5em;
|
||||
}
|
||||
|
||||
/*
|
||||
1. Remove text indentation from table contents in Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=999088, https://bugs.webkit.org/show_bug.cgi?id=201297)
|
||||
2. Correct table border color inheritance in all Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=935729, https://bugs.webkit.org/show_bug.cgi?id=195016)
|
||||
3. Remove gaps between table borders by default.
|
||||
*/
|
||||
|
||||
table {
|
||||
text-indent: 0; /* 1 */
|
||||
border-color: inherit; /* 2 */
|
||||
border-collapse: collapse; /* 3 */
|
||||
}
|
||||
|
||||
/*
|
||||
1. Change the font styles in all browsers.
|
||||
2. Remove the margin in Firefox and Safari.
|
||||
3. Remove default padding in all browsers.
|
||||
*/
|
||||
|
||||
button,
|
||||
input,
|
||||
optgroup,
|
||||
select,
|
||||
textarea {
|
||||
font-family: inherit; /* 1 */
|
||||
font-size: 100%; /* 1 */
|
||||
font-weight: inherit; /* 1 */
|
||||
line-height: inherit; /* 1 */
|
||||
color: inherit; /* 1 */
|
||||
margin: 0; /* 2 */
|
||||
padding: 0; /* 3 */
|
||||
}
|
||||
|
||||
/*
|
||||
Remove the inheritance of text transform in Edge and Firefox.
|
||||
*/
|
||||
|
||||
button,
|
||||
select {
|
||||
text-transform: none;
|
||||
}
|
||||
|
||||
/*
|
||||
1. Correct the inability to style clickable types in iOS and Safari.
|
||||
2. Remove default button styles.
|
||||
*/
|
||||
|
||||
button,
|
||||
[type='button'],
|
||||
[type='reset'],
|
||||
[type='submit'] {
|
||||
-webkit-appearance: button; /* 1 */
|
||||
background-color: transparent; /* 2 */
|
||||
background-image: none; /* 2 */
|
||||
}
|
||||
|
||||
/*
|
||||
Use the modern Firefox focus style for all focusable elements.
|
||||
*/
|
||||
|
||||
:-moz-focusring {
|
||||
outline: auto;
|
||||
}
|
||||
|
||||
/*
|
||||
Remove the additional `:invalid` styles in Firefox. (https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737)
|
||||
*/
|
||||
|
||||
:-moz-ui-invalid {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
/*
|
||||
Add the correct vertical alignment in Chrome and Firefox.
|
||||
*/
|
||||
|
||||
progress {
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
/*
|
||||
Correct the cursor style of increment and decrement buttons in Safari.
|
||||
*/
|
||||
|
||||
::-webkit-inner-spin-button,
|
||||
::-webkit-outer-spin-button {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
/*
|
||||
1. Correct the odd appearance in Chrome and Safari.
|
||||
2. Correct the outline style in Safari.
|
||||
*/
|
||||
|
||||
[type='search'] {
|
||||
-webkit-appearance: textfield; /* 1 */
|
||||
outline-offset: -2px; /* 2 */
|
||||
}
|
||||
|
||||
/*
|
||||
Remove the inner padding in Chrome and Safari on macOS.
|
||||
*/
|
||||
|
||||
::-webkit-search-decoration {
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
/*
|
||||
1. Correct the inability to style clickable types in iOS and Safari.
|
||||
2. Change font properties to `inherit` in Safari.
|
||||
*/
|
||||
|
||||
::-webkit-file-upload-button {
|
||||
-webkit-appearance: button; /* 1 */
|
||||
font: inherit; /* 2 */
|
||||
}
|
||||
|
||||
/*
|
||||
Add the correct display in Chrome and Safari.
|
||||
*/
|
||||
|
||||
summary {
|
||||
display: list-item;
|
||||
}
|
||||
|
||||
/*
|
||||
Removes the default spacing and border for appropriate elements.
|
||||
*/
|
||||
|
||||
blockquote,
|
||||
dl,
|
||||
dd,
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6,
|
||||
hr,
|
||||
figure,
|
||||
p,
|
||||
pre {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
fieldset {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
legend {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
ol,
|
||||
ul,
|
||||
menu {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/*
|
||||
Prevent resizing textareas horizontally by default.
|
||||
*/
|
||||
|
||||
textarea {
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
/*
|
||||
1. Reset the default placeholder opacity in Firefox. (https://github.com/tailwindlabs/tailwindcss/issues/3300)
|
||||
2. Set the default placeholder color to the user's configured gray 400 color.
|
||||
*/
|
||||
|
||||
input::placeholder,
|
||||
textarea::placeholder {
|
||||
opacity: 1; /* 1 */
|
||||
color: theme('colors.gray.400', #9ca3af); /* 2 */
|
||||
}
|
||||
|
||||
/*
|
||||
Set the default cursor for buttons.
|
||||
*/
|
||||
|
||||
button,
|
||||
[role="button"] {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/*
|
||||
Make sure disabled buttons don't get the pointer cursor.
|
||||
*/
|
||||
:disabled {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
/*
|
||||
1. Make replaced elements `display: block` by default. (https://github.com/mozdevs/cssremedy/issues/14)
|
||||
2. Add `vertical-align: middle` to align replaced elements more sensibly by default. (https://github.com/jensimmons/cssremedy/issues/14#issuecomment-634934210)
|
||||
This can trigger a poorly considered lint error in some tools but is included by design.
|
||||
*/
|
||||
|
||||
img,
|
||||
svg,
|
||||
video,
|
||||
canvas,
|
||||
audio,
|
||||
iframe,
|
||||
embed,
|
||||
object {
|
||||
display: block; /* 1 */
|
||||
vertical-align: middle; /* 2 */
|
||||
}
|
||||
|
||||
/*
|
||||
Constrain images and videos to the parent width and preserve their intrinsic aspect ratio. (https://github.com/mozdevs/cssremedy/issues/14)
|
||||
*/
|
||||
|
||||
img,
|
||||
video {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
import colors from 'picocolors'
|
||||
import log from './util/log'
|
||||
|
||||
let defaults = {
|
||||
optimizeUniversalDefaults: false,
|
||||
}
|
||||
|
||||
let featureFlags = {
|
||||
future: ['hoverOnlyWhenSupported', 'respectDefaultRingColorOpacity'],
|
||||
experimental: ['optimizeUniversalDefaults', 'matchVariant' /* , 'variantGrouping' */],
|
||||
}
|
||||
|
||||
export function flagEnabled(config, flag) {
|
||||
if (featureFlags.future.includes(flag)) {
|
||||
return config.future === 'all' || (config?.future?.[flag] ?? defaults[flag] ?? false)
|
||||
}
|
||||
|
||||
if (featureFlags.experimental.includes(flag)) {
|
||||
return (
|
||||
config.experimental === 'all' || (config?.experimental?.[flag] ?? defaults[flag] ?? false)
|
||||
)
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
function experimentalFlagsEnabled(config) {
|
||||
if (config.experimental === 'all') {
|
||||
return featureFlags.experimental
|
||||
}
|
||||
|
||||
return Object.keys(config?.experimental ?? {}).filter(
|
||||
(flag) => featureFlags.experimental.includes(flag) && config.experimental[flag]
|
||||
)
|
||||
}
|
||||
|
||||
export function issueFlagNotices(config) {
|
||||
if (process.env.JEST_WORKER_ID !== undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
if (experimentalFlagsEnabled(config).length > 0) {
|
||||
let changes = experimentalFlagsEnabled(config)
|
||||
.map((s) => colors.yellow(s))
|
||||
.join(', ')
|
||||
|
||||
log.warn('experimental-flags-enabled', [
|
||||
`You have enabled experimental features: ${changes}`,
|
||||
'Experimental features in Tailwind CSS are not covered by semver, may introduce breaking changes, and can change at any time.',
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
export default featureFlags
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
import setupTrackingContext from './lib/setupTrackingContext'
|
||||
import processTailwindFeatures from './processTailwindFeatures'
|
||||
import { env } from './lib/sharedState'
|
||||
|
||||
module.exports = function tailwindcss(configOrPath) {
|
||||
return {
|
||||
postcssPlugin: 'tailwindcss',
|
||||
plugins: [
|
||||
env.DEBUG &&
|
||||
function (root) {
|
||||
console.log('\n')
|
||||
console.time('JIT TOTAL')
|
||||
return root
|
||||
},
|
||||
function (root, result) {
|
||||
let context = setupTrackingContext(configOrPath)
|
||||
|
||||
if (root.type === 'document') {
|
||||
let roots = root.nodes.filter((node) => node.type === 'root')
|
||||
|
||||
for (const root of roots) {
|
||||
if (root.type === 'root') {
|
||||
processTailwindFeatures(context)(root, result)
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
processTailwindFeatures(context)(root, result)
|
||||
},
|
||||
env.DEBUG &&
|
||||
function (root) {
|
||||
console.timeEnd('JIT TOTAL')
|
||||
console.log('\n')
|
||||
return root
|
||||
},
|
||||
].filter(Boolean),
|
||||
}
|
||||
}
|
||||
|
||||
module.exports.postcss = true
|
||||
+52
@@ -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
|
||||
}
|
||||
+58
@@ -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)
|
||||
}
|
||||
}
|
||||
+93
@@ -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
|
||||
}
|
||||
+208
@@ -0,0 +1,208 @@
|
||||
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) {
|
||||
results = [...results, ...(content.match(pattern) ?? [])]
|
||||
}
|
||||
|
||||
return results.filter((v) => v !== undefined).map(clipAtBalancedParens)
|
||||
}
|
||||
}
|
||||
|
||||
function* buildRegExps(context) {
|
||||
let separator = context.tailwindConfig.separator
|
||||
let variantGroupingEnabled = flagEnabled(context.tailwindConfig, 'variantGrouping')
|
||||
let prefix =
|
||||
context.tailwindConfig.prefix !== ''
|
||||
? regex.optional(regex.pattern([/-?/, regex.escape(context.tailwindConfig.prefix)]))
|
||||
: ''
|
||||
|
||||
let utility = regex.any([
|
||||
// Arbitrary properties
|
||||
/\[[^\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([
|
||||
regex.pattern([/([^\s"'`\[\\]+-)?\[[^\s"'`]+\]/, separator]),
|
||||
regex.pattern([/[^\s"'`\[\\]+/, separator]),
|
||||
]),
|
||||
|
||||
// With quotes allowed
|
||||
regex.any([
|
||||
regex.pattern([/([^\s"'`\[\\]+-)?\[[^\s`]+\]/, separator]),
|
||||
regex.pattern([/[^\s`\[\\]+/, separator]),
|
||||
]),
|
||||
]
|
||||
|
||||
for (const variantPattern of variantPatterns) {
|
||||
yield regex.pattern([
|
||||
// Variants
|
||||
'((?=((',
|
||||
variantPattern,
|
||||
')+))\\2)?',
|
||||
|
||||
// Important (optional)
|
||||
/!?/,
|
||||
|
||||
prefix,
|
||||
|
||||
variantGroupingEnabled
|
||||
? regex.any([
|
||||
// Or any of those things but grouped separated by commas
|
||||
regex.pattern([/\(/, utility, regex.zeroOrMore([/,/, utility]), /\)/]),
|
||||
|
||||
// Arbitrary properties, constrained utilities, arbitrary values, etc…
|
||||
utility,
|
||||
])
|
||||
: 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)
|
||||
}
|
||||
|
||||
// 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}]
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
export default function (_context) {
|
||||
return (root, result) => {
|
||||
let found = false
|
||||
|
||||
root.walkAtRules('tailwind', (node) => {
|
||||
if (found) return false
|
||||
|
||||
if (node.parent && node.parent.type !== 'root') {
|
||||
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
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
+248
@@ -0,0 +1,248 @@
|
||||
import dlv from 'dlv'
|
||||
import didYouMean from 'didyoumean'
|
||||
import transformThemeValue from '../util/transformThemeValue'
|
||||
import parseValue from 'postcss-value-parser'
|
||||
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'
|
||||
|
||||
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) {
|
||||
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 ({ tailwindConfig: config }) {
|
||||
let functions = {
|
||||
theme: (node, path, ...defaultValue) => {
|
||||
let { isValid, value, error, alpha } = resolvePath(
|
||||
config,
|
||||
path,
|
||||
defaultValue.length ? defaultValue : undefined
|
||||
)
|
||||
|
||||
if (!isValid) {
|
||||
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)
|
||||
})
|
||||
}
|
||||
}
|
||||
+545
@@ -0,0 +1,545 @@
|
||||
import postcss from 'postcss'
|
||||
import parser from 'postcss-selector-parser'
|
||||
|
||||
import { resolveMatches } from './generateRules'
|
||||
import bigSign from '../util/bigSign'
|
||||
import escapeClassName from '../util/escapeClassName'
|
||||
|
||||
/** @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((root) => root.nodes.map((node) => node.toString()))
|
||||
|
||||
/**
|
||||
* @param {string} ruleSelectors
|
||||
*/
|
||||
function extractSelectors(ruleSelectors) {
|
||||
return selectorExtractor.transformSync(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()
|
||||
|
||||
let highestOffset = context.layerOrder.user >> 4n
|
||||
|
||||
root.walkRules((rule, idx) => {
|
||||
// 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)
|
||||
|
||||
for (let className of extractClasses(rule)) {
|
||||
let list = cache.get(className) || []
|
||||
cache.set(className, list)
|
||||
|
||||
list.push([
|
||||
{
|
||||
layer: 'user',
|
||||
sort: BigInt(idx) + highestOffset,
|
||||
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`
|
||||
*/
|
||||
// TODO: Should we use postcss-selector-parser for this instead?
|
||||
function replaceSelector(selector, utilitySelectors, candidate) {
|
||||
let needle = `.${escapeClassName(candidate)}`
|
||||
let needles = [...new Set([needle, needle.replace(/\\2c /g, '\\,')])]
|
||||
let utilitySelectorsList = extractSelectors(utilitySelectors)
|
||||
|
||||
return extractSelectors(selector)
|
||||
.map((s) => {
|
||||
let replaced = []
|
||||
|
||||
for (let utilitySelector of utilitySelectorsList) {
|
||||
let replacedSelector = utilitySelector
|
||||
for (const needle of needles) {
|
||||
replacedSelector = replacedSelector.replace(needle, s)
|
||||
}
|
||||
if (replacedSelector === utilitySelector) {
|
||||
continue
|
||||
}
|
||||
replaced.push(replacedSelector)
|
||||
}
|
||||
return replaced.join(', ')
|
||||
})
|
||||
.join(', ')
|
||||
}
|
||||
|
||||
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') {
|
||||
const 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 (const [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
|
||||
|
||||
rule.selector = replaceSelector(parentSelector, rule.selector, applyCandidate)
|
||||
|
||||
// And then re-add it if it was removed
|
||||
if (importantSelector && parentSelector !== parent.selector) {
|
||||
rule.selector = `${importantSelector} ${rule.selector}`
|
||||
}
|
||||
|
||||
rule.walkDecls((d) => {
|
||||
d.important = meta.important || important
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// 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([
|
||||
// Ensure that when we are sorting, that we take the layer order into account
|
||||
{ ...meta, sort: meta.sort | context.layerOrder[meta.layer] },
|
||||
root.nodes[0],
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
// Inject the rules, sorted, correctly
|
||||
let nodes = siblings.sort(([a], [z]) => bigSign(a.sort - z.sort)).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)
|
||||
}
|
||||
}
|
||||
+292
@@ -0,0 +1,292 @@
|
||||
import LRU from 'quick-lru'
|
||||
import * as sharedState from './sharedState'
|
||||
import { generateRules } from './generateRules'
|
||||
import bigSign from '../util/bigSign'
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function buildStylesheet(rules, context) {
|
||||
let sortedRules = rules.sort(([a], [z]) => bigSign(a - z))
|
||||
|
||||
let returnValue = {
|
||||
base: new Set(),
|
||||
defaults: new Set(),
|
||||
components: new Set(),
|
||||
utilities: new Set(),
|
||||
variants: new Set(),
|
||||
|
||||
// All the CSS that is not Tailwind related can be put in this bucket. This
|
||||
// will make it easier to later use this information when we want to
|
||||
// `@apply` for example. The main reason we do this here is because we
|
||||
// still need to make sure the order is correct. Last but not least, we
|
||||
// will make sure to always re-inject this section into the css, even if
|
||||
// certain rules were not used. This means that it will look like a no-op
|
||||
// from the user's perspective, but we gathered all the useful information
|
||||
// we need.
|
||||
user: new Set(),
|
||||
}
|
||||
|
||||
for (let [sort, rule] of sortedRules) {
|
||||
if (sort >= context.minimumScreen) {
|
||||
returnValue.variants.add(rule)
|
||||
continue
|
||||
}
|
||||
|
||||
if (sort & context.layerOrder.base) {
|
||||
returnValue.base.add(rule)
|
||||
continue
|
||||
}
|
||||
|
||||
if (sort & context.layerOrder.defaults) {
|
||||
returnValue.defaults.add(rule)
|
||||
continue
|
||||
}
|
||||
|
||||
if (sort & context.layerOrder.components) {
|
||||
returnValue.components.add(rule)
|
||||
continue
|
||||
}
|
||||
|
||||
if (sort & context.layerOrder.utilities) {
|
||||
returnValue.utilities.add(rule)
|
||||
continue
|
||||
}
|
||||
|
||||
if (sort & context.layerOrder.user) {
|
||||
returnValue.user.add(rule)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
return returnValue
|
||||
}
|
||||
|
||||
export default function expandTailwindAtRules(context) {
|
||||
return (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([sharedState.NOT_ON_DEMAND])
|
||||
let seen = new Set()
|
||||
|
||||
env.DEBUG && console.time('Reading changed files')
|
||||
|
||||
for (let { content, extension } of context.changedContent) {
|
||||
let transformer = getTransformer(context.tailwindConfig, extension)
|
||||
let extractor = getExtractor(context, extension)
|
||||
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')
|
||||
let rules = generateRules(candidates, 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) {
|
||||
for (let rule of rules) {
|
||||
context.ruleCache.add(rule)
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
+720
@@ -0,0 +1,720 @@
|
||||
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 } from '../util/pluginUtils'
|
||||
import log from '../util/log'
|
||||
import * as sharedState from './sharedState'
|
||||
import { formatVariantSelector, finalizeSelector } from '../util/formatVariantSelector'
|
||||
import { asClass } from '../util/nameClass'
|
||||
import { normalize } from '../util/dataTypes'
|
||||
import { isValidVariantFormatString, parseVariant } from './setupContextUtils'
|
||||
import isValidArbitraryValue from '../util/isValidArbitraryValue'
|
||||
import { splitAtTopLevelOnly } from '../util/splitAtTopLevelOnly.js'
|
||||
import { flagEnabled } from '../featureFlags'
|
||||
|
||||
let classNameParser = selectorParser((selectors) => {
|
||||
return selectors.first.filter(({ type }) => type === 'class').pop().value
|
||||
})
|
||||
|
||||
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
|
||||
|
||||
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[]
|
||||
dashIdx = ['-', '/'].includes(candidate[bracketIdx - 1]) ? bracketIdx - 1 : -1
|
||||
} else {
|
||||
dashIdx = candidate.lastIndexOf('-', lastIndex)
|
||||
}
|
||||
|
||||
if (dashIdx < 0) {
|
||||
break
|
||||
}
|
||||
|
||||
let prefix = candidate.slice(0, dashIdx)
|
||||
let modifier = candidate.slice(dashIdx + 1)
|
||||
|
||||
yield [prefix, modifier]
|
||||
|
||||
lastIndex = dashIdx - 1
|
||||
}
|
||||
}
|
||||
|
||||
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) => {
|
||||
r.selector = updateAllClasses(r.selector, (className) => {
|
||||
if (className === classCandidate) {
|
||||
return `!${className}`
|
||||
}
|
||||
return className
|
||||
})
|
||||
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
|
||||
}
|
||||
|
||||
let args
|
||||
|
||||
// Find partial arbitrary variants
|
||||
if (variant.endsWith(']') && !variant.startsWith('[')) {
|
||||
args = variant.slice(variant.lastIndexOf('[') + 1, -1)
|
||||
variant = variant.slice(0, variant.indexOf(args) - 1 /* - */ - 1 /* [ */)
|
||||
}
|
||||
|
||||
// Register arbitrary variants
|
||||
if (isArbitraryValue(variant) && !context.variantMap.has(variant)) {
|
||||
let selector = normalize(variant.slice(1, -1))
|
||||
|
||||
if (!isValidVariantFormatString(selector)) {
|
||||
return []
|
||||
}
|
||||
|
||||
let fn = parseVariant(selector)
|
||||
|
||||
let sort = Array.from(context.variantOrder.values()).pop() << 1n
|
||||
context.variantMap.set(variant, [[sort, fn]])
|
||||
context.variantOrder.set(variant, sort)
|
||||
}
|
||||
|
||||
if (context.variantMap.has(variant)) {
|
||||
let variantFunctionTuples = context.variantMap.get(variant).slice()
|
||||
let result = []
|
||||
|
||||
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(selectorFormat)
|
||||
},
|
||||
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([
|
||||
// TODO: This could have potential bugs if we shift the sort order from variant A far
|
||||
// enough into the sort space of variant B. The chances are low, but if this happens
|
||||
// then this might be the place too look at. One potential solution to this problem is
|
||||
// reserving additional X places for these 'unknown' variants in between.
|
||||
variantSort | BigInt(idx << ruleWithVariant.length),
|
||||
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(ruleWithVariant)
|
||||
}
|
||||
|
||||
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(modified.replace(rebuiltBase, '&'))
|
||||
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: variantSort | meta.sort,
|
||||
collectedFormats: (meta.collectedFormats ?? []).concat(collectedFormats),
|
||||
isArbitraryVariant: isArbitraryValue(variant),
|
||||
},
|
||||
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.name, 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)
|
||||
|
||||
if (!isParsableCssValue(property, normalized)) {
|
||||
return null
|
||||
}
|
||||
|
||||
return [
|
||||
[
|
||||
{ sort: context.arbitraryPropertiesSort, 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 Array.from(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, original = candidate) {
|
||||
let separator = context.tailwindConfig.separator
|
||||
let [classCandidate, ...variants] = splitWithSeparator(candidate, separator).reverse()
|
||||
let important = false
|
||||
|
||||
if (classCandidate.startsWith('!')) {
|
||||
important = true
|
||||
classCandidate = classCandidate.slice(1)
|
||||
}
|
||||
|
||||
if (flagEnabled(context.tailwindConfig, 'variantGrouping')) {
|
||||
if (classCandidate.startsWith('(') && classCandidate.endsWith(')')) {
|
||||
let base = variants.slice().reverse().join(separator)
|
||||
for (let part of splitAtTopLevelOnly(classCandidate.slice(1, -1), ',')) {
|
||||
yield* resolveMatches(base + separator + part, context, original)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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) {
|
||||
typesByMatches.set(matchesPerPlugin, sort.options?.type)
|
||||
matches.push(matchesPerPlugin)
|
||||
}
|
||||
}
|
||||
|
||||
if (isArbitraryValue(modifier)) {
|
||||
// When generated arbitrary values are ambiguous, we can't know
|
||||
// which to pick so don't generate any utilities for them
|
||||
if (matches.length > 1) {
|
||||
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
|
||||
if (match[0].collectedFormats) {
|
||||
let finalFormat = formatVariantSelector('&', ...match[0].collectedFormats)
|
||||
let container = postcss.root({ nodes: [match[1].clone()] })
|
||||
container.walkRules((rule) => {
|
||||
if (inKeyframes(rule)) return
|
||||
|
||||
rule.selector = finalizeSelector(finalFormat, {
|
||||
selector: rule.selector,
|
||||
candidate: original,
|
||||
base: candidate
|
||||
.split(new RegExp(`\\${context?.tailwindConfig?.separator ?? ':'}(?![^[]*\\])`))
|
||||
.pop(),
|
||||
isArbitraryVariant: match[0].isArbitraryVariant,
|
||||
|
||||
context,
|
||||
})
|
||||
})
|
||||
match[1] = container.nodes[0]
|
||||
}
|
||||
|
||||
yield match
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function inKeyframes(rule) {
|
||||
return rule.parent && rule.parent.type === 'atrule' && rule.parent.name === 'keyframes'
|
||||
}
|
||||
|
||||
function generateRules(candidates, context) {
|
||||
let allRules = []
|
||||
|
||||
for (let candidate of candidates) {
|
||||
if (context.notClassCache.has(candidate)) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (context.classCache.has(candidate)) {
|
||||
allRules.push(context.classCache.get(candidate))
|
||||
continue
|
||||
}
|
||||
|
||||
let matches = Array.from(resolveMatches(candidate, context))
|
||||
|
||||
if (matches.length === 0) {
|
||||
context.notClassCache.add(candidate)
|
||||
continue
|
||||
}
|
||||
|
||||
context.classCache.set(candidate, matches)
|
||||
allRules.push(matches)
|
||||
}
|
||||
|
||||
// Strategy based on `tailwindConfig.important`
|
||||
let strategy = ((important) => {
|
||||
if (important === true) {
|
||||
return (rule) => {
|
||||
rule.walkDecls((d) => {
|
||||
if (d.parent.type === 'rule' && !inKeyframes(d.parent)) {
|
||||
d.important = true
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof important === 'string') {
|
||||
return (rule) => {
|
||||
rule.selectors = rule.selectors.map((selector) => {
|
||||
return `${important} ${selector}`
|
||||
})
|
||||
}
|
||||
}
|
||||
})(context.tailwindConfig.important)
|
||||
|
||||
return allRules.flat(1).map(([{ sort, layer, options }, rule]) => {
|
||||
if (options.respectImportant) {
|
||||
if (strategy) {
|
||||
let container = postcss.root({ nodes: [rule.clone()] })
|
||||
container.walkRules((r) => {
|
||||
if (inKeyframes(r)) {
|
||||
return
|
||||
}
|
||||
|
||||
strategy(r)
|
||||
})
|
||||
rule = container.nodes[0]
|
||||
}
|
||||
}
|
||||
|
||||
return [sort | context.layerOrder[layer], rule]
|
||||
})
|
||||
}
|
||||
|
||||
function isArbitraryValue(input) {
|
||||
return input.startsWith('[') && input.endsWith(']')
|
||||
}
|
||||
|
||||
export { resolveMatches, generateRules }
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import resolve from 'resolve'
|
||||
import detective from 'detective'
|
||||
|
||||
function createModule(file) {
|
||||
const source = fs.readFileSync(file, 'utf-8')
|
||||
const requires = detective(source)
|
||||
|
||||
return { file, requires }
|
||||
}
|
||||
|
||||
export default function getModuleDependencies(entryFile) {
|
||||
const rootModule = createModule(entryFile)
|
||||
const modules = [rootModule]
|
||||
|
||||
// Iterate over the modules, even when new
|
||||
// ones are being added
|
||||
for (const mdl of modules) {
|
||||
mdl.requires
|
||||
.filter((dep) => {
|
||||
// Only track local modules, not node_modules
|
||||
return dep.startsWith('./') || dep.startsWith('../')
|
||||
})
|
||||
.forEach((dep) => {
|
||||
try {
|
||||
const basedir = path.dirname(mdl.file)
|
||||
const depPath = resolve.sync(dep, { basedir })
|
||||
const depModule = createModule(depPath)
|
||||
|
||||
modules.push(depModule)
|
||||
} catch (_err) {
|
||||
// eslint-disable-next-line no-empty
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return modules
|
||||
}
|
||||
+84
@@ -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 }
|
||||
}
|
||||
+52
@@ -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)
|
||||
}
|
||||
}
|
||||
+74
@@ -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 || ''
|
||||
}
|
||||
+163
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
+979
@@ -0,0 +1,979 @@
|
||||
import fs from 'fs'
|
||||
import url from 'url'
|
||||
import postcss from 'postcss'
|
||||
import dlv from 'dlv'
|
||||
import selectorParser from 'postcss-selector-parser'
|
||||
|
||||
import { flagEnabled } from '../featureFlags.js'
|
||||
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 bigSign from '../util/bigSign'
|
||||
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 isValidArbitraryValue from '../util/isValidArbitraryValue'
|
||||
import { generateRules } from './generateRules'
|
||||
import { hasContentChanged } from './cacheInvalidation.js'
|
||||
|
||||
let MATCH_VARIANT = Symbol()
|
||||
|
||||
function prefix(context, selector) {
|
||||
let prefix = context.tailwindConfig.prefix
|
||||
return typeof prefix === 'function' ? prefix(selector) : prefix + selector
|
||||
}
|
||||
|
||||
function parseVariantFormatString(input) {
|
||||
if (input.includes('{')) {
|
||||
if (!isBalanced(input)) throw new Error(`Your { and } are unbalanced.`)
|
||||
|
||||
return input
|
||||
.split(/{(.*)}/gim)
|
||||
.flatMap((line) => parseVariantFormatString(line))
|
||||
.filter(Boolean)
|
||||
}
|
||||
|
||||
return [input.trim()]
|
||||
}
|
||||
|
||||
function isBalanced(input) {
|
||||
let count = 0
|
||||
|
||||
for (let char of input) {
|
||||
if (char === '{') {
|
||||
count++
|
||||
} else if (char === '}') {
|
||||
if (--count < 0) {
|
||||
return false // unbalanced
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return count === 0
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
function extractCandidates(node, state = { containsNonOnDemandable: false }, depth = 0) {
|
||||
let classes = []
|
||||
|
||||
// Handle normal rules
|
||||
if (node.type === 'rule') {
|
||||
// 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.
|
||||
function ignoreNot(selectors) {
|
||||
selectors.walkPseudos((pseudo) => {
|
||||
if (pseudo.value === ':not') {
|
||||
pseudo.remove()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
for (let selector of node.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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle at-rules (which contains nested rules)
|
||||
else if (node.type === 'atrule') {
|
||||
node.walkRules((rule) => {
|
||||
for (let classCandidate of rule.selectors.flatMap((selector) => getClasses(selector))) {
|
||||
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] = /@(.*?)( .+|[({].*)/g.exec(str)
|
||||
return ({ wrap }) => wrap(postcss.atRule({ name, params: params.trim() }))
|
||||
})
|
||||
.reverse()
|
||||
|
||||
return (api) => {
|
||||
for (let fn of fns) {
|
||||
fn(api)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 = {}) {
|
||||
const [pathRoot, ...subPaths] = toPath(path)
|
||||
const value = getConfigValue(['theme', pathRoot, ...subPaths], defaultValue)
|
||||
return transformThemeValue(pathRoot)(value, opts)
|
||||
}
|
||||
|
||||
const theme = Object.assign(
|
||||
(path, defaultValue = undefined) => resolveThemeValue(path, defaultValue),
|
||||
{
|
||||
withAlpha: (path, opacityValue) => resolveThemeValue(path, undefined, { opacityValue }),
|
||||
}
|
||||
)
|
||||
|
||||
let api = {
|
||||
postcss,
|
||||
prefix: applyConfiguredPrefix,
|
||||
e: escapeClassName,
|
||||
config: getConfigValue,
|
||||
theme,
|
||||
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.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.base++, 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.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.utilities++, layer: 'utilities', options }, rule])
|
||||
}
|
||||
},
|
||||
matchUtilities: function (utilities, options) {
|
||||
let defaultOptions = {
|
||||
respectPrefix: true,
|
||||
respectImportant: true,
|
||||
}
|
||||
|
||||
options = { ...defaultOptions, ...options }
|
||||
|
||||
let offset = offsets.utilities++
|
||||
|
||||
for (let identifier in utilities) {
|
||||
let prefixedIdentifier = prefixIdentifier(identifier, options)
|
||||
let rule = utilities[identifier]
|
||||
|
||||
classList.add([prefixedIdentifier, options])
|
||||
|
||||
function wrapped(modifier, { isOnlyPlugin }) {
|
||||
let { type = 'any' } = options
|
||||
type = [].concat(type)
|
||||
let [value, coercedType] = coerceValue(type, modifier, options, tailwindConfig)
|
||||
|
||||
if (value === undefined) {
|
||||
return []
|
||||
}
|
||||
|
||||
if (!type.includes(coercedType) && !isOnlyPlugin) {
|
||||
return []
|
||||
}
|
||||
|
||||
if (!isValidArbitraryValue(value)) {
|
||||
return []
|
||||
}
|
||||
|
||||
let ruleSets = []
|
||||
.concat(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,
|
||||
}
|
||||
|
||||
options = { ...defaultOptions, ...options }
|
||||
|
||||
let offset = offsets.components++
|
||||
|
||||
for (let identifier in components) {
|
||||
let prefixedIdentifier = prefixIdentifier(identifier, options)
|
||||
let rule = components[identifier]
|
||||
|
||||
classList.add([prefixedIdentifier, options])
|
||||
|
||||
function wrapped(modifier, { isOnlyPlugin }) {
|
||||
let { type = 'any' } = options
|
||||
type = [].concat(type)
|
||||
let [value, coercedType] = coerceValue(type, modifier, options, tailwindConfig)
|
||||
|
||||
if (value === undefined) {
|
||||
return []
|
||||
}
|
||||
|
||||
if (!type.includes(coercedType)) {
|
||||
if (isOnlyPlugin) {
|
||||
log.warn([
|
||||
`Unnecessary typehint \`${coercedType}\` in \`${identifier}-${modifier}\`.`,
|
||||
`You can safely update it to \`${identifier}-${modifier.replace(
|
||||
coercedType + ':',
|
||||
''
|
||||
)}\`.`,
|
||||
])
|
||||
} else {
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
if (!isValidArbitraryValue(value)) {
|
||||
return []
|
||||
}
|
||||
|
||||
let ruleSets = []
|
||||
.concat(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 },
|
||||
variantFunction[MATCH_VARIANT] && { 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)
|
||||
},
|
||||
}
|
||||
|
||||
if (flagEnabled(tailwindConfig, 'matchVariant')) {
|
||||
api.matchVariant = function (variants, options) {
|
||||
for (let variant in variants) {
|
||||
for (let [k, v] of Object.entries(options?.values ?? {})) {
|
||||
api.addVariant(`${variant}-${k}`, variants[variant](v))
|
||||
}
|
||||
|
||||
api.addVariant(
|
||||
variant,
|
||||
Object.assign(({ args }) => variants[variant](args), { [MATCH_VARIANT]: true }),
|
||||
options
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
fileModifiedMap.set(file, newModified)
|
||||
}
|
||||
|
||||
return changed
|
||||
}
|
||||
|
||||
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'],
|
||||
]
|
||||
let afterVariants = [
|
||||
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()
|
||||
let offsets = {
|
||||
defaults: 0n,
|
||||
base: 0n,
|
||||
components: 0n,
|
||||
utilities: 0n,
|
||||
user: 0n,
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
let highestOffset = ((args) => args.reduce((m, e) => (e > m ? e : m)))([
|
||||
offsets.base,
|
||||
offsets.defaults,
|
||||
offsets.components,
|
||||
offsets.utilities,
|
||||
offsets.user,
|
||||
])
|
||||
let reservedBits = BigInt(highestOffset.toString(2).length)
|
||||
|
||||
// A number one less than the top range of the highest offset area
|
||||
// so arbitrary properties are always sorted at the end.
|
||||
context.arbitraryPropertiesSort = ((1n << reservedBits) << 0n) - 1n
|
||||
|
||||
context.layerOrder = {
|
||||
defaults: (1n << reservedBits) << 0n,
|
||||
base: (1n << reservedBits) << 1n,
|
||||
components: (1n << reservedBits) << 2n,
|
||||
utilities: (1n << reservedBits) << 3n,
|
||||
user: (1n << reservedBits) << 4n,
|
||||
}
|
||||
|
||||
reservedBits += 5n
|
||||
|
||||
let offset = 0
|
||||
context.variantOrder = new Map(
|
||||
variantList
|
||||
.map((variant, i) => {
|
||||
let variantFunctions = variantMap.get(variant).length
|
||||
let bits = (1n << BigInt(i + offset)) << reservedBits
|
||||
offset += variantFunctions - 1
|
||||
return [variant, bits]
|
||||
})
|
||||
.sort(([, a], [, z]) => bigSign(a - z))
|
||||
)
|
||||
|
||||
context.minimumScreen = [...context.variantOrder.values()].shift()
|
||||
|
||||
// Build variantMap
|
||||
for (let [variantName, variantFunctions] of variantMap.entries()) {
|
||||
let sort = context.variantOrder.get(variantName)
|
||||
context.variantMap.set(
|
||||
variantName,
|
||||
variantFunctions.map((variantFunction, idx) => [sort << BigInt(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
|
||||
|
||||
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 ([].concat(options?.type).includes('color')) {
|
||||
classes = [
|
||||
...classes,
|
||||
...classes.flatMap((cls) =>
|
||||
Object.keys(context.tailwindConfig.theme.opacity).map(
|
||||
(opacity) => `${cls}/${opacity}`
|
||||
)
|
||||
),
|
||||
]
|
||||
}
|
||||
|
||||
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',
|
||||
])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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 utitlies work properly. (Thanks Biology)
|
||||
let parasiteUtilities = new Set([prefix(context, 'group'), prefix(context, 'peer')])
|
||||
context.getClassOrder = function getClassOrder(classes) {
|
||||
let sortedClassNames = new Map()
|
||||
for (let [sort, rule] of generateRules(new Set(classes), context)) {
|
||||
if (sortedClassNames.has(rule.raws.tailwind.candidate)) continue
|
||||
sortedClassNames.set(rule.raws.tailwind.candidate, sort)
|
||||
}
|
||||
|
||||
return classes.map((className) => {
|
||||
let order = sortedClassNames.get(className) ?? null
|
||||
|
||||
if (order === null && parasiteUtilities.has(className)) {
|
||||
// This will make sure that it is at the very beginning of the
|
||||
// `components` layer which technically means 'before any
|
||||
// components'.
|
||||
order = context.layerOrder.components
|
||||
}
|
||||
|
||||
return [className, order]
|
||||
})
|
||||
}
|
||||
|
||||
// Generate a list of strings for autocompletion purposes, e.g.
|
||||
// ['uppercase', 'lowercase', ...]
|
||||
context.getClassList = function getClassList() {
|
||||
let output = []
|
||||
|
||||
for (let util of classList) {
|
||||
if (Array.isArray(util)) {
|
||||
let [utilName, options] = util
|
||||
let negativeClasses = []
|
||||
|
||||
for (let [key, value] of Object.entries(options?.values ?? {})) {
|
||||
output.push(formatClass(utilName, key))
|
||||
if (options?.supportsNegativeValues && negateValue(value)) {
|
||||
negativeClasses.push(formatClass(utilName, `-${key}`))
|
||||
}
|
||||
}
|
||||
|
||||
output.push(...negativeClasses)
|
||||
} else {
|
||||
output.push(util)
|
||||
}
|
||||
}
|
||||
|
||||
return output
|
||||
}
|
||||
}
|
||||
|
||||
export function createContext(tailwindConfig, changedContent = [], root = postcss.root()) {
|
||||
let context = {
|
||||
disposables: [],
|
||||
ruleCache: new Set(),
|
||||
classCache: new Map(),
|
||||
applyClassCache: new Map(),
|
||||
notClassCache: new Set(),
|
||||
postCssNodeCache: new Map(),
|
||||
candidateRuleMap: new Map(),
|
||||
tailwindConfig,
|
||||
changedContent: changedContent,
|
||||
variantMap: new Map(),
|
||||
stylesheetCache: null,
|
||||
}
|
||||
|
||||
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 = trackModified(
|
||||
[...contextDependencies],
|
||||
getFileModifiedMap(existingContext)
|
||||
)
|
||||
if (!contextDependenciesChanged && !cssDidChange) {
|
||||
return [existingContext, false]
|
||||
}
|
||||
}
|
||||
|
||||
// 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)
|
||||
|
||||
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]
|
||||
}
|
||||
+187
@@ -0,0 +1,187 @@
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
|
||||
import fastGlob from 'fast-glob'
|
||||
import LRU from 'quick-lru'
|
||||
import normalizePath from 'normalize-path'
|
||||
|
||||
import hash from '../util/hashConfig'
|
||||
import getModuleDependencies from '../lib/getModuleDependencies'
|
||||
|
||||
import resolveConfig from '../public/resolve-config'
|
||||
|
||||
import resolveConfigPath from '../util/resolveConfigPath'
|
||||
|
||||
import { env } from './sharedState'
|
||||
|
||||
import { getContext, getFileModifiedMap } from './setupContextUtils'
|
||||
import parseDependency from '../util/parseDependency'
|
||||
import { validateConfig } from '../util/validateConfig.js'
|
||||
|
||||
let configPathCache = new LRU({ maxSize: 100 })
|
||||
|
||||
let candidateFilesCache = new WeakMap()
|
||||
|
||||
function getCandidateFiles(context, tailwindConfig) {
|
||||
if (candidateFilesCache.has(context)) {
|
||||
return candidateFilesCache.get(context)
|
||||
}
|
||||
|
||||
let candidateFiles = tailwindConfig.content.files
|
||||
.filter((item) => typeof item === 'string')
|
||||
.map((contentPath) => normalizePath(contentPath))
|
||||
|
||||
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).map((dep) => dep.file)
|
||||
|
||||
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 = resolveConfig(require(userConfigPath))
|
||||
newConfig = validateConfig(newConfig)
|
||||
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 === undefined ? configOrPath : configOrPath.config
|
||||
)
|
||||
|
||||
newConfig = validateConfig(newConfig)
|
||||
|
||||
return [newConfig, null, hash(newConfig), []]
|
||||
}
|
||||
|
||||
function resolvedChangedContent(context, candidateFiles, fileModifiedMap) {
|
||||
let changedContent = context.tailwindConfig.content.files
|
||||
.filter((item) => typeof item.raw === 'string')
|
||||
.map(({ raw, extension = 'html' }) => ({ content: raw, extension }))
|
||||
|
||||
for (let changedFile of resolveChangedFiles(candidateFiles, fileModifiedMap)) {
|
||||
let content = fs.readFileSync(changedFile, 'utf8')
|
||||
let extension = path.extname(changedFile).slice(1)
|
||||
changedContent.push({ content, extension })
|
||||
}
|
||||
return changedContent
|
||||
}
|
||||
|
||||
function resolveChangedFiles(candidateFiles, fileModifiedMap) {
|
||||
let changedFiles = new Set()
|
||||
env.DEBUG && console.time('Finding changed files')
|
||||
let files = fastGlob.sync(candidateFiles)
|
||||
for (let file of files) {
|
||||
let prevModified = fileModifiedMap.has(file) ? fileModifiedMap.get(file) : -Infinity
|
||||
let modified = fs.statSync(file).mtimeMs
|
||||
|
||||
if (modified > prevModified) {
|
||||
changedFiles.add(file)
|
||||
fileModifiedMap.set(file, modified)
|
||||
}
|
||||
}
|
||||
env.DEBUG && console.timeEnd('Finding changed files')
|
||||
return changedFiles
|
||||
}
|
||||
|
||||
// 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] = getContext(
|
||||
root,
|
||||
result,
|
||||
tailwindConfig,
|
||||
userConfigPath,
|
||||
tailwindConfigHash,
|
||||
contextDependencies
|
||||
)
|
||||
|
||||
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) {
|
||||
let fileModifiedMap = getFileModifiedMap(context)
|
||||
|
||||
// Add template paths as postcss dependencies.
|
||||
for (let fileOrGlob of candidateFiles) {
|
||||
let dependency = parseDependency(fileOrGlob)
|
||||
if (dependency) {
|
||||
registerDependency(dependency)
|
||||
}
|
||||
}
|
||||
|
||||
for (let changedContent of resolvedChangedContent(
|
||||
context,
|
||||
candidateFiles,
|
||||
fileModifiedMap
|
||||
)) {
|
||||
context.changedContent.push(changedContent)
|
||||
}
|
||||
}
|
||||
|
||||
for (let file of configDependencies) {
|
||||
registerDependency({ type: 'dependency', file })
|
||||
}
|
||||
|
||||
return context
|
||||
}
|
||||
}
|
||||
}
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
export const env = {
|
||||
NODE_ENV: process.env.NODE_ENV,
|
||||
DEBUG: resolveDebug(process.env.DEBUG),
|
||||
}
|
||||
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 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
|
||||
}
|
||||
+19
@@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
# tailwindcss/nesting
|
||||
|
||||
This is a PostCSS plugin that wraps [postcss-nested](https://github.com/postcss/postcss-nested) or [postcss-nesting](https://github.com/csstools/postcss-plugins/tree/main/plugins/postcss-nesting) and acts as a compatibility layer to make sure your nesting plugin of choice properly understands Tailwind's custom syntax like `@apply` and `@screen`.
|
||||
|
||||
Add it to your PostCSS configuration, somewhere before Tailwind itself:
|
||||
|
||||
```js
|
||||
// postcss.config.js
|
||||
module.exports = {
|
||||
plugins: [
|
||||
require('postcss-import'),
|
||||
require('tailwindcss/nesting'),
|
||||
require('tailwindcss'),
|
||||
require('autoprefixer'),
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
By default, it uses the [postcss-nested](https://github.com/postcss/postcss-nested) plugin under the hood, which uses a Sass-like syntax and is the plugin that powers nesting support in the [Tailwind CSS plugin API](https://tailwindcss.com/docs/plugins#css-in-js-syntax).
|
||||
|
||||
If you'd rather use [postcss-nesting](https://github.com/csstools/postcss-plugins/tree/main/plugins/postcss-nesting) (which is based on the work-in-progress [CSS Nesting](https://drafts.csswg.org/css-nesting-1/) specification), first install the plugin alongside:
|
||||
|
||||
```shell
|
||||
npm install postcss-nesting
|
||||
```
|
||||
|
||||
Then pass the plugin itself as an argument to `tailwindcss/nesting` in your PostCSS configuration:
|
||||
|
||||
```js
|
||||
// postcss.config.js
|
||||
module.exports = {
|
||||
plugins: [
|
||||
require('postcss-import'),
|
||||
require('tailwindcss/nesting')(require('postcss-nesting')),
|
||||
require('tailwindcss'),
|
||||
require('autoprefixer'),
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
This can also be helpful if for whatever reason you need to use a very specific version of `postcss-nested` and want to override the version we bundle with `tailwindcss/nesting` itself.
|
||||
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
import { nesting } from './plugin'
|
||||
|
||||
export default Object.assign(
|
||||
function (opts) {
|
||||
return {
|
||||
postcssPlugin: 'tailwindcss/nesting',
|
||||
Once(root, { result }) {
|
||||
return nesting(opts)(root, result)
|
||||
},
|
||||
}
|
||||
},
|
||||
{ postcss: true }
|
||||
)
|
||||
+80
@@ -0,0 +1,80 @@
|
||||
import postcss from 'postcss'
|
||||
import postcssNested from 'postcss-nested'
|
||||
|
||||
export function nesting(opts = postcssNested) {
|
||||
return (root, result) => {
|
||||
root.walkAtRules('screen', (rule) => {
|
||||
rule.name = 'media'
|
||||
rule.params = `screen(${rule.params})`
|
||||
})
|
||||
|
||||
root.walkAtRules('apply', (rule) => {
|
||||
rule.before(postcss.decl({ prop: '__apply', value: rule.params, source: rule.source }))
|
||||
rule.remove()
|
||||
})
|
||||
|
||||
let plugin = (() => {
|
||||
if (
|
||||
typeof opts === 'function' ||
|
||||
(typeof opts === 'object' && opts?.hasOwnProperty?.('postcssPlugin'))
|
||||
) {
|
||||
return opts
|
||||
}
|
||||
|
||||
if (typeof opts === 'string') {
|
||||
return require(opts)
|
||||
}
|
||||
|
||||
if (Object.keys(opts).length <= 0) {
|
||||
return postcssNested
|
||||
}
|
||||
|
||||
throw new Error('tailwindcss/nesting should be loaded with a nesting plugin.')
|
||||
})()
|
||||
|
||||
postcss([plugin]).process(root, result.opts).sync()
|
||||
|
||||
root.walkDecls('__apply', (decl) => {
|
||||
decl.before(postcss.atRule({ name: 'apply', params: decl.value, source: decl.source }))
|
||||
decl.remove()
|
||||
})
|
||||
|
||||
/**
|
||||
* Use a private PostCSS API to remove the "clean" flag from the entire AST.
|
||||
* This is done because running process() on the AST will set the "clean"
|
||||
* flag on all nodes, which we don't want.
|
||||
*
|
||||
* This causes downstream plugins using the visitor API to be skipped.
|
||||
*
|
||||
* This is guarded because the PostCSS API is not public
|
||||
* and may change in future versions of PostCSS.
|
||||
*
|
||||
* See https://github.com/postcss/postcss/issues/1712 for more details
|
||||
*
|
||||
* @param {import('postcss').Node} node
|
||||
*/
|
||||
function markDirty(node) {
|
||||
if (!('markDirty' in node)) {
|
||||
return
|
||||
}
|
||||
|
||||
// Traverse the tree down to the leaf nodes
|
||||
if (node.nodes) {
|
||||
node.nodes.forEach((n) => markDirty(n))
|
||||
}
|
||||
|
||||
// If it's a leaf node mark it as dirty
|
||||
// We do this here because marking a node as dirty
|
||||
// will walk up the tree and mark all parents as dirty
|
||||
// resulting in a lot of unnecessary work if we did this
|
||||
// for every single node
|
||||
if (!node.nodes) {
|
||||
node.markDirty()
|
||||
}
|
||||
}
|
||||
|
||||
markDirty(root)
|
||||
|
||||
return root
|
||||
}
|
||||
}
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
import normalizeTailwindDirectives from './lib/normalizeTailwindDirectives'
|
||||
import expandTailwindAtRules from './lib/expandTailwindAtRules'
|
||||
import expandApplyAtRules from './lib/expandApplyAtRules'
|
||||
import evaluateTailwindFunctions from './lib/evaluateTailwindFunctions'
|
||||
import substituteScreenAtRules from './lib/substituteScreenAtRules'
|
||||
import resolveDefaultsAtRules from './lib/resolveDefaultsAtRules'
|
||||
import collapseAdjacentRules from './lib/collapseAdjacentRules'
|
||||
import collapseDuplicateDeclarations from './lib/collapseDuplicateDeclarations'
|
||||
import partitionApplyAtRules from './lib/partitionApplyAtRules'
|
||||
import detectNesting from './lib/detectNesting'
|
||||
import { createContext } from './lib/setupContextUtils'
|
||||
import { issueFlagNotices } from './featureFlags'
|
||||
|
||||
export default function processTailwindFeatures(setupContext) {
|
||||
return function (root, result) {
|
||||
let { tailwindDirectives, applyDirectives } = normalizeTailwindDirectives(root)
|
||||
|
||||
detectNesting()(root, result)
|
||||
|
||||
// Partition apply rules that are found in the css
|
||||
// itself.
|
||||
partitionApplyAtRules()(root, result)
|
||||
|
||||
let context = setupContext({
|
||||
tailwindDirectives,
|
||||
applyDirectives,
|
||||
registerDependency(dependency) {
|
||||
result.messages.push({
|
||||
plugin: 'tailwindcss',
|
||||
parent: result.opts.from,
|
||||
...dependency,
|
||||
})
|
||||
},
|
||||
createContext(tailwindConfig, changedContent) {
|
||||
return createContext(tailwindConfig, changedContent, root)
|
||||
},
|
||||
})(root, result)
|
||||
|
||||
if (context.tailwindConfig.separator === '-') {
|
||||
throw new Error(
|
||||
"The '-' character cannot be used as a custom separator in JIT mode due to parsing ambiguity. Please use another character like '_' instead."
|
||||
)
|
||||
}
|
||||
|
||||
issueFlagNotices(context.tailwindConfig)
|
||||
|
||||
expandTailwindAtRules(context)(root, result)
|
||||
// Partition apply rules that are generated by
|
||||
// addComponents, addUtilities and so on.
|
||||
partitionApplyAtRules()(root, result)
|
||||
expandApplyAtRules(context)(root, result)
|
||||
evaluateTailwindFunctions(context)(root, result)
|
||||
substituteScreenAtRules(context)(root, result)
|
||||
resolveDefaultsAtRules(context)(root, result)
|
||||
collapseAdjacentRules(context)(root, result)
|
||||
collapseDuplicateDeclarations(context)(root, result)
|
||||
}
|
||||
}
|
||||
+300
@@ -0,0 +1,300 @@
|
||||
import log from '../util/log'
|
||||
|
||||
function warn({ version, from, to }) {
|
||||
log.warn(`${from}-color-renamed`, [
|
||||
`As of Tailwind CSS ${version}, \`${from}\` has been renamed to \`${to}\`.`,
|
||||
'Update your configuration file to silence this warning.',
|
||||
])
|
||||
}
|
||||
|
||||
export default {
|
||||
inherit: 'inherit',
|
||||
current: 'currentColor',
|
||||
transparent: 'transparent',
|
||||
black: '#000',
|
||||
white: '#fff',
|
||||
slate: {
|
||||
50: '#f8fafc',
|
||||
100: '#f1f5f9',
|
||||
200: '#e2e8f0',
|
||||
300: '#cbd5e1',
|
||||
400: '#94a3b8',
|
||||
500: '#64748b',
|
||||
600: '#475569',
|
||||
700: '#334155',
|
||||
800: '#1e293b',
|
||||
900: '#0f172a',
|
||||
},
|
||||
gray: {
|
||||
50: '#f9fafb',
|
||||
100: '#f3f4f6',
|
||||
200: '#e5e7eb',
|
||||
300: '#d1d5db',
|
||||
400: '#9ca3af',
|
||||
500: '#6b7280',
|
||||
600: '#4b5563',
|
||||
700: '#374151',
|
||||
800: '#1f2937',
|
||||
900: '#111827',
|
||||
},
|
||||
zinc: {
|
||||
50: '#fafafa',
|
||||
100: '#f4f4f5',
|
||||
200: '#e4e4e7',
|
||||
300: '#d4d4d8',
|
||||
400: '#a1a1aa',
|
||||
500: '#71717a',
|
||||
600: '#52525b',
|
||||
700: '#3f3f46',
|
||||
800: '#27272a',
|
||||
900: '#18181b',
|
||||
},
|
||||
neutral: {
|
||||
50: '#fafafa',
|
||||
100: '#f5f5f5',
|
||||
200: '#e5e5e5',
|
||||
300: '#d4d4d4',
|
||||
400: '#a3a3a3',
|
||||
500: '#737373',
|
||||
600: '#525252',
|
||||
700: '#404040',
|
||||
800: '#262626',
|
||||
900: '#171717',
|
||||
},
|
||||
stone: {
|
||||
50: '#fafaf9',
|
||||
100: '#f5f5f4',
|
||||
200: '#e7e5e4',
|
||||
300: '#d6d3d1',
|
||||
400: '#a8a29e',
|
||||
500: '#78716c',
|
||||
600: '#57534e',
|
||||
700: '#44403c',
|
||||
800: '#292524',
|
||||
900: '#1c1917',
|
||||
},
|
||||
red: {
|
||||
50: '#fef2f2',
|
||||
100: '#fee2e2',
|
||||
200: '#fecaca',
|
||||
300: '#fca5a5',
|
||||
400: '#f87171',
|
||||
500: '#ef4444',
|
||||
600: '#dc2626',
|
||||
700: '#b91c1c',
|
||||
800: '#991b1b',
|
||||
900: '#7f1d1d',
|
||||
},
|
||||
orange: {
|
||||
50: '#fff7ed',
|
||||
100: '#ffedd5',
|
||||
200: '#fed7aa',
|
||||
300: '#fdba74',
|
||||
400: '#fb923c',
|
||||
500: '#f97316',
|
||||
600: '#ea580c',
|
||||
700: '#c2410c',
|
||||
800: '#9a3412',
|
||||
900: '#7c2d12',
|
||||
},
|
||||
amber: {
|
||||
50: '#fffbeb',
|
||||
100: '#fef3c7',
|
||||
200: '#fde68a',
|
||||
300: '#fcd34d',
|
||||
400: '#fbbf24',
|
||||
500: '#f59e0b',
|
||||
600: '#d97706',
|
||||
700: '#b45309',
|
||||
800: '#92400e',
|
||||
900: '#78350f',
|
||||
},
|
||||
yellow: {
|
||||
50: '#fefce8',
|
||||
100: '#fef9c3',
|
||||
200: '#fef08a',
|
||||
300: '#fde047',
|
||||
400: '#facc15',
|
||||
500: '#eab308',
|
||||
600: '#ca8a04',
|
||||
700: '#a16207',
|
||||
800: '#854d0e',
|
||||
900: '#713f12',
|
||||
},
|
||||
lime: {
|
||||
50: '#f7fee7',
|
||||
100: '#ecfccb',
|
||||
200: '#d9f99d',
|
||||
300: '#bef264',
|
||||
400: '#a3e635',
|
||||
500: '#84cc16',
|
||||
600: '#65a30d',
|
||||
700: '#4d7c0f',
|
||||
800: '#3f6212',
|
||||
900: '#365314',
|
||||
},
|
||||
green: {
|
||||
50: '#f0fdf4',
|
||||
100: '#dcfce7',
|
||||
200: '#bbf7d0',
|
||||
300: '#86efac',
|
||||
400: '#4ade80',
|
||||
500: '#22c55e',
|
||||
600: '#16a34a',
|
||||
700: '#15803d',
|
||||
800: '#166534',
|
||||
900: '#14532d',
|
||||
},
|
||||
emerald: {
|
||||
50: '#ecfdf5',
|
||||
100: '#d1fae5',
|
||||
200: '#a7f3d0',
|
||||
300: '#6ee7b7',
|
||||
400: '#34d399',
|
||||
500: '#10b981',
|
||||
600: '#059669',
|
||||
700: '#047857',
|
||||
800: '#065f46',
|
||||
900: '#064e3b',
|
||||
},
|
||||
teal: {
|
||||
50: '#f0fdfa',
|
||||
100: '#ccfbf1',
|
||||
200: '#99f6e4',
|
||||
300: '#5eead4',
|
||||
400: '#2dd4bf',
|
||||
500: '#14b8a6',
|
||||
600: '#0d9488',
|
||||
700: '#0f766e',
|
||||
800: '#115e59',
|
||||
900: '#134e4a',
|
||||
},
|
||||
cyan: {
|
||||
50: '#ecfeff',
|
||||
100: '#cffafe',
|
||||
200: '#a5f3fc',
|
||||
300: '#67e8f9',
|
||||
400: '#22d3ee',
|
||||
500: '#06b6d4',
|
||||
600: '#0891b2',
|
||||
700: '#0e7490',
|
||||
800: '#155e75',
|
||||
900: '#164e63',
|
||||
},
|
||||
sky: {
|
||||
50: '#f0f9ff',
|
||||
100: '#e0f2fe',
|
||||
200: '#bae6fd',
|
||||
300: '#7dd3fc',
|
||||
400: '#38bdf8',
|
||||
500: '#0ea5e9',
|
||||
600: '#0284c7',
|
||||
700: '#0369a1',
|
||||
800: '#075985',
|
||||
900: '#0c4a6e',
|
||||
},
|
||||
blue: {
|
||||
50: '#eff6ff',
|
||||
100: '#dbeafe',
|
||||
200: '#bfdbfe',
|
||||
300: '#93c5fd',
|
||||
400: '#60a5fa',
|
||||
500: '#3b82f6',
|
||||
600: '#2563eb',
|
||||
700: '#1d4ed8',
|
||||
800: '#1e40af',
|
||||
900: '#1e3a8a',
|
||||
},
|
||||
indigo: {
|
||||
50: '#eef2ff',
|
||||
100: '#e0e7ff',
|
||||
200: '#c7d2fe',
|
||||
300: '#a5b4fc',
|
||||
400: '#818cf8',
|
||||
500: '#6366f1',
|
||||
600: '#4f46e5',
|
||||
700: '#4338ca',
|
||||
800: '#3730a3',
|
||||
900: '#312e81',
|
||||
},
|
||||
violet: {
|
||||
50: '#f5f3ff',
|
||||
100: '#ede9fe',
|
||||
200: '#ddd6fe',
|
||||
300: '#c4b5fd',
|
||||
400: '#a78bfa',
|
||||
500: '#8b5cf6',
|
||||
600: '#7c3aed',
|
||||
700: '#6d28d9',
|
||||
800: '#5b21b6',
|
||||
900: '#4c1d95',
|
||||
},
|
||||
purple: {
|
||||
50: '#faf5ff',
|
||||
100: '#f3e8ff',
|
||||
200: '#e9d5ff',
|
||||
300: '#d8b4fe',
|
||||
400: '#c084fc',
|
||||
500: '#a855f7',
|
||||
600: '#9333ea',
|
||||
700: '#7e22ce',
|
||||
800: '#6b21a8',
|
||||
900: '#581c87',
|
||||
},
|
||||
fuchsia: {
|
||||
50: '#fdf4ff',
|
||||
100: '#fae8ff',
|
||||
200: '#f5d0fe',
|
||||
300: '#f0abfc',
|
||||
400: '#e879f9',
|
||||
500: '#d946ef',
|
||||
600: '#c026d3',
|
||||
700: '#a21caf',
|
||||
800: '#86198f',
|
||||
900: '#701a75',
|
||||
},
|
||||
pink: {
|
||||
50: '#fdf2f8',
|
||||
100: '#fce7f3',
|
||||
200: '#fbcfe8',
|
||||
300: '#f9a8d4',
|
||||
400: '#f472b6',
|
||||
500: '#ec4899',
|
||||
600: '#db2777',
|
||||
700: '#be185d',
|
||||
800: '#9d174d',
|
||||
900: '#831843',
|
||||
},
|
||||
rose: {
|
||||
50: '#fff1f2',
|
||||
100: '#ffe4e6',
|
||||
200: '#fecdd3',
|
||||
300: '#fda4af',
|
||||
400: '#fb7185',
|
||||
500: '#f43f5e',
|
||||
600: '#e11d48',
|
||||
700: '#be123c',
|
||||
800: '#9f1239',
|
||||
900: '#881337',
|
||||
},
|
||||
get lightBlue() {
|
||||
warn({ version: 'v2.2', from: 'lightBlue', to: 'sky' })
|
||||
return this.sky
|
||||
},
|
||||
get warmGray() {
|
||||
warn({ version: 'v3.0', from: 'warmGray', to: 'stone' })
|
||||
return this.stone
|
||||
},
|
||||
get trueGray() {
|
||||
warn({ version: 'v3.0', from: 'trueGray', to: 'neutral' })
|
||||
return this.neutral
|
||||
},
|
||||
get coolGray() {
|
||||
warn({ version: 'v3.0', from: 'coolGray', to: 'gray' })
|
||||
return this.gray
|
||||
},
|
||||
get blueGray() {
|
||||
warn({ version: 'v3.0', from: 'blueGray', to: 'slate' })
|
||||
return this.slate
|
||||
},
|
||||
}
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
import createPlugin from '../util/createPlugin'
|
||||
export default createPlugin
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
import { cloneDeep } from '../util/cloneDeep'
|
||||
import defaultConfig from '../../stubs/defaultConfig.stub'
|
||||
|
||||
export default cloneDeep(defaultConfig)
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
import { cloneDeep } from '../util/cloneDeep'
|
||||
import defaultConfig from '../../stubs/defaultConfig.stub'
|
||||
|
||||
export default cloneDeep(defaultConfig.theme)
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
import resolveConfigObjects from '../util/resolveConfig'
|
||||
import getAllConfigs from '../util/getAllConfigs'
|
||||
|
||||
export default function resolveConfig(...configs) {
|
||||
let [, ...defaultConfigs] = getAllConfigs(configs[0])
|
||||
return resolveConfigObjects([...configs, ...defaultConfigs])
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
export default function bigSign(bigIntValue) {
|
||||
return (bigIntValue > 0n) - (bigIntValue < 0n)
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
export default function buildMediaQuery(screens) {
|
||||
screens = Array.isArray(screens) ? screens : [screens]
|
||||
|
||||
return screens
|
||||
.map((screen) =>
|
||||
screen.values.map((screen) => {
|
||||
if (screen.raw !== undefined) {
|
||||
return screen.raw
|
||||
}
|
||||
|
||||
return [
|
||||
screen.min && `(min-width: ${screen.min})`,
|
||||
screen.max && `(max-width: ${screen.max})`,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' and ')
|
||||
})
|
||||
)
|
||||
.join(', ')
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
export function cloneDeep(value) {
|
||||
if (Array.isArray(value)) {
|
||||
return value.map((child) => cloneDeep(child))
|
||||
}
|
||||
|
||||
if (typeof value === 'object' && value !== null) {
|
||||
return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, cloneDeep(v)]))
|
||||
}
|
||||
|
||||
return value
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
export default function cloneNodes(nodes, source = undefined, raws = undefined) {
|
||||
return nodes.map((node) => {
|
||||
let cloned = node.clone()
|
||||
|
||||
// We always want override the source map
|
||||
// except when explicitly told not to
|
||||
let shouldOverwriteSource = node.raws.tailwind?.preserveSource !== true || !cloned.source
|
||||
|
||||
if (source !== undefined && shouldOverwriteSource) {
|
||||
cloned.source = source
|
||||
|
||||
if ('walk' in cloned) {
|
||||
cloned.walk((child) => {
|
||||
child.source = source
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (raws !== undefined) {
|
||||
cloned.raws.tailwind = {
|
||||
...cloned.raws.tailwind,
|
||||
...raws,
|
||||
}
|
||||
}
|
||||
|
||||
return cloned
|
||||
})
|
||||
}
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
import namedColors from 'color-name'
|
||||
|
||||
let HEX = /^#([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})?$/i
|
||||
let SHORT_HEX = /^#([a-f\d])([a-f\d])([a-f\d])([a-f\d])?$/i
|
||||
let VALUE = /(?:\d+|\d*\.\d+)%?/
|
||||
let SEP = /(?:\s*,\s*|\s+)/
|
||||
let ALPHA_SEP = /\s*[,/]\s*/
|
||||
let CUSTOM_PROPERTY = /var\(--(?:[^ )]*?)\)/
|
||||
|
||||
let RGB = new RegExp(
|
||||
`^(rgb)a?\\(\\s*(${VALUE.source}|${CUSTOM_PROPERTY.source})(?:${SEP.source}(${VALUE.source}|${CUSTOM_PROPERTY.source}))?(?:${SEP.source}(${VALUE.source}|${CUSTOM_PROPERTY.source}))?(?:${ALPHA_SEP.source}(${VALUE.source}|${CUSTOM_PROPERTY.source}))?\\s*\\)$`
|
||||
)
|
||||
let HSL = new RegExp(
|
||||
`^(hsl)a?\\(\\s*((?:${VALUE.source})(?:deg|rad|grad|turn)?|${CUSTOM_PROPERTY.source})(?:${SEP.source}(${VALUE.source}|${CUSTOM_PROPERTY.source}))?(?:${SEP.source}(${VALUE.source}|${CUSTOM_PROPERTY.source}))?(?:${ALPHA_SEP.source}(${VALUE.source}|${CUSTOM_PROPERTY.source}))?\\s*\\)$`
|
||||
)
|
||||
|
||||
// In "loose" mode the color may contain fewer than 3 parts, as long as at least
|
||||
// one of the parts is variable.
|
||||
export function parseColor(value, { loose = false } = {}) {
|
||||
if (typeof value !== 'string') {
|
||||
return null
|
||||
}
|
||||
|
||||
value = value.trim()
|
||||
if (value === 'transparent') {
|
||||
return { mode: 'rgb', color: ['0', '0', '0'], alpha: '0' }
|
||||
}
|
||||
|
||||
if (value in namedColors) {
|
||||
return { mode: 'rgb', color: namedColors[value].map((v) => v.toString()) }
|
||||
}
|
||||
|
||||
let hex = value
|
||||
.replace(SHORT_HEX, (_, r, g, b, a) => ['#', r, r, g, g, b, b, a ? a + a : ''].join(''))
|
||||
.match(HEX)
|
||||
|
||||
if (hex !== null) {
|
||||
return {
|
||||
mode: 'rgb',
|
||||
color: [parseInt(hex[1], 16), parseInt(hex[2], 16), parseInt(hex[3], 16)].map((v) =>
|
||||
v.toString()
|
||||
),
|
||||
alpha: hex[4] ? (parseInt(hex[4], 16) / 255).toString() : undefined,
|
||||
}
|
||||
}
|
||||
|
||||
let match = value.match(RGB) ?? value.match(HSL)
|
||||
|
||||
if (match === null) {
|
||||
return null
|
||||
}
|
||||
|
||||
let color = [match[2], match[3], match[4]].filter(Boolean).map((v) => v.toString())
|
||||
|
||||
if (!loose && color.length !== 3) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (color.length < 3 && !color.some((part) => /^var\(.*?\)$/.test(part))) {
|
||||
return null
|
||||
}
|
||||
|
||||
return {
|
||||
mode: match[1],
|
||||
color,
|
||||
alpha: match[5]?.toString?.(),
|
||||
}
|
||||
}
|
||||
|
||||
export function formatColor({ mode, color, alpha }) {
|
||||
let hasAlpha = alpha !== undefined
|
||||
return `${mode}(${color.join(' ')}${hasAlpha ? ` / ${alpha}` : ''})`
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
export default function (pluginConfig, plugins) {
|
||||
if (pluginConfig === undefined) {
|
||||
return plugins
|
||||
}
|
||||
|
||||
const pluginNames = Array.isArray(pluginConfig)
|
||||
? pluginConfig
|
||||
: [
|
||||
...new Set(
|
||||
plugins
|
||||
.filter((pluginName) => {
|
||||
return pluginConfig !== false && pluginConfig[pluginName] !== false
|
||||
})
|
||||
.concat(
|
||||
Object.keys(pluginConfig).filter((pluginName) => {
|
||||
return pluginConfig[pluginName] !== false
|
||||
})
|
||||
)
|
||||
),
|
||||
]
|
||||
|
||||
return pluginNames
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
function createPlugin(plugin, config) {
|
||||
return {
|
||||
handler: plugin,
|
||||
config,
|
||||
}
|
||||
}
|
||||
|
||||
createPlugin.withOptions = function (pluginFunction, configFunction = () => ({})) {
|
||||
const optionsFunction = function (options) {
|
||||
return {
|
||||
__options: options,
|
||||
handler: pluginFunction(options),
|
||||
config: configFunction(options),
|
||||
}
|
||||
}
|
||||
|
||||
optionsFunction.__isOptionsFunction = true
|
||||
|
||||
// Expose plugin dependencies so that `object-hash` returns a different
|
||||
// value if anything here changes, to ensure a rebuild is triggered.
|
||||
optionsFunction.__pluginFunction = pluginFunction
|
||||
optionsFunction.__configFunction = configFunction
|
||||
|
||||
return optionsFunction
|
||||
}
|
||||
|
||||
export default createPlugin
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
import transformThemeValue from './transformThemeValue'
|
||||
|
||||
export default function createUtilityPlugin(
|
||||
themeKey,
|
||||
utilityVariations = [[themeKey, [themeKey]]],
|
||||
{ filterDefault = false, ...options } = {}
|
||||
) {
|
||||
let transformValue = transformThemeValue(themeKey)
|
||||
return function ({ matchUtilities, theme }) {
|
||||
for (let utilityVariation of utilityVariations) {
|
||||
let group = Array.isArray(utilityVariation[0]) ? utilityVariation : [utilityVariation]
|
||||
|
||||
matchUtilities(
|
||||
group.reduce((obj, [classPrefix, properties]) => {
|
||||
return Object.assign(obj, {
|
||||
[classPrefix]: (value) => {
|
||||
return properties.reduce((obj, name) => {
|
||||
if (Array.isArray(name)) {
|
||||
return Object.assign(obj, { [name[0]]: name[1] })
|
||||
}
|
||||
return Object.assign(obj, { [name]: transformValue(value) })
|
||||
}, {})
|
||||
},
|
||||
})
|
||||
}, {}),
|
||||
{
|
||||
...options,
|
||||
values: filterDefault
|
||||
? Object.fromEntries(
|
||||
Object.entries(theme(themeKey) ?? {}).filter(([modifier]) => modifier !== 'DEFAULT')
|
||||
)
|
||||
: theme(themeKey),
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
+254
@@ -0,0 +1,254 @@
|
||||
import { parseColor } from './color'
|
||||
import { parseBoxShadowValue } from './parseBoxShadowValue'
|
||||
|
||||
let cssFunctions = ['min', 'max', 'clamp', 'calc']
|
||||
|
||||
// Ref: https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Types
|
||||
|
||||
let COMMA = /,(?![^(]*\))/g // Comma separator that is not located between brackets. E.g.: `cubiz-bezier(a, b, c)` these don't count.
|
||||
let UNDERSCORE = /_(?![^(]*\))/g // Underscore separator that is not located between brackets. E.g.: `rgba(255,_255,_255)_black` these don't count.
|
||||
|
||||
// This is not a data type, but rather a function that can normalize the
|
||||
// correct values.
|
||||
export function normalize(value, isRoot = true) {
|
||||
// Keep raw strings if it starts with `url(`
|
||||
if (value.includes('url(')) {
|
||||
return value
|
||||
.split(/(url\(.*?\))/g)
|
||||
.filter(Boolean)
|
||||
.map((part) => {
|
||||
if (/^url\(.*?\)$/.test(part)) {
|
||||
return part
|
||||
}
|
||||
|
||||
return normalize(part, false)
|
||||
})
|
||||
.join('')
|
||||
}
|
||||
|
||||
// Convert `_` to ` `, except for escaped underscores `\_`
|
||||
value = value
|
||||
.replace(
|
||||
/([^\\])_+/g,
|
||||
(fullMatch, characterBefore) => characterBefore + ' '.repeat(fullMatch.length - 1)
|
||||
)
|
||||
.replace(/^_/g, ' ')
|
||||
.replace(/\\_/g, '_')
|
||||
|
||||
// Remove leftover whitespace
|
||||
if (isRoot) {
|
||||
value = value.trim()
|
||||
}
|
||||
|
||||
// Add spaces around operators inside math functions like calc() that do not follow an operator
|
||||
// or '('.
|
||||
value = value.replace(/(calc|min|max|clamp)\(.+\)/g, (match) => {
|
||||
return match.replace(
|
||||
/(-?\d*\.?\d(?!\b-.+[,)](?![^+\-/*])\D)(?:%|[a-z]+)?|\))([+\-/*])/g,
|
||||
'$1 $2 '
|
||||
)
|
||||
})
|
||||
|
||||
return value
|
||||
}
|
||||
|
||||
export function url(value) {
|
||||
return value.startsWith('url(')
|
||||
}
|
||||
|
||||
export function number(value) {
|
||||
return !isNaN(Number(value)) || cssFunctions.some((fn) => new RegExp(`^${fn}\\(.+?`).test(value))
|
||||
}
|
||||
|
||||
export function percentage(value) {
|
||||
return value.split(UNDERSCORE).every((part) => {
|
||||
return /%$/g.test(part) || cssFunctions.some((fn) => new RegExp(`^${fn}\\(.+?%`).test(part))
|
||||
})
|
||||
}
|
||||
|
||||
let lengthUnits = [
|
||||
'cm',
|
||||
'mm',
|
||||
'Q',
|
||||
'in',
|
||||
'pc',
|
||||
'pt',
|
||||
'px',
|
||||
'em',
|
||||
'ex',
|
||||
'ch',
|
||||
'rem',
|
||||
'lh',
|
||||
'vw',
|
||||
'vh',
|
||||
'vmin',
|
||||
'vmax',
|
||||
]
|
||||
let lengthUnitsPattern = `(?:${lengthUnits.join('|')})`
|
||||
export function length(value) {
|
||||
return value.split(UNDERSCORE).every((part) => {
|
||||
return (
|
||||
part === '0' ||
|
||||
new RegExp(`${lengthUnitsPattern}$`).test(part) ||
|
||||
cssFunctions.some((fn) => new RegExp(`^${fn}\\(.+?${lengthUnitsPattern}`).test(part))
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
let lineWidths = new Set(['thin', 'medium', 'thick'])
|
||||
export function lineWidth(value) {
|
||||
return lineWidths.has(value)
|
||||
}
|
||||
|
||||
export function shadow(value) {
|
||||
let parsedShadows = parseBoxShadowValue(normalize(value))
|
||||
|
||||
for (let parsedShadow of parsedShadows) {
|
||||
if (!parsedShadow.valid) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
export function color(value) {
|
||||
let colors = 0
|
||||
|
||||
let result = value.split(UNDERSCORE).every((part) => {
|
||||
part = normalize(part)
|
||||
|
||||
if (part.startsWith('var(')) return true
|
||||
if (parseColor(part, { loose: true }) !== null) return colors++, true
|
||||
|
||||
return false
|
||||
})
|
||||
|
||||
if (!result) return false
|
||||
return colors > 0
|
||||
}
|
||||
|
||||
export function image(value) {
|
||||
let images = 0
|
||||
let result = value.split(COMMA).every((part) => {
|
||||
part = normalize(part)
|
||||
|
||||
if (part.startsWith('var(')) return true
|
||||
if (
|
||||
url(part) ||
|
||||
gradient(part) ||
|
||||
['element(', 'image(', 'cross-fade(', 'image-set('].some((fn) => part.startsWith(fn))
|
||||
) {
|
||||
images++
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
})
|
||||
|
||||
if (!result) return false
|
||||
return images > 0
|
||||
}
|
||||
|
||||
let gradientTypes = new Set([
|
||||
'linear-gradient',
|
||||
'radial-gradient',
|
||||
'repeating-linear-gradient',
|
||||
'repeating-radial-gradient',
|
||||
'conic-gradient',
|
||||
])
|
||||
export function gradient(value) {
|
||||
value = normalize(value)
|
||||
|
||||
for (let type of gradientTypes) {
|
||||
if (value.startsWith(`${type}(`)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
let validPositions = new Set(['center', 'top', 'right', 'bottom', 'left'])
|
||||
export function position(value) {
|
||||
let positions = 0
|
||||
let result = value.split(UNDERSCORE).every((part) => {
|
||||
part = normalize(part)
|
||||
|
||||
if (part.startsWith('var(')) return true
|
||||
if (validPositions.has(part) || length(part) || percentage(part)) {
|
||||
positions++
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
})
|
||||
|
||||
if (!result) return false
|
||||
return positions > 0
|
||||
}
|
||||
|
||||
export function familyName(value) {
|
||||
let fonts = 0
|
||||
let result = value.split(COMMA).every((part) => {
|
||||
part = normalize(part)
|
||||
|
||||
if (part.startsWith('var(')) return true
|
||||
|
||||
// If it contains spaces, then it should be quoted
|
||||
if (part.includes(' ')) {
|
||||
if (!/(['"])([^"']+)\1/g.test(part)) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// If it starts with a number, it's invalid
|
||||
if (/^\d/g.test(part)) {
|
||||
return false
|
||||
}
|
||||
|
||||
fonts++
|
||||
|
||||
return true
|
||||
})
|
||||
|
||||
if (!result) return false
|
||||
return fonts > 0
|
||||
}
|
||||
|
||||
let genericNames = new Set([
|
||||
'serif',
|
||||
'sans-serif',
|
||||
'monospace',
|
||||
'cursive',
|
||||
'fantasy',
|
||||
'system-ui',
|
||||
'ui-serif',
|
||||
'ui-sans-serif',
|
||||
'ui-monospace',
|
||||
'ui-rounded',
|
||||
'math',
|
||||
'emoji',
|
||||
'fangsong',
|
||||
])
|
||||
export function genericName(value) {
|
||||
return genericNames.has(value)
|
||||
}
|
||||
|
||||
let absoluteSizes = new Set([
|
||||
'xx-small',
|
||||
'x-small',
|
||||
'small',
|
||||
'medium',
|
||||
'large',
|
||||
'x-large',
|
||||
'x-large',
|
||||
'xxx-large',
|
||||
])
|
||||
export function absoluteSize(value) {
|
||||
return absoluteSizes.has(value)
|
||||
}
|
||||
|
||||
let relativeSizes = new Set(['larger', 'smaller'])
|
||||
export function relativeSize(value) {
|
||||
return relativeSizes.has(value)
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
export function defaults(target, ...sources) {
|
||||
for (let source of sources) {
|
||||
for (let k in source) {
|
||||
if (!target?.hasOwnProperty?.(k)) {
|
||||
target[k] = source[k]
|
||||
}
|
||||
}
|
||||
|
||||
for (let k of Object.getOwnPropertySymbols(source)) {
|
||||
if (!target?.hasOwnProperty?.(k)) {
|
||||
target[k] = source[k]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return target
|
||||
}
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
import parser from 'postcss-selector-parser'
|
||||
import escapeCommas from './escapeCommas'
|
||||
|
||||
export default function escapeClassName(className) {
|
||||
let node = parser.className()
|
||||
node.value = className
|
||||
return escapeCommas(node?.raws?.value ?? node.value)
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
export default function escapeCommas(className) {
|
||||
return className.replace(/\\,/g, '\\2c ')
|
||||
}
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
const flattenColorPalette = (colors) =>
|
||||
Object.assign(
|
||||
{},
|
||||
...Object.entries(colors ?? {}).flatMap(([color, values]) =>
|
||||
typeof values == 'object'
|
||||
? Object.entries(flattenColorPalette(values)).map(([number, hex]) => ({
|
||||
[color + (number === 'DEFAULT' ? '' : `-${number}`)]: hex,
|
||||
}))
|
||||
: [{ [`${color}`]: values }]
|
||||
)
|
||||
)
|
||||
|
||||
export default flattenColorPalette
|
||||
+215
@@ -0,0 +1,215 @@
|
||||
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'
|
||||
|
||||
let MERGE = ':merge'
|
||||
let PARENT = '&'
|
||||
|
||||
export let selectorFunctions = new Set([MERGE])
|
||||
|
||||
export function formatVariantSelector(current, ...others) {
|
||||
for (let other of others) {
|
||||
let incomingValue = resolveFunctionArgument(other, MERGE)
|
||||
if (incomingValue !== null) {
|
||||
let existingValue = resolveFunctionArgument(current, MERGE, incomingValue)
|
||||
if (existingValue !== null) {
|
||||
let existingTarget = `${MERGE}(${incomingValue})`
|
||||
let splitIdx = other.indexOf(existingTarget)
|
||||
let addition = other.slice(splitIdx + existingTarget.length).split(' ')[0]
|
||||
|
||||
current = current.replace(existingTarget, existingTarget + addition)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
current = other.replace(PARENT, current)
|
||||
}
|
||||
|
||||
return current
|
||||
}
|
||||
|
||||
export function finalizeSelector(
|
||||
format,
|
||||
{
|
||||
selector,
|
||||
candidate,
|
||||
context,
|
||||
isArbitraryVariant,
|
||||
|
||||
// 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 = candidate
|
||||
.split(new RegExp(`\\${context?.tailwindConfig?.separator ?? ':'}(?![^[]*\\])`))
|
||||
.pop(),
|
||||
}
|
||||
) {
|
||||
let ast = selectorParser().astSync(selector)
|
||||
|
||||
// We explicitly DO NOT prefix classes in arbitrary variants
|
||||
if (context?.tailwindConfig?.prefix && !isArbitraryVariant) {
|
||||
format = prefixSelector(context.tailwindConfig.prefix, format)
|
||||
}
|
||||
|
||||
format = format.replace(PARENT, `.${escapeClassName(candidate)}`)
|
||||
|
||||
let formatAst = selectorParser().astSync(format)
|
||||
|
||||
// Remove extraneous selectors that do not include the base class/candidate being matched against
|
||||
// For example if we have a utility defined `.a, .b { color: red}`
|
||||
// And the formatted variant is sm:b then we want the final selector to be `.sm\:b` and not `.a, .sm\:b`
|
||||
ast.each((node) => {
|
||||
let hasClassesMatchingCandidate = node.some((n) => n.type === 'class' && n.value === base)
|
||||
|
||||
if (!hasClassesMatchingCandidate) {
|
||||
node.remove()
|
||||
}
|
||||
})
|
||||
|
||||
// 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\\)\\]
|
||||
//
|
||||
ast.walkClasses((node) => {
|
||||
if (node.raws && node.value.includes(base)) {
|
||||
node.raws.value = escapeClassName(unescape(node.raws.value))
|
||||
}
|
||||
})
|
||||
|
||||
// We can safely replace the escaped base now, since the `base` section is
|
||||
// now in a normalized escaped value.
|
||||
ast.walkClasses((node) => {
|
||||
if (node.value === base) {
|
||||
node.replaceWith(...formatAst.nodes)
|
||||
}
|
||||
})
|
||||
|
||||
// This will make sure to move pseudo's to the correct spot (the end for
|
||||
// pseudo elements) because otherwise the selector will never work
|
||||
// anyway.
|
||||
//
|
||||
// E.g.:
|
||||
// - `before:hover:text-center` would result in `.before\:hover\:text-center:hover::before`
|
||||
// - `hover:before:text-center` would result in `.hover\:before\:text-center:hover::before`
|
||||
//
|
||||
// `::before:hover` doesn't work, which means that we can make it work for you by flipping the order.
|
||||
function collectPseudoElements(selector) {
|
||||
let nodes = []
|
||||
|
||||
for (let node of selector.nodes) {
|
||||
if (isPseudoElement(node)) {
|
||||
nodes.push(node)
|
||||
selector.removeChild(node)
|
||||
}
|
||||
|
||||
if (node?.nodes) {
|
||||
nodes.push(...collectPseudoElements(node))
|
||||
}
|
||||
}
|
||||
|
||||
return nodes
|
||||
}
|
||||
|
||||
// Remove unnecessary pseudo selectors that we used as placeholders
|
||||
ast.each((selector) => {
|
||||
selector.walkPseudos((p) => {
|
||||
if (selectorFunctions.has(p.value)) {
|
||||
p.replaceWith(p.nodes)
|
||||
}
|
||||
})
|
||||
|
||||
let pseudoElements = collectPseudoElements(selector)
|
||||
if (pseudoElements.length > 0) {
|
||||
selector.nodes.push(pseudoElements.sort(sortSelector))
|
||||
}
|
||||
})
|
||||
|
||||
return ast.toString()
|
||||
}
|
||||
|
||||
// Note: As a rule, double colons (::) should be used instead of a single colon
|
||||
// (:). This distinguishes pseudo-classes from pseudo-elements. However, since
|
||||
// this distinction was not present in older versions of the W3C spec, most
|
||||
// browsers support both syntaxes for the original pseudo-elements.
|
||||
let pseudoElementsBC = [':before', ':after', ':first-line', ':first-letter']
|
||||
|
||||
// These pseudo-elements _can_ be combined with other pseudo selectors AND the order does matter.
|
||||
let pseudoElementExceptions = ['::file-selector-button']
|
||||
|
||||
// This will make sure to move pseudo's to the correct spot (the end for
|
||||
// pseudo elements) because otherwise the selector will never work
|
||||
// anyway.
|
||||
//
|
||||
// E.g.:
|
||||
// - `before:hover:text-center` would result in `.before\:hover\:text-center:hover::before`
|
||||
// - `hover:before:text-center` would result in `.hover\:before\:text-center:hover::before`
|
||||
//
|
||||
// `::before:hover` doesn't work, which means that we can make it work
|
||||
// for you by flipping the order.
|
||||
function sortSelector(a, z) {
|
||||
// Both nodes are non-pseudo's so we can safely ignore them and keep
|
||||
// them in the same order.
|
||||
if (a.type !== 'pseudo' && z.type !== 'pseudo') {
|
||||
return 0
|
||||
}
|
||||
|
||||
// If one of them is a combinator, we need to keep it in the same order
|
||||
// because that means it will start a new "section" in the selector.
|
||||
if ((a.type === 'combinator') ^ (z.type === 'combinator')) {
|
||||
return 0
|
||||
}
|
||||
|
||||
// One of the items is a pseudo and the other one isn't. Let's move
|
||||
// the pseudo to the right.
|
||||
if ((a.type === 'pseudo') ^ (z.type === 'pseudo')) {
|
||||
return (a.type === 'pseudo') - (z.type === 'pseudo')
|
||||
}
|
||||
|
||||
// Both are pseudo's, move the pseudo elements (except for
|
||||
// ::file-selector-button) to the right.
|
||||
return isPseudoElement(a) - isPseudoElement(z)
|
||||
}
|
||||
|
||||
function isPseudoElement(node) {
|
||||
if (node.type !== 'pseudo') return false
|
||||
if (pseudoElementExceptions.includes(node.value)) return false
|
||||
|
||||
return node.value.startsWith('::') || pseudoElementsBC.includes(node.value)
|
||||
}
|
||||
|
||||
function resolveFunctionArgument(haystack, needle, arg) {
|
||||
let startIdx = haystack.indexOf(arg ? `${needle}(${arg})` : needle)
|
||||
if (startIdx === -1) return null
|
||||
|
||||
// Start inside the `(`
|
||||
startIdx += needle.length + 1
|
||||
|
||||
let target = ''
|
||||
let count = 0
|
||||
|
||||
for (let char of haystack.slice(startIdx)) {
|
||||
if (char !== '(' && char !== ')') {
|
||||
target += char
|
||||
} else if (char === '(') {
|
||||
target += char
|
||||
count++
|
||||
} else if (char === ')') {
|
||||
if (--count < 0) break // unbalanced
|
||||
target += char
|
||||
}
|
||||
}
|
||||
|
||||
return target
|
||||
}
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
import defaultConfig from '../../stubs/defaultConfig.stub.js'
|
||||
import { flagEnabled } from '../featureFlags'
|
||||
|
||||
export default function getAllConfigs(config) {
|
||||
const configs = (config?.presets ?? [defaultConfig])
|
||||
.slice()
|
||||
.reverse()
|
||||
.flatMap((preset) => getAllConfigs(preset instanceof Function ? preset() : preset))
|
||||
|
||||
const features = {
|
||||
// Add experimental configs here...
|
||||
respectDefaultRingColorOpacity: {
|
||||
theme: {
|
||||
ringColor: {
|
||||
DEFAULT: '#3b82f67f',
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
const experimentals = Object.keys(features)
|
||||
.filter((feature) => flagEnabled(config, feature))
|
||||
.map((feature) => features[feature])
|
||||
|
||||
return [config, ...experimentals, ...configs]
|
||||
}
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
import hash from 'object-hash'
|
||||
|
||||
export default function hashConfig(config) {
|
||||
return hash(config, { ignoreUnknown: true })
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
export default function isKeyframeRule(rule) {
|
||||
return rule.parent && rule.parent.type === 'atrule' && /keyframes$/.test(rule.parent.name)
|
||||
}
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
export default function isPlainObject(value) {
|
||||
if (Object.prototype.toString.call(value) !== '[object Object]') {
|
||||
return false
|
||||
}
|
||||
|
||||
const prototype = Object.getPrototypeOf(value)
|
||||
return prototype === null || prototype === Object.prototype
|
||||
}
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
let matchingBrackets = new Map([
|
||||
['{', '}'],
|
||||
['[', ']'],
|
||||
['(', ')'],
|
||||
])
|
||||
let inverseMatchingBrackets = new Map(
|
||||
Array.from(matchingBrackets.entries()).map(([k, v]) => [v, k])
|
||||
)
|
||||
|
||||
let quotes = new Set(['"', "'", '`'])
|
||||
|
||||
// Arbitrary values must contain balanced brackets (), [] and {}. Escaped
|
||||
// values don't count, and brackets inside quotes also don't count.
|
||||
//
|
||||
// E.g.: w-[this-is]w-[weird-and-invalid]
|
||||
// E.g.: w-[this-is\\]w-\\[weird-but-valid]
|
||||
// E.g.: content-['this-is-also-valid]-weirdly-enough']
|
||||
export default function isValidArbitraryValue(value) {
|
||||
let stack = []
|
||||
let inQuotes = false
|
||||
|
||||
for (let i = 0; i < value.length; i++) {
|
||||
let char = value[i]
|
||||
|
||||
if (char === ':' && !inQuotes && stack.length === 0) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Non-escaped quotes allow us to "allow" anything in between
|
||||
if (quotes.has(char) && value[i - 1] !== '\\') {
|
||||
inQuotes = !inQuotes
|
||||
}
|
||||
|
||||
if (inQuotes) continue
|
||||
if (value[i - 1] === '\\') continue // Escaped
|
||||
|
||||
if (matchingBrackets.has(char)) {
|
||||
stack.push(char)
|
||||
} else if (inverseMatchingBrackets.has(char)) {
|
||||
let inverse = inverseMatchingBrackets.get(char)
|
||||
|
||||
// Nothing to pop from, therefore it is unbalanced
|
||||
if (stack.length <= 0) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Popped value must match the inverse value, otherwise it is unbalanced
|
||||
if (stack.pop() !== inverse) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If there is still something on the stack, it is also unbalanced
|
||||
if (stack.length > 0) {
|
||||
return false
|
||||
}
|
||||
|
||||
// All good, totally balanced!
|
||||
return true
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
import colors from 'picocolors'
|
||||
|
||||
let alreadyShown = new Set()
|
||||
|
||||
function log(type, messages, key) {
|
||||
if (typeof process !== 'undefined' && process.env.JEST_WORKER_ID) return
|
||||
|
||||
if (key && alreadyShown.has(key)) return
|
||||
if (key) alreadyShown.add(key)
|
||||
|
||||
console.warn('')
|
||||
messages.forEach((message) => console.warn(type, '-', message))
|
||||
}
|
||||
|
||||
export function dim(input) {
|
||||
return colors.dim(input)
|
||||
}
|
||||
|
||||
export default {
|
||||
info(key, messages) {
|
||||
log(colors.bold(colors.cyan('info')), ...(Array.isArray(key) ? [key] : [messages, key]))
|
||||
},
|
||||
warn(key, messages) {
|
||||
log(colors.bold(colors.yellow('warn')), ...(Array.isArray(key) ? [key] : [messages, key]))
|
||||
},
|
||||
risk(key, messages) {
|
||||
log(colors.bold(colors.magenta('risk')), ...(Array.isArray(key) ? [key] : [messages, key]))
|
||||
},
|
||||
}
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
import escapeClassName from './escapeClassName'
|
||||
import escapeCommas from './escapeCommas'
|
||||
|
||||
export function asClass(name) {
|
||||
return escapeCommas(`.${escapeClassName(name)}`)
|
||||
}
|
||||
|
||||
export default function nameClass(classPrefix, key) {
|
||||
return asClass(formatClass(classPrefix, key))
|
||||
}
|
||||
|
||||
export function formatClass(classPrefix, key) {
|
||||
if (key === 'DEFAULT') {
|
||||
return classPrefix
|
||||
}
|
||||
|
||||
if (key === '-' || key === '-DEFAULT') {
|
||||
return `-${classPrefix}`
|
||||
}
|
||||
|
||||
if (key.startsWith('-')) {
|
||||
return `-${classPrefix}${key}`
|
||||
}
|
||||
|
||||
return `${classPrefix}-${key}`
|
||||
}
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
export default function (value) {
|
||||
value = `${value}`
|
||||
|
||||
if (value === '0') {
|
||||
return '0'
|
||||
}
|
||||
|
||||
// Flip sign of numbers
|
||||
if (/^[+-]?(\d+|\d*\.\d+)(e[+-]?\d+)?(%|\w+)?$/.test(value)) {
|
||||
return value.replace(/^[+-]?/, (sign) => (sign === '-' ? '' : '-'))
|
||||
}
|
||||
|
||||
if (value.includes('var(') || value.includes('calc(')) {
|
||||
return `calc(${value} * -1)`
|
||||
}
|
||||
}
|
||||
+262
@@ -0,0 +1,262 @@
|
||||
import log, { dim } from './log'
|
||||
|
||||
export function normalizeConfig(config) {
|
||||
// Quick structure validation
|
||||
/**
|
||||
* type FilePath = string
|
||||
* type RawFile = { raw: string, extension?: string }
|
||||
* type ExtractorFn = (content: string) => Array<string>
|
||||
* type TransformerFn = (content: string) => string
|
||||
*
|
||||
* type Content =
|
||||
* | Array<FilePath | RawFile>
|
||||
* | {
|
||||
* files: Array<FilePath | RawFile>,
|
||||
* extract?: ExtractorFn | { [extension: string]: ExtractorFn }
|
||||
* transform?: TransformerFn | { [extension: string]: TransformerFn }
|
||||
* }
|
||||
*/
|
||||
let valid = (() => {
|
||||
// `config.purge` should not exist anymore
|
||||
if (config.purge) {
|
||||
return false
|
||||
}
|
||||
|
||||
// `config.content` should exist
|
||||
if (!config.content) {
|
||||
return false
|
||||
}
|
||||
|
||||
// `config.content` should be an object or an array
|
||||
if (
|
||||
!Array.isArray(config.content) &&
|
||||
!(typeof config.content === 'object' && config.content !== null)
|
||||
) {
|
||||
return false
|
||||
}
|
||||
|
||||
// When `config.content` is an array, it should consist of FilePaths or RawFiles
|
||||
if (Array.isArray(config.content)) {
|
||||
return config.content.every((path) => {
|
||||
// `path` can be a string
|
||||
if (typeof path === 'string') return true
|
||||
|
||||
// `path` can be an object { raw: string, extension?: string }
|
||||
// `raw` must be a string
|
||||
if (typeof path?.raw !== 'string') return false
|
||||
|
||||
// `extension` (if provided) should also be a string
|
||||
if (path?.extension && typeof path?.extension !== 'string') {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
// When `config.content` is an object
|
||||
if (typeof config.content === 'object' && config.content !== null) {
|
||||
// Only `files`, `extract` and `transform` can exist in `config.content`
|
||||
if (
|
||||
Object.keys(config.content).some((key) => !['files', 'extract', 'transform'].includes(key))
|
||||
) {
|
||||
return false
|
||||
}
|
||||
|
||||
// `config.content.files` should exist of FilePaths or RawFiles
|
||||
if (Array.isArray(config.content.files)) {
|
||||
if (
|
||||
!config.content.files.every((path) => {
|
||||
// `path` can be a string
|
||||
if (typeof path === 'string') return true
|
||||
|
||||
// `path` can be an object { raw: string, extension?: string }
|
||||
// `raw` must be a string
|
||||
if (typeof path?.raw !== 'string') return false
|
||||
|
||||
// `extension` (if provided) should also be a string
|
||||
if (path?.extension && typeof path?.extension !== 'string') {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
})
|
||||
) {
|
||||
return false
|
||||
}
|
||||
|
||||
// `config.content.extract` is optional, and can be a Function or a Record<String, Function>
|
||||
if (typeof config.content.extract === 'object') {
|
||||
for (let value of Object.values(config.content.extract)) {
|
||||
if (typeof value !== 'function') {
|
||||
return false
|
||||
}
|
||||
}
|
||||
} else if (
|
||||
!(config.content.extract === undefined || typeof config.content.extract === 'function')
|
||||
) {
|
||||
return false
|
||||
}
|
||||
|
||||
// `config.content.transform` is optional, and can be a Function or a Record<String, Function>
|
||||
if (typeof config.content.transform === 'object') {
|
||||
for (let value of Object.values(config.content.transform)) {
|
||||
if (typeof value !== 'function') {
|
||||
return false
|
||||
}
|
||||
}
|
||||
} else if (
|
||||
!(
|
||||
config.content.transform === undefined || typeof config.content.transform === 'function'
|
||||
)
|
||||
) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
})()
|
||||
|
||||
if (!valid) {
|
||||
log.warn('purge-deprecation', [
|
||||
'The `purge`/`content` options have changed in Tailwind CSS v3.0.',
|
||||
'Update your configuration file to eliminate this warning.',
|
||||
'https://tailwindcss.com/docs/upgrade-guide#configure-content-sources',
|
||||
])
|
||||
}
|
||||
|
||||
// Normalize the `safelist`
|
||||
config.safelist = (() => {
|
||||
let { content, purge, safelist } = config
|
||||
|
||||
if (Array.isArray(safelist)) return safelist
|
||||
if (Array.isArray(content?.safelist)) return content.safelist
|
||||
if (Array.isArray(purge?.safelist)) return purge.safelist
|
||||
if (Array.isArray(purge?.options?.safelist)) return purge.options.safelist
|
||||
|
||||
return []
|
||||
})()
|
||||
|
||||
// Normalize prefix option
|
||||
if (typeof config.prefix === 'function') {
|
||||
log.warn('prefix-function', [
|
||||
'As of Tailwind CSS v3.0, `prefix` cannot be a function.',
|
||||
'Update `prefix` in your configuration to be a string to eliminate this warning.',
|
||||
'https://tailwindcss.com/docs/upgrade-guide#prefix-cannot-be-a-function',
|
||||
])
|
||||
config.prefix = ''
|
||||
} else {
|
||||
config.prefix = config.prefix ?? ''
|
||||
}
|
||||
|
||||
// Normalize the `content`
|
||||
config.content = {
|
||||
files: (() => {
|
||||
let { content, purge } = config
|
||||
|
||||
if (Array.isArray(purge)) return purge
|
||||
if (Array.isArray(purge?.content)) return purge.content
|
||||
if (Array.isArray(content)) return content
|
||||
if (Array.isArray(content?.content)) return content.content
|
||||
if (Array.isArray(content?.files)) return content.files
|
||||
|
||||
return []
|
||||
})(),
|
||||
|
||||
extract: (() => {
|
||||
let extract = (() => {
|
||||
if (config.purge?.extract) return config.purge.extract
|
||||
if (config.content?.extract) return config.content.extract
|
||||
|
||||
if (config.purge?.extract?.DEFAULT) return config.purge.extract.DEFAULT
|
||||
if (config.content?.extract?.DEFAULT) return config.content.extract.DEFAULT
|
||||
|
||||
if (config.purge?.options?.extractors) return config.purge.options.extractors
|
||||
if (config.content?.options?.extractors) return config.content.options.extractors
|
||||
|
||||
return {}
|
||||
})()
|
||||
|
||||
let extractors = {}
|
||||
|
||||
let defaultExtractor = (() => {
|
||||
if (config.purge?.options?.defaultExtractor) {
|
||||
return config.purge.options.defaultExtractor
|
||||
}
|
||||
|
||||
if (config.content?.options?.defaultExtractor) {
|
||||
return config.content.options.defaultExtractor
|
||||
}
|
||||
|
||||
return undefined
|
||||
})()
|
||||
|
||||
if (defaultExtractor !== undefined) {
|
||||
extractors.DEFAULT = defaultExtractor
|
||||
}
|
||||
|
||||
// Functions
|
||||
if (typeof extract === 'function') {
|
||||
extractors.DEFAULT = extract
|
||||
}
|
||||
|
||||
// Arrays
|
||||
else if (Array.isArray(extract)) {
|
||||
for (let { extensions, extractor } of extract ?? []) {
|
||||
for (let extension of extensions) {
|
||||
extractors[extension] = extractor
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Objects
|
||||
else if (typeof extract === 'object' && extract !== null) {
|
||||
Object.assign(extractors, extract)
|
||||
}
|
||||
|
||||
return extractors
|
||||
})(),
|
||||
|
||||
transform: (() => {
|
||||
let transform = (() => {
|
||||
if (config.purge?.transform) return config.purge.transform
|
||||
if (config.content?.transform) return config.content.transform
|
||||
|
||||
if (config.purge?.transform?.DEFAULT) return config.purge.transform.DEFAULT
|
||||
if (config.content?.transform?.DEFAULT) return config.content.transform.DEFAULT
|
||||
|
||||
return {}
|
||||
})()
|
||||
|
||||
let transformers = {}
|
||||
|
||||
if (typeof transform === 'function') {
|
||||
transformers.DEFAULT = transform
|
||||
}
|
||||
|
||||
if (typeof transform === 'object' && transform !== null) {
|
||||
Object.assign(transformers, transform)
|
||||
}
|
||||
|
||||
return transformers
|
||||
})(),
|
||||
}
|
||||
|
||||
// Validate globs to prevent bogus globs.
|
||||
// E.g.: `./src/*.{html}` is invalid, the `{html}` should just be `html`
|
||||
for (let file of config.content.files) {
|
||||
if (typeof file === 'string' && /{([^,]*?)}/g.test(file)) {
|
||||
log.warn('invalid-glob-braces', [
|
||||
`The glob pattern ${dim(file)} in your Tailwind CSS configuration is invalid.`,
|
||||
`Update it to ${dim(file.replace(/{([^,]*?)}/g, '$1'))} to silence this warning.`,
|
||||
// TODO: Add https://tw.wtf/invalid-glob-braces
|
||||
])
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return config
|
||||
}
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
/**
|
||||
* A function that normalizes the various forms that the screens object can be
|
||||
* provided in.
|
||||
*
|
||||
* Input(s):
|
||||
* - ['100px', '200px'] // Raw strings
|
||||
* - { sm: '100px', md: '200px' } // Object with string values
|
||||
* - { sm: { min: '100px' }, md: { max: '100px' } } // Object with object values
|
||||
* - { sm: [{ min: '100px' }, { max: '200px' }] } // Object with object array (multiple values)
|
||||
*
|
||||
* Output(s):
|
||||
* - [{ name: 'sm', values: [{ min: '100px', max: '200px' }] }] // List of objects, that contains multiple values
|
||||
*/
|
||||
export function normalizeScreens(screens, root = true) {
|
||||
if (Array.isArray(screens)) {
|
||||
return screens.map((screen) => {
|
||||
if (root && Array.isArray(screen)) {
|
||||
throw new Error('The tuple syntax is not supported for `screens`.')
|
||||
}
|
||||
|
||||
if (typeof screen === 'string') {
|
||||
return { name: screen.toString(), values: [{ min: screen, max: undefined }] }
|
||||
}
|
||||
|
||||
let [name, options] = screen
|
||||
name = name.toString()
|
||||
|
||||
if (typeof options === 'string') {
|
||||
return { name, values: [{ min: options, max: undefined }] }
|
||||
}
|
||||
|
||||
if (Array.isArray(options)) {
|
||||
return { name, values: options.map((option) => resolveValue(option)) }
|
||||
}
|
||||
|
||||
return { name, values: [resolveValue(options)] }
|
||||
})
|
||||
}
|
||||
|
||||
return normalizeScreens(Object.entries(screens ?? {}), false)
|
||||
}
|
||||
|
||||
function resolveValue({ 'min-width': _minWidth, min = _minWidth, max, raw } = {}) {
|
||||
return { min, max, raw }
|
||||
}
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
const DIRECTIONS = new Set(['normal', 'reverse', 'alternate', 'alternate-reverse'])
|
||||
const PLAY_STATES = new Set(['running', 'paused'])
|
||||
const FILL_MODES = new Set(['none', 'forwards', 'backwards', 'both'])
|
||||
const ITERATION_COUNTS = new Set(['infinite'])
|
||||
const TIMINGS = new Set([
|
||||
'linear',
|
||||
'ease',
|
||||
'ease-in',
|
||||
'ease-out',
|
||||
'ease-in-out',
|
||||
'step-start',
|
||||
'step-end',
|
||||
])
|
||||
const TIMING_FNS = ['cubic-bezier', 'steps']
|
||||
|
||||
const COMMA = /\,(?![^(]*\))/g // Comma separator that is not located between brackets. E.g.: `cubiz-bezier(a, b, c)` these don't count.
|
||||
const SPACE = /\ +(?![^(]*\))/g // Similar to the one above, but with spaces instead.
|
||||
const TIME = /^(-?[\d.]+m?s)$/
|
||||
const DIGIT = /^(\d+)$/
|
||||
|
||||
export default function parseAnimationValue(input) {
|
||||
let animations = input.split(COMMA)
|
||||
return animations.map((animation) => {
|
||||
let value = animation.trim()
|
||||
let result = { value }
|
||||
let parts = value.split(SPACE)
|
||||
let seen = new Set()
|
||||
|
||||
for (let part of parts) {
|
||||
if (!seen.has('DIRECTIONS') && DIRECTIONS.has(part)) {
|
||||
result.direction = part
|
||||
seen.add('DIRECTIONS')
|
||||
} else if (!seen.has('PLAY_STATES') && PLAY_STATES.has(part)) {
|
||||
result.playState = part
|
||||
seen.add('PLAY_STATES')
|
||||
} else if (!seen.has('FILL_MODES') && FILL_MODES.has(part)) {
|
||||
result.fillMode = part
|
||||
seen.add('FILL_MODES')
|
||||
} else if (
|
||||
!seen.has('ITERATION_COUNTS') &&
|
||||
(ITERATION_COUNTS.has(part) || DIGIT.test(part))
|
||||
) {
|
||||
result.iterationCount = part
|
||||
seen.add('ITERATION_COUNTS')
|
||||
} else if (!seen.has('TIMING_FUNCTION') && TIMINGS.has(part)) {
|
||||
result.timingFunction = part
|
||||
seen.add('TIMING_FUNCTION')
|
||||
} else if (!seen.has('TIMING_FUNCTION') && TIMING_FNS.some((f) => part.startsWith(`${f}(`))) {
|
||||
result.timingFunction = part
|
||||
seen.add('TIMING_FUNCTION')
|
||||
} else if (!seen.has('DURATION') && TIME.test(part)) {
|
||||
result.duration = part
|
||||
seen.add('DURATION')
|
||||
} else if (!seen.has('DELAY') && TIME.test(part)) {
|
||||
result.delay = part
|
||||
seen.add('DELAY')
|
||||
} else if (!seen.has('NAME')) {
|
||||
result.name = part
|
||||
seen.add('NAME')
|
||||
} else {
|
||||
if (!result.unknown) result.unknown = []
|
||||
result.unknown.push(part)
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
})
|
||||
}
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
import { splitAtTopLevelOnly } from './splitAtTopLevelOnly'
|
||||
|
||||
let KEYWORDS = new Set(['inset', 'inherit', 'initial', 'revert', 'unset'])
|
||||
let SPACE = /\ +(?![^(]*\))/g // Similar to the one above, but with spaces instead.
|
||||
let LENGTH = /^-?(\d+|\.\d+)(.*?)$/g
|
||||
|
||||
export function parseBoxShadowValue(input) {
|
||||
let shadows = Array.from(splitAtTopLevelOnly(input, ','))
|
||||
return shadows.map((shadow) => {
|
||||
let value = shadow.trim()
|
||||
let result = { raw: value }
|
||||
let parts = value.split(SPACE)
|
||||
let seen = new Set()
|
||||
|
||||
for (let part of parts) {
|
||||
// Reset index, since the regex is stateful.
|
||||
LENGTH.lastIndex = 0
|
||||
|
||||
// Keyword
|
||||
if (!seen.has('KEYWORD') && KEYWORDS.has(part)) {
|
||||
result.keyword = part
|
||||
seen.add('KEYWORD')
|
||||
}
|
||||
|
||||
// Length value
|
||||
else if (LENGTH.test(part)) {
|
||||
if (!seen.has('X')) {
|
||||
result.x = part
|
||||
seen.add('X')
|
||||
} else if (!seen.has('Y')) {
|
||||
result.y = part
|
||||
seen.add('Y')
|
||||
} else if (!seen.has('BLUR')) {
|
||||
result.blur = part
|
||||
seen.add('BLUR')
|
||||
} else if (!seen.has('SPREAD')) {
|
||||
result.spread = part
|
||||
seen.add('SPREAD')
|
||||
}
|
||||
}
|
||||
|
||||
// Color or unknown
|
||||
else {
|
||||
if (!result.color) {
|
||||
result.color = part
|
||||
} else {
|
||||
if (!result.unknown) result.unknown = []
|
||||
result.unknown.push(part)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check if valid
|
||||
result.valid = result.x !== undefined && result.y !== undefined
|
||||
|
||||
return result
|
||||
})
|
||||
}
|
||||
|
||||
export function formatBoxShadowValue(shadows) {
|
||||
return shadows
|
||||
.map((shadow) => {
|
||||
if (!shadow.valid) {
|
||||
return shadow.raw
|
||||
}
|
||||
|
||||
return [shadow.keyword, shadow.x, shadow.y, shadow.blur, shadow.spread, shadow.color]
|
||||
.filter(Boolean)
|
||||
.join(' ')
|
||||
})
|
||||
.join(', ')
|
||||
}
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
import isGlob from 'is-glob'
|
||||
import globParent from 'glob-parent'
|
||||
import path from 'path'
|
||||
|
||||
// Based on `glob-base`
|
||||
// https://github.com/micromatch/glob-base/blob/master/index.js
|
||||
function parseGlob(pattern) {
|
||||
let glob = pattern
|
||||
let base = globParent(pattern)
|
||||
|
||||
if (base !== '.') {
|
||||
glob = pattern.substr(base.length)
|
||||
if (glob.charAt(0) === '/') {
|
||||
glob = glob.substr(1)
|
||||
}
|
||||
}
|
||||
|
||||
if (glob.substr(0, 2) === './') {
|
||||
glob = glob.substr(2)
|
||||
}
|
||||
if (glob.charAt(0) === '/') {
|
||||
glob = glob.substr(1)
|
||||
}
|
||||
|
||||
return { base, glob }
|
||||
}
|
||||
|
||||
export default function parseDependency(normalizedFileOrGlob) {
|
||||
if (normalizedFileOrGlob.startsWith('!')) {
|
||||
return null
|
||||
}
|
||||
|
||||
let message
|
||||
|
||||
if (isGlob(normalizedFileOrGlob)) {
|
||||
let { base, glob } = parseGlob(normalizedFileOrGlob)
|
||||
message = { type: 'dir-dependency', dir: path.resolve(base), glob }
|
||||
} else {
|
||||
message = { type: 'dependency', file: path.resolve(normalizedFileOrGlob) }
|
||||
}
|
||||
|
||||
// rollup-plugin-postcss does not support dir-dependency messages
|
||||
// but directories can be watched in the same way as files
|
||||
if (message.type === 'dir-dependency' && process.env.ROLLUP_WATCH === 'true') {
|
||||
message = { type: 'dependency', file: message.dir }
|
||||
}
|
||||
|
||||
return message
|
||||
}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
import postcss from 'postcss'
|
||||
import postcssNested from 'postcss-nested'
|
||||
import postcssJs from 'postcss-js'
|
||||
|
||||
export default function parseObjectStyles(styles) {
|
||||
if (!Array.isArray(styles)) {
|
||||
return parseObjectStyles([styles])
|
||||
}
|
||||
|
||||
return styles.flatMap((style) => {
|
||||
return postcss([
|
||||
postcssNested({
|
||||
bubble: ['screen'],
|
||||
}),
|
||||
]).process(style, {
|
||||
parser: postcssJs,
|
||||
}).root.nodes
|
||||
})
|
||||
}
|
||||
+204
@@ -0,0 +1,204 @@
|
||||
import selectorParser from 'postcss-selector-parser'
|
||||
import escapeCommas from './escapeCommas'
|
||||
import { withAlphaValue } from './withAlphaVariable'
|
||||
import {
|
||||
normalize,
|
||||
length,
|
||||
number,
|
||||
percentage,
|
||||
url,
|
||||
color as validateColor,
|
||||
genericName,
|
||||
familyName,
|
||||
image,
|
||||
absoluteSize,
|
||||
relativeSize,
|
||||
position,
|
||||
lineWidth,
|
||||
shadow,
|
||||
} from './dataTypes'
|
||||
import negateValue from './negateValue'
|
||||
|
||||
export function updateAllClasses(selectors, updateClass) {
|
||||
let parser = selectorParser((selectors) => {
|
||||
selectors.walkClasses((sel) => {
|
||||
let updatedClass = updateClass(sel.value)
|
||||
sel.value = updatedClass
|
||||
if (sel.raws && sel.raws.value) {
|
||||
sel.raws.value = escapeCommas(sel.raws.value)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
let result = parser.processSync(selectors)
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
function resolveArbitraryValue(modifier, validate) {
|
||||
if (!isArbitraryValue(modifier)) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
let value = modifier.slice(1, -1)
|
||||
|
||||
if (!validate(value)) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
return normalize(value)
|
||||
}
|
||||
|
||||
function asNegativeValue(modifier, lookup = {}, validate) {
|
||||
let positiveValue = lookup[modifier]
|
||||
|
||||
if (positiveValue !== undefined) {
|
||||
return negateValue(positiveValue)
|
||||
}
|
||||
|
||||
if (isArbitraryValue(modifier)) {
|
||||
let resolved = resolveArbitraryValue(modifier, validate)
|
||||
|
||||
if (resolved === undefined) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
return negateValue(resolved)
|
||||
}
|
||||
}
|
||||
|
||||
export function asValue(modifier, options = {}, { validate = () => true } = {}) {
|
||||
let value = options.values?.[modifier]
|
||||
|
||||
if (value !== undefined) {
|
||||
return value
|
||||
}
|
||||
|
||||
if (options.supportsNegativeValues && modifier.startsWith('-')) {
|
||||
return asNegativeValue(modifier.slice(1), options.values, validate)
|
||||
}
|
||||
|
||||
return resolveArbitraryValue(modifier, validate)
|
||||
}
|
||||
|
||||
function isArbitraryValue(input) {
|
||||
return input.startsWith('[') && input.endsWith(']')
|
||||
}
|
||||
|
||||
function splitAlpha(modifier) {
|
||||
let slashIdx = modifier.lastIndexOf('/')
|
||||
|
||||
if (slashIdx === -1 || slashIdx === modifier.length - 1) {
|
||||
return [modifier]
|
||||
}
|
||||
|
||||
return [modifier.slice(0, slashIdx), modifier.slice(slashIdx + 1)]
|
||||
}
|
||||
|
||||
export function parseColorFormat(value) {
|
||||
if (typeof value === 'string' && value.includes('<alpha-value>')) {
|
||||
let oldValue = value
|
||||
|
||||
return ({ opacityValue = 1 }) => oldValue.replace('<alpha-value>', opacityValue)
|
||||
}
|
||||
|
||||
return value
|
||||
}
|
||||
|
||||
export function asColor(modifier, options = {}, { tailwindConfig = {} } = {}) {
|
||||
if (options.values?.[modifier] !== undefined) {
|
||||
return parseColorFormat(options.values?.[modifier])
|
||||
}
|
||||
|
||||
let [color, alpha] = splitAlpha(modifier)
|
||||
|
||||
if (alpha !== undefined) {
|
||||
let normalizedColor =
|
||||
options.values?.[color] ?? (isArbitraryValue(color) ? color.slice(1, -1) : undefined)
|
||||
|
||||
if (normalizedColor === undefined) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
normalizedColor = parseColorFormat(normalizedColor)
|
||||
|
||||
if (isArbitraryValue(alpha)) {
|
||||
return withAlphaValue(normalizedColor, alpha.slice(1, -1))
|
||||
}
|
||||
|
||||
if (tailwindConfig.theme?.opacity?.[alpha] === undefined) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
return withAlphaValue(normalizedColor, tailwindConfig.theme.opacity[alpha])
|
||||
}
|
||||
|
||||
return asValue(modifier, options, { validate: validateColor })
|
||||
}
|
||||
|
||||
export function asLookupValue(modifier, options = {}) {
|
||||
return options.values?.[modifier]
|
||||
}
|
||||
|
||||
function guess(validate) {
|
||||
return (modifier, options) => {
|
||||
return asValue(modifier, options, { validate })
|
||||
}
|
||||
}
|
||||
|
||||
let typeMap = {
|
||||
any: asValue,
|
||||
color: asColor,
|
||||
url: guess(url),
|
||||
image: guess(image),
|
||||
length: guess(length),
|
||||
percentage: guess(percentage),
|
||||
position: guess(position),
|
||||
lookup: asLookupValue,
|
||||
'generic-name': guess(genericName),
|
||||
'family-name': guess(familyName),
|
||||
number: guess(number),
|
||||
'line-width': guess(lineWidth),
|
||||
'absolute-size': guess(absoluteSize),
|
||||
'relative-size': guess(relativeSize),
|
||||
shadow: guess(shadow),
|
||||
}
|
||||
|
||||
let supportedTypes = Object.keys(typeMap)
|
||||
|
||||
function splitAtFirst(input, delim) {
|
||||
let idx = input.indexOf(delim)
|
||||
if (idx === -1) return [undefined, input]
|
||||
return [input.slice(0, idx), input.slice(idx + 1)]
|
||||
}
|
||||
|
||||
export function coerceValue(types, modifier, options, tailwindConfig) {
|
||||
if (isArbitraryValue(modifier)) {
|
||||
let arbitraryValue = modifier.slice(1, -1)
|
||||
let [explicitType, value] = splitAtFirst(arbitraryValue, ':')
|
||||
|
||||
// It could be that this resolves to `url(https` which is not a valid
|
||||
// identifier. We currently only support "simple" words with dashes or
|
||||
// underscores. E.g.: family-name
|
||||
if (!/^[\w-_]+$/g.test(explicitType)) {
|
||||
value = arbitraryValue
|
||||
}
|
||||
|
||||
//
|
||||
else if (explicitType !== undefined && !supportedTypes.includes(explicitType)) {
|
||||
return []
|
||||
}
|
||||
|
||||
if (value.length > 0 && supportedTypes.includes(explicitType)) {
|
||||
return [asValue(`[${value}]`, options), explicitType]
|
||||
}
|
||||
}
|
||||
|
||||
// Find first matching type
|
||||
for (let type of [].concat(types)) {
|
||||
let result = typeMap[type](modifier, options, { tailwindConfig })
|
||||
if (result !== undefined) return [result, type]
|
||||
}
|
||||
|
||||
return []
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
import parser from 'postcss-selector-parser'
|
||||
|
||||
export default function (prefix, selector, prependNegative = false) {
|
||||
return parser((selectors) => {
|
||||
selectors.walkClasses((classSelector) => {
|
||||
let baseClass = classSelector.value
|
||||
let shouldPlaceNegativeBeforePrefix = prependNegative && baseClass.startsWith('-')
|
||||
|
||||
classSelector.value = shouldPlaceNegativeBeforePrefix
|
||||
? `-${prefix}${baseClass.slice(1)}`
|
||||
: `${prefix}${baseClass}`
|
||||
})
|
||||
}).processSync(selector)
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
/**
|
||||
* This function removes any uses of CSS variables used as an alpha channel
|
||||
*
|
||||
* This is required for selectors like `:visited` which do not allow
|
||||
* changes in opacity or external control using CSS variables.
|
||||
*
|
||||
* @param {import('postcss').Container} container
|
||||
* @param {string[]} toRemove
|
||||
*/
|
||||
export function removeAlphaVariables(container, toRemove) {
|
||||
container.walkDecls((decl) => {
|
||||
if (toRemove.includes(decl.prop)) {
|
||||
decl.remove()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
for (let varName of toRemove) {
|
||||
if (decl.value.includes(`/ var(${varName})`)) {
|
||||
decl.value = decl.value.replace(`/ var(${varName})`, '')
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
+283
@@ -0,0 +1,283 @@
|
||||
import negateValue from './negateValue'
|
||||
import corePluginList from '../corePluginList'
|
||||
import configurePlugins from './configurePlugins'
|
||||
import defaultConfig from '../../stubs/defaultConfig.stub'
|
||||
import colors from '../public/colors'
|
||||
import { defaults } from './defaults'
|
||||
import { toPath } from './toPath'
|
||||
import { normalizeConfig } from './normalizeConfig'
|
||||
import isPlainObject from './isPlainObject'
|
||||
import { cloneDeep } from './cloneDeep'
|
||||
import { parseColorFormat } from './pluginUtils'
|
||||
import { withAlphaValue } from './withAlphaVariable'
|
||||
import toColorValue from './toColorValue'
|
||||
|
||||
function isFunction(input) {
|
||||
return typeof input === 'function'
|
||||
}
|
||||
|
||||
function isObject(input) {
|
||||
return typeof input === 'object' && input !== null
|
||||
}
|
||||
|
||||
function mergeWith(target, ...sources) {
|
||||
let customizer = sources.pop()
|
||||
|
||||
for (let source of sources) {
|
||||
for (let k in source) {
|
||||
let merged = customizer(target[k], source[k])
|
||||
|
||||
if (merged === undefined) {
|
||||
if (isObject(target[k]) && isObject(source[k])) {
|
||||
target[k] = mergeWith(target[k], source[k], customizer)
|
||||
} else {
|
||||
target[k] = source[k]
|
||||
}
|
||||
} else {
|
||||
target[k] = merged
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return target
|
||||
}
|
||||
|
||||
const configUtils = {
|
||||
colors,
|
||||
negative(scale) {
|
||||
// TODO: Log that this function isn't really needed anymore?
|
||||
return Object.keys(scale)
|
||||
.filter((key) => scale[key] !== '0')
|
||||
.reduce((negativeScale, key) => {
|
||||
let negativeValue = negateValue(scale[key])
|
||||
|
||||
if (negativeValue !== undefined) {
|
||||
negativeScale[`-${key}`] = negativeValue
|
||||
}
|
||||
|
||||
return negativeScale
|
||||
}, {})
|
||||
},
|
||||
breakpoints(screens) {
|
||||
return Object.keys(screens)
|
||||
.filter((key) => typeof screens[key] === 'string')
|
||||
.reduce(
|
||||
(breakpoints, key) => ({
|
||||
...breakpoints,
|
||||
[`screen-${key}`]: screens[key],
|
||||
}),
|
||||
{}
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
function value(valueToResolve, ...args) {
|
||||
return isFunction(valueToResolve) ? valueToResolve(...args) : valueToResolve
|
||||
}
|
||||
|
||||
function collectExtends(items) {
|
||||
return items.reduce((merged, { extend }) => {
|
||||
return mergeWith(merged, extend, (mergedValue, extendValue) => {
|
||||
if (mergedValue === undefined) {
|
||||
return [extendValue]
|
||||
}
|
||||
|
||||
if (Array.isArray(mergedValue)) {
|
||||
return [extendValue, ...mergedValue]
|
||||
}
|
||||
|
||||
return [extendValue, mergedValue]
|
||||
})
|
||||
}, {})
|
||||
}
|
||||
|
||||
function mergeThemes(themes) {
|
||||
return {
|
||||
...themes.reduce((merged, theme) => defaults(merged, theme), {}),
|
||||
|
||||
// In order to resolve n config objects, we combine all of their `extend` properties
|
||||
// into arrays instead of objects so they aren't overridden.
|
||||
extend: collectExtends(themes),
|
||||
}
|
||||
}
|
||||
|
||||
function mergeExtensionCustomizer(merged, value) {
|
||||
// When we have an array of objects, we do want to merge it
|
||||
if (Array.isArray(merged) && isObject(merged[0])) {
|
||||
return merged.concat(value)
|
||||
}
|
||||
|
||||
// When the incoming value is an array, and the existing config is an object, prepend the existing object
|
||||
if (Array.isArray(value) && isObject(value[0]) && isObject(merged)) {
|
||||
return [merged, ...value]
|
||||
}
|
||||
|
||||
// Override arrays (for example for font-families, box-shadows, ...)
|
||||
if (Array.isArray(value)) {
|
||||
return value
|
||||
}
|
||||
|
||||
// Execute default behaviour
|
||||
return undefined
|
||||
}
|
||||
|
||||
function mergeExtensions({ extend, ...theme }) {
|
||||
return mergeWith(theme, extend, (themeValue, extensions) => {
|
||||
// The `extend` property is an array, so we need to check if it contains any functions
|
||||
if (!isFunction(themeValue) && !extensions.some(isFunction)) {
|
||||
return mergeWith({}, themeValue, ...extensions, mergeExtensionCustomizer)
|
||||
}
|
||||
|
||||
return (resolveThemePath, utils) =>
|
||||
mergeWith(
|
||||
{},
|
||||
...[themeValue, ...extensions].map((e) => value(e, resolveThemePath, utils)),
|
||||
mergeExtensionCustomizer
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} key
|
||||
* @return {Iterable<string[] & {alpha: string | undefined}>}
|
||||
*/
|
||||
function* toPaths(key) {
|
||||
let path = toPath(key)
|
||||
|
||||
if (path.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
yield path
|
||||
|
||||
if (Array.isArray(key)) {
|
||||
return
|
||||
}
|
||||
|
||||
let pattern = /^(.*?)\s*\/\s*([^/]+)$/
|
||||
let matches = key.match(pattern)
|
||||
|
||||
if (matches !== null) {
|
||||
let [, prefix, alpha] = matches
|
||||
|
||||
let newPath = toPath(prefix)
|
||||
newPath.alpha = alpha
|
||||
|
||||
yield newPath
|
||||
}
|
||||
}
|
||||
|
||||
function resolveFunctionKeys(object) {
|
||||
// theme('colors.red.500 / 0.5') -> ['colors', 'red', '500 / 0', '5]
|
||||
|
||||
const resolvePath = (key, defaultValue) => {
|
||||
for (const path of toPaths(key)) {
|
||||
let index = 0
|
||||
let val = object
|
||||
|
||||
while (val !== undefined && val !== null && index < path.length) {
|
||||
val = val[path[index++]]
|
||||
|
||||
let shouldResolveAsFn =
|
||||
isFunction(val) && (path.alpha === undefined || index <= path.length - 1)
|
||||
|
||||
val = shouldResolveAsFn ? val(resolvePath, configUtils) : val
|
||||
}
|
||||
|
||||
if (val !== undefined) {
|
||||
if (path.alpha !== undefined) {
|
||||
let normalized = parseColorFormat(val)
|
||||
|
||||
return withAlphaValue(normalized, path.alpha, toColorValue(normalized))
|
||||
}
|
||||
|
||||
if (isPlainObject(val)) {
|
||||
return cloneDeep(val)
|
||||
}
|
||||
|
||||
return val
|
||||
}
|
||||
}
|
||||
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
Object.assign(resolvePath, {
|
||||
theme: resolvePath,
|
||||
...configUtils,
|
||||
})
|
||||
|
||||
return Object.keys(object).reduce((resolved, key) => {
|
||||
resolved[key] = isFunction(object[key]) ? object[key](resolvePath, configUtils) : object[key]
|
||||
|
||||
return resolved
|
||||
}, {})
|
||||
}
|
||||
|
||||
function extractPluginConfigs(configs) {
|
||||
let allConfigs = []
|
||||
|
||||
configs.forEach((config) => {
|
||||
allConfigs = [...allConfigs, config]
|
||||
|
||||
const plugins = config?.plugins ?? []
|
||||
|
||||
if (plugins.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
plugins.forEach((plugin) => {
|
||||
if (plugin.__isOptionsFunction) {
|
||||
plugin = plugin()
|
||||
}
|
||||
allConfigs = [...allConfigs, ...extractPluginConfigs([plugin?.config ?? {}])]
|
||||
})
|
||||
})
|
||||
|
||||
return allConfigs
|
||||
}
|
||||
|
||||
function resolveCorePlugins(corePluginConfigs) {
|
||||
const result = [...corePluginConfigs].reduceRight((resolved, corePluginConfig) => {
|
||||
if (isFunction(corePluginConfig)) {
|
||||
return corePluginConfig({ corePlugins: resolved })
|
||||
}
|
||||
return configurePlugins(corePluginConfig, resolved)
|
||||
}, corePluginList)
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
function resolvePluginLists(pluginLists) {
|
||||
const result = [...pluginLists].reduceRight((resolved, pluginList) => {
|
||||
return [...resolved, ...pluginList]
|
||||
}, [])
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
export default function resolveConfig(configs) {
|
||||
let allConfigs = [
|
||||
...extractPluginConfigs(configs),
|
||||
{
|
||||
prefix: '',
|
||||
important: false,
|
||||
separator: ':',
|
||||
variantOrder: defaultConfig.variantOrder,
|
||||
},
|
||||
]
|
||||
|
||||
return normalizeConfig(
|
||||
defaults(
|
||||
{
|
||||
theme: resolveFunctionKeys(
|
||||
mergeExtensions(mergeThemes(allConfigs.map((t) => t?.theme ?? {})))
|
||||
),
|
||||
corePlugins: resolveCorePlugins(allConfigs.map((c) => c.corePlugins)),
|
||||
plugins: resolvePluginLists(configs.map((c) => c?.plugins ?? [])),
|
||||
},
|
||||
...allConfigs
|
||||
)
|
||||
)
|
||||
}
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
|
||||
function isObject(value) {
|
||||
return typeof value === 'object' && value !== null
|
||||
}
|
||||
|
||||
function isEmpty(obj) {
|
||||
return Object.keys(obj).length === 0
|
||||
}
|
||||
|
||||
function isString(value) {
|
||||
return typeof value === 'string' || value instanceof String
|
||||
}
|
||||
|
||||
export default function resolveConfigPath(pathOrConfig) {
|
||||
// require('tailwindcss')({ theme: ..., variants: ... })
|
||||
if (isObject(pathOrConfig) && pathOrConfig.config === undefined && !isEmpty(pathOrConfig)) {
|
||||
return null
|
||||
}
|
||||
|
||||
// require('tailwindcss')({ config: 'custom-config.js' })
|
||||
if (
|
||||
isObject(pathOrConfig) &&
|
||||
pathOrConfig.config !== undefined &&
|
||||
isString(pathOrConfig.config)
|
||||
) {
|
||||
return path.resolve(pathOrConfig.config)
|
||||
}
|
||||
|
||||
// require('tailwindcss')({ config: { theme: ..., variants: ... } })
|
||||
if (
|
||||
isObject(pathOrConfig) &&
|
||||
pathOrConfig.config !== undefined &&
|
||||
isObject(pathOrConfig.config)
|
||||
) {
|
||||
return null
|
||||
}
|
||||
|
||||
// require('tailwindcss')('custom-config.js')
|
||||
if (isString(pathOrConfig)) {
|
||||
return path.resolve(pathOrConfig)
|
||||
}
|
||||
|
||||
// require('tailwindcss')
|
||||
for (const configFile of ['./tailwind.config.js', './tailwind.config.cjs']) {
|
||||
try {
|
||||
const configPath = path.resolve(configFile)
|
||||
fs.accessSync(configPath)
|
||||
return configPath
|
||||
} catch (err) {}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
import postcss from 'postcss'
|
||||
import cloneNodes from './cloneNodes'
|
||||
|
||||
export default function responsive(rules) {
|
||||
return postcss
|
||||
.atRule({
|
||||
name: 'responsive',
|
||||
})
|
||||
.append(cloneNodes(Array.isArray(rules) ? rules : [rules]))
|
||||
}
|
||||
+71
@@ -0,0 +1,71 @@
|
||||
import * as regex from '../lib/regex'
|
||||
|
||||
/**
|
||||
* This splits a string on a top-level character.
|
||||
*
|
||||
* Regex doesn't support recursion (at least not the JS-flavored version).
|
||||
* So we have to use a tiny state machine to keep track of paren placement.
|
||||
*
|
||||
* Expected behavior using commas:
|
||||
* var(--a, 0 0 1px rgb(0, 0, 0)), 0 0 1px rgb(0, 0, 0)
|
||||
* ─┬─ ┬ ┬ ┬
|
||||
* x x x ╰──────── Split because top-level
|
||||
* ╰──────────────┴──┴───────────── Ignored b/c inside >= 1 levels of parens
|
||||
*
|
||||
* @param {string} input
|
||||
* @param {string} separator
|
||||
*/
|
||||
export function* splitAtTopLevelOnly(input, separator) {
|
||||
let SPECIALS = new RegExp(`[(){}\\[\\]${regex.escape(separator)}]`, 'g')
|
||||
|
||||
let depth = 0
|
||||
let lastIndex = 0
|
||||
let found = false
|
||||
let separatorIndex = 0
|
||||
let separatorStart = 0
|
||||
let separatorLength = separator.length
|
||||
|
||||
// Find all paren-like things & character
|
||||
// And only split on commas if they're top-level
|
||||
for (let match of input.matchAll(SPECIALS)) {
|
||||
let matchesSeparator = match[0] === separator[separatorIndex]
|
||||
let atEndOfSeparator = separatorIndex === separatorLength - 1
|
||||
let matchesFullSeparator = matchesSeparator && atEndOfSeparator
|
||||
|
||||
if (match[0] === '(') depth++
|
||||
if (match[0] === ')') depth--
|
||||
if (match[0] === '[') depth++
|
||||
if (match[0] === ']') depth--
|
||||
if (match[0] === '{') depth++
|
||||
if (match[0] === '}') depth--
|
||||
|
||||
if (matchesSeparator && depth === 0) {
|
||||
if (separatorStart === 0) {
|
||||
separatorStart = match.index
|
||||
}
|
||||
|
||||
separatorIndex++
|
||||
}
|
||||
|
||||
if (matchesFullSeparator && depth === 0) {
|
||||
found = true
|
||||
|
||||
yield input.substring(lastIndex, separatorStart)
|
||||
lastIndex = separatorStart + separatorLength
|
||||
}
|
||||
|
||||
if (separatorIndex === separatorLength) {
|
||||
separatorIndex = 0
|
||||
separatorStart = 0
|
||||
}
|
||||
}
|
||||
|
||||
// Provide the last segment of the string if available
|
||||
// Otherwise the whole string since no `char`s were found
|
||||
// This mirrors the behavior of string.split()
|
||||
if (found) {
|
||||
yield input.substring(lastIndex)
|
||||
} else {
|
||||
yield input
|
||||
}
|
||||
}
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
export function tap(value, mutator) {
|
||||
mutator(value)
|
||||
return value
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
export default function toColorValue(maybeFunction) {
|
||||
return typeof maybeFunction === 'function' ? maybeFunction({}) : maybeFunction
|
||||
}
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
/**
|
||||
* Parse a path string into an array of path segments.
|
||||
*
|
||||
* Square bracket notation `a[b]` may be used to "escape" dots that would otherwise be interpreted as path separators.
|
||||
*
|
||||
* Example:
|
||||
* a -> ['a']
|
||||
* a.b.c -> ['a', 'b', 'c']
|
||||
* a[b].c -> ['a', 'b', 'c']
|
||||
* a[b.c].e.f -> ['a', 'b.c', 'e', 'f']
|
||||
* a[b][c][d] -> ['a', 'b', 'c', 'd']
|
||||
*
|
||||
* @param {string|string[]} path
|
||||
**/
|
||||
export function toPath(path) {
|
||||
if (Array.isArray(path)) return path
|
||||
|
||||
let openBrackets = path.split('[').length - 1
|
||||
let closedBrackets = path.split(']').length - 1
|
||||
|
||||
if (openBrackets !== closedBrackets) {
|
||||
throw new Error(`Path is invalid. Has unbalanced brackets: ${path}`)
|
||||
}
|
||||
|
||||
return path.split(/\.(?![^\[]*\])|[\[\]]/g).filter(Boolean)
|
||||
}
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
import postcss from 'postcss'
|
||||
|
||||
export default function transformThemeValue(themeSection) {
|
||||
if (['fontSize', 'outline'].includes(themeSection)) {
|
||||
return (value) => {
|
||||
if (typeof value === 'function') value = value({})
|
||||
if (Array.isArray(value)) value = value[0]
|
||||
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
[
|
||||
'fontFamily',
|
||||
'boxShadow',
|
||||
'transitionProperty',
|
||||
'transitionDuration',
|
||||
'transitionDelay',
|
||||
'transitionTimingFunction',
|
||||
'backgroundImage',
|
||||
'backgroundSize',
|
||||
'backgroundColor',
|
||||
'cursor',
|
||||
'animation',
|
||||
].includes(themeSection)
|
||||
) {
|
||||
return (value) => {
|
||||
if (typeof value === 'function') value = value({})
|
||||
if (Array.isArray(value)) value = value.join(', ')
|
||||
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
// For backwards compatibility reasons, before we switched to underscores
|
||||
// instead of commas for arbitrary values.
|
||||
if (['gridTemplateColumns', 'gridTemplateRows', 'objectPosition'].includes(themeSection)) {
|
||||
return (value) => {
|
||||
if (typeof value === 'function') value = value({})
|
||||
if (typeof value === 'string') value = postcss.list.comma(value).join(' ')
|
||||
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
return (value, opts = {}) => {
|
||||
if (typeof value === 'function') {
|
||||
value = value(opts)
|
||||
}
|
||||
|
||||
return value
|
||||
}
|
||||
}
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
import log from './log'
|
||||
|
||||
export function validateConfig(config) {
|
||||
if (config.content.files.length === 0) {
|
||||
log.warn('content-problems', [
|
||||
'The `content` option in your Tailwind CSS configuration is missing or empty.',
|
||||
'Configure your content sources or your generated CSS will be missing styles.',
|
||||
'https://tailwindcss.com/docs/content-configuration',
|
||||
])
|
||||
}
|
||||
|
||||
return config
|
||||
}
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
import { parseColor, formatColor } from './color'
|
||||
|
||||
export function withAlphaValue(color, alphaValue, defaultValue) {
|
||||
if (typeof color === 'function') {
|
||||
return color({ opacityValue: alphaValue })
|
||||
}
|
||||
|
||||
let parsed = parseColor(color, { loose: true })
|
||||
|
||||
if (parsed === null) {
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
return formatColor({ ...parsed, alpha: alphaValue })
|
||||
}
|
||||
|
||||
export default function withAlphaVariable({ color, property, variable }) {
|
||||
let properties = [].concat(property)
|
||||
if (typeof color === 'function') {
|
||||
return {
|
||||
[variable]: '1',
|
||||
...Object.fromEntries(
|
||||
properties.map((p) => {
|
||||
return [p, color({ opacityVariable: variable, opacityValue: `var(${variable})` })]
|
||||
})
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
const parsed = parseColor(color)
|
||||
|
||||
if (parsed === null) {
|
||||
return Object.fromEntries(properties.map((p) => [p, color]))
|
||||
}
|
||||
|
||||
if (parsed.alpha !== undefined) {
|
||||
// Has an alpha value, return color as-is
|
||||
return Object.fromEntries(properties.map((p) => [p, color]))
|
||||
}
|
||||
|
||||
return {
|
||||
[variable]: '1',
|
||||
...Object.fromEntries(
|
||||
properties.map((p) => {
|
||||
return [p, formatColor({ ...parsed, alpha: `var(${variable})` })]
|
||||
})
|
||||
),
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user