Browse Source

Merge branch 'main' of https://github.com/thesanjeevsharma/ghostfolio into pr/2408

pull/2408/head
Thomas 2 years ago
parent
commit
cbf2997214
  1. 9
      CHANGELOG.md
  2. 85
      apps/api/src/app/benchmark/benchmark.controller.ts
  3. 37
      apps/api/src/app/benchmark/benchmark.service.ts
  4. 6
      apps/api/src/app/order/order.controller.ts
  5. 6
      apps/api/src/app/order/order.service.ts
  6. 24
      apps/client/project.json
  7. 21
      apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.component.ts
  8. 18
      apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html
  9. 2
      apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.module.ts
  10. 2
      apps/client/src/app/components/admin-overview/admin-overview.component.ts
  11. 4
      apps/client/src/app/components/admin-overview/admin-overview.html
  12. 4
      apps/client/src/app/components/admin-platform/admin-platform.component.ts
  13. 5
      apps/client/src/app/components/admin-tag/admin-tag.component.ts
  14. 2
      apps/client/src/app/pages/user-account/user-account-page.component.ts
  15. 19
      apps/client/src/app/services/data.service.ts
  16. 1
      package.json
  17. 10
      yarn.lock

9
CHANGELOG.md

@ -10,6 +10,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added ### Added
- Supported enter key press to submit the form of the create or update account dialog - Supported enter key press to submit the form of the create or update account dialog
- Added the version to the admin control panel
- Added pagination parameters (`skip`, `take`) to the endpoint `GET api/v1/order`
### Changed
- Harmonized the settings icon of the user account page
- Improved the usability to set an asset profile as a benchmark
- Reload platforms after making a change in the admin control panel
- Reload tags after making a change in the admin control panel
### Fixed ### Fixed

85
apps/api/src/app/benchmark/benchmark.controller.ts

