Back to nextjs

This commit is contained in:
2021-07-31 17:47:08 -04:00
parent edbfd8bb9e
commit b22eb01ca9
61 changed files with 18390 additions and 2373 deletions

View File

@@ -65,86 +65,38 @@
}
},
"client": {
"projectType": "application",
"root": "apps/client",
"sourceRoot": "apps/client/src",
"prefix": "vidgrab2",
"sourceRoot": "apps/client",
"projectType": "application",
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:browser",
"builder": "@nrwl/next:build",
"outputs": ["{options.outputPath}"],
"options": {
"outputPath": "dist/apps/client",
"index": "apps/client/src/index.html",
"main": "apps/client/src/main.ts",
"polyfills": "apps/client/src/polyfills.ts",
"tsConfig": "apps/client/tsconfig.app.json",
"inlineStyleLanguage": "scss",
"assets": ["apps/client/src/favicon.ico", "apps/client/src/assets"],
"styles": ["apps/client/src/styles.scss"],
"scripts": []
"root": "apps/client",
"outputPath": "dist/apps/client"
},
"configurations": {
"production": {
"budgets": [
{
"type": "initial",
"maximumWarning": "500kb",
"maximumError": "1mb"
},
{
"type": "anyComponentStyle",
"maximumWarning": "2kb",
"maximumError": "4kb"
"production": {}
}
],
"fileReplacements": [
{
"replace": "apps/client/src/environments/environment.ts",
"with": "apps/client/src/environments/environment.prod.ts"
}
],
"outputHashing": "all"
},
"development": {
"buildOptimizer": false,
"optimization": false,
"vendorChunk": true,
"extractLicenses": false,
"sourceMap": true,
"namedChunks": true
}
},
"defaultConfiguration": "production"
},
"serve": {
"builder": "@angular-devkit/build-angular:dev-server",
"builder": "@nrwl/next:server",
"options": {
"buildTarget": "client:build",
"dev": true
},
"configurations": {
"production": {
"browserTarget": "client:build:production"
},
"development": {
"browserTarget": "client:build:development"
"buildTarget": "client:build:production",
"dev": false
}
}
},
"defaultConfiguration": "development",
"export": {
"builder": "@nrwl/next:export",
"options": {
"proxyConfig": "apps/client/proxy.conf.json"
}
},
"extract-i18n": {
"builder": "@angular-devkit/build-angular:extract-i18n",
"options": {
"browserTarget": "client:build"
}
},
"lint": {
"builder": "@nrwl/linter:eslint",
"options": {
"lintFilePatterns": [
"apps/client/src/**/*.ts",
"apps/client/src/**/*.html"
]
"buildTarget": "client:build:production"
}
},
"test": {
@@ -154,31 +106,11 @@
"jestConfig": "apps/client/jest.config.js",
"passWithNoTests": true
}
}
}
},
"client-e2e": {
"root": "apps/client-e2e",
"sourceRoot": "apps/client-e2e/src",
"projectType": "application",
"architect": {
"e2e": {
"builder": "@nrwl/cypress:cypress",
"options": {
"cypressConfig": "apps/client-e2e/cypress.json",
"tsConfig": "apps/client-e2e/tsconfig.e2e.json",
"devServerTarget": "client:serve:development"
},
"configurations": {
"production": {
"devServerTarget": "client:serve:production"
}
}
},
"lint": {
"builder": "@nrwl/linter:eslint",
"options": {
"lintFilePatterns": ["apps/client-e2e/**/*.{js,ts}"]
"lintFilePatterns": ["apps/client/**/*.{ts,tsx,js,jsx}"]
}
}
}
@@ -212,6 +144,17 @@
},
"@nrwl/angular:component": {
"style": "scss"
},
"@nrwl/react": {
"application": {
"babel": true
}
},
"@nrwl/next": {
"application": {
"style": "css",
"linter": "eslint"
}
}
},
"defaultProject": "client"

View File

