|
import fs from 'fs' |
|
import LRU from '@alloc/quick-lru' |
|
import * as sharedState from './sharedState' |
|
import { generateRules } from './generateRules' |
|
import log from '../util/log' |
|
import cloneNodes from '../util/cloneNodes' |
|
import { defaultExtractor } from './defaultExtractor' |
|
|
|
let env = sharedState.env |
|
|
|
const builtInExtractors = { |
|
DEFAULT: defaultExtractor, |
|
} |
|
|
|
const builtInTransformers = { |
|
DEFAULT: (content) => content, |
|
svelte: (content) => content.replace(/(?:^|\s)class:/g, ' '), |
|
} |
|
|
|
function getExtractor(context, fileExtension) { |
|
let extractors = context.tailwindConfig.content.extract |
|
|
|
return ( |
|
extractors[fileExtension] || |
|
extractors.DEFAULT || |
|
builtInExtractors[fileExtension] || |
|
builtInExtractors.DEFAULT(context) |
|
) |
|
} |
|
|
|
function getTransformer(tailwindConfig, fileExtension) { |
|
let transformers = tailwindConfig.content.transform |
|
|
|
return ( |
|
transformers[fileExtension] || |
|
transformers.DEFAULT || |
|
builtInTransformers[fileExtension] || |
|
builtInTransformers.DEFAULT |
|
) |
|
} |
|
|
|
let extractorCache = new WeakMap() |
|
|
|
|
|
|
|
|
|
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 = context.offsets.sort(rules) |
|
|
|
let returnValue = { |
|
base: new Set(), |
|
defaults: new Set(), |
|
components: new Set(), |
|
utilities: new Set(), |
|
variants: new Set(), |
|
} |
|
|
|
for (let [sort, rule] of sortedRules) { |
|
returnValue[sort.layer].add(rule) |
|
} |
|
|
|
return returnValue |
|
} |
|
|
|
export default function expandTailwindAtRules(context) { |
|
return async (root) => { |
|
let layerNodes = { |
|
base: null, |
|
components: null, |
|
utilities: null, |
|
variants: null, |
|
} |
|
|
|
root.walkAtRules((rule) => { |
|
|
|
|
|
|
|
|
|
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 |
|
} |
|
|
|
|
|
|
|
|
|
let candidates = new Set([...(context.candidates ?? []), sharedState.NOT_ON_DEMAND]) |
|
let seen = new Set() |
|
|
|
env.DEBUG && console.time('Reading changed files') |
|
|
|
if (__OXIDE__) { |
|
|
|
for (let candidate of require('@tailwindcss/oxide').parseCandidateStringsFromFiles( |
|
context.changedContent |
|
|
|
)) { |
|
candidates.add(candidate) |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
} else { |
|
|
|
let regexParserContent = [] |
|
|
|
for (let item of context.changedContent) { |
|
let transformer = getTransformer(context.tailwindConfig, item.extension) |
|
let extractor = getExtractor(context, item.extension) |
|
regexParserContent.push([item, { transformer, extractor }]) |
|
} |
|
|
|
const BATCH_SIZE = 500 |
|
|
|
for (let i = 0; i < regexParserContent.length; i += BATCH_SIZE) { |
|
let batch = regexParserContent.slice(i, i + BATCH_SIZE) |
|
await Promise.all( |
|
batch.map(async ([{ file, content }, { transformer, extractor }]) => { |
|
content = file ? await fs.promises.readFile(file, 'utf8') : content |
|
getClassCandidates(transformer(content), extractor, candidates, seen) |
|
}) |
|
) |
|
} |
|
} |
|
|
|
env.DEBUG && console.timeEnd('Reading changed files') |
|
|
|
|
|
|
|
|
|
let classCacheCount = context.classCache.size |
|
|
|
env.DEBUG && console.time('Generate rules') |
|
env.DEBUG && console.time('Sorting candidates') |
|
let sortedCandidates = __OXIDE__ |
|
? candidates |
|
: new Set( |
|
[...candidates].sort((a, z) => { |
|
if (a === z) return 0 |
|
if (a < z) return -1 |
|
return 1 |
|
}) |
|
) |
|
env.DEBUG && console.timeEnd('Sorting candidates') |
|
generateRules(sortedCandidates, context) |
|
env.DEBUG && console.timeEnd('Generate rules') |
|
|
|
|
|
env.DEBUG && console.time('Build stylesheet') |
|
if (context.stylesheetCache === null || context.classCache.size !== classCacheCount) { |
|
context.stylesheetCache = buildStylesheet([...context.ruleCache], context) |
|
} |
|
env.DEBUG && console.timeEnd('Build stylesheet') |
|
|
|
let { |
|
defaults: defaultNodes, |
|
base: baseNodes, |
|
components: componentNodes, |
|
utilities: utilityNodes, |
|
variants: screenNodes, |
|
} = context.stylesheetCache |
|
|
|
|
|
|
|
|
|
|
|
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() |
|
} |
|
|
|
|
|
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', |
|
}) |
|
) |
|
} |
|
|
|
|
|
root.source.end = root.source.end ?? root.source.start |
|
|
|
|
|
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) |
|
} |
|
|
|
|
|
context.changedContent = [] |
|
|
|
|
|
root.walkAtRules('layer', (rule) => { |
|
if (Object.keys(layerNodes).includes(rule.params)) { |
|
rule.remove() |
|
} |
|
}) |
|
} |
|
} |
|
|