Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
59 changes: 59 additions & 0 deletions integrations/cli/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
{
Expand Down
70 changes: 70 additions & 0 deletions integrations/postcss/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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']`)
},
)
80 changes: 80 additions & 0 deletions integrations/vite/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
17 changes: 9 additions & 8 deletions packages/@tailwindcss-node/src/compile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = /[*{]/
Expand All @@ -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
}

Expand Down