@ -10,6 +10,7 @@ import type { RequestWithUser } from '@ghostfolio/common/types';
import { import {
Body, Body,
Controller, Controller,
Delete,
Get, Get,
HttpException, HttpException,
Inject, Inject,
@ -32,35 +33,49 @@ export class BenchmarkController {
@Inject(REQUEST) private readonly request: RequestWithUser @Inject(REQUEST) private readonly request: RequestWithUser
) {} ) {}
@Get() @Post()
@UseInterceptors(TransformDataSourceInRequestInterceptor)
@UseInterceptors(TransformDataSourceInResponseInterceptor)
public async getBenchmark(): Promise<BenchmarkResponse> {
return {
benchmarks: await this.benchmarkService.getBenchmarks()
};
}
@Get(':dataSource/:symbol/:startDateString')
@UseGuards(AuthGuard('jwt')) @UseGuards(AuthGuard('jwt'))
@UseInterceptors(TransformDataSourceInRequestInterceptor) public async addBenchmark(@Body() { dataSource, symbol }: UniqueAsset) {
public async getBenchmarkMarketDataBySymbol( if (
@Param('dataSource') dataSource: DataSource, !hasPermission(
@Param('startDateString') startDateString: string, this.request.user.permissions,
@Param('symbol') symbol: string permissions.accessAdminControl
): Promise<BenchmarkMarketDataDetails> { )
const startDate = new Date(startDateString); ) {
throw new HttpException(
getReasonPhrase(StatusCodes.FORBIDDEN),
StatusCodes.FORBIDDEN
);
}
return this.benchmarkService.getMarketDataBySymbol({ try {
const benchmark = await this.benchmarkService.addBenchmark({
dataSource, dataSource,
startDate,
symbol symbol
}); });
if (!benchmark) {
throw new HttpException(
getReasonPhrase(StatusCodes.NOT_FOUND),
StatusCodes.NOT_FOUND
);
} }
@Post() return benchmark;
} catch {
throw new HttpException(
getReasonPhrase(StatusCodes.INTERNAL_SERVER_ERROR),
StatusCodes.INTERNAL_SERVER_ERROR
);
}
}
@Delete(':dataSource/:symbol')
@UseGuards(AuthGuard('jwt')) @UseGuards(AuthGuard('jwt'))
public async addBenchmark(@Body() { dataSource, symbol }: UniqueAsset) { public async deleteBenchmark(
@Param('dataSource') dataSource: DataSource,
@Param('symbol') symbol: string
) {
if ( if (
!hasPermission( !hasPermission(
this.request.user.permissions, this.request.user.permissions,
@ -74,7 +89,7 @@ export class BenchmarkController {
} }
try { try {
const benchmark = await this.benchmarkService.addBenchmark({ const benchmark = await this.benchmarkService.deleteBenchmark({
dataSource, dataSource,
symbol symbol
}); });
@ -94,4 +109,30 @@ export class BenchmarkController {
); );
} }
} }
@Get()
@UseInterceptors(TransformDataSourceInRequestInterceptor)
@UseInterceptors(TransformDataSourceInResponseInterceptor)
public async getBenchmark(): Promise<BenchmarkResponse> {
return {
benchmarks: await this.benchmarkService.getBenchmarks()
};
}
@Get(':dataSource/:symbol/:startDateString')
@UseGuards(AuthGuard('jwt'))
@UseInterceptors(TransformDataSourceInRequestInterceptor)
public async getBenchmarkMarketDataBySymbol(
@Param('dataSource') dataSource: DataSource,
@Param('startDateString') startDateString: string,
@Param('symbol') symbol: string
): Promise<BenchmarkMarketDataDetails> {
const startDate = new Date(startDateString);
return this.benchmarkService.getMarketDataBySymbol({
dataSource,
startDate,
symbol
});
}
} }

37
apps/api/src/app/benchmark/benchmark.service.ts

@ -245,6 +245,43 @@ export class BenchmarkService {
}; };
} }
public async deleteBenchmark({
dataSource,
symbol
}: UniqueAsset): Promise<Partial<SymbolProfile>> {
const assetProfile = await this.prismaService.symbolProfile.findFirst({
where: {
dataSource,
symbol
}
});
if (!assetProfile) {
return null;
}
let benchmarks =
((await this.propertyService.getByKey(
PROPERTY_BENCHMARKS
)) as BenchmarkProperty[]) ?? [];
benchmarks = benchmarks.filter(({ symbolProfileId }) => {
return symbolProfileId !== assetProfile.id;
});
await this.propertyService.put({
key: PROPERTY_BENCHMARKS,
value: JSON.stringify(benchmarks)
});
return {
dataSource,
symbol,
id: assetProfile.id,
name: assetProfile.name
};
}
private getMarketCondition(aPerformanceInPercent: number) { private getMarketCondition(aPerformanceInPercent: number) {
return aPerformanceInPercent <= -0.2 ? 'BEAR_MARKET' : 'NEUTRAL_MARKET'; return aPerformanceInPercent <= -0.2 ? 'BEAR_MARKET' : 'NEUTRAL_MARKET';
} }

6
apps/api/src/app/order/order.controller.ts

@ -89,7 +89,9 @@ export class OrderController {
@Headers(HEADER_KEY_IMPERSONATION.toLowerCase()) impersonationId, @Headers(HEADER_KEY_IMPERSONATION.toLowerCase()) impersonationId,
@Query('accounts') filterByAccounts?: string, @Query('accounts') filterByAccounts?: string,
@Query('assetClasses') filterByAssetClasses?: string, @Query('assetClasses') filterByAssetClasses?: string,
@Query('tags') filterByTags?: string @Query('skip') skip?: number,
@Query('tags') filterByTags?: string,
@Query('take') take?: number
): Promise<Activities> { ): Promise<Activities> {
const filters = this.apiService.buildFiltersFromQueryParams({ const filters = this.apiService.buildFiltersFromQueryParams({
filterByAccounts, filterByAccounts,
@ -105,6 +107,8 @@ export class OrderController {
filters, filters,
userCurrency, userCurrency,
includeDrafts: true, includeDrafts: true,
skip: isNaN(skip) ? undefined : skip,
take: isNaN(take) ? undefined : take,
userId: impersonationUserId || this.request.user.id, userId: impersonationUserId || this.request.user.id,
withExcludedAccounts: true withExcludedAccounts: true
}); });

6
apps/api/src/app/order/order.service.ts

@ -230,6 +230,8 @@ export class OrderService {
public async getOrders({ public async getOrders({
filters, filters,
includeDrafts = false, includeDrafts = false,
skip,
take = Number.MAX_SAFE_INTEGER,
types, types,
userCurrency, userCurrency,
userId, userId,
@ -237,6 +239,8 @@ export class OrderService {
}: { }: {
filters?: Filter[]; filters?: Filter[];
includeDrafts?: boolean; includeDrafts?: boolean;
skip?: number;
take?: number;
types?: TypeOfOrder[]; types?: TypeOfOrder[];
userCurrency: string; userCurrency: string;
userId: string; userId: string;
@ -315,6 +319,8 @@ export class OrderService {
return ( return (
await this.orders({ await this.orders({
skip,
take,
where, where,
include: { include: {
// eslint-disable-next-line @typescript-eslint/naming-convention // eslint-disable-next-line @typescript-eslint/naming-convention

24
apps/client/project.json

@ -104,40 +104,40 @@
"options": { "options": {
"commands": [ "commands": [
{ {
"command": "mkdir -p dist/apps/client" "command": "shx mkdir -p dist/apps/client"
}, },
{ {
"command": "cp -r apps/client/src/assets dist/apps/client" "command": "shx cp -r apps/client/src/assets dist/apps/client"
}, },
{ {
"command": "cp -r apps/client/src/assets/.well-known dist/apps/client" "command": "shx cp -r apps/client/src/assets/.well-known dist/apps/client"
}, },
{ {
"command": "cp apps/client/src/assets/favicon.ico dist/apps/client" "command": "shx cp apps/client/src/assets/favicon.ico dist/apps/client"
}, },
{ {
"command": "cp apps/client/src/assets/index.html dist/apps/client" "command": "shx cp apps/client/src/assets/index.html dist/apps/client"
}, },
{ {
"command": "cp apps/client/src/assets/robots.txt dist/apps/client" "command": "shx cp apps/client/src/assets/robots.txt dist/apps/client"
}, },
{ {
"command": "cp apps/client/src/assets/site.webmanifest dist/apps/client" "command": "shx cp apps/client/src/assets/site.webmanifest dist/apps/client"
}, },
{ {
"command": "cp node_modules/ionicons/dist/index.js dist/apps/client" "command": "shx cp node_modules/ionicons/dist/index.js dist/apps/client"
}, },
{ {
"command": "cp node_modules/ionicons/dist/ionicons.js dist/apps/client" "command": "shx cp node_modules/ionicons/dist/ionicons.js dist/apps/client"
}, },
{ {
"command": "cp -r node_modules/ionicons/dist/ionicons dist/apps/client/ionicons" "command": "shx cp -r node_modules/ionicons/dist/ionicons dist/apps/client/ionicons"
}, },
{ {
"command": "cp CHANGELOG.md dist/apps/client/assets" "command": "shx cp CHANGELOG.md dist/apps/client/assets"
}, },
{ {
"command": "cp LICENSE dist/apps/client/assets" "command": "shx cp LICENSE dist/apps/client/assets"
} }
] ]
} }

21
apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.component.ts

@ -146,9 +146,11 @@ export class AssetProfileDialog implements OnDestroy, OnInit {
.postBenchmark({ dataSource, symbol }) .postBenchmark({ dataSource, symbol })
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntil(this.unsubscribeSubject))
.subscribe(() => { .subscribe(() => {
setTimeout(() => { this.dataService.updateInfo();
window.location.reload();
}, 300); this.isBenchmark = true;
this.changeDetectorRef.markForCheck();
}); });
} }
@ -185,6 +187,19 @@ export class AssetProfileDialog implements OnDestroy, OnInit {
}); });
} }
public onUnsetBenchmark({ dataSource, symbol }: UniqueAsset) {
this.dataService
.deleteBenchmark({ dataSource, symbol })
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe(() => {
this.dataService.updateInfo();
this.isBenchmark = false;
this.changeDetectorRef.markForCheck();
});
}
public ngOnDestroy() { public ngOnDestroy() {
this.unsubscribeSubject.next(); this.unsubscribeSubject.next();
this.unsubscribeSubject.complete(); this.unsubscribeSubject.complete();

18
apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html

@ -37,13 +37,6 @@
> >
<ng-container i18n>Gather Profile Data</ng-container> <ng-container i18n>Gather Profile Data</ng-container>
</button> </button>
<button
mat-menu-item
[disabled]="isBenchmark"
(click)="onSetBenchmark({dataSource: data.dataSource, symbol: data.symbol})"
>
<ng-container i18n>Set as Benchmark</ng-container>
</button>
</mat-menu> </mat-menu>
</div> </div>
@ -151,6 +144,17 @@
</ng-template> </ng-template>
</ng-container> </ng-container>
</div> </div>
<div class="d-flex my-3">
<div class="w-50">
<mat-checkbox
color="primary"
i18n
[checked]="isBenchmark"
(change)="isBenchmark ? onUnsetBenchmark({dataSource: data.dataSource, symbol: data.symbol}) : onSetBenchmark({dataSource: data.dataSource, symbol: data.symbol})"
>Benchmark</mat-checkbox
>
</div>
</div>
<div class="mt-3"> <div class="mt-3">
<mat-form-field appearance="outline" class="w-100"> <mat-form-field appearance="outline" class="w-100">
<mat-label i18n>Symbol Mapping</mat-label> <mat-label i18n>Symbol Mapping</mat-label>

2
apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.module.ts

@ -3,6 +3,7 @@ import { CommonModule } from '@angular/common';
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core'; import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { MatCheckboxModule } from '@angular/material/checkbox';
import { MatDialogModule } from '@angular/material/dialog'; import { MatDialogModule } from '@angular/material/dialog';
import { MatInputModule } from '@angular/material/input'; import { MatInputModule } from '@angular/material/input';
import { MatMenuModule } from '@angular/material/menu'; import { MatMenuModule } from '@angular/material/menu';
@ -21,6 +22,7 @@ import { AssetProfileDialog } from './asset-profile-dialog.component';
GfPortfolioProportionChartModule, GfPortfolioProportionChartModule,
GfValueModule, GfValueModule,
MatButtonModule, MatButtonModule,
MatCheckboxModule,
MatDialogModule, MatDialogModule,
MatInputModule, MatInputModule,
MatMenuModule, MatMenuModule,

2
apps/client/src/app/components/admin-overview/admin-overview.component.ts

@ -1,5 +1,6 @@
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core'; import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
import { MatCheckboxChange } from '@angular/material/checkbox'; import { MatCheckboxChange } from '@angular/material/checkbox';
import { environment } from '@ghostfolio/client/../environments/environment';
import { AdminService } from '@ghostfolio/client/services/admin.service'; import { AdminService } from '@ghostfolio/client/services/admin.service';
import { CacheService } from '@ghostfolio/client/services/cache.service'; import { CacheService } from '@ghostfolio/client/services/cache.service';
import { DataService } from '@ghostfolio/client/services/data.service'; import { DataService } from '@ghostfolio/client/services/data.service';
@ -42,6 +43,7 @@ export class AdminOverviewComponent implements OnDestroy, OnInit {
public transactionCount: number; public transactionCount: number;
public userCount: number; public userCount: number;
public user: User; public user: User;
public version = environment.version;
private unsubscribeSubject = new Subject<void>(); private unsubscribeSubject = new Subject<void>();

4
apps/client/src/app/components/admin-overview/admin-overview.html

@ -3,6 +3,10 @@
<div class="col"> <div class="col">
<mat-card appearance="outlined" class="mb-3"> <mat-card appearance="outlined" class="mb-3">
<mat-card-content> <mat-card-content>
<div class="d-flex my-3">
<div class="w-50" i18n>Version</div>
<div class="w-50">{{ version }}</div>
</div>
<div class="d-flex my-3"> <div class="d-flex my-3">
<div class="w-50" i18n>User Count</div> <div class="w-50" i18n>User Count</div>
<div class="w-50"> <div class="w-50">

4
apps/client/src/app/components/admin-platform/admin-platform.component.ts

@ -13,6 +13,7 @@ import { ActivatedRoute, Router } from '@angular/router';
import { CreatePlatformDto } from '@ghostfolio/api/app/platform/create-platform.dto'; import { CreatePlatformDto } from '@ghostfolio/api/app/platform/create-platform.dto';
import { UpdatePlatformDto } from '@ghostfolio/api/app/platform/update-platform.dto'; import { UpdatePlatformDto } from '@ghostfolio/api/app/platform/update-platform.dto';
import { AdminService } from '@ghostfolio/client/services/admin.service'; import { AdminService } from '@ghostfolio/client/services/admin.service';
import { DataService } from '@ghostfolio/client/services/data.service';
import { UserService } from '@ghostfolio/client/services/user/user.service'; import { UserService } from '@ghostfolio/client/services/user/user.service';
import { Platform } from '@prisma/client'; import { Platform } from '@prisma/client';
import { get } from 'lodash'; import { get } from 'lodash';
@ -40,6 +41,7 @@ export class AdminPlatformComponent implements OnInit, OnDestroy {
public constructor( public constructor(
private adminService: AdminService, private adminService: AdminService,
private changeDetectorRef: ChangeDetectorRef, private changeDetectorRef: ChangeDetectorRef,
private dataService: DataService,
private deviceService: DeviceDetectorService, private deviceService: DeviceDetectorService,
private dialog: MatDialog, private dialog: MatDialog,
private route: ActivatedRoute, private route: ActivatedRoute,
@ -119,6 +121,8 @@ export class AdminPlatformComponent implements OnInit, OnDestroy {
this.dataSource.sort = this.sort; this.dataSource.sort = this.sort;
this.dataSource.sortingDataAccessor = get; this.dataSource.sortingDataAccessor = get;
this.dataService.updateInfo();
this.changeDetectorRef.markForCheck(); this.changeDetectorRef.markForCheck();
}); });
} }

5
apps/client/src/app/components/admin-tag/admin-tag.component.ts

@ -13,6 +13,7 @@ import { ActivatedRoute, Router } from '@angular/router';
import { CreateTagDto } from '@ghostfolio/api/app/tag/create-tag.dto'; import { CreateTagDto } from '@ghostfolio/api/app/tag/create-tag.dto';
import { UpdateTagDto } from '@ghostfolio/api/app/tag/update-tag.dto'; import { UpdateTagDto } from '@ghostfolio/api/app/tag/update-tag.dto';
import { AdminService } from '@ghostfolio/client/services/admin.service'; import { AdminService } from '@ghostfolio/client/services/admin.service';
import { DataService } from '@ghostfolio/client/services/data.service';
import { UserService } from '@ghostfolio/client/services/user/user.service'; import { UserService } from '@ghostfolio/client/services/user/user.service';
import { Tag } from '@prisma/client'; import { Tag } from '@prisma/client';
import { get } from 'lodash'; import { get } from 'lodash';
@ -40,6 +41,7 @@ export class AdminTagComponent implements OnInit, OnDestroy {
public constructor( public constructor(
private adminService: AdminService, private adminService: AdminService,
private changeDetectorRef: ChangeDetectorRef, private changeDetectorRef: ChangeDetectorRef,
private dataService: DataService,
private deviceService: DeviceDetectorService, private deviceService: DeviceDetectorService,
private dialog: MatDialog, private dialog: MatDialog,
private route: ActivatedRoute, private route: ActivatedRoute,
@ -114,10 +116,13 @@ export class AdminTagComponent implements OnInit, OnDestroy {
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntil(this.unsubscribeSubject))
.subscribe((tags) => { .subscribe((tags) => {
this.tags = tags; this.tags = tags;
this.dataSource = new MatTableDataSource(this.tags); this.dataSource = new MatTableDataSource(this.tags);
this.dataSource.sort = this.sort; this.dataSource.sort = this.sort;
this.dataSource.sortingDataAccessor = get; this.dataSource.sortingDataAccessor = get;
this.dataService.updateInfo();
this.changeDetectorRef.markForCheck(); this.changeDetectorRef.markForCheck();
}); });
} }

2
apps/client/src/app/pages/user-account/user-account-page.component.ts

@ -30,7 +30,7 @@ export class UserAccountPageComponent implements OnDestroy, OnInit {
this.tabs = [ this.tabs = [
{ {
iconName: 'cog-outline', iconName: 'settings-outline',
label: $localize`Settings`, label: $localize`Settings`,
path: ['/account'] path: ['/account']
}, },

19
apps/client/src/app/services/data.service.ts

@ -204,6 +204,10 @@ export class DataService {
return this.http.delete<any>(`/api/v1/order/`); return this.http.delete<any>(`/api/v1/order/`);
} }
public deleteBenchmark({ dataSource, symbol }: UniqueAsset) {
return this.http.delete<any>(`/api/v1/benchmark/${dataSource}/${symbol}`);
}
public deleteOrder(aId: string) { public deleteOrder(aId: string) {
return this.http.delete<any>(`/api/v1/order/${aId}`); return this.http.delete<any>(`/api/v1/order/${aId}`);
} }
@ -496,4 +500,19 @@ export class DataService {
couponCode couponCode
}); });
} }
public updateInfo() {
this.http.get<InfoItem>('/api/v1/info').subscribe((info) => {
const utmSource = <'ios' | 'trusted-web-activity'>(
window.localStorage.getItem('utm_source')
);
info.globalPermissions = filterGlobalPermissions(
info.globalPermissions,
utmSource
);
(window as any).info = info;
});
}
} }

1
package.json

@ -194,6 +194,7 @@
"react": "18.2.0", "react": "18.2.0",
"react-dom": "18.2.0", "react-dom": "18.2.0",
"replace-in-file": "7.0.1", "replace-in-file": "7.0.1",
"shx": "0.3.4",
"storybook": "7.0.9", "storybook": "7.0.9",
"ts-jest": "29.1.0", "ts-jest": "29.1.0",
"ts-node": "10.9.1", "ts-node": "10.9.1",

10
yarn.lock

@ -14264,7 +14264,7 @@ minimatch@^9.0.0, minimatch@^9.0.1:
dependencies: dependencies:
brace-expansion "^2.0.1" brace-expansion "^2.0.1"
minimist@^1.2.0, minimist@^1.2.5, minimist@^1.2.6: minimist@^1.2.0, minimist@^1.2.3, minimist@^1.2.5, minimist@^1.2.6:
version "1.2.8" version "1.2.8"
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c"
integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==
@ -16984,6 +16984,14 @@ shelljs@^0.8.5:
interpret "^1.0.0" interpret "^1.0.0"
rechoir "^0.6.2" rechoir "^0.6.2"
shx@0.3.4:
version "0.3.4"
resolved "https://registry.yarnpkg.com/shx/-/shx-0.3.4.tgz#74289230b4b663979167f94e1935901406e40f02"
integrity sha512-N6A9MLVqjxZYcVn8hLmtneQWIJtp8IKzMP4eMnx+nqkvXoqinUPCbUFLp2UcWTEIUONhlk0ewxr/jaVGlc+J+g==
dependencies:
minimist "^1.2.3"
shelljs "^0.8.5"
side-channel@^1.0.4: side-channel@^1.0.4:
version "1.0.4" version "1.0.4"
resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.4.tgz#efce5c8fdc104ee751b25c58d4290011fa5ea2cf" resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.4.tgz#efce5c8fdc104ee751b25c58d4290011fa5ea2cf"

Loading…
Cancel
Save