Robin Malfait 352d1b9fcf
Ensure Symbol.dispose and Symbol.asyncDispose are available (#15404)
We recently introduced some better instrumentation
(https://github.com/tailwindlabs/tailwindcss/pull/15303) which uses the
new `using` keyword. I made sure that this was compiled correctly for
environments where `using` is not available yet.

The issue is that this also relies on `Symbol.dispose` being available.
In my testing on our minimal required Node.js version (18) it did work
fine. However, turns out that I was using `18.20.x` locally where
`Symbol.dispose` **_is_** available, but on older version of Node.js 18
(e.g.: `18.17.x`) it is **_not_** available. This now results in some
completely broken builds, e.g.: when running on Cloudflare Pages. See:
#15399

I could reproduce this error in CI, by temporarily downgrading the used
Node.js version to `18.17.0`. See:

<img width="1142" alt="image"
src="https://github.com/user-attachments/assets/5bf30f80-9ca0-40d9-ad02-d1ffb4e0e5dd"
/>

Implementing the proper polyfill, as recommended by the TypeScript docs
( see:
https://www.typescriptlang.org/docs/handbook/release-notes/typescript-5-2.html#:~:text=Symbol.dispose,-??=%20Symbol(%22Symbol.dispose
), the error goes away. (If you look at CI after the polyfill, it still
fails but for different reasons unrelated to this change)

Fixes: #15399

---

## Test plan

1. I reproduced it in CI, and I kept the commits so that you can take a
look where it fails with the `Object not disposable`.
2. Using the provided reproduction from #15399:

### Before

It works on Node.js v18.20.x, but switching to Node.js v18.17.x you can
see it fail:

<img width="1607" alt="image"
src="https://github.com/user-attachments/assets/cb6ab73a-8eb2-4003-bab7-b2390f1c879d"
/>

### After

Using pnpm's overrides, we can apply the fix from this PR and test it in
the reproduction. You'll notice that it now works in both Node.js
v18.20.x and v18.17.x

<img width="1604" alt="image"
src="https://github.com/user-attachments/assets/b3a65557-0658-4cb0-a2f9-e3079c7936d5"
/>
2024-12-16 14:17:44 +01:00

114 lines
3.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { DefaultMap } from '../../tailwindcss/src/utils/default-map'
import * as env from './env'
// See: https://www.typescriptlang.org/docs/handbook/release-notes/typescript-5-2.html#:~:text=Symbol.dispose,-??=%20Symbol(%22Symbol.dispose
// @ts-expect-error — Ensure Symbol.dispose exists
Symbol.dispose ??= Symbol('Symbol.dispose')
// @ts-expect-error — Ensure Symbol.asyncDispose exists
Symbol.asyncDispose ??= Symbol('Symbol.asyncDispose')
export class Instrumentation implements Disposable {
#hits = new DefaultMap(() => ({ value: 0 }))
#timers = new DefaultMap(() => ({ value: 0n }))
#timerStack: { id: string; label: string; namespace: string; value: bigint }[] = []
constructor(
private defaultFlush = (message: string) => void process.stderr.write(`${message}\n`),
) {}
hit(label: string) {
this.#hits.get(label).value++
}
start(label: string) {
let namespace = this.#timerStack.map((t) => t.label).join('//')
let id = `${namespace}${namespace.length === 0 ? '' : '//'}${label}`
this.#hits.get(id).value++
// Create the timer if it doesn't exist yet
this.#timers.get(id)
this.#timerStack.push({ id, label, namespace, value: process.hrtime.bigint() })
}
end(label: string) {
let end = process.hrtime.bigint()
if (this.#timerStack[this.#timerStack.length - 1].label !== label) {
throw new Error(
`Mismatched timer label: \`${label}\`, expected \`${
this.#timerStack[this.#timerStack.length - 1].label
}\``,
)
}
let parent = this.#timerStack.pop()!
let elapsed = end - parent.value
this.#timers.get(parent.id).value += elapsed
}
reset() {
this.#hits.clear()
this.#timers.clear()
this.#timerStack.splice(0)
}
report(flush = this.defaultFlush) {
let output: string[] = []
let hasHits = false
// Auto end any pending timers
for (let i = this.#timerStack.length - 1; i >= 0; i--) {
this.end(this.#timerStack[i].label)
}
for (let [label, { value: count }] of this.#hits.entries()) {
if (this.#timers.has(label)) continue
if (output.length === 0) {
hasHits = true
output.push('Hits:')
}
let depth = label.split('//').length
output.push(`${' '.repeat(depth)}${label} ${dim(blue(`× ${count}`))}`)
}
if (this.#timers.size > 0 && hasHits) {
output.push('\nTimers:')
}
let max = -Infinity
let computed = new Map<string, string>()
for (let [label, { value }] of this.#timers) {
let x = `${(Number(value) / 1e6).toFixed(2)}ms`
computed.set(label, x)
max = Math.max(max, x.length)
}
for (let label of this.#timers.keys()) {
let depth = label.split('//').length
output.push(
`${dim(`[${computed.get(label)!.padStart(max, ' ')}]`)}${' '.repeat(depth - 1)}${depth === 1 ? ' ' : dim(' ↳ ')}${label.split('//').pop()} ${
this.#hits.get(label).value === 1 ? '' : dim(blue(`× ${this.#hits.get(label).value}`))
}`.trimEnd(),
)
}
flush(`\n${output.join('\n')}\n`)
this.reset()
}
[Symbol.dispose]() {
env.DEBUG && this.report()
}
}
function dim(input: string) {
return `\u001b[2m${input}\u001b[22m`
}
function blue(input: string) {
return `\u001b[34m${input}\u001b[39m`
}