Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
188 changes: 104 additions & 84 deletions packages/plugin-vue/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import { handleHotUpdate, handleTypeDepChange } from './handleHotUpdate'
import { transformTemplateAsModule } from './template'
import { transformStyle } from './style'
import { EXPORT_HELPER_ID, helperCode } from './helper'
import { exactRegex } from './utils/filter'

export { parseVueRequest } from './utils/query'
export type { VueQuery } from './utils/query'
Expand Down Expand Up @@ -338,110 +339,129 @@ export default function vuePlugin(rawOptions: Options = {}): Plugin<Api> {
}
},

async resolveId(id) {
// component export helper
if (id === EXPORT_HELPER_ID) {
return id
}
// serve sub-part requests (*?vue) as virtual modules
if (parseVueRequest(id).query.vue) {
return id
}
resolveId: {
filter: {
id: [exactRegex(EXPORT_HELPER_ID), /[?&]vue\b/],
},
handler(id) {
// component export helper
if (id === EXPORT_HELPER_ID) {
return id
}
// serve sub-part requests (*?vue) as virtual modules
if (parseVueRequest(id).query.vue) {
return id
}
},
},

load(id, opt) {
if (id === EXPORT_HELPER_ID) {
return helperCode
}
load: {
filter: {
id: [exactRegex(EXPORT_HELPER_ID), /[?&]vue\b/],
},
handler(id, opt) {
if (id === EXPORT_HELPER_ID) {
return helperCode
}

const ssr = opt?.ssr === true
const ssr = opt?.ssr === true

const { filename, query } = parseVueRequest(id)
const { filename, query } = parseVueRequest(id)

// select corresponding block for sub-part virtual modules
if (query.vue) {
if (query.src) {
return fs.readFileSync(filename, 'utf-8')
}
const descriptor = getDescriptor(filename, options.value)!
let block: SFCBlock | null | undefined
if (query.type === 'script') {
// handle <script> + <script setup> merge via compileScript()
block = resolveScript(
descriptor,
options.value,
ssr,
customElementFilter.value(filename),
)
} else if (query.type === 'template') {
block = descriptor.template!
} else if (query.type === 'style') {
block = descriptor.styles[query.index!]
} else if (query.index != null) {
block = descriptor.customBlocks[query.index]
}
if (block) {
return {
code: block.content,
map: block.map as any,
// select corresponding block for sub-part virtual modules
if (query.vue) {
if (query.src) {
return fs.readFileSync(filename, 'utf-8')
}
const descriptor = getDescriptor(filename, options.value)!
let block: SFCBlock | null | undefined
if (query.type === 'script') {
// handle <script> + <script setup> merge via compileScript()
block = resolveScript(
descriptor,
options.value,
ssr,
customElementFilter.value(filename),
)
} else if (query.type === 'template') {
block = descriptor.template!
} else if (query.type === 'style') {
block = descriptor.styles[query.index!]
} else if (query.index != null) {
block = descriptor.customBlocks[query.index]
}
if (block) {
return {
code: block.content,
map: block.map as any,
}
}
}
}
},
},

transform(code, id, opt) {
const ssr = opt?.ssr === true
const { filename, query } = parseVueRequest(id)

if (query.raw || query.url) {
return
}
transform: {
filter: {
// FIXME: should include other files if filter is updated
id: {
include: /[?&]vue\b|\.vue$/,
exclude: /[?&](?:raw|url)\b/,
},
},
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To have this part of change, we need to introduce a breaking change.
The filter has to have a same value after configResolved hook is called (because the value is passed to rolldown and cannot be modified), but currently options.value.include / options.value.exclude can be changed in any time.

I can revert this part for now so we can merge the other parts and make a separate PR if that's better.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it doesn't apply to those cases.
In this plugin-vue's case, options.value is exposed via api, which is meant to be used to update the option.value. So I think the contract here is that setting api.options will update the option.
On the other hand, in that unplugin-auto-import case, I think people won't expect options.include to be modified after the plugin is instantiated. For example, the code below would do that, but I guess people won't expect this to work unless explicitly documented.

const options = { include: [] }
setTimeout(() => {
  options.include.push('something') // or options.include = ['something-else']
}, 1000)
const plugin = autoImport(options)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@sxzz What are your thoughts on this? (since you introduced this feature)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Vue Macros is using this API to modify some options like template.compilerOptions.

Can we make api.include/exclude as a getter/setter, linking to transform.filter?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we make api.include/exclude as a getter/setter, linking to transform.filter?

Do you mean something like below?

let filter = {}
const p = {
  api: { get include() { return filter.include }, set include(v){ filter.include = v } },
  transform: { filter, handler() { /* omit */ } }
}

Is it to make it clear that include/exclude cannot be updated after configResolved hook is called?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, we can add a jsdoc

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@sxzz I've updated the PR 👍

handler(code, id, opt) {
const ssr = opt?.ssr === true
const { filename, query } = parseVueRequest(id)

if (!filter.value(filename) && !query.vue) {
return
}
if (query.raw || query.url) {
return
}

if (!query.vue) {
// main request
return transformMain(
code,
filename,
options.value,
this,
ssr,
customElementFilter.value(filename),
)
} else {
// sub block request
const descriptor: ExtendedSFCDescriptor = query.src
? getSrcDescriptor(filename, query) ||
getTempSrcDescriptor(filename, query)
: getDescriptor(filename, options.value)!

if (query.src) {
this.addWatchFile(filename)
if (!filter.value(filename) && !query.vue) {
return
}

if (query.type === 'template') {
return transformTemplateAsModule(
if (!query.vue) {
// main request
return transformMain(
code,
descriptor,
filename,
options.value,
this,
ssr,
customElementFilter.value(filename),
)
} else if (query.type === 'style') {
return transformStyle(
code,
descriptor,
Number(query.index || 0),
options.value,
this,
filename,
)
} else {
// sub block request
const descriptor: ExtendedSFCDescriptor = query.src
? getSrcDescriptor(filename, query) ||
getTempSrcDescriptor(filename, query)
: getDescriptor(filename, options.value)!

if (query.src) {
this.addWatchFile(filename)
}

if (query.type === 'template') {
return transformTemplateAsModule(
code,
descriptor,
options.value,
this,
ssr,
customElementFilter.value(filename),
)
} else if (query.type === 'style') {
return transformStyle(
code,
descriptor,
Number(query.index || 0),
options.value,
this,
filename,
)
}
}
}
},
},
}
}
52 changes: 36 additions & 16 deletions packages/plugin-vue/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { TraceMap, eachMapping } from '@jridgewell/trace-mapping'
import type { EncodedSourceMap as GenEncodedSourceMap } from '@jridgewell/gen-mapping'
import { addMapping, fromMap, toEncodedMap } from '@jridgewell/gen-mapping'
import { normalizePath, transformWithEsbuild } from 'vite'
import * as vite from 'vite'
import {
createDescriptor,
getDescriptor,
Expand Down Expand Up @@ -256,22 +257,41 @@ export async function transformMain(
/tsx?$/.test(lang) &&
!descriptor.script?.src // only normal script can have src
) {
const { code, map } = await transformWithEsbuild(
resolvedCode,
filename,
{
target: 'esnext',
charset: 'utf8',
// #430 support decorators in .vue file
// target can be overridden by esbuild config target
...options.devServer?.config.esbuild,
loader: 'ts',
sourcemap: options.sourceMap,
},
resolvedMap,
)
resolvedCode = code
resolvedMap = resolvedMap ? (map as any) : resolvedMap
if ('transformWithOxc' in vite) {
// @ts-ignore rolldown-vite
const { code, map } = await vite.transformWithOxc(
resolvedCode,
filename,
{
// #430 support decorators in .vue file
// target can be overridden by esbuild config target
// @ts-ignore
...options.devServer?.config.oxc,
lang: 'ts',
sourcemap: options.sourceMap,
},
resolvedMap,
)
resolvedCode = code
resolvedMap = resolvedMap ? (map as any) : resolvedMap
} else {
const { code, map } = await transformWithEsbuild(
resolvedCode,
filename,
{
target: 'esnext',
charset: 'utf8',
// #430 support decorators in .vue file
// target can be overridden by esbuild config target
...options.devServer?.config.esbuild,
loader: 'ts',
sourcemap: options.sourceMap,
},
resolvedMap,
)
resolvedCode = code
resolvedMap = resolvedMap ? (map as any) : resolvedMap
}
}

return {
Expand Down
8 changes: 8 additions & 0 deletions packages/plugin-vue/src/utils/filter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export function exactRegex(input: string): RegExp {
return new RegExp(`^${escapeRegex(input)}$`)
}

const escapeRegexRE = /[-/\\^$*+?.()|[\]{}]/g
function escapeRegex(str: string): string {
return str.replace(escapeRegexRE, '\\$&')
}