Skip to content

Commit 43334ad

Browse files
committed
fix: allow v-for variables
1 parent 48ab705 commit 43334ad

File tree

5 files changed

+159
-50
lines changed

5 files changed

+159
-50
lines changed

package-lock.json

Lines changed: 0 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@
1919
"core-js": "^3.6.5",
2020
"debounce": "^1.2.0",
2121
"hash-sum": "^2.0.0",
22-
"lodash.has": "^4.5.2",
2322
"prismjs": "^1.16.0",
2423
"recast": "^0.19.1",
2524
"vue-inbrowser-compiler": "^3.21.0",

src/Preview.vue

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,9 @@ export default {
105105
*/
106106
this.$emit("error", e);
107107
if (e.constructor === VueLiveParseTemplateError) {
108-
e.message = `Cannot parse template expression: ${e.expression}\n\n${e.message}`;
108+
e.message = `Cannot parse template expression: ${JSON.stringify(
109+
e.expression.content || e.expression
110+
)}\n\n${e.message}`;
109111
}
110112
this.error = e.message;
111113
},

src/utils/__tests__/checkTemplate.js

Lines changed: 68 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,9 @@ test("parse valid template without error with a value", () => {
1212
template: '<div><compo :value="today">hello</compo></div>',
1313
data() {
1414
return {
15-
today: "hello"
15+
today: "hello",
1616
};
17-
}
17+
},
1818
})
1919
).not.toThrow();
2020
});
@@ -29,41 +29,41 @@ test("parse false value as a valid value", () => {
2929
</div>`,
3030
data() {
3131
return {
32-
value: false
32+
value: false,
3333
};
34-
}
34+
},
3535
})
3636
).not.toThrow();
3737
});
3838

3939
test("parse invalid template with an error in the ++", () => {
4040
expect(() =>
4141
checkTemplate({
42-
template: '<div><compo :value="today++">hello</compo></div>'
42+
template: '<div><compo :value="today++">hello</compo></div>',
4343
})
4444
).toThrow();
4545
});
4646

4747
test("parse invalid template with an error in a function call", () => {
4848
expect(() =>
4949
checkTemplate({
50-
template: '<div><compo :value="callit(today)">hello</compo></div>'
50+
template: '<div><compo :value="callit(today)">hello</compo></div>',
5151
})
5252
).toThrow();
5353
});
5454

5555
test("parse invalid template with an error in a function call and a spread", () => {
5656
expect(() =>
5757
checkTemplate({
58-
template: '<div><compo :value="callit(...today)">hello</compo></div>'
58+
template: '<div><compo :value="callit(...today)">hello</compo></div>',
5959
})
6060
).toThrow();
6161
});
6262

6363
test("parse invalid template with an error if the value is not in data", () => {
6464
expect(() =>
6565
checkTemplate({
66-
template: '<div><compo :value="today">hello</compo></div>'
66+
template: '<div><compo :value="today">hello</compo></div>',
6767
})
6868
).toThrowErrorMatchingInlineSnapshot(
6969
`"Variable \\"today\\" is not defined."`
@@ -73,7 +73,7 @@ test("parse invalid template with an error if the value is not in data", () => {
7373
test("parse template interpolatio and detect undefined variables", () => {
7474
expect(() =>
7575
checkTemplate({
76-
template: "<div><compo>{{ hello }}</compo></div>"
76+
template: "<div><compo>{{ hello }}</compo></div>",
7777
})
7878
).toThrowErrorMatchingInlineSnapshot(
7979
`"Variable \\"hello\\" is not defined."`
@@ -83,39 +83,91 @@ test("parse template interpolatio and detect undefined variables", () => {
8383
test("parse invalid : template by throwing an error", () => {
8484
expect(() =>
8585
checkTemplate({
86-
template: '<div><a :href="+++foo()">hello</a></div>'
86+
template: '<div><a :href="+++foo()">hello</a></div>',
8787
})
8888
).toThrowErrorMatchingInlineSnapshot(`"Assigning to rvalue (1:21)"`);
8989
});
9090

9191
test("parse invalid @ template by throwing an error", () => {
9292
expect(() =>
9393
checkTemplate({
94-
template: '<div><a @click="+++foo()">hello</a></div>'
94+
template: '<div><a @click="+++foo()">hello</a></div>',
9595
})
9696
).toThrowErrorMatchingInlineSnapshot(`"Assigning to rvalue (1:21)"`);
9797
});
9898

9999
test("parse valid object not to throw", () => {
100100
expect(() =>
101101
checkTemplate({
102-
template: '<div><CustomSelect :options="{foo:1, bar:2}" /></div>'
102+
template: '<div><CustomSelect :options="{foo:1, bar:2}" /></div>',
103103
})
104104
).not.toThrow();
105105
});
106106

107-
test("parse valid expression with mutiple lines not to throw", () => {
107+
test("parse expression using mutiple lines to throw", () => {
108108
expect(() =>
109109
checkTemplate({
110110
template: `<div><CustomSelect @event="
111111
test();
112112
callFunction(hello);
113113
" /></div>`,
114-
data() {
115-
return {};
116-
}
117114
})
118115
).toThrowErrorMatchingInlineSnapshot(
119116
`"Variable \\"hello\\" is not defined."`
120117
);
121118
});
119+
120+
test("parse v-for expressions and add their vars to available data", () => {
121+
expect(() =>
122+
checkTemplate({
123+
template: `<div v-for="hello in [1,2]"><CustomSelect @event="
124+
test();
125+
callFunction(hello);
126+
" /></div>`,
127+
})
128+
).not.toThrow();
129+
});
130+
131+
test("parse v-for expressions with index and add their vars to available data", () => {
132+
expect(() =>
133+
checkTemplate({
134+
template: `<div v-for="(hello, index) in [1,2]"><CustomSelect @event="
135+
test(index);
136+
callFunction(hello);
137+
" /></div>`,
138+
})
139+
).not.toThrow();
140+
});
141+
142+
test("parse v-slot-scope expressions without issues", () => {
143+
expect(() =>
144+
checkTemplate({
145+
template: `<template v-slot:default="hello"><CustomSelect @event="
146+
test();
147+
callFunction(hello);
148+
" /></template>`,
149+
})
150+
).not.toThrow();
151+
});
152+
153+
test("parse v-slot-scope deconstructed expressions without issues", () => {
154+
expect(() =>
155+
checkTemplate({
156+
template: `<template v-slot:default="{ bye: hello, whats: up, foo }"><CustomSelect @event="
157+
test();
158+
callFunction(hello);
159+
" /></template>`,
160+
})
161+
).not.toThrow();
162+
});
163+
164+
test("parse v-slot-scope deconstructed array expressions without issues", () => {
165+
expect(() =>
166+
checkTemplate({
167+
template: `<template v-slot:default="[ hello ]"><CustomSelect @event="
168+
test();
169+
callFunction(hello);
170+
" /></template>`,
171+
})
172+
).not.toThrow();
173+
});

