diff --git a/packages/vite/src/node/__tests__/fixtures/scan-jsx-runtime/entry-glob-custom-oxc.tsx b/packages/vite/src/node/__tests__/fixtures/scan-jsx-runtime/entry-glob-custom-oxc.tsx
new file mode 100644
index 00000000000000..178cb0b9e87bff
--- /dev/null
+++ b/packages/vite/src/node/__tests__/fixtures/scan-jsx-runtime/entry-glob-custom-oxc.tsx
@@ -0,0 +1,3 @@
+export const pages = import.meta.glob('./pages/*.tsx')
+
+export default
diff --git a/packages/vite/src/node/__tests__/fixtures/scan-jsx-runtime/pages/index.tsx b/packages/vite/src/node/__tests__/fixtures/scan-jsx-runtime/pages/index.tsx
new file mode 100644
index 00000000000000..1bdcfef72ff16e
--- /dev/null
+++ b/packages/vite/src/node/__tests__/fixtures/scan-jsx-runtime/pages/index.tsx
@@ -0,0 +1 @@
+export default
diff --git a/packages/vite/src/node/__tests__/scan.spec.ts b/packages/vite/src/node/__tests__/scan.spec.ts
index b79930393913d3..6658425dd5dd96 100644
--- a/packages/vite/src/node/__tests__/scan.spec.ts
+++ b/packages/vite/src/node/__tests__/scan.spec.ts
@@ -175,6 +175,49 @@ test('scan jsx-runtime', async (ctx) => {
expect(mod1).toBe(mod2)
})
+test('scan import.meta.glob respects rolldown transform jsx options', async (ctx) => {
+ const server = await createServer({
+ configFile: false,
+ logLevel: 'error',
+ root: path.join(import.meta.dirname, 'fixtures', 'scan-jsx-runtime'),
+ oxc: {
+ jsx: {
+ runtime: 'automatic',
+ importSource: 'react',
+ },
+ },
+ optimizeDeps: {
+ force: true,
+ noDiscovery: false,
+ entries: ['./entry-glob-custom-oxc.tsx'],
+ rolldownOptions: {
+ transform: {
+ jsx: {
+ runtime: 'automatic',
+ importSource: 'vue',
+ },
+ },
+ },
+ },
+ })
+ ctx.onTestFinished(() => server.close())
+
+ const { cancel, result } = scanImports(
+ devToScanEnvironment(server.environments.client),
+ )
+ ctx.onTestFinished(cancel)
+
+ const scanResult = await result
+
+ expect(scanResult).toMatchObject({
+ deps: {
+ 'vue/jsx-dev-runtime': expect.any(String),
+ },
+ missing: {},
+ })
+ expect(scanResult.deps).not.toHaveProperty('react/jsx-runtime')
+})
+
test('scan import.meta.glob package imports patterns', async (ctx) => {
const server = await createServer({
configFile: false,
diff --git a/packages/vite/src/node/optimizer/scan.ts b/packages/vite/src/node/optimizer/scan.ts
index be05905a77c54c..06af7aaaecbf48 100644
--- a/packages/vite/src/node/optimizer/scan.ts
+++ b/packages/vite/src/node/optimizer/scan.ts
@@ -3,6 +3,7 @@ import fsp from 'node:fs/promises'
import path from 'node:path'
import { performance } from 'node:perf_hooks'
import { scan } from 'rolldown/experimental'
+import type { TransformOptions as OxcTransformOptions } from 'rolldown/utils'
import { transformSync } from 'rolldown/utils'
import type { PartialResolvedId, Plugin } from 'rolldown'
import colors from 'picocolors'
@@ -252,9 +253,6 @@ async function prepareRolldownScanner(
const { plugins: pluginsFromConfig = [], ...rolldownOptions } =
environment.config.optimizeDeps.rolldownOptions ?? {}
- const plugins = await asyncFlatten(arraify(pluginsFromConfig))
- plugins.push(...rolldownScanPlugin(environment, deps, missing, entries))
-
const transformOptions = deepClone(rolldownOptions.transform) ?? {}
if (transformOptions.jsx === undefined) {
transformOptions.jsx = {}
@@ -267,6 +265,19 @@ async function prepareRolldownScanner(
if (typeof transformOptions.jsx === 'object') {
transformOptions.jsx.development ??= !environment.config.isProduction
}
+ const transformSyncJsxOptions: OxcTransformOptions['jsx'] =
+ transformOptions.jsx === false ? undefined : transformOptions.jsx
+
+ const plugins = await asyncFlatten(arraify(pluginsFromConfig))
+ plugins.push(
+ ...rolldownScanPlugin(
+ environment,
+ deps,
+ missing,
+ entries,
+ transformSyncJsxOptions,
+ ),
+ )
async function build() {
await scan({
@@ -343,6 +354,7 @@ function rolldownScanPlugin(
depImports: Record,
missing: Record,
entries: string[],
+ jsxOptions: OxcTransformOptions['jsx'],
): Plugin[] {
const seen = new Map()
async function resolveId(
@@ -397,6 +409,7 @@ function rolldownScanPlugin(
// transpile because `transformGlobImport` only expects js
if (loader !== 'js') {
const result = transformSync(id, contents, {
+ ...(jsxOptions !== undefined ? { jsx: jsxOptions } : {}),
lang: loader,
tsconfig: false,
})
diff --git a/packages/vite/src/shared/__tests__/forwardConsole.spec.ts b/packages/vite/src/shared/__tests__/forwardConsole.spec.ts
index 44c6b1361f2783..fb3e3880d76f1b 100644
--- a/packages/vite/src/shared/__tests__/forwardConsole.spec.ts
+++ b/packages/vite/src/shared/__tests__/forwardConsole.spec.ts
@@ -1,5 +1,13 @@
-import { describe, expect, test } from 'vitest'
-import { formatConsoleArgs } from '../forwardConsole'
+import { setTimeout } from 'node:timers/promises'
+import { describe, expect, test, vi } from 'vitest'
+import {
+ formatConsoleArgs,
+ setupForwardConsoleHandler,
+} from '../forwardConsole'
+import {
+ type NormalizedModuleRunnerTransport,
+ SendBeforeConnectError,
+} from '../moduleRunnerTransport'
describe('formatConsoleArgs', () => {
test('formats placeholders', () => {
@@ -59,3 +67,73 @@ describe('formatConsoleArgs', () => {
)
})
})
+
+describe('setupForwardConsoleHandler', () => {
+ function createMockConsole() {
+ return {
+ error: vi.fn(),
+ warn: vi.fn(),
+ info: vi.fn(),
+ log: vi.fn(),
+ debug: vi.fn(),
+ } as unknown as Console
+ }
+
+ function createMockTransport(
+ send: (...args: any[]) => Promise,
+ ): NormalizedModuleRunnerTransport {
+ return {
+ connect: () => Promise.resolve(),
+ disconnect: () => Promise.resolve(),
+ send,
+ invoke: () => Promise.resolve({ result: undefined } as any),
+ }
+ }
+
+ test('ignore SendBeforeConnectError from transport.send', async () => {
+ const transport = createMockTransport(() =>
+ Promise.reject(new SendBeforeConnectError('not connected yet')),
+ )
+ const console = createMockConsole()
+
+ setupForwardConsoleHandler(
+ transport,
+ {
+ enabled: true,
+ unhandledErrors: false,
+ logLevels: ['log'],
+ },
+ console,
+ )
+
+ console.log('hi')
+ await setTimeout(50)
+
+ expect(console.error).not.toHaveBeenCalled()
+ })
+
+ test('log errors from transport.send', async () => {
+ const transport = createMockTransport(() =>
+ Promise.reject(new Error('other error')),
+ )
+ const console = createMockConsole()
+
+ setupForwardConsoleHandler(
+ transport,
+ {
+ enabled: true,
+ unhandledErrors: false,
+ logLevels: ['log'],
+ },
+ console,
+ )
+
+ console.log('hi')
+ await setTimeout(50)
+
+ expect(console.error).toHaveBeenCalledWith(
+ 'Failed to send error to Vite server:',
+ new Error('other error'),
+ )
+ })
+})
diff --git a/packages/vite/src/shared/forwardConsole.ts b/packages/vite/src/shared/forwardConsole.ts
index d675cae366006e..6ee999d9c23c89 100644
--- a/packages/vite/src/shared/forwardConsole.ts
+++ b/packages/vite/src/shared/forwardConsole.ts
@@ -1,5 +1,8 @@
import type { ForwardConsolePayload } from '#types/customEvent'
-import type { NormalizedModuleRunnerTransport } from './moduleRunnerTransport'
+import {
+ type NormalizedModuleRunnerTransport,
+ SendBeforeConnectError,
+} from './moduleRunnerTransport'
export type ForwardConsoleLogLevel =
| 'error'
@@ -23,13 +26,14 @@ export interface ResolvedForwardConsoleOptions {
export function setupForwardConsoleHandler(
transport: NormalizedModuleRunnerTransport,
options: ResolvedForwardConsoleOptions,
+ console: Console = globalThis.console,
): void {
if (!options.enabled) {
return
}
- function sendError(type: 'error' | 'unhandled-rejection', error: any) {
- transport.send({
+ async function sendError(type: 'error' | 'unhandled-rejection', error: any) {
+ await transport.send({
type: 'custom',
event: 'vite:forward-console',
data: {
@@ -43,20 +47,32 @@ export function setupForwardConsoleHandler(
})
}
- function sendLog(level: ForwardConsoleLogLevel, args: unknown[]) {
- transport.send({
- type: 'custom',
- event: 'vite:forward-console',
- data: {
- type: 'log',
+ async function sendLog(level: ForwardConsoleLogLevel, args: unknown[]) {
+ try {
+ await transport.send({
+ type: 'custom',
+ event: 'vite:forward-console',
data: {
- level,
- message: formatConsoleArgs(args),
- },
- } satisfies ForwardConsolePayload,
- })
+ type: 'log',
+ data: {
+ level,
+ message: formatConsoleArgs(args),
+ },
+ } satisfies ForwardConsolePayload,
+ })
+ } catch (err) {
+ try {
+ await sendError('unhandled-rejection', err)
+ } catch (err) {
+ if (!(err instanceof SendBeforeConnectError)) {
+ originalConsoleError('Failed to send error to Vite server:', err)
+ }
+ }
+ }
}
+ const originalConsoleError = console.error
+
for (const level of options.logLevels) {
const original = (console as any)[level]
if (typeof original !== 'function') {
@@ -69,18 +85,30 @@ export function setupForwardConsoleHandler(
}
if (options.unhandledErrors && typeof window !== 'undefined') {
- window.addEventListener('error', (event) => {
+ window.addEventListener('error', async (event) => {
// `ErrorEvent` doesn't necessarily have `ErrorEvent.error`.
// Use `ErrorEvent.message` as fallback e.g. for ResizeObserver error.
// https://developer.mozilla.org/en-US/docs/Web/API/ErrorEvent/error
// https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserver#observation_errors
const error =
event.error ?? (event.message ? new Error(event.message) : event)
- sendError('error', error)
+ try {
+ await sendError('error', error)
+ } catch (err) {
+ if (!(err instanceof SendBeforeConnectError)) {
+ originalConsoleError('Failed to send error to Vite server:', err)
+ }
+ }
})
- window.addEventListener('unhandledrejection', (event) => {
- sendError('unhandled-rejection', event.reason)
+ window.addEventListener('unhandledrejection', async (event) => {
+ try {
+ await sendError('unhandled-rejection', event.reason)
+ } catch (err) {
+ if (!(err instanceof SendBeforeConnectError)) {
+ originalConsoleError('Failed to send error to Vite server:', err)
+ }
+ }
})
}
}
diff --git a/packages/vite/src/shared/moduleRunnerTransport.ts b/packages/vite/src/shared/moduleRunnerTransport.ts
index 2728f81d78e778..bc7b2977a1ac22 100644
--- a/packages/vite/src/shared/moduleRunnerTransport.ts
+++ b/packages/vite/src/shared/moduleRunnerTransport.ts
@@ -237,7 +237,7 @@ export const normalizeModuleRunnerTransport = (
if (connectingPromise) {
await connectingPromise
} else {
- throw new Error('send was called before connect')
+ throw new SendBeforeConnectError('send was called before connect')
}
}
await invokeableTransport.send(data)
@@ -247,7 +247,7 @@ export const normalizeModuleRunnerTransport = (
if (connectingPromise) {
await connectingPromise
} else {
- throw new Error('invoke was called before connect')
+ throw new SendBeforeConnectError('invoke was called before connect')
}
}
return invokeableTransport.invoke(name, data)
@@ -255,6 +255,13 @@ export const normalizeModuleRunnerTransport = (
}
}
+export class SendBeforeConnectError extends Error {
+ constructor(message: string) {
+ super(message)
+ this.name = 'SendBeforeConnectError'
+ }
+}
+
export const createWebSocketModuleRunnerTransport = (options: {
// eslint-disable-next-line n/no-unsupported-features/node-builtins
createConnection: () => WebSocket