Skip to content
代码片段 群组 项目
提交 f33f33dd 编辑于 作者: Illya Klymov's avatar Illya Klymov 提交者: Natalia Tepluhina
浏览文件

Switch Vue.js 3 to whitespace: preserve

* implement proper testing to our compiler
上级 04b4da1c
No related branches found
No related tags found
无相关合并请求
显示
213 个添加27 个删除
...@@ -2,16 +2,20 @@ const { parse, compile: compilerDomCompile } = require('@vue/compiler-dom'); ...@@ -2,16 +2,20 @@ const { parse, compile: compilerDomCompile } = require('@vue/compiler-dom');
const COMMENT_NODE_TYPE = 3; const COMMENT_NODE_TYPE = 3;
const getPropIndex = (node, prop) => node.props?.findIndex((p) => p.name === prop) ?? -1; const hasProp = (node, prop) => node.props?.some((p) => p.name === prop);
function modifyKeysInsideTemplateTag(templateNode) { function modifyKeysInsideTemplateTag(templateNode) {
if (!templateNode.tag === 'template' || !hasProp(templateNode, 'for')) {
return;
}
let keyCandidate = null; let keyCandidate = null;
for (const node of templateNode.children) { for (const node of templateNode.children) {
const keyBindingIndex = node.props const keyBindingIndex = node.props
? node.props.findIndex((prop) => prop.arg && prop.arg.content === 'key') ? node.props.findIndex((prop) => prop.arg && prop.arg.content === 'key')
: -1; : -1;
if (keyBindingIndex !== -1 && getPropIndex(node, 'for') === -1) { if (keyBindingIndex !== -1 && !hasProp(node, 'for')) {
if (!keyCandidate) { if (!keyCandidate) {
keyCandidate = node.props[keyBindingIndex]; keyCandidate = node.props[keyBindingIndex];
} }
...@@ -24,40 +28,97 @@ function modifyKeysInsideTemplateTag(templateNode) { ...@@ -24,40 +28,97 @@ function modifyKeysInsideTemplateTag(templateNode) {
} }
} }
function getSlotName(node) {
return node?.props?.find((prop) => prop.name === 'slot')?.arg?.content;
}
function filterCommentNodeAndTrailingSpace(node, idx, list) {
if (node.type === COMMENT_NODE_TYPE) {
return false;
}
if (node.content !== ' ') {
return true;
}
if (list[idx - 1]?.type === COMMENT_NODE_TYPE) {
return false;
}
return true;
}
function filterCommentNodes(node) {
const { length: originalLength } = node.children;
// eslint-disable-next-line no-param-reassign
node.children = node.children.filter(filterCommentNodeAndTrailingSpace);
if (node.children.length !== originalLength) {
// trim remaining spaces
while (node.children.at(-1)?.content === ' ') {
node.children.pop();
}
}
}
function dropVOnceForChildrenInsideVIfBecauseOfIssue7725(node) {
// See https://github.com/vuejs/core/issues/7725 for details
if (!hasProp(node, 'if')) {
return;
}
node.children?.forEach((child) => {
if (Array.isArray(child.props)) {
// eslint-disable-next-line no-param-reassign
child.props = child.props.filter((prop) => prop.name !== 'once');
}
});
}
function fixSameSlotsInsideTemplateFailingWhenUsingWhitespacePreserveDueToIssue6063(node) {
// See https://github.com/vuejs/core/issues/6063 for details
// eslint-disable-next-line no-param-reassign
node.children = node.children.filter((child, idx) => {
if (child.content !== ' ') {
// We need to drop only comment nodes
return true;
}
const previousNodeSlotName = getSlotName(node.children[idx - 1]);
const nextNodeSlotName = getSlotName(node.children[idx + 1]);
if (previousNodeSlotName && previousNodeSlotName === nextNodeSlotName) {
// We have a space beween two slot entries with same slot name, we need to drop it
return false;
}
return true;
});
}
module.exports = { module.exports = {
parse, parse,
compile(template, options) { compile(template, options) {
const rootNode = parse(template, options); const rootNode = parse(template, options);
// We do not want to switch to whitespace: collapse mode which is Vue.js 3 default const pendingNodes = [rootNode];
// It will be too devastating to codebase while (pendingNodes.length) {
const currentNode = pendingNodes.pop();
if (Array.isArray(currentNode.children)) {
// This one will be dropped all together with compiler when we drop Vue.js 2 support
modifyKeysInsideTemplateTag(currentNode);
// However, without `whitespace: condense` Vue will treat spaces between comments dropVOnceForChildrenInsideVIfBecauseOfIssue7725(currentNode);
// and nodes itself as text nodes, resulting in multi-root component
// For multi-root component passing classes / attributes fallthrough will not work
// See https://github.com/vuejs/core/issues/7909 for details // See https://github.com/vuejs/core/issues/7909 for details
// However, this issue applies not only to root-level nodes
// But on any level comments could change slot emptiness detection
// so we simply drop them
filterCommentNodes(currentNode);
// To fix that we simply drop all component comments only on top-level fixSameSlotsInsideTemplateFailingWhenUsingWhitespacePreserveDueToIssue6063(currentNode);
rootNode.children = rootNode.children.filter((n) => n.type !== COMMENT_NODE_TYPE);
const pendingNodes = [rootNode]; currentNode.children.forEach((child) => pendingNodes.push(child));
while (pendingNodes.length) {
const currentNode = pendingNodes.pop();
if (getPropIndex(currentNode, 'for') !== -1) {
if (currentNode.tag === 'template') {
// This one will be dropped all together with compiler when we drop Vue.js 2 support
modifyKeysInsideTemplateTag(currentNode);
}
// This one will be dropped when https://github.com/vuejs/core/issues/7725 will be fixed
const vOncePropIndex = getPropIndex(currentNode, 'once');
if (vOncePropIndex !== -1) {
currentNode.props.splice(vOncePropIndex, 1);
}
} }
currentNode.children?.forEach((child) => pendingNodes.push(child));
} }
return compilerDomCompile(rootNode, options); return compilerDomCompile(rootNode, options);
......
...@@ -51,6 +51,7 @@ module.exports = (path, options = {}) => { ...@@ -51,6 +51,7 @@ module.exports = (path, options = {}) => {
experimentalCSSCompile: false, experimentalCSSCompile: false,
compiler: require.resolve('./config/vue3migration/compiler'), compiler: require.resolve('./config/vue3migration/compiler'),
compilerOptions: { compilerOptions: {
whitespace: 'preserve',
compatConfig: { compatConfig: {
MODE: 2, MODE: 2,
}, },
......
import { mount } from '@vue/test-utils';
import SlotsWithSameName from './components/slots_with_same_name.vue';
import VOnceInsideVIf from './components/v_once_inside_v_if.vue';
import KeyInsideTemplate from './components/key_inside_template.vue';
import CommentsOnRootLevel from './components/comments_on_root_level.vue';
import SlotWithComment from './components/slot_with_comment.vue';
import DefaultSlotWithComment from './components/default_slot_with_comment.vue';
describe('Vue.js 3 compiler edge cases', () => {
it('workarounds issue #6063 when same slot is used with whitespace preserve', () => {
expect(() => mount(SlotsWithSameName)).not.toThrow();
});
it('workarounds issue #7725 when v-once is used inside v-if', () => {
expect(() => mount(VOnceInsideVIf)).not.toThrow();
});
it('renders vue.js 2 component when key is inside template', () => {
const wrapper = mount(KeyInsideTemplate);
expect(wrapper.text()).toBe('12345');
});
it('passes attributes to component with trailing comments on root level', () => {
const wrapper = mount(CommentsOnRootLevel, { propsData: { 'data-testid': 'test' } });
expect(wrapper.html()).toBe('<div data-testid="test"></div>');
});
it('treats empty slots with comments as empty', () => {
const wrapper = mount(SlotWithComment);
expect(wrapper.html()).toBe('<div>Simple</div>');
});
it('treats empty default slot with comments as empty', () => {
const wrapper = mount(DefaultSlotWithComment);
expect(wrapper.html()).toBe('<div>Simple</div>');
});
});
<template>
<!-- root level comment -->
<div><slot></slot></div>
<!-- root level comment -->
</template>
<script>
import Simple from './simple.vue';
export default {
components: {
Simple,
},
};
</script>
<template>
<simple>
<!-- slot comment typical for gitlab-ui, for example -->
<!-- slot comment typical for gitlab-ui, for example -->
<slot></slot>
<!-- slot comment typical for gitlab-ui, for example -->
<!-- slot comment typical for gitlab-ui, for example -->
</simple>
</template>
<template>
<div>
<template v-for="count in 5"
><span :key="count">{{ count }}</span></template
>
</div>
</template>
<script>
export default {
name: 'Simple',
};
</script>
<template>
<div>
<slot>{{ $options.name }}</slot>
</div>
</template>
<script>
import Simple from './simple.vue';
export default {
components: {
Simple,
},
};
</script>
<template>
<simple>
<template #default>
<!-- slot comment typical for gitlab-ui, for example -->
<!-- slot comment typical for gitlab-ui, for example -->
<slot></slot>
<!-- slot comment typical for gitlab-ui, for example -->
<!-- slot comment typical for gitlab-ui, for example -->
</template>
</simple>
</template>
<script>
import Simple from './simple.vue';
export default {
name: 'SlotsWithSameName',
components: { Simple },
};
</script>
<template>
<simple>
<template v-if="true" #default>{{ $options.name }}</template>
<template v-else #default>{{ $options.name }}</template>
</simple>
</template>
<script>
export default {
name: 'VOnceInsideVIf',
};
</script>
<template>
<div>
<template v-if="true">
<div v-once>{{ $options.name }}</div>
</template>
</div>
</template>
...@@ -63,7 +63,7 @@ if (global.document) { ...@@ -63,7 +63,7 @@ if (global.document) {
}; };
let compatH; let compatH;
Vue.config.compilerOptions.whitespace = 'condense'; Vue.config.compilerOptions.whitespace = 'preserve';
Vue.createApp({ Vue.createApp({
compatConfig: { compatConfig: {
MODE: 3, MODE: 3,
......
0% 加载中 .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册