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",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"arg": "^5.0.1",
|
"arg": "^5.0.2",
|
||||||
"chokidar": "^3.5.3",
|
"chokidar": "^3.5.3",
|
||||||
"color-name": "^1.1.4",
|
"color-name": "^1.1.4",
|
||||||
"detective": "^5.2.0",
|
"detective": "^5.2.1",
|
||||||
"didyoumean": "^1.2.2",
|
"didyoumean": "^1.2.2",
|
||||||
"dlv": "^1.1.3",
|
"dlv": "^1.1.3",
|
||||||
"fast-glob": "^3.2.11",
|
"fast-glob": "^3.2.11",
|
||||||
|
|
@ -36,26 +36,26 @@
|
||||||
"postcss-selector-parser": "^6.0.10",
|
"postcss-selector-parser": "^6.0.10",
|
||||||
"postcss-value-parser": "^4.2.0",
|
"postcss-value-parser": "^4.2.0",
|
||||||
"quick-lru": "^5.1.1",
|
"quick-lru": "^5.1.1",
|
||||||
"resolve": "^1.22.0"
|
"resolve": "^1.22.1"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"tailwind": "lib/cli.js",
|
"tailwind": "lib/cli.js",
|
||||||
"tailwindcss": "lib/cli.js"
|
"tailwindcss": "lib/cli.js"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@parcel/css": "^1.11.2",
|
||||||
"@swc/cli": "^0.1.57",
|
"@swc/cli": "^0.1.57",
|
||||||
"@swc/core": "^1.2.160",
|
"@swc/core": "^1.2.196",
|
||||||
"@swc/jest": "^0.2.21",
|
"@swc/jest": "^0.2.21",
|
||||||
"@swc/register": "^0.1.10",
|
"@swc/register": "^0.1.10",
|
||||||
"autoprefixer": "^10.4.7",
|
"autoprefixer": "^10.4.7",
|
||||||
"cssnano": "^5.1.9",
|
"esbuild": "^0.14.48",
|
||||||
"esbuild": "^0.14.39",
|
"eslint": "^8.18.0",
|
||||||
"eslint": "^8.15.0",
|
|
||||||
"eslint-config-prettier": "^8.5.0",
|
"eslint-config-prettier": "^8.5.0",
|
||||||
"eslint-plugin-prettier": "^4.0.0",
|
"eslint-plugin-prettier": "^4.2.1",
|
||||||
"jest": "^28.0.3",
|
"jest": "^28.1.2",
|
||||||
"jest-diff": "^28.1.0",
|
"jest-diff": "^28.1.1",
|
||||||
"prettier": "^2.6.2",
|
"prettier": "^2.7.1",
|
||||||
"prettier-plugin-tailwindcss": "^0.1.11",
|
"prettier-plugin-tailwindcss": "^0.1.11",
|
||||||
"rimraf": "^3.0.0",
|
"rimraf": "^3.0.0",
|
||||||
"source-map-js": "^1.0.2"
|
"source-map-js": "^1.0.2"
|
||||||
|
|
@ -76,27 +76,27 @@
|
||||||
"tailwindcss": {
|
"tailwindcss": {
|
||||||
"version": "file:../..",
|
"version": "file:../..",
|
||||||
"requires": {
|
"requires": {
|
||||||
|
"@parcel/css": "^1.11.2",
|
||||||
"@swc/cli": "^0.1.57",
|
"@swc/cli": "^0.1.57",
|
||||||
"@swc/core": "^1.2.160",
|
"@swc/core": "^1.2.196",
|
||||||
"@swc/jest": "^0.2.21",
|
"@swc/jest": "^0.2.21",
|
||||||
"@swc/register": "^0.1.10",
|
"@swc/register": "^0.1.10",
|
||||||
"arg": "^5.0.1",
|
"arg": "^5.0.2",
|
||||||
"autoprefixer": "^10.4.7",
|
"autoprefixer": "^10.4.7",
|
||||||
"chokidar": "^3.5.3",
|
"chokidar": "^3.5.3",
|
||||||
"color-name": "^1.1.4",
|
"color-name": "^1.1.4",
|
||||||
"cssnano": "^5.1.9",
|
"detective": "^5.2.1",
|
||||||
"detective": "^5.2.0",
|
|
||||||
"didyoumean": "^1.2.2",
|
"didyoumean": "^1.2.2",
|
||||||
"dlv": "^1.1.3",
|
"dlv": "^1.1.3",
|
||||||
"esbuild": "^0.14.39",
|
"esbuild": "^0.14.48",
|
||||||
"eslint": "^8.15.0",
|
"eslint": "^8.18.0",
|
||||||
"eslint-config-prettier": "^8.5.0",
|
"eslint-config-prettier": "^8.5.0",
|
||||||
"eslint-plugin-prettier": "^4.0.0",
|
"eslint-plugin-prettier": "^4.2.1",
|
||||||
"fast-glob": "^3.2.11",
|
"fast-glob": "^3.2.11",
|
||||||
"glob-parent": "^6.0.2",
|
"glob-parent": "^6.0.2",
|
||||||
"is-glob": "^4.0.3",
|
"is-glob": "^4.0.3",
|
||||||
"jest": "^28.0.3",
|
"jest": "^28.1.2",
|
||||||
"jest-diff": "^28.1.0",
|
"jest-diff": "^28.1.1",
|
||||||
"lilconfig": "^2.0.5",
|
"lilconfig": "^2.0.5",
|
||||||
"normalize-path": "^3.0.0",
|
"normalize-path": "^3.0.0",
|
||||||
"object-hash": "^3.0.0",
|
"object-hash": "^3.0.0",
|
||||||
|
|
@ -108,10 +108,10 @@
|
||||||
"postcss-nested": "5.0.6",
|
"postcss-nested": "5.0.6",
|
||||||
"postcss-selector-parser": "^6.0.10",
|
"postcss-selector-parser": "^6.0.10",
|
||||||
"postcss-value-parser": "^4.2.0",
|
"postcss-value-parser": "^4.2.0",
|
||||||
"prettier": "^2.6.2",
|
"prettier": "^2.7.1",
|
||||||
"prettier-plugin-tailwindcss": "^0.1.11",
|
"prettier-plugin-tailwindcss": "^0.1.11",
|
||||||
"quick-lru": "^5.1.1",
|
"quick-lru": "^5.1.1",
|
||||||
"resolve": "^1.22.0",
|
"resolve": "^1.22.1",
|
||||||
"rimraf": "^3.0.0",
|
"rimraf": "^3.0.0",
|
||||||
"source-map-js": "^1.0.2"
|
"source-map-js": "^1.0.2"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -459,6 +459,7 @@ describe('Build command', () => {
|
||||||
-m, --minify Minify the output
|
-m, --minify Minify the output
|
||||||
-c, --config Path to a custom config file
|
-c, --config Path to a custom config file
|
||||||
--no-autoprefixer Disable autoprefixer
|
--no-autoprefixer Disable autoprefixer
|
||||||
|
--experimental-contextual-paths Enable experimental contextual path resolution
|
||||||
-h, --help Display usage information
|
-h, --help Display usage information
|
||||||
`)
|
`)
|
||||||
)
|
)
|
||||||
|
|
|
||||||
810
src/cli.js
810
src/cli.js
|
|
@ -1,28 +1,12 @@
|
||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
|
|
||||||
import { lazyPostcss, lazyPostcssImport, lazyCssnano, lazyAutoprefixer } from '../peers/index.js'
|
|
||||||
|
|
||||||
import chokidar from 'chokidar'
|
|
||||||
import path from 'path'
|
import path from 'path'
|
||||||
import arg from 'arg'
|
import arg from 'arg'
|
||||||
import fs from 'fs'
|
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 = {
|
import { help } from './cli/help.js'
|
||||||
DEBUG: process.env.DEBUG !== undefined && process.env.DEBUG !== '0',
|
import { init } from './cli/init.js'
|
||||||
}
|
import { build } from './cli/build.js'
|
||||||
|
|
||||||
function isESM() {
|
function isESM() {
|
||||||
const pkgPath = path.resolve('./package.json')
|
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) {
|
function oneOf(...options) {
|
||||||
return Object.assign(
|
return Object.assign(
|
||||||
(value = true) => {
|
(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 = {
|
let commands = {
|
||||||
init: {
|
init: {
|
||||||
run: init,
|
run: init,
|
||||||
|
|
@ -351,675 +220,4 @@ if (args['--help']) {
|
||||||
process.exit(0)
|
process.exit(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
run()
|
run(args, configs)
|
||||||
|
|
||||||
// ---
|
|
||||||
|
|
||||||
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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
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',
|
'hoverOnlyWhenSupported',
|
||||||
'respectDefaultRingColorOpacity',
|
'respectDefaultRingColorOpacity',
|
||||||
'disableColorOpacityUtilitiesByDefault',
|
'disableColorOpacityUtilitiesByDefault',
|
||||||
|
'relativeContentPathsByDefault',
|
||||||
|
],
|
||||||
|
experimental: [
|
||||||
|
'optimizeUniversalDefaults',
|
||||||
|
'matchVariant',
|
||||||
|
// 'variantGrouping',
|
||||||
],
|
],
|
||||||
experimental: ['optimizeUniversalDefaults', 'matchVariant' /* , 'variantGrouping' */],
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function flagEnabled(config, flag) {
|
export function flagEnabled(config, flag) {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import setupTrackingContext from './lib/setupTrackingContext'
|
import setupTrackingContext from './lib/setupTrackingContext'
|
||||||
import processTailwindFeatures from './processTailwindFeatures'
|
import processTailwindFeatures from './processTailwindFeatures'
|
||||||
import { env } from './lib/sharedState'
|
import { env } from './lib/sharedState'
|
||||||
|
import { findAtConfigPath } from './lib/findAtConfigPath'
|
||||||
|
|
||||||
module.exports = function tailwindcss(configOrPath) {
|
module.exports = function tailwindcss(configOrPath) {
|
||||||
return {
|
return {
|
||||||
|
|
@ -13,6 +14,10 @@ module.exports = function tailwindcss(configOrPath) {
|
||||||
return root
|
return root
|
||||||
},
|
},
|
||||||
function (root, result) {
|
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)
|
let context = setupTrackingContext(configOrPath)
|
||||||
|
|
||||||
if (root.type === 'document') {
|
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)
|
let context = createContext(tailwindConfig, [], root)
|
||||||
|
|
||||||
|
Object.assign(context, {
|
||||||
|
userConfigPath,
|
||||||
|
})
|
||||||
|
|
||||||
trackModified([...contextDependencies], getFileModifiedMap(context))
|
trackModified([...contextDependencies], getFileModifiedMap(context))
|
||||||
|
|
||||||
// ---
|
// ---
|
||||||
|
|
|
||||||
|
|
@ -1,22 +1,14 @@
|
||||||
import fs from 'fs'
|
import fs from 'fs'
|
||||||
import path from 'path'
|
|
||||||
|
|
||||||
import fastGlob from 'fast-glob'
|
|
||||||
import LRU from 'quick-lru'
|
import LRU from 'quick-lru'
|
||||||
import normalizePath from 'normalize-path'
|
|
||||||
|
|
||||||
import hash from '../util/hashConfig'
|
import hash from '../util/hashConfig'
|
||||||
import getModuleDependencies from '../lib/getModuleDependencies'
|
import getModuleDependencies from '../lib/getModuleDependencies'
|
||||||
|
|
||||||
import resolveConfig from '../public/resolve-config'
|
import resolveConfig from '../public/resolve-config'
|
||||||
|
|
||||||
import resolveConfigPath from '../util/resolveConfigPath'
|
import resolveConfigPath from '../util/resolveConfigPath'
|
||||||
|
|
||||||
import { env } from './sharedState'
|
|
||||||
|
|
||||||
import { getContext, getFileModifiedMap } from './setupContextUtils'
|
import { getContext, getFileModifiedMap } from './setupContextUtils'
|
||||||
import parseDependency from '../util/parseDependency'
|
import parseDependency from '../util/parseDependency'
|
||||||
import { validateConfig } from '../util/validateConfig.js'
|
import { validateConfig } from '../util/validateConfig.js'
|
||||||
|
import { parseCandidateFiles, resolvedChangedContent } from './content.js'
|
||||||
|
|
||||||
let configPathCache = new LRU({ maxSize: 100 })
|
let configPathCache = new LRU({ maxSize: 100 })
|
||||||
|
|
||||||
|
|
@ -27,9 +19,7 @@ function getCandidateFiles(context, tailwindConfig) {
|
||||||
return candidateFilesCache.get(context)
|
return candidateFilesCache.get(context)
|
||||||
}
|
}
|
||||||
|
|
||||||
let candidateFiles = tailwindConfig.content.files
|
let candidateFiles = parseCandidateFiles(context, tailwindConfig)
|
||||||
.filter((item) => typeof item === 'string')
|
|
||||||
.map((contentPath) => normalizePath(contentPath))
|
|
||||||
|
|
||||||
return candidateFilesCache.set(context, candidateFiles).get(context)
|
return candidateFilesCache.set(context, candidateFiles).get(context)
|
||||||
}
|
}
|
||||||
|
|
@ -80,36 +70,6 @@ function getTailwindConfig(configOrPath) {
|
||||||
return [newConfig, null, hash(newConfig), []]
|
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
|
// DISABLE_TOUCH = TRUE
|
||||||
|
|
||||||
// Retrieve an existing context from cache if possible (since contexts are unique per
|
// 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)
|
let fileModifiedMap = getFileModifiedMap(context)
|
||||||
|
|
||||||
// Add template paths as postcss dependencies.
|
// Add template paths as postcss dependencies.
|
||||||
for (let fileOrGlob of candidateFiles) {
|
for (let contentPath of candidateFiles) {
|
||||||
let dependency = parseDependency(fileOrGlob)
|
for (let dependency of parseDependency(contentPath)) {
|
||||||
if (dependency) {
|
|
||||||
registerDependency(dependency)
|
registerDependency(dependency)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -56,9 +56,11 @@ export function normalizeConfig(config) {
|
||||||
|
|
||||||
// When `config.content` is an object
|
// When `config.content` is an object
|
||||||
if (typeof config.content === 'object' && config.content !== null) {
|
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 (
|
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
|
return false
|
||||||
}
|
}
|
||||||
|
|
@ -112,6 +114,14 @@ export function normalizeConfig(config) {
|
||||||
) {
|
) {
|
||||||
return false
|
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
|
return true
|
||||||
|
|
@ -154,6 +164,16 @@ export function normalizeConfig(config) {
|
||||||
|
|
||||||
// Normalize the `content`
|
// Normalize the `content`
|
||||||
config.content = {
|
config.content = {
|
||||||
|
relative: (() => {
|
||||||
|
let { content } = config
|
||||||
|
|
||||||
|
if (content?.relative) {
|
||||||
|
return content.relative
|
||||||
|
}
|
||||||
|
|
||||||
|
return config.future?.relativeContentPathsByDefault ?? false
|
||||||
|
})(),
|
||||||
|
|
||||||
files: (() => {
|
files: (() => {
|
||||||
let { content, purge } = config
|
let { content, purge } = config
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,49 +1,44 @@
|
||||||
import isGlob from 'is-glob'
|
// @ts-check
|
||||||
import globParent from 'glob-parent'
|
|
||||||
import path from 'path'
|
|
||||||
|
|
||||||
// Based on `glob-base`
|
/**
|
||||||
// https://github.com/micromatch/glob-base/blob/master/index.js
|
* @typedef {{type: 'dependency', file: string} | {type: 'dir-dependency', dir: string, glob: string}} Dependency
|
||||||
function parseGlob(pattern) {
|
*/
|
||||||
let glob = pattern
|
|
||||||
let base = globParent(pattern)
|
|
||||||
|
|
||||||
if (base !== '.') {
|
/**
|
||||||
glob = pattern.substr(base.length)
|
*
|
||||||
if (glob.charAt(0) === '/') {
|
* @param {import('../lib/content.js').ContentPath} contentPath
|
||||||
glob = glob.substr(1)
|
* @returns {Dependency[]}
|
||||||
}
|
*/
|
||||||
|
export default function parseDependency(contentPath) {
|
||||||
|
if (contentPath.ignore) {
|
||||||
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
if (glob.substr(0, 2) === './') {
|
if (!contentPath.glob) {
|
||||||
glob = glob.substr(2)
|
return [
|
||||||
}
|
{
|
||||||
if (glob.charAt(0) === '/') {
|
type: 'dependency',
|
||||||
glob = glob.substr(1)
|
file: contentPath.base,
|
||||||
|
},
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
return { base, glob }
|
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
|
||||||
export default function parseDependency(normalizedFileOrGlob) {
|
return [
|
||||||
if (normalizedFileOrGlob.startsWith('!')) {
|
{
|
||||||
return null
|
type: 'dependency',
|
||||||
}
|
file: contentPath.base,
|
||||||
|
},
|
||||||
let message
|
]
|
||||||
|
}
|
||||||
if (isGlob(normalizedFileOrGlob)) {
|
|
||||||
let { base, glob } = parseGlob(normalizedFileOrGlob)
|
return [
|
||||||
message = { type: 'dir-dependency', dir: path.resolve(base), glob }
|
{
|
||||||
} else {
|
type: 'dir-dependency',
|
||||||
message = { type: 'dependency', file: path.resolve(normalizedFileOrGlob) }
|
dir: contentPath.base,
|
||||||
}
|
glob: contentPath.glob,
|
||||||
|
},
|
||||||
// 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
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
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)[]
|
| (FilePath | RawFile)[]
|
||||||
| {
|
| {
|
||||||
files: (FilePath | RawFile)[]
|
files: (FilePath | RawFile)[]
|
||||||
|
relative?: boolean
|
||||||
extract?: ExtractorFn | { [extension: string]: ExtractorFn }
|
extract?: ExtractorFn | { [extension: string]: ExtractorFn }
|
||||||
transform?: TransformerFn | { [extension: string]: TransformerFn }
|
transform?: TransformerFn | { [extension: string]: TransformerFn }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue