Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: v-skip #12767

Open
wants to merge 38 commits into
base: minor
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
81992b0
wip: v-skip
edison1105 Jan 22, 2025
ac5005f
wip: v-skip
edison1105 Jan 22, 2025
1bf4e9c
chore: update error code
edison1105 Jan 22, 2025
32ac9ed
wip: ssr v-skip
edison1105 Jan 22, 2025
80c1e5c
wip: v-skip work with v-if/v-else/v-else-if
edison1105 Jan 22, 2025
4dbcd89
wip: warn work with v-for
edison1105 Jan 23, 2025
7a6f1cb
wip: reuse check logic
edison1105 Jan 23, 2025
6529c3d
refactor: add new node type for v-skip node
edison1105 Jan 23, 2025
7327a57
chore: cache buildSlots result
edison1105 Jan 23, 2025
277e1f0
wip: add test case
edison1105 Jan 23, 2025
1ca1bba
test: add tests
edison1105 Jan 23, 2025
5407b05
wip: save
edison1105 Jan 23, 2025
74e8bb4
Revert "chore: cache buildSlots result"
edison1105 Jan 23, 2025
4974e5d
Revert "Revert "chore: cache buildSlots result""
edison1105 Jan 23, 2025
84a733e
fix: ssr buildSlots not use cache
edison1105 Jan 23, 2025
905805f
test: update
edison1105 Jan 23, 2025
bb62400
wip: test
edison1105 Jan 23, 2025
25b023d
wip: ssr test cases
edison1105 Jan 23, 2025
e756c52
wip: test
edison1105 Jan 23, 2025
172c16d
wip: inject attrs for skip node
edison1105 Jan 23, 2025
f725472
wip: properly inject fallthrough attrs to alternate node
edison1105 Jan 24, 2025
1d49d53
test: add more tests
edison1105 Jan 24, 2025
6c17a37
chore: minor tweaks
edison1105 Jan 24, 2025
453aa03
test: add tests
edison1105 Jan 24, 2025
feda7a0
refactor: tweak v-skip on component
edison1105 Jan 24, 2025
e3fc703
wip: save
edison1105 Jan 24, 2025
7efaafb
wip: analyze slots to find default slot when possible
edison1105 Jan 26, 2025
c0f00d2
wip: add ssrRenderSkipVNode
edison1105 Jan 26, 2025
7031b59
wip: add more ssr test
edison1105 Jan 26, 2025
46825ca
wip: add more test
edison1105 Jan 26, 2025
80b32bf
wip: fix on component slot + v-if in ssr
edison1105 Jan 26, 2025
ac55a02
test: make test pass
edison1105 Jan 26, 2025
f4b0498
chore: tweaks comments
edison1105 Jan 27, 2025
2328f91
chore: update
edison1105 Jan 27, 2025
c3cb610
chore: update
edison1105 Jan 27, 2025
69162d3
fix: codegen
edison1105 Jan 27, 2025
12df730
test: add more tests
edison1105 Jan 31, 2025
e44f4a1
test: update
edison1105 Feb 1, 2025
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

Large diffs are not rendered by default.

552 changes: 552 additions & 0 deletions packages/compiler-core/__tests__/transforms/vSkip.spec.ts

Large diffs are not rendered by default.

