Compare commits
13 commits
main
...
feature/re
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b2b4358dfe | ||
|
|
d76e5db551 | ||
|
|
377f372536 | ||
|
|
df35325e27 | ||
|
|
6bd3327d8e | ||
|
|
788e283379 | ||
|
|
3c07c8b92d | ||
|
|
2102566df8 | ||
|
|
14c39f4d44 | ||
|
|
137a85c0ff | ||
|
|
d18fd513a4 | ||
|
|
a3b42e4fec | ||
|
|
182e3dc480 |
17 changed files with 1175 additions and 919 deletions
46
integrations/tailwindcss-cli/package-lock.json
generated
46
integrations/tailwindcss-cli/package-lock.json
generated
|
|
@ -12,13 +12,13 @@
|
|||
}
|
||||
},
|
||||
"../..": {
|
||||
"version": "3.0.24",
|
||||
"version": "3.1.5",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"arg": "^5.0.1",
|
||||
"arg": "^5.0.2",
|
||||
"chokidar": "^3.5.3",
|
||||
"color-name": "^1.1.4",
|
||||
"detective": "^5.2.0",
|
||||
"detective": "^5.2.1",
|
||||
"didyoumean": "^1.2.2",
|
||||
"dlv": "^1.1.3",
|
||||
"fast-glob": "^3.2.11",
|
||||
|
|
@ -36,26 +36,26 @@
|
|||
"postcss-selector-parser": "^6.0.10",
|
||||
"postcss-value-parser": "^4.2.0",
|
||||
"quick-lru": "^5.1.1",
|
||||
"resolve": "^1.22.0"
|
||||
"resolve": "^1.22.1"
|
||||
},
|
||||
"bin": {
|
||||
"tailwind": "lib/cli.js",
|
||||
"tailwindcss": "lib/cli.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@parcel/css": "^1.11.2",
|
||||
"@swc/cli": "^0.1.57",
|
||||
"@swc/core": "^1.2.160",
|
||||
"@swc/core": "^1.2.196",
|
||||
"@swc/jest": "^0.2.21",
|
||||
"@swc/register": "^0.1.10",
|
||||
"autoprefixer": "^10.4.7",
|
||||
"cssnano": "^5.1.9",
|
||||
"esbuild": "^0.14.39",
|
||||
"eslint": "^8.15.0",
|
||||
"esbuild": "^0.14.48",
|
||||
"eslint": "^8.18.0",
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
"eslint-plugin-prettier": "^4.0.0",
|
||||
"jest": "^28.0.3",
|
||||
"jest-diff": "^28.1.0",
|
||||
"prettier": "^2.6.2",
|
||||
"eslint-plugin-prettier": "^4.2.1",
|
||||
"jest": "^28.1.2",
|
||||
"jest-diff": "^28.1.1",
|
||||
"prettier": "^2.7.1",
|
||||
"prettier-plugin-tailwindcss": "^0.1.11",
|
||||
"rimraf": "^3.0.0",
|
||||
"source-map-js": "^1.0.2"
|
||||
|
|
@ -76,27 +76,27 @@
|
|||
"tailwindcss": {
|
||||
"version": "file:../..",
|
||||
"requires": {
|
||||
"@parcel/css": "^1.11.2",
|
||||
"@swc/cli": "^0.1.57",
|
||||
"@swc/core": "^1.2.160",
|
||||
"@swc/core": "^1.2.196",
|
||||
"@swc/jest": "^0.2.21",
|
||||
"@swc/register": "^0.1.10",
|
||||
"arg": "^5.0.1",
|
||||
"arg": "^5.0.2",
|
||||
"autoprefixer": "^10.4.7",
|
||||
"chokidar": "^3.5.3",
|
||||
"color-name": "^1.1.4",
|
||||
"cssnano": "^5.1.9",
|
||||
"detective": "^5.2.0",
|
||||
"detective": "^5.2.1",
|
||||
"didyoumean": "^1.2.2",
|
||||
"dlv": "^1.1.3",
|
||||
"esbuild": "^0.14.39",
|
||||
"eslint": "^8.15.0",
|
||||
"esbuild": "^0.14.48",
|
||||
"eslint": "^8.18.0",
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
"eslint-plugin-prettier": "^4.0.0",
|
||||
"eslint-plugin-prettier": "^4.2.1",
|
||||
"fast-glob": "^3.2.11",
|
||||
"glob-parent": "^6.0.2",
|
||||
"is-glob": "^4.0.3",
|
||||
"jest": "^28.0.3",
|
||||
"jest-diff": "^28.1.0",
|
||||
"jest": "^28.1.2",
|
||||
"jest-diff": "^28.1.1",
|
||||
"lilconfig": "^2.0.5",
|
||||
"normalize-path": "^3.0.0",
|
||||
"object-hash": "^3.0.0",
|
||||
|
|
@ -108,10 +108,10 @@
|
|||
"postcss-nested": "5.0.6",
|
||||
"postcss-selector-parser": "^6.0.10",
|
||||
"postcss-value-parser": "^4.2.0",
|
||||
"prettier": "^2.6.2",
|
||||
"prettier": "^2.7.1",
|
||||
"prettier-plugin-tailwindcss": "^0.1.11",
|
||||
"quick-lru": "^5.1.1",
|
||||
"resolve": "^1.22.0",
|
||||
"resolve": "^1.22.1",
|
||||
"rimraf": "^3.0.0",
|
||||
"source-map-js": "^1.0.2"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -459,6 +459,7 @@ describe('Build command', () => {
|
|||
-m, --minify Minify the output
|
||||
-c, --config Path to a custom config file
|
||||
--no-autoprefixer Disable autoprefixer
|
||||
--experimental-contextual-paths Enable experimental contextual path resolution
|
||||
-h, --help Display usage information
|
||||
`)
|
||||
)
|
||||
|
|
|
|||
810
src/cli.js
810
src/cli.js
|
|
@ -1,28 +1,12 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
import { lazyPostcss, lazyPostcssImport, lazyCssnano, lazyAutoprefixer } from '../peers/index.js'
|
||||
|
||||
import chokidar from 'chokidar'
|
||||
import path from 'path'
|
||||
import arg from 'arg'
|
||||
import fs from 'fs'
|
||||
import postcssrc from 'postcss-load-config'
|
||||
import { lilconfig } from 'lilconfig'
|
||||
import loadPlugins from 'postcss-load-config/src/plugins' // Little bit scary, looking at private/internal API
|
||||
import loadOptions from 'postcss-load-config/src/options' // Little bit scary, looking at private/internal API
|
||||
import tailwind from './processTailwindFeatures'
|
||||
import resolveConfigInternal from '../resolveConfig'
|
||||
import fastGlob from 'fast-glob'
|
||||
import getModuleDependencies from './lib/getModuleDependencies'
|
||||
import log from './util/log'
|
||||
import packageJson from '../package.json'
|
||||
import normalizePath from 'normalize-path'
|
||||
import micromatch from 'micromatch'
|
||||
import { validateConfig } from './util/validateConfig.js'
|
||||
|
||||
let env = {
|
||||
DEBUG: process.env.DEBUG !== undefined && process.env.DEBUG !== '0',
|
||||
}
|
||||
import { help } from './cli/help.js'
|
||||
import { init } from './cli/init.js'
|
||||
import { build } from './cli/build.js'
|
||||
|
||||
function isESM() {
|
||||
const pkgPath = path.resolve('./package.json')
|
||||
|
|
@ -47,112 +31,6 @@ let configs = isESM()
|
|||
|
||||
// ---
|
||||
|
||||
function indentRecursive(node, indent = 0) {
|
||||
node.each &&
|
||||
node.each((child, i) => {
|
||||
if (!child.raws.before || !child.raws.before.trim() || child.raws.before.includes('\n')) {
|
||||
child.raws.before = `\n${node.type !== 'rule' && i > 0 ? '\n' : ''}${' '.repeat(indent)}`
|
||||
}
|
||||
child.raws.after = `\n${' '.repeat(indent)}`
|
||||
indentRecursive(child, indent + 1)
|
||||
})
|
||||
}
|
||||
|
||||
function formatNodes(root) {
|
||||
indentRecursive(root)
|
||||
if (root.first) {
|
||||
root.first.raws.before = ''
|
||||
}
|
||||
}
|
||||
|
||||
async function outputFile(file, contents) {
|
||||
if (fs.existsSync(file) && (await fs.promises.readFile(file, 'utf8')) === contents) {
|
||||
return // Skip writing the file
|
||||
}
|
||||
|
||||
// Write the file
|
||||
await fs.promises.writeFile(file, contents, 'utf8')
|
||||
}
|
||||
|
||||
function drainStdin() {
|
||||
return new Promise((resolve, reject) => {
|
||||
let result = ''
|
||||
process.stdin.on('data', (chunk) => {
|
||||
result += chunk
|
||||
})
|
||||
process.stdin.on('end', () => resolve(result))
|
||||
process.stdin.on('error', (err) => reject(err))
|
||||
})
|
||||
}
|
||||
|
||||
function help({ message, usage, commands, options }) {
|
||||
let indent = 2
|
||||
|
||||
// Render header
|
||||
console.log()
|
||||
console.log(`${packageJson.name} v${packageJson.version}`)
|
||||
|
||||
// Render message
|
||||
if (message) {
|
||||
console.log()
|
||||
for (let msg of message.split('\n')) {
|
||||
console.log(msg)
|
||||
}
|
||||
}
|
||||
|
||||
// Render usage
|
||||
if (usage && usage.length > 0) {
|
||||
console.log()
|
||||
console.log('Usage:')
|
||||
for (let example of usage) {
|
||||
console.log(' '.repeat(indent), example)
|
||||
}
|
||||
}
|
||||
|
||||
// Render commands
|
||||
if (commands && commands.length > 0) {
|
||||
console.log()
|
||||
console.log('Commands:')
|
||||
for (let command of commands) {
|
||||
console.log(' '.repeat(indent), command)
|
||||
}
|
||||
}
|
||||
|
||||
// Render options
|
||||
if (options) {
|
||||
let groupedOptions = {}
|
||||
for (let [key, value] of Object.entries(options)) {
|
||||
if (typeof value === 'object') {
|
||||
groupedOptions[key] = { ...value, flags: [key] }
|
||||
} else {
|
||||
groupedOptions[value].flags.push(key)
|
||||
}
|
||||
}
|
||||
|
||||
console.log()
|
||||
console.log('Options:')
|
||||
for (let { flags, description, deprecated } of Object.values(groupedOptions)) {
|
||||
if (deprecated) continue
|
||||
|
||||
if (flags.length === 1) {
|
||||
console.log(
|
||||
' '.repeat(indent + 4 /* 4 = "-i, ".length */),
|
||||
flags.slice().reverse().join(', ').padEnd(20, ' '),
|
||||
description
|
||||
)
|
||||
} else {
|
||||
console.log(
|
||||
' '.repeat(indent),
|
||||
flags.slice().reverse().join(', ').padEnd(24, ' '),
|
||||
description
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log()
|
||||
}
|
||||
|
||||
function oneOf(...options) {
|
||||
return Object.assign(
|
||||
(value = true) => {
|
||||
|
|
@ -169,15 +47,6 @@ function oneOf(...options) {
|
|||
)
|
||||
}
|
||||
|
||||
function loadPostcss() {
|
||||
// Try to load a local `postcss` version first
|
||||
try {
|
||||
return require('postcss')
|
||||
} catch {}
|
||||
|
||||
return lazyPostcss()
|
||||
}
|
||||
|
||||
let commands = {
|
||||
init: {
|
||||
run: init,
|
||||
|
|
@ -351,675 +220,4 @@ if (args['--help']) {
|
|||
process.exit(0)
|
||||
}
|
||||
|
||||
run()
|
||||
|
||||
// ---
|
||||
|
||||
function init() {
|
||||
let messages = []
|
||||
|
||||
let tailwindConfigLocation = path.resolve(args['_'][1] ?? `./${configs.tailwind}`)
|
||||
if (fs.existsSync(tailwindConfigLocation)) {
|
||||
messages.push(`${path.basename(tailwindConfigLocation)} already exists.`)
|
||||
} else {
|
||||
let stubFile = fs.readFileSync(
|
||||
args['--full']
|
||||
? path.resolve(__dirname, '../stubs/defaultConfig.stub.js')
|
||||
: path.resolve(__dirname, '../stubs/simpleConfig.stub.js'),
|
||||
'utf8'
|
||||
)
|
||||
|
||||
// Change colors import
|
||||
stubFile = stubFile.replace('../colors', 'tailwindcss/colors')
|
||||
|
||||
fs.writeFileSync(tailwindConfigLocation, stubFile, 'utf8')
|
||||
|
||||
messages.push(`Created Tailwind CSS config file: ${path.basename(tailwindConfigLocation)}`)
|
||||
}
|
||||
|
||||
if (args['--postcss']) {
|
||||
let postcssConfigLocation = path.resolve(`./${configs.postcss}`)
|
||||
if (fs.existsSync(postcssConfigLocation)) {
|
||||
messages.push(`${path.basename(postcssConfigLocation)} already exists.`)
|
||||
} else {
|
||||
let stubFile = fs.readFileSync(
|
||||
path.resolve(__dirname, '../stubs/defaultPostCssConfig.stub.js'),
|
||||
'utf8'
|
||||
)
|
||||
|
||||
fs.writeFileSync(postcssConfigLocation, stubFile, 'utf8')
|
||||
|
||||
messages.push(`Created PostCSS config file: ${path.basename(postcssConfigLocation)}`)
|
||||
}
|
||||
}
|
||||
|
||||
if (messages.length > 0) {
|
||||
console.log()
|
||||
for (let message of messages) {
|
||||
console.log(message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function build() {
|
||||
let input = args['--input']
|
||||
let output = args['--output']
|
||||
let shouldWatch = args['--watch']
|
||||
let shouldPoll = args['--poll']
|
||||
let shouldCoalesceWriteEvents = shouldPoll || process.platform === 'win32'
|
||||
let includePostCss = args['--postcss']
|
||||
|
||||
// Polling interval in milliseconds
|
||||
// Used only when polling or coalescing add/change events on Windows
|
||||
let pollInterval = 10
|
||||
|
||||
// TODO: Deprecate this in future versions
|
||||
if (!input && args['_'][1]) {
|
||||
console.error('[deprecation] Running tailwindcss without -i, please provide an input file.')
|
||||
input = args['--input'] = args['_'][1]
|
||||
}
|
||||
|
||||
if (input && input !== '-' && !fs.existsSync((input = path.resolve(input)))) {
|
||||
console.error(`Specified input file ${args['--input']} does not exist.`)
|
||||
process.exit(9)
|
||||
}
|
||||
|
||||
if (args['--config'] && !fs.existsSync((args['--config'] = path.resolve(args['--config'])))) {
|
||||
console.error(`Specified config file ${args['--config']} does not exist.`)
|
||||
process.exit(9)
|
||||
}
|
||||
|
||||
let configPath = args['--config']
|
||||
? args['--config']
|
||||
: ((defaultPath) => (fs.existsSync(defaultPath) ? defaultPath : null))(
|
||||
path.resolve(`./${configs.tailwind}`)
|
||||
)
|
||||
|
||||
async function loadPostCssPlugins() {
|
||||
let customPostCssPath = typeof args['--postcss'] === 'string' ? args['--postcss'] : undefined
|
||||
let config = customPostCssPath
|
||||
? await (async () => {
|
||||
let file = path.resolve(customPostCssPath)
|
||||
|
||||
// Implementation, see: https://unpkg.com/browse/postcss-load-config@3.1.0/src/index.js
|
||||
let { config = {} } = await lilconfig('postcss').load(file)
|
||||
if (typeof config === 'function') {
|
||||
config = config()
|
||||
} else {
|
||||
config = Object.assign({}, config)
|
||||
}
|
||||
|
||||
if (!config.plugins) {
|
||||
config.plugins = []
|
||||
}
|
||||
|
||||
return {
|
||||
file,
|
||||
plugins: loadPlugins(config, file),
|
||||
options: loadOptions(config, file),
|
||||
}
|
||||
})()
|
||||
: await postcssrc()
|
||||
|
||||
let configPlugins = config.plugins
|
||||
|
||||
let configPluginTailwindIdx = configPlugins.findIndex((plugin) => {
|
||||
if (typeof plugin === 'function' && plugin.name === 'tailwindcss') {
|
||||
return true
|
||||
}
|
||||
|
||||
if (typeof plugin === 'object' && plugin !== null && plugin.postcssPlugin === 'tailwindcss') {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
})
|
||||
|
||||
let beforePlugins =
|
||||
configPluginTailwindIdx === -1 ? [] : configPlugins.slice(0, configPluginTailwindIdx)
|
||||
let afterPlugins =
|
||||
configPluginTailwindIdx === -1
|
||||
? configPlugins
|
||||
: configPlugins.slice(configPluginTailwindIdx + 1)
|
||||
|
||||
return [beforePlugins, afterPlugins, config.options]
|
||||
}
|
||||
|
||||
function loadBuiltinPostcssPlugins() {
|
||||
let postcss = loadPostcss()
|
||||
let IMPORT_COMMENT = '__TAILWIND_RESTORE_IMPORT__: '
|
||||
return [
|
||||
[
|
||||
(root) => {
|
||||
root.walkAtRules('import', (rule) => {
|
||||
if (rule.params.slice(1).startsWith('tailwindcss/')) {
|
||||
rule.after(postcss.comment({ text: IMPORT_COMMENT + rule.params }))
|
||||
rule.remove()
|
||||
}
|
||||
})
|
||||
},
|
||||
(() => {
|
||||
try {
|
||||
return require('postcss-import')
|
||||
} catch {}
|
||||
|
||||
return lazyPostcssImport()
|
||||
})(),
|
||||
(root) => {
|
||||
root.walkComments((rule) => {
|
||||
if (rule.text.startsWith(IMPORT_COMMENT)) {
|
||||
rule.after(
|
||||
postcss.atRule({
|
||||
name: 'import',
|
||||
params: rule.text.replace(IMPORT_COMMENT, ''),
|
||||
})
|
||||
)
|
||||
rule.remove()
|
||||
}
|
||||
})
|
||||
},
|
||||
],
|
||||
[],
|
||||
{},
|
||||
]
|
||||
}
|
||||
|
||||
function resolveConfig() {
|
||||
let config = configPath ? require(configPath) : {}
|
||||
|
||||
if (args['--purge']) {
|
||||
log.warn('purge-flag-deprecated', [
|
||||
'The `--purge` flag has been deprecated.',
|
||||
'Please use `--content` instead.',
|
||||
])
|
||||
if (!args['--content']) {
|
||||
args['--content'] = args['--purge']
|
||||
}
|
||||
}
|
||||
|
||||
if (args['--content']) {
|
||||
let files = args['--content'].split(/(?<!{[^}]+),/)
|
||||
let resolvedConfig = resolveConfigInternal(config, { content: { files } })
|
||||
resolvedConfig.content.files = files
|
||||
resolvedConfig = validateConfig(resolvedConfig)
|
||||
return resolvedConfig
|
||||
}
|
||||
|
||||
let resolvedConfig = resolveConfigInternal(config)
|
||||
resolvedConfig = validateConfig(resolvedConfig)
|
||||
return resolvedConfig
|
||||
}
|
||||
|
||||
function extractFileGlobs(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
|
||||
// is not watchable, so let's remove it.
|
||||
return typeof file === 'string'
|
||||
})
|
||||
.map((glob) => normalizePath(glob))
|
||||
}
|
||||
|
||||
function extractRawContent(config) {
|
||||
return config.content.files.filter((file) => {
|
||||
return typeof file === 'object' && file !== null
|
||||
})
|
||||
}
|
||||
|
||||
function getChangedContent(config) {
|
||||
let changedContent = []
|
||||
|
||||
// Resolve globs from the content config
|
||||
let globs = extractFileGlobs(config)
|
||||
let files = fastGlob.sync(globs)
|
||||
|
||||
for (let file of files) {
|
||||
changedContent.push({
|
||||
content: fs.readFileSync(path.resolve(file), 'utf8'),
|
||||
extension: path.extname(file).slice(1),
|
||||
})
|
||||
}
|
||||
|
||||
// Resolve raw content in the tailwind config
|
||||
for (let { raw: content, extension = 'html' } of extractRawContent(config)) {
|
||||
changedContent.push({ content, extension })
|
||||
}
|
||||
|
||||
return changedContent
|
||||
}
|
||||
|
||||
async function buildOnce() {
|
||||
let config = resolveConfig()
|
||||
let changedContent = getChangedContent(config)
|
||||
|
||||
let tailwindPlugin = () => {
|
||||
return {
|
||||
postcssPlugin: 'tailwindcss',
|
||||
Once(root, { result }) {
|
||||
tailwind(({ createContext }) => {
|
||||
return () => {
|
||||
return createContext(config, changedContent)
|
||||
}
|
||||
})(root, result)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
tailwindPlugin.postcss = true
|
||||
|
||||
let [beforePlugins, afterPlugins, postcssOptions] = includePostCss
|
||||
? await loadPostCssPlugins()
|
||||
: loadBuiltinPostcssPlugins()
|
||||
|
||||
let plugins = [
|
||||
...beforePlugins,
|
||||
tailwindPlugin,
|
||||
!args['--minify'] && formatNodes,
|
||||
...afterPlugins,
|
||||
!args['--no-autoprefixer'] &&
|
||||
(() => {
|
||||
// Try to load a local `autoprefixer` version first
|
||||
try {
|
||||
return require('autoprefixer')
|
||||
} catch {}
|
||||
|
||||
return lazyAutoprefixer()
|
||||
})(),
|
||||
args['--minify'] &&
|
||||
(() => {
|
||||
let options = { preset: ['default', { cssDeclarationSorter: false }] }
|
||||
|
||||
// Try to load a local `cssnano` version first
|
||||
try {
|
||||
return require('cssnano')
|
||||
} catch {}
|
||||
|
||||
return lazyCssnano()(options)
|
||||
})(),
|
||||
].filter(Boolean)
|
||||
|
||||
let postcss = loadPostcss()
|
||||
let processor = postcss(plugins)
|
||||
|
||||
function processCSS(css) {
|
||||
let start = process.hrtime.bigint()
|
||||
return Promise.resolve()
|
||||
.then(() => (output ? fs.promises.mkdir(path.dirname(output), { recursive: true }) : null))
|
||||
.then(() => processor.process(css, { ...postcssOptions, from: input, to: output }))
|
||||
.then((result) => {
|
||||
if (!output) {
|
||||
return process.stdout.write(result.css)
|
||||
}
|
||||
|
||||
return Promise.all(
|
||||
[
|
||||
outputFile(output, result.css),
|
||||
result.map && outputFile(output + '.map', result.map.toString()),
|
||||
].filter(Boolean)
|
||||
)
|
||||
})
|
||||
.then(() => {
|
||||
let end = process.hrtime.bigint()
|
||||
console.error()
|
||||
console.error('Done in', (end - start) / BigInt(1e6) + 'ms.')
|
||||
})
|
||||
}
|
||||
|
||||
let css = await (() => {
|
||||
// Piping in data, let's drain the stdin
|
||||
if (input === '-') {
|
||||
return drainStdin()
|
||||
}
|
||||
|
||||
// Input file has been provided
|
||||
if (input) {
|
||||
return fs.readFileSync(path.resolve(input), 'utf8')
|
||||
}
|
||||
|
||||
// No input file provided, fallback to default atrules
|
||||
return '@tailwind base; @tailwind components; @tailwind utilities'
|
||||
})()
|
||||
|
||||
return processCSS(css)
|
||||
}
|
||||
|
||||
let context = null
|
||||
|
||||
async function startWatcher() {
|
||||
let changedContent = []
|
||||
let configDependencies = []
|
||||
let contextDependencies = new Set()
|
||||
let watcher = null
|
||||
|
||||
function refreshConfig() {
|
||||
env.DEBUG && console.time('Module dependencies')
|
||||
for (let file of configDependencies) {
|
||||
delete require.cache[require.resolve(file)]
|
||||
}
|
||||
|
||||
if (configPath) {
|
||||
configDependencies = getModuleDependencies(configPath).map(({ file }) => file)
|
||||
|
||||
for (let dependency of configDependencies) {
|
||||
contextDependencies.add(dependency)
|
||||
}
|
||||
}
|
||||
env.DEBUG && console.timeEnd('Module dependencies')
|
||||
|
||||
return resolveConfig()
|
||||
}
|
||||
|
||||
let [beforePlugins, afterPlugins] = includePostCss
|
||||
? await loadPostCssPlugins()
|
||||
: loadBuiltinPostcssPlugins()
|
||||
|
||||
let plugins = [
|
||||
...beforePlugins,
|
||||
'__TAILWIND_PLUGIN_POSITION__',
|
||||
!args['--minify'] && formatNodes,
|
||||
...afterPlugins,
|
||||
!args['--no-autoprefixer'] &&
|
||||
(() => {
|
||||
// Try to load a local `autoprefixer` version first
|
||||
try {
|
||||
return require('autoprefixer')
|
||||
} catch {}
|
||||
|
||||
return lazyAutoprefixer()
|
||||
})(),
|
||||
args['--minify'] &&
|
||||
(() => {
|
||||
let options = { preset: ['default', { cssDeclarationSorter: false }] }
|
||||
|
||||
// Try to load a local `cssnano` version first
|
||||
try {
|
||||
return require('cssnano')
|
||||
} catch {}
|
||||
|
||||
return lazyCssnano()(options)
|
||||
})(),
|
||||
].filter(Boolean)
|
||||
|
||||
async function rebuild(config) {
|
||||
env.DEBUG && console.time('Finished in')
|
||||
|
||||
let tailwindPlugin = () => {
|
||||
return {
|
||||
postcssPlugin: 'tailwindcss',
|
||||
Once(root, { result }) {
|
||||
env.DEBUG && console.time('Compiling CSS')
|
||||
tailwind(({ createContext }) => {
|
||||
console.error()
|
||||
console.error('Rebuilding...')
|
||||
|
||||
return () => {
|
||||
if (context !== null) {
|
||||
context.changedContent = changedContent.splice(0)
|
||||
return context
|
||||
}
|
||||
|
||||
env.DEBUG && console.time('Creating context')
|
||||
context = createContext(config, changedContent.splice(0))
|
||||
env.DEBUG && console.timeEnd('Creating context')
|
||||
return context
|
||||
}
|
||||
})(root, result)
|
||||
env.DEBUG && console.timeEnd('Compiling CSS')
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
tailwindPlugin.postcss = true
|
||||
|
||||
let tailwindPluginIdx = plugins.indexOf('__TAILWIND_PLUGIN_POSITION__')
|
||||
let copy = plugins.slice()
|
||||
copy.splice(tailwindPluginIdx, 1, tailwindPlugin)
|
||||
let postcss = loadPostcss()
|
||||
let processor = postcss(copy)
|
||||
|
||||
function processCSS(css) {
|
||||
let start = process.hrtime.bigint()
|
||||
return Promise.resolve()
|
||||
.then(() =>
|
||||
output ? fs.promises.mkdir(path.dirname(output), { recursive: true }) : null
|
||||
)
|
||||
.then(() => processor.process(css, { from: input, to: output }))
|
||||
.then(async (result) => {
|
||||
for (let message of result.messages) {
|
||||
if (message.type === 'dependency') {
|
||||
contextDependencies.add(message.file)
|
||||
}
|
||||
}
|
||||
watcher.add([...contextDependencies])
|
||||
|
||||
if (!output) {
|
||||
return process.stdout.write(result.css)
|
||||
}
|
||||
|
||||
return Promise.all(
|
||||
[
|
||||
outputFile(output, result.css),
|
||||
result.map && outputFile(output + '.map', result.map.toString()),
|
||||
].filter(Boolean)
|
||||
)
|
||||
})
|
||||
.then(() => {
|
||||
let end = process.hrtime.bigint()
|
||||
console.error('Done in', (end - start) / BigInt(1e6) + 'ms.')
|
||||
})
|
||||
.catch((err) => {
|
||||
if (err.name === 'CssSyntaxError') {
|
||||
console.error(err.toString())
|
||||
} else {
|
||||
console.error(err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
let css = await (() => {
|
||||
// Piping in data, let's drain the stdin
|
||||
if (input === '-') {
|
||||
return drainStdin()
|
||||
}
|
||||
|
||||
// Input file has been provided
|
||||
if (input) {
|
||||
return fs.readFileSync(path.resolve(input), 'utf8')
|
||||
}
|
||||
|
||||
// No input file provided, fallback to default atrules
|
||||
return '@tailwind base; @tailwind components; @tailwind utilities'
|
||||
})()
|
||||
|
||||
let result = await processCSS(css)
|
||||
env.DEBUG && console.timeEnd('Finished in')
|
||||
return result
|
||||
}
|
||||
|
||||
let config = refreshConfig(configPath)
|
||||
let contentPatterns = refreshContentPatterns(config)
|
||||
|
||||
/**
|
||||
* @param {import('../types/config.js').RequiredConfig} config
|
||||
* @return {{all: string[], dynamic: string[], static: string[]}}
|
||||
**/
|
||||
function refreshContentPatterns(config) {
|
||||
let globs = extractFileGlobs(config)
|
||||
let tasks = fastGlob.generateTasks(globs, { absolute: true })
|
||||
let dynamicPatterns = tasks.filter((task) => task.dynamic).flatMap((task) => task.patterns)
|
||||
let staticPatterns = tasks.filter((task) => !task.dynamic).flatMap((task) => task.patterns)
|
||||
|
||||
return {
|
||||
all: [...staticPatterns, ...dynamicPatterns],
|
||||
dynamic: dynamicPatterns,
|
||||
}
|
||||
}
|
||||
|
||||
if (input) {
|
||||
contextDependencies.add(path.resolve(input))
|
||||
}
|
||||
|
||||
watcher = chokidar.watch([...contextDependencies, ...extractFileGlobs(config)], {
|
||||
// Force checking for atomic writes in all situations
|
||||
// This causes chokidar to wait up to 100ms for a file to re-added after it's been unlinked
|
||||
// This only works when watching directories though
|
||||
atomic: true,
|
||||
|
||||
usePolling: shouldPoll,
|
||||
interval: shouldPoll ? pollInterval : undefined,
|
||||
ignoreInitial: true,
|
||||
awaitWriteFinish: shouldCoalesceWriteEvents
|
||||
? {
|
||||
stabilityThreshold: 50,
|
||||
pollInterval: pollInterval,
|
||||
}
|
||||
: false,
|
||||
})
|
||||
|
||||
let chain = Promise.resolve()
|
||||
let pendingRebuilds = new Set()
|
||||
|
||||
watcher.on('change', async (file) => {
|
||||
if (contextDependencies.has(file)) {
|
||||
env.DEBUG && console.time('Resolve config')
|
||||
context = null
|
||||
config = refreshConfig(configPath)
|
||||
contentPatterns = refreshContentPatterns(config)
|
||||
env.DEBUG && console.timeEnd('Resolve config')
|
||||
|
||||
env.DEBUG && console.time('Watch new files')
|
||||
let globs = extractFileGlobs(config)
|
||||
watcher.add(configDependencies)
|
||||
watcher.add(globs)
|
||||
env.DEBUG && console.timeEnd('Watch new files')
|
||||
|
||||
chain = chain.then(async () => {
|
||||
changedContent.push(...getChangedContent(config))
|
||||
await rebuild(config)
|
||||
})
|
||||
} else {
|
||||
chain = chain.then(async () => {
|
||||
changedContent.push({
|
||||
content: fs.readFileSync(path.resolve(file), 'utf8'),
|
||||
extension: path.extname(file).slice(1),
|
||||
})
|
||||
|
||||
await rebuild(config)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* When rapidly saving files atomically a couple of situations can happen:
|
||||
* - The file is missing since the external program has deleted it by the time we've gotten around to reading it from the earlier save.
|
||||
* - The file is being written to by the external program by the time we're going to read it and is thus treated as busy because a lock is held.
|
||||
*
|
||||
* To work around this we retry reading the file a handful of times with a delay between each attempt
|
||||
*
|
||||
* @param {string} path
|
||||
* @param {number} tries
|
||||
* @returns {string}
|
||||
* @throws {Error} If the file is still missing or busy after the specified number of tries
|
||||
*/
|
||||
async function readFileWithRetries(path, tries = 5) {
|
||||
for (let n = 0; n <= tries; n++) {
|
||||
try {
|
||||
return await fs.promises.readFile(path, 'utf8')
|
||||
} catch (err) {
|
||||
if (n !== tries) {
|
||||
if (err.code === 'ENOENT' || err.code === 'EBUSY') {
|
||||
await new Promise((resolve) => setTimeout(resolve, 10))
|
||||
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
throw err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Restore watching any files that are "removed"
|
||||
// This can happen when a file is pseudo-atomically replaced (a copy is created, overwritten, the old one is unlinked, and the new one is renamed)
|
||||
// TODO: An an optimization we should allow removal when the config changes
|
||||
watcher.on('unlink', (file) => {
|
||||
file = normalizePath(file)
|
||||
|
||||
// Only re-add the file if it's not covered by a dynamic pattern
|
||||
if (!micromatch.some([file], contentPatterns.dynamic)) {
|
||||
watcher.add(file)
|
||||
}
|
||||
})
|
||||
|
||||
// Some applications such as Visual Studio (but not VS Code)
|
||||
// will only fire a rename event for atomic writes and not a change event
|
||||
// This is very likely a chokidar bug but it's one we need to work around
|
||||
// We treat this as a change event and rebuild the CSS
|
||||
watcher.on('raw', (evt, filePath, meta) => {
|
||||
if (evt !== 'rename') {
|
||||
return
|
||||
}
|
||||
|
||||
let watchedPath = meta.watchedPath
|
||||
|
||||
// Watched path might be the file itself
|
||||
// Or the directory it is in
|
||||
filePath = watchedPath.endsWith(filePath) ? watchedPath : path.join(watchedPath, filePath)
|
||||
|
||||
// Skip this event since the files it is for does not match any of the registered content globs
|
||||
if (!micromatch.some([filePath], contentPatterns.all)) {
|
||||
return
|
||||
}
|
||||
|
||||
// Skip since we've already queued a rebuild for this file that hasn't happened yet
|
||||
if (pendingRebuilds.has(filePath)) {
|
||||
return
|
||||
}
|
||||
|
||||
pendingRebuilds.add(filePath)
|
||||
|
||||
chain = chain.then(async () => {
|
||||
let content
|
||||
|
||||
try {
|
||||
content = await readFileWithRetries(path.resolve(filePath))
|
||||
} finally {
|
||||
pendingRebuilds.delete(filePath)
|
||||
}
|
||||
|
||||
changedContent.push({
|
||||
content,
|
||||
extension: path.extname(filePath).slice(1),
|
||||
})
|
||||
|
||||
await rebuild(config)
|
||||
})
|
||||
})
|
||||
|
||||
watcher.on('add', async (file) => {
|
||||
chain = chain.then(async () => {
|
||||
changedContent.push({
|
||||
content: fs.readFileSync(path.resolve(file), 'utf8'),
|
||||
extension: path.extname(file).slice(1),
|
||||
})
|
||||
|
||||
await rebuild(config)
|
||||
})
|
||||
})
|
||||
|
||||
chain = chain.then(() => {
|
||||
changedContent.push(...getChangedContent(config))
|
||||
return rebuild(config)
|
||||
})
|
||||
}
|
||||
|
||||
if (shouldWatch) {
|
||||
/* Abort the watcher if stdin is closed to avoid zombie processes */
|
||||
process.stdin.on('end', () => process.exit(0))
|
||||
process.stdin.resume()
|
||||
startWatcher()
|
||||
} else {
|
||||
buildOnce()
|
||||
}
|
||||
}
|
||||
run(args, configs)
|
||||
|
|
|
|||
644
src/cli/build.js
Normal file
644
src/cli/build.js
Normal file
|
|
@ -0,0 +1,644 @@
|
|||
// @ts-check
|
||||
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import postcssrc from 'postcss-load-config'
|
||||
import { lilconfig } from 'lilconfig'
|
||||
import loadPlugins from 'postcss-load-config/src/plugins' // Little bit scary, looking at private/internal API
|
||||
import loadOptions from 'postcss-load-config/src/options' // Little bit scary, looking at private/internal API
|
||||
import { loadAutoprefixer, loadCssNano, loadPostcss, loadPostcssImport } from './deps'
|
||||
import log from '../util/log'
|
||||
import chokidar from 'chokidar'
|
||||
import fastGlob from 'fast-glob'
|
||||
import micromatch from 'micromatch'
|
||||
import normalizePath from 'normalize-path'
|
||||
import tailwind from '../processTailwindFeatures'
|
||||
import resolveConfigInternal from '../../resolveConfig'
|
||||
import getModuleDependencies from '../lib/getModuleDependencies'
|
||||
import { validateConfig } from '../util/validateConfig.js'
|
||||
import { parseCandidateFiles } from '../lib/content.js'
|
||||
|
||||
let env = {
|
||||
DEBUG: process.env.DEBUG !== undefined && process.env.DEBUG !== '0',
|
||||
}
|
||||
|
||||
export async function build(args, configs) {
|
||||
let input = args['--input']
|
||||
let output = args['--output']
|
||||
let shouldWatch = args['--watch']
|
||||
let shouldPoll = args['--poll']
|
||||
let shouldCoalesceWriteEvents = shouldPoll || process.platform === 'win32'
|
||||
let includePostCss = args['--postcss']
|
||||
|
||||
// Polling interval in milliseconds
|
||||
// Used only when polling or coalescing add/change events on Windows
|
||||
let pollInterval = 10
|
||||
|
||||
// TODO: Deprecate this in future versions
|
||||
if (!input && args['_'][1]) {
|
||||
console.error('[deprecation] Running tailwindcss without -i, please provide an input file.')
|
||||
input = args['--input'] = args['_'][1]
|
||||
}
|
||||
|
||||
if (input && input !== '-' && !fs.existsSync((input = path.resolve(input)))) {
|
||||
console.error(`Specified input file ${args['--input']} does not exist.`)
|
||||
process.exit(9)
|
||||
}
|
||||
|
||||
if (args['--config'] && !fs.existsSync((args['--config'] = path.resolve(args['--config'])))) {
|
||||
console.error(`Specified config file ${args['--config']} does not exist.`)
|
||||
process.exit(9)
|
||||
}
|
||||
|
||||
// TODO: Reference the @config path here if exists
|
||||
let configPath = args['--config']
|
||||
? args['--config']
|
||||
: ((defaultPath) => (fs.existsSync(defaultPath) ? defaultPath : null))(
|
||||
path.resolve(`./${configs.tailwind}`)
|
||||
)
|
||||
|
||||
async function loadPostCssPlugins() {
|
||||
let customPostCssPath = typeof args['--postcss'] === 'string' ? args['--postcss'] : undefined
|
||||
let config = customPostCssPath
|
||||
? await (async () => {
|
||||
let file = path.resolve(customPostCssPath)
|
||||
|
||||
// Implementation, see: https://unpkg.com/browse/postcss-load-config@3.1.0/src/index.js
|
||||
let { config = {} } = await lilconfig('postcss').load(file)
|
||||
if (typeof config === 'function') {
|
||||
config = config()
|
||||
} else {
|
||||
config = Object.assign({}, config)
|
||||
}
|
||||
|
||||
if (!config.plugins) {
|
||||
config.plugins = []
|
||||
}
|
||||
|
||||
return {
|
||||
file,
|
||||
plugins: loadPlugins(config, file),
|
||||
options: loadOptions(config, file),
|
||||
}
|
||||
})()
|
||||
: await postcssrc()
|
||||
|
||||
let configPlugins = config.plugins
|
||||
|
||||
let configPluginTailwindIdx = configPlugins.findIndex((plugin) => {
|
||||
if (typeof plugin === 'function' && plugin.name === 'tailwindcss') {
|
||||
return true
|
||||
}
|
||||
|
||||
if (typeof plugin === 'object' && plugin !== null && plugin.postcssPlugin === 'tailwindcss') {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
})
|
||||
|
||||
let beforePlugins =
|
||||
configPluginTailwindIdx === -1 ? [] : configPlugins.slice(0, configPluginTailwindIdx)
|
||||
let afterPlugins =
|
||||
configPluginTailwindIdx === -1
|
||||
? configPlugins
|
||||
: configPlugins.slice(configPluginTailwindIdx + 1)
|
||||
|
||||
return [beforePlugins, afterPlugins, config.options]
|
||||
}
|
||||
|
||||
function loadBuiltinPostcssPlugins() {
|
||||
let postcss = loadPostcss()
|
||||
let IMPORT_COMMENT = '__TAILWIND_RESTORE_IMPORT__: '
|
||||
return [
|
||||
[
|
||||
(root) => {
|
||||
root.walkAtRules('import', (rule) => {
|
||||
if (rule.params.slice(1).startsWith('tailwindcss/')) {
|
||||
rule.after(postcss.comment({ text: IMPORT_COMMENT + rule.params }))
|
||||
rule.remove()
|
||||
}
|
||||
})
|
||||
},
|
||||
loadPostcssImport(),
|
||||
(root) => {
|
||||
root.walkComments((rule) => {
|
||||
if (rule.text.startsWith(IMPORT_COMMENT)) {
|
||||
rule.after(
|
||||
postcss.atRule({
|
||||
name: 'import',
|
||||
params: rule.text.replace(IMPORT_COMMENT, ''),
|
||||
})
|
||||
)
|
||||
rule.remove()
|
||||
}
|
||||
})
|
||||
},
|
||||
],
|
||||
[],
|
||||
{},
|
||||
]
|
||||
}
|
||||
|
||||
function resolveConfig() {
|
||||
let config = configPath ? require(configPath) : {}
|
||||
|
||||
if (args['--purge']) {
|
||||
log.warn('purge-flag-deprecated', [
|
||||
'The `--purge` flag has been deprecated.',
|
||||
'Please use `--content` instead.',
|
||||
])
|
||||
if (!args['--content']) {
|
||||
args['--content'] = args['--purge']
|
||||
}
|
||||
}
|
||||
|
||||
if (args['--content']) {
|
||||
let files = args['--content'].split(/(?<!{[^}]+),/)
|
||||
let resolvedConfig = resolveConfigInternal(config, { content: { files } })
|
||||
resolvedConfig.content.files = files
|
||||
resolvedConfig = validateConfig(resolvedConfig)
|
||||
return resolvedConfig
|
||||
}
|
||||
|
||||
let resolvedConfig = resolveConfigInternal(config)
|
||||
resolvedConfig = validateConfig(resolvedConfig)
|
||||
return resolvedConfig
|
||||
}
|
||||
|
||||
function extractFileGlobs(config) {
|
||||
let context = {
|
||||
tailwindConfig: config,
|
||||
userConfigPath: configPath,
|
||||
}
|
||||
|
||||
let contentPaths = parseCandidateFiles(context, config)
|
||||
|
||||
return contentPaths.map((contentPath) => contentPath.pattern)
|
||||
}
|
||||
|
||||
function extractRawContent(config) {
|
||||
return config.content.files.filter((file) => {
|
||||
return typeof file === 'object' && file !== null
|
||||
})
|
||||
}
|
||||
|
||||
function getChangedContent(config) {
|
||||
let changedContent = []
|
||||
|
||||
// Resolve globs from the content config
|
||||
let globs = extractFileGlobs(config)
|
||||
let files = fastGlob.sync(globs)
|
||||
|
||||
for (let file of files) {
|
||||
changedContent.push({
|
||||
content: fs.readFileSync(path.resolve(file), 'utf8'),
|
||||
extension: path.extname(file).slice(1),
|
||||
})
|
||||
}
|
||||
|
||||
// Resolve raw content in the tailwind config
|
||||
for (let { raw: content, extension = 'html' } of extractRawContent(config)) {
|
||||
changedContent.push({ content, extension })
|
||||
}
|
||||
|
||||
return changedContent
|
||||
}
|
||||
|
||||
async function buildOnce() {
|
||||
let config = resolveConfig()
|
||||
let changedContent = getChangedContent(config)
|
||||
|
||||
let tailwindPlugin = () => {
|
||||
return {
|
||||
postcssPlugin: 'tailwindcss',
|
||||
Once(root, { result }) {
|
||||
tailwind(({ createContext }) => {
|
||||
return () => {
|
||||
return createContext(config, changedContent)
|
||||
}
|
||||
})(root, result)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
tailwindPlugin.postcss = true
|
||||
|
||||
let [beforePlugins, afterPlugins, postcssOptions] = includePostCss
|
||||
? await loadPostCssPlugins()
|
||||
: loadBuiltinPostcssPlugins()
|
||||
|
||||
let plugins = [
|
||||
...beforePlugins,
|
||||
tailwindPlugin,
|
||||
!args['--minify'] && formatNodes,
|
||||
...afterPlugins,
|
||||
!args['--no-autoprefixer'] && loadAutoprefixer(),
|
||||
args['--minify'] && loadCssNano(),
|
||||
].filter(Boolean)
|
||||
|
||||
let postcss = loadPostcss()
|
||||
let processor = postcss(plugins)
|
||||
|
||||
function processCSS(css) {
|
||||
let start = process.hrtime.bigint()
|
||||
return Promise.resolve()
|
||||
.then(() => (output ? fs.promises.mkdir(path.dirname(output), { recursive: true }) : null))
|
||||
.then(() => processor.process(css, { ...postcssOptions, from: input, to: output }))
|
||||
.then((result) => {
|
||||
if (!output) {
|
||||
return process.stdout.write(result.css)
|
||||
}
|
||||
|
||||
return Promise.all(
|
||||
[
|
||||
outputFile(output, result.css),
|
||||
result.map && outputFile(output + '.map', result.map.toString()),
|
||||
].filter(Boolean)
|
||||
)
|
||||
})
|
||||
.then(() => {
|
||||
let end = process.hrtime.bigint()
|
||||
console.error()
|
||||
console.error('Done in', (end - start) / BigInt(1e6) + 'ms.')
|
||||
})
|
||||
}
|
||||
|
||||
let css = await (() => {
|
||||
// Piping in data, let's drain the stdin
|
||||
if (input === '-') {
|
||||
return drainStdin()
|
||||
}
|
||||
|
||||
// Input file has been provided
|
||||
if (input) {
|
||||
return fs.readFileSync(path.resolve(input), 'utf8')
|
||||
}
|
||||
|
||||
// No input file provided, fallback to default atrules
|
||||
return '@tailwind base; @tailwind components; @tailwind utilities'
|
||||
})()
|
||||
|
||||
return processCSS(css)
|
||||
}
|
||||
|
||||
let context = null
|
||||
|
||||
async function startWatcher() {
|
||||
let changedContent = []
|
||||
let configDependencies = []
|
||||
let contextDependencies = new Set()
|
||||
let watcher = null
|
||||
|
||||
function refreshConfig() {
|
||||
env.DEBUG && console.time('Module dependencies')
|
||||
for (let file of configDependencies) {
|
||||
delete require.cache[require.resolve(file)]
|
||||
}
|
||||
|
||||
if (configPath) {
|
||||
configDependencies = getModuleDependencies(configPath).map(({ file }) => file)
|
||||
|
||||
for (let dependency of configDependencies) {
|
||||
contextDependencies.add(dependency)
|
||||
}
|
||||
}
|
||||
env.DEBUG && console.timeEnd('Module dependencies')
|
||||
|
||||
return resolveConfig()
|
||||
}
|
||||
|
||||
let [beforePlugins, afterPlugins] = includePostCss
|
||||
? await loadPostCssPlugins()
|
||||
: loadBuiltinPostcssPlugins()
|
||||
|
||||
let plugins = [
|
||||
...beforePlugins,
|
||||
'__TAILWIND_PLUGIN_POSITION__',
|
||||
!args['--minify'] && formatNodes,
|
||||
...afterPlugins,
|
||||
!args['--no-autoprefixer'] && loadAutoprefixer(),
|
||||
args['--minify'] && loadCssNano(),
|
||||
].filter(Boolean)
|
||||
|
||||
async function rebuild(config) {
|
||||
env.DEBUG && console.time('Finished in')
|
||||
|
||||
let tailwindPlugin = () => {
|
||||
return {
|
||||
postcssPlugin: 'tailwindcss',
|
||||
Once(root, { result }) {
|
||||
env.DEBUG && console.time('Compiling CSS')
|
||||
tailwind(({ createContext }) => {
|
||||
console.error()
|
||||
console.error('Rebuilding...')
|
||||
|
||||
return () => {
|
||||
if (context !== null) {
|
||||
context.changedContent = changedContent.splice(0)
|
||||
return context
|
||||
}
|
||||
|
||||
env.DEBUG && console.time('Creating context')
|
||||
context = createContext(config, changedContent.splice(0))
|
||||
env.DEBUG && console.timeEnd('Creating context')
|
||||
return context
|
||||
}
|
||||
})(root, result)
|
||||
env.DEBUG && console.timeEnd('Compiling CSS')
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
tailwindPlugin.postcss = true
|
||||
|
||||
let tailwindPluginIdx = plugins.indexOf('__TAILWIND_PLUGIN_POSITION__')
|
||||
let copy = plugins.slice()
|
||||
copy.splice(tailwindPluginIdx, 1, tailwindPlugin)
|
||||
let postcss = loadPostcss()
|
||||
let processor = postcss(copy)
|
||||
|
||||
function processCSS(css) {
|
||||
let start = process.hrtime.bigint()
|
||||
return Promise.resolve()
|
||||
.then(() =>
|
||||
output ? fs.promises.mkdir(path.dirname(output), { recursive: true }) : null
|
||||
)
|
||||
.then(() => processor.process(css, { from: input, to: output }))
|
||||
.then(async (result) => {
|
||||
for (let message of result.messages) {
|
||||
if (message.type === 'dependency') {
|
||||
contextDependencies.add(message.file)
|
||||
}
|
||||
}
|
||||
watcher.add([...contextDependencies])
|
||||
|
||||
if (!output) {
|
||||
return process.stdout.write(result.css)
|
||||
}
|
||||
|
||||
return Promise.all(
|
||||
[
|
||||
outputFile(output, result.css),
|
||||
result.map && outputFile(output + '.map', result.map.toString()),
|
||||
].filter(Boolean)
|
||||
)
|
||||
})
|
||||
.then(() => {
|
||||
let end = process.hrtime.bigint()
|
||||
console.error('Done in', (end - start) / BigInt(1e6) + 'ms.')
|
||||
})
|
||||
.catch((err) => {
|
||||
if (err.name === 'CssSyntaxError') {
|
||||
console.error(err.toString())
|
||||
} else {
|
||||
console.error(err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
let css = await (() => {
|
||||
// Piping in data, let's drain the stdin
|
||||
if (input === '-') {
|
||||
return drainStdin()
|
||||
}
|
||||
|
||||
// Input file has been provided
|
||||
if (input) {
|
||||
return fs.readFileSync(path.resolve(input), 'utf8')
|
||||
}
|
||||
|
||||
// No input file provided, fallback to default atrules
|
||||
return '@tailwind base; @tailwind components; @tailwind utilities'
|
||||
})()
|
||||
|
||||
let result = await processCSS(css)
|
||||
env.DEBUG && console.timeEnd('Finished in')
|
||||
return result
|
||||
}
|
||||
|
||||
let config = refreshConfig()
|
||||
let contentPatterns = refreshContentPatterns(config)
|
||||
|
||||
/**
|
||||
* @param {import('../../types/config.js').RequiredConfig} config
|
||||
* @return {{all: string[], dynamic: string[]}}
|
||||
**/
|
||||
function refreshContentPatterns(config) {
|
||||
// TODO: This can be optimized since we're using `fastGlob.generateTasks` indirectly in extractFileGlobs
|
||||
let globs = extractFileGlobs(config)
|
||||
let tasks = fastGlob.generateTasks(globs, { absolute: true })
|
||||
let dynamicPatterns = tasks.filter((task) => task.dynamic).flatMap((task) => task.patterns)
|
||||
let staticPatterns = tasks.filter((task) => !task.dynamic).flatMap((task) => task.patterns)
|
||||
|
||||
return {
|
||||
all: [...staticPatterns, ...dynamicPatterns],
|
||||
dynamic: dynamicPatterns,
|
||||
}
|
||||
}
|
||||
|
||||
if (input) {
|
||||
contextDependencies.add(path.resolve(input))
|
||||
}
|
||||
|
||||
watcher = chokidar.watch([...contextDependencies, ...extractFileGlobs(config)], {
|
||||
// Force checking for atomic writes in all situations
|
||||
// This causes chokidar to wait up to 100ms for a file to re-added after it's been unlinked
|
||||
// This only works when watching directories though
|
||||
atomic: true,
|
||||
|
||||
usePolling: shouldPoll,
|
||||
interval: shouldPoll ? pollInterval : undefined,
|
||||
ignoreInitial: true,
|
||||
awaitWriteFinish: shouldCoalesceWriteEvents
|
||||
? {
|
||||
stabilityThreshold: 50,
|
||||
pollInterval: pollInterval,
|
||||
}
|
||||
: false,
|
||||
})
|
||||
|
||||
let chain = Promise.resolve()
|
||||
let pendingRebuilds = new Set()
|
||||
|
||||
watcher.on('change', async (file) => {
|
||||
if (contextDependencies.has(file)) {
|
||||
env.DEBUG && console.time('Resolve config')
|
||||
context = null
|
||||
config = refreshConfig()
|
||||
contentPatterns = refreshContentPatterns(config)
|
||||
env.DEBUG && console.timeEnd('Resolve config')
|
||||
|
||||
env.DEBUG && console.time('Watch new files')
|
||||
let globs = extractFileGlobs(config)
|
||||
watcher.add(configDependencies)
|
||||
watcher.add(globs)
|
||||
env.DEBUG && console.timeEnd('Watch new files')
|
||||
|
||||
chain = chain.then(async () => {
|
||||
changedContent.push(...getChangedContent(config))
|
||||
await rebuild(config)
|
||||
})
|
||||
} else {
|
||||
chain = chain.then(async () => {
|
||||
changedContent.push({
|
||||
content: fs.readFileSync(path.resolve(file), 'utf8'),
|
||||
extension: path.extname(file).slice(1),
|
||||
})
|
||||
|
||||
await rebuild(config)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* When rapidly saving files atomically a couple of situations can happen:
|
||||
* - The file is missing since the external program has deleted it by the time we've gotten around to reading it from the earlier save.
|
||||
* - The file is being written to by the external program by the time we're going to read it and is thus treated as busy because a lock is held.
|
||||
*
|
||||
* To work around this we retry reading the file a handful of times with a delay between each attempt
|
||||
*
|
||||
* @param {string} path
|
||||
* @param {number} tries
|
||||
* @returns {Promise<string | undefined>}
|
||||
* @throws {Error} If the file is still missing or busy after the specified number of tries
|
||||
*/
|
||||
async function readFileWithRetries(path, tries = 5) {
|
||||
for (let n = 0; n <= tries; n++) {
|
||||
try {
|
||||
return await fs.promises.readFile(path, 'utf8')
|
||||
} catch (err) {
|
||||
if (n !== tries) {
|
||||
if (err.code === 'ENOENT' || err.code === 'EBUSY') {
|
||||
await new Promise((resolve) => setTimeout(resolve, 10))
|
||||
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
throw err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Restore watching any files that are "removed"
|
||||
// This can happen when a file is pseudo-atomically replaced (a copy is created, overwritten, the old one is unlinked, and the new one is renamed)
|
||||
// TODO: An an optimization we should allow removal when the config changes
|
||||
watcher.on('unlink', (file) => {
|
||||
file = normalizePath(file)
|
||||
|
||||
// Only re-add the file if it's not covered by a dynamic pattern
|
||||
if (!micromatch.some([file], contentPatterns.dynamic)) {
|
||||
watcher.add(file)
|
||||
}
|
||||
})
|
||||
|
||||
// Some applications such as Visual Studio (but not VS Code)
|
||||
// will only fire a rename event for atomic writes and not a change event
|
||||
// This is very likely a chokidar bug but it's one we need to work around
|
||||
// We treat this as a change event and rebuild the CSS
|
||||
watcher.on('raw', (evt, filePath, meta) => {
|
||||
if (evt !== 'rename') {
|
||||
return
|
||||
}
|
||||
|
||||
let watchedPath = meta.watchedPath
|
||||
|
||||
// Watched path might be the file itself
|
||||
// Or the directory it is in
|
||||
filePath = watchedPath.endsWith(filePath) ? watchedPath : path.join(watchedPath, filePath)
|
||||
|
||||
// Skip this event since the files it is for does not match any of the registered content globs
|
||||
if (!micromatch.some([filePath], contentPatterns.all)) {
|
||||
return
|
||||
}
|
||||
|
||||
// Skip since we've already queued a rebuild for this file that hasn't happened yet
|
||||
if (pendingRebuilds.has(filePath)) {
|
||||
return
|
||||
}
|
||||
|
||||
pendingRebuilds.add(filePath)
|
||||
|
||||
chain = chain.then(async () => {
|
||||
let content
|
||||
|
||||
try {
|
||||
content = await readFileWithRetries(path.resolve(filePath))
|
||||
} finally {
|
||||
pendingRebuilds.delete(filePath)
|
||||
}
|
||||
|
||||
changedContent.push({
|
||||
content,
|
||||
extension: path.extname(filePath).slice(1),
|
||||
})
|
||||
|
||||
await rebuild(config)
|
||||
})
|
||||
})
|
||||
|
||||
watcher.on('add', async (file) => {
|
||||
chain = chain.then(async () => {
|
||||
changedContent.push({
|
||||
content: fs.readFileSync(path.resolve(file), 'utf8'),
|
||||
extension: path.extname(file).slice(1),
|
||||
})
|
||||
|
||||
await rebuild(config)
|
||||
})
|
||||
})
|
||||
|
||||
chain = chain.then(() => {
|
||||
changedContent.push(...getChangedContent(config))
|
||||
return rebuild(config)
|
||||
})
|
||||
}
|
||||
|
||||
if (shouldWatch) {
|
||||
/* Abort the watcher if stdin is closed to avoid zombie processes */
|
||||
process.stdin.on('end', () => process.exit(0))
|
||||
process.stdin.resume()
|
||||
startWatcher()
|
||||
} else {
|
||||
buildOnce()
|
||||
}
|
||||
}
|
||||
|
||||
function drainStdin() {
|
||||
return new Promise((resolve, reject) => {
|
||||
let result = ''
|
||||
process.stdin.on('data', (chunk) => {
|
||||
result += chunk
|
||||
})
|
||||
process.stdin.on('end', () => resolve(result))
|
||||
process.stdin.on('error', (err) => reject(err))
|
||||
})
|
||||
}
|
||||
|
||||
function indentRecursive(node, indent = 0) {
|
||||
node.each &&
|
||||
node.each((child, i) => {
|
||||
if (!child.raws.before || !child.raws.before.trim() || child.raws.before.includes('\n')) {
|
||||
child.raws.before = `\n${node.type !== 'rule' && i > 0 ? '\n' : ''}${' '.repeat(indent)}`
|
||||
}
|
||||
child.raws.after = `\n${' '.repeat(indent)}`
|
||||
indentRecursive(child, indent + 1)
|
||||
})
|
||||
}
|
||||
|
||||
function formatNodes(root) {
|
||||
indentRecursive(root)
|
||||
if (root.first) {
|
||||
root.first.raws.before = ''
|
||||
}
|
||||
}
|
||||
|
||||
async function outputFile(file, contents) {
|
||||
if (fs.existsSync(file) && (await fs.promises.readFile(file, 'utf8')) === contents) {
|
||||
return // Skip writing the file
|
||||
}
|
||||
|
||||
// Write the file
|
||||
await fs.promises.writeFile(file, contents, 'utf8')
|
||||
}
|
||||
40
src/cli/deps.js
Normal file
40
src/cli/deps.js
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
// @ts-check
|
||||
|
||||
import { lazyPostcss, lazyPostcssImport, lazyCssnano, lazyAutoprefixer } from '../../peers/index.js'
|
||||
|
||||
export function loadPostcss() {
|
||||
// Try to load a local `postcss` version first
|
||||
try {
|
||||
return require('postcss')
|
||||
} catch {}
|
||||
|
||||
return lazyPostcss()
|
||||
}
|
||||
|
||||
export function loadPostcssImport() {
|
||||
try {
|
||||
return require('postcss-import')
|
||||
} catch {}
|
||||
|
||||
return lazyPostcssImport()
|
||||
}
|
||||
|
||||
export function loadCssNano() {
|
||||
let options = { preset: ['default', { cssDeclarationSorter: false }] }
|
||||
|
||||
// Try to load a local `cssnano` version first
|
||||
try {
|
||||
return require('cssnano')
|
||||
} catch {}
|
||||
|
||||
return lazyCssnano()(options)
|
||||
}
|
||||
|
||||
export function loadAutoprefixer() {
|
||||
// Try to load a local `autoprefixer` version first
|
||||
try {
|
||||
return require('autoprefixer')
|
||||
} catch {}
|
||||
|
||||
return lazyAutoprefixer()
|
||||
}
|
||||
70
src/cli/help.js
Normal file
70
src/cli/help.js
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
// @ts-check
|
||||
import packageJson from '../../package.json'
|
||||
|
||||
export function help({ message, usage, commands, options }) {
|
||||
let indent = 2
|
||||
|
||||
// Render header
|
||||
console.log()
|
||||
console.log(`${packageJson.name} v${packageJson.version}`)
|
||||
|
||||
// Render message
|
||||
if (message) {
|
||||
console.log()
|
||||
for (let msg of message.split('\n')) {
|
||||
console.log(msg)
|
||||
}
|
||||
}
|
||||
|
||||
// Render usage
|
||||
if (usage && usage.length > 0) {
|
||||
console.log()
|
||||
console.log('Usage:')
|
||||
for (let example of usage) {
|
||||
console.log(' '.repeat(indent), example)
|
||||
}
|
||||
}
|
||||
|
||||
// Render commands
|
||||
if (commands && commands.length > 0) {
|
||||
console.log()
|
||||
console.log('Commands:')
|
||||
for (let command of commands) {
|
||||
console.log(' '.repeat(indent), command)
|
||||
}
|
||||
}
|
||||
|
||||
// Render options
|
||||
if (options) {
|
||||
let groupedOptions = {}
|
||||
for (let [key, value] of Object.entries(options)) {
|
||||
if (typeof value === 'object') {
|
||||
groupedOptions[key] = { ...value, flags: [key] }
|
||||
} else {
|
||||
groupedOptions[value].flags.push(key)
|
||||
}
|
||||
}
|
||||
|
||||
console.log()
|
||||
console.log('Options:')
|
||||
for (let { flags, description, deprecated } of Object.values(groupedOptions)) {
|
||||
if (deprecated) continue
|
||||
|
||||
if (flags.length === 1) {
|
||||
console.log(
|
||||
' '.repeat(indent + 4 /* 4 = "-i, ".length */),
|
||||
flags.slice().reverse().join(', ').padEnd(20, ' '),
|
||||
description
|
||||
)
|
||||
} else {
|
||||
console.log(
|
||||
' '.repeat(indent),
|
||||
flags.slice().reverse().join(', ').padEnd(24, ' '),
|
||||
description
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log()
|
||||
}
|
||||
50
src/cli/init.js
Normal file
50
src/cli/init.js
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
// @ts-check
|
||||
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
|
||||
export function init(args, configs) {
|
||||
let messages = []
|
||||
|
||||
let tailwindConfigLocation = path.resolve(args['_'][1] ?? `./${configs.tailwind}`)
|
||||
if (fs.existsSync(tailwindConfigLocation)) {
|
||||
messages.push(`${path.basename(tailwindConfigLocation)} already exists.`)
|
||||
} else {
|
||||
let stubFile = fs.readFileSync(
|
||||
args['--full']
|
||||
? path.resolve(__dirname, '../stubs/defaultConfig.stub.js')
|
||||
: path.resolve(__dirname, '../stubs/simpleConfig.stub.js'),
|
||||
'utf8'
|
||||
)
|
||||
|
||||
// Change colors import
|
||||
stubFile = stubFile.replace('../colors', 'tailwindcss/colors')
|
||||
|
||||
fs.writeFileSync(tailwindConfigLocation, stubFile, 'utf8')
|
||||
|
||||
messages.push(`Created Tailwind CSS config file: ${path.basename(tailwindConfigLocation)}`)
|
||||
}
|
||||
|
||||
if (args['--postcss']) {
|
||||
let postcssConfigLocation = path.resolve(`./${configs.postcss}`)
|
||||
if (fs.existsSync(postcssConfigLocation)) {
|
||||
messages.push(`${path.basename(postcssConfigLocation)} already exists.`)
|
||||
} else {
|
||||
let stubFile = fs.readFileSync(
|
||||
path.resolve(__dirname, '../stubs/defaultPostCssConfig.stub.js'),
|
||||
'utf8'
|
||||
)
|
||||
|
||||
fs.writeFileSync(postcssConfigLocation, stubFile, 'utf8')
|
||||
|
||||
messages.push(`Created PostCSS config file: ${path.basename(postcssConfigLocation)}`)
|
||||
}
|
||||
}
|
||||
|
||||
if (messages.length > 0) {
|
||||
console.log()
|
||||
for (let message of messages) {
|
||||
console.log(message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -10,8 +10,13 @@ let featureFlags = {
|
|||
'hoverOnlyWhenSupported',
|
||||
'respectDefaultRingColorOpacity',
|
||||
'disableColorOpacityUtilitiesByDefault',
|
||||
'relativeContentPathsByDefault',
|
||||
],
|
||||
experimental: [
|
||||
'optimizeUniversalDefaults',
|
||||
'matchVariant',
|
||||
// 'variantGrouping',
|
||||
],
|
||||
experimental: ['optimizeUniversalDefaults', 'matchVariant' /* , 'variantGrouping' */],
|
||||
}
|
||||
|
||||
export function flagEnabled(config, flag) {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import setupTrackingContext from './lib/setupTrackingContext'
|
||||
import processTailwindFeatures from './processTailwindFeatures'
|
||||
import { env } from './lib/sharedState'
|
||||
import { findAtConfigPath } from './lib/findAtConfigPath'
|
||||
|
||||
module.exports = function tailwindcss(configOrPath) {
|
||||
return {
|
||||
|
|
@ -13,6 +14,10 @@ module.exports = function tailwindcss(configOrPath) {
|
|||
return root
|
||||
},
|
||||
function (root, result) {
|
||||
// Use the path for the `@config` directive if it exists, otherwise use the
|
||||
// path for the file being processed
|
||||
configOrPath = findAtConfigPath(root, result) ?? configOrPath
|
||||
|
||||
let context = setupTrackingContext(configOrPath)
|
||||
|
||||
if (root.type === 'document') {
|
||||
|
|
|
|||
200
src/lib/content.js
Normal file
200
src/lib/content.js
Normal file
|
|
@ -0,0 +1,200 @@
|
|||
// @ts-check
|
||||
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import isGlob from 'is-glob'
|
||||
import fastGlob from 'fast-glob'
|
||||
import normalizePath from 'normalize-path'
|
||||
import { flagEnabled } from '../featureFlags.js'
|
||||
import { parseGlob } from '../util/parseGlob'
|
||||
import { env } from './sharedState'
|
||||
|
||||
/** @typedef {import('../../types/config.js').RawFile} RawFile */
|
||||
/** @typedef {import('../../types/config.js').FilePath} FilePath */
|
||||
|
||||
/**
|
||||
* @typedef {object} ContentPath
|
||||
* @property {string} original
|
||||
* @property {string} base
|
||||
* @property {string | null} glob
|
||||
* @property {boolean} ignore
|
||||
* @property {string} pattern
|
||||
*/
|
||||
|
||||
/**
|
||||
* Turn a list of content paths (absolute or not; glob or not) into a list of
|
||||
* absolute file paths that exist on the filesystem
|
||||
*
|
||||
* If there are symlinks in the path then multiple paths will be returned
|
||||
* one for the symlink and one for the actual file
|
||||
*
|
||||
* @param {*} context
|
||||
* @param {import('tailwindcss').Config} tailwindConfig
|
||||
* @returns {ContentPath[]}
|
||||
*/
|
||||
export function parseCandidateFiles(context, tailwindConfig) {
|
||||
let files = tailwindConfig.content.files
|
||||
|
||||
// Normalize the file globs
|
||||
files = files.filter((filePath) => typeof filePath === 'string')
|
||||
files = files.map(normalizePath)
|
||||
|
||||
// Split into included and excluded globs
|
||||
let tasks = fastGlob.generateTasks(files)
|
||||
|
||||
/** @type {ContentPath[]} */
|
||||
let included = []
|
||||
|
||||
/** @type {ContentPath[]} */
|
||||
let excluded = []
|
||||
|
||||
for (const task of tasks) {
|
||||
included.push(...task.positive.map((filePath) => parseFilePath(filePath, false)))
|
||||
excluded.push(...task.negative.map((filePath) => parseFilePath(filePath, true)))
|
||||
}
|
||||
|
||||
let paths = [...included, ...excluded]
|
||||
|
||||
// Resolve paths relative to the config file or cwd
|
||||
paths = resolveRelativePaths(context, paths)
|
||||
|
||||
// Resolve symlinks if possible
|
||||
paths = paths.flatMap(resolvePathSymlinks)
|
||||
|
||||
// Update cached patterns
|
||||
paths = paths.map(resolveGlobPattern)
|
||||
|
||||
return paths
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} filePath
|
||||
* @param {boolean} ignore
|
||||
* @returns {ContentPath}
|
||||
*/
|
||||
function parseFilePath(filePath, ignore) {
|
||||
let contentPath = {
|
||||
original: filePath,
|
||||
base: filePath,
|
||||
ignore,
|
||||
pattern: filePath,
|
||||
glob: null,
|
||||
}
|
||||
|
||||
if (isGlob(filePath)) {
|
||||
Object.assign(contentPath, parseGlob(filePath))
|
||||
}
|
||||
|
||||
return contentPath
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {ContentPath} contentPath
|
||||
* @returns {ContentPath}
|
||||
*/
|
||||
function resolveGlobPattern(contentPath) {
|
||||
contentPath.pattern = contentPath.glob
|
||||
? `${contentPath.base}/${contentPath.glob}`
|
||||
: contentPath.base
|
||||
|
||||
contentPath.pattern = contentPath.ignore ? `!${contentPath.pattern}` : contentPath.pattern
|
||||
|
||||
return contentPath
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve each path relative to the config file (when possible) if the experimental flag is enabled
|
||||
* Otherwise, resolve relative to the current working directory
|
||||
*
|
||||
* @param {any} context
|
||||
* @param {ContentPath[]} contentPaths
|
||||
* @returns {ContentPath[]}
|
||||
*/
|
||||
function resolveRelativePaths(context, contentPaths) {
|
||||
let resolveFrom = []
|
||||
|
||||
// Resolve base paths relative to the config file (when possible) if the experimental flag is enabled
|
||||
if (context.userConfigPath && context.tailwindConfig.content.relative) {
|
||||
resolveFrom = [path.dirname(context.userConfigPath)]
|
||||
}
|
||||
|
||||
return contentPaths.map((contentPath) => {
|
||||
contentPath.base = path.resolve(...resolveFrom, contentPath.base)
|
||||
|
||||
return contentPath
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the symlink for the base directory / file in each path
|
||||
* These are added as additional dependencies to watch for changes because
|
||||
* some tools (like webpack) will only watch the actual file or directory
|
||||
* but not the symlink itself even in projects that use monorepos.
|
||||
*
|
||||
* @param {ContentPath} contentPath
|
||||
* @returns {ContentPath[]}
|
||||
*/
|
||||
function resolvePathSymlinks(contentPath) {
|
||||
let paths = [contentPath]
|
||||
|
||||
try {
|
||||
let resolvedPath = fs.realpathSync(contentPath.base)
|
||||
if (resolvedPath !== contentPath.base) {
|
||||
paths.push({
|
||||
...contentPath,
|
||||
base: resolvedPath,
|
||||
})
|
||||
}
|
||||
} catch {
|
||||
// TODO: log this?
|
||||
}
|
||||
|
||||
return paths
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {any} context
|
||||
* @param {ContentPath[]} candidateFiles
|
||||
* @param {Map<string, number>} fileModifiedMap
|
||||
* @returns {{ content: string, extension: string }[]}
|
||||
*/
|
||||
export function resolvedChangedContent(context, candidateFiles, fileModifiedMap) {
|
||||
let changedContent = context.tailwindConfig.content.files
|
||||
.filter((item) => typeof item.raw === 'string')
|
||||
.map(({ raw, extension = 'html' }) => ({ content: raw, extension }))
|
||||
|
||||
for (let changedFile of resolveChangedFiles(candidateFiles, fileModifiedMap)) {
|
||||
let content = fs.readFileSync(changedFile, 'utf8')
|
||||
let extension = path.extname(changedFile).slice(1)
|
||||
changedContent.push({ content, extension })
|
||||
}
|
||||
|
||||
return changedContent
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {ContentPath[]} candidateFiles
|
||||
* @param {Map<string, number>} fileModifiedMap
|
||||
* @returns {Set<string>}
|
||||
*/
|
||||
function resolveChangedFiles(candidateFiles, fileModifiedMap) {
|
||||
let paths = candidateFiles.map((contentPath) => contentPath.pattern)
|
||||
|
||||
let changedFiles = new Set()
|
||||
env.DEBUG && console.time('Finding changed files')
|
||||
let files = fastGlob.sync(paths, { absolute: true })
|
||||
for (let file of files) {
|
||||
let prevModified = fileModifiedMap.has(file) ? fileModifiedMap.get(file) : -Infinity
|
||||
let modified = fs.statSync(file).mtimeMs
|
||||
|
||||
if (modified > prevModified) {
|
||||
changedFiles.add(file)
|
||||
fileModifiedMap.set(file, modified)
|
||||
}
|
||||
}
|
||||
env.DEBUG && console.timeEnd('Finding changed files')
|
||||
return changedFiles
|
||||
}
|
||||
40
src/lib/findAtConfigPath.js
Normal file
40
src/lib/findAtConfigPath.js
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
import path from 'path'
|
||||
|
||||
/**
|
||||
* Find the @config at-rule in the given CSS AST and return the relative path to the config file
|
||||
*
|
||||
* @param {import('postcss').Root} root
|
||||
* @param {import('postcss').Result} result
|
||||
*/
|
||||
export function findAtConfigPath(root, result) {
|
||||
let configPath = null
|
||||
let relativeTo = root.source?.input.file ?? result.opts.from ?? null
|
||||
|
||||
root.walkAtRules('config', (rule) => {
|
||||
if (relativeTo === undefined) {
|
||||
throw rule.error(
|
||||
'The `@config` at-rule cannot be used without a `from` option being set on the PostCSS config.'
|
||||
)
|
||||
}
|
||||
|
||||
if (configPath) {
|
||||
throw rule.error('Only `@config` at-rule is allowed per file.')
|
||||
}
|
||||
|
||||
let matches = rule.params.match(/(['"])(.*?)\1/)
|
||||
if (!matches) {
|
||||
throw rule.error(
|
||||
'The `@config` at-rule must be followed by a string containing the path to the config file.'
|
||||
)
|
||||
}
|
||||
|
||||
configPath = matches[2]
|
||||
if (path.isAbsolute(configPath)) {
|
||||
throw rule.error('The `@config` at-rule cannot be used with an absolute path.')
|
||||
}
|
||||
|
||||
rule.remove()
|
||||
})
|
||||
|
||||
return configPath ? path.resolve(path.dirname(relativeTo), configPath) : null
|
||||
}
|
||||
|
|
@ -1000,6 +1000,10 @@ export function getContext(
|
|||
|
||||
let context = createContext(tailwindConfig, [], root)
|
||||
|
||||
Object.assign(context, {
|
||||
userConfigPath,
|
||||
})
|
||||
|
||||
trackModified([...contextDependencies], getFileModifiedMap(context))
|
||||
|
||||
// ---
|
||||
|
|
|
|||
|
|
@ -1,22 +1,14 @@
|
|||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
|
||||
import fastGlob from 'fast-glob'
|
||||
import LRU from 'quick-lru'
|
||||
import normalizePath from 'normalize-path'
|
||||
|
||||
import hash from '../util/hashConfig'
|
||||
import getModuleDependencies from '../lib/getModuleDependencies'
|
||||
|
||||
import resolveConfig from '../public/resolve-config'
|
||||
|
||||
import resolveConfigPath from '../util/resolveConfigPath'
|
||||
|
||||
import { env } from './sharedState'
|
||||
|
||||
import { getContext, getFileModifiedMap } from './setupContextUtils'
|
||||
import parseDependency from '../util/parseDependency'
|
||||
import { validateConfig } from '../util/validateConfig.js'
|
||||
import { parseCandidateFiles, resolvedChangedContent } from './content.js'
|
||||
|
||||
let configPathCache = new LRU({ maxSize: 100 })
|
||||
|
||||
|
|
@ -27,9 +19,7 @@ function getCandidateFiles(context, tailwindConfig) {
|
|||
return candidateFilesCache.get(context)
|
||||
}
|
||||
|
||||
let candidateFiles = tailwindConfig.content.files
|
||||
.filter((item) => typeof item === 'string')
|
||||
.map((contentPath) => normalizePath(contentPath))
|
||||
let candidateFiles = parseCandidateFiles(context, tailwindConfig)
|
||||
|
||||
return candidateFilesCache.set(context, candidateFiles).get(context)
|
||||
}
|
||||
|
|
@ -80,36 +70,6 @@ function getTailwindConfig(configOrPath) {
|
|||
return [newConfig, null, hash(newConfig), []]
|
||||
}
|
||||
|
||||
function resolvedChangedContent(context, candidateFiles, fileModifiedMap) {
|
||||
let changedContent = context.tailwindConfig.content.files
|
||||
.filter((item) => typeof item.raw === 'string')
|
||||
.map(({ raw, extension = 'html' }) => ({ content: raw, extension }))
|
||||
|
||||
for (let changedFile of resolveChangedFiles(candidateFiles, fileModifiedMap)) {
|
||||
let content = fs.readFileSync(changedFile, 'utf8')
|
||||
let extension = path.extname(changedFile).slice(1)
|
||||
changedContent.push({ content, extension })
|
||||
}
|
||||
return changedContent
|
||||
}
|
||||
|
||||
function resolveChangedFiles(candidateFiles, fileModifiedMap) {
|
||||
let changedFiles = new Set()
|
||||
env.DEBUG && console.time('Finding changed files')
|
||||
let files = fastGlob.sync(candidateFiles, { absolute: true })
|
||||
for (let file of files) {
|
||||
let prevModified = fileModifiedMap.has(file) ? fileModifiedMap.get(file) : -Infinity
|
||||
let modified = fs.statSync(file).mtimeMs
|
||||
|
||||
if (modified > prevModified) {
|
||||
changedFiles.add(file)
|
||||
fileModifiedMap.set(file, modified)
|
||||
}
|
||||
}
|
||||
env.DEBUG && console.timeEnd('Finding changed files')
|
||||
return changedFiles
|
||||
}
|
||||
|
||||
// DISABLE_TOUCH = TRUE
|
||||
|
||||
// Retrieve an existing context from cache if possible (since contexts are unique per
|
||||
|
|
@ -161,9 +121,8 @@ export default function setupTrackingContext(configOrPath) {
|
|||
let fileModifiedMap = getFileModifiedMap(context)
|
||||
|
||||
// Add template paths as postcss dependencies.
|
||||
for (let fileOrGlob of candidateFiles) {
|
||||
let dependency = parseDependency(fileOrGlob)
|
||||
if (dependency) {
|
||||
for (let contentPath of candidateFiles) {
|
||||
for (let dependency of parseDependency(contentPath)) {
|
||||
registerDependency(dependency)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -56,9 +56,11 @@ export function normalizeConfig(config) {
|
|||
|
||||
// When `config.content` is an object
|
||||
if (typeof config.content === 'object' && config.content !== null) {
|
||||
// Only `files`, `extract` and `transform` can exist in `config.content`
|
||||
// Only `files`, `relative`, `extract`, and `transform` can exist in `config.content`
|
||||
if (
|
||||
Object.keys(config.content).some((key) => !['files', 'extract', 'transform'].includes(key))
|
||||
Object.keys(config.content).some(
|
||||
(key) => !['files', 'relative', 'extract', 'transform'].includes(key)
|
||||
)
|
||||
) {
|
||||
return false
|
||||
}
|
||||
|
|
@ -112,6 +114,14 @@ export function normalizeConfig(config) {
|
|||
) {
|
||||
return false
|
||||
}
|
||||
|
||||
// `config.content.relative` is optional and can be a boolean
|
||||
if (
|
||||
typeof config.content.relative !== 'boolean' &&
|
||||
typeof config.content.relative !== 'undefined'
|
||||
) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
|
|
@ -154,6 +164,16 @@ export function normalizeConfig(config) {
|
|||
|
||||
// Normalize the `content`
|
||||
config.content = {
|
||||
relative: (() => {
|
||||
let { content } = config
|
||||
|
||||
if (content?.relative) {
|
||||
return content.relative
|
||||
}
|
||||
|
||||
return config.future?.relativeContentPathsByDefault ?? false
|
||||
})(),
|
||||
|
||||
files: (() => {
|
||||
let { content, purge } = config
|
||||
|
||||
|
|
|
|||
|
|
@ -1,49 +1,44 @@
|
|||
import isGlob from 'is-glob'
|
||||
import globParent from 'glob-parent'
|
||||
import path from 'path'
|
||||
// @ts-check
|
||||
|
||||
// Based on `glob-base`
|
||||
// https://github.com/micromatch/glob-base/blob/master/index.js
|
||||
function parseGlob(pattern) {
|
||||
let glob = pattern
|
||||
let base = globParent(pattern)
|
||||
/**
|
||||
* @typedef {{type: 'dependency', file: string} | {type: 'dir-dependency', dir: string, glob: string}} Dependency
|
||||
*/
|
||||
|
||||
if (base !== '.') {
|
||||
glob = pattern.substr(base.length)
|
||||
if (glob.charAt(0) === '/') {
|
||||
glob = glob.substr(1)
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @param {import('../lib/content.js').ContentPath} contentPath
|
||||
* @returns {Dependency[]}
|
||||
*/
|
||||
export default function parseDependency(contentPath) {
|
||||
if (contentPath.ignore) {
|
||||
return []
|
||||
}
|
||||
|
||||
if (glob.substr(0, 2) === './') {
|
||||
glob = glob.substr(2)
|
||||
}
|
||||
if (glob.charAt(0) === '/') {
|
||||
glob = glob.substr(1)
|
||||
if (!contentPath.glob) {
|
||||
return [
|
||||
{
|
||||
type: 'dependency',
|
||||
file: contentPath.base,
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
return { base, glob }
|
||||
}
|
||||
|
||||
export default function parseDependency(normalizedFileOrGlob) {
|
||||
if (normalizedFileOrGlob.startsWith('!')) {
|
||||
return null
|
||||
}
|
||||
|
||||
let message
|
||||
|
||||
if (isGlob(normalizedFileOrGlob)) {
|
||||
let { base, glob } = parseGlob(normalizedFileOrGlob)
|
||||
message = { type: 'dir-dependency', dir: path.resolve(base), glob }
|
||||
} else {
|
||||
message = { type: 'dependency', file: path.resolve(normalizedFileOrGlob) }
|
||||
}
|
||||
|
||||
// rollup-plugin-postcss does not support dir-dependency messages
|
||||
// but directories can be watched in the same way as files
|
||||
if (message.type === 'dir-dependency' && process.env.ROLLUP_WATCH === 'true') {
|
||||
message = { type: 'dependency', file: message.dir }
|
||||
}
|
||||
|
||||
return message
|
||||
if (process.env.ROLLUP_WATCH === 'true') {
|
||||
// rollup-plugin-postcss does not support dir-dependency messages
|
||||
// but directories can be watched in the same way as files
|
||||
return [
|
||||
{
|
||||
type: 'dependency',
|
||||
file: contentPath.base,
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
type: 'dir-dependency',
|
||||
dir: contentPath.base,
|
||||
glob: contentPath.glob,
|
||||
},
|
||||
]
|
||||
}
|
||||
|
|
|
|||
24
src/util/parseGlob.js
Normal file
24
src/util/parseGlob.js
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
import globParent from 'glob-parent'
|
||||
|
||||
// Based on `glob-base`
|
||||
// https://github.com/micromatch/glob-base/blob/master/index.js
|
||||
export function parseGlob(pattern) {
|
||||
let glob = pattern
|
||||
let base = globParent(pattern)
|
||||
|
||||
if (base !== '.') {
|
||||
glob = pattern.substr(base.length)
|
||||
if (glob.charAt(0) === '/') {
|
||||
glob = glob.substr(1)
|
||||
}
|
||||
}
|
||||
|
||||
if (glob.substr(0, 2) === './') {
|
||||
glob = glob.substr(2)
|
||||
}
|
||||
if (glob.charAt(0) === '/') {
|
||||
glob = glob.substr(1)
|
||||
}
|
||||
|
||||
return { base, glob }
|
||||
}
|
||||
1
types/config.d.ts
vendored
1
types/config.d.ts
vendored
|
|
@ -31,6 +31,7 @@ type ContentConfig =
|
|||
| (FilePath | RawFile)[]
|
||||
| {
|
||||
files: (FilePath | RawFile)[]
|
||||
relative?: boolean
|
||||
extract?: ExtractorFn | { [extension: string]: ExtractorFn }
|
||||
transform?: TransformerFn | { [extension: string]: TransformerFn }
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue