diff --git a/integrations/tailwindcss-cli/tests/integration.test.js b/integrations/tailwindcss-cli/tests/integration.test.js index 72c803782c46..8b8578c80c72 100644 --- a/integrations/tailwindcss-cli/tests/integration.test.js +++ b/integrations/tailwindcss-cli/tests/integration.test.js @@ -34,9 +34,9 @@ describe('static build', () => { javascript` module.exports = { content: { - content: ['./src/index.html'], - safelist: ['bg-red-500','bg-red-600'] + files: ['./src/index.html'], }, + safelist: ['bg-red-500','bg-red-600'], theme: { extend: { }, diff --git a/integrations/webpack-5/tests/integration.test.js b/integrations/webpack-5/tests/integration.test.js index b6f654522f9e..58a1c2b191c0 100644 --- a/integrations/webpack-5/tests/integration.test.js +++ b/integrations/webpack-5/tests/integration.test.js @@ -230,9 +230,9 @@ describe.each([{ TAILWIND_MODE: 'watch' }, { TAILWIND_MODE: undefined }])('watch javascript` module.exports = { content: { - content: ['./src/index.html'], - safelist: ['bg-red-500','bg-red-600'] + files: ['./src/index.html'], }, + safelist: ['bg-red-500','bg-red-600'], theme: { extend: { }, diff --git a/src/cli.js b/src/cli.js index 611d446c6cde..c171dc0a4a35 100644 --- a/src/cli.js +++ b/src/cli.js @@ -436,12 +436,8 @@ async function build() { return resolvedConfig } - function extractContent(config) { - return config.content.content.concat(config.content.safelist) - } - function extractFileGlobs(config) { - return extractContent(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 @@ -452,7 +448,7 @@ async function build() { } function extractRawContent(config) { - return extractContent(config).filter((file) => { + return config.content.files.filter((file) => { return typeof file === 'object' && file !== null }) } @@ -467,7 +463,7 @@ async function build() { for (let file of files) { changedContent.push({ content: fs.readFileSync(path.resolve(file), 'utf8'), - extension: path.extname(file), + extension: path.extname(file).slice(1), }) } @@ -729,7 +725,7 @@ async function build() { chain = chain.then(async () => { changedContent.push({ content: fs.readFileSync(path.resolve(file), 'utf8'), - extension: path.extname(file), + extension: path.extname(file).slice(1), }) await rebuild(config) @@ -741,7 +737,7 @@ async function build() { chain = chain.then(async () => { changedContent.push({ content: fs.readFileSync(path.resolve(file), 'utf8'), - extension: path.extname(file), + extension: path.extname(file).slice(1), }) await rebuild(config) diff --git a/src/lib/expandTailwindAtRules.js b/src/lib/expandTailwindAtRules.js index bf8273223801..d090b832f1a9 100644 --- a/src/lib/expandTailwindAtRules.js +++ b/src/lib/expandTailwindAtRules.js @@ -1,10 +1,10 @@ +import LRU from 'quick-lru' import * as sharedState from './sharedState' import { generateRules } from './generateRules' import bigSign from '../util/bigSign' import cloneNodes from '../util/cloneNodes' let env = sharedState.env -let contentMatchCache = sharedState.contentMatchCache const PATTERNS = [ /([^<>"'`\s]*\[\w*'[^"`\s]*'?\])/.source, // font-['some_font',sans-serif] @@ -35,21 +35,6 @@ const builtInTransformers = { function getExtractor(tailwindConfig, fileExtension) { let extractors = tailwindConfig.content.extract - let contentOptions = tailwindConfig.content.options - - if (typeof extractors === 'function') { - extractors = { - DEFAULT: extractors, - } - } - if (contentOptions.defaultExtractor) { - extractors.DEFAULT = contentOptions.defaultExtractor - } - for (let { extensions, extractor } of contentOptions.extractors || []) { - for (let extension of extensions) { - extractors[extension] = extractor - } - } return ( extractors[fileExtension] || @@ -62,12 +47,6 @@ function getExtractor(tailwindConfig, fileExtension) { function getTransformer(tailwindConfig, fileExtension) { let transformers = tailwindConfig.content.transform - if (typeof transformers === 'function') { - transformers = { - DEFAULT: transformers, - } - } - return ( transformers[fileExtension] || transformers.DEFAULT || @@ -76,10 +55,16 @@ function getTransformer(tailwindConfig, fileExtension) { ) } +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, contentMatchCache, candidates, seen) { +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() @@ -88,8 +73,8 @@ function getClassCandidates(content, extractor, contentMatchCache, candidates, s } seen.add(line) - if (contentMatchCache.has(line)) { - for (let match of contentMatchCache.get(line)) { + if (extractorCache.get(extractor).has(line)) { + for (let match of extractorCache.get(extractor).get(line)) { candidates.add(match) } } else { @@ -100,7 +85,7 @@ function getClassCandidates(content, extractor, contentMatchCache, candidates, s candidates.add(match) } - contentMatchCache.set(line, lineMatchesSet) + extractorCache.get(extractor).set(line, lineMatchesSet) } } } @@ -189,7 +174,7 @@ export default function expandTailwindAtRules(context) { for (let { content, extension } of context.changedContent) { let transformer = getTransformer(context.tailwindConfig, extension) let extractor = getExtractor(context.tailwindConfig, extension) - getClassCandidates(transformer(content), extractor, contentMatchCache, candidates, seen) + getClassCandidates(transformer(content), extractor, candidates, seen) } // --- diff --git a/src/lib/setupContextUtils.js b/src/lib/setupContextUtils.js index e3ae20e497e1..5826c4340d2c 100644 --- a/src/lib/setupContextUtils.js +++ b/src/lib/setupContextUtils.js @@ -559,16 +559,13 @@ function registerPlugins(plugins, context) { ) } - context.safelist = function () { - let safelist = (context.tailwindConfig.safelist ?? []).filter(Boolean) - if (safelist.length <= 0) return [] - - let output = [] + let safelist = (context.tailwindConfig.safelist ?? []).filter(Boolean) + if (safelist.length > 0) { let checks = [] for (let value of safelist) { if (typeof value === 'string') { - output.push(value) + context.changedContent.push({ content: value, extension: 'html' }) continue } @@ -584,51 +581,52 @@ function registerPlugins(plugins, context) { checks.push(value) } - if (checks.length <= 0) return output.map((value) => ({ raw: value, extension: 'html' })) - - let patternMatchingCount = new Map() - - for (let util of classList) { - let utils = Array.isArray(util) - ? (() => { - let [utilName, options] = util - return Object.keys(options?.values ?? {}).map((value) => formatClass(utilName, value)) - })() - : [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 (checks.length > 0) { + let patternMatchingCount = new Map() + + for (let util of classList) { + let utils = Array.isArray(util) + ? (() => { + let [utilName, options] = util + return Object.keys(options?.values ?? {}).map((value) => formatClass(utilName, value)) + })() + : [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 + if (!pattern.test(util)) continue - patternMatchingCount.set(pattern, patternMatchingCount.get(pattern) + 1) + patternMatchingCount.set(pattern, patternMatchingCount.get(pattern) + 1) - output.push(util) - for (let variant of variants) { - output.push(variant + context.tailwindConfig.separator + util) + 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 + for (let [regex, count] of patternMatchingCount.entries()) { + if (count !== 0) continue - log.warn([ - // TODO: Improve this warning message - `You have a regex pattern in your "safelist" config (${regex}) that doesn't match any utilities.`, - 'For more info, visit https://tailwindcss.com/docs/...', - ]) + log.warn([ + // TODO: Improve this warning message + `You have a regex pattern in your "safelist" config (${regex}) that doesn't match any utilities.`, + 'For more info, visit https://tailwindcss.com/docs/...', + ]) + } } - - return output.map((value) => ({ raw: value, extension: 'html' })) } // Generate a list of strings for autocompletion purposes, e.g. diff --git a/src/lib/setupTrackingContext.js b/src/lib/setupTrackingContext.js index e3d7898e187b..d4015c5bf59e 100644 --- a/src/lib/setupTrackingContext.js +++ b/src/lib/setupTrackingContext.js @@ -26,7 +26,7 @@ function getCandidateFiles(context, tailwindConfig) { return candidateFilesCache.get(context) } - let candidateFiles = tailwindConfig.content.content + let candidateFiles = tailwindConfig.content.files .filter((item) => typeof item === 'string') .map((contentPath) => normalizePath(contentPath)) @@ -77,11 +77,9 @@ function getTailwindConfig(configOrPath) { } function resolvedChangedContent(context, candidateFiles, fileModifiedMap) { - let changedContent = context.tailwindConfig.content.content + let changedContent = context.tailwindConfig.content.files .filter((item) => typeof item.raw === 'string') - .concat(context.tailwindConfig.content.safelist) - .concat(context.safelist()) - .map(({ raw, extension }) => ({ content: raw, extension })) + .map(({ raw, extension = 'html' }) => ({ content: raw, extension })) for (let changedFile of resolveChangedFiles(candidateFiles, fileModifiedMap)) { let content = fs.readFileSync(changedFile, 'utf8') diff --git a/src/lib/setupWatchingContext.js b/src/lib/setupWatchingContext.js index 2ecee6e6bc07..c20d043dd134 100644 --- a/src/lib/setupWatchingContext.js +++ b/src/lib/setupWatchingContext.js @@ -147,7 +147,7 @@ function getCandidateFiles(context, tailwindConfig) { return candidateFilesCache.get(context) } - let candidateFiles = tailwindConfig.content.content + let candidateFiles = tailwindConfig.content.files .filter((item) => typeof item === 'string') .map((contentPath) => normalizePath(contentPath)) @@ -185,11 +185,9 @@ function getTailwindConfig(configOrPath) { } function resolvedChangedContent(context, candidateFiles) { - let changedContent = context.tailwindConfig.content.content + let changedContent = context.tailwindConfig.content.files .filter((item) => typeof item.raw === 'string') - .concat(context.tailwindConfig.content.safelist) - .concat(context.safelist()) - .map(({ raw, extension }) => ({ content: raw, extension })) + .map(({ raw, extension = 'html' }) => ({ content: raw, extension })) for (let changedFile of resolveChangedFiles(context, candidateFiles)) { let content = fs.readFileSync(changedFile, 'utf8') diff --git a/src/lib/sharedState.js b/src/lib/sharedState.js index 78ed1ce5d9ff..a15e8cc3d122 100644 --- a/src/lib/sharedState.js +++ b/src/lib/sharedState.js @@ -1,5 +1,3 @@ -import LRU from 'quick-lru' - export const env = { TAILWIND_MODE: process.env.TAILWIND_MODE, NODE_ENV: process.env.NODE_ENV, @@ -10,4 +8,3 @@ export const env = { export const contextMap = new Map() export const configContextMap = new Map() export const contextSourcesMap = new Map() -export const contentMatchCache = new LRU({ maxSize: 25000 }) diff --git a/src/util/normalizeConfig.js b/src/util/normalizeConfig.js new file mode 100644 index 000000000000..3a9acb18cb56 --- /dev/null +++ b/src/util/normalizeConfig.js @@ -0,0 +1,234 @@ +import log from './log' + +export function normalizeConfig(config) { + // Quick structure validation + /** + * type FilePath = string + * type RawFile = { raw: string, extension?: string } + * type ExtractorFn = (content: string) => Array + * type TransformerFn = (content: string) => string + * + * type Content = + * | Array + * | { + * files: Array, + * 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 } + if ( + // `raw` must be a string + typeof path?.raw === 'string' && + // `extension` (if provided) should also be a string + path?.extension !== undefined && + typeof path?.extension === 'string' + ) { + return true + } + + return false + }) + } + + // 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 } + if ( + // `raw` must be a string + typeof path?.raw === 'string' && + // `extension` (if provided) should also be a string + path?.extension !== undefined && + typeof path?.extension === 'string' + ) { + return true + } + + return false + }) + ) { + return false + } + + // `config.content.extract` is optional, and can be a Function or a Record + if (typeof config.content.extract === 'object') { + for (let value of Object.values(config.content.extract)) { + if (typeof value !== 'function') { + return false + } + } + 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 + if (typeof config.content.transform === 'object') { + for (let value of Object.values(config.content.transform)) { + if (typeof value !== 'function') { + return false + } + } + 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` option in your tailwind.config.js file has been deprecated.', + 'Please rename this to `content` instead.', + ]) + } + + // 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 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 = {} + + extractors.DEFAULT = (() => { + if (config.purge?.options?.defaultExtractor) { + return config.purge.options.defaultExtractor + } + + if (config.content?.options?.defaultExtractor) { + return config.content.options.defaultExtractor + } + + return undefined + })() + + // 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 + })(), + } + + return config +} diff --git a/src/util/resolveConfig.js b/src/util/resolveConfig.js index 8f8c31fffcb9..e631a973f521 100644 --- a/src/util/resolveConfig.js +++ b/src/util/resolveConfig.js @@ -3,9 +3,9 @@ import corePluginList from '../corePluginList' import configurePlugins from './configurePlugins' import defaultConfig from '../../stubs/defaultConfig.stub' import colors from '../public/colors' -import log from './log' import { defaults } from './defaults' import { toPath } from './toPath' +import { normalizeConfig } from './normalizeConfig' function isFunction(input) { return typeof input === 'function' @@ -221,55 +221,3 @@ export default function resolveConfig(configs) { ) ) } - -function normalizeConfig(config) { - log.warn('purge-deprecation', [ - 'The `purge` option in your tailwind.config.js file has been deprecated.', - 'Please rename this to `content` instead.', - ]) - - config.content = { - content: (() => { - 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 - - return [] - })(), - safelist: (() => { - let { content, purge } = config - - let [safelistKey, safelistPaths] = (() => { - if (Array.isArray(content?.safelist)) return ['content.safelist', content.safelist] - if (Array.isArray(purge?.safelist)) return ['purge.safelist', purge.safelist] - if (Array.isArray(purge?.options?.safelist)) - return ['purge.options.safelist', purge.options.safelist] - return [null, []] - })() - - return safelistPaths.map((content) => { - if (typeof content === 'string') { - return { raw: content, extension: 'html' } - } - - if (content instanceof RegExp) { - throw new Error( - `Values inside '${safelistKey}' can only be of type 'string', found 'regex'.` - ) - } - - throw new Error( - `Values inside '${safelistKey}' can only be of type 'string', found '${typeof content}'.` - ) - }) - })(), - extract: config.content?.extract || config.purge?.extract || {}, - options: config.content?.options || config.purge?.options || {}, - transform: config.content?.transform || config.purge?.transform || {}, - } - - return config -} diff --git a/tests/color-opacity-modifiers.test.js b/tests/color-opacity-modifiers.test.js index 51da84aacae2..18b0be92bd2d 100644 --- a/tests/color-opacity-modifiers.test.js +++ b/tests/color-opacity-modifiers.test.js @@ -62,12 +62,7 @@ test('missing alpha generates nothing', async () => { test('arbitrary color with opacity from scale', async () => { let config = { - mode: 'jit', - purge: [ - { - raw: 'bg-[wheat]/50', - }, - ], + content: [{ raw: 'bg-[wheat]/50' }], theme: {}, plugins: [], } @@ -85,12 +80,7 @@ test('arbitrary color with opacity from scale', async () => { test('arbitrary color with arbitrary opacity', async () => { let config = { - mode: 'jit', - purge: [ - { - raw: 'bg-[#bada55]/[0.2]', - }, - ], + content: [{ raw: 'bg-[#bada55]/[0.2]' }], theme: {}, plugins: [], } @@ -108,12 +98,7 @@ test('arbitrary color with arbitrary opacity', async () => { test('undefined theme color with opacity from scale', async () => { let config = { - mode: 'jit', - purge: [ - { - raw: 'bg-garbage/50', - }, - ], + content: [{ raw: 'bg-garbage/50' }], theme: {}, plugins: [], } diff --git a/tests/custom-extractors.test.js b/tests/custom-extractors.test.js index 042ff27874e8..9a8301230a0b 100644 --- a/tests/custom-extractors.test.js +++ b/tests/custom-extractors.test.js @@ -11,80 +11,84 @@ function customExtractor(content) { let expectedPath = path.resolve(__dirname, './custom-extractors.test.css') let expected = fs.readFileSync(expectedPath, 'utf8') -test('defaultExtractor', () => { - let config = { - content: { - content: [path.resolve(__dirname, './custom-extractors.test.html')], - options: { - defaultExtractor: customExtractor, +describe('modern', () => { + test('extract.DEFAULT', () => { + let config = { + content: { + files: [path.resolve(__dirname, './custom-extractors.test.html')], + extract: { + DEFAULT: customExtractor, + }, }, - }, - } + } - return run('@tailwind utilities', config).then((result) => { - expect(result.css).toMatchFormattedCss(expected) + return run('@tailwind utilities', config).then((result) => { + expect(result.css).toMatchFormattedCss(expected) + }) }) -}) -test('extractors array', () => { - let config = { - content: { - content: [path.resolve(__dirname, './custom-extractors.test.html')], - options: { - extractors: [ - { - extractor: customExtractor, - extensions: ['html'], - }, - ], + test('extract.{extension}', () => { + let config = { + content: { + files: [path.resolve(__dirname, './custom-extractors.test.html')], + extract: { + html: customExtractor, + }, }, - }, - } + } - return run('@tailwind utilities', config).then((result) => { - expect(result.css).toMatchFormattedCss(expected) + return run('@tailwind utilities', config).then((result) => { + expect(result.css).toMatchFormattedCss(expected) + }) }) -}) -test('extract function', () => { - let config = { - content: { - content: [path.resolve(__dirname, './custom-extractors.test.html')], - extract: customExtractor, - }, - } + test('extract function', () => { + let config = { + content: { + files: [path.resolve(__dirname, './custom-extractors.test.html')], + extract: customExtractor, + }, + } - return run('@tailwind utilities', config).then((result) => { - expect(result.css).toMatchFormattedCss(expected) + return run('@tailwind utilities', config).then((result) => { + expect(result.css).toMatchFormattedCss(expected) + }) }) }) -test('extract.DEFAULT', () => { - let config = { - content: { - content: [path.resolve(__dirname, './custom-extractors.test.html')], - extract: { - DEFAULT: customExtractor, +describe('legacy', () => { + test('defaultExtractor', () => { + let config = { + content: { + files: [path.resolve(__dirname, './custom-extractors.test.html')], + options: { + defaultExtractor: customExtractor, + }, }, - }, - } + } - return run('@tailwind utilities', config).then((result) => { - expect(result.css).toMatchFormattedCss(expected) + return run('@tailwind utilities', config).then((result) => { + expect(result.css).toMatchFormattedCss(expected) + }) }) -}) -test('extract.{extension}', () => { - let config = { - content: { - content: [path.resolve(__dirname, './custom-extractors.test.html')], - extract: { - html: customExtractor, + test('extractors array', () => { + let config = { + content: { + files: [path.resolve(__dirname, './custom-extractors.test.html')], + options: { + extractors: [ + { + extractor: customExtractor, + extensions: ['html'], + }, + ], + }, }, - }, - } + } - return run('@tailwind utilities', config).then((result) => { - expect(result.css).toMatchFormattedCss(expected) + return run('@tailwind utilities', config).then((result) => { + expect(result.css).toMatchFormattedCss(expected) + }) }) }) diff --git a/tests/custom-transformers.test.js b/tests/custom-transformers.test.js index 9402dd3007a7..bfee9c8e5266 100644 --- a/tests/custom-transformers.test.js +++ b/tests/custom-transformers.test.js @@ -7,7 +7,7 @@ function customTransformer(content) { test('transform function', () => { let config = { content: { - content: [{ raw: html`
` }], + files: [{ raw: html`
` }], transform: customTransformer, }, } @@ -24,7 +24,7 @@ test('transform function', () => { test('transform.DEFAULT', () => { let config = { content: { - content: [{ raw: html`
` }], + files: [{ raw: html`
` }], transform: { DEFAULT: customTransformer, }, @@ -43,7 +43,7 @@ test('transform.DEFAULT', () => { test('transform.{extension}', () => { let config = { content: { - content: [ + files: [ { raw: html`
`, extension: 'html' }, { raw: html`
`, extension: 'php' }, ], diff --git a/tests/normalize-config.test.js b/tests/normalize-config.test.js new file mode 100644 index 000000000000..602e10963a6c --- /dev/null +++ b/tests/normalize-config.test.js @@ -0,0 +1,97 @@ +import { run, css } from './util/run' + +it.each` + config + ${{ purge: [{ raw: 'text-center' }] }} + ${{ purge: { content: [{ raw: 'text-center' }] } }} + ${{ content: { content: [{ raw: 'text-center' }] } }} +`('should normalize content $config', ({ config }) => { + return run('@tailwind utilities', config).then((result) => { + return expect(result.css).toMatchFormattedCss(css` + .text-center { + text-align: center; + } + `) + }) +}) + +it.each` + config + ${{ purge: { safelist: ['text-center'] } }} + ${{ purge: { options: { safelist: ['text-center'] } } }} + ${{ content: { safelist: ['text-center'] } }} +`('should normalize safelist $config', ({ config }) => { + return run('@tailwind utilities', config).then((result) => { + return expect(result.css).toMatchFormattedCss(css` + .text-center { + text-align: center; + } + `) + }) +}) + +it.each` + config + ${{ content: [{ raw: 'text-center' }], purge: { extract: () => ['font-bold'] } }} + ${{ content: [{ raw: 'text-center' }], purge: { extract: { DEFAULT: () => ['font-bold'] } } }} + ${{ content: [{ raw: 'text-center' }], purge: { options: { defaultExtractor: () => ['font-bold'] } } }} + ${{ content: [{ raw: 'text-center' }], purge: { options: { extractors: [{ extractor: () => ['font-bold'], extensions: ['html'] }] } } }} + ${{ content: [{ raw: 'text-center' }], purge: { extract: { html: () => ['font-bold'] } } }} +`('should normalize extractors $config', ({ config }) => { + return run('@tailwind utilities', config).then((result) => { + return expect(result.css).toMatchFormattedCss(css` + .font-bold { + font-weight: 700; + } + `) + }) +}) + +it('should still be possible to use the "old" v2 config', () => { + let config = { + purge: { + content: [ + { raw: 'text-svelte', extension: 'svelte' }, + { raw: '# My Big Heading', extension: 'md' }, + ], + options: { + defaultExtractor(content) { + return content.split(' ').concat(['font-bold']) + }, + }, + extract: { + svelte(content) { + return content.replace('svelte', 'center').split(' ') + }, + }, + transform: { + md() { + return 'text-4xl' + }, + }, + }, + theme: { + extends: {}, + }, + variants: { + extends: {}, + }, + } + + return run('@tailwind utilities', config).then((result) => { + return expect(result.css).toMatchFormattedCss(css` + .text-center { + text-align: center; + } + + .text-4xl { + font-size: 2.25rem; + line-height: 2.5rem; + } + + .font-bold { + font-weight: 700; + } + `) + }) +}) diff --git a/tests/raw-content.test.js b/tests/raw-content.test.js index 563d2b50cb3e..c24dba919ecd 100644 --- a/tests/raw-content.test.js +++ b/tests/raw-content.test.js @@ -29,19 +29,14 @@ test('raw content with extension', () => { let tailwind = require('../src') let config = { content: { - content: [ + files: [ { raw: fs.readFileSync(path.resolve(__dirname, './raw-content.test.html'), 'utf8'), extension: 'html', }, ], - options: { - extractors: [ - { - extractor: () => ['invisible'], - extensions: ['html'], - }, - ], + extract: { + html: () => ['invisible'], }, }, corePlugins: { preflight: false }, diff --git a/tests/resolveConfig.test.js b/tests/resolveConfig.test.js index dba44d32e137..d8d0d8c3d363 100644 --- a/tests/resolveConfig.test.js +++ b/tests/resolveConfig.test.js @@ -10,6 +10,7 @@ test('prefix key overrides default prefix', () => { prefix: '', important: false, separator: ':', + content: [], theme: { screens: { mobile: '400px', @@ -40,6 +41,7 @@ test('important key overrides default important', () => { prefix: '', important: false, separator: ':', + content: [], theme: { screens: { mobile: '400px', @@ -70,6 +72,7 @@ test('important (selector) key overrides default important', () => { prefix: '', important: false, separator: ':', + content: [], theme: { screens: { mobile: '400px', @@ -100,6 +103,7 @@ test('separator key overrides default separator', () => { prefix: '', important: false, separator: ':', + content: [], theme: { screens: { mobile: '400px', @@ -134,6 +138,7 @@ test('theme key is merged instead of replaced', () => { prefix: '-', important: false, separator: ':', + content: [], theme: { colors: { 'grey-darker': '#606f7b', @@ -197,6 +202,7 @@ test('theme key is deeply merged instead of replaced', () => { prefix: '-', important: false, separator: ':', + content: [], theme: { colors: { grey: { @@ -235,6 +241,7 @@ test('missing top level keys are pulled from the default config', () => { prefix: '-', important: false, separator: ':', + content: [], theme: { colors: { green: '#00ff00' }, screens: { @@ -273,6 +280,7 @@ test('functions in the default theme section are lazily evaluated', () => { prefix: '-', important: false, separator: ':', + content: [], theme: { colors: { cyan: 'cyan', @@ -333,6 +341,7 @@ test('functions in the user theme section are lazily evaluated', () => { prefix: '-', important: false, separator: ':', + content: [], theme: { colors: { cyan: 'cyan', @@ -391,6 +400,7 @@ test('theme values in the extend section extend the existing theme', () => { prefix: '-', important: false, separator: ':', + content: [], theme: { colors: { cyan: 'cyan', @@ -461,6 +471,7 @@ test('theme values in the extend section extend the user theme', () => { prefix: '-', important: false, separator: ':', + content: [], theme: { opacity: { 0: '0', @@ -535,6 +546,7 @@ test('theme values in the extend section can extend values that are depended on prefix: '-', important: false, separator: ':', + content: [], theme: { colors: { cyan: 'cyan', @@ -588,6 +600,7 @@ test('theme values in the extend section are not deeply merged when they are sim prefix: '-', important: false, separator: ':', + content: [], theme: { fonts: { sans: ['system-ui', 'Helvetica Neue', 'sans-serif'], @@ -636,6 +649,7 @@ test('theme values in the extend section are deeply merged, when they are arrays prefix: '-', important: false, separator: ':', + content: [], theme: { typography: { ArrayArray: { @@ -696,6 +710,7 @@ test('the theme function can use a default value if the key is missing', () => { prefix: '-', important: false, separator: ':', + content: [], theme: { colors: { cyan: 'cyan', @@ -750,6 +765,7 @@ test('the theme function can resolve function values', () => { prefix: '-', important: false, separator: ':', + content: [], theme: { colors: { red: 'red', @@ -808,6 +824,7 @@ test('the theme function can resolve deep function values', () => { prefix: '-', important: false, separator: ':', + content: [], theme: { spacing: { 0: '0', @@ -864,6 +881,7 @@ test('theme values in the extend section are lazily evaluated', () => { prefix: '-', important: false, separator: ':', + content: [], theme: { colors: { cyan: 'cyan', @@ -924,6 +942,7 @@ test('lazily evaluated values have access to the config utils', () => { prefix: '-', important: false, separator: ':', + content: [], theme: { spacing: { 1: '1px', @@ -1010,6 +1029,7 @@ test('the original theme is not mutated', () => { prefix: '-', important: false, separator: ':', + content: [], theme: { colors: { cyan: 'cyan', @@ -1058,6 +1078,7 @@ test('custom properties are multiplied by -1 for negative values', () => { prefix: '-', important: false, separator: ':', + content: [], theme: {}, } @@ -1165,6 +1186,7 @@ test('more than two config objects can be resolved', () => { prefix: '-', important: false, separator: ':', + content: [], theme: { fontFamily: { body: ['Arial', 'sans-serif'], @@ -1233,6 +1255,7 @@ test('plugin config modifications are applied', () => { prefix: '', important: false, separator: ':', + content: [], theme: { screens: { mobile: '400px', @@ -1271,6 +1294,7 @@ test('user config takes precedence over plugin config modifications', () => { prefix: '', important: false, separator: ':', + content: [], theme: { screens: { mobile: '400px', @@ -1321,6 +1345,7 @@ test('plugin config can register plugins that also have config', () => { prefix: '', important: false, separator: ':', + content: [], theme: { screens: { mobile: '400px', @@ -1366,6 +1391,7 @@ test('plugin configs take precedence over plugin configs registered by that plug prefix: '', important: false, separator: ':', + content: [], theme: { screens: { mobile: '400px', @@ -1416,6 +1442,7 @@ test('plugin theme extensions are added even if user overrides top-level theme c prefix: '', important: false, separator: ':', + content: [], theme: { width: { sm: '1rem', @@ -1477,6 +1504,7 @@ test('user theme extensions take precedence over plugin theme extensions with th prefix: '', important: false, separator: ':', + content: [], theme: { width: { sm: '1rem', @@ -1549,6 +1577,7 @@ test('extensions are applied in the right order', () => { } const defaultConfig = { + content: [], theme: { colors: { grey: { @@ -1583,6 +1612,7 @@ test('core plugin configuration builds on the default list when starting with an prefix: '', important: false, separator: ':', + content: [], theme: {}, corePlugins: {}, } @@ -1608,6 +1638,7 @@ test('core plugins that are disabled by default can be enabled', () => { prefix: '', important: false, separator: ':', + content: [], theme: {}, corePlugins: { display: false }, } @@ -1631,6 +1662,7 @@ test('core plugin configurations stack', () => { prefix: '', important: false, separator: ':', + content: [], theme: {}, corePlugins: ['float', 'display', 'padding'], } @@ -1660,6 +1692,7 @@ test('plugins are merged', () => { prefix: '', important: false, separator: ':', + content: [], theme: {}, } @@ -1691,6 +1724,7 @@ test('all helpers can be destructured from the first function argument', () => { prefix: '-', important: false, separator: ':', + content: [], theme: { screens: { sm: '640px',