@@ -5,17 +5,19 @@ import { ConfigModule, ConfigService } from '@nestjs/config';
import { EventEmitterModule } from '@nestjs/event-emitter';
import { BullModule } from '@nestjs/bull';
import configuration from '../config/configuration';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { WebModule } from '../web/web.module';
import { YtdlModule } from '../ytdl/ytdl.module';
import { JobGateway } from './job.gateway';
@Module({
imports: [
ConfigModule.forRoot({
ignoreEnvFile: true,
isGlobal: true,
// load: [configuration],
load: [configuration],
}),
EventEmitterModule.forRoot({
wildcard: true,
@@ -38,6 +40,6 @@ import { YtdlModule } from '../ytdl/ytdl.module';
WebModule,
],
controllers: [AppController],
providers: [AppService],
providers: [AppService, JobGateway],
})
export class AppModule {}

View File

@@ -0,0 +1,74 @@
import { Logger } from '@nestjs/common';
import { OnEvent } from '@nestjs/event-emitter';
import {
OnGatewayConnection,
OnGatewayDisconnect,
WebSocketGateway,
WebSocketServer,
} from '@nestjs/websockets';
import { pick } from 'lodash';
import { Server, Socket } from 'socket.io';
import { JobEvent } from '@vidgrab2/api-interfaces';
@WebSocketGateway()
export class JobGateway implements OnGatewayConnection, OnGatewayDisconnect {
private readonly logger = new Logger(JobGateway.name);
@WebSocketServer()
private server: Server | undefined;
private clients: Socket[] = [];
handleConnection(client: Socket) {
this.clients.push(client);
this.logger.verbose('handleConnection', client);
}
handleDisconnect(client: Socket) {
for (let i = 0; i < this.clients.length; i++) {
if (this.clients[i] === client) {
this.clients.splice(i, 1);
break;
}
}
this.logger.verbose('handleDisconnect', client);
}
private broadcast(event: string, message: any) {
const broadCastMessage = JSON.stringify(message);
for (const c of this.clients) {
c.send(event, broadCastMessage);
}
}
@OnEvent('job.added')
async sendAddedJob(payload: JobEvent) {
if (this.server && payload.job) {
const state = await payload.job.getState();
const progress = payload.job.progress();
payload.job = {
...pick(payload.job, ['id', 'name', 'data']),
progress: progress ? `${progress}%` : 'n/a',
state: state,
};
this.server.emit('job.added', payload);
}
}
@OnEvent('job.updated')
async sendJobUpdate(payload: JobEvent) {
if (this.server && payload.job) {
const state = await payload.job.getState();
const progress = payload.job.progress();
payload.job = {
...pick(payload.job, ['id', 'name', 'data']),
progress: progress ? `${progress}%` : 'n/a',
state: state,
};
this.server.emit('job.updated', payload);
}
}
}

View File

@@ -0,0 +1,7 @@
export default () => ({
redisHost: process.env.REDIS_HOST || 'localhost',
redisPort: process.env.REDIS_PORT
? parseInt(process.env.REDIS_PORT, 10)
: 6379,
fileDir: process.env.FILE_DIR || '/tmp',
});

View File

@@ -1,13 +1,15 @@
import { InjectQueue } from '@nestjs/bull';
import {
Body,
CacheInterceptor,
CacheTTL,
Controller,
Get,
HttpCode,
Logger,
Post,
Render,
Req,
UseInterceptors,
UsePipes,
ValidationPipe,
} from '@nestjs/common';
@@ -28,8 +30,9 @@ export class WebController {
private readonly eventEmitter: EventEmitter2,
) {}
@Get()
@Render('Index')
@Get('/info')
// @Render('Index')
// eslint-disable-next-line @typescript-eslint/no-explicit-any
async root(@Req() request: any) {
const fullUrl =
request.protocol + '://' + request.get('host') + request.originalUrl;
@@ -75,6 +78,8 @@ export class WebController {
}
@Get('/extractors')
@UseInterceptors(CacheInterceptor)
@CacheTTL(600)
async listExtractors() {
const extractors = await this.ytdlService.listExtractors();

View File

@@ -1,5 +1,5 @@
import { BullModule } from '@nestjs/bull';
import { Module } from '@nestjs/common';
import { CacheModule, Module } from '@nestjs/common';
import { YtdlModule } from '../ytdl/ytdl.module';
import { WebController } from './web.controller';
@@ -9,6 +9,7 @@ import { WebController } from './web.controller';
BullModule.registerQueue({
name: 'vidgrab',
}),
CacheModule.register(),
YtdlModule,
],
controllers: [WebController],

View File

@@ -12,7 +12,7 @@ import { ConfigService } from '@nestjs/config';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { Job } from 'bull';
import { throttle } from 'lodash';
import split from 'split2';
import * as split from 'split2';
import { QueueDto } from '@vidgrab2/api-interfaces';
import { raw } from 'youtube-dl-exec';

View File

@@ -1,17 +0,0 @@
{
"extends": ["plugin:cypress/recommended", "../../.eslintrc.json"],
"ignorePatterns": ["!**/*"],
"overrides": [
{
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
"rules": {}
},
{
"files": ["src/plugins/index.js"],
"rules": {
"@typescript-eslint/no-var-requires": "off",
"no-undef": "off"
}
}
]
}

View File

@@ -1,12 +0,0 @@
{
"fileServerFolder": ".",
"fixturesFolder": "./src/fixtures",
"integrationFolder": "./src/integration",
"modifyObstructiveCode": false,
"pluginsFile": "./src/plugins/index",
"supportFile": "./src/support/index.ts",
"video": true,
"videosFolder": "../../dist/cypress/apps/client-e2e/videos",
"screenshotsFolder": "../../dist/cypress/apps/client-e2e/screenshots",
"chromeWebSecurity": false
}

View File

@@ -1,4 +0,0 @@
{
"name": "Using fixtures to represent data",
"email": "hello@cypress.io"
}

View File

@@ -1,13 +0,0 @@
import { getGreeting } from '../support/app.po';
describe('client', () => {
beforeEach(() => cy.visit('/'));
it('should display welcome message', () => {
// Custom command example, see `../support/commands.ts` file
cy.login('my-email@something.com', 'myPassword');
// Function helper example, see `../support/app.po.ts` file
getGreeting().contains('Welcome to client!');
});
});

View File

@@ -1,22 +0,0 @@
// ***********************************************************
// This example plugins/index.js can be used to load plugins
//
// You can change the location of this file or turn off loading
// the plugins file with the 'pluginsFile' configuration option.
//
// You can read more here:
// https://on.cypress.io/plugins-guide
// ***********************************************************
// This function is called when a project is opened or re-opened (e.g. due to
// the project's config changing)
const { preprocessTypescript } = require('@nrwl/cypress/plugins/preprocessor');
module.exports = (on, config) => {
// `on` is used to hook into various events Cypress emits
// `config` is the resolved Cypress config
// Preprocess Typescript file using Nx helper
on('file:preprocessor', preprocessTypescript(config));
};

View File

@@ -1 +0,0 @@
export const getGreeting = () => cy.get('h1');

View File

@@ -1,33 +0,0 @@
// ***********************************************
// This example commands.js shows you how to
// create various custom commands and overwrite
// existing commands.
//
// For more comprehensive examples of custom
// commands please read more here:
// https://on.cypress.io/custom-commands
// ***********************************************
// eslint-disable-next-line @typescript-eslint/no-namespace
declare namespace Cypress {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
interface Chainable<Subject> {
login(email: string, password: string): void;
}
}
//
// -- This is a parent command --
Cypress.Commands.add('login', (email, password) => {
console.log('Custom command example: Login', email, password);
});
//
// -- This is a child command --
// Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... })
//
//
// -- This is a dual command --
// Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... })
//
//
// -- This will overwrite an existing command --
// Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... })

View File

@@ -1,17 +0,0 @@
// ***********************************************************
// This example support/index.js is processed and
// loaded automatically before your test files.
//
// This is a great place to put global configuration and
// behavior that modifies Cypress.
//
// You can change the location of this file or turn off
// automatically serving support files with the
// 'supportFile' configuration option.
//
// You can read more here:
// https://on.cypress.io/configuration
// ***********************************************************
// Import commands.js using ES2015 syntax:
import './commands';

View File

@@ -1,19 +0,0 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"sourceMap": false,
"outDir": "../../dist/out-tsc",
"allowJs": true,
"types": ["cypress", "node"],
"forceConsistentCasingInFileNames": true,
"strict": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src/**/*.ts", "src/**/*.js"],
"angularCompilerOptions": {
"strictInjectionParameters": true,
"strictInputAccessModifiers": true,
"strictTemplates": true
}
}

View File

@@ -1,10 +0,0 @@
{
"extends": "../../tsconfig.base.json",
"files": [],
"include": [],
"references": [
{
"path": "./tsconfig.e2e.json"
}
]
}

4
apps/client/.babelrc Normal file
View File

@@ -0,0 +1,4 @@
{
"presets": ["@nrwl/next/babel"],
"plugins": []
}

View File

@@ -1,17 +0,0 @@
# This file is used by the build system to adjust CSS and JS output to support the specified browsers below.
# For additional information regarding the format and rule options, please see:
# https://github.com/browserslist/browserslist#queries
# For the full list of supported browsers by the Angular framework, please see:
# https://angular.io/guide/browser-support
# You can see what browsers were selected by your queries by running:
# npx browserslist
last 1 Chrome version
last 1 Firefox version
last 2 Edge major versions
last 2 Safari major versions
last 2 iOS major versions
Firefox ESR
not IE 11 # Angular supports IE 11 only as an opt-in. To opt-in, remove the 'not' prefix on this line.

View File

@@ -1,36 +1,26 @@
{
"extends": ["../../.eslintrc.json"],
"extends": [
"plugin:@nrwl/nx/react-typescript",
"../../.eslintrc.json",
"next",
"next/core-web-vitals"
],
"ignorePatterns": ["!**/*"],
"overrides": [
{
"files": ["*.ts"],
"extends": [
"plugin:@nrwl/nx/angular",
"plugin:@angular-eslint/template/process-inline-templates"
],
"rules": {
"@angular-eslint/directive-selector": [
"error",
{
"type": "attribute",
"prefix": "vidgrab2",
"style": "camelCase"
}
],
"@angular-eslint/component-selector": [
"error",
{
"type": "element",
"prefix": "vidgrab2",
"style": "kebab-case"
}
]
}
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
"rules": {}
},
{
"files": ["*.html"],
"extends": ["plugin:@nrwl/nx/angular-template"],
"files": ["*.ts", "*.tsx"],
"rules": {}
},
{
"files": ["*.js", "*.jsx"],
"rules": {}
}
]
],
"env": {
"jest": true
}
}

View File

@@ -0,0 +1,50 @@
import React from 'react';
import './jobs.module.css';
interface Props {
jobs?: any[];
}
const Container: React.FC<Props> = ({ jobs }) => {
return (
<table className="table is-striped is-hoverable is-fullwidth is-narrow">
<thead>
<tr>
<th>ID</th>
<th>Status</th>
<th>
<abbr title="Service">Svc</abbr>
</th>
<th>Title</th>
<th>
<abbr title="Percent Downloaded">%</abbr>
</th>
</tr>
</thead>
<tbody>
{jobs &&
jobs.map((j) => (
<tr key={j.id}>
<td>{j.id}</td>
<td>{j.state}</td>
<td>{j.data.extractor}</td>
<td>
<a
href={j.data.url}
title={j.data.title}
target="_blank"
rel="noopener noreferrer"
>
{j.data.title}
</a>
</td>
<td>{j.progress}</td>
</tr>
))}
</tbody>
</table>
);
};
export default Container;

6
apps/client/index.d.ts vendored Normal file
View File

@@ -0,0 +1,6 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
declare module '*.svg' {
const content: any;
export const ReactComponent: any;
export default content;
}

View File

@@ -1,20 +1,10 @@
module.exports = {
displayName: 'client',
preset: '../../jest.preset.js',
setupFilesAfterEnv: ['<rootDir>/src/test-setup.ts'],
globals: {
'ts-jest': {
tsconfig: '<rootDir>/tsconfig.spec.json',
stringifyContentPathRegex: '\\.(html|svg)$',
},
},
coverageDirectory: '../../coverage/apps/client',
transform: {
'^.+\\.(ts|js|html)$': 'jest-preset-angular',
'^(?!.*\\.(js|jsx|ts|tsx|css|json)$)': '@nrwl/react/plugins/jest',
'^.+\\.[tj]sx?$': 'babel-jest',
},
snapshotSerializers: [
'jest-preset-angular/build/serializers/no-ng-attributes',
'jest-preset-angular/build/serializers/ng-snapshot',
'jest-preset-angular/build/serializers/html-comment',
],
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'],
coverageDirectory: '../../coverage/apps/client',
};

3
apps/client/next-env.d.ts vendored Normal file
View File

@@ -0,0 +1,3 @@
/// <reference types="next" />
/// <reference types="next/types/global" />
/// <reference types="next/image-types/global" />

View File

@@ -0,0 +1,15 @@
// eslint-disable-next-line @typescript-eslint/no-var-requires
const withNx = require('@nrwl/next/plugins/with-nx');
/**
* @type {import('@nrwl/next/plugins/with-nx').WithNxOptions}
**/
const nextConfig = {
nx: {
// Set this to false if you do not want to use SVGR
// See: https://github.com/gregberge/svgr
svgr: true,
},
};
module.exports = withNx(nextConfig);

View File

@@ -0,0 +1,6 @@
import '../styles/styles.css';
// This default export is required in a new `pages/_app.js` file.
export default function MyApp({ Component, pageProps }) {
return <Component {...pageProps} />;
}

View File

@@ -0,0 +1,48 @@
import Document, { Head, Html, Main, NextScript } from 'next/document';
class MyDocument extends Document {
static async getInitialProps(ctx) {
const initialProps = await Document.getInitialProps(ctx);
return { ...initialProps };
}
render() {
return (
<Html>
<Head>
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/bulma@0.9.3/css/bulma.min.css"
/>
</Head>
<body>
<div id="wrapper">
<section className="section has-background-light">
<div className="container">
<nav className="level">
<h1 className="title is-1 level-item has-text-centered">
Vidgrab
</h1>
</nav>
</div>
</section>
<Main />
</div>
<footer id="footer" className="footer">
<div className="content has-text-centered">
<p>
Code and &ldquo;Design&rdquo; by <a href="#">Ted Kulp</a>. Based
on youtube-dl.
</p>
</div>
</footer>
<NextScript />
</body>
</Html>
);
}
}
export default MyDocument;

View File

@@ -0,0 +1,41 @@
import { NextPage, NextPageContext } from 'next';
// The component's props type
type PageProps = {
extractors?: string[];
};
// extending the default next context type
type PageContext = NextPageContext & {
query: PageProps;
};
// react component
const Page: NextPage<PageProps> = ({ extractors }) => {
return (
<>
<section className="section">
<div className="container">
<div className="content">
<p>
Here is the list of currently supported services. This is
generated dynamically because youtube-dl adds new services all the
time.
</p>
<ul>{extractors && extractors.map((e) => <li key={e}>{e}</li>)}</ul>
</div>
</div>
</section>
</>
);
};
export async function getServerSideProps({ query }: PageContext) {
const res = await fetch('http://localhost:3333/api/extractors');
return {
props: await res?.json(),
};
}
export default Page;

View File

@@ -0,0 +1,94 @@
import { NextPage, NextPageContext, GetServerSideProps } from 'next';
import React, { useState } from 'react';
import getRawBody from 'raw-body';
import { YtResponse } from 'youtube-dl-exec';
// The component's props type
type PageProps = {
title: string;
extractor: string;
description: string;
videoUrl: string;
thumbnails: string;
upload_date: string;
duration: number;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
formats: any[];
};
// extending the default next context type
type PageContext = NextPageContext & {
query: PageProps;
};
// react component
const Page: NextPage<PageProps> = (props) => {
const [submitted, setSubmitted] = useState(false);
const onSubmit = (e: React.SyntheticEvent) => {
setSubmitted(true);
};
return (
<>
<section className="section">
<div className="container">
<form method="POST" action="/" onSubmit={onSubmit}>
<div className="field is-grouped">
<div className="control is-expanded">
<div className="select is-fullwidth">
<select name="format">
{props.formats &&
props.formats.map((f) => (
<option key={f.format_id} value={f.format_id}>
{f.format} ({f.ext})
</option>
))}
</select>
</div>
</div>
<div className="control">
<button
className={`button is-primary ${submitted?'is-loading':''}`}
id="form-submit"
type="submit"
>
Queue Download
</button>
</div>
</div>
<input type="hidden" name="url" value={props.videoUrl} />
<input type="hidden" name="title" value={props.title} />
<input type="hidden" name="extractor" value={props.extractor} />
</form>
</div>
</section>
</>
);
};
export const getServerSideProps: GetServerSideProps<YtResponse> = async ({ req }) => {
let data: YtResponse;
if (req.method == "POST") {
const body = await getRawBody(req);
const res = await fetch('http://localhost:3333/api/getinfo', {
method: 'post',
body: body.toString('utf-8'),
headers: {
'content-type': 'application/x-www-form-urlencoded',
},
});
data = await res.json();
}
return {
props: {
...data,
},
};
};
export default Page;

138
apps/client/pages/index.tsx Normal file
View File

@@ -0,0 +1,138 @@
import { NextPage, NextPageContext, GetServerSideProps } from 'next';
import Link from 'next/link';
import getRawBody from 'raw-body';
import React, { useEffect, useState } from 'react';
import Client from 'socket.io-client';
import Jobs from '../components/jobs/jobs';
// The component's props type
type PageProps = {
bookmarklet?: string;
jobs?: any[];
};
// extending the default next context type
type PageContext = NextPageContext & {
query: PageProps;
};
// react component
const Page: NextPage<PageProps> = ({ bookmarklet, jobs }) => {
const [_socket, setSocket] = useState<any>();
const [currentJobs, setCurrentJobs] = useState<any>(jobs);
const [submitted, setSubmitted] = useState(false);
const onSubmit = (e: React.SyntheticEvent) => {
setSubmitted(true);
};
useEffect(() => {
const newSocket = Client();
setSocket(newSocket);
newSocket.on('job.added', (payload: any) => {
setCurrentJobs((current: any[]) => {
return [payload.job, ...current.slice(0, -1)];
});
});
newSocket.on('job.updated', (payload: any) => {
setCurrentJobs((current: { id: any }[]) => {
const foundIndex = current.findIndex(
(cj: { id: any }) => cj.id == payload.job.id
);
if (foundIndex > -1) {
current[foundIndex] = payload.job;
}
return [...current];
});
});
return () => {
newSocket.disconnect();
setSocket(undefined);
};
}, []);
return (
<>
<section className="section">
<div className="container">
<form method="POST" action="/getinfo" onSubmit={onSubmit}>
<div className="field is-grouped">
<div className="control is-expanded">
<input
className="input is-medium"
name="url"
placeholder="URL to Download"
/>
<p className="help">
<Link href="/extractors">
* List of currently available services
</Link>
</p>
</div>
<div className="control">
<button
className={`button is-primary is-medium ${submitted?'is-loading':''}`}
id="form-submit"
type="submit"
>
Submit
</button>
</div>
</div>
</form>
</div>
</section>
<div className="container content has-text-centered">
<p>Drag this to your bookmark bar!</p>
<a
className="bookmarklet"
ref={(node) =>
node && bookmarklet && node.setAttribute('href', bookmarklet)
}
>
Vidgrab It!
</a>
</div>
<section className="section">
<div className="container">
<h2 className="subtitle is-4 has-text-centered">Recent Downloads</h2>
<Jobs jobs={currentJobs} />
</div>
</section>
</>
);
};
export const getServerSideProps: GetServerSideProps = async ({ req }) => {
let data;
if (req.method == "POST") {
const body = await getRawBody(req);
const res = await fetch('http://localhost:3333/api/queue', {
method: 'post',
body: body.toString('utf-8'),
headers: {
'content-type': 'application/x-www-form-urlencoded',
},
});
data = await res.json();
}
const res = await fetch('http://localhost:3333/api/info');
return {
props: await res.json(),
};
}
export default Page;

View File

@@ -2,5 +2,10 @@
"/api": {
"target": "http://localhost:3333",
"secure": false
},
"/socket.io": {
"target": "http://localhost:3333",
"secure": false,
"ws": true
}
}

View File

@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="262px" height="163px" viewBox="0 0 262 163" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="Styles-&amp;-Quick-Wins" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="Nx---Quick-Wins" transform="translate(-476.000000, -1284.000000)" fill-rule="nonzero">
<g id="Logos" transform="translate(-11.000000, 782.000000)">
<g id="Nx_Flat_White" transform="translate(487.000000, 502.000000)">
<polygon id="Path" fill="#FFFFFF" points="130.68 104.59 97.49 52.71 97.44 96.3 40.24 0 0 0 0 162.57 39.79 162.57 39.92 66.39 96.53 158.26"/>
<polygon id="Path" fill="#FFFFFF" points="97.5 41.79 137.24 41.79 137.33 41.33 137.33 0 97.54 0 97.49 41.33"/>
<path d="M198.66,86.86 C189.139872,86.6795216 180.538723,92.516445 177.19,101.43 C182.764789,93.0931021 193.379673,89.7432211 202.73,93.37 C207.05,95.13 212.73,97.97 217.23,96.45 C212.950306,90.4438814 206.034895,86.8725952 198.66,86.86 L198.66,86.86 Z" id="Path" fill="#96D8E9"/>
<path d="M243.75,106.42 C243.75,101.55 241.1,100.42 235.6,98.42 C231.52,97 226.89,95.4 223.52,91 C222.86,90.13 222.25,89.15 221.6,88.11 C220.14382,85.4164099 218.169266,83.037429 215.79,81.11 C212.58,78.75 208.37,77.6 202.91,77.6 C191.954261,77.6076705 182.084192,84.2206169 177.91,94.35 C183.186964,87.0278244 191.956716,83.0605026 200.940147,83.9314609 C209.923578,84.8024193 217.767888,90.3805017 221.54,98.58 C223.424615,101.689762 227.141337,103.174819 230.65,102.22 C236.02,101.07 235.65,106.15 243.76,107.87 L243.75,106.42 Z" id="Path" fill="#48C4E5"/>
<path d="M261.46,105.38 L261.46,105.27 C261.34,73.03 235.17,45.45 202.91,45.45 C183.207085,45.4363165 164.821777,55.3450614 154,71.81 L153.79,71.45 L137.23,45.45 L97.5,45.4499858 L135.25,104.57 L98.41,162.57 L137,162.57 L153.79,136.78 L170.88,162.57 L209.48,162.57 L174.48,107.49 C173.899005,106.416838 173.583536,105.220114 173.56,104 C173.557346,96.2203871 176.64661,88.7586448 182.147627,83.2576275 C187.648645,77.7566101 195.110387,74.6673462 202.89,74.67 C219.11,74.67 221.82,84.37 225.32,88.93 C232.23,97.93 246.03,93.99 246.03,105.73 L246.03,105.73 C246.071086,108.480945 247.576662,111.001004 249.979593,112.340896 C252.382524,113.680787 255.317747,113.636949 257.679593,112.225896 C260.041438,110.814842 261.471086,108.250945 261.43,105.5 L261.43,105.5 L261.43,105.38 L261.46,105.38 Z" id="Path" fill="#FFFFFF"/>
<path d="M261.5,113.68 C261.892278,116.421801 261.504116,119.218653 260.38,121.75 C258.18,126.84 254.51,125.14 254.51,125.14 C254.51,125.14 251.35,123.6 253.27,120.65 C255.4,117.36 259.61,117.74 261.5,113.68 Z" id="Path" fill="#FFFFFF"/>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.8 KiB

View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg
className="material-icons"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
>
<path d="M0 0h24v24H0z" fill="none" />
<path d="M12 17.27L18.18 21l-1.64-7.03L22 9.24l-7.19-.61L12 2 9.19 8.63 2 9.24l5.46 4.73L5.82 21z" />
</svg>

After

Width:  |  Height:  |  Size: 347 B

View File

@@ -0,0 +1,11 @@
import React from 'react';
import { render } from '@testing-library/react';
import Index from '../pages/index';
describe('Index', () => {
it('should render successfully', () => {
const { baseElement } = render(<Index />);
expect(baseElement).toBeTruthy();
});
});

View File

@@ -1,10 +0,0 @@
<!-- <div style="text-align: center">
<h1>Welcome to client!</h1>
<img
width="450"
src="https://raw.githubusercontent.com/nrwl/nx/master/images/nx-logo.png"
alt="Nx - Smart, Extensible Build Framework"
/>
</div>
<div>Message: {{ hello$ | async | json }}</div> -->
<router-outlet></router-outlet>

View File

@@ -1,133 +0,0 @@
/*
* Remove template code below
*/
:host {
display: block;
font-family: sans-serif;
min-width: 300px;
max-width: 600px;
margin: 50px auto;
}
.gutter-left {
margin-left: 9px;
}
.col-span-2 {
grid-column: span 2;
}
.flex {
display: flex;
align-items: center;
justify-content: center;
}
header {
background-color: #143055;
color: white;
padding: 5px;
border-radius: 3px;
}
main {
padding: 0 36px;
}
p {
text-align: center;
}
h1 {
text-align: center;
margin-left: 18px;
font-size: 24px;
}
h2 {
text-align: center;
font-size: 20px;
margin: 40px 0 10px 0;
}
.resources {
text-align: center;
list-style: none;
padding: 0;
display: grid;
grid-gap: 9px;
grid-template-columns: 1fr 1fr;
}
.resource {
color: #0094ba;
height: 36px;
background-color: rgba(0, 0, 0, 0);
border: 1px solid rgba(0, 0, 0, 0.12);
border-radius: 4px;
padding: 3px 9px;
text-decoration: none;
}
.resource:hover {
background-color: rgba(68, 138, 255, 0.04);
}
pre {
padding: 9px;
border-radius: 4px;
background-color: black;
color: #eee;
}
details {
border-radius: 4px;
color: #333;
background-color: rgba(0, 0, 0, 0);
border: 1px solid rgba(0, 0, 0, 0.12);
padding: 3px 9px;
margin-bottom: 9px;
}
summary {
cursor: pointer;
outline: none;
height: 36px;
line-height: 36px;
}
.github-star-container {
margin-top: 12px;
line-height: 20px;
}
.github-star-container a {
display: flex;
align-items: center;
text-decoration: none;
color: #333;
}
.github-star-badge {
color: #24292e;
display: flex;
align-items: center;
font-size: 12px;
padding: 3px 10px;
border: 1px solid rgba(27, 31, 35, 0.2);
border-radius: 3px;
background-image: linear-gradient(-180deg, #fafbfc, #eff3f6 90%);
margin-left: 4px;
font-weight: 600;
}
.github-star-badge:hover {
background-image: linear-gradient(-180deg, #f0f3f6, #e6ebf1 90%);
border-color: rgba(27, 31, 35, 0.35);
background-position: -0.5em;
}
.github-star-badge .material-icons {
height: 16px;
width: 16px;
margin-right: 4px;
}

View File

@@ -1,19 +0,0 @@
import { Component } from '@angular/core';
import { TestBed, async } from '@angular/core/testing';
import { HttpClientModule } from '@angular/common/http';
import { AppComponent } from './app.component';
describe('AppComponent', () => {
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [AppComponent],
imports: [HttpClientModule],
}).compileComponents();
}));
it('should create the app', () => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.debugElement.componentInstance;
expect(app).toBeTruthy();
});
});

View File

@@ -1,13 +0,0 @@
import { Component } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Message } from '@vidgrab2/api-interfaces';
@Component({
selector: 'vidgrab2-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss'],
})
export class AppComponent {
hello$ = this.http.get<Message>('/api/hello');
constructor(private http: HttpClient) {}
}

View File

@@ -1,20 +0,0 @@
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { RouterModule, Routes } from '@angular/router'
import { AppComponent } from './app.component';
import { HttpClientModule } from '@angular/common/http';
import { ExtractorsComponent } from './extractors/extractors.component';
const routes: Routes = [
{ path: '', component: AppComponent },
{ path: 'extractors', component: ExtractorsComponent },
];
@NgModule({
declarations: [AppComponent, ExtractorsComponent],
imports: [BrowserModule, HttpClientModule, RouterModule.forRoot(routes)],
providers: [],
bootstrap: [AppComponent],
})
export class AppModule {}

View File

@@ -1,13 +0,0 @@
<section class="section">
<div class="container">
<div class="content">
<p>
Here is the list of currently supported services. This is generated
dynamically because youtube-dl adds new services all the time.
</p>
<ul>
<li *ngFor="let e of extractors$ | async">{{ e }}</li>
</ul>
</div>
</div>
</section>

View File

@@ -1,20 +0,0 @@
/* eslint-disable @angular-eslint/no-empty-lifecycle-method */
import { Component, OnInit } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { map } from 'rxjs/operators';
@Component({
selector: 'vidgrab2-extractors',
templateUrl: './extractors.component.html',
styleUrls: ['./extractors.component.scss']
})
export class ExtractorsComponent implements OnInit {
extractors$ = this.http.get<{extractors: string[]}>('/api/extractors').pipe(map(e => e.extractors));
constructor(private http: HttpClient) {}
ngOnInit(): void {
// Does a thing
}
}

