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 @@
+
+ Transforms
+ This should be cyan
+ {{ transformed }}
+
+
+
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')
})