chore: replace namespace imports with named imports (#67)

* feat: Replace namespace imports with named imports (#54)

* Add .circleci/config.yml

* ci: fix circleci error

* fix: specifiers should be merged into a single importDeclaration

Co-authored-by: 逆寒 <869732751@qq.com>
Co-authored-by: tangjinzhou <415800467@qq.com>
This commit is contained in:
Amour1688 2020-08-27 20:01:11 +08:00 committed by GitHub
parent 86b5051174
commit 5790b83ed6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 115 additions and 51 deletions

28
.circleci/config.yml Normal file
View File

@ -0,0 +1,28 @@
version: 2.1
jobs:
build:
docker:
# specify the version you desire here
- image: vuejs/ci
user: node
working_directory: /home/node/repo
steps:
- checkout
# Download and cache dependencies
- restore_cache:
keys:
- v2-dependencies-{{ checksum "yarn.lock" }}
- run: yarn install --pure-lockfile
- save_cache:
paths:
- node_modules
- ~/.cache/yarn
key: v2-dependencies-{{ checksum "yarn.lock" }}
# run tests!
- run: yarn test

View File

@ -1,19 +0,0 @@
name: test
on: [push, pull_request]
jobs:
setup:
runs-on: ubuntu-latest
steps:
- name: checkout
uses: actions/checkout@v2
- name: install
run: yarn install
- name: lint
run: yarn lint
- name: test
run: yarn test

View File

@ -1,6 +1,9 @@
import syntaxJsx from '@babel/plugin-syntax-jsx'; import syntaxJsx from '@babel/plugin-syntax-jsx';
import * as t from '@babel/types';
import { NodePath } from '@babel/traverse';
import tranformVueJSX from './transform-vue-jsx'; import tranformVueJSX from './transform-vue-jsx';
import sugarFragment from './sugar-fragment'; import sugarFragment from './sugar-fragment';
import { JSX_HELPER_KEY } from './utils';
export type State = { export type State = {
get: (name: string) => any; get: (name: string) => any;
@ -20,6 +23,50 @@ export default () => ({
name: 'babel-plugin-jsx', name: 'babel-plugin-jsx',
inherits: syntaxJsx, inherits: syntaxJsx,
visitor: { visitor: {
Program: {
exit(path: NodePath<t.Program>, state: State) {
const helpers: Set<string> = state.get(JSX_HELPER_KEY);
if (!helpers) {
return;
}
const body = path.get('body');
const specifierNames = new Set<string>();
body
.filter((nodePath) => t.isImportDeclaration(nodePath.node)
&& nodePath.node.source.value === 'vue')
.forEach((nodePath) => {
let shouldKeep = false;
const newSpecifiers = (nodePath.node as t.ImportDeclaration).specifiers
.filter((specifier) => {
if (t.isImportSpecifier(specifier)) {
const { imported, local } = specifier;
specifierNames.add(imported.name);
return local.name !== imported.name;
} if (t.isImportNamespaceSpecifier(specifier)) {
// should keep when `import * as Vue from 'vue'`
shouldKeep = true;
}
return false;
});
if (newSpecifiers.length) {
nodePath.replaceWith(t.importDeclaration(newSpecifiers, t.stringLiteral('vue')));
} else if (!shouldKeep) {
nodePath.remove();
}
});
const importedHelperKeys = new Set([...specifierNames, ...helpers]);
const specifiers: t.ImportSpecifier[] = [...importedHelperKeys].map(
(imported) => t.importSpecifier(
t.identifier(imported), t.identifier(imported),
),
);
const expression = t.importDeclaration(specifiers, t.stringLiteral('vue'));
path.unshiftContainer('body', expression);
},
},
...tranformVueJSX(), ...tranformVueJSX(),
...sugarFragment(), ...sugarFragment(),
}, },

View File

@ -1,9 +1,9 @@
import * as t from '@babel/types'; import * as t from '@babel/types';
import { addNamespace } from '@babel/helper-module-imports';
import { NodePath } from '@babel/traverse'; import { NodePath } from '@babel/traverse';
import { State } from '.'; import { State } from '.';
import { createIdentifier, FRAGMENT } from './utils';
const transformFragment = (path: NodePath<t.JSXElement>, Fragment: t.JSXMemberExpression) => { const transformFragment = (path: NodePath<t.JSXElement>, Fragment: t.JSXIdentifier) => {
const children = path.get('children') || []; const children = path.get('children') || [];
return t.jsxElement( return t.jsxElement(
t.jsxOpeningElement(Fragment, []), t.jsxOpeningElement(Fragment, []),
@ -16,16 +16,10 @@ const transformFragment = (path: NodePath<t.JSXElement>, Fragment: t.JSXMemberEx
export default () => ({ export default () => ({
JSXFragment: { JSXFragment: {
enter(path: NodePath<t.JSXElement>, state: State) { enter(path: NodePath<t.JSXElement>, state: State) {
if (!state.get('vue')) {
state.set('vue', addNamespace(path, 'vue'));
}
path.replaceWith( path.replaceWith(
transformFragment( transformFragment(
path, path,
t.jsxMemberExpression( t.jsxIdentifier(createIdentifier(state, FRAGMENT).name),
t.jsxIdentifier(state.get('vue').name),
t.jsxIdentifier('Fragment'),
),
), ),
); );
}, },

View File

@ -1,6 +1,5 @@
import * as t from '@babel/types'; import * as t from '@babel/types';
import { NodePath } from '@babel/traverse'; import { NodePath } from '@babel/traverse';
import { addNamespace } from '@babel/helper-module-imports';
import { import {
createIdentifier, createIdentifier,
transformJSXSpreadChild, transformJSXSpreadChild,
@ -21,11 +20,11 @@ import { State } from '.';
const getChildren = ( const getChildren = (
paths: NodePath< paths: NodePath<
t.JSXText t.JSXText
| t.JSXExpressionContainer | t.JSXExpressionContainer
| t.JSXSpreadChild | t.JSXSpreadChild
| t.JSXElement | t.JSXElement
| t.JSXFragment | t.JSXFragment
>[], >[],
state: State, state: State,
): t.Expression[] => paths ): t.Expression[] => paths
.map((path) => { .map((path) => {
@ -61,8 +60,8 @@ const getChildren = (
throw new Error(`getChildren: ${path.type} is not supported`); throw new Error(`getChildren: ${path.type} is not supported`);
}).filter(((value: any) => ( }).filter(((value: any) => (
value !== undefined value !== undefined
&& value !== null && value !== null
&& !t.isJSXEmptyExpression(value) && !t.isJSXEmptyExpression(value)
)) as any); )) as any);
const transformJSXElement = ( const transformJSXElement = (
@ -123,15 +122,11 @@ const transformJSXElement = (
t.arrayExpression(directives), t.arrayExpression(directives),
]); ]);
}; };
export { transformJSXElement }; export { transformJSXElement };
export default () => ({ export default () => ({
JSXElement: { JSXElement: {
exit(path: NodePath<t.JSXElement>, state: State) { exit(path: NodePath<t.JSXElement>, state: State) {
if (!state.get('vue')) {
state.set('vue', addNamespace(path, 'vue'));
}
path.replaceWith( path.replaceWith(
transformJSXElement(path, state), transformJSXElement(path, state),
); );

View File

@ -5,15 +5,25 @@ import { NodePath } from '@babel/traverse';
import { State } from '.'; import { State } from '.';
import SlotFlags from './slotFlags'; import SlotFlags from './slotFlags';
const JSX_HELPER_KEY = 'JSX_HELPER_KEY';
const FRAGMENT = 'Fragment';
/** /**
* create Identifier * create Identifier
* @param path NodePath
* @param state * @param state
* @param id string * @param id string
* @returns MemberExpression * @returns MemberExpression
*/ */
const createIdentifier = ( const createIdentifier = (
state: State, id: string, state: State, id: string,
): t.MemberExpression => t.memberExpression(state.get('vue'), t.identifier(id)); ): t.Identifier => {
if (!state.get(JSX_HELPER_KEY)) {
state.set(JSX_HELPER_KEY, new Set());
}
const helpers = state.get(JSX_HELPER_KEY);
helpers.add(id);
return t.identifier(id);
};
/** /**
* Checks if string is describing a directive * Checks if string is describing a directive
@ -30,8 +40,15 @@ const isDirective = (src: string): boolean => src.startsWith('v-')
const isFragment = ( const isFragment = (
path: path:
NodePath<t.JSXIdentifier | t.JSXMemberExpression | t.JSXNamespacedName>, NodePath<t.JSXIdentifier | t.JSXMemberExpression | t.JSXNamespacedName>,
): boolean => t.isJSXMemberExpression(path) ): boolean => {
&& (path.node as t.JSXMemberExpression).property.name === 'Fragment'; if (path.isJSXIdentifier()) {
return path.node.name === FRAGMENT;
}
if (path.isJSXMemberExpression()) {
return (path.node as t.JSXMemberExpression).property.name === FRAGMENT;
}
return false;
};
/** /**
* Check if a Node is a component * Check if a Node is a component
@ -49,7 +66,7 @@ const checkIsComponent = (path: NodePath<t.JSXOpeningElement>): boolean => {
const tag = (namePath as NodePath<t.JSXIdentifier>).node.name; const tag = (namePath as NodePath<t.JSXIdentifier>).node.name;
return !htmlTags.includes(tag) && !svgTags.includes(tag); return tag !== FRAGMENT && !htmlTags.includes(tag) && !svgTags.includes(tag);
}; };
/** /**
@ -85,13 +102,13 @@ const getTag = (
if (namePath.isJSXIdentifier()) { if (namePath.isJSXIdentifier()) {
const { name } = namePath.node; const { name } = namePath.node;
if (!htmlTags.includes(name) && !svgTags.includes(name)) { if (!htmlTags.includes(name) && !svgTags.includes(name)) {
return path.scope.hasBinding(name) return (name === FRAGMENT
? t.identifier(name) ? createIdentifier(state, FRAGMENT)
: ( : path.scope.hasBinding(name)
state.opts.isCustomElement?.(name) ? t.identifier(name)
: state.opts.isCustomElement?.(name)
? t.stringLiteral(name) ? t.stringLiteral(name)
: t.callExpression(createIdentifier(state, 'resolveComponent'), [t.stringLiteral(name)]) : t.callExpression(createIdentifier(state, 'resolveComponent'), [t.stringLiteral(name)]));
);
} }
return t.stringLiteral(name); return t.stringLiteral(name);
@ -171,7 +188,7 @@ const transformJSXText = (path: NodePath<t.JSXText>): t.StringLiteral | null =>
const transformJSXExpressionContainer = ( const transformJSXExpressionContainer = (
path: NodePath<t.JSXExpressionContainer>, path: NodePath<t.JSXExpressionContainer>,
): (t.Expression ): (t.Expression
) => path.get('expression').node as t.Expression; ) => path.get('expression').node as t.Expression;
/** /**
* Transform JSXSpreadChild * Transform JSXSpreadChild
@ -237,6 +254,8 @@ export {
transformJSXSpreadChild, transformJSXSpreadChild,
transformJSXExpressionContainer, transformJSXExpressionContainer,
isFragment, isFragment,
FRAGMENT,
walksScope, walksScope,
buildIIFE, buildIIFE,
JSX_HELPER_KEY,
}; };