add free plan (#549)

- add new fields to support this
- when free:
  - fade out all event types after first
  - hide events after first on booking page
  - make booking page after the first one 404 if accessed directly
- add e2e tests
This commit is contained in:
Alex Johansson 2021-09-06 15:51:15 +02:00 committed by GitHub
parent fa35af7bd8
commit 7e6e935ed3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
29 changed files with 880 additions and 261 deletions

View File

@ -66,3 +66,4 @@ jobs:
name: videos
path: |
cypress/videos
cypress/screenshots

42
components/ui/Alert.tsx Normal file
View File

@ -0,0 +1,42 @@
import { XCircleIcon, InformationCircleIcon, CheckCircleIcon } from "@heroicons/react/solid";
import classNames from "classnames";
import { ReactNode } from "react";
export interface AlertProps {
title: ReactNode;
message?: ReactNode;
className?: string;
severity: "success" | "warning" | "error";
}
export function Alert(props: AlertProps) {
const { severity } = props;
return (
<div
className={classNames(
"rounded-md p-4",
props.className,
severity === "error" && "bg-red-50 text-red-800",
severity === "warning" && "bg-yellow-50 text-yellow-800",
severity === "success" && "bg-gray-900 text-white"
)}>
<div className="flex">
<div className="flex-shrink-0">
{severity === "error" && (
<XCircleIcon className={classNames("h-5 w-5 text-red-400")} aria-hidden="true" />
)}
{severity === "warning" && (
<InformationCircleIcon className={classNames("h-5 w-5 text-yellow-400")} aria-hidden="true" />
)}
{severity === "success" && (
<CheckCircleIcon className={classNames("h-5 w-5 text-gray-400")} aria-hidden="true" />
)}
</div>
<div className="ml-3">
<h3 className="text-sm font-medium">{props.title}</h3>
<div className="text-sm">{props.message}</div>
</div>
</div>
</div>
);
}

View File

@ -68,7 +68,7 @@ export const Button = function Button(props: ButtonProps) {
? "text-gray-400 bg-transparent"
: "text-red-700 bg-transparent hover:bg-red-100 focus:outline-none focus:ring-2 focus:ring-offset-1 focus:bg-red-50 focus:ring-red-500"),
// set not-allowed cursor if disabled
disabled && "cursor-not-allowed",
loading ? "cursor-wait" : disabled ? "cursor-not-allowed" : "",
props.className
),
// if we click a disabled button, we prevent going through the click handler

View File

