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
{props.msg}
diff --git a/create-vite-app/template-preact/package.json b/create-vite-app/template-preact/package.json index 82070c5189e5c2..2f6cd8ba060441 100644 --- a/create-vite-app/template-preact/package.json +++ b/create-vite-app/template-preact/package.json @@ -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" diff --git a/playground/testJsx.jsx b/playground/testJsx.jsx index 8eeb40dd8f527c..bea028657af29e 100644 --- a/playground/testJsx.jsx +++ b/playground/testJsx.jsx @@ -1,4 +1,4 @@ -import { h, render } from 'preact' +import { render } from 'preact' import { Test } from './testTsx.tsx' const Component = () =>
@@ -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') })