View File

@@ -1,3 +0,0 @@
export const environment = {
production: true,
};

View File

@@ -1,16 +0,0 @@
// This file can be replaced during build by using the `fileReplacements` array.
// `ng build` replaces `environment.ts` with `environment.prod.ts`.
// The list of file replacements can be found in `angular.json`.
export const environment = {
production: false,
};
/*
* For easier debugging in development mode, you can import the following file
* to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`.
*
* This import should be commented out in production mode because it will have a negative impact
* on performance if an error is thrown.
*/
// import 'zone.js/plugins/zone-error'; // Included with Angular CLI.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -1,35 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Client</title>
<base href="/" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" type="image/x-icon" href="favicon.ico" />
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/bulma@0.9.3/css/bulma.min.css"
/>
</head>
<body>
<div id="wrapper">
<section class="section has-background-light">
<div class="container">
<nav class="level">
<h1 class="title is-1 level-item has-text-centered">Vidgrab</h1>
</nav>
</div>
</section>
<vidgrab2-root></vidgrab2-root>
</div>
<footer id="footer" class="footer">
<div class="content has-text-centered">
<p>
Code and "Design" by <a href="#">Ted Kulp</a>. Based on youtube-dl.
</p>
</div>
</footer>
</body>
</html>

View File

@@ -1,13 +0,0 @@
import { enableProdMode } from '@angular/core';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app/app.module';
import { environment } from './environments/environment';
if (environment.production) {
enableProdMode();
}
platformBrowserDynamic()
.bootstrapModule(AppModule)
.catch((err) => console.error(err));

View File

