From 87ee9981f8cd03b13f959e3754f9e48697e66022 Mon Sep 17 00:00:00 2001 From: Evan You Date: Sun, 10 May 2020 01:20:01 -0400 Subject: [PATCH] feat: transforms --- playground/App.vue | 5 ++- playground/TestTransform.vue | 18 ++++++++++ playground/plugins/jsPlugin.js | 14 ++++++++ playground/plugins/sassPlugin.js | 19 ++++++++++ playground/testTransform.js | 1 + playground/testTransform.scss | 5 +++ playground/vite.config.ts | 5 ++- src/node/build/buildPluginCss.ts | 27 ++++++++++++-- src/node/build/index.ts | 7 +++- src/node/cli.ts | 6 ++-- src/node/config.ts | 22 ++++-------- src/node/server/index.ts | 32 +++++++++-------- src/node/server/serverPluginCss.ts | 17 +++++++-- src/node/server/serverPluginHmr.ts | 13 +++++-- src/node/transform.ts | 57 ++++++++++++++++++++++++++++++ src/node/utils/pathUtils.ts | 20 +++++++++++ test/test.js | 14 ++++++++ 17 files changed, 239 insertions(+), 43 deletions(-) create mode 100644 playground/TestTransform.vue create mode 100644 playground/plugins/jsPlugin.js create mode 100644 playground/plugins/sassPlugin.js create mode 100644 playground/testTransform.js create mode 100644 playground/testTransform.scss create mode 100644 src/node/transform.ts diff --git a/playground/App.vue b/playground/App.vue index ad750eb7e02fbd..50f8706394e93b 100644 --- a/playground/App.vue +++ b/playground/App.vue @@ -13,11 +13,12 @@ - + +

Async Component

@@ -36,6 +37,7 @@ import TestJsonImport from './TestJsonImport.vue' import TestTs from './ts/TestTs.vue' import TestJsx from './TestJsx.vue' import TestAlias from './TestAlias.vue' +import TestTransform from './TestTransform.vue' export default { data: () => ({ @@ -55,6 +57,7 @@ export default { TestTs, TestJsx, TestAlias, + TestTransform, TestAsync: defineAsyncComponent(() => import('./TestAsync.vue')) } } diff --git a/playground/TestTransform.vue b/playground/TestTransform.vue new file mode 100644 index 00000000000000..446d25c4f78ab4 --- /dev/null +++ b/playground/TestTransform.vue @@ -0,0 +1,18 @@ + + + diff --git a/playground/plugins/jsPlugin.js b/playground/plugins/jsPlugin.js new file mode 100644 index 00000000000000..02f5dd62024754 --- /dev/null +++ b/playground/plugins/jsPlugin.js @@ -0,0 +1,14 @@ +export const jsPlugin = { + transforms: [ + { + test(id) { + return id.endsWith('testTransform.js') + }, + transform(code) { + return code.replace(/__TEST_TRANSFORM__ = (\d)/, (matched, n) => { + return `__TEST_TRANSFORM__ = ${Number(n) + 1}` + }) + } + } + ] +} diff --git a/playground/plugins/sassPlugin.js b/playground/plugins/sassPlugin.js new file mode 100644 index 00000000000000..e6aa7fe9be2687 --- /dev/null +++ b/playground/plugins/sassPlugin.js @@ -0,0 +1,19 @@ +import sass from 'sass' + +export const sassPlugin = { + transforms: [ + { + as: 'css', + test(id) { + return id.endsWith('.scss') + }, + transform(code) { + return sass + .renderSync({ + data: code + }) + .css.toString() + } + } + ] +} diff --git a/playground/testTransform.js b/playground/testTransform.js new file mode 100644 index 00000000000000..40f452a8ec89ad --- /dev/null +++ b/playground/testTransform.js @@ -0,0 +1 @@ +export const __TEST_TRANSFORM__ = 1 diff --git a/playground/testTransform.scss b/playground/testTransform.scss new file mode 100644 index 00000000000000..f8ddd088c2f822 --- /dev/null +++ b/playground/testTransform.scss @@ -0,0 +1,5 @@ +$color: cyan; + +.transform-scss { + color: $color; +} diff --git a/playground/vite.config.ts b/playground/vite.config.ts index d531b707333daf..5afa69946aa1cb 100644 --- a/playground/vite.config.ts +++ b/playground/vite.config.ts @@ -1,4 +1,6 @@ import type { UserConfig } from 'vite' +import { sassPlugin } from './plugins/sassPlugin' +import { jsPlugin } from './plugins/jsPlugin' const config: UserConfig = { alias: { @@ -8,7 +10,8 @@ const config: UserConfig = { factory: 'h', fragment: 'Fragment' }, - minify: false + minify: false, + plugins: [sassPlugin, jsPlugin] } export default config diff --git a/src/node/build/buildPluginCss.ts b/src/node/build/buildPluginCss.ts index ae6f5b773e45f3..ddcc3f95fcc52f 100644 --- a/src/node/build/buildPluginCss.ts +++ b/src/node/build/buildPluginCss.ts @@ -1,7 +1,13 @@ import path from 'path' import { Plugin } from 'rollup' import { resolveAsset, registerAssets } from './buildPluginAsset' -import { isExternalUrl, asyncReplace, loadPostcssConfig } from '../utils' +import { + isExternalUrl, + asyncReplace, + loadPostcssConfig, + parseWithQuery +} from '../utils' +import { Transform } from '../config' const debug = require('debug')('vite:build:css') @@ -13,15 +19,30 @@ export const createBuildCssPlugin = ( assetsDir: string, cssFileName: string, minify: boolean, - inlineLimit: number + inlineLimit: number, + transforms: Transform[] ): Plugin => { const styles: Map = new Map() const assets = new Map() + transforms = transforms.filter((t) => t.as === 'css') return { name: 'vite:css', async transform(css: string, id: string) { - if (id.endsWith('.css')) { + let transformed = false + + if (transforms.length) { + const { path, query } = parseWithQuery(id) + for (const t of transforms) { + if (t.test(path, query)) { + css = await t.transform(css, true) + transformed = true + break + } + } + } + + if (transformed || id.endsWith('.css')) { // process url() - register referenced files as assets // and rewrite the url to the resolved public path if (urlRE.test(css)) { diff --git a/src/node/build/index.ts b/src/node/build/index.ts index b1c2869a6db9fe..67d277a9427c8e 100644 --- a/src/node/build/index.ts +++ b/src/node/build/index.ts @@ -12,6 +12,7 @@ import { createEsbuildPlugin } from './buildPluginEsbuild' import { createReplacePlugin } from './buildPluginReplace' import { stopService } from '../esbuildService' import { BuildConfig } from '../config' +import { createBuildJsTransformPlugin } from '../transform' export interface BuildResult { html: string @@ -56,6 +57,7 @@ export async function build(options: BuildConfig = {}): Promise { assetsDir = 'assets', assetsInlineLimit = 4096, alias = {}, + transforms = [], resolvers = [], vueCompilerOptions, rollupInputOptions = {}, @@ -113,6 +115,8 @@ export async function build(options: BuildConfig = {}): Promise { compilerOptions: vueCompilerOptions }), require('@rollup/plugin-json')(), + // user transforms + ...(transforms.length ? [createBuildJsTransformPlugin(transforms)] : []), require('@rollup/plugin-node-resolve')({ rootDir: root, extensions: supportedExts @@ -135,7 +139,8 @@ export async function build(options: BuildConfig = {}): Promise { assetsDir, cssFileName, !!minify, - assetsInlineLimit + assetsInlineLimit, + transforms ), // vite:asset createBuildAssetPlugin( diff --git a/src/node/cli.ts b/src/node/cli.ts index c55dd8e4d8a6e1..9730fd385183a8 100644 --- a/src/node/cli.ts +++ b/src/node/cli.ts @@ -8,7 +8,7 @@ if (argv.debug) { import os from 'os' import chalk from 'chalk' import { Ora } from 'ora' -import { ServerConfig, BuildConfig, resolveConfig } from './config' +import { UserConfig, resolveConfig } from './config' function logHelp() { console.log(` @@ -91,7 +91,7 @@ async function resolveOptions() { } async function runServe( - options: ServerConfig & { + options: UserConfig & { port?: number open?: boolean } @@ -141,7 +141,7 @@ async function runServe( }) } -async function runBuild(options: BuildConfig) { +async function runBuild(options: UserConfig) { let spinner: Ora | undefined const msg = 'Building for production...' if (process.env.DEBUG || process.env.NODE_ENV === 'test') { diff --git a/src/node/config.ts b/src/node/config.ts index 4aaef4a728e04f..f5eed1ed2dd9f2 100644 --- a/src/node/config.ts +++ b/src/node/config.ts @@ -10,8 +10,9 @@ import Rollup, { InputOptions as RollupInputOptions, OutputOptions as RollupOutputOptions } from 'rollup' +import { Transform } from './transform' -export { Resolver } +export { Resolver, Transform } /** * Options shared between server and build. @@ -138,21 +139,8 @@ export interface BuildConfig extends SharedConfig { } export interface UserConfig extends BuildConfig { - configureServer?: ServerPlugin plugins?: Plugin[] -} - -export type Condition = RegExp | RegExp[] | (() => boolean) - -export interface Transform { - include?: Condition - exclude?: Condition - query?: Condition - /** - * @default 'js' - */ - as?: 'js' | 'css' - transform?: (code: string) => string | Promise + configureServer?: ServerPlugin } export interface Plugin @@ -248,6 +236,9 @@ export async function resolveConfig( for (const plugin of config.plugins) { config = resolvePlugin(config, plugin) } + // delete plugins so it doesn't get passed to `createServer` as server + // plugins. + delete config.plugins } require('debug')('vite:config')( @@ -289,6 +280,7 @@ async function loadConfigFromBundledFile( function resolvePlugin(config: UserConfig, plugin: Plugin): UserConfig { return { + ...config, alias: { ...plugin.alias, ...config.alias diff --git a/src/node/server/index.ts b/src/node/server/index.ts index 9dcdad9621ff01..ef849aeca7bbd8 100644 --- a/src/node/server/index.ts +++ b/src/node/server/index.ts @@ -12,6 +12,7 @@ import { cssPlugin } from './serverPluginCss' import { assetPathPlugin } from './serverPluginAssets' import { esbuildPlugin } from './serverPluginEsbuild' import { ServerConfig } from '../config' +import { createServerTransformPlugin } from '../transform' export { rewriteImports } from './serverPluginModuleRewrite' @@ -26,25 +27,15 @@ export interface ServerPluginContext { config: ServerConfig } -const internalPlugins: ServerPlugin[] = [ - hmrPlugin, - moduleRewritePlugin, - moduleResolvePlugin, - vuePlugin, - esbuildPlugin, - jsonPlugin, - cssPlugin, - assetPathPlugin, - serveStaticPlugin -] - export function createServer(config: ServerConfig = {}): Server { const { root = process.cwd(), plugins = [], resolvers = [], - alias = {} + alias = {}, + transforms = [] } = config + const app = new Koa() const server = http.createServer(app.callback()) const watcher = chokidar.watch(root, { @@ -60,7 +51,20 @@ export function createServer(config: ServerConfig = {}): Server { config } - ;[...plugins, ...internalPlugins].forEach((m) => m(context)) + const resolvedPlugins = [ + ...plugins, + hmrPlugin, + moduleRewritePlugin, + moduleResolvePlugin, + vuePlugin, + esbuildPlugin, + jsonPlugin, + cssPlugin, + assetPathPlugin, + ...(transforms.length ? [createServerTransformPlugin(transforms)] : []), + serveStaticPlugin + ] + resolvedPlugins.forEach((m) => m(context)) return server } diff --git a/src/node/server/serverPluginCss.ts b/src/node/server/serverPluginCss.ts index 3c38e5c763e264..1d5ce03243e56c 100644 --- a/src/node/server/serverPluginCss.ts +++ b/src/node/server/serverPluginCss.ts @@ -12,12 +12,18 @@ interface ProcessedEntry { const processedCSS = new Map() -export const cssPlugin: ServerPlugin = ({ root, app, watcher, resolver }) => { +export const cssPlugin: ServerPlugin = ({ + root, + app, + watcher, + resolver, + config +}) => { app.use(async (ctx, next) => { await next() // handle .css imports if ( - ctx.path.endsWith('.css') && + ctx.response.is('css') && // note ctx.body could be null if upstream set status to 304 ctx.body ) { @@ -46,14 +52,19 @@ export const cssPlugin: ServerPlugin = ({ root, app, watcher, resolver }) => { if (!processedCSS.has(ctx.path)) { await processCss(ctx) } + ctx.type = 'css' ctx.body = processedCSS.get(ctx.path)!.css } } }) // handle hmr + const cssTransforms = config.transforms + ? config.transforms.filter((t) => t.as === 'css') + : [] + watcher.on('change', (file) => { - if (file.endsWith('.css')) { + if (file.endsWith('.css') || cssTransforms.some((t) => t.test(file, {}))) { if (srcImportMap.has(file)) { // this is a vue src import, skip return diff --git a/src/node/server/serverPluginHmr.ts b/src/node/server/serverPluginHmr.ts index d969f40d6a2123..28027d95992114 100644 --- a/src/node/server/serverPluginHmr.ts +++ b/src/node/server/serverPluginHmr.ts @@ -85,7 +85,8 @@ export const hmrPlugin: ServerPlugin = ({ app, server, watcher, - resolver + resolver, + config }) => { app.use(async (ctx, next) => { if (ctx.path !== hmrClientPublicPath) { @@ -126,11 +127,19 @@ export const hmrPlugin: ServerPlugin = ({ watcher.handleJSReload = handleJSReload watcher.send = send + // exclude files declared as css by user transforms + const cssTransforms = config.transforms + ? config.transforms.filter((t) => t.as === 'css') + : [] + watcher.on('change', async (file) => { const timestamp = Date.now() if (file.endsWith('.vue')) { handleVueReload(file, timestamp) - } else if (!file.endsWith('.css') || file.endsWith('.module.css')) { + } else if ( + file.endsWith('.module.css') || + !(file.endsWith('.css') || cssTransforms.some((t) => t.test(file, {}))) + ) { // everything except plain .css are considered HMR dependencies. // plain css has its own HMR logic in ./serverPluginCss.ts. handleJSReload(file, timestamp) diff --git a/src/node/transform.ts b/src/node/transform.ts new file mode 100644 index 00000000000000..9fa2c5160e615f --- /dev/null +++ b/src/node/transform.ts @@ -0,0 +1,57 @@ +import { ServerPlugin } from './server' +import { Plugin as RollupPlugin } from 'rollup' +import { parseWithQuery, readBody, isImportRequest } from './utils' + +export interface Transform { + /** + * @default 'js' + */ + as?: 'js' | 'css' + test: ( + path: string, + query: Record + ) => boolean + transform: (code: string, isImport: boolean) => string | Promise +} + +export function normalizeTransforms(transforms: Transform[]) {} + +export function createServerTransformPlugin( + transforms: Transform[] +): ServerPlugin { + return ({ app }) => { + app.use(async (ctx, next) => { + await next() + for (const t of transforms) { + if (t.test(ctx.path, ctx.query)) { + ctx.type = t.as || 'js' + if (ctx.body) { + const code = await readBody(ctx.body) + if (code) { + ctx.body = await t.transform(code, isImportRequest(ctx)) + ctx._transformed = true + } + } + } + } + }) + } +} + +export function createBuildJsTransformPlugin( + transforms: Transform[] +): RollupPlugin { + transforms = transforms.filter((t) => t.as === 'js' || !t.as) + + return { + name: 'vite:transforms', + async transform(code, id) { + const { path, query } = parseWithQuery(id) + for (const t of transforms) { + if (t.test(path, query)) { + return t.transform(code, true) + } + } + } + } +} diff --git a/src/node/utils/pathUtils.ts b/src/node/utils/pathUtils.ts index 06cbe29a984ead..3e3177989f4b04 100644 --- a/src/node/utils/pathUtils.ts +++ b/src/node/utils/pathUtils.ts @@ -1,6 +1,7 @@ import { Context } from 'koa' import path from 'path' import slash from 'slash' +import qs from 'querystring' import { InternalResolver } from '../resolver' export const queryRE = /\?.*$/ @@ -19,6 +20,25 @@ export const resolveRelativeRequest = (importer: string, id: string) => { } } +export const parseWithQuery = ( + id: string +): { + path: string + query: Record +} => { + const queryMatch = id.match(queryRE) + if (queryMatch) { + return { + path: slash(cleanUrl(id)), + query: qs.parse(queryMatch[0].slice(1)) + } + } + return { + path: id, + query: {} + } +} + const httpRE = /^https?:\/\// export const isExternalUrl = (url: string) => httpRE.test(url) diff --git a/test/test.js b/test/test.js index 7d1f4e189126e1..176a4d746d128a 100644 --- a/test/test.js +++ b/test/test.js @@ -288,6 +288,20 @@ describe('vite', () => { } }) + test('transforms', async () => { + const el = await getEl('.transform-scss') + expect(await getComputedColor(el)).toBe('rgb(0, 255, 255)') + expect(await getText('.transform-js')).toBe('2') + if (!isBuild) { + await updateFile('testTransform.scss', (c) => + c.replace('cyan', 'rgb(0, 0, 0)') + ) + await expectByPolling(() => getComputedColor(el), 'rgb(0, 0, 0)') + await updateFile('testTransform.js', (c) => c.replace('= 1', '= 2')) + await expectByPolling(() => getText('.transform-js'), '3') + } + }) + test('async component', async () => { await expectByPolling(() => getText('.async'), 'should show up') })