Invalidate context when main css changes (#7626)

* Invalidate context when CSS changes

* Remove invalidation count check

* Add sass integration test

* Update changelog
This commit is contained in:
Jordan Pittman 2022-02-25 13:12:45 -05:00 committed by GitHub
parent d9bc25da6a
commit bd167635d5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 3173 additions and 11 deletions

View File

@ -12,7 +12,7 @@ jobs:
strategy:
matrix:
integration: [parcel, postcss-cli, rollup, tailwindcss-cli, vite, webpack-4, webpack-5]
integration: [parcel, postcss-cli, rollup, rollup-sass, tailwindcss-cli, vite, webpack-4, webpack-5]
node-version: [16]
steps:

View File

@ -7,17 +7,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
### Fixed
- Prevent nesting plugin from breaking other plugins ([#7563](https://github.com/tailwindlabs/tailwindcss/pull/7563))
- Recursively collapse adjacent rules ([#7565](https://github.com/tailwindlabs/tailwindcss/pull/7565))
- Allow default ring color to be a function ([#7587](https://github.com/tailwindlabs/tailwindcss/pull/7587))
- Preserve source maps for generated CSS ([#7588](https://github.com/tailwindlabs/tailwindcss/pull/7588))
- Split box shadows on top-level commas only ([#7479](https://github.com/tailwindlabs/tailwindcss/pull/7479))
- Use local user CSS cache for `@apply` ([#7524](https://github.com/tailwindlabs/tailwindcss/pull/7524))
- Invalidate context when main CSS changes ([#7626](https://github.com/tailwindlabs/tailwindcss/pull/7626))
### Changed
- Replace `chalk` with `picocolors` ([#6039](https://github.com/tailwindlabs/tailwindcss/pull/6039))
### Added
- Allow default ring color to be a function ([#7587](https://github.com/tailwindlabs/tailwindcss/pull/7587))
## [3.0.23] - 2022-02-16
### Fixed

4
integrations/rollup-sass/.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
dist/
node_modules/
!tailwind.config.js
!index.html

2688
integrations/rollup-sass/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,19 @@
{
"name": "rollup.js",
"private": true,
"version": "0.0.0",
"scripts": {
"build": "rollup -c",
"test": "jest --runInBand --forceExit"
},
"jest": {
"testTimeout": 10000,
"displayName": "rollup.js",
"setupFilesAfterEnv": ["<rootDir>/../../jest/customMatchers.js"]
},
"devDependencies": {
"rollup": "^2.48.0",
"rollup-plugin-postcss": "^4.0.2",
"sass": "^1.49.9"
}
}

View File

@ -0,0 +1,5 @@
let path = require('path')
module.exports = {
plugins: [require(path.resolve('..', '..'))],
}

View File

@ -0,0 +1,14 @@
import postcss from 'rollup-plugin-postcss'
export default {
input: './src/index.js',
output: {
file: './dist/index.js',
format: 'cjs',
},
plugins: [
postcss({
extract: true,
}),
],
}

View File

@ -0,0 +1 @@
// Stub

View File

@ -0,0 +1,9 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body></body>
</html>

View File

@ -0,0 +1 @@
import './index.scss'

View File

@ -0,0 +1,4 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@import "./imported";

View File

@ -0,0 +1,10 @@
module.exports = {
content: ['./src/index.html'],
theme: {
extend: {},
},
corePlugins: {
preflight: false,
},
plugins: [],
}

View File

@ -0,0 +1,303 @@
let $ = require('../../execute')
let { css, html, javascript } = require('../../syntax')
let { readOutputFile, appendToInputFile, writeInputFile } = require('../../io')({
output: 'dist',
input: 'src',
})
function ready(message) {
return message.includes('created')
}
describe('static build', () => {
it('should be possible to generate tailwind output', async () => {
await writeInputFile('index.html', html`<div class="font-bold"></div>`)
await $('rollup -c', {
env: { NODE_ENV: 'production' },
})
expect(await readOutputFile('index.css')).toIncludeCss(
css`
.font-bold {
font-weight: 700;
}
`
)
})
})
describe('watcher', () => {
test(`classes are generated when the html file changes`, async () => {
await writeInputFile('index.html', html`<div class="font-bold"></div>`)
let runningProcess = $('rollup -c --watch')
await runningProcess.onStderr(ready)
expect(await readOutputFile('index.css')).toIncludeCss(
css`
.font-bold {
font-weight: 700;
}
`
)
await appendToInputFile('index.html', html`<div class="font-normal"></div>`)
await runningProcess.onStderr(ready)
expect(await readOutputFile('index.css')).toIncludeCss(
css`
.font-bold {
font-weight: 700;
}
.font-normal {
font-weight: 400;
}
`
)
await appendToInputFile('index.html', html`<div class="bg-red-500"></div>`)
await runningProcess.onStderr(ready)
expect(await readOutputFile('index.css')).toIncludeCss(
css`
.bg-red-500 {
--tw-bg-opacity: 1;
background-color: rgb(239 68 68 / var(--tw-bg-opacity));
}
.font-bold {
font-weight: 700;
}
.font-normal {
font-weight: 400;
}
`
)
return runningProcess.stop()
})
test(`classes are generated when the tailwind.config.js file changes`, async () => {
await writeInputFile('index.html', html`<div class="font-bold md:font-medium"></div>`)
let runningProcess = $('rollup -c --watch')
await runningProcess.onStderr(ready)
expect(await readOutputFile('index.css')).toIncludeCss(
css`
.font-bold {
font-weight: 700;
}
@media (min-width: 768px) {
.md\:font-medium {
font-weight: 500;
}
}
`
)
await writeInputFile(
'../tailwind.config.js',
javascript`
module.exports = {
content: ['./src/index.html'],
theme: {
extend: {
screens: {
md: '800px'
},
fontWeight: {
bold: 'bold'
}
},
},
corePlugins: {
preflight: false,
},
plugins: [],
}
`
)
await runningProcess.onStderr(ready)
expect(await readOutputFile('index.css')).toIncludeCss(
css`
.font-bold {
font-weight: bold;
}
@media (min-width: 800px) {
.md\:font-medium {
font-weight: 500;
}
}
`
)
return runningProcess.stop()
})
test(`classes are generated when the index.css file changes`, async () => {
await writeInputFile('index.html', html`<div class="btn font-bold"></div>`)
let runningProcess = $('rollup -c --watch')
await runningProcess.onStderr(ready)
expect(await readOutputFile('index.css')).toIncludeCss(
css`
.font-bold {
font-weight: 700;
}
`
)
await writeInputFile(
'index.scss',
css`
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer components {
.btn {
@apply rounded px-2 py-1;
}
}
`
)
await runningProcess.onStderr(ready)
expect(await readOutputFile('index.css')).toIncludeCss(
css`
.btn {
border-radius: 0.25rem;
padding-left: 0.5rem;
padding-right: 0.5rem;
padding-top: 0.25rem;
padding-bottom: 0.25rem;
}
.font-bold {
font-weight: 700;
}
`
)
await writeInputFile(
'index.scss',
css`
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer components {
.btn {
@apply rounded bg-red-500 px-2 py-1;
}
}
`
)
await runningProcess.onStderr(ready)
expect(await readOutputFile('index.css')).toIncludeCss(
css`
.btn {
border-radius: 0.25rem;
--tw-bg-opacity: 1;
background-color: rgb(239 68 68 / var(--tw-bg-opacity));
padding-left: 0.5rem;
padding-right: 0.5rem;
padding-top: 0.25rem;
padding-bottom: 0.25rem;
}
.font-bold {
font-weight: 700;
}
`
)
return runningProcess.stop()
})
test(`classes are generated when the imported.scss file changes`, async () => {
await writeInputFile('index.html', html`<div class="btn font-bold"></div>`)
let runningProcess = $('rollup -c --watch')
await runningProcess.onStderr(ready)
expect(await readOutputFile('index.css')).toIncludeCss(
css`
.font-bold {
font-weight: 700;
}
`
)
await writeInputFile(
'index.scss',
css`
@tailwind base;
@tailwind components;
@tailwind utilities;
@import './imported';
`
)
await writeInputFile(
'imported.scss',
css`
@layer components {
.btn {
@apply rounded px-2 py-1;
}
}
`
)
await runningProcess.onStderr(ready)
expect(await readOutputFile('index.css')).toIncludeCss(
css`
.btn {
border-radius: 0.25rem;
padding-left: 0.5rem;
padding-right: 0.5rem;
padding-top: 0.25rem;
padding-bottom: 0.25rem;
}
.font-bold {
font-weight: 700;
}
`
)
await writeInputFile(
'imported.scss',
css`
@layer components {
.btn {
@apply rounded bg-red-500 px-2 py-1;
}
}
`
)
await runningProcess.onStderr(ready)
expect(await readOutputFile('index.css')).toIncludeCss(
css`
.btn {
border-radius: 0.25rem;
--tw-bg-opacity: 1;
background-color: rgb(239 68 68 / var(--tw-bg-opacity));
padding-left: 0.5rem;
padding-right: 0.5rem;
padding-top: 0.25rem;
padding-bottom: 0.25rem;
}
.font-bold {
font-weight: 700;
}
`
)
return runningProcess.stop()
})
})

View File

@ -0,0 +1,52 @@
import crypto from 'crypto'
import * as sharedState from './sharedState'
/**
* Calculate the hash of a string.
*
* This doesn't need to be cryptographically secure or
* anything like that since it's used only to detect
* when the CSS changes to invalidate the context.
*
* This is wrapped in a try/catch because it's really dependent
* on how Node itself is build and the environment and OpenSSL
* version / build that is installed on the user's machine.
*
* Based on the environment this can just outright fail.
*
* See https://github.com/nodejs/node/issues/40455
*
* @param {string} str
*/
function getHash(str) {
try {
return crypto.createHash('md5').update(str, 'utf-8').digest('binary')
} catch (err) {
return ''
}
}
/**
* Determine if the CSS tree is different from the
* previous version for the given `sourcePath`.
*
* @param {string} sourcePath
* @param {import('postcss').Node} root
*/
export function hasContentChanged(sourcePath, root) {
let css = root.toString()
// We only care about files with @tailwind directives
// Other files use an existing context
if (!css.includes('@tailwind')) {
return false
}
let existingHash = sharedState.sourceHashMap.get(sourcePath)
let rootHash = getHash(css)
let didChange = existingHash !== rootHash
sharedState.sourceHashMap.set(sourcePath, rootHash)
return didChange
}

View File

@ -20,6 +20,7 @@ import log from '../util/log'
import negateValue from '../util/negateValue'
import isValidArbitraryValue from '../util/isValidArbitraryValue'
import { generateRules } from './generateRules'
import { hasContentChanged } from './cacheInvalidation.js'
function prefix(context, selector) {
let prefix = context.tailwindConfig.prefix
@ -822,6 +823,8 @@ export function getContext(
existingContext = context
}
let cssDidChange = hasContentChanged(sourcePath, root)
// If there's already a context in the cache and we don't need to
// reset the context, return the cached context.
if (existingContext) {
@ -829,7 +832,7 @@ export function getContext(
[...contextDependencies],
getFileModifiedMap(existingContext)
)
if (!contextDependenciesChanged) {
if (!contextDependenciesChanged && !cssDidChange) {
return [existingContext, false]
}
}

View File

@ -5,6 +5,7 @@ export const env = {
export const contextMap = new Map()
export const configContextMap = new Map()
export const contextSourcesMap = new Map()
export const sourceHashMap = new Map()
export const NOT_ON_DEMAND = new String('*')
export function resolveDebug(debug) {

View File

@ -7,5 +7,7 @@
<title>Title</title>
<link rel="stylesheet" href="./tailwind.css" />
</head>
<body></body>
<body>
<div class="only:custom-utility"></div>
</body>
</html>

View File

@ -7,7 +7,9 @@ const configPath = path.resolve(__dirname, './context-reuse.tailwind.config.js')
const { css } = require('./util/run.js')
function run(input, config = {}, from = null) {
from = from || path.resolve(__filename)
let { currentTestName } = expect.getState()
from = `${path.resolve(__filename)}?test=${currentTestName}&${from}`
return postcss(tailwind(config)).process(input, { from })
}
@ -26,16 +28,14 @@ afterEach(async () => {
})
it('re-uses the context across multiple files with the same config', async () => {
let from = path.resolve(__filename)
let results = [
await run(`@tailwind utilities;`, configPath, `${from}?id=1`),
await run(`@tailwind utilities;`, configPath, `id=1`),
// Using @apply directives should still re-use the context
// They depend on the config but do not the other way around
await run(`body { @apply bg-blue-400; }`, configPath, `${from}?id=2`),
await run(`body { @apply text-red-400; }`, configPath, `${from}?id=3`),
await run(`body { @apply mb-4; }`, configPath, `${from}?id=4`),
await run(`body { @apply bg-blue-400; }`, configPath, `id=2`),
await run(`body { @apply text-red-400; }`, configPath, `id=3`),
await run(`body { @apply mb-4; }`, configPath, `id=4`),
]
let dependencies = results.map((result) => {
@ -85,3 +85,43 @@ it('re-uses the context across multiple files with the same config', async () =>
// And none of this should have resulted in multiple contexts being created
expect(sharedState.contextSourcesMap.size).toBe(1)
})
it('updates layers when any CSS containing @tailwind directives changes', async () => {
let result
// Compile the initial version once
let input = css`
@tailwind utilities;
@layer utilities {
.custom-utility {
color: orange;
}
}
`
result = await run(input, configPath, `id=1`)
expect(result.css).toMatchFormattedCss(css`
.only\:custom-utility:only-child {
color: orange;
}
`)
// Save the file with a change
input = css`
@tailwind utilities;
@layer utilities {
.custom-utility {
color: blue;
}
}
`
result = await run(input, configPath, `id=1`)
expect(result.css).toMatchFormattedCss(css`
.only\:custom-utility:only-child {
color: blue;
}
`)
})