mirror of
https://github.com/tailwindlabs/tailwindcss.git
synced 2025-12-08 21:36:08 +00:00
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:
parent
e9c9c4f79d
commit
4363426384
@ -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))
|
||||
|
||||
@ -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',
|
||||
{
|
||||
|
||||
@ -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']`)
|
||||
},
|
||||
)
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user