Ensure @source globs with symlinks are preserved (#20203)
This PR fixes an issue when working with `@source` and `@source not`
that involves symlinks.
Internally our sources are mapped to a source entry where we have a
`base` path and a `pattern`. You can think about this where we insert a
`.gitignore` file in the `base` path for the given pattern.
However, we optimize these entries to move as many "static" parts into
the base path. For example:
```css
@source "./some/folder/here/*.html";
```
Is mapped to something like:
```ts
{ base: "/projects/my-project", pattern: "./some/folder/here/*.html" }
```
We then optimize it by turning it into:
```ts
{ base: "/projects/my-project/some/folder/here", pattern: "*.html" }
```
While doing this, we also use `dunce::canonicalize` to resolve the
actual paths on disk. This means that a symlink is resolved to their
real paths. This can cause issues because the "real" path is not what
you wrote in the `@source` directives.
So before, it could be that you have this:
```css
@source "./some/symlinked-folder/here/*.html";
```
Which was mapped to this on the Rust side:
```ts
{ base: "/projects/my-project", pattern: "./some/symlinked-folder/here/*.html" }
```
But was then optimized to:
```ts
{ base: "/projects/my-project/some/actual-folder/here", pattern: "*.html" }
```
...and we lost the `symlinked-folder` information. This causes issues as
seen in #17985.
With this PR, we keep the symlinked information in those globs since
that's what you wrote in those `@source` directives.
While setting up integration tests, I stumbled upon an issue because I
wanted to test that ignoring a symlinked folder, but including a single
particular file of that ignored folder resulted in that file being
ignored as well. Let's look at an example:
```css
@source '../lib';
@source not '../lib/ignored';
@source '../lib/ignored/except.html';
```
Earlier I mentioned that we create `.gitignore` files based on these
`@source` directives. In this case, when we're dealing with a folder, we
use `**/*` as the contents.
Looking at the example above, we should essentially have something like
this:
```gitignore
# lib/.gitignore
# @source '../lib'
!**/*
# lib/ignored/.gitignore
# @source '../lib/ignored'
**/*
# @source '../lib/ignored/except.html'
!except.html
```
Since it's a `.gitignore` file, we have to invert the globs. But the bug
I noticed is that in reality the result of those gitignores didn't look
like the above, it looked like:
```gitignore
# lib/.gitignore
# @source '../lib'
!**/*
# lib/ignored/.gitignore
# @source '../lib/ignored/except.html'
!except.html
# @source '../lib/ignored'
**/*
```
Notice how the `!except.html` and `**/*` are flipped. When dealing with
`.gitignore` files, the order is important.
This was caused because internally we kept a `BTreeMap` of `BTreeSet`s
where the map was the base path and a set of patterns. The patterns were
sorted because of the `BTreeSet`... which is not what we want.
Fixes: #17985
Closes: #20091
## Test plan
1. Added new tests in the scanner tests (on the Rust side)
2. Added integration tests with a symlink to another folder, outside of
the current folder
2. Added integration tests with a symlink to another folder, inside of
the current folder
2. Added integration tests to ensure that the order of `@source` files
with a folder + file is sorted correctly.
3. Since we're dealing with symlinks in these tests, let's test all OSes
[ci-all]
This commit is contained in:
parent
ad6693906a
commit
3f58e52e36
6 changed files with 563 additions and 97 deletions
|
|
@ -1938,7 +1938,7 @@ test(
|
|||
`,
|
||||
},
|
||||
},
|
||||
async ({ fs, exec, spawn, root, expect }) => {
|
||||
async ({ fs, exec, root, expect }) => {
|
||||
await exec('pnpm tailwindcss --input ./index.css --output dist/out.css', { cwd: root })
|
||||
|
||||
let content = await fs.dumpFiles('./dist/*.css')
|
||||
|
|
@ -1949,6 +1949,201 @@ test(
|
|||
},
|
||||
)
|
||||
|
||||
test(
|
||||
'@source order is important (referencing sibling project)',
|
||||
{
|
||||
fs: {
|
||||
'package.json': json`{}`,
|
||||
'pnpm-workspace.yaml': yaml`
|
||||
#
|
||||
packages:
|
||||
- project-a
|
||||
`,
|
||||
'project-a/package.json': json`
|
||||
{
|
||||
"dependencies": {
|
||||
"tailwindcss": "workspace:^",
|
||||
"@tailwindcss/cli": "workspace:^"
|
||||
}
|
||||
}
|
||||
`,
|
||||
'project-a/src/index.css': css`
|
||||
@import 'tailwindcss/utilities' source(none);
|
||||
|
||||
@source '../../project-b';
|
||||
@source not '../../project-b/ignored';
|
||||
@source '../../project-b/ignored/except.html';
|
||||
`,
|
||||
|
||||
'project-b/keep/keep.html': html`<div class="content-['GOOD-1']"></div>`,
|
||||
'project-b/ignored/ignored.html': html`<div class="content-['BAD-1']"></div>`,
|
||||
'project-b/ignored/except.html': html`<div class="content-['GOOD-2']"></div>`,
|
||||
},
|
||||
},
|
||||
async ({ fs, root, exec, expect }) => {
|
||||
await exec('pnpm tailwindcss --input src/index.css --output dist/out.css', {
|
||||
cwd: path.join(root, 'project-a'),
|
||||
})
|
||||
|
||||
expect(await fs.dumpFiles('./project-a/dist/*.css')).toMatchInlineSnapshot(`
|
||||
"
|
||||
--- ./project-a/dist/out.css ---
|
||||
@layer properties;
|
||||
.content-\\[\\'GOOD-1\\'\\] {
|
||||
--tw-content: 'GOOD-1';
|
||||
content: var(--tw-content);
|
||||
}
|
||||
.content-\\[\\'GOOD-2\\'\\] {
|
||||
--tw-content: 'GOOD-2';
|
||||
content: var(--tw-content);
|
||||
}
|
||||
@property --tw-content {
|
||||
syntax: "*";
|
||||
inherits: false;
|
||||
initial-value: "";
|
||||
}
|
||||
@layer properties {
|
||||
@supports ((-webkit-hyphens: none) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color:rgb(from red r g b)))) {
|
||||
*, ::before, ::after, ::backdrop {
|
||||
--tw-content: "";
|
||||
}
|
||||
}
|
||||
}
|
||||
"
|
||||
`)
|
||||
},
|
||||
)
|
||||
|
||||
test(
|
||||
'@source works with symlinks (referencing sibling project)',
|
||||
{
|
||||
fs: {
|
||||
'package.json': json`{}`,
|
||||
'pnpm-workspace.yaml': yaml`
|
||||
#
|
||||
packages:
|
||||
- project-a
|
||||
`,
|
||||
'project-a/package.json': json`
|
||||
{
|
||||
"dependencies": {
|
||||
"tailwindcss": "workspace:^",
|
||||
"@tailwindcss/cli": "workspace:^"
|
||||
}
|
||||
}
|
||||
`,
|
||||
'project-a/src/index.css': css`
|
||||
@import 'tailwindcss/utilities' source(none);
|
||||
|
||||
/* Same as the previous test, but using symlinks instead */
|
||||
@source '../../project-b';
|
||||
@source not '../../project-b/ignored';
|
||||
@source '../../project-b/ignored/except.html';
|
||||
`,
|
||||
|
||||
'project-b/keep/keep.html': html`<div class="content-['GOOD-1']"></div>`,
|
||||
'project-c/ignored/ignored.html': html`<div class="content-['BAD-1']"></div>`,
|
||||
'project-c/ignored/except.html': html`<div class="content-['GOOD-2']"></div>`,
|
||||
|
||||
// Symlink the ignored folder to another project
|
||||
'project-b/ignored': 'symlink:../../project-c/ignored/',
|
||||
},
|
||||
},
|
||||
async ({ fs, root, exec, expect }) => {
|
||||
await exec('pnpm tailwindcss --input src/index.css --output dist/out.css', {
|
||||
cwd: path.join(root, 'project-a'),
|
||||
})
|
||||
|
||||
expect(await fs.dumpFiles('./project-a/dist/*.css')).toMatchInlineSnapshot(`
|
||||
"
|
||||
--- ./project-a/dist/out.css ---
|
||||
@layer properties;
|
||||
.content-\\[\\'GOOD-1\\'\\] {
|
||||
--tw-content: 'GOOD-1';
|
||||
content: var(--tw-content);
|
||||
}
|
||||
.content-\\[\\'GOOD-2\\'\\] {
|
||||
--tw-content: 'GOOD-2';
|
||||
content: var(--tw-content);
|
||||
}
|
||||
@property --tw-content {
|
||||
syntax: "*";
|
||||
inherits: false;
|
||||
initial-value: "";
|
||||
}
|
||||
@layer properties {
|
||||
@supports ((-webkit-hyphens: none) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color:rgb(from red r g b)))) {
|
||||
*, ::before, ::after, ::backdrop {
|
||||
--tw-content: "";
|
||||
}
|
||||
}
|
||||
}
|
||||
"
|
||||
`)
|
||||
},
|
||||
)
|
||||
|
||||
test(
|
||||
'@source works with symlinks (referencing folder in current folder)',
|
||||
{
|
||||
fs: {
|
||||
'package.json': json`
|
||||
{
|
||||
"dependencies": {
|
||||
"tailwindcss": "workspace:^",
|
||||
"@tailwindcss/cli": "workspace:^"
|
||||
}
|
||||
}
|
||||
`,
|
||||
'src/index.css': css`
|
||||
@import 'tailwindcss/utilities' source(none);
|
||||
|
||||
/* Same as the previous test, but using symlinks instead */
|
||||
@source '../lib';
|
||||
@source not '../lib/ignored';
|
||||
@source '../lib/ignored/except.html';
|
||||
`,
|
||||
|
||||
'lib/keep/keep.html': html`<div class="content-['GOOD-1']"></div>`,
|
||||
'vendor/ignored/ignored.html': html`<div class="content-['BAD-1']"></div>`,
|
||||
'vendor/ignored/except.html': html`<div class="content-['GOOD-2']"></div>`,
|
||||
|
||||
// Symlink the ignored folder to another folder
|
||||
'lib/ignored': 'symlink:../../vendor/ignored/',
|
||||
},
|
||||
},
|
||||
async ({ fs, exec, expect }) => {
|
||||
await exec('pnpm tailwindcss --input src/index.css --output dist/out.css')
|
||||
|
||||
expect(await fs.dumpFiles('./dist/*.css')).toMatchInlineSnapshot(`
|
||||
"
|
||||
--- ./dist/out.css ---
|
||||
@layer properties;
|
||||
.content-\\[\\'GOOD-1\\'\\] {
|
||||
--tw-content: 'GOOD-1';
|
||||
content: var(--tw-content);
|
||||
}
|
||||
.content-\\[\\'GOOD-2\\'\\] {
|
||||
--tw-content: 'GOOD-2';
|
||||
content: var(--tw-content);
|
||||
}
|
||||
@property --tw-content {
|
||||
syntax: "*";
|
||||
inherits: false;
|
||||
initial-value: "";
|
||||
}
|
||||
@layer properties {
|
||||
@supports ((-webkit-hyphens: none) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color:rgb(from red r g b)))) {
|
||||
*, ::before, ::after, ::backdrop {
|
||||
--tw-content: "";
|
||||
}
|
||||
}
|
||||
}
|
||||
"
|
||||
`)
|
||||
},
|
||||
)
|
||||
|
||||
test(
|
||||
'auto source detection disabled',
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue