diff --git a/packages/editor/__tests__/model/fieldModel.spec.ts b/packages/editor/__tests__/model/fieldModel.spec.ts index 0c0cb6e7..affd41ac 100644 --- a/packages/editor/__tests__/model/fieldModel.spec.ts +++ b/packages/editor/__tests__/model/fieldModel.spec.ts @@ -1,52 +1,68 @@ +import { AppModel } from '../../src/AppModel/AppModel'; import { FieldModel } from '../../src/AppModel/FieldModel'; +import { ComponentId } from '../../src/AppModel/IAppModel'; +import { registry } from '../services'; +import { ChangeIdMockSchema } from './mock'; +import { CORE_VERSION, CoreTraitName } from '@sunmao-ui/shared'; describe('Field test', () => { it('parse static property', () => { const field = new FieldModel('Hello, world!'); expect(field.isDynamic).toEqual(false); - expect(field.refs).toEqual({}); + expect(field.refComponentInfos).toEqual({}); expect(field.rawValue).toEqual('Hello, world!'); }); it('parse expression', () => { const field = new FieldModel('{{input.value}} + {{list[0].text}}'); expect(field.isDynamic).toEqual(true); - expect(field.refs).toEqual({ input: ['value'], list: ['[0]', '[0].text'] }); + expect(field.refComponentInfos['input' as ComponentId].refProperties).toEqual([ + 'value', + ]); + expect(field.refComponentInfos['list' as ComponentId].refProperties).toEqual([ + '[0]', + '[0].text', + ]); expect(field.rawValue).toEqual('{{input.value}} + {{list[0].text}}'); }); it('parse inline variable in expression', () => { const field = new FieldModel('{{ [].length }}'); expect(field.isDynamic).toEqual(true); - expect(field.refs).toEqual({}); + expect(field.refComponentInfos).toEqual({}); }); it('parse object property', () => { const field = new FieldModel({ raw: '{{input.value}}', format: 'md' }); expect(field.isDynamic).toEqual(false); - expect(field.refs).toEqual({}); + expect(field.refComponentInfos).toEqual({}); expect(field.rawValue).toEqual({ raw: '{{input.value}}', format: 'md' }); expect(field.getProperty('raw')!.rawValue).toEqual('{{input.value}}'); expect(field.getProperty('raw')!.isDynamic).toEqual(true); - expect(field.getProperty('raw')!.refs).toEqual({ input: ['value'] }); + expect( + field.getProperty('raw')!.refComponentInfos['input' as ComponentId].refProperties + ).toEqual(['value']); expect(field.getProperty('format')!.rawValue).toEqual('md'); expect(field.getProperty('format')!.isDynamic).toEqual(false); - expect(field.getProperty('format')!.refs).toEqual({}); + expect(field.getProperty('format')!.refComponentInfos).toEqual({}); }); it('parse array property', () => { const field = new FieldModel({ data: [1, '{{fetch.data}}'] }); expect(field.isDynamic).toEqual(false); - expect(field.refs).toEqual({}); + expect(field.refComponentInfos).toEqual({}); expect(field.rawValue).toEqual({ data: [1, '{{fetch.data}}'] }); expect(field.getProperty('data')!.rawValue).toEqual([1, '{{fetch.data}}']); expect(field.getProperty('data')!.isDynamic).toEqual(false); - expect(field.getProperty('data')!.refs).toEqual({}); + expect(field.getProperty('data')!.refComponentInfos).toEqual({}); expect(field.getProperty('data')!.getProperty(0)!.rawValue).toEqual(1); expect(field.getProperty('data')!.getProperty(0)!.isDynamic).toEqual(false); expect(field.getProperty('data')!.getProperty(1)!.rawValue).toEqual('{{fetch.data}}'); expect(field.getProperty('data')!.getProperty(1)!.isDynamic).toEqual(true); - expect(field.getProperty('data')!.getProperty(1)!.refs).toEqual({ fetch: ['data'] }); + expect( + field.getProperty('data')!.getProperty(1)!.refComponentInfos['fetch' as ComponentId] + .refProperties + ).toEqual(['data']); }); it('update array property', () => { @@ -70,4 +86,50 @@ describe('Field test', () => { field.update({ value: 'text' }); expect(field.rawValue).toEqual({ data: { a: 2 }, value: 'text' }); }); + + it('change component id', () => { + const appModel = new AppModel(ChangeIdMockSchema, registry); + const input = appModel.getComponentById('input' as ComponentId); + const text = appModel.getComponentById('text' as ComponentId); + const button = appModel.getComponentById('button' as ComponentId); + + input!.changeId('input1' as ComponentId); + + expect(input!.id).toEqual('input1' as ComponentId); + expect(text!.properties.rawValue).toEqual({ + value: { + raw: "pre {{(function () {\n const object = { value: input1.value + input1.notExistKey };\n return '-' + object.value + '-';\n}());}} end", + format: 'plain', + }, + string: 'Please input here', + expressionString: "{{ 'input' }}", + array: ['input'], + expressionArray: "{{['input']}}", + object: { input: 'input' }, + expressionObject: "{{{'input': 'input'}}}", + }); + expect( + button!.traits.find( + trait => trait.type === `${CORE_VERSION}/${CoreTraitName.Event}` + )!.properties.rawValue + ).toEqual({ + handlers: [ + { + type: 'onClick', + componentId: 'input1', + method: { + name: 'setInputValue', + parameters: { + value: 'Hello', + }, + }, + disabled: false, + wait: { + type: 'delay', + time: 0, + }, + }, + ], + }); + }); }); diff --git a/packages/editor/__tests__/model/mock.ts b/packages/editor/__tests__/model/mock.ts index 0f5660ff..3f0ff24e 100644 --- a/packages/editor/__tests__/model/mock.ts +++ b/packages/editor/__tests__/model/mock.ts @@ -185,3 +185,128 @@ export const EventHandlerMockSchema: ComponentSchema[] = [ ], }, ]; + +export const ChangeIdMockSchema: ComponentSchema[] = [ + { + id: 'stack', + type: 'core/v1/stack', + properties: { + spacing: 12, + direction: 'horizontal', + align: 'auto', + wrap: '', + justify: '', + }, + traits: [], + }, + { + id: 'button', + type: 'chakra_ui/v1/button', + properties: { + text: { + raw: 'text', + format: 'plain', + }, + isLoading: false, + colorScheme: 'blue', + }, + traits: [ + { + type: 'core/v1/event', + properties: { + handlers: [ + { + type: 'onClick', + componentId: 'input', + method: { + name: 'setInputValue', + parameters: { + value: 'Hello', + }, + }, + disabled: false, + wait: { + type: 'delay', + time: 0, + }, + }, + ], + }, + }, + { + type: 'core/v1/slot', + properties: { + container: { + id: 'stack', + slot: 'content', + }, + }, + }, + ], + }, + { + id: 'text', + type: 'core/v1/text', + properties: { + value: { + raw: "pre {{(function () {\n const object = { value: input.value + input.notExistKey };\n return '-' + object.value + '-';\n}());}} end", + format: 'plain', + }, + string: 'Please input here', + expressionString: "{{ 'input' }}", + array: ['input'], + expressionArray: "{{['input']}}", + object: { input: 'input' }, + expressionObject: "{{{'input': 'input'}}}", + }, + traits: [ + { + type: 'core/v1/style', + properties: { + styles: [ + { + styleSlot: 'content', + style: '', + cssProperties: { + padding: '8px 0px 9px 0px ', + }, + }, + ], + }, + }, + { + type: 'core/v1/slot', + properties: { + container: { + id: 'stack', + slot: 'content', + }, + }, + }, + ], + }, + { + id: 'input', + type: 'chakra_ui/v1/input', + properties: { + variant: 'outline', + placeholder: 'Please input value', + size: 'md', + focusBorderColor: '', + isDisabled: false, + isRequired: false, + defaultValue: '', + }, + traits: [ + { + type: 'core/v1/slot', + properties: { + container: { + id: 'stack', + slot: 'content', + }, + }, + }, + ], + }, +]; diff --git a/packages/editor/package.json b/packages/editor/package.json index a9ddbb09..9a4b6936 100644 --- a/packages/editor/package.json +++ b/packages/editor/package.json @@ -48,6 +48,7 @@ "acorn-walk": "^8.2.0", "ajv": "^8.8.2", "codemirror": "^5.63.3", + "escodegen": "^2.0.0", "formik": "^2.2.9", "framer-motion": "^4", "immer": "^9.0.6", @@ -65,6 +66,7 @@ "@sunmao-ui/vite-plugins": "^1.0.2", "@swc/core": "^1.2.121", "@types/codemirror": "^5.60.5", + "@types/escodegen": "^0.0.7", "@types/json-schema": "^7.0.7", "@types/lodash-es": "^4.17.5", "@types/tern": "^0.23.4", diff --git a/packages/editor/src/AppModel/AppModel.ts b/packages/editor/src/AppModel/AppModel.ts index 5cebef6d..328da59a 100644 --- a/packages/editor/src/AppModel/AppModel.ts +++ b/packages/editor/src/AppModel/AppModel.ts @@ -11,9 +11,11 @@ import { SlotName, } from './IAppModel'; import { genComponent } from './utils'; +import mitt from 'mitt'; export class AppModel implements IAppModel { topComponents: IComponentModel[] = []; + emitter: IAppModel['emitter'] = mitt(); // modules: IModuleModel[] = []; private schema: ComponentSchema[] = []; private componentMap: Record = {}; @@ -66,7 +68,7 @@ export class AppModel implements IAppModel { createComponent(type: ComponentType, id?: ComponentId): IComponentModel { const component = genComponent(this.registry, type, id || this.genId(type)); - return new ComponentModel(this, component, this.registry); + return new ComponentModel(component, this.registry, this); } getComponentById(componentId: ComponentId): IComponentModel | undefined { @@ -109,7 +111,7 @@ export class AppModel implements IAppModel { if (this.componentMap[c.id as ComponentId]) { throw new Error(`Duplicate component id: ${c.id}`); } else { - const comp = new ComponentModel(this, c, this.registry); + const comp = new ComponentModel(c, this.registry, this); this.componentMap[c.id as ComponentId] = comp; return comp; } diff --git a/packages/editor/src/AppModel/ComponentModel.ts b/packages/editor/src/AppModel/ComponentModel.ts index 32c52f52..8060620c 100644 --- a/packages/editor/src/AppModel/ComponentModel.ts +++ b/packages/editor/src/AppModel/ComponentModel.ts @@ -42,9 +42,9 @@ export class ComponentModel implements IComponentModel { _isDirty = false; constructor( - public appModel: IAppModel, private schema: ComponentSchema, - private registry: RegistryInterface + private registry: RegistryInterface, + public appModel: IAppModel ) { this.schema = schema; @@ -52,11 +52,18 @@ export class ComponentModel implements IComponentModel { this.type = schema.type as ComponentType; this.spec = this.registry.getComponentByType(this.type) as any; - this.traits = schema.traits.map(t => new TraitModel(t, this, this.registry)); + this.traits = schema.traits.map( + t => new TraitModel(t, this.registry, this.appModel, this) + ); this.genStateExample(); this.parentId = this._slotTrait?.rawProperties.container.id; this.parentSlot = this._slotTrait?.rawProperties.container.slot; - this.properties = new FieldModel(schema.properties); + this.properties = new FieldModel( + schema.properties, + this.spec.spec.properties, + this.appModel, + this + ); } get slots() { @@ -151,7 +158,7 @@ export class ComponentModel implements IComponentModel { addTrait(traitType: TraitType, properties: Record): ITraitModel { const traitSchema = genTrait(traitType, properties); - const trait = new TraitModel(traitSchema, this, this.registry); + const trait = new TraitModel(traitSchema, this.registry, this.appModel, this); this.traits.push(trait); this._isDirty = true; this.genStateExample(); @@ -205,24 +212,23 @@ export class ComponentModel implements IComponentModel { changeId(newId: ComponentId) { const oldId = this.id; const isIdExist = !!this.appModel.getComponentById(newId); + if (isIdExist) { throw Error(`Id ${newId} already exist`); } + this.id = newId; for (const slot in this.children) { const slotChildren = this.children[slot as SlotName]; + slotChildren.forEach(child => { child.parentId = newId; - const slotTrait = child.traits.find(t => t.type === SlotTraitType); - if (slotTrait) { - slotTrait.properties.update({ container: { id: newId, slot } }); - slotTrait._isDirty = true; - } - child._isDirty = true; }); } this._isDirty = true; this.appModel.changeComponentMapId(oldId, newId); + this.appModel.emitter.emit('idChange', { oldId, newId }); + return this; } diff --git a/packages/editor/src/AppModel/FieldModel.ts b/packages/editor/src/AppModel/FieldModel.ts index e6a32585..8cd1819b 100644 --- a/packages/editor/src/AppModel/FieldModel.ts +++ b/packages/editor/src/AppModel/FieldModel.ts @@ -1,18 +1,38 @@ -import { parseExpression } from '@sunmao-ui/shared'; +import { parseExpression, expChunkToString, SpecOptions } from '@sunmao-ui/shared'; import * as acorn from 'acorn'; import * as acornLoose from 'acorn-loose'; import { simple as simpleWalk } from 'acorn-walk'; import { flattenDeep, isArray, isObject } from 'lodash-es'; import { isExpression } from '../validator/utils'; -import { ComponentId, IFieldModel, ModuleId } from './IAppModel'; +import { + ComponentId, + IAppModel, + IComponentModel, + ITraitModel, + IFieldModel, + ModuleId, + RefInfo, + ASTNode, + AppModelEventType, +} from './IAppModel'; +import escodegen from 'escodegen'; +import { JSONSchema7 } from 'json-schema'; export class FieldModel implements IFieldModel { isDynamic = false; - refs: Record = {}; + refComponentInfos: Record = {}; + private astNodes: Record = {}; private value: unknown | Array | Record; - constructor(value: unknown) { + constructor( + value: unknown, + public spec?: JSONSchema7 & SpecOptions, + private appModel?: IAppModel, + private componentModel?: IComponentModel, + private traitModel?: ITraitModel + ) { this.update(value); + this.appModel?.emitter.on('idChange', this.onReferenceIdChange.bind(this)); } get rawValue() { @@ -47,7 +67,15 @@ export class FieldModel implements IFieldModel { (oldValue as FieldModel).updateValue(value[key], false); newValue = oldValue; } else { - newValue = new FieldModel(value[key]); + newValue = new FieldModel( + value[key], + (this.spec?.properties?.[key] || this.spec?.items) as + | (JSONSchema7 & SpecOptions) + | undefined, + this.appModel, + this.componentModel, + this.traitModel + ); } if (isArray(result)) { @@ -109,23 +137,44 @@ export class FieldModel implements IFieldModel { parseExpression(this.value as string).filter(exp => typeof exp !== 'string') ); + this.refComponentInfos = {}; + this.astNodes = {}; + exps.forEach(exp => { let lastIdentifier: ComponentId = '' as ComponentId; - simpleWalk((acornLoose as typeof acorn).parse(exp, { ecmaVersion: 2020 }), { - Expression: node => { - switch (node.type) { + const node = (acornLoose as typeof acorn).parse(exp, { ecmaVersion: 2020 }); + + this.astNodes[exp] = node as ASTNode; + + simpleWalk(node, { + Expression: expressionNode => { + switch (expressionNode.type) { case 'Identifier': - const key = exp.slice(node.start, node.end) as ComponentId; - this.refs[key] = []; + const key = exp.slice( + expressionNode.start, + expressionNode.end + ) as ComponentId; + + if (this.refComponentInfos[key]) { + this.refComponentInfos[key].componentIdASTNodes.push( + expressionNode as ASTNode + ); + } else { + this.refComponentInfos[key] = { + componentIdASTNodes: [expressionNode as ASTNode], + refProperties: [], + }; + } lastIdentifier = key; + break; case 'MemberExpression': - const str = exp.slice(node.start, node.end); + const str = exp.slice(expressionNode.start, expressionNode.end); let path = str.replace(lastIdentifier, ''); if (path.startsWith('.')) { path = path.slice(1, path.length); } - this.refs[lastIdentifier]?.push(path); + this.refComponentInfos[lastIdentifier]?.refProperties.push(path); break; default: } @@ -133,4 +182,50 @@ export class FieldModel implements IFieldModel { }); }); } + + private onReferenceIdChange({ oldId, newId }: AppModelEventType['idChange']) { + if (!this.componentModel) { + return; + } + + if (this.spec?.isComponentId && this.value === oldId) { + // the normal string property but the `isComponentId` which like the event trait's `componentId` property + // just simply change its value + if (this.traitModel) { + this.traitModel._isDirty = true; + } + this.componentModel._isDirty = true; + this.update(newId); + } else if (this.refComponentInfos[oldId]) { + // the component vars in the expressions + // change the AST nodes values of the related component vars + // then generate the new expression + const exps = parseExpression(this.value as string); + const newExps = exps.map(exp => { + const node = this.astNodes[exp.toString()]; + + if (node) { + const ref = this.refComponentInfos[oldId]; + + ref.componentIdASTNodes.forEach(refNode => { + refNode.name = newId; + }); + + this.refComponentInfos[newId] = ref; + delete this.refComponentInfos[oldId]; + + return [escodegen.generate(node)]; + } + + return exp; + }); + const value = expChunkToString(newExps); + + if (this.traitModel) { + this.traitModel._isDirty = true; + } + this.componentModel._isDirty = true; + this.update(value); + } + } } diff --git a/packages/editor/src/AppModel/IAppModel.ts b/packages/editor/src/AppModel/IAppModel.ts index 6b19f605..d16c2f1d 100644 --- a/packages/editor/src/AppModel/IAppModel.ts +++ b/packages/editor/src/AppModel/IAppModel.ts @@ -4,6 +4,10 @@ import { MethodSchema, RuntimeTrait, } from '@sunmao-ui/core'; +import { SpecOptions } from '@sunmao-ui/shared'; +import { Emitter } from 'mitt'; +import { Node } from 'acorn'; +import { JSONSchema7 } from 'json-schema'; export type ComponentId = string & { kind: 'componentId'; @@ -36,7 +40,12 @@ export type EventName = string & { kind: 'eventName'; }; +export type AppModelEventType = { + idChange: { oldId: ComponentId; newId: ComponentId }; +}; + export interface IAppModel { + emitter: Emitter; topComponents: IComponentModel[]; // modules: IModuleModel[]; moduleIds: ModuleId[]; @@ -116,14 +125,22 @@ export interface ITraitModel { updateProperty: (key: string, value: any) => void; } +export type ASTNode = Node & { name: string }; + +export type RefInfo = { + componentIdASTNodes: ASTNode[]; + refProperties: string[]; +}; + export interface IFieldModel { // value: any; + spec?: JSONSchema7 & SpecOptions; isDynamic: boolean; + rawValue: any; update: (value: unknown) => void; getProperty: (key: string) => IFieldModel | void; getValue: () => unknown | void | IFieldModel; traverse: (cb: (f: IFieldModel, key: string) => void) => void; - rawValue: any; // ids of used components in the expression - refs: Record; + refComponentInfos: Record; } diff --git a/packages/editor/src/AppModel/TraitModel.ts b/packages/editor/src/AppModel/TraitModel.ts index efe3ed4e..22d7c080 100644 --- a/packages/editor/src/AppModel/TraitModel.ts +++ b/packages/editor/src/AppModel/TraitModel.ts @@ -6,6 +6,7 @@ import { ITraitModel, IFieldModel, TraitId, + IAppModel, } from './IAppModel'; import { FieldModel } from './FieldModel'; import { genTrait } from './utils'; @@ -22,8 +23,9 @@ export class TraitModel implements ITraitModel { constructor( trait: TraitSchema, - public parent: IComponentModel, - private registry: RegistryInterface + private registry: RegistryInterface, + private appModel: IAppModel, + public parent: IComponentModel ) { this.schema = trait; this.parent = parent; @@ -31,7 +33,13 @@ export class TraitModel implements ITraitModel { this.id = `${this.parent.id}_trait${traitIdCount++}` as TraitId; this.spec = this.registry.getTraitByType(this.type); - this.properties = new FieldModel(trait.properties); + this.properties = new FieldModel( + trait.properties, + this.spec.spec.properties, + this.appModel, + this.parent, + this + ); } get rawProperties() { diff --git a/packages/editor/src/validator/SchemaValidator.ts b/packages/editor/src/validator/SchemaValidator.ts index 91fbf96b..78841dc9 100644 --- a/packages/editor/src/validator/SchemaValidator.ts +++ b/packages/editor/src/validator/SchemaValidator.ts @@ -136,7 +136,8 @@ export class SchemaValidator implements ISchemaValidator { .addKeyword('category') .addKeyword('widgetOptions') .addKeyword('conditions') - .addKeyword('name'); + .addKeyword('name') + .addKeyword('isComponentId'); this.validatorMap = { components: {}, diff --git a/packages/editor/src/validator/rules/PropertiesRules.ts b/packages/editor/src/validator/rules/PropertiesRules.ts index 40768a92..ce16ec0e 100644 --- a/packages/editor/src/validator/rules/PropertiesRules.ts +++ b/packages/editor/src/validator/rules/PropertiesRules.ts @@ -67,9 +67,9 @@ class ExpressionValidatorRule implements PropertiesValidatorRule { // validate expression properties.traverse((fieldModel, key) => { - Object.keys(fieldModel.refs).forEach((id: string) => { + Object.keys(fieldModel.refComponentInfos).forEach((id: string) => { const targetComponent = appModel.getComponentById(id as ComponentId); - const paths = fieldModel.refs[id as ComponentId]; + const paths = fieldModel.refComponentInfos[id as ComponentId].refProperties; if (targetComponent) { // case 1: id is a component diff --git a/packages/runtime/src/traits/core/Slot.tsx b/packages/runtime/src/traits/core/Slot.tsx index efb94d35..73a1740c 100644 --- a/packages/runtime/src/traits/core/Slot.tsx +++ b/packages/runtime/src/traits/core/Slot.tsx @@ -4,7 +4,7 @@ import { implementRuntimeTrait } from '../../utils/buildKit'; const ContainerPropertySpec = Type.Object( { - id: Type.String(), + id: Type.String({ isComponentId: true }), slot: Type.String(), }, // don't show this property in the editor diff --git a/packages/shared/src/specs/event.ts b/packages/shared/src/specs/event.ts index e7d08d1b..56aa83ba 100644 --- a/packages/shared/src/specs/event.ts +++ b/packages/shared/src/specs/event.ts @@ -4,6 +4,7 @@ import { CORE_VERSION, CoreWidgetName } from '../constants/core'; const BaseEventSpecObject = { componentId: Type.String({ title: 'Component ID', + isComponentId: true, }), method: Type.Object( { diff --git a/packages/shared/src/types/spec.ts b/packages/shared/src/types/spec.ts index 8cc03f62..befadef3 100644 --- a/packages/shared/src/types/spec.ts +++ b/packages/shared/src/types/spec.ts @@ -11,4 +11,6 @@ export type SpecOptions> = { name?: string; // conditional render conditions?: Condition[]; + // is a reference of component id + isComponentId?: boolean; }; diff --git a/packages/shared/src/utils/expression.ts b/packages/shared/src/utils/expression.ts index 5dc20ec8..ad089acf 100644 --- a/packages/shared/src/utils/expression.ts +++ b/packages/shared/src/utils/expression.ts @@ -82,3 +82,15 @@ export const parseExpression = (rawExp: string, parseListItem = false): ExpChunk return result; }; + +export const expChunkToString = (exps: ExpChunk[]): string => { + return exps + .map(expOrExpChunk => { + if (expOrExpChunk instanceof Array) { + return `{{${expChunkToString(expOrExpChunk)}}}`; + } + + return expOrExpChunk; + }) + .join(''); +}; diff --git a/yarn.lock b/yarn.lock index 41483f00..b48c6939 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3594,6 +3594,11 @@ dependencies: "@types/ms" "*" +"@types/escodegen@^0.0.7": + version "0.0.7" + resolved "https://registry.yarnpkg.com/@types/escodegen/-/escodegen-0.0.7.tgz#a1c3e3dfd76da89f01d7d196eebe227ebe4b6eec" + integrity sha512-46oENdSRNEJXCNrPJoC3vRolZJpfeEm7yvATkd2bCncKFG0PUEyfBCaoacfpcXH4Y5RRuqdVj3J7TI+wwn2SbQ== + "@types/estree@*": version "0.0.50" resolved "https://registry.npmjs.org/@types/estree/-/estree-0.0.50.tgz" @@ -5965,7 +5970,7 @@ escape-string-regexp@^4.0.0: escodegen@^2.0.0: version "2.0.0" - resolved "https://registry.npmjs.org/escodegen/-/escodegen-2.0.0.tgz" + resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-2.0.0.tgz#5e32b12833e8aa8fa35e1bf0befa89380484c7dd" integrity sha512-mmHKys/C8BFUGI+MAWNcSYoORYLMdPzjrknd2Vc+bUsjN5bXcr8EhrNB+UTqfL1y3I9c4fw2ihgtMPQLBRiQxw== dependencies: esprima "^4.0.1"