mirror of https://github.com/vuejs/core.git
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:
parent
29d256c39d
commit
67d1aac6ae
|
@ -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({
|
||||||
|
|
|
@ -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
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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`)
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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]
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 (
|
||||||
|
|
|
@ -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
|
||||||
})
|
})
|
||||||
|
|
Loading…
Reference in New Issue