From 199e799266aeb94b0a16c0824e95f795cbb17163 Mon Sep 17 00:00:00 2001 From: amanecer2levi Date: Thu, 2 Mar 2023 09:35:43 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20=F0=9F=8E=B8=20edit=20project=20configu?= =?UTF-8?q?ration?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ✅ Closes: 15 --- .../src/app/workspace/dto/schema.dto.ts | 13 +++ .../entities/workspace-configuration.ts | 12 +++ .../src/app/workspace/workspace.controller.ts | 6 ++ .../src/app/workspace/workspace.service.ts | 18 +++++ apps/cli-daemon/tsconfig.json | 3 +- .../workspace-settings.component.ts | 10 +-- .../workspace-settings.service.ts | 14 ---- .../configuration.component.html | 13 ++- .../configuration.component.spec.ts | 38 ++++++++- .../configuration/configuration.component.ts | 79 ++++++++++++++++++- libs/configuration/src/lib/services/index.ts | 1 + .../lib/services/workspace-settings/index.ts | 1 + .../workspace-settings.service.spec.ts | 0 .../workspace-settings.service.ts | 34 ++++++++ libs/configuration/src/testing/index.ts | 1 + .../workspace-settings.service.mock.ts | 59 ++++++++++++++ package.json | 2 + pnpm-lock.yaml | 35 +++++++- 18 files changed, 308 insertions(+), 31 deletions(-) create mode 100644 apps/cli-daemon/src/app/workspace/dto/schema.dto.ts create mode 100644 apps/cli-daemon/src/app/workspace/entities/workspace-configuration.ts delete mode 100644 apps/cli-gui/src/app/workspace-settings/workspace-settings.service.ts create mode 100644 libs/configuration/src/lib/services/index.ts create mode 100644 libs/configuration/src/lib/services/workspace-settings/index.ts rename {apps/cli-gui/src/app => libs/configuration/src/lib/services}/workspace-settings/workspace-settings.service.spec.ts (100%) create mode 100644 libs/configuration/src/lib/services/workspace-settings/workspace-settings.service.ts create mode 100644 libs/configuration/src/testing/index.ts create mode 100644 libs/configuration/src/testing/workspace-settings.service.mock.ts diff --git a/apps/cli-daemon/src/app/workspace/dto/schema.dto.ts b/apps/cli-daemon/src/app/workspace/dto/schema.dto.ts new file mode 100644 index 0000000..740514a --- /dev/null +++ b/apps/cli-daemon/src/app/workspace/dto/schema.dto.ts @@ -0,0 +1,13 @@ +import { JSONSchema7 } from 'json-schema'; + +export class SchemaDto implements JSONSchema7 { + properties: JSONSchema7['properties']; + title: JSONSchema7['title']; + description: JSONSchema7['title']; + + constructor({ title, properties, description }: JSONSchema7) { + this.properties = properties; + this.title = title; + this.description = description; + } +} diff --git a/apps/cli-daemon/src/app/workspace/entities/workspace-configuration.ts b/apps/cli-daemon/src/app/workspace/entities/workspace-configuration.ts new file mode 100644 index 0000000..94d69b1 --- /dev/null +++ b/apps/cli-daemon/src/app/workspace/entities/workspace-configuration.ts @@ -0,0 +1,12 @@ +export interface ExtractValuePaths { + path: string; + overridePath?: string; +} +export const configurationsPaths: ExtractValuePaths[] = [ + { path: 'definitions.project.properties.prefix', overridePath: 'prefix' }, + { path: 'definitions.project.properties.root', overridePath: 'root' }, + { + path: 'definitions.project.properties.sourceRoot', + overridePath: 'sourceRoot', + }, +]; diff --git a/apps/cli-daemon/src/app/workspace/workspace.controller.ts b/apps/cli-daemon/src/app/workspace/workspace.controller.ts index ce8350f..2ac0894 100644 --- a/apps/cli-daemon/src/app/workspace/workspace.controller.ts +++ b/apps/cli-daemon/src/app/workspace/workspace.controller.ts @@ -9,6 +9,7 @@ import { Patch, Post, } from '@nestjs/common'; +import { JSONSchema7 } from 'json-schema'; import { ExecResult } from '../generators/dto'; import { GeneratorsService } from '../generators/generators.service'; @@ -60,6 +61,11 @@ export class WorkspaceController { return this.workspaceService.readWorkspaceProjectNames(); } + @Get('workspace-configuration') + async getWorkspaceConfiguration(): Promise { + return this.workspaceService.getWorkspaceConfiguration(); + } + @Get('project/:projectName') async getProject( @Param('projectName') projectName: string diff --git a/apps/cli-daemon/src/app/workspace/workspace.service.ts b/apps/cli-daemon/src/app/workspace/workspace.service.ts index 04e4e84..a5134af 100644 --- a/apps/cli-daemon/src/app/workspace/workspace.service.ts +++ b/apps/cli-daemon/src/app/workspace/workspace.service.ts @@ -15,10 +15,14 @@ import { Logger, NotFoundException, } from '@nestjs/common'; +import { JSONSchema7 } from 'json-schema'; +import { Draft07 } from 'json-schema-library'; +import * as angularSchema from 'node_modules/@angular/cli/lib/config/schema.json'; import { SessionService } from '../session/session.service'; import { ProjectDto, UpdateProjectDto } from './dto'; +import { SchemaDto } from './dto/schema.dto'; import { ANGULAR_WORKSPACE_NOT_FOUND_EXCEPTION, BAD_PATH_EXCEPTION, @@ -157,4 +161,18 @@ export class WorkspaceService { return new InternalServerErrorException(err); } } + + getWorkspaceConfiguration(): Partial { + const jsonSchema = new Draft07(angularSchema); + + const { root, prefix, sourceRoot } = + jsonSchema.getSchema()['definitions']['project']['properties']; + const properties = new Draft07({ root, prefix, sourceRoot }).getSchema(); + + return new SchemaDto({ + title: 'Angular CLI Workspace Configuration', + description: 'Browser target options', + properties: properties, + }); + } } diff --git a/apps/cli-daemon/tsconfig.json b/apps/cli-daemon/tsconfig.json index 04f8c95..c8381cb 100644 --- a/apps/cli-daemon/tsconfig.json +++ b/apps/cli-daemon/tsconfig.json @@ -18,6 +18,7 @@ "noImplicitOverride": true, "noPropertyAccessFromIndexSignature": true, "noImplicitReturns": true, - "noFallthroughCasesInSwitch": true + "noFallthroughCasesInSwitch": true, + "resolveJsonModule": true } } diff --git a/apps/cli-gui/src/app/workspace-settings/workspace-settings.component.ts b/apps/cli-gui/src/app/workspace-settings/workspace-settings.component.ts index f6cd774..025260e 100644 --- a/apps/cli-gui/src/app/workspace-settings/workspace-settings.component.ts +++ b/apps/cli-gui/src/app/workspace-settings/workspace-settings.component.ts @@ -1,8 +1,6 @@ import { CommonModule } from '@angular/common'; import { ChangeDetectionStrategy, Component } from '@angular/core'; -import { WorkspaceSettingsService } from './workspace-settings.service'; - @Component({ selector: 'cli-workspace-settings', standalone: true, @@ -11,10 +9,4 @@ import { WorkspaceSettingsService } from './workspace-settings.service'; styleUrls: ['./workspace-settings.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush, }) -export class WorkspaceSettingsComponent { - angularJson$ = this.workspaceSettingsService.readWorkspaceProjectNames(); - - constructor( - private readonly workspaceSettingsService: WorkspaceSettingsService - ) {} -} +export class WorkspaceSettingsComponent {} diff --git a/apps/cli-gui/src/app/workspace-settings/workspace-settings.service.ts b/apps/cli-gui/src/app/workspace-settings/workspace-settings.service.ts deleted file mode 100644 index a4d65c0..0000000 --- a/apps/cli-gui/src/app/workspace-settings/workspace-settings.service.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { HttpClient } from '@angular/common/http'; -import { Injectable } from '@angular/core'; -import { Observable } from 'rxjs'; - -@Injectable({ - providedIn: 'root', -}) -export class WorkspaceSettingsService { - constructor(private readonly http: HttpClient) {} - - readWorkspaceProjectNames(): Observable { - return this.http.get(`/api/workspace`); - } -} diff --git a/libs/configuration/src/lib/configuration/configuration.component.html b/libs/configuration/src/lib/configuration/configuration.component.html index 0f57cf4..b2e96b3 100644 --- a/libs/configuration/src/lib/configuration/configuration.component.html +++ b/libs/configuration/src/lib/configuration/configuration.component.html @@ -1 +1,12 @@ -

configuration works!

+

Edit project configuration

+ + +
+ + +
+
diff --git a/libs/configuration/src/lib/configuration/configuration.component.spec.ts b/libs/configuration/src/lib/configuration/configuration.component.spec.ts index 35373d2..6472ec1 100644 --- a/libs/configuration/src/lib/configuration/configuration.component.spec.ts +++ b/libs/configuration/src/lib/configuration/configuration.component.spec.ts @@ -1,22 +1,58 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; + +import { + createWorkspaceSettingsServiceMockk, + workspaceConfigurationMock, +} from '../../testing'; +import { WorkspaceSettingsService } from '../services'; import { ConfigurationComponent } from './configuration.component'; describe('ConfigurationComponent', () => { let component: ConfigurationComponent; let fixture: ComponentFixture; + let service: WorkspaceSettingsService; beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [ConfigurationComponent], + imports: [ConfigurationComponent, NoopAnimationsModule], + providers: [ + { + provide: WorkspaceSettingsService, + useValue: createWorkspaceSettingsServiceMockk(), + }, + ], }).compileComponents(); fixture = TestBed.createComponent(ConfigurationComponent); component = fixture.componentInstance; fixture.detectChanges(); + service = TestBed.inject(WorkspaceSettingsService); }); it('should create', () => { expect(component).toBeTruthy(); }); + + it('should formly create descriptions for fields', () => { + const { root, sourceRoot, prefix } = workspaceConfigurationMock.properties; + const innerHTML = fixture.debugElement.nativeElement.innerHTML; + expect(innerHTML).toContain(root.description); + expect(innerHTML).toContain(sourceRoot.description); + expect(innerHTML).toContain(prefix.description); + }); + + it('should submit form', () => { + const updateWorkspaceProjectConfigurationSpy = jest.spyOn( + service, + 'updateWorkspaceProjectConfiguration' + ); + fixture.debugElement.nativeElement.querySelector('button').click(); + expect(updateWorkspaceProjectConfigurationSpy).toBeCalledWith('name', { + prefix: 'prefix', + root: 'root', + sourceRoot: 'sourceRoot', + }); + }); }); diff --git a/libs/configuration/src/lib/configuration/configuration.component.ts b/libs/configuration/src/lib/configuration/configuration.component.ts index a5256a1..852c72c 100644 --- a/libs/configuration/src/lib/configuration/configuration.component.ts +++ b/libs/configuration/src/lib/configuration/configuration.component.ts @@ -1,11 +1,82 @@ -import { CommonModule } from '@angular/common'; -import { Component } from '@angular/core'; +import { AsyncPipe, NgIf } from '@angular/common'; +import { ChangeDetectionStrategy, Component } from '@angular/core'; +import { FormBuilder, ReactiveFormsModule } from '@angular/forms'; +import { Project } from '@angular-cli-gui/shared/data'; +import { FormlyFieldConfig, FormlyModule } from '@ngx-formly/core'; +import { FormlyJsonschema } from '@ngx-formly/core/json-schema'; +import { FormlyMaterialModule } from '@ngx-formly/material'; +import { + combineLatest, + distinctUntilChanged, + map, + shareReplay, + switchMap, +} from 'rxjs'; + +import { WorkspaceSettingsService } from '../services'; @Component({ selector: 'cli-configuration', standalone: true, - imports: [CommonModule], + imports: [ + NgIf, + AsyncPipe, + FormlyModule, + FormlyMaterialModule, + ReactiveFormsModule, + ], templateUrl: './configuration.component.html', styleUrls: ['./configuration.component.css'], + changeDetection: ChangeDetectionStrategy.OnPush, }) -export class ConfigurationComponent {} +export class ConfigurationComponent { + private readonly projectConfiguration$ = this.workspaceSettingsService + .readWorkspaceProjectConfiguration() + .pipe( + map( + (configuration) => + this.schema.toFieldConfig(configuration) + .fieldGroup as FormlyFieldConfig[] + ) + ); + + private readonly projectName$ = this.workspaceSettingsService + .readWorkspaceProjectNames() + .pipe( + map((names) => names[0]), + shareReplay({ bufferSize: 1, refCount: true }) + ); + + private readonly workspaceProject$ = this.projectName$.pipe( + switchMap((currentProjectName: string) => + this.workspaceSettingsService.readWorkspaceProject(currentProjectName) + ), + distinctUntilChanged() + ); + + readonly formly$ = combineLatest([ + this.projectConfiguration$, + this.workspaceProject$, + ]).pipe(map(([formFields, formData]) => ({ formFields, formData }))); + + form = this.fb.group({}); + + constructor( + private workspaceSettingsService: WorkspaceSettingsService, + private fb: FormBuilder, + private schema: FormlyJsonschema + ) {} + + onSubmit(): void { + this.projectName$ + .pipe( + switchMap((projectName) => { + return this.workspaceSettingsService.updateWorkspaceProjectConfiguration( + projectName, + this.form.value as Project + ); + }) + ) + .subscribe(); + } +} diff --git a/libs/configuration/src/lib/services/index.ts b/libs/configuration/src/lib/services/index.ts new file mode 100644 index 0000000..6cefea7 --- /dev/null +++ b/libs/configuration/src/lib/services/index.ts @@ -0,0 +1 @@ +export * from './workspace-settings'; diff --git a/libs/configuration/src/lib/services/workspace-settings/index.ts b/libs/configuration/src/lib/services/workspace-settings/index.ts new file mode 100644 index 0000000..fb01b88 --- /dev/null +++ b/libs/configuration/src/lib/services/workspace-settings/index.ts @@ -0,0 +1 @@ +export * from './workspace-settings.service'; diff --git a/apps/cli-gui/src/app/workspace-settings/workspace-settings.service.spec.ts b/libs/configuration/src/lib/services/workspace-settings/workspace-settings.service.spec.ts similarity index 100% rename from apps/cli-gui/src/app/workspace-settings/workspace-settings.service.spec.ts rename to libs/configuration/src/lib/services/workspace-settings/workspace-settings.service.spec.ts diff --git a/libs/configuration/src/lib/services/workspace-settings/workspace-settings.service.ts b/libs/configuration/src/lib/services/workspace-settings/workspace-settings.service.ts new file mode 100644 index 0000000..1de2f79 --- /dev/null +++ b/libs/configuration/src/lib/services/workspace-settings/workspace-settings.service.ts @@ -0,0 +1,34 @@ +import { HttpClient } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { Project } from '@angular-cli-gui/shared/data'; +import { JsonObject } from '@angular-devkit/core/src/json/utils'; +import { JSONSchema7 } from 'json-schema'; +import { Observable } from 'rxjs'; + +@Injectable({ + providedIn: 'root', +}) +export class WorkspaceSettingsService { + constructor(private readonly http: HttpClient) {} + + readWorkspaceProjectNames(): Observable { + return this.http.get(`/api/workspace/project-names`); + } + + readWorkspaceProject(projectName: string): Observable { + return this.http.get(`/api/workspace/project/${projectName}`); + } + + readWorkspaceProjectConfiguration(): Observable { + return this.http.get(`/api/workspace/workspace-configuration`); + } + + updateWorkspaceProjectConfiguration( + projectName: string, + projectData: Project + ): Observable { + return this.http.patch(`/api/workspace/project/${projectName}`, { + ...projectData, + }); + } +} diff --git a/libs/configuration/src/testing/index.ts b/libs/configuration/src/testing/index.ts new file mode 100644 index 0000000..028e247 --- /dev/null +++ b/libs/configuration/src/testing/index.ts @@ -0,0 +1 @@ +export * from './workspace-settings.service.mock'; diff --git a/libs/configuration/src/testing/workspace-settings.service.mock.ts b/libs/configuration/src/testing/workspace-settings.service.mock.ts new file mode 100644 index 0000000..67fda42 --- /dev/null +++ b/libs/configuration/src/testing/workspace-settings.service.mock.ts @@ -0,0 +1,59 @@ +import { BehaviorSubject, Observable, Subject } from 'rxjs'; + +import { WorkspaceSettingsService } from '../lib/services'; + +type WorkspaceSettingsServiceMock = Partial< + Record | Observable> +>; + +export const workspaceConfigurationMock = { + properties: { + root: { + type: 'string', + description: 'Root of the project files.', + }, + prefix: { + type: 'string', + format: 'html-selector', + description: 'The prefix to apply to generated selectors.', + }, + sourceRoot: { + type: 'string', + description: + 'The root of the source files, assets and index.html file structure.', + }, + }, + title: 'Angular CLI Workspace Configuration', + description: 'Browser target options', +}; + +export const workspaceNamesMock = ['name']; + +export const workspaceProjectMock = { + root: 'root', + prefix: 'prefix', + sourceRoot: 'sourceRoot', + extensions: { + projectType: 'application', + schematics: { + '@schematics/angular:component': { + style: 'scss', + }, + }, + }, +}; + +export function createWorkspaceSettingsServiceMockk(): WorkspaceSettingsServiceMock { + return { + readWorkspaceProject: jest.fn( + () => new BehaviorSubject(workspaceProjectMock) + ), + readWorkspaceProjectNames: jest.fn( + () => new BehaviorSubject(workspaceNamesMock) + ), + readWorkspaceProjectConfiguration: jest.fn( + () => new BehaviorSubject(workspaceConfigurationMock) + ), + updateWorkspaceProjectConfiguration: jest.fn(() => new Subject()), + }; +} diff --git a/package.json b/package.json index c60c5ec..a979618 100644 --- a/package.json +++ b/package.json @@ -37,6 +37,7 @@ "axios": "^1.0.0", "class-transformer": "^0.5.1", "class-validator": "^0.14.0", + "json-schema-library": "^7.4.7", "reflect-metadata": "^0.1.13", "rxjs": "~7.5.0", "tslib": "^2.3.0", @@ -67,6 +68,7 @@ "@nrwl/workspace": "15.7.2", "@types/jest": "28.1.1", "@types/node": "18.7.1", + "@types/json-schema": "^7.0.11", "@typescript-eslint/eslint-plugin": "^5.36.1", "@typescript-eslint/parser": "^5.36.1", "eslint": "~8.15.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 330533f..a32dfa6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -41,6 +41,7 @@ specifiers: '@nrwl/webpack': 15.7.2 '@nrwl/workspace': 15.7.2 '@types/jest': 28.1.1 + '@types/json-schema': ^7.0.11 '@types/node': 18.7.1 '@typescript-eslint/eslint-plugin': ^5.36.1 '@typescript-eslint/parser': ^5.36.1 @@ -57,6 +58,7 @@ specifiers: jest: 28.1.1 jest-environment-jsdom: 28.1.1 jest-preset-angular: ~12.2.3 + json-schema-library: ^7.4.7 lint-staged: ^13.1.2 ng-packagr: ~15.1.0 nx: 15.7.2 @@ -95,6 +97,7 @@ dependencies: axios: 1.3.4 class-transformer: 0.5.1 class-validator: 0.14.0 + json-schema-library: 7.4.7 reflect-metadata: 0.1.13 rxjs: 7.5.7 tslib: 2.5.0 @@ -124,6 +127,7 @@ devDependencies: '@nrwl/webpack': 15.7.2_gbxag7umhulbfx43xwpj6p4w5y '@nrwl/workspace': 15.7.2_3joxs7tzhkrjjacnd53qs32tau '@types/jest': 28.1.1 + '@types/json-schema': 7.0.11 '@types/node': 18.7.1 '@typescript-eslint/eslint-plugin': 5.53.0_bzv5gegv2fhuzdqltzmps7k6li '@typescript-eslint/parser': 5.53.0_oy7hgmlo6357d5kkcjbkfgtg4q @@ -5517,6 +5521,17 @@ packages: rollup: 3.17.3 dev: true + /@sagold/json-pointer/5.0.1: + resolution: {integrity: sha512-cKY4N+3YC7CHUW28lOgqH6020EMqFS5nW9cT+/MBdUNXWEmLWeTsOtiaHxXuixL5jCJNv4AFqtGi4dQIYGG8XQ==} + dev: false + + /@sagold/json-query/6.0.0: + resolution: {integrity: sha512-fk9BimvNrzlhXiy+dvlwyA97W2N6GmPWCJo/2kwKEtU9oc93cVKamca9NcnjKx1hhjECjPfu30NQ8Tg2JGv/pA==} + dependencies: + '@sagold/json-pointer': 5.0.1 + ebnf: 1.9.0 + dev: false + /@schematics/angular/15.1.6: resolution: {integrity: sha512-y2kIQ1wJL0wR6v/LM5+PFJUivrYtdaIJVRdOXLLWl0AB5aLwObiWgLzAuBsbGm/9//WPPhw9PglS5EFFxTBDzg==} engines: {node: ^14.20.0 || ^16.13.0 || >=18.10.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'} @@ -7951,7 +7966,6 @@ packages: /deepmerge/4.3.0: resolution: {integrity: sha512-z2wJZXrmeHdvYJp/Ux55wIjqo81G5Bp4c+oELTW+7ar6SogWHajt5a9gO3s3IDaGSAXjDk0vlQKN3rms8ab3og==} engines: {node: '>=0.10.0'} - dev: true /default-gateway/6.0.3: resolution: {integrity: sha512-fwSOJsbbNzZ/CUFpqFBqYfYNLj1NbMPm8MMCIzHjC83iSJRBEGmDUxU+WP661BaBQImeC2yHwXtz+P/O9o+XEg==} @@ -8138,6 +8152,11 @@ packages: resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} dev: true + /ebnf/1.9.0: + resolution: {integrity: sha512-LKK899+j758AgPq00ms+y90mo+2P86fMKUWD28sH0zLKUj7aL6iIH2wy4jejAMM9I2BawJ+2kp6C3mMXj+Ii5g==} + hasBin: true + dev: false + /ee-first/1.1.1: resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} @@ -10747,6 +10766,16 @@ packages: engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} dev: true + /json-schema-library/7.4.7: + resolution: {integrity: sha512-m7MzIVbwDH6L0RzrPjQAZfpEffaa0wUyyJ049xK5YWX9yXQfOh89ER4WqiFGA+6OPK8dlCPGLmFb3q12FkeriQ==} + dependencies: + '@sagold/json-pointer': 5.0.1 + '@sagold/json-query': 6.0.0 + deepmerge: 4.3.0 + fast-deep-equal: 3.1.3 + valid-url: 1.0.9 + dev: false + /json-schema-traverse/0.4.1: resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} dev: true @@ -14852,6 +14881,10 @@ packages: convert-source-map: 1.9.0 dev: true + /valid-url/1.0.9: + resolution: {integrity: sha512-QQDsV8OnSf5Uc30CKSwG9lnhMPe6exHtTXLRYX8uMwKENy640pU+2BgBL0LRbDh/eYRahNCS7aewCx0wf3NYVA==} + dev: false + /validate-npm-package-license/3.0.4: resolution: {integrity: sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==} dependencies: