Skip to content

Commit 4363426

Browse files
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.
1 parent e9c9c4f commit 4363426

File tree

5 files changed

+222
-8
lines changed

5 files changed

+222
-8
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
### Fixed
11+
12+
- Ensure validation of `source(…)` happens relative to the file it is in ([#19274](https://github.com/tailwindlabs/tailwindcss/pull/19274))
13+
1014
### Added
1115

1216
- _Experimental_: Add `@container-size` utility ([#18901](https://github.com/tailwindlabs/tailwindcss/pull/18901))

integrations/cli/index.test.ts

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1332,6 +1332,65 @@ test(
13321332
},
13331333
)
13341334

1335+
test(
1336+
'source(…) and `@source` are relative to the file they are in',
1337+
{
1338+
fs: {
1339+
'package.json': json`
1340+
{
1341+
"dependencies": {
1342+
"tailwindcss": "workspace:^",
1343+
"@tailwindcss/cli": "workspace:^"
1344+
}
1345+
}
1346+
`,
1347+
'index.css': css` @import './project-a/src/index.css'; `,
1348+
1349+
'project-a/src/index.css': css`
1350+
/* Run auto-content detection in ../../project-b */
1351+
@import 'tailwindcss/utilities' source('../../project-b');
1352+
1353+
/* Explicitly using node_modules in the @source allows git ignored folders */
1354+
@source '../../project-c';
1355+
`,
1356+
1357+
// Project A is the current folder, but we explicitly configured
1358+
// `source(project-b)`, therefore project-a should not be included in
1359+
// the output.
1360+
'project-a/src/index.html': html`
1361+
<div
1362+
class="content-['SHOULD-NOT-EXIST-IN-OUTPUT'] content-['project-a/src/index.html']"
1363+
></div>
1364+
`,
1365+
1366+
// Project B is the configured `source(…)`, therefore auto source
1367+
// detection should include known extensions and folders in the output.
1368+
'project-b/src/index.html': html`
1369+
<div
1370+
class="content-['project-b/src/index.html']"
1371+
></div>
1372+
`,
1373+
1374+
// Project C should apply auto source detection, therefore known
1375+
// extensions and folders should be included in the output.
1376+
'project-c/src/index.html': html`
1377+
<div
1378+
class="content-['project-c/src/index.html']"
1379+
></div>
1380+
`,
1381+
},
1382+
},
1383+
async ({ fs, exec, spawn, root, expect }) => {
1384+
await exec('pnpm tailwindcss --input ./index.css --output dist/out.css', { cwd: root })
1385+
1386+
let content = await fs.dumpFiles('./dist/*.css')
1387+
1388+
expect(content).not.toContain(candidate`content-['project-a/src/index.html']`)
1389+
expect(content).toContain(candidate`content-['project-b/src/index.html']`)
1390+
expect(content).toContain(candidate`content-['project-c/src/index.html']`)
1391+
},
1392+
)
1393+
13351394
test(
13361395
'auto source detection disabled',
13371396
{

integrations/postcss/index.test.ts

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -825,3 +825,73 @@ test(
825825
})
826826
},
827827
)
828+
829+
test(
830+
'source(…) and `@source` are relative to the file they are in',
831+
{
832+
fs: {
833+
'package.json': json`
834+
{
835+
"dependencies": {
836+
"postcss": "^8",
837+
"postcss-cli": "^10",
838+
"tailwindcss": "workspace:^",
839+
"@tailwindcss/postcss": "workspace:^"
840+
}
841+
}
842+
`,
843+
844+
'postcss.config.js': js`
845+
module.exports = {
846+
plugins: {
847+
'@tailwindcss/postcss': {},
848+
},
849+
}
850+
`,
851+
852+
'index.css': css` @import './project-a/src/index.css'; `,
853+
854+
'project-a/src/index.css': css`
855+
/* Run auto-content detection in ../../project-b */
856+
@import 'tailwindcss/utilities' source('../../project-b');
857+
858+
/* Explicitly using node_modules in the @source allows git ignored folders */
859+
@source '../../project-c';
860+
`,
861+
862+
// Project A is the current folder, but we explicitly configured
863+
// `source(project-b)`, therefore project-a should not be included in
864+
// the output.
865+
'project-a/src/index.html': html`
866+
<div
867+
class="content-['SHOULD-NOT-EXIST-IN-OUTPUT'] content-['project-a/src/index.html']"
868+
></div>
869+
`,
870+
871+
// Project B is the configured `source(…)`, therefore auto source
872+
// detection should include known extensions and folders in the output.
873+
'project-b/src/index.html': html`
874+
<div
875+
class="content-['project-b/src/index.html']"
876+
></div>
877+
`,
878+
879+
// Project C should apply auto source detection, therefore known
880+
// extensions and folders should be included in the output.
881+
'project-c/src/index.html': html`
882+
<div
883+
class="content-['project-c/src/index.html']"
884+
></div>
885+
`,
886+
},
887+
},
888+
async ({ fs, exec, root, expect }) => {
889+
await exec('pnpm postcss ./index.css --output dist/out.css', { cwd: root })
890+
891+
let content = await fs.dumpFiles('./dist/*.css')
892+
893+
expect(content).not.toContain(candidate`content-['project-a/src/index.html']`)
894+
expect(content).toContain(candidate`content-['project-b/src/index.html']`)
895+
expect(content).toContain(candidate`content-['project-c/src/index.html']`)
896+
},
897+
)

integrations/vite/index.test.ts

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -730,6 +730,86 @@ describe.each(['postcss', 'lightningcss'])('%s', (transformer) => {
730730
expect(files).toHaveLength(0)
731731
},
732732
)
733+
734+
test(
735+
'source(…) and `@source` are relative to the file they are in',
736+
{
737+
fs: {
738+
'package.json': json`
739+
{
740+
"type": "module",
741+
"dependencies": {
742+
"@tailwindcss/vite": "workspace:^",
743+
"tailwindcss": "workspace:^"
744+
},
745+
"devDependencies": {
746+
${transformer === 'lightningcss' ? `"lightningcss": "^1",` : ''}
747+
"vite": "^7"
748+
}
749+
}
750+
`,
751+
'vite.config.ts': ts`
752+
import tailwindcss from '@tailwindcss/vite'
753+
import { defineConfig } from 'vite'
754+
755+
export default defineConfig({
756+
css: ${transformer === 'postcss' ? '{}' : "{ transformer: 'lightningcss' }"},
757+
build: { cssMinify: false },
758+
plugins: [tailwindcss()],
759+
})
760+
`,
761+
'index.html': html`
762+
<head>
763+
<link rel="stylesheet" href="/index.css" />
764+
</head>
765+
<body></body>
766+
`,
767+
'index.css': css` @import './project-a/src/index.css'; `,
768+
769+
'project-a/src/index.css': css`
770+
/* Run auto-content detection in ../../project-b */
771+
@import 'tailwindcss/utilities' source('../../project-b');
772+
773+
/* Explicitly using node_modules in the @source allows git ignored folders */
774+
@source '../../project-c';
775+
`,
776+
777+
// Project A is the current folder, but we explicitly configured
778+
// `source(project-b)`, therefore project-a should not be included in
779+
// the output.
780+
'project-a/src/index.html': html`
781+
<div
782+
class="content-['SHOULD-NOT-EXIST-IN-OUTPUT'] content-['project-a/src/index.html']"
783+
></div>
784+
`,
785+
786+
// Project B is the configured `source(…)`, therefore auto source
787+
// detection should include known extensions and folders in the output.
788+
'project-b/src/index.html': html`
789+
<div
790+
class="content-['project-b/src/index.html']"
791+
></div>
792+
`,
793+
794+
// Project C should apply auto source detection, therefore known
795+
// extensions and folders should be included in the output.
796+
'project-c/src/index.html': html`
797+
<div
798+
class="content-['project-c/src/index.html']"
799+
></div>
800+
`,
801+
},
802+
},
803+
async ({ fs, exec, spawn, root, expect }) => {
804+
await exec('pnpm vite build', { cwd: root })
805+
806+
let content = await fs.dumpFiles('./dist/assets/*.css')
807+
808+
expect(content).not.toContain(candidate`content-['project-a/src/index.html']`)
809+
expect(content).toContain(candidate`content-['project-b/src/index.html']`)
810+
expect(content).toContain(candidate`content-['project-c/src/index.html']`)
811+
},
812+
)
733813
})
734814