@ -1,19 +1,8 @@
import { XCircleIcon } from "@heroicons/react/solid";
import { Alert } from "../Alert";
export default function ErrorAlert(props) {
return (
<div className="rounded-md bg-red-50 p-4">
<div className="flex">
<div className="flex-shrink-0">
<XCircleIcon className="h-5 w-5 text-red-400" aria-hidden="true" />
</div>
<div className="ml-3">
<h3 className="text-sm font-medium text-red-800">Something went wrong</h3>
<div className="text-sm text-red-700">
<p>{props.message}</p>
</div>
</div>
</div>
</div>
);
/**
* @deprecated use `<Alert severity="error" message="x" />` instead
*/
export default function ErrorAlert(props: { message: string; className?: string }) {
return <Alert severity="errror" message={props.message} />;
}

View File

@ -1,3 +1,4 @@
{
"baseUrl": "http://localhost:3000"
"baseUrl": "http://localhost:3000",
"chromeWebSecurity": false
}

View File

@ -0,0 +1,62 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
describe("booking pages", () => {
describe("free user", () => {
it("only one visibile event", () => {
cy.visit("/free");
cy.get("[data-testid=event-types]").children().should("have.length", 1);
cy.get('[href="/free/30min"]').should("exist");
cy.get('[href="/free/60min"]').should("not.exist");
});
it("/free/30min is bookable", () => {
cy.request({
method: "GET",
url: "/free/30min",
failOnStatusCode: false,
}).then((res) => {
expect(res.status).to.eql(200);
});
});
it("/free/60min is not bookable", () => {
cy.request({
method: "GET",
url: "/free/60min",
failOnStatusCode: false,
}).then((res) => {
expect(res.status).to.eql(404);
});
});
});
it("pro user's page has at least 2 visibile events", () => {
cy.visit("/pro");
cy.get("[data-testid=event-types]").children().should("have.length.at.least", 2);
});
describe("free user with first hidden", () => {
it("has no visible events", () => {
cy.visit("/free-first-hidden");
cy.contains("This user hasn't set up any event types yet.");
});
it("/free-first-hidden/30min is not bookable", () => {
cy.request({
method: "GET",
url: "/free-first-hidden/30min",
failOnStatusCode: false,
}).then((res) => {
expect(res.status).to.eql(404);
});
});
it("/free-first-hidden/60min is not bookable", () => {
cy.request({
method: "GET",
url: "/free-first-hidden/60min",
failOnStatusCode: false,
}).then((res) => {
expect(res.status).to.eql(404);
});
});
});
});

View File

@ -0,0 +1,66 @@
function randomString(length: number) {
let result = "";
const characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
const charactersLength = characters.length;
for (let i = 0; i < length; i++) {
result += characters.charAt(Math.floor(Math.random() * charactersLength));
}
return result;
}
describe("pro user", () => {
before(() => {
cy.visit("/event-types");
cy.login("pro@example.com", "pro");
});
beforeEach(() => {
cy.visit("/event-types");
});
it("has at least 2 events", () => {
cy.get("[data-testid=event-types]").children().should("have.length.at.least", 2);
cy.get("[data-testid=event-types]")
.children()
.each(($el) => {
expect($el).to.have.attr("data-disabled", "0");
});
});
it("can add new event type", () => {
cy.get("[data-testid=new-event-type]").click();
const nonce = randomString(3);
const eventTitle = `hello ${nonce}`;
cy.get("[name=title]").focus().type(eventTitle);
cy.get("[name=length]").focus().type("10");
cy.get("[type=submit]").click();
cy.location("pathname").should("not.eq", "/event-types");
cy.visit("/event-types");
cy.get("[data-testid=event-types]").contains(eventTitle);
});
});
describe("free user", () => {
before(() => {
cy.visit("/event-types");
cy.login("free@example.com", "free");
});
describe("/event-types", () => {
beforeEach(() => {
cy.visit("/event-types");
});
it("has at least 2 events where first is enabled", () => {
cy.get("[data-testid=event-types]").children().should("have.length.at.least", 2);
cy.get("[data-testid=event-types]").children().first().should("have.attr", "data-disabled", "0");
cy.get("[data-testid=event-types]").children().last().should("have.attr", "data-disabled", "1");
});
it("can not add new event type", () => {
cy.get("[data-testid=new-event-type]").should("be.disabled");
});
});
});

View File

@ -1,6 +1,4 @@
/// <reference types="cypress" />
describe("silly test", () => {
describe("smoke test", () => {
it("loads /", () => {
cy.visit("/");
cy.contains("Sign in to your account");

View File

@ -1,25 +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
// ***********************************************
//
//
// -- This is a parent command --
// Cypress.Commands.add('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

@ -0,0 +1,26 @@
/* eslint-disable @typescript-eslint/no-namespace */
declare global {
namespace Cypress {
interface Chainable {
login(email: string, password: string): Chainable;
}
}
}
Cypress.Commands.add("login", (email: string, password: string) => {
cy.log(` 🗝 Logging in with ${email}`);
Cypress.Cookies.defaults({
preserve: /next-auth/,
});
cy.clearCookies();
cy.clearCookie("next-auth.session-token");
cy.reload();
cy.get("[name=email]").focus().clear().type(email);
cy.get("[name=password]").focus().clear().type(password);
cy.get("[type=submit]").click();
cy.wait(500);
});
export {};

View File

@ -1,20 +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";
// Alternatively you can use CommonJS syntax:
// require('./commands')

4
cypress/support/index.ts Normal file
View File

@ -0,0 +1,4 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
/* eslint-disable @typescript-eslint/no-namespace */
import "./commands";

10
cypress/tsconfig.json Normal file
View File

@ -0,0 +1,10 @@
{
"compilerOptions": {
"strict": true,
"baseUrl": "../node_modules",
"target": "es5",
"lib": ["es5", "dom"],
"types": ["cypress"]
},
"include": ["**/*.ts"]
}

View File

@ -1,3 +1,11 @@
export function asStringOrNull(str: unknown) {
return typeof str === "string" ? str : null;
}
export function asStringOrThrow(str: unknown): string {
const type = typeof str;
if (type !== "string") {
throw new Error(`Expected "string" - got ${type}`);
}
return str;
}

View File

@ -0,0 +1,31 @@
import { useRouter } from "next/router";
import { useMemo } from "react";
export function useToggleQuery(name: string) {
const router = useRouter();
const hrefOff = useMemo(() => {
const query = {
...router.query,
};
delete query[name];
return {
query,
};
}, [router.query, name]);
const hrefOn = useMemo(() => {
const query = {
...router.query,
[name]: "1",
};
return {
query,
};
}, [router.query, name]);
return {
hrefOn,
hrefOff,
isOn: router.query[name] === "1",
};
}

View File

@ -7,7 +7,9 @@ if (process.env.NODE_ENV === "production") {
prisma = new PrismaClient();
} else {
if (!globalAny.prisma) {
globalAny.prisma = new PrismaClient();
globalAny.prisma = new PrismaClient({
log: ["query", "error", "warn"],
});
}
prisma = globalAny.prisma;
}

View File

@ -2,7 +2,7 @@
type GetSSRResult<TProps> =
//
{ props: TProps } | { redirect: any } | { notFound: true };
{ props: TProps } | { redirect: any } | { notFound: boolean };
type GetSSRFn<TProps> = (...args: any[]) => Promise<GetSSRResult<TProps>>;

View File

@ -8,7 +8,7 @@
"db-migrate": "yarn prisma migrate dev",
"db-seed": "yarn ts-node scripts/seed.ts",
"db-nuke": "docker-compose down --volumes --remove-orphans",
"dx": "DATABASE_URL=postgresql://postgres:@localhost:5450/calendso eval 'yarn db-up && yarn prisma migrate dev && yarn db-seed && yarn dev'",
"dx": "BASE_URL='http://localhost:3000' DATABASE_URL=postgresql://postgres:@localhost:5450/calendso eval 'yarn db-up && yarn prisma migrate dev && yarn db-seed && yarn dev'",
"test": "node node_modules/.bin/jest",
"build": "next build",
"start": "next start",
@ -51,6 +51,7 @@
"next-seo": "^4.26.0",
"next-transpile-modules": "^8.0.0",
"nodemailer": "^6.6.3",
"npm-run-all": "^4.1.5",
"react": "17.0.2",
"react-dates": "^21.8.0",
"react-dom": "17.0.2",
@ -73,6 +74,7 @@
"@types/nodemailer": "^6.4.4",
"@types/react": "^17.0.18",
"@types/react-dates": "^21.8.3",
"@types/react-select": "^4.0.17",
"@typescript-eslint/eslint-plugin": "^4.30.0",
"@typescript-eslint/parser": "^4.29.2",
"autoprefixer": "^10.3.1",
@ -91,7 +93,7 @@
"prisma": "^2.30.2",
"tailwindcss": "^2.2.7",
"ts-node": "^10.2.1",
"typescript": "^4.3.5"
"typescript": "^4.4.2"
},
"lint-staged": {
"./{*,{pages,components,lib}/**/*}.{js,ts,jsx,tsx}": [

View File

@ -1,51 +1,17 @@
import { GetServerSideProps } from "next";
import { HeadSeo } from "@components/seo/head-seo";
import Link from "next/link";
import prisma, { whereAndSelect } from "@lib/prisma";
import Avatar from "@components/Avatar";
import { HeadSeo } from "@components/seo/head-seo";
import Theme from "@components/Theme";
import { ClockIcon, InformationCircleIcon, UserIcon } from "@heroicons/react/solid";
import React from "react";
import { ArrowRightIcon } from "@heroicons/react/outline";
import { ClockIcon, InformationCircleIcon, UserIcon } from "@heroicons/react/solid";
import prisma from "@lib/prisma";
import { inferSSRProps } from "@lib/types/inferSSRProps";
import { GetServerSidePropsContext } from "next";
import Link from "next/link";
import React from "react";
export default function User(props): User {
export default function User(props: inferSSRProps<typeof getServerSideProps>) {
const { isReady } = Theme(props.user.theme);
const eventTypes = props.eventTypes.map((type) => (
<div
key={type.id}
className="group relative dark:bg-neutral-900 dark:border-0 dark:hover:border-neutral-600 bg-white hover:bg-gray-50 border border-neutral-200 hover:border-black rounded-sm">
<ArrowRightIcon className="absolute transition-opacity h-4 w-4 right-3 top-3 text-black dark:text-white opacity-0 group-hover:opacity-100" />
<Link href={`/${props.user.username}/${type.slug}`}>
<a className="block px-6 py-4">
<h2 className="font-semibold text-neutral-900 dark:text-white">{type.title}</h2>
<div className="mt-2 flex space-x-4">
<div className="flex text-sm text-neutral-500">
<ClockIcon
className="flex-shrink-0 mt-0.5 mr-1.5 h-4 w-4 text-neutral-400 dark:text-white"
aria-hidden="true"
/>
<p className="dark:text-white">{type.length}m</p>
</div>
<div className="flex text-sm min-w-16 text-neutral-500">
<UserIcon
className="flex-shrink-0 mt-0.5 mr-1.5 h-4 w-4 text-neutral-400 dark:text-white"
aria-hidden="true"
/>
<p className="dark:text-white">1-on-1</p>
</div>
<div className="flex text-sm text-neutral-500">
<InformationCircleIcon
className="flex-shrink-0 mt-0.5 mr-1.5 h-4 w-4 text-neutral-400 dark:text-white"
aria-hidden="true"
/>
<p className="dark:text-white">{type.description}</p>
</div>
</div>
</a>
</Link>
</div>
));
return (
<>
<HeadSeo
@ -68,8 +34,44 @@ export default function User(props): User {
</h1>
<p className="text-neutral-500 dark:text-white">{props.user.bio}</p>
</div>
<div className="space-y-6">{eventTypes}</div>
{eventTypes.length == 0 && (
<div className="space-y-6" data-testid="event-types">
{props.eventTypes.map((type) => (
<div
key={type.id}
className="group relative dark:bg-neutral-900 dark:border-0 dark:hover:border-neutral-600 bg-white hover:bg-gray-50 border border-neutral-200 hover:border-black rounded-sm">
<ArrowRightIcon className="absolute transition-opacity h-4 w-4 right-3 top-3 text-black dark:text-white opacity-0 group-hover:opacity-100" />
<Link href={`/${props.user.username}/${type.slug}`}>
<a className="block px-6 py-4">
<h2 className="font-semibold text-neutral-900 dark:text-white">{type.title}</h2>
<div className="mt-2 flex space-x-4">
<div className="flex text-sm text-neutral-500">
<ClockIcon
className="flex-shrink-0 mt-0.5 mr-1.5 h-4 w-4 text-neutral-400 dark:text-white"
aria-hidden="true"
/>
<p className="dark:text-white">{type.length}m</p>
</div>
<div className="flex text-sm min-w-16 text-neutral-500">
<UserIcon
className="flex-shrink-0 mt-0.5 mr-1.5 h-4 w-4 text-neutral-400 dark:text-white"
aria-hidden="true"
/>
<p className="dark:text-white">1-on-1</p>
</div>
<div className="flex text-sm text-neutral-500">
<InformationCircleIcon
className="flex-shrink-0 mt-0.5 mr-1.5 h-4 w-4 text-neutral-400 dark:text-white"
aria-hidden="true"
/>
<p className="dark:text-white">{type.description}</p>
</div>
</div>
</a>
</Link>
</div>
))}
</div>
{props.eventTypes.length == 0 && (
<div className="shadow overflow-hidden rounded-sm">
<div className="p-8 text-center text-gray-400 dark:text-white">
<h2 className="font-semibold text-3xl text-gray-600 dark:text-white">Uh oh!</h2>
@ -84,33 +86,45 @@ export default function User(props): User {
);
}
export const getServerSideProps: GetServerSideProps = async (context) => {
const user = await whereAndSelect(
prisma.user.findFirst,
{
username: context.query.user.toLowerCase(),
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
const username = (context.query.user as string).toLowerCase();
const user = await prisma.user.findUnique({
where: {
username,
},
["id", "username", "email", "name", "bio", "avatar", "theme"]
);
select: {
id: true,
username: true,
email: true,
name: true,
bio: true,
avatar: true,
theme: true,
plan: true,
},
});
if (!user) {
return {
notFound: true,
};
}
const eventTypes = await prisma.eventType.findMany({
const eventTypesWithHidden = await prisma.eventType.findMany({
where: {
userId: user.id,
hidden: false,
},
select: {
id: true,
slug: true,
title: true,
length: true,
description: true,
hidden: true,
},
take: user.plan === "FREE" ? 1 : undefined,
});
const eventTypes = eventTypesWithHidden.filter((evt) => !evt.hidden);
return {
props: {
user,

View File

@ -235,6 +235,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
availability: true,
hideBranding: true,
theme: true,
plan: true,
},
});
@ -243,11 +244,12 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
notFound: true,
} as const;
}
const eventType = await prisma.eventType.findFirst({
const eventType = await prisma.eventType.findUnique({
where: {
userId: user.id,
slug: typeParam,
userId_slug: {
userId: user.id,
slug: typeParam,
},
},
select: {
id: true,
@ -262,15 +264,32 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
periodEndDate: true,
periodCountCalendarDays: true,
minimumBookingNotice: true,
hidden: true,
},
});
if (!eventType) {
if (!eventType || eventType.hidden) {
return {
notFound: true,
} as const;
}
// check this is the first event
if (user.plan === "FREE") {
const firstEventType = await prisma.eventType.findFirst({
where: {
userId: user.id,
},
select: {
id: true,
},
});
if (firstEventType?.id !== eventType.id) {
return {
notFound: true,
} as const;
}
}
const getWorkingHours = (providesAvailability: { availability: Availability[] }) =>
providesAvailability.availability && providesAvailability.availability.length
? providesAvailability.availability

View File

@ -1,8 +1,7 @@
import Link from "next/link";
import { useRouter } from "next/router";
import Modal from "@components/Modal";
import React, { useEffect, useRef, useState } from "react";
import Select, { OptionBase } from "react-select";
import Select, { OptionTypeBase } from "react-select";
import prisma from "@lib/prisma";
import { EventTypeCustomInput, EventTypeCustomInputType } from "@prisma/client";
import { LocationType } from "@lib/location";
@ -25,7 +24,7 @@ import {
import dayjs from "dayjs";
import utc from "dayjs/plugin/utc";
import timezone from "dayjs/plugin/timezone";
import { Availability, EventType, User } from "@prisma/client";
import { Availability } from "@prisma/client";
import { validJson } from "@lib/jsonUtils";
import classnames from "classnames";
import throttle from "lodash.throttle";
@ -42,6 +41,8 @@ import updateEventType from "@lib/mutations/event-types/update-event-type";
import deleteEventType from "@lib/mutations/event-types/delete-event-type";
import showToast from "@lib/notification";
import { inferSSRProps } from "@lib/types/inferSSRProps";
import { asStringOrThrow } from "@lib/asStringOrNull";
import Button from "@components/ui/Button";
dayjs.extend(utc);
dayjs.extend(timezone);
@ -66,7 +67,7 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
const router = useRouter();
const [successModalOpen, setSuccessModalOpen] = useState(false);
const inputOptions: OptionBase[] = [
const inputOptions: OptionTypeBase[] = [
{ value: EventTypeCustomInputType.TEXT, label: "Text" },
{ value: EventTypeCustomInputType.TEXTLONG, label: "Multiline Text" },
{ value: EventTypeCustomInputType.NUMBER, label: "Number" },
@ -130,8 +131,8 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
const [showLocationModal, setShowLocationModal] = useState(false);
const [showAddCustomModal, setShowAddCustomModal] = useState(false);
const [selectedTimeZone, setSelectedTimeZone] = useState("");
const [selectedLocation, setSelectedLocation] = useState<OptionBase | undefined>(undefined);
const [selectedInputOption, setSelectedInputOption] = useState<OptionBase>(inputOptions[0]);
const [selectedLocation, setSelectedLocation] = useState<OptionTypeBase | undefined>(undefined);
const [selectedInputOption, setSelectedInputOption] = useState<OptionTypeBase>(inputOptions[0]);
const [locations, setLocations] = useState(eventType.locations || []);
const [selectedCustomInput, setSelectedCustomInput] = useState<EventTypeCustomInput | undefined>(undefined);
const [customInputs, setCustomInputs] = useState<EventTypeCustomInput[]>(
@ -162,14 +163,14 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
});
const [hidden, setHidden] = useState<boolean>(eventType.hidden);
const titleRef = useRef<HTMLInputElement>();
const slugRef = useRef<HTMLInputElement>();
const descriptionRef = useRef<HTMLTextAreaElement>();
const lengthRef = useRef<HTMLInputElement>();
const requiresConfirmationRef = useRef<HTMLInputElement>();
const eventNameRef = useRef<HTMLInputElement>();
const periodDaysRef = useRef<HTMLInputElement>();
const periodDaysTypeRef = useRef<HTMLSelectElement>();
const titleRef = useRef<HTMLInputElement>(null);
const slugRef = useRef<HTMLInputElement>(null);
const descriptionRef = useRef<HTMLTextAreaElement>(null);
const lengthRef = useRef<HTMLInputElement>(null);
const requiresConfirmationRef = useRef<HTMLInputElement>(null);
const eventNameRef = useRef<HTMLInputElement>(null);
const periodDaysRef = useRef<HTMLInputElement>(null);
const periodDaysTypeRef = useRef<HTMLSelectElement>(null);
useEffect(() => {
setSelectedTimeZone(eventType.timeZone || user.timeZone);
@ -804,17 +805,11 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
</>
)}
</Disclosure>
<div className="flex justify-end mt-4">
<Link href="/event-types">
<a className="inline-flex items-center px-4 py-2 mr-2 text-sm font-medium bg-white border border-transparent rounded-sm shadow-sm text-neutral-700 hover:bg-neutral-100 border-neutral-300 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-black">
Cancel
</a>
</Link>
<button
type="submit"
className="inline-flex items-center px-4 py-2 text-sm font-medium text-white border border-transparent rounded-sm shadow-sm bg-neutral-900 hover:bg-neutral-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-black">
Update
</button>
<div className="flex justify-end mt-4 space-x-2">
<Button href="/event-types" color="secondary" tabIndex={-1}>
Cancel
</Button>
<Button type="submit">Update</Button>
</div>
</form>
<Modal
@ -1033,7 +1028,9 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
const { req, query } = context;
const session = await getSession({ req });
if (!session) {
const typeParam = asStringOrThrow(query.type);
if (!session?.user?.id) {
return {
redirect: {
permanent: false,
@ -1042,22 +1039,38 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
};
}
const user: User = await prisma.user.findFirst({
const user = await prisma.user.findUnique({
where: {
email: session.user.email,
id: session.user.id,
},
select: {
id: true,
username: true,
timeZone: true,
startTime: true,
endTime: true,
availability: true,
plan: true,
},
});
const eventType: EventType | null = await prisma.eventType.findUnique({
if (!user) {
return {
notFound: true,
} as const;
}
const eventType = await prisma.eventType.findFirst({
where: {
id: parseInt(query.type as string),
userId: user.id,
OR: [
{
slug: typeParam,
},
{
id: parseInt(typeParam),
},
],
},
select: {
id: true,
@ -1116,10 +1129,10 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
},
];
const locationOptions: OptionBase[] = [
const locationOptions: OptionTypeBase[] = [
{ value: LocationType.InPerson, label: "In-person meeting" },
{ value: LocationType.Phone, label: "Phone call" },
{ value: LocationType.Zoom, label: "Zoom Video" },
{ value: LocationType.Zoom, label: "Zoom Video", disabled: true },
];
const hasGoogleCalendarIntegration = integrations.find(

View File

@ -1,4 +1,4 @@
import { Dialog, DialogClose, DialogContent } from "@components/Dialog";
import { Dialog, DialogContent } from "@components/Dialog";
import Loader from "@components/Loader";
import { Tooltip } from "@components/Tooltip";
import { Button } from "@components/ui/Button";
@ -21,81 +21,57 @@ import { useRouter } from "next/router";
import React, { Fragment, useRef } from "react";
import Shell from "@components/Shell";
import prisma from "@lib/prisma";
import { GetServerSidePropsContext, InferGetServerSidePropsType } from "next";
import { GetServerSidePropsContext } from "next";
import { useMutation } from "react-query";
import createEventType from "@lib/mutations/event-types/create-event-type";
import { ONBOARDING_INTRODUCED_AT } from "@lib/getting-started";
import { getSession } from "@lib/auth";
import { ONBOARDING_INTRODUCED_AT } from "@lib/getting-started";
import { useToggleQuery } from "@lib/hooks/useToggleQuery";
import { inferSSRProps } from "@lib/types/inferSSRProps";
import { Alert } from "@components/ui/Alert";
const EventTypesPage = (props: InferGetServerSidePropsType<typeof getServerSideProps>) => {
const EventTypesPage = (props: inferSSRProps<typeof getServerSideProps>) => {
const { user, types } = props;
const [session, loading] = useSession();
const router = useRouter();
const createMutation = useMutation(createEventType, {
onSuccess: async ({ eventType }) => {
await router.replace("/event-types/" + eventType.id);
await router.push("/event-types/" + eventType.id);
showToast(`${eventType.title} event type created successfully`, "success");
},
onError: (err: Error) => {
showToast(err.message, "error");
},
});
const modalOpen = useToggleQuery("new");
const titleRef = useRef<HTMLInputElement>();
const slugRef = useRef<HTMLInputElement>();
const descriptionRef = useRef<HTMLTextAreaElement>();
const lengthRef = useRef<HTMLInputElement>();
const dialogOpen = router.query.new === "1";
async function createEventTypeHandler(event) {
event.preventDefault();
const enteredTitle = titleRef.current.value;
const enteredSlug = slugRef.current.value;
const enteredDescription = descriptionRef.current.value;
const enteredLength = parseInt(lengthRef.current.value);
const body = {
title: enteredTitle,
slug: enteredSlug,
description: enteredDescription,
length: enteredLength,
};
createMutation.mutate(body);
}
function autoPopulateSlug() {
let t = titleRef.current.value;
t = t.replace(/\s+/g, "-").toLowerCase();
slugRef.current.value = t;
}
const slugRef = useRef<HTMLInputElement>(null);
if (loading) {
return <Loader />;
}
const CreateNewEventDialog = () => (
const renderEventDialog = () => (
<Dialog
open={dialogOpen}
open={modalOpen.isOn}
onOpenChange={(isOpen) => {
const newQuery = {
...router.query,
};
delete newQuery["new"];
if (!isOpen) {
router.push({ pathname: router.pathname, query: newQuery });
}
router.push(isOpen ? modalOpen.hrefOn : modalOpen.hrefOff);
}}>
<Button
className="mt-2 hidden sm:block"
StartIcon={PlusIcon}
href={{ query: { ...router.query, new: "1" } }}>
data-testid="new-event-type"
{...(props.canAddEvents
? {
href: modalOpen.hrefOn,
}
: {
disabled: true,
})}>
New event type
</Button>
<Button size="fab" className="block sm:hidden" href={{ query: { ...router.query, new: "1" } }}>
<Button size="fab" className="block sm:hidden" href={modalOpen.hrefOn}>
<PlusIcon className="w-8 h-8 text-white" />
</Button>
@ -108,7 +84,24 @@ const EventTypesPage = (props: InferGetServerSidePropsType<typeof getServerSideP
<p className="text-sm text-gray-500">Create a new event type for people to book times with.</p>
</div>
</div>
<form onSubmit={createEventTypeHandler}>
<form
onSubmit={(e) => {
e.preventDefault();
const target = e.target as unknown as Record<
"title" | "slug" | "description" | "length",
{ value: string }
>;
const body = {
title: target.title.value,
slug: target.slug.value,
description: target.description.value,
length: parseInt(target.length.value),
};
createMutation.mutate(body);
}}>
<div>
<div className="mb-4">
<label htmlFor="title" className="block text-sm font-medium text-gray-700">
@ -116,8 +109,13 @@ const EventTypesPage = (props: InferGetServerSidePropsType<typeof getServerSideP
</label>
<div className="mt-1">
<input
onChange={autoPopulateSlug}
ref={titleRef}
onChange={(e) => {
if (!slugRef.current) {
return;
}
const slug = e.target.value.replace(/\s+/g, "-").toLowerCase();
slugRef.current.value = slug;
}}
type="text"
name="title"
id="title"
@ -137,10 +135,10 @@ const EventTypesPage = (props: InferGetServerSidePropsType<typeof getServerSideP
{location.hostname}/{user.username}/
</span>
<input
ref={slugRef}
type="text"
name="slug"
id="slug"
ref={slugRef}
required
className="flex-1 block w-full min-w-0 border-gray-300 rounded-none focus:border-neutral-900 rounded-r-md focus:ring-neutral-900 sm:text-sm"
/>
@ -153,7 +151,6 @@ const EventTypesPage = (props: InferGetServerSidePropsType<typeof getServerSideP
</label>
<div className="mt-1">
<textarea
ref={descriptionRef}
name="description"
id="description"
className="block w-full border-gray-300 rounded-sm shadow-sm focus:border-neutral-900 focus:ring-neutral-900 sm:text-sm"
@ -166,7 +163,6 @@ const EventTypesPage = (props: InferGetServerSidePropsType<typeof getServerSideP
</label>
<div className="relative mt-1 rounded-sm shadow-sm">
<input
ref={lengthRef}
type="number"
name="length"
id="length"
@ -181,12 +177,12 @@ const EventTypesPage = (props: InferGetServerSidePropsType<typeof getServerSideP
</div>
</div>
<div className="mt-8 sm:flex sm:flex-row-reverse">
<button type="submit" className="btn btn-primary">
<Button type="submit" loading={createMutation.isLoading}>
Continue
</button>
<DialogClose as="button" className="mx-2 btn btn-white">
</Button>
<Button href={modalOpen.hrefOff} color="secondary" className="mr-2">
Cancel
</DialogClose>
</Button>
</div>
</form>
</DialogContent>
@ -198,19 +194,41 @@ const EventTypesPage = (props: InferGetServerSidePropsType<typeof getServerSideP
<Shell
heading="Event Types"
subtitle="Create events to share for people to book on your calendar."
CTA={types.length !== 0 && <CreateNewEventDialog />}>
CTA={types.length !== 0 && renderEventDialog()}>
{props.user.plan === "FREE" && (
<Alert
severity="warning"
title={<>You need to upgrade your plan to have more than one active event type.</>}
message={
<>
To upgrade go to{" "}
<a href="https://calendso.com/upgrade" className="underline">
calendso.com/upgrade
</a>
</>
}
className="my-4"
/>
)}
<div className="-mx-4 overflow-hidden bg-white border border-gray-200 rounded-sm sm:mx-0">
<ul className="divide-y divide-neutral-200">
{types.map((type) => (
<li key={type.id}>
<div className="hover:bg-neutral-50">
<div className="flex items-center px-4 py-4 sm:px-6">
<Link href={"/event-types/" + type.id}>
<a className="flex-1 min-w-0 sm:flex sm:items-center sm:justify-between">
<ul className="divide-y divide-neutral-200" data-testid="event-types">
{types.map((item) => (
<li
key={item.id}
className={classNames(
item.$disabled && "opacity-30 cursor-not-allowed pointer-events-none select-none"
)}
data-disabled={item.$disabled ? 1 : 0}>
<div
className={classNames("hover:bg-neutral-50", item.$disabled && "pointer-events-none")}
tabIndex={item.$disabled ? -1 : undefined}>
<div className={"flex items-center px-4 py-4 sm:px-6"}>
<Link href={"/event-types/" + item.id}>
<a className="flex-1 min-w-0 sm:flex sm:items-center sm:justify-between hover:bg-neutral-50">
<span className="truncate">
<div className="flex text-sm">
<p className="font-medium truncate text-neutral-900">{type.title}</p>
{type.hidden && (
<p className="font-medium truncate text-neutral-900">{item.title}</p>
{item.hidden && (
<span className="inline-flex items-center ml-2 px-1.5 py-0.5 text-yellow-800 text-xs font-medium bg-yellow-100 rounded-sm">
Hidden
</span>
@ -222,7 +240,7 @@ const EventTypesPage = (props: InferGetServerSidePropsType<typeof getServerSideP
className="flex-shrink-0 mr-1.5 w-4 h-4 text-neutral-400"
aria-hidden="true"
/>
<p>{type.length}m</p>
<p>{item.length}m</p>
</div>
<div className="flex items-center text-sm text-neutral-500">
<UserIcon
@ -231,14 +249,14 @@ const EventTypesPage = (props: InferGetServerSidePropsType<typeof getServerSideP
/>
<p>1-on-1</p>
</div>
{type.description && (
{item.description && (
<div className="flex items-center text-sm text-neutral-500">
<InformationCircleIcon
className="flex-shrink-0 mr-1.5 w-4 h-4 text-neutral-400"
aria-hidden="true"
/>
<div className="truncate max-w-32 sm:max-w-full">
{type.description.substring(0, 100)}
{item.description.substring(0, 100)}
</div>
</div>
)}
@ -251,7 +269,7 @@ const EventTypesPage = (props: InferGetServerSidePropsType<typeof getServerSideP
<div className="flex space-x-5 overflow-hidden">
<Tooltip content="Preview">
<a
href={"/" + session.user.username + "/" + type.slug}
href={"/" + session.user.username + "/" + item.slug}
target="_blank"
rel="noreferrer"
className="p-2 border border-transparent cursor-pointer group text-neutral-400 hover:border-gray-200">
@ -264,7 +282,7 @@ const EventTypesPage = (props: InferGetServerSidePropsType<typeof getServerSideP
onClick={() => {
showToast("Link copied!", "success");
navigator.clipboard.writeText(
window.location.hostname + "/" + session.user.username + "/" + type.slug
window.location.hostname + "/" + session.user.username + "/" + item.slug
);
}}
className="p-2 border border-transparent group text-neutral-400 hover:border-gray-200">
@ -300,7 +318,7 @@ const EventTypesPage = (props: InferGetServerSidePropsType<typeof getServerSideP
<Menu.Item>
{({ active }) => (
<a
href={"/" + session.user.username + "/" + type.slug}
href={"/" + session.user.username + "/" + item.slug}
target="_blank"
rel="noreferrer"
className={classNames(
@ -325,7 +343,7 @@ const EventTypesPage = (props: InferGetServerSidePropsType<typeof getServerSideP
"/" +
session.user.username +
"/" +
type.slug
item.slug
);
}}
className={classNames(
@ -633,7 +651,7 @@ const EventTypesPage = (props: InferGetServerSidePropsType<typeof getServerSideP
Event types enable you to share links that show available times on your calendar and allow
people to make bookings with you.
</p>
<CreateNewEventDialog />
{renderEventDialog()}
</div>
</div>
)}
@ -646,13 +664,18 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
const { req } = context;
const session = await getSession({ req });
if (!session) {
return { redirect: { permanent: false, destination: "/auth/login" } };
if (!session?.user?.id) {
return {
redirect: {
permanent: false,
destination: "/auth/login",
},
} as const;
}
const user = await prisma.user.findFirst({
const user = await prisma.user.findUnique({
where: {
email: session.user.email,
id: session.user.id,
},
select: {
id: true,
@ -662,19 +685,30 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
bufferTime: true,
completedOnboarding: true,
createdDate: true,
plan: true,
},
});
if (!user) {
// this shouldn't happen
return {
redirect: {
permanent: false,
destination: "/auth/login",
},
} as const;
}
if (!user.completedOnboarding && dayjs(user.createdDate).isAfter(ONBOARDING_INTRODUCED_AT)) {
return {
redirect: {
permanent: false,
destination: "/getting-started",
},
};
} as const;
}
const types = await prisma.eventType.findMany({
const typesRaw = await prisma.eventType.findMany({
where: {
userId: user.id,
},
@ -688,14 +722,29 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
},
});
const types = typesRaw.map((type, index) =>
user.plan === "FREE" && index > 0
? {
...type,
$disabled: true,
}
: {
...type,
$disabled: false,
}
);
const userObj = Object.assign({}, user, {
createdDate: user.createdDate.toString(),
});
const canAddEvents = user.plan !== "FREE" || types.length < 1;
return {
props: {
user: userObj,
types,
canAddEvents,
},
};
};

54
pages/sandbox/Alert.tsx Normal file
View File

@ -0,0 +1,54 @@
import { Alert, AlertProps } from "@components/ui/Alert";
import Head from "next/head";
import React from "react";
export default function AlertPage() {
const list: AlertProps[] = [
{ title: "Something went wrong", severity: "error" },
{ title: "Something went kinda wrong", severity: "warning" },
{ title: "Something went great", severity: "success" },
{ title: "Something went wrong", severity: "error", message: "Some extra context" },
{
title: "Something went wrong",
severity: "error",
message: (
<p>
Some extra context
<br />
hey
</p>
),
},
];
return (
<>
<Head>
<meta name="googlebot" content="noindex" />
</Head>
<div className="p-4 bg-gray-200">
<h1>Alert component</h1>
<div className="flex flex-col">
{list.map((props, index) => (
<div key={index} className="p-2 m-2 bg-white">
<h3>
<code>
{JSON.stringify(
props,
(key, value) => {
if (key.includes("message")) {
return "..";
}
return value;
},
2
)}
</code>
</h3>
<Alert {...props}>Alert text</Alert>
</div>
))}
</div>
</div>
</>
);
}

View File

@ -0,0 +1,8 @@
/*
Warnings:
- A unique constraint covering the columns `[userId,slug]` on the table `EventType` will be added. If there are existing duplicate values, this will fail.
*/
-- CreateIndex
CREATE UNIQUE INDEX "EventType.userId_slug_unique" ON "EventType"("userId", "slug");

View File

@ -0,0 +1,5 @@
-- CreateEnum
CREATE TYPE "UserPlan" AS ENUM ('FREE', 'TRIAL', 'PRO');
-- AlterTable
ALTER TABLE "users" ADD COLUMN "plan" "UserPlan" NOT NULL DEFAULT E'PRO';

View File

@ -0,0 +1,8 @@
/*
Warnings:
- A unique constraint covering the columns `[username]` on the table `users` will be added. If there are existing duplicate values, this will fail.
*/
-- CreateIndex
CREATE UNIQUE INDEX "users.username_unique" ON "users"("username");

View File

@ -33,6 +33,8 @@ model EventType {
requiresConfirmation Boolean @default(false)
minimumBookingNotice Int @default(120)
Schedule Schedule[]
@@unique([userId, slug])
}
model Credential {
@ -43,9 +45,15 @@ model Credential {
userId Int?
}
enum UserPlan {
FREE
TRIAL
PRO
}
model User {
id Int @id @default(autoincrement())
username String?
username String? @unique
name String?
email String? @unique
emailVerified DateTime?
@ -68,6 +76,8 @@ model User {
selectedCalendars SelectedCalendar[]
completedOnboarding Boolean? @default(false)
plan UserPlan @default(PRO)
Schedule Schedule[]
@@map(name: "users")
}
@ -195,7 +205,7 @@ model EventTypeCustomInput {
label String
type EventTypeCustomInputType
required Boolean
placeholder String @default("")
placeholder String @default("")
}
model ResetPasswordRequest {

View File

@ -2,7 +2,6 @@ import { hashPassword } from "../lib/auth";
import { Prisma, PrismaClient } from "@prisma/client";
const prisma = new PrismaClient();
let idx = 0;
async function createUserAndEventType(opts: {
user: Omit<Prisma.UserCreateArgs["data"], "password" | "email"> & { password: string; email: string };
eventTypes: Array<Prisma.EventTypeCreateArgs["data"]>;
@ -11,6 +10,7 @@ async function createUserAndEventType(opts: {
...opts.user,
password: await hashPassword(opts.user.password),
emailVerified: new Date(),
completedOnboarding: true,
};
const user = await prisma.user.upsert({
where: { email: opts.user.email },
@ -19,16 +19,17 @@ async function createUserAndEventType(opts: {
});
console.log(
`👤 Created '${opts.user.username}' with email "${opts.user.email}" & password "${opts.user.password}". Booking page 👉 http://localhost:3000/${opts.user.username}`
`👤 Upserted '${opts.user.username}' with email "${opts.user.email}" & password "${opts.user.password}". Booking page 👉 http://localhost:3000/${opts.user.username}`
);
for (const rawData of opts.eventTypes) {
const id = ++idx;
const eventTypeData: Prisma.EventTypeCreateArgs["data"] = { ...rawData };
eventTypeData.userId = user.id;
eventTypeData.id = id;
await prisma.eventType.upsert({
where: {
id,
userId_slug: {
slug: eventTypeData.slug,
userId: user.id,
},
},
update: eventTypeData,
create: eventTypeData,
@ -45,7 +46,7 @@ async function main() {
email: "free@example.com",
password: "free",
username: "free",
// plan: "FREE",
plan: "FREE",
},
eventTypes: [
{
@ -60,12 +61,34 @@ async function main() {
},
],
});
await createUserAndEventType({
user: {
email: "free-first-hidden@example.com",
password: "free-first-hidden",
username: "free-first-hidden",
plan: "FREE",
},
eventTypes: [
{
title: "30min",
slug: "30min",
length: 30,
hidden: true,
},
{
title: "60min",
slug: "60min",
length: 30,
},
],
});
await createUserAndEventType({
user: {
email: "pro@example.com",
password: "pro",
username: "pro",
// plan: "PRO",
plan: "PRO",
},
eventTypes: [
@ -86,7 +109,7 @@ async function main() {
email: "trial@example.com",
password: "trial",
username: "trial",
// plan: "TRIAL",
plan: "TRIAL",
},
eventTypes: [
{

237
yarn.lock
View File

@ -11,6 +11,7 @@
"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.12.13", "@babel/code-frame@^7.14.5":
version "7.14.5"
resolved "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.14.5.tgz#23b08d740e83f49c5e59945fbf1b43e80bbf4edb"
integrity sha512-9pzDqyc6OLDaqe+zbACgFkb6fKMNG6CObKpnYXChRsvYGyEdc7CA2BaqeOM+vOtCS5ndmJicPJhKAwYRI6UfFw==
dependencies:
"@babel/highlight" "^7.14.5"
@ -359,7 +360,7 @@
"@emotion/weak-memoize" "^0.2.5"
hoist-non-react-statics "^3.3.1"
"@emotion/serialize@^1.0.2":
"@emotion/serialize@^1.0.0", "@emotion/serialize@^1.0.2":
version "1.0.2"
resolved "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.0.2.tgz#77cb21a0571c9f68eb66087754a65fa97bfcd965"
dependencies:
@ -1195,12 +1196,36 @@
"@types/react-outside-click-handler" "*"
moment "^2.26.0"
"@types/react-dom@*":
version "17.0.9"
resolved "https://registry.npmjs.org/@types/react-dom/-/react-dom-17.0.9.tgz#441a981da9d7be117042e1a6fd3dac4b30f55add"
integrity sha512-wIvGxLfgpVDSAMH5utdL9Ngm5Owu0VsGmldro3ORLXV8CShrL8awVj06NuEXFQ5xyaYfdca7Sgbk/50Ri1GdPg==
dependencies:
"@types/react" "*"
"@types/react-outside-click-handler@*":
version "1.3.0"
resolved "https://registry.npmjs.org/@types/react-outside-click-handler/-/react-outside-click-handler-1.3.0.tgz#ccf0014032fc6ec286210f8a05d26a5c1f94cc96"
dependencies:
"@types/react" "*"
"@types/react-select@^4.0.17":
version "4.0.17"
resolved "https://registry.npmjs.org/@types/react-select/-/react-select-4.0.17.tgz#2e5ab4042c09c988bfc2711550329b0c3c9f8513"
integrity sha512-ZK5wcBhJaqC8ntQl0CJvK2KXNNsk1k5flM7jO+vNPPlceRzdJQazA6zTtQUyNr6exp5yrAiwiudtYxgGlgGHLg==
dependencies:
"@emotion/serialize" "^1.0.0"
"@types/react" "*"
"@types/react-dom" "*"
"@types/react-transition-group" "*"
"@types/react-transition-group@*":
version "4.4.2"
resolved "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.2.tgz#38890fd9db68bf1f2252b99a942998dc7877c5b3"
integrity sha512-KibDWL6nshuOJ0fu8ll7QnV/LVTo3PzQ9aCPnRUYPfX7eZohHwLIdNHj7pftanREzHNP4/nJa8oeM73uSiavMQ==
dependencies:
"@types/react" "*"
"@types/react@*", "@types/react@^17.0.18":
version "17.0.18"
resolved "https://registry.npmjs.org/@types/react/-/react-17.0.18.tgz#4109cbbd901be9582e5e39e3d77acd7b66bb7fbe"
@ -1422,6 +1447,7 @@ airbnb-prop-types@^2.10.0, airbnb-prop-types@^2.14.0, airbnb-prop-types@^2.15.0:
ajv@^6.10.0, ajv@^6.12.3, ajv@^6.12.4:
version "6.12.6"
resolved "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4"
integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==
dependencies:
fast-deep-equal "^3.1.1"
fast-json-stable-stringify "^2.0.0"
@ -1969,7 +1995,7 @@ caseless@~0.12.0:
resolved "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc"
integrity sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=
chalk@2.4.2, chalk@^2.0.0:
chalk@2.4.2, chalk@^2.0.0, chalk@^2.4.1:
version "2.4.2"
resolved "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424"
dependencies:
@ -2161,6 +2187,7 @@ colors@^1.1.2:
combined-stream@^1.0.6, combined-stream@^1.0.8, combined-stream@~1.0.6:
version "1.0.8"
resolved "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f"
integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==
dependencies:
delayed-stream "~1.0.0"
@ -2271,9 +2298,21 @@ cross-fetch@^3.1.4:
dependencies:
node-fetch "2.6.1"
cross-spawn@^6.0.5:
version "6.0.5"
resolved "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4"
integrity sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==
dependencies:
nice-try "^1.0.4"
path-key "^2.0.1"
semver "^5.5.0"
shebang-command "^1.2.0"
which "^1.2.9"
cross-spawn@^7.0.0, cross-spawn@^7.0.2, cross-spawn@^7.0.3:
version "7.0.3"
resolved "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6"
integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==
dependencies:
path-key "^3.1.0"
shebang-command "^2.0.0"
@ -3428,6 +3467,11 @@ hoist-non-react-statics@^3.2.1, hoist-non-react-statics@^3.3.0, hoist-non-react-
dependencies:
react-is "^16.7.0"
hosted-git-info@^2.1.4:
version "2.8.9"
resolved "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz#dffc0bf9a21c02209090f2aa69429e1414daf3f9"
integrity sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==
html-encoding-sniffer@^2.0.1:
version "2.0.1"
resolved "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-2.0.1.tgz#42a6dc4fd33f00281176e8b23759ca4e4fa185f3"
@ -3779,6 +3823,7 @@ is-typed-array@^1.1.3, is-typed-array@^1.1.6:
is-typedarray@^1.0.0, is-typedarray@~1.0.0:
version "1.0.0"
resolved "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a"
integrity sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=
is-unicode-supported@^0.1.0:
version "0.1.0"
@ -4295,6 +4340,11 @@ json-bigint@^1.0.0:
dependencies:
bignumber.js "^9.0.0"
json-parse-better-errors@^1.0.1:
version "1.0.2"
resolved "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz#bb867cfb3450e69107c131d1c514bab3dc8bcaa9"
integrity sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==
json-parse-even-better-errors@^2.3.0:
version "2.3.1"
resolved "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d"
@ -4461,7 +4511,7 @@ lint-staged@^11.1.2:
string-argv "0.3.1"
stringify-object "^3.3.0"
listr2@^3.8.2, listr2@^3.8.3:
listr2@^3.8.2:
version "3.11.0"
resolved "https://registry.npmjs.org/listr2/-/listr2-3.11.0.tgz#9771b02407875aa78e73d6e0ff6541bbec0aaee9"
dependencies:
@ -4473,6 +4523,29 @@ listr2@^3.8.2, listr2@^3.8.3:
through "^2.3.8"
wrap-ansi "^7.0.0"
listr2@^3.8.3:
version "3.11.1"
resolved "https://registry.npmjs.org/listr2/-/listr2-3.11.1.tgz#a9bab5cd5874fd3cb7827118dbea6fedefbcb43f"
integrity sha512-ZXQvQfmH9iWLlb4n3hh31yicXDxlzB0pE7MM1zu6kgbVL4ivEsO4H8IPh4E682sC8RjnYO9anose+zT52rrpyg==
dependencies:
cli-truncate "^2.1.0"
colorette "^1.2.2"
log-update "^4.0.0"
p-map "^4.0.0"
rxjs "^6.6.7"
through "^2.3.8"
wrap-ansi "^7.0.0"
load-json-file@^4.0.0:
version "4.0.0"
resolved "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz#2f5f45ab91e33216234fd53adab668eb4ec0993b"
integrity sha1-L19Fq5HjMhYjT9U62rZo607AmTs=
dependencies:
graceful-fs "^4.1.2"
parse-json "^4.0.0"
pify "^3.0.0"
strip-bom "^3.0.0"
loader-utils@1.2.3:
version "1.2.3"
resolved "https://registry.npmjs.org/loader-utils/-/loader-utils-1.2.3.tgz#1ff5dc6911c9f0a062531a4c04b609406108c2c7"
@ -4554,6 +4627,7 @@ lodash@^4.1.1, lodash@^4.17.20, lodash@^4.17.21, lodash@^4.7.0:
log-symbols@^4.0.0, log-symbols@^4.1.0:
version "4.1.0"
resolved "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz#3fbdbb95b4683ac9fc785111e792e558d4abd503"
integrity sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==
dependencies:
chalk "^4.1.0"
is-unicode-supported "^0.1.0"
@ -4615,6 +4689,11 @@ memoize-one@^5.0.0:
version "5.2.1"
resolved "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz#8337aa3c4335581839ec01c3d594090cebe8f00e"
memorystream@^0.3.1:
version "0.3.1"
resolved "https://registry.npmjs.org/memorystream/-/memorystream-0.3.1.tgz#86d7090b30ce455d63fbae12dda51a47ddcaf9b2"
integrity sha1-htcJCzDORV1j+64S3aUaR93K+bI=
merge-stream@^2.0.0:
version "2.0.0"
resolved "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60"
@ -4830,6 +4909,11 @@ next@^11.1.1:
"@next/swc-linux-x64-gnu" "11.1.2"
"@next/swc-win32-x64-msvc" "11.1.2"
nice-try@^1.0.4:
version "1.0.5"
resolved "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366"
integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==
node-emoji@^1.8.1:
version "1.11.0"
resolved "https://registry.npmjs.org/node-emoji/-/node-emoji-1.11.0.tgz#69a0150e6946e2f115e9d7ea4df7971e2628301c"
@ -4894,6 +4978,16 @@ nodemailer@^6.4.16, nodemailer@^6.6.3:
version "6.6.3"
resolved "https://registry.npmjs.org/nodemailer/-/nodemailer-6.6.3.tgz#31fb53dd4d8ae16fc088a65cb9ffa8d928a69b48"
normalize-package-data@^2.3.2:
version "2.5.0"
resolved "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz#e66db1838b200c1dfc233225d12cb36520e234a8"
integrity sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==
dependencies:
hosted-git-info "^2.1.4"
resolve "^1.10.0"
semver "2 || 3 || 4 || 5"
validate-npm-package-license "^3.0.1"
normalize-path@^3.0.0, normalize-path@~3.0.0:
version "3.0.0"
resolved "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65"
@ -4906,9 +5000,25 @@ normalize-wheel@^1.0.1:
version "1.0.1"
resolved "https://registry.npmjs.org/normalize-wheel/-/normalize-wheel-1.0.1.tgz#aec886affdb045070d856447df62ecf86146ec45"
npm-run-all@^4.1.5:
version "4.1.5"
resolved "https://registry.npmjs.org/npm-run-all/-/npm-run-all-4.1.5.tgz#04476202a15ee0e2e214080861bff12a51d98fba"
integrity sha512-Oo82gJDAVcaMdi3nuoKFavkIHBRVqQ1qvMb+9LHk/cF4P6B2m8aP04hGf7oL6wZ9BuGwX1onlLhpuoofSyoQDQ==
dependencies:
ansi-styles "^3.2.1"
chalk "^2.4.1"
cross-spawn "^6.0.5"
memorystream "^0.3.1"
minimatch "^3.0.4"
pidtree "^0.3.0"
read-pkg "^3.0.0"
shell-quote "^1.6.1"
string.prototype.padend "^3.0.0"
npm-run-path@^4.0.0, npm-run-path@^4.0.1:
version "4.0.1"
resolved "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz#b7ecd1e5ed53da8e37a55e1c2269e0b97ed748ea"
integrity sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==
dependencies:
path-key "^3.0.0"
@ -5080,6 +5190,14 @@ parse-asn1@^5.0.0, parse-asn1@^5.1.5:
pbkdf2 "^3.0.3"
safe-buffer "^5.1.1"
parse-json@^4.0.0:
version "4.0.0"
resolved "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz#be35f5425be1f7f6c747184f98a788cb99477ee0"
integrity sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=
dependencies:
error-ex "^1.3.1"
json-parse-better-errors "^1.0.1"
parse-json@^5.0.0:
version "5.2.0"
resolved "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz#c76fc66dee54231c962b22bcc8a72cf2f99753cd"
@ -5119,6 +5237,11 @@ path-is-absolute@^1.0.0:
version "1.0.1"
resolved "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f"
path-key@^2.0.1:
version "2.0.1"
resolved "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40"
integrity sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=
path-key@^3.0.0, path-key@^3.1.0:
version "3.1.1"
resolved "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375"
@ -5127,6 +5250,13 @@ path-parse@^1.0.6:
version "1.0.7"
resolved "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735"
path-type@^3.0.0:
version "3.0.0"
resolved "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz#cef31dc8e0a1a3bb0d105c0cd97cf3bf47f4e36f"
integrity sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==
dependencies:
pify "^3.0.0"
path-type@^4.0.0:
version "4.0.0"
resolved "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b"
@ -5154,11 +5284,21 @@ picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.3:
version "2.3.0"
resolved "https://registry.npmjs.org/picomatch/-/picomatch-2.3.0.tgz#f1f061de8f6a4bf022892e2d128234fb98302972"
pidtree@^0.3.0:
version "0.3.1"
resolved "https://registry.npmjs.org/pidtree/-/pidtree-0.3.1.tgz#ef09ac2cc0533df1f3250ccf2c4d366b0d12114a"
integrity sha512-qQbW94hLHEqCg7nhby4yRC7G2+jYHY4Rguc2bjw7Uug4GIJuu1tvf2uHaZv5Q8zdt+WKJ6qK1FOI6amaWUo5FA==
pify@^2.2.0:
version "2.3.0"
resolved "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c"
integrity sha1-7RQaasBDqEnqWISY59yosVMw6Qw=
pify@^3.0.0:
version "3.0.0"
resolved "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz#e5a4acd2c101fdf3d9a4d07f0dbc4db49dd28176"
integrity sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=
pirates@^4.0.1:
version "4.0.1"
resolved "https://registry.npmjs.org/pirates/-/pirates-4.0.1.tgz#643a92caf894566f91b2b986d2c66950a8e2fb87"
@ -5344,6 +5484,7 @@ property-expr@^2.0.4:
psl@^1.1.28, psl@^1.1.33:
version "1.8.0"
resolved "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz#9326f8bcfb013adcc005fdff056acce020e51c24"
integrity sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ==
public-encrypt@^4.0.0:
version "4.0.3"
@ -5651,6 +5792,15 @@ react@17.0.2:
loose-envify "^1.1.0"
object-assign "^4.1.1"
read-pkg@^3.0.0:
version "3.0.0"
resolved "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz#9cbc686978fee65d16c00e2b19c237fcf6e38389"
integrity sha1-nLxoaXj+5l0WwA4rGcI3/Pbjg4k=
dependencies:
load-json-file "^4.0.0"
normalize-package-data "^2.3.2"
path-type "^3.0.0"
readable-stream@^2.0.2, readable-stream@^2.3.3, readable-stream@^2.3.6:
version "2.3.7"
resolved "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57"
@ -5757,7 +5907,7 @@ resolve-from@^5.0.0:
version "5.0.0"
resolved "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz#c35225843df8f776df21c57557bc087e9dfdfc69"
resolve@^1.20.0:
resolve@^1.10.0, resolve@^1.20.0:
version "1.20.0"
resolved "https://registry.npmjs.org/resolve/-/resolve-1.20.0.tgz#629a013fb3f70755d6f0b7935cc1c2c5378b1975"
dependencies:
@ -5840,7 +5990,7 @@ semver-compare@^1.0.0:
version "1.0.0"
resolved "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz#0dee216a1c941ab37e9efb1788f6afc5ff5537fc"
semver@^5.1.0, semver@^5.6.0:
"semver@2 || 3 || 4 || 5", semver@^5.1.0, semver@^5.5.0, semver@^5.6.0:
version "5.7.1"
resolved "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7"
@ -5869,17 +6019,29 @@ sha.js@^2.4.0, sha.js@^2.4.11, sha.js@^2.4.8:
inherits "^2.0.1"
safe-buffer "^5.0.1"
shebang-command@^1.2.0:
version "1.2.0"
resolved "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea"
integrity sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=
dependencies:
shebang-regex "^1.0.0"
shebang-command@^2.0.0:
version "2.0.0"
resolved "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea"
dependencies:
shebang-regex "^3.0.0"
shebang-regex@^1.0.0:
version "1.0.0"
resolved "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3"
integrity sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=
shebang-regex@^3.0.0:
version "3.0.0"
resolved "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172"
shell-quote@1.7.2:
shell-quote@1.7.2, shell-quote@^1.6.1:
version "1.7.2"
resolved "https://registry.npmjs.org/shell-quote/-/shell-quote-1.7.2.tgz#67a7d02c76c9da24f99d20808fcaded0e0e04be2"
@ -5971,6 +6133,32 @@ spacetime@^6.16.2:
version "6.16.3"
resolved "https://registry.npmjs.org/spacetime/-/spacetime-6.16.3.tgz#86d3b05db33421a9ee478b1f2ca025582fc61fcf"
spdx-correct@^3.0.0:
version "3.1.1"
resolved "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.1.tgz#dece81ac9c1e6713e5f7d1b6f17d468fa53d89a9"
integrity sha512-cOYcUWwhCuHCXi49RhFRCyJEK3iPj1Ziz9DpViV3tbZOwXD49QzIN3MpOLJNxh2qwq2lJJZaKMVw9qNi4jTC0w==
dependencies:
spdx-expression-parse "^3.0.0"
spdx-license-ids "^3.0.0"
spdx-exceptions@^2.1.0:
version "2.3.0"
resolved "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz#3f28ce1a77a00372683eade4a433183527a2163d"
integrity sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==
spdx-expression-parse@^3.0.0:
version "3.0.1"
resolved "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz#cf70f50482eefdc98e3ce0a6833e4a53ceeba679"
integrity sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==
dependencies:
spdx-exceptions "^2.1.0"
spdx-license-ids "^3.0.0"
spdx-license-ids@^3.0.0:
version "3.0.10"
resolved "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.10.tgz#0d9becccde7003d6c658d487dd48a32f0bf3014b"
integrity sha512-oie3/+gKf7QtpitB0LYLETe+k8SifzsX4KixvpOsbI6S0kRiRQ5MKOio8eMSAKQ17N06+wdEOXRiId+zOxo0hA==
sprintf-js@~1.0.2:
version "1.0.3"
resolved "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c"
@ -6081,6 +6269,15 @@ string.prototype.matchall@^4.0.5:
regexp.prototype.flags "^1.3.1"
side-channel "^1.0.4"
string.prototype.padend@^3.0.0:
version "3.1.2"
resolved "https://registry.npmjs.org/string.prototype.padend/-/string.prototype.padend-3.1.2.tgz#6858ca4f35c5268ebd5e8615e1327d55f59ee311"
integrity sha512-/AQFLdYvePENU3W5rgurfWSMU6n+Ww8n/3cUt7E+vPBB/D7YDG8x+qjoFs4M/alR2bW7Qg6xMjVwWUOvuQ0XpQ==
dependencies:
call-bind "^1.0.2"
define-properties "^1.1.3"
es-abstract "^1.18.0-next.2"
string.prototype.trimend@^1.0.4:
version "1.0.4"
resolved "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.4.tgz#e75ae90c2942c63504686c18b287b4a0b1a45f80"
@ -6127,6 +6324,11 @@ strip-ansi@^3.0.0:
dependencies:
ansi-regex "^2.0.0"
strip-bom@^3.0.0:
version "3.0.0"
resolved "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3"
integrity sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=
strip-bom@^4.0.0:
version "4.0.0"
resolved "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz#9c3505c1db45bcedca3d9cf7a16f5c5aa3901878"
@ -6302,6 +6504,7 @@ timers-browserify@2.0.12, timers-browserify@^2.0.4:
tmp@^0.2.1, tmp@~0.2.1:
version "0.2.1"
resolved "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz#8457fc3037dcf4719c251367a1af6500ee1ccf14"
integrity sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==
dependencies:
rimraf "^3.0.0"
@ -6491,9 +6694,10 @@ typeorm@^0.2.30:
yargs "^17.0.1"
zen-observable-ts "^1.0.0"
typescript@^4.3.5:
version "4.3.5"
resolved "https://registry.npmjs.org/typescript/-/typescript-4.3.5.tgz#4d1c37cc16e893973c45a06886b7113234f119f4"
typescript@^4.4.2:
version "4.4.2"
resolved "https://registry.npmjs.org/typescript/-/typescript-4.4.2.tgz#6d618640d430e3569a1dfb44f7d7e600ced3ee86"
integrity sha512-gzP+t5W4hdy4c+68bfcv0t400HVJMMd2+H9B7gae1nQlBzCqvrXX+6GL/b3GAgyTH966pzrZ70/fRjwAtZksSQ==
uglify-js@^3.1.4:
version "3.14.1"
@ -6609,6 +6813,14 @@ v8-to-istanbul@^8.0.0:
convert-source-map "^1.6.0"
source-map "^0.7.3"
validate-npm-package-license@^3.0.1:
version "3.0.4"
resolved "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz#fc91f6b9c7ba15c857f4cb2c5defeec39d4f410a"
integrity sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==
dependencies:
spdx-correct "^3.0.0"
spdx-expression-parse "^3.0.0"
verror@1.10.0:
version "1.10.0"
resolved "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400"
@ -6706,6 +6918,13 @@ which-typed-array@^1.1.2:
has-tostringtag "^1.0.0"
is-typed-array "^1.1.6"
which@^1.2.9:
version "1.3.1"
resolved "https://registry.npmjs.org/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a"
integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==
dependencies:
isexe "^2.0.0"
which@^2.0.1:
version "2.0.2"
resolved "https://registry.npmjs.org/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1"