16 changes: 15 additions & 1 deletion packages/compiler-core/src/ast.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ export enum NodeTypes {
IF_BRANCH,
FOR,
TEXT_CALL,
SKIP,
// codegen
VNODE_CALL,
JS_CALL_EXPRESSION,
Expand Down Expand Up @@ -100,6 +101,7 @@ export type TemplateChildNode =
| IfBranchNode
| ForNode
| TextCallNode
| SkipNode

export interface RootNode extends Node {
type: NodeTypes.ROOT
Expand Down Expand Up @@ -144,12 +146,15 @@ export interface PlainElementNode extends BaseElementNode {
| SimpleExpressionNode // when hoisted
| CacheExpression // when cached by v-once
| MemoExpression // when cached by v-memo
| ConditionalExpression
| undefined
ssrCodegenNode?: TemplateLiteral
}

export interface ComponentNode extends BaseElementNode {
tagType: ElementTypes.COMPONENT
slots: SlotsExpression
hasDynamicSlots: boolean
codegenNode:
| VNodeCall
| CacheExpression // when cached by v-once
Expand Down Expand Up @@ -405,6 +410,15 @@ export interface FunctionExpression extends Node {
isNonScopedSlot?: boolean
}

export interface SkipNode extends Node {
type: NodeTypes.SKIP
test: ExpressionNode
consequent: IfBranchNode | CallExpression
alternate: IfBranchNode
newline: boolean
codegenNode: ConditionalExpression | undefined
}

export interface ConditionalExpression extends Node {
type: NodeTypes.JS_CONDITIONAL_EXPRESSION
test: JSChildNode
Expand Down Expand Up @@ -454,7 +468,7 @@ export interface TemplateLiteral extends Node {
export interface IfStatement extends Node {
type: NodeTypes.JS_IF_STATEMENT
test: ExpressionNode
consequent: BlockStatement
consequent: BlockStatement | CallExpression
alternate: IfStatement | BlockStatement | ReturnStatement | undefined
}

Expand Down
3 changes: 2 additions & 1 deletion packages/compiler-core/src/codegen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -656,10 +656,11 @@ function genNode(node: CodegenNode | symbol | string, context: CodegenContext) {
case NodeTypes.ELEMENT:
case NodeTypes.IF:
case NodeTypes.FOR:
case NodeTypes.SKIP:
__DEV__ &&
assert(
node.codegenNode != null,
`Codegen node is missing for element/if/for node. ` +
`Codegen node is missing for element/if/for/skip node. ` +
`Apply appropriate transforms first.`,
)
genNode(node.codegenNode!, context)
Expand Down
2 changes: 2 additions & 0 deletions packages/compiler-core/src/compile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { transformModel } from './transforms/vModel'
import { transformFilter } from './compat/transformFilter'
import { ErrorCodes, createCompilerError, defaultOnError } from './errors'
import { transformMemo } from './transforms/vMemo'
import { transformSkip } from './transforms/vSkip'

export type TransformPreset = [
NodeTransform[],
Expand All @@ -35,6 +36,7 @@ export function getBaseTransformPreset(
[
transformOnce,
transformIf,
transformSkip,
transformMemo,
transformFor,
...(__COMPAT__ ? [transformFilter] : []),
Expand Down
8 changes: 8 additions & 0 deletions packages/compiler-core/src/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,10 @@ export enum ErrorCodes {
X_V_MODEL_ON_PROPS,
X_INVALID_EXPRESSION,
X_KEEP_ALIVE_INVALID_CHILDREN,
X_V_SKIP_NO_EXPRESSION,
X_V_SKIP_MISPLACED,
X_V_SKIP_UNEXPECTED_SLOT,
X_V_SKIP_WITH_V_FOR,

// generic errors
X_PREFIX_ID_NOT_SUPPORTED,
Expand Down Expand Up @@ -179,6 +183,10 @@ export const errorMessages: Record<ErrorCodes, string> = {
[ErrorCodes.X_INVALID_EXPRESSION]: `Error parsing JavaScript expression: `,
[ErrorCodes.X_KEEP_ALIVE_INVALID_CHILDREN]: `<KeepAlive> expects exactly one child component.`,
[ErrorCodes.X_VNODE_HOOKS]: `@vnode-* hooks in templates are no longer supported. Use the vue: prefix instead. For example, @vnode-mounted should be changed to @vue:mounted. @vnode-* hooks support has been removed in 3.4.`,
[ErrorCodes.X_V_SKIP_NO_EXPRESSION]: `v-skip is missing expression.`,
[ErrorCodes.X_V_SKIP_MISPLACED]: `v-skip can only be used on elements or components.`,
[ErrorCodes.X_V_SKIP_UNEXPECTED_SLOT]: `v-skip requires the component to have a default slot without slot props`,
[ErrorCodes.X_V_SKIP_WITH_V_FOR]: `v-skip with v-for is not supported.`,

// generic errors
[ErrorCodes.X_PREFIX_ID_NOT_SUPPORTED]: `"prefixIdentifiers" option is not supported in this build of compiler.`,
Expand Down
1 change: 1 addition & 0 deletions packages/compiler-core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ export { transformBind } from './transforms/vBind'
export { noopDirectiveTransform } from './transforms/noopDirectiveTransform'
export { processIf } from './transforms/vIf'
export { processFor, createForLoopParams } from './transforms/vFor'
export { processSkip } from './transforms/vSkip'
export {
transformExpression,
processExpression,
Expand Down
4 changes: 4 additions & 0 deletions packages/compiler-core/src/runtimeHelpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ export const CREATE_STATIC: unique symbol = Symbol(
export const RESOLVE_COMPONENT: unique symbol = Symbol(
__DEV__ ? `resolveComponent` : ``,
)
export const RESOLVE_SKIP_COMPONENT: unique symbol = Symbol(
__DEV__ ? `resolveSkipComponent` : ``,
)
export const RESOLVE_DYNAMIC_COMPONENT: unique symbol = Symbol(
__DEV__ ? `resolveDynamicComponent` : ``,
)
Expand Down Expand Up @@ -99,6 +102,7 @@ export const helperNameMap: Record<symbol, string> = {
[CREATE_STATIC]: `createStaticVNode`,
[RESOLVE_COMPONENT]: `resolveComponent`,
[RESOLVE_DYNAMIC_COMPONENT]: `resolveDynamicComponent`,
[RESOLVE_SKIP_COMPONENT]: `resolveSkipComponent`,
[RESOLVE_DIRECTIVE]: `resolveDirective`,
[RESOLVE_FILTER]: `resolveFilter`,
[WITH_DIRECTIVES]: `withDirectives`,
Expand Down
12 changes: 12 additions & 0 deletions packages/compiler-core/src/transform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -466,6 +466,18 @@ export function traverseNode(
traverseNode(node.branches[i], context)
}
break
case NodeTypes.SKIP:
// in non-SSR mode, `alternate` already includes `consequent` content,
// so no need to traverse `consequent` node
// during `inSSR` transform, we need to traverse both since we use the cloned nodes,
// see `createBranchNode` in `vSkip.ts`
if (context.inSSR) {
const { consequent } = node
if (consequent.type === NodeTypes.IF_BRANCH)
traverseNode(consequent, context)
}
traverseNode(node.alternate, context)
break
case NodeTypes.IF_BRANCH:
case NodeTypes.FOR:
case NodeTypes.ELEMENT:
Expand Down
1 change: 1 addition & 0 deletions packages/compiler-core/src/transforms/cacheStatic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -341,6 +341,7 @@ export function getConstantType(
case NodeTypes.IF:
case NodeTypes.FOR:
case NodeTypes.IF_BRANCH:
case NodeTypes.SKIP:
return ConstantTypes.NOT_CONSTANT
case NodeTypes.INTERPOLATION:
case NodeTypes.TEXT_CALL:
Expand Down
7 changes: 6 additions & 1 deletion packages/compiler-core/src/transforms/transformElement.ts
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,12 @@ export const transformElement: NodeTransform = (node, context) => {
vnodeTag !== KEEP_ALIVE

if (shouldBuildAsSlots) {
const { slots, hasDynamicSlots } = buildSlots(node, context)
const { slots, hasDynamicSlots } = buildSlots(
node as ComponentNode,
context,
undefined,
true,
)
vnodeChildren = slots
if (hasDynamicSlots) {
patchFlag |= PatchFlags.DYNAMIC_SLOTS
Expand Down
2 changes: 1 addition & 1 deletion packages/compiler-core/src/transforms/vIf.ts
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,7 @@ function createIfBranch(node: ElementNode, dir: DirectiveNode): IfBranchNode {
}
}

function createCodegenNodeForBranch(
export function createCodegenNodeForBranch(
branch: IfBranchNode,
keyIndex: number,
context: TransformContext,
Expand Down
215 changes: 215 additions & 0 deletions packages/compiler-core/src/transforms/vSkip.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,215 @@
import {
type ComponentNode,
type DirectiveNode,
type ElementNode,
ElementTypes,
type ExpressionNode,
type IfBranchNode,
NodeTypes,
type SimpleExpressionNode,
type SkipNode,
type SourceLocation,
type TemplateChildNode,
type VNodeCall,
createCallExpression,
createConditionalExpression,
createSimpleExpression,
} from '../ast'
import {
type NodeTransform,
type TransformContext,
createStructuralDirectiveTransform,
} from '../transform'
import {
CREATE_COMMENT,
ErrorCodes,
RESOLVE_SKIP_COMPONENT,
WITH_MEMO,
buildSlots,
createCompilerError,
findDir,
findProp,
processExpression,
} from '@vue/compiler-core'
import { createCodegenNodeForBranch } from './vIf'
import { validateBrowserExpression } from '../validateExpression'
import { cloneLoc } from '../parser'
import { clone } from '@vue/shared'

export const transformSkip: NodeTransform = createStructuralDirectiveTransform(
'skip',
(node, dir, context) => {
return processSkip(node, dir, context, (skipNode?: SkipNode) => {
return () => {
const codegenNode = node.codegenNode!
if (!skipNode) {
if (codegenNode.type === NodeTypes.VNODE_CALL) {
codegenNode.tag = getVNodeTag(
context,
dir.exp!,
codegenNode.tag as string,
)
} else if (
codegenNode.type === NodeTypes.JS_CALL_EXPRESSION &&
codegenNode.callee === WITH_MEMO
) {
const vnodeCall = codegenNode.arguments[1].returns as VNodeCall
vnodeCall.tag = getVNodeTag(
context,
dir.exp!,
vnodeCall.tag as string,
)
}
} else {
const { consequent, alternate, test } = skipNode!
skipNode!.codegenNode = createConditionalExpression(
test,
consequent.type === NodeTypes.IF_BRANCH
? createCodegenNodeForBranch(consequent, 0, context)
: consequent,
createCodegenNodeForBranch(alternate, 1, context),
)
}
}
})
},
)

export function processSkip(
node: ElementNode,
dir: DirectiveNode,
context: TransformContext,
processCodegen?: (skipNode?: SkipNode) => () => void,
): (() => void) | undefined {
const loc = dir.exp ? dir.exp.loc : node.loc
if (
// v-skip is not allowed on <template> or <slot>
!(
node.type === NodeTypes.ELEMENT &&
(node.tagType === ElementTypes.ELEMENT ||
node.tagType === ElementTypes.COMPONENT) &&
node.tag !== 'template' &&
node.tag !== 'slot'
)
) {
context.onError(createCompilerError(ErrorCodes.X_V_SKIP_MISPLACED, loc))
return
}

if (findDir(node, 'for')) {
context.onError(createCompilerError(ErrorCodes.X_V_SKIP_WITH_V_FOR, loc))
return
}

if (!dir.exp || !(dir.exp as SimpleExpressionNode).content.trim()) {
context.onError(createCompilerError(ErrorCodes.X_V_SKIP_NO_EXPRESSION, loc))
dir.exp = createSimpleExpression(`true`, false, loc)
}

if (!__BROWSER__ && context.prefixIdentifiers && dir.exp) {
dir.exp = processExpression(dir.exp as SimpleExpressionNode, context)
}

if (__DEV__ && __BROWSER__ && dir.exp) {
validateBrowserExpression(dir.exp as SimpleExpressionNode, context)
}

// element will be processed as a skip node
// - native element
// - teleport, since it has children
// - component without dynamic slots
let processAsSkipNode = false
const isComponent = node.tagType === ElementTypes.COMPONENT
let children: TemplateChildNode[] = []
if (
node.tagType === ElementTypes.ELEMENT ||
(isComponent && node.tag === 'Teleport')
) {
processAsSkipNode = true
children = node.children
} else if (isComponent) {
const { hasDynamicSlots, defaultSlot } = resolveDefaultSlot(node, context)
if (!hasDynamicSlots) {
if (defaultSlot) {
processAsSkipNode = true
children = defaultSlot
} else {
context.onError(
createCompilerError(ErrorCodes.X_V_SKIP_UNEXPECTED_SLOT, loc),
)
}
}
}

let skipNode: SkipNode | undefined
if (processAsSkipNode) {
// if children is empty, create comment node
const consequent =
children.length !== 0
? createBranchNode(context, node, node.loc, children)
: createCallExpression(context.helper(CREATE_COMMENT), [
__DEV__ ? '"v-skip"' : '""',
'true',
])

skipNode = {
type: NodeTypes.SKIP,
loc: cloneLoc(node.loc),
test: dir.exp,
consequent,
alternate: createBranchNode(context, node, node.loc, [node]),
newline: true,
codegenNode: undefined,
}

context.replaceNode(skipNode)
}

if (processCodegen) return processCodegen(skipNode)
}

function resolveDefaultSlot(node: ComponentNode, context: TransformContext) {
let defaultSlot: TemplateChildNode[] | undefined = undefined
const { slots, hasDynamicSlots } = buildSlots(node, context, undefined, true)
// find default slot without slot props if not has dynamic slots
if (!hasDynamicSlots && slots.type === NodeTypes.JS_OBJECT_EXPRESSION) {
const prop = slots.properties.find(
p =>
p.type === NodeTypes.JS_PROPERTY &&
p.key.type === NodeTypes.SIMPLE_EXPRESSION &&
p.key.content === 'default' &&
p.value.params === undefined,
)
if (prop) {
defaultSlot = prop.value.returns as TemplateChildNode[]
}
}
return { hasDynamicSlots, defaultSlot }
}

function createBranchNode(
context: TransformContext,
node: ElementNode,
loc: SourceLocation,
children: TemplateChildNode[],
): IfBranchNode {
return {
type: NodeTypes.IF_BRANCH,
loc,
condition: undefined,
// using cloned node during `inSSR` transform
children: context.inSSR ? clone(children) : children,
userKey: findProp(node, `key`),
}
}

function getVNodeTag(
context: TransformContext,
exp: ExpressionNode,
tag: string,
) {
return createCallExpression(context.helper(RESOLVE_SKIP_COMPONENT), [
exp,
tag,
])
}
Loading