diff --git a/src/featureFlags.js b/src/featureFlags.js index bd75108ea..a03916c5b 100644 --- a/src/featureFlags.js +++ b/src/featureFlags.js @@ -10,6 +10,7 @@ let featureFlags = { 'hoverOnlyWhenSupported', 'respectDefaultRingColorOpacity', 'disableColorOpacityUtilitiesByDefault', + 'relativeContentPathsByDefault', ], experimental: [ 'optimizeUniversalDefaults', diff --git a/src/index.js b/src/index.js index 9e9e291eb..7dc41c317 100644 --- a/src/index.js +++ b/src/index.js @@ -1,6 +1,7 @@ import setupTrackingContext from './lib/setupTrackingContext' import processTailwindFeatures from './processTailwindFeatures' import { env } from './lib/sharedState' +import { findAtConfigPath } from './lib/findAtConfigPath' module.exports = function tailwindcss(configOrPath) { return { @@ -13,6 +14,10 @@ module.exports = function tailwindcss(configOrPath) { return root }, function (root, result) { + // Use the path for the `@config` directive if it exists, otherwise use the + // path for the file being processed + configOrPath = findAtConfigPath(root, result) ?? configOrPath + let context = setupTrackingContext(configOrPath) if (root.type === 'document') { diff --git a/src/lib/content.js b/src/lib/content.js index f0ceefa9f..96d03d345 100644 --- a/src/lib/content.js +++ b/src/lib/content.js @@ -116,7 +116,7 @@ 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.postcssOptions?.experimental?.contextualPaths) { + if (context.userConfigPath && context.tailwindConfig.content.relative) { resolveFrom = [path.dirname(context.userConfigPath)] } diff --git a/src/lib/findAtConfigPath.js b/src/lib/findAtConfigPath.js new file mode 100644 index 000000000..067e8e312 --- /dev/null +++ b/src/lib/findAtConfigPath.js @@ -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(/^['"]|['"]$/, '') + + 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 +} diff --git a/src/lib/setupContextUtils.js b/src/lib/setupContextUtils.js index ddaaa481c..d15e8db8c 100644 --- a/src/lib/setupContextUtils.js +++ b/src/lib/setupContextUtils.js @@ -940,8 +940,7 @@ export function getContext( tailwindConfig, userConfigPath, tailwindConfigHash, - contextDependencies, - postcssOptions + contextDependencies ) { let sourcePath = result.opts.from let isConfigFile = userConfigPath !== null @@ -1003,7 +1002,6 @@ export function getContext( Object.assign(context, { userConfigPath, - postcssOptions, }) trackModified([...contextDependencies], getFileModifiedMap(context)) diff --git a/src/lib/setupTrackingContext.js b/src/lib/setupTrackingContext.js index eae375e98..8c68e9f18 100644 --- a/src/lib/setupTrackingContext.js +++ b/src/lib/setupTrackingContext.js @@ -27,7 +27,6 @@ function getCandidateFiles(context, tailwindConfig) { // Get the config object based on a path function getTailwindConfig(configOrPath, inputPath) { let userConfigPath = resolveConfigPath(configOrPath, inputPath) - let postcssOptions = typeof configOrPath === 'object' && configOrPath !== null ? configOrPath : {} if (userConfigPath !== null) { let [prevConfig, prevConfigHash, prevDeps, prevModified] = @@ -47,7 +46,7 @@ function getTailwindConfig(configOrPath, inputPath) { // It hasn't changed (based on timestamps) if (!modified) { - return [prevConfig, userConfigPath, prevConfigHash, prevDeps, postcssOptions] + return [prevConfig, userConfigPath, prevConfigHash, prevDeps] } // It has changed (based on timestamps), or first run @@ -58,7 +57,7 @@ function getTailwindConfig(configOrPath, inputPath) { newConfig = validateConfig(newConfig) let newHash = hash(newConfig) configPathCache.set(userConfigPath, [newConfig, newHash, newDeps, newModified]) - return [newConfig, userConfigPath, newHash, newDeps, postcssOptions] + return [newConfig, userConfigPath, newHash, newDeps] } // It's a plain object, not a path @@ -68,7 +67,7 @@ function getTailwindConfig(configOrPath, inputPath) { newConfig = validateConfig(newConfig) - return [newConfig, null, hash(newConfig), [], postcssOptions] + return [newConfig, null, hash(newConfig), []] } // DISABLE_TOUCH = TRUE @@ -79,7 +78,7 @@ function getTailwindConfig(configOrPath, inputPath) { export default function setupTrackingContext(configOrPath) { return ({ tailwindDirectives, registerDependency }) => { return (root, result) => { - let [tailwindConfig, userConfigPath, tailwindConfigHash, configDependencies, postcssOptions] = + let [tailwindConfig, userConfigPath, tailwindConfigHash, configDependencies] = getTailwindConfig(configOrPath, result.opts.from) let contextDependencies = new Set(configDependencies) @@ -108,8 +107,7 @@ export default function setupTrackingContext(configOrPath) { tailwindConfig, userConfigPath, tailwindConfigHash, - contextDependencies, - postcssOptions + contextDependencies ) let candidateFiles = getCandidateFiles(context, tailwindConfig) diff --git a/src/util/normalizeConfig.js b/src/util/normalizeConfig.js index 2d23a864d..70ff792fb 100644 --- a/src/util/normalizeConfig.js +++ b/src/util/normalizeConfig.js @@ -56,9 +56,11 @@ export function normalizeConfig(config) { // When `config.content` is an object if (typeof config.content === 'object' && config.content !== null) { - // Only `files`, `extract` and `transform` can exist in `config.content` + // Only `files`, `relative`, `extract`, and `transform` can exist in `config.content` if ( - Object.keys(config.content).some((key) => !['files', 'extract', 'transform'].includes(key)) + Object.keys(config.content).some( + (key) => !['files', 'extract', 'transform', 'relative'].includes(key) + ) ) { return false } @@ -112,6 +114,14 @@ export function normalizeConfig(config) { ) { return false } + + // `config.content.relative` is optional and can be a boolean + if ( + typeof config.content.relative !== 'boolean' && + typeof config.content.relative !== 'undefined' + ) { + return false + } } return true @@ -154,6 +164,16 @@ export function normalizeConfig(config) { // Normalize the `content` config.content = { + relative: (() => { + let { content } = config + + if (content?.relative) { + return content.relative + } + + return config.future?.relativeContentPathsByDefault ?? false + })(), + files: (() => { let { content, purge } = config diff --git a/src/util/resolveConfigPath.js b/src/util/resolveConfigPath.js index c2b8e862b..24fda7b5e 100644 --- a/src/util/resolveConfigPath.js +++ b/src/util/resolveConfigPath.js @@ -55,9 +55,9 @@ export default function resolveConfigPath(pathOrConfig, inputPath) { pathOrConfig.config !== undefined && isString(pathOrConfig.config) ) { - if (pathOrConfig.experimental?.contextualPaths) { - return pickResolvedPath(pathOrConfig.config, inputPath) - } + // if (pathOrConfig.experimental?.contextualPaths) { + // return pickResolvedPath(pathOrConfig.config, inputPath) + // } return path.resolve(pathOrConfig.config) } diff --git a/types/config.d.ts b/types/config.d.ts index 855b9bb02..61cd9546f 100644 --- a/types/config.d.ts +++ b/types/config.d.ts @@ -31,6 +31,7 @@ type ContentConfig = | (FilePath | RawFile)[] | { files: (FilePath | RawFile)[] + relative?: boolean extract?: ExtractorFn | { [extension: string]: ExtractorFn } transform?: TransformerFn | { [extension: string]: TransformerFn } }