From 7cbaf5d8e5b70db2ec642bd1d34f1e0322927ccf Mon Sep 17 00:00:00 2001 From: Evan You Date: Tue, 5 May 2020 21:48:35 -0400 Subject: [PATCH] feat: dev support for ts --- src/node/esbuildService.ts | 30 +++++++++++++++++++++++-- src/node/server.ts | 38 +++++++++++++++++++++++--------- src/node/serverPluginEsbuild.ts | 39 +++++++++++++++++++++++++++++++++ src/node/serverPluginVue.ts | 31 +++++++++++--------------- src/node/utils.ts | 12 ++++++++++ 5 files changed, 120 insertions(+), 30 deletions(-) create mode 100644 src/node/serverPluginEsbuild.ts diff --git a/src/node/esbuildService.ts b/src/node/esbuildService.ts index 39e568d9e0057d..696650958d82e9 100644 --- a/src/node/esbuildService.ts +++ b/src/node/esbuildService.ts @@ -1,7 +1,33 @@ import { startService, Service, TransformOptions } from 'esbuild' import { Plugin } from 'rollup' -const transform = async ( +// Note: when the esbuild service is held in a module level variable, it +// somehow prevents the build process from exiting even after explicitly +// calling service.stop(). Therefore make sure to only use `ensureService` +// in server plugins. Build plugins should contain the service in its creation +// closure and close it in `generateBundle`. + +// lazy start the service +let _service: Service | undefined + +const ensureService = async () => { + if (!_service) { + _service = await startService() + } + return _service +} + +// transform used in server plugins with a more friendly API +export const transform = async ( + code: string, + options: TransformOptions, + operation: string +) => { + return _transform(await ensureService(), code, options, operation) +} + +// trasnform that takes the service via arguments, used in build plugins +const _transform = async ( service: Service, code: string, options: TransformOptions, @@ -33,7 +59,7 @@ export const createMinifyPlugin = async (): Promise => { return { name: 'vite:minify', async renderChunk(code, chunk) { - return transform( + return _transform( service, code, { minify: true }, diff --git a/src/node/server.ts b/src/node/server.ts index ff059af4bf5848..30c8c1ddf95f01 100644 --- a/src/node/server.ts +++ b/src/node/server.ts @@ -9,6 +9,7 @@ import { hmrPlugin, HMRWatcher } from './serverPluginHmr' import { serveStaticPlugin } from './serverPluginServeStatic' import { jsonPlugin } from './serverPluginJson' import { cssPlugin } from './serverPluginCss' +import { esbuildPlugin } from './serverPluginEsbuild' export { Resolver } @@ -20,18 +21,27 @@ export interface PluginContext { server: Server watcher: HMRWatcher resolver: InternalResolver + jsxConfig: { + jsxFactory: string | undefined + jsxFragment: string | undefined + } } export interface ServerConfig { root?: string plugins?: Plugin[] resolvers?: Resolver[] + jsx?: { + factory?: string + fragment?: string + } } const internalPlugins: Plugin[] = [ moduleRewritePlugin, moduleResolvePlugin, vuePlugin, + esbuildPlugin, jsonPlugin, cssPlugin, hmrPlugin, @@ -39,23 +49,31 @@ const internalPlugins: Plugin[] = [ ] export function createServer(config: ServerConfig = {}): Server { - const { root = process.cwd(), plugins = [], resolvers = [] } = config + const { + root = process.cwd(), + plugins = [], + resolvers = [], + jsx = {} + } = config const app = new Koa() const server = http.createServer(app.callback()) const watcher = chokidar.watch(root, { ignored: [/node_modules/] }) as HMRWatcher const resolver = createResolver(root, resolvers) + const context = { + root, + app, + server, + watcher, + resolver, + jsxConfig: { + jsxFactory: jsx.factory, + jsxFragment: jsx.fragment + } + } - ;[...plugins, ...internalPlugins].forEach((m) => - m({ - root, - app, - server, - watcher, - resolver - }) - ) + ;[...plugins, ...internalPlugins].forEach((m) => m(context)) return server } diff --git a/src/node/serverPluginEsbuild.ts b/src/node/serverPluginEsbuild.ts new file mode 100644 index 00000000000000..bd1741caa38677 --- /dev/null +++ b/src/node/serverPluginEsbuild.ts @@ -0,0 +1,39 @@ +import { Plugin } from './server' +import { readBody, isImportRequest, genSourceMapString } from './utils' +import { TransformOptions } from 'esbuild' +import { transform } from './esbuildService' + +const testRE = /\.(tsx?|jsx)$/ + +export const esbuildPlugin: Plugin = ({ app, watcher, jsxConfig }) => { + app.use(async (ctx, next) => { + await next() + if (isImportRequest(ctx) && ctx.body && testRE.test(ctx.path)) { + ctx.type = 'js' + let options: TransformOptions = {} + if (ctx.path.endsWith('.ts')) { + options = { loader: 'ts' } + } else if (ctx.path.endsWith('tsx')) { + options = { loader: 'tsx', ...jsxConfig } + } else if (ctx.path.endsWith('jsx')) { + options = { loader: 'jsx', ...jsxConfig } + } + const src = await readBody(ctx.body) + const { code, map } = await transform( + src!, + options, + `transpiling ${ctx.path}` + ) + ctx.body = code + if (map) { + ctx.body += genSourceMapString(map) + } + } + }) + + watcher.on('change', (file) => { + if (testRE.test(file)) { + watcher.handleJSReload(file) + } + }) +} diff --git a/src/node/serverPluginVue.ts b/src/node/serverPluginVue.ts index b3d4463f1c28ba..cc308b64131f4a 100644 --- a/src/node/serverPluginVue.ts +++ b/src/node/serverPluginVue.ts @@ -11,9 +11,10 @@ import hash_sum from 'hash-sum' import LRUCache from 'lru-cache' import { hmrClientId } from './serverPluginHmr' import resolve from 'resolve-from' -import { cachedRead } from './utils' +import { cachedRead, genSourceMapString } from './utils' import { loadPostcssConfig } from './config' import { Context } from 'koa' +import { transform } from './esbuildService' const debug = require('debug')('vite:sfc') const getEtag = require('etag') @@ -54,7 +55,7 @@ export const vuePlugin: Plugin = ({ root, app, resolver }) => { if (!query.type) { ctx.type = 'js' - ctx.body = compileSFCMain(descriptor, filePath, publicPath) + ctx.body = await compileSFCMain(descriptor, filePath, publicPath) return etagCacheCheck(ctx) } @@ -138,23 +139,26 @@ export async function parseSFC( return descriptor } -function compileSFCMain( +async function compileSFCMain( descriptor: SFCDescriptor, filePath: string, publicPath: string -): string { +): Promise { let cached = vueCache.get(filePath) if (cached && cached.script) { return cached.script } - // inject hmr client let code = '' if (descriptor.script) { - code += descriptor.script.content.replace( - `export default`, - 'const __script =' - ) + let content = descriptor.script.content + if (descriptor.script.lang === 'ts') { + content = ( + await transform(content, { loader: 'ts' }, `transpiling ${publicPath}`) + ).code + } + + code += content.replace(`export default`, 'const __script =') } else { code += `const __script = {}` } @@ -299,12 +303,3 @@ async function compileSFCStyle( debug(`${publicPath} style compiled in ${Date.now() - start}ms`) return result } - -function genSourceMapString(map: object | undefined) { - if (!map) { - return '' - } - return `\n//# sourceMappingURL=data:application/json;base64,${Buffer.from( - JSON.stringify(map) - ).toString('base64')}` -} diff --git a/src/node/utils.ts b/src/node/utils.ts index fe477e7c9b2d21..14ccbbe13d26d0 100644 --- a/src/node/utils.ts +++ b/src/node/utils.ts @@ -109,3 +109,15 @@ export async function readBody( return !stream || typeof stream === 'string' ? stream : stream.toString() } } + +export function genSourceMapString(map: object | string | undefined) { + if (!map) { + return '' + } + if (typeof map !== 'string') { + map = JSON.stringify(map) + } + return `\n//# sourceMappingURL=data:application/json;base64,${Buffer.from( + map + ).toString('base64')}` +}