From a7121f85532087aeded51beb4f55d9a5b06dc57b Mon Sep 17 00:00:00 2001 From: Amour1688 Date: Fri, 30 Apr 2021 18:29:48 +0800 Subject: [PATCH] chore: use airbnb-typescript/base eslint plugin (#420) --- .circleci/config.yml | 2 + .eslintrc.js | 17 +- package.json | 5 +- packages/babel-plugin-jsx/src/buildProps.ts | 382 ------------------ packages/babel-plugin-jsx/src/index.ts | 25 +- packages/babel-plugin-jsx/src/interface.ts | 28 ++ .../babel-plugin-jsx/src/parseDirectives.ts | 2 +- packages/babel-plugin-jsx/src/patchFlags.ts | 2 +- packages/babel-plugin-jsx/src/slotFlags.ts | 2 +- .../babel-plugin-jsx/src/sugar-fragment.ts | 2 +- .../babel-plugin-jsx/src/transform-vue-jsx.ts | 302 +++++++++++++- packages/babel-plugin-jsx/src/utils.ts | 133 ++++-- packages/babel-plugin-jsx/tsconfig.json | 1 - tsconfig.json | 11 +- yarn.lock | 106 ++++- 15 files changed, 541 insertions(+), 479 deletions(-) delete mode 100644 packages/babel-plugin-jsx/src/buildProps.ts create mode 100644 packages/babel-plugin-jsx/src/interface.ts diff --git a/.circleci/config.yml b/.circleci/config.yml index 7ae06c2..556eb68 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -24,5 +24,7 @@ jobs: - ~/.cache/yarn key: v2-dependencies-{{ checksum "yarn.lock" }} + - run: yarn lint + # run tests! - run: yarn test diff --git a/.eslintrc.js b/.eslintrc.js index c5e7efa..28ef6d0 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -5,6 +5,7 @@ module.exports = { ecmaFeatures: { jsx: true, }, + project: './tsconfig.json', }, env: { browser: true, @@ -15,8 +16,7 @@ module.exports = { parser: '@typescript-eslint/parser', plugins: ['@typescript-eslint', 'import'], extends: [ - 'eslint-config-airbnb-base', - 'plugin:@typescript-eslint/recommended', + 'airbnb-typescript/base', ], rules: { 'no-nested-ternary': [0], @@ -27,19 +27,8 @@ module.exports = { 'import/no-extraneous-dependencies': [0], 'consistent-return': [0], 'no-bitwise': [0], + '@typescript-eslint/no-use-before-define': [0], 'prefer-destructuring': [2, { array: false }], - 'import/extensions': [0], - '@typescript-eslint/ban-ts-comment': [0], - '@typescript-eslint/explicit-module-boundary-types': [0], - '@typescript-eslint/no-explicit-any': [0], - '@typescript-eslint/no-non-null-assertion': [0], 'max-len': [0], }, - settings: { - 'import/resolver': { - node: { - extensions: ['.js', '.jsx', '.ts', '.tsx'], - }, - }, - }, }; diff --git a/package.json b/package.json index 3b4a61e..420cd10 100644 --- a/package.json +++ b/package.json @@ -16,9 +16,10 @@ "jsx" ], "devDependencies": { + "@typescript-eslint/eslint-plugin": "^4.4.1", "eslint": "^7.7.0", - "eslint-config-airbnb-base": "^14.1.0", - "eslint-plugin-import": "^2.20.2", + "eslint-config-airbnb-typescript": "^12.3.1", + "eslint-plugin-import": "^2.22.1", "lerna": "^3.19.0" } } diff --git a/packages/babel-plugin-jsx/src/buildProps.ts b/packages/babel-plugin-jsx/src/buildProps.ts deleted file mode 100644 index 05636f4..0000000 --- a/packages/babel-plugin-jsx/src/buildProps.ts +++ /dev/null @@ -1,382 +0,0 @@ -import * as t from '@babel/types'; -import { NodePath } from '@babel/traverse'; -import { addDefault } from '@babel/helper-module-imports'; -import { - createIdentifier, - isDirective, - checkIsComponent, - getTag, - getJSXAttributeName, - walksScope, - transformJSXExpressionContainer, -} from './utils'; -import parseDirectives from './parseDirectives'; -import { PatchFlags } from './patchFlags'; -import { State } from '.'; -import { transformJSXElement } from './transform-vue-jsx'; -import SlotFlags from './slotFlags'; - -const xlinkRE = /^xlink([A-Z])/; -const onRE = /^on[^a-z]/; - -const isOn = (key: string) => onRE.test(key); - -export type Slots = t.Identifier | t.ObjectExpression | null; - -const getJSXAttributeValue = ( - path: NodePath, - state: State, -): ( - t.StringLiteral | t.Expression | null - ) => { - const valuePath = path.get('value'); - if (valuePath.isJSXElement()) { - return transformJSXElement(valuePath, state); - } - if (valuePath.isStringLiteral()) { - return valuePath.node; - } - if (valuePath.isJSXExpressionContainer()) { - return transformJSXExpressionContainer(valuePath); - } - - return null; -}; - -const transformJSXSpreadAttribute = ( - nodePath: NodePath, - path: NodePath, - mergeProps: boolean, - args: (t.ObjectProperty | t.Expression | t.SpreadElement)[], -) => { - const argument = path.get('argument') as NodePath; - const properties = t.isObjectExpression(argument.node) ? argument.node.properties : undefined; - if (!properties) { - if (argument.isIdentifier()) { - walksScope(nodePath, (argument.node as t.Identifier).name, SlotFlags.DYNAMIC); - } - args.push(mergeProps ? argument.node : t.spreadElement(argument.node)); - } else if (mergeProps) { - args.push(t.objectExpression(properties)); - } else { - args.push(...(properties as t.ObjectProperty[])); - } -}; - -const mergeAsArray = (existing: t.ObjectProperty, incoming: t.ObjectProperty) => { - if (t.isArrayExpression(existing.value)) { - existing.value.elements.push(incoming.value as t.Expression); - } else { - existing.value = t.arrayExpression([ - existing.value as t.Expression, - incoming.value as t.Expression, - ]); - } -}; - -const dedupeProperties = (properties: t.ObjectProperty[] = [], mergeProps?: boolean) => { - if (!mergeProps) { - return properties; - } - const knownProps = new Map(); - const deduped: t.ObjectProperty[] = []; - properties.forEach((prop) => { - if (t.isStringLiteral(prop.key)) { - const { value: name } = prop.key; - const existing = knownProps.get(name); - if (existing) { - if (name === 'style' || name === 'class' || name.startsWith('on')) { - mergeAsArray(existing, prop); - } - } else { - knownProps.set(name, prop); - deduped.push(prop); - } - } else { - // v-model target with variable - deduped.push(prop); - } - }); - - return deduped; -}; - -/** - * Check if an attribute value is constant - * @param node - * @returns boolean - */ -const isConstant = ( - node: t.Expression | t.Identifier | t.Literal | t.SpreadElement | null, -): boolean => { - if (t.isIdentifier(node)) { - return node.name === 'undefined'; - } - if (t.isArrayExpression(node)) { - const { elements } = node; - return elements.every((element) => element && isConstant(element)); - } - if (t.isObjectExpression(node)) { - return node.properties.every((property) => isConstant((property as any).value)); - } - if (t.isLiteral(node)) { - return true; - } - return false; -}; - -const buildProps = (path: NodePath, state: State) => { - const tag = getTag(path, state); - const isComponent = checkIsComponent(path.get('openingElement')); - const props = path.get('openingElement').get('attributes'); - const directives: t.ArrayExpression[] = []; - const dynamicPropNames = new Set(); - - let slots: Slots = null; - let patchFlag = 0; - - if (props.length === 0) { - return { - tag, - isComponent, - slots, - props: t.nullLiteral(), - directives, - patchFlag, - dynamicPropNames, - }; - } - - let properties: t.ObjectProperty[] = []; - - // patchFlag analysis - let hasRef = false; - let hasClassBinding = false; - let hasStyleBinding = false; - let hasHydrationEventBinding = false; - let hasDynamicKeys = false; - - const mergeArgs: (t.CallExpression | t.ObjectExpression | t.Identifier)[] = []; - const { mergeProps = true } = state.opts; - props - .forEach((prop) => { - if (prop.isJSXAttribute()) { - let name = getJSXAttributeName(prop); - - const attributeValue = getJSXAttributeValue(prop, state); - - if (!isConstant(attributeValue) || name === 'ref') { - if ( - !isComponent - && isOn(name) - // omit the flag for click handlers becaues hydration gives click - // dedicated fast path. - && name.toLowerCase() !== 'onclick' - // omit v-model handlers - && name !== 'onUpdate:modelValue' - ) { - hasHydrationEventBinding = true; - } - - if (name === 'ref') { - hasRef = true; - } else if (name === 'class' && !isComponent) { - hasClassBinding = true; - } else if (name === 'style' && !isComponent) { - hasStyleBinding = true; - } else if ( - name !== 'key' - && !isDirective(name) - && name !== 'on' - ) { - dynamicPropNames.add(name); - } - } - if (state.opts.transformOn && (name === 'on' || name === 'nativeOn')) { - if (!state.get('transformOn')) { - state.set('transformOn', addDefault( - path, - '@vue/babel-helper-vue-transform-on', - { nameHint: '_transformOn' }, - )); - } - mergeArgs.push(t.callExpression( - state.get('transformOn'), - [attributeValue || t.booleanLiteral(true)], - )); - return; - } - if (isDirective(name)) { - const { - directive, modifiers, values, args, directiveName, - } = parseDirectives({ - tag, - isComponent, - name, - path: prop, - state, - value: attributeValue, - }); - - if (directiveName === 'slots') { - slots = attributeValue as Slots; - return; - } - if (directive) { - directives.push(t.arrayExpression(directive)); - } else if (directiveName === 'html') { - properties.push(t.objectProperty( - t.stringLiteral('innerHTML'), - values[0] as any, - )); - dynamicPropNames.add('innerHTML'); - } else if (directiveName === 'text') { - properties.push(t.objectProperty( - t.stringLiteral('textContent'), - values[0] as any, - )); - dynamicPropNames.add('textContent'); - } - - if (['models', 'model'].includes(directiveName)) { - values.forEach((value, index) => { - const propName = args[index]; - // v-model target with variable - const isDynamic = propName && !t.isStringLiteral(propName) && !t.isNullLiteral(propName); - - // must be v-model or v-models and is a component - if (!directive) { - properties.push( - t.objectProperty(t.isNullLiteral(propName) - ? t.stringLiteral('modelValue') : propName, value as any, isDynamic), - ); - if (!isDynamic) { - dynamicPropNames.add((propName as t.StringLiteral)?.value || 'modelValue'); - } - - if (modifiers[index]?.size) { - properties.push( - t.objectProperty( - isDynamic - ? t.binaryExpression('+', propName, t.stringLiteral('Modifiers')) - : t.stringLiteral(`${(propName as t.StringLiteral)?.value || 'model'}Modifiers`), - t.objectExpression( - [...modifiers[index]].map((modifier) => t.objectProperty( - t.stringLiteral(modifier), - t.booleanLiteral(true), - )), - ), - isDynamic, - ), - ); - } - } - - const updateName = isDynamic - ? t.binaryExpression('+', t.stringLiteral('onUpdate'), propName) - : t.stringLiteral(`onUpdate:${(propName as t.StringLiteral)?.value || 'modelValue'}`); - - properties.push( - t.objectProperty( - updateName, - t.arrowFunctionExpression( - [t.identifier('$event')], - t.assignmentExpression('=', value as any, t.identifier('$event')), - ), - isDynamic, - ), - ); - - if (!isDynamic) { - dynamicPropNames.add((updateName as t.StringLiteral).value); - } else { - hasDynamicKeys = true; - } - }); - } - } else { - if (name.match(xlinkRE)) { - name = name.replace(xlinkRE, (_, firstCharacter) => `xlink:${firstCharacter.toLowerCase()}`); - } - properties.push(t.objectProperty( - t.stringLiteral(name), - attributeValue || t.booleanLiteral(true), - )); - } - } else { - if (properties.length && mergeProps) { - mergeArgs.push(t.objectExpression(dedupeProperties(properties, mergeProps))); - properties = []; - } - - // JSXSpreadAttribute - hasDynamicKeys = true; - transformJSXSpreadAttribute( - path as NodePath, - prop as NodePath, - mergeProps, - mergeProps ? mergeArgs : properties, - ); - } - }); - - // patchFlag analysis - if (hasDynamicKeys) { - patchFlag |= PatchFlags.FULL_PROPS; - } else { - if (hasClassBinding) { - patchFlag |= PatchFlags.CLASS; - } - if (hasStyleBinding) { - patchFlag |= PatchFlags.STYLE; - } - if (dynamicPropNames.size) { - patchFlag |= PatchFlags.PROPS; - } - if (hasHydrationEventBinding) { - patchFlag |= PatchFlags.HYDRATE_EVENTS; - } - } - - if ( - (patchFlag === 0 || patchFlag === PatchFlags.HYDRATE_EVENTS) - && (hasRef || directives.length > 0) - ) { - patchFlag |= PatchFlags.NEED_PATCH; - } - - let propsExpression: t.Expression | t.ObjectProperty | t.Literal = t.nullLiteral(); - if (mergeArgs.length) { - if (properties.length) { - mergeArgs.push(t.objectExpression(dedupeProperties(properties, mergeProps))); - } - if (mergeArgs.length > 1) { - propsExpression = t.callExpression( - createIdentifier(state, 'mergeProps'), - mergeArgs, - ); - } else { - // single no need for a mergeProps call - propsExpression = mergeArgs[0]; - } - } else if (properties.length) { - // single no need for spread - if (properties.length === 1 && t.isSpreadElement(properties[0])) { - propsExpression = (properties[0] as unknown as t.SpreadElement).argument; - } else { - propsExpression = t.objectExpression(dedupeProperties(properties, mergeProps)); - } - } - - return { - tag, - props: propsExpression, - isComponent, - slots, - directives, - patchFlag, - dynamicPropNames, - }; -}; - -export default buildProps; diff --git a/packages/babel-plugin-jsx/src/index.ts b/packages/babel-plugin-jsx/src/index.ts index 2e3822f..a607745 100644 --- a/packages/babel-plugin-jsx/src/index.ts +++ b/packages/babel-plugin-jsx/src/index.ts @@ -6,30 +6,9 @@ import { addNamed, isModule, addNamespace } from '@babel/helper-module-imports'; import { NodePath } from '@babel/traverse'; import transformVueJSX from './transform-vue-jsx'; import sugarFragment from './sugar-fragment'; +import type { VueJSXPluginOptions, State } from './interface'; -export type State = { - get: (name: string) => any; - set: (name: string, value: any) => any; - opts: VueJSXPluginOptions; - file: BabelCore.BabelFile -} - -export interface VueJSXPluginOptions { - /** transform `on: { click: xx }` to `onClick: xxx` */ - transformOn?: boolean; - /** enable optimization or not. */ - optimize?: boolean; - /** merge static and dynamic class / style attributes / onXXX handlers */ - mergeProps?: boolean; - /** configuring custom elements */ - isCustomElement?: (tag: string) => boolean; - /** enable object slots syntax */ - enableObjectSlots?: boolean; - /** Replace the function used when compiling JSX expressions */ - pragma?: string; -} - -export type ExcludesBoolean = (x: T | false | true) => x is T; +export { VueJSXPluginOptions }; const hasJSX = (parentPath: NodePath) => { let fileHasJSX = false; diff --git a/packages/babel-plugin-jsx/src/interface.ts b/packages/babel-plugin-jsx/src/interface.ts new file mode 100644 index 0000000..53cd503 --- /dev/null +++ b/packages/babel-plugin-jsx/src/interface.ts @@ -0,0 +1,28 @@ +import * as t from '@babel/types'; +import * as BabelCore from '@babel/core'; + +export type Slots = t.Identifier | t.ObjectExpression | null; + +export type State = { + get: (name: string) => any; + set: (name: string, value: any) => any; + opts: VueJSXPluginOptions; + file: BabelCore.BabelFile +}; + +export interface VueJSXPluginOptions { + /** transform `on: { click: xx }` to `onClick: xxx` */ + transformOn?: boolean; + /** enable optimization or not. */ + optimize?: boolean; + /** merge static and dynamic class / style attributes / onXXX handlers */ + mergeProps?: boolean; + /** configuring custom elements */ + isCustomElement?: (tag: string) => boolean; + /** enable object slots syntax */ + enableObjectSlots?: boolean; + /** Replace the function used when compiling JSX expressions */ + pragma?: string; +} + +export type ExcludesBoolean = (x: T | false | true) => x is T; diff --git a/packages/babel-plugin-jsx/src/parseDirectives.ts b/packages/babel-plugin-jsx/src/parseDirectives.ts index d4b7c41..22bae90 100644 --- a/packages/babel-plugin-jsx/src/parseDirectives.ts +++ b/packages/babel-plugin-jsx/src/parseDirectives.ts @@ -1,7 +1,7 @@ import * as t from '@babel/types'; import { NodePath } from '@babel/traverse'; import { createIdentifier } from './utils'; -import { State } from '.'; +import type { State } from './interface'; export type Tag = t.Identifier | t.MemberExpression | t.StringLiteral | t.CallExpression; diff --git a/packages/babel-plugin-jsx/src/patchFlags.ts b/packages/babel-plugin-jsx/src/patchFlags.ts index 0a0ab42..25434f9 100644 --- a/packages/babel-plugin-jsx/src/patchFlags.ts +++ b/packages/babel-plugin-jsx/src/patchFlags.ts @@ -13,7 +13,7 @@ export const enum PatchFlags { NEED_PATCH = 1 << 9, DYNAMIC_SLOTS = 1 << 10, HOISTED = -1, - BAIL = -2 + BAIL = -2, } // dev only flag -> name mapping diff --git a/packages/babel-plugin-jsx/src/slotFlags.ts b/packages/babel-plugin-jsx/src/slotFlags.ts index 4c425fb..8c32138 100644 --- a/packages/babel-plugin-jsx/src/slotFlags.ts +++ b/packages/babel-plugin-jsx/src/slotFlags.ts @@ -18,7 +18,7 @@ const enum SlotFlags { * received. This has to be refined at runtime, when the child's vnode * is being created (in `normalizeChildren`) */ - FORWARDED = 3 + FORWARDED = 3, } export default SlotFlags; diff --git a/packages/babel-plugin-jsx/src/sugar-fragment.ts b/packages/babel-plugin-jsx/src/sugar-fragment.ts index ae6afea..89e449a 100644 --- a/packages/babel-plugin-jsx/src/sugar-fragment.ts +++ b/packages/babel-plugin-jsx/src/sugar-fragment.ts @@ -1,6 +1,6 @@ import * as t from '@babel/types'; import { NodePath } from '@babel/traverse'; -import { State } from '.'; +import type { State } from './interface'; import { createIdentifier, FRAGMENT } from './utils'; const transformFragment = ( diff --git a/packages/babel-plugin-jsx/src/transform-vue-jsx.ts b/packages/babel-plugin-jsx/src/transform-vue-jsx.ts index 926adc9..7a22bdc 100644 --- a/packages/babel-plugin-jsx/src/transform-vue-jsx.ts +++ b/packages/babel-plugin-jsx/src/transform-vue-jsx.ts @@ -1,5 +1,6 @@ import * as t from '@babel/types'; import { NodePath } from '@babel/traverse'; +import { addDefault } from '@babel/helper-module-imports'; import { createIdentifier, transformJSXSpreadChild, @@ -7,10 +8,295 @@ import { transformJSXExpressionContainer, walksScope, buildIIFE, + isDirective, + checkIsComponent, + getTag, + getJSXAttributeName, + isOn, + isConstant, + dedupeProperties, + transformJSXSpreadAttribute, } from './utils'; -import buildProps from './buildProps'; import SlotFlags from './slotFlags'; -import { State, ExcludesBoolean } from '.'; +import { PatchFlags } from './patchFlags'; +import parseDirectives from './parseDirectives'; +import type { State, ExcludesBoolean, Slots } from './interface'; + +const xlinkRE = /^xlink([A-Z])/; + +const getJSXAttributeValue = ( + path: NodePath, + state: State, +): ( + t.StringLiteral | t.Expression | null + ) => { + const valuePath = path.get('value'); + if (valuePath.isJSXElement()) { + return transformJSXElement(valuePath, state); + } + if (valuePath.isStringLiteral()) { + return valuePath.node; + } + if (valuePath.isJSXExpressionContainer()) { + return transformJSXExpressionContainer(valuePath); + } + + return null; +}; + +const buildProps = (path: NodePath, state: State) => { + const tag = getTag(path, state); + const isComponent = checkIsComponent(path.get('openingElement')); + const props = path.get('openingElement').get('attributes'); + const directives: t.ArrayExpression[] = []; + const dynamicPropNames = new Set(); + + let slots: Slots = null; + let patchFlag = 0; + + if (props.length === 0) { + return { + tag, + isComponent, + slots, + props: t.nullLiteral(), + directives, + patchFlag, + dynamicPropNames, + }; + } + + let properties: t.ObjectProperty[] = []; + + // patchFlag analysis + let hasRef = false; + let hasClassBinding = false; + let hasStyleBinding = false; + let hasHydrationEventBinding = false; + let hasDynamicKeys = false; + + const mergeArgs: (t.CallExpression | t.ObjectExpression | t.Identifier)[] = []; + const { mergeProps = true } = state.opts; + props + .forEach((prop) => { + if (prop.isJSXAttribute()) { + let name = getJSXAttributeName(prop); + + const attributeValue = getJSXAttributeValue(prop, state); + + if (!isConstant(attributeValue) || name === 'ref') { + if ( + !isComponent + && isOn(name) + // omit the flag for click handlers becaues hydration gives click + // dedicated fast path. + && name.toLowerCase() !== 'onclick' + // omit v-model handlers + && name !== 'onUpdate:modelValue' + ) { + hasHydrationEventBinding = true; + } + + if (name === 'ref') { + hasRef = true; + } else if (name === 'class' && !isComponent) { + hasClassBinding = true; + } else if (name === 'style' && !isComponent) { + hasStyleBinding = true; + } else if ( + name !== 'key' + && !isDirective(name) + && name !== 'on' + ) { + dynamicPropNames.add(name); + } + } + if (state.opts.transformOn && (name === 'on' || name === 'nativeOn')) { + if (!state.get('transformOn')) { + state.set('transformOn', addDefault( + path, + '@vue/babel-helper-vue-transform-on', + { nameHint: '_transformOn' }, + )); + } + mergeArgs.push(t.callExpression( + state.get('transformOn'), + [attributeValue || t.booleanLiteral(true)], + )); + return; + } + if (isDirective(name)) { + const { + directive, modifiers, values, args, directiveName, + } = parseDirectives({ + tag, + isComponent, + name, + path: prop, + state, + value: attributeValue, + }); + + if (directiveName === 'slots') { + slots = attributeValue as Slots; + return; + } + if (directive) { + directives.push(t.arrayExpression(directive)); + } else if (directiveName === 'html') { + properties.push(t.objectProperty( + t.stringLiteral('innerHTML'), + values[0] as any, + )); + dynamicPropNames.add('innerHTML'); + } else if (directiveName === 'text') { + properties.push(t.objectProperty( + t.stringLiteral('textContent'), + values[0] as any, + )); + dynamicPropNames.add('textContent'); + } + + if (['models', 'model'].includes(directiveName)) { + values.forEach((value, index) => { + const propName = args[index]; + // v-model target with variable + const isDynamic = propName && !t.isStringLiteral(propName) && !t.isNullLiteral(propName); + + // must be v-model or v-models and is a component + if (!directive) { + properties.push( + t.objectProperty(t.isNullLiteral(propName) + ? t.stringLiteral('modelValue') : propName, value as any, isDynamic), + ); + if (!isDynamic) { + dynamicPropNames.add((propName as t.StringLiteral)?.value || 'modelValue'); + } + + if (modifiers[index]?.size) { + properties.push( + t.objectProperty( + isDynamic + ? t.binaryExpression('+', propName, t.stringLiteral('Modifiers')) + : t.stringLiteral(`${(propName as t.StringLiteral)?.value || 'model'}Modifiers`), + t.objectExpression( + [...modifiers[index]].map((modifier) => t.objectProperty( + t.stringLiteral(modifier), + t.booleanLiteral(true), + )), + ), + isDynamic, + ), + ); + } + } + + const updateName = isDynamic + ? t.binaryExpression('+', t.stringLiteral('onUpdate'), propName) + : t.stringLiteral(`onUpdate:${(propName as t.StringLiteral)?.value || 'modelValue'}`); + + properties.push( + t.objectProperty( + updateName, + t.arrowFunctionExpression( + [t.identifier('$event')], + t.assignmentExpression('=', value as any, t.identifier('$event')), + ), + isDynamic, + ), + ); + + if (!isDynamic) { + dynamicPropNames.add((updateName as t.StringLiteral).value); + } else { + hasDynamicKeys = true; + } + }); + } + } else { + if (name.match(xlinkRE)) { + name = name.replace(xlinkRE, (_, firstCharacter) => `xlink:${firstCharacter.toLowerCase()}`); + } + properties.push(t.objectProperty( + t.stringLiteral(name), + attributeValue || t.booleanLiteral(true), + )); + } + } else { + if (properties.length && mergeProps) { + mergeArgs.push(t.objectExpression(dedupeProperties(properties, mergeProps))); + properties = []; + } + + // JSXSpreadAttribute + hasDynamicKeys = true; + transformJSXSpreadAttribute( + path as NodePath, + prop as NodePath, + mergeProps, + mergeProps ? mergeArgs : properties, + ); + } + }); + + // patchFlag analysis + if (hasDynamicKeys) { + patchFlag |= PatchFlags.FULL_PROPS; + } else { + if (hasClassBinding) { + patchFlag |= PatchFlags.CLASS; + } + if (hasStyleBinding) { + patchFlag |= PatchFlags.STYLE; + } + if (dynamicPropNames.size) { + patchFlag |= PatchFlags.PROPS; + } + if (hasHydrationEventBinding) { + patchFlag |= PatchFlags.HYDRATE_EVENTS; + } + } + + if ( + (patchFlag === 0 || patchFlag === PatchFlags.HYDRATE_EVENTS) + && (hasRef || directives.length > 0) + ) { + patchFlag |= PatchFlags.NEED_PATCH; + } + + let propsExpression: t.Expression | t.ObjectProperty | t.Literal = t.nullLiteral(); + if (mergeArgs.length) { + if (properties.length) { + mergeArgs.push(t.objectExpression(dedupeProperties(properties, mergeProps))); + } + if (mergeArgs.length > 1) { + propsExpression = t.callExpression( + createIdentifier(state, 'mergeProps'), + mergeArgs, + ); + } else { + // single no need for a mergeProps call + propsExpression = mergeArgs[0]; + } + } else if (properties.length) { + // single no need for spread + if (properties.length === 1 && t.isSpreadElement(properties[0])) { + propsExpression = (properties[0] as unknown as t.SpreadElement).argument; + } else { + propsExpression = t.objectExpression(dedupeProperties(properties, mergeProps)); + } + } + + return { + tag, + props: propsExpression, + isComponent, + slots, + directives, + patchFlag, + dynamicPropNames, + }; +}; /** * Get children from Array of JSX children @@ -19,11 +305,11 @@ import { State, ExcludesBoolean } from '.'; */ const getChildren = ( paths: NodePath< - t.JSXText - | t.JSXExpressionContainer - | t.JSXSpreadChild - | t.JSXElement - | t.JSXFragment + t.JSXText + | t.JSXExpressionContainer + | t.JSXSpreadChild + | t.JSXElement + | t.JSXFragment >[], state: State, ): t.Expression[] => paths @@ -211,8 +497,6 @@ const transformJSXElement = ( ]); }; -export { transformJSXElement }; - export default ({ JSXElement: { exit(path: NodePath, state: State) { diff --git a/packages/babel-plugin-jsx/src/utils.ts b/packages/babel-plugin-jsx/src/utils.ts index f2e44e5..eae7d53 100644 --- a/packages/babel-plugin-jsx/src/utils.ts +++ b/packages/babel-plugin-jsx/src/utils.ts @@ -2,12 +2,12 @@ import * as t from '@babel/types'; import htmlTags from 'html-tags'; import svgTags from 'svg-tags'; import { NodePath } from '@babel/traverse'; -import { State } from '.'; +import type { State } from './interface'; import SlotFlags from './slotFlags'; -const JSX_HELPER_KEY = 'JSX_HELPER_KEY'; -const FRAGMENT = 'Fragment'; -const KEEP_ALIVE = 'KeepAlive'; +export const JSX_HELPER_KEY = 'JSX_HELPER_KEY'; +export const FRAGMENT = 'Fragment'; +export const KEEP_ALIVE = 'KeepAlive'; /** * create Identifier @@ -16,7 +16,7 @@ const KEEP_ALIVE = 'KeepAlive'; * @param name string * @returns MemberExpression */ -const createIdentifier = ( +export const createIdentifier = ( state: State, name: string, ): t.Identifier | t.MemberExpression => state.get(name)(); @@ -24,7 +24,7 @@ const createIdentifier = ( * Checks if string is describing a directive * @param src string */ -const isDirective = (src: string): boolean => src.startsWith('v-') +export const isDirective = (src: string): boolean => src.startsWith('v-') || (src.startsWith('v') && src.length >= 2 && src[1] >= 'A' && src[1] <= 'Z'); /** @@ -32,7 +32,7 @@ const isDirective = (src: string): boolean => src.startsWith('v-') * @param tag string * @returns boolean */ -const shouldTransformedToSlots = (tag: string) => !(tag.endsWith(FRAGMENT) || tag === KEEP_ALIVE); +export const shouldTransformedToSlots = (tag: string) => !(tag.endsWith(FRAGMENT) || tag === KEEP_ALIVE); /** * Check if a Node is a component @@ -41,7 +41,7 @@ const shouldTransformedToSlots = (tag: string) => !(tag.endsWith(FRAGMENT) || ta * @param path JSXOpeningElement * @returns boolean */ -const checkIsComponent = (path: NodePath): boolean => { +export const checkIsComponent = (path: NodePath): boolean => { const namePath = path.get('name'); if (namePath.isJSXMemberExpression()) { @@ -58,7 +58,7 @@ const checkIsComponent = (path: NodePath): boolean => { * @param path JSXMemberExpression * @returns MemberExpression */ -const transformJSXMemberExpression = ( +export const transformJSXMemberExpression = ( path: NodePath, ): t.MemberExpression => { const objectPath = path.node.object; @@ -78,7 +78,7 @@ const transformJSXMemberExpression = ( * @param state State * @returns Identifier | StringLiteral | MemberExpression | CallExpression */ -const getTag = ( +export const getTag = ( path: NodePath, state: State, ): t.Identifier | t.CallExpression | t.StringLiteral | t.MemberExpression => { @@ -104,7 +104,7 @@ const getTag = ( throw new Error(`getTag: ${namePath.type} is not supported`); }; -const getJSXAttributeName = (path: NodePath): string => { +export const getJSXAttributeName = (path: NodePath): string => { const nameNode = path.node.name; if (t.isJSXIdentifier(nameNode)) { return nameNode.name; @@ -118,7 +118,7 @@ const getJSXAttributeName = (path: NodePath): string => { * @param path JSXText * @returns StringLiteral | null */ -const transformJSXText = (path: NodePath): t.StringLiteral | null => { +export const transformJSXText = (path: NodePath): t.StringLiteral | null => { const { node } = path; const lines = node.value.split(/\r\n|\n|\r/); @@ -169,10 +169,10 @@ const transformJSXText = (path: NodePath): t.StringLiteral | null => * @param path JSXExpressionContainer * @returns Expression */ -const transformJSXExpressionContainer = ( +export const transformJSXExpressionContainer = ( path: NodePath, ): ( - t.Expression + t.Expression ) => path.get('expression').node as t.Expression; /** @@ -180,11 +180,11 @@ const transformJSXExpressionContainer = ( * @param path JSXSpreadChild * @returns SpreadElement */ -const transformJSXSpreadChild = ( +export const transformJSXSpreadChild = ( path: NodePath, ): t.SpreadElement => t.spreadElement(path.get('expression').node); -const walksScope = (path: NodePath, name: string, slotFlag: SlotFlags): void => { +export 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('slotFlag', slotFlag); @@ -193,7 +193,7 @@ const walksScope = (path: NodePath, name: string, slotFlag: SlotFlags): void => } }; -const buildIIFE = (path: NodePath, children: t.Expression[]) => { +export const buildIIFE = (path: NodePath, children: t.Expression[]) => { const { parentPath } = path; if (t.isAssignmentExpression(parentPath)) { const { left } = parentPath.node as t.AssignmentExpression; @@ -221,19 +221,88 @@ const buildIIFE = (path: NodePath, children: t.Expression[]) => { return children; }; -export { - createIdentifier, - isDirective, - checkIsComponent, - transformJSXMemberExpression, - getTag, - getJSXAttributeName, - transformJSXText, - transformJSXSpreadChild, - transformJSXExpressionContainer, - shouldTransformedToSlots, - FRAGMENT, - walksScope, - buildIIFE, - JSX_HELPER_KEY, +const onRE = /^on[^a-z]/; + +export const isOn = (key: string) => onRE.test(key); + +const mergeAsArray = (existing: t.ObjectProperty, incoming: t.ObjectProperty) => { + if (t.isArrayExpression(existing.value)) { + existing.value.elements.push(incoming.value as t.Expression); + } else { + existing.value = t.arrayExpression([ + existing.value as t.Expression, + incoming.value as t.Expression, + ]); + } +}; + +export const dedupeProperties = (properties: t.ObjectProperty[] = [], mergeProps?: boolean) => { + if (!mergeProps) { + return properties; + } + const knownProps = new Map(); + const deduped: t.ObjectProperty[] = []; + properties.forEach((prop) => { + if (t.isStringLiteral(prop.key)) { + const { value: name } = prop.key; + const existing = knownProps.get(name); + if (existing) { + if (name === 'style' || name === 'class' || name.startsWith('on')) { + mergeAsArray(existing, prop); + } + } else { + knownProps.set(name, prop); + deduped.push(prop); + } + } else { + // v-model target with variable + deduped.push(prop); + } + }); + + return deduped; +}; + +/** + * Check if an attribute value is constant + * @param node + * @returns boolean + */ +export const isConstant = ( + node: t.Expression | t.Identifier | t.Literal | t.SpreadElement | null, +): boolean => { + if (t.isIdentifier(node)) { + return node.name === 'undefined'; + } + if (t.isArrayExpression(node)) { + const { elements } = node; + return elements.every((element) => element && isConstant(element)); + } + if (t.isObjectExpression(node)) { + return node.properties.every((property) => isConstant((property as any).value)); + } + if (t.isLiteral(node)) { + return true; + } + return false; +}; + +export const transformJSXSpreadAttribute = ( + nodePath: NodePath, + path: NodePath, + mergeProps: boolean, + args: (t.ObjectProperty | t.Expression | t.SpreadElement)[], +) => { + const argument = path.get('argument') as NodePath; + const properties = t.isObjectExpression(argument.node) ? argument.node.properties : undefined; + if (!properties) { + if (argument.isIdentifier()) { + walksScope(nodePath, (argument.node as t.Identifier).name, SlotFlags.DYNAMIC); + } + args.push(mergeProps ? argument.node : t.spreadElement(argument.node)); + } else if (mergeProps) { + args.push(t.objectExpression(properties)); + } else { + args.push(...(properties as t.ObjectProperty[])); + } }; diff --git a/packages/babel-plugin-jsx/tsconfig.json b/packages/babel-plugin-jsx/tsconfig.json index 269e1fb..4215a6e 100644 --- a/packages/babel-plugin-jsx/tsconfig.json +++ b/packages/babel-plugin-jsx/tsconfig.json @@ -4,7 +4,6 @@ "rootDirs": ["./src"], "outDir": "dist", "downlevelIteration": true, - "types": ["node", "jest"], "declaration": true }, "include": ["src/**/*", "global.d.ts"], diff --git a/tsconfig.json b/tsconfig.json index 5e6c6cc..5eee14f 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -12,10 +12,15 @@ "esModuleInterop": true, "removeComments": false, "jsx": "preserve", - "lib": ["esnext", "dom"], - "types": ["node"] + "lib": [ + "esnext", + "dom" + ], + "types": ["node", "jest"], }, "include": [ - "global.d.ts" + "global.d.ts", + "packages/*/src", + "packages/*/test", ] } diff --git a/yarn.lock b/yarn.lock index 180795e..6e7487f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2167,9 +2167,35 @@ semver "^7.3.2" tsutils "^3.17.1" +"@typescript-eslint/eslint-plugin@^4.4.1": + version "4.15.2" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.15.2.tgz#981b26b4076c62a5a55873fbef3fe98f83360c61" + integrity sha512-uiQQeu9tWl3f1+oK0yoAv9lt/KXO24iafxgQTkIYO/kitruILGx3uH+QtIAHqxFV+yIsdnJH+alel9KuE3J15Q== + dependencies: + "@typescript-eslint/experimental-utils" "4.15.2" + "@typescript-eslint/scope-manager" "4.15.2" + debug "^4.1.1" + functional-red-black-tree "^1.0.1" + lodash "^4.17.15" + regexpp "^3.0.0" + semver "^7.3.2" + tsutils "^3.17.1" + +"@typescript-eslint/experimental-utils@4.15.2": + version "4.15.2" + resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-4.15.2.tgz#5efd12355bd5b535e1831282e6cf465b9a71cf36" + integrity sha512-Fxoshw8+R5X3/Vmqwsjc8nRO/7iTysRtDqx6rlfLZ7HbT8TZhPeQqbPjTyk2RheH3L8afumecTQnUc9EeXxohQ== + dependencies: + "@types/json-schema" "^7.0.3" + "@typescript-eslint/scope-manager" "4.15.2" + "@typescript-eslint/types" "4.15.2" + "@typescript-eslint/typescript-estree" "4.15.2" + eslint-scope "^5.0.0" + eslint-utils "^2.0.0" + "@typescript-eslint/experimental-utils@4.17.0": version "4.17.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-4.17.0.tgz#762c44aaa1a6a3c05b6d63a8648fb89b89f84c80" + resolved "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-4.17.0.tgz#762c44aaa1a6a3c05b6d63a8648fb89b89f84c80" integrity sha512-ZR2NIUbnIBj+LGqCFGQ9yk2EBQrpVVFOh9/Kd0Lm6gLpSAcCuLLe5lUCibKGCqyH9HPwYC0GIJce2O1i8VYmWA== dependencies: "@types/json-schema" "^7.0.3" @@ -2189,9 +2215,27 @@ "@typescript-eslint/typescript-estree" "4.22.0" debug "^4.1.1" +"@typescript-eslint/parser@^4.4.1": + version "4.15.2" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-4.15.2.tgz#c804474321ef76a3955aec03664808f0d6e7872e" + integrity sha512-SHeF8xbsC6z2FKXsaTb1tBCf0QZsjJ94H6Bo51Y1aVEZ4XAefaw5ZAilMoDPlGghe+qtq7XdTiDlGfVTOmvA+Q== + dependencies: + "@typescript-eslint/scope-manager" "4.15.2" + "@typescript-eslint/types" "4.15.2" + "@typescript-eslint/typescript-estree" "4.15.2" + debug "^4.1.1" + +"@typescript-eslint/scope-manager@4.15.2": + version "4.15.2" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-4.15.2.tgz#5725bda656995960ae1d004bfd1cd70320f37f4f" + integrity sha512-Zm0tf/MSKuX6aeJmuXexgdVyxT9/oJJhaCkijv0DvJVT3ui4zY6XYd6iwIo/8GEZGy43cd7w1rFMiCLHbRzAPQ== + dependencies: + "@typescript-eslint/types" "4.15.2" + "@typescript-eslint/visitor-keys" "4.15.2" + "@typescript-eslint/scope-manager@4.17.0": version "4.17.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-4.17.0.tgz#f4edf94eff3b52a863180f7f89581bf963e3d37d" + resolved "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-4.17.0.tgz#f4edf94eff3b52a863180f7f89581bf963e3d37d" integrity sha512-OJ+CeTliuW+UZ9qgULrnGpPQ1bhrZNFpfT/Bc0pzNeyZwMik7/ykJ0JHnQ7krHanFN9wcnPK89pwn84cRUmYjw== dependencies: "@typescript-eslint/types" "4.17.0" @@ -2199,25 +2243,43 @@ "@typescript-eslint/scope-manager@4.22.0": version "4.22.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-4.22.0.tgz#ed411545e61161a8d702e703a4b7d96ec065b09a" + resolved "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-4.22.0.tgz#ed411545e61161a8d702e703a4b7d96ec065b09a" integrity sha512-OcCO7LTdk6ukawUM40wo61WdeoA7NM/zaoq1/2cs13M7GyiF+T4rxuA4xM+6LeHWjWbss7hkGXjFDRcKD4O04Q== dependencies: "@typescript-eslint/types" "4.22.0" "@typescript-eslint/visitor-keys" "4.22.0" +"@typescript-eslint/types@4.15.2": + version "4.15.2" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-4.15.2.tgz#04acf3a2dc8001a88985291744241e732ef22c60" + integrity sha512-r7lW7HFkAarfUylJ2tKndyO9njwSyoy6cpfDKWPX6/ctZA+QyaYscAHXVAfJqtnY6aaTwDYrOhp+ginlbc7HfQ== + "@typescript-eslint/types@4.17.0": version "4.17.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-4.17.0.tgz#f57d8fc7f31b348db946498a43050083d25f40ad" + resolved "https://registry.npmjs.org/@typescript-eslint/types/-/types-4.17.0.tgz#f57d8fc7f31b348db946498a43050083d25f40ad" integrity sha512-RN5z8qYpJ+kXwnLlyzZkiJwfW2AY458Bf8WqllkondQIcN2ZxQowAToGSd9BlAUZDB5Ea8I6mqL2quGYCLT+2g== "@typescript-eslint/types@4.22.0": version "4.22.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-4.22.0.tgz#0ca6fde5b68daf6dba133f30959cc0688c8dd0b6" + resolved "https://registry.npmjs.org/@typescript-eslint/types/-/types-4.22.0.tgz#0ca6fde5b68daf6dba133f30959cc0688c8dd0b6" integrity sha512-sW/BiXmmyMqDPO2kpOhSy2Py5w6KvRRsKZnV0c4+0nr4GIcedJwXAq+RHNK4lLVEZAJYFltnnk1tJSlbeS9lYA== +"@typescript-eslint/typescript-estree@4.15.2": + version "4.15.2" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-4.15.2.tgz#c2f7a1e94f3428d229d5ecff3ead6581ee9b62fa" + integrity sha512-cGR8C2g5SPtHTQvAymEODeqx90pJHadWsgTtx6GbnTWKqsg7yp6Eaya9nFzUd4KrKhxdYTTFBiYeTPQaz/l8bw== + dependencies: + "@typescript-eslint/types" "4.15.2" + "@typescript-eslint/visitor-keys" "4.15.2" + debug "^4.1.1" + globby "^11.0.1" + is-glob "^4.0.1" + semver "^7.3.2" + tsutils "^3.17.1" + "@typescript-eslint/typescript-estree@4.17.0": version "4.17.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-4.17.0.tgz#b835d152804f0972b80dbda92477f9070a72ded1" + resolved "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-4.17.0.tgz#b835d152804f0972b80dbda92477f9070a72ded1" integrity sha512-lRhSFIZKUEPPWpWfwuZBH9trYIEJSI0vYsrxbvVvNyIUDoKWaklOAelsSkeh3E2VBSZiNe9BZ4E5tYBZbUczVQ== dependencies: "@typescript-eslint/types" "4.17.0" @@ -2241,9 +2303,17 @@ semver "^7.3.2" tsutils "^3.17.1" +"@typescript-eslint/visitor-keys@4.15.2": + version "4.15.2" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-4.15.2.tgz#3d1c7979ce75bf6acf9691109bd0d6b5706192b9" + integrity sha512-TME1VgSb7wTwgENN5KVj4Nqg25hP8DisXxNBojM4Nn31rYaNDIocNm5cmjOFfh42n7NVERxWrDFoETO/76ePyg== + dependencies: + "@typescript-eslint/types" "4.15.2" + eslint-visitor-keys "^2.0.0" + "@typescript-eslint/visitor-keys@4.17.0": version "4.17.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-4.17.0.tgz#9c304cfd20287c14a31d573195a709111849b14d" + resolved "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-4.17.0.tgz#9c304cfd20287c14a31d573195a709111849b14d" integrity sha512-WfuMN8mm5SSqXuAr9NM+fItJ0SVVphobWYkWOwQ1odsfC014Vdxk/92t4JwS1Q6fCA/ABfCKpa3AVtpUKTNKGQ== dependencies: "@typescript-eslint/types" "4.17.0" @@ -4602,7 +4672,7 @@ escodegen@^1.14.1: optionalDependencies: source-map "~0.6.1" -eslint-config-airbnb-base@^14.1.0: +eslint-config-airbnb-base@^14.2.0, eslint-config-airbnb-base@^14.2.1: version "14.2.1" resolved "https://registry.yarnpkg.com/eslint-config-airbnb-base/-/eslint-config-airbnb-base-14.2.1.tgz#8a2eb38455dc5a312550193b319cdaeef042cd1e" integrity sha512-GOrQyDtVEc1Xy20U7vsB2yAoB4nBlfH5HZJeatRXHleO+OS5Ot+MWij4Dpltw4/DyIkqUfqz1epfhVR5XWWQPA== @@ -4611,6 +4681,24 @@ eslint-config-airbnb-base@^14.1.0: object.assign "^4.1.2" object.entries "^1.1.2" +eslint-config-airbnb-typescript@^12.3.1: + version "12.3.1" + resolved "https://registry.yarnpkg.com/eslint-config-airbnb-typescript/-/eslint-config-airbnb-typescript-12.3.1.tgz#83ab40d76402c208eb08516260d1d6fac8f8acbc" + integrity sha512-ql/Pe6/hppYuRp4m3iPaHJqkBB7dgeEmGPQ6X0UNmrQOfTF+dXw29/ZjU2kQ6RDoLxaxOA+Xqv07Vbef6oVTWw== + dependencies: + "@typescript-eslint/parser" "^4.4.1" + eslint-config-airbnb "^18.2.0" + eslint-config-airbnb-base "^14.2.0" + +eslint-config-airbnb@^18.2.0: + version "18.2.1" + resolved "https://registry.yarnpkg.com/eslint-config-airbnb/-/eslint-config-airbnb-18.2.1.tgz#b7fe2b42f9f8173e825b73c8014b592e449c98d9" + integrity sha512-glZNDEZ36VdlZWoxn/bUR1r/sdFKPd1mHPbqUtkctgNG4yT2DLLtJ3D+yCV+jzZCc2V1nBVkmdknOJBZ5Hc0fg== + dependencies: + eslint-config-airbnb-base "^14.2.1" + object.assign "^4.1.2" + object.entries "^1.1.2" + eslint-import-resolver-node@^0.3.4: version "0.3.4" resolved "https://registry.yarnpkg.com/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.4.tgz#85ffa81942c25012d8231096ddf679c03042c717" @@ -4627,7 +4715,7 @@ eslint-module-utils@^2.6.0: debug "^2.6.9" pkg-dir "^2.0.0" -eslint-plugin-import@^2.20.2: +eslint-plugin-import@^2.22.1: version "2.22.1" resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.22.1.tgz#0896c7e6a0cf44109a2d97b95903c2bb689d7702" integrity sha512-8K7JjINHOpH64ozkAhpT3sd+FswIZTfMZTjdx052pnWrgRCVfp8op9tbjpAk3DdUeI/Ba4C8OjdC0r90erHEOw==