Fix validation of source(…) paths (#19274)

Fixes #18833

- [x] Needs tests

Basically we were correctly resolving the path given to `source()`
inside Oxide *but* inside `@tailwindcss/node` when we validated that the
path was a directory we were not.

We incorrectly used the base path of the input file rather than the file
the `source(…)` directive was defined in. This PR fixes that.
This commit is contained in:
Jordan Pittman 2025-11-06 16:24:17 -05:00 committed by GitHub
parent e9c9c4f79d
commit 4363426384
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 222 additions and 8 deletions

View File

@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
### Fixed
- Ensure validation of `source(…)` happens relative to the file it is in ([#19274](https://github.com/tailwindlabs/tailwindcss/pull/19274))
### Added
- _Experimental_: Add `@container-size` utility ([#18901](https://github.com/tailwindlabs/tailwindcss/pull/18901))

View File

@ -1332,6 +1332,65 @@ test(
},
)
test(
'source(…) and `@source` are relative to the file they are in',
{
fs: {
'package.json': json`
{
"dependencies": {
"tailwindcss": "workspace:^",
"@tailwindcss/cli": "workspace:^"
}
}
`,
'index.css': css` @import './project-a/src/index.css'; `,
'project-a/src/index.css': css`
/* Run auto-content detection in ../../project-b */
@import 'tailwindcss/utilities' source('../../project-b');
/* Explicitly using node_modules in the @source allows git ignored folders */
@source '../../project-c';
`,
// Project A is the current folder, but we explicitly configured
// `source(project-b)`, therefore project-a should not be included in
// the output.
'project-a/src/index.html': html`
<div
class="content-['SHOULD-NOT-EXIST-IN-OUTPUT'] content-['project-a/src/index.html']"
></div>
`,
// Project B is the configured `source(…)`, therefore auto source
// detection should include known extensions and folders in the output.
'project-b/src/index.html': html`
<div
class="content-['project-b/src/index.html']"
></div>
`,
// Project C should apply auto source detection, therefore known
// extensions and folders should be included in the output.
'project-c/src/index.html': html`
<div
class="content-['project-c/src/index.html']"
></div>
`,
},
},
async ({ fs, exec, spawn, root, expect }) => {
await exec('pnpm tailwindcss --input ./index.css --output dist/out.css', { cwd: root })
let content = await fs.dumpFiles('./dist/*.css')
expect(content).not.toContain(candidate`content-['project-a/src/index.html']`)
expect(content).toContain(candidate`content-['project-b/src/index.html']`)
expect(content).toContain(candidate`content-['project-c/src/index.html']`)
},
)
test(
'auto source detection disabled',
{

View File

@ -825,3 +825,73 @@ test(
})
},
)
test(
'source(…) and `@source` are relative to the file they are in',
{
fs: {
'package.json': json`
{
"dependencies": {
"postcss": "^8",
"postcss-cli": "^10",
"tailwindcss": "workspace:^",
"@tailwindcss/postcss": "workspace:^"
}
}
`,
'postcss.config.js': js`
module.exports = {
plugins: {
'@tailwindcss/postcss': {},
},
}
`,
'index.css': css` @import './project-a/src/index.css'; `,
'project-a/src/index.css': css`
/* Run auto-content detection in ../../project-b */
@import 'tailwindcss/utilities' source('../../project-b');
/* Explicitly using node_modules in the @source allows git ignored folders */
@source '../../project-c';
`,
// Project A is the current folder, but we explicitly configured
// `source(project-b)`, therefore project-a should not be included in
// the output.
'project-a/src/index.html': html`
<div
class="content-['SHOULD-NOT-EXIST-IN-OUTPUT'] content-['project-a/src/index.html']"
></div>
`,
// Project B is the configured `source(…)`, therefore auto source
// detection should include known extensions and folders in the output.
'project-b/src/index.html': html`
<div
class="content-['project-b/src/index.html']"
></div>
`,
// Project C should apply auto source detection, therefore known
// extensions and folders should be included in the output.
'project-c/src/index.html': html`
<div
class="content-['project-c/src/index.html']"
></div>
`,
},
},
async ({ fs, exec, root, expect }) => {
await exec('pnpm postcss ./index.css --output dist/out.css', { cwd: root })
let content = await fs.dumpFiles('./dist/*.css')
expect(content).not.toContain(candidate`content-['project-a/src/index.html']`)
expect(content).toContain(candidate`content-['project-b/src/index.html']`)
expect(content).toContain(candidate`content-['project-c/src/index.html']`)
},
)

View File

@ -730,6 +730,86 @@ describe.each(['postcss', 'lightningcss'])('%s', (transformer) => {
expect(files).toHaveLength(0)
},
)
test(
'source(…) and `@source` are relative to the file they are in',
{
fs: {
'package.json': json`
{
"type": "module",
"dependencies": {
"@tailwindcss/vite": "workspace:^",
"tailwindcss": "workspace:^"
},
"devDependencies": {
${transformer === 'lightningcss' ? `"lightningcss": "^1",` : ''}
"vite": "^7"
}
}
`,
'vite.config.ts': ts`
import tailwindcss from '@tailwindcss/vite'
import { defineConfig } from 'vite'
export default defineConfig({
css: ${transformer === 'postcss' ? '{}' : "{ transformer: 'lightningcss' }"},
build: { cssMinify: false },
plugins: [tailwindcss()],
})
`,
'index.html': html`
<head>
<link rel="stylesheet" href="/index.css" />
</head>
<body></body>
`,
'index.css': css` @import './project-a/src/index.css'; `,
'project-a/src/index.css': css`
/* Run auto-content detection in ../../project-b */
@import 'tailwindcss/utilities' source('../../project-b');
/* Explicitly using node_modules in the @source allows git ignored folders */
@source '../../project-c';
`,
// Project A is the current folder, but we explicitly configured
// `source(project-b)`, therefore project-a should not be included in
// the output.
'project-a/src/index.html': html`
<div
class="content-['SHOULD-NOT-EXIST-IN-OUTPUT'] content-['project-a/src/index.html']"
></div>
`,
// Project B is the configured `source(…)`, therefore auto source
// detection should include known extensions and folders in the output.
'project-b/src/index.html': html`
<div
class="content-['project-b/src/index.html']"
></div>
`,
// Project C should apply auto source detection, therefore known
// extensions and folders should be included in the output.
'project-c/src/index.html': html`
<div
class="content-['project-c/src/index.html']"
></div>
`,
},
},
async ({ fs, exec, spawn, root, expect }) => {
await exec('pnpm vite build', { cwd: root })
let content = await fs.dumpFiles('./dist/assets/*.css')
expect(content).not.toContain(candidate`content-['project-a/src/index.html']`)
expect(content).toContain(candidate`content-['project-b/src/index.html']`)
expect(content).toContain(candidate`content-['project-c/src/index.html']`)
},
)
})
test(

View File

@ -63,10 +63,9 @@ function createCompileOptions({
}
}
async function ensureSourceDetectionRootExists(
compiler: { root: Awaited<ReturnType<typeof compile>>['root'] },
base: string,
) {
async function ensureSourceDetectionRootExists(compiler: {
root: Awaited<ReturnType<typeof compile>>['root']
}) {
// Verify if the `source(…)` path exists (until the glob pattern starts)
if (compiler.root && compiler.root !== 'none') {
let globSymbols = /[*{]/
@ -80,25 +79,27 @@ async function ensureSourceDetectionRootExists(
}
let exists = await fsPromises
.stat(path.resolve(base, basePath.join('/')))
.stat(path.resolve(compiler.root.base, basePath.join('/')))
.then((stat) => stat.isDirectory())
.catch(() => false)
if (!exists) {
throw new Error(`The \`source(${compiler.root.pattern})\` does not exist`)
throw new Error(
`The \`source(${compiler.root.pattern})\` does not exist or is not a directory.`,
)
}
}
}
export async function compileAst(ast: AstNode[], options: CompileOptions) {
let compiler = await _compileAst(ast, createCompileOptions(options))
await ensureSourceDetectionRootExists(compiler, options.base)
await ensureSourceDetectionRootExists(compiler)
return compiler
}
export async function compile(css: string, options: CompileOptions) {
let compiler = await _compile(css, createCompileOptions(options))
await ensureSourceDetectionRootExists(compiler, options.base)
await ensureSourceDetectionRootExists(compiler)
return compiler
}