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