feat(compiler-sfc): compileScript parseOnly mode

This is an internal feature meant for IDE support
This commit is contained in:
Evan You 2021-06-29 17:56:49 -04:00
parent 96cc335aa7
commit 601a290caa
3 changed files with 277 additions and 58 deletions

View File

@ -0,0 +1,77 @@
import { TextRange } from '../src/parse'
import { compileSFCScript } from './utils'
describe('compileScript parseOnly mode', () => {
function compile(src: string) {
return compileSFCScript(src, { parseOnly: true })
}
function getRange(src: string, range: TextRange) {
return src.slice(range.start, range.end)
}
test('bindings', () => {
const scriptSrc = `
import { foo } from './x'
`
const scriptSetupSrc = `
import { bar } from './x'
const a = 123
function b() {}
class c {}
`
const src = `
<script>${scriptSrc}</script>
<script setup>${scriptSetupSrc}</script>
`
const { ranges } = compile(src)
expect(getRange(scriptSrc, ranges!.scriptBindings[0])).toBe('foo')
expect(
ranges!.scriptSetupBindings.map(r => getRange(scriptSetupSrc, r))
).toMatchObject(['bar', 'a', 'b', 'c'])
})
test('defineProps', () => {
const src = `
defineProps({ foo: String })
`
const { ranges } = compile(`<script setup>${src}</script>`)
expect(getRange(src, ranges!.propsRuntimeArg!)).toBe(`{ foo: String }`)
})
test('defineProps (type)', () => {
const src = `
interface Props { x?: number }
defineProps<Props>()
`
const { ranges } = compile(`<script setup lang="ts">${src}</script>`)
expect(getRange(src, ranges!.propsTypeArg!)).toBe(`Props`)
})
test('withDefaults', () => {
const src = `
interface Props { x?: number }
withDefaults(defineProps<Props>(), { x: 1 })
`
const { ranges } = compile(`<script setup lang="ts">${src}</script>`)
expect(getRange(src, ranges!.withDefaultsArg!)).toBe(`{ x: 1 }`)
})
test('defineEmits', () => {
const src = `
defineEmits(['foo'])
`
const { ranges } = compile(`<script setup>${src}</script>`)
expect(getRange(src, ranges!.emitsRuntimeArg!)).toBe(`['foo']`)
})
test('defineEmits (type)', () => {
const src = `
defineEmits<{ (e: 'x'): void }>()
`
const { ranges } = compile(`<script setup lang="ts">${src}</script>`)
expect(getRange(src, ranges!.emitsTypeArg!)).toBe(`{ (e: 'x'): void }`)
})
})

View File