@@ -1,64 +0,0 @@
/**
* This file includes polyfills needed by Angular and is loaded before the app.
* You can add your own extra polyfills to this file.
*
* This file is divided into 2 sections:
* 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers.
* 2. Application imports. Files imported after ZoneJS that should be loaded before your main
* file.
*
* The current setup is for so-called "evergreen" browsers; the last versions of browsers that
* automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera),
* Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile.
*
* Learn more in https://angular.io/guide/browser-support
*/
/***************************************************************************************************
* BROWSER POLYFILLS
*/
/**
* IE11 requires the following for NgClass support on SVG elements
*/
// import 'classlist.js'; // Run `npm install --save classlist.js`.
/**
* Web Animations `@angular/platform-browser/animations`
* Only required if AnimationBuilder is used within the application and using IE/Edge or Safari.
* Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0).
*/
// import 'web-animations-js'; // Run `npm install --save web-animations-js`.
/**
* By default, zone.js will patch all possible macroTask and DomEvents
* user can disable parts of macroTask/DomEvents patch by setting following flags
* because those flags need to be set before `zone.js` being loaded, and webpack
* will put import in the top of bundle, so user need to create a separate file
* in this directory (for example: zone-flags.ts), and put the following flags
* into that file, and then add the following code before importing zone.js.
* import './zone-flags';
*
* The flags allowed in zone-flags.ts are listed here.
*
* The following flags will work for all browsers.
*
* (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame
* (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick
* (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames
*
* in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js
* with the following flag, it will bypass `zone.js` patch for IE/Edge
*
* (window as any).__Zone_enable_cross_context_check = true;
*
*/
/***************************************************************************************************
* Zone JS is required by default for Angular itself.
*/
import 'zone.js'; // Included with Angular CLI.
/***************************************************************************************************
* APPLICATION IMPORTS
*/

View File

@@ -1 +0,0 @@
import 'jest-preset-angular/setup-jest';

View File

@@ -1,4 +1,3 @@
/* You can add global styles to this file, and also import other style files */
body {
display: flex;
min-height: 100vh;

View File

@@ -1,9 +0,0 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../../dist/out-tsc",
"types": []
},
"files": ["src/main.ts", "src/polyfills.ts"],
"include": ["src/**/*.d.ts"]
}

View File

@@ -1,7 +0,0 @@
{
"extends": "./tsconfig.json",
"include": ["**/*.ts"],
"compilerOptions": {
"types": ["jest", "node"]
}
}

View File

@@ -1,27 +1,17 @@
{
"extends": "../../tsconfig.base.json",
"files": [],
"include": [],
"references": [
{
"path": "./tsconfig.app.json"
},
{
"path": "./tsconfig.spec.json"
},
{
"path": "./tsconfig.editor.json"
}
],
"compilerOptions": {
"jsx": "preserve",
"allowJs": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"types": ["node", "jest"],
"strict": false,
"forceConsistentCasingInFileNames": true,
"strict": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true
"noEmit": true,
"resolveJsonModule": true,
"isolatedModules": true
},
"angularCompilerOptions": {
"strictInjectionParameters": true,
"strictInputAccessModifiers": true,
"strictTemplates": true
}
"include": ["**/*.ts", "**/*.tsx", "**/*.js", "**/*.jsx", "next-env.d.ts"],
"exclude": ["node_modules"]
}

View File

@@ -3,8 +3,14 @@
"compilerOptions": {
"outDir": "../../dist/out-tsc",
"module": "commonjs",
"types": ["jest", "node"]
"types": ["jest", "node"],
"jsx": "react"
},
"files": ["src/test-setup.ts"],
"include": ["**/*.spec.ts", "**/*.d.ts"]
"include": [
"**/*.spec.ts",
"**/*.spec.tsx",
"**/*.spec.js",
"**/*.spec.jsx",
"**/*.d.ts"
]
}

3
babel.config.json Normal file
View File

@@ -0,0 +1,3 @@
{
"babelrcRoots": ["*"]
}

View File

@@ -1,69 +0,0 @@
/**
* This file decorates the Angular CLI with the Nx CLI to enable features such as computation caching
* and faster execution of tasks.
*
* It does this by:
*
* - Patching the Angular CLI to warn you in case you accidentally use the undecorated ng command.
* - Symlinking the ng to nx command, so all commands run through the Nx CLI
* - Updating the package.json postinstall script to give you control over this script
*
* The Nx CLI decorates the Angular CLI, so the Nx CLI is fully compatible with it.
* Every command you run should work the same when using the Nx CLI, except faster.
*
* Because of symlinking you can still type `ng build/test/lint` in the terminal. The ng command, in this case,
* will point to nx, which will perform optimizations before invoking ng. So the Angular CLI is always invoked.
* The Nx CLI simply does some optimizations before invoking the Angular CLI.
*
* To opt out of this patch:
* - Replace occurrences of nx with ng in your package.json
* - Remove the script from your postinstall script in your package.json
* - Delete and reinstall your node_modules
*/
const fs = require('fs');
const os = require('os');
const cp = require('child_process');
const isWindows = os.platform() === 'win32';
let output;
try {
output = require('@nrwl/workspace').output;
} catch (e) {
console.warn('Angular CLI could not be decorated to enable computation caching. Please ensure @nrwl/workspace is installed.');
process.exit(0);
}
/**
* Symlink of ng to nx, so you can keep using `ng build/test/lint` and still
* invoke the Nx CLI and get the benefits of computation caching.
*/
function symlinkNgCLItoNxCLI() {
try {
const ngPath = './node_modules/.bin/ng';
const nxPath = './node_modules/.bin/nx';
if (isWindows) {
/**
* This is the most reliable way to create symlink-like behavior on Windows.
* Such that it works in all shells and works with npx.
*/
['', '.cmd', '.ps1'].forEach(ext => {
if (fs.existsSync(nxPath + ext)) fs.writeFileSync(ngPath + ext, fs.readFileSync(nxPath + ext));
});
} else {
// If unix-based, symlink
cp.execSync(`ln -sf ./nx ${ngPath}`);
}
}
catch(e) {
output.error({ title: 'Unable to create a symlink from the Angular CLI to the Nx CLI:' + e.message });
throw e;
}
}
try {
symlinkNgCLItoNxCLI();
require('@nrwl/cli/lib/decorate-cli').decorateCli();
output.log({ title: 'Angular CLI has been decorated to enable computation caching.' });
} catch(e) {
output.error({ title: 'Decoration of the Angular CLI did not complete successfully' });
}

