Ensure content globs defined in @config files are relative to that file (#14314)

When you configure custom content globs inside an `@config` file, we
want to tread these globs as being relative to that config file and not
the CSS file that requires the content file. A config can be used by
multiple CSS configs.

---------

Co-authored-by: Adam Wathan <adam.wathan@gmail.com>
This commit is contained in:
Philipp Spiess 2024-09-03 16:54:08 +02:00 • committed by GitHub
parent dcfaaac8f6
commit a1d56d8e24
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 115 additions and 24 deletions

View file

@ -7,7 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
- Nothing yet!
### Fixed
- Ensure content globs defined in `@config` files are relative to that file ([#14314](https://github.com/tailwindlabs/tailwindcss/pull/14314))
## [4.0.0-alpha.21] - 2024-09-02

View file

@ -52,15 +52,24 @@ describe.each([
addVariant('hocus', ['&:focus', '&:hover'])
}
`,
'project-a/tailwind.config.js': js`
module.exports = {
content: ['../project-b/src/**/*.js'],
}
`,
'project-a/src/index.css': css`
@import 'tailwindcss/utilities';
@source '../../project-b/src/**/*.js';
@config '../tailwind.config.js';
@source '../../project-b/src/**/*.html';
@plugin '../plugin.js';
`,
'project-a/src/index.js': js`
const className = "content-['project-a/src/index.js']"
module.exports = { className }
`,
'project-b/src/index.html': html`
<div class="flex" />
`,
'project-b/src/index.js': js`
const className = "content-['project-b/src/index.js']"
module.exports = { className }
@ -74,6 +83,7 @@ describe.each([
await fs.expectFileToContain('project-a/dist/out.css', [
candidate`underline`,
candidate`flex`,
candidate`content-['project-a/src/index.js']`,
candidate`content-['project-b/src/index.js']`,
candidate`inverted:flex`,
@ -111,15 +121,24 @@ describe.each([
addVariant('hocus', ['&:focus', '&:hover'])
}
`,
'project-a/tailwind.config.js': js`
module.exports = {
content: ['../project-b/src/**/*.js'],
}
`,
'project-a/src/index.css': css`
@import 'tailwindcss/utilities';
@source '../../project-b/src/**/*.js';
@config '../tailwind.config.js';
@source '../../project-b/src/**/*.html';
@plugin '../plugin.js';
`,
'project-a/src/index.js': js`
const className = "content-['project-a/src/index.js']"
module.exports = { className }
`,
'project-b/src/index.html': html`
<div class="flex" />
`,
'project-b/src/index.js': js`
const className = "content-['project-b/src/index.js']"
module.exports = { className }
@ -133,6 +152,7 @@ describe.each([
await fs.expectFileToContain('project-a/dist/out.css', [
candidate`underline`,
candidate`flex`,
candidate`content-['project-a/src/index.js']`,
candidate`content-['project-b/src/index.js']`,
candidate`inverted:flex`,

View file

@ -39,15 +39,24 @@ test(
addVariant('hocus', ['&:focus', '&:hover'])
}
`,
'project-a/tailwind.config.js': js`
module.exports = {
content: ['../project-b/src/**/*.js'],
}
`,
'project-a/src/index.css': css`
@import 'tailwindcss/utilities';
@source '../../project-b/src/**/*.js';
@config '../tailwind.config.js';
@source '../../project-b/src/**/*.html';
@plugin '../plugin.js';
`,
'project-a/src/index.js': js`
const className = "content-['a/src/index.js']"
module.exports = { className }
`,
'project-b/src/index.html': html`
<div class="flex" />
`,
'project-b/src/index.js': js`
const className = "content-['b/src/index.js']"
module.exports = { className }
@ -61,6 +70,7 @@ test(
await fs.expectFileToContain('project-a/dist/out.css', [
candidate`underline`,
candidate`flex`,
candidate`content-['a/src/index.js']`,
candidate`content-['b/src/index.js']`,
candidate`inverted:flex`,
@ -106,15 +116,24 @@ test(
addVariant('hocus', ['&:focus', '&:hover'])
}
`,
'project-a/tailwind.config.js': js`
module.exports = {
content: ['../project-b/src/**/*.js'],
}
`,
'project-a/src/index.css': css`
@import 'tailwindcss/utilities';
@source '../../project-b/src/**/*.js';
@config '../tailwind.config.js';
@source '../../project-b/src/**/*.html';
@plugin '../plugin.js';
`,
'project-a/src/index.js': js`
const className = "content-['a/src/index.js']"
module.exports = { className }
`,
'project-b/src/index.html': html`
<div class="flex" />
`,
'project-b/src/index.js': js`
const className = "content-['b/src/index.js']"
module.exports = { className }
@ -128,6 +147,7 @@ test(
await fs.expectFileToContain('project-a/dist/out.css', [
candidate`underline`,
candidate`flex`,
candidate`content-['a/src/index.js']`,
candidate`content-['b/src/index.js']`,
candidate`inverted:flex`,
@ -173,15 +193,24 @@ test(
addVariant('hocus', ['&:focus', '&:hover'])
}
`,
'project-a/tailwind.config.js': js`
module.exports = {
content: ['../project-b/src/**/*.js'],
}
`,
'project-a/src/index.css': css`
@import 'tailwindcss/utilities';
@source '../../project-b/src/**/*.js';
@config '../tailwind.config.js';
@source '../../project-b/src/**/*.html';
@plugin '../plugin.js';
`,
'project-a/src/index.js': js`
const className = "content-['a/src/index.js']"
module.exports = { className }
`,
'project-b/src/index.html': html`
<div class="flex" />
`,
'project-b/src/index.js': js`
const className = "content-['b/src/index.js']"
module.exports = { className }
@ -195,6 +224,7 @@ test(
await fs.expectFileToContain('project-a/dist/out.css', [
candidate`underline`,
candidate`flex`,
candidate`content-['a/src/index.js']`,
candidate`content-['b/src/index.js']`,
candidate`inverted:flex`,
@ -241,15 +271,24 @@ test(
addVariant('hocus', ['&:focus', '&:hover'])
}
`,
'project-a/tailwind.config.js': js`
module.exports = {
content: ['../project-b/src/**/*.js'],
}
`,
'project-a/src/index.css': css`
@import 'tailwindcss/utilities';
@source '../../project-b/src/**/*.js';
@config '../tailwind.config.js';
@source '../../project-b/src/**/*.html';
@plugin '../plugin.js';
`,
'project-a/src/index.js': js`
const className = "content-['a/src/index.js']"
module.exports = { className }
`,
'project-b/src/index.html': html`
<div class="flex" />
`,
'project-b/src/index.js': js`
const className = "content-['b/src/index.js']"
module.exports = { className }
@ -265,6 +304,7 @@ test(
await fs.expectFileToContain('project-a/dist/out.css', [
candidate`underline`,
candidate`flex`,
candidate`content-['a/src/index.js']`,
candidate`content-['b/src/index.js']`,
candidate`inverted:flex`,

View file

@ -52,10 +52,19 @@ test(
<div class="underline m-2">Hello, world!</div>
</body>
`,
'project-a/tailwind.config.js': js`
export default {
content: ['../project-b/src/**/*.js'],
}
`,
'project-a/src/index.css': css`
@import 'tailwindcss/theme' theme(reference);
@import 'tailwindcss/utilities';
@source '../../project-b/src/**/*.js';
@config '../tailwind.config.js';
@source '../../project-b/src/**/*.html';
`,
'project-b/src/index.html': html`
<div class="flex" />
`,
'project-b/src/index.js': js`
const className = "content-['project-b/src/index.js']"
@ -72,6 +81,7 @@ test(
await fs.expectFileToContain(filename, [
candidate`underline`,
candidate`flex`,
candidate`m-2`,
candidate`content-['project-b/src/index.js']`,
])
@ -125,10 +135,19 @@ test(
<div class="font-bold ">Tailwind Labs</div>
</body>
`,
'project-a/tailwind.config.js': js`
export default {
content: ['../project-b/src/**/*.js'],
}
`,
'project-a/src/index.css': css`
@import 'tailwindcss/theme' theme(reference);
@import 'tailwindcss/utilities';
@source '../../project-b/src/**/*.js';
@config '../tailwind.config.js';
@source '../../project-b/src/**/*.html';
`,
'project-b/src/index.html': html`
<div class="flex" />
`,
'project-b/src/index.js': js`
const className = "content-['project-b/src/index.js']"
@ -147,6 +166,7 @@ test(
await retryAssertion(async () => {
let css = await fetchStyles(port, '/index.html')
expect(css).toContain(candidate`underline`)
expect(css).toContain(candidate`flex`)
expect(css).not.toContain(candidate`font-bold`)
})
@ -155,6 +175,7 @@ test(
await retryAssertion(async () => {
let css = await fetchStyles(port, '/about.html')
expect(css).toContain(candidate`underline`)
expect(css).toContain(candidate`flex`)
expect(css).toContain(candidate`font-bold`)
})

View file

@ -143,8 +143,10 @@ export async function handle(args: Result<ReturnType<typeof options>>) {
let compiler = await createCompiler(input)
let scanner = new Scanner({
detectSources: { base },
sources: compiler.globs.map((pattern) => ({
base: inputBasePath, // Globs are relative to the input.css file
sources: compiler.globs.map(({ origin, pattern }) => ({
// Ensure the glob is relative to the input CSS file or the config file
// where it is specified.
base: origin ? path.dirname(path.resolve(inputBasePath, origin)) : inputBasePath,
pattern,
})),
})
@ -212,8 +214,10 @@ export async function handle(args: Result<ReturnType<typeof options>>) {
// Re-scan the directory to get the new `candidates`
scanner = new Scanner({
detectSources: { base },
sources: compiler.globs.map((pattern) => ({
base: inputBasePath, // Globs are relative to the input.css file
sources: compiler.globs.map(({ origin, pattern }) => ({
// Ensure the glob is relative to the input CSS file or the
// config file where it is specified.
base: origin ? path.dirname(path.resolve(inputBasePath, origin)) : inputBasePath,
pattern,
})),
})

View file

@ -141,8 +141,10 @@ function tailwindcss(opts: PluginOptions = {}): AcceptedPlugin {
// Look for candidates used to generate the CSS
let scanner = new Scanner({
detectSources: { base },
sources: context.compiler.globs.map((pattern) => ({
base: inputBasePath, // Globs are relative to the input.css file
sources: context.compiler.globs.map(({ origin, pattern }) => ({
// Ensure the glob is relative to the input CSS file or the config
// file where it is specified.
base: origin ? path.dirname(path.resolve(inputBasePath, origin)) : inputBasePath,
pattern,
})),
})

View file

@ -93,8 +93,10 @@ export default function tailwindcss(): Plugin[] {
})
scanner = new Scanner({
sources: globs.map((pattern) => ({
base: inputBasePath, // Globs are relative to the input.css file
sources: globs.map(({ origin, pattern }) => ({
// Ensure the glob is relative to the input CSS file or the config file
// where it is specified.
base: origin ? path.dirname(path.resolve(inputBasePath, origin)) : inputBasePath,
pattern,
})),
})

View file

@ -14,7 +14,7 @@ test('Config files can add content', async ({ expect }) => {
loadConfig: async () => ({ content: ['./file.txt'] }),
})
expect(compiler.globs).toEqual(['./file.txt'])
expect(compiler.globs).toEqual([{ origin: './config.js', pattern: './file.txt' }])
})
test('Config files can change dark mode (media)', async ({ expect }) => {

View file

@ -1776,7 +1776,7 @@ describe('@source', () => {
@source "./foo/bar/*.ts";
`)
expect(globs).toEqual(['./foo/bar/*.ts'])
expect(globs).toEqual([{ pattern: './foo/bar/*.ts' }])
})
test('emits multiple @source files', async () => {
@ -1785,7 +1785,7 @@ describe('@source', () => {
@source "./php/secr3t/smarty.php";
`)
expect(globs).toEqual(['./foo/**/*.ts', './php/secr3t/smarty.php'])
expect(globs).toEqual([{ pattern: './foo/**/*.ts' }, { pattern: './php/secr3t/smarty.php' }])
})
})

View file

@ -54,7 +54,7 @@ async function parseCss(
let customUtilities: ((designSystem: DesignSystem) => void)[] = []
let firstThemeRule: Rule | null = null
let keyframesRules: Rule[] = []
let globs: string[] = []
let globs: { origin?: string; pattern: string }[] = []
walk(ast, (node, { parent, replaceWith }) => {
if (node.kind !== 'rule') return
@ -178,7 +178,7 @@ async function parseCss(
) {
throw new Error('`@source` paths must be quoted.')
}
globs.push(path.slice(1, -1))
globs.push({ pattern: path.slice(1, -1) })
replaceWith([])
return
}
@ -398,7 +398,7 @@ async function parseCss(
)
}
globs.push(file.pattern)
globs.push({ origin: file.base, pattern: file.pattern })
}
return {
@ -413,7 +413,7 @@ export async function compile(
css: string,
opts: CompileOptions = {},
): Promise<{
globs: string[]
globs: { origin?: string; pattern: string }[]
build(candidates: string[]): string
}> {
let { designSystem, ast, globs, pluginApi } = await parseCss(css, opts)