Skip to content

Commit

Permalink
Add transform and extract APIs (#4469)
Browse files Browse the repository at this point in the history
* add `transform` and `extract` APIs

* make svelte transform part of the transformer stuff
  • Loading branch information
bradlc authored and adamwathan committed May 26, 2021
1 parent 26454f7 commit 232cd97
Show file tree
Hide file tree
Showing 5 changed files with 432 additions and 29 deletions.
64 changes: 44 additions & 20 deletions src/jit/lib/expandTailwindAtRules.js
@@ -1,5 +1,3 @@
import fs from 'fs'
import path from 'path'
import * as sharedState from './sharedState'
import { generateRules } from './generateRules'
import bigSign from '../../util/bigSign'
Expand All @@ -11,38 +9,63 @@ let contentMatchCache = sharedState.contentMatchCache
const BROAD_MATCH_GLOBAL_REGEXP = /[^<>"'`\s]*[^<>"'`\s:]/g
const INNER_MATCH_GLOBAL_REGEXP = /[^<>"'`\s.(){}[\]#=%]*[^<>"'`\s.(){}[\]#=%:]/g

function getDefaultExtractor(fileExtension) {
return function (content) {
if (fileExtension === 'svelte') {
content = content.replace(/(?:^|\s)class:/g, ' ')
}
const builtInExtractors = {
DEFAULT: (content) => {
let broadMatches = content.match(BROAD_MATCH_GLOBAL_REGEXP) || []
let innerMatches = content.match(INNER_MATCH_GLOBAL_REGEXP) || []

return [...broadMatches, ...innerMatches]
}
},
}

const builtInTransformers = {
DEFAULT: (content) => content,
svelte: (content) => content.replace(/(?:^|\s)class:/g, ' '),
}

function getExtractor(tailwindConfig, fileExtension) {
const purgeOptions = tailwindConfig && tailwindConfig.purge && tailwindConfig.purge.options
let extractors = (tailwindConfig && tailwindConfig.purge && tailwindConfig.purge.extract) || {}
const purgeOptions =
(tailwindConfig && tailwindConfig.purge && tailwindConfig.purge.options) || {}

if (!fileExtension) {
return (purgeOptions && purgeOptions.defaultExtractor) || getDefaultExtractor()
if (typeof extractors === 'function') {
extractors = {
DEFAULT: extractors,
}
}

if (!purgeOptions) {
return getDefaultExtractor(fileExtension)
if (purgeOptions.defaultExtractor) {
extractors.DEFAULT = purgeOptions.defaultExtractor
}
for (let { extensions, extractor } of purgeOptions.extractors || []) {
for (let extension of extensions) {
extractors[extension] = extractor
}
}

const fileSpecificExtractor = (purgeOptions.extractors || []).find((extractor) =>
extractor.extensions.includes(fileExtension)
return (
extractors[fileExtension] ||
extractors.DEFAULT ||
builtInExtractors[fileExtension] ||
builtInExtractors.DEFAULT
)
}

if (fileSpecificExtractor) {
return fileSpecificExtractor.extractor
function getTransformer(tailwindConfig, fileExtension) {
let transformers =
(tailwindConfig && tailwindConfig.purge && tailwindConfig.purge.transform) || {}

if (typeof transformers === 'function') {
transformers = {
DEFAULT: transformers,
}
}

return purgeOptions.defaultExtractor || getDefaultExtractor(fileExtension)
return (
transformers[fileExtension] ||
transformers.DEFAULT ||
builtInTransformers[fileExtension] ||
builtInTransformers.DEFAULT
)
}

// Scans template contents for possible classes. This is a hot path on initial build but
Expand Down Expand Up @@ -141,8 +164,9 @@ export default function expandTailwindAtRules(context) {
env.DEBUG && console.time('Reading changed files')

for (let { content, extension } of context.changedContent) {
let transformer = getTransformer(context.tailwindConfig, extension)
let extractor = getExtractor(context.tailwindConfig, extension)
getClassCandidates(content, extractor, contentMatchCache, candidates, seen)
getClassCandidates(transformer(content), extractor, contentMatchCache, candidates, seen)
}

// ---
Expand Down
68 changes: 59 additions & 9 deletions src/lib/purgeUnusedStyles.js
Expand Up @@ -34,6 +34,18 @@ export function tailwindExtractor(content) {
return broadMatches.concat(broadMatchesWithoutTrailingSlash).concat(innerMatches)
}

function getTransformer(config, fileExtension) {
let transformers = (config.purge && config.purge.transform) || {}

if (typeof transformers === 'function') {
transformers = {
DEFAULT: transformers,
}
}

return transformers[fileExtension] || transformers.DEFAULT || ((content) => content)
}

export default function purgeUnusedUtilities(config, configChanged, resolvedConfigPath) {
const purgeEnabled = _.get(
config,
Expand All @@ -58,7 +70,50 @@ export default function purgeUnusedUtilities(config, configChanged, resolvedConf
return removeTailwindMarkers
}

const { defaultExtractor, ...purgeOptions } = config.purge.options || {}
const extractors = config.purge.extract || {}
const transformers = config.purge.transform || {}
let { defaultExtractor: originalDefaultExtractor, ...purgeOptions } = config.purge.options || {}

if (!originalDefaultExtractor) {
originalDefaultExtractor =
typeof extractors === 'function' ? extractors : extractors.DEFAULT || tailwindExtractor
}

const defaultExtractor = (content) => {
const preserved = originalDefaultExtractor(content)

if (_.get(config, 'purge.preserveHtmlElements', true)) {
preserved.push(...htmlTags)
}

return preserved
}

// If `extractors` is a function then we don't have any file-specific extractors,
// only a default one.
let fileSpecificExtractors = typeof extractors === 'function' ? {} : extractors

// PurgeCSS doesn't support "transformers," so we implement those using extractors.
// If we have a custom transformer for an extension, but not a matching extractor,
// then we need to create an extractor that we can augment later.
if (typeof transformers !== 'function') {
for (let [extension] of Object.entries(transformers)) {
if (!fileSpecificExtractors[extension]) {
fileSpecificExtractors[extension] = defaultExtractor
}
}
}

// Augment file-specific extractors by running the transformer before we extract classes.
fileSpecificExtractors = Object.entries(fileSpecificExtractors).map(([extension, extractor]) => {
return {
extensions: [extension],
extractor: (content) => {
const transformer = getTransformer(config, extension)
return extractor(transformer(content))
},
}
})

return postcss([
function (css) {
Expand Down Expand Up @@ -106,15 +161,10 @@ export default function purgeUnusedUtilities(config, configChanged, resolvedConf
removeTailwindMarkers,
purgecss({
defaultExtractor: (content) => {
const extractor = defaultExtractor || tailwindExtractor
const preserved = [...extractor(content)]

if (_.get(config, 'purge.preserveHtmlElements', true)) {
preserved.push(...htmlTags)
}

return preserved
const transformer = getTransformer(config)
return defaultExtractor(transformer(content))
},
extractors: fileSpecificExtractors,
...purgeOptions,
content: (Array.isArray(config.purge)
? config.purge
Expand Down
55 changes: 55 additions & 0 deletions tests/jit/custom-extractors.test.js
Expand Up @@ -65,3 +65,58 @@ test('extractors array', () => {
expect(result.css).toMatchFormattedCss(expected)
})
})

test('extract function', () => {
let config = {
mode: 'jit',
purge: {
content: [path.resolve(__dirname, './custom-extractors.test.html')],
extract: customExtractor,
},
corePlugins: { preflight: false },
theme: {},
plugins: [],
}

return run(css, config).then((result) => {
expect(result.css).toMatchFormattedCss(expected)
})
})

test('extract.DEFAULT', () => {
let config = {
mode: 'jit',
purge: {
content: [path.resolve(__dirname, './custom-extractors.test.html')],
extract: {
DEFAULT: customExtractor,
},
},
corePlugins: { preflight: false },
theme: {},
plugins: [],
}

return run(css, config).then((result) => {
expect(result.css).toMatchFormattedCss(expected)
})
})

test('extract.{extension}', () => {
let config = {
purge: {
content: [path.resolve(__dirname, './custom-extractors.test.html')],
extract: {
html: customExtractor,
},
},
mode: 'jit',
corePlugins: { preflight: false },
theme: {},
plugins: [],
}

return run(css, config).then((result) => {
expect(result.css).toMatchFormattedCss(expected)
})
})
93 changes: 93 additions & 0 deletions tests/jit/custom-transformers.test.js
@@ -0,0 +1,93 @@
import postcss from 'postcss'
import path from 'path'

function run(input, config = {}) {
jest.resetModules()
const tailwind = require('../../src/jit/index.js').default
return postcss(tailwind(config)).process(input, {
from: path.resolve(__filename),
})
}

function customTransformer(content) {
return content.replace(/uppercase/g, 'lowercase')
}

const css = `
@tailwind base;
@tailwind components;
@tailwind utilities;
`

test('transform function', () => {
let config = {
mode: 'jit',
purge: {
content: [{ raw: '<div class="uppercase"></div>' }],
transform: customTransformer,
},
corePlugins: { preflight: false, borderColor: false, ringWidth: false, boxShadow: false },
theme: {},
plugins: [],
}

return run(css, config).then((result) => {
expect(result.css).toMatchFormattedCss(`
.lowercase {
text-transform: lowercase;
}
`)
})
})

test('transform.DEFAULT', () => {
let config = {
mode: 'jit',
purge: {
content: [{ raw: '<div class="uppercase"></div>' }],
transform: {
DEFAULT: customTransformer,
},
},
corePlugins: { preflight: false, borderColor: false, ringWidth: false, boxShadow: false },
theme: {},
plugins: [],
}

return run(css, config).then((result) => {
expect(result.css).toMatchFormattedCss(`
.lowercase {
text-transform: lowercase;
}
`)
})
})

test('transform.{extension}', () => {
let config = {
mode: 'jit',
purge: {
content: [
{ raw: '<div class="uppercase"></div>', extension: 'html' },
{ raw: '<div class="uppercase"></div>', extension: 'php' },
],
transform: {
html: customTransformer,
},
},
corePlugins: { preflight: false, borderColor: false, ringWidth: false, boxShadow: false },
theme: {},
plugins: [],
}

return run(css, config).then((result) => {
expect(result.css).toMatchFormattedCss(`
.uppercase {
text-transform: uppercase;
}
.lowercase {
text-transform: lowercase;
}
`)
})
})

0 comments on commit 232cd97

Please sign in to comment.