mirror of
https://github.com/aphrody-labs/tailwindcss.git
synced 2026-10-14 13:49:01 +00:00
Release loader callbacks after compilation (#20531)
## Summary Cached compilers retain the module and stylesheet loader callbacks after compilation. Callbacks supplied by build tools can capture completed build state, keeping it alive during later builds. Capture the `from` and `polyfills` values used by subsequent builds and isolate config and plugin loading from the compatibility theme resolver's scope. This allows the loader callbacks and their captured state to be collected after compilation. ## Test plan The regression keeps the compiler alive across garbage collection, verifies that both loader callbacks can be collected, and confirms that a subsequent utility build still works. - `corepack pnpm test:integrations postcss/compiler-lifetime.test.ts` (Node.js 24.19.0) - `corepack pnpm run test` (Node.js 24.19.0) - `corepack pnpm build` (Node.js 22.23.3) The complete integration suite remains unverified locally because several framework fixtures require filesystem and network capabilities unavailable in the sandbox. Lint stopped on generated files and embedded templates; the scoped type check reports the same diagnostics on the unchanged base. --------- Co-authored-by: Robin Malfait <robin.malfait@shopify.com>
This commit is contained in:
1 parent
fa81d697fe
commit
de2255ba3f
4 files changed
+92
-37
No files matched your search
@@ -36,6 +36,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
- Sort `min-*`, `max-*`, and container query variants with decimal values numerically (e.g. `min-[40.25rem]` before `min-[40.5rem]`) ([#20512](https://github.com/tailwindlabs/tailwindcss/pull/20512))
|
||||
- Ensure CSS comments ending with `\*/` are closed correctly instead of swallowing the CSS that follows (e.g. `/* C:\temp\*/`) ([#20508](https://github.com/tailwindlabs/tailwindcss/pull/20508))
|
||||
- Improve style invalidation performance of `group-*` and `peer-*` variants ([#20513](https://github.com/tailwindlabs/tailwindcss/pull/20513))
|
||||
- Ensure cached compilers don't keep `loadModule` and `loadStylesheet` callbacks alive after compilation ([#20531](https://github.com/tailwindlabs/tailwindcss/pull/20531))
|
||||
|
||||
## [4.3.3] - 2026-07-16
|
||||
|
||||
|
||||
@@ -18,6 +18,25 @@ import { registerThemeVariantOverrides } from './theme-variants'
|
||||
|
||||
const IS_VALID_PREFIX = /^[a-z]+$/
|
||||
|
||||
type LoadModule = (
|
||||
path: string,
|
||||
base: string,
|
||||
resourceHint: 'plugin' | 'config',
|
||||
) => Promise<{
|
||||
path: string
|
||||
base: string
|
||||
module: any
|
||||
}>
|
||||
|
||||
type ModulePath = {
|
||||
id: string
|
||||
base: string
|
||||
reference: boolean
|
||||
src: SourceLocation | undefined
|
||||
}
|
||||
|
||||
type PluginPath = [ModulePath, CssPluginOptions | null]
|
||||
|
||||
export async function applyCompatibilityHooks({
|
||||
designSystem,
|
||||
base,
|
||||
@@ -28,28 +47,12 @@ export async function applyCompatibilityHooks({
|
||||
designSystem: DesignSystem
|
||||
base: string
|
||||
ast: AstNode[]
|
||||
loadModule: (
|
||||
path: string,
|
||||
base: string,
|
||||
resourceHint: 'plugin' | 'config',
|
||||
) => Promise<{
|
||||
path: string
|
||||
base: string
|
||||
module: any
|
||||
}>
|
||||
loadModule: LoadModule
|
||||
sources: { base: string; pattern: string; negated: boolean }[]
|
||||
}) {
|
||||
let features = Features.None
|
||||
let pluginPaths: [
|
||||
{ id: string; base: string; reference: boolean; src: SourceLocation | undefined },
|
||||
CssPluginOptions | null,
|
||||
][] = []
|
||||
let configPaths: {
|
||||
id: string
|
||||
base: string
|
||||
reference: boolean
|
||||
src: SourceLocation | undefined
|
||||
}[] = []
|
||||
let pluginPaths: PluginPath[] = []
|
||||
let configPaths: ModulePath[] = []
|
||||
|
||||
walk(ast, (node, _ctx) => {
|
||||
if (node.kind !== 'at-rule') return
|
||||
@@ -174,7 +177,27 @@ export async function applyCompatibilityHooks({
|
||||
// any additional backwards compatibility hooks.
|
||||
if (!pluginPaths.length && !configPaths.length) return Features.None
|
||||
|
||||
let [configs, pluginDetails] = await Promise.all([
|
||||
// Keep loader callbacks out of the scope retained by the theme resolver.
|
||||
let [configs, pluginDetails] = await loadConfigsAndPlugins(loadModule, configPaths, pluginPaths)
|
||||
|
||||
features |= upgradeToFullPluginSupport({
|
||||
designSystem,
|
||||
base,
|
||||
ast,
|
||||
sources,
|
||||
configs,
|
||||
pluginDetails,
|
||||
})
|
||||
|
||||
return features
|
||||
}
|
||||
|
||||
function loadConfigsAndPlugins(
|
||||
loadModule: LoadModule,
|
||||
configPaths: ModulePath[],
|
||||
pluginPaths: PluginPath[],
|
||||
) {
|
||||
return Promise.all([
|
||||
Promise.all(
|
||||
configPaths.map(async ({ id, base, reference, src }) => {
|
||||
let loaded = await loadModule(id, base, 'config')
|
||||
@@ -201,17 +224,6 @@ export async function applyCompatibilityHooks({
|
||||
}),
|
||||
),
|
||||
])
|
||||
|
||||
features |= upgradeToFullPluginSupport({
|
||||
designSystem,
|
||||
base,
|
||||
ast,
|
||||
sources,
|
||||
configs,
|
||||
pluginDetails,
|
||||
})
|
||||
|
||||
return features
|
||||
}
|
||||
|
||||
function upgradeToFullPluginSupport({
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import { setImmediate } from 'node:timers/promises'
|
||||
import { setFlagsFromString } from 'node:v8'
|
||||
import { runInNewContext } from 'node:vm'
|
||||
import { expect, test } from 'vitest'
|
||||
import { compile } from './index'
|
||||
|
||||
// Expose `gc()` without requiring the `--expose-gc` Node.js flag. The flag is
|
||||
// process-wide, so we turn it off again once we have a reference to `gc()`.
|
||||
setFlagsFromString('--expose-gc')
|
||||
const gc: () => void = runInNewContext('gc')
|
||||
setFlagsFromString('--no-expose-gc')
|
||||
|
||||
test('compiler releases loader callbacks after compilation', async () => {
|
||||
let plugin = () => {}
|
||||
|
||||
async function createCompiler() {
|
||||
let loadStylesheet = async () => ({
|
||||
path: 'utilities.css',
|
||||
base: '.',
|
||||
content: '@tailwind utilities;',
|
||||
})
|
||||
let loadModule = async () => ({ path: 'plugin.js', base: '.', module: plugin })
|
||||
let callbacks = [new WeakRef(loadStylesheet), new WeakRef(loadModule)]
|
||||
let compiler = await compile('@import "utilities"; @plugin "fixture";', {
|
||||
from: 'input.css',
|
||||
loadStylesheet,
|
||||
loadModule,
|
||||
})
|
||||
return { compiler, callbacks }
|
||||
}
|
||||
|
||||
let { compiler, callbacks } = await createCompiler()
|
||||
await setImmediate()
|
||||
gc()
|
||||
|
||||
expect(callbacks.map((callback) => callback.deref())).toEqual([undefined, undefined])
|
||||
expect(compiler.build(['flex'])).toContain('.flex {')
|
||||
})
|
||||
@@ -747,6 +747,9 @@ export async function compileAst(
|
||||
}
|
||||
}
|
||||
|
||||
// Avoid retaining loader callbacks, which may keep build tool state alive.
|
||||
let { from, polyfills } = opts
|
||||
|
||||
return {
|
||||
sources,
|
||||
root,
|
||||
@@ -757,7 +760,7 @@ export async function compileAst(
|
||||
}
|
||||
|
||||
if (!utilitiesNode) {
|
||||
compiled ??= optimizeAst(ast, designSystem, opts.polyfills)
|
||||
compiled ??= optimizeAst(ast, designSystem, polyfills)
|
||||
return compiled
|
||||
}
|
||||
|
||||
@@ -783,7 +786,7 @@ export async function compileAst(
|
||||
// If no new candidates were added, we can return the original CSS. This
|
||||
// currently assumes that we only add new candidates and never remove any.
|
||||
if (!didChange) {
|
||||
compiled ??= optimizeAst(ast, designSystem, opts.polyfills)
|
||||
compiled ??= optimizeAst(ast, designSystem, polyfills)
|
||||
return compiled
|
||||
}
|
||||
|
||||
@@ -791,7 +794,7 @@ export async function compileAst(
|
||||
onInvalidCandidate,
|
||||
}).astNodes
|
||||
|
||||
if (opts.from) {
|
||||
if (from) {
|
||||
walk(newNodes, (node) => {
|
||||
// We do this conditionally to preserve source locations from both
|
||||
// `@utility` and `@custom-variant`. Even though generated nodes are
|
||||
@@ -805,7 +808,7 @@ export async function compileAst(
|
||||
// CSS. This currently assumes that we only add new ast nodes and never
|
||||
// remove any.
|
||||
if (!didAddExternalVariable && previousAstNodeCount === newNodes.length) {
|
||||
compiled ??= optimizeAst(ast, designSystem, opts.polyfills)
|
||||
compiled ??= optimizeAst(ast, designSystem, polyfills)
|
||||
return compiled
|
||||
}
|
||||
|
||||
@@ -813,7 +816,7 @@ export async function compileAst(
|
||||
|
||||
utilitiesNode.nodes = newNodes
|
||||
|
||||
compiled = optimizeAst(ast, designSystem, opts.polyfills)
|
||||
compiled = optimizeAst(ast, designSystem, polyfills)
|
||||
return compiled
|
||||
},
|
||||
}
|
||||
@@ -835,6 +838,7 @@ export async function compile(
|
||||
let api = await compileAst(ast, opts)
|
||||
let compiledAst = ast
|
||||
let compiledCss = css
|
||||
let from = opts.from
|
||||
|
||||
return {
|
||||
...api,
|
||||
@@ -845,7 +849,7 @@ export async function compile(
|
||||
return compiledCss
|
||||
}
|
||||
|
||||
compiledCss = toCss(newAst, !!opts.from)
|
||||
compiledCss = toCss(newAst, !!from)
|
||||
compiledAst = newAst
|
||||
|
||||
return compiledCss
|
||||
|
||||
Reference in new issue
Block a user