Merge facd0e8f50
into 5f8314cb7f
This commit is contained in:
commit
929ce1a793
|
@ -0,0 +1,86 @@
|
|||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`compiler: v-scope transform > complex expression 1`] = `
|
||||
"import { toDisplayString as _toDisplayString, createTextVNode as _createTextVNode, createElementVNode as _createElementVNode, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"
|
||||
|
||||
export function render(_ctx, _cache) {
|
||||
return (_openBlock(), _createElementBlock("div", null, [
|
||||
((a = _ctx.foo + _ctx.bar) => _createElementVNode("div", null, [
|
||||
((b = a + _ctx.baz) => _createElementVNode("span", null, [
|
||||
_createTextVNode(_toDisplayString(b), 1 /* TEXT */)
|
||||
]))()
|
||||
]))(),
|
||||
((exp = _ctx.getExp()) => _createElementVNode("div", null, [
|
||||
_createTextVNode(_toDisplayString(exp), 1 /* TEXT */)
|
||||
]))()
|
||||
]))
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`compiler: v-scope transform > nested v-scope 1`] = `
|
||||
"import { toDisplayString as _toDisplayString, createTextVNode as _createTextVNode, createElementVNode as _createElementVNode, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"
|
||||
|
||||
export function render(_ctx, _cache) {
|
||||
return (_openBlock(), _createElementBlock("div", null, [
|
||||
((a = 1) => _createElementVNode("div", null, [
|
||||
((b = 1) => _createElementVNode("span", null, [
|
||||
_createTextVNode(_toDisplayString(a) + _toDisplayString(b), 1 /* TEXT */)
|
||||
]))()
|
||||
]))()
|
||||
]))
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`compiler: v-scope transform > ok v-if 1`] = `
|
||||
"import { toDisplayString as _toDisplayString, createTextVNode as _createTextVNode, openBlock as _openBlock, createElementBlock as _createElementBlock, createCommentVNode as _createCommentVNode } from "vue"
|
||||
|
||||
export function render(_ctx, _cache) {
|
||||
return (_openBlock(), _createElementBlock("div", null, [
|
||||
(_ctx.ok)
|
||||
? ((a = true) => (_openBlock(), _createElementBlock("div", { key: 0 }, [
|
||||
_createTextVNode(_toDisplayString(a), 1 /* TEXT */)
|
||||
])))()
|
||||
: _createCommentVNode("v-if", true)
|
||||
]))
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`compiler: v-scope transform > on v-for 1`] = `
|
||||
"import { renderList as _renderList, Fragment as _Fragment, openBlock as _openBlock, createElementBlock as _createElementBlock, toDisplayString as _toDisplayString, createTextVNode as _createTextVNode, createElementVNode as _createElementVNode } from "vue"
|
||||
|
||||
export function render(_ctx, _cache) {
|
||||
return (_openBlock(), _createElementBlock("div", null, [
|
||||
(_openBlock(), _createElementBlock(_Fragment, null, _renderList([1,2,3], (i) => {
|
||||
return ((a = i+1) => _createElementVNode("div", null, [
|
||||
_createTextVNode(_toDisplayString(a), 1 /* TEXT */)
|
||||
]))()
|
||||
}), 64 /* STABLE_FRAGMENT */))
|
||||
]))
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`compiler: v-scope transform > should work 1`] = `
|
||||
"import { toDisplayString as _toDisplayString, createTextVNode as _createTextVNode, createElementVNode as _createElementVNode, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"
|
||||
|
||||
export function render(_ctx, _cache) {
|
||||
return (_openBlock(), _createElementBlock("div", null, [
|
||||
((a = 1, b = 2) => _createElementVNode("div", null, [
|
||||
_createTextVNode(_toDisplayString(a) + " " + _toDisplayString(b), 1 /* TEXT */)
|
||||
]))()
|
||||
]))
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`compiler: v-scope transform > work with variable 1`] = `
|
||||
"import { toDisplayString as _toDisplayString, createTextVNode as _createTextVNode, createElementVNode as _createElementVNode, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"
|
||||
|
||||
export function render(_ctx, _cache) {
|
||||
return (_openBlock(), _createElementBlock("div", null, [
|
||||
((a = _ctx.msg) => _createElementVNode("div", null, [
|
||||
((b = a) => _createElementVNode("span", null, [
|
||||
_createTextVNode(_toDisplayString(b), 1 /* TEXT */)
|
||||
]))()
|
||||
]))()
|
||||
]))
|
||||
}"
|
||||
`;
|
|
@ -0,0 +1,84 @@
|
|||
import { type CompilerOptions, baseCompile } from '../../src'
|
||||
|
||||
describe('compiler: v-scope transform', () => {
|
||||
function compile(content: string, options: CompilerOptions = {}) {
|
||||
return baseCompile(`<div>${content}</div>`, {
|
||||
mode: 'module',
|
||||
prefixIdentifiers: true,
|
||||
...options,
|
||||
}).code
|
||||
}
|
||||
|
||||
test('should work', () => {
|
||||
expect(
|
||||
compile(
|
||||
`<div v-scope="{ a:1, b:2 }">
|
||||
{{a}} {{b}}
|
||||
</div>`,
|
||||
),
|
||||
).toMatchSnapshot()
|
||||
})
|
||||
|
||||
test('nested v-scope', () => {
|
||||
expect(
|
||||
compile(
|
||||
`<div v-scope="{ a:1 }">
|
||||
<span v-scope="{ b:1 }">{{ a }}{{ b }}</span>
|
||||
</div>`,
|
||||
),
|
||||
).toMatchSnapshot()
|
||||
})
|
||||
|
||||
test('work with variable', () => {
|
||||
expect(
|
||||
compile(
|
||||
`<div v-scope="{ a:msg }">
|
||||
<span v-scope="{ b:a }">{{ b }}</span>
|
||||
</div>`,
|
||||
),
|
||||
).toMatchSnapshot()
|
||||
})
|
||||
|
||||
test('complex expression', () => {
|
||||
expect(
|
||||
compile(`
|
||||
<div v-scope="{ a:foo + bar }">
|
||||
<span v-scope="{ b:a + baz }">{{ b }}</span>
|
||||
</div>
|
||||
<div v-scope="{ exp:getExp() }">{{ exp }}</div>
|
||||
`),
|
||||
).toMatchSnapshot()
|
||||
})
|
||||
|
||||
test('on v-for', () => {
|
||||
expect(
|
||||
compile(`
|
||||
<div v-for="i in [1,2,3]" v-scope="{ a:i+1 }">
|
||||
{{ a }}
|
||||
</div>
|
||||
`),
|
||||
).toMatchSnapshot()
|
||||
})
|
||||
|
||||
test('ok v-if', () => {
|
||||
expect(
|
||||
compile(`
|
||||
<div v-if="ok" v-scope="{ a:true }" >
|
||||
{{ a }}
|
||||
</div>
|
||||
`),
|
||||
).toMatchSnapshot()
|
||||
})
|
||||
|
||||
test('error', () => {
|
||||
const onError = vi.fn()
|
||||
expect(compile(`<div v-scope="{ a:, b:1 }">{{ a }}</div>`, { onError }))
|
||||
expect(onError.mock.calls).toMatchInlineSnapshot(`
|
||||
[
|
||||
[
|
||||
[SyntaxError: Error parsing JavaScript expression: Unexpected token (1:5)],
|
||||
],
|
||||
]
|
||||
`)
|
||||
})
|
||||
})
|
|
@ -144,6 +144,7 @@ export interface PlainElementNode extends BaseElementNode {
|
|||
| SimpleExpressionNode // when hoisted
|
||||
| CacheExpression // when cached by v-once
|
||||
| MemoExpression // when cached by v-memo
|
||||
| CallExpression
|
||||
| undefined
|
||||
ssrCodegenNode?: TemplateLiteral
|
||||
}
|
||||
|
@ -360,7 +361,7 @@ export type JSChildNode =
|
|||
|
||||
export interface CallExpression extends Node {
|
||||
type: NodeTypes.JS_CALL_EXPRESSION
|
||||
callee: string | symbol
|
||||
callee: string | symbol | FunctionExpression
|
||||
arguments: (
|
||||
| string
|
||||
| symbol
|
||||
|
|
|
@ -886,11 +886,24 @@ function genNullableArgs(args: any[]): CallExpression['arguments'] {
|
|||
// JavaScript
|
||||
function genCallExpression(node: CallExpression, context: CodegenContext) {
|
||||
const { push, helper, pure } = context
|
||||
const callee = isString(node.callee) ? node.callee : helper(node.callee)
|
||||
let callee
|
||||
if (isString(node.callee)) {
|
||||
callee = node.callee
|
||||
} else if (isSymbol(node.callee)) {
|
||||
callee = helper(node.callee)
|
||||
} else {
|
||||
// anonymous function.
|
||||
if (context.inSSR) push(';')
|
||||
push(`(`)
|
||||
genNode(node.callee, context)
|
||||
push(`)`)
|
||||
}
|
||||
|
||||
if (pure) {
|
||||
push(PURE_ANNOTATION)
|
||||
}
|
||||
push(callee + `(`, NewlineType.None, node)
|
||||
callee && push(callee)
|
||||
push(`(`, NewlineType.None, node)
|
||||
genNodeList(node.arguments, context)
|
||||
push(`)`)
|
||||
}
|
||||
|
|
|
@ -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 { trackVScopeScopes, transformScope } from './transforms/vScope'
|
||||
|
||||
export type TransformPreset = [
|
||||
NodeTransform[],
|
||||
|
@ -36,6 +37,7 @@ export function getBaseTransformPreset(
|
|||
transformOnce,
|
||||
transformIf,
|
||||
transformMemo,
|
||||
transformScope,
|
||||
transformFor,
|
||||
...(__COMPAT__ ? [transformFilter] : []),
|
||||
...(!__BROWSER__ && prefixIdentifiers
|
||||
|
@ -50,6 +52,7 @@ export function getBaseTransformPreset(
|
|||
transformSlotOutlet,
|
||||
transformElement,
|
||||
trackSlotScopes,
|
||||
trackVScopeScopes,
|
||||
transformText,
|
||||
],
|
||||
{
|
||||
|
|
|
@ -59,6 +59,11 @@ export {
|
|||
trackVForSlotScopes,
|
||||
trackSlotScopes,
|
||||
} from './transforms/vSlot'
|
||||
export {
|
||||
transformScope,
|
||||
trackVScopeScopes,
|
||||
transformScopeExpression,
|
||||
} from './transforms/vScope'
|
||||
export {
|
||||
transformElement,
|
||||
resolveComponentType,
|
||||
|
|
|
@ -412,7 +412,7 @@ function getConstantTypeOfHelperCall(
|
|||
): ConstantTypes {
|
||||
if (
|
||||
value.type === NodeTypes.JS_CALL_EXPRESSION &&
|
||||
!isString(value.callee) &&
|
||||
isSymbol(value.callee) &&
|
||||
allowHoistedHelperSet.has(value.callee)
|
||||
) {
|
||||
const arg = value.arguments[0] as JSChildNode
|
||||
|
|
|
@ -49,6 +49,7 @@ import { processExpression } from './transformExpression'
|
|||
import { validateBrowserExpression } from '../validateExpression'
|
||||
import { PatchFlags } from '@vue/shared'
|
||||
import { transformBindShorthand } from './vBind'
|
||||
import { transformScopeExpression } from './vScope'
|
||||
|
||||
export const transformFor: NodeTransform = createStructuralDirectiveTransform(
|
||||
'for',
|
||||
|
@ -62,6 +63,7 @@ export const transformFor: NodeTransform = createStructuralDirectiveTransform(
|
|||
]) as ForRenderListExpression
|
||||
const isTemplate = isTemplateNode(node)
|
||||
const memo = findDir(node, 'memo')
|
||||
const vScope = findDir(node, 'scope')
|
||||
const keyProp = findProp(node, `key`, false, true)
|
||||
const isDirKey = keyProp && keyProp.type === NodeTypes.DIRECTIVE
|
||||
if (isDirKey && !keyProp.exp) {
|
||||
|
@ -244,10 +246,22 @@ export const transformFor: NodeTransform = createStructuralDirectiveTransform(
|
|||
// increment cache count
|
||||
context.cached.push(null)
|
||||
} else {
|
||||
let child
|
||||
if (vScope) {
|
||||
child = createCallExpression(
|
||||
createFunctionExpression(
|
||||
transformScopeExpression(vScope.exp!),
|
||||
childBlock,
|
||||
),
|
||||
)
|
||||
} else {
|
||||
child = childBlock
|
||||
}
|
||||
|
||||
renderExp.arguments.push(
|
||||
createFunctionExpression(
|
||||
createForLoopParams(forNode.parseResult),
|
||||
childBlock,
|
||||
child,
|
||||
true /* force newline */,
|
||||
) as ForIteratorExpression,
|
||||
)
|
||||
|
|
|
@ -8,16 +8,19 @@ import {
|
|||
type AttributeNode,
|
||||
type BlockCodegenNode,
|
||||
type CacheExpression,
|
||||
type CallExpression,
|
||||
ConstantTypes,
|
||||
type DirectiveNode,
|
||||
type ElementNode,
|
||||
ElementTypes,
|
||||
type FunctionExpression,
|
||||
type IfBranchNode,
|
||||
type IfConditionalExpression,
|
||||
type IfNode,
|
||||
type MemoExpression,
|
||||
NodeTypes,
|
||||
type SimpleExpressionNode,
|
||||
type VNodeCall,
|
||||
convertToBlock,
|
||||
createCallExpression,
|
||||
createConditionalExpression,
|
||||
|
@ -293,7 +296,12 @@ function createChildrenCodegenNode(
|
|||
const ret = (firstChild as ElementNode).codegenNode as
|
||||
| BlockCodegenNode
|
||||
| MemoExpression
|
||||
const vnodeCall = getMemoedVNodeCall(ret)
|
||||
|
||||
const vnodeCall = findDir(firstChild, 'scope')
|
||||
? (((ret as CallExpression).callee as FunctionExpression)
|
||||
.returns as VNodeCall)
|
||||
: getMemoedVNodeCall(ret)
|
||||
|
||||
// Change createVNode to createBlock.
|
||||
if (vnodeCall.type === NodeTypes.VNODE_CALL) {
|
||||
convertToBlock(vnodeCall, context)
|
||||
|
|
|
@ -0,0 +1,71 @@
|
|||
import type { NodeTransform } from '../transform'
|
||||
import { findDir } from '../utils'
|
||||
import {
|
||||
type ExpressionNode,
|
||||
NodeTypes,
|
||||
type PlainElementNode,
|
||||
type SimpleExpressionNode,
|
||||
createCallExpression,
|
||||
createFunctionExpression,
|
||||
createSimpleExpression,
|
||||
} from '../ast'
|
||||
import { stringifyExpression } from './transformExpression'
|
||||
|
||||
const seen = new WeakSet()
|
||||
const extractKeyValueRE = /(\w+)\s*:\s*"*(.*?)"*\s*[,}\n]/g
|
||||
|
||||
export const transformScope: NodeTransform = (node, context) => {
|
||||
if (node.type === NodeTypes.ELEMENT) {
|
||||
const dir = findDir(node, 'scope')
|
||||
if (!dir || seen.has(node)) {
|
||||
return
|
||||
}
|
||||
seen.add(node)
|
||||
return () => {
|
||||
const codegenNode =
|
||||
node.codegenNode ||
|
||||
(context.currentNode as PlainElementNode).codegenNode
|
||||
if (codegenNode && codegenNode.type === NodeTypes.VNODE_CALL) {
|
||||
node.codegenNode = createCallExpression(
|
||||
createFunctionExpression(
|
||||
transformScopeExpression(dir.exp!),
|
||||
codegenNode,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function transformScopeExpression(
|
||||
exp: string | ExpressionNode,
|
||||
): ExpressionNode[] {
|
||||
const params: SimpleExpressionNode[] = []
|
||||
const rExpString = stringifyExpression(exp)
|
||||
let match
|
||||
while ((match = extractKeyValueRE.exec(rExpString))) {
|
||||
params.push(createSimpleExpression(`${match[1]} = ${match[2]}`))
|
||||
}
|
||||
return params
|
||||
}
|
||||
|
||||
export const trackVScopeScopes: NodeTransform = (node, context) => {
|
||||
if (node.type === NodeTypes.ELEMENT) {
|
||||
const vScope = findDir(node, 'scope')
|
||||
if (vScope) {
|
||||
const keys: string[] = []
|
||||
let match
|
||||
while ((match = extractKeyValueRE.exec(vScope.exp!.loc.source))) {
|
||||
keys.push(match[1])
|
||||
}
|
||||
if (!__BROWSER__ && context.prefixIdentifiers) {
|
||||
keys.forEach(context.addIdentifiers)
|
||||
}
|
||||
return () => {
|
||||
if (!__BROWSER__ && context.prefixIdentifiers) {
|
||||
keys.forEach(context.removeIdentifiers)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -37,7 +37,7 @@ import {
|
|||
TO_HANDLERS,
|
||||
WITH_MEMO,
|
||||
} from './runtimeHelpers'
|
||||
import { NOOP, isObject, isString } from '@vue/shared'
|
||||
import { NOOP, isObject, isString, isSymbol } from '@vue/shared'
|
||||
import type { PropsExpression } from './transforms/transformElement'
|
||||
import { parseExpression } from '@babel/parser'
|
||||
import type { Expression, Node } from '@babel/types'
|
||||
|
@ -373,7 +373,7 @@ function getUnnormalizedProps(
|
|||
props.type === NodeTypes.JS_CALL_EXPRESSION
|
||||
) {
|
||||
const callee = props.callee
|
||||
if (!isString(callee) && propsHelperSet.has(callee)) {
|
||||
if (isSymbol(callee) && propsHelperSet.has(callee)) {
|
||||
return getUnnormalizedProps(
|
||||
props.arguments[0] as PropsExpression,
|
||||
callPath.concat(props),
|
||||
|
|
|
@ -0,0 +1,66 @@
|
|||
import { compile } from '../src'
|
||||
|
||||
describe('ssr: v-scope', () => {
|
||||
test('basic', () => {
|
||||
expect(compile(`<div v-scope="{ a:1 }">{{a}}</div>`).code)
|
||||
.toMatchInlineSnapshot(`
|
||||
"const { ssrRenderAttrs: _ssrRenderAttrs, ssrInterpolate: _ssrInterpolate } = require("vue/server-renderer")
|
||||
|
||||
return function ssrRender(_ctx, _push, _parent, _attrs) {
|
||||
_push(\`<div\${_ssrRenderAttrs(_attrs)}>\`)
|
||||
;((a = 1) => {
|
||||
_push(\`\${_ssrInterpolate(a)}\`)
|
||||
})()
|
||||
_push(\`</div>\`)
|
||||
}"
|
||||
`)
|
||||
})
|
||||
|
||||
test('nested', () => {
|
||||
expect(
|
||||
compile(
|
||||
`<div v-scope="{ a:1 }">
|
||||
<div v-scope="{ b:2 }">{{a}} {{b}}</div>
|
||||
</div>`,
|
||||
).code,
|
||||
).toMatchInlineSnapshot(`
|
||||
"const { ssrRenderAttrs: _ssrRenderAttrs, ssrInterpolate: _ssrInterpolate } = require("vue/server-renderer")
|
||||
|
||||
return function ssrRender(_ctx, _push, _parent, _attrs) {
|
||||
_push(\`<div\${_ssrRenderAttrs(_attrs)}>\`)
|
||||
;((a = 1) => {
|
||||
_push(\`<div>\`)
|
||||
;((b = 2) => {
|
||||
_push(\`\${_ssrInterpolate(a)} \${_ssrInterpolate(b)}\`)
|
||||
})()
|
||||
_push(\`</div>\`)
|
||||
})()
|
||||
_push(\`</div>\`)
|
||||
}"
|
||||
`)
|
||||
})
|
||||
|
||||
test('ok v-if', () => {
|
||||
expect(
|
||||
compile(`
|
||||
<div v-if="ok" v-scope="{ a:true }" >
|
||||
{{ a }}
|
||||
</div>
|
||||
`).code,
|
||||
).toMatchInlineSnapshot(`
|
||||
"const { ssrRenderAttrs: _ssrRenderAttrs, ssrInterpolate: _ssrInterpolate } = require("vue/server-renderer")
|
||||
|
||||
return function ssrRender(_ctx, _push, _parent, _attrs) {
|
||||
if (_ctx.ok) {
|
||||
_push(\`<div\${_ssrRenderAttrs(_attrs)}>\`)
|
||||
;((a = true) => {
|
||||
_push(\`\${_ssrInterpolate(a)}\`)
|
||||
})()
|
||||
_push(\`</div>\`)
|
||||
} else {
|
||||
_push(\`<!---->\`)
|
||||
}
|
||||
}"
|
||||
`)
|
||||
})
|
||||
})
|
|
@ -8,6 +8,7 @@ import {
|
|||
parserOptions,
|
||||
trackSlotScopes,
|
||||
trackVForSlotScopes,
|
||||
trackVScopeScopes,
|
||||
transform,
|
||||
transformBind,
|
||||
transformExpression,
|
||||
|
@ -65,6 +66,7 @@ export function compile(
|
|||
ssrTransformElement,
|
||||
ssrTransformComponent,
|
||||
trackSlotScopes,
|
||||
trackVScopeScopes,
|
||||
transformStyle,
|
||||
...(options.nodeTransforms || []), // user transforms
|
||||
],
|
||||
|
|
|
@ -24,6 +24,7 @@ import {
|
|||
createCompilerError,
|
||||
createCompoundExpression,
|
||||
createConditionalExpression,
|
||||
createFunctionExpression,
|
||||
createInterpolation,
|
||||
createSequenceExpression,
|
||||
createSimpleExpression,
|
||||
|
@ -32,6 +33,7 @@ import {
|
|||
hasDynamicKeyVBind,
|
||||
isStaticArgOf,
|
||||
isStaticExp,
|
||||
transformScopeExpression,
|
||||
} from '@vue/compiler-dom'
|
||||
import {
|
||||
NO,
|
||||
|
@ -56,6 +58,7 @@ import {
|
|||
import {
|
||||
type SSRTransformContext,
|
||||
processChildren,
|
||||
processChildrenAsStatement,
|
||||
} from '../ssrCodegenTransform'
|
||||
|
||||
// for directives with children overwrite (e.g. v-html & v-text), we need to
|
||||
|
@ -460,7 +463,17 @@ export function ssrProcessElement(
|
|||
if (rawChildren) {
|
||||
context.pushStringPart(rawChildren)
|
||||
} else if (node.children.length) {
|
||||
processChildren(node, context)
|
||||
// process v-scope
|
||||
const dir = findDir(node, 'scope')
|
||||
if (dir) {
|
||||
const scopeFn = createFunctionExpression(
|
||||
transformScopeExpression(dir.exp!),
|
||||
)
|
||||
scopeFn.body = processChildrenAsStatement(node, context)
|
||||
context.pushStatement(createCallExpression(scopeFn))
|
||||
} else {
|
||||
processChildren(node, context)
|
||||
}
|
||||
}
|
||||
|
||||
if (!isVoidTag(node.tag)) {
|
||||
|
|
|
@ -90,7 +90,7 @@ export const isReservedProp: (key: string) => boolean = /*@__PURE__*/ makeMap(
|
|||
|
||||
export const isBuiltInDirective: (key: string) => boolean =
|
||||
/*@__PURE__*/ makeMap(
|
||||
'bind,cloak,else-if,else,for,html,if,model,on,once,pre,show,slot,text,memo',
|
||||
'bind,cloak,else-if,else,for,html,if,model,on,once,pre,show,slot,text,memo,scope',
|
||||
)
|
||||
|
||||
const cacheStringFunction = <T extends (str: string) => string>(fn: T): T => {
|
||||
|
|
Loading…
Reference in New Issue