735815
test(

packages/@tailwindcss-node/src/compile.ts

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -63,10 +63,9 @@ function createCompileOptions({
6363
}
6464
}
6565

66-
async function ensureSourceDetectionRootExists(
67-
compiler: { root: Awaited<ReturnType<typeof compile>>['root'] },
68-
base: string,
69-
) {
66+
async function ensureSourceDetectionRootExists(compiler: {
67+
root: Awaited<ReturnType<typeof compile>>['root']
68+
}) {
7069
// Verify if the `source(…)` path exists (until the glob pattern starts)
7170
if (compiler.root && compiler.root !== 'none') {
7271
let globSymbols = /[*{]/
@@ -80,25 +79,27 @@ async function ensureSourceDetectionRootExists(
8079
}
8180

8281
let exists = await fsPromises
83-
.stat(path.resolve(base, basePath.join('/')))
82+
.stat(path.resolve(compiler.root.base, basePath.join('/')))
8483
.then((stat) => stat.isDirectory())
8584
.catch(() => false)
8685

8786
if (!exists) {
88-
throw new Error(`The \`source(${compiler.root.pattern})\` does not exist`)
87+
throw new Error(
88+
`The \`source(${compiler.root.pattern})\` does not exist or is not a directory.`,
89+
)
8990
}
9091
}
9192
}
9293

9394
export async function compileAst(ast: AstNode[], options: CompileOptions) {
9495
let compiler = await _compileAst(ast, createCompileOptions(options))
95-
await ensureSourceDetectionRootExists(compiler, options.base)
96+
await ensureSourceDetectionRootExists(compiler)
9697
return compiler
9798
}
9899

99100
export async function compile(css: string, options: CompileOptions) {
100101
let compiler = await _compile(css, createCompileOptions(options))
101-
await ensureSourceDetectionRootExists(compiler, options.base)
102+
await ensureSourceDetectionRootExists(compiler)
102103
return compiler
103104
}
104105

0 commit comments

Comments
 (0)