This PR fixes an issue introduced with the changed candidate cache behavior in #14187. Prior to #14187, candidates were cached globally within an instance of Oxide. This meant that once a candidate was discovered, it would not reset until you either manually cleared the cache or restarted the Oxide process. With the changes in #14187 however, the cache was scoped to the instance of the `Scanner` class with the intention of making the caching behavior more easy to understand and to avoid a global cache. This, however, had an unforeseen side-effect in our Vite extension. Vite, in dev mode, discovers files _lazily_. So when a developer goes to `/index.html` the first time, we will scan the `/index.html` file for Tailwind candidates and then build a CSS file with those candidate. When they go to `/about.html` later, we will _append_ the candidates from the new file and so forth. The problem now arises when the dev server detects changes to the input CSS file. This requires us to do a re-scan of that CSS file which, after #14187, caused the candidate cache to be gone. This is usually fine since we would just scan files again for the changed candidate list but in the Vite case we would only get the input CSS file change _but no subsequent change events for all other files, including those currently rendered in the browser_). This caused updates to the CSS file to remove all candidates from the CSS file again. Ideally, we can separate between two concepts: The candidate cache and the CSS input file scan. An instance of the `Scanner` could re-parse the input CSS file without having to throw away previous candidates. This, however, would have another issue with the current Vite extension where we do not properly retain instances of the `Scanner` class anyways. To properly improve the cache behavior, we will have to fix the Vite `Scanner` retaining behavior first. Unfortunately this means that for the short term, we have to add some manual bookkeeping to the Vite client and retain the candidate cache between builds ourselves. --------- Co-authored-by: Jordan Pittman <jordan@cryptica.me>
217 lines
5.9 KiB
TypeScript
217 lines
5.9 KiB
TypeScript
import path from 'node:path'
|
|
import { expect } from 'vitest'
|
|
import {
|
|
candidate,
|
|
css,
|
|
fetchStyles,
|
|
html,
|
|
js,
|
|
json,
|
|
retryAssertion,
|
|
test,
|
|
ts,
|
|
yaml,
|
|
} from '../utils'
|
|
|
|
test(
|
|
'production build',
|
|
{
|
|
fs: {
|
|
'package.json': json`{}`,
|
|
'pnpm-workspace.yaml': yaml`
|
|
#
|
|
packages:
|
|
- project-a
|
|
`,
|
|
'project-a/package.json': json`
|
|
{
|
|
"type": "module",
|
|
"dependencies": {
|
|
"@tailwindcss/vite": "workspace:^",
|
|
"tailwindcss": "workspace:^"
|
|
},
|
|
"devDependencies": {
|
|
"vite": "^5.3.5"
|
|
}
|
|
}
|
|
`,
|
|
'project-a/vite.config.ts': ts`
|
|
import tailwindcss from '@tailwindcss/vite'
|
|
import { defineConfig } from 'vite'
|
|
|
|
export default defineConfig({
|
|
build: { cssMinify: false },
|
|
plugins: [tailwindcss()],
|
|
})
|
|
`,
|
|
'project-a/index.html': html`
|
|
<head>
|
|
<link rel="stylesheet" href="./src/index.css" />
|
|
</head>
|
|
<body>
|
|
<div class="underline m-2">Hello, world!</div>
|
|
</body>
|
|
`,
|
|
'project-a/src/index.css': css`
|
|
@import 'tailwindcss/theme' theme(reference);
|
|
@import 'tailwindcss/utilities';
|
|
@source '../../project-b/src/**/*.js';
|
|
`,
|
|
'project-b/src/index.js': js`
|
|
const className = "content-['project-b/src/index.js']"
|
|
module.exports = { className }
|
|
`,
|
|
},
|
|
},
|
|
async ({ root, fs, exec }) => {
|
|
await exec('pnpm vite build', { cwd: path.join(root, 'project-a') })
|
|
|
|
let files = await fs.glob('project-a/dist/**/*.css')
|
|
expect(files).toHaveLength(1)
|
|
let [filename] = files[0]
|
|
|
|
await fs.expectFileToContain(filename, [
|
|
candidate`underline`,
|
|
candidate`m-2`,
|
|
candidate`content-['project-b/src/index.js']`,
|
|
])
|
|
},
|
|
)
|
|
|
|
test(
|
|
'dev mode',
|
|
{
|
|
fs: {
|
|
'package.json': json`{}`,
|
|
'pnpm-workspace.yaml': yaml`
|
|
#
|
|
packages:
|
|
- project-a
|
|
`,
|
|
'project-a/package.json': json`
|
|
{
|
|
"type": "module",
|
|
"dependencies": {
|
|
"@tailwindcss/vite": "workspace:^",
|
|
"tailwindcss": "workspace:^"
|
|
},
|
|
"devDependencies": {
|
|
"vite": "^5.3.5"
|
|
}
|
|
}
|
|
`,
|
|
'project-a/vite.config.ts': ts`
|
|
import tailwindcss from '@tailwindcss/vite'
|
|
import { defineConfig } from 'vite'
|
|
|
|
export default defineConfig({
|
|
build: { cssMinify: false },
|
|
plugins: [tailwindcss()],
|
|
})
|
|
`,
|
|
'project-a/index.html': html`
|
|
<head>
|
|
<link rel="stylesheet" href="./src/index.css" />
|
|
</head>
|
|
<body>
|
|
<div class="underline">Hello, world!</div>
|
|
</body>
|
|
`,
|
|
'project-a/about.html': html`
|
|
<head>
|
|
<link rel="stylesheet" href="./src/index.css" />
|
|
</head>
|
|
<body>
|
|
<div class="font-bold ">Tailwind Labs</div>
|
|
</body>
|
|
`,
|
|
'project-a/src/index.css': css`
|
|
@import 'tailwindcss/theme' theme(reference);
|
|
@import 'tailwindcss/utilities';
|
|
@source '../../project-b/src/**/*.js';
|
|
`,
|
|
'project-b/src/index.js': js`
|
|
const className = "content-['project-b/src/index.js']"
|
|
module.exports = { className }
|
|
`,
|
|
},
|
|
},
|
|
async ({ root, spawn, getFreePort, fs }) => {
|
|
let port = await getFreePort()
|
|
await spawn(`pnpm vite dev --port ${port}`, {
|
|
cwd: path.join(root, 'project-a'),
|
|
})
|
|
|
|
// Candidates are resolved lazily, so the first visit of index.html
|
|
// will only have candidates from this file.
|
|
await retryAssertion(async () => {
|
|
let css = await fetchStyles(port, '/index.html')
|
|
expect(css).toContain(candidate`underline`)
|
|
expect(css).not.toContain(candidate`font-bold`)
|
|
})
|
|
|
|
// Going to about.html will extend the candidate list to include
|
|
// candidates from about.html.
|
|
await retryAssertion(async () => {
|
|
let css = await fetchStyles(port, '/about.html')
|
|
expect(css).toContain(candidate`underline`)
|
|
expect(css).toContain(candidate`font-bold`)
|
|
})
|
|
|
|
// Updates are additive and cause new candidates to be added.
|
|
await fs.write(
|
|
'project-a/index.html',
|
|
html`
|
|
<head>
|
|
<link rel="stylesheet" href="./src/index.css" />
|
|
</head>
|
|
<body>
|
|
<div class="underline m-2">Hello, world!</div>
|
|
</body>
|
|
`,
|
|
)
|
|
await retryAssertion(async () => {
|
|
let css = await fetchStyles(port)
|
|
expect(css).toContain(candidate`underline`)
|
|
expect(css).toContain(candidate`font-bold`)
|
|
expect(css).toContain(candidate`m-2`)
|
|
})
|
|
|
|
// Manually added `@source`s are watched and trigger a rebuild
|
|
await fs.write(
|
|
'project-b/src/index.js',
|
|
js`
|
|
const className = "[.changed_&]:content-['project-b/src/index.js']"
|
|
module.exports = { className }
|
|
`,
|
|
)
|
|
await retryAssertion(async () => {
|
|
let css = await fetchStyles(port)
|
|
expect(css).toContain(candidate`underline`)
|
|
expect(css).toContain(candidate`font-bold`)
|
|
expect(css).toContain(candidate`m-2`)
|
|
expect(css).toContain(candidate`[.changed_&]:content-['project-b/src/index.js']`)
|
|
})
|
|
|
|
// After updates to the CSS file, all previous candidates should still be in
|
|
// the generated CSS
|
|
await fs.write(
|
|
'project-a/src/index.css',
|
|
css`
|
|
${await fs.read('project-a/src/index.css')}
|
|
|
|
.red {
|
|
color: red;
|
|
}
|
|
`,
|
|
)
|
|
await retryAssertion(async () => {
|
|
let css = await fetchStyles(port)
|
|
expect(css).toContain(candidate`red`)
|
|
expect(css).toContain(candidate`m-2`)
|
|
expect(css).toContain(candidate`underline`)
|
|
expect(css).toContain(candidate`[.changed_&]:content-['project-b/src/index.js']`)
|
|
expect(css).toContain(candidate`font-bold`)
|
|
})
|
|
},
|
|
)
|