src/utils/checkTemplate.js

Lines changed: 88 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import { parse as parseVue } from "@vue/compiler-dom";
22
import { createCompilerError } from "@vue/compiler-core/dist/compiler-core.cjs";
33
import { parse as parseEs } from "acorn";
44
import { visit } from "recast";
5-
import has from "lodash.has";
65

76
const ELEMENT = 1;
87
const SIMPLE_EXPRESSION = 4;
@@ -18,8 +17,10 @@ export default function($options) {
1817
} catch (e) {
1918
throw createCompilerError(e.code);
2019
}
20+
2121
traverse(ast, [
22-
(templateAst) => {
22+
(templateAst, availableVarNames) => {
23+
const templateVars = [];
2324
if (templateAst.type === ELEMENT) {
2425
templateAst.props.forEach((attr) => {
2526
const exp =
@@ -29,16 +30,58 @@ export default function($options) {
2930
if (!exp) {
3031
return;
3132
}
32-
try {
33-
checkExpression(exp, $options);
34-
} catch (e) {
35-
throw new VueLiveParseTemplateError(e.message, exp, e);
33+
if (attr.name === "slot") {
34+
const astSlot = parseEs(`var ${exp}=1`);
35+
visit(astSlot, {
36+
visitVariableDeclarator(declarator) {
37+
const { id } = declarator.node;
38+
switch (id.type) {
39+
case "ArrayPattern":
40+
id.elements.forEach((e) => {
41+
templateVars.push(e.name);
42+
});
43+
break;
44+
case "ObjectPattern":
45+
id.properties.forEach((e) => {
46+
templateVars.push(e.value.name);
47+
});
48+
break;
49+
case "Identifier":
50+
templateVars.push(id.name);
51+
break;
52+
}
53+
return false;
54+
},
55+
});
56+
} else if (attr.name === "for") {
57+
const [vForLeft] = exp.split(/( in | of )/);
58+
const doubleForRE = /\((\w+),(\w+)\)/;
59+
if (doubleForRE.test(vForLeft.replace(" ", ""))) {
60+
const vars = doubleForRE.exec(vForLeft.replace(" ", ""));
61+
templateVars.push(vars[1]);
62+
templateVars.push(vars[2]);
63+
} else {
64+
templateVars.push(vForLeft);
65+
}
66+
} else {
67+
try {
68+
checkExpression(exp, $options, [
69+
...availableVarNames,
70+
...templateVars,
71+
]);
72+
} catch (e) {
73+
throw new VueLiveParseTemplateError(e.message, exp, e);
74+
}
3675
}
3776
});
3877
} else if (templateAst.type === INTERPOLATION) {
3978
try {
4079
if (templateAst.content) {
41-
checkExpression(templateAst.content.content, $options);
80+
checkExpression(
81+
templateAst.content.content,
82+
$options,
83+
availableVarNames
84+
);
4285
}
4386
} catch (e) {
4487
throw new VueLiveParseTemplateError(
@@ -48,14 +91,33 @@ export default function($options) {
4891
);
4992
}
5093
}
94+
return templateVars;
5195
},
5296
]);
5397
}
5498

55-
export function checkExpression(expression, $options) {
99+
export function checkExpression(expression, $options, templateVars) {
56100
// try and parse the expression
57101
const ast = parseEs(`(function(){return ${expression}})()`);
58102

103+
const propNamesArray =
104+
$options && $options.props
105+
? Array.isArray($options.props)
106+
? $options.props
107+
: Object.keys($options.props)
108+
: [];
109+
110+
const dataArray =
111+
$options && typeof $options.data === "function"
112+
? Object.keys($options.data())
113+
: [];
114+
115+
const availableIdentifiers = [
116+
...propNamesArray,
117+
...dataArray,
118+
...templateVars,
119+
];
120+
59121
// identify all variables that would be undefined because not in the data object
60122
visit(ast, {
61123
visitIdentifier(identifier) {
@@ -65,19 +127,9 @@ export function checkExpression(expression, $options) {
65127
identifier.name === "argument" ||
66128
identifier.parentPath.name === "arguments"
67129
) {
68-
if (!$options || typeof $options.data !== "function") {
69-
throw new VueLiveUndefinedVariableError(
70-
`Variable "${varName}" is not defined.`,
71-
varName
72-
);
73-
}
74130
if (
75-
!has($options.data(), varName) &&
76-
!has($options.props, varName) &&
77-
!(
78-
Array.isArray($options.props) &&
79-
$options.props.indexOf(varName) !== -1
80-
)
131+
!availableIdentifiers ||
132+
availableIdentifiers.indexOf(varName) === -1
81133
) {
82134
throw new VueLiveUndefinedVariableError(
83135
`Variable "${varName}" is not defined.`,
@@ -91,21 +143,30 @@ export function checkExpression(expression, $options) {
91143
});
92144
}
93145

94-
export function traverse(templateAst, handlers) {
95-
const traverseAstChildren = (templateAst) => {
146+
export function traverse(templateAst, handlers, availableVarNames = []) {
147+
const traverseAstChildren = (templateAst, availableVarNamesChildren) => {
96148
const { children } = templateAst;
97149
if (children) {
98150
for (const childNode of children) {
99-
traverse(childNode, handlers);
151+
traverse(childNode, handlers, availableVarNamesChildren);
100152
}
101153
}
102154
};
103155

104-
handlers.forEach((handler) => {
105-
handler(templateAst);
106-
});
156+
// we load this object with all available varnames
157+
// discovered in the template parsing on v-for and v-slot
158+
const availableVarNamesThisLevel = handlers.reduce((acc, handler) => {
159+
const result = handler(templateAst, availableVarNames);
160+
if (result && result.length) {
161+
return acc.concat(result);
162+
}
163+
return acc;
164+
}, []);
107165

108-
traverseAstChildren(templateAst);
166+
traverseAstChildren(templateAst, [
167+
...availableVarNames,
168+
...availableVarNamesThisLevel,
169+
]);
109170
}
110171

111172
export function VueLiveUndefinedVariableError(message, varName) {

0 commit comments

Comments
 (0)