From 3a3af6eeafbc9fc686fc909ec6a61c61283316fc Mon Sep 17 00:00:00 2001 From: Evan You Date: Thu, 22 Jul 2021 18:55:43 -0400 Subject: [PATCH] feat(plugin-vue): support importing vue files as custom elements --- packages/plugin-vue/README.md | 30 +++++++++++++++++++++++++ packages/plugin-vue/src/index.ts | 24 +++++++++++++++++++- packages/plugin-vue/src/main.ts | 38 ++++++++++++++++++++++++++++---- 3 files changed, 87 insertions(+), 5 deletions(-) diff --git a/packages/plugin-vue/README.md b/packages/plugin-vue/README.md index 1c5e29d3ec2ef3..390562169baf9e 100644 --- a/packages/plugin-vue/README.md +++ b/packages/plugin-vue/README.md @@ -21,6 +21,15 @@ export interface Options { ssr?: boolean isProduction?: boolean + /** + * Transform Vue SFCs into custom elements (requires Vue >= 3.2.0) + * - `true` -> all `*.vue` imports are converted into custom elements + * - `string | RegExp` -> matched files are converted into custom elements + * + * @default /\.ce\.vue$/ + */ + customElement?: boolean | string | RegExp | (string | RegExp)[] + // options to pass on to @vue/compiler-sfc script?: Partial template?: Partial @@ -71,6 +80,27 @@ export default { } ``` +## Using Vue SFCs as Custom Elements + +> Requires `vue@^3.2.0` + +By default, files ending in `*.ce.vue` will be processed as native Custom Elements when imported (created with `defineCustomElement` from Vue core): + +```js +import Example from './Example.ce.vue' + +// register +customElements.define('my-example', Example) + +// can also be instantiated +const myExample = new Example() +``` + +The `customElement` plugin option can be used to configure the behavior: + +- `{ customElement: true }` will import all `*.vue` files as Custom Elements. +- Use a string or regex pattern to change how files should be loaded as Custom Elements (this check is applied after `include` and `exclude` matches). + ## License MIT diff --git a/packages/plugin-vue/src/index.ts b/packages/plugin-vue/src/index.ts index 0a156f7fca2bf8..b57a90465e3810 100644 --- a/packages/plugin-vue/src/index.ts +++ b/packages/plugin-vue/src/index.ts @@ -43,6 +43,16 @@ export interface Options { script?: Partial template?: Partial style?: Partial + + /** + * Transform Vue SFCs into custom elements (requires Vue >= 3.2.0) + * - `true` -> all `*.vue` imports are converted into custom elements + * - `string | RegExp` -> matched files are converted into custom elements + * + * @default /\.ce\.vue$/ + */ + customElement?: boolean | string | RegExp | (string | RegExp)[] + /** * @deprecated the plugin now auto-detects whether it's being invoked for ssr. */ @@ -66,6 +76,11 @@ export default function vuePlugin(rawOptions: Options = {}): Plugin { rawOptions.exclude ) + const customElementFilter = + rawOptions.customElement === true + ? () => true + : createFilter(rawOptions.customElement || /\.ce\.vue$/) + return { name: 'vite:vue', @@ -144,7 +159,14 @@ export default function vuePlugin(rawOptions: Options = {}): Plugin { if (!query.vue) { // main request - return transformMain(code, filename, options, this, ssr) + return transformMain( + code, + filename, + options, + this, + ssr, + customElementFilter(filename) + ) } else { // sub block request const descriptor = getDescriptor(filename)! diff --git a/packages/plugin-vue/src/main.ts b/packages/plugin-vue/src/main.ts index 9751be4a6f8e4a..5f007701f722a3 100644 --- a/packages/plugin-vue/src/main.ts +++ b/packages/plugin-vue/src/main.ts @@ -14,6 +14,7 @@ import { transformTemplateInMain } from './template' import { isOnlyTemplateChanged, isEqualBlock } from './handleHotUpdate' import { RawSourceMap, SourceMapConsumer, SourceMapGenerator } from 'source-map' import { createRollupError } from './utils/error' +import { transformStyle } from './style' // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types export async function transformMain( @@ -21,7 +22,8 @@ export async function transformMain( filename: string, options: ResolvedOptions, pluginContext: TransformPluginContext, - ssr: boolean + ssr: boolean, + asCustomElement: boolean ) { const { root, devServer, isProduction } = options @@ -92,7 +94,9 @@ export async function transformMain( } // styles - const stylesCode = await genStyleCode(descriptor, pluginContext) + const stylesCode = asCustomElement + ? await genCustomElementStyleCode(descriptor, options, pluginContext) + : await genStyleCode(descriptor, pluginContext) // custom blocks const customBlocksCode = await genCustomBlockCode(descriptor, pluginContext) @@ -113,7 +117,6 @@ export async function transformMain( // expose filename during serve for devtools to pickup output.push(`_sfc_main.__file = ${JSON.stringify(filename)}`) } - output.push('export default _sfc_main') // HMR if ( @@ -132,7 +135,9 @@ export async function transformMain( output.push(`export const _rerender_only = true`) } output.push( - `import.meta.hot.accept(({ default: updated, _rerender_only }) => {`, + `import.meta.hot.accept(({ default: ${ + asCustomElement ? `{ def: updated }` : `updated` + }, _rerender_only }) => {`, ` if (_rerender_only) {`, ` __VUE_HMR_RUNTIME__.rerender(updated.__hmrId, updated.render)`, ` } else {`, @@ -185,6 +190,15 @@ export async function transformMain( resolvedMap.sourcesContent = templateMap.sourcesContent } + if (asCustomElement) { + output.push( + `import { defineCustomElement as __ce } from 'vue'`, + `export default __ce(_sfc_main)` + ) + } else { + output.push(`export default _sfc_main`) + } + return { code: output.join('\n'), map: resolvedMap || { @@ -397,3 +411,19 @@ function attrsToQuery( } return query } + +async function genCustomElementStyleCode( + descriptor: SFCDescriptor, + options: ResolvedOptions, + pluginContext: TransformPluginContext +) { + const styles = ( + await Promise.all( + descriptor.styles.map((style, index) => + transformStyle(style.content, descriptor, index, options, pluginContext) + ) + ) + ).map((res) => JSON.stringify(res!.code)) + + return `_sfc_main.styles = [${styles.join(',')}]` +}