Skip to content

Commit

Permalink
feat: Vue JSX support
Browse files Browse the repository at this point in the history
BREAKING CHANGE: JSX support has been adjusted

  - Default JSX support is now configured for Vue 3 JSX
  - `jsx` option now accepts string presets ('vue' | 'preact' | 'react')
    e.g. to Use Preact with Vite, use `vite --jsx preact`. In addition,
    when using the Preact preset, Vite auto injects `h` import in `.jsx`
    and `.tsx` files so the user no longer need to import it.
  • Loading branch information
yyx990803 committed May 10, 2020
1 parent d6dd2f0 commit efc853f
Show file tree
Hide file tree
Showing 12 changed files with 138 additions and 58 deletions.
2 changes: 1 addition & 1 deletion create-vite-app/template-preact/main.jsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { h, render } from 'preact'
import { render } from 'preact'

function MyComponent(props) {
return <div>{props.msg}</div>
Expand Down
4 changes: 2 additions & 2 deletions create-vite-app/template-preact/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
"name": "vite-preact-starter",
"version": "0.0.0",
"scripts": {
"dev": "vite --jsx-factory=h --jsx-fragment=Fragment",
"build": "vite build --jsx-factory=h --jsx-fragment=Fragment"
"dev": "vite --jsx preact",
"build": "vite build --jsx preact"
},
"dependencies": {
"preact": "^10.4.1"
Expand Down
4 changes: 2 additions & 2 deletions playground/testJsx.jsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { h, render } from 'preact'
import { render } from 'preact'
import { Test } from './testTsx.tsx'

const Component = () => <div>
Expand All @@ -7,5 +7,5 @@ const Component = () => <div>
</div>

export function renderPreact(el) {
render(h(Component), el)
render(<Component/>, el)
}
4 changes: 1 addition & 3 deletions playground/testTsx.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import { h } from 'preact'

export function Test(props: { count: 0 }) {
return <div>Rendered from TSX: count is {props.count}</div>
return <div>Rendered from Preact TSX: count is {props.count}</div>
}
5 changes: 1 addition & 4 deletions playground/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,7 @@ const config: UserConfig = {
alias: {
alias: '/aliased'
},
jsx: {
factory: 'h',
fragment: 'Fragment'
},
jsx: 'preact',
minify: false,
plugins: [sassPlugin, jsPlugin]
}
Expand Down
17 changes: 17 additions & 0 deletions src/client/vueJsxCompat.ts
Original file line number Diff line number Diff line change
@@ -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)
}
26 changes: 13 additions & 13 deletions src/node/build/buildPluginEsbuild.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,28 @@
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<Plugin> => {
const jsxConfig = {
jsxFactory: jsx.factory,
jsxFragment: jsx.fragment
}
const jsxConfig = reoslveJsxOptions(jsx)

return {
name: 'vite:esbuild',

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
)
}
},

Expand Down
2 changes: 2 additions & 0 deletions src/node/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: /)
Expand All @@ -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)
`)
Expand Down
16 changes: 10 additions & 6 deletions src/node/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,11 @@ export interface SharedConfig {
*/
root?: string
/**
* TODO
* Import alias. Can only be exact mapping, does not support wildcard syntax.
*/
alias?: Record<string, string>
/**
* TODO
* Custom file transforms.
*/
transforms?: Transform[]
/**
Expand All @@ -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 {
Expand Down
57 changes: 51 additions & 6 deletions src/node/esbuildService.ts
Original file line number Diff line number Diff line change
@@ -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<TransformOptions, 'jsxFactory' | 'jsxFragment'>
> = {
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

Expand All @@ -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 = {
Expand All @@ -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: '',
Expand Down
28 changes: 18 additions & 10 deletions src/node/server/serverPluginEsbuild.ts
Original file line number Diff line number Diff line change
@@ -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
}
})
}
31 changes: 20 additions & 11 deletions test/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -49,6 +50,8 @@ afterAll(async () => {
forceKillAfterTimeout: 2000
})
}
// console.log(browserLogs)
// console.log(serverLogs)
})

describe('vite', () => {
Expand All @@ -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)
})
Expand Down Expand Up @@ -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')
})
}

Expand Down Expand Up @@ -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'))
Expand Down Expand Up @@ -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')
Expand All @@ -340,21 +346,24 @@ 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()
}
})
})

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')
})

Expand Down

0 comments on commit efc853f

Please sign in to comment.