feat(ssr): support custom directive getSSRProps in optimized compilation
close #5304
This commit is contained in:
parent
a51f935b72
commit
60cf175d88
@ -5,7 +5,8 @@ import {
|
||||
ErrorCodes,
|
||||
BindingTypes,
|
||||
NodeTransform,
|
||||
transformExpression
|
||||
transformExpression,
|
||||
baseCompile
|
||||
} from '../../src'
|
||||
import {
|
||||
RESOLVE_COMPONENT,
|
||||
@ -66,6 +67,7 @@ function parseWithBind(template: string, options?: CompilerOptions) {
|
||||
return parseWithElementTransform(template, {
|
||||
...options,
|
||||
directiveTransforms: {
|
||||
...options?.directiveTransforms,
|
||||
bind: transformBind
|
||||
}
|
||||
})
|
||||
@ -932,7 +934,11 @@ describe('compiler: element transform', () => {
|
||||
})
|
||||
|
||||
test('NEED_PATCH (vnode hooks)', () => {
|
||||
const { node } = parseWithBind(`<div @vnodeUpdated="foo" />`)
|
||||
const root = baseCompile(`<div @vnodeUpdated="foo" />`, {
|
||||
prefixIdentifiers: true,
|
||||
cacheHandlers: true
|
||||
}).ast
|
||||
const node = (root as any).children[0].codegenNode
|
||||
expect(node.patchFlag).toBe(genFlagText(PatchFlags.NEED_PATCH))
|
||||
})
|
||||
|
||||
|
@ -54,7 +54,9 @@ export {
|
||||
export {
|
||||
transformElement,
|
||||
resolveComponentType,
|
||||
buildProps
|
||||
buildProps,
|
||||
buildDirectiveArgs,
|
||||
PropsExpression
|
||||
} from './transforms/transformElement'
|
||||
export { processSlotOutlet } from './transforms/transformSlotOutlet'
|
||||
export { generateCodeFrame } from '@vue/shared'
|
||||
|
@ -29,7 +29,8 @@ import {
|
||||
isObject,
|
||||
isReservedProp,
|
||||
capitalize,
|
||||
camelize
|
||||
camelize,
|
||||
isBuiltInDirective
|
||||
} from '@vue/shared'
|
||||
import { createCompilerError, ErrorCodes } from '../errors'
|
||||
import {
|
||||
@ -665,7 +666,7 @@ export function buildProps(
|
||||
directiveImportMap.set(prop, needRuntime)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
} else if (!isBuiltInDirective(name)) {
|
||||
// no built-in transform, this is a user custom directive.
|
||||
runtimeDirectives.push(prop)
|
||||
// custom dirs may use beforeUpdate so they need to force blocks
|
||||
@ -853,7 +854,7 @@ function mergeAsArray(existing: Property, incoming: Property) {
|
||||
}
|
||||
}
|
||||
|
||||
function buildDirectiveArgs(
|
||||
export function buildDirectiveArgs(
|
||||
dir: DirectiveNode,
|
||||
context: TransformContext
|
||||
): ArrayExpression {
|
||||
|
@ -37,14 +37,11 @@ exports[`compiler: transform v-model input w/ dynamic v-bind 2`] = `
|
||||
|
||||
return function render(_ctx, _cache) {
|
||||
with (_ctx) {
|
||||
const { vModelDynamic: _vModelDynamic, resolveDirective: _resolveDirective, withDirectives: _withDirectives, openBlock: _openBlock, createElementBlock: _createElementBlock } = _Vue
|
||||
|
||||
const _directive_bind = _resolveDirective(\\"bind\\")
|
||||
const { vModelDynamic: _vModelDynamic, withDirectives: _withDirectives, openBlock: _openBlock, createElementBlock: _createElementBlock } = _Vue
|
||||
|
||||
return _withDirectives((_openBlock(), _createElementBlock(\\"input\\", {
|
||||
\\"onUpdate:modelValue\\": $event => ((model) = $event)
|
||||
}, null, 8 /* PROPS */, [\\"onUpdate:modelValue\\"])), [
|
||||
[_directive_bind, val, key],
|
||||
[_vModelDynamic, model]
|
||||
])
|
||||
}
|
||||
@ -152,14 +149,11 @@ exports[`compiler: transform v-model simple expression for input (dynamic type)
|
||||
|
||||
return function render(_ctx, _cache) {
|
||||
with (_ctx) {
|
||||
const { vModelDynamic: _vModelDynamic, resolveDirective: _resolveDirective, withDirectives: _withDirectives, openBlock: _openBlock, createElementBlock: _createElementBlock } = _Vue
|
||||
|
||||
const _directive_bind = _resolveDirective(\\"bind\\")
|
||||
const { vModelDynamic: _vModelDynamic, withDirectives: _withDirectives, openBlock: _openBlock, createElementBlock: _createElementBlock } = _Vue
|
||||
|
||||
return _withDirectives((_openBlock(), _createElementBlock(\\"input\\", {
|
||||
\\"onUpdate:modelValue\\": $event => ((model) = $event)
|
||||
}, null, 8 /* PROPS */, [\\"onUpdate:modelValue\\"])), [
|
||||
[_directive_bind, foo, \\"type\\"],
|
||||
[_vModelDynamic, model]
|
||||
])
|
||||
}
|
||||
|
@ -377,4 +377,20 @@ describe('ssr: components', () => {
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('custom directive', () => {
|
||||
test('basic', () => {
|
||||
expect(compile(`<foo v-xxx:x.y="z" />`).code).toMatchInlineSnapshot(`
|
||||
"const { resolveComponent: _resolveComponent, resolveDirective: _resolveDirective, mergeProps: _mergeProps } = require(\\"vue\\")
|
||||
const { ssrGetDirectiveProps: _ssrGetDirectiveProps, ssrRenderComponent: _ssrRenderComponent } = require(\\"vue/server-renderer\\")
|
||||
|
||||
return function ssrRender(_ctx, _push, _parent, _attrs) {
|
||||
const _component_foo = _resolveComponent(\\"foo\\")
|
||||
const _directive_xxx = _resolveDirective(\\"xxx\\")
|
||||
|
||||
_push(_ssrRenderComponent(_component_foo, _mergeProps(_attrs, _ssrGetDirectiveProps(_ctx, _directive_xxx, _ctx.z, \\"x\\", { y: true })), null, _parent))
|
||||
}"
|
||||
`)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
@ -288,5 +288,56 @@ describe('ssr: element', () => {
|
||||
}></div>\`"
|
||||
`)
|
||||
})
|
||||
|
||||
test('custom dir', () => {
|
||||
expect(getCompiledString(`<div v-xxx:x.y="z" />`)).toMatchInlineSnapshot(`
|
||||
"\`<div\${
|
||||
_ssrRenderAttrs(_ssrGetDirectiveProps(_ctx, _directive_xxx, _ctx.z, \\"x\\", { y: true }))
|
||||
}></div>\`"
|
||||
`)
|
||||
})
|
||||
|
||||
test('custom dir with normal attrs', () => {
|
||||
expect(getCompiledString(`<div class="foo" v-xxx />`))
|
||||
.toMatchInlineSnapshot(`
|
||||
"\`<div\${
|
||||
_ssrRenderAttrs(_mergeProps({ class: \\"foo\\" }, _ssrGetDirectiveProps(_ctx, _directive_xxx)))
|
||||
}></div>\`"
|
||||
`)
|
||||
})
|
||||
|
||||
test('custom dir with v-bind', () => {
|
||||
expect(getCompiledString(`<div :title="foo" :class="bar" v-xxx />`))
|
||||
.toMatchInlineSnapshot(`
|
||||
"\`<div\${
|
||||
_ssrRenderAttrs(_mergeProps({
|
||||
title: _ctx.foo,
|
||||
class: _ctx.bar
|
||||
}, _ssrGetDirectiveProps(_ctx, _directive_xxx)))
|
||||
}></div>\`"
|
||||
`)
|
||||
})
|
||||
|
||||
test('custom dir with object v-bind', () => {
|
||||
expect(getCompiledString(`<div v-bind="x" v-xxx />`))
|
||||
.toMatchInlineSnapshot(`
|
||||
"\`<div\${
|
||||
_ssrRenderAttrs(_mergeProps(_ctx.x, _ssrGetDirectiveProps(_ctx, _directive_xxx)))
|
||||
}></div>\`"
|
||||
`)
|
||||
})
|
||||
|
||||
test('custom dir with object v-bind + normal bindings', () => {
|
||||
expect(
|
||||
getCompiledString(`<div v-bind="x" class="foo" v-xxx title="bar" />`)
|
||||
).toMatchInlineSnapshot(`
|
||||
"\`<div\${
|
||||
_ssrRenderAttrs(_mergeProps(_ctx.x, {
|
||||
class: \\"foo\\",
|
||||
title: \\"bar\\"
|
||||
}, _ssrGetDirectiveProps(_ctx, _directive_xxx)))
|
||||
}></div>\`"
|
||||
`)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
@ -17,6 +17,7 @@ export const SSR_RENDER_DYNAMIC_MODEL = Symbol(`ssrRenderDynamicModel`)
|
||||
export const SSR_GET_DYNAMIC_MODEL_PROPS = Symbol(`ssrGetDynamicModelProps`)
|
||||
export const SSR_RENDER_TELEPORT = Symbol(`ssrRenderTeleport`)
|
||||
export const SSR_RENDER_SUSPENSE = Symbol(`ssrRenderSuspense`)
|
||||
export const SSR_GET_DIRECTIVE_PROPS = Symbol(`ssrGetDirectiveProps`)
|
||||
|
||||
export const ssrHelpers = {
|
||||
[SSR_INTERPOLATE]: `ssrInterpolate`,
|
||||
@ -35,7 +36,8 @@ export const ssrHelpers = {
|
||||
[SSR_RENDER_DYNAMIC_MODEL]: `ssrRenderDynamicModel`,
|
||||
[SSR_GET_DYNAMIC_MODEL_PROPS]: `ssrGetDynamicModelProps`,
|
||||
[SSR_RENDER_TELEPORT]: `ssrRenderTeleport`,
|
||||
[SSR_RENDER_SUSPENSE]: `ssrRenderSuspense`
|
||||
[SSR_RENDER_SUSPENSE]: `ssrRenderSuspense`,
|
||||
[SSR_GET_DIRECTIVE_PROPS]: `ssrGetDirectiveProps`
|
||||
}
|
||||
|
||||
// Note: these are helpers imported from @vue/server-renderer
|
||||
|
@ -33,7 +33,8 @@ import {
|
||||
TELEPORT,
|
||||
TRANSITION_GROUP,
|
||||
CREATE_VNODE,
|
||||
CallExpression
|
||||
CallExpression,
|
||||
JSChildNode
|
||||
} from '@vue/compiler-dom'
|
||||
import { SSR_RENDER_COMPONENT, SSR_RENDER_VNODE } from '../runtimeHelpers'
|
||||
import {
|
||||
@ -48,6 +49,7 @@ import {
|
||||
} from './ssrTransformSuspense'
|
||||
import { ssrProcessTransitionGroup } from './ssrTransformTransitionGroup'
|
||||
import { isSymbol, isObject, isArray } from '@vue/shared'
|
||||
import { buildSSRProps } from './ssrTransformElement'
|
||||
|
||||
// We need to construct the slot functions in the 1st pass to ensure proper
|
||||
// scope tracking, but the children of each slot cannot be processed until
|
||||
@ -110,12 +112,15 @@ export const ssrTransformComponent: NodeTransform = (node, context) => {
|
||||
})
|
||||
}
|
||||
|
||||
const props =
|
||||
node.props.length > 0
|
||||
? // note we are not passing ssr: true here because for components, v-on
|
||||
// handlers should still be passed
|
||||
buildProps(node, context).props || `null`
|
||||
: `null`
|
||||
let propsExp: string | JSChildNode = `null`
|
||||
if (node.props.length) {
|
||||
// note we are not passing ssr: true here because for components, v-on
|
||||
// handlers should still be passed
|
||||
const { props, directives } = buildProps(node, context)
|
||||
if (props || directives.length) {
|
||||
propsExp = buildSSRProps(props, directives, context)
|
||||
}
|
||||
}
|
||||
|
||||
const wipEntries: WIPSlotEntry[] = []
|
||||
wipMap.set(node, wipEntries)
|
||||
@ -151,7 +156,7 @@ export const ssrTransformComponent: NodeTransform = (node, context) => {
|
||||
`_push`,
|
||||
createCallExpression(context.helper(CREATE_VNODE), [
|
||||
component,
|
||||
props,
|
||||
propsExp,
|
||||
slots
|
||||
]),
|
||||
`_parent`
|
||||
@ -160,7 +165,7 @@ export const ssrTransformComponent: NodeTransform = (node, context) => {
|
||||
} else {
|
||||
node.ssrCodegenNode = createCallExpression(
|
||||
context.helper(SSR_RENDER_COMPONENT),
|
||||
[component, props, slots, `_parent`]
|
||||
[component, propsExp, slots, `_parent`]
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -26,11 +26,15 @@ import {
|
||||
createSequenceExpression,
|
||||
InterpolationNode,
|
||||
isStaticExp,
|
||||
AttributeNode
|
||||
AttributeNode,
|
||||
buildDirectiveArgs,
|
||||
TransformContext,
|
||||
PropsExpression
|
||||
} from '@vue/compiler-dom'
|
||||
import {
|
||||
escapeHtml,
|
||||
isBooleanAttr,
|
||||
isBuiltInDirective,
|
||||
isSSRSafeAttrName,
|
||||
NO,
|
||||
propsToAttrMap
|
||||
@ -44,7 +48,8 @@ import {
|
||||
SSR_RENDER_ATTRS,
|
||||
SSR_INTERPOLATE,
|
||||
SSR_GET_DYNAMIC_MODEL_PROPS,
|
||||
SSR_INCLUDE_BOOLEAN_ATTR
|
||||
SSR_INCLUDE_BOOLEAN_ATTR,
|
||||
SSR_GET_DIRECTIVE_PROPS
|
||||
} from '../runtimeHelpers'
|
||||
import { SSRTransformContext, processChildren } from '../ssrCodegenTransform'
|
||||
|
||||
@ -71,16 +76,26 @@ export const ssrTransformElement: NodeTransform = (node, context) => {
|
||||
const needTagForRuntime =
|
||||
node.tag === 'textarea' || node.tag.indexOf('-') > 0
|
||||
|
||||
// v-bind="obj" or v-bind:[key] can potentially overwrite other static
|
||||
// attrs and can affect final rendering result, so when they are present
|
||||
// we need to bail out to full `renderAttrs`
|
||||
// v-bind="obj", v-bind:[key] and custom directives can potentially
|
||||
// overwrite other static attrs and can affect final rendering result,
|
||||
// so when they are present we need to bail out to full `renderAttrs`
|
||||
const hasDynamicVBind = hasDynamicKeyVBind(node)
|
||||
if (hasDynamicVBind) {
|
||||
const { props } = buildProps(node, context, node.props, true /* ssr */)
|
||||
if (props) {
|
||||
const hasCustomDir = node.props.some(
|
||||
p => p.type === NodeTypes.DIRECTIVE && !isBuiltInDirective(p.name)
|
||||
)
|
||||
const needMergeProps = hasDynamicVBind || hasCustomDir
|
||||
if (needMergeProps) {
|
||||
const { props, directives } = buildProps(
|
||||
node,
|
||||
context,
|
||||
node.props,
|
||||
true /* ssr */
|
||||
)
|
||||
if (props || directives.length) {
|
||||
const mergedProps = buildSSRProps(props, directives, context)
|
||||
const propsExp = createCallExpression(
|
||||
context.helper(SSR_RENDER_ATTRS),
|
||||
[props]
|
||||
[mergedProps]
|
||||
)
|
||||
|
||||
if (node.tag === 'textarea') {
|
||||
@ -99,7 +114,7 @@ export const ssrTransformElement: NodeTransform = (node, context) => {
|
||||
propsExp.arguments = [
|
||||
createAssignmentExpression(
|
||||
createSimpleExpression(tempId, false),
|
||||
props
|
||||
mergedProps
|
||||
)
|
||||
]
|
||||
rawChildrenMap.set(
|
||||
@ -128,7 +143,7 @@ export const ssrTransformElement: NodeTransform = (node, context) => {
|
||||
const tempExp = createSimpleExpression(tempId, false)
|
||||
propsExp.arguments = [
|
||||
createSequenceExpression([
|
||||
createAssignmentExpression(tempExp, props),
|
||||
createAssignmentExpression(tempExp, mergedProps),
|
||||
createCallExpression(context.helper(MERGE_PROPS), [
|
||||
tempExp,
|
||||
createCallExpression(
|
||||
@ -176,10 +191,10 @@ export const ssrTransformElement: NodeTransform = (node, context) => {
|
||||
createCompilerError(ErrorCodes.X_V_SLOT_MISPLACED, prop.loc)
|
||||
)
|
||||
} else if (isTextareaWithValue(node, prop) && prop.exp) {
|
||||
if (!hasDynamicVBind) {
|
||||
if (!needMergeProps) {
|
||||
node.children = [createInterpolation(prop.exp, prop.loc)]
|
||||
}
|
||||
} else if (!hasDynamicVBind) {
|
||||
} else if (!needMergeProps) {
|
||||
// Directive transforms.
|
||||
const directiveTransform = context.directiveTransforms[prop.name]
|
||||
if (directiveTransform) {
|
||||
@ -277,7 +292,7 @@ export const ssrTransformElement: NodeTransform = (node, context) => {
|
||||
// special case: value on <textarea>
|
||||
if (node.tag === 'textarea' && prop.name === 'value' && prop.value) {
|
||||
rawChildrenMap.set(node, escapeHtml(prop.value.content))
|
||||
} else if (!hasDynamicVBind) {
|
||||
} else if (!needMergeProps) {
|
||||
if (prop.name === 'key' || prop.name === 'ref') {
|
||||
continue
|
||||
}
|
||||
@ -307,6 +322,37 @@ export const ssrTransformElement: NodeTransform = (node, context) => {
|
||||
}
|
||||
}
|
||||
|
||||
export function buildSSRProps(
|
||||
props: PropsExpression | undefined,
|
||||
directives: DirectiveNode[],
|
||||
context: TransformContext
|
||||
): JSChildNode {
|
||||
let mergePropsArgs: JSChildNode[] = []
|
||||
if (props) {
|
||||
if (props.type === NodeTypes.JS_CALL_EXPRESSION) {
|
||||
// already a mergeProps call
|
||||
mergePropsArgs = props.arguments as JSChildNode[]
|
||||
} else {
|
||||
mergePropsArgs.push(props)
|
||||
}
|
||||
}
|
||||
if (directives.length) {
|
||||
for (const dir of directives) {
|
||||
context.directives.add(dir.name)
|
||||
mergePropsArgs.push(
|
||||
createCallExpression(context.helper(SSR_GET_DIRECTIVE_PROPS), [
|
||||
`_ctx`,
|
||||
...buildDirectiveArgs(dir, context).elements
|
||||
] as JSChildNode[])
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return mergePropsArgs.length > 1
|
||||
? createCallExpression(context.helper(MERGE_PROPS), mergePropsArgs)
|
||||
: mergePropsArgs[0]
|
||||
}
|
||||
|
||||
function isTrueFalseValue(prop: DirectiveNode | AttributeNode) {
|
||||
if (prop.type === NodeTypes.DIRECTIVE) {
|
||||
return (
|
||||
|
@ -12,7 +12,7 @@ return withDirectives(h(comp), [
|
||||
*/
|
||||
|
||||
import { VNode } from './vnode'
|
||||
import { isFunction, EMPTY_OBJ, makeMap } from '@vue/shared'
|
||||
import { isFunction, EMPTY_OBJ, isBuiltInDirective } from '@vue/shared'
|
||||
import { warn } from './warning'
|
||||
import { ComponentInternalInstance, Data } from './component'
|
||||
import { currentRenderingInstance } from './componentRenderContext'
|
||||
@ -63,10 +63,6 @@ export type Directive<T = any, V = any> =
|
||||
|
||||
export type DirectiveModifiers = Record<string, boolean>
|
||||
|
||||
const isBuiltInDirective = /*#__PURE__*/ makeMap(
|
||||
'bind,cloak,else-if,else,for,html,if,model,on,once,pre,show,slot,text,memo'
|
||||
)
|
||||
|
||||
export function validateDirectiveName(name: string) {
|
||||
if (isBuiltInDirective(name)) {
|
||||
warn('Do not use built-in directive ids as custom directive id: ' + name)
|
||||
|
@ -10,8 +10,10 @@ import {
|
||||
vShow,
|
||||
vModelText,
|
||||
vModelRadio,
|
||||
vModelCheckbox
|
||||
vModelCheckbox,
|
||||
resolveDirective
|
||||
} from 'vue'
|
||||
import { ssrGetDirectiveProps, ssrRenderAttrs } from '../src'
|
||||
|
||||
describe('ssr: directives', () => {
|
||||
describe('template v-show', () => {
|
||||
@ -374,7 +376,7 @@ describe('ssr: directives', () => {
|
||||
})
|
||||
})
|
||||
|
||||
test('custom directive w/ getSSRProps', async () => {
|
||||
test('custom directive w/ getSSRProps (vdom)', async () => {
|
||||
expect(
|
||||
await renderToString(
|
||||
createApp({
|
||||
@ -394,4 +396,35 @@ describe('ssr: directives', () => {
|
||||
)
|
||||
).toBe(`<div id="foo"></div>`)
|
||||
})
|
||||
|
||||
test('custom directive w/ getSSRProps (optimized)', async () => {
|
||||
expect(
|
||||
await renderToString(
|
||||
createApp({
|
||||
data() {
|
||||
return {
|
||||
x: 'foo'
|
||||
}
|
||||
},
|
||||
directives: {
|
||||
xxx: {
|
||||
getSSRProps({ value, arg, modifiers }) {
|
||||
return { id: [value, arg, modifiers.ok].join('-') }
|
||||
}
|
||||
}
|
||||
},
|
||||
ssrRender(_ctx, _push, _parent, _attrs) {
|
||||
const _directive_xxx = resolveDirective('xxx')!
|
||||
_push(
|
||||
`<div${ssrRenderAttrs(
|
||||
ssrGetDirectiveProps(_ctx, _directive_xxx, _ctx.x, 'arg', {
|
||||
ok: true
|
||||
})
|
||||
)}></div>`
|
||||
)
|
||||
}
|
||||
})
|
||||
)
|
||||
).toBe(`<div id="foo-arg-true"></div>`)
|
||||
})
|
||||
})
|
||||
|
26
packages/server-renderer/src/helpers/ssrGetDirectiveProps.ts
Normal file
26
packages/server-renderer/src/helpers/ssrGetDirectiveProps.ts
Normal file
@ -0,0 +1,26 @@
|
||||
import { ComponentPublicInstance, Directive } from '@vue/runtime-core'
|
||||
|
||||
export function ssrGetDirectiveProps(
|
||||
instance: ComponentPublicInstance,
|
||||
dir: Directive,
|
||||
value?: any,
|
||||
arg?: string,
|
||||
modifiers: Record<string, boolean> = {}
|
||||
): Record<string, any> {
|
||||
if (typeof dir !== 'function' && dir.getSSRProps) {
|
||||
return (
|
||||
dir.getSSRProps(
|
||||
{
|
||||
dir,
|
||||
instance,
|
||||
value,
|
||||
oldValue: undefined,
|
||||
arg,
|
||||
modifiers
|
||||
},
|
||||
null as any
|
||||
) || {}
|
||||
)
|
||||
}
|
||||
return {}
|
||||
}
|
@ -30,6 +30,7 @@ export {
|
||||
export { ssrInterpolate } from './helpers/ssrInterpolate'
|
||||
export { ssrRenderList } from './helpers/ssrRenderList'
|
||||
export { ssrRenderSuspense } from './helpers/ssrRenderSuspense'
|
||||
export { ssrGetDirectiveProps } from './helpers/ssrGetDirectiveProps'
|
||||
export { includeBooleanAttr as ssrIncludeBooleanAttr } from '@vue/shared'
|
||||
|
||||
// v-model helpers
|
||||
|
@ -90,6 +90,10 @@ export const isReservedProp = /*#__PURE__*/ makeMap(
|
||||
'onVnodeBeforeUnmount,onVnodeUnmounted'
|
||||
)
|
||||
|
||||
export const isBuiltInDirective = /*#__PURE__*/ makeMap(
|
||||
'bind,cloak,else-if,else,for,html,if,model,on,once,pre,show,slot,text,memo'
|
||||
)
|
||||
|
||||
const cacheStringFunction = <T extends (str: string) => string>(fn: T): T => {
|
||||
const cache: Record<string, string> = Object.create(null)
|
||||
return ((str: string) => {
|
||||
|
Loading…
Reference in New Issue
Block a user