Skip to content

Commit f4799a3

Browse files
authored
add tests for the --postcss option in the new CLI (tailwindlabs#4607)
* add tests for the --postcss option in the new CLI * add `oneOf` ability to the `arg()` functionality By default, `arg()` doesn't have a way to define multiple types. We want the possibility of using `--postcss` (Boolean) or `--postcss ./custom-path.js`. But by default this is not possible. This commit will allow us to do a few things, mainly: - Keep the same API using the `{ type: oneOf(String, Boolean), description: '...' }` - Keep the `--help` output similar What we did behind the scenes is make sure to put the non recognized flags in the `_` arguments list. This is possible by doing `permissive: true`. We then manually parse those and resolve the correct value. * ensure that we can use a custom `--postcss ./with-custom-path.js`
1 parent b86aa5c commit f4799a3

File tree

2 files changed

+185
-8
lines changed

2 files changed

+185
-8
lines changed

integrations/tailwindcss-cli/tests/cli.test.js

Lines changed: 103 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
let path = require('path')
22
let $ = require('../../execute')
3-
let { css, html } = require('../../syntax')
3+
let { css, html, javascript } = require('../../syntax')
44
let resolveToolRoot = require('../../resolve-tool-root')
55

66
let { readOutputFile, writeInputFile, cleanupFile, fileExists, removeFile } = require('../../io')({
@@ -157,6 +157,108 @@ describe('Build command', () => {
157157
)
158158
})
159159

160+
test('--postcss (postcss.config.js)', async () => {
161+
await writeInputFile('index.html', html`<div class="font-bold"></div>`)
162+
163+
let customConfig = javascript`
164+
let path = require('path')
165+
let postcss = require('postcss')
166+
167+
module.exports = {
168+
plugins: [
169+
function before(root, result) {
170+
// Inject a custom component with @apply rules to prove that we run
171+
// this _before_ the actual tailwind plugin.
172+
let btn = postcss.parse('.btn { @apply bg-red-500 px-2 py-1 }')
173+
root.append(btn.nodes)
174+
},
175+
function tailwindcss() {
176+
return require(path.resolve('..', '..'))
177+
},
178+
function after(root, result) {
179+
// Add '-after' to all the selectors
180+
root.walkRules(rule => {
181+
if (!rule.selector.startsWith('.')) return
182+
rule.selector = rule.selector + '-after'
183+
})
184+
},
185+
],
186+
}
187+
`
188+
189+
await writeInputFile('../postcss.config.js', customConfig)
190+
191+
await $(`${EXECUTABLE} --output ./dist/main.css --postcss`)
192+
193+
expect(await readOutputFile('main.css')).toIncludeCss(
194+
css`
195+
.font-bold-after {
196+
font-weight: 700;
197+
}
198+
199+
.btn-after {
200+
--tw-bg-opacity: 1;
201+
background-color: rgba(239, 68, 68, var(--tw-bg-opacity));
202+
padding-left: 0.5rem;
203+
padding-right: 0.5rem;
204+
padding-top: 0.25rem;
205+
padding-bottom: 0.25rem;
206+
}
207+
`
208+
)
209+
})
210+
211+
test('--postcss (custom.postcss.config.js)', async () => {
212+
await writeInputFile('index.html', html`<div class="font-bold"></div>`)
213+
214+
let customConfig = javascript`
215+
let path = require('path')
216+
let postcss = require('postcss')
217+
218+
module.exports = {
219+
plugins: [
220+
function before(root, result) {
221+
// Inject a custom component with @apply rules to prove that we run
222+
// this _before_ the actual tailwind plugin.
223+
let btn = postcss.parse('.btn { @apply bg-red-500 px-2 py-1 }')
224+
root.append(btn.nodes)
225+
},
226+
function tailwindcss() {
227+
return require(path.resolve('..', '..'))
228+
},
229+
function after(root, result) {
230+
// Add '-after' to all the selectors
231+
root.walkRules(rule => {
232+
if (!rule.selector.startsWith('.')) return
233+
rule.selector = rule.selector + '-after'
234+
})
235+
},
236+
],
237+
}
238+
`
239+
240+
await writeInputFile('../custom.postcss.config.js', customConfig)
241+
242+
await $(`${EXECUTABLE} --output ./dist/main.css --postcss ./custom.postcss.config.js`)
243+
244+
expect(await readOutputFile('main.css')).toIncludeCss(
245+
css`
246+
.font-bold-after {
247+
font-weight: 700;
248+
}
249+
250+
.btn-after {
251+
--tw-bg-opacity: 1;
252+
background-color: rgba(239, 68, 68, var(--tw-bg-opacity));
253+
padding-left: 0.5rem;
254+
padding-right: 0.5rem;
255+
padding-top: 0.25rem;
256+
padding-bottom: 0.25rem;
257+
}
258+
`
259+
)
260+
})
261+
160262
test('--help', async () => {
161263
let { combined } = await $(`${EXECUTABLE} --help`)
162264

src/cli.js

Lines changed: 82 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import path from 'path'
77
import arg from 'arg'
88
import fs from 'fs'
99
import postcssrc from 'postcss-load-config'
10+
import { cosmiconfig } from 'cosmiconfig'
11+
import loadPlugins from 'postcss-load-config/src/plugins' // Little bit scary, looking at private/internal API
1012
import tailwindJit from './jit/processTailwindFeatures'
1113
import tailwindAot from './processTailwindFeatures'
1214
import resolveConfigInternal from '../resolveConfig'
@@ -104,6 +106,22 @@ function help({ message, usage, commands, options }) {
104106
console.log()
105107
}
106108

109+
function oneOf(...options) {
110+
return Object.assign(
111+
(value = true) => {
112+
for (let option of options) {
113+
let parsed = option(value)
114+
if (parsed === value) {
115+
return parsed
116+
}
117+
}
118+
119+
throw new Error('...')
120+
},
121+
{ manualParsing: true }
122+
)
123+
}
124+
107125
let commands = {
108126
init: {
109127
run: init,
@@ -123,7 +141,10 @@ let commands = {
123141
'--watch': { type: Boolean, description: 'Watch for changes and rebuild as needed' },
124142
'--jit': { type: Boolean, description: 'Build using JIT mode' },
125143
'--purge': { type: String, description: 'Content paths to use for removing unused classes' },
126-
'--postcss': { type: Boolean, description: 'Load custom PostCSS configuration' },
144+
'--postcss': {
145+
type: oneOf(String, Boolean),
146+
description: 'Load custom PostCSS configuration',
147+
},
127148
'--minify': { type: Boolean, description: 'Minify the output' },
128149
'--config': {
129150
type: String,
@@ -191,13 +212,47 @@ let args = (() => {
191212
try {
192213
let result = arg(
193214
Object.fromEntries(
194-
Object.entries({ ...flags, ...sharedFlags }).map(([key, value]) => [
195-
key,
196-
typeof value === 'object' ? value.type : value,
197-
])
198-
)
215+
Object.entries({ ...flags, ...sharedFlags })
216+
.filter(([_key, value]) => !value?.type?.manualParsing)
217+
.map(([key, value]) => [key, typeof value === 'object' ? value.type : value])
218+
),
219+
{ permissive: true }
199220
)
200221

222+
// Manual parsing of flags to allow for special flags like oneOf(Boolean, String)
223+
for (let i = result['_'].length - 1; i >= 0; --i) {
224+
let flag = result['_'][i]
225+
if (!flag.startsWith('-')) continue
226+
227+
let flagName = flag
228+
let handler = flags[flag]
229+
230+
// Resolve flagName & handler
231+
while (typeof handler === 'string') {
232+
flagName = handler
233+
handler = flags[handler]
234+
}
235+
236+
if (!handler) continue
237+
238+
let args = []
239+
let offset = i + 1
240+
241+
// Parse args for current flag
242+
while (result['_'][offset] && !result['_'][offset].startsWith('-')) {
243+
args.push(result['_'][offset++])
244+
}
245+
246+
// Cleanup manually parsed flags + args
247+
result['_'].splice(i, 1 + args.length)
248+
249+
// Set the resolved value in the `result` object
250+
result[flagName] = handler.type(
251+
args.length === 0 ? undefined : args.length === 1 ? args[0] : args,
252+
flagName
253+
)
254+
}
255+
201256
// Ensure that the `command` is always the first argument in the `args`.
202257
// This is important so that we don't have to check if a default command
203258
// (build) was used or not from within each plugin.
@@ -317,7 +372,27 @@ async function build() {
317372
)
318373

319374
async function loadPostCssPlugins() {
320-
let { plugins: configPlugins } = await postcssrc()
375+
let customPostCssPath = typeof args['--postcss'] === 'string' ? args['--postcss'] : undefined
376+
let { plugins: configPlugins } = customPostCssPath
377+
? await (async () => {
378+
let file = path.resolve(customPostCssPath)
379+
380+
// Implementation, see: https://unpkg.com/browse/postcss-load-config@3.0.1/src/index.js
381+
let { config = {} } = await cosmiconfig('postcss').load(file)
382+
if (typeof config === 'function') {
383+
config = config()
384+
} else {
385+
config = Object.assign({}, config)
386+
}
387+
388+
if (!config.plugins) {
389+
config.plugins = []
390+
}
391+
392+
return { plugins: loadPlugins(config, file) }
393+
})()
394+
: await postcssrc()
395+
321396
let configPluginTailwindIdx = configPlugins.findIndex((plugin) => {
322397
if (typeof plugin === 'function' && plugin.name === 'tailwindcss') {
323398
return true

0 commit comments

Comments
 (0)