Compare commits

...
Sign in to create a new pull request.

16 commits

Author SHA1 Message Date
Jordan Pittman
215e86f0f3 Add prototype using lightningcss for minification
fixup
2022-09-22 09:34:06 -04:00
Jordan Pittman
9735d8d82a cleanup 2022-09-22 09:34:06 -04:00
Jordan Pittman
78eea5bee0 wip 2022-09-22 09:34:03 -04:00
Jordan Pittman
59fc32e3ad wip 2022-09-22 09:34:01 -04:00
Jordan Pittman
20ab1b3c64 wip 2022-09-22 09:33:56 -04:00
Jordan Pittman
9ac928b92c wip 2022-09-22 09:33:52 -04:00
Jordan Pittman
a15bee463f wip 2022-09-22 09:33:49 -04:00
Jordan Pittman
1d84e5b734 wip 2022-09-22 09:33:49 -04:00
Jordan Pittman
3871cc54ea wip 2022-09-22 09:33:47 -04:00
Jordan Pittman
056d18022a Normalize and resolve content paths earlier
Not just for PostCSS dependencies
2022-09-22 09:33:39 -04:00
Jordan Pittman
383354c478 Resolve symlinks for paths 2022-09-22 09:33:39 -04:00
Jordan Pittman
91ddcedb37 Add flag to resolve content relative to the config file 2022-09-22 09:33:39 -04:00
Jordan Pittman
4bff7654f5 Refactor parseDependency 2022-09-22 09:33:39 -04:00
Jordan Pittman
04132c33ca Add option to load config file relative to the input file 2022-09-22 09:33:39 -04:00
Jordan Pittman
9dec4f28ef Tweak formatting 2022-09-22 09:33:39 -04:00
Jordan Pittman
eed0e8b92b Update lockfile 2022-09-15 13:52:33 -04:00
15 changed files with 467 additions and 148 deletions

View file

@ -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"
} }

View file

