diff --git a/create-vite-app/template-preact/main.jsx b/create-vite-app/template-preact/main.jsx
index 53725fb7a6be0a..298513cea48268 100644
--- a/create-vite-app/template-preact/main.jsx
+++ b/create-vite-app/template-preact/main.jsx
@@ -1,4 +1,4 @@
-import { h, render } from 'preact'
+import { render } from 'preact'
function MyComponent(props) {
return
@@ -7,5 +7,5 @@ const Component = () =>
export function renderPreact(el) {
- render(h(Component), el)
+ render(
, el)
}
diff --git a/playground/testTsx.tsx b/playground/testTsx.tsx
index 6f9e6445adb04e..0491ce1d1d0d20 100644
--- a/playground/testTsx.tsx
+++ b/playground/testTsx.tsx
@@ -1,5 +1,3 @@
-import { h } from 'preact'
-
export function Test(props: { count: 0 }) {
- return
Rendered from TSX: count is {props.count}
+ return
Rendered from Preact TSX: count is {props.count}
}
diff --git a/playground/vite.config.ts b/playground/vite.config.ts
index 5afa69946aa1cb..8aa3bbb8074b27 100644
--- a/playground/vite.config.ts
+++ b/playground/vite.config.ts
@@ -6,10 +6,7 @@ const config: UserConfig = {
alias: {
alias: '/aliased'
},
- jsx: {
- factory: 'h',
- fragment: 'Fragment'
- },
+ jsx: 'preact',
minify: false,
plugins: [sassPlugin, jsPlugin]
}
diff --git a/src/client/vueJsxCompat.ts b/src/client/vueJsxCompat.ts
new file mode 100644
index 00000000000000..81b0d16718a255
--- /dev/null
+++ b/src/client/vueJsxCompat.ts
@@ -0,0 +1,17 @@
+import { createVNode } from 'vue'
+
+declare const __DEV__: boolean
+
+if (__DEV__) {
+ console.log(
+ `[vue tip] You are using an non-optimized version of Vue 3 JSX, ` +
+ `which does not take advantage of Vue 3's runtime fast paths. An improved ` +
+ `JSX transform will be provided at a later stage.`
+ )
+}
+
+export function jsx(tag: any, props = null) {
+ const c =
+ arguments.length > 2 ? Array.prototype.slice.call(arguments, 2) : null
+ return createVNode(tag, props, typeof tag === 'string' ? c : () => c)
+}
diff --git a/src/node/build/buildPluginEsbuild.ts b/src/node/build/buildPluginEsbuild.ts
index c177b75a2c3cbb..8a13fc1839d228 100644
--- a/src/node/build/buildPluginEsbuild.ts
+++ b/src/node/build/buildPluginEsbuild.ts
@@ -1,17 +1,12 @@
import { Plugin } from 'rollup'
-import { tjsxRE, transform } from '../esbuildService'
+import { tjsxRE, transform, reoslveJsxOptions } from '../esbuildService'
+import { SharedConfig } from '../config'
export const createEsbuildPlugin = async (
minify: boolean,
- jsx: {
- factory?: string
- fragment?: string
- }
+ jsx: SharedConfig['jsx']
): Promise
=> {
- const jsxConfig = {
- jsxFactory: jsx.factory,
- jsxFragment: jsx.fragment
- }
+ const jsxConfig = reoslveJsxOptions(jsx)
return {
name: 'vite:esbuild',
@@ -19,10 +14,15 @@ export const createEsbuildPlugin = async (
async transform(code, id) {
const isVueTs = /\.vue\?/.test(id) && id.endsWith('lang=ts')
if (tjsxRE.test(id) || isVueTs) {
- return transform(code, id, {
- ...jsxConfig,
- ...(isVueTs ? { loader: 'ts' } : null)
- })
+ return transform(
+ code,
+ id,
+ {
+ ...jsxConfig,
+ ...(isVueTs ? { loader: 'ts' } : null)
+ },
+ jsx
+ )
}
},
diff --git a/src/node/cli.ts b/src/node/cli.ts
index 9730fd385183a8..f7f375c48a5143 100644
--- a/src/node/cli.ts
+++ b/src/node/cli.ts
@@ -22,6 +22,7 @@ Commands:
Options:
--help, -h [boolean] show help
--version, -v [boolean] show version
+ --config, -c [string] use specified config file
--port [number] port to use for serve
--open [boolean] open browser on server start
--base [string] public base path for build (default: /)
@@ -32,6 +33,7 @@ Options:
--minify [boolean | 'terser' | 'esbuild'] disable minification, or specify
minifier to use. (default: 'terser')
--ssr [boolean] build for server-side rendering
+ --jsx ['vue' | 'preact' | 'react'] choose jsx preset (default: 'vue')
--jsx-factory [string] (default: React.createElement)
--jsx-fragment [string] (default: React.Fragment)
`)
diff --git a/src/node/config.ts b/src/node/config.ts
index ec78aedd90ea1b..c063a5e290f871 100644
--- a/src/node/config.ts
+++ b/src/node/config.ts
@@ -25,11 +25,11 @@ export interface SharedConfig {
*/
root?: string
/**
- * TODO
+ * Import alias. Can only be exact mapping, does not support wildcard syntax.
*/
alias?: Record
/**
- * TODO
+ * Custom file transforms.
*/
transforms?: Transform[]
/**
@@ -49,10 +49,14 @@ export interface SharedConfig {
* fragment: 'React.Fragment'
* }
*/
- jsx?: {
- factory?: string
- fragment?: string
- }
+ jsx?:
+ | 'vue'
+ | 'preact'
+ | 'react'
+ | {
+ factory?: string
+ fragment?: string
+ }
}
export interface ServerConfig extends SharedConfig {
diff --git a/src/node/esbuildService.ts b/src/node/esbuildService.ts
index 6a0d1f6bdf7580..755db0510f72e1 100644
--- a/src/node/esbuildService.ts
+++ b/src/node/esbuildService.ts
@@ -1,11 +1,39 @@
import path from 'path'
import chalk from 'chalk'
import { startService, Service, TransformOptions, Message } from 'esbuild'
+import { SharedConfig } from './config'
const debug = require('debug')('vite:esbuild')
export const tjsxRE = /\.(tsx?|jsx)$/
+export const vueJsxPublicPath = '/vite/jsx'
+
+export const vueJsxFilePath = path.resolve(__dirname, 'vueJsxCompat.js')
+
+const JsxPresets: Record<
+ string,
+ Pick
+> = {
+ vue: { jsxFactory: 'jsx', jsxFragment: 'Fragment' },
+ preact: { jsxFactory: 'h', jsxFragment: 'Fragment' },
+ react: {} // use esbuild default
+}
+
+export function reoslveJsxOptions(options: SharedConfig['jsx'] = 'vue') {
+ if (typeof options === 'string') {
+ if (!(options in JsxPresets)) {
+ console.error(`[vite] unknown jsx preset: '${options}'.`)
+ }
+ return JsxPresets[options] || {}
+ } else if (options) {
+ return {
+ jsxFactory: options.factory,
+ jsxFragment: options.fragment
+ }
+ }
+}
+
// lazy start the service
let _service: Service | undefined
@@ -24,9 +52,10 @@ const sourceMapRE = /\/\/# sourceMappingURL.*/
// transform used in server plugins with a more friendly API
export const transform = async (
- code: string,
+ src: string,
file: string,
- options: TransformOptions = {}
+ options: TransformOptions = {},
+ jsxOption?: SharedConfig['jsx']
) => {
const service = await ensureService()
options = {
@@ -35,20 +64,36 @@ export const transform = async (
sourcemap: true
}
try {
- const result = await service.transform(code, options)
+ const result = await service.transform(src, options)
if (result.warnings.length) {
console.error(`[vite] warnings while transforming ${file} with esbuild:`)
- result.warnings.forEach((m) => printMessage(m, code))
+ result.warnings.forEach((m) => printMessage(m, src))
}
+
+ let code = (result.js || '').replace(sourceMapRE, '')
+
+ // if transpiling (j|t)sx file, inject the imports for the jsx helper and
+ // Fragment.
+ if (file.endsWith('x')) {
+ if (!jsxOption || jsxOption === 'vue') {
+ code +=
+ `\nimport { jsx } from '${vueJsxPublicPath}'` +
+ `\nimport { Fragment } from 'vue'`
+ }
+ if (jsxOption === 'preact') {
+ code += `\nimport { h, Fragment } from 'preact'`
+ }
+ }
+
return {
- code: (result.js || '').replace(sourceMapRE, ''),
+ code,
map: result.jsSourceMap
}
} catch (e) {
console.error(
chalk.red(`[vite] error while transforming ${file} with esbuild:`)
)
- e.errors.forEach((m: Message) => printMessage(m, code))
+ e.errors.forEach((m: Message) => printMessage(m, src))
debug(`options used: `, options)
return {
code: '',
diff --git a/src/node/server/serverPluginEsbuild.ts b/src/node/server/serverPluginEsbuild.ts
index 0babae507026a5..f070059297be91 100644
--- a/src/node/server/serverPluginEsbuild.ts
+++ b/src/node/server/serverPluginEsbuild.ts
@@ -1,24 +1,32 @@
import { ServerPlugin } from '.'
-import { tjsxRE, transform } from '../esbuildService'
-import { readBody, genSourceMapString } from '../utils'
+import {
+ tjsxRE,
+ transform,
+ reoslveJsxOptions,
+ vueJsxPublicPath,
+ vueJsxFilePath
+} from '../esbuildService'
+import { readBody, genSourceMapString, cachedRead } from '../utils'
export const esbuildPlugin: ServerPlugin = ({ app, config }) => {
- const options = {
- jsxFactory: config.jsx && config.jsx.factory,
- jsxFragment: config.jsx && config.jsx.fragment
- }
+ const jsxConfig = reoslveJsxOptions(config.jsx)
app.use(async (ctx, next) => {
+ // intercept and return vue jsx helper import
+ if (ctx.path === vueJsxPublicPath) {
+ await cachedRead(ctx, vueJsxFilePath)
+ }
+
await next()
+
if (ctx.body && tjsxRE.test(ctx.path)) {
ctx.type = 'js'
const src = await readBody(ctx.body)
- const { code, map } = await transform(src!, ctx.path, options)
- let res = code
+ let { code, map } = await transform(src!, ctx.path, jsxConfig, config.jsx)
if (map) {
- res += genSourceMapString(map)
+ code += genSourceMapString(map)
}
- ctx.body = res
+ ctx.body = code
}
})
}
diff --git a/test/test.js b/test/test.js
index 176a4d746d128a..6fcd5c34b1b0dd 100644
--- a/test/test.js
+++ b/test/test.js
@@ -13,7 +13,8 @@ const tempDir = path.join(__dirname, 'temp')
let devServer
let browser
let page
-const logs = []
+const browserLogs = []
+const serverLogs = []
const getEl = async (selectorOrEl) => {
return typeof selectorOrEl === 'string'
@@ -49,6 +50,8 @@ afterAll(async () => {
forceKillAfterTimeout: 2000
})
}
+ // console.log(browserLogs)
+ // console.log(serverLogs)
})
describe('vite', () => {
@@ -66,9 +69,9 @@ describe('vite', () => {
})
test('should generate correct asset paths', async () => {
- const has404 = logs.some((msg) => msg.match('404'))
+ const has404 = browserLogs.some((msg) => msg.match('404'))
if (has404) {
- console.log(logs)
+ console.log(browserLogs)
}
expect(has404).toBe(false)
})
@@ -131,10 +134,13 @@ describe('vite', () => {
await updateFile('testHmrManual.js', (content) =>
content.replace('foo = 1', 'foo = 2')
)
- await expectByPolling(() => logs[logs.length - 1], 'foo is now: 2')
+ await expectByPolling(
+ () => browserLogs[browserLogs.length - 1],
+ 'foo is now: 2'
+ )
// there will be a "js module reloaded" message in between because
// disposers are called before the new module is loaded.
- expect(logs[logs.length - 3]).toMatch('foo was: 1')
+ expect(browserLogs[browserLogs.length - 3]).toMatch('foo was: 1')
})
}
@@ -269,8 +275,8 @@ describe('vite', () => {
test('jsx', async () => {
const text = await getText('.jsx-root')
- expect(text).toMatch('from Preact')
- expect(text).toMatch('from TSX')
+ expect(text).toMatch('from Preact JSX')
+ expect(text).toMatch('from Preact TSX')
expect(text).toMatch('count is 1337')
if (!isBuild) {
await updateFile('testJsx.jsx', (c) => c.replace('1337', '2046'))
@@ -315,7 +321,7 @@ describe('vite', () => {
})
test('should build without error', async () => {
- const buildOutput = await execa(binPath, ['build', '--jsx-factory=h'], {
+ const buildOutput = await execa(binPath, ['build'], {
cwd: tempDir
})
expect(buildOutput.stdout).toMatch('Build completed')
@@ -340,13 +346,14 @@ describe('vite', () => {
describe('dev', () => {
beforeAll(async () => {
- logs.length = 0
+ browserLogs.length = 0
// start dev server
- devServer = execa(binPath, ['--jsx-factory=h'], {
+ devServer = execa(binPath, {
cwd: tempDir
})
await new Promise((resolve) => {
devServer.stdout.on('data', (data) => {
+ serverLogs.push(data.toString())
if (data.toString().match('running')) {
resolve()
}
@@ -354,7 +361,9 @@ describe('vite', () => {
})
page = await browser.newPage()
- page.on('console', (msg) => logs.push(msg.text()))
+ page.on('console', (msg) => {
+ browserLogs.push(msg.text())
+ })
await page.goto('http://localhost:3000')
})