feat(sfc): allow sfcs to recursively self-reference in template via name inferred from filename

e.g. A file named `FooBar.vue` can refer to itself as `<FooBar/>`. This gets rid of the need for the `name` option.
This commit is contained in:
Evan You 2020-11-30 12:30:35 -05:00
parent 29d256c39d
commit 67d1aac6ae
8 changed files with 54 additions and 24 deletions

View File

@ -70,6 +70,14 @@ describe('compiler: element transform', () => {
expect(root.components).toContain(`Foo`) expect(root.components).toContain(`Foo`)
}) })
test('resolve implcitly self-referencing component', () => {
const { root } = parseWithElementTransform(`<Example/>`, {
filename: `/foo/bar/Example.vue?vue&type=template`
})
expect(root.helpers).toContain(RESOLVE_COMPONENT)
expect(root.components).toContain(`_self`)
})
test('static props', () => { test('static props', () => {
const { node } = parseWithElementTransform(`<div id="foo" class="bar" />`) const { node } = parseWithElementTransform(`<div id="foo" class="bar" />`)
expect(node).toMatchObject({ expect(node).toMatchObject({

View File

@ -128,6 +128,12 @@ interface SharedTransformCodegenOptions {
* Indicates that transforms and codegen should try to output valid TS code * Indicates that transforms and codegen should try to output valid TS code
*/ */
isTS?: boolean isTS?: boolean
/**
* Filename for source map generation.
* Also used for self-recursive reference in templates
* @default 'template.vue.html'
*/
filename?: string
} }
export interface TransformOptions extends SharedTransformCodegenOptions { export interface TransformOptions extends SharedTransformCodegenOptions {
@ -218,11 +224,6 @@ export interface CodegenOptions extends SharedTransformCodegenOptions {
* @default false * @default false
*/ */
sourceMap?: boolean sourceMap?: boolean
/**
* Filename for source map generation.
* @default 'template.vue.html'
*/
filename?: string
/** /**
* SFC scoped styles ID * SFC scoped styles ID
*/ */

View File

@ -24,7 +24,9 @@ import {
NOOP, NOOP,
PatchFlags, PatchFlags,
PatchFlagNames, PatchFlagNames,
EMPTY_OBJ EMPTY_OBJ,
capitalize,
camelize
} from '@vue/shared' } from '@vue/shared'
import { defaultOnError } from './errors' import { defaultOnError } from './errors'
import { import {
@ -79,7 +81,9 @@ export interface ImportItem {
path: string path: string
} }
export interface TransformContext extends Required<TransformOptions> { export interface TransformContext
extends Required<Omit<TransformOptions, 'filename'>> {
selfName: string | null
root: RootNode root: RootNode
helpers: Set<symbol> helpers: Set<symbol>
components: Set<string> components: Set<string>
@ -112,6 +116,7 @@ export interface TransformContext extends Required<TransformOptions> {
export function createTransformContext( export function createTransformContext(
root: RootNode, root: RootNode,
{ {
filename = '',
prefixIdentifiers = false, prefixIdentifiers = false,
hoistStatic = false, hoistStatic = false,
cacheHandlers = false, cacheHandlers = false,
@ -130,8 +135,10 @@ export function createTransformContext(
onError = defaultOnError onError = defaultOnError
}: TransformOptions }: TransformOptions
): TransformContext { ): TransformContext {
const nameMatch = filename.replace(/\?.*$/, '').match(/([^/\\]+)\.\w+$/)
const context: TransformContext = { const context: TransformContext = {
// options // options
selfName: nameMatch && capitalize(camelize(nameMatch[1])),
prefixIdentifiers, prefixIdentifiers,
hoistStatic, hoistStatic,
cacheHandlers, cacheHandlers,

View File

@ -263,7 +263,16 @@ export function resolveComponentType(
} }
} }
// 4. user component (resolve) // 4. Self referencing component (inferred from filename)
if (!__BROWSER__ && context.selfName) {
if (capitalize(camelize(tag)) === context.selfName) {
context.helper(RESOLVE_COMPONENT)
context.components.add(`_self`)
return toValidAssetId(`_self`, `component`)
}
}
// 5. user component (resolve)
context.helper(RESOLVE_COMPONENT) context.helper(RESOLVE_COMPONENT)
context.components.add(tag) context.components.add(tag)
return toValidAssetId(tag, `component`) return toValidAssetId(tag, `component`)

View File

@ -33,7 +33,7 @@ export interface SFCBlock {
export interface SFCTemplateBlock extends SFCBlock { export interface SFCTemplateBlock extends SFCBlock {
type: 'template' type: 'template'
functional?: boolean ast: ElementNode
} }
export interface SFCScriptBlock extends SFCBlock { export interface SFCScriptBlock extends SFCBlock {
@ -79,7 +79,7 @@ export function parse(
source: string, source: string,
{ {
sourceMap = true, sourceMap = true,
filename = 'component.vue', filename = 'anonymous.vue',
sourceRoot = '', sourceRoot = '',
pad = false, pad = false,
compiler = CompilerDOM compiler = CompilerDOM
@ -143,31 +143,32 @@ export function parse(
switch (node.tag) { switch (node.tag) {
case 'template': case 'template':
if (!descriptor.template) { if (!descriptor.template) {
descriptor.template = createBlock( const templateBlock = (descriptor.template = createBlock(
node, node,
source, source,
false false
) as SFCTemplateBlock ) as SFCTemplateBlock)
templateBlock.ast = node
} else { } else {
errors.push(createDuplicateBlockError(node)) errors.push(createDuplicateBlockError(node))
} }
break break
case 'script': case 'script':
const block = createBlock(node, source, pad) as SFCScriptBlock const scriptBlock = createBlock(node, source, pad) as SFCScriptBlock
const isSetup = !!block.attrs.setup const isSetup = !!scriptBlock.attrs.setup
if (isSetup && !descriptor.scriptSetup) { if (isSetup && !descriptor.scriptSetup) {
descriptor.scriptSetup = block descriptor.scriptSetup = scriptBlock
break break
} }
if (!isSetup && !descriptor.script) { if (!isSetup && !descriptor.script) {
descriptor.script = block descriptor.script = scriptBlock
break break
} }
errors.push(createDuplicateBlockError(node, isSetup)) errors.push(createDuplicateBlockError(node, isSetup))
break break
case 'style': case 'style':
const style = createBlock(node, source, pad) as SFCStyleBlock const styleBlock = createBlock(node, source, pad) as SFCStyleBlock
if (style.attrs.vars) { if (styleBlock.attrs.vars) {
errors.push( errors.push(
new SyntaxError( new SyntaxError(
`<style vars> has been replaced by a new proposal: ` + `<style vars> has been replaced by a new proposal: ` +
@ -175,7 +176,7 @@ export function parse(
) )
) )
} }
descriptor.styles.push(style) descriptor.styles.push(styleBlock)
break break
default: default:
descriptor.customBlocks.push(createBlock(node, source, pad)) descriptor.customBlocks.push(createBlock(node, source, pad))
@ -290,8 +291,6 @@ function createBlock(
} else if (p.name === 'module') { } else if (p.name === 'module') {
;(block as SFCStyleBlock).module = attrs[p.name] ;(block as SFCStyleBlock).module = attrs[p.name]
} }
} else if (type === 'template' && p.name === 'functional') {
;(block as SFCTemplateBlock).functional = true
} else if (type === 'script' && p.name === 'setup') { } else if (type === 'script' && p.name === 'setup') {
;(block as SFCScriptBlock).setup = attrs.setup ;(block as SFCScriptBlock).setup = attrs.setup
} }

View File

@ -811,7 +811,7 @@ export function formatComponentName(
? Component.displayName || Component.name ? Component.displayName || Component.name
: Component.name : Component.name
if (!name && Component.__file) { if (!name && Component.__file) {
const match = Component.__file.match(/([^/\\]+)\.vue$/) const match = Component.__file.match(/([^/\\]+)\.\w+$/)
if (match) { if (match) {
name = match[1] name = match[1]
} }

View File

@ -67,6 +67,12 @@ function resolveAsset(
// self name has highest priority // self name has highest priority
if (type === COMPONENTS) { if (type === COMPONENTS) {
// special self referencing call generated by compiler
// inferred from SFC filename
if (name === `_self`) {
return Component
}
const selfName = const selfName =
(Component as FunctionalComponent).displayName || Component.name (Component as FunctionalComponent).displayName || Component.name
if ( if (

View File

@ -53,7 +53,7 @@ window.init = () => {
const compileFn = ssrMode.value ? ssrCompile : compile const compileFn = ssrMode.value ? ssrCompile : compile
const start = performance.now() const start = performance.now()
const { code, ast, map } = compileFn(source, { const { code, ast, map } = compileFn(source, {
filename: 'template.vue', filename: 'ExampleTemplate.vue',
...compilerOptions, ...compilerOptions,
sourceMap: true, sourceMap: true,
onError: err => { onError: err => {
@ -150,7 +150,7 @@ window.init = () => {
clearEditorDecos() clearEditorDecos()
if (lastSuccessfulMap) { if (lastSuccessfulMap) {
const pos = lastSuccessfulMap.generatedPositionFor({ const pos = lastSuccessfulMap.generatedPositionFor({
source: 'template.vue', source: 'ExampleTemplate.vue',
line: e.position.lineNumber, line: e.position.lineNumber,
column: e.position.column - 1 column: e.position.column - 1
}) })