@ -96,6 +96,24 @@ describe('Build command', () => {
expect(withoutMinify.length).toBeGreaterThan(withMinify.length) expect(withoutMinify.length).toBeGreaterThan(withMinify.length)
}) })
test('--minify applies Lightning CSS optimizations', async () => {
await writeInputFile('index.html', html`<div class="font-bold"></div>`)
await writeInputFile(
'index.css',
css`
.text-teal {
color: hsl(calc(360deg / 2) 50% 50%);
}
`
)
await $(`${EXECUTABLE} --input ./src/index.css --output ./dist/main.css --minify`)
let withMinify = await readOutputFile('main.css')
expect(withMinify).toEqual(`.text-teal{color:#40bfbf}`)
})
test('--no-autoprefixer', async () => { test('--no-autoprefixer', async () => {
await writeInputFile('index.html', html`<div class="select-none"></div>`) await writeInputFile('index.html', html`<div class="select-none"></div>`)
@ -459,6 +477,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
`) `)
) )

View file

@ -15,7 +15,7 @@
"scripts": { "scripts": {
"preswcify": "npm run generate && rimraf lib", "preswcify": "npm run generate && rimraf lib",
"swcify": "swc src --out-dir lib --copy-files", "swcify": "swc src --out-dir lib --copy-files",
"postswcify": "esbuild lib/cli-peer-dependencies.js --bundle --platform=node --outfile=peers/index.js", "postswcify": "esbuild lib/cli-peer-dependencies.js --bundle --platform=node --outfile=peers/index.js --define:process.env.CSS_TRANSFORMER_WASM=false",
"rebuild-fixtures": "npm run swcify && node -r @swc/register scripts/rebuildFixtures.js", "rebuild-fixtures": "npm run swcify && node -r @swc/register scripts/rebuildFixtures.js",
"prepublishOnly": "npm install --force && npm run swcify", "prepublishOnly": "npm install --force && npm run swcify",
"style": "eslint .", "style": "eslint .",
@ -47,13 +47,13 @@
"@swc/jest": "^0.2.22", "@swc/jest": "^0.2.22",
"@swc/register": "^0.1.10", "@swc/register": "^0.1.10",
"autoprefixer": "^10.4.8", "autoprefixer": "^10.4.8",
"cssnano": "^5.1.13",
"esbuild": "^0.15.7", "esbuild": "^0.15.7",
"eslint": "^8.23.0", "eslint": "^8.23.0",
"eslint-config-prettier": "^8.5.0", "eslint-config-prettier": "^8.5.0",
"eslint-plugin-prettier": "^4.2.1", "eslint-plugin-prettier": "^4.2.1",
"jest": "^28.1.3", "jest": "^28.1.3",
"jest-diff": "^28.1.3", "jest-diff": "^28.1.3",
"lightningcss": "^1.11.2",
"prettier": "^2.7.1", "prettier": "^2.7.1",
"prettier-plugin-tailwindcss": "^0.1.13", "prettier-plugin-tailwindcss": "^0.1.13",
"rimraf": "^3.0.0", "rimraf": "^3.0.0",

View file

@ -10,6 +10,6 @@ export function lazyAutoprefixer() {
return require('autoprefixer') return require('autoprefixer')
} }
export function lazyCssnano() { export function lazyLightningCss() {
return require('cssnano') return require('lightningcss')
} }

View file

@ -1,6 +1,11 @@
#!/usr/bin/env node #!/usr/bin/env node
import { lazyPostcss, lazyPostcssImport, lazyCssnano, lazyAutoprefixer } from '../peers/index.js' import {
lazyPostcss,
lazyPostcssImport,
lazyLightningCss,
lazyAutoprefixer,
} from '../peers/index.js'
import chokidar from 'chokidar' import chokidar from 'chokidar'
import path from 'path' import path from 'path'
@ -19,6 +24,8 @@ import packageJson from '../package.json'
import normalizePath from 'normalize-path' import normalizePath from 'normalize-path'
import micromatch from 'micromatch' import micromatch from 'micromatch'
import { validateConfig } from './util/validateConfig.js' import { validateConfig } from './util/validateConfig.js'
import { parseCandidateFiles } from './lib/content.js'
import resolveConfigPath from './util/resolveConfigPath.js'
let env = { let env = {
DEBUG: process.env.DEBUG !== undefined && process.env.DEBUG !== '0', DEBUG: process.env.DEBUG !== undefined && process.env.DEBUG !== '0',
@ -219,6 +226,10 @@ let commands = {
type: Boolean, type: Boolean,
description: 'Disable autoprefixer', description: 'Disable autoprefixer',
}, },
'--experimental-contextual-paths': {
type: Boolean,
description: 'Enable experimental contextual path resolution',
},
'-c': '--config', '-c': '--config',
'-i': '--input', '-i': '--input',
'-o': '--output', '-o': '--output',
@ -353,6 +364,67 @@ if (args['--help']) {
run() run()
/// ---
/** @type {import('lightningcss')} */
let lightningCss
/** @returns {import('lightningcss')} */
function loadLightningCss() {
if (lightningCss) {
return lightningCss
}
// Try to load a local version first
try {
return (lightningCss = require('lightningcss'))
} catch {}
return (lightningCss = lazyLightningCss())
}
/**
*
* @param {boolean} shouldMinify
* @param {import('postcss').Result} result
* @returns {import('postcss').Result}
*/
async function minifyCss(shouldMinify, result) {
if (!shouldMinify) {
return result
}
let css = loadLightningCss()
let transformed
try {
transformed = css.transform({
filename: result.opts.from || 'input.css',
code: Buffer.from(result.css, 'utf-8'),
minify: true,
sourceMap: true,
sourceMap: !!result.map,
inputSourceMap: result.map ? result.map.toString() : undefined,
})
} catch (err) {
console.error('Unable to minify CSS. Using unminified version instead.')
console.error(err)
return result
}
return Object.assign(result, {
css: transformed.code.toString('utf8'),
map: result.map
? Object.assign(result.map, {
toString() {
return transformed.map.toString()
},
})
: result.map,
})
}
// --- // ---
function init() { function init() {
@ -429,6 +501,7 @@ async function build() {
process.exit(9) process.exit(9)
} }
// TODO: Reference the @config path here if exists
let configPath = args['--config'] let configPath = args['--config']
? args['--config'] ? args['--config']
: ((defaultPath) => (fs.existsSync(defaultPath) ? defaultPath : null))( : ((defaultPath) => (fs.existsSync(defaultPath) ? defaultPath : null))(
@ -551,14 +624,14 @@ async function build() {
} }
function extractFileGlobs(config) { function extractFileGlobs(config) {
return config.content.files let context = {
.filter((file) => { tailwindConfig: config,
// Strings in this case are files / globs. If it is something else, userConfigPath: configPath,
// 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' let contentPaths = parseCandidateFiles(context, config)
})
.map((glob) => normalizePath(glob)) return contentPaths.map((contentPath) => contentPath.pattern)
} }
function extractRawContent(config) { function extractRawContent(config) {
@ -626,17 +699,6 @@ async function build() {
return lazyAutoprefixer() 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) ].filter(Boolean)
let postcss = loadPostcss() let postcss = loadPostcss()
@ -647,6 +709,7 @@ async function build() {
return Promise.resolve() return Promise.resolve()
.then(() => (output ? fs.promises.mkdir(path.dirname(output), { recursive: true }) : null)) .then(() => (output ? fs.promises.mkdir(path.dirname(output), { recursive: true }) : null))
.then(() => processor.process(css, { ...postcssOptions, from: input, to: output })) .then(() => processor.process(css, { ...postcssOptions, from: input, to: output }))
.then((result) => minifyCss(!!args['--minify'], result))
.then((result) => { .then((result) => {
if (!output) { if (!output) {
return process.stdout.write(result.css) return process.stdout.write(result.css)
@ -728,17 +791,6 @@ async function build() {
return lazyAutoprefixer() 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) ].filter(Boolean)
async function rebuild(config) { async function rebuild(config) {
@ -785,6 +837,7 @@ async function build() {
output ? fs.promises.mkdir(path.dirname(output), { recursive: true }) : null output ? fs.promises.mkdir(path.dirname(output), { recursive: true }) : null
) )
.then(() => processor.process(css, { from: input, to: output })) .then(() => processor.process(css, { from: input, to: output }))
.then((result) => minifyCss(!!args['--minify'], result))
.then(async (result) => { .then(async (result) => {
for (let message of result.messages) { for (let message of result.messages) {
if (message.type === 'dependency') { if (message.type === 'dependency') {
@ -845,6 +898,7 @@ async function build() {
* @return {{all: string[], dynamic: string[], static: string[]}} * @return {{all: string[], dynamic: string[], static: string[]}}
**/ **/
function refreshContentPatterns(config) { function refreshContentPatterns(config) {
// TODO: This can be optimized since we're using `fastGlob.generateTasks` indirectly in extractFileGlobs
let globs = extractFileGlobs(config) let globs = extractFileGlobs(config)
let tasks = fastGlob.generateTasks(globs, { absolute: true }) let tasks = fastGlob.generateTasks(globs, { absolute: true })
let dynamicPatterns = tasks.filter((task) => task.dynamic).flatMap((task) => task.patterns) let dynamicPatterns = tasks.filter((task) => task.dynamic).flatMap((task) => task.patterns)

View file

@ -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) {

View file

@ -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
View 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
}

View file

@ -0,0 +1,33 @@
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.')
}
configPath = rule.params.replace(/^['"]|['"]$/g, '')
rule.remove()
if (path.isAbsolute(configPath)) {
throw rule.error('The `@config` at-rule cannot be used with an absolute path.')
}
})
return configPath ? path.resolve(path.dirname(relativeTo), configPath) : null
}

View file

@ -997,6 +997,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))
// --- // ---

View file

@ -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)
} }
} }

View file

@ -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

View file

@ -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
View 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
View file

@ -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 }
} }