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 2829fd44bd1c..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.files - } - 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 }) } diff --git a/src/util/normalizeConfig.js b/src/util/normalizeConfig.js index 11e4d4571483..3a9acb18cb56 100644 --- a/src/util/normalizeConfig.js +++ b/src/util/normalizeConfig.js @@ -1,49 +1,126 @@ import log from './log' -function validate(config, options) { - if (!config) return false - - for (let [k, v] of Object.entries(config)) { - let types = options[k] - - if (!types) return false - - // Property SHOULD exist, this catches unused keys like `options` - if (!types.includes(undefined) && !options.hasOwnProperty(k)) { - return false - } - +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 ( - !types.some((type) => { - if (type === undefined) return true - return v instanceof type - }) + !Array.isArray(config.content) || + !(typeof config.content === 'object' && config.content !== null) ) { return false } - } - for (let [k, types] of Object.entries(options)) { - let value = config[k] - if ( - !types.some((type) => { - if (type === undefined) return true - return value instanceof type + // 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 }) - ) { - 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 + } -export function normalizeConfig(config) { - // Quick structure validation - let valid = validate(config.content, { - files: [Array], - extract: [undefined, Function, Object], - }) + // `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', [ @@ -86,10 +163,6 @@ export function normalizeConfig(config) { if (config.purge?.extract?.DEFAULT) return config.purge.extract.DEFAULT if (config.content?.extract?.DEFAULT) return config.content.extract.DEFAULT - if (config.purge?.options?.defaultExtractor) return config.purge.options.defaultExtractor - if (config.content?.options?.defaultExtractor) - return config.content.options.defaultExtractor - if (config.purge?.options?.extractors) return config.purge.options.extractors if (config.content?.options?.extractors) return config.content.options.extractors @@ -98,6 +171,18 @@ export function normalizeConfig(config) { 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 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 9ce08dcb254f..9a8301230a0b 100644 --- a/tests/custom-extractors.test.js +++ b/tests/custom-extractors.test.js @@ -41,13 +41,26 @@ describe('modern', () => { expect(result.css).toMatchFormattedCss(expected) }) }) + + 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) + }) + }) }) describe('legacy', () => { test('defaultExtractor', () => { let config = { content: { - content: [path.resolve(__dirname, './custom-extractors.test.html')], + files: [path.resolve(__dirname, './custom-extractors.test.html')], options: { defaultExtractor: customExtractor, }, @@ -62,7 +75,7 @@ describe('legacy', () => { test('extractors array', () => { let config = { content: { - content: [path.resolve(__dirname, './custom-extractors.test.html')], + files: [path.resolve(__dirname, './custom-extractors.test.html')], options: { extractors: [ { @@ -78,17 +91,4 @@ describe('legacy', () => { expect(result.css).toMatchFormattedCss(expected) }) }) - - test('extract function', () => { - let config = { - content: { - content: [path.resolve(__dirname, './custom-extractors.test.html')], - extract: customExtractor, - }, - } - - 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 index 4ead50794476..602e10963a6c 100644 --- a/tests/normalize-config.test.js +++ b/tests/normalize-config.test.js @@ -46,3 +46,52 @@ it.each` `) }) }) + +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',