Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support pre-bundle #187

Closed
wants to merge 15 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
7 changes: 7 additions & 0 deletions packages/plugin-vue/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,13 @@ export interface Options {
* Use custom compiler-sfc instance. Can be used to force a specific version.
*/
compiler?: typeof _compiler

/**
* Pre-bundle SFC files
*
* @default true
*/
prebundle?: boolean
}
```

Expand Down
150 changes: 91 additions & 59 deletions packages/plugin-vue/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ import { handleHotUpdate, handleTypeDepChange } from './handleHotUpdate'
import { transformTemplateAsModule } from './template'
import { transformStyle } from './style'
import { EXPORT_HELPER_ID, helperCode } from './helper'
import type { SharedHooks } from './prebundle'
import { createOptimizeDeps } from './prebundle'

export { parseVueRequest } from './utils/query'
export type { VueQuery } from './utils/query'
Expand Down Expand Up @@ -80,6 +82,13 @@ export interface Options {
* Use custom compiler-sfc instance. Can be used to force a specific version.
*/
compiler?: typeof _compiler

/**
* Prebundle SFC libs
*
* @default false
*/
prebundle?: boolean
sxzz marked this conversation as resolved.
Show resolved Hide resolved
}

export interface ResolvedOptions extends Options {
Expand Down Expand Up @@ -115,7 +124,7 @@ export default function vuePlugin(rawOptions: Options = {}): Plugin {

let options: ResolvedOptions = {
isProduction: process.env.NODE_ENV === 'production',
compiler: null as any, // to be set in buildStart
compiler: null as any, // to be set in configResolved or buildStart
...rawOptions,
include,
exclude,
Expand All @@ -127,55 +136,9 @@ export default function vuePlugin(rawOptions: Options = {}): Plugin {
devToolsEnabled: process.env.NODE_ENV !== 'production',
}

return {
name: 'vite:vue',

handleHotUpdate(ctx) {
if (options.compiler.invalidateTypeCache) {
options.compiler.invalidateTypeCache(ctx.file)
}
if (typeDepToSFCMap.has(ctx.file)) {
return handleTypeDepChange(typeDepToSFCMap.get(ctx.file)!, ctx)
}
if (filter(ctx.file)) {
return handleHotUpdate(ctx, options)
}
},

config(config) {
return {
resolve: {
dedupe: config.build?.ssr ? [] : ['vue'],
},
define: {
__VUE_OPTIONS_API__: config.define?.__VUE_OPTIONS_API__ ?? true,
__VUE_PROD_DEVTOOLS__: config.define?.__VUE_PROD_DEVTOOLS__ ?? false,
},
ssr: {
external: config.legacy?.buildSsrCjsExternalHeuristics
? ['vue', '@vue/server-renderer']
: [],
},
}
},

configResolved(config) {
options = {
...options,
root: config.root,
sourceMap: config.command === 'build' ? !!config.build.sourcemap : true,
cssDevSourcemap: config.css?.devSourcemap ?? false,
isProduction: config.isProduction,
devToolsEnabled:
!!config.define!.__VUE_PROD_DEVTOOLS__ || !config.isProduction,
}
},

configureServer(server) {
options.devServer = server
},

buildStart() {
const hooks: SharedHooks = {
buildStart: () => {
// compatible with Rollup
const compiler = (options.compiler =
options.compiler || resolveCompiler(options.root))
if (compiler.invalidateTypeCache) {
Expand All @@ -184,8 +147,7 @@ export default function vuePlugin(rawOptions: Options = {}): Plugin {
})
}
},

async resolveId(id) {
resolveId: (id) => {
// component export helper
if (id === EXPORT_HELPER_ID) {
return id
Expand All @@ -195,8 +157,7 @@ export default function vuePlugin(rawOptions: Options = {}): Plugin {
return id
}
},

load(id, opt) {
load: (id, opt) => {
const ssr = opt?.ssr === true
if (id === EXPORT_HELPER_ID) {
return helperCode
Expand Down Expand Up @@ -228,8 +189,7 @@ export default function vuePlugin(rawOptions: Options = {}): Plugin {
}
}
},

transform(code, id, opt) {
transform: (pluginContext, code, id, opt) => {
const ssr = opt?.ssr === true
const { filename, query } = parseVueRequest(id)
if (query.raw || query.url) {
Expand All @@ -255,7 +215,7 @@ export default function vuePlugin(rawOptions: Options = {}): Plugin {
code,
filename,
options,
this,
pluginContext,
ssr,
customElementFilter(filename),
)
Expand All @@ -266,18 +226,90 @@ export default function vuePlugin(rawOptions: Options = {}): Plugin {
: getDescriptor(filename, options)!

if (query.type === 'template') {
return transformTemplateAsModule(code, descriptor, options, this, ssr)
return transformTemplateAsModule(
code,
descriptor,
options,
pluginContext,
ssr,
)
} else if (query.type === 'style') {
return transformStyle(
code,
descriptor,
Number(query.index),
options,
this,
pluginContext,
filename,
)
}
}
},
}

return {
name: 'vite:vue',

handleHotUpdate(ctx) {
if (options.compiler.invalidateTypeCache) {
options.compiler.invalidateTypeCache(ctx.file)
}
if (typeDepToSFCMap.has(ctx.file)) {
return handleTypeDepChange(typeDepToSFCMap.get(ctx.file)!, ctx)
}
if (filter(ctx.file)) {
return handleHotUpdate(ctx, options)
}
},

config(config) {
return {
resolve: {
dedupe: config.build?.ssr ? [] : ['vue'],
},
define: {
__VUE_OPTIONS_API__: config.define?.__VUE_OPTIONS_API__ ?? true,
__VUE_PROD_DEVTOOLS__: config.define?.__VUE_PROD_DEVTOOLS__ ?? false,
},
ssr: {
external: config.legacy?.buildSsrCjsExternalHeuristics
? ['vue', '@vue/server-renderer']
: [],
},
optimizeDeps: createOptimizeDeps(config, options, hooks),
}
},

configResolved(config) {
options = {
...options,
root: config.root,
sourceMap: config.command === 'build' ? !!config.build.sourcemap : true,
cssDevSourcemap: config.css?.devSourcemap ?? false,
isProduction: config.isProduction,
devToolsEnabled:
!!config.define!.__VUE_PROD_DEVTOOLS__ || !config.isProduction,
}
},

configureServer(server) {
options.devServer = server
},

buildStart() {
hooks.buildStart()
},

resolveId(id) {
return hooks.resolveId(id)
},

load(id, opt) {
return hooks.load(id, opt)
},

transform(code, id, opt) {
return hooks.transform(this, code, id, opt)
},
}
}
155 changes: 155 additions & 0 deletions packages/plugin-vue/src/prebundle.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
import { dirname, extname } from 'node:path'
import { readFile } from 'node:fs/promises'
import type {
LoadResult,
ResolveIdResult,
TransformPluginContext,
TransformResult,
} from 'rollup'
import type { DepOptimizationOptions, UserConfig } from 'vite'
import type { ResolvedOptions } from './index'

export type SharedHooks = {
buildStart: () => void
resolveId: (id: string) => Promise<ResolveIdResult> | ResolveIdResult
load: (
id: string,
opt?: { ssr?: boolean },
) => Promise<LoadResult> | LoadResult
transform: (
pluginContext: TransformPluginContext,
code: string,
id: string,
opt?: { ssr?: boolean },
) => Promise<TransformResult> | TransformResult
}

type ESBuildPlugin = NonNullable<
NonNullable<DepOptimizationOptions['esbuildOptions']>['plugins']
>[number]

const PLUGIN_NAME = 'vite-vue:prebundle'

export function createOptimizeDeps(
config: UserConfig,
options: ResolvedOptions,
hooks: SharedHooks,
): DepOptimizationOptions | undefined {
if (!options.prebundle) {
return config.optimizeDeps
}

const nextOptimizeDeps = (config.optimizeDeps ||= {})
const exts = (nextOptimizeDeps.extensions ||= [])
if (!exts.includes('.vue')) {
exts.push('.vue')
}

const esbuildOpts = (nextOptimizeDeps.esbuildOptions ||= {})
const plugins = (esbuildOpts.plugins ||= [])
plugins.push(createPrebundlePlugin(hooks))

return nextOptimizeDeps
}

function createPrebundlePlugin(hooks: SharedHooks): ESBuildPlugin {
return {
name: PLUGIN_NAME,
setup(build) {
if (build.initialOptions.plugins?.some((v) => v.name === 'vite:dep-scan'))
return

build.onStart(() => {
hooks.buildStart()
})

build.onResolve({ filter: /.*/ }, async ({ path: id }) => {
const resolveId = await hooks.resolveId(id)
if (!resolveId) {
return
}

return {
path: id,
namespace: PLUGIN_NAME,
}
})

build.onLoad({ filter: /.*/ }, async ({ path, suffix }) => {
const id = path + suffix
const resolveDir = dirname(path)
const { errors, warnings, pluginContext } = createFakeContext()

let code: string | undefined

const loadResult = await hooks.load(id)
if (loadResult) {
if (typeof loadResult === 'string') {
code = loadResult
} else if (typeof loadResult === 'object') {
code = loadResult.code!
}
}

if (!code) {
code = await readFile(path, 'utf8')
}

const transformResult = await hooks.transform(pluginContext, code, id)
if (transformResult) {
if (typeof transformResult === 'string') {
code = transformResult
} else if (typeof transformResult === 'object') {
code = transformResult.code
}
}

if (code) {
return {
contents: code,
errors,
warnings,
loader: guessLoader(path),
resolveDir,
}
}
})
},
}
}

const ExtToLoader = {
'.js': 'js',
'.mjs': 'js',
'.cjs': 'js',
'.jsx': 'jsx',
'.ts': 'ts',
'.cts': 'ts',
'.mts': 'ts',
'.tsx': 'tsx',
} as const

function guessLoader(id: string) {
return (
ExtToLoader[extname(id).toLowerCase() as keyof typeof ExtToLoader] || 'js'
)
}

function createFakeContext() {
const errors: { text: string }[] = []
const warnings: { text: string }[] = []
const pluginContext = {
error(message: any) {
errors.push({ text: String(message) })
},
warn(message: any) {
warnings.push({ text: String(message) })
},
} as any

return {
errors,
warnings,
pluginContext,
}
}