Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/pr-698.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<!-- auto-generated -->
---
'@sanity/cli-core': minor
'@sanity/cli': minor
---

upgrade vite to v8, plugin-react to v6, vite-node to v6
2 changes: 1 addition & 1 deletion fixtures/worst-case-studio/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,6 @@
"@sanity/color": "^3.0.6",
"@types/react": "^19.2.14",
"typescript": "^5.9.3",
"vite": "^7.3.2"
"vite": "catalog:"
}
}
2 changes: 1 addition & 1 deletion packages/@sanity/cli-core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@
"rxjs": "catalog:",
"tsx": "catalog:",
"vite": "catalog:",
"vite-node": "^5.3.0",
"vite-node": "^6.0.0",
"zod": "catalog:"
},
"devDependencies": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -221,4 +221,10 @@ const runner = new ViteNodeRunner({
// point why this is, so we should investigate whether it's necessary or not.
await runner.executeId('/@vite/env')

await runner.executeId(workerScriptPath)
try {
await runner.executeId(workerScriptPath)
} finally {
// Close the Vite server to release handles. Especially important with Vite 8+
// where Rolldown's native bindings can keep the worker thread's event loop alive.
await server.close()
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ function createMockWorker() {
for (const key of Object.keys(listeners)) delete listeners[key]
}),
terminate: vi.fn(),
unref: vi.fn(),
}
}

Expand Down Expand Up @@ -103,15 +104,13 @@ describe('promisifyWorker', () => {
await expect(promise).rejects.toThrow('Worker exited without sending a message')
})