View File

@@ -13,7 +13,8 @@ export interface QueueDto {
extractor?: string;
}
export type JobEvent = {
job?: unknown;
export interface JobEvent {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
job?: any;
err?: Error;
};

View File

@@ -35,10 +35,6 @@
},
"client": {
"tags": []
},
"client-e2e": {
"tags": [],
"implicitDependencies": ["client"]
}
}
}

19314
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -4,16 +4,17 @@
"license": "MIT",
"scripts": {
"ng": "nx",
"postinstall": "node ./decorate-angular-cli.js && ngcc --properties es2015 browser module main",
"nx": "nx",
"start": "ng serve",
"start:fe": "ng serve client",
"start:fe:prod": "sleep 10 && ng serve client --prod",
"start:be": "ng serve api",
"build": "ng build",
"start:be:prod": "node dist/apps/api/main.js",
"build": "ng build api --prod && ng build client",
"test": "ng test",
"lint": "nx workspace-lint && ng lint",
"e2e": "ng e2e",
"dev": "concurrently -p=\"{name}\" -n=\"Angular,NestJS\" -c=\"green,blue\" \"npm run start:fe\" \"npm run start:be\"",
"dev": "concurrently -p=\"{name}\" -n=\"Next,NestJS\" -c=\"green,blue\" \"npm run start:fe\" \"npm run start:be\"",
"prod": "concurrently -p=\"{name}\" -n=\"Next,NestJS\" -c=\"green,blue\" \"npm run start:fe:prod\" \"npm run start:be:prod\"",
"affected:apps": "nx affected:apps",
"affected:libs": "nx affected:libs",
"affected:build": "nx affected:build",
@@ -32,14 +33,6 @@
},
"private": true,
"dependencies": {
"@angular/animations": "^12.1.0",
"@angular/common": "^12.1.0",
"@angular/compiler": "^12.1.0",
"@angular/core": "^12.1.0",
"@angular/forms": "^12.1.0",
"@angular/platform-browser": "^12.1.0",
"@angular/platform-browser-dynamic": "^12.1.0",
"@angular/router": "^12.1.0",
"@nestjs/bull": "^0.4.0",
"@nestjs/common": "^7.0.0",
"@nestjs/config": "^1.0.1",
@@ -49,12 +42,18 @@
"@nestjs/platform-socket.io": "^7.6.18",
"@nestjs/serve-static": "^2.2.2",
"@nestjs/websockets": "^7.6.18",
"@nrwl/angular": "12.6.3",
"bull": "^3.27.0",
"cache-manager": "^3.4.4",
"class-transformer": "^0.4.0",
"class-validator": "^0.13.1",
"core-js": "^3.6.5",
"document-register-element": "1.13.1",
"lodash": "^4.17.21",
"next": "11.0.1",
"react": "17.0.2",
"react-dom": "17.0.2",
"reflect-metadata": "^0.1.13",
"regenerator-runtime": "0.13.7",
"rxjs": "~6.6.0",
"split2": "^3.2.2",
"tslib": "^2.0.0",
@@ -62,34 +61,42 @@
"zone.js": "~0.11.4"
},
"devDependencies": {
"@angular-devkit/build-angular": "^12.1.0",
"@angular-eslint/eslint-plugin": "~12.3.0",
"@angular-eslint/eslint-plugin-template": "~12.3.0",
"@angular-eslint/template-parser": "~12.3.0",
"@angular/cli": "^12.0.0",
"@angular/compiler-cli": "^12.1.0",
"@angular/language-service": "^12.1.0",
"@angular/cli": "^12.1.4",
"@babel/core": "7.12.13",
"@babel/preset-env": "7.12.13",
"@babel/preset-react": "7.12.13",
"@babel/preset-typescript": "7.12.13",
"@nestjs/schematics": "^7.0.0",
"@nestjs/testing": "^7.0.0",
"@nrwl/cli": "12.6.3",
"@nrwl/cypress": "12.6.3",
"@nrwl/eslint-plugin-nx": "12.6.3",
"@nrwl/jest": "12.6.3",
"@nrwl/linter": "12.6.3",
"@nrwl/nest": "^12.6.3",
"@nrwl/next": "^12.6.3",
"@nrwl/node": "12.6.3",
"@nrwl/react": "12.6.3",
"@nrwl/tao": "12.6.3",
"@nrwl/web": "12.6.3",
"@nrwl/workspace": "12.6.3",
"@testing-library/react": "11.2.6",
"@types/cache-manager": "^3.4.2",
"@types/jest": "26.0.24",
"@types/node": "14.14.33",
"@types/react": "17.0.3",
"@types/react-dom": "17.0.3",
"@typescript-eslint/eslint-plugin": "~4.28.3",
"@typescript-eslint/parser": "~4.28.3",
"babel-jest": "27.0.6",
"concurrently": "^6.2.0",
"cypress": "^7.3.0",
"dotenv": "~10.0.0",
"eslint": "7.22.0",
"eslint-config-next": "11.0.1",
"eslint-config-prettier": "8.1.0",
"eslint-plugin-cypress": "^2.10.3",
"eslint-plugin-import": "2.22.1",
"eslint-plugin-jsx-a11y": "6.4.1",
"eslint-plugin-react": "7.23.1",
"eslint-plugin-react-hooks": "4.2.0",
"jest": "27.0.3",
"jest-preset-angular": "9.0.4",
"prettier": "^2.3.1",