chore: mark slotflag and fix some bugs (#45)

* fix: reassign variable as component should work

* feat: remove `compatibleProps`

* feat: mark slotFlag

* chore: istanbul ignore in babel.config.js

* test: reassign variable as component should work
This commit is contained in:
Amour1688 2020-07-28 21:25:30 +08:00 committed by GitHub
parent ebbd992ba0
commit 89afeac355
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 122 additions and 39 deletions

View File

@ -1,3 +1,4 @@
/* istanbul ignore next */
module.exports = {
presets: [
'@babel/preset-env',

View File

@ -14,6 +14,7 @@ import parseDirectives from './parseDirectives';
import { PatchFlags } from './patchFlags';
import { State, ExcludesBoolean } from '.';
import { transformJSXElement } from './transform-vue-jsx';
import SlotFlags from './slotFlags';
const xlinkRE = /^xlink([A-Z])/;
const onRE = /^on[^a-z]/;
@ -51,7 +52,7 @@ const transformJSXSpreadAttribute = (
const { properties } = argument.node;
if (!properties) {
if (argument.isIdentifier()) {
walksScope(nodePath, (argument.node as t.Identifier).name);
walksScope(nodePath, (argument.node as t.Identifier).name, SlotFlags.DYNAMIC);
}
mergeArgs.push(argument.node);
} else {
@ -173,8 +174,8 @@ const buildProps = (path: NodePath<t.JSXElement>, state: State) => {
hasStyleBinding = true;
} else if (
name !== 'key'
&& !isDirective(name)
&& name !== 'on'
&& !isDirective(name)
&& name !== 'on'
) {
dynamicPropNames.add(name);
}

View File

@ -10,7 +10,6 @@ export type State = {
interface Opts {
transformOn?: boolean;
compatibleProps?: boolean;
optimize?: boolean;
isCustomElement?: (tag: string) => boolean;
}

View File

@ -0,0 +1,24 @@
// https://github.com/vuejs/vue-next/blob/master/packages/shared/src/slotFlags.ts
const enum SlotFlags {
/**
* Stable slots that only reference slot props or context state. The slot
* can fully capture its own dependencies so when passed down the parent won't
* need to force the child to update.
*/
STABLE = 1,
/**
* Slots that reference scope variables (v-for or an outer slot prop), or
* has conditional structure (v-if, v-for). The parent will need to force
* the child to update because the slot does not fully capture its dependencies.
*/
DYNAMIC = 2,
/**
* `<slot/>` being forwarded into a child component. Whether the parent needs
* to update the child is dependent on what kind of slots the parent itself
* received. This has to be refined at runtime, when the child's vnode
* is being created (in `normalizeChildren`)
*/
FORWARDED = 3
}
export default SlotFlags;

View File

@ -1,16 +1,17 @@
import * as t from '@babel/types';
import { NodePath } from '@babel/traverse';
import { addDefault, addNamespace } from '@babel/helper-module-imports';
import { addNamespace } from '@babel/helper-module-imports';
import {
createIdentifier,
transformJSXSpreadChild,
transformJSXText,
transformJSXExpressionContainer,
walksScope,
buildIIFE,
} from './utils';
import buildProps from './buildProps';
import { PatchFlags } from './patchFlags';
import { State, ExcludesBoolean } from '.';
import SlotFlags from './slotFlags';
import { State } from '.';
/**
* Get children from Array of JSX children
@ -42,7 +43,7 @@ const getChildren = (
const { name } = expression as t.Identifier;
const { referencePaths = [] } = path.scope.getBinding(name) || {};
referencePaths.forEach((referencePath) => {
walksScope(referencePath, name);
walksScope(referencePath, name, SlotFlags.DYNAMIC);
});
}
@ -79,45 +80,39 @@ const transformJSXElement = (
slots,
} = buildProps(path, state);
const useOptimate = path.getData('optimize') !== false;
const { optimize = false } = state.opts;
const { compatibleProps = false, optimize = false } = state.opts;
if (compatibleProps && !state.get('compatibleProps')) {
state.set('compatibleProps', addDefault(
path, '@ant-design-vue/babel-helper-vue-compatible-props', { nameHint: '_compatibleProps' },
));
}
const slotFlag = path.getData('slotFlag') || SlotFlags.STABLE;
// @ts-ignore
const createVNode = t.callExpression(createIdentifier(state, optimize ? 'createVNode' : 'h'), [
const createVNode = t.callExpression(createIdentifier(state, 'createVNode'), [
tag,
// @ts-ignore
compatibleProps ? t.callExpression(state.get('compatibleProps'), [props]) : props,
props,
(children.length || slots) ? (
isComponent
? t.objectExpression([
!!children.length && t.objectProperty(
t.identifier('default'),
t.arrowFunctionExpression([], t.arrayExpression(children)),
t.arrowFunctionExpression([], t.arrayExpression(buildIIFE(path, children))),
),
...(slots ? (
t.isObjectExpression(slots)
? (slots! as t.ObjectExpression).properties
: [t.spreadElement(slots!)]
) : []),
].filter(Boolean as any as ExcludesBoolean))
optimize && t.objectProperty(
t.identifier('_'),
t.numericLiteral(slotFlag),
),
].filter(Boolean as any))
: t.arrayExpression(children)
) : t.nullLiteral(),
!!patchFlag && optimize && (
useOptimate
? t.numericLiteral(patchFlag)
: t.numericLiteral(PatchFlags.BAIL)
),
!!patchFlag && optimize && t.numericLiteral(patchFlag),
!!dynamicPropNames.size && optimize
&& t.arrayExpression(
[...dynamicPropNames.keys()].map((name) => t.stringLiteral(name as string)),
),
].filter(Boolean as any as ExcludesBoolean));
].filter(Boolean as any));
if (!directives.length) {
return createVNode;

View File

@ -3,6 +3,7 @@ import htmlTags from 'html-tags';
import svgTags from 'svg-tags';
import { NodePath } from '@babel/traverse';
import { State } from '.';
import SlotFlags from './slotFlags';
/**
* create Identifier
@ -181,15 +182,50 @@ const transformJSXSpreadChild = (
path: NodePath<t.JSXSpreadChild>,
): t.SpreadElement => t.spreadElement(path.get('expression').node);
const walksScope = (path: NodePath, name: string): void => {
const walksScope = (path: NodePath, name: string, slotFlag: SlotFlags): void => {
if (path.scope.hasBinding(name) && path.parentPath) {
if (t.isJSXElement(path.parentPath.node)) {
path.parentPath.setData('optimize', false);
path.parentPath.setData('slotFlag', slotFlag);
}
walksScope(path.parentPath, name);
walksScope(path.parentPath, name, slotFlag);
}
};
const createInsertName = (path: NodePath, name: string): t.Identifier => {
if (path.scope.hasBinding(name)) {
return createInsertName(path, `_${name}`);
}
return t.identifier(name);
};
const buildIIFE = (path: NodePath<t.JSXElement>, children: t.Expression[]) => {
const { parentPath } = path;
if (t.isAssignmentExpression(parentPath)) {
const { left } = parentPath.node as t.AssignmentExpression;
if (t.isIdentifier(left)) {
return children.map((child) => {
if (t.isIdentifier(child) && child.name === left.name) {
const insertName = createInsertName(parentPath, `_${child.name}`);
parentPath.insertBefore(
t.variableDeclaration('const', [
t.variableDeclarator(
insertName,
t.callExpression(
t.functionExpression(null, [], t.blockStatement([t.returnStatement(child)])),
[],
),
),
]),
);
return insertName;
}
return child;
});
}
}
return children;
};
export {
createIdentifier,
isDirective,
@ -202,4 +238,5 @@ export {
transformJSXExpressionContainer,
isFragment,
walksScope,
buildIIFE,
};

View File

@ -469,3 +469,30 @@ describe('variables outside slots', () => {
expect(wrapper.get('#textarea').element.innerHTML).toBe('1');
});
});
test('reassign variable as component should work', () => {
let a: any = 1;
const A = defineComponent({
setup(_, { slots }) {
return () => <span>{slots.default!()}</span>;
},
});
/* eslint-disable */
// @ts-ignore
const _a = 1;
// @ts-ignore
const __a = 2;
/* eslint-enable */
a = <A>{a}</A>;
const wrapper = mount({
render() {
return a;
},
});
expect(wrapper.html()).toBe('<span>1</span>');
});

View File

@ -63,7 +63,7 @@ const compile = () => {
console.clear();
transform(src, {
babelrc: false,
plugins: [[babelPluginJSx, { transformOn: true }]],
plugins: [[babelPluginJSx, { transformOn: true, optimize: true }]],
ast: true,
}, (err, result = {}) => {
const res = result!;
@ -87,18 +87,17 @@ compile();
// update compile output when input changes
editor.onDidChangeModelContent(debounce(compile));
function debounce<T extends (...args: any[]) => any>(
function debounce<T extends(...args: any[]) => any>(
fn: T,
delay: number = 300
): T {
let prevTimer: number | null = null
delay = 300): T {
let prevTimer: number | null = null;
return ((...args: any[]) => {
if (prevTimer) {
clearTimeout(prevTimer)
clearTimeout(prevTimer);
}
prevTimer = window.setTimeout(() => {
fn(...args)
prevTimer = null
}, delay)
}) as any
fn(...args);
prevTimer = null;
}, delay);
}) as any;
}