test('terminates the worker after receiving a message', async () => {
test('unrefs the worker after receiving a message', async () => {
const promise = promisifyWorker(TEST_WORKER_URL, {name: 'test'})

lastCreatedWorker.emit('message', 'data')
await promise

// terminate is called via setImmediate, so flush it
await new Promise((resolve) => setImmediate(resolve))
expect(lastCreatedWorker.terminate).toHaveBeenCalledOnce()
expect(lastCreatedWorker.unref).toHaveBeenCalledOnce()
})

test('removes all listeners after receiving a message', async () => {
Expand All @@ -123,25 +122,23 @@ describe('promisifyWorker', () => {
expect(lastCreatedWorker.removeAllListeners).toHaveBeenCalledOnce()
})

test('terminates the worker after an error', async () => {
test('unrefs the worker after an error', async () => {
const promise = promisifyWorker(TEST_WORKER_URL, {name: 'test'})

lastCreatedWorker.emit('error', new Error('fail'))
await promise.catch(() => {})

await new Promise((resolve) => setImmediate(resolve))
expect(lastCreatedWorker.terminate).toHaveBeenCalledOnce()
expect(lastCreatedWorker.unref).toHaveBeenCalledOnce()
expect(lastCreatedWorker.removeAllListeners).toHaveBeenCalledOnce()
})

test('terminates the worker after a messageerror', async () => {
test('unrefs the worker after a messageerror', async () => {
const promise = promisifyWorker(TEST_WORKER_URL, {name: 'test'})

lastCreatedWorker.emit('messageerror', new Error('bad message'))
await promise.catch(() => {})

await new Promise((resolve) => setImmediate(resolve))
expect(lastCreatedWorker.terminate).toHaveBeenCalledOnce()
expect(lastCreatedWorker.unref).toHaveBeenCalledOnce()
expect(lastCreatedWorker.removeAllListeners).toHaveBeenCalledOnce()
})

Expand Down Expand Up @@ -170,7 +167,7 @@ describe('promisifyWorker', () => {
await promise.catch(() => {})

expect(lastCreatedWorker.removeAllListeners).not.toHaveBeenCalled()
expect(lastCreatedWorker.terminate).not.toHaveBeenCalled()
expect(lastCreatedWorker.unref).not.toHaveBeenCalled()
})

test('rejects with error when error is followed by non-zero exit', async () => {
Expand Down Expand Up @@ -213,10 +210,15 @@ describe('promisifyWorker', () => {
lastCreatedWorker.emit('error', new Error('fail'))
await promise.catch(() => {})

vi.advanceTimersByTime(1000)
// Advance past both the user timeout (1s) and deferred terminate (5s)
vi.advanceTimersByTime(6000)

expect(lastCreatedWorker.terminate).toHaveBeenCalledOnce()
// Only one unref + removeAllListeners from the error handler cleanup,
// the timeout handler should NOT fire again
expect(lastCreatedWorker.unref).toHaveBeenCalledOnce()
expect(lastCreatedWorker.removeAllListeners).toHaveBeenCalledOnce()
// Deferred terminate fires after 5s
expect(lastCreatedWorker.terminate).toHaveBeenCalledOnce()
})

test('cleans up timer after a messageerror', async () => {
Expand All @@ -227,10 +229,11 @@ describe('promisifyWorker', () => {
lastCreatedWorker.emit('messageerror', new Error('bad message'))
await promise.catch(() => {})

vi.advanceTimersByTime(1000)
vi.advanceTimersByTime(6000)

expect(lastCreatedWorker.terminate).toHaveBeenCalledOnce()
expect(lastCreatedWorker.unref).toHaveBeenCalledOnce()
expect(lastCreatedWorker.removeAllListeners).toHaveBeenCalledOnce()
expect(lastCreatedWorker.terminate).toHaveBeenCalledOnce()
})

test('cleans up timer after receiving a message', async () => {
Expand All @@ -241,9 +244,10 @@ describe('promisifyWorker', () => {
lastCreatedWorker.emit('message', 'result')
await promise

vi.advanceTimersByTime(1000)
vi.advanceTimersByTime(6000)

expect(lastCreatedWorker.terminate).toHaveBeenCalledOnce()
expect(lastCreatedWorker.unref).toHaveBeenCalledOnce()
expect(lastCreatedWorker.removeAllListeners).toHaveBeenCalledOnce()
expect(lastCreatedWorker.terminate).toHaveBeenCalledOnce()
})
})
15 changes: 14 additions & 1 deletion packages/@sanity/cli-core/src/util/promisifyWorker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,8 +78,21 @@ export function promisifyWorker<T = unknown>(
})

function cleanup() {
setImmediate(() => worker.terminate())
// Unref first so the parent process can exit immediately without
// waiting for the worker thread to finish shutting down.
worker.unref()
worker.removeAllListeners()

// Schedule a deferred terminate() as a safety net to force-kill
// workers that don't exit on their own (e.g. native addons holding
// handles). The timer is unref'd so it won't keep the process alive
// — it only fires if the process is still running for other reasons.
//
// We avoid calling terminate() synchronously because it creates an
// internal ref'd MessagePort that would keep the parent process alive
// if the worker is slow to respond (e.g. Rolldown in Vite 8).
const terminateTimer = setTimeout(() => void worker.terminate(), 5000)
terminateTimer.unref()
}
})
}
1 change: 1 addition & 0 deletions packages/@sanity/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@
"@oclif/core": "catalog:",
"@oclif/plugin-help": "catalog:",
"@oclif/plugin-not-found": "catalog:",
"@rolldown/plugin-babel": "^0.2.1",
"@sanity/cli-core": "workspace:^",
"@sanity/client": "catalog:",
"@sanity/codegen": "catalog:",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,13 @@ vi.mock('read-package-up', () => ({
readPackageUp: vi.fn(),
}))

vi.mock('@rolldown/plugin-babel', () => ({
default: vi.fn(() => ({name: 'babel-plugin'})),
}))

vi.mock('@vitejs/plugin-react', () => ({
default: vi.fn(() => ({name: 'react-plugin'})),
default: vi.fn(() => [{name: 'react-plugin'}]),
reactCompilerPreset: vi.fn(() => ({name: 'react-compiler-preset'})),
}))

vi.mock('vite', () => ({
Expand Down Expand Up @@ -249,11 +254,11 @@ describe('#getViteConfig', () => {
})

test('should handle react compiler configuration', async () => {
const {default: viteReact} = await import('@vitejs/plugin-react')
const {default: babel} = await import('@rolldown/plugin-babel')
const {reactCompilerPreset} = await import('@vitejs/plugin-react')

const reactCompilerConfig = {
sources: ['src/**/*.tsx'],
target: '18' as const,
target: '19' as const,
}

const options = {
Expand All @@ -264,13 +269,12 @@ describe('#getViteConfig', () => {

await getViteConfig(options)

expect(viteReact).toHaveBeenCalledWith({
babel: {
generatorOpts: {
compact: true,
},
plugins: [['babel-plugin-react-compiler', reactCompilerConfig]],
},
expect(reactCompilerPreset).toHaveBeenCalledWith({
compilationMode: undefined,
target: '19',
})
expect(babel).toHaveBeenCalledWith({
presets: [expect.objectContaining({name: 'react-compiler-preset'})],
})
})

Expand Down
2 changes: 0 additions & 2 deletions packages/@sanity/cli/src/actions/build/buildStaticFiles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import {writeSanityRuntime} from './writeSanityRuntime.js'

export interface ChunkModule {
name: string
originalLength: number
renderedLength: number
}

Expand Down Expand Up @@ -127,7 +126,6 @@ export async function buildStaticFiles(

return {
name: path.isAbsolute(filePath) ? path.relative(cwd, filePath) : filePath,
originalLength: chunkModule.originalLength,
renderedLength: chunkModule.renderedLength,
}
}),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,7 @@ export async function buildVendorDependencies({
exports: 'named',
format: 'es',
},
treeshake: {preset: 'recommended'},
treeshake: true,
},
},
// Define a custom cache directory so that sanity's vite cache
Expand Down
26 changes: 15 additions & 11 deletions packages/@sanity/cli/src/actions/build/getViteConfig.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import path from 'node:path'

import babel from '@rolldown/plugin-babel'
import {
type CliConfig,
findProjectRoot,
getCliTelemetry,
type UserViteConfig,
} from '@sanity/cli-core'
import viteReact from '@vitejs/plugin-react'
import viteReact, {reactCompilerPreset} from '@vitejs/plugin-react'
import {type PluginOptions as ReactCompilerConfig} from 'babel-plugin-react-compiler'
import debug from 'debug'
import {readPackageUp} from 'read-package-up'
Expand Down Expand Up @@ -144,16 +145,19 @@ export async function getViteConfig(options: ViteOptions): Promise<InlineConfig>
logLevel: mode === 'production' ? 'silent' : 'info',
mode,
plugins: [
viteReact(
reactCompiler
? {
babel: {
generatorOpts: {compact: true},
plugins: [['babel-plugin-react-compiler', reactCompiler]],
},
}
: {},
),
...viteReact(),
...(reactCompiler
? [
babel({
presets: [
reactCompilerPreset({
compilationMode: reactCompiler.compilationMode,
target: reactCompiler.target,
}),
],
}),
]
: []),
sanityFaviconsPlugin({customFaviconsPath, defaultFaviconsPath, staticUrlPath: staticPath}),
sanityRuntimeRewritePlugin(),
sanityBuildEntries({basePath, cwd, importMap, isApp}),
Expand Down
Loading
Loading