@ -1,6 +1,11 @@
import MagicString from 'magic-string' import MagicString from 'magic-string'
import { BindingMetadata, BindingTypes, UNREF } from '@vue/compiler-core' import { BindingMetadata, BindingTypes, UNREF } from '@vue/compiler-core'
import { SFCDescriptor, SFCScriptBlock } from './parse' import {
ScriptSetupTextRanges,
SFCDescriptor,
SFCScriptBlock,
TextRange
} from './parse'
import { parse as _parse, ParserOptions, ParserPlugin } from '@babel/parser' import { parse as _parse, ParserOptions, ParserPlugin } from '@babel/parser'
import { babelParserDefaultPlugins, generateCodeFrame } from '@vue/shared' import { babelParserDefaultPlugins, generateCodeFrame } from '@vue/shared'
import { import {
@ -71,7 +76,31 @@ export interface SFCScriptCompileOptions {
* from being hot-reloaded separately from component state. * from being hot-reloaded separately from component state.
*/ */
inlineTemplate?: boolean inlineTemplate?: boolean
/**
* Options for template compilation when inlining. Note these are options that
* would normally be pased to `compiler-sfc`'s own `compileTemplate()`, not
* options passed to `compiler-dom`.
*/
templateOptions?: Partial<SFCTemplateCompileOptions> templateOptions?: Partial<SFCTemplateCompileOptions>
/**
* Skip codegen and only return AST / binding / text range information.
* Also makes the call error-tolerant.
* Used for IDE support.
*/
parseOnly?: boolean
}
interface ImportBinding {
isType: boolean
imported: string
source: string
rangeNode: Node
isFromSetup: boolean
}
interface VariableBinding {
type: BindingTypes
rangeNode: Node
} }
/** /**
@ -83,10 +112,22 @@ export function compileScript(
sfc: SFCDescriptor, sfc: SFCDescriptor,
options: SFCScriptCompileOptions options: SFCScriptCompileOptions
): SFCScriptBlock { ): SFCScriptBlock {
const { script, scriptSetup, source, filename } = sfc let { script, scriptSetup, source, filename } = sfc
// feature flags
const enableRefSugar = !!options.refSugar
const parseOnly = !!options.parseOnly
if (scriptSetup) { if (scriptSetup) {
warnExperimental(`<script setup>`, 227) !parseOnly && warnExperimental(`<script setup>`, 227)
} else if (parseOnly) {
// in parse-only mode, construct a fake script setup so we still perform
// the full parse logic.
scriptSetup = {
type: 'script',
content: '',
attrs: {},
loc: null as any
}
} }
// for backwards compat // for backwards compat
@ -134,7 +175,8 @@ export function compileScript(
try { try {
const scriptAst = _parse(script.content, { const scriptAst = _parse(script.content, {
plugins, plugins,
sourceType: 'module' sourceType: 'module',
errorRecovery: parseOnly
}).program.body }).program.body
const bindings = analyzeScriptBindings(scriptAst) const bindings = analyzeScriptBindings(scriptAst)
let content = script.content let content = script.content
@ -165,7 +207,8 @@ export function compileScript(
if (script && scriptLang !== scriptSetupLang) { if (script && scriptLang !== scriptSetupLang) {
throw new Error( throw new Error(
`[@vue/compiler-sfc] <script> and <script setup> must have the same language type.` `[@vue/compiler-sfc] <script> and <script setup> must have the same ` +
`language type.`
) )
} }
@ -174,22 +217,22 @@ export function compileScript(
return scriptSetup return scriptSetup
} }
const defaultTempVar = `__default__` // metadata that needs to be returned
const bindingMetadata: BindingMetadata = {} const bindingMetadata: BindingMetadata = {}
const helperImports: Set<string> = new Set() const ranges: ScriptSetupTextRanges | undefined = parseOnly
const userImports: Record< ? {
string, scriptBindings: [],
{ scriptSetupBindings: []
isType: boolean
imported: string
source: string
} }
> = Object.create(null) : undefined
const defaultTempVar = `__default__`
const helperImports: Set<string> = new Set()
const userImports: Record<string, ImportBinding> = Object.create(null)
const userImportAlias: Record<string, string> = Object.create(null) const userImportAlias: Record<string, string> = Object.create(null)
const setupBindings: Record<string, BindingTypes> = Object.create(null) const setupBindings: Record<string, VariableBinding> = Object.create(null)
const refBindings: Record<string, BindingTypes> = Object.create(null) const refBindings: Record<string, VariableBinding> = Object.create(null)
const refIdentifiers: Set<Identifier> = new Set() const refIdentifiers: Set<Identifier> = new Set()
const enableRefSugar = !!options.refSugar
let defaultExport: Node | undefined let defaultExport: Node | undefined
let hasDefinePropsCall = false let hasDefinePropsCall = false
let hasDefineEmitCall = false let hasDefineEmitCall = false
@ -197,9 +240,15 @@ export function compileScript(
let propsRuntimeDecl: Node | undefined let propsRuntimeDecl: Node | undefined
let propsRuntimeDefaults: Node | undefined let propsRuntimeDefaults: Node | undefined
let propsTypeDecl: TSTypeLiteral | TSInterfaceBody | undefined let propsTypeDecl: TSTypeLiteral | TSInterfaceBody | undefined
let propsTypeDeclRaw: Node | undefined
let propsIdentifier: string | undefined let propsIdentifier: string | undefined
let emitRuntimeDecl: Node | undefined let emitsRuntimeDecl: Node | undefined
let emitTypeDecl: TSFunctionType | TSTypeLiteral | TSInterfaceBody | undefined let emitsTypeDecl:
| TSFunctionType
| TSTypeLiteral
| TSInterfaceBody
| undefined
let emitsTypeDeclRaw: Node | undefined
let emitIdentifier: string | undefined let emitIdentifier: string | undefined
let hasAwait = false let hasAwait = false
let hasInlinedSsrRenderFn = false let hasInlinedSsrRenderFn = false
@ -227,6 +276,7 @@ export function compileScript(
offset: number offset: number
): Statement[] { ): Statement[] {
try { try {
options.errorRecovery = parseOnly
return _parse(input, options).program.body return _parse(input, options).program.body
} catch (e) { } catch (e) {
e.message = `[@vue/compiler-sfc] ${e.message}\n\n${ e.message = `[@vue/compiler-sfc] ${e.message}\n\n${
@ -254,7 +304,9 @@ export function compileScript(
source: string, source: string,
local: string, local: string,
imported: string | false, imported: string | false,
isType: boolean isType: boolean,
isFromSetup: boolean,
rangeNode: Node
) { ) {
if (source === 'vue' && imported) { if (source === 'vue' && imported) {
userImportAlias[imported] = local userImportAlias[imported] = local
@ -262,7 +314,9 @@ export function compileScript(
userImports[local] = { userImports[local] = {
isType, isType,
imported: imported || 'default', imported: imported || 'default',
source source,
rangeNode,
isFromSetup
} }
} }
@ -288,8 +342,9 @@ export function compileScript(
) )
} }
propsTypeDeclRaw = node.typeParameters.params[0]
propsTypeDecl = resolveQualifiedType( propsTypeDecl = resolveQualifiedType(
node.typeParameters.params[0], propsTypeDeclRaw,
node => node.type === 'TSTypeLiteral' node => node.type === 'TSTypeLiteral'
) as TSTypeLiteral | TSInterfaceBody | undefined ) as TSTypeLiteral | TSInterfaceBody | undefined
@ -297,7 +352,7 @@ export function compileScript(
error( error(
`type argument passed to ${DEFINE_PROPS}() must be a literal type, ` + `type argument passed to ${DEFINE_PROPS}() must be a literal type, ` +
`or a reference to an interface or literal type.`, `or a reference to an interface or literal type.`,
node.typeParameters.params[0] propsTypeDeclRaw
) )
} }
} }
@ -335,9 +390,9 @@ export function compileScript(
error(`duplicate ${DEFINE_EMITS}() call`, node) error(`duplicate ${DEFINE_EMITS}() call`, node)
} }
hasDefineEmitCall = true hasDefineEmitCall = true
emitRuntimeDecl = node.arguments[0] emitsRuntimeDecl = node.arguments[0]
if (node.typeParameters) { if (node.typeParameters) {
if (emitRuntimeDecl) { if (emitsRuntimeDecl) {
error( error(
`${DEFINE_EMIT}() cannot accept both type and non-type arguments ` + `${DEFINE_EMIT}() cannot accept both type and non-type arguments ` +
`at the same time. Use one or the other.`, `at the same time. Use one or the other.`,
@ -345,16 +400,17 @@ export function compileScript(
) )
} }
emitTypeDecl = resolveQualifiedType( emitsTypeDeclRaw = node.typeParameters.params[0]
node.typeParameters.params[0], emitsTypeDecl = resolveQualifiedType(
emitsTypeDeclRaw,
node => node.type === 'TSFunctionType' || node.type === 'TSTypeLiteral' node => node.type === 'TSFunctionType' || node.type === 'TSTypeLiteral'
) as TSFunctionType | TSTypeLiteral | TSInterfaceBody | undefined ) as TSFunctionType | TSTypeLiteral | TSInterfaceBody | undefined
if (!emitTypeDecl) { if (!emitsTypeDecl) {
error( error(
`type argument passed to ${DEFINE_EMITS}() must be a function type, ` + `type argument passed to ${DEFINE_EMITS}() must be a function type, ` +
`a literal type with call signatures, or a reference to the above types.`, `a literal type with call signatures, or a reference to the above types.`,
node.typeParameters.params[0] emitsTypeDeclRaw
) )
} }
} }
@ -469,7 +525,10 @@ export function compileScript(
if (id.name[0] === '$') { if (id.name[0] === '$') {
error(`ref variable identifiers cannot start with $.`, id) error(`ref variable identifiers cannot start with $.`, id)
} }
refBindings[id.name] = setupBindings[id.name] = BindingTypes.SETUP_REF refBindings[id.name] = setupBindings[id.name] = {
type: BindingTypes.SETUP_REF,
rangeNode: id
}
refIdentifiers.add(id) refIdentifiers.add(id)
} }
@ -635,7 +694,9 @@ export function compileScript(
node.source.value, node.source.value,
specifier.local.name, specifier.local.name,
imported, imported,
node.importKind === 'type' node.importKind === 'type',
false,
specifier.local
) )
} }
} else if (node.type === 'ExportDefaultDeclaration') { } else if (node.type === 'ExportDefaultDeclaration') {
@ -724,7 +785,7 @@ export function compileScript(
node.body.type === 'ExpressionStatement' node.body.type === 'ExpressionStatement'
) { ) {
if (enableRefSugar) { if (enableRefSugar) {
warnExperimental(`ref: sugar`, 228) !parseOnly && warnExperimental(`ref: sugar`, 228)
s.overwrite( s.overwrite(
node.label.start! + startOffset, node.label.start! + startOffset,
node.body.start! + startOffset, node.body.start! + startOffset,
@ -795,7 +856,9 @@ export function compileScript(
source, source,
local, local,
imported, imported,
node.importKind === 'type' node.importKind === 'type',
true,
specifier.local
) )
} }
} }
@ -925,6 +988,44 @@ export function compileScript(
} }
} }
// in parse only mode, we should have collected all the information we need,
// return early.
if (parseOnly) {
for (const key in userImports) {
const { rangeNode, isFromSetup } = userImports[key]
const bindings = isFromSetup
? ranges!.scriptSetupBindings
: ranges!.scriptBindings
bindings.push(toTextRange(rangeNode))
}
for (const key in setupBindings) {
ranges!.scriptSetupBindings.push(
toTextRange(setupBindings[key].rangeNode)
)
}
if (propsRuntimeDecl) {
ranges!.propsRuntimeArg = toTextRange(propsRuntimeDecl)
}
if (propsTypeDeclRaw) {
ranges!.propsTypeArg = toTextRange(propsTypeDeclRaw)
}
if (emitsRuntimeDecl) {
ranges!.emitsRuntimeArg = toTextRange(emitsRuntimeDecl)
}
if (emitsTypeDeclRaw) {
ranges!.emitsTypeArg = toTextRange(emitsTypeDeclRaw)
}
if (propsRuntimeDefaults) {
ranges!.withDefaultsArg = toTextRange(propsRuntimeDefaults)
}
return {
...scriptSetup,
ranges,
scriptAst,
scriptSetupAst
}
}
// 3. Do a full walk to rewrite identifiers referencing let exports with ref // 3. Do a full walk to rewrite identifiers referencing let exports with ref
// value access // value access
if (enableRefSugar && Object.keys(refBindings).length) { if (enableRefSugar && Object.keys(refBindings).length) {
@ -958,15 +1059,15 @@ export function compileScript(
if (propsTypeDecl) { if (propsTypeDecl) {
extractRuntimeProps(propsTypeDecl, typeDeclaredProps, declaredTypes) extractRuntimeProps(propsTypeDecl, typeDeclaredProps, declaredTypes)
} }
if (emitTypeDecl) { if (emitsTypeDecl) {
extractRuntimeEmits(emitTypeDecl, typeDeclaredEmits) extractRuntimeEmits(emitsTypeDecl, typeDeclaredEmits)
} }
// 5. check useOptions args to make sure it doesn't reference setup scope // 5. check useOptions args to make sure it doesn't reference setup scope
// variables // variables
checkInvalidScopeReference(propsRuntimeDecl, DEFINE_PROPS) checkInvalidScopeReference(propsRuntimeDecl, DEFINE_PROPS)
checkInvalidScopeReference(propsRuntimeDefaults, DEFINE_PROPS) checkInvalidScopeReference(propsRuntimeDefaults, DEFINE_PROPS)
checkInvalidScopeReference(emitRuntimeDecl, DEFINE_PROPS) checkInvalidScopeReference(emitsRuntimeDecl, DEFINE_PROPS)
// 6. remove non-script content // 6. remove non-script content
if (script) { if (script) {
@ -1009,7 +1110,7 @@ export function compileScript(
: BindingTypes.SETUP_MAYBE_REF : BindingTypes.SETUP_MAYBE_REF
} }
for (const key in setupBindings) { for (const key in setupBindings) {
bindingMetadata[key] = setupBindings[key] bindingMetadata[key] = setupBindings[key].type
} }
// 8. inject `useCssVars` calls // 8. inject `useCssVars` calls
@ -1051,10 +1152,10 @@ export function compileScript(
} }
if (destructureElements.length) { if (destructureElements.length) {
args += `, { ${destructureElements.join(', ')} }` args += `, { ${destructureElements.join(', ')} }`
if (emitTypeDecl) { if (emitsTypeDecl) {
args += `: { emit: (${scriptSetup.content.slice( args += `: { emit: (${scriptSetup.content.slice(
emitTypeDecl.start!, emitsTypeDecl.start!,
emitTypeDecl.end! emitsTypeDecl.end!
)}), expose: any, slots: any, attrs: any }` )}), expose: any, slots: any, attrs: any }`
} }
} }
@ -1156,11 +1257,11 @@ export function compileScript(
} else if (propsTypeDecl) { } else if (propsTypeDecl) {
runtimeOptions += genRuntimeProps(typeDeclaredProps) runtimeOptions += genRuntimeProps(typeDeclaredProps)
} }
if (emitRuntimeDecl) { if (emitsRuntimeDecl) {
runtimeOptions += `\n emits: ${scriptSetup.content runtimeOptions += `\n emits: ${scriptSetup.content
.slice(emitRuntimeDecl.start!, emitRuntimeDecl.end!) .slice(emitsRuntimeDecl.start!, emitsRuntimeDecl.end!)
.trim()},` .trim()},`
} else if (emitTypeDecl) { } else if (emitsTypeDecl) {
runtimeOptions += genRuntimeEmits(typeDeclaredEmits) runtimeOptions += genRuntimeEmits(typeDeclaredEmits)
} }
@ -1230,9 +1331,20 @@ export function compileScript(
} }
} }
function registerBinding(
bindings: Record<string, VariableBinding>,
node: Identifier,
type: BindingTypes
) {
bindings[node.name] = {
type,
rangeNode: node
}
}
function walkDeclaration( function walkDeclaration(
node: Declaration, node: Declaration,
bindings: Record<string, BindingTypes>, bindings: Record<string, VariableBinding>,
userImportAlias: Record<string, string> userImportAlias: Record<string, string>
) { ) {
if (node.type === 'VariableDeclaration') { if (node.type === 'VariableDeclaration') {
@ -1272,7 +1384,7 @@ function walkDeclaration(
} else { } else {
bindingType = BindingTypes.SETUP_LET bindingType = BindingTypes.SETUP_LET
} }
bindings[id.name] = bindingType registerBinding(bindings, id, bindingType)
} else if (id.type === 'ObjectPattern') { } else if (id.type === 'ObjectPattern') {
walkObjectPattern(id, bindings, isConst, isDefineCall) walkObjectPattern(id, bindings, isConst, isDefineCall)
} else if (id.type === 'ArrayPattern') { } else if (id.type === 'ArrayPattern') {
@ -1285,13 +1397,16 @@ function walkDeclaration(
) { ) {
// export function foo() {} / export class Foo {} // export function foo() {} / export class Foo {}
// export declarations must be named. // export declarations must be named.
bindings[node.id!.name] = BindingTypes.SETUP_CONST bindings[node.id!.name] = {
type: BindingTypes.SETUP_CONST,
rangeNode: node.id!
}
} }
} }
function walkObjectPattern( function walkObjectPattern(
node: ObjectPattern, node: ObjectPattern,
bindings: Record<string, BindingTypes>, bindings: Record<string, VariableBinding>,
isConst: boolean, isConst: boolean,
isDefineCall = false isDefineCall = false
) { ) {
@ -1301,11 +1416,12 @@ function walkObjectPattern(
if (p.key.type === 'Identifier') { if (p.key.type === 'Identifier') {
if (p.key === p.value) { if (p.key === p.value) {
// const { x } = ... // const { x } = ...
bindings[p.key.name] = isDefineCall const type = isDefineCall
? BindingTypes.SETUP_CONST ? BindingTypes.SETUP_CONST
: isConst : isConst
? BindingTypes.SETUP_MAYBE_REF ? BindingTypes.SETUP_MAYBE_REF
: BindingTypes.SETUP_LET : BindingTypes.SETUP_LET
registerBinding(bindings, p.key, type)
} else { } else {
walkPattern(p.value, bindings, isConst, isDefineCall) walkPattern(p.value, bindings, isConst, isDefineCall)
} }
@ -1313,16 +1429,15 @@ function walkObjectPattern(
} else { } else {
// ...rest // ...rest
// argument can only be identifer when destructuring // argument can only be identifer when destructuring
bindings[(p.argument as Identifier).name] = isConst const type = isConst ? BindingTypes.SETUP_CONST : BindingTypes.SETUP_LET
? BindingTypes.SETUP_CONST registerBinding(bindings, p.argument as Identifier, type)
: BindingTypes.SETUP_LET
} }
} }
} }
function walkArrayPattern( function walkArrayPattern(
node: ArrayPattern, node: ArrayPattern,
bindings: Record<string, BindingTypes>, bindings: Record<string, VariableBinding>,
isConst: boolean, isConst: boolean,
isDefineCall = false isDefineCall = false
) { ) {
@ -1333,32 +1448,33 @@ function walkArrayPattern(
function walkPattern( function walkPattern(
node: Node, node: Node,
bindings: Record<string, BindingTypes>, bindings: Record<string, VariableBinding>,
isConst: boolean, isConst: boolean,
isDefineCall = false isDefineCall = false
) { ) {
if (node.type === 'Identifier') { if (node.type === 'Identifier') {
bindings[node.name] = isDefineCall const type = isDefineCall
? BindingTypes.SETUP_CONST ? BindingTypes.SETUP_CONST
: isConst : isConst
? BindingTypes.SETUP_MAYBE_REF ? BindingTypes.SETUP_MAYBE_REF
: BindingTypes.SETUP_LET : BindingTypes.SETUP_LET
registerBinding(bindings, node, type)
} else if (node.type === 'RestElement') { } else if (node.type === 'RestElement') {
// argument can only be identifer when destructuring // argument can only be identifer when destructuring
bindings[(node.argument as Identifier).name] = isConst const type = isConst ? BindingTypes.SETUP_CONST : BindingTypes.SETUP_LET
? BindingTypes.SETUP_CONST registerBinding(bindings, node.argument as Identifier, type)
: BindingTypes.SETUP_LET
} else if (node.type === 'ObjectPattern') { } else if (node.type === 'ObjectPattern') {
walkObjectPattern(node, bindings, isConst) walkObjectPattern(node, bindings, isConst)
} else if (node.type === 'ArrayPattern') { } else if (node.type === 'ArrayPattern') {
walkArrayPattern(node, bindings, isConst) walkArrayPattern(node, bindings, isConst)
} else if (node.type === 'AssignmentPattern') { } else if (node.type === 'AssignmentPattern') {
if (node.left.type === 'Identifier') { if (node.left.type === 'Identifier') {
bindings[node.left.name] = isDefineCall const type = isDefineCall
? BindingTypes.SETUP_CONST ? BindingTypes.SETUP_CONST
: isConst : isConst
? BindingTypes.SETUP_MAYBE_REF ? BindingTypes.SETUP_MAYBE_REF
: BindingTypes.SETUP_LET : BindingTypes.SETUP_LET
registerBinding(bindings, node.left, type)
} else { } else {
walkPattern(node.left, bindings, isConst) walkPattern(node.left, bindings, isConst)
} }
@ -1958,3 +2074,10 @@ function extractIdentifiers(
return nodes return nodes
} }
function toTextRange(node: Node): TextRange {
return {
start: node.start!,
end: node.end!
}
}

View File

@ -42,6 +42,25 @@ export interface SFCScriptBlock extends SFCBlock {
bindings?: BindingMetadata bindings?: BindingMetadata
scriptAst?: Statement[] scriptAst?: Statement[]
scriptSetupAst?: Statement[] scriptSetupAst?: Statement[]
ranges?: ScriptSetupTextRanges
}
/**
* Text range data for IDE support
*/
export interface ScriptSetupTextRanges {
scriptBindings: TextRange[]
scriptSetupBindings: TextRange[]
propsTypeArg?: TextRange
propsRuntimeArg?: TextRange
emitsTypeArg?: TextRange
emitsRuntimeArg?: TextRange
withDefaultsArg?: TextRange
}
export interface TextRange {
start: number
end: number
} }
export interface SFCStyleBlock extends SFCBlock { export interface SFCStyleBlock extends SFCBlock {