Merge 4c2efc936f into fa81d697fe
This commit is contained in:
commit
da324ed205
3 changed files with 166 additions and 12 deletions
|
|
@ -693,13 +693,17 @@ test(
|
|||
`,
|
||||
)
|
||||
|
||||
// 2.5 Write to a content file
|
||||
await fs.write('src/index.html', html`
|
||||
<div class="flex underline"></div>
|
||||
`)
|
||||
|
||||
await process.onStderr((message) => message.includes('"./does-not-exist" is not exported'))
|
||||
|
||||
// The recovered build above still needs to finish registering its dependencies
|
||||
// (including logging "Waiting for file changes...") before it's safe to write
|
||||
// again -- writing sooner races a second, concurrent build against the one still
|
||||
// settling, which is a separate, pre-existing sharp edge of this plugin's caching
|
||||
// model, unrelated to error recovery itself. Flush first so this wait can't match
|
||||
// a stale occurrence of that same message from an earlier, already-settled build.
|
||||
process.flush()
|
||||
await process.onStderr((message) => message.includes('Waiting for file changes...'))
|
||||
|
||||
expect(await fs.dumpFiles('dist/*.css')).toMatchInlineSnapshot(`
|
||||
"
|
||||
--- dist/out.css ---
|
||||
|
|
@ -709,6 +713,16 @@ test(
|
|||
"
|
||||
`)
|
||||
|
||||
// 2.5 Write to a content file
|
||||
await fs.write('src/index.html', html`
|
||||
<div class="flex underline"></div>
|
||||
`)
|
||||
|
||||
await process.onStderr((message) => message.includes('"./does-not-exist" is not exported'))
|
||||
|
||||
process.flush()
|
||||
await process.onStderr((message) => message.includes('Waiting for file changes...'))
|
||||
|
||||
// 3. Fix the CSS file
|
||||
await fs.write(
|
||||
'src/tailwind.css',
|
||||
|
|
|
|||
|
|
@ -516,3 +516,83 @@ test('does not register the input file as a dependency, even if it is passed in
|
|||
plugin: expect.any(String),
|
||||
})
|
||||
})
|
||||
|
||||
describe('error recovery', () => {
|
||||
let dir: string
|
||||
beforeEach(async () => {
|
||||
dir = await mkdtemp(path.join(tmpdir(), 'tw-postcss'))
|
||||
await writeFile(path.join(dir, 'index.html'), `<div class="underline"></div>`)
|
||||
await writeFile(path.join(dir, 'index.css'), css` @import './dependency.css'; `)
|
||||
await writeFile(path.join(dir, 'dependency.css'), css` @tailwind utilities; `)
|
||||
})
|
||||
afterEach(() => rm(dir, { recursive: true, force: true }))
|
||||
|
||||
test('dependency messages are still emitted when a build fails outside of `optimize` mode, so watchers do not lose track of the CSS import graph', async () => {
|
||||
let from = path.join(dir, 'index.css')
|
||||
let processor = postcss([tailwindcss({ base: dir, optimize: false })])
|
||||
|
||||
let dependencyFile = path.join(dir, 'dependency.css')
|
||||
|
||||
// 1. A successful build establishes `dependency.css` as a known dependency.
|
||||
let ok = await processor.process(await readFile(from, 'utf8'), { from })
|
||||
expect(ok.css).toContain('.underline')
|
||||
expect(ok.messages).toContainEqual({
|
||||
type: 'dependency',
|
||||
plugin: expect.any(String),
|
||||
file: expect.stringMatching(/dependency\.css$/),
|
||||
parent: expect.stringMatching(/index\.css$/),
|
||||
})
|
||||
|
||||
// Ensure the mtime below is actually observed as a change.
|
||||
await new Promise((resolve) => setTimeout(resolve, 10))
|
||||
|
||||
// 2. Introduce a compile error (an unknown utility class) into that same file.
|
||||
await writeFile(
|
||||
dependencyFile,
|
||||
css`
|
||||
@tailwind utilities;
|
||||
.broken {
|
||||
@apply this-class-does-not-exist;
|
||||
}
|
||||
`,
|
||||
)
|
||||
|
||||
// The build must not reject here. Tools like `postcss-loader` (used by webpack)
|
||||
// only read `result.messages` -- and therefore only register file dependencies --
|
||||
// from a *resolved* result. If this rejects instead, every dependency collected
|
||||
// above is invisible to the consumer, and the entire CSS import graph stops being
|
||||
// watched until some other, still-watched file happens to change.
|
||||
let failed = await processor.process(await readFile(from, 'utf8'), { from })
|
||||
|
||||
expect(failed.warnings().length).toBeGreaterThan(0)
|
||||
expect(failed.messages).toContainEqual({
|
||||
type: 'dependency',
|
||||
plugin: expect.any(String),
|
||||
file: expect.stringMatching(/dependency\.css$/),
|
||||
parent: expect.stringMatching(/index\.css$/),
|
||||
})
|
||||
})
|
||||
|
||||
test('still fails the build when `optimize` is enabled, so broken CSS is never shipped', async () => {
|
||||
let from = path.join(dir, 'index.css')
|
||||
let processor = postcss([tailwindcss({ base: dir, optimize: true })])
|
||||
|
||||
await processor.process(await readFile(from, 'utf8'), { from })
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 10))
|
||||
|
||||
await writeFile(
|
||||
path.join(dir, 'dependency.css'),
|
||||
css`
|
||||
@tailwind utilities;
|
||||
.broken {
|
||||
@apply this-class-does-not-exist;
|
||||
}
|
||||
`,
|
||||
)
|
||||
|
||||
await expect(processor.process(await readFile(from, 'utf8'), { from })).rejects.toThrow(
|
||||
/this-class-does-not-exist/,
|
||||
)
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -348,7 +348,14 @@ function tailwindcss(opts: PluginOptions = {}): AcceptedPlugin {
|
|||
context.compiler = null
|
||||
|
||||
// Ensure all dependencies we have collected thus far are included so that the rebuild
|
||||
// is correctly triggered
|
||||
// is correctly triggered. This may run before we ever got to register the CSS import
|
||||
// graph (`context.fullRebuildPaths`) and/or the candidate-scanning dependencies
|
||||
// (`context.scanner`) for *this* build, e.g. when the failure happens while resolving
|
||||
// `@import`/`@reference` targets, before scanning even starts. Re-report whatever we
|
||||
// still know from the last successful build so watchers don't lose track of files that
|
||||
// haven't changed.
|
||||
let resolvedInputFile = path.resolve(base, inputFile)
|
||||
|
||||
for (let file of context.fullRebuildPaths) {
|
||||
result.messages.push({
|
||||
type: 'dependency',
|
||||
|
|
@ -358,15 +365,68 @@ function tailwindcss(opts: PluginOptions = {}): AcceptedPlugin {
|
|||
})
|
||||
}
|
||||
|
||||
// We found that throwing the error will cause PostCSS to no longer watch for changes
|
||||
// in some situations so we instead log the error and continue with an empty stylesheet.
|
||||
console.error(error)
|
||||
if (context.scanner) {
|
||||
for (let file of context.scanner.files) {
|
||||
let absolutePath = path.resolve(file)
|
||||
if (absolutePath === resolvedInputFile) continue
|
||||
result.messages.push({
|
||||
type: 'dependency',
|
||||
plugin: '@tailwindcss/postcss',
|
||||
file: absolutePath,
|
||||
parent: result.opts.from,
|
||||
})
|
||||
}
|
||||
|
||||
if (error && typeof error === 'object' && 'message' in error) {
|
||||
throw root.error(`${error.message}`)
|
||||
for (let { base: globBase, pattern } of context.scanner.globs) {
|
||||
if (pattern === '*' && base === globBase) continue
|
||||
|
||||
if (pattern === '') {
|
||||
result.messages.push({
|
||||
type: 'dependency',
|
||||
plugin: '@tailwindcss/postcss',
|
||||
file: path.resolve(globBase),
|
||||
parent: result.opts.from,
|
||||
})
|
||||
} else {
|
||||
result.messages.push({
|
||||
type: 'dir-dependency',
|
||||
plugin: '@tailwindcss/postcss',
|
||||
dir: path.resolve(globBase),
|
||||
glob: pattern,
|
||||
parent: result.opts.from,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw root.error(`${error}`)
|
||||
console.error(error)
|
||||
|
||||
let message =
|
||||
error && typeof error === 'object' && 'message' in error ? `${error.message}` : `${error}`
|
||||
|
||||
// In optimized (typically production) builds we want compilation errors to fail
|
||||
// the build outright so broken CSS never ships.
|
||||
//
|
||||
// Otherwise, we avoid throwing here. Throwing causes PostCSS's `process()` promise
|
||||
// to reject instead of resolve. Tools that rely on `result.messages` to register
|
||||
// file dependencies (e.g. `postcss-loader`/webpack) only ever read `result.messages`
|
||||
// from a *resolved* result, so on rejection none of the dependency messages above —
|
||||
// or from any previously successful build — are seen. That drops every file in the
|
||||
// CSS import graph from the watcher, not just the one that failed, and nothing
|
||||
// recompiles again until some other, still-watched file happens to change.
|
||||
//
|
||||
// We instead report the error as a warning so `result` still resolves and dependency
|
||||
// tracking keeps working. We keep serving the last known-good output (if any) rather
|
||||
// than clearing the stylesheet, so a broken edit doesn't strip all styling while it's
|
||||
// being fixed.
|
||||
if (optimize) {
|
||||
throw root.error(message)
|
||||
}
|
||||
|
||||
result.warn(message, { plugin: '@tailwindcss/postcss' })
|
||||
root.removeAll()
|
||||
root.append(context.cachedPostCssAst.clone().nodes)
|
||||
root.raws.indent = ' '
|
||||
}
|
||||
},
|
||||
},
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue