Skip to content

Commit b0104fb

Browse files
committed
feat(compiler-core): allow directive modifiers to be dynamic (fix #8281)
1 parent 0c8dd94 commit b0104fb

File tree

14 files changed

+402
-20
lines changed

14 files changed

+402
-20
lines changed

packages/compiler-core/__tests__/parse.spec.ts

Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1446,6 +1446,202 @@ describe('compiler: parse', () => {
14461446
})
14471447
})
14481448

1449+
test('directive with dynamic modifiers', () => {
1450+
const ast = baseParse('<div v-on.enter.[a]/>')
1451+
const directive = (ast.children[0] as ElementNode).props[0]
1452+
1453+
expect(directive).toStrictEqual({
1454+
type: NodeTypes.DIRECTIVE,
1455+
name: 'on',
1456+
rawName: 'v-on.enter.[a]',
1457+
arg: undefined,
1458+
modifiers: [
1459+
{
1460+
constType: 3,
1461+
content: 'enter',
1462+
isStatic: true,
1463+
loc: {
1464+
end: {
1465+
column: 16,
1466+
line: 1,
1467+
offset: 15,
1468+
},
1469+
source: 'enter',
1470+
start: {
1471+
column: 11,
1472+
line: 1,
1473+
offset: 10,
1474+
},
1475+
},
1476+
type: 4,
1477+
},
1478+
{
1479+
constType: 0,
1480+
content: 'a',
1481+
isStatic: false,
1482+
loc: {
1483+
end: {
1484+
column: 20,
1485+
line: 1,
1486+
offset: 19,
1487+
},
1488+
source: '[a]',
1489+
start: {
1490+
column: 17,
1491+
line: 1,
1492+
offset: 16,
1493+
},
1494+
},
1495+
type: 4,
1496+
},
1497+
],
1498+
exp: undefined,
1499+
loc: {
1500+
start: { offset: 5, line: 1, column: 6 },
1501+
end: { column: 20, line: 1, offset: 19 },
1502+
source: 'v-on.enter.[a]',
1503+
},
1504+
})
1505+
})
1506+
1507+
test('directive with empty modifier name', () => {
1508+
let errorCode = -1
1509+
const ast = baseParse('<div v-on./>', {
1510+
onError: err => {
1511+
errorCode = err.code as number
1512+
},
1513+
})
1514+
const directive = (ast.children[0] as ElementNode).props[0]
1515+
1516+
expect(errorCode).toBe(ErrorCodes.X_MISSING_DIRECTIVE_MODIFIER_NAME)
1517+
1518+
expect(directive).toStrictEqual({
1519+
type: NodeTypes.DIRECTIVE,
1520+
name: 'on',
1521+
rawName: 'v-on.',
1522+
arg: undefined,
1523+
modifiers: [],
1524+
exp: undefined,
1525+
loc: {
1526+
start: { offset: 5, line: 1, column: 6 },
1527+
end: { column: 11, line: 1, offset: 10 },
1528+
source: 'v-on.',
1529+
},
1530+
})
1531+
})
1532+
1533+
test('directive with empty modifier name and value', () => {
1534+
let errorCode = -1
1535+
const ast = baseParse('<div v-on.="a"/>', {
1536+
onError: err => {
1537+
errorCode = err.code as number
1538+
},
1539+
})
1540+
const directive = (ast.children[0] as ElementNode).props[0]
1541+
1542+
expect(errorCode).toBe(ErrorCodes.X_MISSING_DIRECTIVE_MODIFIER_NAME)
1543+
1544+
expect(directive).toStrictEqual({
1545+
type: NodeTypes.DIRECTIVE,
1546+
name: 'on',
1547+
rawName: 'v-on.',
1548+
arg: undefined,
1549+
modifiers: [],
1550+
exp: {
1551+
constType: 0,
1552+
content: 'a',
1553+
isStatic: false,
1554+
loc: {
1555+
end: {
1556+
column: 14,
1557+
line: 1,
1558+
offset: 13,
1559+
},
1560+
source: 'a',
1561+
start: {
1562+
column: 13,
1563+
line: 1,
1564+
offset: 12,
1565+
},
1566+
},
1567+
type: 4,
1568+
},
1569+
loc: {
1570+
start: { offset: 5, line: 1, column: 6 },
1571+
end: { column: 15, line: 1, offset: 14 },
1572+
source: 'v-on.="a"',
1573+
},
1574+
})
1575+
})
1576+
1577+
test('directive with missing dynamic modifier value', () => {
1578+
let errorCode = -1
1579+
const ast = baseParse('<div v-on.[] />', {
1580+
onError: err => {
1581+
errorCode = err.code as number
1582+
},
1583+
})
1584+
const directive = (ast.children[0] as ElementNode).props[0]
1585+
1586+
expect(errorCode).toBe(
1587+
ErrorCodes.X_MISSING_DYNAMIC_DIRECTIVE_MODIFIER_VALUE,
1588+
)
1589+
1590+
expect(directive).toStrictEqual({
1591+
type: NodeTypes.DIRECTIVE,
1592+
name: 'on',
1593+
rawName: 'v-on.[]',
1594+
arg: undefined,
1595+
modifiers: [],
1596+
exp: undefined,
1597+
loc: {
1598+
start: { offset: 5, line: 1, column: 6 },
1599+
end: { column: 13, line: 1, offset: 12 },
1600+
source: 'v-on.[]',
1601+
},
1602+
})
1603+
})
1604+
1605+
test('directive with invalid dynamic modifier value', () => {
1606+
const possibleWrongValues = [
1607+
'[]',
1608+
'null',
1609+
'123',
1610+
'"foo"',
1611+
'`foo`',
1612+
'!false',
1613+
]
1614+
1615+
possibleWrongValues.forEach(val => {
1616+
let errorCode = -1
1617+
const ast = baseParse(`<div v-on.[${val}] />`, {
1618+
onError: err => {
1619+
errorCode = err.code as number
1620+
},
1621+
prefixIdentifiers: true,
1622+
})
1623+
const directive = (ast.children[0] as ElementNode).props[0]
1624+
1625+
expect(errorCode).toBe(
1626+
ErrorCodes.X_INVALID_VALUE_IN_DYNAMIC_DIRECTIVE_MODIFIER,
1627+
)
1628+
1629+
expect(directive).toStrictEqual({
1630+
type: NodeTypes.DIRECTIVE,
1631+
name: 'on',
1632+
rawName: `v-on.[${val}]`,
1633+
arg: undefined,
1634+
modifiers: [],
1635+
exp: undefined,
1636+
loc: {
1637+
end: { column: 13 + val.length, line: 1, offset: 12 + val.length },
1638+
source: `v-on.[${val}]`,
1639+
start: { column: 6, line: 1, offset: 5 },
1640+
},
1641+
})
1642+
})
1643+
})
1644+
14491645
test('directive with argument and modifiers', () => {
14501646
const ast = baseParse('<div v-on:click.enter.exact/>')
14511647
const directive = (ast.children[0] as ElementNode).props[0]

packages/compiler-core/__tests__/transforms/transformElement.spec.ts

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -664,12 +664,13 @@ describe('compiler: element transform', () => {
664664

665665
test('runtime directives', () => {
666666
const { root, node } = parseWithElementTransform(
667-
`<div v-foo v-bar="x" v-baz:[arg].mod.mad="y" />`,
667+
`<div v-foo v-bar="x" v-baz:[arg].mod.mad="y" v-baa.[{dyn:true}].[mid].boo="z" />`,
668668
)
669669
expect(root.helpers).toContain(RESOLVE_DIRECTIVE)
670670
expect(root.directives).toContain(`foo`)
671671
expect(root.directives).toContain(`bar`)
672672
expect(root.directives).toContain(`baz`)
673+
expect(root.directives).toContain(`baa`)
673674

674675
expect(node).toMatchObject({
675676
directives: {
@@ -740,6 +741,54 @@ describe('compiler: element transform', () => {
740741
},
741742
],
742743
},
744+
{
745+
type: NodeTypes.JS_ARRAY_EXPRESSION,
746+
elements: [
747+
`_directive_baa`,
748+
// exp
749+
{
750+
type: NodeTypes.SIMPLE_EXPRESSION,
751+
content: `z`,
752+
isStatic: false,
753+
},
754+
//arg
755+
'void 0',
756+
// modifiers
757+
{
758+
type: NodeTypes.JS_CALL_EXPRESSION,
759+
callee: 'Object.assign',
760+
arguments: [
761+
createObjectMatcher({}),
762+
{
763+
type: NodeTypes.SIMPLE_EXPRESSION,
764+
content: `{dyn:true}`,
765+
},
766+
{
767+
type: NodeTypes.SIMPLE_EXPRESSION,
768+
content: `mid`,
769+
},
770+
{
771+
type: NodeTypes.JS_OBJECT_EXPRESSION,
772+
properties: [
773+
{
774+
type: NodeTypes.JS_PROPERTY,
775+
key: {
776+
type: NodeTypes.SIMPLE_EXPRESSION,
777+
content: `boo`,
778+
isStatic: true,
779+
},
780+
value: {
781+
type: NodeTypes.SIMPLE_EXPRESSION,
782+
content: `true`,
783+
isStatic: false,
784+
},
785+
},
786+
],
787+
},
788+
],
789+
},
790+
],
791+
},
743792
],
744793
},
745794
})

packages/compiler-core/src/ast.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -203,7 +203,7 @@ export interface DirectiveNode extends Node {
203203
rawName?: string
204204
exp: ExpressionNode | undefined
205205
arg: ExpressionNode | undefined
206-
modifiers: SimpleExpressionNode[]
206+
modifiers: ExpressionNode[]
207207
/**
208208
* optional property to cache the expression parse result for v-for
209209
*/

packages/compiler-core/src/errors.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,10 @@ export enum ErrorCodes {
6969
X_MISSING_INTERPOLATION_END,
7070
X_MISSING_DIRECTIVE_NAME,
7171
X_MISSING_DYNAMIC_DIRECTIVE_ARGUMENT_END,
72+
X_MISSING_DYNAMIC_DIRECTIVE_MODIFIER_END,
73+
X_MISSING_DIRECTIVE_MODIFIER_NAME,
74+
X_MISSING_DYNAMIC_DIRECTIVE_MODIFIER_VALUE,
75+
X_INVALID_VALUE_IN_DYNAMIC_DIRECTIVE_MODIFIER,
7276

7377
// transform errors
7478
X_V_IF_NO_EXPRESSION,
@@ -150,6 +154,16 @@ export const errorMessages: Record<ErrorCodes, string> = {
150154
[ErrorCodes.X_MISSING_DYNAMIC_DIRECTIVE_ARGUMENT_END]:
151155
'End bracket for dynamic directive argument was not found. ' +
152156
'Note that dynamic directive argument cannot contain spaces.',
157+
[ErrorCodes.X_MISSING_DYNAMIC_DIRECTIVE_MODIFIER_END]:
158+
'End bracket for dynamic directive modifier was not found. ' +
159+
'Note that dynamic directive modifier cannot contain spaces.',
160+
[ErrorCodes.X_MISSING_DIRECTIVE_MODIFIER_NAME]:
161+
'Directive modifier name cannot be empty. ',
162+
[ErrorCodes.X_MISSING_DYNAMIC_DIRECTIVE_MODIFIER_VALUE]:
163+
'Dynamic directive modifier value cannot be empty. ',
164+
[ErrorCodes.X_INVALID_VALUE_IN_DYNAMIC_DIRECTIVE_MODIFIER]:
165+
'Invalid value in dynamic directive modifier. ' +
166+
'Note that dynamic directive modifier can only be objects or arrays.',
153167
[ErrorCodes.X_MISSING_DIRECTIVE_NAME]: 'Legal directive name was expected.',
154168

155169
// transform errors

packages/compiler-core/src/parser.ts

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -274,7 +274,50 @@ const tokenizer = new Tokenizer(stack, {
274274
setLocEnd(arg.loc, end)
275275
}
276276
} else {
277-
const exp = createSimpleExpression(mod, true, getLoc(start, end))
277+
const isStatic = mod[0] !== `[`
278+
const exp = createExp(
279+
isStatic ? mod : mod.slice(1, -1),
280+
isStatic,
281+
getLoc(start, end),
282+
isStatic ? ConstantTypes.CAN_STRINGIFY : ConstantTypes.NOT_CONSTANT,
283+
)
284+
285+
if (!exp.ast && !exp.content.trim()) {
286+
if (isStatic) {
287+
emitError(
288+
ErrorCodes.X_MISSING_DIRECTIVE_MODIFIER_NAME,
289+
exp.loc.start.offset,
290+
)
291+
} else {
292+
emitError(
293+
ErrorCodes.X_MISSING_DYNAMIC_DIRECTIVE_MODIFIER_VALUE,
294+
exp.loc.start.offset,
295+
)
296+
}
297+
return
298+
}
299+
300+
const invalidBabelNodeTypes = [
301+
'ArrayExpression',
302+
'UnaryExpression',
303+
'StringLiteral',
304+
'NumericLiteral',
305+
'TemplateLiteral',
306+
]
307+
308+
const invalidSimpleValues = ['true', 'false', 'null', 'undefined']
309+
310+
if (
311+
(exp.ast && invalidBabelNodeTypes.includes(exp.ast.type)) ||
312+
(!exp.ast && invalidSimpleValues.includes(exp.content))
313+
) {
314+
emitError(
315+
ErrorCodes.X_INVALID_VALUE_IN_DYNAMIC_DIRECTIVE_MODIFIER,
316+
exp.loc.start.offset + 1,
317+
)
318+
return
319+
}
320+
278321
;(currentProp as DirectiveNode).modifiers.push(exp)
279322
}
280323
},
@@ -382,7 +425,7 @@ const tokenizer = new Tokenizer(stack, {
382425
__COMPAT__ &&
383426
currentProp.name === 'bind' &&
384427
(syncIndex = currentProp.modifiers.findIndex(
385-
mod => mod.content === 'sync',
428+
mod => (mod as SimpleExpressionNode).content === 'sync',
386429
)) > -1 &&
387430
checkCompatEnabled(
388431
CompilerDeprecationTypes.COMPILER_V_BIND_SYNC,

0 commit comments

Comments
 (0)